Description
Building off of #49432, #49295 (comment) and #31415, we’re considering a new mode, probably enabled by flag, where all of the current places where Node defaults to CommonJS would instead default to ESM. One of the trickiest questions to answer for defining such a new mode is how to handle package.json
files that lack a type
field.
A package.json
file, whether or not it contains a type
field, defines a “package scope”: the folder that the package.json
file is in, and all subfolders that don’t themselves contain a package.json
file. Within this package scope, currently a package.json
containing "type": "module"
will cause .js
files to be interpreted as ES modules; a package.json
file containing "type": "commonjs"
or no "type"
field will cause .js
files to be interpreted as CommonJS modules.
In a naïve “just flip all the defaults” implementation, where one literally goes through the Node codebase and symmetrically reverses everywhere that we default to CommonJS to instead default to ESM, a package.json
lacking a type
field would cause all the files in that scope to be treated as ES modules. The problem with this is that there are lots of popular dependencies that people install that have no type
field. Most real-world apps would fail to run in an ESM-first mode that behaved this naïve way, because it would be rare for every dependency of an app to contain a package.json
with a type
field.
To make an ESM-first mode that’s actually usable, we need to find a solution for this problem. As I see things, the solutions fall into two categories: one where we preserve the pure symmetrical “no type
= ESM” behavior, and one where we don’t. Here’s a running list that I’ll update if people suggest additional ideas:
1. Preserving symmetry: no type
field is interpreted as ESM
-
a. After installing packages, users would run a script that patched any dependencies’
package.json
files to add"type": "commonjs"
wherever thetype
field wasn’t specified.- This might upset package authors, as their packages are getting modified by the user which might cause bugs. This also goes against a lot of the work the
npm
team has done in recent years around reproducible builds and immutable packages. Ideally a patch script such as this would be part of thenpm install
command and its equivalents in other managers, but it’s probably unlikely that we would get support for such an approach from many (or any?) of the popular package managers.
- This might upset package authors, as their packages are getting modified by the user which might cause bugs. This also goes against a lot of the work the
-
b. After installing packages, users would run a script to warn them if any of their packages lack a
type
field. (Or as part of the package installation command, the command would error on attempting to install anytype
-less package. This would presumably be an option that users would enable.) Then users would presumably uninstall that package in favor of some alternative (or choose to patch it).- This would result in user-driven pressure on package authors to republish their packages with the
type
field added, even if just"type": "commonjs"
, which it’s probably safe to assume would rankle many package authors. Besides those authors complaining that we’ve pushed a requirement onto them, they may reasonably argue that atype
field makes no sense for packages intended for non-Node environments such as browsers. - This only really works when adding a particular package for the first time to an app. If a user is trying to migrate an old app to run in ESM-first mode, any
type
-less package would need to be patched or upgraded to the latest version (assuming the author has kindly published a new version with the field). This option creates a lot of friction for both users and package authors.
- This would result in user-driven pressure on package authors to republish their packages with the
-
c. The
npm
registry starts requiring thetype
field in order for packages to be published, just as they already require thename
andversion
fields.- I think it’s unlikely that the
npm
folks agree to this, as there are countless packages not intended for use in Node, and it would be unclear whattype
field value those packages should have; andnpm
surely doesn’t want to put themselves in the position of getting many package authors angry at them. - This still doesn’t solve the problem of what to do about all the existing packages published without a
type
field, as thenpm
registry strictly disallows old packages to be modified.
- I think it’s unlikely that the
2. Different behavior in ESM-first mode
Assuming that no workable option for preserving symmetry is found, the question then becomes “okay, now what?” Here are some options, that I’ll update as people comment:
-
a. Keep current behavior. All
type
-less package scopes are still treated as CommonJS.- This means we aren’t really ESM-first, at least not fully, and we fail to achieve one of the primary goals of providing an ESM-first mode: that a new user can download Node and just start coding using ESM syntax without needing to opt into it somehow.
-
b. Error on
type
-less packages.- This would be the runtime equivalent to the option above where the package manager errored or warned on installing a package that lacked a
type
field. It presents the same problems: users would need to patch, or pressure the package authors to update their packages.
- This would be the runtime equivalent to the option above where the package manager errored or warned on installing a package that lacked a
-
c. Under a
node_modules
folder,type
-less packages are treated as CommonJS; but are treated as ESM otherwise. This follows the precedent that the ESM resolution algorithm already special-cases folders namednode_modules
.- This would come quite close to a “just works” experience: users could run the current
npm init
, assuming it never changes, and still write ESM code for their app without needing to enable it somehow; and the user couldnpm install
any dependency which should “just work” like today. - Package managers other than
npm
might need to adjust to support this behavior, if they don’t save packages in subfolders undernode_modules
. Package managers that save packages in a cache folder might create anode_modules
folder at the root of their cache and put the packages one level down inside that, for example. But some package managers might have trouble working with this behavior.
- This would come quite close to a “just works” experience: users could run the current
-
d. The “no
type
means ESM” behavior only applies to the package scope of the entry point. So in anapp
folder withapp/package.json
(that lacks atype
field) andapp/entry.js
, runningnode --experimental-flag-for-esm-first-mode-name-tbd entry.js
would interpretentry.js
and any other files in that package scope as ESM, but all other package scopes anywhere on the disk would be interpreted as CommonJS.- Compared to the previous option this might fix some of the non-
npm
package managers, but at the cost of the app possibly not being statically analyzable by tools. It would be ambiguous whether a particular package scope will be the one that an entry point uses and would therefore be acquiring this new behavior, unlike “is it under a folder namednode_modules
“ which is easily determined by external tools.
- Compared to the previous option this might fix some of the non-
Any other ideas? Or additional pros/cons to any of these suggestions. @LiviaMedeiros @nodejs/loaders @nodejs/wasi @nodejs/tsc
Activity