This proposal describes a dynamic API for loading modules in controlled and selectively isolated contexts. Loaders also provide reflective methods for querying information about those contexts. Every host environment comes with a built-in system loader, and users can create custom loaders via the Loader constructor.
All code is compiled by a specific loader, and the code is statically and permanently associated with that loader. This association is used for requiring external modules, dynamic code loading via eval, and resolving global variables.
Loaders encapsulate several pieces of information:
Code is also statically associated with a URL, representing the URL from which it was loaded. If the code was loaded via eval, then its URL is the base URL of its loader.
The initial global environment contains a global binding System, which is an object that reflects the host environment’s code loading capability as a loader object. This section describes the interface of loader objects.
There is a constructor Loader, available via a standard module, which is a constructor for creating new loaders.
new Loader : function(Loader, { global: Object = Object.create(null), baseURL: string = arguments[0].baseURL, linkedTo: Loader | null = arguments[0], strict: boolean = false, resolve: function(string, string) -> any fetch: function(string, string, request, any) -> void translate: function(string, string, string, any) -> string }) -> Loader
Loader.prototype.get global : Object Loader.prototype.get baseURL : string Loader.prototype.load : function(string, function(Module) -> any, function(any) -> any) -> this Loader.prototype.eval : function(string) -> any Loader.prototype.evalAsync : function(string, function(any) -> any, function(any) -> any) -> this Loader.prototype.get : function(string) -> Module | null Loader.prototype.set : function(string, Object) -> this : function({ string: Object, ... }) -> this Loader.prototype.defineBuiltins : function(Object = this.global) -> Object
The details of this API can be found below.
Module instance objects can be created dynamically with the Module constructor function, which uses a semantic function ToModule for converting an object to a module instance object.
new Module({ string: Object, ... }) -> Module Module({ string: Object, ... }) -> Module
A module instance object has the [[Class]] “Module” and a null [[Prototype]].
The Module constructor takes an argument x and produces the result of applying the semantic conversion ToModule(ToObject(x)).
The ToModule semantic function converts an object to a module instance object. If the object is already a module instance object, the result is the object itself. Otherwise, the result is a new module instance object whose exports are the properties of the object.
At the top level of a Program, this is bound to the Program’s global object.
After a Program is compiled, its global object contains all the bindings of the global scope as non-configurable properties. All var bindings are writable value properties. All let bindings are accessor properties that throw if they are read before being written. All const bindings are write-once accessor properties that throw if they are read before being written.
The intrinsics of a loader are the core objects, prototypes, and constructors associated with the evaluation semantics. For example, when evaluating an array literal:
let a = [1, 2, 3];
the literal causes the runtime to construct a new Array object. The intrinsics associated with the code’s loader determine which Array constructor it uses.
The intrinsics are used for a variety of semantic operations, including:
.constructor property of literals and their standard prototypes.prototype property of the standard constructors pointed to by those prototypes
The global object is not required to correspond in any particular way to the intrinsics. However, in most ordinary cases the global object of a given loader will match that loader’s intrinsics. The defineBuiltins method can be used to populate the global object with the standard set of built-in objects and functions for the loader’s intrinsics.
This API does not directly enable virtualizing intrinsics with custom implementations. However, the translation hooks make this possible indirectly via compilation. For example, array literals could be translated to new VirtualArray(...) for some custom VirtualArray constructor.
It is possible to create a locked-down loader by freezing the intrinsics. You can do this even without directly placing the intrinsics on the global object by copying them into a temporary object:
let intrinsics = {}; loader.defineBuiltins(intrinsics); [ intrinsics[key] for key of Object.keys(intrinsics) ].forEach(function(builtin) { Object.freeze(builtin); });
Since it appears that the spec only talks about “the original ___” for constructors and prototypes, and the .prototype properties of all the built-in constructors are non-writable and non-configurable, the built-ins always expose the complete set of intrinsics.
The Loader constructor accepts three hooks that can be used for custom loading and compilation: resolve, fetch, and translate.
Whenever an MRL is requested, either via a call to the load method of the loader or by evaluating code with import declarations that load from MRL’s, the resolve hook of the loader is invoked. This hook determines whether the module has already been loaded by returning a value which is automatically looked up (via ===) in the module instance table. If so, the existing module instance is produced, and the loading process is skipped. If not, the loading process is initiated, and the resulting module instance is stored in the module instance table, keyed by the result of the resolve hook.
Before an MRL is passed to the resolve hook, it is canonicalized by eliminating ./ path entries, canonicalizing ../ entries, and collapsing occurrences of //.
Once an MRL has been resolved, if there is not currently an entry in the module instance table for its canonical URL, the module must be fetched. The fetch hook has the option of fetching the code from an external resource and providing its source via the first callback, or rejecting the code (with an optional error message, filename, and line number) via the second callback. The fetch hook is also provided with the MRL and the resolved key as its second and third arguments, respectively.
If the resolver does not provide a fetch hook, the parent loader’s fetching behavior is used.
Whenever code is evaluated, the resolver has the option of translating the source via the translate hook. The translation hook may produce the resulting source code or throw an exception.
Once the code is translated, it is recursively translated by the parent loaders.
The Loader constructor creates a new loader. The first argument is the parent loader. The second is an options object with the following options:
The loader’s global object (see below).
The loader’s base URL.
The source of the loader’s intrinsics (see below). This can either be an existing loader or null, the latter indicating that the loader’s intrinsics should be created fresh.
A flag indicating whether code evaluated in the loader should implicitly be in strict mode.
The MRL resolution hook (see below).
The module loading hook (see below). This hook receives a relative URL, the base URL of the referring code, a request object, and the resolved module instance key (i.e., the result of the MRL resolution).
A request object has three callback methods:
request = { fulfill: function(string) -> void redirect: function(string, string) -> void reject: function(string) -> void }
A callback for the loading hook to produce the successfully loaded source.
A callback for the loading hook to indicate that the source should be loaded from a different URL. The first argument is the redirected URL, and the second argument is a base URL to use for resolving the redirected URL. If omitted, the base URL is assumed to be the same as the base URL for the current request.
A callback for the loading hook to indicate that an error occurred in loading. The optional message is used for generating an error.
The source translation hook (see below).
The global accessor returns the global object encapsulated by the loader.
The baseURL accessor returns the base URL encapsulated by the loader.
The load method takes a string representing a module URL and a callback that receives the result of loading, compiling, and executing the module at that URL. The compiled code is statically associated with this loader, and its URL is the given URL. The additional callback is used if an error occurs.
The eval method takes a string representing a Program and returns the result of compiling and executing the program. Any MRL’s that are statically imported that the host environments deems remote result in a SyntaxError being thrown. The compiled code is statically associated with this loader, and its URL is the base URL of this loader.
If and when compilation succeeds, globals defined by the evaluated Program are added to the global object. If compilation fails, the global object is unchanged.
The evalAsync method takes a string representing a Program and a callback that receives the result of compiling and executing the program. The compiled code is statically associated with this loader, and its URL is the base URL of this loader. The additional callback is used if an error occurs.
If and when compilation succeeds, globals defined by the evaluated Program are added to the global object. If compilation fails, the global object is unchanged.
The get method looks up a module in the loader’s module instance table. The MRL is resolved to a key by calling the loader’s resolve operation.
The set method stores a module or set of modules in the loader’s module instance table. Each MRL is resolved to a key by calling the loader’s resolve operation.
The defineBuiltins method takes an object and defines all the built-in objects and functions of the ES6 standard library associated with this loader’s intrinsics as properties on the object.