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!