Tuesday, June 26, 2012

Comments on Isaac's ES modules post

Isaac Schlueter posted some thoughts on the ES modules proposal. He works on Node and NPM, so it is great to see things from his perspective vs. my browser-based perspective.

I believe his post an my previous ES modules post fit together well. Here is some feedback on Isaac's post to point out where we align and what may still need more discussion. Section titles and numbered points below match the ones in Isaac's post.

Problems with the Current Spec

1. It seems to be based on the assumption that nesting module systems is a thing that people want.

As Isaac says, "no one wants to write a module system". Agreed. The default behavior of a built in loader should be enough for most, nearly all people. Others can build their own systems with existing tech, and if they catch on, consider them later.

I prefer to have support for loader plugins that have a constrained API. I'm not sure how that fits with Isaac's view. My previous post outlines uses cases where AMD folks have found them useful, and they help reduce the "pyramid of doom" of callbacks for resources that are important for a module to do its work, and therefore reduces the complexity of the module's external API. If Isaac has a different way to solve those issues, it would be good to know.

2. It puts too many things in JavaScript (as either API or syntax) which belong in the host (browser/node.js).

I think Isaac means that the browser implements a default Loader.resolve(), and Node does its own thing, but the language does not have a built default.

I think there is value in using the browser model as the default language one, with Node having the ability to override as it sees fit. The browser model of single file IO lookup based on a declarative config can work in "everything is available locally" model.

But for me this is a small distinction. I do not relish trying to convince a separate standards body that is even more unwieldy than es-discuss to code the default browser resolver, but if that is how it must go, so be it.

My main desire: the front end developer should not have to ship a library file to do imperative configuration of a loader.

3. It borrows syntax from Python that many Python users do not even recommend using.

Agreed. I would not include import * if it were just up to me. The "evaluate dependencies first before evaluating the current module" means that just normal let/var destructuring could be used, no more need for import at all. All that is needed is a way to call out a dependency either via a keyword like "from" or via an API like "from()" or "require()". Or reusing "import" to be simpler, as Isaac does later in the post.

4. It favors the “object bag of exported members” approach, rather than the “single user-defined export” approach.

I too prefer a way to set the exported value (return in AMD parlance, module.exports = in Node).  There needs to be a way to allow just the "exports" property assignment for circular dependencies, but for everything else (the vast majority of modules) assignment is nice.

I believe that exports approach in the ES design was chosen so that they can do the type checking of export values, supports circular dependencies, and with destructuring, it was argued that it was more palatable for getting the export value with this model:

import {jQuery} from 'jquery';

Not my favorite, but I could live with it.

The "middle way" evaluation approach would support module value assignment.

A Simpler Proposal

1. A Loader built-in object, with a few methods that must be specified before modules can be used.

See feedback above. If a front end developer needs to ship a JS library to bootstrap the Loader, that is a failure condition for me.

I can understand exposing a Loader API so that for example someone could make an AMD API shim, but the ideal, future case should be no need for imperative setup. Simple declarative setup only, and just for how to deal with module IDs-to-paths, and possibly a declarative shim config for legacy scripts. Note that this config is about specific modules, not implementing the general module/resolution API.

2. Within a module, the import pathstring syntax that can be easily detected statically in program text before evaluation, and returns a module’s exported object.

I was thinking in terms of "from", but "import" is fine if it is used like this. I do not care about the name.

I also prefer an API that could be used/detected by browser scripts that need to live in the on-ES modules world and the new one.

This is what AMD loaders do today for the sugared form, except it looks for require('string literal').

3. Loader.define(path, program text) defines a module at the specified path, with the program text contents.

I suggest using "module ID" instead of path here. Once modules are combined, it is best to refer to logical IDs, like 'jquery' instead of 'my/specific/path/to/jquery.js', because it makes it hard to combine sets of built modules together.

AMD loaders use module IDs, and it works out well for us.

This Loader.define call is similar to the capabilities in AMD. AMD uses a function (require, exports, module) {} wrapper for , but I can see where perhaps an ES loader could just get by with {}.

No comments on 4,5,6. Those are about the resolver API, which I do not care too much as long as what is used in the browser means an app developer does not have to ship another library to use it.

7. Within a module, the export expression statement marks the result of expression as the exported value from the module.

As Isaac alluded, I think circular dependencies will kill this one. Or at least, there needs to be a way to do what is possible in Node/AMD where an exports object can be created for a module before it executes, and that exports is available for other dependencies in a cycle.

8. The global object within a module context is equivalent to Object.create(global) from the main global context. and:
9. If a module does not contain an export statement, then its global object is its export. 

This is tricky since some libs may export more than one global. I would probably still favor allowing all scripts loaded within a container to share the same "global" space.

With a shim config, this would allow consuming scripts that use browser globals, and for using library plugins that use a browser global to attach functionality (jQuery or Backbone plugins for example).

I wouldn't mind a declarative loader config that allowed for #8, not sure if it should default to on or off. Maybe on, and if using legacy scripts, you have to do a bit of work by configuring it to off.

This area needs more thought, but my general goal is not needing a library.js to set up imperative loader APIs to do loading in the browser. As long as that works out, I will probably be fine with it.

In Web Browsers

As mentioned above, I believe it is better to use module ID instead of <path> for the name of a module via Loader.define(), but to use module IDs instead. That would probably mean transforming the script tag to be script suggestion to script module="moduleId" src="".

But really, I'm fine with just a JS API for this, so just doing System.load('id'); or Loader.load('id') in an inline script tag is fine by me.

What’s Missing from this Proposal

For me, Loader.define('id', {}) is just another way to say module 'id' {}. I like having an API instead of/in addition to a keyword, for shimming something that could work in today's browsers, although in this case shimming may not be important. So I'm neutral on that. module 'id' {} is slightly shorter.

As for sourcemaps: maybe what was meant there was sourceURL, and I agree, it would be good if the Loader.define() method or whatever it is automatically got the same script debugger treatment similar to what sourceURL gets today.


Anonymous said...

Mr. Schlueter wrote:
Anyone who actually enjoys writing module systems is too insane to be trusted.

You seem to be enjoying this necessary evil ☺

James Burke said...

skierpage: I would prefer to not be writing one. If the one in es harmony is good enough for browser use and gets adoption, I will be happy to retire from it.