Skip to content

Instantly share code, notes, and snippets.

@jbrantly
Created March 22, 2016 15:34
Show Gist options
  • Save jbrantly/29969e8448d40538832a to your computer and use it in GitHub Desktop.
Save jbrantly/29969e8448d40538832a to your computer and use it in GitHub Desktop.
The problem with the CJS-ES6 gap
// ES6 exports
export var foo = 1
export var bar = true
// CJS analog
module.exports = {
foo: 1,
bar: true
}
// Importing all exports from the above ES6 module
import * as mod from 'module'
// I think it can be argued that if you're allowing CJS to be imported using ES6 syntax, then
// the above CJS module could also be imported using:
import * as mod from 'module'
// However, if you allow the above, then it in turn also means you can do this:
// CJS module
module.exports = function foo() {}
// Importing...
import * as func from 'module'
@bmeck
Copy link

bmeck commented Mar 22, 2016

Completely disagree as * imports the ModuleNamespaceObject, which is a dictionary of [identifier,value] pairs, keys are

  • identifier is a valid JS identifier, it is not any given string
  • it is always and object and has [[Get]] so property access works.

CJS always exports a single value which is the value of module.exports this does not have multiple values. A great example to illustrate is:

module.exports = null

null is a single primitive immutable value, with no properties; it cannot represent a module namespace by its very nature since it cannot have property access.

So! How do you turn this into a module namespace? It is a single value. Use the same facilities that default exports use.

module.exports = null

becomes a ModuleNamespaceObject ~=:

namespace = {
  default: null
}

Another example:

module.exports = JSON.parse

Since ModuleNamespaceObject cannot use [[Call]] they cannot be treated as functions, once again, move it to default

Another example:

module.exports = {"not valid id":true}

not valid id is not a valid JS identifier, it cannot be put onto a ModuleNamespaceObject.

So! Why does import {foo} from 'bar' work?

Hoisting. After getting

module.exports = {foo:1};

Transpilers and interop "hoist" the property onto the module namespace using getters (or raw property copy [this has problems if they don't use getters]).

So the namespace becomes:

namespace = {
  default: {foo:1}
  get foo() {return namespace.default.foo}
}

@jbrantly
Copy link
Author

@bmeck First off, just to clarify my position, I'm not really arguing for or against how things should be done. I'm trying to address the original question by @RyanCavanaugh:

Baffled by folks wanting to use ES6 import syntax to do things ES6 modules can't do (e.g. importing functions with "import * as fn ...")

This is how people arrive at that conclusion: conceptually, not technically. You make a lot of great technical points, but let me talk about some conceptual stuff for a second.

CJS always exports a single value which is the value of module.exports this does not have multiple values

While technically true, this was not the original concept. From the CJS Spec:

  • In a module, there is a free variable called "exports", that is an object that the module may add its API to as it executes.
    • modules must use the "exports" object as the only means of exporting.

So conceptually, CJS originally did export a dictionary (I use the term loosely, not technically) of multiple values that represented the API of the module. In fact, the early code typically looked like this:

exports.foo = 1
exports.bar = true

I believe Node (not the spec) introduced the concept of setting the exports object directly by using module.exports =. It was also a debated topic.

So I feel like people coming from a CJS background understand that while a CJS module might return a single value, often the conceptual intent is to be a dictionary of values that constitute the API of the module (eg, the original spec). I think this is in the same conceptual boat as ModuleNamespaceObject albeit with some real technical differences that you've pointed out.

So, if someone had that in mind and is being introduced to ES6 modules and they see something like

export var foo = 1
export var bar = true

I think it's reasonable that they can immediately see the similarities with CJS there. Then they see

import * as mod from 'module'

as the way to import the whole dictionary of exports from an ES6 module. It's not much of a leap to say that if you're doing CJS/ES6 interop, it might make sense that the same syntax would import the conceptual dictionary of exports from a CJS module as well.

So now import * means, to them, the "whole" of the module. What do you do when you encounter a CJS module that uses module.exports =? Not a big leap to go to the "whole" of the module.

Again, I'm not disputing the technical points that you've mentioned, I'm trying to explain how a normal JS coder (not someone who's deep into specs and interop loaders) could think that it's OK to do import * to import a module that has module.exports = function () {}, and how while that might be a technical issue, I think it's fair to say there is some conceptual basis to the argument.

@bmeck
Copy link

bmeck commented Mar 22, 2016

Actually there are a bunch of differences that are visible from ModuleNamespaceObjects, it should become pretty apparent for some cases like module.exports = Promise.resolve(1) since .then would fail (anything using this for that matter will probably have problems), primitives would be boxed as objects, the namespace is frozen so it is read-only and cannot be extended. Explaining hoisting is just something that will have to be done if we want named imports to work with CJS.

@bmeck
Copy link

bmeck commented Mar 22, 2016

I heavily recommend people use import mydefault from 'foo' over * when doing interop with CJS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment