Ship a joined island data path when another directory has its name

A module that names fallback/payload.txt through join(here, "fallback",
"payload.txt") also names "payload.txt" alone. That name was checked
against the first file of that name the directory walk found, so a
payload.txt in another subdirectory made the build fail with SC8006 even
though the joined path ships. A name whose file a named path of the same
module ships is no longer reported.
This commit is contained in:
Chris Tate
2026-10-30 00:18:39 -05:00
parent 932a14673c
commit 6874c3b562
2 changed files with 22 additions and 2 deletions
@@ -334,6 +334,21 @@ describe("the collected module tree", () => {
expect(paths.filter((path) => !path.endsWith(".cjs"))).toEqual(["assets/config.json"]);
});
test("a joined data path ships even when another directory has a file of that name", () => {
const root = project({
"src/main.ts": "",
"src/loader.cjs":
'const { join } = require("node:path");\nrequire("node:fs").readFileSync(join(__dirname, "fallback", "payload.txt"));\n',
"src/a-other/payload.txt": "not named by the module\n",
"src/fallback/payload.txt": "shipped\n",
});
const collector = new NodeIslandModuleCollector();
expect(collector.add(join(root, "src/main.ts"), "./loader.cjs")).toBeNull();
const paths = collector.tree()!.files.map((file) => file.path);
expect(paths).toContain("fallback/payload.txt");
expect(paths).not.toContain("a-other/payload.txt");
});
test("a named file that is only elsewhere in the module's directory is a build error", () => {
const root = project({
"src/main.ts": "",
@@ -368,6 +368,8 @@ export class NodeIslandModuleCollector {
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
};
const unresolved: { name: string; at: number }[] = [];
/** The names of the files this module's named paths ship. */
const shippedNames = new Set<string>();
for (const [name, at] of namedDataPaths(text)) {
if (isAbsolute(name) || name.includes("\0")) continue;
const path = resolve(root, name);
@@ -382,6 +384,7 @@ export class NodeIslandModuleCollector {
if (target === null || !inside(target)) continue;
// A link inside the directory stays a link in the tree (placed).
this.files.add(this.placed(path));
shippedNames.add(basename(path));
}
if (unresolved.length === 0) return null;
// The files of the module's directory by name, links leading outside it
@@ -405,9 +408,11 @@ export class NodeIslandModuleCollector {
};
walk(root);
for (const { name, at } of unresolved) {
// A name of a file another named path ships (`"payload.txt"` beside
// `join(here, "fallback", "payload.txt")`) is not missing, wherever
// other files of that name are.
if (shippedNames.has(basename(name))) continue;
const found = byName.get(basename(name));
// A file another named path ships (`"payload.txt"` beside
// `join(here, "fallback", "payload.txt")`) is not missing.
if (found === undefined || this.files.has(trackedRealpath(found) ?? found)) continue;
const line = text.slice(0, at).split("\n").length;
const there = portable(relative(root, found));