DEV Community

Cover image for Uglify private methods/members
Domenik Reitzner
Domenik Reitzner

Posted on

Uglify private methods/members

This is part four of seven.

Why angularjs, why....?

One problem with angularjs is that it transforms methods/functions inside of components/directives/services into an object. The result from that is that minifiers can't replace private methods automatically (we use _privateMethod in our project as convention).

_makeAwesome() {
    this.isAwesome = true;
}
Enter fullscreen mode Exit fullscreen mode

will become (after angularjs-annotate)

{
    key: '_makeAwesome',
    value: function _makeAwesome() {
        this.isAwesome = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Because _makeAwesome is now also represented as string we can't use a normal uglify (even with regex options).

Fix it before annotation

One day I had an epiphany, why not switch them out before the annotation happens? After searching for some npm package and finding nothing, I decided to just write my own gulp replace function. For each file I needed to replace all private members and methods with a unique id (for that file).

gulp
    .src(src)
    .pipe(p.sourcemaps.init())
    // replace private function names with stringId
    .pipe(through.obj((file, _, cb) => {
        const newFile = file.clone();
        let code = newFile.contents.toString();
        const ids = new StringIdGenerator();
        // gets all private function declarations/methods in a file
        let matchesPrivateFunctions = code.match(/( _[a-z0-9\$_]+\()/gim);
        if (matchesPrivateFunctions) {
            // remove bracket and space from match to not miss this._func.bind(this)
            matchesPrivateFunctions = matchesPrivateFunctions
                .map((match) => match.replace(/\(|\s/g, ''))
                .sort((a, b) => b.length - a.length);
            // replace all the matchesPrivateFunctions
            matchesPrivateFunctions.forEach((match) => {
                code = code.replace(new RegExp(match, 'gm'), ids.next());
            });
        }
        // get all private Members inside of classes
        const matchesPrivateMembers = code.match(/this._[a-z0-9\$_]+/gim) || [];
        // sort with longest names on top to not miss this._privates after replacing this._priv 
        const uniquePrivateMembers = new Set(matchesPrivateMembers.sort((a, b) => b.length - a.length));
        if (uniquePrivateMembers.size) {
            uniquePrivateMembers.forEach((match) => {
                // replace without this. prefix => bindings might have members with `_`
                match = match
                    .replace('this.', '')
                    .replace('$', '\\$');
                code = code.replace(new RegExp(match, 'gm'), ids.next());
            });
        }

        // overwrite contents of vinyl with new buffer
        newFile.contents = Buffer.from(code);
        cb(null, newFile);
    }))
    .pipe(p.babel({
        // babel and angularjs-annotate
    }))
    // ... some more build stuff
Enter fullscreen mode Exit fullscreen mode

String id generator

here is the id generator that I used:

class StringIdGenerator {
    constructor(chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') {
        this._chars = chars;
        this._nextId = [0];
    }

    next() {
        const r = [];
        for (const char of this._nextId) {
            r.unshift(this._chars[char]);
        }
        this._increment();
        return r.join('');
    }

    _increment() {
        for (let i = 0; i < this._nextId.length; i++) {
            const val = ++this._nextId[i];
            if (val >= this._chars.length) {
                this._nextId[i] = 0;
            } else {
                return;
            }
        }
        this._nextId.push(0);
    }

    * [Symbol.iterator]() {
        while (true) {
            yield this.next();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Bonus tip

In development we don't really care about this transformation (or any minification). In our project we set a flag (isDev) when it is a local build. So it was relatively easy to wrap this steps in a little helper function.

return gulp
    .pipe(_stepOverOnDevBuild(/*...*/))
    // ...
Enter fullscreen mode Exit fullscreen mode
const _stepOverOnDevBuild = (func) => {
    return isDev
        ? p.util.noop()
        : func;
};
Enter fullscreen mode Exit fullscreen mode

Word of caution:
test with minification/transformation in place before you deploy

Coming up next

  • Make service injections private (to minify with what we build today)

Discussion (0)