v14.4.0git clone [email protected]:MicahZoltu/dual-module-package-repro.git
cd dual-module-package-repro/app-package
npm install
node index.mjs
# notice it works
node index.cjs
# notice it does not work
Then remove "type": "module" from library-package/package.json and repeat the process:
npm install
node index.mjs
# notice it doesn't work
node index.cjs
# notice it does work
Always.
The ability to ship an NPM package that can be used by either CJS users (any version of NodeJS) or ESM users (running NodeJS 14+)
I can either define type: module and it will work as an ESM, or I can not define it or set it to CommonJS and it will work as a CJS module, but I cannot define the package.json such that the package works in both environments.
The error indicates that NodeJS is correctly following the exports path and locating the right module in each situation, however it proceeds to load the module using a loader picked by the presence of the type property in package.json. My expectation is that if it loads via the exports: { import: ... } entrypoint then that entire call stack from there down should be ESM, and if it loads via exports: { require: ... } then the entire call stack from there down should be CJS.
Error when type: module is not set:
dual-module-package-repro\library-package\index-esm.js:1
export const apple = 'apple'
^^^^^^
SyntaxError: Unexpected token 'export'
at wrapSafe (internal/modules/cjs/loader.js:1116:16)
Error when type: module is set:
internal/modules/cjs/loader.js:1216
throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
^
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: ...\dual-module-package-repro\library-package\index-cjs.js
require() of ES modules is not supported.
require() of ...\dual-module-package-repro\library-package\index-cjs.js from ...\dual-module-package-repro\app-package\index.cjs is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename index-cjs.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from ...\dual-module-package-repro\library-package\package.json.
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1216:13)
The documentation makes it sound like NodeJS 14 supports packages that can be used as either ESM or CJS, but thus far I have not figured out how to actually accomplish this.
I believe this specific problem could be worked around by renaming files to .cjs and .mjs in the library-package. However, in the real world this solution is far less tenable because I'm compiling from TypeScript which does not do import rewrites during emit (and they maintain a very strong position that they have no intent to ever change this policy). This means that I cannot have TypeScript emit files where the import statements have different extensions depending on the module type being targeted. So in order to have everything be mjs in the ESM build output and cjs in the CJS build output I would need to run my code through a transformer that rewrites all import statements (both static and dynamic) and also rename all of the files to have extensions by output folder.
Note: Even if this is "not a bug", I believe the documentation could use an update to include a working example of a dual module package. Ideally, that example would be one that can be extended to be used in the real world and can work when compiling from TypeScript (i.e., an example that says to just rename all your files and all of your imports isn't particularly useful). It is also worth noting that the library should be native browser compatible as well, which means following the "MJS wrapper around CJS" pattern won't work.
For reference, the library-package/package.json contains:
{
"name": "library-package",
"version": "1.0.0",
"main": "./index-cjs.js",
"exports": {
"import": "./index-esm.js",
"require": "./index-cjs.js"
},
"type": "module"
}
Setting "type": "module" makes Node.js interpret all .js files as ESM, including index-cjs.js. When you remove it, all .js files will be interpreted as CJS, including index-esm.js.
If you want to support both with .js extension, you should create two subfolders:
$ mkdir ./cjs ./esm
$ echo '{"type":"commonjs"}' > cjs/package.json
$ echo '{"type":"module"}' > esm/package.json
$ git mv index-cjs.js cjs/index.js
$ git mv index-esm.js esm/index.js
And then have your package exports point to those subfolders:
{
"name": "library-package",
"version": "1.0.0",
"main": "./cjs/index.js",
"exports": {
"import": "./esm/index.js",
"require": "./cjs/index.js"
},
"type": "module"
}
Are there any side effects to putting a mostly-empty package.json in a subfolder like that? When I do npm publish of the root does everything else work the same, despite the nested package.json files?
Correct, npm uses only the "root" package.json file IIRC. I've used this on some of my packages, and haven't got any issue from nested package.json files.
I have updated the title of this to be a documentation improvement that illustrates the above thing with multiple output folders and package.json files.
Most helpful comment
Note: Even if this is "not a bug", I believe the documentation could use an update to include a working example of a dual module package. Ideally, that example would be one that can be extended to be used in the real world and can work when compiling from TypeScript (i.e., an example that says to just rename all your files and all of your imports isn't particularly useful). It is also worth noting that the library should be native browser compatible as well, which means following the "MJS wrapper around CJS" pattern won't work.