Skip to content

Discussion: New “ESM by default” mode #49432

Description

@GeoffreyBooth

Building off of #49295 (comment), we’re considering a new mode where all of the current places where Node defaults to CommonJS would instead default to ESM. I think this should be enabled by flag, and once it ships we can potentially ship a separate binary where it’s enabled by default; and/or we can someday make the new mode Node’s default in a semver-major change.

The use cases solved by such a mode are (and I’ll edit this comment to update this list as people think of more):

  • I want to write ESM syntax by default without needing to opt into it, such as via package.json file or file extension or --input-type=module.

  • I want to write what are typically called shell scripts in ESM JavaScript, where I can have a single extensionless file anywhere on my disk that uses ESM syntax and it doesn’t need a nearby package.json file or a symlink or wrapper file in order to run as ESM.

  • I want consistency in how the CLI handles references to files; currently --import accepts URL strings but the main entry point must be a path string (and therefore can’t be a data: URL or https: URL, such as to work with --experimental-network-imports).

  • If this mode becomes enabled by default in a future version of Node, when using that version of Node I want to be able to start a new project and write ESM syntax without needing to take any steps to opt into enabling ESM support.

To handle these use cases, the changes that this mode should make are (and likewise I’ll edit this to reflect consensus):

  • When a .js file is outside of any defined package.json scope, because there are no package.json files in its folder or any folders above it, it will be treated as ESM JavaScript.

  • When a .js file is in a package scope where its nearest parent package.json lacks a "type" field, it will be interpreted per the conditions we establish in Discussion: In an ESM-first mode, how should a package.json file with no type field be handled? #49494: when the package scope is under a folder named node_modules, the file will be treated as CommonJS; otherwise it will be treated as ESM.

  • When an extensionless file is outside of any defined package.json scope, it can be run as an entry point or imported per the conditions we establish in Discussion: In an ESM-first mode, how should extensionless entry points be handled? #49431: if the file begins with the Wasm magic bytes, it will be evaluated as Wasm, and likewise for any other future file types with defined headers that don’t conflict with JavaScript; otherwise it will be evaluated as ESM JavaScript. (Still subject to --experimental-wasm-modules until that stabilizes.)

  • Input from STDIN or --eval would be treated as ESM unless --input-type=commonjs is passed. Or in other words, the default value of --input-type would flip from commonjs to module.

  • The CLI will parse the main entry point as an URL string, similar to how the value of --import is parsed. process.argv[1] will not be parsed by the CommonJS loader.

This new flag will be named per the discussion in #49541.

Prior art: #32394, #49295, #49407

@LiviaMedeiros @nodejs/loaders @nodejs/wasi @nodejs/tsc

Activity

  1. added
    moduleIssues and PRs related to the module subsystem.
    cliIssues and PRs related to the Node.js command-line interface.
    esmIssues and PRs related to the ECMAScript Modules implementation.
    wasmIssues and PRs related to WebAssembly.
    wasiIssues and PRs related to the WebAssembly System Interface.
    on Sep 1, 2023
  2. mcollina commented on Sep 1, 2023

    @mcollina
    SponsorMember

    I don't think this is feasible to change the default if package.json is present due to the massive amount of modules available on npm.

    My 2 cents on how to solve this in a much easier way: let's make npm init add the necessary field in package.json.

  3. GeoffreyBooth commented on Sep 1, 2023

    @GeoffreyBooth
    MemberAuthor

    I don’t think this is feasible to change the default if package.json is present due to the massive amount of modules available on npm.

    I’ve considered this and I don’t think it’s a blocker. As discussed in #49295 (comment), there are many potential solutions, the simplest of which is to just crawl through subfolders adding "type": "commonjs" to package.json files that lack it. I don’t think this needs to be a worry or something we need to deal with at this point.

    Also, the proposal isn’t to go straight to changing Node’s default. That will be months if not years away, once this flag has shipped for a long time and issues have been resolved. The time for considering the impact of changing Node’s default is once this flag has been available for a while and we can see how it works. We may never change Node’s default, and that’s fine; this flag and mode has merit regardless. The point of mentioning that this might lead to changing Node’s default is so that we design with that potential goal in mind, so that whatever we add has a clear way to opt out if/when it becomes the new default.

  4. bmeck commented on Sep 1, 2023

    @bmeck
    Member

    @GeoffreyBooth how does adding that field work for people using other languages/processes that spawn child processes for existing codebases? I'm mostly curious since it is a much larger breaking change than most we see and affects all sorts of runners of node applications in non-simplistic ways.

  5. GeoffreyBooth commented on Sep 1, 2023

    @GeoffreyBooth
    MemberAuthor

    how does adding that field work for people using other languages/processes that spawn child processes for existing codebases? I’m mostly curious since it is a much larger breaking change than most we see and affects all sorts of runners of node applications in non-simplistic ways.

    First off, adding a flag isn’t a breaking change. The breaking change would only be if we make the flag the new default behavior, which I don’t think we need to worry about just yet. It’s premature to consider larger ecosystem effects of making this new mode the default when we haven’t designed, much less implemented, the flag to enable this new mode.

    The flag is no different than achieving these effects via module customization hooks (the changes that the hooks can achieve, anyway). Just as other languages/processes wouldn’t know how you’re customizing the files in your project when using hooks, they wouldn’t know whether your project is meant to run under this new flag or not. This is a problem we already have, and it’s on those other tools to provide a way for the user to signal to them that they should expect Node to be in this new mode, like how tsconfig.json is used. Shipping the flag long before we change Node’s default allows the ecosystem time to adapt.

    One thing we can do to help ease the transition is to start making the "type" field mandatory. As in, you don’t need to have a package.json file, but if you have one, it needs a "type" field. Then at least for applications, which are our dominant use case, there’s still a deterministic way for tools that work with apps to know how all the files within that application should be interpreted. We could make this one of the effects of the flag, so that in the new mode we need explicit package.json types.

  6. bmeck commented on Sep 1, 2023

    @bmeck
    Member

    @GeoffreyBooth I'm not opposing the flag in any way, I'm just concerned about the default swap which has been attempted in the past in #32394

  7. bmeck commented on Sep 1, 2023

    @bmeck
    Member

    Then at least for applications, which are our dominant use case, there’s still a deterministic way for tools that work with apps to know how all the files within that application should be interpreted. We could make this one of the effects of the flag, so that in the new mode we need explicit package.json types.

    I'm more skeptical on this due to files being loaded for config/init like ~/.*rc files outside of the application directory.

  8. joyeecheung commented on Sep 1, 2023

    @joyeecheung
    Member

    I think the idea of having multiple releases in general is not great - if we want to do it, builds with pointer compression enabled would’ve been a thing already and then we would have another headache about what combinations of releases we want to maintain….

    A runtime flag that makes ESM the default sounds good to me though, why can’t we just have that and allow it in NODE_OPTIONS? That should be enough for the use cases mentioned in the OP already

  9. GeoffreyBooth commented on Sep 1, 2023

    @GeoffreyBooth
    MemberAuthor

    I’m more skeptical on this due to files being loaded for config/init like ~/.*rc files outside of the application directory.

    There’s an infinitely long tail of ecosystem tools and libraries that don’t support whatever latest and greatest features we ship. Several years ago there were presumably tools that relied on the fact that .js files were CommonJS, and those tools needed to update to check for the type field before making that assumption; this is no different. If we limit ourselves by what today’s tools expect out of Node, then we’d never be able to change anything, even additive/non-breaking changes. Users can make the choice of deciding to use this flag or deciding to use a tool that's incompatible with it.

    I think the idea of having multiple releases in general is not great

    What do you mean by “multiple releases”?

  10. joyeecheung commented on Sep 1, 2023

    @joyeecheung
    Member

    What do you mean by “multiple releases”?

    multiple binaries, e.g. if you can have node-esm, then you can also also have node-pc (pointer compression), then you can also have a combination of node-esm-pc, then you can have…other combinations

  11. GeoffreyBooth commented on Sep 1, 2023

    @GeoffreyBooth
    MemberAuthor

    What do you mean by “multiple releases”?

    multiple binaries, e.g. if you can have node-esm, then you can also also have node-pc (pointer compression), then you can also have a combination of node-esm-pc, then you can have…other combinations

    Yes, that’s perhaps an argument against shipping an alternate binary (see #49407). I think we should worry about the flag first before contemplating binaries; I think the flag stands on its own, and maybe there’s a solution for a binary with that flag enabled that doesn’t involve us needing to ship or maintain it, but we can worry about it later.

    A runtime flag that makes ESM the default sounds good to me though, why can’t we just have that and allow it in NODE_OPTIONS? That should be enough for the use cases mentioned in the OP already

    Shebangs can’t include flags on many platforms. See #49295 (comment) and linked and following comments. I don’t know if shell scripts have much need for pointer compression, but assuming they don’t, then the shebang case is an argument for a separate binary, as shell scripts being unable to specify flags is a particular constraint that only a separate binary can solve, and hopefully this is the only Node option that such scripts need control over.

    Anyway to your larger point, yes, let’s ship the flag first and go from there. Many platforms do support flags in shebangs, and there are other use cases solved by the flag, so we’re still making a lot of progress just with the flag by itself at first.

  12. 84 remaining items

  13. GeoffreyBooth commented on Sep 28, 2023

    @GeoffreyBooth
    MemberAuthor

    It shouldn’t be an issue, the ESM loader is able to load CJS, why would we error?

    I was thinking how would we handle something like node --entry-url ./entry.cjs?foo=bar but I guess you’re right, it would get handled however an import() of that would be handled currently, which I assume is along the lines of “convert the URL to a file path and import/require it.” So --entry-url would become another of those “if this is used, the user has opted into having the ESM loader handle the entry point” flags like --import and --loader.

    Anyway getting back to the UX question first though, which do we want:

    • 👀 --import is used for this case, like node --import ./entry.js, and if the user wants the REPL with --import they need to add -i or --interactive
    • ❤️ --entry-url is a new boolean flag that causes the entry point string to be parsed as a URL and loaded by the ESM loader: node --entry-url --enable-source-maps ./entry.js?foo=bar
  14. GeoffreyBooth commented on Sep 29, 2023

    @GeoffreyBooth
    MemberAuthor

    I know people are still debating the best approach for supporting URL entry points, but since one of the options under consideration is a semver-major change and the others aren’t, I opened a PR for the semver-major one in case that wins consensus: #49946. Let’s hopefully decide whether or not to go with that approach before the 21.0.0 cutoff on Tuesday.

  15. LiviaMedeiros commented on Sep 29, 2023

    @LiviaMedeiros
    Member

    I think it would be better to open spinoff issue so we can have consensus on more details.
    For example, do we want to support relative URLs? Do we want to support bare specifiers? Do we want to support shortcut specifiers? Do we want to allow users to override mime-type? Should we support loading CJS with URL? Should we seek for pjsons for file: URLs?

    For the UX question, it might be worth mentioning that third option: providing main entry URL as named argument.

    As for when it should land, I see it as a feature tied to import.meta.main rather than feature tied to --default-type. If we are planning to have .main soon, I'd suggest to land this feature directly afterwards (if not in the same PR, assuming that import.meta.main isn't semver-major).
    The reasons for that are that it's much harder (if possible at all) to test if it actually works as main entry point, and it would be unclear for users that there's difference between main entry point and import until we provide a mechanism to differentiate.

  16. GeoffreyBooth commented on Sep 29, 2023

    @GeoffreyBooth
    MemberAuthor

    I think it would be better to open spinoff issue so we can have consensus on more details.

    We can, but since the first PR implemented everything else I think the URL entry stuff is all that’s left to design, so maybe we can just finish it here?

    For example, do we want to support relative URLs?

    Yes. The input would be relative to the file URL of process.cwd().

    Do we want to support bare specifiers?

    I don’t think so. That’s what --import is for, or one could do node --entry-url data:text/javascript,import('bare-specifier').

    Do we want to support shortcut specifiers?

    What is a shortcut specifier?

    Do we want to allow users to override mime-type?

    From an HTTPS URL, you mean? I think not as part of the CLI, that can be achieved via a module customization hook.

    Should we support loading CJS with URL? Should we seek for pjsons for file: URLs?

    Yes and yes. It would be loaded by the ESM loader, the same as if you had referenced a relative or absolute file URL via import().

  17. LiviaMedeiros commented on Oct 1, 2023

    @LiviaMedeiros
    Member

    What is a shortcut specifier?

    Subpath imports, the ones starting from #.

    Do we want to allow users to override mime-type?

    From an HTTPS URL, you mean? I think not as part of the CLI, that can be achieved via a module customization hook.

    From any non-file: URL, to be precise.
    For example, we could expand --input-type to enable node --input-type=commonjs --entry-url https://example.com/legacy.cjs and node --input-type=commonjs --entry-url 'data:text/javascript,console.log("am CJS");' (I don't think we should, but it's worth considering).

  18. GeoffreyBooth commented on Oct 1, 2023

    @GeoffreyBooth
    MemberAuthor

    Subpath imports, the ones starting from #.

    No, I think we shouldn’t implement the ESM resolution algorithm here. # would just mean a hash, like a URL.

    From any non-file: URL, to be precise.

    We already have --experimental-network-imports. The way data: and https: imports work currently is that Node uses the MIME types from the header of the resource. That can be overridden by the load module customization hook; I don’t think we want to provide another way to customize this.

  19. thescientist13 commented on Jun 6, 2025

    @thescientist13
  20. github-actions commented on May 28, 2026

    @github-actions
    Contributor

    This issue has been marked as stale due to 210 days of inactivity.
    It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.

  21. added
    staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.
    on May 28, 2026
  22. SimonFischer04 commented on May 28, 2026

    @SimonFischer04

    Not stale

  23. removed
    staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.
    on May 29, 2026
  24. aduh95 commented on Aug 12, 2026

    @aduh95
    Contributor

    Not stale

    Yes it is, the last comment was from almost 3 years ago. Maybe you meant "still relevant"?

    I would argue that is no longer relevant, now that we have syntax detection enabled by default, I think the use-case is mostly fulfilled, and I don't think anyone is working on this. I don't see the point of keeping that issue open, closing as not planned.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    cliIssues and PRs related to the Node.js command-line interface.esmIssues and PRs related to the ECMAScript Modules implementation.moduleIssues and PRs related to the module subsystem.wasiIssues and PRs related to the WebAssembly System Interface.wasmIssues and PRs related to WebAssembly.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions