fix(javascript): store source map URLs once

This commit is contained in:
morluto committed 2026-10-10 02:49:46 +08:00
1 parent 7915ee5178
commit ed8fcae94b
4 files changed
+212 -62

No files matched your search

+8
View File
@@ -73,6 +73,14 @@ The directed relation vocabulary is `contains`, `loads`, `imports`, `maps_to`,
and `changed_from`. Provider names are deliberately absent from both
vocabularies.
For a `maps_to` edge targeting a source-map node, the edge carries
`resolved_path` and `declared_url_sha256`. The target node's reference
observation carries that same digest and the exact `declared_url`; match the
digest to select the URL belonging to that edge when several references share
one physical source map. The full URL is kept out of node identity, display
label, and edge properties, so large inline data URLs are stored once per
source-map reference.
For example, a source-owned synthetic fixture proves this path without using a
third-party application:
@@ -1,3 +1,5 @@
import { createHash } from "node:crypto";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -283,6 +285,9 @@ const addSourceMapEdge = (
context: JavaScriptArtifactGraphContext,
input: FindingInput<StaticSourceMap>,
): void => {
const declaredUrlSha256 = createHash("sha256")
.update(input.value.declared_url)
.digest("hex");
const resolvedPath = resolveArtifactPathByContext({
declaredPath: input.value.declared_url,
sourcePath: input.file.path,
@@ -293,38 +298,43 @@ const addSourceMapEdge = (
resolvedPath === null ? undefined : context.filesByPath.get(resolvedPath);
const resolvedNode =
resolvedPath === null ? undefined : context.fileNodes.get(resolvedPath);
const target =
resolvedFile?.kind === "source-map" && resolvedNode !== undefined
? resolvedNode
: context.accumulator.addNode({
kind: "source-map",
identity: artifactLocalIdentity(
const isAvailable =
resolvedFile?.kind === "source-map" && resolvedNode !== undefined;
const target = context.accumulator.addNode({
kind: "source-map",
identity:
isAvailable && resolvedNode !== undefined
? resolvedNode.identity
: artifactLocalIdentity(
input.file.sha256,
"unresolved-source-map",
input.value.declared_url,
declaredUrlSha256,
),
observations: [
{
label: input.value.declared_url,
properties: {
declared_url: input.value.declared_url,
resolved_path: resolvedPath,
available: false,
},
evidence: staticInferenceEvidence({
sha256: input.file.sha256,
path: input.file.path,
range: input.value.location,
operation: "discover-source-map",
coverage: input.coverage,
confidence: "medium",
limitations: [
"The declared source map was not present in the inventoried artifact.",
],
}),
},
],
});
observations: [
{
label: "source map reference",
properties: {
declared_url: input.value.declared_url,
declared_url_sha256: declaredUrlSha256,
resolved_path: resolvedPath,
available: isAvailable,
},
evidence: staticInferenceEvidence({
sha256: input.file.sha256,
path: input.file.path,
range: input.value.location,
operation: "discover-source-map",
coverage: input.coverage,
confidence: "medium",
limitations: isAvailable
? []
: [
"The declared source map was not present in the inventoried artifact.",
],
}),
},
],
});
addStaticInferenceEdge(context, {
source: input.asset,
target,
@@ -333,7 +343,7 @@ const addSourceMapEdge = (
coverage: input.coverage,
relation: "maps_to",
properties: {
declared_url: input.value.declared_url,
declared_url_sha256: declaredUrlSha256,
resolved_path: resolvedPath,
},
});
@@ -20,8 +20,12 @@ it("creates source-map graph relationships only for real comments", async () =>
).toEqual([]);
expect(
withoutDirective.graph.edges.filter(
({ relation, properties }) =>
relation === "maps_to" && properties.declared_url !== undefined,
({ relation, target_node_id }) =>
relation === "maps_to" &&
withoutDirective.graph.nodes.some(
({ kind, node_id }) =>
kind === "source-map" && node_id === target_node_id,
),
),
).toEqual([]);
@@ -32,12 +36,17 @@ it("creates source-map graph relationships only for real comments", async () =>
expect(
withDirective.graph.nodes.filter(({ kind }) => kind === "source-map"),
).toHaveLength(1);
const nodes = new Map(
withDirective.graph.nodes.map((node) => [node.node_id, node]),
);
const sourceMapEdge = withDirective.graph.edges.find(
({ relation, target_node_id }) =>
relation === "maps_to" &&
nodes.get(target_node_id)?.kind === "source-map",
);
expect(
withDirective.graph.edges
.filter(
({ relation, properties }) =>
relation === "maps_to" && properties.declared_url !== undefined,
)
.map(({ properties }) => properties.declared_url),
).toEqual(["real.map"]);
nodes
.get(sourceMapEdge?.target_node_id ?? "")
?.observations.map(({ properties }) => properties.declared_url),
).toContain("real.map");
});
@@ -6,6 +6,10 @@ import { expect, it } from "vitest";
import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
parseJavaScriptApplicationGraph,
serializeJavaScriptApplicationGraph,
} from "../../../src/domain/javascript/javascriptApplicationGraph.js";
it.each(["\n", "\r\n", "\r", "\u2028", "\u2029"])(
"preserves source-map graph evidence coordinates after %j",
@@ -16,33 +20,139 @@ it.each(["\n", "\r\n", "\r", "\u2028", "\u2029"])(
await writeFile(join(root, "app.js"), source);
const result = await reconstructJavaScriptArtifact({ input_path: root });
expect(result.statistics.parse_failures).toBe(0);
const edges = result.graph.edges.filter(
({ relation, properties }) =>
relation === "maps_to" && properties.declared_url === "app.js.map",
const nodes = new Map(
result.graph.nodes.map((node) => [node.node_id, node]),
);
expect(edges).toHaveLength(1);
expect(edges[0]?.evidence).toMatchObject({
state: "inferred",
artifact: {
sha256: createHash("sha256").update(source).digest("hex"),
const edge = result.graph.edges.find(
({ relation, target_node_id }) =>
relation === "maps_to" &&
nodes.get(target_node_id)?.kind === "source-map",
);
expect(edge).toMatchObject({
properties: {
declared_url_sha256: createHash("sha256")
.update("app.js.map")
.digest("hex"),
resolved_path: null,
},
location: {
available: true,
value: {
kind: "source-range",
source: "app.js",
start: { line: 2, column: 2 },
end: { line: 2, column: directive.length + 2 },
evidence: {
state: "inferred",
artifact: {
sha256: createHash("sha256").update(source).digest("hex"),
},
location: {
available: true,
value: {
kind: "source-range",
source: "app.js",
start: { line: 2, column: 2 },
end: { line: 2, column: directive.length + 2 },
},
},
},
});
expect(edges[0]?.properties).toMatchObject({
declared_url: "app.js.map",
resolved_path: null,
});
expect(
nodes
.get(edge?.target_node_id ?? "")
?.observations.some(
({ properties }) => properties.declared_url === "app.js.map",
),
).toBe(true);
},
);
it("owns an inline source-map URL once and recovers it from the target after JSON roundtrip", async () => {
const root = await createTestTempDirectory("rea-inline-source-map-url-");
const sourceContent = "source;".repeat(16_384);
const sourceMap = JSON.stringify({
version: 3,
sources: ["src/app.js"],
sourcesContent: [sourceContent],
names: [],
mappings: "AAAA",
});
const declaredUrl = `data:application/json;base64,${Buffer.from(sourceMap).toString("base64")}`;
await writeFile(
join(root, "app.js"),
`const app = true;\n//# sourceMappingURL=${declaredUrl}\n`,
);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const serialized = serializeJavaScriptApplicationGraph(result.graph);
const graph = parseJavaScriptApplicationGraph(JSON.parse(serialized));
const edge = graph.edges.find(
({ relation, target_node_id }) =>
relation === "maps_to" &&
graph.nodes.some(
({ kind, node_id }) =>
kind === "source-map" && node_id === target_node_id,
),
);
const target = graph.nodes.find(
({ node_id }) => node_id === edge?.target_node_id,
);
const edgeUrlSha256 = edge?.properties.declared_url_sha256;
const observation = target?.observations.find(
({ properties }) => properties.declared_url_sha256 === edgeUrlSha256,
);
expect(edgeUrlSha256).toBe(
createHash("sha256").update(declaredUrl).digest("hex"),
);
expect(observation?.properties.declared_url).toBe(declaredUrl);
expect(observation?.properties).toMatchObject({
resolved_path: null,
available: false,
});
expect(serialized.split(declaredUrl)).toHaveLength(2);
});
it("associates each source-map URL with its edge when aliases share one physical map", async () => {
const root = await createTestTempDirectory("rea-source-map-aliases-");
await mkdir(join(root, "dist"));
await writeFile(
join(root, "dist", "app.js.map"),
JSON.stringify({ version: 3, sources: [], names: [], mappings: "" }),
);
await writeFile(
join(root, "dist", "one.js"),
"//# sourceMappingURL=app.js.map\n",
);
await writeFile(
join(root, "dist", "two.js"),
"//# sourceMappingURL=./app.js.map\n",
);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const serialized = serializeJavaScriptApplicationGraph(result.graph);
const graph = parseJavaScriptApplicationGraph(JSON.parse(serialized));
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
const mapEdges = graph.edges.filter(
({ relation, target_node_id }) =>
relation === "maps_to" &&
nodes.get(target_node_id)?.kind === "source-map",
);
expect(mapEdges).toHaveLength(2);
expect(mapEdges[0]?.target_node_id).toBe(mapEdges[1]?.target_node_id);
expect(
mapEdges.map((edge) => {
const target = nodes.get(edge.target_node_id);
const observation = target?.observations.find(
({ properties }) =>
properties.declared_url_sha256 ===
edge.properties.declared_url_sha256,
);
return {
declared_url: observation?.properties.declared_url,
resolved_path: edge.properties.resolved_path,
};
}),
).toEqual([
{ declared_url: "app.js.map", resolved_path: "dist/app.js.map" },
{ declared_url: "./app.js.map", resolved_path: "dist/app.js.map" },
]);
});
it("resolves a bare source-map file name and worker URL next to the script", async () => {
const root = await createTestTempDirectory("rea-source-map-relative-");
await mkdir(join(root, "dist"));
@@ -72,13 +182,26 @@ it("resolves a bare source-map file name and worker URL next to the script", asy
const result = await reconstructJavaScriptArtifact({ input_path: root });
const nodes = new Map(result.graph.nodes.map((node) => [node.node_id, node]));
const mapEdge = result.graph.edges.find(
({ relation, properties }) =>
relation === "maps_to" && properties.declared_url === "app.js.map",
({ relation, target_node_id }) =>
relation === "maps_to" &&
nodes.get(target_node_id)?.kind === "source-map",
);
expect(mapEdge?.properties).toMatchObject({
declared_url: "app.js.map",
expect(mapEdge?.properties).toEqual({
declared_url_sha256: createHash("sha256")
.update("app.js.map")
.digest("hex"),
resolved_path: "dist/app.js.map",
});
expect(
nodes
.get(mapEdge?.target_node_id ?? "")
?.observations.some(
({ properties }) =>
properties.declared_url === "app.js.map" &&
properties.resolved_path === "dist/app.js.map" &&
properties.available === true,
),
).toBe(true);
expect(nodes.get(mapEdge?.target_node_id ?? "")?.identity.strategy).toBe(
"content-digest",
);