588 lines
17 KiB
TypeScript
588 lines
17 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
resolveArtifactPathByContext,
|
|
type ResolveArtifactPathInput,
|
|
} from "./JavaScriptArtifactPathResolution.js";
|
|
import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js";
|
|
import { artifactLocalIdentity } from "./JavaScriptArtifactGraphContext.js";
|
|
import { applicationNodeIdentitySchema } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
|
import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
|
|
|
|
describe("artifact-local graph identity", () => {
|
|
it("preserves complete long namespace and key values", () => {
|
|
const digest = "a".repeat(64);
|
|
const namespacePrefix = "n".repeat(512);
|
|
const keyPrefix = "k".repeat(4_096);
|
|
const first = artifactLocalIdentity(
|
|
digest,
|
|
`${namespacePrefix}a`,
|
|
`${keyPrefix}a`,
|
|
);
|
|
const second = artifactLocalIdentity(
|
|
digest,
|
|
`${namespacePrefix}b`,
|
|
`${keyPrefix}b`,
|
|
);
|
|
|
|
expect(applicationNodeIdentitySchema.parse(first)).toEqual(first);
|
|
expect(applicationNodeIdentitySchema.parse(second)).toEqual(second);
|
|
expect(first).not.toEqual(second);
|
|
});
|
|
});
|
|
|
|
describe("contextual JavaScript artifact path resolution", () => {
|
|
it.each([
|
|
["main.js", "app/main.js"],
|
|
["./main.js", "app/main.js"],
|
|
["src/main", "app/src/main.cjs"],
|
|
["entry", "app/entry/start.mjs"],
|
|
["fallback", "app/fallback/index.ts"],
|
|
])("resolves package entrypoint %s", (declaredPath, expected) => {
|
|
const files = fileMap([
|
|
file("app/package.json", "root"),
|
|
file("app/main.js", "root"),
|
|
file("app/src/main.cjs", "root"),
|
|
file("app/entry/package.json", "root", '{"main":"start"}'),
|
|
file("app/entry/start.mjs", "root"),
|
|
file("app/fallback/index.ts", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "app/package.json",
|
|
context: "package-entrypoint",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_context: "package-entrypoint",
|
|
resolution_status: "resolved",
|
|
resolved_path: expected,
|
|
limitations: [],
|
|
});
|
|
});
|
|
|
|
it("resolves dirname-derived bare paths but keeps bare module specifiers external", () => {
|
|
const files = fileMap([
|
|
file("app/main.js", "root"),
|
|
file("app/preload.js", "root"),
|
|
]);
|
|
const base = {
|
|
declaredPath: "preload.js",
|
|
sourcePath: "app/main.js",
|
|
files,
|
|
} as const;
|
|
|
|
expect(
|
|
resolve({ ...base, context: "filesystem-expression" }),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/preload.js",
|
|
});
|
|
expect(resolve({ ...base, context: "module-specifier" })).toMatchObject({
|
|
resolution_status: "external",
|
|
resolved_path: null,
|
|
});
|
|
});
|
|
|
|
it("applies document and base-href semantics to HTML references", () => {
|
|
const files = fileMap([
|
|
file("renderer/index.html", "root"),
|
|
file("assets/app.js", "root"),
|
|
file("renderer/local.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "app.js?v=1",
|
|
sourcePath: "renderer/index.html",
|
|
context: "html-reference",
|
|
htmlBaseHref: "/assets/",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "assets/app.js",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./local.js",
|
|
sourcePath: "renderer/index.html",
|
|
context: "html-reference",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "renderer/local.js",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "file:///assets/app.js#entry",
|
|
sourcePath: "main.js",
|
|
context: "module-specifier",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "assets/app.js",
|
|
});
|
|
});
|
|
|
|
it.each(["node:fs", "electron", "../../escape.js", "%2e%2e/escape.js"])(
|
|
"fails closed for unsafe or external module declaration %s",
|
|
(declaredPath) => {
|
|
const result = resolve({
|
|
declaredPath,
|
|
sourcePath: "app/main.js",
|
|
context: "module-specifier",
|
|
files: fileMap([file("app/main.js", "root")]),
|
|
});
|
|
expect(result.resolved_path).toBeNull();
|
|
expect(["external", "rejected"]).toContain(result.resolution_status);
|
|
expect(result.limitations.length).toBeGreaterThan(0);
|
|
},
|
|
);
|
|
|
|
it("does not cross nested ASAR container identities", () => {
|
|
const files = fileMap([
|
|
file("app.asar/main.js", "asar-a"),
|
|
file("app.asar/preload.js", "asar-b"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "preload.js",
|
|
sourcePath: "app.asar/main.js",
|
|
context: "filesystem-expression",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
});
|
|
|
|
it("resolves inventoried paths longer than the former character ceiling", () => {
|
|
const declaredPath = `./${"a".repeat(5_000)}.js`;
|
|
const expectedPath = `app/${declaredPath.slice(2)}`;
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file(expectedPath, "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: expectedPath,
|
|
limitations: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("contextual JavaScript package path resolution", () => {
|
|
it("follows package entrypoints beyond the former recursion ceiling", () => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/pkg-1/package.json",
|
|
"root",
|
|
'{"main":"../pkg-2"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-2/package.json",
|
|
"root",
|
|
'{"main":"../pkg-3"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-3/package.json",
|
|
"root",
|
|
'{"main":"../pkg-4"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-4/package.json",
|
|
"root",
|
|
'{"main":"../pkg-5"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-5/package.json",
|
|
"root",
|
|
'{"main":"../pkg-6"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-6/package.json",
|
|
"root",
|
|
'{"main":"entry.js"}',
|
|
),
|
|
file("app/node_modules/pkg-6/entry.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "pkg-1",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/pkg-6/entry.js",
|
|
limitations: [],
|
|
});
|
|
});
|
|
|
|
it("keeps an inventoried but unreadable directory package unavailable", () => {
|
|
const files = fileMap([
|
|
file("app/package.json", "root"),
|
|
file("app/entry/package.json", "root", null),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "entry",
|
|
sourcePath: "app/package.json",
|
|
context: "package-entrypoint",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("text is unavailable")],
|
|
});
|
|
});
|
|
|
|
it("resolves nearest inventoried bare packages with import/require conditions", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({
|
|
exports: {
|
|
".": {
|
|
import: "./esm.mjs",
|
|
require: "./cjs.cjs",
|
|
default: "./fallback.js",
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
file("app/node_modules/fixture/esm.mjs", "root"),
|
|
file("app/node_modules/fixture/cjs.cjs", "root"),
|
|
file("app/node_modules/fixture/fallback.js", "root"),
|
|
file("app/node_modules/fixture/hidden.js", "root"),
|
|
file("app/node_modules/fs/index.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/esm.mjs",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/cjs.cjs",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fs",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "external", resolved_path: null });
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/hidden.js",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "external",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("subpaths remain unresolved")],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("directory package entrypoint precedence", () => {
|
|
it.each([
|
|
["require", '{"main":"actual.cjs"}', "actual.cjs"],
|
|
["require", '{"exports":{"require":"./actual.cjs"}}', "actual.cjs"],
|
|
["import", '{"exports":{"import":"./actual.mjs"}}', "actual.mjs"],
|
|
] as const)(
|
|
"prefers declared %s entrypoints over index files",
|
|
(moduleKind, metadata, entry) => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", metadata),
|
|
file(`app/node_modules/fixture/${entry}`, "root"),
|
|
file("app/node_modules/fixture/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: `app/node_modules/fixture/${entry}`,
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["{}", '{"main":"."}'])(
|
|
"preserves index fallback for package metadata %s",
|
|
(metadata) => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/entry/package.json", "root", metadata),
|
|
file("app/entry/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./entry",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/entry/index.js",
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([null, "{"])(
|
|
"keeps unreadable or invalid metadata explicit even when an index exists",
|
|
(metadata) => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/entry/package.json", "root", metadata),
|
|
file("app/entry/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./entry",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("does not replace a missing exports target with an index", () => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
'{"exports":"./missing.js"}',
|
|
),
|
|
file("app/node_modules/fixture/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "not-found",
|
|
resolved_path: null,
|
|
});
|
|
});
|
|
|
|
it("keeps direct files ahead of a relative directory package", () => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/entry.js", "root"),
|
|
file("app/entry/package.json", "root", '{"main":"actual.js"}'),
|
|
file("app/entry/actual.js", "root"),
|
|
file("app/entry/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./entry",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/entry.js",
|
|
});
|
|
});
|
|
|
|
it("retains cycle diagnostics when directory packages refer to each other", () => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/a/package.json", "root", '{"main":"../b"}'),
|
|
file("app/b/package.json", "root", '{"main":"../a"}'),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./a",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("cycle")],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("contextual JavaScript module identity", () => {
|
|
it("retains CommonJS and ESM file-base identity during inert extraction", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(
|
|
`
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.resolve("ignored", __dirname, "preload.cjs")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: fileURLToPath(new URL("./preload.mjs", import.meta.url))
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.join(path.dirname(__filename), "legacy-preload.js")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.posix.resolve("/ignored", dirname(fileURLToPath(import.meta.url)), "esm-preload.js")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.posix.resolve(__dirname, "/shared", "absolute-preload.js/")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.posix.resolve(\`/templates\`, "template-preload.js")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: new URL("./remote-base.js", location.href)
|
|
}});
|
|
window.loadURL("file:///renderer/index.html");
|
|
`,
|
|
);
|
|
|
|
expect(analysis.role_paths).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: "preload.cjs",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "./preload.mjs",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "legacy-preload.js",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "esm-preload.js",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "/shared/absolute-preload.js",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "/templates/template-preload.js",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "file:///renderer/index.html",
|
|
resolution_context: "module-specifier",
|
|
}),
|
|
]),
|
|
);
|
|
expect(analysis.electron.browser_windows).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
preload_path: "preload.cjs",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
preload_path: "./preload.mjs",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
preload_path: "esm-preload.js",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
preload_path: "/shared/absolute-preload.js",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
preload_path: "/templates/template-preload.js",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
]),
|
|
);
|
|
expect(analysis.role_paths).not.toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ path: "./remote-base.js" }),
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
const resolve = (input: ResolveArtifactPathInput) =>
|
|
resolveArtifactPathByContext(input);
|
|
|
|
const fileMap = (files: readonly JavaScriptArtifactFile[]) =>
|
|
new Map(files.map((value) => [value.path, value]));
|
|
|
|
const file = (
|
|
path: string,
|
|
container: string,
|
|
text: string | null = "",
|
|
): JavaScriptArtifactFile => ({
|
|
path,
|
|
container_sha256: container.padEnd(64, "0").slice(0, 64),
|
|
sha256: path.padEnd(64, "0").slice(0, 64),
|
|
bytes: text?.length ?? 0,
|
|
inventory_artifact_id: `artifact-${path}`,
|
|
kind: path.endsWith("package.json") ? "package-json" : "javascript",
|
|
unpacked: false,
|
|
text:
|
|
text === null
|
|
? { included: false, reason: "invalid-utf8" }
|
|
: { included: true, value: text },
|
|
});
|