You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Sep 2, 2023. It is now read-only.
Repository navigation
This repository was archived by the owner on Sep 2, 2023. It is now read-only.
Currently in NodeJS, if I import from a CJS module,
importnsfrom`./cjs-module.js`
It will allow this, but won’t allow named exports, i.e. the following is not allowed:
import{namedSomething}from`./cjs-module.js`.
This is called transparent interop.
The reason that NodeJS doesn’t allow named exports is that determining what those exports are MUST happen in the parsing phase of the ESM Spec (according to some, although there is contention about that too), and executing code is not allowed at or prior to that phase. But a CJS module’s exports can only be determined by evaluating the code in it (static analysis of JS is not an option as it is not determined to always give the correct results).
This would maybe have been cool if NodeJS was the first ESM implementation, but people are used to the babel way of doing modules, and in the babel world, named exports from CJS modules are allowed. This is because babel does not conform to the ESM spec to the letter (it can’t, because it just transpiles ESM to CJS).
And, good or bad, developers expect to use named exports when importing CJS.
I see these options:
Continue the existing way (no named exports for CJS)
Don’t conform to the spec when importing CJS
Do late-linking/shaping of named modules based on late evaluation of CJS module
Disallow transparent interop, and enable import.meta.require (or equivalent) to enable importing CJS from ES modules
Enable metadata in the CJS module that can statically describe the exports for ESM, e.g. something like //export default; export foo, bar; at the head of the CJS file, thus enabling named exports when importing the file.
I am sure there are others options, so if you have another option besides those four, please add it here.
It would be great if for each options you specify pros and cons, or at least if you don’t like the option, specify a clear and simple use case that would be problematic if the option was chosen.
It’s worth reading the comments in #80 too, especially concerning the observability (or not) of what gets done in the parsing phase.
I would encourage us to try to find a solution that threads the needle if at all possible. This probably isn’t a stark decision between honoring the spec or not. I bet we can find a solution that enables this while still adhering to spec, or adhering to the spec as far as any user-executable code could ever know, or adheres mostly to spec. I think it’s worth exploring the gray area as much as possible.
personally I'm not a fan of importing cjs at all. (as a bonus we could drop the mjs extension if we didn't support it.) I would prefer something like import { makeRequire } from 'module'; const require = makeRequire(import.meta.url);
A very large number of the use cases we've documented require being able to import 'cjs' and require('esm') - that's probably one of the more contentious issues overall, of which "should import 'cjs' support named imports" is a small subset.
I think spec compliance is paramount, and I'd love to have import { Children } from 'react', for example, work (where "react" is a CJS module) - but I suspect we'd have to come up with a way to get consensus to change the spec for that to become tenable.
Some of the discussion in #80 revolved around how to define “spec compliance.” If external code can’t see what Node is doing under the hood, and if Node is compliant as far as the external code can detect, than can that be considered “spec compliance”? Because if so, then that’s a way to have it both ways: do whatever parsing or evaluating that needs to be done in the parsing phase, in a non-observable way that has no side effects, and you get the ability to import CommonJS named exports without violating the spec. The trick is figuring out that “non-observable way that has no side effects.” But I think that’s a straightforward engineering problem that can be solved.
@GeoffreyBooth i believe that it's always possible to write code that observes the ordering of evaluation and linking, which is why it would require a spec change in EcmaScript to have node do that and be compliant.
@mcollina I believe @bmeck has already done so, but if there's new information it might be worth another shot.
I'm cool having CJS evaluate before resolving named exports. It's what we're doing for builtins modules today (with the assumption errors/side-effects won't happen during evaluation). For the broader case, of more than just builtin modules, if errors do happen during evaluation I don't find the difference that ghastly, since that is something CJS users expect. The concern is scoped to CJS interop, so isn't something that bleeds into browser interop scenario, and can be something that is opted-in-or-out of as needed without broader approval from others like the TC39.
Reacted by Anna Henningsen, Geoffrey Booth, Benjamin Gruenbaum and Nick E
That’s another good point—could we do this evaluation during the parsing phase for CommonJS imports only? Because the spec doesn’t concern itself with CommonJS, right? So however Node wants to handle CommonJS is up to us, so long as we follow spec with regard to true ES modules?
its worth noting that (afaik, please correct me if i'm wrong) source text refers to whatever the original thing is that rules what is exposed from the import, which is in this case the cjs
As far as I know the user-land esm loader works with named exports of CJS modules in ESM without order of evaluation issues or pre-parsing CJS export pragmas (named-export-core). This means that the root issue may be something deeper like being able to perform creation, instantiation, and evaluation phases synchronously.
@bmeck yes that is the cost of the approach, that exact execution ordering only breaks down between imports of different module formats, but weighed against an entire ecosystem of expectations it seems a worthwhile one to me.
@guybedford I'm not sure how this is only a specific minority of imports, it affects any import that may change formats over time. And, when that import changes formats it will be hard to debug.
The ecosystem also has an expectation - for both require and import - that everything evaluates in lexical order. I think that’s much more important than named imports from CJS (as long as there’s at least default imports of CJS)
I've yet to see even two CommonJS packages on npm that require exact execution order between them that aren't polyfills - I have personally never written a NodeJS app that had to carefully order require statements apart from dealing with circular references.
Predictable semantics are incredibly important yes, but two-phase execution like this was the original plan for CommonJS laid out by TC39 to begin with through zebra striping. It was never deemed a spec violation then, so I'm not sure why it should be so now.
If your npm package requires a polyfill to be used and is written in CommonJS, then yes, you would need your polyfill to be written in CommonJS as well.
I'm not convinced that removing ordering predictability is worth getting named exports when we can just read the properties off a default value. That costs the ecosystem just like not having named exports does. However, I value predictability over named exports since I cannot recreate the feature of predictability/debugging as easily as I can recreate the feature of named exports.
It's not just polyfills; it's anything that sets up state. Bootstrapping react flux stores, connecting to databases, configuring stateful libraries, etc.
In the internal loader, do we have access to the names that are being imported?
For example, with import { default as x, y, z } from 'cjs', that would be ['default', 'y', 'z']
@ljharb most packages in the JS ecosystem require a top-level call to start these things. I've never once used a library that did any of that work on top-level execution - please by all means find a counter example though.
I know much less than I wish about modules, but I did manage to publish an ESModule that's backwards-compatible with CJS, by setting main to index (no extension) in package.json. Would this method be worth advocating to module authors, as (obviously) an interim and partial solution until this issue reaches consensus and resolution?
Activity
giltayar commentedon May 14, 2018
This issue was raised in #80
GeoffreyBooth commentedon May 14, 2018
It’s worth reading the comments in #80 too, especially concerning the observability (or not) of what gets done in the parsing phase.
I would encourage us to try to find a solution that threads the needle if at all possible. This probably isn’t a stark decision between honoring the spec or not. I bet we can find a solution that enables this while still adhering to spec, or adhering to the spec as far as any user-executable code could ever know, or adheres mostly to spec. I think it’s worth exploring the gray area as much as possible.
devsnek commentedon May 14, 2018
personally I'm not a fan of importing cjs at all. (as a bonus we could drop the mjs extension if we didn't support it.) I would prefer something like
import { makeRequire } from 'module'; const require = makeRequire(import.meta.url);ljharb commentedon May 14, 2018
A very large number of the use cases we've documented require being able to
import 'cjs'andrequire('esm')- that's probably one of the more contentious issues overall, of which "shouldimport 'cjs'support named imports" is a small subset.I think spec compliance is paramount, and I'd love to have
import { Children } from 'react', for example, work (where "react" is a CJS module) - but I suspect we'd have to come up with a way to get consensus to change the spec for that to become tenable.GeoffreyBooth commentedon May 14, 2018
Some of the discussion in #80 revolved around how to define “spec compliance.” If external code can’t see what Node is doing under the hood, and if Node is compliant as far as the external code can detect, than can that be considered “spec compliance”? Because if so, then that’s a way to have it both ways: do whatever parsing or evaluating that needs to be done in the parsing phase, in a non-observable way that has no side effects, and you get the ability to import CommonJS named exports without violating the spec. The trick is figuring out that “non-observable way that has no side effects.” But I think that’s a straightforward engineering problem that can be solved.
mcollina commentedon May 14, 2018
I think we should bring this question to TC39.
ljharb commentedon May 14, 2018
@GeoffreyBooth i believe that it's always possible to write code that observes the ordering of evaluation and linking, which is why it would require a spec change in EcmaScript to have node do that and be compliant.
@mcollina I believe @bmeck has already done so, but if there's new information it might be worth another shot.
MylesBorins commentedon May 14, 2018
jdalton commentedon May 14, 2018
I'm cool having CJS evaluate before resolving named exports. It's what we're doing for builtins modules today (with the assumption errors/side-effects won't happen during evaluation). For the broader case, of more than just builtin modules, if errors do happen during evaluation I don't find the difference that ghastly, since that is something CJS users expect. The concern is scoped to CJS interop, so isn't something that bleeds into browser interop scenario, and can be something that is opted-in-or-out of as needed without broader approval from others like the TC39.
GeoffreyBooth commentedon May 14, 2018
That’s another good point—could we do this evaluation during the parsing phase for CommonJS imports only? Because the spec doesn’t concern itself with CommonJS, right? So however Node wants to handle CommonJS is up to us, so long as we follow spec with regard to true ES modules?
devsnek commentedon May 14, 2018
i actually proposed this way back in my first pr (nodejs/node#16675) and TC39 discussed it here: https://github.com/rwaldron/tc39-notes/blob/master/es8/2017-11/nov-28.md#9iie-discuss-module-order-instantiationevaluation-guarantees
its worth noting that (afaik, please correct me if i'm wrong) source text refers to whatever the original thing is that rules what is exposed from the import, which is in this case the cjs
bmeck commentedon May 14, 2018
@GeoffreyBooth there have been a variety of approaches implemented and or looked at but they seem to fall into 3 real areas of investigation.
loader, docs, test: named exports from commonjs modules node#16675 which did out of order evaluation.
https://github.com/bmeck/node/tree/named-export-core did pre-parsing to form the shape instead of evaluation.
https://github.com/rwaldron/tc39-notes/blob/master/es7/2016-09/sept-28.md#es-modules-lifecycle discussed late linking (shape of dependencies not verified until evaluation reached).
jdalton commentedon May 14, 2018
As far as I know the user-land
esmloader works with named exports of CJS modules in ESM without order of evaluation issues or pre-parsing CJS export pragmas (named-export-core). This means that the root issue may be something deeper like being able to perform creation, instantiation, and evaluation phases synchronously.bmeck commentedon May 14, 2018
@jdalton it relies on the late linking in the last link I provided to my knowledge.
82 remaining items
guybedford commentedon May 23, 2018
@bmeck yes that is the cost of the approach, that exact execution ordering only breaks down between imports of different module formats, but weighed against an entire ecosystem of expectations it seems a worthwhile one to me.
bmeck commentedon May 23, 2018
@guybedford I'm not sure how this is only a specific minority of imports, it affects any import that may change formats over time. And, when that import changes formats it will be hard to debug.
ljharb commentedon May 23, 2018
The ecosystem also has an expectation - for both require and import - that everything evaluates in lexical order. I think that’s much more important than named imports from CJS (as long as there’s at least default imports of CJS)
guybedford commentedon May 23, 2018
I've yet to see even two CommonJS packages on npm that require exact execution order between them that aren't polyfills - I have personally never written a NodeJS app that had to carefully order require statements apart from dealing with circular references.
Predictable semantics are incredibly important yes, but two-phase execution like this was the original plan for CommonJS laid out by TC39 to begin with through zebra striping. It was never deemed a spec violation then, so I'm not sure why it should be so now.
devsnek commentedon May 23, 2018
@guybedford polyfills and circular requires aren't nothing. if your polyfill is written in esm and we do out-of-band you're screwed.
guybedford commentedon May 23, 2018
If your npm package requires a polyfill to be used and is written in CommonJS, then yes, you would need your polyfill to be written in CommonJS as well.
bmeck commentedon May 23, 2018
I'm not convinced that removing ordering predictability is worth getting named exports when we can just read the properties off a
defaultvalue. That costs the ecosystem just like not having named exports does. However, I value predictability over named exports since I cannot recreate the feature of predictability/debugging as easily as I can recreate the feature of named exports.ljharb commentedon May 23, 2018
It's not just polyfills; it's anything that sets up state. Bootstrapping react flux stores, connecting to databases, configuring stateful libraries, etc.
targos commentedon May 23, 2018
In the internal loader, do we have access to the names that are being imported?
For example, with
import { default as x, y, z } from 'cjs', that would be['default', 'y', 'z']bmeck commentedon May 23, 2018
@targos we do not, and even if we did there are problems with creating module records from them. Particularly if 2 modules have
and
What is the shape of the module that we create? Or what if they don't list the names:
guybedford commentedon May 23, 2018
@ljharb most packages in the JS ecosystem require a top-level call to start these things. I've never once used a library that did any of that work on top-level execution - please by all means find a counter example though.
targos commentedon May 23, 2018
For the first case, my idea was to create two different module records. I didn't think about the namespace import. That's a dead-end...
dandv commentedon Sep 26, 2018
Neophyte chiming in.
I know much less than I wish about modules, but I did manage to publish an ESModule that's backwards-compatible with CJS, by setting
maintoindex(no extension) inpackage.json. Would this method be worth advocating to module authors, as (obviously) an interim and partial solution until this issue reaches consensus and resolution?devsnek commentedon Sep 26, 2018
@danbev thanks for chiming in. i believe extensionless
mainhas indeed been brought up before.the real meat of this thread, however, is dealing with modules that have no esm source (the vast majority of the npm registry).