mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): preserve and resolve complete source-map URLs (#1788)
Keep line-comment URL declarations intact and percent-decode local URL paths once, preserving raw Evidence identity and artifact-root confinement. Fixes #1781. Fixes #1782. Assisted-by: Suika (gensokyo-skills:suika-scatter-gather-planning)
This commit is contained in:
@@ -1106,6 +1106,33 @@ describe("contextual JavaScript module identity", () => {
|
||||
});
|
||||
|
||||
describe("url-reference JavaScript artifact path resolution", () => {
|
||||
it.each([
|
||||
"app%00.js.map",
|
||||
"app%FF.js.map",
|
||||
"app%C0%AF.js.map",
|
||||
"app%2ejs.map",
|
||||
"nested%2fworker.js",
|
||||
"nested%5cworker.js",
|
||||
"../../outside.js.map",
|
||||
])("rejects an inadmissible URL path: %s", (declaredPath) => {
|
||||
const resolution = resolve({
|
||||
declaredPath,
|
||||
sourcePath: "dist/app.js",
|
||||
context: "url-reference",
|
||||
files: fileMap([
|
||||
file("dist/app.js.map", "root"),
|
||||
file("dist/nested/worker.js", "root"),
|
||||
]),
|
||||
});
|
||||
expect(resolution).toMatchObject({
|
||||
declared_path: declaredPath,
|
||||
resolution_context: "url-reference",
|
||||
resolution_status: "rejected",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(resolution.limitations.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("resolves sibling paths and rejects external URL forms", () => {
|
||||
const files = fileMap([
|
||||
file("dist/app.js", "root"),
|
||||
|
||||
@@ -162,6 +162,14 @@ const contextualCandidate = (
|
||||
return unresolvedCandidate("external", [
|
||||
"URL schemes and protocol-relative URLs are outside this local artifact path context.",
|
||||
]);
|
||||
if (context === "url-reference") {
|
||||
const decoded = percentDecodeUrlPath(declared);
|
||||
if (decoded === null)
|
||||
return unresolvedCandidate("rejected", [
|
||||
"The URL reference path percent-decodes to NUL or to bytes that are not UTF-8.",
|
||||
]);
|
||||
declared = decoded;
|
||||
}
|
||||
return relativeCandidate(input.sourcePath, declared);
|
||||
};
|
||||
|
||||
|
||||
@@ -353,7 +353,9 @@ export const addSourceMapDirectives = (
|
||||
const urlOffset = commentSource.slice(equalsStart + 1).search(/\S/u);
|
||||
if (urlOffset === -1) continue;
|
||||
const declaredStart = equalsStart + 1 + urlOffset;
|
||||
const terminator = commentSource.slice(declaredStart).search(/[\s*]/u);
|
||||
const terminator = commentSource
|
||||
.slice(declaredStart)
|
||||
.search(commentSource.startsWith("//") ? /\s/u : /[\s*]/u);
|
||||
const declaredEnd =
|
||||
terminator === -1 ? commentSource.length : declaredStart + terminator;
|
||||
if (declaredEnd === declaredStart) continue;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
@@ -445,6 +446,174 @@ describe("Electron unavailable identity through stdio MCP", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves complete line-comment source-map URLs through CLI and MCP", async () => {
|
||||
const root = await createTestTempDirectory("rea-source-map-url-cli-");
|
||||
const directives = [
|
||||
"//# sourceMappingURL=app.js.map?variant=a*modern",
|
||||
"//@ sourceMappingURL=app.js.map?variant=a*legacy",
|
||||
"//# sourceMappingURL=app.js.map#first*section",
|
||||
];
|
||||
await writeFile(
|
||||
join(root, "app.js"),
|
||||
["export const value = 1;", ...directives, ""].join("\n"),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "app.js.map"),
|
||||
JSON.stringify({ version: 3, sources: [], names: [], mappings: "" }),
|
||||
);
|
||||
const { graph } = z
|
||||
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
||||
.parse(
|
||||
await runCli(["analyze-javascript-application", root, "--json"]),
|
||||
).normalized_result;
|
||||
const map = graph.nodes.find(
|
||||
({ kind, observations }) =>
|
||||
kind === "source-map" &&
|
||||
observations.some(({ properties }) => properties.path === "app.js.map"),
|
||||
);
|
||||
const references = map?.observations.filter(
|
||||
({ label }) => label === "source map reference",
|
||||
);
|
||||
expect(references).toHaveLength(directives.length);
|
||||
for (const [index, directive] of directives.entries()) {
|
||||
const declaredUrl = directive.slice(directive.indexOf("=") + 1);
|
||||
const digest = createHash("sha256").update(declaredUrl).digest("hex");
|
||||
expect(references).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: {
|
||||
declared_url: declaredUrl,
|
||||
declared_url_sha256: digest,
|
||||
resolved_path: "app.js.map",
|
||||
available: true,
|
||||
},
|
||||
evidence: expect.objectContaining({
|
||||
location: {
|
||||
available: true,
|
||||
value: {
|
||||
kind: "source-range",
|
||||
source: "app.js",
|
||||
start: { line: index + 2, column: 0 },
|
||||
end: { line: index + 2, column: directive.length },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "maps_to",
|
||||
target_node_id: map?.node_id,
|
||||
properties: {
|
||||
declared_url_sha256: digest,
|
||||
resolved_path: "app.js.map",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
expect((await analyzeThroughStdioMcp(root)).graph).toEqual(graph);
|
||||
}, 20_000);
|
||||
|
||||
it("resolves source-map URL paths once without changing their declarations", async () => {
|
||||
const root = await createTestTempDirectory("rea-source-map-encoding-cli-");
|
||||
const cases = [
|
||||
["space%20part.js.map", "space part.js.map"],
|
||||
["literal%25.js.map", "literal%.js.map"],
|
||||
["encoded%2520.js.map", "encoded%20.js.map"],
|
||||
["space%20part.js.map?v=1#section", "space part.js.map"],
|
||||
["caf%C3%A9.js.map", "café.js.map"],
|
||||
["literal%ZZ.js.map", "literal%ZZ.js.map"],
|
||||
] as const;
|
||||
for (const path of new Set(cases.map(([, path]) => path))) {
|
||||
await writeFile(
|
||||
join(root, path),
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
file: path,
|
||||
sources: [],
|
||||
names: [],
|
||||
mappings: "",
|
||||
}),
|
||||
);
|
||||
}
|
||||
await writeFile(
|
||||
join(root, "app.js"),
|
||||
cases.map(([url]) => `//# sourceMappingURL=${url}\n`).join("") +
|
||||
'new Worker("./worker%20file.js");\n' +
|
||||
'navigator.serviceWorker.register("./service%20worker.js");\n',
|
||||
);
|
||||
await writeFile(join(root, "worker file.js"), "export const worker = 1;\n");
|
||||
await writeFile(
|
||||
join(root, "service worker.js"),
|
||||
"export const service = 2;\n",
|
||||
);
|
||||
const { graph } = z
|
||||
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
||||
.parse(
|
||||
await runCli(["analyze-javascript-application", root, "--json"]),
|
||||
).normalized_result;
|
||||
for (const [index, [declaredUrl, path]] of cases.entries()) {
|
||||
const map = graph.nodes.find(
|
||||
({ kind, observations }) =>
|
||||
kind === "source-map" &&
|
||||
observations.some(({ properties }) => properties.path === path),
|
||||
);
|
||||
const digest = createHash("sha256").update(declaredUrl).digest("hex");
|
||||
expect(map?.observations).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: {
|
||||
declared_url: declaredUrl,
|
||||
declared_url_sha256: digest,
|
||||
resolved_path: path,
|
||||
available: true,
|
||||
},
|
||||
evidence: expect.objectContaining({
|
||||
location: {
|
||||
available: true,
|
||||
value: {
|
||||
kind: "source-range",
|
||||
source: "app.js",
|
||||
start: { line: index + 1, column: 0 },
|
||||
end: {
|
||||
line: index + 1,
|
||||
column: "//# sourceMappingURL=".length + declaredUrl.length,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "maps_to",
|
||||
target_node_id: map?.node_id,
|
||||
properties: { declared_url_sha256: digest, resolved_path: path },
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const [kind, specifier, path] of [
|
||||
["worker", "./worker%20file.js", "worker file.js"],
|
||||
["service-worker", "./service%20worker.js", "service worker.js"],
|
||||
]) {
|
||||
const worker = graph.nodes.find((node) => node.kind === kind);
|
||||
expect(worker?.observations).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
declared_specifier: specifier,
|
||||
resolved_path: path,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
source_node_id: worker?.node_id,
|
||||
relation: "maps_to",
|
||||
properties: { resolved_path: path },
|
||||
}),
|
||||
);
|
||||
}
|
||||
expect((await analyzeThroughStdioMcp(root)).graph).toEqual(graph);
|
||||
}, 20_000);
|
||||
|
||||
describe("JavaScript application path CLI", () => {
|
||||
it("accepts a relative local application path and preserves canonical Evidence identity", async () => {
|
||||
const root = await createTestTempDirectory("rea-relative-application-cli-");
|
||||
|
||||
Reference in New Issue
Block a user