Giter Club home page Giter Club logo

es-module-loader's Introduction

ES Module Loader Polyfill Build Status

Provides low-level hooks for creating ES module loaders, roughly based on the API of the WhatWG loader spec, but with adjustments to match the current proposals for the HTML modules specification, unspecified WhatWG changes, and NodeJS ES module adoption.

Supports the loader import and registry API with the System.register module format to provide exact module loading semantics for ES modules in environments today. In addition, support for the System.registerDynamic is provided to allow the linking of module graphs consisting of inter-dependent ES modules and CommonJS modules with their respective semantics retained.

This project aims to provide a fast, minimal, unopinionated loader API on top of which custom loaders can easily be built.

See the spec differences section for a detailed description of some of the specification decisions made.

ES6 Module Loader Polyfill, the previous version of this project was built to the outdated ES6 loader specification and can still be found 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 API is provided in core/loader-polyfill.js. On top of this main API file is core/register-loader.js which provides a base loader class with the non-spec System.register and System.registerDynamic support to enable the exact linking semantics.

Helper functions are available in core/resolve.js and core/common.js. 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.

Base Loader Polyfill API

The Loader and ModuleNamespace classes in core/loader-polyfill.js provide the basic spec API method shells for a loader instance loader:

  • new Loader(): Instantiate a new loader instance. Defaults to environment baseURI detection in NodeJS and browsers.
  • loader.import(key [, parentKey]): Promise for importing and executing a given module, returning its module instance.
  • loader.resolve(key [, parentKey]): Promise for resolving the idempotent fully-normalized string key for a module.
  • new ModuleNamespace(bindings): Creates a new module namespace object instance for the given bindings object. The iterable properties of the bindings object are created as getters returning the corresponding values from the bindings object.
  • loader.registry.set(resolvedKey, namespace): Set a module namespace into the registry.
  • loader.registry.get(resolvedKey): Get a module namespace (if any) from the registry.
  • loader.registry.has(resolvedKey): Boolean indicating whether the given key is present in the registry.
  • loader.registry.delete(resolvedKey): Removes the given module from the registry (if any), returning true or false.
  • loader.registry.keys(): Function returning the keys iterator for the registry.
  • loader.registry.values(): Function returning the values iterator for the registry.
  • loader.registry.entries(): Function returning the entries iterator for the registry (keys and values).
  • loader.registry[Symbol.iterator]: In supported environments, provides registry entries iteration.

Example of using the base loader API:

import { Loader, ModuleNamespace } from 'es-module-loader/core/loader-polyfill.js';

let loader = new Loader();

// override the resolve hook
loader[Loader.resolve] = function (key, parent) {
  // intercept the load of "x"
  if (key === 'x') {
    this.registry.set('x', new ModuleNamespace({ some: 'exports' }));
    return key;
  }
  return Loader.prototype[Loader.resolve](key, parent);
};

loader.import('x').then(function (m) {
  console.log(m.some);
});

RegisterLoader Hooks

Instead of just hooking modules within the resolve hook, the RegisterLoader base class provides an instantiate hook to separate execution from resolution and enable spec linking semantics.

Implementing a loader on top of the RegisterLoader base class involves extending that class and providing these resolve and instantiate prototype hook methods:

import RegisterLoader from 'es-module-loader/core/register-loader.js';
import { ModuleNamespace } from 'es-module-loader/core/loader-polyfill.js';

class MyCustomLoader extends RegisterLoader {
  /*
   * Constructor
   * Purely for completeness in this example
   */
  constructor (baseKey) {
    super(baseKey);
  }

  /*
   * Default resolve hook
   *
   * The default parent resolution matches the HTML spec module resolution
   * So super[RegisterLoader.resolve](key, parentKey) will return:
   *  - undefined if "key" is a plain names (eg 'lodash')
   *  - URL resolution if "key" is a relative URL (eg './x' will resolve to parentKey as a URL, or the baseURI)
   *
   * So relativeResolved becomes either a fully normalized URL or a plain name (|| key) in this example
   */
  [RegisterLoader.resolve] (key, parentKey) {
    var relativeResolved = super[RegisterLoader.resolve](key, parentKey, metadata) || key;
    return relativeResolved;
  }

  /*
   * Default instantiate hook
   *
   * This is one form of instantiate which is to return a ModuleNamespace directly
   * This will result in every module supporting:
   *
   *   import { moduleName } from 'my-module-name';
   *   assert(moduleName === 'my-module-name');
   */
  [RegisterLoader.instantiate] (key) {
    return new ModuleNamespace({ moduleName: key });
  }
}

The return value of resolve is the final key that is set in the registry.

The 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.

So for example lodash will return undefined, while ./x will resolve to [baseURI]/x. In NodeJS a file:/// URL is used for the baseURI.

Instantiate Hook

Using these three types of return values for the RegisterLoader instantiate hook, we can recreate ES module semantics interacting with legacy module formats:

1. Instantiating Dynamic Modules via ModuleNamespace

If 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), then the direct module namespace value can be returned from instantiate:

import { ModuleNamespace } from 'es-module-loader/core/loader-polyfill.js';

// ...

  instantiate (key) {
    var module = customModuleLoad(key);

    return new ModuleNamespace({
      default: module,
      customExport: 'value'
    });
  }
2. 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) {
    // System.register
    this.register(key, ['./dep'], function (_export) {
      // ...
    });
  }

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 processAnonRegister method passed to instantiate:

  [RegisterLoader.instantiate] (key, processAnonRegister) {
    // System.register
    this.register(deps, declare);

    processAnonRegister();
  }

The loader can then match the anonymous System.register call to correct module in the registry. This is used to support <script> loading.

System.register is not designed to be a handwritten module format, and would usually generated from a Babel or TypeScript conversion into the "system" module format.

3. Instantiating Legacy Modules via System.registerDynamic

This is identical to the System.register process above, only running loader.registerDynamic instead of loader.register:

  [RegisterLoader.instantiate] (key, processAnonRegister) {

    // System.registerDynamic CommonJS wrapper format
    this.registerDynamic(['dep'], true, function (require, exports, module) {
      module.exports = require('dep').y;
    });

    processAnonRegister();
  }

For more information on the System.registerDynamic format see the format explanation.

Performance

Some simple benchmarks loading System.register modules are provided in the bench folder:

Each test operation includes a new loader class instantiation, System.register declarations, binding setup for ES module trees, loading and execution.

Sample results:

Test ES Module Loader 1.3
Importing multiple trees at the same time 654 ops/sec
Importing a deep tree of modules 4,162 ops/sec
Importing a single module with deps 8,817 ops/sec
Importing a single module without deps 16,536 ops/sec

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, // String, key
  deps, // Array, unnormalized dependencies
  depMap, // Object, mapping unnormalized dependencies to normalized dependencies
  metadata // Object, exactly as from normalize and instantiate hooks
}

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 whatwg/loader#147.

  • 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 - whatwg/html#2595.
  • A direct ModuleNamespace constructor is provided over the Module mutator proposal in the WhatWG specification. 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 (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. The custom private ModuleNamespace constructor is used over the Module.Status proposal to ensure a stable API instead of tracking in-progress specification work.
  • Linking between module formats does not use zebra striping anymore, 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 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.
  • Loader is available as a named export from core/loader-polyfill.js but is 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.
  • The RegisterLoader splits up the resolve hook into resolve and instantiate. The WhatWG reduced specification proposal 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 whatwg/loader#147 (comment), 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.

License

Licensed under the MIT license.

es-module-loader's People

Contributors

addyosmani avatar also avatar arv avatar brettz9 avatar calvinmetcalf avatar caridy avatar danharper avatar douglasduteil avatar dtinth avatar erikringsmuth avatar guybedford avatar jansauer avatar jgable avatar joeldenning avatar johnjbarton avatar josh avatar justinbmeyer avatar leedm777 avatar mason-stewart avatar matthewp avatar nevir avatar orand avatar passy avatar paulirish avatar pkozlowski-opensource avatar probins avatar schnittstabil avatar sheerun avatar sindresorhus avatar unional avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

es-module-loader's Issues

Documentation Details

@guybedford for the README:

  1. Mention if loading a JS file (or files) is done sync or async
  2. Document the pre-configured loader, all attributes. For example, what you mean with System.baseURL. What does the System object contain? (btw, System is a bit generic, perhaps use SystemLoader would be better). Include comments/document what each of the Custom Loader attributes does (ie normalize).

Thanks :)

Considering a System addon library

Having spent a while working on jspm, I think it might just be possible to append functionality to the System loader instead of having an entirely separate loader.

I'm thinking of putting together a library as a "System addon" for things like AMD support, CommonJS support, map config, etc, which can be selectively included in the loader as needed, enhancing functionality.

A full list of features would be:

  • AMD, CommonJS, and Global Shim support
  • map config support
  • plugin support

