Google Closure Guide: Compiling Without Errors

Google Closure Compiler

Google Closure Compiler is an excellent tool for compiling a huge JavaScript Single Page Application to a ridiculously small production size. Unfortunately, Google Closure's power to produce small production JavaScript is inversely proportional to its documentation. Since it was open sourced on 05-Nov-2009 there has only been one book about it and a handful of blog posts compared to the deluge of information about Webpack, Uglify, and rollup.

This series is a step by step guide through Google Closure with a basic starting point and ending up with highly optimized modules (what WebPack users would call bundles). Each step is numbered and has a link to the corresponding configuration file in the demo repository.

Google Closure will be compiling this TypeScript and React Hello World Application.

import * as React from 'react';
import * as ReactDOM from 'react-dom';

class HelloWorld extends React.Component<{}, {}> {
  public render () {
    return (
      <div>Hello World!</div>
    );
  }
}

function render() {
  ReactDOM.render(
    <HelloWorld/>,
    document.getElementById('app')
  );
}

render();

Setup

Here's the setup needed to compile with Google Closure.

You'll need Java and Yarn installed on your system.

Clone and setup the Closure Compiler Demonstration repository as follows:

git clone git@github.com:spinningtopsofdoom/closure-compiler-demo.git
cd closure-compiler-demo
yarn install                                    # Install NPM Libraries
./node_modules/typescript/bin/tsc -p src/react/ # Compile TypeScript

We'll start off compiling Google Closure with flagfile.conf using this command

java -jar node_modules/google-closure-compiler/compiler.jar --flagfile flagfile.conf  --debug --formatting PRETTY_PRINT

Each step will detail changes made to flagfile .conf and have a link to what flagfile.conf will look like.

Overview of Compiling with Google Closure

Before we begin compiling our Hello World app let's go through some of the basics of compiling with Google Closure.

java -jar closure-compiler.jar --js input1.js --js input2.js --js_output_file=output.js

The above command passes input1.js and input2.js to Google Closure with compiles them into output.js. Input files are passed to Google Closure with the js option and the compiled JavaScript is written to the file specified by the js_output_file option.

There are many more options then js and js_output_file which you can find by running

java -jar closure-compiler.jar --help

We can put these commands into a "flag file" named flagfile.conf

--js input.js
--js_output_file=output.js

and then run Google Closure passing in flagfile.conf with the flagfile option

java -jar closure-compiler.jar --flagfile flagfile.conf

When you're starting out with Google Closure, adding debugging options helps a great deal with understanding the compiled JavaScript.

java -jar closure-compiler.jar --flagfile params.conf --debug --formatting PRETTY_PRINT

debug

Turns off variable renaming so in the compiled JavaScript you'll see variables like React.$render$ instead of al.e.

formatting PRETTY_PRINT

Pretty prints the compiled JavaScript instead of having all the JavaScript code on a single line with no formatting.

01 - The smallest configuration possible

--compilation_level=ADVANCED_OPTIMIZATIONS
--language_out=ES5

--js_output_file=dist/bundle.js
--js src/build-react/**.js

compilation_level

ADVANCED_OPTIMIZATIONS tells Google Closure to compile JavaScript at the highest optimization level.

language_out

ES5 gives Google Closure the target language of ECMAScript5

Our goal is to compile production quality JavaScript for modern browsers. To do this we're turning on the most advanced compilation level and creating an ECMAScript5 JavaScript file dist/bundle.js.

Sadly this minimal flag file fails to compile. Instead Google Closure throws these errors:

src/build-react/app.js:2: ERROR - variable exports is undeclared
Object.defineProperty(exports, "__esModule", \{ value: true});
                      ^^^^^^^

src/build-react/app.js:3:
Originally at:
src/react/app.tsx:1: ERROR - variable require is undeclared
import * as React from 'react';
^^^^^^^

Google Closure Compiler was created before CommonJS, NPM, or ECMAScript Modules were in widespread use. Here's the Google Closure Module solution

/** Declare a namespace */
goog.provide("some.namespace");

/** Pull in code from another file */
goog.require("a.required.namespace")

Google Closure needs to be informed that we're using modern JavaScript modules

02 - Use CommonJS Modules

--process_common_js_modules

process_common_js_modules

Translates CommonJS modules into a format Google Closure recognizes

Running Google Closure compiler again we get a different error

