jkrems/proposal-pkg-exports

Making exports viable for CommonJS

guybedford opened this issue · 4 comments

At today's meeting there was interest in upstreaming this proposal already to CommonJS.

If we do this, we need to have the ability to provide "exports" differently between ESM and CJS, in order to handle dual mode use cases (if we want that!).

I will start off with a proposal here, feedback and suggestions further are welcome.

The idea would be to branch based on a "condition" in exports:

{
  "exports": {
    ".": "./main.js",
    "./x": "./x.js"
  }
}

would still be supported fine, and apply for both CommonJS and ESM loaders.

But then we would also support an object form to branch between the two:

{
  "exports": {
    ".": {
      "cjs": "./main-cjs.js",
      "esm": "./main.js"
    },
    "./x": {
      "cjs": "./x-cjs.js",
      "esm": "./x.js"
    }
  }
}

The nice thing about this approach is the branch conditionals could also be extended to things like "browser", "node", "production", "development" allowing a general conditional subpathing based on the environment.

Do we want to move forward with a proposal along these lines? Perhaps we should set up a call to discuss further?

Wouldn't it be easier to create the same general idea for CommonJS, just without the locked down default? It feels a bit fragile to allow this level of context-sensitive logic inside of exports. It opens the door to "what happens when some are mapped for CJS and some aren't". Does that introduce a new implicit deny..? I'm thinking about this kind of config:

{
  "./public/": {
    "esm": "./lib/api",
    "cjs": "./src/api"
  },
  "./public/data/": "./data"
}

Does this mean ./data is mapped for both? Just ESM? What if it would be "./public/data/": { "esm": "./data" }?

As an alternative, I would see the following as a lot nicer:

{
  "exports": {
    ".": "./main.mjs",
    "./x": "./x.mjs"
  },
  "main": {
    ".": "./main.js",
    "./x": "./x.js"
  }
}

Does this mean ./data is mapped for both? Just ESM? What if it would be "./public/data/": { "esm": "./data" }?

If we match ./public/data/ then it matches for both (specificity always wins). If there wasn't a CJS mapping for ./public/data then it falls through as a deny (we don't bubble up the match, or we could?).

As an alternative, I would see the following as a lot nicer:

Note we can't turn main into an object as that breaks backwards compat meaning no one would do it.

As for the suggestion of using a different key for CommonJS, I feel like there is a nice synnergy here with the "browser" mapping needs. It is a proven use case that we need to fork browser mappings sometimes. If we could support a "browser" condition just like "cjs" then we solve the browser problem as well very nicely.

What I like about exports is that it provides explicit renaming, and prevents requires/imports on non-export-declared files.

The problem of implicitly aliasing multiple files, including extensions, is something that i think should be different.

(Note that the "browser" field only provides 1:1 mapping, even in object form)

exports is now an extended main field and applies to both CJS and ESM.