nodejs / modules

Node.js Modules Team
MIT License
413 stars 42 forks source link

Proposal for dual ESM/CommonJS packages #273

Closed GeoffreyBooth closed 4 years ago

GeoffreyBooth commented 5 years ago

@guybedford, @jkrems and I discussed the package dual-ESM/CommonJS case and we have a small proposal, based on the current ecmascript-modules implementation:

Notes:

And that’s it! This should cover the case while preserving design space for future proposals, and for Node potentially switching to ESM by default someday.

GeoffreyBooth commented 5 years ago

@antstanley One solution is the proposal at the top of this thread: a new field to define the ESM entry point, just like the community has adopted de facto with "module". That way we get dual packages while still preserving both .js everywhere and no extension searching in ESM.

To take your GraphQL.js as an example, you have src and dist folders, with ESM in the former and presumably CommonJS in the latter. Under this proposal, your package.json could include this:

"type": "module",
"main": "dist/index.js",
"exports": "src/index.js"

CommonJS ignores "type", so require('graphql') would load dist/index.js as CommonJS; and import 'graphql' would load src/index.js as ESM. So literally the only thing you would need to do to make GraphQL.js a compatible dual package would be to add "exports": "src/index.js". That’s it.

If you wanted to be more explicit, you could create a dist/package.json file containing "mode": "commonjs", and then both ESM and CommonJS environments would treat all the files in dist/ as CommonJS. You could do the same with src/package.json containing "type": "module", to fully separate the scopes of these folders from the root of your package.

One more thing to note is that older versions of Node treat unknown extensions as CommonJS, so a .cjs file would load as CommonJS JavaScript.

mysticatea commented 5 years ago

I understood that the change is reasonable to allow different directories for commonjs and esm. I have some questions:

jkrems commented 5 years ago

Why was the word exports chosen?

It's a reference to import maps which uses imports to declare mappers from the consumer ("importer") perspective. So exports is like imports but from the provider/package ("exporter") perspective. The name export, to me, would imply a single export but the file being referenced may have various exports. But I can definitely see where for somebody very familiar with CommonJS, it may look like a reference to module.exports.

GeoffreyBooth commented 5 years ago
  • Why was the word exports chosen? - exports is the variable name in commonjs, and export is the keyword in esm. I would guess the exports is related to commonjs if I didn’t know it.

We’re also considering mainModule (see above). Names aren’t final.

  • About the example of #273 (comment), the type field is needed? Because the main and exports indicate that the package supports both forms, the type field looks like confusion.

A package can have multiple package.json files; you’re not restricted to just one at the root. The top-level one needs to be the “big one,” with your package’s name and version and so on; any others elsewhere in the package could contain a type field and nothing else, if you want. For example:

/package.json      - no "type"
/dist/package.json - {"type": "commonjs"}
/src/package.json  - {"type": "module"}

This avoids the confusion of “what is this "type" referring to?” while still being explicit about the type of each folder. I would avoid putting any JavaScript files at the top level in this case. You could also have all the src/ files be .mjs and all the dist/ files be .cjs for maximum explicitness.

mysticatea commented 5 years ago

It's a reference to import maps which uses imports to declare mappers from the consumer ("importer") perspective.

I see. Thank you for the pointing.

But I can definitely see where for somebody very familiar with CommonJS, it may look like a reference to module.exports.

We’re also considering mainModule (see above). Names aren’t final.

This is the interop feature of commonjs and esm and many npm (commonjs) package authors will use it. Also, this feature is closely related to require/exports in commonjs and import/export in esm. Personally, I think that it's better to avoid exports, an important keyword for commonjs.

A package can have multiple package.json files; you’re not restricted to just one at the root. The top-level one needs to be the “big one,” with your package’s name and version and so on; any others elsewhere in the package could contain a type field and nothing else, if you want. ...

Thank you for the elaboration.

Is those type field needed? Or can we omit the type field if we used both main and exports (mainModule) fields? Personally, I don't see the reason that the type field is important in that case.

jaydenseric commented 5 years ago

As a conversational nitpick, I think we should steer clear of using src/ and dist/ as examples of ESM and CJS entrypoints, as this is almost always incorrect practice in real world projects. Both entry points should be transpiled or "dist".

The community has had problems in the past with people naively pointing the package module field at source files which causes app bundles to contain untranspiled syntax that's experimental or unsupported by browsers.

