-
Notifications
You must be signed in to change notification settings - Fork 1k
goog.module: an ES6 module like alternative to goog.provide
goog.module
is a new way of structuring JavaScript files with a few advantages over the
traditional goog.provide/goog.require structure. Basic example:
before:
goog.provide(‘foo’);
goog.require(‘baz.Quux’);
foo.Bar = function() { /* … */ };
after
goog.module(‘foo’);
var Quux = goog.require(‘baz.Quux’);
exports.Bar = function() { /* … */ };
goog.module
s are similar to CommonJS or ES6 modules. Main features:
- Inside a goog.module file, goog.require now has a return value: it returns the
exports
object of the required module. (If you goog.require a non-module file, the return value is the associated namespace.) - Top-level declarations are file-scoped, not global.
- Module exports are not globals. They are accessed through the module
exports
object. - module files are always in strict mode.
The original proposal is here here
Here is a typical goog.module
for a class:
goog.module('some.module.identifier');
// Require a traditional namespace:
var array = goog.require('goog.array');
// Require a module dependency:
var SomeClass = goog.require('my.namespace.SomeClass');
exports = goog.defineClass(SomeClass, {
constructor: function() {
doSomething();
}
});
Here is a typical goog.module for a namespace:
goog.module('some.module.identifier');
// Require a traditional namespace:
var array = goog.require('goog.array');
// Require a module dependency:
var SomeClass = goog.require('my.namespace.SomeClass');
exports.method1 = function() {};
exports.method2 = function() {};
Module identifiers may not contain “/”, “\” or begin with “.”. This restriction leaves a path open for using relative paths when goog.require’ing a dependency. For example, we might allow “./foo.js”, which indicate dependency on a file “foo.js” in the same directory. Bundling and roots, and other issues makes this a interesting enough to be solved independently from this proposal.
The goog.module
call does three things:
- declares that the code within the file is a module and must be loaded differently
- ensures that the code is loaded as a module (not as global code)
- provides a unique id for the module independent from the filesystem and compatible with the existing dependency management
When the code is evaluated, it is wrapped in a function to create the module scope. For bundling it would look like:
goog.loadModule(function(exports) {
“use strict”;
… module source …
return exports;
});
//# sourceUrl . . . source url ...
Note: In practice the code injected for loading the module would all be on the first line of the file. To avoid changing line numbers when the code is inspected in the browser. When building uncompiled bundles a “sourceUrl” is appended to allow supporting browsers to show the original file names, etc.
Note: the sourceUrl is not useful when concatenating files, only for individual files as inline script or using strict eval to create module scope. To preserve the filename in the browsers that support SourceUrl we need to use eval when bundling. See below.
In cases where eval hasn’t be prohibited, an alternate form of bundling is permitted:
goog.loadModule(“... json-escaped-source … \n//# sourceUrl … source url ...”);
For the debug loader, the source is loaded via synchronous Xhr.
goog.module
requires special loading. If you use custom bundling logic, it will need to be
updated to handle goog.module
. These techniques are already compatible with goog.module
:
- Karma test runner
- Closure Compiler compiled code
From within a goog.module, another goog.module’s exports are returned by the goog.require for that module. However, this isn’t the case from a traditional file. Here is an example of how to access the file:
goog.provide('my.namespace.Foo');
goog.require('some.module');
goog.scope(function() {
var namespace = my.namespace;
var module = goog.module.get('some.module');
namespace.Foo = function() {
use(module);
}
});
To migrate code it is possible to use goog.module.declareLegacyNamespace
to allow the use of a goog.module in place of a traditional file without
migrating them all in advance.
Traditionally, jsunit test files declared global functions using top level declarations, however a goog.module (like other module systems) top level declaration are hidden (the test runner will complain if it doesn’t see any test methods, so failing to export the tests shouldn't go unnoticed).
A closure style unit test would look like this:
goog.module('goog.baseModuleTest');
goog.setTestOnly('goog.baseModuleTest');
var jsunit = goog.require('goog.testing.jsunit');
var testSuite = goog.require('goog.testing.testSuite');
testSuite({
testMethod: function() {},
});
goog.setTestOnly()
isn’t required but is good practice. This pattern will work for ES6 modules as well.
By default a goog.module
files exports are sealed. If the export object is a
constructor, then AngularJS won’t be able to add the $inject
property it
expects to. This happens both during development (AngularJS adds the $inject
property to cache injections) and during production when using --angular_pass
.
You can work around this by disabling using the goog.SEAL_MODULE_EXPORTS
define.
Common logic for building bundles with goog.module
can be found here:
https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/deps/ClosureBundler.java
Currently, goog.module is rewritten by the Closure Compiler as a goog.scope. We expect the compiler at some point to include first class support for goog.module. File bugs if this causes you problems.
Bundling is preferred.
AsyncTestCase doesn't work well with goog.module and in fact it is no longer needed. Instead regular test can return a promise which should be resolved once test is finished. Example:
var Promise = goog.require('goog.Promise');
var testSuite = goog.require('goog.testing.testSuite');
testSuite({
testAsyncMethod: function() {
return new Promise(function(resolve) {
// finish test after 1 second.
setTimeout(function() { resolve(); }, 1000);
})
}
});