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? eg,
<script src="mylib/browser.min.js"></script>
<script>
const mylib = window.mylib;
mylib.inc(41);
//=> 42
mylib.utils.is_number(42);
//=> true
</script>
The canonical post for this answer is this Gist .
Create mylib/index_browser.js
window.mylib = { inc: require('./inc'), utils: { is_number: require('./is_number') } };
Create mylib/externs.js
/** @externs */ var mylib; var inc; var utils; var is_number;
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.
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:
require
you're talking about 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);
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.