Preserve missing ASAR companion occurrence identity, declared size and unknown content in graph nodes and resolution limitations. Apply URL text semantics once for ESM lookup while preserving literal CommonJS paths and original specifiers. Keep CJS wrapper var bindings and assignment history, recognizing only untouched wrapper redeclarations. Validation: real ASAR producer workflow and CLI/MCP path/coverage cases; semantic reassignment controls. Context7 Node documentation and direct Node wrapper execution confirm wrapper behavior. Fixes #3213 Fixes #3214 Fixes #3223 Related: #3278, #3254 Co-authored-by: Geronimo <varungore1@gmail.com> Co-authored-by: Paolo Valletta <paovalletta@hotmail.it>
296 lines
10 KiB
TypeScript
296 lines
10 KiB
TypeScript
import { rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
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 { createStrippedAsarAddon } from "../../fixtures/strippedAsarAddon.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
import {
|
|
findExportNode,
|
|
findGraphEdge,
|
|
findSourceModule,
|
|
writeFixtureFiles,
|
|
} from "../../support/javascriptApplicationFixture.js";
|
|
|
|
cliTest(
|
|
"resolves ESM specifiers containing whitespace using URL text semantics",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-esm-whitespace-path-");
|
|
const rawSpecifier = "./feature\tname.js";
|
|
const encodedSpecifier = "./feature%09name.js";
|
|
await writeFixtureFiles(root, {
|
|
"main.mjs": `import ${JSON.stringify(rawSpecifier)};\nimport ${JSON.stringify(encodedSpecifier)};\n`,
|
|
"featurename.js": "export const plain = 1;\n",
|
|
"feature\tname.js": "export const value = 1;\n",
|
|
});
|
|
const output = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--json"],
|
|
});
|
|
expect(output.exitCode, output.stderr).toBe(0);
|
|
const result = javascriptApplicationAnalysisResultSchema.parse(
|
|
parseEvidence(output.json).normalized_result,
|
|
);
|
|
expect(
|
|
findGraphEdge(
|
|
result.graph,
|
|
findSourceModule(result.graph, "main.mjs"),
|
|
findSourceModule(result.graph, "featurename.js"),
|
|
"imports",
|
|
)?.properties,
|
|
).toMatchObject({
|
|
specifier: rawSpecifier,
|
|
resolved_path: "featurename.js",
|
|
});
|
|
expect(
|
|
findGraphEdge(
|
|
result.graph,
|
|
findSourceModule(result.graph, "main.mjs"),
|
|
findSourceModule(result.graph, "feature\tname.js"),
|
|
"imports",
|
|
)?.properties,
|
|
).toMatchObject({
|
|
specifier: encodedSpecifier,
|
|
resolved_path: "feature\tname.js",
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"retains an inventoried native addon when its ASAR companion is missing",
|
|
async ({ cli }) => {
|
|
const { archive, addon } = await createStrippedAsarAddon();
|
|
await rm(join(`${archive}.unpacked`, addon));
|
|
const output = await cli.run({
|
|
arguments: ["analyze-javascript-application", archive, "--json"],
|
|
});
|
|
expect(output.exitCode, output.stderr).toBe(0);
|
|
const result = javascriptApplicationAnalysisResultSchema.parse(
|
|
parseEvidence(output.json).normalized_result,
|
|
);
|
|
expect(result.graph.coverage.status).toBe("partial");
|
|
expect(
|
|
result.graph.nodes.some(
|
|
(node) =>
|
|
node.kind === "native-addon" &&
|
|
node.observations.some(
|
|
({ label, properties }) =>
|
|
label === addon && properties.availability === "unavailable",
|
|
),
|
|
),
|
|
).toBe(true);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"resolves emitted module imports without substituting Electron runtime paths",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-typescript-runtime-paths-");
|
|
await writeFixtureFiles(root, {
|
|
"package.json": JSON.stringify({ main: "main.ts" }),
|
|
"main.ts": `import { BrowserWindow, utilityProcess } from "electron";
|
|
import "./worker.js";
|
|
import "./config.mjs";
|
|
import "./legacy.cjs";
|
|
import "./view.js";
|
|
import "./runtime.js";
|
|
const win = new BrowserWindow({webPreferences:{preload:"./worker.js"}});
|
|
win.loadURL("./renderer.js");
|
|
utilityProcess.fork("./worker.js");
|
|
utilityProcess.fork("./runtime.js");
|
|
throw new Error("source must stay inert");`,
|
|
"worker.ts": "export const worker = 1;",
|
|
"config.mts": "export const config = 1;",
|
|
"legacy.cts": "export const legacy = 1;",
|
|
"view.tsx": "export const view = <div />;",
|
|
"renderer.ts": "export const renderer = 1;",
|
|
"runtime.ts": "export const source = 1;",
|
|
"runtime.js": "throw new Error('runtime must stay inert');",
|
|
});
|
|
const output = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--json"],
|
|
});
|
|
expect(output.exitCode, output.stderr).toBe(0);
|
|
const result = javascriptApplicationAnalysisResultSchema.parse(
|
|
parseEvidence(output.json).normalized_result,
|
|
);
|
|
const { graph } = result;
|
|
const source = findSourceModule(graph, "main.ts");
|
|
for (const [specifier, target] of [
|
|
["./worker.js", "worker.ts"],
|
|
["./config.mjs", "config.mts"],
|
|
["./legacy.cjs", "legacy.cts"],
|
|
["./view.js", "view.tsx"],
|
|
["./runtime.js", "runtime.js"],
|
|
] as const)
|
|
expect(
|
|
findGraphEdge(graph, source, findSourceModule(graph, target), "imports")
|
|
?.properties,
|
|
).toMatchObject({ specifier, resolved_path: target });
|
|
expect(result.summary).toMatchObject({
|
|
utility_processes: 2,
|
|
resolved_utility_entrypoints: 1,
|
|
});
|
|
expect(
|
|
graph.edges.find(
|
|
(edge) =>
|
|
edge.evidence.extractor.operation ===
|
|
"associate-browser-window-preload",
|
|
)?.properties,
|
|
).toMatchObject({
|
|
declared_path: "./worker.js",
|
|
resolved_path: null,
|
|
resolution_status: "not-found",
|
|
});
|
|
expect(
|
|
graph.edges
|
|
.filter(
|
|
(edge) =>
|
|
edge.properties.declared_path === "./worker.js" ||
|
|
edge.properties.declared_path === "./renderer.js",
|
|
)
|
|
.some((edge) => edge.properties.resolved_path != null),
|
|
).toBe(false);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"returns literal self-import evidence while preserving sibling imports and self-re-exports",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-self-import-");
|
|
await writeFixtureFiles(root, {
|
|
"a.js":
|
|
'import "./a.js";\nexport const value = 1;\nexport { value as forwarded } from "./a.js";\n',
|
|
"b.js": 'import { value } from "./a.js";\nconsole.log(value);\n',
|
|
});
|
|
const output = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--json"],
|
|
});
|
|
expect(output.exitCode).toBe(0);
|
|
const result = javascriptApplicationAnalysisResultSchema.parse(
|
|
parseEvidence(output.json).normalized_result,
|
|
);
|
|
const { graph } = result;
|
|
expect(
|
|
graph.edges.filter((edge) => edge.source_node_id === edge.target_node_id),
|
|
).toEqual([]);
|
|
expect(graph.limitations).toContainEqual(
|
|
expect.stringMatching(/^1 import specifier .*was omitted;/u),
|
|
);
|
|
expect(result.limitations).toEqual(
|
|
expect.arrayContaining(graph.limitations),
|
|
);
|
|
const source = findSourceModule(graph, "a.js");
|
|
expect(source).toBeDefined();
|
|
expect(
|
|
findGraphEdge(graph, findSourceModule(graph, "b.js"), source, "imports")
|
|
?.properties,
|
|
).toMatchObject({
|
|
specifier: "./a.js",
|
|
resolution_status: "resolved",
|
|
});
|
|
expect(
|
|
findGraphEdge(
|
|
graph,
|
|
findExportNode(graph, "a.js", "forwarded"),
|
|
source,
|
|
"imports",
|
|
)?.properties,
|
|
).toMatchObject({
|
|
specifier: "./a.js",
|
|
resolution_status: "resolved",
|
|
});
|
|
expect(
|
|
graph.edges.find(
|
|
(edge) =>
|
|
edge.source_node_id === source?.node_id &&
|
|
edge.properties.kind === "static-import" &&
|
|
edge.properties.specifier === "./a.js" &&
|
|
edge.evidence.location.available &&
|
|
edge.evidence.location.value.kind === "source-range" &&
|
|
edge.evidence.location.value.start.line === 1,
|
|
)?.evidence.location,
|
|
).toMatchObject({
|
|
available: true,
|
|
value: { kind: "source-range", source: "a.js", start: { line: 1 } },
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"discloses CommonJS and TypeScript self-imports including emitted-name rewrites",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-self-require-");
|
|
await writeFixtureFiles(root, {
|
|
"c.cjs": 'const self = require("./c.cjs");\nmodule.exports = self;\n',
|
|
"util.ts":
|
|
'import "./util.ts";\nimport "./util.js";\nexport const value = 1;\n',
|
|
});
|
|
const output = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--json"],
|
|
});
|
|
expect(output.exitCode).toBe(0);
|
|
const result = javascriptApplicationAnalysisResultSchema.parse(
|
|
parseEvidence(output.json).normalized_result,
|
|
);
|
|
const { graph } = result;
|
|
expect(
|
|
graph.edges.filter((edge) => edge.source_node_id === edge.target_node_id),
|
|
).toEqual([]);
|
|
// ./util.ts is a literal self-import and ./util.js resolves under NodeNext
|
|
// emitted-name semantics back to util.ts itself, as ./c.cjs does for CJS.
|
|
expect(graph.limitations).toContainEqual(
|
|
expect.stringMatching(/^3 import specifiers .*were omitted;/u),
|
|
);
|
|
expect(result.limitations).toEqual(
|
|
expect.arrayContaining(graph.limitations),
|
|
);
|
|
for (const [path, specifier] of [
|
|
["c.cjs", "./c.cjs"],
|
|
["util.ts", "./util.ts"],
|
|
] as const) {
|
|
const source = findSourceModule(graph, path);
|
|
expect(source).toBeDefined();
|
|
expect(
|
|
graph.edges.find(
|
|
(edge) =>
|
|
edge.source_node_id === source?.node_id &&
|
|
edge.properties.specifier === specifier &&
|
|
edge.properties.resolved_path === path,
|
|
)?.evidence.location,
|
|
).toMatchObject({
|
|
available: true,
|
|
value: { kind: "source-range", source: path, start: { line: 1 } },
|
|
});
|
|
expect(
|
|
graph.nodes.some((node) =>
|
|
node.observations.some(
|
|
({ properties }) =>
|
|
properties.semantic_role === "module-reference" &&
|
|
properties.declared_specifier === specifier,
|
|
),
|
|
),
|
|
).toBe(false);
|
|
}
|
|
expect(
|
|
graph.edges.some(
|
|
(edge) =>
|
|
edge.properties.specifier === "./util.js" &&
|
|
(edge.properties.module_link_kind === "import" ||
|
|
edge.properties.resolution_status === "not-found"),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
graph.nodes.some((node) =>
|
|
node.observations.some(
|
|
({ properties }) =>
|
|
properties.semantic_role === "module-reference" &&
|
|
properties.declared_specifier === "./util.js",
|
|
),
|
|
),
|
|
).toBe(false);
|
|
},
|
|
);
|