Module import/export bindings

# Kyle Simpson (10 years ago)

From my current understanding (based on other threads), this module:

var foo = 42;
export default foo;
export foo;
foo = 10;

When imported:

import foo, * as FOO from "coolmodule";
foo; // 10
FOO.default; // 10
FOO.foo; // 10

However, I am curious if this binding is 2-way or only 1-way. What happens if I do:

import foo, * as FOO from "coolmodule";
foo = 100;
FOO.default = 200;
FOO.foo = 300;

Have I changed the local foo variable inside the module? If so, to which value?

Moreover, what are now the values of these three in my imported context:

foo; // ??
FOO.default; // ??
FOO.foo; // ??

Have they all become 300? or are they 100, 200, and 300 separately?

# Kyle Simpson (10 years ago)

Of course in my exports, I meant export {x} instead of export x, but I tried to edit my OP and I get "internal server error". :)

# Fabrício Matté (10 years ago)

Regarding the internal server error, see esdiscuss/esdiscuss.org#83

# André Bargull (10 years ago)

From my current understanding (based on other threads), this module:

var foo = 42;
export default foo;
export foo;
foo = 10;

I guess you'd intended to write export {foo as default} instead of export default foo, because the latter exports the value of the expression when it gets evaluated. IOW it's the same as export default 42.

However, I am curious if this binding is 2-way or only 1-way. What happens if I do:

import foo, * as FOO from "coolmodule";
foo = 100;
FOO.default = 200;
FOO.foo = 300;

All three assignments throw a TypeError exception:

  • The first assignment throws a TypeError in 8.1.1.1.5 SetMutableBinding, step 6. (Import bindings are immutable - 8.1.1.5.5 CreateImportBinding, step 5.)
  • The second and third assignment throw a TypeError in 6.2.3.2 PutValue, step 6.d. (Module namespace objects always return false from [[Set]] - 9.4.6.9 [[Set]].)
# Kyle Simpson (10 years ago)

Regarding the internal server error

Ahh, thanks. Yeah, not only is it confusing to see the "edit" button, but especially since clicking it asks you to login to the site as if to verify your authorization to do such. :)

I guess you'd intended to write export {foo as default} instead of export default foo...

Well, no, until your reply I didn't realize there was a difference in the two! Yikes. That's… surprising.

Just to make sure I'm crystal clear… in my original example:

var foo = 42;
export default foo;
foo = 10;

That exports a binding only to 42, not 10. But:

var foo = 42;
export {foo as default};
foo = 10;

That exports a binding to foo so the importer sees 10. Correct?

All three assignments throw a TypeError exception

Thanks for the clarifications!

Would it then be appropriate to explain that conceptually the binding would otherwise indeed be 2-way, but that the immutable/read-only nature of the bindings is what prevents an outside mutation of a module's internals? That is, without such bindings (and errors), a module could be changed from the outside?

Also, are these errors runtime or static? I'm guessing from the spec text you quoted they're runtime. If so, was there a reason they couldn't be static?


I have some modules where the intentional design of the API has been for a consumer to be able to change property value(s) on the public API, and the module would behave differently corresponding to those values, almost like if they were "configuration".

I take it there's no way to do this directly on the module namespace (even with the namespace import) with ES6 modules? So basically to do that I'd have to export an object (called like config) that holds such intentionally mutable properties?

Similarly, the (much maligned but still somewhat popular) pattern of having module "plugins" which attach themselves on top of an existing API… sounds like this pattern is also not supported?

# Caridy Patino (10 years ago)

inline

On Sun, Mar 15, 2015 at 11:42 AM, Kyle Simpson <getify at gmail.com> wrote:

Regarding the internal server error

Ahh, thanks. Yeah, not only is it confusing to see the "edit" button, but especially since clicking it asks you to login to the site as if to verify your authorization to do such. :)

I guess you'd intended to write export {foo as default} instead of export default foo...

Well, no, until your reply I didn't realize there was a difference in the two! Yikes. That's… surprising.

Just to make sure I'm crystal clear… in my original example:

var foo = 42;
export default foo;
foo = 10;

That exports a binding only to 42, not 10. But:

correct

var foo = 42;
export {foo as default};
foo = 10;

That exports a binding to foo so the importer sees 10. Correct?

correct

All three assignments throw a TypeError exception

Thanks for the clarifications!

Would it then be appropriate to explain that conceptually the binding would otherwise indeed be 2-way, but that the immutable/read-only nature of the bindings is what prevents an outside mutation of a module's internals? That is, without such bindings (and errors), a module could be changed from the outside?

that's well documented as part of the description of the namespace exotic object.

Also, are these errors runtime or static? I'm guessing from the spec text you quoted they're runtime. If so, was there a reason they couldn't be static?

named imports will throw as part of the static verification, but namespaces can only throw during runtime due to the nature of the exotic objects.


I have some modules where the intentional design of the API has been for a consumer to be able to change property value(s) on the public API, and the module would behave differently corresponding to those values, almost like if they were "configuration".

use a method to mutate internal named exports.

I take it there's no way to do this directly on the module namespace (even with the namespace import) with ES6 modules? So basically to do that I'd have to export an object (called like config) that holds such intentionally mutable properties?

Similarly, the (much maligned but still somewhat popular) pattern of having module "plugins" which attach themselves on top of an existing API… sounds like this pattern is also not supported?

plugins are orthogonal to modules, modules CANNOT be extended, but you can plug things into objects exported from a module.

# Kyle Simpson (10 years ago)

Thanks, all answers super helpful!

One last clarification:

import "foo";

This doesn't do any binding does it? AFAICT, it just downloads and runs the module (if it hasn't already)?

If that's true, what's the use-case here besides "preloading" a module performance wise?

# Glen Huang (10 years ago)

My guess is that it’s for importing modules that expose objects via the global object (e.g., window).

However I do have a question: is it possible for a module to access the global object without relying on the host environment?

One use case is to polyfill the language's standard library. es6-shim uses a pretty ugly hack (paulmillr/es6-shim/blob/e17ca7ad73528261a3fc4af2ad71ebc3c8f84c0e/es6-shim.js#L76, paulmillr/es6-shim/blob/e17ca7ad73528261a3fc4af2ad71ebc3c8f84c0e/es6-shim.js#L76).

I wonder what’s the most elegant way to do that?

# caridy (10 years ago)

correct, it is about evaluation.

yes, modules will have access to window, and any other runtime feature.

no, I don't recommend using modules to patch the runtime, features should probably be patched before attempting to load any module (maybe using FT polyfill service).

# Glen Huang (10 years ago)

Didn’t know about the service. It looks really promising. Thank you for letting me know.

So you are saying polyfills should really be included as scripts and not modules? That makes sense.

# Domenic Denicola (10 years ago)

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

Would it then be appropriate to explain that conceptually the binding would otherwise indeed be 2-way, but that the immutable/read-only nature of the bindings is what prevents an outside mutation of a module's internals? That is, without such bindings (and errors), a module could be changed from the outside?

I wouldn't really find this an appropriate explanation. That's kind of like saying "this building's 6th story would be blue, but the 5-story nature of its blueprints is what prevents you from accessing the 6th story." There just isn't any 6th story at all. (Similarly, there just isn't any defined [[Set]] behavior for module namespace objects at all. You could make up a plausible one, like pretending it would modify the original module's bindings, and write a revisionist history in which it was removed. But that's not really how the spec works.)

# Glen Huang (10 years ago)

On second thought, this does seem to imply that polyfills can’t use the module syntax, which means they can’t use utility libraries written in module syntax, and, if you are writing a complex polyfill, managing dependencies requires ensuring correct script loading order (whether that means managing script element order or file concatenating order in a build step). Not to mention having to wrap each script in an IIFE.

This feels like being thrown back into the dark days, and I don’t think writing polyfills should be that different.

So now I have more questions:

  1. When the environment you are trying to polyfill exposes the global object, should you use the module syntax?
  2. When polyfilling the language’s standard library, should you use the module syntax? If so, how do you get hold of the global object?
  3. If the answers to these questions are no, how do you use a library, written in module syntax, in the polyfill that can later be reuse by the project (without converting the library to script syntax and duplicate the code in the end of cause)?
# Allen Wirfs-Brock (10 years ago)

On Mar 15, 2015, at 9:43 PM, Domenic Denicola wrote:

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

Would it then be appropriate to explain that conceptually the binding would otherwise indeed be 2-way, but that the immutable/read-only nature of the bindings is what prevents an outside mutation of a module's internals? That is, without such bindings (and errors), a module could be changed from the outside?

I wouldn't really find this an appropriate explanation. That's kind of like saying "this building's 6th story would be blue, but the 5-story nature of its blueprints is what prevents you from accessing the 6th story." There just isn't any 6th story at all. (Similarly, there just isn't any defined [[Set]] behavior for module namespace objects at all. You could make up a plausible one, like pretending it would modify the original module's bindings, and write a revisionist history in which it was removed. But that's not really how the spec works.)

the simple story:

imported bindings are all const bindings. Think of them as if if they were written

const import {a,b,c} from "foo";
# Mark Miller (10 years ago)

Story is way too simple. JS const means constant, unchanging. By contrast, import bindings, like const in C++, means "read-only view". This is very different from constant.

Don't use the "const" analogy when changes are still observable.

# Allen Wirfs-Brock (10 years ago)

On Mar 16, 2015, at 9:26 AM, Mark Miller wrote:

Story is way too simple. JS const means constant, unchanging. By contrast, import bindings, like const in C++, means "read-only view". This is very different from constant.

Don't use the "const" analogy when changes are still observable.

got me...not enough morning coffee yet...

Yes, they are like const in that they can't be assigned to. They are unlike const in that in some cases their observable values may change.

# caridy (10 years ago)

inline

On Mar 16, 2015, at 2:21 AM, Glen Huang <curvedmark at gmail.com> wrote:

On second thought, this does seem to imply that polyfills can’t use the module syntax, which means they can’t use utility libraries written in module syntax, and, if you are writing a complex polyfill, managing dependencies requires ensuring correct script loading order (whether that means managing script element order or file concatenating order in a build step). Not to mention having to wrap each script in an IIFE.

Glen, I said "I don't recommend it" (as my personal opinion), I never said it can't be done. The reality is that we don't know yet how are we going to introduce new features after modules become ubiquitous, maybe new features will come in the form of modules that you have to require, and to polyfill new features you just need a synthetic module that exports the right API, we just don't know yet. But polyfills, in their current state, are simply runtime patches, and using them should NOT require a module system as today, they are effectible scripts. That does not means you can't write polyfills using ES6 syntax, you can, just transpile them to scripts :)

# Glen Huang (10 years ago)

Thank you for showing me the bigger picture.

If there ever come a day when web/stdlib APIs are exposed as modules, then polyfills are themselves modules.

But if modules aren’t supposed to patch the runtime as effectible scripts do, then I have the same question as Kyle does, what’s the use case for importing a module without binding it to an identifier?

# Kyle Simpson (10 years ago)

If you assign another variable from an imported binding, is that assignment done as a "reference to the binding" (aka creating another binding) or via normal reference-copy/value-copy assignment behavior?

export var a = 42;
export function b() { console.log("orig"); };
export function change() {
   a = 100;
   b = function b() { console.log("new"); };
};

And then:

import { a, b, change } from "...";

var x = a, y = b;

x;    // 42
y();  // "orig"

change();

a;    // 100
b();  // "new"

x;    // ??
y();  // ??

Will the final x and y() result in:

  • 42 / "orig"
  • 42 / "new"
  • 100 / "orig"
  • 100 / "new"
# caridy (10 years ago)

Kyle, we are NOT changing the semantic of the assignment expression. Your example has nothing to do with import/export ;)

# Kyle Simpson (10 years ago)

we are NOT changing the semantic of the assignment expression.

So the result is going to be 42 / "orig", right? :)

The reason I asked is not because I thought we were changing the semantic of the assignment expression, but because I wasn't sure if this top-level "const or whatever binding" was some sort of special thing that you can only hold a reference to. Clearly not.

Also, since y keeps a reference to the "orig"inal function reference imported, even if the module updates itself, this very well may affect those who (in places other than this thread, and for different reasons) have often suggested they plan to do stuff like:

import { y } from "..";

let x = y;
..
// use x now

In those cases, I was trying to find out if x could be updated by the module itself, like y can, because that matters (either good or bad) to the desire to use such a pattern. Since x can't get updated here, it's now clear to me that I wouldn't want to use such pattern, for fear that I am not using the latest API binding.

Thanks for clarifications!

# caridy (10 years ago)

Kyle, same answer, let x = y is just an assignment. btw, you cannot change y in that module, that's a static error.

# Mark S. Miller (10 years ago)

I have not been following this thread, so my response may be out of context. But I have to point out that

let x = y;

is not an assignment. It is an initialization.

# caridy (10 years ago)

ah, right, I was thinking about x = y. In any case, the same applies, import and export declarations will not break the mental model for assignments, initializations, etc, the result for the original example will be 42 / "orig".

# Allen Wirfs-Brock (10 years ago)

On Mar 18, 2015, at 9:34 AM, caridy wrote:

Kyle, same answer, let x = y is just an assignment. btw, you cannot change y in that module, that's a static error.

Assignment to 'y' a dynamic error. A while ago we eliminated all early errors that required static determination that their target was going to be an immutable binding.

# Brendan Eich (10 years ago)

Allen Wirfs-Brock wrote:

On Mar 18, 2015, at 9:34 AM, caridy wrote:

Kyle, same answer, let x = y is just an assignment. btw, you cannot change y in that module, that's a static error.

Assignment to 'y' a dynamic error. A while ago we eliminated all early errors that required static determination that their target was going to be an immutable binding.

Right. Import bindings are like getter-only accessors. Dynamic error to assign to them.