Skip to content
This repository was archived by the owner on Sep 2, 2023. It is now read-only.
This repository was archived by the owner on Sep 2, 2023. It is now read-only.

Feature: Named exports when importing CJS #81

Description

@giltayar

Currently in NodeJS, if I import from a CJS module,

import ns from `./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:

  1. Continue the existing way (no named exports for CJS)
  2. Don’t conform to the spec when importing CJS
  3. Do late-linking/shaping of named modules based on late evaluation of CJS module
  4. Disallow transparent interop, and enable import.meta.require (or equivalent) to enable importing CJS from ES modules
  5. 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.

Edit by @GeoffreyBooth: Use case 12.

Activity

  1. giltayar commented on May 14, 2018

    @giltayar
    Author

    This issue was raised in #80

  2. GeoffreyBooth commented on May 14, 2018

    @GeoffreyBooth
    Member

    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.

  3. devsnek commented on May 14, 2018

    @devsnek
    Member

    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);

  4. ljharb commented on May 14, 2018

    @ljharb
    SponsorMember

    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.

  5. GeoffreyBooth commented on May 14, 2018

    @GeoffreyBooth
    Member

    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.

  6. mcollina commented on May 14, 2018

    @mcollina
    SponsorMember

    I think we should bring this question to TC39.

  7. ljharb commented on May 14, 2018

    @ljharb
    SponsorMember

    @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.

  8. MylesBorins commented on May 14, 2018

    @MylesBorins
    Contributor
  9. jdalton commented on May 14, 2018

    @jdalton
    Member

    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.

  10. GeoffreyBooth commented on May 14, 2018

    @GeoffreyBooth
    Member

    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?

  11. devsnek commented on May 14, 2018

    @devsnek
    Member

    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

  12. bmeck commented on May 14, 2018

    @bmeck
    Member

    @GeoffreyBooth there have been a variety of approaches implemented and or looked at but they seem to fall into 3 real areas of investigation.

  13. jdalton commented on May 14, 2018

    @jdalton
    Member

    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.

  14. bmeck commented on May 14, 2018

    @bmeck
    Member

    @jdalton it relies on the late linking in the last link I provided to my knowledge.

  15. 82 remaining items

  16. guybedford commented on May 23, 2018

    @guybedford
    Contributor

    @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.

  17. bmeck commented on May 23, 2018

    @bmeck
    Member

    @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.

  18. ljharb commented on May 23, 2018

    @ljharb
    SponsorMember

    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)

  19. guybedford commented on May 23, 2018

    @guybedford
    Contributor

    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.

  20. devsnek commented on May 23, 2018

    @devsnek
    Member

    @guybedford polyfills and circular requires aren't nothing. if your polyfill is written in esm and we do out-of-band you're screwed.

  21. guybedford commented on May 23, 2018

    @guybedford
    Contributor

    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.

  22. bmeck commented on May 23, 2018

    @bmeck
    Member

    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.

  23. ljharb commented on May 23, 2018

    @ljharb
    SponsorMember

    It's not just polyfills; it's anything that sets up state. Bootstrapping react flux stores, connecting to databases, configuring stateful libraries, etc.

  24. targos commented on May 23, 2018

    @targos
    Member

    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']

  25. bmeck commented on May 23, 2018

    @bmeck
    Member

    @targos we do not, and even if we did there are problems with creating module records from them. Particularly if 2 modules have

    import {a} from 'cjs';

    and

    import {b} from 'cjs';

    What is the shape of the module that we create? Or what if they don't list the names:

    import * as cjs from 'cjs';
    const cjs = import('cjs');
  26. guybedford commented on May 23, 2018

    @guybedford
    Contributor

    @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.

  27. targos commented on May 23, 2018

    @targos
    Member

    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...

  28. dandv commented on Sep 26, 2018

    @dandv

    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 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?

  29. devsnek commented on Sep 26, 2018

    @devsnek
    Member

    @danbev thanks for chiming in. i believe extensionless main has 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).

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions