ModuleImport

# David Herman (10 years ago)

Thanks to everyone for working through the issues around ModuleImport. I know it's been frustrating, but the discussions have really helped clarify the key constraints.

Constraints

First, let me restate the most important arguments. In favor of removal:

  • The syntactic distinction between ModuleImport and default import is vague.

We've consistently seen confusion between the semantics of ModuleImport and default export. While certainly some of the confusion might be chalked up to misinformation and lack of documentation, we've seen people be confused by this even when correctly explained the semantics. The problem with the syntax is that the only visual distinction is the initial keyword (module vs import), and it's not clear from that keyword which concept you're talking about. (Once you've internalized the structure of module instance objects, you get a hint from the module keyword, but that doesn't help people who are just learning the system, particularly if they're already familiar with other JS module systems.)

Against removal:

  • Without ModuleImport, authors of multi-export modules would be pressured to circumvent the named exports functionality.

Without ModuleImport, clients of multi-export modules have two options. Either they use named import:

import { readFile, writeFile } from "fs";

or if they want to explicitly namespace those functions, they have to use the dynamic API:

import "fs"; // state the dependency
var fs = this.get("fs");

That's inconvenient, confusing, and makes you feel like you're stepping outside the normal usage patterns intended for the system. Since this is a choice forced on the clients of a module, the module author will feel pressure to circumvent the named export feature altogether and instead export a default object with all the properties. This is bad -- it creates pressure for people to abandon part of the module system.

Conclusion

Here's the conclusion I've come to based on all of the above.

  • We need a form like ModuleImport.

As many have said, clients of named-export modules need the freedom to choose whether to explicitly namespace those imports, and they need a syntax that doesn't feel like they've stepped outside the normal system.

  • The current syntax of ModuleImport is wrong.

The confusion reported in developer feedback is real, and it's important.

  • The syntax should still favor default import.

ES6 favors the single/default export style, and gives the sweetest syntax to importing the default. Importing named exports can and even should be slightly less concise.

  • A better ModuleImport syntax is possible, and we should settle it soon.

I'll propose a better ModuleImport syntax below. Syntax being syntax, everyone will of course unsheathe their bikeshed paintbrush. That's OK. The champions will keep it from going on unbounded and help settle the debate, and we'll make sure to capture the conclusion in the next TC39 meeting.

I do acknowledge the concerns about reopening topics for debate and delay. But given the usability feedback, I think this case is worth fixing. We should resolve it for ES6, perhaps in part because it's less editorial work to change the ModuleImport production than to defer it, but more because I don't want to delay the resolution so that implementations can ship the better syntax. But keep in mind it doesn't matter what spec it lands in as long as implementations are shipping it. We're still in the early days of transpiler implementations, and there are no native implementations of modules yet. So there's time, as long as we don't let the bikeshed go on forever. So let's get to it!

Proposal

OK, so we're talking about a better syntax for importing a module and binding its named exports to a variable (as distinct from importing a module and binding its default export to a variable). Here's my proposal:

import * as fs from "fs"; // importing the named exports as an object
import Dict from "dict";  // importing a default export, same as ever

Why is this better? Because it dovetails with the named export syntax, which makes it clear that it's binding fs to the named exports. It saves the reader from having to know that the module instance object is not the same thing as the default export.

So there you have it... Feedback welcome, and as always we'll collect it all, weigh it, and try to shepherd consensus.

Oh, and just to put parameters on the discussion:

  • I'm not proposing additional new import forms.

If this syntax is future-compatible with other possible syntactic forms, that's great, but that's not what we're discussing.

  • I'm not interested in discussing changes to the rest of the module syntax.

This isn't the time or place to redesign the whole system.

  • The this binding in modules is a separate topic.

The this binding in modules can be discussed independently of this topic.

tl;dr we're only talking about the ModuleImport syntax here.

# Axel Rauschmayer (10 years ago)

That’s a good solution. Logically, import { * } from "fs" may make more sense, but I prefer the “unbraced” asterisk, because it results in less clutter.

Does the proposed syntax clash with export * FromClause (which, I’m assuming, re-exports everything, not just the named exports)?

I’d prefer it if named exports and a default export were mutually exclusive, but I understand that is off the table(?) Which is also fine with me…

Axel

# David Herman (10 years ago)

On Jun 19, 2014, at 2:03 AM, Axel Rauschmayer <axel at rauschma.de> wrote:

Does the proposed syntax clash with export * FromClause (which, I’m assuming, re-exports everything, not just the named exports)?

No inconsistency; it imports everything. Exactly the same semantics as always, just a change in surface syntax. Remember that the module instance object contains all named exports, but the default export is simply an export with the name default.

# Michał Gołębiowski (10 years ago)

Thanks, Dave, for bringing that up, it shows you're open for feedback. That said (bikeshed begins), what's wrong with:

import "fs" as fs;

? I feel that a lot of effort went in ES6 into reducing boilerplate via e.g. arrow functions, classes etc. but if you start with Node's require, this adds clutter. Compare these 3 forms of importing all the module "lodash" bindings to an object _:

var _ = require("lodash"); // Node
import * as _ from "lodash"; // Dave's syntax
import "lodash" as _;
# Axel Rauschmayer (10 years ago)

This is a key sentence in David’s proposal: “ES6 favors the single/default export style, and gives the sweetest syntax to importing the default. Importing named exports can and even should be slightly less concise.”

# Calvin Metcalf (10 years ago)

One other option could be for import name from 'path' to resolve to the module body there is no default export, thanks to the static analysis you'll always know when default is present.

The import 'path'/this.get syntax is a lot less burdensome if it's only required by edge cases like needing both default export and all the named ones.

# Michał Gołębiowski (10 years ago)

On Thu, Jun 19, 2014 at 12:40 PM, Axel Rauschmayer <axel at rauschma.de> wrote:

This is a key sentence in David’s proposal: “ES6 favors the single/default export style, and gives the sweetest syntax to importing the default. Importing named exports can and even should be slightly less concise.”

There are a lot of large modules that will not disappear overnight. I assume ES6's recommendation for such things (Node's fs module, Lo-Dash) is to make those container objects default exports?

# Mark Volkmann (10 years ago)

What is the behavior if a module doesn't define a default export and the syntax for importing the default export is used?

What is the behavior if a module only defines a default export and the syntax for importing named imports is used?


R. Mark Volkmann Object Computing, Inc.

# Kevin Smith (10 years ago)
import * as fs from "fs"; // importing the named exports as an object
import Dict from "dict";  // importing a default export, same as ever

It's a little wordy, but it fulfills the goals and feels pleasant to write. +1 from me.

# Erik Arvidsson (10 years ago)

On Thu, Jun 19, 2014 at 6:41 AM, Calvin Metcalf <calvin.metcalf at gmail.com> wrote:

One other option could be for import name from 'path' to resolve to the module body there is no default export, thanks to the static analysis you'll always know when default is present.

That is a refactoring hazard. If the module changes to add/remove the default export the import will still succeed but the value is either a module instance object or anything:

// a.js
export default class C { ... }

// importer.js
import A from './a';
new A();

Now a.js changes.

// a.js V2
export class C { ... }

// importer.js
import A from './a';
new A();  // TypeError: A is not a function

With this idea you cannot look at the import statement to see if the imported binding is a module instance object or not.

# David Herman (10 years ago)

On Jun 19, 2014, at 3:31 AM, Michał Gołębiowski <m.goleb at gmail.com> wrote:

Thanks, Dave, for bringing that up, it shows you're open for feedback. That said (bikeshed begins),

lol :)

what's wrong with:

import "fs" as fs;

Indeed we've talked about that one before. Some objected to the inconsistency of having the binding appear at the end and not having the module name at the end (all other import forms have the binding on the left and the module name at the end). But more importantly, this doesn't help with the problem of visually disambiguating from importing the default export. To put it another way, the * is critical in the * as _ syntax, because it makes it explicit that we're talking about the named exports.

? I feel that a lot of effort went in ES6 into reducing boilerplate via e.g. arrow functions, classes etc. but if you start with Node's require, this adds clutter. Compare these 3 forms of importing all the module "lodash" bindings to an object _:

var _ = require("lodash"); // Node
import * as _ from "lodash"; // Dave's syntax
import "lodash" as _;

My feeling is that the clutter is small, and it's acceptable to have it be slightly less minimal than default export since that is the case we are favoring. This isn't so cluttered as to be downright punishing multi-export utility modules (it's literally only two characters longer than your Node code, albeit admittedly a little chattier), but it's good that default import is the winner.

# Calvin Metcalf (10 years ago)

With this idea you cannot look at the import statement to see if the

imported binding is a module instance object or not.

the flip side of that is that you don't need to know whether something is a default export or a named export to import it e.g.

export let foo = function () {}
export let bar = function () {}

and

class Baz {
    foo(){}
    bar(){}
}

let baz = new Baz();
export default baz;

could both be imported the same.

# John Barton (10 years ago)

Sorry to be dense, but I would appreciate more elaboration of this sentence:

On Thu, Jun 19, 2014 at 3:40 AM, Axel Rauschmayer <axel at rauschma.de> wrote:

This is a key sentence in David’s proposal: “ES6 favors the single/default export style,

What is the "single/default" export style? If I understand this claim, it says that a module will typically contain a single export statement, either named 'default' or not. Is there any evidence to support this? Everything I've seen contradicts this claim, assuming I understand it.

and gives the sweetest syntax to importing the default. Importing named exports can and even should be slightly less concise.”

Could you please give an example? In my experience, "export default" is rare or at least divisive since it seems stylistically incompatible with named exports.

# Juan Ignacio Dopazo (10 years ago)

On Thursday, June 19, 2014 5:17 AM, David Herman <dherman at mozilla.com> wrote:

import * as fs from "fs"; // importing the named exports as an object
import Dict from "dict"// importing a default export, same as ever

My first reaction is to think that a lot of developers will ask: why can't I do import * from 'fs';?

But it's easier to teach than the alternative. And I agree with the premises: ModuleImport should be there and it should be less convenient than using the default export.

Thanks David!

Juan

# James Burke (10 years ago)

On Thu, Jun 19, 2014 at 1:15 AM, David Herman <dherman at mozilla.com> wrote:

Proposal

OK, so we're talking about a better syntax for importing a module and binding its named exports to a variable (as distinct from importing a module and binding its default export to a variable). Here's my proposal:

import * as fs from "fs"; // importing the named exports as an object
import Dict from "dict";  // importing a default export, same as ever

Two other possibilities:

  1. Only allow export default or named exports, not both.

The reason default export is used in module systems today is because there is just one thing that wants to be exported, and it does not matter what its name is because it is indicated by the module ID. Sometimes it is also easier to just use an object literal syntax for the export than expanding that out into individual export statements.

Allowing both default and named exports from the same module is providing this syntax/API extension. If there are ancillary capabilities available, a submodule in a package is more likely the way it will be used, accessed as a default export via a module ID like ‘mainModule/sub', instead of wanting to use a default and named export from the same module.

It would look like this:

import fs from ‘fs’; // only has named exports, so get object holding
all the exports
import { readFile } from ‘fs’; // only the readFile export
import Dict from ‘dict’; // a default export

— or —

  1. Only allow export of one thing from a module, and import {} just means allowing getting the first property on that export. This removes the named export checking, but that benefit was always a bit weak, even weaker with the favoring of default export.
//d.js, module with a default export, note it does not need a name,
//"export" can only appear once in a file.
export function() {};

//fs.js, module with “multiple” us
export {
  readFile: function(){}
};

//main.js using ‘d’ and ‘fs'
import d from ‘d’;
import { readFile } from ‘fs’;

Both of those possibilities also fix the disjointed Sytem.import() use with a default export. No need to know that a .default is needed to get the usable part. It will match the import Name from ‘id’ syntax better.

James

# Jacob Gable (10 years ago)

On Thu, Jun 19, 2014 at 6:41 AM, Calvin Metcalf <calvin.metcalf at gmail.com> wrote:

One other option could be for import name from 'path' to resolve to the module body there is no default export, thanks to the static analysis you'll always know when default is present.

That is a refactoring hazard. If the module changes to add/remove the default export the import will still succeed but the value is either a module instance object or anything:

// a.js export default class C { ... }

// importer.js import A from './a'; new A();

Now a.js changes.

// a.js V2 export class C { ... }

// importer.js import A from './a'; new A(); // TypeError: A is not a function

With this idea you cannot look at the import statement to see if the imported binding is a module instance object or not.

For what it's worth, I have built a sort-of static analysis tool that would give you visibility to these sort of refactoring changes. We currently use it on our code base of around 250 module files that are transpiled on deployment.

Jacob

# Chris Toshok (10 years ago)

On Thu, Jun 19, 2014 at 7:13 AM, Calvin Metcalf <calvin.metcalf at gmail.com>

wrote:

With this idea you cannot look at the import statement to see if the imported binding is a module instance object or not.

the flip side of that is that you don't need to know whether something is a default export or a named export to import it e.g.

and elsewhere James Burke wrote:

  1. Only allow export default or named exports, not both.

I wrote about both of these issues here the other day:

blog.toshokelectric.com/too-little-too-late-my-thoughts-on-es6-modules

IMO you can't really implement Calvin's suggestion without making default+named exports an early error, since you don't know where the bindings should come from if a module contains both.

This would be great from the perspective of decoupling import/export syntaxes. But mutable bindings gum up the works, in that they provide another avenue for anti-refactoring pressure to flow from module consumer to module author, and the proposed syntax changes here (not by David) as well as in my post would make their behavior even more of a hazard. The syntaxes need to remain coupled in some form because you really do want to know if you're getting a mutable binding or not.

David's import * as foo from 'foo' proposal gets us close enough to the ideal of what's possible now, that it's definitely a +1 here.

ES6 favors the single/default export style, and gives the sweetest syntax

to importing the default

Coupling in action :/

# Chris Toshok (10 years ago)

On Thu, Jun 19, 2014 at 6:57 AM, Erik Arvidsson <erik.arvidsson at gmail.com>

wrote:

On Thu, Jun 19, 2014 at 6:41 AM, Calvin Metcalf <calvin.metcalf at gmail.com> wrote:

One other option could be for import name from 'path' to resolve to the module body there is no default export, thanks to the static analysis you'll always know when default is present.

That is a refactoring hazard. If the module changes to add/remove the default export the import will still succeed but the value is either a module instance object or anything:

// a.js export default class C { ... }

// importer.js import A from './a'; new A();

Now a.js changes.

// a.js V2 export class C { ... }

// importer.js import A from './a'; new A(); // TypeError: A is not a function

With this idea you cannot look at the import statement to see if the imported binding is a module instance object or not.

I think you're example misses one point - The module author changed the exported api, going from exporting a function named C to exporting an object with a property named C. Problems caused by this refactoring would exist regardless of Calvin's suggestion.

Calvin's suggestion would allow the following refactoring to be done by the module author without impacting his users, something not possible with current ES6:

// a.js V1
export default { C: class C { ... } }

// a.js V2
export class C { ... }

The refactoring hazard is real, but exists iff the module consumer uses implicit exports (i.e. an Object.prototype method with the above default export). This is another reason why the named export form is better - everything is explicit. IMO the only good reason to use default export is to export a single function.

# Axel Rauschmayer (10 years ago)

On Jun 19, 2014, at 16:17 , John Barton <johnjbarton at google.com> wrote:

Sorry to be dense, but I would appreciate more elaboration of this sentence:

On Thu, Jun 19, 2014 at 3:40 AM, Axel Rauschmayer <axel at rauschma.de> wrote: This is a key sentence in David’s proposal: “ES6 favors the single/default export style,

What is the "single/default" export style? If I understand this claim, it says that a module will typically contain a single export statement, either named 'default' or not. Is there any evidence to support this? Everything I've seen contradicts this claim, assuming I understand it.

The syntax is ( people.mozilla.org/~jorendorff/es6-draft.html#sec-exports ):

export default AssignmentExpression

and gives the sweetest syntax to importing the default. Importing named exports can and even should be slightly less concise.”

Could you please give an example? In my experience, "export default" is rare or at least divisive since it seems stylistically incompatible with named exports.

I’m surprised, too. But that seems to be the feedback from people working with large module-based client-side projects and from the Node.js community: single exports are most common. I think in client-side projects, one class per module was reported as a frequent use case:

// MyClass.js
export default class {
    ...
};

// main.js
import MyClass from "MyClass";
# Matthew Robb (10 years ago)

What if the import "module/id"; form was an expression that evaluated to the module instance object.

This means everything stays as it is now except we remove the ModuleImport form and if you want to use the module instance object you can do: var foo = import "foo";. You could also do var { bar } = import "foo";

  • Matthew Robb
# Erik Arvidsson (10 years ago)

On Thu, Jun 19, 2014 at 1:23 PM, Chris Toshok <toshok at gmail.com> wrote:

On Thu, Jun 19, 2014 at 6:57 AM, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:

On Thu, Jun 19, 2014 at 6:41 AM, Calvin Metcalf <calvin.metcalf at gmail.com

wrote:

One other option could be for import name from 'path' to resolve to the module body there is no default export, thanks to the static analysis you'll always know when default is present.

That is a refactoring hazard. If the module changes to add/remove the default export the import will still succeed but the value is either a module instance object or anything:

// a.js export default class C { ... }

// importer.js import A from './a'; new A();

Now a.js changes.

// a.js V2 export class C { ... }

// importer.js import A from './a'; new A(); // TypeError: A is not a function

With this idea you cannot look at the import statement to see if the imported binding is a module instance object or not.

I think you're example misses one point - The module author changed the exported api, going from exporting a function named C to exporting an object with a property named C. Problems caused by this refactoring would exist regardless of Calvin's suggestion.

With the current spec this is a compile time error since a.js (V2) does not export default.

Calvin's suggestion would allow the following refactoring to be done by the module author without impacting his users, something not possible with current ES6:

 // a.js V1
export default { C: class C { ... } }

But this is the thing we are trying hard to have people never do.

# Chris Toshok (10 years ago)

On Thu, Jun 19, 2014 at 10:53 AM, Matthew Robb <matthewwrobb at gmail.com>

wrote:

What if the import "module/id"; form was an expression that evaluated to the module instance object.

This means everything stays as it is now except we remove the ModuleImport form and if you want to use the module instance object you can do: var foo = import "foo";. You could also do var { bar } = import "foo";

This is essentially identical to System.get("foo"), other than the restriction on the argument being a string. It lends itself to imperative code when what you're after is a declarative syntax.

Making it an expression also means it's not a toplevel form in the grammar, and so you could do things like this:

function foo() { if (someCondition) return import "module1"; else return import "module2"; }

or even similar to your examples:

var { bar } = someCondition ? import "foo" : import "bar";

which would make precise and static computation of the set of imports impossible, and likely complicate module resolution as well.

# Chris Toshok (10 years ago)

On Thu, Jun 19, 2014 at 11:06 AM, Erik Arvidsson <erik.arvidsson at gmail.com>

wrote:

On Thu, Jun 19, 2014 at 1:23 PM, Chris Toshok <toshok at gmail.com> wrote:

On Thu, Jun 19, 2014 at 6:57 AM, Erik Arvidsson <erik.arvidsson at gmail.com

wrote:

On Thu, Jun 19, 2014 at 6:41 AM, Calvin Metcalf < calvin.metcalf at gmail.com> wrote:

One other option could be for import name from 'path' to resolve to the module body there is no default export, thanks to the static analysis you'll always know when default is present.

That is a refactoring hazard. If the module changes to add/remove the default export the import will still succeed but the value is either a module instance object or anything:

// a.js export default class C { ... }

// importer.js import A from './a'; new A();

Now a.js changes.

// a.js V2 export class C { ... }

// importer.js import A from './a'; new A(); // TypeError: A is not a function

With this idea you cannot look at the import statement to see if the imported binding is a module instance object or not.

I think you're example misses one point - The module author changed the exported api, going from exporting a function named C to exporting an object with a property named C. Problems caused by this refactoring would exist regardless of Calvin's suggestion.

With the current spec this is a compile time error since a.js (V2) does not export default.

Correct, but:

// a.js
export default class C { ... }

// a.js V2
export default { C: class C { ... } }

The author changed the shape of the api here as well, preserving default export. Hazards abound once you have an importer. That's what I meant by problems caused by that sort of refactoring existing regardless of Calvin's suggestion - if the shape changes there's nothing you can do in the general case.

By adopting something like Calvin's suggestion we can remove a possible module-side hazard, caused by the fact that the shape of an api artificially differs between default export and named exports.

Calvin's suggestion would allow the following refactoring to be done by the module author without impacting his users, something not possible with current ES6:

 // a.js V1
export default { C: class C { ... } }

But this is the thing we are trying hard to have people never do.

What exactly is it we're trying to have people do? never export more than 1 thing? or never export an object literal, instead favoring Object.create(null, { })?

# Sam Tobin-Hochstadt (10 years ago)

On Thu, Jun 19, 2014 at 2:29 PM, Chris Toshok <toshok at gmail.com> wrote:

Calvin's suggestion would allow the following refactoring to be done by the module author without impacting his users, something not possible with current ES6:

// a.js V1
export default { C: class C { ... } }

But this is the thing we are trying hard to have people never do.

What exactly is it we're trying to have people do? never export more than 1 thing? or never export an object literal, instead favoring Object.create(null, { })?

If people want to export a bunch of distinct things, each of which should be addressed under a separate name, they should use named exports. If they want to export a single abstraction, then they should use default export. If they want to use both, then they should do both. But if they want to export a bunch of named things, they don't need to use default export for that.

# Axel Rauschmayer (10 years ago)

On Jun 19, 2014, at 13:36 , Michał Gołębiowski <m.goleb at gmail.com> wrote:

On Thu, Jun 19, 2014 at 12:40 PM, Axel Rauschmayer <axel at rauschma.de> wrote: This is a key sentence in David’s proposal: “ES6 favors the single/default export style, and gives the sweetest syntax to importing the default. Importing named exports can and even should be slightly less concise.”

There are a lot of large modules that will not disappear overnight. I assume ES6's recommendation for such things (Node's fs module, Lo-Dash) is to make those container objects default exports?

What are you saying? That you find this syntax too verbose?

import * as fs from "fs";

There is a little more clutter, but it’s only 2 characters longer than:

var fs = require('fs');

This design decision does make sense if single-export modules are indeed much more common. It’s not what I expected, but it does seem to be the case.

# John Barton (10 years ago)

On Thu, Jun 19, 2014 at 10:48 AM, Axel Rauschmayer <axel at rauschma.de> wrote:

On Jun 19, 2014, at 16:17 , John Barton <johnjbarton at google.com> wrote:

Sorry to be dense, but I would appreciate more elaboration of this sentence:

On Thu, Jun 19, 2014 at 3:40 AM, Axel Rauschmayer <axel at rauschma.de> wrote:

This is a key sentence in David’s proposal: “ES6 favors the single/default export style,

What is the "single/default" export style? If I understand this claim, it says that a module will typically contain a single export statement, either named 'default' or not. Is there any evidence to support this? Everything I've seen contradicts this claim, assuming I understand it.

The syntax is ( people.mozilla.org/~jorendorff/es6-draft.html#sec-exports ):

export default AssignmentExpression

Yes, but I was not clear: where is the evidence that ES6 favors this form or single named export. My experience is opposite: the simple thing in ES6 is to export those things you want exported. If it's one thing, then one; if it's 6 things then 6. One is not favored.

and gives the sweetest syntax to importing the default. Importing named

exports can and even should be slightly less concise.”

Could you please give an example? In my experience, "export default" is rare or at least divisive since it seems stylistically incompatible with named exports.

I’m surprised, too. But that seems to be the feedback from people working with large module-based client-side projects and from the Node.js community: single exports are most common.

I still don't get it. If you want one export, just export one thing. If you want one big export object called _, then export var _ = {tons of stuffs}

One of the best references for devs to understand modules: www.2ality.com/2013/07/es6-modules.html says: The syntax for importing a default export is similar to normal importing, but there are no braces

To me this is a bug not a feature; we should keep it simple and just have one way to get one import from one export.

Just to make a connection to the topic, Dave's intro says: We've consistently seen confusion between the semantics of ModuleImport and default export.

For me this confusion lands on default export equally as 'module` import.

I think in client-side projects, one class per module was reported as a frequent use case

# Domenic Denicola (10 years ago)

From: es-discuss <es-discuss-bounces at mozilla.org> on behalf of David Herman <dherman at mozilla.com>

  • Without ModuleImport, authors of multi-export modules would be pressured to circumvent the named exports functionality.

I am glad this point was recognized and acted upon. I feel listened-to :)

  • The syntax should still favor default import.

Glad that this piece of (strong, pervasive) community feedback is still being kept at the forefront, despite continual naysaying and disbelief from various quarters.

I do acknowledge the concerns about reopening topics for debate and delay. [...] But keep in mind it doesn't matter what spec it lands in as long as implementations are shipping it.

This is part of a larger issue regarding the messaging of TC39 and versioned ECMAScript, which is somewhat out of touch with reality. I hope we can discuss this at the next TC39 meeting. But I don't mean to derail the thread.

import * as fs from "fs"; // importing the named exports as an object

This looks great. Other alternatives I thought about in response included import { * } as fs from "fs" and import module fs from "fs", but upon consideration the brevity of yours wins.

I just hope we can do better documenting it, this time around, and fixing the many transpilers with their confusing semantics.

# Axel Rauschmayer (10 years ago)

To me this is a bug not a feature; we should keep it simple and just have one way to get one import from one export.

Just to make a connection to the topic, Dave's intro says: We've consistently seen confusion between the semantics of ModuleImport and default export.

For me this confusion lands on default export equally as 'module` import.

Personally, I agree, but many people feel strongly about the single-export use case. If default exports help with getting ES6 modules adopted more broadly then the slight increase in complexity is worth it.

Let’s compare. Your approach – slightly more redundant:

// MyClass.js
export class MyClass { ... }
// main1.js
import { MyClass } from "MyClass";

// myFunc.js
export function myFunc(...) { ... }
// main2.js
import { myFunc } from "myFunc";

Default exports:

// MyClass.js
export default class { ... };
// main1.js
import MyClass from "MyClass";

// myFunc.js
export default function (...) { ... };
// main2.js
import myFunc from "myFunc";
# Domenic Denicola (10 years ago)

From: es-discuss <es-discuss-bounces at mozilla.org> on behalf of James Burke <jrburke at gmail.com>

  1. Only allow export default or named exports, not both.

As a modification of the current design, this hurts use cases like

import glob, { sync as syncGlob } from "glob";
import _, { zip } from "underscore";
  1. Only allow export of one thing from a module, and import {} just means allowing getting the first property on that export. This removes the named export checking, but that benefit was always a bit weak, even weaker with the favoring of default export.

I definitely believe that if the community were designing a syntax-based module format, incorporating the lessons learned so far, it would end up being along these lines. We've learned enough from browserify and RequireJS's CommonJS-wrapping to know that a top-level static import form has serious benefits, but we've also learned that module.exports = or return are the recommended and primary pattern, and you can always attach things to that "default export" as properties if necessary. (In CommonJS terms, we've learned that the benefits of typing exports.x = y instead of module.exports.x = y are not great enough to outweigh the minor confusion of having two export forms.)

