Repository: node
Issue: Discussion: In an ESM-first mode, how should a `package.json` file with no `type` field be handled?
Labels: module,esm

Building off of https://github.com/nodejs/node/issues/49432, https://github.com/nodejs/node/pull/49295#issuecomment-1703017541 and https://github.com/nodejs/node/pull/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](https://github.com/nodejs/node/issues/49432#issuecomment-1704501693) that patched any dependencies’ `package.json` files to add `"type": "commonjs"` wherever the `type` 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 the `npm 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.

- 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 any `type`-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 a `type` 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.

- c. The `npm` registry starts requiring the `type` field in order for packages to be published, just as they [already require the `name` and `version` fields](https://docs.npmjs.com/creating-a-package-json-file#required-name-and-version-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 what `type` field value those packages should have; and `npm` 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 the `npm` registry strictly disallows old packages to be modified.

#### 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.

- 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](https://nodejs.org/api/esm.html#resolution-algorithm-specification) already special-cases folders named `node_modules`.
  - This would come quite close to a “just works” experience: users could run the current `npm init`, [assuming it never changes](https://github.com/nodejs/node/issues/49432#issuecomment-1704548259), and still write ESM code for their app without needing to enable it somehow; and the user could `npm 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 under `node_modules`. Package managers that save packages in a cache folder might create a `node_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.

- d. The “no `type` means ESM” behavior only applies to the package scope of the entry point. So in an `app` folder with `app/package.json` (that lacks a `type` field) and `app/entry.js`, running `node --experimental-flag-for-esm-first-mode-name-tbd entry.js` would interpret `entry.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 named `node_modules`“ which is easily determined by external tools.

Any other ideas? Or additional pros/cons to any of these suggestions. @LiviaMedeiros @nodejs/loaders @nodejs/wasi @nodejs/tsc