The only time you can do that is if the only thing you transpile is ESM to CJS. Howsever, most people are using Babel presets such as @babel/preset-env and @babel/react to transpile all sorts of stuff such as object rest spread, JSX, etc.

GeoffreyBooth commented 5 years ago

As a conversational nitpick, I think we should steer clear of using src/ and dist/ as examples of ESM and CJS entrypoints, as this is almost always incorrect practice in real world projects. Both entry points should be transpiled or “dist”.

Yes, you’re correct. I was using src/dist just to make my example simpler. But a more realistic example would be something like:

/package.json               - no "type"
/dist-commonjs/package.json - {"type": "commonjs"}
/dist-module/package.json   - {"type": "module"}

And then it doesn’t matter what’s in src, or whether src contains a package.json or not. The src folder could contain TypeScript or anything else, since the files in src aren’t intended to be run.

devsnek commented 5 years ago

catching up from the last bunch of messages and feedback from people, my suggested plan is to:

what we get from the above:

😅

WebReflection commented 5 years ago

@devsnek it'd be great to share numbers of feedbacks before changing everything again, but regardless, I wonder if the dist part could be revisited so that you might have both extension resolution by default, but also future friendly path, example:

and so on and so fort. This would allow dual packaging, but it would also keep modules with just data available to ESM too.

The main field would remain untouched, but it'd be sensible for authors that like CJS to use dist/cjs/index.js as main entry point in their future/revisited packages.

Last, but not least, if we could confine all the things inside a single package.json field, it'd be easier to change/expand without bothering the community, or ever conflicting with it (as it's been for the module field).

Example

{
  "moduleHandling": {
    "formats": {".js": "esm"},
    "main": "dist/esm" // will look for dist/esm/index.js
    // ... other fields either today or tomorrow
  }
}
antstanley commented 5 years ago

I think @devsnek's suggestion could work. To better understand the changes we would have a package.json with the following

{
  ...
  "main": "dist/cjs/index.js",
  "moduleMainBikeshed": "dist/bikeshed/index.js",
  "formatsBikeshed": [ 
    { ".js": "stmBikeshed" }, 
    { ".wasm": "stmGardenshed" }
   ]
  ...
}

My question is what happens if you've got dependencies with only CJS (ie 99.9% of npm), but you've mapped .js to ESM in your package.json, and but none of your dependencies have any of these new package.json fields in to map .js back to CJS? Would the behaviour be if a package.json file is found it reverts back to the default of .js is CJS unless explicitly defined as otherwise in that package.json?

Going back to the reasons for dual ESM/CJS packages, the biggest for me is compatibility to allow library authors to migrate their code base to ESM. They need to be able to do this with the following constraints

