Skip to content

Discussion: In an ESM-first mode, how should a package.json file with no type field be handled? #49494

Open
@GeoffreyBooth

Description

@GeoffreyBooth

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

    • 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 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, 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

Activity

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

    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