From 791524c2faa1c43f487a0b22e51c2d5dc72fdeb1 Mon Sep 17 00:00:00 2001 From: Hemang Varshney Date: Sat, 24 Oct 2026 19:44:19 +0530 Subject: [PATCH] fix(importer): reach package-main selection for trailing-slash directory references (#4219) * fix(importer): reach package-main selection for trailing-slash directory references posix.normalize preserves a trailing slash in the specifier, so normalized was sub/ while dirname(match) was sub; the resolvedThroughIndex comparison failed and selectPackageMain never ran for require("./sub/"). Compare against the slash-stripped directory so trailing-slash directory references take the same package-main path as the plain spelling. Boundary rows pair each trailing-slash request with the existing manifest fixtures and are validated against Node's own resolver in the same table. Fixes #4189 * fix(importer): derive trailing-slash directory identity with path semantics The lookbehind strip kept a trailing slash when the final component ended in a line terminator (require("./pkg\n/")), so dirname(match) still never matched and the trailing-slash path skipped package-main selection for those directories. Join the normalized path with "." instead: path semantics collapse any trailing slash run regardless of the preceding character, and behave identically to the strip on every other spelling. Boundary fixture gains a newline directory with a package.json, an index and a declared main so the index path (not the no-match branch) exercises the comparison. Addresses Codex P1 on #4219 * fix(importer): normalize package-main targets with path semantics --------- Co-authored-by: Hemang Varshney <218273197+hemv-857@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- src/application/ReferenceSourceImportEntries.ts | 14 ++++++++++---- .../cli/referenceSourceDirectoryImports.test.ts | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/application/ReferenceSourceImportEntries.ts b/src/application/ReferenceSourceImportEntries.ts index 1fac687310..717f52b5a3 100644 --- a/src/application/ReferenceSourceImportEntries.ts +++ b/src/application/ReferenceSourceImportEntries.ts @@ -269,9 +269,9 @@ const selectPackageMain = ( // package, and a trailing separator does not make the entry a directory. if (posix.isAbsolute(manifest.main)) return { to: posix.normalize(manifest.main), resolution: "unresolved" }; - const target = posix - .normalize(posix.join(packageDirectory, manifest.main)) - .replace(/(?<=.)\/+$/u, ""); + // Joining "." strips the trailing separator run with path semantics; a + // lookbehind regex kept separators behind line terminators (main "lib\n/"). + const target = posix.join(posix.join(packageDirectory, manifest.main), "."); // Node would load a declared entry outside the imported root. if (logicalPathEscapesRoot(target)) return { to: target, resolution: "unresolved" }; @@ -395,9 +395,15 @@ const resolveInternalSpecifier = ( return packageMainForDirectory(normalized, context) !== undefined ? selectPackageMain(normalized, undefined, context, mode) : { to: normalized, resolution: "unresolved" }; + // posix.normalize preserves the specifier's trailing slash while + // dirname(match) never carries one; derive the directory identity with path + // semantics (joining "." collapses trailing slashes, including ones a regex + // would keep behind a line terminator) so a trailing-slash reference still + // reaches package-main selection the same way the plain spelling does. + const directory = posix.join(normalized, "."); const resolvedThroughIndex = posix.basename(match).startsWith("index.") && - posix.dirname(match).normalize("NFC") === normalized.normalize("NFC"); + posix.dirname(match).normalize("NFC") === directory.normalize("NFC"); return resolvedThroughIndex ? selectPackageMain(posix.dirname(match), match, context, mode) : { to: match, resolution: "internal" }; diff --git a/tests/boundary/cli/referenceSourceDirectoryImports.test.ts b/tests/boundary/cli/referenceSourceDirectoryImports.test.ts index cb2395a15a..aefe55aaea 100644 --- a/tests/boundary/cli/referenceSourceDirectoryImports.test.ts +++ b/tests/boundary/cli/referenceSourceDirectoryImports.test.ts @@ -37,10 +37,14 @@ const prepareReferenceSourceDirectoryFixture = ["legacy-order.cjs", "./dep", "dep/index.js"], ["bare-index.cjs", "./bare", "bare.js"], ["package-main.cjs", "./pkg", "pkg/actual.js"], + ["package-main-slash-specifier.cjs", "./pkg/", "pkg/actual.js"], + ["package-main-newline-slash.cjs", "./pkg\n/", "pkg\n/actual.js"], ["package-main-no-index.cjs", "./no-index-pkg", "no-index-pkg/actual.js"], ["package-main-fallback.cjs", "./stale", "stale/index.js"], + ["package-main-fallback-slash.cjs", "./stale/", "stale/index.js"], ["typed-index-collision.cjs", "./typed", "typed.js"], ["package-main-slash.cjs", "./slashed", "slashed/lib.js"], + ["package-main-manifest-newline-slash.cjs", "./nlpkg/", "nlpkg/lib\n.js"], ["json.cjs", "./data", "data.json"], ["json-explicit.cjs", "./data.json", "data.json"], ["json-before-index.cjs", "./conf", "conf.json"], @@ -70,6 +74,12 @@ const prepareReferenceSourceDirectoryFixture = ["bare/index", 'module.exports = "extensionless index";\n'], ["only/index", 'module.exports = "extensionless index";\n'], ["pkg/package.json", '{"main":"actual.js"}\n'], + ["pkg\n/package.json", '{"main":"actual.js"}\n'], + [ + "pkg\n/actual.js", + 'module.exports = "declared main behind line terminator";\n', + ], + ["pkg\n/index.js", 'module.exports = "index not selected";\n'], ["pkg/actual.js", 'module.exports = "declared main";\n'], ["pkg/index.js", 'module.exports = "ignored index";\n'], ["no-index-pkg/package.json", '{"main":"actual.js"}\n'], @@ -87,6 +97,13 @@ const prepareReferenceSourceDirectoryFixture = ["slashed/lib.js", 'module.exports = "file main";\n'], ["slashed/lib/index.js", 'module.exports = "not selected";\n'], ["slashed/index.js", 'module.exports = "not selected";\n'], + ["nlpkg/package.json", '{"main":"lib\\n/"}\n'], + [ + "nlpkg/lib\n.js", + 'module.exports = "file main behind line terminator";\n', + ], + ["nlpkg/lib\n/index.js", 'module.exports = "not selected";\n'], + ["nlpkg/index.js", 'module.exports = "entry index";\n'], ["absolute/package.json", `${JSON.stringify({ main: outside })}\n`], ["absolute/index.js", 'module.exports = "not selected";\n'], ["data.json", '{"value":1}\n'],