fix(javascript): resolve package exports targets only to the exact file (#1181)
A selected package exports target names one file, and Node tries no extension or directory index for it. REA still resolved it like a relative specifier, so `exports: "./file"` with only `file.js` present, or `"./folder"` with only `folder/index.js`, reported a resolved import that Node rejects under both import and require. Resolve an exports target only to its exact file and otherwise report not-found with a limitation naming the target. Legacy main entries keep their extension and index fallbacks. Fixes #1174 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e1afbc1914
commit
2a620c06cd
|
|
@ -391,6 +391,68 @@ describe("generic package entry heuristics", () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe("package exports exact targets", () => {
|
||||
it.each([
|
||||
["./file", "app/node_modules/fixture/file.js"],
|
||||
["./folder", "app/node_modules/fixture/folder/index.js"],
|
||||
])(
|
||||
"resolves exports target %s only to that exact file",
|
||||
(target, nearMiss) => {
|
||||
const files = fileMap([
|
||||
file("app/consumer.js", "root"),
|
||||
file(
|
||||
"app/node_modules/fixture/package.json",
|
||||
"root",
|
||||
JSON.stringify({ exports: target }),
|
||||
),
|
||||
file(nearMiss, "root"),
|
||||
]);
|
||||
for (const moduleKind of ["import", "require"] as const) {
|
||||
const resolution = resolve({
|
||||
declaredPath: "fixture",
|
||||
sourcePath: "app/consumer.js",
|
||||
context: "module-specifier",
|
||||
moduleKind,
|
||||
files,
|
||||
});
|
||||
expect(resolution).toMatchObject({
|
||||
resolution_status: "not-found",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(resolution.limitations.join(" ")).toContain(
|
||||
"exports targets are exact files",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps extension and index fallbacks for a legacy main entry", () => {
|
||||
for (const [main, target] of [
|
||||
["./file", "app/node_modules/fixture/file.js"],
|
||||
["./folder", "app/node_modules/fixture/folder/index.js"],
|
||||
] as const) {
|
||||
const files = fileMap([
|
||||
file("app/consumer.js", "root"),
|
||||
file(
|
||||
"app/node_modules/fixture/package.json",
|
||||
"root",
|
||||
JSON.stringify({ main }),
|
||||
),
|
||||
file(target, "root"),
|
||||
]);
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "fixture",
|
||||
sourcePath: "app/consumer.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({ resolution_status: "resolved", resolved_path: target });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("directory package entrypoint precedence", () => {
|
||||
it.each([
|
||||
["require", '{"main":"actual.cjs"}', "actual.cjs"],
|
||||
|
|
|
|||
|
|
@ -105,8 +105,11 @@ const resolvePackagePath = (
|
|||
],
|
||||
},
|
||||
);
|
||||
const resolved =
|
||||
mode === "files-and-index"
|
||||
// An exports target names one exact file; Node tries no extension or index.
|
||||
const resolved = exportsTargetUrl
|
||||
? (resolveFileCandidates(input, [confined]) ??
|
||||
exactExportsTargetNotFound(confined))
|
||||
: mode === "files-and-index"
|
||||
? (resolveFileCandidates(input, [
|
||||
...fileCandidates(confined),
|
||||
...indexCandidates(confined),
|
||||
|
|
@ -619,6 +622,14 @@ const notFoundCandidate = (): CandidateResolution => ({
|
|||
],
|
||||
});
|
||||
|
||||
const exactExportsTargetNotFound = (path: string): CandidateResolution => ({
|
||||
resolvedPath: null,
|
||||
status: "not-found",
|
||||
limitations: [
|
||||
`The selected package exports target ${path} does not exist in the inventoried artifact container; exports targets are exact files, so no extension or directory index was tried.`,
|
||||
],
|
||||
});
|
||||
|
||||
const fileUrlPath = (value: string): string | undefined => {
|
||||
if (!value.startsWith("file://")) return undefined;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,18 @@ const cases: readonly {
|
|||
nodeError: "ERR_INVALID_PACKAGE_TARGET",
|
||||
rejectedTarget: "./../outside.cjs",
|
||||
},
|
||||
{
|
||||
name: "extensionless target is not completed with an extension",
|
||||
exports: "./actual",
|
||||
expected: null,
|
||||
nodeError: "MODULE_NOT_FOUND",
|
||||
},
|
||||
{
|
||||
// Node reports ERR_UNSUPPORTED_DIR_IMPORT for import, MODULE_NOT_FOUND for require.
|
||||
name: "directory target does not load its index",
|
||||
exports: "./folder",
|
||||
expected: null,
|
||||
},
|
||||
{
|
||||
name: "top-level array skips an invalid target",
|
||||
exports: ["./../outside.cjs", "./actual.cjs"],
|
||||
|
|
@ -166,6 +178,7 @@ it.each(
|
|||
"node_modules/fixture/node_modules/actual.cjs",
|
||||
"node_modules/fixture/node_modules.cjs",
|
||||
"node_modules/fixture/literal%2e.cjs",
|
||||
"node_modules/fixture/folder/index.js",
|
||||
].map((path) => [path, `module.exports = ${JSON.stringify(path)};`]),
|
||||
),
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue