SystemJS
Spec-compliant universal module loader - loads ES6 modules, AMD, CommonJS and global scripts.
Designed as a collection of small extensions to the ES6 specification System loader, which can also be applied individually.
Designed to work with the ES6 Module Loader polyfill (8KB) for a combined total footprint of 16KB minified and gzipped. In future, with native implementations, the ES6 Module Loader polyfill should no longer be necessary. As jQuery provides for the DOM, this library can smooth over inconsistiencies and missing practical functionality provided by the native System loader.
Runs in IE8+ and NodeJS.
For discussion, see the Google Group.
Basic Configuration
Setup
Download es6-module-loader.js
and traceur.js
and locate them in the same folder as system.js
from this repo.
We then include dist/system.js
with a script tag in the page.
es6-module-loader.js
will then be included from the same folder automatically and Traceur is dynamically included from traceur.js
when loading an ES6 module only.
Alternatively, es6-module-loader.js
or traceur.js
can be included before system.js
with a script tag in the page.
Simple Application Structure
The standard application structure would be something like the following:
index.html:
<script src="system.js"></script>
<script>
System.config({
baseURL: '/lib',
paths: {
'app/*': '/app/*.js'
}
});
System.import('app/app')
.catch(console.error.bind(console));
</script>
app/app.js:
require('./local-dep');
var $ = require('jquery');
console.log('loaded CommonJS');
Module format detection happens in the order System.register, ES6, AMD, then CommonJS and falls back to global modules.
Named defines are also supported, with the return value for a module containing named defines being its last named define.
Note that when running locally, ensure you are running from a local server or a browser with local XHR requests enabled. If not you will get an error message.
For Chrome on Mac, you can run it with: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --allow-file-access-from-files &> /dev/null &
In Firefox this requires navigating to about:config
, entering security.fileuri.strict_origin_policy
in the filter box and toggling the option to false.
Loading ES6
app/es6-file.js:
export class q {
constructor() {
this.es6 = 'yay';
}
}
<script>
System.import('app/es6-file').then(function(m) {
console.log(new m.q().es6);
}, console.error.bind(console));
</script>
ES6 modules define named exports, provided as getters on a special immutable Module
object.
To build for production, see the System.register build workflow.
For further infomation on ES6 module loading, see the ES6 Module Loader polyfill documentation.
Loading Other Formats
When loading from CommonJS, AMD or globals, SystemJS will detect the format automatically.
Any module type can be loaded from any other type.
When loading CommonJS, AMD or globals from ES6, use the default
import syntax:
app/es6-loading-commonjs:
import _ from './underscore';
Where underscore.js is located in the same folder.
Features
Map Configuration
Map configuration alters the module name at the normalization stage. It is useful for creating aliases:
System.map['jquery'] = 'location/for/jquery';
System.import('jquery')
System.import('jquery/submodule')
Contexual map configuration can also be used to provide maps only for certain modules, which is useful for version mappings:
System.map['some-module'] = {
jquery: 'jquery@2.0.3'
};
Contextual maps apply from the most specific module name match only.
Meta Configuration
The ES6 module loader uses a special metadata
object that is passed between hooks.
An example of meta config is the module format of a module, which is stored at metadata.format
.
The meta extension opens up this object for setting defaults through System.meta
as well as inline module syntax.
In this way, we can specify the module format of a module through config:
System.meta['some/module'] = {
format: 'amd'
};
System.import('some/module')
Or the module can even specify its own meta:
some/module.js
"format amd";
if (typeof module != 'undefined' && module.exports)
module.exports = 'cjs';
else
define(function() { return 'amd' });
Since it is impossible to write 100% accurate module detection, this inline format
hint provides a useful way of informing the module format of a module.
The format options are - register
, es6
, amd
, cjs
, global
.
Global Module Format Support
When a module is loaded as a global, the global object is detected automatically from the change in the window
properties:
app/sample-global.js
hello = 'world';
System.import('app/sample-global').then(function(m) {
m == 'world';
}, console.error.bind(console));
When multiple global properties are detected, the module object becomes the collection of those objects:
app/multi-global.js
first = 'global1';
var second = 'global2';
System.import('app/multi-global').then(function(m) {
m.first == 'global1';
m.second == 'global2';
}, console.error.bind(console));
Global dependencies can be specified with the deps
meta config:
app/another-global.js
$(document).fn();
this.is = 'a global dependent on jQuery';
System.meta['app/another-global'] = { deps: ['jquery'] };
Note that the name used in System.meta
must be the fully normalized name that is returned by Promise.resolve(System.normalize('module-name')).then(console.log.bind(console))
.
The exports
meta config can also be set (using inline meta as an example):
app/more-global.js
"format global";
"deps jquery";
"exports my.export";
(function(global) {
global.my = {
export: 'value'
};
$(document).fn();
})(typeof window != 'undefined' ? window : global);
There is also supports for the init
function meta config just like RequireJS as well.
IE8 Support
In IE8, automatic global detection does not work for globals declared as variables or implicitly:
var someGlobal = 'IE8 wont detect this';
anotherGlobal = 'unless using an explicit shim';
IF IE8 support is needed, these exports will need to be declared manually with configuration.
Versions
An optional syntax for version support can be used: moduleName@version
.
For example, consider an app which wants to specify the jQuery version through config:
System.versions['jquery'] = '2.0.3';
Now an import of the form:
System.import('jquery')
.catch(console.error.bind(console));
will load a load will be made to the file /lib/jquery@2.0.3.js
.
This centralises the version management to the configuration file, which is key to handling versions with correct caching.
For multi-version support, provide an array of versions:
System.versions['jquery'] = ['2.0.3', '1.8.3'];
These correspond to /lib/jquery@2.0.3.js
and /lib/jquery@1.8.3.js
.
Semver-compatible requires of any of the following forms can be used:
System.import('jquery')
System.import('jquery@2')
System.import('jquery@2.0')
System.import('jquery@1')
System.import('jquery@1.8')
System.import('jquery@1.8.2')
System.import('jquery@^2')
System.import('jquery@^1.8.2')
System.import('jquery@^1.8')
Relative Dynamic Loading
Modules can check their own name from the global variable __moduleName
.
This allows easy relative dynamic loading, allowing modules to load additional functionality after the initial load:
export function moreFunctionality() {
return System.import('./extrafunctionality', { name: __moduleName });
}
This can be useful for modules that may only know during runtime which functionality they need to load.
Plugins
Plugins handle alternative loading scenarios, including loading assets such as CSS or images, and providing custom transpilation scenarios.
Plugins are indicated by !
syntax, which unlike RequireJS is appended at the end of the module name, not the beginning.
The plugin name is just a module name itself, and if not specified, is assumed to be the extension name of the module.
Supported Plugins:
- CSS
System.import('my/file.css!')
- Image
System.import('some/image.png!image')
- JSON
System.import('some/data.json!').then(function(json){})
- Text
System.import('some/text.txt!text').then(function(text) {})
Additional Plugins:
- Markdown
System.import('app/some/project/README.md!').then(function(html) {})
- WebFont
System.import('google Port Lligat Slab, Droid Sans !font')
Creating custom plugins can be quite simple. See the plugins above, and read the guide here.
ES6 System.register Compilation
If writing an application in ES6, we can compile into ES5 with Traceur:
npm install traceur -g
traceur --dir app app-built --modules=instantiate
This will compile all ES6 files in the directory app
into corresponding ES5 System.register
files in app-built
.
The instantiate
modules option writes the modules out using a System.register
call, which is supported by SystemJS.
Then include traceur-runtime.js
(also found inside traceur's bin
folder when installed via npm) before es6-module-loader.js:
<script src="traceur-runtime.js"></script>
<script src="system.js"></script>
<script>
System.paths['app/*'] = 'app-built/*.js';
</script>
We can then use map or paths config to ensure that app/main
gets directed to the new folder. Alternatively rename app-built
to replace app
.
Now the application will continue to behave identically without needing to compile ES6 in the browser.
Compiling ES6 to ES5 and AMD
The same method above can also be used to compile ES6 into AMD with --modules=amd
.
We can then use the r.js optimizer to create a bundle with named defines, which are supported by SystemJS.
Note that the ES6 live bindings and circular references don't work in AMD, although circular references still work in many cases.
Bundles
Bundles configuration allows a single bundle file to be loaded in place of separate module files.
System.bundles['build/core'] = ['jquery', 'app/app', 'app/dep', 'lib/third-party'];
System.import('app/app'));
A built file must contain the exact named defines or named System.register
statements for the modules
it contains. Mismatched names will result in separate requests still being made.
We can create a custom bundle with Traceur by combining together a module with all its dependencies into a single file:
traceur --out build.js app/main.js app/core.js app/another.js
Each file will be traced and all its dependencies included in the final build file.
We can also just include this bundle with a <script>
tag in the page.
CSP-Compatible Production
SystemJS comes with a separate build for production only. This is fully CSP-compatible using script tag injection to load scripts, while still remaining an
extension of the ES6 Module Loader.
Replace the system.js
file with dist/system-csp.js
.
If we have compiled all our modules into a System.register
bundle, we can do:
<script src="system-csp.js"></script>
<script>
System.paths['app-built'] = '/app-built.js';
System.bundles['app-built'] = ['app/main'];
System.import('app/main').then(function(m) {
}, console.error.bind(console));
</script>
To make all module formats work with CSP, we need to ensure everything is built with a suitable wrapper.
See SystemJS Builder for a single-file build workflow that can wrap up all module formats.
RequireJS Support
To use SystemJS side-by-side in a RequireJS project, make sure to include RequireJS before SystemJS.
Conversely, to have SystemJS provide a RequireJS-like API in an application set:
window.define = loader.amdDefine;
window.require = window.requirejs = loader.amdRequire;
NodeJS Usage
To load modules in NodeJS, install SystemJS with:
npm install systemjs
We can then load modules equivalently to in the browser:
var System = require('systemjs');
System.import('./app').then(function(m) {
console.log(m);
}, function(e) {
console.log(e);
});
Contributing
Contributions are welcome. The goal of SystemJS is to encourage loaders made out of small self-contained features.
Since different builds can be created for different use cases, new builds or new features are welcome to be submitted for
consideration with pull requests.
Running the tests
To install the dependencies correctly, run bower install
from the root of the repo, then open test/test.html
in a browser with a local server
or file access flags enabled.
License
MIT