This idea did not go anywhere when presented to TC39. See the es-discuss thread where it was presented, and the subsequent thread where the underlying problem of aliasing vs. non-aliasing bindings was discussed.
In short, TC39 thinks of modules as something more akin to "namespaces" from C# or Java than to JS modules as we see them in AMD or CommonJS, and those semantics are incompatible with the semantics given below.
By Domenic Denicola (@domenic) and Yehuda Katz (@wycats).
The current thinking on modules supports two styles for imports and exports:
- A module exports a single value "as itself"
- A module exports a number of values bound to names on itself
This document explores a way to combine these styles into a single syntax. It also tries to make import and export more symmetric and consistent with destructuring assignment and object literals ("structuring"), respectively.
A major goal of this document is to continue to support static verification of imports and exports.
The proposal on the ECMAScript wiki has been changed over the last few months in a few ways:
- "As itself" exporting is supported via an
export =syntax. - Importing syntax was updated to be only
import { Model } from "backbone"orimport "jquery" as $.
These changes introduced two microsyntaxes: ExportSpecifierSet, which in its
most general form looks like
export { export1: a, export2: x.y.z };and ImportSpecifierSet, which in its most general form looks like
import { a, originalName: localName };These microsyntaxes are subsets, respectively, of object literal syntax for the export case, and object destructuring in the import case. This leads to potential confusion for users: for example, they might expect
export { "export1": a, export2() {} };
import { name: { first: firstName } };to work. While the new microsyntaxes are convenient for the simplest case, they lead to broken intuition and extra cognitive load more generally, as one must be careful never to step outside their boundaries.
Furthermore, the addition of "as itself" exporting introduced a new issue: now
the syntax has to be matched on both side of the module boundary. That is, it
must be known to module consumers which exporting style the module author
used; you cannot do import { ajax } from "jquery", for example, since jQuery
necessarily chooses the single-export style.
Finally, the export syntax has a number of variations. An ExportSpecifierSet
can either use the microsyntax above, or an identifier can be exported, or a
declaration can be placed after the export keyword to export the declared
value, or the export keyword can be used as the left-hand side of an
assignment in order to achieve "as itself" exporting.
While each of the individual variations has a good use case, in the end it is
hard to form a coherent mental picture of what import and export are
doing. This document attempts to solve these issues.
The export declaration supports any JavaScript expression.
If an object literal is used, its keys become static names that can be
verified on import. Concise object literal syntax makes export { foo } work
with no additional syntax required.
Multiple export declarations are allowed if all of the expressions are object
literals. In this case, the exports object is built up across declarations.
All other cases of multiple export declarations are early errors.
This unified export syntax makes standard destructuring a straightforward
parallel on the import side, with added static verification if possible.
By leveraging existing semantics for object literals and destructuring, we make
the meaning of the import and export syntaxes plain. export { func() { } }
works, as does import { name: { first: firstName } } (with func and name
being statically verifiable).
This document maintains many of the semantics from the existing wiki proposal.
For example, export and import declarations must still be at the top level
of a script to ensure static verification is possible, and the named module
declaration syntax is unchanged. The entire compilation and loading process also
remains as-is.
// one single export is allowed per module
export jQuery;// static exports, using concise object literal syntax
export { get, set };
// subsequent static exports extend the exports object
export { Model, View, Controller };If static exports are used, the exports object is frozen once the module has finished instantiating.
With a unified syntax for exports, we can also unify the syntax for imports. The opposite of structuring is destructuring!
// jQuery is bound to the exports object from the module "jquery". This works
// whether "jquery" uses single-export or static exports.
import jQuery from "jquery";
// Use destructuring assignment to extract Model, View and Controller from the
// exports object of the module "backbone". If "backbone" used static exports,
// statically verify these imports.
import { Model, View, Controller } from "backbone";When a module exports a class, it is natural to have a one-to-one correspondence between the module and the class. It is also most natural to allow the import side to choose its own name:
module "jquery" {
export class jQuery {
};
}
module "app1" {
import $ from "jquery";
}
module "app2" {
import jQuery from "jquery";
import { $ } from "prototype"; // kickin' it old-school
}export { Event, Deferred };
// Can't use static exports with the single export syntax
export jQuery;export jQuery;
// Can't use static exports with the single export syntax
export { Event, ajax };export jQuery;
// Can't use single export syntax multiple times
export jQuery.ajax;export { fairlyChosenRandomNumber: 4 };
// Can't export the same static export twice.
export { fairlyChosenRandomNumber: 5 };import { Controller } from "backbone";
// Can't import the same identifier twice.
import { Controller } from "ember";import $ from "jquery";
// Can't import the same identifier twice.
import { $ } from "punctuation";Two new module properties are added:
[[StaticExports]]: An object whose keys are the names of the static exports, if any were specified.[[SingleExport]]: The value of the single export, if it was specified.
Add these steps to the end of Module Declaration Instantiation:
- If the module does not have
[[StaticExports]]or[[SingleExport]]:- Let
Obe the result of the abstract operationObjectCreatewith argumentnull. MakeObjectSecure(O, true).- Set
[[StaticExports]]toO.
- Let
- Otherwise, if the module has
[[StaticExports]]:- Let
Obe the value of[[StaticExports]]. MakeObjectSecure(O, true).
- Let
ExportDeclaration ::= "export" Expression
When Expression is ObjectLiteral:
- If
[[StaticExports]]exists, LetObe the value of[[StaticExports]]. - Otherwise, let
Obe the result of the abstract operationObjectCreatewith argumentnull. - For each property name in the
ObjectLiteral, define a new property onOusing the Property Definition Algorithm (section 11.1.5 of the 2012-11-22 draft spec).
Otherwise, assign the result of evaluating Expression to the module's
[[SingleExport]].
The following are early errors:
- Using both
ExportDeclarationforms in a module. - Using the non-
ObjectLiteralform more than once in a module. - Using the
ObjectLiteralform more than once with the same property name.
export { writeFile };
export { readFile };
export { writeFile, readFile };
export {
WriteStream,
FileWriteStream: WriteStream // support the legacy name
};
export 5;
export jQuery;
export function (selector) {
return [...document.querySelectorAll(selector)];
};
export class {
get() {}
put() {}
delete() {}
post() {}
};Note that CommonJS modules do not benefit from static verification, so this is purely illustrative.
Object.assign(exports, { writeFile });
Object.assign(exports, { readFile });
Object.assign(exports, { writeFile, readFile });
Object.assign(exports, {
WriteStream,
FileWriteStream: WriteStream // support the legacy name
});
module.exports = 5;
module.exports = jQuery;
module.exports = function (selector) {
return [...document.querySelectorAll(selector)];
};
module.exports = class {
get() {}
put() {}
delete() {}
post() {}
};ImportDeclaration ::= ImportExecutionDeclaration
| ImportFromDeclaration
ImportExecutionDeclaration ::= "import" ModuleId
ImportFromDeclaration ::= "import" ModuleContents "from" ModuleId
ModuleContents ::= Identifier
| ObjectAssignmentPattern
ModuleId ::= StringLiteral
When ImportDeclaration is ImportExecutionDeclaration, the module found using
ModuleId is simply executed. No new name bindings are introduced into scope.
When ImportDeclaration is ImportFromDeclaration, determine the import
value of the module found using ModuleId:
- If the module has
[[SingleExport]], return the value of[[SingleExport]]. - Otherwise, return
[[StaticExports]].
When ModuleContents is Identifier, the import value of the module found
using ModuleId is bound to a new local variable with that identifier
(let-scoped).
When ModuleContents is ObjectAssignmentPattern, the import value of the
module found using ModuleId is bound to new local variables (again,
let-scoped), using the Destructuring Assignment Evaluation semantics on the
ObjectAssignmentPattern.
If a module has [[StaticExports]] and ModuleContents is an
ObjectAssignmentPattern, the result of calling [[HasOwnProperty]] on
the value of [[StaticExports]] must be true for each property name in the
ObjectAssignmentPattern. It is an early error if this is not the case.
It is an early error for the same binding to be introduced by two
ImportDeclarations.
import $ from "jquery";
import { ajax, parseXML } from "jquery";
import { draw: drawShape } from "shape";
import { draw: drawGun } from "cowboy";
import { fx: { interval: fxInterval } } from "jquery";let $ = require("jquery");
let { ajax, parseXML } = require("jquery");
let { draw: drawShape } = require("shape");
let { draw: drawGun } = require("cowboy");
let { fx: { interval: fxInterval } } = require("jquery");
I like this.