Spec-compliant universal module loader - loads ES6 modules, AMD, CommonJS and global scripts.
Designed as a small collection of extensions to the ES6 specification System loader, which can be applied individually (see lib) or all together (dist/system.js).
Features include:
- Core: Some small fixes to the spec loader behavior.
- Formats: Dynamically load AMD, CommonJS and global scripts (as well as ES6 modules) detecting the format automatically, or with format hints.
- Map: Map configuration.
- Versions: Multi-version support for semver compatible version ranges (
@^1.2.3
syntax). - Plugins: A dynamic plugin system for modular loading rules.
- Bundles: Dynamically link requires to bundle files.
Designed to work with the ES6 Module Loader polyfill (15KB minified) for a combined footprint of 27KB. 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 the browser and NodeJS.
- Module Format Hints
- Loading Global Scripts
- AMD Compatibilty Layer
- Map Config
- Multi-version Semver Support
- Relative Dynamic Loading
- Plugins
- NodeJS Use
- Compiling ES6 to ES5 and AMD
- Compiling into a single file
- Production Bundles
- CSP-Compatible AMD Production
Download es6-module-loader.js
and traceur.js
from the ES6-loader polyfill and locate them in the same folder as system.js
from this repo.
Then include dist/system.js
with a script tag in the page:
<script src="system.js"></script>
es6-module-loader.js
will then be included automatically and the Traceur parser is dynamically included from traceur.js
when loading an ES6 module only.
app/test.js:
define(function() {
return {
isAMD: 'yup'
};
});
In the index.html
page we can then load a module with:
<script>
System.import('app/test').then(function(test) {
console.log(test.isAMD); // yup
}).catch(function(err) {
setTimeout(function() {
throw err;
}, 1);
});
</script>
The module file at URL app/test.js
will be loaded, its module format detected and any dependencies in turn loaded before returning the defined module.
The entire loading class is implemented identically to the ES6 module specification, with the module format detection rules being the only addition.
The loading function uses promises. To ensure errors are thrown a catch handler needs to be attached as shown.
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
, enteringsecurity.fileuri.strict_origin_policy
in the filter box and toggling the option to false.
Most of what is discussed in this section is simply the basics of using the new
System
loader. Only the extra module format support and plugin system is additional to this browser specification.
Modules are dependency-managed JavaScript files. They are loaded by a module name reference.
Each module name directly corresponds to a JavaScript file URL, but without the .js
extension, and with some additional resolution rules.
The default resolution rule is:
my/module -> resolve(pageURL, 'my/module') + '.js'
The System
loader specification describes a paths configuration system.
Note: The implementation is currently in discussion only and not yet specified, thus it is subject to change.
Typically one would like all modules to be loaded from a library folder containing different modules. We can set this up with:
System.paths['*'] = '/lib/*.js';
This is useful to reference shared library scripts like jquery
, underscore
etc.
We then create a path for our local application scripts in their own separate folder, which can also be set up with paths config:
System.paths['app/*'] = '/app/*.js';
Non-wildcard paths are also supported, and the most specific rule will always be used.
In this example, we can now write local application code in its own folder (app
), without conflict with library code (js
):
app/main.js:
define(['jquery'], function($) {
return {
// ...
};
});
index.html:
<script>
System.paths['*'] = '/lib/*.js';
System.paths['app/*'] = '/app/*.js';
</script>
<script>
System.import('main');
</script>
This will load /app/main.js
, which in turn is only loaded after loading the dependency /lib/jquery.js
.
For SystemJS it is recommended to write modular code in either AMD or CommonJS. Both are equally supported by SystemJS, with the format detected automatically.
For example, we can write modular CommonJS:
app/module.js:
var subModule = require('./submodule/submodule');
//...
subModule.someMethod();
//...
app/submodule/submodule.js:
exports.someMethod = function() {
}
and load this with System.import('app/module')
in the page.
Note: always use relative requires of the form
./
or../
to reference modules in the same package. This is important within any package for modularity.
SystemJS is an ES6 module loader. It will detect and load ES6 modules, parsing them with Traceur dynamically. This allows for dynamic loading of ES6 without a build step, although a build step still needs to be run to transpile ES6 back to ES5 and AMD for production.
A very simple example:
app/es6-file.js:
export class q {
constructor() {
this.es6 = 'yay';
}
}
<script>
System.import('es6-file').then(function(m) {
console.log(new m.q().es6); // yay
});
</script>
ES6 modules define named exports, provided as getters on a special immutable Module
object.
For the production build workflow, see the compiling ES6 to ES5 and AMD.
For further examples of loading ES6 modules, see the ES6 Module Loader polyfill documentation.
The module format detection is well-tested over a large variety of libraries including complex UMD patterns. It will detect in order ES6, AMD, then CommonJS and fall back to global modules.
It is still impossible to write 100% accurate detection though.
For this reason, it is also possible to write modules with the module format specified. The module format is provided as a string, as the first line of code (excluding comments) in a file:
"amd";
define(['some-dep'], function() {
return {};
});
Similarly, "global"
, "cjs"
and "es6"
can be used in module files to set the detection.
It is recommended to use a format hint only in the few cases where the format detection would otherwise fail.
When no module format is detected, or when the "global"
hint is present, modules are treated as global scripts.
Any properties written to the global object (window
, this
, or the outer scope) will be detected and stored. Then any dependencies of the global will have these properties rewritten before execution.
In this way, global collissions are avoided. Multiple versions of jQuery can run on the same page, for example.
When only one new property is added to the global object, that is taken to be the global module.
When many properties are written to the global object, the collection of those properties becomes the global module.
This provides loading as expected in the majority of cases:
app/sample-global.js:
hello = 'world';
System.import('app/sample-global').then(function(sampleGlobal) {
console.log(sampleGlobal); // 'world'
});
The automatic detection handles most cases, but there are still scenarios where it is necessary to define the exported global name.
To specify the exported name, provide an "export"
string, directly beneath the "global"
hint.
app/my-global.js:
"global";
"export MyGlobal.obj";
window.MyGlobal = {
obj: "hello world"
};
window.__some__other_global = true;
System.import('app/my-global').then(function(m) {
console.log(m); // 'hello world'
});
Global modules can also specify dependencies using this same hint system.
We write an "import"
string, directly beneath the "global"
hint.
js/jquery-plugin.js:
"global";
"import jquery";
"export $";
$.fn.myPlugin = function() {
// ...
}
System.import('jquery-plugin').then(function($) {
$('#some-el').myPlugin();
});
The primary use for having all this information in the module is that global scripts can be converted into modular scripts with complete accuracy by an automated process based on simple configuration instead of manual conversion.
This information can equally be provided through configuration with System.shim[module/name] = { deps: [], exports: '' }
, but it is recommended to inline it within the file for stronger modularity.
As part of providing AMD support, SystemJS provides a small AMD compatibility layer, with the goal of supporting as much of the RequireJS test suite as possible to ensure functioning of existing AMD code.
To create the requirejs
and require
globals as AMD globals, simply include the following <script>
tag immediately after the inclusion of the System loader:
<script>
require = requirejs = System.require;
</script>
This should replicate a fair amount of the dynamic RequireJS functionality, and support is improving over time.
Note that AMD-style plugins are not yet supported.
Map configuration alters the module name at the normalization stage. It is useful for creating aliases and version mappings.
Example:
System.map['jquery'] = 'app/[email protected]';
System.import('jquery') // behaves identical to System.import('app/[email protected]')
Map configuration also affects submodules:
System.import('jquery/submodule') // normalizes to -> `app/[email protected]/submodule'
An optional syntax for version support can be used: moduleName@version
.
For example, consider an app which uses [email protected]
, but would like to invalidate the cache when jQuery is updated.
We write:
System.versions['jquery'] = ['2.0.3'];
Now when I do:
System.import('jquery');
a load will be made to the file /lib/[email protected]
.
This way, the version can be updated through configuration.
For multi-version support, we can provide multiple versions:
System.versions['jquery'] = ['2.0.3', '1.8.3'];
These corresponds to /lib/[email protected]
and /lib/[email protected]
.
I can now write semver-compatible requires of any of the following forms, to get one of the above:
System.import('jquery') // -> /lib/[email protected]
System.import('jquery@2') // -> /lib/[email protected]
System.import('[email protected]') // -> /lib/[email protected]
System.import('jquery@1') // -> /lib/[email protected]
System.import('[email protected]') // -> /lib/[email protected]
System.import('[email protected]') // -> /lib/[email protected]
// semver compatible form (caret operator ^)
System.import('jquery@^2') // -> /lib/[email protected]
System.import('jquery@^1.8.2') // -> /lib/jquery@^1.8.3.js
System.import('jquery@^1.8') // -> /lib/jquery@^1.8.3.js
The semver compatibility operator (^
) is the most useful way of referencing versions for full semver support.
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 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')
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');
// loads './app.js' from the current directory
System.import('./app').then(function(m) {
console.log(m);
}, function(e) {
console.log(e);
});
If writing an application in ES6, we can compile back into AMD and ES5 by installing Traceur globally and using the command-line tool:
Install Traceur:
npm install traceur -g
Build the application into AMD and ES5:
traceur --dir app app-built --modules=amd
This will compile all ES6 files in the directory app
into corresponding AMD files in app-built
.
In our application HTML, we now need to include traceur-runtime.js
before es6-module-loader.js:
<script src="traceur-runtime.js"></script>
<script src="es6-module-loader.js"></script>
Now the application will continue to behave identically without needing to compile ES6 in the browser.
The next step for production is to then compile all of these separate AMD files into a single file for production, described below.
To build separate AMD modules into a single file, we can use the RequireJS optimizer:
Install the optimzer:
npm install requirejs -g
Build modules into a single file (assuming the main entry point is app-built/main
):
r.js -o name=app-built/main out=app-built.js paths.app=app-built
If not compiling from ES6, replace app-built
with app
, and the last argument setting paths.app
is not necessary.
This will build all dependencies of app-built/main
into a single file, app-built.js
located in the same folder as the app
folder.
Note that this build workflow only supports ES6 and AMD, and doesn't fully support plugins, CommonJS or global script loading. ES6-specific build workflows are the area of active development.
To use this single bundle instead of having separate requires, we can use bundle configuration.
This is necessary so that an import of app/main
can be routed to the correct bundle, instead of triggering a request to /app/main.js
.
We set this up with the configuration:
<script>
// we want to load 'app-built' from this location, not '/lib/app-built.js'
System.paths['app-built'] = '/app-built.js';
// create the bundle
System.bundles['app-built'] = ['app/main'];
System.import('app/main').then(function(m) {
// app/main now loaded from the bundle, and not a separate request
});
</script>
This informs the loader that it should load the bundle module /app-built.js
to find the module app/main
.
Any number of modules can be listed as belonging to the bundle.
SystemJS comes with a separate build for AMD 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-amd-production.js
.
Since we have compiled everything into AMD with the above, our production config can still work:
<script src="system-production.js"></script>
<script>
System.paths['app-built'] = '/app-built.js';
System.bundles['app-built'] = ['app/main'];
System.import('app/main').then(function(m) {
//...
});
</script>
Note that this CSP-compatibility mode doesn't fully support plugins, CommonJS or global script loading.
SystemJS is simply a build of a collection of separate addons. Different build collections can be customized for different loading scenarios in the Makefile.
Alternatively individual addons can also just be applied individually copying them from the lib folder.
To understand the loader hooks, read the custom loader section of the ES6 Module Loader readme.
The order in which module format detection is performed, is provided by the System.formats
. The default value is ['amd', 'cjs', 'global']
.
To add a new module format, specify it in the System.formats
array, and then provide a System.format
rule for it.
The format rule provides two functions - detection which returns dependencies if detection passes, and an execution function.
System.formats = ['amd', 'cjs', 'myformat', 'global'];
System.format.myformat = {
detect: function(source, load) {
if (!source.match(formatRegEx))
return false;
// return the array of dependencies
return getDeps(source);
},
execute: function(load, depMap, global, execute) {
// provide any globals
global.myFormatGlobal = function(dep) {
return depMap[dep];
}
// alter the source before execution
load.source = '(function() {' + load.source + '}();';
// execute source code
execute();
// clean up any globals
delete global.myFormatGlobal;
// return the defined module object
return global.module;
}
}
For further examples, see the internal AMD or CommonJS support implemented in this way here.
A plugin is just a set of overrides for the loader hooks of the ES6 module specification.
The hooks plugins can override are locate
, fetch
and translate
.
Read more on the loader hooks at the ES6 Module Loader polyfill page.
For example, we can write a CoffeeScript plugin with the following (CommonJS as an example, any module format works fine):
js/coffee.js:
var CoffeeScript = require('coffeescript');
exports.translate = function(load) {
return CoffeeScript.compile(load.source);
}
By overriding the translate
hook, we now support CoffeeScript loading with:
- js/
- coffee.js our plugin above
- coffeescript.js the CoffeeScript compiler
- app/
- main.coffee
Then assuming we have a app
[path config](#Paths Configuration) set to the /app
folder, and the baseURL set to /js/
, we can write:
System.import('app/main.coffee!').then(function(main) {
// main is now loaded from CoffeeScript
});
A CSS plugin, on the other hand, would override the fetch hook:
js/css.js:
exports.fetch = function(load) {
// return a thenable for fetching (as per specification)
// alternatively return new Promise(function(resolve, reject) { ... })
return {
then: function(resolve, reject) {
var cssFile = load.address;
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = cssFile;
link.onload = resolve;
document.head.appendChild(link);
}
};
}
Each loader hook can either return directly or return a thenable for the value.
The other loader hooks are also treated identically to the specification.
MIT