Effectively this would be a very stripped down version of the jspm loader, but appending its functionality to the System loader in a backwards compatible way.

Thanks to @probins for the initial inspiration on this.

What do others think? Also any ideas for names? SystemJS is the best I've come up with... could do with some creative input!

I can probably get a prototype up within a week, working out interest and how it is presented is probably the hardest part.

System.baseUrl

Looks like this is something that used to be in the spec, but disappeared in the last revision.

Should all loaders use the same module registry?

At the moment, each custom loader is using its own registry, but is this a good idea? It remains to be seen how browsers implement loaders, but for the moment ISTM it would be better if they all used the same one.

Provide example of usage for a page

There should be a simple example project where using modules should help the development. It should also have quite many comments from devs. :)

Include specification info in source file

The top section of the source file should include the date of the currently implemented specification draft. When es6 is ready, it will mention the final specification details.

Switch to Traceur entirely for parsing

While this is the difference between an 800KB parser and a 100KB parser, since this module is being used to polyfill ES6 syntax, we need to unify the parser and not have two parsers being used.

Additionally, this module can be used to polyfill module loading of all other module formats, without having to download the parser anyway.

The only use case that would suffer from using Traceur is for users who want to use ES6 module syntax with ES5 code. But that is what AMD and CommonJS are for anyway.

So I think this change can align the module quite nicely around the full ES6 workflow,

Source maps

It is including the //# sourceURL comment, but source maps are necessary to maintain the syntax with the original source. Should be straightforward with the data-uri approach.

Latest Specification Update

The full update to the latest specification has just been merged into master. After further testing this can become the 0.4.0 release.

This entirely implements the specification algorithms to the letter now, with all exceptions fully documented.

Feedback and testing welcome. Suggestions on making the readme clearer also very welcome.

IE10 issue with global properties

IE10 doesn't and apparently hasn't worked when trying to access the global/_global properties. defineProperty does not appear to be the problem for IE10 (and IE10 can use Object.defineProperty on normal objects unlike IE8 which can only use it on DOM objects), but rather these lines at the top of the Loader constructor:

Global.prototype = options.global || (isBrowser ? window : {});
this._builtins = new Global();

Adding alert(this._builtins) gives an error here.

When the this._builtins property is used later, there are therefore problems there as well when access is attempted:

Global = function () {}
Global.prototype = this._builtins;

this._global = new Global();

Adding alert(Global.prototype); or alert(this._global); after the above lines gives an error too.

IE10 appears not to allow access if the window is used as another object's prototype (I also got the same errors when I tried iterating over the window's properties to assign them to the prototype instead of using window directly).

Integrate Traceur (source map support)

I've been considering how to go about polyfilling ES6 functionality dynamically (classes, etc) with this module, and there is a development workflow that could make sense.

Particularly, I was considering how one could load https://github.com/addyosmani/traceur-todomvc without a build step when in development.

The basic idea would be to compile the ES6 with Traceur before execution. For an example of this see https://github.com/guybedford/es6.

Basically it would be added as a "translate" step in the loader:

  System.translate = function(source) {
    // copy code from https://github.com/guybedford/es6/blob/master/es6.js#L4
  }

The integration is actually quite simple as all the code is readily available. The question is whether we want to encourage this or not?

Resolve conflicts in fetch implementation

The fetch function is given a different specification between the prototype (Loader.prototype.fetch) and loader instance (options.fetch).

As instance functions are provided on the instance object as in the @wycats essay (System.normalize, System.fetch etc), there seems to be a conflict between these.

We should address this.

Tag a new release

I think that we should consider tagging a new release before more work is done on the roadmap. This will at least give developers a semi-stable point they can try out while development is on-going in master.

Esprima parser update

As per the readme, the ES6 Esprima Harmony parser is being used to do parsing, loaded only when necessary. This parser still uses an older syntax, which is currently the major critical issue to sort out for this polyfill.

@guybedford suggested that we explore alternatives and I agree.

Esprima is being transcoded by traceur in traceur-test.html

While tracking down issue #45, I discovered that traceur-test.html cause esprima to be transcoded by traceur.

After we xhr the content of es6-file.js we call
fulfill(xhr.responseText);
which is fetch callback
this.fetch(url, function(source) {
opt.address = url;
opt.type = 'module';
source = self.translate(source, opt);
self._linkExecute(name, source, opt, _callback, _errback);
}, _errback, opt);
so we enter _linkExecute with the transcoding of es6-file.js.

There we call loadEsprima. But the loader is now transcoding all inputs.

