Wherein I try to figure out how to make the Google Closure Compiler play nicely with a bunch of other stuff like Gulp and Preact from the scary outside-of-Google world. I think I mostly succeeded. Hopefully.
For some time I've been working on a fairly large JavaScript project: a browser-based version of the Furcadia game client. I started this without having much JavaScript experience (most of my web development in the past had been server-side stuff using PHP or Python/Flask) so this has been an interesting journey.
I wasn't really up-to-speed with JavaScript best practices, and things had moved on so much that I didn't really know where to begin, so I just kind of jumped in and started writing code using what I already knew. I wanted to keep the client lightweight and understand what was happening 'behind the scenes', so I tried to avoid using libraries or frameworks unless absolutely necessary. The one concession I made was to use the Google Closure Compiler, which is a bit of magic that performs heavy optimisations and obfuscation on your code - far further than what tools like UglifyJS do.
I decided to reconsider this though when a friend introduced me to Preact, a lightweight alternative to React with a practically identical API. I've reached the point where I can't really justify writing any more UI code that just manipulates the DOM - this was fine in the beginning but it's now proving to be a pain - and Preact seemed like a good fit for my project. Well, except for one thing...
I thought this would be a good opportunity to finally learn how to do things
'the right way'. I read a really useful post,
Modern JavaScript Explained For Dinosaurs
which as it turns out was exactly what I needed to take my understanding from
2007-level to somewhere in this decade. Finally, package.json
made sense!
That's great, but I had one sticking point - I really wanted to keep on using
the Closure Compiler, for several reasons.
You might have used some of Google's web-based products in the past... like, say, Gmail. They were some of the first developers to create and deploy complex JavaScript apps, so they ended up creating a lot of their own tooling (like this!) and then releasing some of it.
The Closure Compiler does some really cool stuff: it basically takes a bunch of JavaScript files in, analyses what it's doing, optimises it, bundles it into one file and generates a blob of minified code.
It goes further by including a type system (using type annotations in comments) and offering advanced optimisations, which place some limits on what you can do in JavaScript but in return allow the compiler to better optimise your code.
One of the biggest issues in that approach - which has bit me when using some
libraries - is that in typical JavaScript, obj.age
and obj['age']
mean the
same thing. With Closure's advanced optimisations on, these mean different
things: it will rename properties, so obj.age
may become something like
obj.Z
, but it won't touch obj['age']
.
// With advanced optimisations on, this breaks: the red and green keys will be
// renamed when creating the object, but the second line will still try to
// read from a property called 'red', and it'll fail!
const colours = {red: '#ff0000', green: '#00ff00'}
document.body.style.backgroundColor = colours['red']
The crux of the matter is that the Closure Compiler was written by Google, for Google, and it doesn't always play nicely with other software. It's assumed that everything you feed into it was written specifically for it. That's not always the case, and this bit me when I wanted to start playing with Preact.
Closure supports a bunch of ES6 features, but it doesn't support JSX
(for either React or Preact), which is what lets you write terrifying-looking
code like return <button>Hello {name}!</button>;
. You can write Preact code
without using JSX, but I wanted to use it, so there began the fighting.
My first attempt at doing this was to set up Webpack and Babel, and then feed the output into Closure. Conveniently, there's a Webpack plugin for Closure: webpack-closure-compiler. Unfortunately I couldn't actually get this to work, and after twiddling with it for a few days I decided it was futile.
From what I've gathered, a typical flow for JavaScript code in a Webpack setup looks somewhat like this:
I set up a .babelrc
that disables every feature in Babel, as I only wanted
the JSX transformation. This alone wasn't good enough, though. The way Webpack
bundles modules together generates some syntax which while perfectly valid,
does not play nicely with the assumptions that Closure makes.
Issue #2182 in the
Closure repository deals with this, but there's no fix. I tried to patch
Closure to fix it, but didn't succeed. So... maybe it just wasn't meant to be.
I'm stubborn, and I wasn't giving up. I decided to investigate Gulp, since it seemed to be a better fit for my needs: it would simplify my build process in a similar form to Webpack, but it didn't have any bundling built-in, so in theory I shouldn't encounter the issue that I did with Webpack.
It ended up working out for the most part, with a couple of caveats that I'll mention later. Here's my setup, with explanation:
const gulp = require('gulp');
const babel = require('gulp-babel');
const sourcemaps = require('gulp-sourcemaps');
const closureCompiler = require('google-closure-compiler').gulp();
gulp.task('scripts', () =>
gulp.src(['js/*.js'], {base: 'js'})
.pipe(sourcemaps.init())
.pipe(babel())
.pipe(closureCompiler({
js: ['node_modules/preact/package.json', 'node_modules/preact/dist/preact.esm.js'],
compilation_level: 'ADVANCED',
language_out: 'ES5',
module_resolution: 'NODE',
dependency_mode: 'STRICT',
entry_point: 'index.js'
}))
.pipe(sourcemaps.mapSources((sourcePath, file) => {
if (sourcePath.startsWith('node_modules'))
return '../' + sourcePath;
else
return '../js/' + sourcePath;
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('dist'))
);
gulp.task('default', ['scripts']);
There's a bit going on here, and I'm still quite new to Gulp, but it all seems to work.
Every JavaScript file is inside a js
directory, and it gets output to
dist/combined.js
. A source map is generated.
Closure Compiler's advanced optimisations are enabled, and it spits out ES5.
{
"sourceMaps": true,
"presets": [],
"plugins": [
["transform-react-jsx", { "pragma": "h" }]
]
}
I'm only using Babel for JSX transformations, nothing else.
I mentioned above that the Closure Compiler requires you to quote properties
in order to stop them from being renamed. Babel's JSX transformation breaks
this by default, as it'll generate code with stuff like { onClick: ... }
which Closure dutifully renames.
I've filed Issue #6812 with Babel as a feature request for an option that controls this, and I may take a stab at writing it myself if I have time.
In the meantime, there's a hacky fix you can perform by patching the
node_modules/babel-helper-builder-react-jsx/lib/index.js
file. Find the
convertAttribute
function, and the following code within it:
if (t.isValidIdentifier(node.name.name)) {
node.name.type = "Identifier";
} else {
node.name = t.stringLiteral(node.name.name);
}
You'll want the t.stringLiteral
path to always be used.
patch-package seems like an interesting approach for applying this, but I'd still rather have it integrated into Babel if possible.
The Closure Compiler supports ES6 modules - to an extent. Notably, unlike
Webpack, you cannot simply import something in your code and expect Closure
to magically locate it; you need to pass in every file it might need,
including the package.json
.
For Preact, you'll notice that I was able to get away with just including the
package.json
and preact.esm.js
file as references in the Gulp file.
I haven't yet tried to use other NPM packages from within compiled code, but
I'm hoping a similar process will work for them - check package.json
to see
what JavaScript file is defined by the module
property, then include that
in my Gulp file alongside the package.json
path for that package.
I should really note that if it wasn't obvious from the introduction, I'm still pretty new to modern JavaScript development and I'm not always sure about what I'm doing. I haven't yet done much with Preact in this environment and there may be more issues that haven't come up with the test scripts I've written - this is just what I've gotten so far after a few weeks of on-and-off tinkering.
Let me know if you spot any issues in this post or if there's something that isn't clear and I'll try to sort it out!
Previous Post: A Rant About Proxying API Requests on iOS (and others)
Next Post: Relaying OpenVPN through a Remote Server