Skip to content

Instantly share code, notes, and snippets.

@wycats
Created September 16, 2013 20:48
Show Gist options
  • Save wycats/7417be0dc361a69d5916 to your computer and use it in GitHub Desktop.
Save wycats/7417be0dc361a69d5916 to your computer and use it in GitHub Desktop.

AMD Wrapping for Interoperability

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.

Default and Named Exports

The existing module systems have overlapping semantics around default and named exports:

  • Node uses the modules.exports property to define a default export, and exports.* for named exports. They are mutually exclusive, and require 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 special exports 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, and export Declaration (e.g. export function foo() {}, export class Foo {}, export var foo = 1) to define named exports. The default export is sugar for the named default 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 a default named export
  • AMD return values are normalized into a default named export
  • Node and AMD imports must have either a default property or other named properties, but not both. If a default 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.

AMD Modules

A wrapping implementation that uses an AMD loader and wants to support interoperability between all three module systems SHOULD use these semantics.

  1. Let imports be the dynamic imports provided by the AMD loader.
  2. Let importNames be the imports returned by the Imports algorithm.
  3. Let exports be an empty object.
  4. For each importName, index in importNames:
    1. If importName is "exports", replace the imports entry at index with exports and continue.
    2. If importValue has an own property name "default" and other own properties, raise an Error.
    3. If importValue has an own property named "default", replace importValue in imports with import.default.
  5. Let defaultExport be the result of calling the original factory function with imports.
  6. Assign the default property of exports to defaultExport
  7. 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;
});

Node Modules

A wrapping implementation that uses an AMD loader and wants to support interoperability between all three module systems SHOULD use these semantics.

  1. Let imports be the result of the Imports algorithm.
  2. Let script be the result of parsing the original Node module.
  3. Let factoryBody be a new Script.
  4. Append the result of parsing var exports = {} to factoryBody.
  5. Append the result of parsing var module = {} to factoryBody.
  6. Append script to factoryBody.
  7. Append the result of parsing if (exports.hasOwnProperty('default') exports.default = module.exports to body.
  8. Let formalParameters be a new list.
  9. For each import in imports:
    1. Let generatedName be a new generated name that is not used as a binding at the top-level of script.
    2. Insert generatedName into the list.
    3. Replace any calls to the top-level require function with import as its first parameter with an Identifier named generatedName.
  10. Append the result of parsing return exports to factoryBody
  11. Let factory be a new function with formal parameters formalParameters and with factoryBody as its body.
  12. 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;
});

ES6 Modules

  1. Let script be the result of parsing the module source.
  2. Let importNames be the list of all imports in the script as strings.
  3. If importNames includes "exports", throw an Error.
  4. Let parameters be an empty list.'
  5. Let importMap be a new map.
  6. For name in importNames:
    1. Let generatedName be a new generated name that is not used as a binding at the top-level of script.
    2. Insert generatedName into parameters.
    3. Insert a new entry into importMap with name as the key and generatedName as the value.
  7. Append "exports" to importNames.
  8. Let exportBinding be a new generated name that is not used as a binding at the top-level of script.
  9. Append exportBinding to parameters.
  10. Let decls be the list of all ImportDeclarations in script
  11. For decl in decls:
    1. Let moduleName be the string representation of the decl's ModuleSpecifier.
    2. If the ImportSpecifierSet in decl is an Identifier (default import):
      1. Let defaultName be the Identifier's string representation.
      2. Let fragment be the result of invoking Import Fragment with defaultName, importMap and moduleName
      3. Insert fragment immediately following decl.
    3. Otherwise, for specifier in the ImportSpecifierSet:
      1. Let importName be specifier's string representation.
      2. Let fragment be the result of invoking Import Fragment with importName, importMap and moduleName
      3. Insert fragment immediately following decl
    4. Remove decl.
  12. TODO: Exports

Import Fragment

This algorithm takes importName, importMap and moduleName as parameters.

  1. Let importParam be the result of looking up moduleName in importMap
  2. 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;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment