2. Module Formats - rbarilani/systemjs GitHub Wiki

Module Formats

The following module formats are supported:

The module format can be set via meta configuration:

SystemJS.config({
  meta: {
    './module/path.js': {
      format: 'esm'
    }
  }
});

SystemJS 0.20 also comes with experimental support for Web Assembly. This can be enabled via:

SystemJS.config({
  wasm: true
});

Web Assembly binaries can then be loaded alongside SystemJS modules normally.

Module format detection

When the module format is not set, automatic regular-expression-based detection is used. This module format detection is never completely accurate, but caters well for the majority use cases.

The module format detection happens in the following order:

  • System.register / System.registerDynamic If the source code starts with any number of comments, followed by System.register or System.registerDynamic as the first line of code.
  • ES modules The source is only detected as an ES module if it contains explicit module syntax - valid import or export statements. This detection does not do comment removal, so comments in the code containing valid ES module syntax will trigger this detection.
  • AMD modules The presence of a valid AMD define statement in the code, with all forms of the define statement supported.
  • CommonJS modules The presence of require(...) or exports / module.exports assigments.
  • Global This is the fallback module format after all the above fail.

When Web Assembly is enabled, Web Assembly modules are automatically detected from their binary header. This is the same detection process that will likely be implemented in browsers.

Note that ES6 modules are detected via the presence of import and export module syntax and no other features at all. This is because the transpilation applies to the module format specifically, not the language.

Inter-Format Dependencies

Any module type can be loaded from any other type with full support.

When loading CommonJS, AMD or Global modules from within ES6, the full module is available at the default export which can be loaded with the default import syntax.

For convenience, named exports are also auto-populated but may not be correctly bound as expected, so use these carefully.

./app/es6-loading-commonjs:

// entire underscore instance
import _ from './underscore.js';

// unbound named export
import {map} from './underscore.js';

ES Modules

ES6 modules are automatically transpiled as they are loaded, using the loader transpiler option set. A transpiler plugin must be configured for this to work.

Circular references and bindings are implemented to the ES6 specification.

The __moduleName local variable is also available, pending clarification of the module meta in the WhatWG loader spec.

This provides the fully normalized name of the current module which can be useful for dynamically loading modules relative to the current module via:

SystemJS.import('./local-module', __moduleName);

In due course this will be entirely replaced by the contextual loader once this has been specified.

ES modules are loaded via XHR making it incompatible with scriptLoad: true. ES modules should always be built for production to avoid transpiler costs, making this a development-only feature.

CommonJS

  • The module, exports, require, global, GLOBAL, __dirname and __filename variables are all declared in scope.
  • module.id is set.
  • require.resolve is provided.

When executing CommonJS any global define is temporarily removed from the global object, before being reverted after synchronous module execution has completed. This ensures that any UMD patterns trigger the CommonJS path.

For comprehensive handling of NodeJS modules, a conversion process is needed to make them SystemJS-compatible, such as the one used by jspm.

CommonJS is loaded via XHR making it incompatible with scriptLoad: true unless pre-compiling with SystemJS Builder.

Note that CommonJS modules on npm, loaded as CommonJS may well not load correctly through SystemJS. This is because SystemJS does not implement the NodeJS loading algorithm.

If you want to load NodeJS modules through SystemJS you can use import nodeModule from '@node/node-module-name', but this should only be used when absolutely necessary as it stops code from being universal, and makes it only compatible with NodeJS.

AMD

  • AMD support includes all AMD structural variations including the CommonJS wrapper form.
  • The special module, exports, and require module names are handled at the AMD format level and are not defined in the primary loader registry. module.uri and module.id are provided with module.config as a no-op.
  • Named defines are supported and will write directly into the loader registry.
  • A single named define will write into the loader registry but also be treated as the value of the module loaded if the names do not match. This enables loading a module containing define('jquery', ....
  • Contextual dynamic requires are fully supported (define(function(require) { require(['./dynamic/require'], callback) }))

When executing AMD, the global module, exports are temporarily removed, and the global define and require are set to the SystemJS AMD functions.

By default AMD modules are loaded via <script> tag injection making them CSP-compatible, provided that modules that are AMD are indicated via meta so that SystemJS knows to skip format detection and load them with script tags.

RequireJS Support

To use SystemJS side-by-side in a RequireJS project, make sure to include RequireJS after ES6 Module Loader but before SystemJS.

Conversely, to have SystemJS provide a RequireJS-like API in an application set:

window.define = SystemJS.amdDefine;
window.require = window.requirejs = SystemJS.amdRequire;

Globals

The global format loads globals identically to if they were included via <script> tags but with some extra features including the ability to shim dependencies, set custom globals, and define the exports of the global module.

By default, the exports of a global are calculated as the diff of the environment global from before to after execution.

This provides a convenient mechanism for auto-conversion of globals into modules.

For example:

var MyGlobal = 42;

Will get converted into the module Module({ default: 42 }).

While the script:

(function(global) {
  global.globalA = 'global A';
  global.globalB = 'global B';
})(typeof self != 'undefined' ? self : global);

Will get converted into the module Module({ globalA: 'global A', globalB: 'global B' })

Globals are picked up by variable assignment and undeclared assignment:

var x = 'global'; // detected as a global
y = 'global';     // detected as a global

These two cases fail in WebWorkers, so do need to have their exports explicitly declared if compatibility is desired.

Globals are not removed from the global object for shim compatibility, but this could become possible in future if all globals use the globals meta for shims instead of deps.

Shim Dependencies

When loading plugins of globals like Angular or jQuery plugins, we always need to shim the dependencies of the plugin to be dependent on the global it expects to find.

We do this via deps metadata on the module:

SystemJS.config({
  meta: {
    'vendor/angular-ui-router.js': {
      deps: ['/vendor/angular.js']
    }
  }
});
SystemJS.import('vendor/angular-ui-router.js');

Note that deps is only supported for global modules.

It is always advisable to explicitly shim global modules as above for any globals they expect to be present. For example, the above module may work fine without the shim if Angular always happens to load first in the page, but this isn't always guaranteed, and problems will likely be hit later on when the load order happens to change.

Custom Globals

When shimming dependencies, the issue with this is that every dependency needs to be a global in order to be loadable by a global.

This holds the entire ecosystem back as globals become the lowest common denominator.

If we want to upgrade Angular to an ES6 version of Angular, while still supporting old Angular global modules, we can do this via custom globals:

SystemJS.config({
  meta: {
    'vendor/angular-ui-router.js': {
      globals: {
        angular: 'vendor/angular.js'
      }
    }
  }
});
SystemJS.import('vendor/angular-ui-router.js');

In the above scenario, a globally scoped angular will be set to the module value for the Angular ES6 module only for the duration of execution of the global plugin. They will be reverted to whatever they where before after execution, if they didn't exist they're removed. This doesn't influence the globals that might already be generated by the referenced package.

The globals meta-configuration option is only available for the global and cjs module formats. as these are the only formats that are source-code-transformation based.

Referenced packages automatically becomes dependencies.

Exports

When automatic detection of exports is not enough, a custom exports meta value can be set.

This is a member expression on the global object to be taken as the exports of the module.

For example, angular or jQuery.fn.pluginName.

Globals can be loaded in a way that is CSP-compatible by setting their format and exports metadata when not setting any globals metadata. SystemJS then knows it can use script tag injection for this case. For example, Google Analytics can be loaded without requiring CORS or CSP via setting:

SystemJS.config({
  meta: {
    'https://www.google-analytics.com/analytics.js': {
      exports: 'ga',
      format: 'global'
    }
  }
});
⚠️ **GitHub.com Fallback** ⚠️