fix(reference): never resolve a directory to an extensionless index (#3789)
resolveInternalSpecifier offered posix.join(dir, "index") as the
second candidate, ahead of every extension. With both dep.js and an
extensionless dep/index present, require("./dep") therefore resolved to
dep/index, a file that neither Node nor TypeScript ever selects.
Directory candidates now carry an extension. File and index ordering is
otherwise unchanged, so Node-validated cases such as dep/index.js
winning over dep.cjs keep their meaning.
Fixes #3381
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d69acc3455
commit
3ded799bc7
2 files changed
+11
-1
No files matched your search
@@ -110,9 +110,11 @@ const resolveInternalSpecifier = (
|
||||
const directoryReference =
|
||||
specifier.endsWith("/") || [".", ".."].includes(posix.basename(specifier));
|
||||
const suffixes = ["", ".ts", ".js", ".mjs", ".cjs", ".tsx", ".jsx"];
|
||||
// A directory resolves only to an index with an extension; neither Node nor
|
||||
// TypeScript selects an extensionless `index` file.
|
||||
const candidates = suffixes.flatMap((suffix) => [
|
||||
...(directoryReference ? [] : [`${normalized}${suffix}`]),
|
||||
posix.join(normalized, `index${suffix}`),
|
||||
...(suffix === "" ? [] : [posix.join(normalized, `index${suffix}`)]),
|
||||
]);
|
||||
// Retain present runtime files first; substitute only a missing counterpart.
|
||||
// Counterparts cover explicit filenames, not directory/index resolution.
|
||||
|
||||
@@ -29,11 +29,13 @@ describe.skipIf(process.platform === "win32")(
|
||||
["direct.cjs", "./lib.js", "lib.js"],
|
||||
["fallback.cjs", "./fallback", "fallback/index.js"],
|
||||
["legacy-order.cjs", "./dep", "dep/index.js"],
|
||||
["bare-index.cjs", "./bare", "bare.js"],
|
||||
] as const;
|
||||
const unresolved = [
|
||||
["missing.cjs", "./missing/", "missing/"],
|
||||
["escape.cjs", "../outside/", "../outside/"],
|
||||
["main.ts", "./folder.js/.", "folder.js"],
|
||||
["only-bare.cjs", "./only/", "only/"],
|
||||
] as const;
|
||||
const files = new Map<string, string>([
|
||||
["index.js", 'module.exports = "root index";\n'],
|
||||
@@ -48,6 +50,9 @@ describe.skipIf(process.platform === "win32")(
|
||||
'export const value = "emitted filename counterpart";\n',
|
||||
],
|
||||
["missing.js", 'module.exports = "not a directory";\n'],
|
||||
["bare.js", 'module.exports = "file before directory";\n'],
|
||||
["bare/index", 'module.exports = "extensionless index";\n'],
|
||||
["only/index", 'module.exports = "extensionless index";\n'],
|
||||
...[...imports, ...unresolved].map(
|
||||
([path, specifier]) =>
|
||||
[path, `require(${JSON.stringify(specifier)});\n`] as const,
|
||||
@@ -75,6 +80,9 @@ describe.skipIf(process.platform === "win32")(
|
||||
expect(() =>
|
||||
createRequire(join(root, "main.ts")).resolve("./folder.js/."),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
createRequire(join(root, "only-bare.cjs")).resolve("./only/"),
|
||||
).toThrow();
|
||||
expect(
|
||||
createRequire(join(root, "escape.cjs")).resolve("../outside/"),
|
||||
).toBe(outside);
|
||||
|
||||
Reference in new issue
Block a user