Description
Version
v22.14.0
Platform
Microsoft Windows NT 10.0.26100.0
x64
Subsystem
internal/modules/cjs/loader
What steps will reproduce the bug?
- Create a package which declares explicit exports with the respective loaders to be used. The
type
is set tomodule
and the file extensions forrequire
exports arejs
.
packages/pkg1/package.json
{
"name": "pkg1",
"version": "1.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"require": "./main.js",
"import": "./main.mjs"
},
"./module": {
"require": "./module.js",
"import": "./module.mjs"
}
}
}
- In the
.js
files use commonjs code (require
)
packages/pkg1/main.js
const util = require('node:util');
module.exports = function() {
console.log(util.styleText('green', 'Hello from pkg1/main (esm)'));
}
- Consume the package in a CJS project
packages/consumer/package.json
{
"name": "consumer",
"version": "1.0.0",
"private": true,
"scripts": {
"test": "node test.js"
},
"dependencies": {
"pkg1": "^1.0.0"
}
}
packages/consumer/test.js
const pkg1 = require('pkg1');
const pkg1Module = require('pkg1/module');
pkg1();
pkg1Module();
- Run the
test.js
in node and get presented an error:
PS D:\Dev\node-cjs-loading\packages\consumer> npm run test
> consumer@1.0.0 test
> node test.js
file:///D:/Dev/node-cjs-loading/packages/pkg1/main.js:1
const util = require('node:util');
^
ReferenceError: require is not defined
at file:///D:/Dev/node-cjs-loading/packages/pkg1/main.js:1:14
at ModuleJobSync.runSync (node:internal/modules/esm/module_job:395:35)
at ModuleLoader.importSyncForRequire (node:internal/modules/esm/loader:360:47)
at loadESMFromCJS (node:internal/modules/cjs/loader:1385:24)
at Module._compile (node:internal/modules/cjs/loader:1536:5)
at Object..js (node:internal/modules/cjs/loader:1706:10)
at Module.load (node:internal/modules/cjs/loader:1289:32)
at Function._load (node:internal/modules/cjs/loader:1108:12)
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
Node.js v22.14.0
npm ERR! Lifecycle script `test` failed with error:
npm ERR! Error: command failed
npm ERR! in workspace: consumer@1.0.0
npm ERR! at location: D:\Dev\node-cjs-loading\packages\consumer
How often does it reproduce? Is there a required condition?
Reproduces always.
What is the expected behavior? Why is that the expected behavior?
I would expect that node loads the main.js
with the commonjs loader as it is and ignore the fact that .js
maps to ESM from a "type: module" spec.
IMO the conditions of the conditional exports, need to also control which loader is used for the file. Its not just defining which file to use when resolving a module from CJS or ESM context.
What do you see instead?
Node.js only uses the exports for looking up the filename but then does a fallback to the default extension based loading. Due to type: module
it treats it as ESM and wrongly loads it.
Additional information
I know my setup is a bit special so I want to share some insights why I have it like this, and why changing is tricky.
In reality my files are not "commonjs" but rather Universal Module Definition (UMD) files. My library is also used in classical browser script tag setups. This is for backwards compatibility with the current major version.
Changing to .cjs
is a workaround that could work. But its has these impacts:
a) I considering the rename a breaking change for my library and I don't want to bump my major version just for this sake.
b) It could impact CDNs and Web Servers which might not have the extension registered as text/javascript
and browsers triggering warning/errors on load due to wrong mimetypes. (e.g. jsdelivr serves it as application/node
, or IIS 10 in doesn't have it either)
c) Its not the "correct" file extension for browser usage.
Another workaround is to remove type: module
. But within my git repository I want to use primarily ESM (e.g. for scripts or configuration files). And this becomes problematic when using typescript configuration files. Some tools do not support something like a .mts
breaking a variety of things.
I've looked through the following docs and generally I'd expect a .js
to be loaded with the CJS loader if its declared accordingly in the exports
section.
https://nodejs.org/api/packages.html#dual-commonjses-module-packages
https://nodejs.org/api/packages.html#conditional-exports
https://nodejs.org/api/packages.html#exports
Looking at the code I can also see that Node just resolves the plain path, but then ignores the "loader hint".
- File path resolve: https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js#L753
- Module load:
https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js#L1286 .js
always usingtype
from the package json: https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js#L1887
Node should remember the loader type from the conditional export and load the file accordingly, not use to some alternative/automatic loading behaviors than specified. That's why I report this behavior rather as a bug than an improvement request.