fix(javascript): match source-map originals that name parent directories (#1215)
* fix(javascript): match source-map originals that name parent directories * fix(javascript): resolve parent segments against the source map's directory * fix(javascript): resolve source originals against their map location * fix(javascript): preserve URI and empty source map path semantics * fix(javascript): separate source URL paths from opaque URI payloads * fix(javascript): preserve filesystem names during URI classification * style(javascript): format source URI presence guard * fix(javascript): resolve source map references with URL semantics * fix(javascript): preserve literal UNC source map paths --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
1 parent
24a3aa08d1
commit
889fb374df
4 files changed
+713
-75
No files matched your search
@@ -0,0 +1,69 @@
|
||||
import { posix } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
interface SourceMapPath {
|
||||
readonly value: string;
|
||||
readonly scope: "artifact-relative" | "suffix";
|
||||
}
|
||||
|
||||
/** Resolve a source-map URL reference without changing its recorded identity. */
|
||||
export const resolveJavaScriptSourceMapPath = (
|
||||
source: string,
|
||||
mapPath?: string,
|
||||
): SourceMapPath | null => {
|
||||
const portable = source.replaceAll("\\", "/");
|
||||
const windowsPath = /^[a-z]:\//iu.test(portable) || source.startsWith("\\\\");
|
||||
// Preserve parent traversal rather than letting URL resolution clamp at '/'.
|
||||
const anchor = `/${"source/".repeat(portable.split("/").length + 1)}`;
|
||||
const directory = posix.join(
|
||||
anchor,
|
||||
posix.dirname(mapPath?.replaceAll("\\", "/") ?? "."),
|
||||
);
|
||||
const base = pathToFileURL(`${directory}/`, { windows: false });
|
||||
const absolute = windowsPath ? windowsFileUrl(source) : URL.parse(source);
|
||||
const resolved = windowsPath ? absolute : URL.parse(source, base);
|
||||
if (
|
||||
resolved === null ||
|
||||
!resolved.pathname.startsWith("/") ||
|
||||
resolved.pathname.endsWith("/")
|
||||
)
|
||||
return null;
|
||||
const relative =
|
||||
absolute === null
|
||||
? !portable.startsWith("/")
|
||||
: absolute.href !== resolved.href;
|
||||
const path = decodedUrlPath(resolved);
|
||||
if (path === null) return null;
|
||||
const value = relative
|
||||
? posix.relative(anchor, path)
|
||||
: path.replace(/^\/+/, "");
|
||||
if (value === "" || value === ".." || value.startsWith("../")) return null;
|
||||
return {
|
||||
value,
|
||||
scope: relative && mapPath !== undefined ? "artifact-relative" : "suffix",
|
||||
};
|
||||
};
|
||||
|
||||
const windowsFileUrl = (path: string): URL | null => {
|
||||
try {
|
||||
return pathToFileURL(path, { windows: true });
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof TypeError) return null;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
const decodedUrlPath = (url: URL): string | null => {
|
||||
// Encoded separators must not manufacture a different filesystem hierarchy.
|
||||
if (/%(?:2f|5c)/iu.test(url.pathname)) return null;
|
||||
try {
|
||||
if (url.protocol === "file:")
|
||||
return fileURLToPath(url, {
|
||||
windows: url.hostname !== "" && url.hostname !== "localhost",
|
||||
}).replaceAll("\\", "/");
|
||||
return `${url.host}${decodeURIComponent(url.pathname)}`;
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof URIError || cause instanceof TypeError) return null;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
createJavaScriptApplicationNode,
|
||||
} from "./javascriptApplicationGraph.js";
|
||||
import { createHistoricalSourceGraph } from "../referenceSourceGraph.js";
|
||||
import { compareCodePoints } from "../canonicalOrdering.js";
|
||||
import { compareSourceToBundle } from "./sourceToBundleComparison.js";
|
||||
import {
|
||||
SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS,
|
||||
@@ -187,44 +188,47 @@ describe("source to bundle digest changes", () => {
|
||||
});
|
||||
|
||||
describe("source to bundle path syntax", () => {
|
||||
it.each(["src/modified#part.ts", "src/modified?part.ts"])(
|
||||
"preserves literal filesystem punctuation in %s",
|
||||
(path) => {
|
||||
const node = moduleNode("punctuated", { path });
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: [node.node_id],
|
||||
nodes: [node],
|
||||
edges: [],
|
||||
coverage: {
|
||||
status: "complete",
|
||||
truncated: false,
|
||||
omitted_count: 0,
|
||||
limits: [],
|
||||
},
|
||||
limitations: [],
|
||||
});
|
||||
const result = compareSourceToBundle({
|
||||
reference: historicalGraph("complete", [path]),
|
||||
application: {
|
||||
evidenceId: EVIDENCE_ID,
|
||||
rootArtifactSha256: HASH.artifact,
|
||||
graph,
|
||||
},
|
||||
});
|
||||
expect(item(result, path)).toMatchObject({
|
||||
status: "unknown",
|
||||
current_node_ids: [node.node_id],
|
||||
candidates: [
|
||||
expect.objectContaining({
|
||||
signals: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "current-path-exact" }),
|
||||
]),
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"src/modified#part.ts",
|
||||
"src/modified?part.ts",
|
||||
"pkg:app/src/modified#part.ts",
|
||||
"pkg:app/src/modified?part.ts",
|
||||
"data:/src/modified.ts",
|
||||
])("preserves literal filesystem punctuation in %s", (path) => {
|
||||
const node = moduleNode("punctuated", { path });
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: [node.node_id],
|
||||
nodes: [node],
|
||||
edges: [],
|
||||
coverage: {
|
||||
status: "complete",
|
||||
truncated: false,
|
||||
omitted_count: 0,
|
||||
limits: [],
|
||||
},
|
||||
limitations: [],
|
||||
});
|
||||
const result = compareSourceToBundle({
|
||||
reference: historicalGraph("complete", [path]),
|
||||
application: {
|
||||
evidenceId: EVIDENCE_ID,
|
||||
rootArtifactSha256: HASH.artifact,
|
||||
graph,
|
||||
},
|
||||
});
|
||||
expect(item(result, path)).toMatchObject({
|
||||
status: "unknown",
|
||||
current_node_ids: [node.node_id],
|
||||
candidates: [
|
||||
expect.objectContaining({
|
||||
signals: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "current-path-exact" }),
|
||||
]),
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"https://example.test/src/modified.ts?revision=2#section",
|
||||
@@ -347,7 +351,7 @@ const historicalGraph = (
|
||||
content_state: "hashed" as const,
|
||||
limitations: [],
|
||||
})),
|
||||
],
|
||||
].sort((left, right) => compareCodePoints(left.path, right.path)),
|
||||
relationships: [],
|
||||
parse_failures: [],
|
||||
exclusions: [],
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type SourceToBundleSignal,
|
||||
} from "./sourceToBundleComparisonSchemas.js";
|
||||
import { isDigest } from "../digests.js";
|
||||
import { resolveJavaScriptSourceMapPath } from "./javascriptSourceMapPaths.js";
|
||||
|
||||
type SourceFile = Extract<
|
||||
HistoricalSourceGraph["entries"][number],
|
||||
@@ -23,6 +24,8 @@ type CurrentPathKind =
|
||||
interface CurrentPath {
|
||||
readonly kind: CurrentPathKind;
|
||||
readonly value: string;
|
||||
/** Artifact-relative map resolution identifies one path, not its suffixes. */
|
||||
readonly allowSuffix: boolean;
|
||||
}
|
||||
|
||||
interface CurrentProjection {
|
||||
@@ -93,7 +96,9 @@ export const buildSourceToBundleCandidateIndex = (
|
||||
for (const digest of projection.digests)
|
||||
addIndexValue(byDigest, digest, projection.node.node_id);
|
||||
for (const path of projection.paths) {
|
||||
for (const suffix of pathSuffixes(path.value))
|
||||
for (const suffix of path.allowSuffix
|
||||
? pathSuffixes(path.value)
|
||||
: [path.value])
|
||||
addIndexValue(byPathSuffix, suffix, projection.node.node_id);
|
||||
addIndexValue(
|
||||
byBasename,
|
||||
@@ -169,7 +174,22 @@ const projectCurrentNode = (node: ApplicationNode): CurrentProjection => {
|
||||
if (node.identity.strategy === "source-map-original") {
|
||||
if (node.identity.source_sha256 !== null)
|
||||
digests.add(node.identity.source_sha256);
|
||||
addPath(paths, "source-map-original", node.identity.original_source);
|
||||
const mapLocations = node.observations.flatMap(({ evidence }) =>
|
||||
evidence.extractor.operation === "parse-local-source-map" &&
|
||||
evidence.location.available &&
|
||||
evidence.location.value.kind === "artifact-path"
|
||||
? [evidence.location.value.path]
|
||||
: [],
|
||||
);
|
||||
if (mapLocations.length === 0)
|
||||
addPath(paths, "source-map-original", node.identity.original_source);
|
||||
for (const mapPath of mapLocations)
|
||||
addPath(
|
||||
paths,
|
||||
"source-map-original",
|
||||
node.identity.original_source,
|
||||
mapPath,
|
||||
);
|
||||
}
|
||||
if (node.identity.strategy === "canonical-path")
|
||||
addPath(paths, "canonical-path", node.identity.path);
|
||||
@@ -177,8 +197,18 @@ const projectCurrentNode = (node: ApplicationNode): CurrentProjection => {
|
||||
const sourceDigest = observation.properties.source_sha256;
|
||||
if (typeof sourceDigest === "string" && isDigest(sourceDigest))
|
||||
digests.add(sourceDigest);
|
||||
for (const key of PATH_PROPERTIES)
|
||||
addJsonPath(paths, observation.properties[key]);
|
||||
for (const key of PATH_PROPERTIES) {
|
||||
const location = observation.evidence.location;
|
||||
const mapPath =
|
||||
node.identity.strategy === "source-map-original" &&
|
||||
observation.evidence.extractor.operation === "parse-local-source-map" &&
|
||||
(key === "source" || key === "original_source") &&
|
||||
location.available &&
|
||||
location.value.kind === "artifact-path"
|
||||
? location.value.path
|
||||
: undefined;
|
||||
addJsonPath(paths, observation.properties[key], mapPath);
|
||||
}
|
||||
}
|
||||
return {
|
||||
node,
|
||||
@@ -212,7 +242,10 @@ const candidateSignals = (
|
||||
signals.push(signal("current-path-exact", source.path, exactPaths));
|
||||
const suffixPaths = otherPaths
|
||||
.filter(
|
||||
({ value }) => value !== source.path && value.endsWith(`/${source.path}`),
|
||||
({ value, allowSuffix }) =>
|
||||
allowSuffix &&
|
||||
value !== source.path &&
|
||||
value.endsWith(`/${source.path}`),
|
||||
)
|
||||
.map(({ value }) => value);
|
||||
if (suffixPaths.length > 0)
|
||||
@@ -259,45 +292,70 @@ const matchingPaths = (
|
||||
): string[] =>
|
||||
currentPaths
|
||||
.filter(
|
||||
({ value }) => value === sourcePath || value.endsWith(`/${sourcePath}`),
|
||||
({ value, allowSuffix }) =>
|
||||
value === sourcePath ||
|
||||
(allowSuffix && value.endsWith(`/${sourcePath}`)),
|
||||
)
|
||||
.map(({ value }) => value);
|
||||
|
||||
const addJsonPath = (paths: CurrentPath[], value: unknown): void => {
|
||||
if (typeof value === "string") addPath(paths, "observation-path", value);
|
||||
const addJsonPath = (
|
||||
paths: CurrentPath[],
|
||||
value: unknown,
|
||||
mapPath?: string,
|
||||
): void => {
|
||||
if (typeof value === "string")
|
||||
addPath(paths, "observation-path", value, mapPath);
|
||||
};
|
||||
|
||||
const addPath = (
|
||||
paths: CurrentPath[],
|
||||
kind: CurrentPathKind,
|
||||
raw: string,
|
||||
mapPath?: string,
|
||||
): void => {
|
||||
if (kind === "source-map-original" || mapPath !== undefined) {
|
||||
const resolved = resolveJavaScriptSourceMapPath(raw, mapPath);
|
||||
if (resolved !== null)
|
||||
paths.push({
|
||||
kind,
|
||||
value: resolved.value,
|
||||
allowSuffix: resolved.scope === "suffix",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const value = normalizeCurrentPath(raw);
|
||||
if (value !== null) paths.push({ kind, value });
|
||||
if (value !== null) paths.push({ kind, value, allowSuffix: true });
|
||||
};
|
||||
|
||||
/** Matches a `scheme://...` URL prefix; bare filesystem paths keep `?`/`#`. */
|
||||
const SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\//iu;
|
||||
/** Matches a `scheme://...` URL prefix with any run of slashes, for stripping. */
|
||||
const SCHEME_URL_PREFIX = /^[a-z][a-z0-9+.-]*:\/\/+/iu;
|
||||
|
||||
const normalizeCurrentPath = (raw: string): string | null => {
|
||||
const withoutQuery = SCHEME_URL.test(raw)
|
||||
? (raw.split(/[?#]/u, 1)[0] ?? "")
|
||||
: raw;
|
||||
const withoutScheme = withoutQuery.replace(SCHEME_URL_PREFIX, "");
|
||||
const parts = withoutScheme
|
||||
.replaceAll("\\", "/")
|
||||
.split("/")
|
||||
.filter((part) => part !== "" && part !== ".");
|
||||
if (parts.length === 0 || parts.includes("..")) return null;
|
||||
const portable = withoutQuery
|
||||
.replace(SCHEME_URL_PREFIX, "")
|
||||
.replaceAll("\\", "/");
|
||||
const parts: string[] = [];
|
||||
for (const part of portable.split("/")) {
|
||||
if (part === "" || part === ".") continue;
|
||||
if (part === "..") {
|
||||
if (parts.length === 0) return null;
|
||||
parts.pop();
|
||||
} else parts.push(part);
|
||||
}
|
||||
if (parts.length === 0) return null;
|
||||
return parts.join("/");
|
||||
};
|
||||
|
||||
const uniquePaths = (paths: readonly CurrentPath[]): CurrentPath[] =>
|
||||
[
|
||||
...new Map(
|
||||
paths.map((path) => [`${path.kind}\0${path.value}`, path]),
|
||||
paths.map((path) => [
|
||||
`${path.kind}\0${path.value}\0${String(path.allowSuffix)}`,
|
||||
path,
|
||||
]),
|
||||
).values(),
|
||||
].sort((left, right) =>
|
||||
compareCodePoints(
|
||||
|
||||
@@ -6,6 +6,7 @@ import { analyzeJavaScriptApplication } from "../../../src/application/javascrip
|
||||
import { parseApplicationGraphEvidence } from "../../../src/application/javascript/JavaScriptApplicationEvidenceGraph.js";
|
||||
import { importReferenceSource } from "../../../src/application/ReferenceSourceImport.js";
|
||||
import { compareSourceToBundle } from "../../../src/domain/javascript/sourceToBundleComparison.js";
|
||||
import { resolveSourceMapSource } from "../../../src/javascript/sourceMaps/DecodedSourceMap.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it("keeps literal hash characters in historical/current filesystem paths", async () => {
|
||||
@@ -17,25 +18,9 @@ it("keeps literal hash characters in historical/current filesystem paths", async
|
||||
writeFile(join(previous, "worker#main.js"), "export const value = 1;"),
|
||||
writeFile(join(current, "worker#main.js"), "export const value = 2;"),
|
||||
]);
|
||||
const reference = await importReferenceSource({
|
||||
root: previous,
|
||||
caller: "source-to-bundle-path-test",
|
||||
policy: { secretPatterns: [] },
|
||||
});
|
||||
if (!reference.ok) throw new Error(reference.error.message);
|
||||
const analyzed = await analyzeJavaScriptApplication({ input_path: current });
|
||||
if (!analyzed.ok) throw analyzed.error;
|
||||
const application = parseApplicationGraphEvidence(analyzed.value);
|
||||
const comparison = compareSourceToBundle({
|
||||
reference: reference.value,
|
||||
application: {
|
||||
evidenceId: application.evidence.evidence_id,
|
||||
rootArtifactSha256: application.rootArtifactSha256,
|
||||
graph: application.graph,
|
||||
},
|
||||
});
|
||||
const { reference, comparison } = await compareTrees(previous, current);
|
||||
|
||||
expect(reference.value.entries).toContainEqual(
|
||||
expect(reference.entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "file",
|
||||
path: "worker#main.js",
|
||||
@@ -59,6 +44,507 @@ it("keeps literal hash characters in historical/current filesystem paths", async
|
||||
);
|
||||
});
|
||||
|
||||
it("matches source-map originals named relative to the map", async () => {
|
||||
const root = await createTestTempDirectory("rea-source-map-parent-");
|
||||
const previous = join(root, "previous");
|
||||
const current = join(root, "current");
|
||||
await Promise.all([
|
||||
mkdir(join(previous, "src"), { recursive: true }),
|
||||
mkdir(join(current, "dist"), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(join(previous, "src", "a.js"), "export const value = 1;\n"),
|
||||
writeFile(
|
||||
join(current, "dist", "main.js"),
|
||||
"var value = 2;\n//# sourceMappingURL=main.js.map\n",
|
||||
),
|
||||
writeFile(
|
||||
join(current, "dist", "main.js.map"),
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
file: "main.js",
|
||||
sources: ["../src/a.js"],
|
||||
sourcesContent: ["export const value = 2;\n"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
),
|
||||
]);
|
||||
const { comparison } = await compareTrees(previous, current);
|
||||
|
||||
expect(comparison.items).toContainEqual(
|
||||
expect.objectContaining({
|
||||
source_path: "src/a.js",
|
||||
candidates: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
current_node_kind: "source-module",
|
||||
signals: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "source-map-original-path",
|
||||
current_values: ["src/a.js"],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
mapPath: "pkg/dist/main.js.map",
|
||||
source: "..\\src\\a.js",
|
||||
expected: "pkg/src/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/dist/main.js.map",
|
||||
source: "../src/a.js",
|
||||
expected: "pkg/src/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "apps/pkg/dist/maps/main.js.map",
|
||||
source: "../../src/a.js",
|
||||
expected: "apps/pkg/src/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/dist/main.js.map",
|
||||
sourceRoot: "../src",
|
||||
source: "a.js",
|
||||
expected: "pkg/src/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/maps/main.map",
|
||||
sourceRoot: "../src",
|
||||
source: "/vendor/a.js",
|
||||
expected: "pkg/src/vendor/a.js",
|
||||
decoy: "vendor/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/maps/main.js.map",
|
||||
source: "modules/a.js",
|
||||
expected: "pkg/maps/modules/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/maps/main.js.map",
|
||||
source: "generated/../src/a.js",
|
||||
expected: "pkg/maps/src/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/maps/main.js.map",
|
||||
sourceRoot: "generated/..",
|
||||
source: "src/a.js",
|
||||
expected: "pkg/maps/src/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/maps/main.js.map",
|
||||
source: "../actual/%2e%2e/src/a.js",
|
||||
expected: "pkg/src/a.js",
|
||||
decoy: "pkg/actual/%2e%2e/src/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/maps/main.js.map",
|
||||
sourceRoot: "../actual/.%2E",
|
||||
source: "src/a.js",
|
||||
expected: "pkg/src/a.js",
|
||||
decoy: "pkg/actual/.%2E/src/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg#name?/maps/main.js.map",
|
||||
source: "../src/a.js",
|
||||
expected: "pkg#name?/src/a.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/maps/main.js.map",
|
||||
source: "modules/a%20b.js",
|
||||
expected: "pkg/maps/modules/a b.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/maps/main.js.map",
|
||||
source: "modules/a%23b.js",
|
||||
expected: "pkg/maps/modules/a#b.js",
|
||||
},
|
||||
{
|
||||
mapPath: "pkg/maps/main.js.map",
|
||||
source: "../actual/%252e%252e/src/a.js",
|
||||
expected: "pkg/actual/%2e%2e/src/a.js",
|
||||
decoy: "pkg/src/a.js",
|
||||
},
|
||||
])(
|
||||
"resolves $source against $mapPath without selecting a same-basename decoy",
|
||||
async ({
|
||||
mapPath,
|
||||
source,
|
||||
sourceRoot,
|
||||
expected,
|
||||
decoy: decoyPath = "src/a.js",
|
||||
}) => {
|
||||
if (sourceRoot !== undefined) {
|
||||
const resolved = resolveSourceMapSource(
|
||||
source,
|
||||
sourceRoot,
|
||||
`https://example.test/${mapPath}`,
|
||||
);
|
||||
expect(decodeURIComponent(new URL(resolved).pathname)).toBe(
|
||||
`/${expected}`,
|
||||
);
|
||||
}
|
||||
const root = await createTestTempDirectory("rea-nested-map-");
|
||||
const previous = join(root, "previous");
|
||||
const current = join(root, "current");
|
||||
await Promise.all([
|
||||
mkdir(join(previous, expected, ".."), { recursive: true }),
|
||||
mkdir(join(previous, decoyPath, ".."), { recursive: true }),
|
||||
mkdir(join(current, mapPath, ".."), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(join(previous, expected), "export const value = 1;"),
|
||||
writeFile(join(previous, decoyPath), "export const decoy = 99;"),
|
||||
writeFile(
|
||||
join(current, mapPath),
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: [source],
|
||||
...(sourceRoot === undefined ? {} : { sourceRoot }),
|
||||
sourcesContent: ["export const value = 2;"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
),
|
||||
]);
|
||||
const { comparison } = await compareTrees(previous, current);
|
||||
const correct = comparison.items.find(
|
||||
({ source_path }) => source_path === expected,
|
||||
);
|
||||
expect(correct?.current_node_ids).toHaveLength(1);
|
||||
expect(correct?.candidates).toContainEqual(
|
||||
expect.objectContaining({
|
||||
confidence: "high",
|
||||
signals: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "source-map-original-path",
|
||||
current_values: [expected],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
const decoy = comparison.items.find(
|
||||
({ source_path }) => source_path === decoyPath,
|
||||
);
|
||||
expect(decoy?.current_node_ids).toEqual([]);
|
||||
expect(
|
||||
decoy?.candidates
|
||||
.flatMap(({ signals }) => signals)
|
||||
.some(({ kind }) => kind === "current-path-exact"),
|
||||
).toBe(false);
|
||||
// A suffix alone must not claim a second map-relative original identity.
|
||||
expect(
|
||||
decoy?.candidates
|
||||
.flatMap(({ signals }) => signals)
|
||||
.some(({ kind }) => kind === "source-map-original-path"),
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains every map location when identical map artifacts merge", async () => {
|
||||
const root = await createTestTempDirectory("rea-source-map-merged-");
|
||||
const previous = join(root, "previous");
|
||||
const current = join(root, "current");
|
||||
const map = JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["../src/x.js"],
|
||||
sourcesContent: ["export const value = 2;"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
});
|
||||
for (const prefix of ["a", "b"]) {
|
||||
await Promise.all([
|
||||
mkdir(join(previous, prefix, "src"), { recursive: true }),
|
||||
mkdir(join(current, prefix, "maps"), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(join(previous, prefix, "src/x.js"), "export const value = 1;"),
|
||||
writeFile(join(current, prefix, "maps/main.js.map"), map),
|
||||
]);
|
||||
}
|
||||
const { comparison } = await compareTrees(previous, current);
|
||||
for (const prefix of ["a", "b"]) {
|
||||
const path = `${prefix}/src/x.js`;
|
||||
const item = comparison.items.find(
|
||||
({ source_path }) => source_path === path,
|
||||
);
|
||||
expect(item?.current_node_ids).toHaveLength(1);
|
||||
expect(item?.candidates).toContainEqual(
|
||||
expect.objectContaining({
|
||||
confidence: "high",
|
||||
signals: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "source-map-original-path",
|
||||
current_values: [path],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ source: "file:/repo/src/a.js", hasPath: true },
|
||||
{ source: "file:C:/repo/src/a.js", hasPath: true },
|
||||
{ source: "file:C:\\repo\\src\\a.js", hasPath: true },
|
||||
{ source: "C:\\repo\\src\\a.js", hasPath: true },
|
||||
{ source: "file://server/share/src/a.js", hasPath: true },
|
||||
{ source: "file:/repo/src/a.js?version=2#original", hasPath: true },
|
||||
{ source: "webpack:///src/a.js", hasPath: true },
|
||||
{ source: "https://example.com/src/a.js#original", hasPath: true },
|
||||
{ source: "node:internal/src/a.js", hasPath: false },
|
||||
{ source: "data:text/javascript,virtual/src/a.js", hasPath: false },
|
||||
{ source: "data:text/javascript,virtual/../src/a.js", hasPath: false },
|
||||
{ source: "data:\\src\\a.js", hasPath: false },
|
||||
])(
|
||||
"projects source references from $source only when they name a file",
|
||||
async ({ source, hasPath }) => {
|
||||
const root = await createTestTempDirectory("rea-source-map-uri-");
|
||||
const previous = join(root, "previous");
|
||||
const current = join(root, "current");
|
||||
await Promise.all([
|
||||
mkdir(join(previous, "src"), { recursive: true }),
|
||||
mkdir(join(current, "maps"), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(join(previous, "src/a.js"), "export const value = 1;"),
|
||||
writeFile(
|
||||
join(current, "maps/main.js.map"),
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: [source],
|
||||
sourcesContent: ["export const value = 2;"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
),
|
||||
]);
|
||||
const { comparison, application } = await compareTrees(previous, current);
|
||||
expect(application.graph.nodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
identity: expect.objectContaining({ original_source: source }),
|
||||
}),
|
||||
);
|
||||
if (hasPath)
|
||||
expect(comparison.items[0]?.candidates).toContainEqual(
|
||||
expect.objectContaining({
|
||||
confidence: "high",
|
||||
signals: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "source-map-original-path" }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
else expect(comparison.items[0]?.candidates).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["#original.js", "?version.js", "?version.js#original.js"])(
|
||||
"uses the relative URL pathname rather than a literal %s filename",
|
||||
async (suffix) => {
|
||||
const root = await createTestTempDirectory("rea-source-map-suffix-");
|
||||
const previous = join(root, "previous");
|
||||
const current = join(root, "current");
|
||||
await Promise.all([
|
||||
mkdir(join(previous, "src"), { recursive: true }),
|
||||
mkdir(join(current, "maps"), { recursive: true }),
|
||||
]);
|
||||
const source = `../src/a.js${suffix}`;
|
||||
await Promise.all([
|
||||
writeFile(join(previous, "src/a.js"), "export const value = 1;"),
|
||||
writeFile(join(previous, `src/a.js${suffix}`), "export const decoy = 3;"),
|
||||
writeFile(
|
||||
join(current, "maps/main.js.map"),
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: [source],
|
||||
sourcesContent: ["export const value = 2;"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
),
|
||||
]);
|
||||
const { comparison, application } = await compareTrees(previous, current);
|
||||
const actual = comparison.items.find(
|
||||
({ source_path }) => source_path === "src/a.js",
|
||||
);
|
||||
expect(actual?.current_node_ids).toHaveLength(1);
|
||||
expect(actual?.candidates).toContainEqual(
|
||||
expect.objectContaining({
|
||||
confidence: "high",
|
||||
signals: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "source-map-original-path",
|
||||
current_values: ["src/a.js"],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
const decoy = comparison.items.find(
|
||||
({ source_path }) => source_path === `src/a.js${suffix}`,
|
||||
);
|
||||
expect(decoy?.current_node_ids).toEqual([]);
|
||||
expect(decoy?.candidates).toEqual([]);
|
||||
expect(application.graph.nodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
identity: expect.objectContaining({ original_source: source }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ filename: "a#b.js", decoyFilename: "a" },
|
||||
{ filename: "a?b.js", decoyFilename: "a" },
|
||||
{ filename: "a%23b.js", decoyFilename: "a#b.js" },
|
||||
])(
|
||||
"preserves the literal UNC filesystem name $filename without selecting a URL decoy",
|
||||
async ({ filename, decoyFilename }) => {
|
||||
const root = await createTestTempDirectory("rea-source-map-unc-");
|
||||
const previous = join(root, "previous");
|
||||
const current = join(root, "current");
|
||||
const source = `\\\\server\\share\\src\\${filename}`;
|
||||
const expected = `src/${filename}`;
|
||||
const decoy = `src/${decoyFilename}`;
|
||||
await Promise.all([
|
||||
mkdir(join(previous, "src"), { recursive: true }),
|
||||
mkdir(join(current, "maps"), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(join(previous, expected), "export const value = 1;"),
|
||||
writeFile(join(previous, decoy), "export const decoy = 3;"),
|
||||
writeFile(
|
||||
join(current, "maps/main.map"),
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: [source],
|
||||
sourcesContent: ["export const value = 2;"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
),
|
||||
]);
|
||||
const { comparison, application } = await compareTrees(previous, current);
|
||||
const actual = comparison.items.find(
|
||||
({ source_path }) => source_path === expected,
|
||||
);
|
||||
expect(actual?.current_node_ids).toHaveLength(1);
|
||||
expect(actual?.candidates).toContainEqual(
|
||||
expect.objectContaining({
|
||||
signals: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "source-map-original-path" }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
comparison.items.find(({ source_path }) => source_path === decoy)
|
||||
?.current_node_ids,
|
||||
).toEqual([]);
|
||||
expect(application.graph.nodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
identity: expect.objectContaining({ original_source: source }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ source: "", historicalPath: "maps.js" },
|
||||
{ source: ".", historicalPath: "maps.js" },
|
||||
{ source: "./", historicalPath: "maps.js" },
|
||||
{ source: "child/..", historicalPath: "maps.js" },
|
||||
{ source: "child.js/", historicalPath: "maps.js/child.js" },
|
||||
{ source: "#original.js", historicalPath: "maps.js" },
|
||||
{ source: "?version.js", historicalPath: "maps.js" },
|
||||
{ source: "./?version.js", historicalPath: "maps.js" },
|
||||
{ source: "%2e", historicalPath: "maps.js" },
|
||||
{ source: "%2e%2e", historicalPath: "maps.js" },
|
||||
{ source: "child%2fpart.js", historicalPath: "maps.js/child/part.js" },
|
||||
{ source: "child%5cpart.js", historicalPath: "maps.js/child/part.js" },
|
||||
{ source: "bad%zz.js", historicalPath: "maps.js/bad%zz.js" },
|
||||
{ source: "\\\\bad host\\share\\a.js", historicalPath: "share/a.js" },
|
||||
{ source: "../../outside/a.js", historicalPath: "outside/a.js" },
|
||||
])(
|
||||
"declines source references without a trustworthy file path: $source",
|
||||
async ({ source, historicalPath }) => {
|
||||
const root = await createTestTempDirectory("rea-source-map-empty-");
|
||||
const previous = join(root, "previous");
|
||||
const current = join(root, "current");
|
||||
await Promise.all([
|
||||
mkdir(join(previous, historicalPath, ".."), { recursive: true }),
|
||||
mkdir(join(current, "maps.js"), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(join(previous, historicalPath), "export const value = 1;"),
|
||||
writeFile(
|
||||
join(current, "maps.js/main.js.map"),
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sources: [source],
|
||||
sourcesContent: ["export const value = 2;"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
),
|
||||
]);
|
||||
const { comparison } = await compareTrees(previous, current);
|
||||
expect(comparison.items[0]?.source_path).toBe(historicalPath);
|
||||
expect(comparison.items[0]?.candidates).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("resolves parent segments against a nested source map's directory", async () => {
|
||||
const root = await createTestTempDirectory("rea-source-map-nested-");
|
||||
const previous = join(root, "previous");
|
||||
const current = join(root, "current");
|
||||
await Promise.all([
|
||||
mkdir(join(previous, "web", "src"), { recursive: true }),
|
||||
mkdir(join(current, "web", "maps"), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(previous, "web", "src", "a.js"),
|
||||
"export const value = 1;\n",
|
||||
),
|
||||
writeFile(
|
||||
join(current, "web", "main.js"),
|
||||
"var value = 2;\n//# sourceMappingURL=maps/main.js.map\n",
|
||||
),
|
||||
writeFile(
|
||||
join(current, "web", "maps", "main.js.map"),
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
file: "../main.js",
|
||||
sources: ["../src/a.js"],
|
||||
sourcesContent: ["export const value = 2;\n"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
),
|
||||
]);
|
||||
const { comparison } = await compareTrees(previous, current);
|
||||
|
||||
expect(comparison.items).toContainEqual(
|
||||
expect.objectContaining({
|
||||
source_path: "web/src/a.js",
|
||||
candidates: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
current_node_kind: "source-module",
|
||||
signals: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "source-map-original-path",
|
||||
current_values: ["web/src/a.js"],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects static application Evidence whose subject path disagrees with its result", async () => {
|
||||
const root = await createTestTempDirectory("rea-application-subject-path-");
|
||||
const analyzed = await analyzeJavaScriptApplication({ input_path: root });
|
||||
@@ -77,3 +563,24 @@ it("rejects static application Evidence whose subject path disagrees with its re
|
||||
"JavaScript application Evidence subject does not match its result",
|
||||
);
|
||||
});
|
||||
|
||||
const compareTrees = async (previous: string, current: string) => {
|
||||
const reference = await importReferenceSource({
|
||||
root: previous,
|
||||
caller: "source-to-bundle-path-test",
|
||||
policy: { secretPatterns: [] },
|
||||
});
|
||||
if (!reference.ok) throw new Error(reference.error.message);
|
||||
const analyzed = await analyzeJavaScriptApplication({ input_path: current });
|
||||
if (!analyzed.ok) throw analyzed.error;
|
||||
const application = parseApplicationGraphEvidence(analyzed.value);
|
||||
const comparison = compareSourceToBundle({
|
||||
reference: reference.value,
|
||||
application: {
|
||||
evidenceId: application.evidence.evidence_id,
|
||||
rootArtifactSha256: application.rootArtifactSha256,
|
||||
graph: application.graph,
|
||||
},
|
||||
});
|
||||
return { reference: reference.value, comparison, application };
|
||||
};
|
||||
Reference in new issue
Block a user