I鈥檓 the publisher of the coffeescript package, which is written using CommonJS and has a Node API that includes exports compile and VERSION. Currently dependents can use my package in CoffeeScript via CommonJS code like:
const { compile } = require('coffeescript');
I would like to be able to provide named exports for dependents that are using ESM syntax, for example:
import { compile } from 'coffeescript';
Currently, the best I can offer my dependents is:
import CoffeeScript from 'coffeescript';
const { compile } = CoffeeScript;
because the --experimental-modules implementation doesn鈥檛 currently support named exports from CommonJS.
Alternatively, I could create a proxy ESM file, like this at the root of my package:
// module.mjs at package root
import CoffeeScript from './lib/coffeescript/coffeescript.js'; // CommonJS main
export default CoffeeScript;
const { VERSION, compile } = CoffeeScript;
export { VERSION, compile };
but then users would have to reference it as from 'coffeescript/module.mjs', not from 'coffeescript'. That鈥檚 not terrible, but it鈥檚 not as intuitive and familiar (and equivalent to CommonJS) as just from 'coffeescript'.
We鈥檝e been blocked on a solution for named exports from CommonJS for a long time. I won鈥檛 get into the details, but at the moment none of the solutions currently on the table seem like they will likely be viable.
Package authors want to provide equivalent, and equally good, user experiences for both CommonJS and ESM consumers of their packages. This applies whether the packages themselves are originally written in CommonJS or ESM. Personally, I wouldn鈥檛 feel the need for a better dual packages solution if we had a way to provide named exports from the package root (the import { compile } from 'coffeescript' example above). And under the dual CommonJS/ESM package approach, the same specifier (e.g. 'coffeescript') would produce two separate instances, causing potentially unwanted results.
What all the CommonJS named exports solutions proposed so far have in common is that they dynamically attempt to figure out what the named exports from a CommonJS package should be. What if, instead, we rely on the package author to define their CommonJS named exports?
package.jsonRather than a separate entry point for ESM, what if the package.json simply declared what the named exports are for my package? Then import { compile } from 'coffeescript' could work, because the resolver would know that compile was available as a named export for the package root.
This would nicely complement the package path maps feature we鈥檙e also working on. As paths are defined in package.json, the CommonJS named exports could be defined along with them.
If we really want to be ambitious, the named exports could be added to the package.json automatically by the package manager during installation or by npm (the company) via a scan through their registry. The named exports could be determined by running Node in a locked-down sandbox environment and using Reflect.ownKeys on the path, e.g. Reflect.ownKeys(require('coffeescript')). The resulting array could be added to package.json as the named exports for the root path of the package.
why not
import C from './index.cjs';
export const compile = C.compile;
...
i assume you're thinking of something like:
"exportNamesBikeshed": [
"compile",
...
]
but if the first solution already exists....
i really really dislike having the export bindings out of band from the file that provides them.
oh this is proposed because we haven't figured out dual mode nvm...
i still really dislike out of band export declarations
does this need to be an agenda item before we solve dual mode? i wouldn't expect this to be an issue unless we determine that dual mode won't happen.
Extension resolution would address this, ftr, and is quite viable.
@GeoffreyBooth can we close this?
Most helpful comment
Extension resolution would address this, ftr, and is quite viable.