Unfortunately, that's not the world we live in, and instead TC39 is designing a module system based on their own priorities. (Static checking of multi-export names, mutable bindings, etc.)

They've (wisely) decided to add affordances for the community's use cases, such as layering default exports on top of the multi-export model. As well as Dave's proposal in this thread to de-grossify usage of modules like "fs". By doing so, they increase their chances of the module system being "good enough" for the community, so that the path of least resistance will be to adopt it, despite it not being designed for them primarily. It's still an open question whether this will be enough to win over the community from their existing tools, but with Dave's suggestion I think it has a better-than-even chance.

The transitional era will be a particularly vulnerable time for TC39's module design, however: as long as people are using transpilers, there's an opportunity for a particularly well-crafted, documented, and supported transpiler to give alternate semantics grounded in the community's preferred model, and win over enough of an audience to bleed the life out of TC39's modules. We already see signs of community interest in such "ES6+" transpilers, as Angular illustrates. Even a transpiler that maintains a subset of ES6 syntax would work: if it supported only export default x, and then gave import { x } from "y" destructuring semantics instead of named-binding-import semantics, that would do the trick. Interesting times.

# Axel Rauschmayer (10 years ago)

All good points. I don’t see “TC39 versus the community”, though, I’m seeing many factions with different opinions.

# Kevin Smith (10 years ago)

Yes, but I was not clear: where is the evidence that ES6 favors this form or single named export. My experience is opposite: the simple thing in ES6 is to export those things you want exported. If it's one thing, then one; if it's 6 things then 6. One is not favored.

FWIW, my experience (again) agrees with yours. In all the ES6 modular code I've written over the past couple of years, I've never felt a need or a desire to use the "default" feature. For me, personally, it's a distraction.

However, we are where we are, and we need to make progress. I think the current design is something that we can all move forward with, even if we don't agree with every design decision. Put another way, you and I can happily ignore the default feature, and have a bit of a laugh when we see it resulting in confusion ; )

# James Burke (10 years ago)

On Thu, Jun 19, 2014 at 12:13 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

From: es-discuss <es-discuss-bounces at mozilla.org> on behalf of James Burke <jrburke at gmail.com>

  1. Only allow export default or named exports, not both.

As a modification of the current design, this hurts use cases like

import glob, { sync as syncGlob } from "glob";
import _, { zip } from "underscore";

It is just as likely the module author will specify sync and zip as properties of their respective default export. Particularly since those are coming from JS modules that come from existing module systems.

So a destructuring assignment would be needed, following the default import. It works out though, and is not that much more typing. That, or those pieces would be available as ‘underscore/zip’ or ‘glob/sync' imports.

The argument for allowing both a default and named exports seems ill-defined based on data points known so far, and by avoiding it, it reduces the number of import forms and aligns better with System.import use.

James

# Domenic Denicola (10 years ago)

From: James Burke [mailto:jrburke at gmail.com]

The argument for allowing both a default and named exports seems ill-defined based on data points known so far

I mean, it seems based on the idea that named exports are super-important, and that packages like glob and underscore should use them. I agree that it's unclear whether this will occur, but that seems to be the reasoning.

# C. Scott Ananian (10 years ago)

But why? The benefit of named exports in general is that you get the magic mutable bindings -- but underscore and glob are mature libraries without circular dependencies on other code. They would gain exactly nothing from switching to named exports.

# Brendan Eich (10 years ago)

Domenic Denicola wrote:

This is part of a larger issue regarding the messaging of TC39 and versioned ECMAScript, which is somewhat out of touch with reality.

(Spinach.)

I hope we can discuss this at the next TC39 meeting. But I don't mean to derail the thread.

Too late.

import * as fs from "fs"; // importing the named exports as an object

This looks great. Other alternatives I thought about in response included import { * } as fs from "fs" and import module fs from "fs", but upon consideration the brevity of yours wins.

I just hope we can do better documenting it, this time around, and fixing the many transpilers with their confusing semantics.

Finalize the normative spec, see what happens.

# Brendan Eich (10 years ago)

Domenic Denicola wrote:

The transitional era will be a particularly vulnerable time for TC39's module design, however: as long as people are using transpilers, there's an opportunity for a particularly well-crafted, documented, and supported transpiler to give alternate semantics grounded in the community's preferred model, and win over enough of an audience to bleed the life out of TC39's modules.

You're doing a disservice to everyone here by implying that such a transpiler could trump native modules as implemented per the normative ES6 spec in Chakra, JSC, SpiderMonkey, and V8.

Idle speculations are not even worth betting on, but filling es-discuss with tendentious idle speculations is worse. Cut it out.

# Marius Gundersen (10 years ago)

ES6 favors the single/default export style, and gives the sweetest syntax

to importing the default. Importing named exports can and even should be slightly less concise.

It seems like ES6 favors single import (import singleThing from "module") but prefers multiple export (export var a = 5;), with single/default export being a special case of named/multiple export. In the spec[1] there are 5 ways to export named things, but only one way to export a default value. The focus in the spec is therefore on multiple export, while single export is a special case using a keyword to differentiate itself. This means that a module which exports only a single (named) thing is not a single-export module, and cannot be imported using the single-import syntax:

//moduleA.js
export class A{
  //...
}
//app.js
import A from "moduleA"; //wont work, even though moduleA only has a single
export

This will likely lead to confusion, since the ES6 module system does not actually favor single export modules, but default export modules. If the ES6 module system is trying to push developers towards single exporting modules, then the syntax for export should prefer single export, without requiring a keyword for it to actually be a single export module. Multiple export should be the special case, requiring a keyword or new syntax, and throwing if a module exports multiple times without the extra keyword.

Unfortunately I don't have any suggestions for how this syntax could work without deviating very far from the current spec. It seems easier to get the spec to favor multiple export (which, to me, it looks like it was originally designed to favor). I don't have any preference either way, I just want it to favor one, both on the export and import side.

Marius Gundersen

1

# Sébastien Cevey (10 years ago)

Reading Marius' email, I realised what I find confusing in the newly proposed syntax that uses `*' to import the default export.

The `*' symbol universally represents a glob of "everything", but when used to import from a module that has multiple exports, you won't get everything, you will get either the single default export (if there is one) or nothing.

// letters.js
export var a = 1;
export var b = 2;

// main.js
import {a, b} from "letters"; // ok
import * as letters from "letters"; // not what you think

Note that `{ * }' would be even more confusing.

Would `default' be a clearer keyword for import, so import and export logically match?

import default as Dict from "dict";

As a final note, and at the risk of erring in the world of speculation that Brendan fears, are we just sleepwalking towards pushing people to work around the whole debate with the "universal":

// letters.js
export var a = 1;
export var b = 2;
export default {a: a, b: b};

// main.js
import {a, b} from "letters"; // ok
import * as letters from "letters"; // ok

Sorry to add more data to this already loaded thread. I guess it shows how much people care about finding the best, most intuitive solution to this whole issue.

# Axel Rauschmayer (10 years ago)

On Jun 20, 2014, at 11:36 , Sébastien Cevey <seb.cevey at guardian.co.uk> wrote:

Reading Marius' email, I realised what I find confusing in the newly proposed syntax that uses `*' to import the default export.

The `*' symbol universally represents a glob of "everything", but when used to import from a module that has multiple exports, you won't get everything, you will get either the single default export (if there is one) or nothing.

What gives you that impression? Quoting David’s original email:

import * as fs from "fs"; // importing the named exports as an object
import Dict from "dict";  // importing a default export, same as ever

As a final note, and at the risk of erring in the world of speculation that Brendan fears, are we just sleepwalking towards pushing people to work around the whole debate with the "universal":

“are we just sleepwalking” – what are you implying?

Axel

# Sébastien Cevey (10 years ago)

On 20 June 2014 11:39, Axel Rauschmayer <axel at rauschma.de> wrote:

The `*' symbol universally represents a glob of "everything", but when used to import from a module that has multiple exports, you won't get everything, you will get either the single default export (if there is one) or nothing.

What gives you that impression? Quoting David’s original email:

import * as fs from "fs"; // importing the named exports as an object
import Dict from "dict";  // importing a default export, same as ever

Ah my bad, I must have got confused somewhere along the thread. That makes perfect sense to me too, ignore what I said.

As a final note, and at the risk of erring in the world of speculation that Brendan fears, are we just sleepwalking towards pushing people to work around the whole debate with the "universal":

“are we just sleepwalking” – what are you implying?

Given what you highlighted above, there would be a native syntax to give people the different cases they might need, so they would have no reason to do other silly workarounds.

Thanks for the clarification, and apologies for my misunderstanding!

# Forrest Norvell (10 years ago)

On Thu, Jun 19, 2014 at 8:48 PM, Brendan Eich <brendan at mozilla.org> wrote:

Domenic Denicola wrote:

The transitional era will be a particularly vulnerable time for TC39's module design, however: as long as people are using transpilers, there's an opportunity for a particularly well-crafted, documented, and supported transpiler to give alternate semantics grounded in the community's preferred model, and win over enough of an audience to bleed the life out of TC39's modules.

You're doing a disservice to everyone here by implying that such a transpiler could trump native modules as implemented per the normative ES6 spec in Chakra, JSC, SpiderMonkey, and V8.

Idle speculations are not even worth betting on, but filling es-discuss with tendentious idle speculations is worse. Cut it out.

Domenic may have erred by conflating many different constituencies into a monolithic, definite version of “the community”, but I don’t think his characterization of a “transitional era” or the risks therein are tendentious. Some people on es-discuss (including, at times, Domenic) seem to assume that Node.js developers will embrace ES6 part and parcel, when the feedback I receive on a daily basis is very different. (As I’m primarily a Node developer, I can’t speak to the concerns of other groups within the broader JS community, but I do think the feedback coming from e.g. James Burke is valuable here.)

I bring up these concerns not because I agree with them, but because I feel awkward about the disconnect between the message I see coming out of TC39 and the response from my friends and peers. I’m also concerned about schisms happening within the Node community, especially if the team developing the platform and the broader developer community find themselves at odds when it comes to module systems. That said, there are two overarching themes that tend to come up in discussions among Node developers when the ES6 module system is discussed:

(1) Node’s module system is frozen.

There has long been an ideology within Node that there will be a point at which it is “finished”, and at that point the platform/kernel will have a frozen API and all of the innovation around Node will happen in the module ecosystem. The forcing function for this is Node’s stability index nodejs.org/api/documentation.html#documentation_stability_index, which is a ratchet much like the ES7 design process, where once a piece of the platform attains a higher level of stability, it can’t move to a lower one.

The module system is one of the few pieces of Node that has the highest, locked level of stability. This is in part because it’s the piece that connects Node to npm and the module ecosystem. It has also long been the locus of long and difficult debates joyent/node#6960, and getting the module system to the locked state was a key move in fostering the active growth of the Node ecosystem, even though (as that thread shows) it clearly still has unresolved issues.

I can see a future in which ES6 modules are a part of V8 and therefore included in Node, and also where somebody builds a module loader that maps ES6 modules onto the Node module loader. Projects like jspm jspm.io are interesting in this regard, but this is all being done in the context of a “finished” module system, so there’s a deep skepticism (sometimes bordering on dismissiveness) on the part of Node’s core development team that moving Node to using ES6 modules by default is a thing that could even happen.

(2) Node’s modules are good enough for all use cases.

This is obviously a subjective statement that’s burdened with a large load of implicit assumptions, but it’s one I see a lot, especially from the community built up around browserify. Node’s core APIs tend to use multi-export because they’re themed grab bags (fs, util, os, etc), but the community has converged on single-export / single-purpose modules as the dominant convention for library development. Because CommonJS modules are just a protocol built atop plain JS objects, Node’s module system is easily flexible enough to support both styles of export, and both are entrenched. Any new proposal that ditches or deprecates either of these styles is therefore nearly guaranteed to cause dismay, which is (I believe) what happened when the notes from the module side discussion from the last face to face started circulating.

It also shows how high the bar is set when it comes to the Node community switching module systems. There’s little question in my mind that browser-first developers who aren’t heavily invested in AMD will move en masse to whatever ends up being in ES6 (and I believe that this comprises a majority of working JavaScript developers). I do believe that unless the path from Node to ES6 modules is painless, and unless the Node community sees clear value in making the switch, they’re just not going to bother.

Maybe this is fine! Transpilers exist, and browserify isn’t going anywhere. But I see people here evangelizing the new system, and occasionally acting as if they believe that everybody is going to switch to ES6 modules, and that seems very far from being a foregone conclusion to me. The status quo before the last face to face was a very delicate compromise that more or less successfully balanced all the competing requirements from JavaScript’s various constituencies. Therefore, when a new document shows up that proposes knocking out one of the load-bearing members of that compromise, nobody should be surprised when people get upset. (For what it’s worth, I think Dave Herman’s latest proposal is great, especially because it only changes syntax, not semantics.)

Domenic has been a big proponent of ES6 within the Node world, and has tended to err on the side of promoting the view that Node core and the Node developer community must adopt ES6 at the risk of getting left behind. I don’t feel like he has a particular axe to grind here, he’s just more attuned to what’s going on in Node than most of the people on this list, and is raising what seems like a pretty tempered version of a widely-held concern. Again, I know the Node world is pretty small in the grand scheme of things, and I don’t want to misrepresent the breadth of the objections, but I do think that the risk of fragmentation is significant, and I would like us to find a way to mitigate that risk if we can.

F

# Ron Buckton (10 years ago)

-----Original Message-----

From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of

Sébastien Cevey

Sent: Friday, June 20, 2014 3:46 AM

To: Axel Rauschmayer

Cc: es-discuss list

Subject: Re: ModuleImport

On 20 June 2014 11:39, Axel Rauschmayer <axel at rauschma.de> wrote:

The `*' symbol universally represents a glob of "everything", but

when used to import from a module that has multiple exports, you

won't get everything, you will get either the single default export

(if there is

one) or nothing.

What gives you that impression? Quoting David’s original email:

import * as fs from "fs"; // importing the named exports as an object

import Dict from "dict"; // importing a default export, same as ever

With all due respect, why is it that we cannot change the specification to allow import name from "module" for both the default export (for single export modules) and the Module object (for multi-named export modules). The same question goes for using import { name as "name" } from "module" for both. As specified, a default export is equivalent to a Module object with a "default" property, and as a result requires special handling with respect to how it is bound to the ImportBinding in import name from "module". Wouldn't it make sense to simplify the syntax and expand the static and runtime semantics for imports? Are we sure that the current semantics are the right approach that we should shoehorn the syntax into?

Is it imperative for module authors to be able to provide both a default export and named exports within the same module? From most of the comments in this thread, it seems that expected module use falls into two categories: Single-export modules and Multi-export modules. Is there a use-case in the wild (via ES6 module transpilers) where a single module today uses both a default export as well as named exports?

With respect to Node libraries, I often see one of three approaches to exporting from a module:

Named Exports:


exports.foo = 1;

// or

module.exports.foo = 1;

Single Export Object:


module.exports = {

  foo: 1,

  bar: function() {}
}

Single Export Function:


module.exports = function() { }

In Node, if you wanted to have a default export that is a function, but also have additional exports you would most likely add them as data properties on the function:


module.exports = function() {}

module.exports.foo = 1;

Given that, why not simplify the syntax and semantics to just the following three forms:


import "module"; // imports the module but does not perform binding

import name from "module"; // imports the module (either the default export or a module object with the named exports, see below)

import { name1, name2 as "otherName" } from "module"; // imports members of the module.

Simplifying this requires the following (approximate) changes in semantics:

  • Either (A) a module cannot have both a default export and named exports, or..

  • (B) A modules named exports become attached properties of the default export if provided.

    • If (B), it becomes an runtime error to add a default export after a named export, and a runtime error to add a named export if the default export is not an Object.
  • The ImportBinding (name above) becomes bound to a [[Value]] property of an (not part of the current spec) Import exotic object.

  • When the Module exotic object is loaded, if it has a property named "default", that becomes the value of the [[Value]] property of the Import exotic object.

  • If the Module exotic object does not have a property named "default", the Module itself becomes the value of the [[Value]] property of the Import exotic object.

  • NamedImports now points to bindings to the [[Value]] property of the Import exotic object. If you want both a default export and named exports, attach the named exports as properties of the default export.

With the above changes, whether you're using a default export or named exports becomes transparent to the developer. If the developer really wants the module object, they could fall back to:


import "module";

var name = System.get("module"); // Returns the Module object without the transformations applied from above.

The above is a rough approximation of the semantics changes. If anyone finds merit to this proposal, I'll find some time this weekend to write out exactly what kind of changes there would need to be in the static and runtime semantics in the current spec. The overall goal is to keep the import syntax simple and expand the static and runtime semantics to support that simplicity. This includes continuing to support the ability to handle cyclic dependencies. Engine authors will need to write the code for the import semantics once, while the development community will use the import syntax over and over for some time to come. It seems like a simpler syntax should win out over possibly changing the semantics.

Best ,

Ron

p.s. Here are some examples of various inputs and outputs based on this proposal:

Exports

[named-exports.js]


export var version = "1.0";

export function println(text) { console.log(text); }

[single-export.js]


function hello (text) { alert("Hello " + text + "!"); }

hello.goodbye = function (text) { alert("Goodbye " + text + "!"); }

export default hello;

[multi-export-with-default.js]


// if (A) above, this is an early error

// if (B) above, version becomes Person.version

export default class Person {

  jump() { return "How high?"; }
}

export var version = "2.0";

[single-export-of-object.js]


export default {

  version: "1.0",

  assert(test) { if (!test) throw new Error("failed!"); }

}

Imports

[import-named-imports.js]


import namedExports from "named-exports"; // namedExports = binding to Import.[[Value]] which results in a Module object with properties "version" and "println";

import { version, println } from "named-exports"; // version = binding to Import.[[Value]].version, println = binding to Import.[[Value]].println

// or...

import namedExports, { version, println } from "named-exports"; // both of the above

[import-single-export.js]


import hello from "single-export"; // hello = binding to Import.[[Value]] which results in the "hello" function

import { goodbye } from "single-export"; // goodbye = binding to Import.[[Value]].goodbye

// or...

import hello, { goodbye } from "single-export"; // both of the above

[import-multi-export-with-default.js]


// assumes (B) above

import Person from "multi-export-with-default"; // Person = binding to Import.[[Value]] which results in the "Person" class

import { version } from "multi-export-with-default"; // version = binding to Import.[[Value]].version

// or...

import Person, { version } from "multi-export-with-default"; // both of the above

[import-single-export-of-object.js]


import test from "single-export-of-object"; // test = binding to Import.[[Value]] which results in an Object with properties "version" and "assert"

import { version, assert } from "single-export-of-object"; // version = binding to Import.[[Value]].version, assert = binding to Import.[[Value]].assert

// or...

import test, { version, assert } from "single-export-of-object"; // both of the above.

# John Barton (10 years ago)

ES6 already has what you want:

Named Exports:

export var foo = 1;

Single Export Object:

export var moduleName = {
  foo: 1,
  bar: function() {}
};

Single Export Function:

export function fName() { }

And even cooler, the syntax for import is uniform,

import {foo} from './namedExport';

import {moduleName} from './singleExportObject';

import {fName} from './singleExportFunction';
# Ron Buckton (10 years ago)

From: John Barton [mailto:johnjbarton at google.com] Sent: Friday, June 20, 2014 3:48 PM

ES6 already has what you want:

Named Exports:

export var foo = 1;

Single Export Object:

export var moduleName = { foo: 1, bar: function() {} };

Single Export Function:

export function fName() { }

And even cooler, the syntax for import is uniform,

import {foo} from './namedExport';

import {moduleName} from './singleExportObject';

import {fName} from './singleExportFunction';

I'm not stating that I specifically "want" anything here, but was rather recommending an alternative approach to the single export vs named export debate and the removal of the ModuleImport production. David's mail was proposing the addition of the following syntax:

import * as fs from "fs"

This is designed to work around the fact that without ModuleImport, there's no simple way to get the module object for the named exports. What you really want to write is:

import fs from "fs";

However, the current semantics don't allow this. David proposed the new syntax as a replacement for ModuleImport. My only issue is that for the end user this could be confusing, and its possibly future-hostile for refactoring.

If I have a library today that uses an object literal as a default export in Node, and I want to migrate to ES6, the easiest approach is to just replace module.exports = with export default. My consumers would happy use import foo from "foo". If I later want to move to named exports, I would break my consumers as they would have to change this to import * as foo from "foo". The whole reason for this is that there is a semantic distinction with how a default export is handled vs. how named exports are handled.

If I were to use TypeScript's syntax for exports and imports, changing from a default export to named exports results in no change for the consumer:

[before.ts]

export = {
  foo: 1,
  bar() {}
}

[after.ts]

export var foo = 1;
export function bar() {}

[consumer.ts]

import before = require("before");
import after = require("after");
before.foo; // 1
before.bar; // function bar() {}
after.foo // 1
after.bar; // function bar() {}

Albeit, TypeScript does not have a Module exotic object, nor does it have mutable bindings, nor an ImportList in its import clause. That said, as far as the consumer is concerned there's no real distinction between the "default export" approach in before.ts and the "named export" approach in after.ts. We have this distinction in ES6 because it was designed that way to support mutable bindings and cyclic dependencies. I'm proposing that we come up with alternative semantics that preserve that approach while keeping the import syntax simple.

As a module consumer, I would constantly need to be aware of whether I need to use the import * as foo from "foo" syntax or the import foo from "foo" syntax. Where in Node I would use require("foo") for both cases. By changing the semantics of ImportDeclaration in ES6 and using a simpler syntax, we could would save developers the cognitive cost of determining which import syntax to among two very similar forms, as well as supporting the ability for a module author to refactor their module from a default export to named exports for the single-export-as-object case without affecting their consumers.

# John Barton (10 years ago)

On Fri, Jun 20, 2014 at 4:17 PM, Ron Buckton <rbuckton at chronicles.org>

wrote:

From: John Barton [mailto:johnjbarton at google.com] Sent: Friday, June 20, 2014 3:48 PM

ES6 already has what you want:

Named Exports:

export var foo = 1;

Single Export Object:

export var moduleName = { foo: 1, bar: function() {} };

Single Export Function:

export function fName() { }

And even cooler, the syntax for import is uniform,

import {foo} from './namedExport';

import {moduleName} from './singleExportObject';

import {fName} from './singleExportFunction';

I'm not stating that I specifically "want" anything here, but was rather recommending an alternative approach to the single export vs named export debate and the removal of the ModuleImport production.

Sorry, I did understand that. I was (cryptically) recommending that removing ModuleImport and export default until a later date gives us a good-enough solution to the cases you outline. These features can be added at a later very easily and since they would be based on real users asking for more features we could move on them quickly.

David's mail was proposing the addition of the following syntax:

import * as fs from "fs"

This is designed to work around the fact that without ModuleImport, there's no simple way to get the module object for the named exports. What you really want to write is:

import fs from "fs";

However, the current semantics don't allow this. David proposed the new syntax as a replacement for ModuleImport. My only issue is that for the end user this could be confusing, and its possibly future-hostile for refactoring.

So let's just remove it and export default and go. No confusion. We make progress. We study real feedback. We reconsider.

If I have a library today that uses an object literal as a default export in Node, and I want to migrate to ES6, the easiest approach is to just replace module.exports = with export default.

Easing migration is a great goal for the next release. We can refine ModuleImport between now and then.

My consumers would happy use import foo from "foo". If I later want to move to named exports, I would break my consumers as they would have to change this to import * as foo from "foo". The whole reason for this is that there is a semantic distinction with how a default export is handled vs. how named exports are handled.

Exactly. So don't allow default export. Presto, problem solved.

If I were to use TypeScript's syntax for exports and imports, changing from a default export to named exports results in no change for the consumer:

[before.ts]

export = {
  foo: 1,
  bar() {}
}

[after.ts]

export var foo = 1;
export function bar() {}

[consumer.ts]

import before = require("before");
import after = require("after");
before.foo; // 1
before.bar; // function bar() {}
after.foo // 1
after.bar; // function bar() {}

Albeit, TypeScript does not have a Module exotic object, nor does it have mutable bindings, nor an ImportList in its import clause. That said, as far as the consumer is concerned there's no real distinction between the "default export" approach in before.ts and the "named export" approach in after.ts. We have this distinction in ES6 because it was designed that way to support mutable bindings and cyclic dependencies. I'm proposing that we come up with alternative semantics that preserve that approach while keeping the import syntax simple.

I think both export default/module import and cyclic dependency support are marginal features that cause as many wacky problems as the solve. They just don't matter one way or another. The export default/module-import are easy to add later: removing them now is simple. Changing the semantics means redesigning modules.

As a module consumer, I would constantly need to be aware of whether I need to use the import * as foo from "foo" syntax or the import foo from "foo" syntax. Where in Node I would use require("foo") for both cases. By changing the semantics of ImportDeclaration in ES6 and using a simpler syntax, we could would save developers the cognitive cost of determining which import syntax to among two very similar forms, as well as supporting the ability for a module author to refactor their module from a default export to named exports for the single-export-as-object case without affecting their consumers.

Omitting export default and module-import achieves the same cognitive savings.

jjb

# Karolis Narkevičius (10 years ago)

Ron Buckton's suggestion above makes the most sense to me personally. And I saw many people arrive to a very similar idea a few times in these recent module threads - but it kind of always gets rejected with no substantial reasoning.

Please don't dismiss his suggestion super quickly with "es6 already has that" - because I don't think it does.

Specifically Ron's suggestion

  1. unifies the syntax for importing a default export, a named export and the module (which is what this thread is about)
  2. makes destructuring optional, which is very important in my opinion. Module consumers can choose to keep the module namespaced (i.e. when.all, when.map) instead of importing the names into the local scope. (es6 without the module import syntax doesn't have that)
  3. considers a fairly popular and a very interesting use case of exporting a function as the default export, but attaching named exports to it (es6 only supports that if named exports feature is bypassed). Examples of libraries that export a function with things attached are: underscore, when, request, moment, numeral, express, jquery. It's a pity this form of exporting won't be naturally supported in es6.

Having said all that, David's suggestion of import * as fs from "fs" helps with points 1 and 2 which is great!

My only feedback there is that I personally don't see much value in destructuring imports, e.g. I like to be able to easily see in the JavaScript code what module does each function belong to in each call site. So I can see myself using the module import syntax most of the time. Making that the same as as default import syntax as Ron suggested would therefore be nicer.

# Kevin Smith (10 years ago)

Ron Buckton's suggestion above makes the most sense to me personally. And I saw many people arrive to a very similar idea a few times in these recent module threads - but it kind of always gets rejected with no substantial reasoning.

I'm not really sure what Ron's proposal is, but I'm going to assume that the suggestion is to have the default "default" be the module instance object.

The reason why that's problematic is that it makes adding a default export at a later date a breaking change for consumers of the module. You would essentially have to decide whether your module is going to have a default export (other than the module instance object of course) when you first create it and never change it thereafter.

# C. Scott Ananian (10 years ago)

On Tue, Jun 24, 2014 at 9:26 AM, Kevin Smith <zenparsing at gmail.com> wrote:

Ron Buckton's suggestion above makes the most sense to me personally. And I saw many people arrive to a very similar idea a few times in these

recent

module threads - but it kind of always gets rejected with no substantial reasoning.

I'm not really sure what Ron's proposal is, but I'm going to assume that

the

suggestion is to have the default "default" be the module instance object.

Quoting Ron's proposal:

[W]hy not simplify the syntax and semantics to just the following three

forms:

import "module"; // imports the module but does not perform binding
import name from "module"; // imports the module (either the default
export or a module object with the named exports, see below)
import { name1, name2 as "otherName" } from "module"; // imports members
of the module.

Simplifying this requires the following (approximate) changes in semantics:

  • Either (A) a module cannot have both a default export and named exports, or..
  • (B) A modules named exports become attached properties of the default export if provided.
  • If (B), it becomes an runtime error to add a default export after a named export, and a runtime error to add a named export if the default export is not an Object. ** The ImportBinding (name above) becomes bound to a [[Value]] property of an (not part of the current spec) Import exotic object. ** When the Module exotic object is loaded, if it has a property named "default", that becomes the value of the [[Value]] property of the Import exotic object. ** If the Module exotic object does not have a property named "default", the Module itself becomes the value of the [[Value]] property of the Import exotic object. ** NamedImports now points to bindings to the [[Value]] property of the Import exotic object. If you want both a default export and named exports, attach the named exports as properties of the default export. With the above changes, whether you're using a default export or named exports becomes transparent to the developer. If the developer really wants the module object, they could fall back to:
import "module";
var name = System.get("module"); // Returns the Module object without the
transformations applied from above.

I favor (A) for ES6. I believe option (B) can be added later. (It's easy to later allow things which we initially forbid.) In my ideal world, (B) would be added in ES7ish as part of a more general "binding destructing mechanism". (That is, currently the only way to get magical lazy bound identifiers is via an import of named exports from a module. But in the future there might be other ways to do that which would be consistent with the way that named exports are bound to a default export object when both named properties and a default export object are used.)

The reason why that's problematic is that it makes adding a default export at a later date a breaking change for consumers of the module. You would essentially have to decide whether your module is going to have a default export (other than the module instance object of course) when you first create it and never change it thereafter.

Lots of things are breaking API changes. Changing the name of one of the named exports, for instance. Critically, in Ron's proposal, you can change a default export to named exports later (or vice versa) without changing the users of the module.

# Karolis Narkevičius (10 years ago)

The reason why that's problematic is that it makes adding a default export at a later date a breaking change for consumers of the module. You would essentially have to decide whether your module is going to have a default export (other than the module instance object of course) when you first create it and never change it thereafter.

That sounds perfectly reasonable to me.

# Karolis Narkevičius (10 years ago)

The reason why that's problematic is that it makes adding a default

export at a later date a breaking change for consumers of the module. You would essentially have to decide whether your module is going to have a default export (other than the module instance object of course) when you first create it and never change it thereafter.

That sounds perfectly reasonable to me.

As in yes - you want to decide on the API of your module upfront. If you want to add default only later - that probably means you intend the module to be used quite differently anyway, so it's fine to have a breaking API change for that sort of thing.

# Kevin Smith (10 years ago)

Lots of things are breaking API changes. Changing the name of one of the named exports, for instance. Critically, in Ron's proposal, you can change a default export to named exports later (or vice versa) without changing the users of the module.

Maybe, but certianly not without completely redoing the semantics. Do you think that's possible, at this point?

# C. Scott Ananian (10 years ago)

On Tue, Jun 24, 2014 at 10:05 AM, Kevin Smith <zenparsing at gmail.com> wrote:

Lots of things are breaking API changes. Changing the name of one of the named exports, for instance. Critically, in Ron's proposal, you can change a default export to named exports later (or vice versa) without changing the users of the module.

Maybe, but certainly not without completely redoing the semantics. Do you think that's possible, at this point?

I don't agree that the changes to the semantics are large, if we're talking about simply allowing a single syntactic form for both named and default import and Ron's option (A) (where default and named exports can not co-exist... until ES7 at least).

# Kevin Smith (10 years ago)

I don't agree that the changes to the semantics are large, if we're talking about simply allowing a single syntactic form for both named and default import and Ron's option (A) (where default and named exports can not co-exist... until ES7 at least).

But unless you want to rewrite the design, you cannot prevent a default export and named exports from co-existing:

function F() { }
export { F as default };  // Named and default
# Calvin Metcalf (10 years ago)

Side note: is that legal? I assumed you wouldn't be able to do that due to default being a reserved word.

# Ron Buckton (10 years ago)

If it is considered legal, then I'd say maybe the default export should be named @@default (or a similar symbol) instead.

Ron

Sent from my Windows Phone

# Kevin Smith (10 years ago)

Side note: is that legal? I assumed you wouldn't be able to do that due to default being a reserved word.

Definitely. Exports can be named any IdentifierName, so all of this is valid:

export { foo as class, bar as switch, baz as default };

And you can import IdentifierNames as well:

import { class as foo, switch as bar, default as baz } from "wherever";

Here's the crucial point:

In fact, the default export is nothing other than an export named "default". These two import declarations are entirely equivalent:

import { default as whatever } from "module";
import whatever from "module";

Does that make sense?

# Kevin Smith (10 years ago)

If it is considered legal, then I'd say maybe the default export should be named @@default (or a similar symbol) instead.

That was proposed at one time, but then the whole "default" business (which is arguably a bit dodgy to begin with) becomes something more than just sugar. A user cannot export a Symbol-named export, right? And really, why bother using a symbol in any case? What advantage would that give?

# Calvin Metcalf (10 years ago)

Thanks Kevin.

Back to bikeshedding import export syntax.

The refactoring hazards of defaulting to an object with all the exports are going to depend on how the feature is described, talking in terms of looking first for a default export and then returning the module body and now you get unforseen changes when you add an export named default.

But if modules were said to have a default default export that was an object with all the exports that could be overridden then that's different.

Adding an export named default and the import changed is unexpected, overriding the default export and the default import changed, on the other hand, is not unexpected behavior.

# Kevin Smith (10 years ago)

But if modules were said to have a default default export that was an object with all the exports that could be overridden then that's different.

I see where you're coming from, and honestly I don't have a better argument against a default default. (I actually tried to argue for that once upon a time.)

In any case, I still think the design is not complete unless you have some way to access the module instance object (even in the case where the default has been "overridden"), so we still need ModuleImport.

# Calvin Metcalf (10 years ago)

The

import 'foo'
let foo = this.get('foo')

Is less of a hassle if you only need it for when you have default and named exports and need the module object.

# Kevin Smith (10 years ago)
import 'foo'
let foo = this.get('foo')

Please no. Just...no.

: )

# Kevin Smith (10 years ago)
import 'foo'
let foo = this.get('foo')

To be less flippant, this isn't a solution to the problem: how do I statically bind to a module instance object? It's a runtime workaround.

Default-default aside, what's the payoff in trying to obscure the fact that a module is always a named set of exported bindings?

# Calvin Metcalf (10 years ago)

import 'foo' let foo = this.get('foo')

that was what came out of the last meeting to replace module/from semantics

the payoff is decoupling how something is imported from how something is exported. A module is a bag of names, but there is no reason an import has to be as well.

# Domenic Denicola (10 years ago)

From: es-discuss <es-discuss-bounces at mozilla.org> on behalf of Calvin Metcalf <calvin.metcalf at gmail.com>

the payoff is decoupling how something is imported from how something is exported.

+1. Although I am uneasy about the default-default export idea, since it has several edge cases that make it seem too messy for a language-level feature, it does fix the major problem of encapsulation breakage that the current design introduces. Having to know how the module author wrote their code, and using the correct import form based on that, is frustrating and a regression from current CommonJS/AMD module usability.

# Kevin Smith (10 years ago)

the payoff is decoupling how something is imported from how something is exported.

Maybe I'm being dense, but I must admit I find this statement to be completely baffling. The import side always has to match the export side - how could it possibly be any different? What am I missing here?

A module is a bag of names, but there is no reason an import has to be as well.

Between the default import (which is what you want) and ModuleImport it seems that all of our bases are covered. What's the problem, then?

Is it merely that you want a default-default? Is that the only issue for you?

# C. Scott Ananian (10 years ago)

On Wed, Jun 25, 2014 at 1:38 PM, Kevin Smith <zenparsing at gmail.com> wrote:

the payoff is decoupling how something is imported from how something is exported.

Maybe I'm being dense, but I must admit I find this statement to be completely baffling. The import side always has to match the export side - how could it possibly be any different? What am I missing here?

One import syntax. C'mon, we've been over this for months Kevin.

# Kevin Smith (10 years ago)

One import syntax. C'mon, we've been over this for months Kevin.

I get that, I really do. But specifically, that would mean a default-default, right?

# Calvin Metcalf (10 years ago)

default-default is the least worst one I have heard that doesn't totally change everything,

# Kevin Smith (10 years ago)

default-default is the least worst one I have heard that doesn't totally change everything,

Fair enough. Thanks for putting up with my questioning - I think I have a better understanding of where you and Scott are coming from now.

Correct me if I'm wrong, but the perspective says: "why would I need to import the multiple-exports if I'm specifically overriding the exports with a default? Having a way to import both the default and multiple-exports is silly and confusing."

# C. Scott Ananian (10 years ago)

On Wed, Jun 25, 2014 at 2:59 PM, Kevin Smith <zenparsing at gmail.com> wrote:

Correct me if I'm wrong, but the perspective says: "why would I need to import the multiple-exports if I'm specifically overriding the exports with a default? Having a way to import both the default and multiple-exports is silly and confusing."

For my part, my personal perspective is, "I have a module named foo. I want to write foo.bar to get the export named bar. I don't care what foo is. Perhaps its a function object for backwards-compatibility. Perhaps it's a module object because of some circular dependency. Perhaps it's a plain object. To me it's just a namespace. Please let me use the same import syntax regardless. In exchange, I promise never to use bare foo in my code."

There are a couple of different solutions; default-default is one of those.

# John Barton (10 years ago)

// I have a module named foo. // I don't care what foo is. // Including whether or not its a namespace. // I need make no promises about identifier foo. import {bar} from './foo';

# C. Scott Ananian (10 years ago)

@John Barton: Yes, ideally that syntax would work as well when you don't need a namespace. But sometimes you do need a namespace, even if you don't care precisely what it is:

import {format} from 'url';
import {format} from 'util';
import {connect} from 'tls';
import {connect} from 'net';
import {fork} from 'cluster';
import {fork} from 'child_process';
// etc
# Brian Donovan (10 years ago)

You don’t actually need a namespace for this, though it may be more convenient/aesthetically pleasing than the alternative:

import {format as formatURL} from 'url';
import {format} from 'util';
import {connect as tlsConnect} from 'tls';
import {connect as netConnect} from 'net';
import {fork as clusterFork} from 'cluster';
import {fork} from 'child_process';
// etc
# C. Scott Ananian (10 years ago)

On Wed, Jun 25, 2014 at 6:17 PM, Brian Donovan <me at brian-donovan.com> wrote:

You don’t actually need a namespace for this, though it may be more convenient/aesthetically pleasing than the alternative:

"may"? (!)

# Kevin Smith (10 years ago)

On Wed, Jun 25, 2014 at 4:50 PM, C. Scott Ananian <ecmascript at cscott.net>

wrote:

@John Barton: Yes, ideally that syntax would work as well when you don't need a namespace. But sometimes you do need a namespace, even if you don't care precisely what it is:

import {format} from 'url';
import {format} from 'util';
import {connect} from 'tls';
import {connect} from 'net';
import {fork} from 'cluster';
import {fork} from 'child_process';
// etc

I agree, and importing as a namespace is what ModuleImport is all about. Crazy idea: what if we had this:

// ModuleImport: import Identifier from StringLiteral
import fs from "fs";
import url from "url";

And just got rid of the default monkey-business? Simple, no confusion, no refactoring hazards.

Do we really need assignable default exports? If we could jettison that feature, it would (as John points out) make all of this pain go away.

# Marius Gundersen (10 years ago)

On Thu, Jun 26, 2014 at 8:15 AM, Kevin Smith <zenparsing at gmail.com> wrote:

I agree, and importing as a namespace is what ModuleImport is all about. Crazy idea: what if we had this:

// ModuleImport: import Identifier from StringLiteral
import fs from "fs";
import url from "url";

And just got rid of the default monkey-business? Simple, no confusion, no refactoring hazards.

Do we really need assignable default exports? If we could jettison that feature, it would (as John points out) make all of this pain go away.

That would simplify the usage of modules (users wouldn't need to look up which of the two ways the module author (arbitrarily) decided to use) and it would simplify authoring of modules (only one way to export things). The only real complaint we are likely to hear is that it would not allow single export modules, which is the favoured usage of modules. I disagree with this; a module that only exports one named thing is a single export module. If you want to author a single export module then you only export one thing, and users of your module only import one thing:

//file AbstractSingletonProxyFactoryBean.js
export class AbstractSingletonProxyFactoryBean {
  //...
}

//file MyApp.js
import {AbstractSingletonProxyFactoryBean} from
"AbstractSingletonProxyFactoryBean";

Now the author can choose to export more things later without making breaking changes to the module. The only downside to this is the (apparently mandatory) curly braces around the imported object. If single export/import becomes the convention with ES6 modules then users will be forced to type an extra pair of {} several times in most of their files. Is the two extra characters something we can live with?

Marius Gundersen

# Kevin Smith (10 years ago)

Now the author can choose to export more things later without making breaking changes to the module. The only downside to this is the (apparently mandatory) curly braces around the imported object. If single export/import becomes the convention with ES6 modules then users will be forced to type an extra pair of {} several times in most of their files. Is the two extra characters something we can live with?

I have a good bit of experience coding ES modules, and I was worried about that at first. But hasn't been a problem for me. Then again, I'm just one person - it would be good to get more data from developers actually coding with ES modules.

This syntax would make things completely obvious and simple though: if there's curly braces, then you're reaching into the bag and pulling out things, and if there's no curly braces, you're getting the bag itself. There's even a visual analogy at play here: the curly braces themselves resemble the sides of a bag.

# Russell Leggett (10 years ago)

Now the author can choose to export more things later without making

breaking changes to the module. The only downside to this is the (apparently mandatory) curly braces around the imported object. If single export/import becomes the convention with ES6 modules then users will be forced to type an extra pair of {} several times in most of their files. Is the two extra characters something we can live with?

I have a good bit of experience coding ES modules, and I was worried about that at first. But hasn't been a problem for me. Then again, I'm just one person - it would be good to get more data from developers actually coding with ES modules.

This syntax would make things completely obvious and simple though: if there's curly braces, then you're reaching into the bag and pulling out things, and if there's no curly braces, you're getting the bag itself. There's even a visual analogy at play here: the curly braces themselves resemble the sides of a bag.

To me, though, that goes back to the destructuring/not destructuring aspect. Maybe this has floated by during the bikeshedding, but why not something like:

//import a single named export
import foo from "bar";

//import multiple named exports
import foo, baz from "bar";

//alias an imported named export
import foo as fooAlias from "bar";

//import the module
import "bar" as bar;

So basically, just get rid of the {} for importing named exports, and move the whole module import after the as. It reads better to me and I think is more intuitive. As for default exports - I think they only make sense if done the way it works in node. A single default export that effectively replaces the module. Let's remember how it gets used in node:

var _ = require("underscore");

In node, if they didn't do it as a single export, then you would have to do:

var _ = require("underscore")._;

Given that underscore (and jquery and several others) are only a single export, it would be annoying and error prone to do that all over the place. The value added was for the importer - not the exporter. With the new syntax I'm proposing, importing underscore would be exactly the same as what the current proposal is for default export imports.

import _ from "underscore";

Its just that underscore would have to have the named export _. As I said, though, that's not really a burden, and the argument for default exports was never really for the module writer.

There is one small case I'm missing, which is that default export imports let you name the import whatever you want without writing extra code for the alias. I don't really find that as a drawback, but then again, I do write a lot of Java in addition to JavaScript. I don't really see any other languages that operate this way either, and I would come back again to the distinct history and constraints that JS/node has had up until now. If keep that flexibility/matching semantics for smoothest continuity, I would propose that default exports have to be a single default export, and that it would replace module importing instead of export importing.

import "underscore" as _;

Where there is a single default export in the underscore module. This would mean that the normal module import semantics don't work in this case, but that is the tradeoff decided by the module author. I personally don't think its worth adding this feature, but if it were added, I would expect it to work this way.

# C. Scott Ananian (10 years ago)

On Thu, Jun 26, 2014 at 10:50 AM, Russell Leggett <russell.leggett at gmail.com

wrote:

To me, though, that goes back to the destructuring/not destructuring aspect. Maybe this has floated by during the bikeshedding, but why not something like:

//import a single named export
import foo from "bar";

//import multiple named exports
import foo, baz from "bar";

//alias an imported named export
import foo as fooAlias from "bar";

//import the module
import "bar" as bar;

I like the fact that this doesn't look like destructuring, since variable binding is different from destructuring assignment. Could

import foo, baz from "bar" as bar;

allowed simultanous import of named exports and the module itself? If so, the grammar gains a pleasing regularity.

OTOH, I think this syntax reorganization is orthogonal to some of the other issues discussed. In particular, your latter proposal still allows for user confusion between:

import _ from "underscore";
import "underscore" as _;

The various proposals seem to try to address this by making these semantically identical (or near-identical, with some cooperation from the module author).

# Mark Volkmann (10 years ago)

On Thu, Jun 26, 2014 at 9:50 AM, Russell Leggett <russell.leggett at gmail.com>

wrote:

To me, though, that goes back to the destructuring/not destructuring aspect. Maybe this has floated by during the bikeshedding, but why not something like:

//import a single named export
import foo from "bar";

//import multiple named exports
import foo, baz from "bar";

//alias an imported named export
import foo as fooAlias from "bar";

//import the module
import "bar" as bar;

So basically, just get rid of the {} for importing named exports, and move the whole module import after the as.

Yes! I would SO much happier with this if the curly braces were removed.

# Russell Leggett (10 years ago)

I like the fact that this doesn't look like destructuring, since variable binding is different from destructuring assignment. Could

import foo, baz from "bar" as bar;

allowed simultanous import of named exports and the module itself? If so, the grammar gains a pleasing regularity.

You know, I hadn't even thought of that, but yes, I think that could work and be useful.

OTOH, I think this syntax reorganization is orthogonal to some of the other issues discussed. In particular, your latter proposal still allows for user confusion between:

import _ from "underscore";
import "underscore" as _;

The various proposals seem to try to address this by making these semantically identical (or near-identical, with some cooperation from the module author).

Default exports are confusing. No way around it. No other system that I have seen really does it. If you look at the way node does default exports, its really so much a default export as a module replacement. As in, instead of giving you a module when you import, the system gives you that one exported thing. I tried to mirror those same semantics, as I think that is the only real argument for default exports. There is existing code in place that uses those semantics heavily. If we are trying to pull from experience, keep it consistent. The way to do that is to make default exports get imported using the same syntax as module imports.

import _ from "underscore"; //looks like getting a member from the
underscore module
import "underscore" as _; //looks like importing the whole module and
naming it

I don't think most developers will really understand the clear difference between a module and a regular object and therefore won't seem weird if a module is a function. They key point is - are you importing the whole thing or a member of the thing.

# Kevin Smith (10 years ago)

To me, though, that goes back to the destructuring/not destructuring aspect. Maybe this has floated by during the bikeshedding, but why not something like:

//import a single named export
import foo from "bar";

//import multiple named exports
import foo, baz from "bar";

//alias an imported named export
import foo as fooAlias from "bar";

//import the module
import "bar" as bar;

: )

esdiscuss/2013-April/029841

I tried this out in a parser and in real code, and was only moderately happy with the result. I find that I prefer the curlies.

Bikeshedding aside, I think we pretty much agree across the board that the current setup which attaches special importance to a "default" export is confusing and suboptimal. It is clear to me that it does not satisfy the constituents that it is meant to satisfy.

I agree with Russell that the only way to make "default" work is to have override semantics. That is, the user must be allowed to "override" the module instance object somehow. Since that approach is inherently "hacky", I doubt it will get consensus in TC39.

With those two facts in mind, I think we need to see evidence that the "default" feature provides more than marginal gains in terms of user experience. If such evidence cannot be produced, then the "default" feature does not carry its weight and must be removed.

So.... what's the evidence?

# Michał Gołębiowski (10 years ago)

On Thu, Jun 26, 2014 at 4:50 PM, Russell Leggett <russell.leggett at gmail.com>

wrote:

//import a single named export
import foo from "bar";

//import multiple named exports
import foo, baz from "bar";

//alias an imported named export
import foo as fooAlias from "bar";

//import the module
import "bar" as bar;

That would put a lot of Node modules exporting a single object/function at a disadvantage. Compare:

var mkdr = require('mkdirp');

to:

import mkdirp as mkdr from mkdirp;

I like that the most common module usage can be done simpler.

# Russell Leggett (10 years ago)

On Fri, Jun 27, 2014 at 3:41 AM, Michał Gołębiowski <m.goleb at gmail.com>

wrote:

On Thu, Jun 26, 2014 at 4:50 PM, Russell Leggett < russell.leggett at gmail.com> wrote:

//import a single named export
import foo from "bar";

//import multiple named exports
import foo, baz from "bar";

//alias an imported named export
import foo as fooAlias from "bar";

//import the module
import "bar" as bar;

That would put a lot of Node modules exporting a single object/function at a disadvantage. Compare:

var mkdr = require('mkdirp');

to:

import mkdirp as mkdr from mkdirp;

I like that the most common module usage can be done simpler.

No, that example would be:

import "mkdirp" as mkdir;

Its actually shorter than node.

# Michał Gołębiowski (10 years ago)

On Thu, Jun 19, 2014 at 4:04 PM, David Herman <dherman at mozilla.com> wrote:

On Jun 19, 2014, at 3:31 AM, Michał Gołębiowski <m.goleb at gmail.com> wrote:

Compare these 3 forms of importing all the module "lodash" bindings to an object _:

var _ = require("lodash"); // Node
import * as _ from "lodash"; // Dave's syntax
import "lodash" as _;

My feeling is that the clutter is small, and it's acceptable to have it be slightly less minimal than default export since that is the case we are favoring. This isn't so cluttered as to be downright punishing multi-export utility modules (it's literally only two characters longer than your Node code, albeit admittedly a little chattier), but it's good that default import is the winner.

IMO it's not even that much a question of taken space but of potential confusion. People will think if they can do:

import * as _ from "lodash";

they can do:

import * from "lodash";

as well. Also, there are reports of thinking that the first form exports not only the _ container object but also all individual methods as in Python.

I'd still prefer:

import "lodash" as _;

I think it's less confusing that other proposals and reads naturally; it also corresponds more to other module systems. But if that's out of the picture, sth like:

import module _ from "lodash";

would be IMO less confusing that the proposed * as _ form.

# Michał Gołębiowski (10 years ago)

On Fri, Jun 27, 2014 at 9:44 AM, Russell Leggett <russell.leggett at gmail.com>

wrote:

No, that example would be:

import "mkdirp" as mkdir;

Its actually shorter than node.

But in current proposal the module object cannot be a function from what I understand. So you'd have to create a named export and import it as I wrote.

# Andreas Rossberg (10 years ago)

Some observations:

  • I think the 'import * as x' syntax for module imports is not an improvement, for at least two reasons:

    • It raises the expectation that you can actually write 'import * from' (as already noted in this thread).

    • It removes the syntactic marker for binding a module identifier. That is problematic because (a) module identifiers come with extra static checks on their uses (or so I thought, see below), and it is (b) future hostile to lexical module declarations, because with those, only module identifiers could be used in certain contexts (e.g., on the RHS of an import-from). Thinking forward, I think it would be highly preferable to consistently mark all bindings of module identifiers with a 'module' keyword.

  • I think the controversy is because the module design tries pleasing two incompatible community goals, but instead of achieving that, it has acquired all the characteristics of a committee design (although, ironically, modules are probably the least committee-designed part of ES6 :) ):

    • Some want modules in the conventional sense, as encapsulated namespaces with named exports, which you can import and access qualified or unqualified, and where imports are checked.

    • Some want modules in the specific pre-ES6 JavaScript style, where they can be any arbitrary JS value, and in the good old JavaScript tradition that checking doesn't matter.

The current design (including the newest suggestions for imports) seems to please neither side, because it tries to be the former semantically, but wants to optimise for the latter syntactically. The main outcome is confusion.

  • This is probably the 3rd or even 4th time round this aspect of modules is discussed controversially. That is not a good sign. I really think we need to make up our minds:

    • Either we think "real" modules are an improvement, and checking is important. Then the model and the syntax should be consistent about that. Moreover, checking needs to consistently apply, no matter how a module and its components are defined or accessed.

    • Or we come to the conclusion that supporting the legacy singleton export model as a primary use case is a mandatory matter. Then we should drop attempts of building something in/semi-compatible, and limit innovation to making export and import declarative, and designing a loader API.

The current design falls between stools with respect to syntax, leading to the confusion people complain about. And it falls between stools with respect to import checking, making it dependent on superficially syntactic choices (like, whether you use default export or not, or whether you use unqualified import or not), and thereby unreliable in practice.

Quite honestly, I rather have no import checking than checking that only applies in some syntactic cases -- because that gives a false sense of security, harms refactoring, and/or encourages overuse of unqualified imports, hampering readability. And if we are willing to cut partial checking, then there actually is much less compelling reason to have all the machinery around module objects, import as aliasing, etc. The system could probably be simplified quite a bit, and get much closer to what proponents of legacy JS modules are used to.

Of course, I'd much rather go with "real" modules. But more importantly, the system should make a choice. Something in the middle is looking more and more like the least attractive alternative -- it seems like substantial extra complexity for too little (or even negative) benefit. As surprising as it may sound to some, I'm starting to warm up to the legacy option if it avoids the floor between the stools. :)

# Kevin Smith (10 years ago)
  • Either we think "real" modules are an improvement, and checking is important. Then the model and the syntax should be consistent about that. Moreover, checking needs to consistently apply, no matter how a module and its components are defined or accessed.

  • Or we come to the conclusion that supporting the legacy singleton export model as a primary use case is a mandatory matter. Then we should drop attempts of building something in/semi-compatible, and limit innovation to making export and import declarative, and designing a loader API.

Since "real" modules are at the core of the current design, I think the first option is feasible and equates to simply dropping the "default" feature, leaving everything else intact. The second option, on the other hand, will take us back to the drawing board and I don't see how we can do that within the ES6 timeline. We don't want Promises all over again.

So, if we cannot settle on option 1, then I think modules must be deferred from ES6.

# Matthew Robb (10 years ago)

My opinion is that CommonJS and AMD work today and will continue to work into the future so we should optimize for the ideal "looking forward, not backward" case when adding to the language. Loader will still be overload-able and since both CommonJS and AMD require a library today it seems completely reasonable that they will continue to do that and can hook into es6 through loader extension.

Conclusions:

  • Drop default exports

  • export function readFile(){}

  • import fs from "es6-fs"; // fs.readfile();

  • import { readFile } from "es6-fs"; // readFile();

  • Done.

  • Matthew Robb

# Domenic Denicola (10 years ago)

From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Kevin Smith

The second option, on the other hand, will take us back to the drawing board and I don't see how we can do that within the ES6 timeline.

We should not be concerned about timeframe in terms of what official Ecma spec revision modules land in. ES has moved to a train model, recognizing that it's more important when features ship in browsers than when Ecma publishes a copyrighted document containing the features. Modules are probably in stage 2, verging on stage 3, of the TC39 process. They won't make it into ES6 until they land in stage 4, and no implementation has started on them. It's not clear to me that keeping the current design would actually incentivize implementers to implement them any faster than if they dropped back to stage 1, especially since implementers generally prioritize features with high user demand, whereas modules in their current state are definitely "mixed signals."

# Domenic Denicola (10 years ago)

From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Matthew Robb

My opinion is that CommonJS and AMD work today and will continue to work into the future so we should optimize for the ideal "looking forward, not backward" case when adding to the language.

While this is a compelling argument in general, I am not sure it applies to modules, whose usefulness is based almost entirely on network effects. Indeed, the future we may be looking forward to is just one in which ES6 modules remain a third competing standard among the modules in the wild, and if they're sufficiently bad at satisfying existing use cases (as the rest of your message argues for), they will not compete very well.

# Kevin Smith (10 years ago)

We should not be concerned about timeframe in terms of what official Ecma spec revision modules land in. ES has moved to a train model, recognizing that it's more important when features ship in browsers than when Ecma publishes a copyrighted document containing the features.

Fair enough - but the point still remains: the second option pushes us backward.

# John Barton (10 years ago)

+1

# Kevin Smith (10 years ago)

+1

Yeah - obviously +1 from me as well.

# Russell Leggett (10 years ago)

On Fri, Jun 27, 2014 at 3:52 AM, Michał Gołębiowski <m.goleb at gmail.com>

wrote:

On Fri, Jun 27, 2014 at 9:44 AM, Russell Leggett < russell.leggett at gmail.com> wrote:

No, that example would be:

import "mkdirp" as mkdir;

Its actually shorter than node.

But in current proposal the module object cannot be a function from what I understand. So you'd have to create a named export and import it as I wrote.

Ok, so can I just ask a serious question. I looked at the mkdirp library, and in their own documentation, they use:

var mkdirp = require('mkdirp');

So let's say in the new world order, no default exports, this is a named export mkdirp. Is it really that bad a thing to just use the name they chose for their API? I mean what about all of the likely cases of exporting a class. Would you advocate for those classes to be aliased before use? What about methods? Those are typically unchanged. The argument has been made that without default exports, library users would then have to known the name of the exported member. Is that really a serious complaint? You have to know what everything else is called, what parameters are expected, etc. I would go as far as to say that either 1 of 2 things is happening.

# Kevin Smith (10 years ago)
var mkdirp = require('mkdirp');

Exactly. In ES, you would see this:

import { mkdrip } from "mkdirp";

Python has a somewhat similar module system, and import-renaming is the exception, not the rule. Also, I've never heard anyone complain about having to know the name of a module member. In fact, specifying the exported name is essential for tracing out a code base that you're unfamiliar with.

# Russell Leggett (10 years ago)

Ok, so can I just ask a serious question. I looked at the mkdirp library, and in their own documentation, they use:

var mkdirp = require('mkdirp');

So let's say in the new world order, no default exports, this is a named export mkdirp. Is it really that bad a thing to just use the name they chose for their API? I mean what about all of the likely cases of exporting a class. Would you advocate for those classes to be aliased before use? What about methods? Those are typically unchanged. The argument has been made that without default exports, library users would then have to known the name of the exported member. Is that really a serious complaint? You have to know what everything else is called, what parameters are expected, etc. I would go as far as to say that either 1 of 2 things is happening.

Ok, accidentally sent... either 1 of 2 things is happening....

  1. The imported function is always called the same thing when imported. Its a de facto standard.
  2. There is no de facto standard, and people call it a variety of things.

I would argue that 1 is easy to transition, just export with that name, and 2 is not really a good thing. Its much harder to read or grep your code. It is an anti-pattern that should not be given so much sugar. Aliasing is still possible, but more awkward.

I know Kevin and I both said this, but IMO the reason this pattern became so popular was to avoid:

var mkdirp = require("mkdirp").mkdirp;

In my proposal (no curlies), that's simply:

import mkdirp from "mkdirp";

The big thing missing is just ease of aliasing, and I don't really see that as a problem. I'd love to hear an argument for why I'm wrong.

# Russell Leggett (10 years ago)

On Fri, Jun 27, 2014 at 10:11 AM, Kevin Smith <zenparsing at gmail.com> wrote:

var mkdirp = require('mkdirp');

Exactly. In ES, you would see this:

import { mkdrip } from "mkdirp";

Python has a somewhat similar module system, and import-renaming is the exception, not the rule. Also, I've never heard anyone complain about having to know the name of a module member. In fact, specifying the exported name is essential for tracing out a code base that you're unfamiliar with.

Haha, yes, you and I seem to be sharing a brain on this, I sent early and you wrote most of what I wrote. I still like curly-free, but that's bikeshedding to me. The important point is removing default exports.

# Calvin Metcalf (10 years ago)

I wrote up a gist summarizing the different proposals that were being tossed around, it mainly says the same things as Andreas, just not as well gist.github.com/calvinmetcalf/5d9a88abaa9fe094e960\

# Domenic Denicola (10 years ago)

This might be a good time to bring up this old thing:

gist.github.com/domenic/1ab3f0daa7b37859ce43

At the time Yehuda and I put it together, we were much younger and more naive, but upon cursory review it seems not-horrible. It gives up mutable bindings but retains statically-verifiable multi-exports, along with a unified import syntax that doesn't require knowledge of how the module creator exported their objects, and exposes no refactoring hazards for transitioning between styles.

I just realize it is probably not very compatible with computed property names though :(

# Kevin Smith (10 years ago)

My opinion is that CommonJS and AMD work today and will continue to work into the future so we should optimize for the ideal "looking forward, not backward" case when adding to the language.

I think this statement points the way to something that we haven't yet discussed.

A general question that we can apply to any language enhancement is: does this change merely satisfy existing users, or does this change carry the potential to expand the user base?

I would say that "Node-module-sugar" would merely satisfy existing users, but "real" modules have the capacity to expand the JS user base to into segments that want static checking to be a part of their workflow.

So to me the path forward is clear: we keep real modules, axe the default feature, and take a temporary hit of dissatisfaction from existing users so that we can expand the JS user base.

The overriding concern should be the long-term viability of JS.

# C. Scott Ananian (10 years ago)

On Fri, Jun 27, 2014 at 9:07 AM, Andreas Rossberg <rossberg at google.com>

wrote:

  • It removes the syntactic marker for binding a module identifier. That is problematic because (a) module identifiers come with extra static checks on their uses (or so I thought, see below), and it is (b) future hostile to lexical module declarations, because with those, only module identifiers could be used in certain contexts (e.g., on the RHS of an import-from). Thinking forward, I think it would be highly preferable to consistently mark all bindings of module identifiers with a 'module' keyword.

I disagree here. I think certain people are too hung up on what is a "real module" and what is an "object pretending to be a module". For lots of legacy code (think jquery), we have objects like $ which are actually both functions and modules (in the form of namespaces). The JS community can handle this. Honestly, most of the fun features of "real modules" are obscure corner cases anyway (like lazy binding). We can deal with a few oddball "modules" which are not actually module objects.

  • Some want modules in the conventional sense, as encapsulated namespaces with named exports, which you can import and access qualified or unqualified, and where imports are checked.

  • Some want modules in the specific pre-ES6 JavaScript style, where they can be any arbitrary JS value, and in the good old JavaScript tradition that checking doesn't matter.

The current design (including the newest suggestions for imports) seems to please neither side, because it tries to be the former semantically, but wants to optimise for the latter syntactically. The main outcome is confusion.

I think the problem is somewhat different, in that certain people resist the idea that some "modules" might not be actual modules. I think we could embrace that and the result would be less confusion (since we wouldn't require a syntactic marker for "real module"). It will be rare for someone to notice that $.foo is not lazily-bound, or that $.module.path (or whatever) isn't actually defined. If they care, they can file a bug against jquery. jquery should be able to either emulate the module functionality (duck typing! but this is one of the things that annoys me about the current way lazy binding works) or decide that backwards-compatibility or whatever is more important and ignore the issue.

  • Either we think "real" modules are an improvement, and checking is important. Then the model and the syntax should be consistent about that. Moreover, checking needs to consistently apply, no matter how a module and its components are defined or accessed.

  • Or we come to the conclusion that supporting the legacy singleton export model as a primary use case is a mandatory matter. Then we should drop attempts of building something in/semi-compatible, and limit innovation to making export and import declarative, and designing a loader API.

Again, I think it's the "either/or" mindset which is the problem here. We can actually have both. Sometimes we won't be able to do strict checking. Sometimes we will. We don't need to force everyone into the same mold. Let the author of the module decide how important static checking, lazy binding, ease-of-use, etc, is and define their module appropriately. (And the same for the "importer of the module".)

Quite honestly, I rather have no import checking than checking that only applies in some syntactic cases -- because that gives a false sense of security, harms refactoring, and/or encourages overuse of unqualified imports, hampering readability.

Again: is this what's hampering compromise? Can't we let the module author (and/or importer) decide the extent to which they think checking is important?

Yes, modules will be a compromise design. But it's a compromise design because of network effects and programmer preference. There are large and useful codebases organized around different principles. We can't force them all into the same mold. Programming is still an art form and a matter of taste; let's go easy on the mandates. --scott (who actually thinks that current modules spec is not that bad, although it could be slightly improved)

# Brian Di Palma (10 years ago)

I'm echoing Kevin here.

I'd hope ES6 modules would offer me, a programmer working on large complex JS web apps, something more then what CommonJS/AMD offers. I work with codebases of 1K+ JS classes, 200K+ LOC and with many dependecies that are updated frequently. When our teams update depedencies the more static fast failures we can get compared to silent bugs the better.

I will eat my hat if in 10 years people will be doing greenfield development using CommonJS/AMD instead of ES6 modules.

Provide the language users with something better then what we have, don't just match it.

I like CommonJS, npm, node as they offer great solutions given their constraints. This is the language level though, the expressive power and constraints are different.

Knowing the name of a module export is not an issue, it's not an issue for people using global scripts or any other module systems/languages. I'd prefer a "real" module system. Especially considering that classes are being added and when you are building front end applications with a lot of state classes fit well. I can see many applications being mainly composed of modules with a single class exported and the most logical export name would be the classname. I've tried to use default exports for these situations and they don't seem to offer much value at all.

import {MyClass} from './myclass';

The value of default exports seems low here.

# Andreas Rossberg (10 years ago)

On 27 June 2014 18:36, C. Scott Ananian <ecmascript at cscott.net> wrote:

On Fri, Jun 27, 2014 at 9:07 AM, Andreas Rossberg <rossberg at google.com> wrote:

  • It removes the syntactic marker for binding a module identifier. That is problematic because (a) module identifiers come with extra static checks on their uses (or so I thought, see below), and it is (b) future hostile to lexical module declarations, because with those, only module identifiers could be used in certain contexts (e.g., on the RHS of an import-from). Thinking forward, I think it would be highly preferable to consistently mark all bindings of module identifiers with a 'module' keyword.

I disagree here. I think certain people are too hung up on what is a "real module" and what is an "object pretending to be a module". For lots of legacy code (think jquery), we have objects like $ which are actually both functions and modules (in the form of namespaces). The JS community can handle this. Honestly, most of the fun features of "real modules" are obscure corner cases anyway (like lazy binding). We can deal with a few oddball "modules" which are not actually module objects.

I think you are missing the central problem. If imports/exports are to be statically checked, then module bindings, their exports, and their uses have to be statically recognisable and analysable. That prohibits intermixing "proper" modules with other random objects. Furthermore, it also requires module identifiers to be distinguishable by context. (And my point above was that, since this matters for the semantics, the programmer is best served when she can immediately distinguish binding forms accordingly.)

  • Some want modules in the conventional sense, as encapsulated namespaces with named exports, which you can import and access qualified or unqualified, and where imports are checked.

  • Some want modules in the specific pre-ES6 JavaScript style, where they can be any arbitrary JS value, and in the good old JavaScript tradition that checking doesn't matter.

The current design (including the newest suggestions for imports) seems to please neither side, because it tries to be the former semantically, but wants to optimise for the latter syntactically. The main outcome is confusion.

I think the problem is somewhat different, in that certain people resist the idea that some "modules" might not be actual modules. I think we could embrace that and the result would be less confusion (since we wouldn't require a syntactic marker for "real module"). It will be rare for someone to notice that $.foo is not lazily-bound, or that $.module.path (or whatever) isn't actually defined. If they care, they can file a bug against jquery. jquery should be able to either emulate the module functionality (duck typing! but this is one of the things that annoys me about the current way lazy binding works) or decide that backwards-compatibility or whatever is more important and ignore the issue.

You argue that throwing out checking is fine, i.e., you are arguing for the second goal, and against the first. But others disagree.

  • Either we think "real" modules are an improvement, and checking is important. Then the model and the syntax should be consistent about that. Moreover, checking needs to consistently apply, no matter how a module and its components are defined or accessed.

  • Or we come to the conclusion that supporting the legacy singleton export model as a primary use case is a mandatory matter. Then we should drop attempts of building something in/semi-compatible, and limit innovation to making export and import declarative, and designing a loader API.

Again, I think it's the "either/or" mindset which is the problem here. We can actually have both. Sometimes we won't be able to do strict checking. Sometimes we will. We don't need to force everyone into the same mold. Let the author of the module decide how important static checking, lazy binding, ease-of-use, etc, is and define their module appropriately. (And the same for the "importer of the module".)

Quite honestly, I rather have no import checking than checking that only applies in some syntactic cases -- because that gives a false sense of security, harms refactoring, and/or encourages overuse of unqualified imports, hampering readability.

Again: is this what's hampering compromise? Can't we let the module author (and/or importer) decide the extent to which they think checking is important?

No, the importers have no choice, they have to go with whatever the module provider picked. You cannot turn on checking on the use site alone, because it will lack the necessary static information from the definition site. Only "real" modules can provide that information, because only their definitions are sufficiently structured. That is fundamentally so. The hybrid, best of both worlds approach you seem to have in mind is technically impossible, unfortunately.

# C. Scott Ananian (10 years ago)

On Fri, Jun 27, 2014 at 3:17 PM, Andreas Rossberg <rossberg at google.com>

wrote:

I think you are missing the central problem. If imports/exports are to be statically checked, then module bindings, their exports, and their uses have to be statically recognisable and analysable. That prohibits intermixing "proper" modules with other random objects.

No, it means that your checker will only work with 'proper' modules. The linter can easily have a white/blacklist to handle oddball cases.

You argue that throwing out checking is fine, i.e., you are arguing

for the second goal, and against the first. But others disagree.

No. I believe that allowing the user (or module author) to selectively ignore the checking (for example, by using a default import of an object which is not a module). You seem to be missing my fundamental point: this is not a dichotomy. You can check many imports, even if you can't check all. If the module author chooses to export a function instead of an object, you can't check the imports of that one module. Use a different module if that bothers you!

No, the importers have no choice, they have to go with whatever the

module provider picked.

The importers have the choice to use a different module (or fork the module, or use a transpiler, or...).

The module author has the choice to re/structure his module to allow lazy binding/checking iff the users demand.

The hybrid, best of both worlds approach you seem to have in mind is technically impossible, unfortunately.

I look forward to a technical proof then. You have not yet provided that.

# Andreas Rossberg (10 years ago)

On 27 June 2014 17:32, Kevin Smith <zenparsing at gmail.com> wrote:

So to me the path forward is clear: we keep real modules, axe the default feature, and take a temporary hit of dissatisfaction from existing users so that we can expand the JS user base.

Note that the other half of my argument was that "real" modules are only worth the complexity when they provide checking consistently. That is, it shouldn't matter whether I write

import {f, g, h} from "url" f(); g(); h()

or

module M from "url" M.f(); M.g(); M.h()

These should be freely interchangeable -- the programmer shouldn't need to pick between getting import checking but polluting the scope and making uses less readable, or the other way round. The current semantics falls short on that, it doesn't check in the latter case.

So from my perspective, that would need to be fixed as well. Which would be fairly easy.

# Kevin Smith (10 years ago)

import {f, g, h} from "url" f(); g(); h()

or

module M from "url" M.f(); M.g(); M.h()

These should be freely interchangeable -- the programmer shouldn't need to pick between getting import checking but polluting the scope and making uses less readable, or the other way round. The current semantics falls short on that, it doesn't check in the latter case.

I didn't even realize that was the case.

So from my perspective, that would need to be fixed as well. Which would be fairly easy.

I totally agree.

# Andreas Rossberg (10 years ago)

On 27 June 2014 21:26, C. Scott Ananian <ecmascript at cscott.net> wrote:

On Fri, Jun 27, 2014 at 3:17 PM, Andreas Rossberg <rossberg at google.com> wrote:

I think you are missing the central problem. If imports/exports are to be statically checked, then module bindings, their exports, and their uses have to be statically recognisable and analysable. That prohibits intermixing "proper" modules with other random objects.

No, it means that your checker will only work with 'proper' modules. The linter can easily have a white/blacklist to handle oddball cases.

You argue that throwing out checking is fine, i.e., you are arguing for the second goal, and against the first. But others disagree.

No. I believe that allowing the user (or module author) to selectively ignore the checking (for example, by using a default import of an object which is not a module). You seem to be missing my fundamental point: this is not a dichotomy. You can check many imports, even if you can't check all. If the module author chooses to export a function instead of an object, you can't check the imports of that one module. Use a different module if that bothers you!

No, the importers have no choice, they have to go with whatever the module provider picked.

The importers have the choice to use a different module (or fork the module, or use a transpiler, or...).

The module author has the choice to re/structure his module to allow lazy binding/checking iff the users demand.

All this means is that there will effectively be two different module systems, and in practice, every module provider has to pick one. Which is a problem, not a solution.

The hybrid, best of both worlds approach you seem to have in mind is technically impossible, unfortunately.

I look forward to a technical proof then. You have not yet provided that.

Well, I don't know how it could be done. I think the proof obligation is on those who claim it's possible. ;)

# C. Scott Ananian (10 years ago)

On Fri, Jun 27, 2014 at 3:34 PM, Andreas Rossberg <rossberg at google.com>

wrote:

All this means is that there will effectively be two different module systems, and in practice, every module provider has to pick one. Which is a problem, not a solution.

...this is why I've been arguing strongly for consistent syntax regardless. Static checking and lazy binding should be "value added features", not something I have to think about every time I import a module.

The hybrid, best of both worlds approach you seem to

have in mind is technically impossible, unfortunately.

I look forward to a technical proof then. You have not yet provided that.

Well, I don't know how it could be done. I think the proof obligation is on those who claim it's possible. ;)

jslint/jshint is already a (hacky) proof for the most common cases. It detects undeclared variables. All you need to do is write a loader which will prepopulate the jshint's 'predef' field appropriately.

More complicated sorts of static checking fall to Turing completeness. Ie:

import foo from "foo";

var bat = (Math.random() & 1) ? foo : { };

console.log(bat.baz); // is this a static error?

The reasonable thing is to accept incompleteness and provide static checking of the common cases: barewords and <module name>.<identifier>

productions. Both of those tests are quite capable of skipping tests if <module name> is not a "pure" module.

# Kevin Smith (10 years ago)

The module author has the choice to re/structure his module to allow lazy binding/checking iff the users demand.

All this means is that there will effectively be two different module systems, and in practice, every module provider has to pick one. Which is a problem, not a solution.

Right - and this piles complexity on top of complexity, from both the user's point of view and the implementer's/analyzer's point of view. If you're going to demand such complexity, then you at least have to back it up with some real, concrete advantages. What are they? Bullet points are good. : )

Just so we agree what we're talking about, this is the default-default solution, right?

# Marius Gundersen (10 years ago)

On 27 Jun 2014 21:29, "Andreas Rossberg" <rossberg at google.com> wrote:

Note that the other half of my argument was that "real" modules are only worth the complexity when they provide checking consistently. That is, it shouldn't matter whether I write

import {f, g, h} from "url" f(); g(); h()

or

module M from "url" M.f(); M.g(); M.h()

These should be freely interchangeable -- the programmer shouldn't need to pick between getting import checking but polluting the scope and making uses less readable, or the other way round. The current semantics falls short on that, it doesn't check in the latter case.

So from my perspective, that would need to be fixed as well. Which would be fairly easy.

/Andreas

And this is yet another counter argument to the default export. If the default export is an object with properties (or a function with properties, which I quite common in npm) then there is no static checking of those properties. So with today's spec there is one way to get static checking and one ways (default) to not get static checking.

Marius Gundersen

# Brian Di Palma (10 years ago)

Andreas, I hope you're not just getting my hopes up with the possibility of checking ModuleImports too. That would be great. I was disappointed that they weren't originally.

Making

module fs from 'fs';

fs.readFile(...);

and

import {readFile} from 'fs';

equivalent in terms of static checks is the right approach.

If the cost of these improvements is not having default exports it seems acceptable. In essence we would have to write

import {jquery} from 'jquery';

instead of

import jquery from 'jquery';

The "flexibility" of not having to know the export identifier is worthless to me. I can't see how I could make use of a module without reading some documentation on it, which will show me the identifiers.

Static checking and lazy binding should be "value added features", not something I have to think about every time I import a module.

I don't understand how you can make use of what you require without reading some sort of documentation or code. This will be an even smaller "cost" once tooling supports ES6 modules as it will allow autocompletion of imports statements.

# Isiah Meadows (10 years ago)

(I get the digest...)

First, I will say that you all beat me to my (almost) exact suggestion on the syntax, @Russell and @Scott.

Second, module foo from 'foo' is counterintuitive, confusing, non-obvious, and really needs trashed IMHO.

Now, to supplement these ideas, I will summarize how I think it may work best:

// import default export(s)

import 'lo-dash' as _; import 'fs' as fs;

// import specific named export // imports as $.extend

import extend in $ from 'jquery';

// import multiple named exports // imports as _, with members .map and .each

import map, each in _ from 'underscore';

// import members of an export // imports those members into the module namespace

import map, each from 'underscore';

// import all named exports as properties of _ import * in _ from 'lo-dash';

// import all named exports into module namespace

import * from 'lo-dash';

// import as different name in current module

import reallyReallyLongFunctionName as foo from './foo';

// import as different property name // likely atypical, would show up as something most wouldn't think // of a while down the road

import reallyReallyLongFunctionName as func in foo from './foo';

The keyword "in" is used to both note that they are properties in the module. It is not "as" to limit confusion: "as" is better to dictate names. The basic syntax grammar could be defined as follows (please pardon my inability to format...I'm typing this from a phone):

ModuleName: "module-path"

ModuleName: 'module-path'

ImportStatement: One of: DefaultImportStatement NamedImportStatement

DefaultImportStatement: import ModuleName as Identifier;

NamedImportStatement: import NamedImports from ModuleName;

NamedImports: One of: NamedIdentifiers NamedImport

NamedImport: NamedIdentifiers in Identifier

NamedIdentifiers: NamedIdentifier , NamedIdentifiers

NamedIdenifiers: NamedIdentifier

NamedIdentifier: One of: Identifier Identifier as Identifier

This is my proposed syntax. It is somewhat Pythonic, but slightly more verbose because file names aren't themselves restricted like identifiers.

Here's a couple more real world examples:

import 'jquery' as $; $('#button').attr('onClick', () => alert('You clicked the button!'));

// ========================== //

import readdir, stat from 'fs'; import 'path' as path;

function walk(dir, cb) { readdir(path.resolve(dir), (error, files) => files.forEach(file => { let file = dir + path.sep + file; let fileStat = stat(file); if (fileStat && fileStat.isDirectory()) { walk(file, cb); } else { cb(file); } })); }

Here's the version more popular out of the other suggestions:

import {$} from 'jquery'; $('#button').attr('onClick', () => alert('You clicked the button!'));

// ========================== //

import {readdir, stat} from 'fs'; import path from 'path';

function walk(dir, cb) { readdir(path.resolve(dir), (error, files) => files.forEach(file => { let file = dir + path.sep + file; let fileStat = stat(file); if (fileStat && fileStat.isDirectory()) { walk(file, cb); } else { cb(file); } }));

Which looks better? Which is more obvious and intuitive?

On another note, look at the bright side and compare these with the ES5 equivalents:

define(['jquery'], function ($) { $('#button').attr('onClick', function () { alert('You clicked the button!'); }); });

// ========================== //

var fs = require('fs'); var path = require('path');

function walk(dir, cb) { fs.readdir(path.resolve(dir), function (error, files) { files.forEach(function (file) { var file = dir + path.sep + file; var stat = fs.stat(file); if (stat && stat.isDirectory()) { walk(file, cb); } else { cb(file); } }); }); }

I think we've made some relatively good progress so far. That last is just unnecessarily complicated in syntax.

<aside>

Is it me, or is ECMAScript starting to become Python crossed with a curly-brace version of Scheme? I'm starting to see a lot of new functionally oriented syntax and methods being added to ES6 and considered for ES7, while at the same time Pythonic OOP and reflection being considered simultaneously. The arrow functions are effectively lambdas that can be complete functions in their own right.

I wonder how long it will take for people will find out the ease of currying and more availability of functional techniques in ES6. var foo = (a, b) => (c) => a * b + c;

var adder = a => b => a + b;

var addOne = adder(1); var two = addOne(1); </aside>

---------- Forwarded message ---------- From: Russell Leggett <russell.leggett at gmail.com> To: Kevin Smith <zenparsing at gmail.com> Cc: es-discuss <es-discuss at mozilla.org> Date: Thu, 26 Jun 2014 10:50:20 -0400 Subject: Re: ModuleImport

Now the author can choose to export more things later without making

breaking changes to the module. The only downside to this is the (apparently mandatory) curly braces around the imported object. If single export/import becomes the convention with ES6 modules then users will be forced to type an extra pair of {} several times in most of their files. Is the two extra characters something we can live with?

I have a good bit of experience coding ES modules, and I was worried

about that at first. But hasn't been a problem for me. Then again, I'm just one person - it would be good to get more data from developers actually coding with ES modules.

This syntax would make things completely obvious and simple though: if

there's curly braces, then you're reaching into the bag and pulling out things, and if there's no curly braces, you're getting the bag itself. There's even a visual analogy at play here: the curly braces themselves resemble the sides of a bag.

To me, though, that goes back to the destructuring/not destructuring

aspect. Maybe this has floated by during the bikeshedding, but why not something like:

//import a single named export
import foo from "bar";

//import multiple named exports
import foo, baz from "bar";

//alias an imported named export
import foo as fooAlias from "bar";

//import the module
import "bar" as bar;

So basically, just get rid of the {} for importing named exports, and

move the whole module import after the as. It reads better to me and I think is more intuitive. As for default exports - I think they only make sense if done the way it works in node. A single default export that effectively replaces the module. Let's remember how it gets used in node:

var _ = require("underscore");

In node, if they didn't do it as a single export, then you would have to

do:

var _ = require("underscore")._;

Given that underscore (and jquery and several others) are only a single

export, it would be annoying and error prone to do that all over the place. The value added was for the importer - not the exporter. With the new syntax I'm proposing, importing underscore would be exactly the same as what the current proposal is for default export imports.

import _ from "underscore";

Its just that underscore would have to have the named export _. As I

said, though, that's not really a burden, and the argument for default exports was never really for the module writer.

There is one small case I'm missing, which is that default export imports

let you name the import whatever you want without writing extra code for the alias. I don't really find that as a drawback, but then again, I do write a lot of Java in addition to JavaScript. I don't really see any other languages that operate this way either, and I would come back again to the distinct history and constraints that JS/node has had up until now. If keep that flexibility/matching semantics for smoothest continuity, I would propose that default exports have to be a single default export, and that it would replace module importing instead of export importing.

import "underscore" as _;

Where there is a single default export in the underscore module. This

would mean that the normal module import semantics don't work in this case, but that is the tradeoff decided by the module author. I personally don't think its worth adding this feature, but if it were added, I would expect it to work this way.

---------- Forwarded message ---------- From: "C. Scott Ananian" <ecmascript at cscott.net> To: Russell Leggett <russell.leggett at gmail.com> Cc: es-discuss <es-discuss at mozilla.org> Date: Thu, 26 Jun 2014 11:56:23 -0400 Subject: Re: ModuleImport On Thu, Jun 26, 2014 at 10:50 AM, Russell Leggett <

russell.leggett at gmail.com> wrote:

To me, though, that goes back to the destructuring/not destructuring

aspect. Maybe this has floated by during the bikeshedding, but why not something like:

//import a single named export
import foo from "bar";

//import multiple named exports
import foo, baz from "bar";

//alias an imported named export
import foo as fooAlias from "bar";

//import the module
import "bar" as bar;

I like the fact that this doesn't look like destructuring, since variable

binding is different from destructuring assignment. Could

import foo, baz from "bar" as bar;

allowed simultanous import of named exports and the module itself? If

so, the grammar gains a pleasing regularity.

OTOH, I think this syntax reorganization is orthogonal to some of the

other issues discussed. In particular, your latter proposal still allows for user confusion between:

import _ from "underscore";
import "underscore" as _;

The various proposals seem to try to address this by making these

semantically identical (or near-identical, with some cooperation from the module author).

# Isiah Meadows (10 years ago)

I fully admit that I'm probably hanging a little behind in terms of emails because I get them bundled into 4-5 each.

# Bruno Jouhier (10 years ago)

Why make things so complex and introduce so many syntax variations? The following should be sufficient:

import "underscore" as _; // var _ = require('underscore');

And there should be an API (not a language construct) to import a module dynamically (and asynchronously). Something like:

promise = importModule(path);

Why introduce an export syntax? Why not just use this (or exports)?

this.foo = ... // or exports.foo = ... this = .... // or exports = ..., similar to module.exports = ... // importer obtains value of this / exports at end of module file, // as if return this was added at the end of the file.

This would give the same power / flexibility as CommonJS and a simple compatibility path. This would eliminate the "default" exports problem because it would allow a module to export a function. It is also easy to explain.

There is an operational requirement to have a special syntax for import because this is what allows loaders to build a dependency graph and optimize dependency loading. But there is no "operational" requirement to have a special syntax for export. Static checking on exported members feels odd.

Bruno

# Kevin Smith (10 years ago)

This would give the same power / flexibility as CommonJS and a simple compatibility path. This would eliminate the "default" exports problem because it would allow a module to export a function. It is also easy to explain.

So this is basically the "sugar over CommonJS modules" solution. But why bother? "require" already does all of this and there already exist tools to generate dependency graphs from "require". No syntax is required.

Static checking on exported members feels odd.

Static checking of imports and exports has well-known advantages and would help the long-term viability of the language. Segments that want static checking to be a part of their workflow might very well just leave JS for some other language that provides it.

# John Barton (10 years ago)

On Sat, Jun 28, 2014 at 9:03 AM, Kevin Smith <zenparsing at gmail.com> wrote:

Static checking on exported members feels odd.

Static checking of imports and exports has well-known advantages and would help the long-term viability of the language.

Enumerating these specific advantages would inform this discussion. These advantages are not well-known. Many developers have experienced the disadvantages of complex systems of rules and thus favor simple solutions over ones with theoretical advantages. Explaining the benefits concretely would help them balance the well-known costs.

jjb

# Matthew Robb (10 years ago)

What if the Loader spec had some attention given to match AMD/CommonJS for some cases and leave the new syntax for the new module semantics. Really what we want is for non-es6 module systems to be able to hook into the loader registry in a way that makes sense for them and will also make sense for IMPORTING those modules into es6 modules. require/define work in browsers and in node TODAY, the conversation imo shouldn't be about giving those systems better syntax it should be about creating a single registry/loader that easily supports all paths.

  • Matthew Robb
# Kevin Smith (10 years ago)

Static checking of imports and exports has well-known advantages and would help the long-term viability of the language.

Enumerating these specific advantages would inform this discussion. These advantages are not well-known. Many developers have experienced the disadvantages of complex systems of rules and thus favor simple solutions over ones with theoretical advantages. Explaining the benefits concretely would help them balance the well-known costs.

So pretty much everything in Javascript is dynamic, which is one reason why IDE support for Javascript has always lagged behind. You simply can't know what anything is until you actually run the program. Statically verifiable exports gives us the ability to inspect and analyze code without having to run it. There are two big benefits that this affords us:

Early Errors and Warnings

Let's say that you want to deprecate and remove an exported member from a module within a large JS code base. With static imports, the system will generate an error at compile time if something on the other side of that codebase is importing it.

For exported function declarations that use default parameters to indicate optional parameters, we can generate build-time warnings when such an function is imported and called with too few arguments.

For exported classes, we have even more static information at our hands. Without having to run the program, we know the number of arguments for the constructor and we know the list of methods for class instances. We can generate warnings when we see an instance using a misspelled method name, for instance.

Computer Aided Coding

The information used above to generate lint-like warnings can also be used to give the developer in-editor feedback. Reliable code-completion for imported function and class declarations becomes possible. Again, for exported classes we can also do code completion for instance methods.

These advantages may not seem like a big deal now, but imagine writing JS in a large team five years from now. Do you want the power of static analysis at your team's fingertips, or do you want to be stuck with "anything goes so anything can break" CommonJS modules?

Does that do it?

# Bruno Jouhier (10 years ago)

Static checking will be limited anyway. If you want to go this way you shoud use typescript.

Big projects are perfectly manageable with CommonJS from my experience with a 20+ team. The trick is to enforce code reviews and unit tests.

CommonJS falls a bit short on the import side because static analysis of require calls is brittle. A special language syntax would enable a robust static analysis of dependencies.

But the export side of CommonJS is basically ok. JS APIs are dynamic and there is little value in introducing a shallow and leaky static API verification at the module interface.

Two things should not be overlooked in the design:

  • ability to dynamically import modules in addition to static imports. IMO this should be packaged as an async API

  • hooks for transpilers. This should also be an API

Bruno

# Angel Java Lopez (10 years ago)

Maybe, I cannot see all the landscape, but a minor comment, in my "limited" English

Related to:

Early Errors and Warnings

Usually, I obtain the same benefit running the tests (and more, test that were the product of TDD workflow). In this way, I'm sure not only of no removal of something I needed, but also the underlying behavior of imported modules are still the same. Relaying on static imports only warns me about the presence or not of some functions, but the app could be broken

Angel "Java" Lopez @ajlopez

# Kevin Smith (10 years ago)

Static checking will be limited anyway. If you want to go this way you should use typescript.

That's the point that I'm trying to make, shops will choose other languages that provide more static information. We should be thinking about expanding the user base and ensuring that JS is a viable option years down the road.

CommonJS falls a bit short on the import side because static analysis of require calls is brittle. A special language syntax would enable a robust static analysis of dependencies.

If you don't have static exports, then how are you going to know if what you import is valid? You can't, without executing the program.

# Kevin Smith (10 years ago)

Usually, I obtain the same benefit running the tests (and more, test that were the product of TDD workflow). In this way, I'm sure not only of no removal of something I needed, but also the underlying behavior of imported modules are still the same. Relaying on static imports only warns me about the presence or not of some functions, but the app could be

Of course, there's no substitute for unit tests with good coverage. But the question is, what kind of help does that language provide for static analysis? Currently, JS provides almost none.

# Isiah Meadows (10 years ago)

I think that a possible compromise that can still make the ES6 module system more compatible with both AMD and CommonJS modules is by this:

If there are no exports from a module, named or not, make the export process implementation-defined. If an ES5 Node module uses module.exports, then Node could configure the exports to be importable through the ES6 syntax. Some people use named exports, while others have even exported a constructor via module.exports. In the browser environment, it could be something along the lines of using the added Window properties as named exports.

# John Barton (10 years ago)

Thanks Kevin. I'm going to challenge your list below, but I hope you don't take it negatively. I want the case for the module system to be as strong as possible.

On Sat, Jun 28, 2014 at 11:51 AM, Kevin Smith <zenparsing at gmail.com> wrote:

Static checking of imports and exports has well-known advantages and would help the long-term viability of the language.

Enumerating these specific advantages would inform this discussion. These advantages are not well-known. Many developers have experienced the disadvantages of complex systems of rules and thus favor simple solutions over ones with theoretical advantages. Explaining the benefits concretely would help them balance the well-known costs.

So pretty much everything in Javascript is dynamic, which is one reason why IDE support for Javascript has always lagged behind. You simply can't know what anything is until you actually run the program. Statically verifiable exports gives us the ability to inspect and analyze code without having to run it. There are two big benefits that this affords us:

Early Errors and Warnings

Let's say that you want to deprecate and remove an exported member from a module within a large JS code base. With static imports, the system will generate an error at compile time if something on the other side of that codebase is importing it.

It seems to me that the compiler can verify these two statements with equal success: import {foo} from './foo'; var foo = require('./foo.js').foo; I agree that as a practical matter compilers may be more likely to implement checks on the first form simply because it is standard. And I agree that the language-defined module syntax will lead to better quality tools simply because more developers have formal training in compiler technology than have training in dynamic analysis. These are important pragmatic issues.

Or am I wrong and these are not equivalent? Or there are examples which show the issue more clearly?

For exported function declarations that use default parameters to indicate optional parameters, we can generate build-time warnings when such an function is imported and called with too few arguments.

A good argument for default parameters.

For exported classes, we have even more static information at our hands. Without having to run the program, we know the number of arguments for the constructor and we know the list of methods for class instances. We can generate warnings when we see an instance using a misspelled method name, for instance.

A good argument for standard rather than ad-hoc class class syntax.

Computer Aided Coding

The information used above to generate lint-like warnings can also be used to give the developer in-editor feedback. Reliable code-completion for imported function and class declarations becomes possible. Again, for exported classes we can also do code completion for instance methods.

These advantages may not seem like a big deal now, but imagine writing JS in a large team five years from now. Do you want the power of static analysis at your team's fingertips, or do you want to be stuck with "anything goes so anything can break" CommonJS modules?

Does that do it?

These are arguments for statically computable import module names -- so the compiler can determine the imports without executing source code -- and for top-level or hoisted (static) import statements -- so every import in a module is a dependency of the module without runtime conditionals. I believe that both of these limitations would be acceptable to almost all developers interested in using modules.

Unfortunately I think we need more specific and detailed examples to understand the advantages of the static form. Thanks, jjb

# John Barton (10 years ago)

On Sat, Jun 28, 2014 at 3:58 PM, Kevin Smith <zenparsing at gmail.com> wrote:

Static checking will be limited anyway. If you want to go this way you

should use typescript.

That's the point that I'm trying to make, shops will choose other languages that provide more static information. We should be thinking about expanding the user base and ensuring that JS is a viable option years down the road.

JavaScript's enormous user base is the strongest possible evidence that static analysis provides no advantage to programming language viability. Static analysis may encourage some new users; overall complexity may discourage as many. (I recently started using a typed version of JS; I am not impressed.)

Any survey of the top languages in actual use clear demonstrates that the runtime platform and app goals dominate language choice. Even within a platform it is clear static checks are way down the list of valued features.

Rather than point towards type-checking, I think we should focus on the actual checks offered by the module design. It seems that these would come with a small cost quite unlike type-checking.

CommonJS falls a bit short on the import side because static analysis of require calls is brittle. A special language syntax would enable a robust static analysis of dependencies.

If you don't have static exports, then how are you going to know if what you import is valid? You can't, without executing the program.

If you don't execute the program, how do you know if the code you are checking is even called? Oh, you do plan to execute the program. Well there you go.

(I just think it is so weird that JavaScript's huge advantage of rapid dynamic feedback for developers receives so little attention while so much is lavished on static technologies developed decades ago for a computing environment vastly inferior to our current world.)

jjb

# Rick Waldron (10 years ago)

On Sun, Jun 29, 2014 at 12:25 PM, John Barton <johnjbarton at google.com>

wrote:

On Sat, Jun 28, 2014 at 3:58 PM, Kevin Smith <zenparsing at gmail.com> wrote:

Static checking will be limited anyway. If you want to go this way you

should use typescript.

That's the point that I'm trying to make, shops will choose other languages that provide more static information. We should be thinking about expanding the user base and ensuring that JS is a viable option years down the road.

JavaScript's enormous user base is the strongest possible evidence that static analysis provides no advantage to programming language viability. Static analysis may encourage some new users; overall complexity may discourage as many. (I recently started using a typed version of JS; I am not impressed.)

Any survey of the top languages in actual use clear demonstrates that the runtime platform and app goals dominate language choice. Even within a platform it is clear static checks are way down the list of valued features.

Rather than point towards type-checking, I think we should focus on the actual checks offered by the module design. It seems that these would come with a small cost quite unlike type-checking.

Static analysis would be necessary if JavaScript ever wanted to make macros possible in modules. I don't have exact numbers nor have I done any formal surveys, but the general response to Sweet.js has been overwhelmingly positive. It would be a shame to close that door.

# Domenic Denicola (10 years ago)

On Jun 29, 2014, at 12:50, "Rick Waldron" <waldron.rick at gmail.com> wrote:

Static analysis would be necessary if JavaScript ever wanted to make macros possible in modules. I don't have exact numbers nor have I done any formal surveys, but the general response to Sweet.js has been overwhelmingly positive. It would be a shame to close that door.

My understanding is that door is already closed, since we were not able to eliminate the global scope contour from modules (which is necessary to get completely-static knowledge of all free identifiers). Part of the rationalization for this was that compile-time tools (like Sweet.js) are working out pretty well.

See "Back to Static Checking" in esdiscuss.org/notes/2013

# Bruno Jouhier (10 years ago)

2014-06-29 0:58 GMT+02:00 Kevin Smith <zenparsing at gmail.com>:

# Bruno Jouhier (10 years ago)

CommonJS falls a bit short on the import side because static analysis of

require calls is brittle. A special language syntax would enable a robust static analysis of dependencies.

If you don't have static exports, then how are you going to know if what you import is valid? You can't, without executing the program.

The main purpose of modules is not to provide static type checking but to prevent global scope pollution and allow loaders to load source code reliably and efficiently. What I meant by "static analysis" was the ability for a loader to bundle all the dependencies so that they can be transported efficiently. With require this analysis is brittle because require is not a reserved keyword and its argument can be any expression.

It is important to focus the design on loader issues and keep things orthogonal.

Why reinvent a special destructuring syntax when this is already addressed by existing language constructs? It can be handled by allowing any LHS after the as keyword: import "module_path" as lhs_expression;

Why bother about about static type checking? If you want static typing, use Typescript, your modules will be exporting typed APIs; if you don't care use JavaScript.

Sorry for the previous empty post. I hit the wrong key.

# Brian Di Palma (10 years ago)

Static checking will be limited anyway. If you want to go this way you shoud use typescript.

If you don't want static checking you should stick with ES3. Fixed that for you.

Yes big projects are possible with JS, I work on them everyday. It would be nice if the language made them easier, that's what we are talking about. Big projects are possible with C, why bother with any other language? With sufficient rigor Assembly will do the trick. Just have good code reviews and test.

ability to dynamically import modules in addition to static imports. IMO this should be packaged as an async API

I think this is already possible using the loader.

hooks for transpilers. This should also be an API

Ditto.

Usually, I obtain the same benefit running the tests (and more, test that were the product of TDD workflow)

I can forsee many people writing tests where they configure the module loader to load mocks instead of true dependencies. This could result in tests passing while there is a breakage in the application.

If the language can make some of these problems go away then we are better off. It's just like tests, code review and linting, it's another type of verification.

I think that a possible compromise that can still make the ES6 module system more compatible with both AMD and CommonJS modules

The ES6 module system is compatible with CommonJS and AMD, I'm happily mixing the two together with libraries like SystemJS

systemjs/systemjs

For example

briandipalma/flux-es6/blob/master/src/Store.js

The Emitr class here is imported from a CommonJS module

It is important to focus the design on loader issues and keep things orthogonal.

The Loader is quite solid and well designed, I've not heard any major issues with it. What does it have to do with this discussion?

import {foo} from './foo'; var foo = require('./foo.js').foo;

I though those two statements weren't comparable? The import statement can only be present at the module top level while the require can be written in any code block. That leaves it open to the random number require issues which makes static checking impossible. As you pointed out it's not a language standard so I'd imagine that's another reason why tooling is so weak.

Rather than point towards type-checking, I think we should focus on the actual checks offered by the module design. It seems that these would come with a small cost quite unlike type-checking.

Unless I'm misunderstanding Kevin I think we've both talking about exactly that. I guess people saw "static" and automatically added "type". Just static checking of import and export bindings. A much smaller scope feature then static type checking.

This discussion has veered off track, it's about a new ModuleImport form, which grew into questioning if default imports/exports was the real problem. I'd be interested in knowing if it's possible that instead of changing the ModuleImport form the default import/export idea could be postponed instead.

It could be added in later if there really was need for it. As far as I can tell libraries like SystemJS can smooth over issues caused by importing from legacy module systems like CommonJS and AMD. Leaving default imports/exports an odd third way to import that was added based on some notion of backward compatability that wasn't needed.

# Kevin Smith (10 years ago)

Bruno and John's arguments are classic examples of the straw man fallacy. In my concrete examples I made no reference to static type systems (or any type systems at all, for that matter). I merely pointed out that by allowing the programmer to provide compile-time information in the form of exports and declarative forms, a world of possibilities opens up.

Of course, static information can always be inferred from dynamic. That's basically how JS engines work, but raising that up to some ideal principle is foolish dogmatism.

They accuse me of advocating decades-old technology, but it is purely dynamic JS that is decades old. "Evolve or die" is the way. The "we don't need no stinkin' classes" argument is counter-productive, counter-intuitive reactionary garbage, and quite frankly it bores me.

: P

# Kevin Smith (10 years ago)

I'd be interested in knowing if it's possible that instead of changing the ModuleImport form the default import/export idea could be postponed instead.

It can. Just to be clear, that means that we'd have these two forms on the import side:

module FS from "fs";
import { stat } from "fs";

which would be cool with me.

I think, though, that the default thing has syntactically been raised up to an architectural level, and that's the core problem that Andreas has pointed out. Once you make the architectural decision to drop it, then you'll probably never want it.

# Andreas Rossberg (10 years ago)

On 27 June 2014 21:45, C. Scott Ananian <ecmascript at cscott.net> wrote:

On Fri, Jun 27, 2014 at 3:34 PM, Andreas Rossberg <rossberg at google.com> wrote:

All this means is that there will effectively be two different module systems, and in practice, every module provider has to pick one. Which is a problem, not a solution.

...this is why I've been arguing strongly for consistent syntax regardless. Static checking and lazy binding should be "value added features", not something I have to think about every time I import a module.

Painting over semantic differences by using the same syntax is not an improvement in usability. It decreases readability, and increases confusion and surprise. You probably weren't around when this was discussed last time, but the differences between importing from a "proper" module and importing from a singleton export object are not just in static checking. They create different bindings. The former creates an alias for the variable from the module. The latter would need to destructure the property into a new variable, i.e., would need to copy its state. This leads to visibly different results when the variable is mutated (internally or externally).

Please assume that all these ideas have been discussed extensively, and carefully considered, in particular by Dave and Sam. And while I do not agree with all conclusions, the current design has gotten much more thought in it than many in this thread seem to assume. In particular, none of the suggestions made in this thread so far are new.

The hybrid, best of both worlds approach you seem to have in mind is technically impossible, unfortunately.

I look forward to a technical proof then. You have not yet provided that.

Well, I don't know how it could be done. I think the proof obligation is on those who claim it's possible. ;)

jslint/jshint is already a (hacky) proof for the most common cases. It detects undeclared variables. All you need to do is write a loader which will prepopulate the jshint's 'predef' field appropriately.

More complicated sorts of static checking fall to Turing completeness. Ie:

import foo from "foo";

var bat = (Math.random() & 1) ? foo : { };

console.log(bat.baz); // is this a static error?

The reasonable thing is to accept incompleteness and provide static checking of the common cases: barewords and <module name>.<identifier> productions. Both of those tests are quite capable of skipping tests if <module name> is not a "pure" module.

And that is exactly why "real" ES6 modules are more structured. They are carefully crafted such that checking of imports/exports is always possible. And as I said earlier, any attempt to make them interchangeable with legacy-style modules would break this property.

We can decide that this checking is not worth it and drop it. But we cannot have our cake and eat it too.

# John Barton (10 years ago)

Let me try to restate my request for clear information on the advantages of module bindings vs module object architectures.

The import syntax already allows pre-execution tools to collect every module that will be accessed by a root dependent module (including some that may not be used). This advantage is clear and it can be weighed against the disadvantages, including more syntax restrictions to learn and the need for a separate dynamic API. On balance I think it's a win, because I understand the advantage.

What's not clear is the advantage of module bindings form for modules. When the world of possibilities opens up, what specific things will I see there?

# Rick Waldron (10 years ago)

On Sun, Jun 29, 2014 at 1:00 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:

On Jun 29, 2014, at 12:50, "Rick Waldron" <waldron.rick at gmail.com> wrote:

Static analysis would be necessary if JavaScript ever wanted to make macros possible in modules. I don't have exact numbers nor have I done any formal surveys, but the general response to Sweet.js has been overwhelmingly positive. It would be a shame to close that door.

My understanding is that door is already closed, since we were not able to eliminate the global scope contour from modules (which is necessary to get completely-static knowledge of all free identifiers). Part of the rationalization for this was that compile-time tools (like Sweet.js) are working out pretty well.

See "Back to Static Checking" in esdiscuss.org/notes/2013-09-18

Yes, it appears I forgot about this. Thanks for the follow up.

# C. Scott Ananian (10 years ago)

On Mon, Jun 30, 2014 at 7:14 AM, Andreas Rossberg <rossberg at google.com>

wrote:

...this is why I've been arguing strongly for consistent syntax regardless. Static checking and lazy binding should be "value added features", not something I have to think about every time I import a module.

Painting over semantic differences by using the same syntax is not an improvement in usability. It decreases readability, and increases confusion and surprise. You probably weren't around when this was discussed last time, but the differences between importing from a "proper" module and importing from a singleton export object are not just in static checking. They create different bindings. The former creates an alias for the variable from the module. The latter would need to destructure the property into a new variable, i.e., would need to copy its state. This leads to visibly different results when the variable is mutated (internally or externally).

Yes, I am aware of that. I also claim that in most module code this difference is insignificant. That is, it continues to be best practice to avoid creating circular dependencies in modules. Further, self-contained modules will have no need to export lazy-bound references, even if they use them internally.

I have been around for these discussions. And my conclusion is that the dogmatists are getting in the way. Just because there are some differences between modules and objects doesn't mean that all users must be confronted with them. Just because some modules may be defined in a way to prevents robust checking of modules doesn't mean that all modules must be prevented from doing so.

Please assume that all these ideas have been discussed extensively, and carefully considered, in particular by Dave and Sam. And while I do not agree with all conclusions, the current design has gotten much more thought in it than many in this thread seem to assume. In particular, none of the suggestions made in this thread so far are new.

I agree that the discussion has gone in many circles. But it is also clear that the consensus is fragile and the compromises not well-liked. I don't know of a better way to make continued progress other than (a) proposing tweaks to the existing compromise that might enhance its appeal, or (b) continuing to discuss the trade-offs made in an effort to solidify the consensus.

We can decide that this checking is not worth it and drop it. But we

cannot have our cake and eat it too.

Concretely I'm proposing that we drop only the "must" and "all" phrases with respect to checking. I think we can come up with a design where checking is "mostly" and "usually" possible. This will put the decision in the hands of the code authors, who can either decide to (a) gradually transition their legacy code bases to allow more checking, or (b) decide that the checking is not worth it for their particular code base.

# Andreas Rossberg (10 years ago)

On 30 June 2014 19:01, C. Scott Ananian <ecmascript at cscott.net> wrote:

On Mon, Jun 30, 2014 at 7:14 AM, Andreas Rossberg <rossberg at google.com> wrote:

...this is why I've been arguing strongly for consistent syntax regardless. Static checking and lazy binding should be "value added features", not something I have to think about every time I import a module.

Painting over semantic differences by using the same syntax is not an improvement in usability. It decreases readability, and increases confusion and surprise. You probably weren't around when this was discussed last time, but the differences between importing from a "proper" module and importing from a singleton export object are not just in static checking. They create different bindings. The former creates an alias for the variable from the module. The latter would need to destructure the property into a new variable, i.e., would need to copy its state. This leads to visibly different results when the variable is mutated (internally or externally).

Yes, I am aware of that. I also claim that in most module code this difference is insignificant. That is, it continues to be best practice to avoid creating circular dependencies in modules. Further, self-contained modules will have no need to export lazy-bound references, even if they use them internally.

I don't understand what you mean by "lazy-bound" references. In any case, the problem exists independent of circular module dependencies.

I have been around for these discussions. And my conclusion is that the dogmatists are getting in the way. Just because there are some differences between modules and objects doesn't mean that all users must be confronted with them. Just because some modules may be defined in a way to prevents robust checking of modules doesn't mean that all modules must be prevented from doing so.

I don't follow this line of reasoning. The language should define clearly what a module is, what you can expect from it, and it should do so consistently. Making it fuzzy and unreliable will just bite everybody, especially since modules will define the boundaries between code developed by different parties. If that's dogmatic then call me that.

# Kevin Smith (10 years ago)

What's not clear is the advantage of module bindings form for modules. When the world of possibilities opens up, what specific things will I see there?

I think I'm following you now. So let's pose the question like this:

Let's posit that there are no default export or import forms, otherwise the current syntax is the same. Now, we have two semantic options that we can attach to that syntax:

  1. Modules are just regular objects with getters. The import declarations just get properties from those objects at runtime.
  2. Modules are a collection of named bindings. Those bindings are resolved at compile-time.

Now, since the syntax is the same, our ability to do offline static analysis is the same. So what are the differences between these two semantics?

(All of this has been gone over before, of course...)

Advantages of 1:

  • None?

Advantages of 2:

  • Good support for circular dependencies
  • Mutable bindings
  • Compile-time optimizations for "module.member" expressions

The advantage of 1 only appears if we allow module object overwriting. Those advantages are:

  • Auto-renaming the import (questionable)
  • Smoother interoperability with CommonJS modules.

On the other hand, module object overwriting disables our ability to do offline static analysis.

So I think the only forceful argument if favor of option 1 (i.e. modules are regular objects with getters) is interoperability, and only if we allow export overwriting.

The interoperability question is therefore of central importance.

# Karolis Narkevičius (10 years ago)

It's not just about interoperability. It's also about enabling the pattern that proved itself to work quite well - module as a function, module as a class, module as a function with named exports attached in one "namespace".

I suppose if that pattern is not explicitly supported it might be just fine since perhaps we'll discover better patterns. And if not, we'll be able to just always use 1 named export and do

import {fs} from "fs";
import {moment} from "moment";
import {$} from "jquery";
// etc.

In fact, doesn't being able to import things like above make es6 modules already interoperable with CJS?

# C. Scott Ananian (10 years ago)

On Jun 30, 2014 3:59 PM, "Karolis Narkevičius" <karolis.n at gmail.com> wrote:

In fact, doesn't being able to import things like above make es6 modules

already interoperable with CJS?

Almost, but not quite, since the name of the module itself ($, fs, etc) is not included in a typical commonjs module. So it would really be:

import {_ as $} from "jquery";
// etc

And from here the jump to "default import" as a small syntactic improvement on this seemed clear. (At the time at least.)

# Kevin Smith (10 years ago)

It's not just about interoperability. It's also about enabling the pattern that proved itself to work quite well - module as a function, module as a class, module as a function with named exports attached in one "namespace".

But we have to ask why that pattern worked out, and my take is that the user experience with CJS modules (which exported just a single) was poor. ES6 modules don't have that problem.

I suppose if that pattern is not explicitly supported it might be just fine since perhaps we'll discover better patterns.

That's the idea.

And if not, we'll be able to just always use 1 named export and do

import {fs} from "fs";
import {moment} from "moment";
import {$} from "jquery";
// etc.

Right.

In fact, doesn't being able to import things like above make es6 modules already interoperable with CJS?

(Looks like Scott got here first...)

Mostly. If you have a Node module which does the overwrite thing then you might have to assign an arbitrary export name to import it:

import { exports as Emitr } from "emitr";

(Where "exports" is arbitrarily chosen by the module environment.)

But there are other issues which make the interoperability thing more complicated: modules have different syntax than scripts, and modules must be parsed in strict mode. So you need to know before you load the module, what kind of module it is (ES6 or CJS).

What we need is a complete interoperability story. I have some ideas which I'll write up when I get a chance.

# Calvin Metcalf (10 years ago)

Another interoperability issue is circular dependencies, es6 modules have support for certain kinds that cjs doesn't (like function declarations) and cjs has support for 2 modules using each others exports (as long as they arn't single exports).

If it wasn't for this you could load cjs modules with just normalize and translate hooks. At the moment you can likely get around the difference with an instantiate hook, but I haven't had time to check yet.

# John Barton (10 years ago)

On Mon, Jun 30, 2014 at 12:00 PM, Kevin Smith <zenparsing at gmail.com> wrote:

What's not clear is the advantage of module bindings form for modules. When the world of possibilities opens up, what specific things will I see there?

I think I'm following you now. So let's pose the question like this:

Let's posit that there are no default export or import forms, otherwise the current syntax is the same. Now, we have two semantic options that we can attach to that syntax:

  1. Modules are just regular objects with getters. The import declarations just get properties from those objects at runtime.
  2. Modules are a collection of named bindings. Those bindings are resolved at compile-time.

Now, since the syntax is the same, our ability to do offline static analysis is the same. So what are the differences between these two semantics?

Thanks! This is the kind of information I was hoping for.

(All of this has been gone over before, of course...)

Perhaps, but clear explanation of complex topics is difficult. Isolating the issues and getting to specifics helps. In that spirit I'll ask for more details.

Advantages of 1:

  • None?

Advantages of 2:

  • Good support for circular dependencies
  • Mutable bindings

As far as I know, this is not something JS developers understand. What is it and what makes it an advantage?

  • Compile-time optimizations for "module.member" expressions

Is this known to be significant and fundamental? As in there is no way to achieve this optimization with the other format? Seems to me that the compiler introduces the getters and thus can inline them.

The advantage of 1 only appears if we allow module object overwriting.

Concretely you mean monkey patching module objects in the importing code? Or?

Those advantages are:

  • Auto-renaming the import (questionable)
  • Smoother interoperability with CommonJS modules.

On the other hand, module object overwriting disables our ability to do offline static analysis.

So I think the only forceful argument if favor of option 1 (i.e. modules are regular objects with getters) is interoperability, and only if we allow export overwriting.

The interoperability question is therefore of central importance.

Based on other discussion on this point, it seems like module-as-named-bindings can interoperate with CJS and AMD except perhaps in some unusual cases. Clarifying these cases would help.

To me, these two lists are pretty darn small compared with the overall advantages of modules.

jjb

# C. Scott Ananian (10 years ago)

On Mon, Jun 30, 2014 at 4:32 PM, John Barton <johnjbarton at google.com> wrote:

Based on other discussion on this point, it seems like module-as-named-bindings can interoperate with CJS and AMD except perhaps in some unusual cases. Clarifying these cases would help.

To me, these two lists are pretty darn small compared with the overall advantages of modules.

Note that, although this particular thread has gotten hung up on default import/export, mutable bindings, and export, there are some other significant incompatibilities. Bundling is one such example. Integration with HTML fetch is another. I defer to @jrburke, @domenic, and @hixie for the details.

# Kevin Smith (10 years ago)
  • Mutable bindings

As far as I know, this is not something JS developers understand. What is it and what makes it an advantage?

Since imported bindings are just aliases for variables over in the originating module, changing the value of that variable in the exporting module will change value of the corresponding binding on the import side.

This is important for circular dependencies, because an imported binding needs to get updated when the initialization phase runs to completion.

Outside of circular dependencies, I don't know. I'm sure someone can find a good use case for it : )

  • Compile-time optimizations for "module.member" expressions

Is this known to be significant and fundamental? As in there is no way to achieve this optimization with the other format? Seems to me that the compiler introduces the getters and thus can inline them.

If there is no module object overwriting, then I suppose it could be inlined. If you allow overwriting, then you would need the further restriction that you cannot import from overridden module objects.

The advantage of 1 only appears if we allow module object overwriting.

Concretely you mean monkey patching module objects in the importing code? Or?

I mean something equivalent to module.exports = whatever, where the object stored in the registry was replaced by some arbitrary, user-specified object.

The interoperability question is therefore of central importance.

Based on other discussion on this point, it seems like module-as-named-bindings can interoperate with CJS and AMD except perhaps in some unusual cases. Clarifying these cases would help.

As Scott and I pointed out, unless you have module-object overwriting (or default exports), then you have to use some arbitrary name for CJS modules that use the module.exports = x idiom. And that's the only real advantage of the module-as-object semantics. In fact, it doesn't really fly without it.

To me, these two lists are pretty darn small compared with the overall advantages of modules.

So we should just pick a core semantics and get on with it? I agree, but we have to decide whether module-object-overwriting is a design requirement or not.

To sum up, the strongest advantages are:

Option 1A (modules are regular objects which can be overwritten): Possibly better interoperability with CJS modules

Option 2 (modules as named remote bindings): Better support for circular dependencies

# Kevin Smith (10 years ago)

If there is no module object overwriting, then I suppose it could be inlined. If you allow overwriting, then you would need the further restriction that you cannot import from overridden module objects.

Sorry, strike the second sentence. (Trying to do too many things at once!)

# Kevin Smith (10 years ago)

So I think we've gone over the interoperability argument already. That is, with default exports (or module-object-overwriting), you can write something like this:

import x from "some-old-module";

Without default exports, you'll have to use something like one of the following:

import { exports as x } from "some-old-module";
import { default as x } from "some-old-module";
import { _ as x } from "some-old-module";

The user experience is a little better with default exports, but we shouldn't be overly concerned with optimizing the loading old-style modules. The experience without default exports is good enough.

# Kevin Smith (10 years ago)

There are three arguments for default exports:

  1. Anonymous exports are a value-adding feature, as it allows the user to use a library without having to know the export name.
  2. Default exports allow more streamlined renaming of the imported binding.
  3. Default exports allow smoother interoperability with legacy modules.

The responses to these arguments are:

  1. It's not clear that anonymous APIs add value, since the user of an API always has to refer to it's documentation, and therefore will always be exposed to its exported API names.
  2. From experience with other languages like Python, we know that import renaming is the exception rather than the rule. As such, streamlined renaming only results in a marginal benefit.
  3. Interoperability with legacy modules is possible by using a simple naming convention. Default exports would improve the user experience of importing from legacy modules somewhat, but optimizing legacy modules should not be a primary motivating factor in the design.

And

  1. The default export feature is confusing to users, because it gives the appearance of module-object-overwriting (in the Node sense) while acting completely different on a semantic level. Syntactically, the design favors default exports while semantically it favors named exports. Since the module system is the entry point to the language for users of all experience levels, the module system should not be confusing. It should be as simple and obvious as possible.

Given that the costs (4) do not outweigh the benefits (1, 2, 3), default exports should be dropped.

# Kevin Smith (10 years ago)

My recommendation is to drop the default export feature and leave everything else as is (except perhaps for making module.x equivalent to x per Andreas). Given that the current module system has far better support for circular dependencies than "module-as-module" designs, the static export design should be retained.

# Kevin Smith (10 years ago)

My recommendation is to drop the default export feature and leave everything else as is (except perhaps for making module.x equivalent to x per Andreas). Given that the current module system has far better support for circular dependencies than "module-as-module" designs, the static export design should be retained.

er, "module-as-object" designs : )

# Mark S. Miller (10 years ago)

If it had good enough support for circular dependencies, would we be able to make sense of "module-as-module" designs?

# Kevin Smith (10 years ago)

If it had good enough support for circular dependencies, would we be able to make sense of "module-as-module" designs?

Side note: I meant "object-as-module" really. It's kinda funny that I capped off all of that with such a silly mistype. : )

# Calvin Metcalf (10 years ago)

one benefit of default exports is forcing people to choose one of

export { x as exports };
export { x as default };
export { x as _ };

this is the one chance to get everyone on the same page

as far as object-as-module having circular dependency issues, can you elaborate on that, I understand how

let {foo, bar} = import './baz';

would have circular reference problems (amongst other issues), but

module name from './path'
...
name.method();

is (~)what node uses and has comparable (better for certain things, worse for others) circular dependency support (source: writing a CJS loader using the es6 loader hooks api) the difficulties I can see with modules as objects involve static analysis.

# C. Scott Ananian (10 years ago)

On Tue, Jul 1, 2014 at 1:36 PM, Calvin Metcalf <calvin.metcalf at gmail.com>

wrote:

one benefit of default exports is forcing people to choose one of

export { x as exports };
export { x as default };
export { x as _ };

this is the one chance to get everyone on the same page

I think if default exports were dropped, it would certainly be worth an explicit non-normative mention in the spec text that "the export named _ is reserved for interoperability with legacy module systems" or something like that. There aren't that many widely-used module systems, social pressure and an explicit recommendation is probably sufficient to get them all on the same page. (And presumably the legacy loader implementations which provide interoperability have a good deal of influence here.)

as far as object-as-module having circular dependency issues, can you

elaborate on that, I understand how

[...]

is (~)what node uses and has comparable (better for certain things, worse for others) circular dependency support (source: writing a CJS loader using the es6 loader hooks api) the difficulties I can see with modules as objects involve static analysis.

This is, indeed, worth exploring further. Node.js has a very straightforward mechanism for dealing with circular dependencies. As long as you make all of your references indirectly through the "module object" they are mutable. This is pretty much exactly equivalent to what ES6 modules provide, with the exception that they save the required "modulename." prefix -- but at the cost of possible user confusion, since JavaScript has never had mutable bindings in the way that the ES6 module system provides them.

If we're cutting things from the ES6 module spec, can we consider cutting the magical mutable import {foo} from "./foo"; bindings as well? Experience has shown that the rare cases where circular dependencies are intended and used can deal with the overhead of prefixing the module object.

Personally, I would like to introduce mutable bindings as a future language-level feature, not as some weird wart on the module design.

# Marius Gundersen (10 years ago)

Can't proxies or getters/setters be used to get objects to behave like they have named exports? As in, when getting a property of the object it looks up the current value of the bound value in the module. I can't see why this wouldn't work, but there is probably a reason since nobody has proposed it yet.

Marius Gundersen

# Kevin Smith (10 years ago)
module name from './path'
...
name.method();

Right - that would work for objects-as-modules, but this wouldn't:

import { something } from "./path";

Unless you somehow emitted getters for all references to "something". Without consulting documentation, I would expect that these two things:

module x from "./path";
    // From within some function or other
    x.foo();

import { foo } from "./path";
    // From within some function or other
    foo();

would be equivalent. I think a beginner would find a difference there surprising.

Also, consider hoisting.

// Call an imported function directly
import { foo } from "./path";
foo(123);

module m from "./path";
m.foo();

With "object-as-module", neither one is going to work if you have a circular dependency at play.

Of course, these gains aren't giant - they support edge cases. But they are real.

# Domenic Denicola (10 years ago)

From: es-discuss <es-discuss-bounces at mozilla.org> on behalf of C. Scott Ananian <ecmascript at cscott.net>

If we're cutting things from the ES6 module spec, can we consider cutting the magical mutable import {foo} from "./foo"; bindings as well?  Experience has shown that the rare cases where circular dependencies are intended and used can deal with the overhead of prefixing the module object.

Yes, please. Strong +1. As long as we're in the magical land of theorycraft where we're dropping features of consensus-modules, and completely off the topic of the OP: let's drop mutable bindings, and keep default exports.

Personally, I would like to introduce mutable bindings as a future language-level feature, not as some weird wart on the module design.

I would rather never introduce them at all. But I agree with the sentiment that having them sneak in through the module syntax backdoor is silly.

# Kevin Smith (10 years ago)

If it had good enough support for circular dependencies, would we be able to make sense of "module-as-module" designs?

OK, so let's assume for the sake of argument that "objects-as-modules" is not confusing, so (4) doesn't apply. All of the arguments for and against default exports also apply to "module-object-overwriting". As such, we are balancing the marginal user experience gains of "export-overwriting" against the better support for circular dependencies of "real" modules.

And we must note that allowing users to overwrite the module-object eliminates the advantages of statically analyzable exports in those cases.

# Kevin Smith (10 years ago)

As such, we are balancing the marginal user experience gains of "export-overwriting" against the better support for circular dependencies of "real" modules.

Another way of looking at it:

Being in control of the language, you can always invent new sugar to optimize user experience (within limits) when real usage data proves its worth. But the core model can't be changed.

As such, it seems like it would be better to err on the side of making the core model simple and solid, leaving minor UX optimizations to future syntax.

# Jussi Kalliokoski (10 years ago)

On Tue, Jul 1, 2014 at 10:28 PM, Kevin Smith <zenparsing at gmail.com> wrote:

As such, we are balancing the marginal user experience gains of

"export-overwriting" against the better support for circular dependencies of "real" modules.

Another way of looking at it:

Being in control of the language, you can always invent new sugar to optimize user experience (within limits) when real usage data proves its worth. But the core model can't be changed.

As such, it seems like it would be better to err on the side of making the core model simple and solid, leaving minor UX optimizations to future syntax.

But it's neither simple nor solid. It's overtly complicated to support features that shouldn't be there.

Sorry in advance for the tone of this message, it is quite negative. But the intent is constructive. To me modules are the most anticipated feature in ES6 and I've been closely following the discussion and the proposal's evolution and I've been extremely thrilled. Finally we could have a chance of alleviating the situation of having a ton of non-intercompatible different module loaders. I haven't contributed much to the discussion since I liked the overall direction. However, now that I've been actually using the modules for a while with a couple of different transpilers, it's become obvious that the design is inherently broken (not the fault of the transpilers, they are actually probably better in some aspects than the real thing), and in order for ES modules to help the situation instead of making it worse, it needs to be better than the existing solutions.

The core unique selling points of ES6 modules as opposed to the other module loading systems:

  • Language built-in: the strongest point. This makes sure that most tooling should support the feature out of the box and that module authors are more inclined to provide their modules in this format. But the design can be whatever and this point will still hold true, so no points to the design here.
  • (Reliably) statically analyzable syntax. This is my favourite feature and really awesome. Allows engine to fetch next modules while the current one is still being parsed, and tooling to better understand what the code does. However, this would hold true even if all we standardized was syntactic sugar on top of requirejs.
  • Cyclic dependencies: First of all, this is not a feature you want to necessarily encourage. It looks good in academia, but in my career I've yet to see real world code that would benefit more from cyclic dependencies more than refactoring. Not to mention that having cyclic dependencies have been possible in global scope modules since LiveScript, and is possible in CommonJS as well if you do a little DI: gist.github.com/jussi-kalliokoski/50cc79951a59945c17a2 (I had such a hard time coming up with an example that could use cyclic dependencies that I had to dig the example from modules examples wiki). And honestly I don't think it's a feature that deserves to be easier to do than my example, and especially not worth making other design compromises for.
  • Mutable bindings: This will make a good chapter in a future edition of JavaScript: The bad parts, along with being able to redefine undefined. You can have mutable bindings in other module systems as well, just reassign something in your exports and then your consumer uses the exports property directly. A lot of WTFs avoided and even doing it like that will ring alarm bells in code review.
  • Compile-time errors: This not a feature, it's a bug. Try finding a website that doesn't somewhere in its code check for whether you have a feature / module available, i.e. optional dependencies. Some examples: Angular has jQuery as an optional dependency; spritesheet generator modules for node that have multiple engines as optional dependencies because some of them may be native modules that don't compile on all platforms. Also things get worse if platform features are implemented as modules. Let's say things like localStorage, IndexedDB and friends were provided as modules and as a result, things like localforage would either not exist or would be infinitely more complex. Just look at github search for keywords try and require search?l=javascript&q=try+require&ref=cmdform&type=Code to see how widely used the pattern of wrapping a module load in a try-catch is.

Now let's look at some things that tip the odds into existing solutions favor:

  • Massive amount of existing modules.
  • Existing large-scale user-bases.
  • Node has stated that the core will always be CommonJS, meaning that on node, in order to use ES6 modules, you'll have to be using two different module systems which doesn't sound like a thing that people would do unless there's proven benefits.
  • Completely dynamic. Now, I know there are people that think that this isn't not good, but it is. It gives you a lot of power when debugging things or playing around with new things (something I haven't seen discussed re:modules on this list). One of the greatest things in JS is that instead of reading the usually poor documentation, let alone the code, of something you want to use you can just load it up in node or the developer tools and play around with it. With node, you require() something in the repl and you see immediately what it exports. You can do whatever with the exports: enumerate, extend your own exports with them, whatever you want, it's just an object. In debugging situations and learning of new libraries, it's really straightforward to just require the module and give it different inputs to see what the outputs to see if it's something you want to use or what's the edge case of the method that causes the bug you're trying to fix (and then just make a failing test out of it). I can't tell you how many times a day I write angular.element(document.body).injector().get("Something") or require(function (Something) { window.Something = Something }) in the dev tools. With the current design of ES6 modules, I don't think it's even feasible to allow the users to type into the devtools console first import something from "somewhere" and then in the next entry refer to something. Not to mention that the dynamic loading in the existing solutions caters for situations like build tools, for example loading grunt tasks. The case there is that a lot of grunt plugins have dependencies lists from here to the end of the world, so when you run a task, you only want to load the modules that are required by that task to avoid concatenating files taking 20 seconds because it loads a ton of crap for other tasks. So people started loading the modules when the tasks get called, not all at the same time when initializing and this proved to have significant performance benefits. So much that now there's jit-grunt that by guessing and configuration is able to load the modules for a given task when the given task is invoked. This is simply not possible with ES6 modules without a massive boilerplate spaghetti with async stuff.

Given all this, how are we supposed to convince people to use this stuff? These concerns are not something that can be fixed later either, they're fundamental to the current design.

Now I can guess you're thinking that this was purely destructive, so where's the constructive part? The key takeaway is that I'm no longer sure that we should even try to ship modules with ES6. On the web platform, it's better to not hurry shipping something that's just going to end up on the wall of shame of short-sighted decisions. Brendan probably knows this the best. :) Let's take the time to actually ship something that we can confidently say is better than the status quo rather than doing this: xkcd.com/927

# Kevin Smith (10 years ago)

But it's neither simple nor solid. It's overtly complicated to support features that shouldn't be there.

I have to disagree here. If we drop default imports, then we can describe the module system like this:

"Variables can be exported by name. Variables can be imported by name."

It doesn't get any more simple than that. What I mean by solid is that it has good coverage of the edge cases, meaning primarily cyclic dependencies.

Sorry in advance for the tone of this message, it is quite negative.

I didn't perceive this as negative. I think it's quite constructive to uncover all of the arguments.

  • Massive amount of existing modules.
  • Existing large-scale user-bases.

We've already taken a look at the technical side of interoperability with old-style modules, and there's no problem there. What remains, I suppose, is a sociological argument. More on that later.

  • Node has stated that the core will always be CommonJS, meaning that on node, in order to use ES6 modules, you'll have to be using two different module systems which doesn't sound like a thing that people would do unless there's proven benefits.

If users want Node core to be exposed as ES6 modules, then the Node developers will provide it. It's not some ideological battle - it's about whatever is good for the platform. Regarding two module systems at the same time: more later.

  • Completely dynamic. Now, I know there are people that think that this

isn't not good, but it is. It gives you a lot of power when debugging things or playing around with new things (something I haven't seen discussed re:modules on this list). One of the greatest things in JS is that instead of reading the usually poor documentation, let alone the code, of something you want to use you can just load it up in node or the developer tools and play around with it. With node, you require() something in the repl and you see immediately what it exports. (...edit...) This is simply not possible with ES6 modules without a massive boilerplate spaghetti with async stuff.

You're right, but I don't think we need "objects-as-modules" to address this. We want a blocking load API for these situations:

  1. Any REPL (strong)
  2. Server-only programs which don't care about async loading and don't want the complications (weaker)

In es6now, I provide a loadModule function for loading "ES6" modules synchronously in the REPL. I think Node would want to provide a synchronous loading method as part of the so-called module meta object. That API needs eyes, BTW.

Given all this, how are we supposed to convince people to use this stuff? These concerns are not something that can be fixed later either, they're fundamental to the current design.

I don't see any technical problem here. So let's look at the sociological argument:

ES6 modules are different from Node/AMD modules, and seem to be at odds philosophically. Since Node/AMD modules already have established user bases, and important members of the Node community are critical, ES6 modules won't be successful at penetrating the "market".

Counter-argument:

Take a look at this package: zenparsing/zen-sh . It's an experimental ES6 package which allows you to open up a shell and execute commands using tagged template strings. I use it at work to automate git tasks. It's awesome (but very rough). It's completely installable using NPM, today. I encourage anyone to try it out (you'll need to install es6now zenparsing/es6now first, though).

It exports a single thing, but that thing is given a name. It is set up to work with both require and import.

Now, the sociological argument says that because it's written as an ES6 module the community will reject this package. Does that sound plausible to you?

# Calvin Metcalf (10 years ago)

circular dependencies in CJS are much easer then that gist.github.com/calvinmetcalf/57be20b8eda0ee8fe6de

# Kevin Smith (10 years ago)

circular dependencies in CJS are much easer then that gist.github.com/calvinmetcalf/57be20b8eda0ee8fe6de

Umm... I would rather call that tricky as hell ; )

# Kevin Smith (10 years ago)

circular dependencies in CJS are much easer then that

gist.github.com/calvinmetcalf/57be20b8eda0ee8fe6de

Umm... I would rather call that tricky as hell ; )

Besides, it fails if you do require("./b"), IIUC.

# C. Scott Ananian (10 years ago)

I agree with everything Jussi wrote. He gets to the heart of the issue.

The points that have been previously made about "bundling modules" also apply to REPLs. If a module must be in a file, I can't easily define modules at a REPL prompt.

# Kevin Smith (10 years ago)

The points that have been previously made about "bundling modules" also apply to REPLs. If a module must be in a file, I can't easily define modules at a REPL prompt.

Lexical modules would give you these abilities.

module X {
    export const whatever;
}

They've been deferred, and we need to make sure that ES6 modules are not future-hostile to them.

# John Barton (10 years ago)

On Wed, Jul 2, 2014 at 1:09 AM, Jussi Kalliokoski < jussi.kalliokoski at gmail.com> wrote:

On Tue, Jul 1, 2014 at 10:28 PM, Kevin Smith <zenparsing at gmail.com> wrote:

As such, we are balancing the marginal user experience gains of

"export-overwriting" against the better support for circular dependencies of "real" modules.

Another way of looking at it:

Being in control of the language, you can always invent new sugar to optimize user experience (within limits) when real usage data proves its worth. But the core model can't be changed.

As such, it seems like it would be better to err on the side of making the core model simple and solid, leaving minor UX optimizations to future syntax.

But it's neither simple nor solid. It's overtly complicated to support features that shouldn't be there.

Actually I think you have missed important features that the current proposal supports and are needed for your use cases.

Sorry in advance for the tone of this message, it is quite negative. But the intent is constructive. To me modules are the most anticipated feature in ES6 and I've been closely following the discussion and the proposal's evolution and I've been extremely thrilled. Finally we could have a chance of alleviating the situation of having a ton of non-intercompatible different module loaders. I haven't contributed much to the discussion since I liked the overall direction. However, now that I've been actually using the modules for a while with a couple of different transpilers, it's become obvious that the design is inherently broken (not the fault of the transpilers, they are actually probably better in some aspects than the real thing), and in order for ES modules to help the situation instead of making it worse, it needs to be better than the existing solutions.

The core unique selling points of ES6 modules as opposed to the other module loading systems:

  • Language built-in: the strongest point. This makes sure that most tooling should support the feature out of the box and that module authors are more inclined to provide their modules in this format. But the design can be whatever and this point will still hold true, so no points to the design here.
  • (Reliably) statically analyzable syntax. This is my favourite feature and really awesome. Allows engine to fetch next modules while the current one is still being parsed,

This isn't true -- all module designs in play require parsing (include of course CJS and AMD).

and tooling to better understand what the code does. However, this would hold true even if all we standardized was syntactic sugar on top of requirejs.

I don't believe that anyone expects such an outcome.

  • Cyclic dependencies: First of all, this is not a feature you want to necessarily encourage. It looks good in academia, but in my career I've yet to see real world code that would benefit more from cyclic dependencies more than refactoring. Not to mention that having cyclic dependencies have been possible in global scope modules since LiveScript, and is possible in CommonJS as well if you do a little DI: gist.github.com/jussi-kalliokoski/50cc79951a59945c17a2 (I had such a hard time coming up with an example that could use cyclic dependencies that I had to dig the example from modules examples wiki). And honestly I don't think it's a feature that deserves to be easier to do than my example, and especially not worth making other design compromises for.

The arguments for and against supporting cyclic dependencies seem to be academic. I'm yet to see any evidence of their importance in practice nor proof they they are fundamental ... or not.

  • Mutable bindings: This will make a good chapter in a future edition of JavaScript: The bad parts, along with being able to redefine undefined. You can have mutable bindings in other module systems as well, just reassign something in your exports and then your consumer uses the exports property directly. A lot of WTFs avoided and even doing it like that will ring alarm bells in code review.
  • Compile-time errors: This not a feature, it's a bug. Try finding a website that doesn't somewhere in its code check for whether you have a feature / module available, i.e. optional dependencies. Some examples: Angular has jQuery as an optional dependency; spritesheet generator modules for node that have multiple engines as optional dependencies because some of them may be native modules that don't compile on all platforms. Also things get worse if platform features are implemented as modules. Let's say things like localStorage, IndexedDB and friends were provided as modules and as a result, things like localforage would either not exist or would be infinitely more complex. Just look at github search for keywords try and require search?l=javascript&q=try+require&ref=cmdform&type=Code to see how widely used the pattern of wrapping a module load in a try-catch is.

Optional dependency is completely supported by Loader.import(). Furthermore its promise based API avoids try/catch goop.

Now let's look at some things that tip the odds into existing solutions favor:

  • Massive amount of existing modules.
  • Existing large-scale user-bases.
  • Node has stated that the core will always be CommonJS, meaning that on node, in order to use ES6 modules, you'll have to be using two different module systems which doesn't sound like a thing that people would do unless there's proven benefits.

These points are not relevant since nothing in the current design prevents these success stories from continuing.

  • Completely dynamic.

Neither Common JS nor AMD modules are not complete dynamic. Both systems rely on preprocessing JS to package modules for browsers.

Now, I know there are people that think that this isn't not good, but it is. It gives you a lot of power when debugging things or playing around with new things (something I haven't seen discussed re:modules on this list). One of the greatest things in JS is that instead of reading the usually poor documentation, let alone the code, of something you want to use you can just load it up in node or the developer tools and play around with it. With node, you require() something in the repl and you see immediately what it exports.

Loader.get() provides the module.

You can do whatever with the exports: enumerate, extend your own exports with them, whatever you want, it's just an object.

As far as I understand it, the return value from Loader.get() is also an object. The mutable bindings are not mutable after import: they are just properties.

In debugging situations and learning of new libraries, it's really straightforward to just require the module and give it different inputs to see what the outputs to see if it's something you want to use or what's the edge case of the method that causes the bug you're trying to fix (and then just make a failing test out of it). I can't tell you how many times a day I write angular.element(document.body).injector().get("Something") or require(function (Something) { window.Something = Something }) in the dev tools.

IMO, poor support for modules in devtools is a consequence of delay in the ES process for modules.

With the current design of ES6 modules, I don't think it's even feasible to allow the users to type into the devtools console first import something from "somewhere" and then in the next entry refer to something.

Loader.import() provides this ability.

Not to mention that the dynamic loading in the existing solutions caters for situations like build tools, for example loading grunt tasks. The case there is that a lot of grunt plugins have dependencies lists from here to the end of the world, so when you run a task, you only want to load the modules that are required by that task to avoid concatenating files taking 20 seconds because it loads a ton of crap for other tasks. So people started loading the modules when the tasks get called, not all at the same time when initializing and this proved to have significant performance benefits. So much that now there's jit-grunt that by guessing and configuration is able to load the modules for a given task when the given task is invoked. This is simply not possible with ES6 modules without a massive boilerplate spaghetti with async stuff.

Badly designed systems will exist independent of language features. The combination of statically analyzed declarative imports and dynamic imperative import allows much better tool support than is possible with Common JS and AMD.

Given all this, how are we supposed to convince people to use this stuff? These concerns are not something that can be fixed later either, they're fundamental to the current design.

I think that you are misunderstanding the current design. I do agree that devtools support is very important. Sadly this support is not given enough weight. We warned about the issue with exceptions in Promises and now we have Promises that easily make programs essentially undebuggable. Modules are less fundamentally broken, but they have more surface area for tools to cover.

Now I can guess you're thinking that this was purely destructive, so where's the constructive part? The key takeaway is that I'm no longer sure that we should even try to ship modules with ES6. On the web platform, it's better to not hurry shipping something that's just going to end up on the wall of shame of short-sighted decisions. Brendan probably knows this the best. :) Let's take the time to actually ship something that we can confidently say is better than the status quo rather than doing this: xkcd.com/927

This commonly cited comic is both true and not a sign of problems: progress is in fact N+1 standards. That's they way human interaction works. jjb

# Kevin Smith (10 years ago)

The arguments for and against supporting cyclic dependencies seem to be academic. I'm yet to see any evidence of their importance in practice nor proof they they are fundamental ... or not.

I wouldn't say that cyclic dependencies are academic, in the sense of "not real". If they are allowed by the language, they will be used. I see them in Python code and I see them in Node modules. So unless you plan to disallow them altogether, it makes sense to provide good support, all other things being equal.

It's an edge case for the module system, to be sure, but we should expect the module system to have reasonable coverage over the edge cases.

# Rob Sayre (10 years ago)

On Wed, Jul 2, 2014 at 9:09 AM, John Barton <johnjbarton at google.com> wrote:

The arguments for and against supporting cyclic dependencies seem to be academic. I'm yet to see any evidence of their importance in practice nor proof they they are fundamental ... or not.

Data point: I once implemented a system like for Firefox. I got asked for cyclic dependency support 3 weeks after shipping.

bugzilla.mozilla.org/show_bug.cgi?id=384168#c7

That system that Alex Fritze invented and I worked on is not perfect, and the syntax isn't very pretty. But it's still getting used 7 years later, so it must have gotten something right.

developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Using

# Jussi Kalliokoski (10 years ago)

On Wed, Jul 2, 2014 at 7:09 PM, John Barton <johnjbarton at google.com> wrote:

  • (Reliably) statically analyzable syntax. This is my favourite feature and really awesome. Allows engine to fetch next modules while the current one is still being parsed,

This isn't true -- all module designs in play require parsing (include of course CJS and AMD).

Huh? I wasn't saying that they don't. I mean that with the static syntax the parser can initiate the request immediately when it hits an import statement, which is a good thing and not possible with what is out there. You can of course assume that the require call with a static string does what you'd expect but then you might end up loading something that was never actually required but someone had their own require function there instead that has something else.

and tooling to better understand what the code does. However, this would

hold true even if all we standardized was syntactic sugar on top of requirejs.

I don't believe that anyone expects such an outcome.

Heh of course not, that would be horrible; I was referring to the fact that this is a low-hanging fruit to pick.

  • Cyclic dependencies: First of all, this is not a feature you want to

necessarily encourage. It looks good in academia, but in my career I've yet to see real world code that would benefit more from cyclic dependencies more than refactoring. Not to mention that having cyclic dependencies have been possible in global scope modules since LiveScript, and is possible in CommonJS as well if you do a little DI: gist.github.com/jussi-kalliokoski/50cc79951a59945c17a2 (I had such a hard time coming up with an example that could use cyclic dependencies that I had to dig the example from modules examples wiki). And honestly I don't think it's a feature that deserves to be easier to do than my example, and especially not worth making other design compromises for.

The arguments for and against supporting cyclic dependencies seem to be academic. I'm yet to see any evidence of their importance in practice nor proof they they are fundamental ... or not.

True, and that being the case I don't see the reason of putting them on a pedestal. If they happen to be a nice side effect, that's fine, but I'm mostly referring to arguments against different proposals using "doesn't support cyclic dependencies".

  • Compile-time errors: This not a feature, it's a bug. Try finding a

website that doesn't somewhere in its code check for whether you have a feature / module available, i.e. optional dependencies. Some examples: Angular has jQuery as an optional dependency; spritesheet generator modules for node that have multiple engines as optional dependencies because some of them may be native modules that don't compile on all platforms. Also things get worse if platform features are implemented as modules. Let's say things like localStorage, IndexedDB and friends were provided as modules and as a result, things like localforage would either not exist or would be infinitely more complex. Just look at github search for keywords try and require search?l=javascript&q=try+require&ref=cmdform&type=Code to see how widely used the pattern of wrapping a module load in a try-catch is.

Optional dependency is completely supported by Loader.import(). Furthermore its promise based API avoids try/catch goop.

Try/catch is far less goop than promises, and furthermore your non-optional dependencies don't come in as promises, and neither can you define your module asynchronously and wait to see whether the optional dependency is available before exposing your interface. If you could define your module like this it would be less of a problem but still ugly and inferior (in this specific case) to for example CJS:

import someRequiredDependency from "somewhere";

Loader.import("someOptionalDependency") .catch(function noop () {}) .then(function (someRequiredDependency) { exports function doSomething (foo) { if ( someOptionalDependency ) { return someOptionalDependency(foo + 5); } else { return someRequiredDependency(foo + 2); } }; });

Now let's look at some things that tip the odds into existing solutions favor:

  • Massive amount of existing modules.
  • Existing large-scale user-bases.
  • Node has stated that the core will always be CommonJS, meaning that on node, in order to use ES6 modules, you'll have to be using two different module systems which doesn't sound like a thing that people would do unless there's proven benefits.

These points are not relevant since nothing in the current design prevents these success stories from continuing.

The two first points are relevant if they decrease the chances of ES6 modules becoming the most used module system (which is obviously a goal because otherwise we'll just be making things worse by contributing to fragmentation). The last one is relevant because it actively hinders that goal.

  • Completely dynamic.

Neither Common JS nor AMD modules are not complete dynamic. Both systems rely on preprocessing JS to package modules for browsers.

At least in the browser, RequireJS is dynamic during runtime, as is CJS as implemented by browserify and the APIs in both are completely dynamic. There's of course the (for requirejs optional) build steps that do preprocessing and node uses preprocessing by default to make it seem as if module, exports and require were globals to avoid having to create different global subcontexts for each module. This is however not required by CJS and can be turned off if you want to for example make node look bad in performance benchmarks. :P

Now, I know there are people that think that this isn't not good, but it is. It gives you a lot of power when debugging things or playing around with new things (something I haven't seen discussed re:modules on this list). One of the greatest things in JS is that instead of reading the usually poor documentation, let alone the code, of something you want to use you can just load it up in node or the developer tools and play around with it. With node, you require() something in the repl and you see immediately what it exports.

Loader.get() provides the module.

Hmm, my bad, I actually thought that Loader.get() works only when the module has already been fetched. Well that improves things a lot but that still leaves the disparity between what you'd write in actual code and the repl and thus fails to be better (in this case) than for example CommonJS.

You can do whatever with the exports: enumerate, extend your own exports with them, whatever you want, it's just an object.

As far as I understand it, the return value from Loader.get() is also an object.

Hopefully, I don't see what else it could be. :S

The mutable bindings are not mutable after import: they are just properties.

That's a relief, but what's the point then anyway? It's not like you can do:

import meh from "somewhere";

console.log(meh);

exports function wat () { // at this point the module changed its meh binding import meh from "somewhere"; console.log(meh); };

right? Please say yes...

In debugging situations and learning of new libraries, it's really straightforward to just require the module and give it different inputs to see what the outputs to see if it's something you want to use or what's the edge case of the method that causes the bug you're trying to fix (and then just make a failing test out of it). I can't tell you how many times a day I write angular.element(document.body).injector().get("Something") or require(function (Something) { window.Something = Something }) in the dev tools.

IMO, poor support for modules in devtools is a consequence of delay in the ES process for modules.

The status quo implementation status is not the problem, but the disparity between what you'd write in the devtools and what you'd write in the actual code is. We're making the load time programmable and behave differently than the runtime, and the devtools live in runtime, while the module imports you'll write in your actual code will be mostly load time.

With the current design of ES6 modules, I don't think it's even feasible to allow the users to type into the devtools console first import something from "somewhere" and then in the next entry refer to something.

Loader.import() provides this ability.

See above.

As an aside, maybe we should encourage devtools REPLs to wait if the user's expression evaluates to a promise? That would make debugging async stuff easier. Of course then there'd need to be a mechanism to forcefully yield back to the REPL so that a hanging promise doesn't hang the REPL.

Not to mention that the dynamic loading in the existing solutions caters for situations like build tools, for example loading grunt tasks. The case there is that a lot of grunt plugins have dependencies lists from here to the end of the world, so when you run a task, you only want to load the modules that are required by that task to avoid concatenating files taking 20 seconds because it loads a ton of crap for other tasks. So people started loading the modules when the tasks get called, not all at the same time when initializing and this proved to have significant performance benefits. So much that now there's jit-grunt that by guessing and configuration is able to load the modules for a given task when the given task is invoked. This is simply not possible with ES6 modules without a massive boilerplate spaghetti with async stuff.

Badly designed systems will exist independent of language features.

True.

The combination of statically analyzed declarative imports and dynamic imperative import allows much better tool support than is possible with Common JS and AMD.

Right you are. Again, I had a blackout about Loader.get() while writing this, and Loader.get() would work in that particular case (since Grunt's design is not exactly async-friendly, but sometimes you have to try to make the best out of badly designed things).

Given all this, how are we supposed to convince people to use this stuff? These concerns are not something that can be fixed later either, they're fundamental to the current design.

I think that you are misunderstanding the current design.

Do you mean my misunderstanding of Loader.get(), or are you referring to something else?

I do agree that devtools support is very important. Sadly this support is not given enough weight.

Yes. :/

We warned about the issue with exceptions in Promises and now we have Promises that easily make programs essentially undebuggable.

Yes. :/

Modules are less fundamentally broken, but they have more surface area for tools to cover.

I don't think I can agree on modules being less broken. Promises are lacking proper devtools support, but at least it's clear how it should be implemented. (The last I looked at the discussion, the consensus seemed to be that non-handled errors would be logged once GCed).

# Calvin Metcalf (10 years ago)

You can of course assume that the require call with a static string does

what you'd expect but then you might end up loading something that was never actually required but someone had their own require function there instead that has something else.

This is a solved problem actually calvinmetcalf/derequire

# John Barton (10 years ago)

This seems like a bit too many issues, so let me just correct one (important) one.

On Wed, Jul 2, 2014 at 2:09 PM, Jussi Kalliokoski < jussi.kalliokoski at gmail.com> wrote:

On Wed, Jul 2, 2014 at 7:09 PM, John Barton <johnjbarton at google.com> wrote:

Now, I know there are people that think that this isn't not good, but it is. It gives you a lot of power when debugging things or playing around with new things (something I haven't seen discussed re:modules on this list). One of the greatest things in JS is that instead of reading the usually poor documentation, let alone the code, of something you want to use you can just load it up in node or the developer tools and play around with it. With node, you require() something in the repl and you see immediately what it exports.

Loader.get() provides the module.

Hmm, my bad, I actually thought that Loader.get() works only when the module has already been fetched.

Your thought was correct: Loader.get() only works if the module is fetched. It was my impression that you were describing a debugging scenario where the module would be loaded and where you are likely to want to avoid module-loading since your goal is to debug the live image.

Well that improves things a lot but that still leaves the disparity between what you'd write in actual code and the repl and thus fails to be better (in this case) than for example CommonJS.

I expect devtools to support declarative import in their REPL, so the code you would write is the same.

jjb

# Jussi Kalliokoski (10 years ago)

On Wed, Jul 2, 2014 at 3:38 PM, Kevin Smith <zenparsing at gmail.com> wrote:

But it's neither simple nor solid. It's overtly complicated to support features that shouldn't be there.

I have to disagree here. If we drop default imports, then we can describe the module system like this:

"Variables can be exported by name. Variables can be imported by name."

FWIW, I don't have a problem with dropping / deferring default exports, although my personal ideal is that like functions, modules should do one thing and thus export one thing, but having no default exports doesn't prevent exporting just one thing.

It doesn't get any more simple than that. What I mean by solid is that it has good coverage of the edge cases, meaning primarily cyclic dependencies.

The complexity is in having multiple different ways of doing one thing and introducing new kind of bindings to the language that didn't exist before to support those edge cases.

Sorry in advance for the tone of this message, it is quite negative.

I didn't perceive this as negative. I think it's quite constructive to uncover all of the arguments.

Happy to hear!

  • Massive amount of existing modules.
  • Existing large-scale user-bases.

We've already taken a look at the technical side of interoperability with old-style modules, and there's no problem there. What remains, I suppose, is a sociological argument. More on that later.

  • Node has stated that the core will always be CommonJS, meaning that on node, in order to use ES6 modules, you'll have to be using two different module systems which doesn't sound like a thing that people would do unless there's proven benefits.

If users want Node core to be exposed as ES6 modules, then the Node developers will provide it. It's not some ideological battle - it's about whatever is good for the platform. Regarding two module systems at the same time: more later.

  • Completely dynamic. Now, I know there are people that think that this

isn't not good, but it is. It gives you a lot of power when debugging things or playing around with new things (something I haven't seen discussed re:modules on this list). One of the greatest things in JS is that instead of reading the usually poor documentation, let alone the code, of something you want to use you can just load it up in node or the developer tools and play around with it. With node, you require() something in the repl and you see immediately what it exports. (...edit...) This is simply not possible with ES6 modules without a massive boilerplate spaghetti with async stuff.

You're right, but I don't think we need "objects-as-modules" to address this.

But depending on how you load the modules (i.e. syntax or Loader API) the module might end up being an object anyway so it's just confusing if it sometimes is and sometimes isn't.

We want a blocking load API for these situations:

  1. Any REPL (strong)
  2. Server-only programs which don't care about async loading and don't want the complications (weaker)

In es6now, I provide a loadModule function for loading "ES6" modules synchronously in the REPL. I think Node would want to provide a synchronous loading method as part of the so-called module meta object.

The term "module meta object" and simple design don't go hand in hand.

That API needs eyes, BTW.

Thanks for the reference, I'll take a look at it after having a good night's sleep first. :)

Given all this, how are we supposed to convince people to use this stuff? These concerns are not something that can be fixed later either, they're fundamental to the current design.

I don't see any technical problem here. So let's look at the sociological argument:

ES6 modules are different from Node/AMD modules, and seem to be at odds philosophically. Since Node/AMD modules already have established user bases, and important members of the Node community are critical, ES6 modules won't be successful at penetrating the "market".

Counter-argument:

Take a look at this package: zenparsing/zen-sh . It's an experimental ES6 package which allows you to open up a shell and execute commands using tagged template strings. I use it at work to automate git tasks. It's awesome (but very rough). It's completely installable using NPM, today. I encourage anyone to try it out (you'll need to install es6now zenparsing/es6now first, though).

It exports a single thing, but that thing is given a name. It is set up to work with both require and import.

Now, the sociological argument says that because it's written as an ES6 module the community will reject this package. Does that sound plausible to you?

No of course not, but why would anyone introduce more complexity to their project to do what you did? Experimental curiosity is probably your case, but the only other reason I can think of is sadism, deliberately fragmenting the platform. In that example, you're using two module systems at the same time for exactly zero benefit.

Now, we can take the stance that node is a market that we don't need to penetrate, but that is a potentially dangerous stance to take, since the trend and interest in using the same code on the server and the client seems to just keep climbing, we risk making things like browserify the path of least resistance to that goal, since for a not-very-insignificant period people will have to use transpilers for ES6 modules as well.

# Brian Di Palma (10 years ago)

The arguments for and against supporting cyclic dependencies seem to be academic. I'm yet to see any evidence of their importance in practice nor proof they they are fundamental ... or not.

Transitive cyclic dependencies. I'd say that's the case that was in the minds of the authors of the module system. In large codebases those can happen and a module system that does not handle them gracefully would be poor.

Support for them is needed, and what CommonJS has is not good enough. They are acknowledged in the modules documentation for node nodejs.org/api/all.html#all_cycles This does not mean they are recommended, the same holds true for ES6 modules.

It is an acceptance of the reality of complex and large codebases that sometimes cyclic dependencies can occur.

It boils down to this.

You can import a dependency in three ways

import MyClass from 'MyClass'; import {MyClass} from 'MyClass'; module myClass from 'MyClass';

That's one too many ways for the simplest module system that fulfills all requirements.

import MyClass from 'MyClass'; import {MyClass} from 'MyClass'; import * as myClass from 'MyClass';

Is not the fix.

The confusion stemmed from the first production not the last.

"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away."

B.

# Rob Sayre (10 years ago)

On Wed, Jul 2, 2014 at 3:29 PM, Brian Di Palma <offler at gmail.com> wrote:

The arguments for and against supporting cyclic dependencies seem to be academic. I'm yet to see any evidence of their importance in practice nor proof they they are fundamental ... or not.

Support for them is needed, and what CommonJS has is not good enough. They are acknowledged in the modules documentation for node nodejs.org/api/all.html#all_cycles This does not mean they are recommended, the same holds true for ES6 modules.

It is an acceptance of the reality of complex and large codebases that sometimes cyclic dependencies can occur.

That's awesome--I didn't know that node had arrived at exactly the same solution as Firefox did. It even uses the same test file names. Node's module system has way more users, and Firefox's is twice as old.

Here's the 7-year old test for it:

hg.mozilla.org/mozilla-central/annotate/38ecfc3922b8/js/xpconnect/tests/unit/test_recursive_import.js

I wrote that, but I didn't have any conceptual insight. All I did was go "what does python do" and made the Mozilla JS engine do that. I think that's the critical disconnect in this thread--we have the ability to change the JS implementations, so we're not fenced in by the way JavaScript currently works. The authors of node.js and Firefox arrived at that conclusion, anyway.

# Jussi Kalliokoski (10 years ago)

On Thu, Jul 3, 2014 at 1:29 AM, Brian Di Palma <offler at gmail.com> wrote:

The arguments for and against supporting cyclic dependencies seem to be academic. I'm yet to see any evidence of their importance in practice nor proof they they are fundamental ... or not.

Transitive cyclic dependencies. I'd say that's the case that was in the minds of the authors of the module system. In large codebases those can happen and a module system that does not handle them gracefully would be poor.

Support for them is needed, and what CommonJS has is not good enough. They are acknowledged in the modules documentation for node nodejs.org/api/all.html#all_cycles This does not mean they are recommended, the same holds true for ES6 modules.

It is an acceptance of the reality of complex and large codebases that sometimes cyclic dependencies can occur.

So sometimes someone can need it, so we must have good support? Is that how we operate these days?

It boils down to this.

You can import a dependency in three ways

import MyClass from 'MyClass'; import {MyClass} from 'MyClass'; module myClass from 'MyClass';

And (in the same order): System.import("MyClass").then(function (MyClassModule) { // I don't actually know how someone would even access the default exports from the module object unless in case of default exports there is no module object, just the default exports as the module. var MyClass = MyClassModule; });

System.import("MyClass").then(function (MyClassModule) { var { MyClass } = MyClassModule; });

System.import("MyClass").then(function (MyClassModule) { var myClass = MyClassModule; });

var MyClass = System.get("MyClass"); var {MyClass} = System.get("MyClass"); var myClass = System.get("MyClass");

That's one too many ways for the simplest module system that fulfills all requirements.

import MyClass from 'MyClass'; import {MyClass} from 'MyClass'; import * as myClass from 'MyClass';

Is not the fix.

The confusion stemmed from the first production not the last.

I agree. Thanks, I'm actually no longer even indifferent towards default exports but I also think it should go.

# Brian Di Palma (10 years ago)

So sometimes someone can need it, so we must have good support? Is that how we operate these days?

Imagine a large codebase which already has transitive cyclic dependencies. If the module system has poor support for them it might still work with them until one day a developer reordered the import statements. How would you feel if such a simple operation caused you issues?

Or upgrading to the latest version of a popular utility toolchain like lo-dash could introduce an issue purely because the upgrade created a transitive cyclic dependency. And the fix for that would be to reorder your import statements and add comments in your module telling people not to change the order. Again, how would you feel?

TC39 has decided to spare us all those special moments by including good cyclic dependency support.

// I don't actually know how someone would even access the default exports from the module object

Like so,

System.import("MyClass")
    .then(function (myClassModule) {
        myClassModule.default
    });

Default import and exports are purely sugar over

import {default as MyClass} from 'MyClass';

It saves you a few character typing out when importing from legacy module system.

import MyClass from 'MyClass';

Those two are the same thing.

From birth the brand new module system is going to have this

superfluous appendage to support module systems that 10 years from now people will struggle to remember.

# Jussi Kalliokoski (10 years ago)

On Thu, Jul 3, 2014 at 11:41 AM, Brian Di Palma <offler at gmail.com> wrote:

So sometimes someone can need it, so we must have good support? Is that how we operate these days?

Imagine a large codebase which already has transitive cyclic dependencies. If the module system has poor support for them it might still work with them until one day a developer reordered the import statements. How would you feel if such a simple operation caused you issues?

Happy; finally having a good motivator to refactor and get rid of the cyclic dependencies, at least in that compartment.

Or upgrading to the latest version of a popular utility toolchain like lo-dash could introduce an issue purely because the upgrade created a transitive cyclic dependency. And the fix for that would be to reorder your import statements and add comments in your module telling people not to change the order. Again, how would you feel?

Angry at lo-dash for breaking backwards compatibility, then I'd revert the upgrade and file a bug against it. I'd probably also consider whether lo-dash were in this imaginary case something I want to depend on given that for a utility library they need cyclic dependencies. I'd also be happy that the module system made this obvious.

But I get your point since I'm well aware that my thoughts are likely not to be a very good representation of how most people would feel.

However, I still don't think it's something worth making other sacrifices over.

Default import and exports are purely sugar over

import {default as MyClass} from 'MyClass';

It saves you a few character typing out when importing from legacy module system.

import MyClass from 'MyClass';

Those two are the same thing.

Ughh, I see. Not cool.

From birth the brand new module system is going to have this superfluous appendage to support

Let's not have it. At least not in ES6. Tools like Traceur can support it for an easier migration path since they already have diverged from ES.next anyway with all the annotations (for which, off topic, I haven't seen even a proposal here yet) and stuff.

module systems that 10 years from now people will struggle to remember.

I certainly hope that will be the case, but it's 2014 and people are still implementing banking with Cobol, security protocols with C++ and website cryptography with Java applets.

# John Barton (10 years ago)

On Thu, Jul 3, 2014 at 2:31 AM, Jussi Kalliokoski < jussi.kalliokoski at gmail.com> wrote:

Tools like Traceur can support it for an easier migration path since they already have diverged from ES.next anyway with all the annotations (for which, off topic, I haven't seen even a proposal here yet) and stuff.

Jussi, I would appreciate a bug report on the Traceur github project pointing to information that makes you think this statement is correct. We consider divergence from ES.next to be a bug and do not support any feature outside of the proposal from TC39. Our project does provides great technology that has been used to develop migration tools, annotation experiments, and "stuff". All of that comes on other projects or under opt-in flags on traceur.

jjb

# Jussi Kalliokoski (10 years ago)

On Thu, Jul 3, 2014 at 5:42 PM, John Barton <johnjbarton at google.com> wrote:

On Thu, Jul 3, 2014 at 2:31 AM, Jussi Kalliokoski < jussi.kalliokoski at gmail.com> wrote:

Tools like Traceur can support it for an easier migration path since they already have diverged from ES.next anyway with all the annotations (for which, off topic, I haven't seen even a proposal here yet) and stuff.

Jussi, I would appreciate a bug report on the Traceur github project pointing to information that makes you think this statement is correct. We consider divergence from ES.next to be a bug and do not support any feature outside of the proposal from TC39.

Annotations are marked as experimental, but I filed a bug anyway to either get a proposal or update the wiki to inform that there's no official spec or proposal for it: google/traceur-compiler#1156 ;)

Our project does provides great technology that has been used to develop migration tools, annotation experiments, and "stuff". All of that comes on other projects or under opt-in flags on traceur.

Exactly, and that's how it could be done in traceur.

# Jussi Kalliokoski (10 years ago)

On Thu, Jul 3, 2014 at 9:05 PM, Brendan Eich <brendan at mozilla.org> wrote:

Jussi Kalliokoski wrote:

So sometimes someone can need it, so we must have good support? Is that how we operate these days?

Cool down a minute :-|.

Heh, the internet is a funny place when it comes to interpreting emotion; I've actually been very calm the whole time. ;D

JS is a mature language on a big rich-and-messy evolving platform-set (browser JS, Node.js, other embeddings). We don't preach "only majority use cases" or "there should be only one way to do it" -- more TIMTOWDI or TimToady Bicarbonate:

en.wikipedia.org/wiki/There's_more_than_one_way_to_do_it

JS systems start small and grow. Modules often merge, split, merge again. Cycles happen in the large. ES6 modules address them, they were always a design goal among several goals.

I'm well aware, to me it looks like cycles are the defining feature of the module system on which other features have been built on. The reason this is a problem is because the requirement of cyclic dependencies not only complicates the API surface, reasoning about it and implementations, it also excludes a lot of features.

For example, if we look at the problem of optional dependencies (which, btw, unlike cyclic dependencies are an extremely common corner case, especially on the web, and can't really be refactored away) is not that you can't load things dynamically, because you can, but in the fact that importing a module is async but initializing and declaring is not, it's static to support mutable bindings magic (that are required for transitive cyclic dependencies) and compile-time errors.

Now, let's have a hypothetical change to the module system. Let's say that we allow only exporting one thing, and that one thing can be any value. When you import it, it's like assigning a variable, except that resolving the value you are assigning to is done async at compile time. Like:

// somewhere.js

export { something: function something () {} };

// doSomethingElse.js import { something } from "somewhere"; export function doSomethingElse () {};

Benefit #1: No module meta object crap, the only new concept needed to understand this is compile-time prefetching. Benefit #2: No special destructuring syntax (since you're doing normal destructuring on a normal value).

Cool, but we broke cyclic dependencies without fixing optional dependencies:

// optional dependency here var fasterAdd = System.import("fasterAdd"); var basicAdd = function (a, b) { return a + b; };

// because there's no async initialize, we have to impose an async interface for an otherwise sync operation export function add (a, b) { return fasterAdd .catch( => basicAdd ) .then( (addMethod) => addMethod(a, b) ); };

However, with this design, we can allow exporting a promise of what we want to export, thus deferring the import process until that promise is resolved or rejected:

var basicAdd = function (a, b) { return a + b; };

export System.import("fasterAdd") .catch( => basicAdd );

And there you have it, voilá.

Benefit #3: See #1, if you don't want to, you don't even have to comprehend compile-time prefetching anymore. It's just optimization sugar. Benefit #4: No need to impose async interfaces for inherently sync operations just because the initialize phase is not async while loading is. Benefit #5: You can do stuff like async feature detects in the initialize phase, something that is completely broken in existing module systems (you can do this with RequireJS through some effort, but I'm not sure it's officially supported or part of AMD). Benefit #6: High compatibility with existing module systems, including edge cases, providing a solid foundation for transpiling "legacy" modules to the new system. See some sketches I made yesterday when playing around with the idea: gist.github.com/jussi-kalliokoski/6e0bf476760d254e5465 (includes an example of how you could implement localforage if the platform dependencies were provided as modules). Benefit #7: Addresses the issue of libraries depending on more than just JS:

import {loadImages, importStylesheets} from "fancy-loader";

var myModule = { ... };

export Promise.all([ loadImages(["foo.jpg", "bar.png"]), importStylesheets(["style.css"]) ]).then( => myModule );

There's probably more that didn't come to my mind. So, this would support pretty much all the features of the existing solutions and more. Just not cyclic dependencies. At all.

Now, we could of course try to add this as an afterthought to the current design by letting individual exports be promises, but in order to preserve transitive cyclic dependencies in that case, you'd have to wait for the promise of that to resolve when the importing module accesses the imported binding, not only causing execution to yield to the event loop unexpectedly (which is what we're trying to avoid with async functions) but also makes it transitive only most of the time, ending up with cyclic dependencies that sometimes work maybe if things happen in a specific order. It would be even worse than CJS cyclic dependencies support because unlike CJS it'd be extremely vulnerable to async race conditions.

I don't know about other people, but I'd rather receive an outright error for making cyclic dependencies than it being a fragile little beast that does what you want if you don't do async stuff or if the right stars align. Evolution just doesn't cut it if the foundation is broken. Unless we're talking about the part where the weak diminish into non-existence, coming to haunt us every now and then in old literature.

# Jussi Kalliokoski (10 years ago)

On Fri, Jul 4, 2014 at 10:19 AM, Jussi Kalliokoski < jussi.kalliokoski at gmail.com> wrote:

On Thu, Jul 3, 2014 at 9:05 PM, Brendan Eich <brendan at mozilla.org> wrote:

Cool down a minute :-|.

I now realize that my tone on this thread hasn't been very considerate, and apologize if I offended anyone, or even if I didn't.

I don't want anyone to think that I don't respect the work that's been done to get ES6 modules where they are today. Considering the use cases and requirements they've been designed for, the design is actually great. My goal was only to challenge those use cases and requirements and their priority over other ones, but I failed at conveying that properly. Sorry.