The goal of this document is to describe a strategy for wrapping and executing existing AMD-, Node-, ES6-, and globals-based modules so that they can interoperate.
It uses the AMD definition and AMD-compatible loaders as the substrate for this interoperability.
The existing module systems have overlapping semantics around default and named exports:
- Node uses the
modules.exports
property to define a default export, andexports.*
for named exports. They are mutually exclusive, andrequire
will either return the default export or a generated object with all defined exports. - AMD uses
return
values to define a default export, and the specialexports
dependency for named exports. They are mutually exclusive, and module dependencies will resolve to either the default export or a generated object with all defined exports. - Global-based modules use a single namespace to define default exports, and properties under that namespace to define named exports (for example, jQuery). Node and AMD modules can use a similar pattern by exporting a single object (for example, a function) and include additional properties on it.
- ES6 uses the
export default = Expression
syntax to define a default export, andexport Declaration
(e.g.export function foo() {}
,export class Foo {}
,export var foo = 1
) to define named exports. The default export is sugar for the nameddefault
export, with dedicated syntax on the import side.
In order to make these systems interoperable, the wrapping implementations normalize these differences:
- Node
module.exports
values are normalized into adefault
named export - AMD
return
values are normalized into adefault
named export - Node and AMD imports must have either a
default
property or other named properties, but not both. If adefault
property is found, it becomes the value of the import. - ES6 modules can support both default imports and named imports at the same time.
IMPORTANT NOTE: from the perspective of AMD->AMD, Node->Node, AMD->Node and Node->AMD interoperability, the default
wrapping and unwrapping is transparent. The wrapping allows AMD and Node modules to *idiomatically import from ES6 modules that use the export default =
syntax. This is important for transitional purposes, as AMD and Node modules are ported to ES6 syntax.
A wrapping implementation that uses an AMD loader and wants to support interoperability between all three module systems SHOULD use these semantics.
- Let imports be the dynamic imports provided by the AMD loader.
- Let importNames be the imports returned by the Imports algorithm.
- Let exports be an empty object.
- For each importName, index in importNames:
- If importName is
"exports"
, replace the imports entry at index with exports and continue. - If importValue has an own property name
"default"
and other own properties, raise an Error. - If importValue has an own property named
"default"
, replace importValue in imports withimport.default
.
- If importName is
- Let defaultExport be the result of calling the original factory function with imports.
- Assign the
default
property of exports to defaultExport - Return exports.
Example:
// Input AMD module
define(["some-parser", "exports"], function(Parser, exports) {
exports.AST = Parser.AST;
exports.yy = {};
exports.parse = function(string) {
var parser = new Parser(exports.yy),
lexer = new Parser.Lexer(string, exports.yy);
var lexemes = [], lexeme;
while (lexeme = lexer.lex(string)) { lexemes.push(lexeme); }
return parser.parse(lexemes);
}
});
// Wrapped AMD module
function __normalize(import) {
if (import.hasOwnProperty('default')) {
var defaultImport = import.default;
for (var prop in import) {
if (prop !== 'default' && import.hasOwnProperty(prop)) {
throw new Error(/* default and named exports */)
}
}
return defaultImport;
}
return import;
}
function factory(Parser, exports) {
exports.AST = Parser.AST;
exports.yy = {};
exports.parse = function(string) {
var parser = new Parser(exports.yy),
lexer = new Parser.Lexer(string, exports.yy);
var lexemes = [], lexeme;
while (lexeme = lexer.lex(string)) { lexemes.push(lexeme); }
return parser.parse(lexemes);
}
}
define(["some-parser"], function(Parser) {
var exports = {};
Parser = __normalize(Parser);
var defaultExport = factory(Parser, exports);
if (defaultExport !== undefined) {
exports.default = defaultExport;
}
return exports;
});
A wrapping implementation that uses an AMD loader and wants to support interoperability between all three module systems SHOULD use these semantics.
- Let imports be the result of the Imports algorithm.
- Let script be the result of parsing the original Node module.
- Let factoryBody be a new Script.
- Append the result of parsing
var exports = {}
to factoryBody. - Append the result of parsing
var module = {}
to factoryBody. - Append script to factoryBody.
- Append the result of parsing
if (exports.hasOwnProperty('default') exports.default = module.exports
to body. - Let formalParameters be a new list.
- For each import in imports:
- Let generatedName be a new generated name that is not used as a binding at the top-level of script.
- Insert generatedName into the list.
- Replace any calls to the top-level
require
function with import as its first parameter with an Identifier named generatedName.
- Append the result of parsing
return exports
to factoryBody - Let factory be a new function with formal parameters formalParameters and with factoryBody as its body.
- Register an AMD module with dependencies imports and factory factoryBody.
Example:
// Input node module
var Parser = require("some-parser").Parser;
exports.AST = parser.AST;
exports.yy = {};
exports.parse = function(string) {
var parser = new Parser(exports.yy),
lexer = new Parser.Lexer(string, exports.yy);
var lexemes = [], lexeme;
while (lexeme = lexer.lex(string)) { lexemes.push(lexeme); }
return parser.parse(lexemes);
}
// Output wrapped module
define(["some-parser"], function(dep1) {
var exports = {};
var module = {};
// dep1 is a generated name, and replaces require("some-parser")
var Parser = dep1.Parser;
exports.AST = parser.AST;
exports.yy = {};
exports.parse = function(string) {
var parser = new Parser.Parser(exports.yy),
lexer = new Parser.Lexer(string, exports.yy);
var lexemes = [], lexeme;
while (lexeme = lexer.lex(string)) { lexemes.push(lexeme); }
return parser.parse(lexemes);
}
return exports;
});
- Let script be the result of parsing the module source.
- Let importNames be the list of all imports in the script as strings.
- If importNames includes
"exports"
, throw an Error. - Let parameters be an empty list.'
- Let importMap be a new map.
- For name in importNames:
- Let generatedName be a new generated name that is not used as a binding at the top-level of script.
- Insert generatedName into parameters.
- Insert a new entry into importMap with name as the key and generatedName as the value.
- Append
"exports"
to importNames. - Let exportBinding be a new generated name that is not used as a binding at the top-level of script.
- Append exportBinding to parameters.
- Let decls be the list of all ImportDeclarations in script
- For decl in decls:
- Let moduleName be the string representation of the decl's ModuleSpecifier.
- If the ImportSpecifierSet in decl is an Identifier (default import):
- Let defaultName be the Identifier's string representation.
- Let fragment be the result of invoking Import Fragment with defaultName, importMap and moduleName
- Insert fragment immediately following decl.
- Otherwise, for specifier in the ImportSpecifierSet:
- Let importName be specifier's string representation.
- Let fragment be the result of invoking Import Fragment with importName, importMap and moduleName
- Insert fragment immediately following decl
- Remove decl.
- TODO: Exports
This algorithm takes importName, importMap and moduleName as parameters.
- Let importParam be the result of looking up moduleName in importMap
- Return the result of parsing
var ${importName} = ${importParam}.default
// input ES6 module
import { Parser } from "some-parser";
export AST = parser.AST;
export yy = {};
export function parse(string) {
var parser = new Parser(yy),
lexer = new Parser.Lexer(string, yy);
var lexemes = [], lexeme;
while (lexeme = lexer.lex(string)) { lexemes.push(lexeme); }
return parser.parse(lexemes);
}
// output AMD module
define(["some-parser", "exports"], function(dep1, __exports) {
var Parser = dep1.Parser;
var AST = __exports.AST = Parser.AST;
var yy = __exports.yy = {};
function parse(string) {
var parser = new Parser(exports.yy),
lexer = new Parser.Lexer(string, exports.yy);
var lexemes = [], lexeme;
while (lexeme = lexer.lex(string)) { lexemes.push(lexeme); }
return parser.parse(lexemes);
}
__exports.parse = parse;
});