ES Modules: suggestions for improvement

Isaac Schlueter i at izs.me
Wed Jun 27 13:06:35 PDT 2012


On Wed, Jun 27, 2012 at 11:51 AM, David Herman <dherman at mozilla.com> wrote:
> On Jun 27, 2012, at 11:15 AM, Isaac Schlueter wrote:
>
>> That was a bug caused by a lack of global isolation, which current
>> module systems cannot fix.
>
> It was caused by accidentally creating a global variable instead of a local variable. Not totally sure what you mean by global isolation? If you mean giving separate modules separate global objects, I don't agree that that would solve this kind of bug. He doesn't show us the whole code, but it looks like it was local code that was accessing the (accidentally) global variable, but probably different event handlers were interleaving and causing data races.

So, I've encountered two flavors of this in production, in my own
programs:  (The first in npm, the second in the no.de portal)

1. leaked global

app.route("/my/login", function (req, res) {
  x = res.query.x || 100
  if (x < 100) blerg()
})

In testing, you don't spot the leaked global, and things work, because
there's only one request at a time.  Linters all catch this, and tests
can check for leaked globals.

2. Module-local var

var x = someThing
// ... many lines later ..
app.route("/my/login", function (req, res) {
  x = res.query.x || 100
  if (x < 100) blerg()
})

A linter *won't* catch this, since it'll assume that you meant to do
exactly what you did.  Global isolation won't catch it either, and
neither will Harmony Modules or any existing require() thing.  (And in
fact, often you DO mean to do what this does!)

I think this is one of the cases where we just have to make
programming easier, by making choices that encourage smaller, more
discrete modules.


> Partly agree? I believe that obviating the *need* for globals is the core purpose of a module system. I don't believe that modules should necessarily be strictly separated. Modules should be given clean local scopes so that they don't overwrite each other, but that doesn't mean they shouldn't be able to still communicate via the global object.

Right, perhaps isolation is the wrong word.  Missing a "var" keyword
should not be so hazardous, that's what I'm saying.

> That bug was particularly bad because it was *assigning* to an accidentally global variable. But in my personal experience I certainly forget to import common libraries like 'path' and 'fs' in Node all the time and end up with unbound variable references. When this happens in a control flow that got missed by tests, then it can end up in production.

You mean something like this?

var fs = require('fs')
// no path here...
function notCoveredByTests () {
  fs.open(path.resolve("yabbadabba"), ...)
}

How would any of this solve that?


>> var Foo = require("foo")
>> var f = new Foo()
>
> Just import it directly:
>
> import Foo from "foo";
> var f = new Foo();

But wait... those are two different things, aren't they?  Isn't yours
more akin to: `var Foo = require('foo").Foo`?

> I just disagree. I think it's fine if you like that style [one module exports one thing], and you can use it. But we shouldn't force it on users.

I'm having trouble articulating why it is that module.exports=blah is
better than exports.blah=blah.  Surely, you can just choose to only
put one thing on the exports object, right?  It seems obviously better
to allow the flexibility, and I was strongly in favor of this early in
node's history.

However, after using it a lot, I've found that `exports.foo = bar`
often ends up being more painful than `module.exports = foo`, even
with the transitive issues.  I'm not sure why that is, and "Go write
couple hundred KLoC of module JS and then you'll get it" is not an
argument, I know.


> Moreover, it would be hostile to adding static constructs in the future, such as macros, that can be exported from a module.

Can you elaborate on that?


>> How does this proposal address transitive dependency cycles?
>
> Better than yours. ;-P
>
>> Unfinished export objects?
>
> The exports are all there from the beginning but uninitialized.

That's sort of like unfinished objects, then, but with the keys all
set to undefined.

So, then `export x = 10` hoists the `export x` and leaves the `x = 10`
where it is, var-like?

Does a_c === c, or not?


> Maybe! Happy to discuss. I don't believe there's that much boilerplate. In fact, there's *less* boilerplate than either CommonJS or AMD, and compared to your sketches on your post I suspect the differences in character count could be counted on one hand.

It's quite a lot of new syntax, including special syntax for things
that are not obviously required by the stated goals. (Not obvious to
me anyway, I'm definitely willing to be educated.)  The existing AMD
and CommonJS patterns are a good "must be at least this useful" bar.

But if we're changing the language, we should crush them and make them
no longer even worth considering, because this new thing is so good,
in the same way that ()=>{} absolutely cruses function(){}.bind(this).
 I consider AMD to be too much boilerplate, personally.  And while ES
modules have the capacity to fix some problems we can't fix with
CommonJS API alone, it does so by adding a lot of moving parts and
obviously more new syntax.

The cost of new syntax can be justified, clearly, but it IS a cost,
that's all I'm saying.  If we can add 2 new magic tokens instead of 5,
then I think that's a massive improvement.


More information about the es-discuss mailing list