-
Notifications
You must be signed in to change notification settings - Fork 29.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conditional exports doesn't respect alternative paths #37928
Comments
I debugged into |
@nodejs/modules |
Exports is generally designed to resolve unambiguously without hitting the disk. So what you're observing is working as intended: As soon as we find a valid file name, we assume that it's the intended target. It explicitly doesn't matter if the file currently exists or not. E.g. it would be valid to resolve to a non-existing path, create the file, and then load it. |
@jkrems Tanks for the explanation. That would mean that the this feature only applies to Webpack when resolving module paths. Then directly a follow-up question: When the cjs loader is loading a CommonJS module with disabled package encapsulation by using the exports field in package.json, then the cjs loader doesn’t try to resolve a directory import to an index.js file (see example below). So in the end the cjs loader is acting like the esm loader as described in the cjs loader pseudo algorithm. I would have assumed now, as this is a CommonJS module, that the normal directory resolution behavior of the cjs loader is applied. Since this is not explicitly mentioned in the node docs (at least I didn't find it), does this mean that using the exports field in the package.json will disable the resolution of index.js directory files in CommonJS Modules? Moreover, I wonder about this because the node docs also says that Additionally, there is also the Package.json
then
|
You can add a {
"name": "foo",
"exports": {
"./bar": "./dist/cjs/bar/index.js",
"./*": {
"require": [
"./dist/cjs/*.js"
]
}
}
} |
@aduh95 That’s what I meant. In the end, I want to export all files from a transpiled directory including the directory paths with the index.js. Packages using this one, can then pick the modules they need. It would be really tedious for large packages to enter all directory exports manually into the exports field (apart from the fact that it would not be well maintainable in the long run) or to write a script here that would enter it automatically would be a bit overengineered. It took me some time to realize that Node handles CommonJS modules differently than before when using exports, and that they then behave like ES modules. This change in behaviour is also not directly apparent from reading the Node documentation. Thanks for the clarification. |
Yes the
Maybe the documentation could use a bit of improvement to remove the ambiguity here. If you are willing to open a PR for that, that'd be awesome :) |
I didn't think of that - simple workaround that can be used by cjs and esm alike 👍
I'll take a look at it and add a few lines when I have a little more time. |
Have the same expection, the most common usage is leverage the exports field to import the dist artifacts without the dist folder name disappearing in path.
|
Can someone share a link where I can read about the rationale for the design of "target" alternatives? Based on this thread it seems that for a vanilla package consumed by Node they are not useful. Curious to learn more. I am in a tool author context here so looking for deeper details beyond day to day usage from consumer point of view. |
Your proposal spec explains how the fallback array can be used: {
"exports": {
"./core-polyfill": ["std:core-module", "./core-polyfill.js"]
}
}
Wouldn't "validation failures" include the "Module Not Found error" mentioned in the sentence above? |
As we transition to a This is essentially "differential loading" in Node (CJS/ESM) and I would like this behaviour to be retained when I eventually stop distributing CJS code entirely. To do this I (want to) use conditional exports and subpattern exports. From the consumer's perspective: If importing a package from a CJS context, they would expect the following resolution rules: const pkg = require('pkg') /** resolves to */ 'node_modules/pkg/require/index.js'
const pkg = require('pkg/index') /** resolves to */ 'node_modules/pkg/require/index'
/** or */ 'node_modules/pkg/require/index.js'
/** or */ 'node_modules/pkg/require/index/index.js'
const pkg = require('pkg/index.js') /** resolves to */ 'node_modules/pkg/require/index.js'
const pkg = require('pkg/foo.js') /** resolves to */ 'node_modules/pkg/require/foo.js'
const pkg = require('pkg/foo') /** resolves to */ 'node_modules/pkg/require/foo'
/** or */ 'node_modules/pkg/require/foo.js'
/** or */ 'node_modules/pkg/require/foo/index.js'
const pkg = require('pkg/foo/bar') /** resolves to */ 'node_modules/pkg/require/foo/bar'
/** or */ 'node_modules/pkg/require/foo/bar.js'
/** or */ 'node_modules/pkg/require/foo/bar/index.js'
const pkg = require('pkg/foo/bar.js') /** resolves to */ 'node_modules/pkg/require/foo/bar.js' And if importing the package from a MJS context, they would expect the following resolution rules: import pkg from 'pkg' /** resolves to */ 'node_modules/pkg/import/index.js'
import pkg from 'pkg/index' /** not valid, requires exact match (file extension in this case) */
import pkg from 'pkg/index.js' /** resolves to */ 'node_modules/pkg/import/index.js'
import pkg from 'pkg/foo.js' /** resolves to */ 'node_modules/pkg/import/foo.js'
import pkg from 'pkg/foo' /** not valid, requires exact match (file extension in this case) */
import pkg from 'pkg/foo/bar.js' /** resolves to */ 'node_modules/pkg/import/foo/bar.js' In concept, a {
"name": "pkg",
"exports": {
".": {
"import": "./import/index.js",
"require": "./require/index.js"
},
"./*": {
"import": "./import/*",
"require": ["./require/*", "./require/*.js", "./require/*/index.js"]
}
}
} However due to Node returning the first item in the While I understand that Node is try to minimize hitting the FS as much as possible - I believe supporting this use case is vital in transitioning from commonjs to modules as library maintainers can better distribute packages with both formats such that they behave consistently with expectations. Looking at the resolution and loading algorithm described here: https://nodejs.org/api/esm.html#resolution-and-loading-algorithm If This would not perform any worse than CJS imports that exclude extensions or import folders do currently - because Node already needs to test the FS for those alternative paths. |
I believe you should just be able to do: {
"./*": {
"import": "./import/*",
"require": "./require/*"
}
} Because it's the CommonJS resolution algorithm that attempts the extension-less and directory index paths. |
I would have expected so too however it does not work that way in practice: Example repo ( Where I set the /project/node_modules/pkg/package.json /project/src/cjs/case-1.js <- does not work /project/src/cjs/case-1_1.js <- works Note that I have an additional superficial I would expect: require('pkg/foo') /** resolves to */ 'node_modules/pkg/require/foo'
/** or */ 'node_modules/pkg/require/foo.js' // <---- Match this
/** or */ 'node_modules/pkg/require/foo/index.js' However Node does not fall back to alternative imports when coming from a commonjs consumer, instead I get the error:
Indicating that it only tested If I change const { foo } = require('../../node_modules/pkg/require/foo')
console.log(foo) Have I misconfigured my package? Is this a bug or expected behaviour? If it's a bug and we expect the commonjs resolution algorithm to work here then my comment about using arrays to recreate it is not relevant. |
What steps will reproduce the bug?
When importing a package with conditional exports declared in the package.json, then node doesn't respect alternative path exports:
Node only tries to resolve the first one and fails immediately if the first path to a module doesn't exist. I don't know if this is the intended behaviour, but in the Webpack guide it is explicitly mentioned as a feature (https://webpack.js.org/guides/package-exports/#alternatives).
What is the expected behavior?
Node tries to resolve an import against each entry in the conditional exports array and returns the first successful loaded module.
What do you see instead?
Node only resolves the first entry of the conditional exports alternatives.
The text was updated successfully, but these errors were encountered: