Extensions for the new ES6 System browser loader, which will be natively provided in browsers.
A small (10KB minfied) collection of extensions to the System loader, for supporting AMD, CommonJS and global script loading, aiming to ease the transition to ES6.
Extensions are self-contained additions to the System
global, which can be applied individually (see lib) or all together (dist/system.js).
- Formats: Dynamically load AMD, CommonJS and global scripts (as well as ES6 modules) detecting the format automatically, or with format hints.
- Map: Map configuration.
- Plugins: A dynamic plugin system for modular loading rules.
- Versions: Multi-version support for semver compatible version ranges (
@^1.2.3
syntax).
Designed to work with the ES6 Module Loader polyfill (17KB minified) for a combined footprint of 32KB.
Runs in the browser and NodeJS.
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 from the baseURL folder with:
<script>
System.import('app/test').then(function(test) {
console.log(test.isAMD); // yup
});
</script>
The module file at 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.
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 baseURL rules, mostly identical to RequireJS.
The default baseURL rule is:
my/module -> resolve(baseURL, 'my/module') + '.js'
By default, the baseURL is set to the current page, but it can be changed with:
<script>
System.baseURL = '/js/';
</script>
The System
loader comes with a paths configuration system. While not part of SystemJS, it is described here for completion.
Note: The implementation is currently in discussion and not specified, thus it is subject to change.
One can use the baseURL to reference library scripts like jquery
, underscore
etc.
We then create a path for our local application scripts in their own separate folder, which can 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['app/*'] = '/app/*.js'; </script>
<script>
System.import('app/main');
</script>
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.
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.
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 further examples of loading ES6 modules, see the ES6 Module Loader polyfill documentation.
For examples of build workflows, see the jspm CLI documentation.
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.
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 supported.
Map configuration works just like other module loaders, altering the module name at the normalization stage.
Example:
System.map['jquery'] = 'app/jquery@1.8.3';
System.import('jquery') // behaves identical to System.import('app/jquery@1.8.2')
Map configuration also affects submodules:
System.import('jquery/submodule') // normalizes to -> `app/jquery@1.82/submodule'
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.
Plugins can be created to 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){})
- Markdown
System.import('app/some/project/README.md!').then(function(html) {})
- Text
System.import('some/text.txt!text').then(function(text) {})
- WebFont
System.import('google Port Lligat Slab, Droid Sans !font')
Links will be provided soon!
Note that the AMD compatibility layer could provide a mapping from AMD plugins into SystemJS plugins that provide the same functionality as associated SystemJS plugins.
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