src/build-react/app.js:3:
Originally at:
src/react/app.tsx:1: WARNING - Invalid module path "react" for resolution mode "BROWSER"
import * as React from 'react';
^

src/build-react/app.js:3:
Originally at:
src/react/app.tsx:1: WARNING - Invalid module path "react" for resolution mode "BROWSER"
import * as React from 'react';
^

For CommonJS modules Google Closure defaults to using the Browser resolution algorithm. The module resolution should be set to the Node resolution algorithm.

03 - Use Node Resolution Algorithm

--module_resolution=NODE

module_resolution

Set the algorithm Google Closure uses to resolve CommonJS modules.

Now Google Closure is correctly resolving CommonJS modules. However we run into another problem.

src/build-react/app.js:3:
Originally at:
src/react/app.tsx:1: WARNING - Failed to load module "react"
import * as React from 'react';
^

src/build-react/app.js:4:
Originally at:
src/react/app.tsx:2: WARNING - Failed to load module "react-dom"
import * as ReactDOM from 'react-dom';
^

Google Closure only includes JavaScript files declared with the js flag.

04 - Include React and ReactDOM libraries

--js node_modules/react-dom/package.json
--js node_modules/react-dom/**.js

--js node_modules/react/package.json
--js node_modules/react/**.js

Including the package.json from the React and ReactDOM libraries gives Google Closure metadata about the library. Without the package.json Google Closure would not know where the entry point of React is located (node_modules/react/index.js)

Running Google Closure again we get more module dependency errors

node_modules/react-dom/cjs/react-dom-server.browser.development.js:17: WARNING - Failed to load module "object-assign"
var objectAssign$1 = require('object-assign');
    ^

node_modules/react-dom/cjs/react-dom-server.browser.development.js:18: WARNING - Failed to load module "fbjs/lib/invariant"
var invariant = require('fbjs/lib/invariant');
    ^

node_modules/react-dom/cjs/react-dom-server.browser.development.js:22: WARNING - Failed to load module "prop-types"
var propTypes = require('prop-types');
    ^

node_modules/react-dom/cjs/react-dom-server.node.development.js:28: WARNING - Failed to load module "stream"
var stream = require('stream');
    ^

These missing modules are the dependencies of React and ReactDOM.

05 - Include React and ReactDOM's dependencies

--js node_modules/react/package.json
--js node_modules/react/**.js

--js node_modules/fbjs/package.json
--js node_modules/fbjs/lib/**.js

--js node_modules/object-assign/package.json
--js node_modules/object-assign/**.js

--js node_modules/prop-types/package.json
--js node_modules/prop-types/**.js

Fortunately React and ReactDOM's dependencies don't have dependencies of their own. Unfortunately if we want to use any more NPM libraries we're going to have to include the library and it's entire tree of dependencies.

There are tools to find all the NPM libraries and their dependencies

There is a way we can use Google Closure to list all the dependencies for us that involves some options we haven't discussed yet. The flag file to get the list of NPM dependencies will be in the next post in this series.

With all our dependencies included we run Google Closure again and get

node_modules/react/index.js:4: ERROR - Variable module$node_modules$react$index declared more than once. First occurrence: node_modules/react/index.js
  module.exports = require('./cjs/react.production.min.js');
  ^^^^^^^^^^^^^^

node_modules/react/index.js:6: ERROR - Variable module$node_modules$react$index declared more than once. First occurrence: node_modules/react/index.js
  module.exports = require('./cjs/react.development.js');
  ^^^^^^^^^^^^^^

Currently for a CommonJS module, Google Closure transforms modules.exports to a synthetic variable (module$node_modules$react$index in this case). Google Closure throws an error when the same variable is declared more than once.

06 - Ignore Duplicate Variable Declaration

--jscomp_off=checkVars

jscomp_off

Selectively turns off errors and warnings that Google Closure throws. checkVars is the Google Closure check for duplicate variable declarations

Google Closure compiles our JavaScript without any errors. While we're not quite done with all of our optimizations, we still managed to reduce the gzipped filesize from 400 kb to around 200 kb. At the end of the series we'll see the gzipped filesize get down to a little bit more than 30 kb!

Next Time - Compile Time Information and Optimization

Next time we'll be showing how to get useful compile time information from Google Closure. Google Closure will show how it does name mangling and what files it reads and in what order. We can also optimize compilation speed and compiled JavaScript file size.

As a bonus the optimizations will also give us a way to find all the NPM dependencies required by our project!