test: extract shared fixtures, drop knip carve-outs (#627)
* refactor(domain): consolidate canonical ordering and digest helpers (#624) * refactor(domain): consolidate canonical ordering and digest helpers Single owner for code-point ordering (domain/canonicalOrdering.ts) and canonical JSON digests (domain/comparisonSemantics.ts). Delete all local compareCodePoints/compareText/digestCanonical duplicates and migrate every importer. No re-export shims. * fix(ci): allow slower real browser startup * chore(config): remove dead eslint, dedupe scripts, cacheable turbo tasks Delete eslint.config.js (oxlint is the linter), drop lint:dead alias, pin smol-toml, fold jscpd/scan:todos into check:pr, make test:run cacheable, align oxlint ignores. * fix(config): hash all test inputs for cached runs * docs: fix architecture diagram and pretty-print tool catalog Drop dangling REPLAY edges, add dotnet/reference/error/evaluation nodes. Emit 2-space JSON from the catalog generator and regenerate. * test: extract shared fixtures, drop knip carve-outs Dedupe android/apple session tests, bundler adapter headers, and reconstruction helpers into tests/support fixtures. Delete unused type aliases and the knip ignoreIssues block. Lower jscpd threshold 2.7 to 2.5.
This commit is contained in:
parent
5e7f35c7c3
commit
55e5eb090b
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.6% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
|
||||
"threshold": 2.7,
|
||||
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.4% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
|
||||
"threshold": 2.5,
|
||||
"reporters": ["consoleFull"],
|
||||
"ignore": [
|
||||
"node_modules/**",
|
||||
|
|
|
|||
13
knip.json
13
knip.json
|
|
@ -1,21 +1,10 @@
|
|||
{
|
||||
"$schema": "./node_modules/knip/schema.json",
|
||||
"entry": [
|
||||
"src/main.ts",
|
||||
"src/cli.ts",
|
||||
"src/contracts/errorSchemas.ts",
|
||||
"scripts/run-development-tests.mjs"
|
||||
],
|
||||
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
"ignoreDependencies": ["electron"],
|
||||
"ignoreBinaries": ["ps"],
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"ignoreIssues": {
|
||||
"src/domain/mobileApplicationInvestigation.ts": ["types"],
|
||||
"src/domain/objcSwiftMetadata.ts": ["types"],
|
||||
"src/domain/interfaceBuilderGraph.ts": ["types"],
|
||||
"src/domain/nativeInvestigationGraph.ts": ["types"]
|
||||
},
|
||||
"rules": {
|
||||
"files": "error",
|
||||
"dependencies": "error",
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ export const iosManifestSchema = z.strictObject({
|
|||
ui_required_device_capabilities: z.array(z.string()).default([]),
|
||||
app_permissions: z.array(z.string()).default([]),
|
||||
});
|
||||
export type IosManifest = z.infer<typeof iosManifestSchema>;
|
||||
/** Signing information. */
|
||||
export const signingInfoSchema = z.strictObject({
|
||||
is_signed: z.boolean().default(false),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export const objcPropertyAttributeSchema = z.strictObject({
|
|||
is_copy: z.boolean().default(false),
|
||||
is_strong: z.boolean().default(false),
|
||||
});
|
||||
export type ObjcPropertyAttribute = z.infer<typeof objcPropertyAttributeSchema>;
|
||||
|
||||
/** Objective-C method metadata. */
|
||||
export const objcMethodSchema = z.strictObject({
|
||||
|
|
@ -39,7 +38,6 @@ export const objcPropertySchema = z.strictObject({
|
|||
getter: z.string().nullable(),
|
||||
setter: z.string().nullable(),
|
||||
});
|
||||
export type ObjcProperty = z.infer<typeof objcPropertySchema>;
|
||||
|
||||
/** Objective-C protocol metadata. */
|
||||
export const objcProtocolSchema = z.strictObject({
|
||||
|
|
@ -48,7 +46,6 @@ export const objcProtocolSchema = z.strictObject({
|
|||
optional_methods: z.array(objcMethodSchema).default([]),
|
||||
properties: z.array(objcPropertySchema).default([]),
|
||||
});
|
||||
export type ObjcProtocol = z.infer<typeof objcProtocolSchema>;
|
||||
|
||||
/** Objective-C class metadata. */
|
||||
export const objcClassSchema = z.strictObject({
|
||||
|
|
@ -106,7 +103,6 @@ export const nativeMetadataDecodeSchema = z.strictObject({
|
|||
status: z.enum(["decoded", "partial", "unsupported", "invalid"]),
|
||||
reason: z.string().nullable(),
|
||||
});
|
||||
export type NativeMetadataDecode = z.infer<typeof nativeMetadataDecodeSchema>;
|
||||
|
||||
/** Evidence attached to one dispatch edge or metadata record. */
|
||||
export const nativeMetadataEvidenceSchema = z.strictObject({
|
||||
|
|
@ -142,7 +138,6 @@ export const objcIvarSchema = z.strictObject({
|
|||
decode: nativeMetadataDecodeSchema,
|
||||
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
|
||||
});
|
||||
export type ObjcIvar = z.infer<typeof objcIvarSchema>;
|
||||
|
||||
/** Encoded Objective-C protocol declaration with required and optional method facts. */
|
||||
export const objcProtocolRecordSchema = z.strictObject({
|
||||
|
|
@ -179,7 +174,6 @@ export const swiftConformanceSchema = z.strictObject({
|
|||
decode: nativeMetadataDecodeSchema,
|
||||
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
|
||||
});
|
||||
export type SwiftConformance = z.infer<typeof swiftConformanceSchema>;
|
||||
|
||||
/** A typed slot in a Swift protocol witness table or class vtable. */
|
||||
export const swiftDispatchSlotSchema = z.strictObject({
|
||||
|
|
@ -194,7 +188,6 @@ export const swiftDispatchSlotSchema = z.strictObject({
|
|||
decode: nativeMetadataDecodeSchema,
|
||||
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
|
||||
});
|
||||
export type SwiftDispatchSlot = z.infer<typeof swiftDispatchSlotSchema>;
|
||||
|
||||
/** Signed-relative pointer as stored in an Apple binary metadata structure. */
|
||||
export const relativePointerSchema = z.strictObject({
|
||||
|
|
@ -206,7 +199,6 @@ export const relativePointerSchema = z.strictObject({
|
|||
decode: nativeMetadataDecodeSchema,
|
||||
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
|
||||
});
|
||||
export type RelativePointer = z.infer<typeof relativePointerSchema>;
|
||||
|
||||
/** A raw Swift-mangled symbol when a provider has not decoded its metadata. */
|
||||
export const swiftSymbolSchema = z.strictObject({
|
||||
|
|
@ -225,9 +217,6 @@ export const nativeMetadataCoverageSchema = z.strictObject({
|
|||
examined: z.number().int().nonnegative(),
|
||||
decoded: z.number().int().nonnegative(),
|
||||
});
|
||||
export type NativeMetadataCoverage = z.infer<
|
||||
typeof nativeMetadataCoverageSchema
|
||||
>;
|
||||
|
||||
/** Swift declaration kind. */
|
||||
export const swiftDeclKindSchema = z.enum([
|
||||
|
|
@ -250,7 +239,6 @@ export const swiftAccessLevelSchema = z.enum([
|
|||
"public",
|
||||
"open",
|
||||
]);
|
||||
export type SwiftAccessLevel = z.infer<typeof swiftAccessLevelSchema>;
|
||||
/** Swift declaration metadata. */
|
||||
export const swiftDeclSchema = z.strictObject({
|
||||
kind: swiftDeclKindSchema,
|
||||
|
|
@ -275,8 +263,6 @@ export const dbOperationSchema = z.enum([
|
|||
"export",
|
||||
"import",
|
||||
]);
|
||||
export type DbOperation = z.infer<typeof dbOperationSchema>;
|
||||
|
||||
/** Database save operation result. */
|
||||
export const dbSaveResultSchema = z.strictObject({
|
||||
operation: dbOperationSchema,
|
||||
|
|
@ -322,9 +308,6 @@ export const nativeDispatchMetadataResultSchema = z.strictObject({
|
|||
analysis_profile_digest: z.string().min(1).nullable(),
|
||||
result: objcSwiftMetadataSchema,
|
||||
});
|
||||
export type NativeDispatchMetadataResult = z.infer<
|
||||
typeof nativeDispatchMetadataResultSchema
|
||||
>;
|
||||
|
||||
/** Decode only facts visible in a provider's existing name inventory. */
|
||||
export const inspectNativeDispatchMetadata = (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ import {
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import {
|
||||
createNonApplicationZipInventory,
|
||||
requireSuccessfulProjection,
|
||||
} from "../../support/applicationSessionFixture.js";
|
||||
|
||||
import { projectAndroidApplicationEvidence } from "../../../src/application/AndroidApplicationService.js";
|
||||
import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js";
|
||||
|
|
@ -50,14 +54,11 @@ describe("Android application projection", () => {
|
|||
const second = projectAndroidApplicationEvidence({
|
||||
inventory_evidence: [inventory],
|
||||
});
|
||||
expect(first.ok).toBe(true);
|
||||
expect(second.ok).toBe(true);
|
||||
if (!first.ok || !second.ok) return;
|
||||
const left = androidApplicationProjectionResultSchema.parse(
|
||||
first.value.normalized_result,
|
||||
requireSuccessfulProjection(first).normalized_result,
|
||||
);
|
||||
const right = androidApplicationProjectionResultSchema.parse(
|
||||
second.value.normalized_result,
|
||||
requireSuccessfulProjection(second).normalized_result,
|
||||
);
|
||||
expect(left).toEqual(right);
|
||||
expect(left).toMatchObject({
|
||||
|
|
@ -87,13 +88,8 @@ describe("Android application projection", () => {
|
|||
});
|
||||
|
||||
it("rejects non-APK inventory Evidence", async () => {
|
||||
const root = await createTestTempDirectory("rea-android-invalid-");
|
||||
const path = join(root, "fixture.zip");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("one.js", new TextReader("one"));
|
||||
await writeFile(path, await writer.close());
|
||||
const inventory = parseEvidence(
|
||||
await runProviderAnalysis(path, "inventory_artifact", {}),
|
||||
const inventory = await createNonApplicationZipInventory(
|
||||
"rea-android-invalid-",
|
||||
);
|
||||
expect(
|
||||
projectAndroidApplicationEvidence({ inventory_evidence: [inventory] }),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
import { fragmentInventoryEvidence } from "../../fixtures/artifactEvidence.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import {
|
||||
createNonApplicationZipInventory,
|
||||
requireSuccessfulProjection,
|
||||
} from "../../support/applicationSessionFixture.js";
|
||||
|
||||
import { projectAppleApplicationEvidence } from "../../../src/application/AppleApplicationService.js";
|
||||
import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js";
|
||||
|
|
@ -109,14 +113,11 @@ describe("Apple application projection", () => {
|
|||
const second = projectAppleApplicationEvidence({
|
||||
inventory_evidence: [inventory],
|
||||
});
|
||||
expect(first.ok).toBe(true);
|
||||
expect(second.ok).toBe(true);
|
||||
if (!first.ok || !second.ok) return;
|
||||
const left = appleApplicationProjectionResultSchema.parse(
|
||||
first.value.normalized_result,
|
||||
requireSuccessfulProjection(first).normalized_result,
|
||||
);
|
||||
const right = appleApplicationProjectionResultSchema.parse(
|
||||
second.value.normalized_result,
|
||||
requireSuccessfulProjection(second).normalized_result,
|
||||
);
|
||||
expect(left).toEqual(right);
|
||||
expect(left).toMatchObject({
|
||||
|
|
@ -149,14 +150,8 @@ describe("Apple application projection", () => {
|
|||
});
|
||||
|
||||
it("rejects non-IPA Evidence", async () => {
|
||||
const root = await createTestTempDirectory("rea-apple-invalid-");
|
||||
const path = join(root, "fixture.zip");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("one.js", new TextReader("one"));
|
||||
await writeFile(path, await writer.close());
|
||||
const inventory = parseEvidence(
|
||||
await runProviderAnalysis(path, "inventory_artifact", {}),
|
||||
);
|
||||
const inventory =
|
||||
await createNonApplicationZipInventory("rea-apple-invalid-");
|
||||
expect(
|
||||
projectAppleApplicationEvidence({ inventory_evidence: [inventory] }),
|
||||
).toMatchObject({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import type {
|
|||
AnalysisClient,
|
||||
AnalysisProvider,
|
||||
} from "../../../src/application/AnalysisProvider.js";
|
||||
import type { BinarySession } from "../../../src/application/BinarySession.js";
|
||||
import type { JsonValue } from "../../../src/domain/jsonValue.js";
|
||||
import type { RecordUnknownInput } from "../../../src/domain/residualUnknown.js";
|
||||
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
|
||||
import {
|
||||
HopperStartError,
|
||||
|
|
@ -93,6 +96,44 @@ const client = (fail = false): AnalysisClient => ({
|
|||
close: () => Promise.resolve(),
|
||||
});
|
||||
|
||||
/** Open a target and run one address_name query, asserting both succeed. */
|
||||
const openAndExecuteAddressName = async (
|
||||
session: BinarySession,
|
||||
target: string,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
): Promise<void> => {
|
||||
expect((await session.open(target)).ok).toBe(true);
|
||||
expect((await session.execute("address_name", parameters)).ok).toBe(true);
|
||||
};
|
||||
|
||||
/** Shared network-reachability unknown input at one severity. */
|
||||
const networkReachabilityUnknown = (
|
||||
severity: "medium" | "high",
|
||||
): RecordUnknownInput => ({
|
||||
question: "Does this function reach the network?",
|
||||
severity,
|
||||
domain: "network",
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
required_authority: "shipped-artifact",
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
recommended_probes: [
|
||||
{ operation: "analyze_function", rationale: "Inspect its callers." },
|
||||
],
|
||||
relationships: [],
|
||||
});
|
||||
|
||||
/** Record the shared network-reachability unknown, asserting success. */
|
||||
const recordNetworkReachabilityUnknown = (
|
||||
session: BinarySession,
|
||||
severity: "medium" | "high",
|
||||
): void => {
|
||||
expect(session.recordUnknown(networkReachabilityUnknown(severity)).ok).toBe(
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
describe("detached provider and target metadata", () => {
|
||||
it("returns detached provider and target metadata", async () => {
|
||||
const [first] = await createBinarySessionTargets();
|
||||
|
|
@ -349,31 +390,11 @@ describe("snapshot switch rollback", () => {
|
|||
await copyFile(first, second);
|
||||
const calls: string[] = [];
|
||||
const session = createTestBinarySession(createCacheProvider(calls));
|
||||
expect((await session.open(first)).ok).toBe(true);
|
||||
expect(
|
||||
(
|
||||
await session.execute("address_name", {
|
||||
address: "0x1000",
|
||||
document: "first",
|
||||
})
|
||||
).ok,
|
||||
).toBe(true);
|
||||
expect(
|
||||
session.recordUnknown({
|
||||
question: "Does this function reach the network?",
|
||||
severity: "medium",
|
||||
domain: "network",
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
required_authority: "shipped-artifact",
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
recommended_probes: [
|
||||
{ operation: "analyze_function", rationale: "Inspect its callers." },
|
||||
],
|
||||
relationships: [],
|
||||
}).ok,
|
||||
).toBe(true);
|
||||
await openAndExecuteAddressName(session, first, {
|
||||
address: "0x1000",
|
||||
document: "first",
|
||||
});
|
||||
recordNetworkReachabilityUnknown(session, "medium");
|
||||
const originalCache = session.exportAnalysisSnapshot();
|
||||
expect(originalCache).toMatchObject({
|
||||
ok: true,
|
||||
|
|
@ -382,22 +403,7 @@ describe("snapshot switch rollback", () => {
|
|||
|
||||
const conflicting = createTestBinarySession(createCacheProvider([]));
|
||||
expect((await conflicting.open(second)).ok).toBe(true);
|
||||
expect(
|
||||
conflicting.recordUnknown({
|
||||
question: "Does this function reach the network?",
|
||||
severity: "high",
|
||||
domain: "network",
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
required_authority: "shipped-artifact",
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
recommended_probes: [
|
||||
{ operation: "analyze_function", rationale: "Inspect its callers." },
|
||||
],
|
||||
relationships: [],
|
||||
}).ok,
|
||||
).toBe(true);
|
||||
recordNetworkReachabilityUnknown(conflicting, "high");
|
||||
const conflictingSnapshot = conflicting.exportAnalysisSnapshot();
|
||||
expect(conflictingSnapshot.ok).toBe(true);
|
||||
if (!conflictingSnapshot.ok || !originalCache.ok) return;
|
||||
|
|
@ -425,8 +431,7 @@ describe("snapshot cache eligibility", () => {
|
|||
const [first] = await createBinarySessionTargets();
|
||||
const calls: string[] = [];
|
||||
const session = createTestBinarySession(createCacheProvider(calls));
|
||||
expect((await session.open(first)).ok).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
await openAndExecuteAddressName(session, first, {});
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(calls).toEqual(["health", "address_name", "address_name"]);
|
||||
expect(session.exportAnalysisSnapshot()).toMatchObject({
|
||||
|
|
|
|||
|
|
@ -184,26 +184,18 @@ it("keeps suffix-named ASAR directories in the graph and analyzes their files",
|
|||
),
|
||||
).toBe(true);
|
||||
|
||||
const result = await reconstructJavaScriptArtifact({
|
||||
input_path: archive,
|
||||
format: "asar",
|
||||
});
|
||||
|
||||
expect(result.statistics.parsed_javascript_files).toBeGreaterThan(0);
|
||||
expect(result.graph.nodes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "javascript-asset",
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
path: "node_modules/@zip.js/entry.js",
|
||||
}),
|
||||
await reconstructAsarArchive(archive, [
|
||||
expect.objectContaining({
|
||||
kind: "javascript-asset",
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
path: "node_modules/@zip.js/entry.js",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps ASAR analysis usable when unpacked native companion bytes are absent", async () => {
|
||||
|
|
@ -228,17 +220,9 @@ it("keeps ASAR analysis usable when unpacked native companion bytes are absent",
|
|||
]),
|
||||
});
|
||||
|
||||
const result = await reconstructJavaScriptArtifact({
|
||||
input_path: archive,
|
||||
format: "asar",
|
||||
});
|
||||
|
||||
expect(result.statistics.parsed_javascript_files).toBeGreaterThan(0);
|
||||
expect(result.graph.nodes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "javascript-asset" }),
|
||||
]),
|
||||
);
|
||||
await reconstructAsarArchive(archive, [
|
||||
expect.objectContaining({ kind: "javascript-asset" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("recurses into filesystem-backed ASAR containers without losing container-relative paths", async () => {
|
||||
|
|
@ -360,6 +344,21 @@ const assertSemanticLinks = (
|
|||
expect(endpointJson).toContain("token=fixture-secret");
|
||||
};
|
||||
|
||||
const reconstructAsarArchive = async (
|
||||
archive: string,
|
||||
expectedNodes: readonly unknown[],
|
||||
): Promise<Awaited<ReturnType<typeof reconstructJavaScriptArtifact>>> => {
|
||||
const result = await reconstructJavaScriptArtifact({
|
||||
input_path: archive,
|
||||
format: "asar",
|
||||
});
|
||||
expect(result.statistics.parsed_javascript_files).toBeGreaterThan(0);
|
||||
expect(result.graph.nodes).toEqual(
|
||||
expect.arrayContaining([...expectedNodes]),
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
const fixtureDirectory = async (): Promise<string> => {
|
||||
const root = await createTestTempDirectory("rea-javascript-artifact-");
|
||||
await writeJavaScriptArtifactFixture(root);
|
||||
|
|
|
|||
|
|
@ -1,29 +1,43 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { reconstructJavaScriptArtifact } from "../../../src/application/JavaScriptArtifactReconstruction.js";
|
||||
import type {
|
||||
ApplicationEdge,
|
||||
ApplicationNode,
|
||||
JavaScriptApplicationGraph,
|
||||
} from "../../../src/domain/javascriptApplicationGraph.js";
|
||||
import {
|
||||
findChunkNode,
|
||||
findGraphEdge,
|
||||
findModuleNodeByKey,
|
||||
reconstructJavaScriptFixture,
|
||||
writeFixtureFiles,
|
||||
} from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
describe("Webpack and Rspack runtime adapters", () => {
|
||||
it("recovers runtime entry modules, async chunks, and factory require aliases", async () => {
|
||||
const root = await bundlerFixture();
|
||||
const result = await reconstructJavaScriptArtifact({ input_path: root });
|
||||
const result = await reconstructJavaScriptFixture(root);
|
||||
const graph = result.graph;
|
||||
|
||||
const webpackEntry = chunk(graph, "webpackChunkcustomPortal", "main");
|
||||
const webpackLazy = chunk(graph, "webpackChunkcustomPortal", "lazy");
|
||||
const webpackModule = moduleNode(graph, "10");
|
||||
const rspackEntry = chunk(graph, "rspackChunkcustomPortal", "editor");
|
||||
const rspackModel = chunk(graph, "rspackChunkcustomPortal", "model");
|
||||
const rspackModule = moduleNode(graph, "src/entry.ts");
|
||||
const webpackEntry = findChunkNode(
|
||||
graph,
|
||||
"webpackChunkcustomPortal",
|
||||
"main",
|
||||
);
|
||||
const webpackLazy = findChunkNode(
|
||||
graph,
|
||||
"webpackChunkcustomPortal",
|
||||
"lazy",
|
||||
);
|
||||
const webpackModule = findModuleNodeByKey(graph, "10");
|
||||
const rspackEntry = findChunkNode(
|
||||
graph,
|
||||
"rspackChunkcustomPortal",
|
||||
"editor",
|
||||
);
|
||||
const rspackModel = findChunkNode(
|
||||
graph,
|
||||
"rspackChunkcustomPortal",
|
||||
"model",
|
||||
);
|
||||
const rspackModule = findModuleNodeByKey(graph, "src/entry.ts");
|
||||
|
||||
expect(webpackEntry?.observations[0]?.properties).toMatchObject({
|
||||
bundler: "webpack",
|
||||
|
|
@ -49,32 +63,42 @@ describe("Webpack and Rspack runtime adapters", () => {
|
|||
runtime_entry: true,
|
||||
});
|
||||
|
||||
expect(edge(graph, webpackEntry, webpackModule, "loads")).toMatchObject({
|
||||
expect(
|
||||
findGraphEdge(graph, webpackEntry, webpackModule, "loads"),
|
||||
).toMatchObject({
|
||||
properties: expect.objectContaining({
|
||||
kind: "bundler-entry-module",
|
||||
resolution_status: "resolved",
|
||||
}),
|
||||
});
|
||||
expect(edge(graph, webpackEntry, webpackLazy, "imports")).toMatchObject({
|
||||
expect(
|
||||
findGraphEdge(graph, webpackEntry, webpackLazy, "imports"),
|
||||
).toMatchObject({
|
||||
properties: expect.objectContaining({
|
||||
kind: "bundler-async-chunk",
|
||||
resolution_status: "resolved",
|
||||
}),
|
||||
});
|
||||
expect(edge(graph, webpackModule, webpackLazy, "imports")).toMatchObject({
|
||||
expect(
|
||||
findGraphEdge(graph, webpackModule, webpackLazy, "imports"),
|
||||
).toMatchObject({
|
||||
properties: expect.objectContaining({
|
||||
kind: "dynamic-import",
|
||||
specifier: "chunk:lazy",
|
||||
resolved_path: "renderer/chunks/runtime.js#chunk:lazy",
|
||||
}),
|
||||
});
|
||||
expect(edge(graph, rspackEntry, rspackModel, "imports")).toMatchObject({
|
||||
expect(
|
||||
findGraphEdge(graph, rspackEntry, rspackModel, "imports"),
|
||||
).toMatchObject({
|
||||
properties: expect.objectContaining({
|
||||
kind: "bundler-async-chunk",
|
||||
resolution_status: "resolved",
|
||||
}),
|
||||
});
|
||||
expect(edge(graph, rspackModule, rspackModel, "imports")).toMatchObject({
|
||||
expect(
|
||||
findGraphEdge(graph, rspackModule, rspackModel, "imports"),
|
||||
).toMatchObject({
|
||||
properties: expect.objectContaining({
|
||||
kind: "dynamic-import",
|
||||
specifier: "chunk:model",
|
||||
|
|
@ -103,11 +127,9 @@ describe("Webpack and Rspack runtime adapters", () => {
|
|||
|
||||
it("recovers esbuild wrapper modules and Vite preload dependencies", async () => {
|
||||
const root = await esmRuntimeFixture();
|
||||
const { graph } = await reconstructJavaScriptArtifact({
|
||||
input_path: root,
|
||||
});
|
||||
const commonJsModule = moduleNode(graph, "src/dep.js");
|
||||
const esmModule = moduleNode(graph, "src/core.js");
|
||||
const { graph } = await reconstructJavaScriptFixture(root);
|
||||
const commonJsModule = findModuleNodeByKey(graph, "src/dep.js");
|
||||
const esmModule = findModuleNodeByKey(graph, "src/core.js");
|
||||
|
||||
expect(commonJsModule?.observations[0]?.properties).toMatchObject({
|
||||
bundler: "esbuild",
|
||||
|
|
@ -132,10 +154,8 @@ describe("Webpack and Rspack runtime adapters", () => {
|
|||
|
||||
const bundlerFixture = async (): Promise<string> => {
|
||||
const root = await createTestTempDirectory("rea-bundler-adapters-");
|
||||
await mkdir(join(root, "renderer", "chunks"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "renderer", "chunks", "runtime.js"),
|
||||
`
|
||||
await writeFixtureFiles(root, {
|
||||
"renderer/chunks/runtime.js": `
|
||||
var __webpack_module_cache__ = {};
|
||||
(globalThis["webpackChunkcustomPortal"] = globalThis["webpackChunkcustomPortal"] || []).push([
|
||||
["main"],
|
||||
|
|
@ -179,15 +199,14 @@ const bundlerFixture = async (): Promise<string> => {
|
|||
}
|
||||
]);
|
||||
`,
|
||||
);
|
||||
});
|
||||
return root;
|
||||
};
|
||||
|
||||
const esmRuntimeFixture = async (): Promise<string> => {
|
||||
const root = await createTestTempDirectory("rea-esm-runtime-adapters-");
|
||||
await writeFile(
|
||||
join(root, "main.js"),
|
||||
`
|
||||
await writeFixtureFiles(root, {
|
||||
"main.js": `
|
||||
const __vite__mapDeps = (indexes, map = __vite__mapDeps, dependencies =
|
||||
(map.f || (map.f = ["./feature.js", "./main.css"]))) =>
|
||||
indexes.map((index) => dependencies[index]);
|
||||
|
|
@ -204,51 +223,8 @@ const esmRuntimeFixture = async (): Promise<string> => {
|
|||
__vite__mapDeps([0, 1]);
|
||||
init_core();
|
||||
`,
|
||||
);
|
||||
await writeFile(join(root, "feature.js"), "export const feature = true;");
|
||||
await writeFile(join(root, "main.css"), ".app { color: green; }");
|
||||
"feature.js": "export const feature = true;",
|
||||
"main.css": ".app { color: green; }",
|
||||
});
|
||||
return root;
|
||||
};
|
||||
|
||||
const chunk = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
runtime: string,
|
||||
chunkKey: string,
|
||||
): ApplicationNode | undefined =>
|
||||
graph.nodes.find(
|
||||
({ kind, observations }) =>
|
||||
kind === "javascript-chunk" &&
|
||||
observations.some(
|
||||
({ properties }) =>
|
||||
properties.runtime === runtime &&
|
||||
Array.isArray(properties.chunk_keys) &&
|
||||
properties.chunk_keys.includes(chunkKey),
|
||||
),
|
||||
);
|
||||
|
||||
const moduleNode = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
moduleKey: string,
|
||||
): ApplicationNode | undefined =>
|
||||
graph.nodes.find(
|
||||
({ kind, observations }) =>
|
||||
kind === "javascript-module" &&
|
||||
observations.some(
|
||||
({ properties }) => properties.module_key === moduleKey,
|
||||
),
|
||||
);
|
||||
|
||||
const edge = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
source: ApplicationNode | undefined,
|
||||
target: ApplicationNode | undefined,
|
||||
relation: string,
|
||||
): ApplicationEdge | undefined =>
|
||||
graph.edges.find(
|
||||
({ source_node_id, target_node_id, relation: edgeRelation }) =>
|
||||
source !== undefined &&
|
||||
target !== undefined &&
|
||||
source_node_id === source.node_id &&
|
||||
target_node_id === target.node_id &&
|
||||
edgeRelation === relation,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { reconstructJavaScriptArtifact } from "../../../src/application/JavaScriptArtifactReconstruction.js";
|
||||
import type {
|
||||
ApplicationEdge,
|
||||
ApplicationNode,
|
||||
JavaScriptApplicationGraph,
|
||||
} from "../../../src/domain/javascriptApplicationGraph.js";
|
||||
import {
|
||||
findExportNode,
|
||||
findRelationshipEdge,
|
||||
findSourceModule,
|
||||
reconstructJavaScriptFixture,
|
||||
writeFixtureFiles,
|
||||
} from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
describe("CommonJS and ESM module relationships", () => {
|
||||
it("composes bindings, re-exports, dynamic imports, and JSON modules", async () => {
|
||||
const root = await moduleFixture();
|
||||
const first = await reconstructJavaScriptArtifact({ input_path: root });
|
||||
const second = await reconstructJavaScriptArtifact({ input_path: root });
|
||||
const first = await reconstructJavaScriptFixture(root);
|
||||
const second = await reconstructJavaScriptFixture(root);
|
||||
const graph = first.graph;
|
||||
|
||||
expect(second.graph).toEqual(graph);
|
||||
expect(sourceModule(graph, "main.cjs")).toBeDefined();
|
||||
expect(sourceModule(graph, "consumer.mjs")).toBeDefined();
|
||||
expect(sourceModule(graph, "dependency.mjs")).toBeDefined();
|
||||
expect(findSourceModule(graph, "main.cjs")).toBeDefined();
|
||||
expect(findSourceModule(graph, "consumer.mjs")).toBeDefined();
|
||||
expect(findSourceModule(graph, "dependency.mjs")).toBeDefined();
|
||||
|
||||
expect(
|
||||
relationship(graph, {
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "require",
|
||||
specifier: "./dependency.mjs",
|
||||
resolvedPath: "dependency.mjs",
|
||||
|
|
@ -46,7 +47,7 @@ describe("CommonJS and ESM module relationships", () => {
|
|||
]),
|
||||
);
|
||||
expect(
|
||||
relationship(graph, {
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "require",
|
||||
specifier: "fixture-package",
|
||||
resolvedPath: "node_modules/fixture-package/cjs.cjs",
|
||||
|
|
@ -54,7 +55,7 @@ describe("CommonJS and ESM module relationships", () => {
|
|||
}),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
relationship(graph, {
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "import",
|
||||
specifier: "fixture-package",
|
||||
resolvedPath: "node_modules/fixture-package/esm.mjs",
|
||||
|
|
@ -63,7 +64,7 @@ describe("CommonJS and ESM module relationships", () => {
|
|||
}),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
relationship(graph, {
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "import",
|
||||
specifier: "./values.js",
|
||||
resolvedPath: "values.js",
|
||||
|
|
@ -72,7 +73,7 @@ describe("CommonJS and ESM module relationships", () => {
|
|||
}),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
relationship(graph, {
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "re-export",
|
||||
specifier: "./star.js",
|
||||
resolvedPath: "star.js",
|
||||
|
|
@ -81,7 +82,7 @@ describe("CommonJS and ESM module relationships", () => {
|
|||
}),
|
||||
).toBeDefined();
|
||||
|
||||
const forwarded = exportNode(graph, "main.cjs", "forwarded");
|
||||
const forwarded = findExportNode(graph, "main.cjs", "forwarded");
|
||||
expect(forwarded).toBeDefined();
|
||||
expect(
|
||||
graph.edges.some(
|
||||
|
|
@ -101,11 +102,11 @@ describe("CommonJS and ESM module relationships", () => {
|
|||
properties.resolved_path === "lazy.js",
|
||||
),
|
||||
).toMatchObject({
|
||||
source_node_id: sourceModule(graph, "consumer.mjs")?.node_id,
|
||||
source_node_id: findSourceModule(graph, "consumer.mjs")?.node_id,
|
||||
});
|
||||
|
||||
expect(
|
||||
relationship(graph, {
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "require",
|
||||
specifier: "./data.json",
|
||||
resolvedPath: "data.json",
|
||||
|
|
@ -115,7 +116,7 @@ describe("CommonJS and ESM module relationships", () => {
|
|||
target_json_status: "included",
|
||||
});
|
||||
expect(
|
||||
relationship(graph, {
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "require",
|
||||
specifier: "./broken.json",
|
||||
resolvedPath: "broken.json",
|
||||
|
|
@ -125,7 +126,7 @@ describe("CommonJS and ESM module relationships", () => {
|
|||
target_json_status: "invalid",
|
||||
});
|
||||
expect(
|
||||
relationship(graph, {
|
||||
findRelationshipEdge(graph, {
|
||||
kind: "require",
|
||||
specifier: "electron",
|
||||
})?.properties,
|
||||
|
|
@ -164,8 +165,8 @@ describe("complete static application projections", () => {
|
|||
writeFile(join(root, "many.json"), JSON.stringify(jsonKeys)),
|
||||
]);
|
||||
|
||||
const { graph } = await reconstructJavaScriptArtifact({ input_path: root });
|
||||
const module = sourceModule(graph, "many-exports.mjs");
|
||||
const { graph } = await reconstructJavaScriptFixture(root);
|
||||
const module = findSourceModule(graph, "many-exports.mjs");
|
||||
const exportObservation = module?.observations.find(
|
||||
({ properties }) => properties.semantic_role === "source-module",
|
||||
);
|
||||
|
|
@ -187,14 +188,9 @@ describe("complete static application projections", () => {
|
|||
|
||||
const moduleFixture = async (): Promise<string> => {
|
||||
const root = await createTestTempDirectory("rea-module-relationships-");
|
||||
await mkdir(join(root, "node_modules", "fixture-package"), {
|
||||
recursive: true,
|
||||
});
|
||||
await Promise.all([
|
||||
writeFile(join(root, "package.json"), '{"main":"main.cjs"}'),
|
||||
writeFile(
|
||||
join(root, "main.cjs"),
|
||||
`
|
||||
await writeFixtureFiles(root, {
|
||||
"package.json": '{"main":"main.cjs"}',
|
||||
"main.cjs": `
|
||||
const { value: importedValue } = require("./dependency.mjs");
|
||||
const data = require("./data.json");
|
||||
const broken = require("./broken.json");
|
||||
|
|
@ -207,104 +203,32 @@ const moduleFixture = async (): Promise<string> => {
|
|||
exports.packageValue = packageValue;
|
||||
require(dynamicName);
|
||||
`,
|
||||
),
|
||||
writeFile(
|
||||
join(root, "dependency.mjs"),
|
||||
`
|
||||
"dependency.mjs": `
|
||||
export { value } from "./values.js";
|
||||
export * from "./star.js";
|
||||
`,
|
||||
),
|
||||
writeFile(
|
||||
join(root, "consumer.mjs"),
|
||||
`
|
||||
"consumer.mjs": `
|
||||
import { value as alias } from "./values.js";
|
||||
import packageDefault from "fixture-package";
|
||||
export { alias as relayed };
|
||||
void import("./lazy.js");
|
||||
void packageDefault;
|
||||
`,
|
||||
),
|
||||
writeFile(join(root, "values.js"), "export const value = 42;"),
|
||||
writeFile(join(root, "star.js"), "export const extra = true;"),
|
||||
writeFile(join(root, "lazy.js"), "export default 'lazy';"),
|
||||
writeFile(join(root, "data.json"), '{"name":"fixture","value":42}'),
|
||||
writeFile(join(root, "broken.json"), '{"name":'),
|
||||
writeFile(
|
||||
join(root, "node_modules", "fixture-package", "package.json"),
|
||||
JSON.stringify({
|
||||
exports: {
|
||||
".": {
|
||||
import: "./esm.mjs",
|
||||
require: "./cjs.cjs",
|
||||
},
|
||||
"values.js": "export const value = 42;",
|
||||
"star.js": "export const extra = true;",
|
||||
"lazy.js": "export default 'lazy';",
|
||||
"data.json": '{"name":"fixture","value":42}',
|
||||
"broken.json": '{"name":',
|
||||
"node_modules/fixture-package/package.json": JSON.stringify({
|
||||
exports: {
|
||||
".": {
|
||||
import: "./esm.mjs",
|
||||
require: "./cjs.cjs",
|
||||
},
|
||||
}),
|
||||
),
|
||||
writeFile(
|
||||
join(root, "node_modules", "fixture-package", "esm.mjs"),
|
||||
"export default 'esm';",
|
||||
),
|
||||
writeFile(
|
||||
join(root, "node_modules", "fixture-package", "cjs.cjs"),
|
||||
"module.exports = 'cjs';",
|
||||
),
|
||||
]);
|
||||
},
|
||||
}),
|
||||
"node_modules/fixture-package/esm.mjs": "export default 'esm';",
|
||||
"node_modules/fixture-package/cjs.cjs": "module.exports = 'cjs';",
|
||||
});
|
||||
return root;
|
||||
};
|
||||
|
||||
interface RelationshipQuery {
|
||||
readonly kind: string;
|
||||
readonly specifier: string;
|
||||
readonly resolvedPath?: string;
|
||||
readonly importedName?: string;
|
||||
readonly localName?: string;
|
||||
readonly exportedName?: string;
|
||||
}
|
||||
|
||||
const relationship = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
query: RelationshipQuery,
|
||||
): ApplicationEdge | undefined =>
|
||||
graph.edges.find(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" &&
|
||||
properties.module_link_kind === query.kind &&
|
||||
properties.specifier === query.specifier &&
|
||||
(query.resolvedPath === undefined ||
|
||||
properties.resolved_path === query.resolvedPath) &&
|
||||
(query.importedName === undefined ||
|
||||
properties.imported_name === query.importedName) &&
|
||||
(query.localName === undefined ||
|
||||
properties.local_name === query.localName) &&
|
||||
(query.exportedName === undefined ||
|
||||
properties.exported_name === query.exportedName),
|
||||
);
|
||||
|
||||
const sourceModule = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
path: string,
|
||||
): ApplicationNode | undefined =>
|
||||
graph.nodes.find(
|
||||
(node) =>
|
||||
node.kind === "javascript-module" &&
|
||||
node.observations.some(
|
||||
({ properties }) => properties.logical_module_key === path,
|
||||
),
|
||||
);
|
||||
|
||||
const exportNode = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
path: string,
|
||||
name: string,
|
||||
): ApplicationNode | undefined =>
|
||||
graph.nodes.find(
|
||||
(node) =>
|
||||
node.kind === "javascript-module" &&
|
||||
node.observations.some(
|
||||
({ properties }) =>
|
||||
properties.semantic_role === "export-binding" &&
|
||||
properties.module_path === path &&
|
||||
properties.exported_name === name,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
completeVerifierRun,
|
||||
createVerifierRun,
|
||||
} from "../../scripts/lib/verifier-run.mjs";
|
||||
import { waitForExit } from "../support/process/processFixture.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const CHILD_SHUTDOWN_GRACE_MS = 1_000;
|
||||
|
|
@ -154,27 +155,6 @@ const stopChild = async (child: ChildProcess): Promise<void> => {
|
|||
}
|
||||
};
|
||||
|
||||
const waitForExit = (
|
||||
child: ChildProcess,
|
||||
timeoutMs: number,
|
||||
): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
child.removeListener("exit", onExit);
|
||||
resolve(false);
|
||||
}, timeoutMs);
|
||||
timer.unref();
|
||||
const onExit = (): void => {
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
};
|
||||
child.once("exit", onExit);
|
||||
});
|
||||
|
||||
const restoreRunId = (runId: string | undefined): void => {
|
||||
if (runId === undefined) delete process.env.REA_PROCESS_RUN_ID;
|
||||
else process.env.REA_PROCESS_RUN_ID = runId;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { runProviderAnalysis } from "../../src/application/DirectAnalysis.js";
|
||||
import type { AnalysisError } from "../../src/domain/errors.js";
|
||||
import { type Evidence, parseEvidence } from "../../src/domain/evidence.js";
|
||||
import type { Result } from "../../src/domain/result.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
|
||||
/** Assert a projection succeeded and return its Evidence for schema parsing. */
|
||||
export const requireSuccessfulProjection = (
|
||||
result: Result<Evidence, AnalysisError>,
|
||||
): Evidence => {
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok)
|
||||
throw new TypeError("Expected application projection to succeed");
|
||||
return result.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build inventory Evidence from a ZIP that is neither an APK nor an IPA.
|
||||
* Both mobile projections must reject it with an AnalysisInputError.
|
||||
*/
|
||||
export const createNonApplicationZipInventory = async (
|
||||
prefix: string,
|
||||
): Promise<Evidence> => {
|
||||
const root = await createTestTempDirectory(prefix);
|
||||
const path = join(root, "fixture.zip");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("one.js", new TextReader("one"));
|
||||
await writeFile(path, await writer.close());
|
||||
return parseEvidence(
|
||||
await runProviderAnalysis(path, "inventory_artifact", {}),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { reconstructJavaScriptArtifact } from "../../src/application/JavaScriptArtifactReconstruction.js";
|
||||
import type {
|
||||
ApplicationEdge,
|
||||
ApplicationNode,
|
||||
JavaScriptApplicationGraph,
|
||||
} from "../../src/domain/javascriptApplicationGraph.js";
|
||||
|
||||
/** Write text fixture files, creating parent directories as needed. */
|
||||
export const writeFixtureFiles = async (
|
||||
root: string,
|
||||
files: Readonly<Record<string, string>>,
|
||||
): Promise<void> => {
|
||||
await Promise.all(
|
||||
Object.entries(files).map(async ([relative, contents]) => {
|
||||
const path = join(root, relative);
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, contents);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/** Reconstruct one JavaScript artifact fixture directory. */
|
||||
export const reconstructJavaScriptFixture = (
|
||||
inputPath: string,
|
||||
): Promise<Awaited<ReturnType<typeof reconstructJavaScriptArtifact>>> =>
|
||||
reconstructJavaScriptArtifact({ input_path: inputPath });
|
||||
|
||||
/** Find a bundler chunk node by runtime and chunk key. */
|
||||
export const findChunkNode = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
runtime: string,
|
||||
chunkKey: string,
|
||||
): ApplicationNode | undefined =>
|
||||
graph.nodes.find(
|
||||
({ kind, observations }) =>
|
||||
kind === "javascript-chunk" &&
|
||||
observations.some(
|
||||
({ properties }) =>
|
||||
properties.runtime === runtime &&
|
||||
Array.isArray(properties.chunk_keys) &&
|
||||
properties.chunk_keys.includes(chunkKey),
|
||||
),
|
||||
);
|
||||
|
||||
/** Find a bundler module node by module key. */
|
||||
export const findModuleNodeByKey = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
moduleKey: string,
|
||||
): ApplicationNode | undefined =>
|
||||
graph.nodes.find(
|
||||
({ kind, observations }) =>
|
||||
kind === "javascript-module" &&
|
||||
observations.some(
|
||||
({ properties }) => properties.module_key === moduleKey,
|
||||
),
|
||||
);
|
||||
|
||||
/** Find one directed edge between two application nodes. */
|
||||
export const findGraphEdge = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
source: ApplicationNode | undefined,
|
||||
target: ApplicationNode | undefined,
|
||||
relation: string,
|
||||
): ApplicationEdge | undefined =>
|
||||
graph.edges.find(
|
||||
({ source_node_id, target_node_id, relation: edgeRelation }) =>
|
||||
source !== undefined &&
|
||||
target !== undefined &&
|
||||
source_node_id === source.node_id &&
|
||||
target_node_id === target.node_id &&
|
||||
edgeRelation === relation,
|
||||
);
|
||||
|
||||
/** Module-link facts used to query one import edge. */
|
||||
export interface RelationshipQuery {
|
||||
readonly kind: string;
|
||||
readonly specifier: string;
|
||||
readonly resolvedPath?: string;
|
||||
readonly importedName?: string;
|
||||
readonly localName?: string;
|
||||
readonly exportedName?: string;
|
||||
}
|
||||
|
||||
/** Find one module-link import edge matching the query. */
|
||||
export const findRelationshipEdge = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
query: RelationshipQuery,
|
||||
): ApplicationEdge | undefined =>
|
||||
graph.edges.find(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" &&
|
||||
properties.module_link_kind === query.kind &&
|
||||
properties.specifier === query.specifier &&
|
||||
(query.resolvedPath === undefined ||
|
||||
properties.resolved_path === query.resolvedPath) &&
|
||||
(query.importedName === undefined ||
|
||||
properties.imported_name === query.importedName) &&
|
||||
(query.localName === undefined ||
|
||||
properties.local_name === query.localName) &&
|
||||
(query.exportedName === undefined ||
|
||||
properties.exported_name === query.exportedName),
|
||||
);
|
||||
|
||||
/** Find a source module node by logical path. */
|
||||
export const findSourceModule = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
path: string,
|
||||
): ApplicationNode | undefined =>
|
||||
graph.nodes.find(
|
||||
(node) =>
|
||||
node.kind === "javascript-module" &&
|
||||
node.observations.some(
|
||||
({ properties }) => properties.logical_module_key === path,
|
||||
),
|
||||
);
|
||||
|
||||
/** Find one export-binding node by module path and export name. */
|
||||
export const findExportNode = (
|
||||
graph: JavaScriptApplicationGraph,
|
||||
path: string,
|
||||
name: string,
|
||||
): ApplicationNode | undefined =>
|
||||
graph.nodes.find(
|
||||
(node) =>
|
||||
node.kind === "javascript-module" &&
|
||||
node.observations.some(
|
||||
({ properties }) =>
|
||||
properties.semantic_role === "export-binding" &&
|
||||
properties.module_path === path &&
|
||||
properties.exported_name === name,
|
||||
),
|
||||
);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
spawn as nodeSpawn,
|
||||
type ChildProcess,
|
||||
type ChildProcessWithoutNullStreams,
|
||||
type SpawnOptionsWithoutStdio,
|
||||
} from "node:child_process";
|
||||
|
|
@ -37,8 +38,9 @@ export interface TestProcesses {
|
|||
shutdown(): Promise<void>;
|
||||
}
|
||||
|
||||
const waitForExit = (
|
||||
child: ChildProcessWithoutNullStreams,
|
||||
/** Wait for a child process to exit within a timeout, reporting completion. */
|
||||
export const waitForExit = (
|
||||
child: ChildProcess,
|
||||
timeoutMs: number,
|
||||
): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue