fix(javascript): normalize package URLs before array selection (#1916)

This commit is contained in:
morluto authored and GitHub committed 2026-10-12 01:36:42 +08:00
1 parent 2976fac30d
commit 369cd3dfdc
5 files changed
+318 -33

No files matched your search

@@ -741,6 +741,51 @@ describe("nested and array package export targets", () => {
},
);
});
describe("package export target URL normalization compared with Node", () => {
it.each([
[["./.\t./outside.cjs", "./array.cjs"]],
[["./.%\n2e/outside.cjs", "./array.cjs"]],
[["./.\r./outside.cjs", "./array.cjs"]],
["./arr\tay.cjs"],
["./array.cjs \t"],
["./array.cjs?selected=one#fragment"],
["./.\t./example/array.cjs"],
] as const)(
"resolves normalized target %j through both loaders",
async (target) => {
for (const moduleKind of ["import", "require"] as const) {
const { node, outcome } = await compareWithNode(target, moduleKind);
expect(node).toBe("array.cjs");
expect(outcome).toMatchObject({
resolution_status: "resolved",
resolved_path: "node_modules/example/array.cjs",
});
}
},
);
it.each([
[["./arr%2fay.cjs", "./array.cjs"], "rejected"],
[["./miss\ting.cjs", "./array.cjs"], "not-found"],
] as const)(
"retains the first selected target failure in %j",
async (target, status) => {
for (const moduleKind of ["import", "require"] as const) {
const { node, node_error, outcome } = await compareWithNode(
target,
moduleKind,
);
expect(node).toBeNull();
expect(node_error).not.toBeNull();
expect(outcome).toMatchObject({
resolution_status: status,
resolved_path: null,
});
}
},
);
});
describe("unmatched package export conditions", () => {
it.each([
[[{ browser: "./default.cjs" }], "browser"],
@@ -455,6 +455,138 @@ describe("package import target arrays compared with Node", () => {
});
});
describe("package import target URL normalization compared with Node", () => {
it("normalizes targets before package-root validation and array selection", async () => {
const root = await createTestTempDirectory("rea-package-target-url-");
const directory = "app space#?%λ";
const imports = {
"#tab-escape": ["./.\t./outside.cjs", "./fallback.cjs"],
"#newline-escape": ["./.%\n2e/outside.cjs", "./fallback.cjs"],
"#cr-escape": ["./.\r./outside.cjs", "./fallback.cjs"],
"#normalized": "./fall\tback.cjs",
"#trimmed": "./fallback.cjs \t",
"#suffix": "./fallback.cjs?selected=one#fragment",
"#return": `./.\t./${encodeURIComponent(directory)}/fallback.cjs`,
};
const contents = await writeTree(root, {
[`${directory}/package.json`]: JSON.stringify({ imports }),
[`${directory}/main.cjs`]: "module.exports = 1;",
[`${directory}/fallback.cjs`]: "module.exports = 1;",
});
const inventoried = inventory(contents);
for (const specifier of Object.keys(imports)) {
const required = await nodeRequireTarget(
join(root, directory, "main.cjs"),
specifier,
);
expect(required.error_code, specifier).toBeNull();
expect(posixRelative(root, required.target ?? "")).toBe(
`${directory}/fallback.cjs`,
);
const imported = await nodeImportTarget(join(root, directory), specifier);
expect(imported.error_code, specifier).toBeNull();
expect(posixRelative(root, fileURLToPath(imported.target ?? ""))).toBe(
`${directory}/fallback.cjs`,
);
for (const moduleKind of ["require", "import"] as const)
expect(
resolveAlias(
inventoried,
specifier,
`${directory}/main.cjs`,
moduleKind,
),
specifier,
).toMatchObject({
declared_path: specifier,
resolution_status: "resolved",
resolved_path: `${directory}/fallback.cjs`,
});
}
});
it("keeps URL decoding and missing-file failures after array selection", async () => {
const root = await createTestTempDirectory(
"rea-package-target-url-refusal-",
);
const contents = await writeTree(root, {
"package.json": JSON.stringify({
imports: {
"#escape": "./.\t./outside.cjs",
"#separator": ["./fall%2fback.cjs", "./fallback.cjs"],
"#missing": ["./miss\ting.cjs", "./fallback.cjs"],
"#query-separator": ["./first.cjs?selected=%2f", "./fallback.cjs"],
},
}),
"main.cjs": "module.exports = 1;",
"fallback.cjs": "module.exports = 1;",
"first.cjs": "module.exports = 2;",
});
const inventoried = inventory(contents);
for (const [specifier, code] of [
["#escape", "ERR_INVALID_PACKAGE_TARGET"],
["#separator", "ERR_INVALID_MODULE_SPECIFIER"],
] as const) {
expect(
(await nodeRequireTarget(join(root, "main.cjs"), specifier)).error_code,
).toBe(code);
expect((await nodeImportTarget(root, specifier)).error_code).toBe(code);
for (const moduleKind of ["require", "import"] as const)
expect(
resolveAlias(inventoried, specifier, "main.cjs", moduleKind),
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
}
expect(
(await nodeRequireTarget(join(root, "main.cjs"), "#missing")).error_code,
).toBe("MODULE_NOT_FOUND");
const imported = await nodeImportTarget(root, "#missing");
expect(posixRelative(root, fileURLToPath(imported.target ?? ""))).toBe(
"missing.cjs",
);
for (const moduleKind of ["require", "import"] as const)
expect(
resolveAlias(inventoried, "#missing", "main.cjs", moduleKind),
).toMatchObject({
resolution_status: "not-found",
resolved_path: null,
limitations: [expect.stringContaining("missing.cjs")],
});
expect(
resolveAlias(
inventoried,
"#escape",
"main.cjs",
"require",
).limitations.join(" "),
).toContain(JSON.stringify("./.\t./outside.cjs"));
expect(
(await nodeRequireTarget(join(root, "main.cjs"), "#query-separator"))
.error_code,
).toBe("ERR_INVALID_MODULE_SPECIFIER");
expect(
resolveAlias(inventoried, "#query-separator", "main.cjs", "require"),
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
const withQuery = await nodeImportTarget(root, "#query-separator");
expect(posixRelative(root, fileURLToPath(withQuery.target ?? ""))).toBe(
"first.cjs",
);
expect(
resolveAlias(inventoried, "#query-separator", "main.cjs", "import"),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "first.cjs",
});
expect(
resolveAlias(
inventoried,
"#missing",
"main.cjs",
"require",
).limitations.join(" "),
).toContain(JSON.stringify("./miss\ting.cjs"));
});
});
describe("package import aliases compared with Node", () => {
it("matches Node require and import for an exact root alias", async () => {
const root = await createTestTempDirectory("rea-import-alias-root-");