Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

goog.module: an ES6 module like alternative to goog.provide

Bradford C. Smith edited this page Jan 23, 2023 · 14 revisions

goog.module

goog.module is a module system for JavaScript tied to Closure Compiler and Closure Library. It was originally introduced to replace the traditional goog.provide and goog.require structure while behaving similarly to ES modules. Presently goog.module is recommended over goog.provide for new Closure files.

Basic example:

goog.module('foo');

const Quux = goog.require('baz.Quux');

exports.Bar = function() { /* … */ };

IMPORTANT: The compiler expects to see declarations for the goog.* methods in its input files. You should make sure to include the closure/goog/base.js file from the closure-library repository among the --js inputs to the compiler.

goog.modules are similar to CommonJS or ES6 modules. Main features:

  • Inside a goog.module file, goog.require 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 is a typical goog.module for a class:

goog.module('some.module.identifier');

// Require a traditional namespace:
const array = goog.require('goog.array');
// Require a module dependency:
const SomeClass = goog.require('my.namespace.SomeClass');

class MyClass extends SomeClass {
  constructor() {
    doSomething();
  }
}

exports = {MyClass};

Here is a typical goog.module for a namespace:

goog.module('some.module.identifier');

// Require a traditional namespace:
const array = goog.require('goog.array');
// Require a module dependency:
const SomeClass = goog.require('my.namespace.SomeClass');

exports.method1 = function() {};
exports.method2 = function() {};

Migrating from goog.provide

Here's a simple example of a file with goog.provide vs. goog.module:

goog.provide('foo');

goog.require('baz.Quux');

foo.Bar = function() { /* … */ };

after:

goog.module('foo');

const Quux = goog.require('baz.Quux');

exports.Bar = function() { /* … */ };

A Note about Module Identifiers

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.

Export styles

In a goog.module file, there are two distinct styles, based on the two styles of exports in ES6 modules. Note that unlike with ES6 modules, goog.module cannot use both styles in a single module; each goog.module file is considered to be using either a default export or named exports.

Default exports

In goog.module, a default export can be created by assigning directly to the export of a module, and is the most common export style.

goog.module('a.b.c.Foo');

class Foo {}

exports = Foo; 

Default exports don't translate easily to ES module semantics. Hence, do not use default exports; use named exports instead.

Named exports

Named exports from a goog.module can be defined in one of two ways, the first way is to assign to fields of exports:

goog.module('d.e.f');

exports.value = 5;

class Foo {}
exports.Foo = Foo;

The other way is to assign a struct of predefined symbols to exports, with a style similar to the revealing module pattern:

goog.module('d.e.f');

const value = 5;

class Foo {}

exports = {Foo, value};

Note that this style closely mirrors the the similar syntax in ES6 module exports.

Destructuring imports

Named exports can be imported using destructuring to bring in the names of interest as unqualified names in the importing module, such as:

goog.module('x.y.z');

const {value} = goog.require('d.e.f');

alert(value);

A common point of confusion when using destructuring requires is when an assignment like exports = {...} does or does not named exports. In the following case, the module m cannot be imported with destructuring:

goog.module('m');
exports = {NAME: 'Foo Bar'};
goog.module('n');
const {NAME} = goog.require('m');

The compiler reports:

input1:2: ERROR - [JSC_DOES_NOT_HAVE_EXPORT] Requested module does not have an export "NAME".
const {NAME} = goog.require('m');
^

Why can't you destructure NAME, given that m includes exports = {NAME: 'Foo Bar'};? In short: it's because 'Foo Bar' is a string, not a name.

In order to make goog.module exports more closely align with ES module exports, the compiler considers assigning an object literal to exports to create 'named exports' if and only if all the values in the object literal are names. In order to export arbitrary expressions, use one of the following patterns:

goog.module('m');
const NAME = 'FOO BAR';
exports = {NAME};

or

goog.module('m');
exports.NAME = 'FOO BAR';

Non-destructuring imports

Any goog.module can be required without using destructuring, regardless of whether the module has named exports or default exports. For example, all of the following modules can be imported as a name:

goog.module('empty');
goog.module('default.export.of.Klass');
exports = class Klass {};
goog.module('multiple.exports.of.names');
exports.foo = 'foo';
exports.bar = 'bar';
goog.module('client');
const empty = goog.require('empty');  // {}
const Klass = goog.require('default.export.of.Klass'); // class Klass {}
const names = goog.require('multiple.exports.of.names'); // {foo: 'foo', bar: 'bar'}
alert(empty, new Klass(), names.foo + names.bar);

Implementation details

Why the goog.module declaration?

The goog.module call does three things:

  1. declares that the code within the file is a module and must be loaded differently
  2. ensures that the code is loaded as a module (not as global code)
  3. provides a unique id for the module independent from the filesystem and compatible with the existing dependency management

Bundling goog.module files with other javascript.

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.

Who can use goog.module now

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

FAQ

How do I use a goog.module from a traditional Closure file?

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. For example:

goog.module('foo.Bar');
goog.module.declareLegacyNamespace();

class Bar {}
exports = Bar;

Any existing files referencing a global foo.Bar may still reference that global, e.g.

goog.provide('foo.Client');
goog.require('foo.Bar');

foo.Client = class extends foo.Bar {};

How do I write a unit test for a goog.module?

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');

const jsunit = goog.require('goog.testing.jsunit');
const testSuite = goog.require('goog.testing.testSuite');

testSuite({
  testMethod() {},
});

goog.setTestOnly() isn’t required but is good practice. This pattern will work for ES6 modules as well.

Known Issues

goog.module causes problems with Angular injected classes

By default a goog.module file's 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.

Custom Code Bundlers need to be updated

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

Closure Debug loader uses synchronous XHR

Bundling is preferred.

Writing async tests

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);
    })
  }
});