Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save customcommander/22cb5d98da3f3d4b804ed9dae6b5b109 to your computer and use it in GitHub Desktop.
Save customcommander/22cb5d98da3f3d4b804ed9dae6b5b109 to your computer and use it in GitHub Desktop.
How to use the Google Closure Compiler to browserify your Node.js library

Canonical post for my Stack Overflow Q&A

How to use the Google Closure Compiler to browserify your Node.js library

I have this simple Node.js library:

mylib/
|- inc.js
|- index.js
|- is_number.js
|- package.json

mylib/is_number.js

module.exports = x => typeof x === 'number';

mylib/inc.js

const is_number = require('./is_number');

module.exports = x => is_number(x) ? x + 1 : x;

mylib/index.js (value of the main property in my package.json)

module.exports = {
  inc: require('./inc'),
  utils: {
    is_number: require('./is_number')
  }
};

Example:

const mylib = require('mylib');

mylib.inc(41);
//=> 42

mylib.utils.is_number(42);
//=> true

How can I use the Google Closure Compiler to "browserify" my Node.js library so that it can work in a browser too? e.g.,

<script src="mylib/browser.min.js"></script>
<script>
const mylib = window.mylib;

mylib.inc(41);
//=> 42

mylib.utils.is_number(42);
//=> true
</script>

TL; DR

  1. Create mylib/index_browser.js

    window.mylib = {
      inc: require('./inc'),
      utils: {
        is_number: require('./is_number')
      }
    };
  2. Create mylib/externs.js

    /** @externs */
    var mylib;
    var inc;
    var utils;
    var is_number;
  3. Then:

    $ cc --compilation_level ADVANCED \
        --language_out ES5 \
        --process_common_js_modules \
        --module_resolution NODE \
        --externs mylib/externs.js \
        --isolation_mode IIFE \
        --js mylib/index_browser.js mylib/inc.js mylib/is_number.js \
        --js_output_file mylib/browser.min.js

    Where cc is an alias to your Google Closure Compiler instance; see below for an example


Before we start:

I wrote this alias to make it easier to invoke the Google Closure Compiler (CC)

$ alias cc="java -jar /devtools/closure-compiler/compiler.jar"
$ cc --version
Closure Compiler (http://github.com/google/closure-compiler)
Version: v20210106

The browserified version of the library will be compiled down to ES5.

Step-by-step instructions

Your first attempt might look like this: just compile the exports file mylib/index.js

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --js mylib/index.js
mylib/index.js:1:0: ERROR - [JSC_UNDEFINED_VARIABLE] variable module is undeclared
  1| module.exports = {
     ^^^^^^

mylib/index.js:2:7: ERROR - [JSC_UNDEFINED_VARIABLE] variable require is undeclared
  2|   inc: require('./inc'),
            ^^^^^^^

2 error(s), 0 warning(s)

If CC doesn't know about module and require that's not a great start.

Fortunately we're only missing the --process_common_js_modules flag:

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --process_common_js_modules \
     --js mylib/index.js
mylib/index.js:2:7: ERROR - [JSC_JS_MODULE_LOAD_WARNING] Failed to load module "./inc"
  2|   inc: require('./inc'),
            ^

mylib/index.js:4:15: ERROR - [JSC_JS_MODULE_LOAD_WARNING] Failed to load module "./is_number"
  4|     is_number: require('./is_number')
                    ^

2 error(s), 0 warning(s)

Still not great but this time the errors are different:

  1. CC doesn't know which require you're talking about
  2. CC doesn't know where these two other modules are

We need the --module_resolution flag and tell CC where the other modules are:

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --js mylib/index.js mylib/inc.js mylib/is_number.js 

However the output is empty...

Why? In ADVANCED compilation mode CC removes any code that is not used. Which is the case actually: so far all this stuff isn't used at all!

Let's check with a less aggressive compilation mode:

$ cc --compilation_level WHITESPACE_ONLY --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --js mylib/index.js mylib/inc.js mylib/is_number.js 
var module$mylib$index = {default:{}};
module$mylib$index.default.inc = module$mylib$inc.default;
module$mylib$index.default.utils = {is_number:module$mylib$is_number.default};
var module$mylib$inc = {};
var is_number$$module$mylib$inc = module$mylib$is_number.default;
module$mylib$inc.default = function(x) {
  return (0,module$mylib$is_number.default)(x) ? x + 1 : x;
};
var module$mylib$is_number = {};
module$mylib$is_number.default = function(x) {
  return typeof x === "number";
};

We can see that even if the ADVANCED compilation mode didn't remove everything, this wouldn't be very useful anyway. Where is window.mylib for example?

The only way I managed to get both my library available at window.mylib and compiled with the most aggressive compilation mode, is to have a separate exports file for the browser.

From this mylib/index.js

module.exports = {
  inc: require('./inc'),
  utils: {
    is_number: require('./is_number')
  }
};

To this mylib/index_browser.js

window.mylib = {
  inc: require('./inc'),
  utils: {
    is_number: require('./is_number')
  }
};

When you add to the window object CC knows that this code may be reached so it can't safely remove it anymore.

Let's try again with this file:

$ cc --compilation_level ADVANCED --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js
function b(a) {
  return "number" === typeof a;
}
;window.g = {h:function(a) {
  return b(a) ? a + 1 : a;
}, j:{i:b}};

That is looking better but there is a major problem: CC has mangled all the names!

Don't worry! We only need to tell which names CC should leave alone. That is the purpose of an externs file.

mylib/externs.js

/** @externs */
var foo;
var inc;
var utils;
var is_number;

We need another flag: --externs

$ cc --compilation_level ADVANCED --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --externs mylib/externs.js \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js
function b(a) {
  return "number" === typeof a;
}
;window.mylib = {inc:function(a) {
  return b(a) ? a + 1 : a;
}, utils:{is_number:b}};

Getting there...

One obvious improvement is to wrap all of this in an IIFE to avoid polluting the global scope more than necessary.

We need the --isolation_mode flag:

$ cc --compilation_level ADVANCED --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --externs mylib/externs.js \
     --isolation_mode IIFE \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js
(function(){function b(a) {
  return "number" === typeof a;
}
;window.mylib = {inc:function(a) {
  return b(a) ? a + 1 : a;
}, utils:{is_number:b}};
}).call(this);

Fantastic!

All that is left to do is save that into a file and remove the formatting to save up a few extra bytes:

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --externs mylib/externs.js \
     --isolation_mode IIFE \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js \
     --js_output_file mylib/browser.min.js

mylib/browser.min.js

(function(){function b(a){return"number"===typeof a};window.mylib={inc:function(a){return b(a)?a+1:a},utils:{is_number:b}};}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment