Modules feedback from March 2013 meeting

James Burke jrburke at gmail.com
Mon Mar 25 10:31:38 PDT 2013


I expect the module champions to be busy, so I am not expecting a
response. This is just some feedback to consider or discard at their
discretion. I'll wait for the next public update on modules to see
where things end up. In general, sounds promising.

I'm going off the meeting notes from here (thanks Rick and all who
make these possible!):
https://github.com/rwldrn/tc39-notes/blob/master/es6/2013-03/mar-12.md#42-modules

### Single Anonymous Export ###

The latest update was more about semantics, but some thoughts on how
single anonymous export might work:

Just use `export` for the single anonymous export:

module "m" {
    export function calculate() {}
}

where calculate is just the local name for use internally by the
module, but `calculate` is not visible to outside modules, they just
import that single anonymous export.

For exporting a named property:

module "n" {
    export calculate: function () {}
}

This would still result in a local `calculate`, let-equivalent local
name, but then also allows for other modules to import `calculate`
from this module.

Single anonymous export of something that is not a function:

module "crypto" {
    export let c = {
        encrypt: function () {},
        decrypt: function () {}
    }
}

Inside this module, `c` is just the local name within the module, not
visible to the outside world.

Syntax is hard though, so I will not be surprised if this falls down.

### Import ###

If the above holds together:

For importing single anonymous export, using the "m" above:

import calc from "m";

This module gets a handle on the single anonymous export and calls it
calc locally. The "n" example:

import { calculate } from "n";

---- start extremely speculative section:

This next part is very speculative, and the most likely of this
feedback to be a waste of your time:

"crypto" is a bit more interesting. It would be neat to allow:

let { encrypt } from "crypto";

which is shorthand for:

import temp from "crypto";
let { encrypt } = temp;

This could all work out because `from` would still be restricted to
the top level of a module (not nested in control structures). `from`
would be the parse hook for finding dependencies, and not `import`.

If refutable matching:
http://wiki.ecmascript.org/doku.php?id=harmony:refutable_matching

applied to destructuring allowed some sort of "throw if property is
not there" semantics (making this up, assume a ! prefix for that):

let { !encrypt } from "crypto";

This would give a similar validity check to what `import { namedExport
}` would give. It may happen later in the lifecycle of the module (so
when the code was run, vs linking time) but since `from` is top level,
it would seem difficult to observe the difference.

Going one step further:

With that capability, it may be possible to go without `import` at
all, at least at this stage of ES (macros later may require it). The
one case where I think `import` may help are cycles, but if the cycle
parts are placed in separate modules with a single export, it may
still work out. Using the assumption of single anonymous export and
the "odd even" example from the doku wiki:

module 'E' {
    let odd from 'O';
    export function even(n) {
        return n == 0 || odd(n - 1);
    }
}

module 'O' {
    let even from 'E';
    export function odd(n) {
        return n != 0 && even(n - 1);
    }
}

Going even further: then the `export publicName: value` syntax may not
be needed either.

My gut says getting to this point may not be possible. Maybe it is for
the short term/ES 6, but macros may require `import` and `export
publicName:`. When documenting the final design decisions, it would be
good to address where these speculative steps fall down, as I expect
there are some folks in the node community that would also take this
train of thought.

--- end extremely speculative section

### Loader pipeline hooks ###

I know more needs to be specified for this, so this feedback may be too early.

The examples look like they assign to the hooks:

System.translate = function () {}

Are these additive assignments? If more than one thing wants to
translate, is this more like addEventListener?

I recommend allowing something like AMD loader plugins, since they
allow participating in the pipeline without needing to be loaded first
before any other modules. It also allows the caller to decide what
hooks/transforms should be done, instead of some global handler
sneaking in and making the decision.

So, if a dependency is "text at some.html" (AMD loader plugins use !,
"text!some.html", pick whatever you all think works as a separator):

* load the text module.
* Grab the export value. If the export value has a property (or an
explicit export property) that matches one of the pipeline names,
normalize, resolve, fetch, translate, etc…, use those when trying to
process "some.html".

The main feedback here is to not expect to load all pipeline hooks up
front. This would break dependency encapsulation. It seems fine to
also allow those hooks to be specified up front before module loading
begins, but that should not be the only mechanism.

### Declarative loader configuration

Related to System.ondemand: I suggest considering the declarative
config worked out by AMD loaders:
https://github.com/amdjs/amdjs-api/wiki/Common-Config

I would probably use "alias" instead of "map" now, and I will likely
add a "layers" config that is similar to what "ondemand" is: given
this layer module ID, here are the fully qualified, **exact match**
module IDs in that file.

"shim" config has proved to be very useful when bridging the gap with
legacy code.

### Nested modules ###

This was explicitly stated as a non-goal, but it is something that
shows up in the AMD module world (library builds with almond wrapped
with a UMD style boilerplate for use by the outside world):

module "publicThing" {
  module "j" {}
  module "k" {}

  export …. //something visible outside publicThing
}

The idea being that internally "publicThing" uses some modules, but
they are only for its internal use, not for use outside of
"publicThing".

It would be nice to allow a chain of ID lookup tables, favoring a
local lookup table/closer tables and working up the chain to find a
match. If no match, a module is fetched/loaded and placed in the top
level table.

If something like the declarative "map" config from AMD is supported
in the loader, it would allow resolving possible top level ID/version
conflicts.

### Legacy opt-in use case ###

I did not see this explicitly mentioned in the use cases, but I
believe it would help the bootstrapping phase for ES modules if the
default loader also parses for `System.get` and `System.set` usage in
addition to the `import` or `from` new syntax. This would allow JS
libraries that need to operate in pre-ES6 worlds to still stick with
ES3-5 syntax but then be usable in a project that can just use ES6:

//Some base library that needs to be in ES5 syntax:
var dep1, dep2
if (typeof System !== 'undefined' && System.get) {
    //ES6 module loader. The loader will fetch and process
    //these dependencies before executing this file
    dep1 = System.get('dep1');
    dep2 = System.get('dep2');
} else {
    //browser globals case, assume the scripts have already loaded
    dep1 = global.dep1;
    dep2 = global.dep2;
}

The above, coupled with a declarative "shim" config would mean ES6
users could start using ES6 without needing another "script loader"
library to use their base libraries that will need to stay in
ES3-5-land for a while. This will avoid confusion about a "built in"
ES6 feature needing a helper script to use code that exists today.

However, for processing node/commonjs/AMD modules, it is fine to say a
helper script is needed that sets up the module loader hooks (for node
I expect it will be built-in anyway to their bootstrapping code).

James


More information about the es-discuss mailing list