At the end of translate:
source = traceur.outputgeneration.ProjectWriter.write(res, opt);
if (isBrowser)
source += '\n//# sourceMappingURL=data:application/json;base64,' + btoa(opt.sourceMap) + '\n';

we call btoa() on the sourceMap. I added the input source to the sourcemap to get around issue #45, and this source causes a DOM exception
"'btoa' failed: The string to be encoded contains characters outside of the Latin1 range."

Probably something to do with utf16 silliness new recent JS.

Maybe btoa() is a problem, but I don't think you want to transcode esprima in this scenario anyway.

Exact System normalization of js extension

It is still unclear how exactly the js extension is handled. Unlike RequireJS, the .js extension does not imply a URL and still gets normalized. The question then is whether file.coffee.js should normalize back to file.coffee or not. Awaiting further spec discussion.

Support other js environments

Ideally the code should be adapted to work as a NodeJS module equally.

Then the System loader could potentially also support fetch in Node, or alternatively one would construct a loader for NodeJS environments.

Add download links to readme

We can probably add links to:

  • Unminified build
  • Minified build
  • Dependencies

along with some estimated size information.

A browser compatibility list might also be useful.

Specification Changes

It would be good to have a single location to track areas of the specification still subject to change, and pointing to the relevant discussion.

To start -

  • Bundling: The extra metadata property in the resolve function is currently aiming to allow metadata relating to the multiple IDs defined by a module bundle. This is still being worked out. Awaiting a link to the discussion emails.
  • Fetch: The fetch function is given a different specification between the prototype (Loader.prototype.fetch) and loader instance (options.fetch). This seems to be a spec issue.
  • System loader resolution: It is still unclear how exactly the js extension is handled. For example, it is unknown whether file.coffee.js should normalize to file.coffee or not.

Export default behavior

When importing code like:

export default ...

...

...I would have expected that such an export would cause the exported object to be directly available on the corresponding import callback argument without needing to get to it with the "default" property...

If this is the correct behavior, Is the "default" property specced anywhere?

Thanks...

use @sourceURL

Since we're eval'ing, this content doesnt get added a script to the devtools.

Let's use sourceURL to turn this into a psuedofile so it populates in the devtools

Updated hooks and syntax parsing

I've managed to adapt the loader to use the new "link" hook and updated the other hooks as documented here - https://gist.github.com/wycats/51c96e3adcdb3a68cbc3.

I have also included Esprima's syntax parser to fully support "import", "module" and "export" statements in loaded modules. The great thing about the Esprima parser is that it is only 60KB making the full polyfill less than 70KB minified.

I can provide this as a pull request if you are interested?

https://github.com/guybedford/es6-loader

Allow for compiling separate files from terminal

I have a project that consists from ~100 modules (files). It doesn't make sense to compile all those files in browser each time when I reload the app. There should be a terminal option to build a single ES6 file with import/export statements into corresponding ES5 file.

Esprima in package.json

Hi,

Exciting to see this project!

I just installed through npm and got an error...Should esprima not be listed as a dependency in package.json?

Thanks...

Local tests

Currently the only tests are from the jspm-loader running against this.

We need to bring in a full test coverage here, including examples such as:

// scripts/crypto/random.js
export default function() { ... }

// scripts/crypto/guid.js
import random from './random';
export default function() { ... }

// test.js
import guid from './scripts/crypto/guid';
console.log(guid())

// bar/test.js
import guid from '../scripts/crypto/guid';
console.log(guid())

Dogfooding

Would be nice if this was written in ES6 using Traceur.

Since it's a polyfill for an ES6 feature it kinda makes sense.

Refreshed site for the project

@guybedford has done some incredible work taking this project further and I think it would help accelerate both testing and general usage by improving both our docs and the current site. I think the recent Traceur changes are worth making a splash about.

This could be something that might be worth exploring in Q1, 2014. I also think it would be useful to define what the goals for ModuleLoader are for next year. E.g:

Do we see ourselves as:

  • A reference implementation that provides a stable, supported version of the draft work Mozilla has done on the loader spec?
  • A reliable loader that we encourage other tools to build on and developers to use in their production apps - e.g JSPM has done a fantastic job building on the project and I could see others similarly switching over to using us.

Other questions:

  • Do we want to push JSPM further as a part of these efforts and do we see it being ready for use in building either demoware or production-level apps too?. I think everyone that's seen it agrees its an amazing sister project, but don't feel its had (yet) the kind of uptake it could. What can we do to change that?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.