Adding .cjs for CJS, to allow .js for ESM breaks downstream dependents (they don't understand .cjs and expect .js to be CJS).

The proposal above to add moduleMainBikeshed and formatsBikeshed to package.json could work with the follow caveats around default behaviour for future versions of Node that support ESM without a flag...

I would add two more options to formatBikeshed namely the ability to add strings default and browser, which are just predefined resolution definitions. default is just there if you want to explicitly use the default file extension resolution. The browser option would be to use browser compatible resolution, so no CJS, and no automatic file resolution, for those library authors that require it.

So the decision tree would look like this every time a package.json is hit.

Module decision tree

This essentially relegates main in package.json to legacy CJS behaviour only, but it does resolve compatibility issues for upstream and downstream modules.

WebReflection commented 5 years ago

@antstanley if "formatBikeshed": "browser" means both .js and .mjs are loaded as ESM then above graph might make sense. I still think having this inside a single moduleBikeshed field would make everyone life easier (including bundlers).

what happens if you've got dependencies with only CJS (ie 99.9% of npm), but you've mapped .js to ESM in your package.json

I think dependencies are still all independently resolved so this issue won't actually happen, right?

antstanley commented 5 years ago

@WebReflection "formatBikeshed": "browser" would be for browser compatibility, so no CJS, explicit file references, though no extension validation. Match browser resolution exactly.

The other options would support mixed modules in the graph, with the ability for a package author to define their own resolution precedenc, and supported extensions mapped to a module loader scoped just to their package. This also adds a level of future proofing when wasm modules come along.

If we named moduleBikeshed to just module and that resolution default for it was .js with ESM syntax with precedence over CJS, then bundlers would work with no changes as they support module today for ESM. One advantage bundlers and transpilers have is they can parse module to evaluate what type of module it is, as they are working at build time, not execution time, so less time sensitive.

When shipping we probably want to change moduleBikeshed -> module and formatBikeshed -> moduleFormat

With regard to

what happens if you've got dependencies with only CJS (ie 99.9% of npm), but you've mapped .js to ESM in your package.json

I was looking for clarity. My assumption is that the resolution used would be scoped to that package and not follow through to sub packages with their own package.json, it just wasn't explicitly said in the previous comment.

devsnek commented 5 years ago

You can't magically allow .js in moduleMainBikeshed because it doesn't actually signal a change. If I, in the same application, deep imported package/dist-mod/index.js, we can't scour every file on the file system to check if one of them has moduleMainBikeshed pointing to that file, and even if we can, what happens if we find two files that conflict? This is why only the formatBikeshed field can be used to change the format of a file.

antstanley commented 5 years ago

@devsnek I agree with you. The point of moduleMainbikeshed would be to signal the entry point, and formatBikeshed would be to signal the module system and extension mapping to use at that entry point.

I think I've just confused things in my response to @WebReflection who mentioned bundlers, which don't have the performance constraints of evaluating modules at run time. It was a hypothetical on what would need to happen to for bundlers to work without change to existing modules.

In the context of bundlers, for them to work they would just need a module field, but this wouldn't run in Node, as there is no signal to change module loader. For it work in node, with the proposed new fields, it would need module and a format field ie formatBikeshed.

Apologies for any confusion.

michael-ciniawsky commented 5 years ago

app.js

// ESM
import ns from './module.js|pkg'
import { name1, name2, nameN } from './module.js|pkg'

const require = import.meta.require

// CJS 
const ns = require('./|pkg')
const { name1, name2, nameN } = require('./|pkg')

package.json

{
  name: 'pkg',
  main: 'path/to/cjs',
  module: 'path/to/esm.js' // or exports
  ...
}
node -m|--module ./app.js
// No interop (What's the point with 'default' (ns) only anyways)
import ns from '/.cjs.js' ❌ 

const { name, name2, nameN } = ns

The inferior interop complicates the current implementation a lot, while having zero benefit

import ns  from 'pkg'
import ns2 from 'pkg2'

const { a, b, c } = ns
const { c, d, e } = ns2 
// ----------------------------------
const require = import.meta.require

const { a, b, c } = require('pkg')
const { c, d, e } = require('pkg2')
demurgos commented 5 years ago

Hi, I've been experimenting with ESM for over a year and wanted to share my experience.

  1. I definitely agree that dual modules are needed for the transition period. A library should transparently support ESM and CJS consumers. It means that there should be a way to select the file based on the import mechanism (require or import). The previous implementation used a different priority for file extensions, this proposal uses a different field in package.json.

  2. Extension-based dual packages such as GraphQL definitely worked with the old --experimental-modules. I published all of my libraries this way and it allowed users to seamlessly use either the CJS or ESM versions. I am not aware of any transparent dual package solution with the current implementation. (Extension-based resolution now requires the user to use --es-module-specifier-resolution=node). I am okay to perform a build step on my side if it allows user to consume the package more easily.

  3. Deep imports / namespaced imports / paths within packages are an important use case. I consider any proposal that does not handle them to be incomplete. Extension-based dual packages handle them naturally. This proposal depends on the proposal-pkg-export for this. I'd like you to expand on how to handle these imports.

GeoffreyBooth commented 5 years ago

@demurgos Thanks for your feedback. With the implementation as things stand today, the closest one can come to a dual package is to have "main" point to the CommonJS entry point and to tell users via your README to access the ESM entry point via a deep import, e.g. import { foo } from 'pkg/esm.mjs'; (yes, deep imports also currently work, but they require full filenames with extensions). There are many members of the group eager to allow that 'pkg/esm.mjs' to be simply 'pkg' for both CommonJS and ESM. The issue is how to achieve that within the constraints of the design decisions that have been made so far.

The group has some enthusiastic supporters of extension searching and overloading "main" to allow "main" to behave differently for CommonJS and ESM, but the group also has some who are strongly opposed. Since we operate by consensus, that means that extension searching/overloading "main" aren’t options unless several people change their minds. That’s why we shipped --es-module-specifier-resolution, and why its default is explicit. There might never be a consensus to drop this flag or flip its default. I’m operating under the assumption that the current behavior is what we have to work with, as it’s safer to assume the status quo than to assume otherwise. Even if we eventually enable extension searching, there’s possibly even stronger opposition to overloading "main", as people want to not need entry points to be side-by-side in the same folder and also people want to use .js everywhere and not need a secondary extension. That’s why I’ve said above that this proposal is still relevant even if --es-module-specifier-resolution=node becomes the default.

So the political issue that we have is that 'pkg/esm.mjs' might be considered good enough for enough people in the group such that no other solution finds consensus. I think introducing a new field is less controversial than overloading "main"—it at least avoids depending on the already-controversial extension searching, and if extension searching someday gets turned on by default it can apply to the new field too. Or the new field could get removed as part of enabling extension searching. I also proposed nodejs/modules#324 as a different proposal that aims for satisfying the need with as little unnecessary complexity as possible, to present as small a target as possible for dissension.

I would like to solve this problem. I would like to provide a better UX than 'pkg/esm.mjs'. I think the way to get there is to work within the design we have, either via this proposal or nodejs/modules#324. I think the require of ESM proposal in nodejs/node#49450 can get added on top of either base “new field” proposal, as can an enabling of extension searching by default if that ends up happening. This new field could be changed or even removed when those later PRs get approved, if they do. But I think the way forward is to take small steps and work within the framework we’ve established, as that seems to be the most likely path to finding consensus.

demurgos commented 5 years ago

Thank you for your detailed reply.

The main strength of extension-based dual packages was that consumers did not have to change specifiers. This opened a path where consumers could migrate to ESM without waiting for their dependencies, and then the dependencies could update to a dual package while keeping the interface compatible. Manually appending /esm.mjs or extensions requires more coordination.

I like your user story in nodejs/modules#324. What I am calling for is to keep deep import specifiers such as import { map } from 'rxjs/operators'; as part of the discussion. Even if most packages have a single entry point, deep imports are common too since they are simpler to tree-shake. I regret that they are deferred to import maps or pkg-exports, even if I really like the pkg-exports proposal. But I guess you're right: "the way forward is to take small steps".

WebReflection commented 5 years ago

the closest one can come to a dual package is to have "main" point to the CommonJS entry point and to tell users via your README to access the ESM entry point via a deep import

that would make the module ambiguous in case it's meant to be published as "type": "module"

With the current state that doesn't really help with dual modules, there is a not too ugly workaround that seems to work already.

The TL;DR is that you specify the type as module, but you point at the CJS entry, and you use the module field to point at the module file. Such field could point directly at esm/index.js or it could be shortcut as m.js (see the example).

package.json

{
  "type": "module",
  "main": "cjs",
  "module": "m.js"
}

folder structure

cjs/index.js + others
esm/index.js + others
m.js
package.json

m.js

export * from './esm/index.js';

// in case the default should be exported too
import $ from './esm/index.js';
export default $;

Doing this way you have the following benefits:


edit

You can already verify/experiment this workaround via dual-packaging-test module, already in npm

import log, {name} from 'dual-packaging-test/m.js';

log(name); // will log "dual-packaging-test"
GeoffreyBooth commented 5 years ago

With the current state that doesn’t really help with dual modules, there is a not too ugly workaround that seems to work already.

@WebReflection I think what we’re looking for is a good solution that works without needing bundlers or loaders. Certainly once the user is using bundlers or loaders, all rough edges can be smoothed away and the user can get whatever they want. It would be nice if the vanilla experience was good as well, if only to lessen the load on bundlers/loaders and reduce the need for them.

@demurgos Yes, I also like the look of 'pkg/deep' rather than 'pkg/deep/index.js'. I suppose if we allowed import of extensionless files, one could create a deep extensionless file that was something like export * from './deep/index.js';; or a symlink from deep to ./deep/index.js. Both of those solutions have their own issues: extensionless files are hard for IDEs to work with, and symlinks have cross-platform concerns. But they’re options, I suppose, if package path maps don’t happen. But I think path maps will happen, as there hasn’t been opposition voiced to them and they provide a new benefit that people want, namely that they define a public API for a package. They also dovetail nicely with the import maps that are coming to browsers.

So anyway, we’re getting there. The fact that at this point what we’re debating is the appearance of import specifiers and how many or few ugly characters they need to contain is, to me, a sign of success 😄

WebReflection commented 5 years ago

@GeoffreyBooth

I think what we’re looking for is a good solution that works without needing bundlers or loaders

not sure it's clear but my workaround doesn't need bundlers to work: it works already ... but ...

It would be nice if the vanilla experience was good as well, if only to lessen the load on bundlers/loaders and reduce the need for them.

agreed, indeed mine is a workaround due current limitations where pointing at CJS in the main when publishing a primary ESM module is a no-go for me, and it makes the type field misleading.

I've tried to combine all features (vanilla + bundlers + legacy) in one, but I'm looking forward to not needing the workaround at all, being able to specify the type too.

GeoffreyBooth commented 5 years ago

Should we perhaps add to the docs the import 'pkg/module.mjs' solution as a recommendation for now? Where pkg’s "main" points to the CommonJS entry point, and pkg’s README tells people to use 'pkg/module.mjs' as the ESM entry point. (With module.mjs just an example, any path would work.)

That’s a solution for dual packages that works today and I think is likely to continue to work under any of the proposals we’re considering. If this PR is accepted or if the require of ESM PR is accepted, and/or if extension searching is enabled, under all those scenarios I think import pkg from 'pkg/module.mjs' would still work as intended, even if the later improvements make the /module.mjs part unnecessary. We would definitely include a warning that this area is still a work-in-progress and likely to change, but documenting this method sends a message that there is already at least this method for publishing both CommonJS and ESM sources in the same package, that will likely remain workable while other options hopefully improve the usability.

Two more thoughts, though probably not to add to the docs:

ljharb commented 5 years ago

I don't think we should bother trying to document a non-programmatic approach like "read the readme".

MylesBorins commented 5 years ago

I would like to explicitly object to dual mode packages. I genuinely believe that the removal of dual mode due to lack of automatic extension searching is a feature not a bug. Have a single specifier mean two different things depending on what graph you load it into is a massive hazard. I will spend some time this week going through some scenarios to explain some situations that we might be creating with dual mode and ways in which it creates extremely nasty and hard to debug errors.

edit:

to clarify I meant dual mode packages sharing a single specifier @GeoffreyBooth does a good job of explaining what I meant in https://github.com/nodejs/modules/issues/273#issuecomment-492408041

weswigham commented 5 years ago

Have a single specifier mean two different things depending on what graph you load it into is a massive hazard.

Disagree with lack of a kind of dual mode, but agree with this sentiment, which is why I think the cjs and esm resolver need to be unified (and then cross-format calls made ok via a syncification of the resolution process as described in the other proposal).

Even if you disagree with require(esm) for whatever reason, this sentiment should be a good driver for unifying the cjs and esm resolvers (related: we probably need to reserve .wasm in cjs same as .mjs in preparation for wasm modules).

ljharb commented 5 years ago

(We may want to reserve all unknown extensions in .cjs, in preparation for whatever module types we might want to add in the future)

SMotaal commented 5 years ago

I think what is becoming more visible is the disconnect that exists between simulated interoperability and actual interoperability hidden behind some form of conventionally opinionated façade.

Even if you disagree with require(esm) for whatever reason

So maybe we can recognize that historically this has been actually doing a require(transpileToCJS(esm)) and that is probably where various concerns about failing to meet expectations for all forms of conventionally opinionated façades in a one-size de facto implementation are challenging (at least at this moment).

Have a single specifier mean two different things depending on what graph you load

So if we said a specifier can mean two things depending how the means by which it was specified — as in require(‹x⑴›) == import(‹x⑵›) where x⑴ ≃ x⑵ then imho we can say that the module record for both can be a single record; assuming of course a conforming module map keys x⑵ and use two-way mapping facilitator to adapt calls to require(…).

Yeah, that sounds like two specifiers, but they are actually one specifier meaning one this, with a separate form that adapts them to the CJS layer.

GeoffreyBooth commented 5 years ago

I would like to explicitly object to dual mode packages. … Have a single specifier mean two different things depending on what graph you load it into

Specifically, I think @MylesBorins is objecting to the latter—a single specifier (like 'pkg') resolving to different files based on whether it’s referenced via import in ESM or require in CommonJS. I’m assuming he doesn’t object to the limited “dual packages” support we have in --experimental-modules now, where "main" can point to a single entry point in either ESM or CommonJS and deep paths like 'pkg/module.mjs' can point to one or more other entry points also in either ESM or CommonJS. That’s the recommended best practice I wrote about above in https://github.com/nodejs/modules/issues/273#issuecomment-490376184, that I suggested we add to the docs. I think if we’re not likely to move forward on any other version of dual packages, that’s all the more reason we should write this up and put it in the docs to start setting expectations that this is all that’s likely to ship.

And @weswigham, this still applies even if require of ESM happens, because people will still want to publish packages that are required into CommonJS in older versions of Node. If and when require of ESM lands, this new section of the docs can be updated appropriately.

AlexanderOMara commented 5 years ago

In relation to https://github.com/nodejs/modules/issues/323#issuecomment-511273657 I just wanted to say that while the deep import option (import 'pkg/module.mjs', documented here) does work, it's far from ideal especially from a tooling perspective.

It's not so bad if everyone who uses your package only uses it for ESM, but if you are also making a dual package which depends on another dual package you might run into issues.

Suppose you write the following, where you import something from one of those dual packages:

import {something} from 'pkg';

Now suppose you want to transpile it so other people can use both your MJS and CJS modules. Here you run into an issue, because you need to output two different files like the following:

const {something} = require('pkg');
import {something} from 'pkg/module.mjs';

Essentially, the path needs to be different based on the module system you are compiling for. This is also the case for relative imports to files in your own package (since automatic extension resolution was disabled), but that's a relatively easy problem for tooling to solve. This is more complicated, and requires knowledge of the layout of 3rd-party packages (how should it know?), and the expectation that layout will not change.

GeoffreyBooth commented 5 years ago

if you are also making a dual package which depends on another dual package you might run into issues.

Can you give an example? I'm not seeing this in the example you gave above. What's different about dual depending on dual?

AlexanderOMara commented 5 years ago

@GeoffreyBooth You somehow need to have your ESM modules import that 3rd-party module as 'pkg/module.mjs'; while your CJS modules import it as 'pkg';.

Currently that would mean editing every file that references it after transpiling.

Ideally all one would need to do is transpile it and be done, but how can the transpiler know what to change to do that for you?

GeoffreyBooth commented 5 years ago

Technically the CommonJS and ESM versions of a package are really two separate packages, that just happen to be published in the same folder tree. They don't necessarily behave identically, so it's not safe to assume that they're interchangeable.

If you're outputting a dual package, then all of your package's dependencies need to be the CommonJS ones, at least for the CommonJS version of your dual package. But your ESM version could also use all CommonJS dependencies; there's no reason it needs to be ESM all the way down. Switch to ESM dependencies when you stop publishing a CommonJS version of your package.

AlexanderOMara commented 5 years ago

then all of your package's dependencies need to be the CommonJS ones

Couldn't they also be dual packages? I've had no problem doing that.

Also, for tree-shaking purposes, it's ideal to have it be ESM as far down as possible.

GeoffreyBooth commented 5 years ago

Couldn't they also be dual packages? I've had no problem doing that.

They'd need to be the CommonJS exports of dual packages, unless you want the CommonJS side of your dual package to only be usable in Node 12+ (where ESM is supported).

jkrems commented 5 years ago

Okay, trying to recap @AlexanderOMara's concern (let me know if I'm off):

  1. I maintain package mine that depends on deep-dep.
  2. deep-dep is using the recommended flow of exposing deep-dep/cjs and deep-dep/esm.
  3. I want to support both CJS and ESM in mine.
  4. To do that, I want to write ESM code and compile it to CJS.
  5. In my ESM code, I would write something like this:
// file:///mine/lib/mine.mjs
import 'deep-dep/esm';

Problem: No compiler is smart enough right now to rewrite that to a working CJS file (if that's even reliably possible). Without manually fixing the compiled code, I will not be able to publish a package that supports both webpack's ESM-only tree-shaking and being required in node.

P.S.: I think @GeoffreyBooth's read of the situation is correct and right now the solution is "if you want to use ESM dependencies anywhere, you have to drop CJS support or write additional code manually".

GeoffreyBooth commented 5 years ago

I think this deserves its own issue. Apologies if I sounded dismissive, I was only trying to explain how to do this in current --experimental-modules, not to imply that that shouldn’t change.

Off the top of my head I would think that dual packages should continue publishing their ESM entry point in "module", and CommonJS entry point in "main", and that should provide build tools with all the information they’d need to output the two variations for each version of your package.

GeoffreyBooth commented 4 years ago

Closing in favor of https://github.com/nodejs/node/pull/29978.