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;
}
will become (after angularjs-annotate
)
{
key: '_makeAwesome',
value: function _makeAwesome() {
this.isAwesome = true;
}
}
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
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();
}
}
}
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(/*...*/))
// ...
const _stepOverOnDevBuild = (func) => {
return isDev
? p.util.noop()
: func;
};
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)
Top comments (0)