813 lines
27 KiB
TypeScript
813 lines
27 KiB
TypeScript
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js";
|
|
import { parseApplicationGraphEvidence } from "../../../src/application/javascript/JavaScriptApplicationEvidenceGraph.js";
|
|
import { findApplicationFeatureSeeds } from "../../../src/domain/javascript/javascriptFeatureSeed.js";
|
|
import { compareSourceToBundle } from "../../../src/domain/javascript/sourceToBundleComparison.js";
|
|
import {
|
|
resolveJavaScriptSourceMapReferences,
|
|
resolveJavaScriptSourceMapUrl,
|
|
} from "../../../src/domain/javascript/javascriptSourceMapPaths.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { importReferenceSource } from "../../support/referenceSourceResourceScope.js";
|
|
|
|
it("keeps literal hash characters in historical/current filesystem paths", async () => {
|
|
const root = await createTestTempDirectory("rea-source-path-");
|
|
const previous = join(root, "previous");
|
|
const current = join(root, "current");
|
|
await Promise.all([mkdir(previous), mkdir(current)]);
|
|
await Promise.all([
|
|
writeFile(join(previous, "worker#main.js"), "export const value = 1;"),
|
|
writeFile(join(current, "worker#main.js"), "export const value = 2;"),
|
|
]);
|
|
const { reference, comparison } = await compareTrees(previous, current);
|
|
|
|
expect(reference.entries).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "file",
|
|
path: "worker#main.js",
|
|
content_state: "hashed",
|
|
}),
|
|
);
|
|
expect(comparison.items).toContainEqual(
|
|
expect.objectContaining({
|
|
source_path: "worker#main.js",
|
|
candidates: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
signals: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "current-path-exact",
|
|
source_value: "worker#main.js",
|
|
}),
|
|
]),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
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 = resolveJavaScriptSourceMapUrl(
|
|
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, application } = await compareTrees(previous, current);
|
|
const originals = application.graph.nodes.filter(
|
|
({ kind }) => kind === "source-module",
|
|
);
|
|
expect(originals).toHaveLength(1);
|
|
expect(
|
|
originals[0]?.observations.map(
|
|
({ source_map_reference }) => source_map_reference,
|
|
),
|
|
).toEqual(
|
|
expect.arrayContaining(
|
|
["a", "b"].map((prefix) => ({
|
|
source_name: "../src/x.js",
|
|
source_root: null,
|
|
map_path: `${prefix}/maps/main.js.map`,
|
|
resolution: { kind: "artifact-relative", path: `${prefix}/src/x.js` },
|
|
})),
|
|
),
|
|
);
|
|
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("keeps leaf source roots distinct after indexed maps are serialized", async () => {
|
|
const root = await createTestTempDirectory("rea-source-map-leaf-roots-");
|
|
const previous = join(root, "previous");
|
|
const current = join(root, "current");
|
|
await mkdir(join(current, "maps"), { recursive: true });
|
|
for (const prefix of ["one", "two"]) {
|
|
await mkdir(join(previous, prefix), { recursive: true });
|
|
await writeFile(join(previous, prefix, "same.js"), "export const old = 1;");
|
|
}
|
|
await writeFile(join(previous, "same.js"), "export const decoy = 3;");
|
|
await writeFile(
|
|
join(current, "maps/main.map"),
|
|
JSON.stringify({
|
|
version: 3,
|
|
sections: ["one", "two"].map((prefix, line) => ({
|
|
offset: { line, column: 0 },
|
|
map: {
|
|
version: 3,
|
|
sources: ["same.js"],
|
|
sourceRoot: `../${prefix}`,
|
|
sourcesContent: ["export const updated = 2;"],
|
|
mappings: "AAAA",
|
|
names: [],
|
|
},
|
|
})),
|
|
}),
|
|
);
|
|
const { comparison, application } = await compareTrees(previous, current);
|
|
const originals = application.graph.nodes.filter(
|
|
({ kind }) => kind === "source-module",
|
|
);
|
|
expect(originals).toHaveLength(2);
|
|
for (const prefix of ["one", "two"]) {
|
|
expect(originals).toContainEqual(
|
|
expect.objectContaining({
|
|
identity: expect.objectContaining({
|
|
original_source: "same.js",
|
|
source_root: `../${prefix}`,
|
|
}),
|
|
observations: [
|
|
expect.objectContaining({
|
|
source_map_reference: {
|
|
source_name: "same.js",
|
|
source_root: `../${prefix}`,
|
|
map_path: "maps/main.map",
|
|
resolution: {
|
|
kind: "artifact-relative",
|
|
path: `${prefix}/same.js`,
|
|
},
|
|
},
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
expect(
|
|
comparison.items.find(
|
|
({ source_path }) => source_path === `${prefix}/same.js`,
|
|
),
|
|
).toMatchObject({
|
|
current_node_ids: [expect.any(String)],
|
|
candidates: expect.arrayContaining([
|
|
expect.objectContaining({ confidence: "high" }),
|
|
]),
|
|
});
|
|
}
|
|
expect(
|
|
comparison.items.find(({ source_path }) => source_path === "same.js")
|
|
?.current_node_ids,
|
|
).toEqual([]);
|
|
for (const prefix of ["one", "two"])
|
|
for (const value of [`${prefix}/same.js`, `../${prefix}`])
|
|
expect(
|
|
findApplicationFeatureSeeds(originals, {
|
|
kind: "module",
|
|
value,
|
|
match: "exact",
|
|
case_sensitive: true,
|
|
}),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
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, application } = await compareTrees(previous, current);
|
|
const original = application.graph.nodes.find(
|
|
({ kind }) => kind === "source-module",
|
|
);
|
|
expect(original?.observations[0]?.source_map_reference).toMatchObject({
|
|
source_name: source,
|
|
source_root: null,
|
|
map_path: "maps.js/main.js.map",
|
|
resolution: { kind: "unresolved", reason: expect.any(String) },
|
|
});
|
|
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 });
|
|
if (!analyzed.ok) throw analyzed.error;
|
|
const subject = analyzed.value.subject;
|
|
if (subject === null) throw new Error("Analysis Evidence subject missing");
|
|
const inconsistent = {
|
|
...analyzed.value,
|
|
subject: {
|
|
...subject,
|
|
local_path: join(root, "relocated"),
|
|
},
|
|
};
|
|
|
|
const parsed = parseApplicationGraphEvidence(inconsistent);
|
|
expect(parsed.ok).toBe(false);
|
|
if (!parsed.ok)
|
|
expect(parsed.error.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
path: ["subject"],
|
|
reason: "invalid_value",
|
|
message: expect.stringContaining("subject must match"),
|
|
}),
|
|
);
|
|
});
|
|
|
|
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 parsedApplication = parseApplicationGraphEvidence(
|
|
JSON.parse(JSON.stringify(analyzed.value)),
|
|
);
|
|
if (!parsedApplication.ok) throw new Error("Analysis Evidence must parse");
|
|
const application = parsedApplication.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 };
|
|
};
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"does not promote a POSIX literal backslash name to a different historical slash path",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-backslash-source-match-");
|
|
const previous = join(root, "previous");
|
|
const current = join(root, "current");
|
|
await mkdir(join(previous, "literal"), { recursive: true });
|
|
await mkdir(current);
|
|
await writeFile(
|
|
join(previous, "literal", "name.js"),
|
|
"export const value = 1;\n",
|
|
);
|
|
await writeFile(
|
|
join(current, "literal\\name.js"),
|
|
"export const value = 2;\n",
|
|
);
|
|
const { comparison } = await compareTrees(previous, current);
|
|
const item = comparison.items.find(
|
|
({ source_path }) => source_path === "literal/name.js",
|
|
);
|
|
expect(item?.current_node_ids).toEqual([]);
|
|
expect(
|
|
item?.candidates
|
|
.flatMap(({ signals }) => signals)
|
|
.some(({ kind }) => kind === "current-path-exact"),
|
|
).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32").each([
|
|
{ mapPath: "literal\\bundle.js.map", expected: "a.js" },
|
|
{ mapPath: "literal\\dir/bundle.js.map", expected: "literal\\dir/a.js" },
|
|
])(
|
|
"resolves source-map declarations from literal artifact base $mapPath",
|
|
async ({ mapPath, expected }) => {
|
|
const root = await createTestTempDirectory("rea-backslash-source-map-");
|
|
const previous = join(root, "previous");
|
|
const current = join(root, "current");
|
|
await mkdir(previous);
|
|
await mkdir(current);
|
|
await writeFile(join(previous, "a.js"), "export const value = 1;\n");
|
|
await mkdir(join(current, "literal\\dir"), { recursive: true });
|
|
await writeFile(join(current, mapPath.slice(0, -4)), "var value = 2;\n");
|
|
await writeFile(
|
|
join(current, mapPath),
|
|
JSON.stringify({
|
|
version: 3,
|
|
file: mapPath.slice(0, -4),
|
|
sources: ["a.js"],
|
|
sourcesContent: ["export const value = 2;\n"],
|
|
names: [],
|
|
mappings: "AAAA",
|
|
}),
|
|
);
|
|
const { application, comparison } = await compareTrees(previous, current);
|
|
const references = application.graph.nodes
|
|
.flatMap(({ observations }) =>
|
|
observations.map(({ source_map_reference }) => source_map_reference),
|
|
)
|
|
.filter((reference) => reference?.map_path === mapPath);
|
|
expect(references).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
resolution: { kind: "artifact-relative", path: expected },
|
|
}),
|
|
]),
|
|
);
|
|
const item = comparison.items.find(
|
|
({ source_path }) => source_path === "a.js",
|
|
);
|
|
const signals = item?.candidates.flatMap(({ signals }) => signals) ?? [];
|
|
if (expected === "a.js")
|
|
expect(signals).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "source-map-original-path",
|
|
current_values: ["a.js"],
|
|
}),
|
|
]),
|
|
);
|
|
else
|
|
expect(
|
|
signals.some(({ kind }) => kind === "source-map-original-path"),
|
|
).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ source: "https://user%2Fname@host.test/a.js", expected: "host.test/a.js" },
|
|
{
|
|
source: "C:\\code\\literal%5Cname.js",
|
|
expected: "C:/code/literal%5Cname.js",
|
|
},
|
|
])(
|
|
"checks encoded source-map separators in the parsed pathname of $source",
|
|
({ source, expected }) => {
|
|
expect(
|
|
resolveJavaScriptSourceMapReferences([source], null, "ordinary.js.map")[0]
|
|
?.resolution,
|
|
).toEqual({ kind: "suffix", path: expected });
|
|
},
|
|
);
|