guybedford / ecmascript-modules-mode

ECMAScript Modules esm boundary proposal for Node.js
0 stars 0 forks source link

Types/Dual-Mode #4

Closed GeoffreyBooth closed 5 years ago

GeoffreyBooth commented 5 years ago

Readable version: https://github.com/guybedford/ecmascript-modules-mode/tree/types

So I started rewriting the proposal to both incorporate the ideas from #2 (the concept of a “package type”) and to describe how this configuration could work for a package with multiple types, such as the most common example a “dual-mode” CommonJS and ESM package. Whether or not you want to keep the “type” language, I ran into an issue with trying to make dual-mode work: "main".

We can’t change "main". We can’t say that going forward, "main" can take an object like "main": { "esm": "./index.mjs", "cjs": "./index.js" }. That would be a breaking change, and the whole point of supporting CommonJS as a target environment is to support older versions of Node that can’t import ESM.

So either we accept that we’re conflating two of @guybedford’s three concerns—the package entry point (main) and the package exports (encapsulation)—or we need to introduce a new key to replace "main". For the purposes of this proposal I opted to roll up the entry point as part of "exports", the way that the package exports proposal describes it, and the object form is supported: "exports": { ".": { "esm": "./src/index.js", "commonjs": "./dist/index.js" } }. I think package exports and the package main entry point are very close if not joined concepts, as the entry point is basically the / export and the others are the /path exports, so I can see users grasping them as part of the same configuration block.

Alternatively, we could introduce a new key to replace "main", like "entrypoint". This could take the object form, like "entrypoint": { "esm": "./src/index.js", "commonjs": "./dist/index.js" }. Then we would still get the full separation of concerns that Guy was after, albeit with duplication as now the user can specify the entry point in either this new field or in "exports". But I’m not sure we should be giving the user two places to define the same thing, and I don’t think we should take it out of "exports" as the entry point really is the main export.

I don’t think we should assume that most packages will have only a single type/mode, and therefore it’s okay to keep using main. I think CommonJS/ESM dual-mode packages will be quite common for a few years, and whatever configuration we come up with should work well for such packages. Using main also raises the question of whether it supports automatic extensions and folder lookups ("main": "./index", or "main": "./dist/") for ESM or other non-CommonJS environments. It would be inconsistent either way—inconsistent if it does allow such things, as then this behaves differently from "exports" and import statements; and inconsistent if it doesn’t, as then "main" behaves uniquely for CommonJS as opposed to other environments. I think it’s best to just leave "main" as a CommonJS-only legacy thing, not to be further abused.

GeoffreyBooth commented 5 years ago

In short, we have three ways of handling "main":

  1. For single-mode packages, "main" is the package entry point. So if it’s a CommonJS package, "main" can be "index"; but an ESM package requires the extension, so it would be "index.js". (This is the inconsistency I discussed above: either "main" behaves differently based on package type, or it behaves differently than import statements.) A dual/multi-mode package would necessitate the use of "exports" for defining the entry points for the non-CommonJS modes. A user could potentially define two CommonJS entry points (in "main" and "exports") and one would have to take precedence or we would throw. This is the current behavior of the branch based on this proposal, but the behavior I find the most confusing from a UX perspective.

  2. Create a new key, e.g. "entrypoint", that behaves the way we wish "main" would behave. It could accept an object in the case of dual-/multi-mode packages:

"entrypoint": {
  "esm": "./src/index.js",
  "commonjs": "./dist/index.js"
}

We still have the issue of how to resolve conflicts between entry points set here, in "exports" and in "main" (in the case of CommonJS). Having three ways to set a CJS entry point, and two ways to set an ESM entry point, seems less than ideal.

  1. Just accept that we’re conflating "exports" to both define exported paths as well as the root package entry point. There’s still a "main" where users can define the CommonJS entry point in addition to defining it in "exports", but "main" is treated as deprecated/legacy.

"exports": {
  ".": {
    "esm": "./src/index.js",
    "commonjs": "./dist/index.js",
    "browser": "./browser/index.js"
  }
}