242 lines
8.3 KiB
TypeScript
242 lines
8.3 KiB
TypeScript
import { expect } from "vitest";
|
|
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
|
|
|
const runtimeSource = (
|
|
runtime: string,
|
|
name: string,
|
|
entry = 1,
|
|
dependency = 2,
|
|
) => `
|
|
globalThis.${runtime}.push([["main"], {
|
|
${String(entry)}: (module, exports, req) => {
|
|
req(${String(dependency)}); req.e("lazy"); fetch("/${name}");
|
|
localStorage.setItem("${name}", "value");
|
|
const { BrowserWindow } = require("electron"); new BrowserWindow({});
|
|
},
|
|
${String(dependency)}: (module) => { module.exports = "${name}"; }
|
|
}, (req) => req(${String(entry)})]);
|
|
globalThis.${runtime}.push([["lazy"], {9: () => {}}]);
|
|
`;
|
|
|
|
const appA = runtimeSource("webpackChunkAppA", "a");
|
|
const appB = runtimeSource("webpackChunkAppB", "b");
|
|
|
|
cliTest(
|
|
"keeps source-derived runtime, module and chunk tuples collision-free",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-bundler-runtime-tuple-");
|
|
const registrations = [
|
|
{
|
|
runtime: "webpackChunkA",
|
|
module: "x\0y",
|
|
chunk: "x\0lazy",
|
|
endpoint: "/a",
|
|
},
|
|
{
|
|
runtime: "webpackChunkA\0x",
|
|
module: "y",
|
|
chunk: "lazy",
|
|
endpoint: "/b",
|
|
},
|
|
];
|
|
const source = registrations
|
|
.map(
|
|
({ runtime, module, chunk, endpoint }) => `
|
|
globalThis[${JSON.stringify(runtime)}].push([["main"], {
|
|
[${JSON.stringify(module)}]: (m, e, req) => { fetch(${JSON.stringify(endpoint)}); req.e(${JSON.stringify(chunk)}); }
|
|
}, (req) => req(${JSON.stringify(module)})]);
|
|
globalThis[${JSON.stringify(runtime)}].push([[${JSON.stringify(chunk)}], {}]);
|
|
`,
|
|
)
|
|
.join("\n");
|
|
await writeFixtureFiles(root, { "bundle.js": source });
|
|
const output = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--json"],
|
|
});
|
|
expect(output.exitCode).toBe(0);
|
|
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
|
|
parseEvidence(output.json).normalized_result,
|
|
);
|
|
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
|
for (const { runtime, module, chunk, endpoint } of registrations) {
|
|
const entry = graph.edges.find(
|
|
(edge) =>
|
|
edge.properties.kind === "bundler-entry-module" &&
|
|
edge.properties.runtime === runtime,
|
|
);
|
|
expect(nodes.get(entry?.target_node_id ?? "")?.identity).toMatchObject({
|
|
namespace: `${runtime}:module`,
|
|
key: module,
|
|
});
|
|
const call = graph.edges.find(
|
|
(edge) =>
|
|
edge.relation === "calls" &&
|
|
nodes.get(edge.target_node_id)?.observations[0]?.properties.value ===
|
|
endpoint,
|
|
);
|
|
expect(call?.source_node_id).toBe(entry?.target_node_id);
|
|
const lazy = graph.edges.find(
|
|
(edge) =>
|
|
edge.source_node_id === entry?.target_node_id &&
|
|
edge.properties.specifier === `chunk:${chunk}`,
|
|
);
|
|
expect(
|
|
nodes.get(lazy?.target_node_id ?? "")?.observations[0]?.properties,
|
|
).toMatchObject({ runtime, chunk_keys: [chunk] });
|
|
}
|
|
},
|
|
);
|
|
|
|
for (const scenario of [
|
|
{
|
|
name: "colliding module IDs",
|
|
source: appA + appB,
|
|
runtimes: ["webpackChunkAppA", "webpackChunkAppB"],
|
|
keys: [1, 1],
|
|
},
|
|
{
|
|
name: "reversed registrations",
|
|
source: appB + appA,
|
|
runtimes: ["webpackChunkAppA", "webpackChunkAppB"],
|
|
keys: [1, 1],
|
|
},
|
|
{
|
|
name: "single runtime",
|
|
source: appA,
|
|
runtimes: ["webpackChunkAppA"],
|
|
keys: [1],
|
|
},
|
|
{
|
|
name: "disjoint module IDs",
|
|
source: appA + runtimeSource("webpackChunkAppB", "b", 3, 4),
|
|
runtimes: ["webpackChunkAppA", "webpackChunkAppB"],
|
|
keys: [1, 3],
|
|
},
|
|
{
|
|
name: "Webpack and Rspack collision",
|
|
source: appA + runtimeSource("rspackChunkAppB", "b"),
|
|
runtimes: ["webpackChunkAppA", "rspackChunkAppB"],
|
|
keys: [1, 1],
|
|
},
|
|
]) {
|
|
cliTest(
|
|
`keeps bundled relationships within their runtime for ${scenario.name}`,
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-bundler-runtime-scope-");
|
|
await writeFixtureFiles(root, { "bundle.js": scenario.source });
|
|
const output = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--json"],
|
|
});
|
|
expect(output.exitCode).toBe(0);
|
|
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
|
|
parseEvidence(output.json).normalized_result,
|
|
);
|
|
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
|
expect(graph.coverage.status).toBe("complete");
|
|
for (const [index, runtime] of scenario.runtimes.entries()) {
|
|
const name = index === 0 ? "a" : "b";
|
|
const key = String(scenario.keys[index]);
|
|
const module = graph.nodes.find(
|
|
(node) =>
|
|
node.identity.strategy === "artifact-local-key" &&
|
|
node.identity.namespace === `${runtime}:module` &&
|
|
node.identity.key === key,
|
|
);
|
|
expect(module).toBeDefined();
|
|
const entry = graph.edges.find(
|
|
(edge) =>
|
|
edge.properties.kind === "bundler-entry-module" &&
|
|
edge.properties.runtime === runtime,
|
|
);
|
|
expect(entry?.target_node_id).toBe(module?.node_id);
|
|
expect(entry?.properties.resolution_status).toBe("resolved");
|
|
const ownedEdges = graph.edges.filter(
|
|
(edge) => edge.source_node_id === module?.node_id,
|
|
);
|
|
expect(
|
|
ownedEdges.find(
|
|
(edge) =>
|
|
edge.relation === "calls" &&
|
|
nodes.get(edge.target_node_id)?.observations[0]?.properties
|
|
.value === `/${name}`,
|
|
),
|
|
).toBeDefined();
|
|
expect(
|
|
ownedEdges.find(
|
|
(edge) =>
|
|
edge.relation === "persists_to" &&
|
|
nodes.get(edge.target_node_id)?.observations[0]?.properties
|
|
.name === name,
|
|
),
|
|
).toBeDefined();
|
|
expect(
|
|
ownedEdges.find(
|
|
(edge) =>
|
|
edge.relation === "contains" &&
|
|
nodes.get(edge.target_node_id)?.kind === "browser-window",
|
|
),
|
|
).toBeDefined();
|
|
const dependency = ownedEdges.find(
|
|
(edge) =>
|
|
edge.properties.kind === "require" &&
|
|
edge.properties.specifier === String(Number(key) + 1),
|
|
);
|
|
expect(
|
|
nodes.get(dependency?.target_node_id ?? "")?.identity,
|
|
).toMatchObject({
|
|
namespace: `${runtime}:module`,
|
|
key: String(Number(key) + 1),
|
|
});
|
|
const lazy = ownedEdges.find(
|
|
(edge) => edge.properties.specifier === "chunk:lazy",
|
|
);
|
|
expect(
|
|
nodes.get(lazy?.target_node_id ?? "")?.observations[0]?.properties,
|
|
).toMatchObject({ runtime, chunk_keys: ["lazy"] });
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
cliTest(
|
|
"shares module IDs across chunks only within the same runtime",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-bundler-shared-runtime-");
|
|
await writeFixtureFiles(root, {
|
|
"bundle.js": `
|
|
globalThis.webpackChunkApp.push([["main"], {1: (module, exports, req) => { req(2); }} , (req) => req(1)]);
|
|
globalThis.webpackChunkApp.push([["other"], {2: (module) => { module.exports = "dependency"; }}]);
|
|
`,
|
|
});
|
|
const output = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--json"],
|
|
});
|
|
expect(output.exitCode).toBe(0);
|
|
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
|
|
parseEvidence(output.json).normalized_result,
|
|
);
|
|
const module = (key: string) =>
|
|
graph.nodes.find(
|
|
(node) =>
|
|
node.identity.strategy === "artifact-local-key" &&
|
|
node.identity.namespace === "webpackChunkApp:module" &&
|
|
node.identity.key === key,
|
|
);
|
|
expect(
|
|
graph.edges.find(
|
|
(edge) =>
|
|
edge.properties.kind === "require" &&
|
|
edge.properties.specifier === "2",
|
|
),
|
|
).toMatchObject({
|
|
source_node_id: module("1")?.node_id,
|
|
target_node_id: module("2")?.node_id,
|
|
});
|
|
},
|
|
);
|