fix(sourcemaps)!: preserve nullable source declarations
Accept absent map-level sourcesContent and preserve null source-name slots alongside known originals and aligned content. Represent unnamed local declarations explicitly, retain leaf-local and flattened indices, and preserve declared indexed-section order across consumers. Account for added indices before browser output allocation. Accept legacy retained rows without indices. Consolidate public CLI/MCP source-map fixtures and update the stale named-source-only assertion. BREAKING CHANGE: Browser source-map original and mapping rows expose null for unavailable source names instead of inventing an unknown-source string. New index fields are optional when reading older retained evidence. Refs #2023, #2024
This commit is contained in:
1 parent
c0904b4c1a
commit
ee50ed121e
13 files changed
+361
-70
No files matched your search
@@ -539,6 +539,7 @@ const collectSourceMapOriginals = (
|
||||
maps: readonly Readonly<Record<string, unknown>>[],
|
||||
): JavaScriptSourceMapObservation => {
|
||||
const sources: JavaScriptSourceMapOriginal[] = [];
|
||||
let declarationIndex = 0;
|
||||
for (const map of maps) {
|
||||
const names = map.sources;
|
||||
if (!Array.isArray(names))
|
||||
@@ -556,7 +557,7 @@ const collectSourceMapOriginals = (
|
||||
);
|
||||
const root = typeof map.sourceRoot === "string" ? map.sourceRoot : null;
|
||||
for (const [index, raw] of names.entries()) {
|
||||
if (typeof raw !== "string")
|
||||
if (raw !== null && typeof raw !== "string")
|
||||
return invalidSourceMap(
|
||||
file,
|
||||
"Source map contains a non-string source name.",
|
||||
@@ -564,7 +565,12 @@ const collectSourceMapOriginals = (
|
||||
const rawContent = Array.isArray(contents) ? contents[index] : undefined;
|
||||
const content = typeof rawContent === "string" ? rawContent : null;
|
||||
sources.push({
|
||||
reference: resolveJavaScriptSourceMapReference(raw, root, file.path),
|
||||
source_index: index,
|
||||
declaration_index: declarationIndex++,
|
||||
reference:
|
||||
typeof raw === "string"
|
||||
? resolveJavaScriptSourceMapReference(raw, root, file.path)
|
||||
: null,
|
||||
content,
|
||||
content_sha256: content === null ? null : sha256(content),
|
||||
});
|
||||
|
||||
@@ -47,7 +47,11 @@ export interface JavaScriptHtmlScriptObservation {
|
||||
|
||||
/** One original source declared by a local source map. */
|
||||
export interface JavaScriptSourceMapOriginal {
|
||||
readonly reference: JavaScriptSourceMapReference;
|
||||
/** Index within the leaf map's sources and sourcesContent arrays. */
|
||||
readonly source_index: number;
|
||||
/** Index across the flattened map's leaf declarations. */
|
||||
readonly declaration_index: number;
|
||||
readonly reference: JavaScriptSourceMapReference | null;
|
||||
readonly content: string | null;
|
||||
readonly content_sha256: string | null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
addArtifactContainsEdge,
|
||||
addStaticInferenceEdge,
|
||||
artifactLocalIdentity,
|
||||
createElectronRoleNode,
|
||||
linkElectronRoleToAsset,
|
||||
type JavaScriptArtifactGraphContext,
|
||||
@@ -118,22 +119,34 @@ export const addJavaScriptSourceMapOriginals = (
|
||||
if (mapFile === undefined || mapNode === undefined) continue;
|
||||
const coverage = completeApplicationCoverage();
|
||||
for (const original of sourceMap.sources) {
|
||||
const reference = original.reference;
|
||||
const node = context.accumulator.addNode({
|
||||
kind: "source-module",
|
||||
identity: {
|
||||
strategy: "source-map-original",
|
||||
stability: "source-map-exact",
|
||||
source_map_sha256: mapFile.sha256,
|
||||
original_source: original.reference.source_name,
|
||||
source_root: original.reference.source_root,
|
||||
source_sha256: original.content_sha256,
|
||||
},
|
||||
kind: reference === null ? "unknown" : "source-module",
|
||||
identity:
|
||||
reference === null
|
||||
? artifactLocalIdentity(
|
||||
mapFile.sha256,
|
||||
"source-map-unnamed-declaration",
|
||||
String(original.declaration_index),
|
||||
)
|
||||
: {
|
||||
strategy: "source-map-original",
|
||||
stability: "source-map-exact",
|
||||
source_map_sha256: mapFile.sha256,
|
||||
original_source: reference.source_name,
|
||||
source_root: reference.source_root,
|
||||
source_sha256: original.content_sha256,
|
||||
},
|
||||
observations: [
|
||||
{
|
||||
label: original.reference.source_name || null,
|
||||
source_map_reference: original.reference,
|
||||
label: reference?.source_name || null,
|
||||
source_map_reference: reference,
|
||||
properties: {
|
||||
source_index: original.source_index,
|
||||
declaration_index: original.declaration_index,
|
||||
source_name_available: reference !== null,
|
||||
content_available: original.content !== null,
|
||||
content_sha256: original.content_sha256,
|
||||
},
|
||||
evidence: artifactObservationEvidence({
|
||||
sha256: mapFile.sha256,
|
||||
@@ -151,8 +164,10 @@ export const addJavaScriptSourceMapOriginals = (
|
||||
target_node_id: node.node_id,
|
||||
relation: "contains",
|
||||
properties: {
|
||||
original_source: original.reference.source_name,
|
||||
source_root: original.reference.source_root,
|
||||
source_index: original.source_index,
|
||||
declaration_index: original.declaration_index,
|
||||
original_source: reference?.source_name ?? null,
|
||||
source_root: reference?.source_root ?? null,
|
||||
},
|
||||
evidence: artifactObservationEvidence({
|
||||
sha256: mapFile.sha256,
|
||||
|
||||
@@ -567,17 +567,21 @@ const normalizeSourceMap = (
|
||||
const sourceJsonBytes = new Map<string, number>();
|
||||
const nameJsonBytes = new Map<string, number>();
|
||||
const originalSources: ParsedSourceMapItem["original_sources"] = [];
|
||||
let declarationIndex = 0;
|
||||
for (const { leaf, resolvedSources } of decoded) {
|
||||
for (const [index, rawSource] of leaf.map.sources.entries()) {
|
||||
if ((index & 255) === 0) check();
|
||||
const resolved =
|
||||
resolvedSources[index] ?? rawSource ?? "[unknown-source]";
|
||||
const sanitized = cachedSanitized(sourcesByRaw, resolved);
|
||||
rawSource === null ? null : (resolvedSources[index] ?? null);
|
||||
const sanitized =
|
||||
resolved === null ? null : cachedSanitized(sourcesByRaw, resolved);
|
||||
const content = leaf.map.sourcesContent?.[index] ?? null;
|
||||
consumeExpandedBytes(
|
||||
context.budget,
|
||||
cachedJsonBytes(sourceJsonBytes, sanitized) +
|
||||
sourceEntryOverheadBytes() +
|
||||
(sanitized === null
|
||||
? 4
|
||||
: cachedJsonBytes(sourceJsonBytes, sanitized)) +
|
||||
sourceEntryOverheadBytes(index, declarationIndex) +
|
||||
(originalSources.length === 0 ? 0 : 1) +
|
||||
(content === null
|
||||
? 0
|
||||
@@ -586,6 +590,8 @@ const normalizeSourceMap = (
|
||||
4),
|
||||
);
|
||||
originalSources.push({
|
||||
source_index: index,
|
||||
declaration_index: declarationIndex++,
|
||||
source: sanitized,
|
||||
artifact:
|
||||
content === null
|
||||
@@ -595,16 +601,21 @@ const normalizeSourceMap = (
|
||||
}
|
||||
}
|
||||
const mappings: ParsedSourceMapItem["mappings"] = [];
|
||||
for (const { leaf, rows, resolvedSources } of decoded)
|
||||
declarationIndex = 0;
|
||||
for (const { leaf, rows, resolvedSources } of decoded) {
|
||||
const declarationBase = declarationIndex;
|
||||
declarationIndex += leaf.map.sources.length;
|
||||
for (const [line, row] of rows.entries()) {
|
||||
if ((line & 255) === 0) check();
|
||||
for (const segment of row) {
|
||||
if (!isBeforeSourceMapLeafStop(leaf, line, segment[0])) continue;
|
||||
if (segment.length === 1) continue;
|
||||
const rawSource = leaf.map.sources[segment[1]];
|
||||
if (rawSource === undefined) continue;
|
||||
const resolved =
|
||||
resolvedSources[segment[1]] ?? rawSource ?? "[unknown-source]";
|
||||
const source = cachedSanitized(sourcesByRaw, resolved);
|
||||
rawSource === null ? null : (resolvedSources[segment[1]] ?? null);
|
||||
const source =
|
||||
resolved === null ? null : cachedSanitized(sourcesByRaw, resolved);
|
||||
const name =
|
||||
segment.length === 5 ? (leaf.map.names[segment[4]] ?? null) : null;
|
||||
const generatedLine = leaf.offset.line + line + 1;
|
||||
@@ -612,9 +623,11 @@ const normalizeSourceMap = (
|
||||
segment[0] + (line === 0 ? leaf.offset.column : 0);
|
||||
consumeExpandedBytes(
|
||||
context.budget,
|
||||
cachedJsonBytes(sourceJsonBytes, source) +
|
||||
(source === null ? 4 : cachedJsonBytes(sourceJsonBytes, source)) +
|
||||
(name === null ? 4 : cachedJsonBytes(nameJsonBytes, name)) +
|
||||
mappingRowBytes(
|
||||
segment[1],
|
||||
declarationBase + segment[1],
|
||||
generatedLine,
|
||||
generatedColumn,
|
||||
segment[2] + 1,
|
||||
@@ -623,6 +636,8 @@ const normalizeSourceMap = (
|
||||
(mappings.length === 0 ? 0 : 1),
|
||||
);
|
||||
mappings.push({
|
||||
source_index: segment[1],
|
||||
declaration_index: declarationBase + segment[1],
|
||||
generated_line: generatedLine,
|
||||
generated_column: generatedColumn,
|
||||
source,
|
||||
@@ -632,6 +647,7 @@ const normalizeSourceMap = (
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const modules = originalModuleEdges(originalSources, check, (edge) =>
|
||||
consumeExpandedBytes(
|
||||
context.budget,
|
||||
@@ -705,15 +721,22 @@ const originalModuleEdges = (
|
||||
for (const source of sources) {
|
||||
check();
|
||||
if (source.artifact === null) continue;
|
||||
const sourceName = source.source;
|
||||
if (sourceName === null) {
|
||||
incomplete.push(
|
||||
`unnamed source at declaration ${String(source.declaration_index)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const parsed = parseJavaScriptSource(source.artifact.text);
|
||||
check();
|
||||
if (parsed === null) {
|
||||
incomplete.push(source.source);
|
||||
incomplete.push(sourceName);
|
||||
continue;
|
||||
}
|
||||
// A recovered program still yields the imports it did parse, but nodes
|
||||
// after an unrecoverable point are missing, so the edges are a subset.
|
||||
if (parsed.errors.length > 0) incomplete.push(source.source);
|
||||
if (parsed.errors.length > 0) incomplete.push(sourceName);
|
||||
let unboundRequires: ReadonlySet<string> | undefined;
|
||||
traverseJavaScriptAst(parsed, {
|
||||
enter: (node) => {
|
||||
@@ -742,14 +765,14 @@ const originalModuleEdges = (
|
||||
)
|
||||
return;
|
||||
}
|
||||
const key = `${source.source}\0${kind}\0${specifier}`;
|
||||
const key = `${sourceName}\0${kind}\0${specifier}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
const edge = {
|
||||
from_source: source.source,
|
||||
from_source: sourceName,
|
||||
kind,
|
||||
specifier,
|
||||
resolved_source: resolveOriginalSource(specifier, source.source),
|
||||
resolved_source: resolveOriginalSource(specifier, sourceName),
|
||||
};
|
||||
beforeEdge(edge);
|
||||
edges.push(edge);
|
||||
@@ -840,7 +863,8 @@ const preflightResolvedSourceBytes = (
|
||||
}
|
||||
for (const source of map.sources) {
|
||||
check();
|
||||
const sourceKey = source ?? "";
|
||||
if (source === null) continue;
|
||||
const sourceKey = source;
|
||||
let resolved = sourcesForRoot.get(sourceKey);
|
||||
if (resolved === undefined)
|
||||
resolved = resolveSourceMapSource(source, map.sourceRoot, mapUrl);
|
||||
@@ -948,8 +972,13 @@ const sourceMapFailureReservation = (
|
||||
jsonBytes(status) -
|
||||
jsonBytes("fetch_failed");
|
||||
|
||||
const sourceEntryOverheadBytes = (): number =>
|
||||
Buffer.byteLength('{"source":,"artifact":null}');
|
||||
const sourceEntryOverheadBytes = (
|
||||
sourceIndex: number,
|
||||
declarationIndex: number,
|
||||
): number =>
|
||||
Buffer.byteLength(
|
||||
`{"source_index":${String(sourceIndex)},"declaration_index":${String(declarationIndex)},"source":,"artifact":null}`,
|
||||
);
|
||||
|
||||
const cachedJsonBytes = (cache: Map<string, number>, value: string): number => {
|
||||
const cached = cache.get(value);
|
||||
@@ -968,13 +997,15 @@ const cachedSanitized = (cache: Map<string, string>, value: string): string => {
|
||||
};
|
||||
|
||||
const mappingRowBytes = (
|
||||
sourceIndex: number,
|
||||
declarationIndex: number,
|
||||
generatedLine: number,
|
||||
generatedColumn: number,
|
||||
originalLine: number,
|
||||
originalColumn: number,
|
||||
): number =>
|
||||
Buffer.byteLength(
|
||||
`{"generated_line":${String(generatedLine)},"generated_column":${String(generatedColumn)},"source":,"original_line":${String(originalLine)},"original_column":${String(originalColumn)},"name":}`,
|
||||
`{"source_index":${String(sourceIndex)},"declaration_index":${String(declarationIndex)},"generated_line":${String(generatedLine)},"generated_column":${String(generatedColumn)},"source":,"original_line":${String(originalLine)},"original_column":${String(originalColumn)},"name":}`,
|
||||
);
|
||||
|
||||
const resolveOriginalSource = (
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { hasValidSourceMapContents } from "./sourceMapContents.js";
|
||||
|
||||
it("accepts absent map-level source contents and aligned nullable slots", () => {
|
||||
expect(hasValidSourceMapContents(2, undefined)).toBe(true);
|
||||
expect(hasValidSourceMapContents(2, null)).toBe(true);
|
||||
expect(hasValidSourceMapContents(2, [null, "retained source"])).toBe(true);
|
||||
expect(hasValidSourceMapContents(2, ["retained source"])).toBe(false);
|
||||
expect(hasValidSourceMapContents(2, [null, 42])).toBe(false);
|
||||
});
|
||||
@@ -4,6 +4,7 @@ export const hasValidSourceMapContents = (
|
||||
contents: unknown,
|
||||
): boolean =>
|
||||
contents === undefined ||
|
||||
contents === null ||
|
||||
(Array.isArray(contents) &&
|
||||
contents.length === sourceCount &&
|
||||
contents.every(
|
||||
|
||||
@@ -14,11 +14,31 @@ describe("source-map envelope walk", () => {
|
||||
const root = {
|
||||
version: 3,
|
||||
sections: [
|
||||
{ offset: { line: 0, column: 0 }, map: leaf },
|
||||
{ offset: { line: 10, column: 0 }, map: { ...leaf, sources: [] } },
|
||||
{
|
||||
offset: { line: 0, column: 0 },
|
||||
map: {
|
||||
version: 3,
|
||||
sections: [
|
||||
{
|
||||
offset: { line: 0, column: 0 },
|
||||
map: { ...leaf, sources: ["b.ts"] },
|
||||
},
|
||||
{
|
||||
offset: { line: 1, column: 0 },
|
||||
map: { ...leaf, sources: ["c.ts"] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
offset: { line: 10, column: 0 },
|
||||
map: { ...leaf, sources: ["d.ts"] },
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(flattenSourceMapLeaves(root)).toHaveLength(2);
|
||||
expect(flattenSourceMapLeaves(root)?.map(({ sources }) => sources)).toEqual(
|
||||
[["b.ts"], ["c.ts"], ["d.ts"]],
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects sections without embedded maps", () => {
|
||||
|
||||
@@ -37,7 +37,8 @@ export const flattenSourceMapLeaves = (
|
||||
const map = pending.pop();
|
||||
if (!isVersion3Map(map)) return undefined;
|
||||
if (Array.isArray(map.sections)) {
|
||||
for (const section of map.sections) {
|
||||
for (let index = map.sections.length - 1; index >= 0; index -= 1) {
|
||||
const section = map.sections[index];
|
||||
if (!isRecord(section) || !isRecord(section.map)) return undefined;
|
||||
pending.push(section.map);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ const findingSchema = z.object({
|
||||
});
|
||||
|
||||
const originalSourceSchema = z.object({
|
||||
source: z.string(),
|
||||
source_index: z.number().int().min(0).optional(),
|
||||
declaration_index: z.number().int().min(0).optional(),
|
||||
source: z.string().nullable(),
|
||||
artifact: webTextArtifactSchema.nullable(),
|
||||
});
|
||||
const originalModuleEdgeSchema = z.object({
|
||||
@@ -42,9 +44,11 @@ const originalModuleEdgeSchema = z.object({
|
||||
resolved_source: z.string().nullable(),
|
||||
});
|
||||
const sourceMapMappingSchema = z.object({
|
||||
source_index: z.number().int().min(0).optional(),
|
||||
declaration_index: z.number().int().min(0).optional(),
|
||||
generated_line: z.number().int().min(1),
|
||||
generated_column: z.number().int().min(0),
|
||||
source: z.string(),
|
||||
source: z.string().nullable(),
|
||||
original_line: z.number().int().min(1),
|
||||
original_column: z.number().int().min(0),
|
||||
name: z.string().nullable(),
|
||||
|
||||
@@ -3,12 +3,10 @@ import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
||||
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"recovers retained prefixed source-map originals and byte identity through %s",
|
||||
@@ -29,27 +27,9 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
);
|
||||
await writeFile(mapPath, mapText);
|
||||
|
||||
let document: unknown;
|
||||
if (surface === "CLI") {
|
||||
const response = await cli.run({
|
||||
arguments: ["analyze-javascript-application", root, "--format", "json"],
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
document = response.json;
|
||||
} else {
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
const response = await call("analyze_javascript_application", {
|
||||
input_path: root,
|
||||
format: "directory",
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
document = response.structuredContent;
|
||||
}
|
||||
const { graph } = z
|
||||
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
||||
.parse(document).normalized_result;
|
||||
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
const mapDigest = createHash("sha256").update(mapText).digest("hex");
|
||||
const originalDigest = createHash("sha256").update(original).digest("hex");
|
||||
expect(graph.nodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "source-module",
|
||||
@@ -57,7 +37,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
strategy: "source-map-original",
|
||||
source_map_sha256: mapDigest,
|
||||
original_source: "original.ts",
|
||||
source_sha256: createHash("sha256").update(original).digest("hex"),
|
||||
source_sha256: originalDigest,
|
||||
}),
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -65,7 +45,13 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
source_name: "original.ts",
|
||||
map_path: "bundle.js.map",
|
||||
}),
|
||||
properties: { content_available: true },
|
||||
properties: {
|
||||
source_index: 0,
|
||||
declaration_index: 0,
|
||||
source_name_available: true,
|
||||
content_available: true,
|
||||
content_sha256: originalDigest,
|
||||
},
|
||||
evidence: expect.objectContaining({
|
||||
artifact: expect.objectContaining({ sha256: mapDigest }),
|
||||
}),
|
||||
@@ -77,3 +63,91 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
expect(await readFile(mapPath, "utf8")).toBe(mapText);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"retains nullable source-map declarations and aligned content through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-nullable-map-");
|
||||
const original = "export const answer: number = 42;";
|
||||
const unnamed = "retained source text without a declared name";
|
||||
const maps = {
|
||||
"absent.js.map": JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["unembedded.ts"],
|
||||
sourcesContent: null,
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
"mixed.js.map": JSON.stringify({
|
||||
version: 3,
|
||||
sources: [null, "original.ts"],
|
||||
sourcesContent: [unnamed, original],
|
||||
names: [],
|
||||
mappings: "ACAA",
|
||||
}),
|
||||
};
|
||||
await writeFixtureFiles(root, {
|
||||
...maps,
|
||||
"absent.js":
|
||||
"export const absent = 1;\n//# sourceMappingURL=absent.js.map\n",
|
||||
"mixed.js":
|
||||
"export const answer = 42;\n//# sourceMappingURL=mixed.js.map\n",
|
||||
});
|
||||
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
const digest = (text: string) =>
|
||||
createHash("sha256").update(text).digest("hex");
|
||||
for (const [name, mapPath, content] of [
|
||||
["unembedded.ts", "absent.js.map", null],
|
||||
["original.ts", "mixed.js.map", original],
|
||||
] as const) {
|
||||
const mapDigest = digest(maps[mapPath]);
|
||||
expect(graph.nodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "source-module",
|
||||
identity: expect.objectContaining({
|
||||
strategy: "source-map-original",
|
||||
source_map_sha256: mapDigest,
|
||||
original_source: name,
|
||||
source_sha256: content === null ? null : digest(content),
|
||||
}),
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
source_map_reference: expect.objectContaining({
|
||||
source_name: name,
|
||||
map_path: mapPath,
|
||||
}),
|
||||
properties: expect.objectContaining({
|
||||
content_available: content !== null,
|
||||
}),
|
||||
evidence: expect.objectContaining({
|
||||
artifact: expect.objectContaining({ sha256: mapDigest }),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
}
|
||||
expect(graph.nodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "unknown",
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
source_name_available: false,
|
||||
content_available: true,
|
||||
content_sha256: digest(unnamed),
|
||||
}),
|
||||
evidence: expect.objectContaining({
|
||||
artifact: expect.objectContaining({
|
||||
sha256: digest(maps["mixed.js.map"]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
expect(graph.coverage.status).toBe("complete");
|
||||
for (const [path, text] of Object.entries(maps))
|
||||
expect(await readFile(join(root, path), "utf8")).toBe(text);
|
||||
},
|
||||
);
|
||||
@@ -751,7 +751,7 @@ describe("web source-map sourcesContent validation", () => {
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["source.ts"],
|
||||
sourcesContent: [null],
|
||||
sourcesContent: null,
|
||||
names: [],
|
||||
mappings: "",
|
||||
}),
|
||||
@@ -763,10 +763,61 @@ describe("web source-map sourcesContent validation", () => {
|
||||
expect(result.items[0]).toMatchObject({
|
||||
status: "included",
|
||||
original_sources: [
|
||||
{ source: `${origin}/assets/source.ts`, artifact: null },
|
||||
{
|
||||
source_index: 0,
|
||||
declaration_index: 0,
|
||||
source: `${origin}/assets/source.ts`,
|
||||
artifact: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps unnamed source slots and mapping indices", async () => {
|
||||
const result = await fetchWebSourceMaps([request], input(), undefined, {
|
||||
fetch: () =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: [null, "source.ts"],
|
||||
sourcesContent: ["unknown source text", "named source text"],
|
||||
names: [],
|
||||
mappings: "ACAA",
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
expect(result.items[0]).toMatchObject({
|
||||
status: "partial",
|
||||
original_sources: [
|
||||
{
|
||||
source_index: 0,
|
||||
declaration_index: 0,
|
||||
source: null,
|
||||
artifact: { text: "unknown source text" },
|
||||
},
|
||||
{
|
||||
source_index: 1,
|
||||
declaration_index: 1,
|
||||
source: `${origin}/assets/source.ts`,
|
||||
artifact: { text: "named source text" },
|
||||
},
|
||||
],
|
||||
mappings: [
|
||||
{
|
||||
source_index: 1,
|
||||
declaration_index: 1,
|
||||
source: `${origin}/assets/source.ts`,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.items[0]?.limitation).toContain(
|
||||
"unnamed source at declaration 0",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("source-map original dependency syntax", () => {
|
||||
|
||||
@@ -345,7 +345,7 @@ it.each(cases)(
|
||||
item.mappings.map((entry) => [
|
||||
entry.generated_line,
|
||||
entry.generated_column,
|
||||
entry.source.replace(origin + "/", ""),
|
||||
entry.source?.replace(origin + "/", "") ?? null,
|
||||
]),
|
||||
).toEqual(expected);
|
||||
expect(item.artifact.sha256).toBe(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Readable } from "node:stream";
|
||||
import { createHash } from "node:crypto";
|
||||
import { symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
@@ -135,6 +136,7 @@ it.each([
|
||||
["a non-array sourcesContent value", { sourcesContent: "source" }],
|
||||
["a non-string, non-null source entry", { sourcesContent: [42] }],
|
||||
["a source-content array with the wrong length", { sourcesContent: [] }],
|
||||
["a non-string source name", { sources: [42] }],
|
||||
])(
|
||||
"reports %s as an invalid local source map",
|
||||
async (_description, content) => {
|
||||
@@ -187,7 +189,7 @@ it("accepts omitted and explicitly null source-map source contents", async () =>
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["null.ts"],
|
||||
sourcesContent: [null],
|
||||
sourcesContent: null,
|
||||
names: [],
|
||||
mappings: "",
|
||||
}),
|
||||
@@ -209,6 +211,77 @@ it("accepts omitted and explicitly null source-map source contents", async () =>
|
||||
expect(result.graph.coverage.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("retains unnamed source declarations without shifting later source evidence", async () => {
|
||||
const root = await createTestTempDirectory("rea-javascript-unnamed-source-");
|
||||
const text = JSON.stringify({
|
||||
version: 3,
|
||||
sections: [
|
||||
{
|
||||
offset: { line: 0, column: 0 },
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["prior.ts"],
|
||||
sourcesContent: ["export const prior = 1;"],
|
||||
names: [],
|
||||
mappings: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
offset: { line: 1, column: 0 },
|
||||
map: {
|
||||
version: 3,
|
||||
sources: [null, "original.ts"],
|
||||
sourcesContent: [null, "export const answer: number = 42;"],
|
||||
names: [],
|
||||
mappings: "ACAA",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await writeFile(join(root, "bundle.js.map"), text);
|
||||
|
||||
const result = await reconstructJavaScriptArtifact({ input_path: root });
|
||||
const nodes = result.graph.nodes;
|
||||
const original = nodes.find(
|
||||
({ kind, identity }) =>
|
||||
kind === "source-module" &&
|
||||
identity.strategy === "source-map-original" &&
|
||||
identity.original_source === "original.ts",
|
||||
);
|
||||
const unnamed = nodes.find(
|
||||
({ kind, observations }) =>
|
||||
kind === "unknown" &&
|
||||
observations.some(
|
||||
({ properties }) => properties.source_name_available === false,
|
||||
),
|
||||
);
|
||||
|
||||
expect(original?.identity).toMatchObject({
|
||||
strategy: "source-map-original",
|
||||
source_sha256: createHash("sha256")
|
||||
.update("export const answer: number = 42;")
|
||||
.digest("hex"),
|
||||
});
|
||||
expect(original?.observations[0]?.properties).toMatchObject({
|
||||
source_index: 1,
|
||||
declaration_index: 2,
|
||||
content_available: true,
|
||||
});
|
||||
expect(unnamed?.observations[0]?.properties).toMatchObject({
|
||||
source_index: 0,
|
||||
declaration_index: 1,
|
||||
source_name_available: false,
|
||||
content_available: false,
|
||||
content_sha256: null,
|
||||
});
|
||||
expect(unnamed?.identity).toMatchObject({
|
||||
strategy: "artifact-local-key",
|
||||
namespace: "source-map-unnamed-declaration",
|
||||
key: "1",
|
||||
});
|
||||
expect(result.graph.coverage.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("retains every repeated content observation and containment path", async () => {
|
||||
const root = await createTestTempDirectory("rea-javascript-observations-");
|
||||
await Promise.all(
|
||||
|
||||
Reference in new issue
Block a user