ES Module Loader Polyfill
Provides a polyfill and low-level API for the WhatWG loader spec to create a custom module loaders.
Supports the System.register module format to provide identical module loading semantics as ES modules in environments today.
ES6 Module Loader Polyfill, the previous version of this project built to the outdated ES6 loader specification is available at the 0.17 branch.
Module Loader Examples
Some examples of common use case module loaders built with this project are provided below:
-
Browser ES Module Loader:
A demonstration-only loader to load ES modules in the browser including support for the <script type="module">
tag as specified in HTML.
-
Node ES Module Loader
Allows loading ES modules with CommonJS interop in Node via node-esml module/path.js
in line with the current Node
plans for implementing ES modules. Used to run the tests and benchmarks in this project.
-
System Register Loader:
A fast optimized production loader that only loads System.register
modules, recreating ES module semantics with CSP support.
Installation
npm install es-module-loader --save-dev
Creating a Loader
This project exposes a public API of ES modules in the core
folder.
The minimal polyfill loader is provided in core/loader-polyfill.js
. On top of this the main API file is
'core/register-loader.js'` which provides the base loader class.
Helper functions are available in core/resolve.js
, core/common.js
, core/fetch.js
and everything that is exported can be considered
part of the publicly versioned API of this project.
Any tool can be used to build the loader distribution file from these core modules - Rollup is used to do these builds in the example loaders above,
provided by the rollup.config.js
file in the example loader repos listed above.
Loader Hooks
Implementing a loader on top of the RegisterLoader
base class involves extending that class and providing normalize
and instantiate
prototype
methods.
These hooks are not in the spec, but defined here and as an abstraction provided by this project to make it easy to create custom loaders:
import RegisterLoader from 'es-module-loader/core/register-loader.js';
class MyCustomLoader extends RegisterLoader {
constructor(baseKey) {
super(baseKey);
}
[RegisterLoader.normalize](key, parentKey, metadata) {
var relativeResolved = super[RegisterLoader.normalize](key, parentKey, metadata) || key;
return relativeResolved;
}
[RegisterLoader.instantiate](key, metadata) {
return undefined;
}
}
The default loader as described above would support loading modules if they have already been registered by key via
loader.register
calls (the System.register
module format, where System
is the global loader name).
Normalize Hook
Relative normalization of the form ./x
is already performed using the internal resolver in core/resolve.js
so that the key provided into normalize will never be a relative URL - it will either be a plain / bare name
or an absolute URL.
The return value of normalize
is the final key that is set in the registry (available and iterable as per the spec
at loader.registry
).
Instantiate Hook
Instantiating ES Modules via System.register
When instantiate returns undefined
, it is assumed that the module key has already been registered through a
loader.register(key, deps, declare)
call, following the System.register module format.
For example:
[RegisterLoader.instantate](key, metadata) {
this.register(key, deps, declare);
return undefined;
}
When using the anonymous form of System.register - loader.register(deps, declare)
, in order to know
the context in which it was called, it is necessary to call the loader.processRegisterContext(contextKey)
method:
[RegisterLoader.instantiate](key, metadata) {
this.register(deps, declare);
this.processRegisterContext(key);
return undefined;
}
The loader can then match the anonymous register call to the right module key. This is used to support <script>
loading
of anonymous System.register
modules.
Instantiating Dynamic / Legacy Modules via ModuleNamespace
Legacy module formats are not transpiled into System.register
, rather they need to be executed according to their own semantics.
The instantiate can handle its own execution pipeline for these legacy modules (like calling out to the Node require in the node-es-module-loader).
Having created a module instance, we wrap it in a ModuleNamespace
object and can return that directly from instantiate:
import { InternalModuleNamespace } from 'es-module-loader/core/loader-polyfill.js'
instantiate(key, metadata) {
var module = customModuleLoad(key);
return new InternalModuleNamespace({ default: module });
}
Using these two types of return values, we can thus recreate ES module semantics interacting with legacy module formats.
Note that InternalModuleNamespace
is not provided in the WhatWG loader specification - the specification actually uses a Module.Status
constructor.
We've chosen to take the route of implementing a custom private method over the spec, until that spec work can be fully stabilized, instead of having
to track small changes of this spec API over major versions of this project.
Tracing API
When loader.trace = true
is set, loader.loads
provides a simple tracing API.
Also not in the spec, this allows useful tooling to build on top of the loader.
loader.loads
is keyed by the module ID, with each record of the form:
{
key,
dependencies,
depMap,
metadata
}
Instantiate functions that return an InternalModuleNamespace
instance directly are not included in the trace registry.
Custom loaders that want to share the same trace format, should populate the trace themselves using their internal knowledge of the legacy module dependency information.
Spec Differences
The loader API in core/loader-polyfill.js
matches the API of the current WhatWG loader specification as closely as possible, while
making a best-effort implementation of the upcoming loader simplification changes as descibred in https://github.com/whatwg/loader/issues/147.
Error handling is implemented as in the HTML specification for module loading, such that rejections reject the current load tree, but
are immediately removed from the registry to allow further loads to retry loading.
- Instead of storing a registry of ModuleStatus objects, we store a registry of Module Namespace objects. The reason for this is that asynchronous rejection of registry entries as a source of truth leads to partial inconsistent rejection states
(it is possible for the tick between the rejection of one load and its parent to have to deal with an overlapping in-progress tree),
so in order to have a predictable load error rejection process, loads are only stored in the registry as fully-linked Namespace objects
and not ModuleStatus objects as promises for Namespace objects (Module.evaluate is still supported though).
Loader
and Module
are available as named exports from core/loader-polyfill.js
but are not by default exported to the global.Reflect
object.
This is to allow individual loader implementations to determine their own impact on the environment.- A constructor argument is added to the loader that takes the environment baseKey to be used as the default normalization parent.
- An internal
Loader.prototype[Loader.instantiate]
hook is used as well as the Loader.prototype[Loader.resolve]
hook
in order to ensure that uses of loader.resolve
do not have to result in module loading and execution, as discussed in https://github.com/whatwg/loader/issues/147#issuecomment-230407764.
License
Licensed under the MIT license.