mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
fix(javascript): store source map URLs once
This commit is contained in:
1 parent
7915ee5178
commit
ed8fcae94b
4 files changed
+212
-62
No files matched your search
@@ -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",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user