Skip to content

Instantly share code, notes, and snippets.

@domenic
Last active June 8, 2021 08:29
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save domenic/1ab3f0daa7b37859ce43 to your computer and use it in GitHub Desktop.
Save domenic/1ab3f0daa7b37859ce43 to your computer and use it in GitHub Desktop.
ES6 modules syntax idea

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.

Background

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" or import "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.

Overview and Motivation

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.

Exports

// 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.

Symmetry With Imports

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";

Naturalness for Classes

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
}

Early Errors

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";

Formal Language

Internal Module Properties

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.

Module Instantiation

Add these steps to the end of Module Declaration Instantiation:

  1. If the module does not have [[StaticExports]] or [[SingleExport]]:
    1. Let O be the result of the abstract operation ObjectCreate with argument null.
    2. MakeObjectSecure(O, true).
    3. Set [[StaticExports]] to O.
  2. Otherwise, if the module has [[StaticExports]]:
    1. Let O be the value of [[StaticExports]].
    2. MakeObjectSecure(O, true).

Export

Syntax

ExportDeclaration ::= "export" Expression

Semantics

When Expression is ObjectLiteral:

  1. If [[StaticExports]] exists, Let O be the value of [[StaticExports]].
  2. Otherwise, let O be the result of the abstract operation ObjectCreate with argument null.
  3. For each property name in the ObjectLiteral, define a new property on O 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:

  1. Using both ExportDeclaration forms in a module.
  2. Using the non-ObjectLiteral form more than once in a module.
  3. Using the ObjectLiteral form more than once with the same property name.

Examples

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() {}
};
Parallel to CommonJS

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() {}
};

Import

Syntax

ImportDeclaration ::= ImportExecutionDeclaration
                   |  ImportFromDeclaration

ImportExecutionDeclaration ::= "import" ModuleId

ImportFromDeclaration ::= "import" ModuleContents "from" ModuleId

ModuleContents ::= Identifier
                |  ObjectAssignmentPattern

ModuleId ::= StringLiteral

Semantics

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:

  1. If the module has [[SingleExport]], return the value of [[SingleExport]].
  2. 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.

Early Errors

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.

Examples

import $ from "jquery";
import { ajax, parseXML } from "jquery";

import { draw: drawShape } from "shape";
import { draw: drawGun } from "cowboy";

import { fx: { interval: fxInterval } } from "jquery";
Parallel to CommonJS
let $ = require("jquery");

let { ajax, parseXML } = require("jquery");

let { draw: drawShape } = require("shape");
let { draw: drawGun } = require("cowboy");

let { fx: { interval: fxInterval } } = require("jquery");
@isaacs
Copy link

isaacs commented Dec 6, 2012

I like this.

@matthewrobb
Copy link

This is great work. I fully support this direction!

@dignifiedquire
Copy link

This would make js development so much cleaner. 👍

@junosuarez
Copy link

This is a much cleaner way to export. Looks good to me.

@mariusGundersen
Copy link

Is there any difference between import $ from "jQuery" and import { $ } from "jQuery", or between export $ and export { $ }, apart from a syntax difference?

@ForbesLindesay
Copy link

@mariusGundersen my understanding is that the difference is that one is single export while the other uses object restructuring.

Say I have module, called foo which exports as follows:

export {a, b}

I could then import that:

Import foo from "too"
//foo => {a:object, b: object}

Or I could import it:

import bar from "foo"
// bar => {a: object, b: object}

Or I could do:

import {a, b} from"foo"
//a => object
//b => object

So ideally your jQuery library would export a single variable:

export jQuery


import $ from "jQuery"
//or
import jQuery from "jQuery"

That way you can rename it if you want. Here's what happens if we export jQuery using the other syntax:

export {jQuery}



import {jQuery} from "jQuery"//  works great

import {$} from "jQuery"// fails because we didn't export a $

import $ from "jQuery"// works but now jQuery is in $.jQuery

Copy link

ghost commented Feb 11, 2013

I really like this proposal, especially how it gets rid of a lot of the harrier parts of the "the variable name I want to use is different from the module author's exported name" problem while avoiding the special-case spaghetti for single-function exports of the standard proposal.

@edef1c
Copy link

edef1c commented Feb 11, 2013

@geddski
Copy link

geddski commented Feb 11, 2013

This looks really good @domenic. For ES modules we need three things:

  1. Simple, intuitive import/export which you've nailed.
  2. Ability to load both sync and async depending on the env.
  3. Ability to load groups of modules combined into a single script for performance.

Could you talk to 2 & 3?

What excites me most about this is the possibility of a single JS module format that is awesome for node, browsers, extensions, mobile OS's, nashorn, and anywhere else JS runs.

@isaacs is this format something you could see Node getting behind?

@unscriptable
Copy link

<3 <3 <3 This makes so much sense to me. Thanks @wycats and @domenic!

@unscriptable
Copy link

@GEDDesign:

  1. agree!
  2. AMD loaders should be able to "prefetch" imports (at dev time) by doing simple static analysis on the modules. So, even if it's not truly "sync", it's close enough (again, at dev time!) to be workable in most situations.
  3. In a file that bundles several modules, the module "name" {} wrapper serves as a "transport format" and is ES6-browser-friendly. It's unfortunate that the format can't work in legacy browsers since it's impossible to shim this new syntax. I'm not about to fight that battle... :)

Note: afaict "anonymous modules" can no longer be supported. This will have to be worked-around by the loaders.

@domenic
Copy link
Author

domenic commented Feb 11, 2013

@GEDDesign, @unscriptable: this document is meant to be about syntax only. The loader semantics, and even the transport format semantics, are as in the existing harmony:modules proposal.

In particular, the loader from that proposal would stay the same. It would be asynchronous in all cases, from what I understand.

And modules are "anonymous" by default if they aren't wrapped in module "name" { ... }. That is, we use the existing proposal's modifications to ScriptElement, and its ModuleDeclaration/ModuleBody/ModuleElement productions.

Copy link

ghost commented Feb 11, 2013

This is really sad. TC39 is building something that nobody wants.

@Raynos
Copy link

Raynos commented Feb 12, 2013

@substack Java developers everywhere rejoice. Namespaces and classes! Finally, we're putting the Java back into JavaScript.

@Matt-Esch
Copy link

@Raynos ES(2N) --> Insane class semantics. ES(2N + 1) --> sane iteration on ES(2N - 1) after complaints.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment