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
O
be the result of the abstract operationObjectCreate
with argumentnull
. MakeObjectSecure(O, true)
.- Set
[[StaticExports]]
toO
.
- Let
- Otherwise, if the module has
[[StaticExports]]
:- Let
O
be the value of[[StaticExports]]
. MakeObjectSecure(O, true)
.
- Let
ExportDeclaration ::= "export" Expression
When Expression
is ObjectLiteral
:
- If
[[StaticExports]]
exists, LetO
be the value of[[StaticExports]]
. - Otherwise, let
O
be the result of the abstract operationObjectCreate
with argumentnull
. - For each property name in the
ObjectLiteral
, define a new property onO
using 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
ExportDeclaration
forms in a module. - Using the non-
ObjectLiteral
form more than once in a module. - Using the
ObjectLiteral
form 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
ImportDeclaration
s.
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.