Skip to content

Proposal: Set --experimental-default-type mode by detecting ESM syntax in entry point #50043

@GeoffreyBooth

Description

@GeoffreyBooth

After landing #49869, I opened nodejs/TSC#1445 to discuss when to flip the default value of --experimental-default-type from commonjs to module, which would make Node itself default to ESM whenever the user didn’t make it explicit that they wanted to work in CommonJS by using "type": "commonjs" in package.json, or a .cjs extension, etc. There were concerns raised on that thread around breaking the “loose” files case, where there is no package.json present, and breaking tutorials such as https://nodejs.org/en/docs/guides/getting-started-guide that assume a CommonJS default.

I propose we create --experimental-default-type=detect-module that would function as follows:

  • As with --experimental-default-type=commonjs and --experimental-default-type=module, it would only apply to ambiguous cases: a file with no explicit .mjs or .cjs extension, either no package.json or one that lacks a type field, no --input-type or --experimental-default-type=module flags passed, not under node_modules.
  • The entry point would be parsed (not evaluated) and we would look for import or export statements. (Not import() expressions that are allowed in CommonJS, not the CommonJS wrapper variables which could be set as globals by user code.) If the file cannot be parsed, error.
  • If an import or export statement is found, run the entry point (and the rest of the app) as if --experimental-default-type=module had been passed. Else run as if --experimental-default-type=commonjs had been passed.

This would solve the “loose files” and tutorials cases: ambiguous files would continue to be run as CommonJS. Only files with import or export statements, which currently error if you try to run them, would start to run as ESM. This would solve the goal of flipping, which is to let people start using ESM syntax by default without first opting in somehow.

I implemented big parts of this in 2019 in nodejs/ecmascript-modules#55. It’s very similar to a feature request from 2021, #39353. The main difference between then and now is the existence of --experimental-default-type. I can respond to some of the concerns raised on those earlier issues:

  • People won’t understand the distinction between disambiguating CommonJS and ESM, and what this is doing. This is why I called the value detect-module, not auto, and I think “it runs as ESM if import or export statements are found, or as CommonJS otherwise” is simple enough that users will get it.
  • CommonJS and ES modules cannot be disambiguated. This is true, but I’m not trying to disambiguate them. There are three sets of files, basically: files that can only run as CommonJS modules, files that can only run as ES modules, and files that can run as either (ambiguous syntax, like console.log(3)). I can’t tell apart the “ambiguous syntax” files from either of the other two, but I don’t have to: those run as CommonJS today, and they can continue to run as CommonJS. That would be the status quo, avoiding a breaking change. I’m only changing how the “runs only as ESM” files are interpreted, which turns a guaranteed error (Unexpected token import) into running code.
  • How would files imported by the entry point be interpreted? This is where the existence of --experimental-default-type today helps us out, because now I have a framework to use that defines how all files in various scopes are interpreted. The detection triggers either --experimental-default-type=module or leaves us in the default --experimental-default-type=commonjs, and that’s it.
  • What about future syntax, like whatever comes after import attributes? In my PR in 2019, I used Acorn since it’s already a dependency within Node. Acorn throws on any syntax that it doesn’t know, and I think we would likewise error. The error would be informative, instructing users to try again with an explicit marker (type field, file extension).
  • What about the performance impact? The entry point is parsed twice, yes, but only the entry point; and only when no explicit marker is present.

@nodejs/loaders @nodejs/tsc

Metadata

Metadata

Assignees

No one assigned

    Labels

    esmIssues and PRs related to the ECMAScript Modules implementation.moduleIssues and PRs related to the module subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions