An Unholy Combination: Closure Compiler, Preact and JSX

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.

Introduction

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.

About the Closure Compiler

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.

Attempt 1: Webpack + Babel + Closure

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.

Attempt 2: Gulp + Babel + Closure

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:

gulpfile.js

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.

.babelrc

{
  "sourceMaps": true,
  "presets": [],
  "plugins": [
    ["transform-react-jsx", { "pragma": "h" }]
  ]
}

I'm only using Babel for JSX transformations, nothing else.

Caveat: Properties in JSX

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.

Caveat: ES6 Modules and Closure

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.

Caveat: This is Pretty Experimental (for me)

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