fix(javascript): distinguish file URL paths from query identity
This commit is contained in:
parent
dc038d16bb
commit
f43fc02475
|
|
@ -509,17 +509,6 @@ describe("contextual JavaScript artifact path resolution", () => {
|
|||
resolution_status: "resolved",
|
||||
resolved_path: "renderer/local.js",
|
||||
});
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "file:///assets/app.js#entry",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "assets/app.js",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["node:fs", "electron", "../../escape.js", "%2e%2e/escape.js"])(
|
||||
|
|
@ -576,6 +565,55 @@ describe("contextual JavaScript artifact path resolution", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("JavaScript file URL resolution", () => {
|
||||
it("resolves file URL paths without interpreting query bytes as path syntax", () => {
|
||||
const files = fileMap([file("assets/app.js", "root")]);
|
||||
for (const declaredPath of [
|
||||
"file:///assets/app.js#entry",
|
||||
"FILE:///assets/app.js?token=%2F#entry",
|
||||
]) {
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath,
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({
|
||||
declared_path: declaredPath,
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "assets/app.js",
|
||||
});
|
||||
}
|
||||
|
||||
for (const declaredPath of [
|
||||
"file:///assets%2Fapp.js",
|
||||
"file:///assets%2\nFapp.js",
|
||||
]) {
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath,
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
|
||||
}
|
||||
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "file:/assets/app.js",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({ resolution_status: "external", resolved_path: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe("contextual JavaScript package path resolution", () => {
|
||||
it("follows package entrypoints beyond the former recursion ceiling", () => {
|
||||
const files = fileMap([
|
||||
|
|
|
|||
|
|
@ -205,17 +205,26 @@ const contextualCandidate = (
|
|||
context === "url-reference"
|
||||
? stripQueryAndFragment(input.declaredPath)
|
||||
: input.declaredPath;
|
||||
const rejected = rejectDeclaration(declared);
|
||||
if (rejected !== null) return rejected;
|
||||
if (context === "html-reference") return htmlCandidate(input);
|
||||
if (context === "module-specifier") {
|
||||
if (context === "module-specifier" && /^file:/iu.test(declared)) {
|
||||
const fileUrl = fileUrlPath(declared);
|
||||
if (fileUrl === null)
|
||||
return unresolvedCandidate("rejected", [
|
||||
"The file URL path contains syntax that cannot identify a canonical artifact path.",
|
||||
]);
|
||||
if (fileUrl !== undefined)
|
||||
return input.moduleKind === "require"
|
||||
? unresolvedCandidate("external", [
|
||||
"Node CommonJS require specifiers do not resolve file URLs as local artifacts.",
|
||||
])
|
||||
: fileUrl;
|
||||
return unresolvedCandidate("external", [
|
||||
"File URL authorities or invalid file URL forms cannot be mapped to artifact-local paths.",
|
||||
]);
|
||||
}
|
||||
const rejected = rejectDeclaration(declared);
|
||||
if (rejected !== null) return rejected;
|
||||
if (context === "html-reference") return htmlCandidate(input);
|
||||
if (context === "module-specifier") {
|
||||
if (hasScheme(declared))
|
||||
return unresolvedCandidate("external", [
|
||||
"URL and Node builtin schemes are outside static artifact module resolution.",
|
||||
|
|
@ -1194,12 +1203,26 @@ const exactPackageTargetNotFound = (
|
|||
],
|
||||
});
|
||||
|
||||
const fileUrlPath = (value: string): string | undefined => {
|
||||
if (!value.startsWith("file://")) return undefined;
|
||||
const fileUrlPath = (value: string): string | null | undefined => {
|
||||
if (!/^file:\/\//iu.test(value)) return undefined;
|
||||
// URL parsing removes tabs and newlines and normalizes encoded dot segments.
|
||||
// Check the raw path first, excluding URL identity suffixes, then check the
|
||||
// parsed pathname so parser normalization cannot introduce a separator.
|
||||
const rawPath = stripQueryAndFragment(value);
|
||||
if (
|
||||
value.includes("\0") ||
|
||||
value.includes("\\") ||
|
||||
/[\t\n\r]/u.test(rawPath) ||
|
||||
!admitsCanonicalPathSyntax(rawPath)
|
||||
)
|
||||
return null;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "file:" || url.hostname !== "") return undefined;
|
||||
return decodeURIComponent(url.pathname).replace(/^\/+/, "");
|
||||
if (!admitsCanonicalPathSyntax(url.pathname)) return null;
|
||||
const decoded = decodeURIComponent(url.pathname);
|
||||
if (decoded.includes("\0")) return null;
|
||||
return decoded.replace(/^\/+/, "");
|
||||
} catch (cause: unknown) {
|
||||
// Unparseable file URLs have no path to resolve.
|
||||
void cause;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const createPackageSubpathFixture = async () => {
|
|||
const selfSource = 'const self = require("fi%xture"); module.exports = self;';
|
||||
const importSource = [
|
||||
'import literal from "fi%xture";',
|
||||
'import uri from "file:///src/uri-dep.cjs";',
|
||||
'import uri from "FILE:///src/uri-dep.cjs?token=%2F#entry";',
|
||||
].join("\n");
|
||||
const files = {
|
||||
"src/main.cjs": source,
|
||||
|
|
@ -181,7 +181,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
|||
relation: "imports",
|
||||
properties: expect.objectContaining({
|
||||
module_link_kind: "import",
|
||||
specifier: "file:///src/uri-dep.cjs",
|
||||
specifier: "FILE:///src/uri-dep.cjs?token=%2F#entry",
|
||||
resolved_path: "src/uri-dep.cjs",
|
||||
resolution_status: "resolved",
|
||||
}),
|
||||
|
|
|
|||
Loading…
Reference in New Issue