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:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-09 11:49:32 +08:00
1 parent e1afbc1914
commit 2a620c06cd
3 files changed
+88 -2

No files matched your search

@@ -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"],