UNPKG

15.4 kBMarkdownView Raw
1# ES Module Loader Polyfill [![Build Status][travis-image]][travis-url]
2
3Provides [low-level hooks](#registerloader-hooks) for creating ES module loaders, roughly based on the API of the [WhatWG loader spec](https://github.com/whatwg/loader),
4but with [adjustments](#spec-differences) to match the current proposals for the HTML modules specification, [unspecified WhatWG changes](https://github.com/whatwg/loader/issues/147), and [NodeJS ES module adoption](https://github.com/nodejs/node/issues/8866).
5
6Supports the [loader import and registry API](#base-loader-polyfill-api) with the [System.register](docs/system-register.md) module format to provide exact module loading semantics for ES modules in environments today. In addition, support for the [System.registerDynamic](docs/system-register-dynamic.md) is provided to allow the linking
7of module graphs consisting of inter-dependent ES modules and CommonJS modules with their respective semantics retained.
8
9This project aims to provide a fast, minimal, unopinionated loader API on top of which custom loaders can easily be built.
10
11See the [spec differences](#spec-differences) section for a detailed description of some of the specification decisions made.
12
13ES6 Module Loader Polyfill, the previous version of this project was built to the [outdated ES6 loader specification](http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts#august_24_2014_draft_rev_27) and can still be found at the [0.17 branch](https://github.com/ModuleLoader/es-module-loader/tree/0.17).
14
15### Module Loader Examples
16
17Some examples of common use case module loaders built with this project are provided below:
18
19- [Browser ES Module Loader](https://github.com/ModuleLoader/browser-es-module-loader):
20 A demonstration-only loader to load ES modules in the browser including support for the `<script type="module">` tag as specified in HTML.
21
22- [Node ES Module Loader](https://github.com/ModuleLoader/node-es-module-loader)
23 Allows loading ES modules with CommonJS interop in Node via `node-esml module/path.js` in line with the current Node
24 plans for implementing ES modules. Used to run the tests and benchmarks in this project.
25
26- [System Register Loader](https://github.com/ModuleLoader/system-register-loader):
27 A fast optimized production loader that only loads `System.register` modules, recreating ES module semantics with CSP support.
28
29### Installation
30
31```
32npm install es-module-loader --save-dev
33```
34
35### Creating a Loader
36
37This project exposes a public API of ES modules in the `core` folder.
38
39The minimal [polyfill loader API](#base-loader-polyfill-api) is provided in `core/loader-polyfill.js`. On top of this main API file is
40`core/register-loader.js` which provides a base loader class with the non-spec `System.register` and `System.registerDynamic` support to enable the exact
41linking semantics.
42
43Helper functions are available in `core/resolve.js` and `core/common.js`. Everything that is exported can be considered
44part of the publicly versioned API of this project.
45
46Any tool can be used to build the loader distribution file from these core modules - [Rollup](http://rollupjs.org) 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.
47
48### Base Loader Polyfill API
49
50The `Loader` and `ModuleNamespace` classes in `core/loader-polyfill.js` provide the basic spec API method shells for a loader instance `loader`:
51
52- *`new Loader()`*: Instantiate a new `loader` instance.
53 Defaults to environment baseURI detection in NodeJS and browsers.
54- *`loader.import(key [, parentKey])`*: Promise for importing and executing a given module, returning its module instance.
55- *`loader.resolve(key [, parentKey])`*: Promise for resolving the idempotent fully-normalized string key for a module.
56- *`new ModuleNamespace(bindings)`*: Creates a new module namespace object instance for the given bindings object. The iterable properties
57 of the bindings object are created as getters returning the corresponding values from the bindings object.
58- *`loader.registry.set(resolvedKey, namespace)`*: Set a module namespace into the registry.
59- *`loader.registry.get(resolvedKey)`*: Get a module namespace (if any) from the registry.
60- *`loader.registry.has(resolvedKey)`*: Boolean indicating whether the given key is present in the registry.
61- *`loader.registry.delete(resolvedKey)`*: Removes the given module from the registry (if any), returning true or false.
62- *`loader.registry.keys()`*: Function returning the keys iterator for the registry.
63- *`loader.registry.values()`*: Function returning the values iterator for the registry.
64- *`loader.registry.entries()`*: Function returning the entries iterator for the registry (keys and values).
65- *`loader.registry[Symbol.iterator]`*: In supported environments, provides registry entries iteration.
66
67Example of using the base loader API:
68
69```javascript
70import { Loader, ModuleNamespace } from 'es-module-loader/core/loader-polyfill.js';
71
72let loader = new Loader();
73
74// override the resolve hook
75loader[Loader.resolve] = function (key, parent) {
76 // intercept the load of "x"
77 if (key === 'x') {
78 this.registry.set('x', new ModuleNamespace({ some: 'exports' }));
79 return key;
80 }
81 return Loader.prototype[Loader.resolve](key, parent);
82};
83
84loader.import('x').then(function (m) {
85 console.log(m.some);
86});
87```
88
89### RegisterLoader Hooks
90
91Instead of just hooking modules within the resolve hook, the `RegisterLoader` base class provides an instantiate hook
92to separate execution from resolution and enable spec linking semantics.
93
94Implementing a loader on top of the `RegisterLoader` base class involves extending that class and providing these
95`resolve` and `instantiate` prototype hook methods:
96
97```javascript
98import RegisterLoader from 'es-module-loader/core/register-loader.js';
99import { ModuleNamespace } from 'es-module-loader/core/loader-polyfill.js';
100
101class MyCustomLoader extends RegisterLoader {
102 /*
103 * Constructor
104 * Purely for completeness in this example
105 */
106 constructor (baseKey) {
107 super(baseKey);
108 }
109
110 /*
111 * Default resolve hook
112 *
113 * The default parent resolution matches the HTML spec module resolution
114 * So super[RegisterLoader.resolve](key, parentKey) will return:
115 * - undefined if "key" is a plain names (eg 'lodash')
116 * - URL resolution if "key" is a relative URL (eg './x' will resolve to parentKey as a URL, or the baseURI)
117 *
118 * So relativeResolved becomes either a fully normalized URL or a plain name (|| key) in this example
119 */
120 [RegisterLoader.resolve] (key, parentKey) {
121 var relativeResolved = super[RegisterLoader.resolve](key, parentKey, metadata) || key;
122 return relativeResolved;
123 }
124
125 /*
126 * Default instantiate hook
127 *
128 * This is one form of instantiate which is to return a ModuleNamespace directly
129 * This will result in every module supporting:
130 *
131 * import { moduleName } from 'my-module-name';
132 * assert(moduleName === 'my-module-name');
133 */
134 [RegisterLoader.instantiate] (key) {
135 return new ModuleNamespace({ moduleName: key });
136 }
137}
138```
139
140The return value of `resolve` is the final key that is set in the registry.
141
142The default normalization provided (`super[RegisterLoader.resolve]` above) follows the same approach as the HTML specification for module resolution, whereby _plain module names_ that are not valid URLs, and not starting with `./`, `../` or `/` return `undefined`.
143
144So for example `lodash` will return `undefined`, while `./x` will resolve to `[baseURI]/x`. In NodeJS a `file:///` URL is used for the baseURI.
145
146#### Instantiate Hook
147
148Using these three types of return values for the `RegisterLoader` instantiate hook,
149we can recreate ES module semantics interacting with legacy module formats:
150
151##### 1. Instantiating Dynamic Modules via ModuleNamespace
152
153If the exact module definition is already known, or loaded through another method (like calling out fully to the Node require in the node-es-module-loader),
154then the direct module namespace value can be returned from instantiate:
155
156```javascript
157
158import { ModuleNamespace } from 'es-module-loader/core/loader-polyfill.js';
159
160// ...
161
162 instantiate (key) {
163 var module = customModuleLoad(key);
164
165 return new ModuleNamespace({
166 default: module,
167 customExport: 'value'
168 });
169 }
170```
171
172##### 2. Instantiating ES Modules via System.register
173
174When instantiate returns `undefined`, it is assumed that the module key has already been registered through a
175`loader.register(key, deps, declare)` call, following the System.register module format.
176
177For example:
178
179```javascript
180 [RegisterLoader.instantate] (key) {
181 // System.register
182 this.register(key, ['./dep'], function (_export) {
183 // ...
184 });
185 }
186```
187
188When using the anonymous form of System.register - `loader.register(deps, declare)`, in order to know
189the context in which it was called, it is necessary to call the `processAnonRegister` method passed to instantiate:
190
191```javascript
192 [RegisterLoader.instantiate] (key, processAnonRegister) {
193 // System.register
194 this.register(deps, declare);
195
196 processAnonRegister();
197 }
198```
199
200The loader can then match the anonymous `System.register` call to correct module in the registry. This is used to support `<script>` loading.
201
202> System.register is not designed to be a handwritten module format, and would usually generated from a Babel or TypeScript conversion into the "system"
203 module format.
204
205##### 3. Instantiating Legacy Modules via System.registerDynamic
206
207This is identical to the `System.register` process above, only running `loader.registerDynamic` instead of `loader.register`:
208
209```javascript
210 [RegisterLoader.instantiate] (key, processAnonRegister) {
211
212 // System.registerDynamic CommonJS wrapper format
213 this.registerDynamic(['dep'], true, function (require, exports, module) {
214 module.exports = require('dep').y;
215 });
216
217 processAnonRegister();
218 }
219```
220
221For more information on the `System.registerDynamic` format [see the format explanation](docs/system-register-dynamic.md).
222
223### Performance
224
225Some simple benchmarks loading System.register modules are provided in the `bench` folder:
226
227Each test operation includes a new loader class instantiation, `System.register` declarations, binding setup for ES module trees, loading and execution.
228
229Sample results:
230
231| Test | ES Module Loader 1.3 |
232| ----------------------------------------- |:--------------------:|
233| Importing multiple trees at the same time | 654 ops/sec |
234| Importing a deep tree of modules | 4,162 ops/sec |
235| Importing a single module with deps | 8,817 ops/sec |
236| Importing a single module without deps | 16,536 ops/sec |
237
238### Tracing API
239
240When `loader.trace = true` is set, `loader.loads` provides a simple tracing API.
241
242Also not in the spec, this allows useful tooling to build on top of the loader.
243
244`loader.loads` is keyed by the module ID, with each record of the form:
245
246```javascript
247{
248 key, // String, key
249 deps, // Array, unnormalized dependencies
250 depMap, // Object, mapping unnormalized dependencies to normalized dependencies
251 metadata // Object, exactly as from normalize and instantiate hooks
252}
253```
254
255### Spec Differences
256
257The loader API in `core/loader-polyfill.js` matches the API of the current [WhatWG loader](https://whatwg.github.io/loader/) specification as closely as possible, while
258making a best-effort implementation of the upcoming loader simplification changes as descibred in https://github.com/whatwg/loader/issues/147.
259
260- Default normalization and error handling is implemented as in the HTML specification for module loading. Default normalization follows the HTML specification treatment of module keys as URLs, with plain names ignored by default (effectively erroring unless altering this behaviour through the hooks). Errors are cached in the registry, until the `delete` API method is called for the module that has errored. Resolve and fetch errors throw during the tree instantiation phase, while evaluation errors throw during the evaluation phase, and this is true for cached errors as well in line with the spec - https://github.com/whatwg/html/pull/2595.
261- A direct `ModuleNamespace` constructor is provided over the `Module` mutator proposal in the WhatWG specification.
262 Instead of storing a registry of `Module.Status` objects, we then 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
263(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),
264so in order to have a predictable load error rejection process, loads are only stored in the registry as fully-linked Namespace objects
265and not ModuleStatus objects as promises for Namespace objects. The custom private `ModuleNamespace` constructor is used over the `Module.Status` proposal to ensure a stable API instead of tracking in-progress specification work.
266- Linking between module formats does not use [zebra striping anymore](https://github.com/ModuleLoader/es6-module-loader/blob/v0.17.0/docs/circular-references-bindings.md#zebra-striping), but rather relies on linking the whole graph in deterministic order for each module format down the tree as is planned for NodeJS. This is made possible by the [dynamic modules TC39 proposal](https://github.com/caridy/proposal-dynamic-modules) which allows the export named bindings to only be determined at evaluation time for CommonJS modules. We do not currently provide tracking of circular references across module format boundaries so these will hang indefinitely like writing an infinite loop.
267- `Loader` is available as a named export from `core/loader-polyfill.js` but is not by default exported to the `global.Reflect` object.
268 This is to allow individual loader implementations to determine their own impact on the environment.
269- A constructor argument is added to the loader that takes the environment `baseKey` to be used as the default normalization parent.
270- The `RegisterLoader` splits up the `resolve` hook into `resolve` and `instantiate`. The [WhatWG reduced specification proposal](https://github.com/whatwg/loader/issues/147) to remove the loader hooks implies having a single `resolve` hook by having the module set into the registry using the `registry.set` API as a side-effect of resolution to allow custom interception as in the first loader example above. As discussed in https://github.com/whatwg/loader/issues/147#issuecomment-230407764, this may cause unwanted execution of modules when only resolution is needed via `loader.resolve` calls, so the approach taken in the `RegisterLoader` is to implement separate `resolve` and `instantiate` hooks.
271
272## License
273Licensed under the MIT license.
274
275[travis-url]: https://travis-ci.org/ModuleLoader/es-module-loader
276[travis-image]: https://travis-ci.org/ModuleLoader/es-module-loader.svg?branch=master