fix(javascript): exclude overwritten Electron API identities (#1664)

* fix(javascript): exclude overwritten Electron API identities

* test(javascript): bind Electron in file-base path fixture

* test(electron): bind BrowserWindow in preload override fixtures

* test(mcp): allow cold catalog validation in runtime probe

* test(electron): bind APIs in option reconstruction fixtures
This commit is contained in:
Ysopwnable 2026-10-11 09:25:32 +08:00 committed by GitHub
parent ad41afa2b5
commit 29cc1b8746
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 434 additions and 62 deletions

View File

@ -270,6 +270,14 @@ substitute version-dependent Electron defaults for omitted `webPreferences`.
Dynamic option objects, bridge keys, API objects, and IPC channel expressions
remain unknown and make coverage partial.
Electron API identity follows proven lexical module aliases. Reassigned bindings,
dynamic selections, and visibly overwritten namespace members are unresolved.
Direct and destructuring assignment, loop, update, and delete targets are checked
through namespace aliases; unrelated member writes do not discard observed API
identity. This static check does
not evaluate arbitrary call side effects or runtime registration. Missing findings
do not establish that an application has no Electron boundary usage.
IPC pairing is an inference, not an observation. A renderer `invoke` pairs only
with one unique `ipcMain.handle`/`handleOnce` candidate on the same exact literal
channel; a renderer send pairs only with one unique `ipcMain.on`/`once`

View File

@ -1,3 +1,4 @@
import { ELECTRON_IDENTITY_LIMITATION } from "../../domain/javascript/javascriptElectronMemberWrites.js";
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
import {
createJavaScriptApplicationGraph,
@ -255,6 +256,11 @@ const graphLimitations = (
"CommonJS and ESM binding relationships were recovered from inert syntax and resolved only within the inventoried artifact container.",
"Webpack/Rspack factories were recovered from AST literals; REA did not invoke push handlers or bundle bootstrap code.",
"Static imports, entrypoints, workers, endpoints, and storage relationships do not prove runtime execution.",
...(context.analysis.files.some(({ javascript }) =>
javascript?.limitations.includes(ELECTRON_IDENTITY_LIMITATION),
)
? [ELECTRON_IDENTITY_LIMITATION]
: []),
...(electronFindings === 0
? []
: [

View File

@ -1015,6 +1015,7 @@ describe("contextual JavaScript module identity", () => {
it("retains CommonJS and ESM file-base identity during inert extraction", () => {
const analysis = analyzeJavaScriptStaticSource(
`
import { BrowserWindow } from "electron";
new BrowserWindow({ webPreferences: {
preload: path.resolve("ignored", __dirname, "preload.cjs")
}});

View File

@ -86,6 +86,31 @@ export const semanticObjectPatternKeys = (
return key === null ? [] : [key];
});
/** Visit member write targets in direct and destructuring assignment forms. */
export function* semanticMemberWriteTargets(
root: t.Node,
): Generator<t.MemberExpression | t.OptionalMemberExpression> {
const pending = [root];
while (pending.length > 0) {
const next = pending.pop();
if (next === undefined) break;
const node = unwrapJavaScriptExpression(next).node;
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
yield node;
else if (t.isRestElement(node)) pending.push(node.argument);
else if (t.isAssignmentPattern(node)) pending.push(node.left);
else if (t.isArrayPattern(node)) {
for (const element of node.elements.toReversed())
if (element !== null) pending.push(element);
} else if (t.isObjectPattern(node)) {
for (const property of node.properties.toReversed())
pending.push(
t.isRestElement(property) ? property.argument : property.value,
);
}
}
}
/**
* Display an exact JavaScript string as nonempty label text. Graph labels and
* artifact-local keys are nonempty, so the legal empty value is shown as `""`;

View File

@ -0,0 +1,61 @@
import * as t from "@babel/types";
import {
semanticStaticPropertyKey,
semanticMemberWriteTargets,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
/** Match Electron's supported module entry points. */
export const ELECTRON_MODULE =
/^electron(?:\/(?:common|main|renderer|utility))?$/u;
/** Static identity recovery cannot establish the absence of Electron usage. */
export const ELECTRON_IDENTITY_LIMITATION =
"Electron API identity follows proven lexical module aliases. Reassigned bindings, dynamic selections, and visibly overwritten namespace members are unresolved; missing findings do not establish absence. Arbitrary call side effects and runtime registration are not evaluated.";
/** Preserve exact property segments instead of flattening literal dotted keys. */
export const electronMemberPath = (
node: t.Node,
): {
readonly root: t.Node;
readonly members: readonly (string | null)[];
} => {
let root = unwrapJavaScriptExpression(node).node;
const members: (string | null)[] = [];
while (t.isMemberExpression(root) || t.isOptionalMemberExpression(root)) {
members.push(semanticStaticPropertyKey(root.property, root.computed));
root = unwrapJavaScriptExpression(root.object).node;
}
return { root, members: members.toReversed() };
};
/** Index visible writes through proven Electron aliases, without executing them. */
export function* collectElectronMemberWritesSteps(
program: t.Program,
origin: (root: t.Node) => readonly string[] | undefined,
): Generator<void, SemanticPropertyPathCoverage> {
const writes = new SemanticPropertyPathCoverage();
yield* traverseJavaScriptAstSteps(program, {
enter: (node) => {
const target = t.isAssignmentExpression(node)
? node.left
: t.isUpdateExpression(node)
? node.argument
: t.isUnaryExpression(node, { operator: "delete" })
? node.argument
: t.isForOfStatement(node) || t.isForInStatement(node)
? node.left
: undefined;
if (target === undefined) return;
for (const member of semanticMemberWriteTargets(target)) {
const { root, members } = electronMemberPath(member);
const exported = origin(root);
if (exported !== undefined) writes.retain([...exported, ...members]);
}
},
});
return writes;
}

View File

@ -44,6 +44,11 @@ import {
} from "./javascriptSemanticReturns.js";
import { collectJavaScriptDerivedSemanticsSteps } from "./javascriptSemanticDerivedAnalysis.js";
import { evaluateSemanticProvenance } from "./javascriptSemanticValues.js";
import {
collectElectronMemberWritesSteps,
electronMemberPath,
ELECTRON_MODULE,
} from "./javascriptElectronMemberWrites.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { propertyName } from "./javascriptAstValues.js";
import { semanticCoverage } from "./javascriptSemanticCoverage.js";
@ -311,8 +316,6 @@ const resolveGlobalAliasFact = (
return openGlobalFact(name);
};
const ELECTRON_MODULE = /^electron(?:\/(?:common|main|renderer|utility))?$/u;
/**
* Proven Electron export path segments for call and construction roots, keyed by
* root offset. Only proven Electron origins are present; callers must not
@ -331,6 +334,27 @@ export function* classifyParsedJavaScriptElectronBindingsSteps(
const state = createState(file.program);
yield* collectDefinitionsSteps(file.program, state);
const exportsByBinding = new Map<string, readonly string[] | null>();
const rootExport = (root: t.Node): readonly string[] | undefined => {
if (t.isIdentifier(root)) {
if (semanticResolutionBlocked(state, root, root.name)) return undefined;
const binding = resolveSemanticBindingState(state, root, root.name);
if (binding === undefined) return undefined;
let exported = exportsByBinding.get(binding.bindingId);
if (exported === undefined) {
exported = electronExport(binding, state);
exportsByBinding.set(binding.bindingId, exported);
}
return exported ?? undefined;
}
const origin = semanticRequireOrigin(root, state);
return origin !== undefined && ELECTRON_MODULE.test(origin.specifier)
? origin.importedPath
: undefined;
};
const writes = yield* collectElectronMemberWritesSteps(
file.program,
rootExport,
);
yield* traverseJavaScriptAstSteps(file.program, {
enter: (node) => {
if (
@ -339,35 +363,15 @@ export function* classifyParsedJavaScriptElectronBindingsSteps(
!t.isNewExpression(node)
)
return;
const root = calleeRoot(node.callee);
if (root === undefined) return;
if (t.isIdentifier(root)) {
const binding = resolveSemanticBindingState(state, root, root.name);
if (binding === undefined) return;
let exported = exportsByBinding.get(binding.bindingId);
if (exported === undefined) {
exported = electronExport(binding, state);
exportsByBinding.set(binding.bindingId, exported);
}
if (exported !== null) facts.set(root.start ?? -1, exported);
return;
}
const origin = semanticRequireOrigin(root, state);
if (origin !== undefined && ELECTRON_MODULE.test(origin.specifier))
facts.set(root.start ?? -1, origin.importedPath);
const { root, members } = electronMemberPath(node.callee);
const exported = rootExport(root);
if (exported !== undefined && !writes.covers([...exported, ...members]))
facts.set(root.start ?? -1, exported);
},
});
return facts;
}
const calleeRoot = (node: t.Node): t.Node | undefined => {
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
if (!t.isNode(node.object)) return undefined;
node = node.object;
}
return node;
};
// Read import and require origins directly; general value evaluation is
// unnecessary here and costly across large vendor bundles.
const electronExport = (

View File

@ -28,6 +28,7 @@ import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
import {
semanticStaticPropertyKey,
semanticMemberWriteTargets,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
@ -356,26 +357,14 @@ export function* collectSemanticMemberMutationsSteps(
markEscaped(expression.object, mutation);
};
const markTarget = (node: t.Node, mutation: t.Node): void => {
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
for (const target of semanticMemberWriteTargets(node))
markValue(
node.object,
[semanticStaticPropertyKey(node.property, node.computed)],
target.object,
[semanticStaticPropertyKey(target.property, target.computed)],
new Set(),
"write",
mutation,
);
else if (t.isRestElement(node)) markTarget(node.argument, mutation);
else if (t.isAssignmentPattern(node)) markTarget(node.left, mutation);
else if (t.isArrayPattern(node)) {
for (const element of node.elements)
if (element !== null) markTarget(element, mutation);
} else if (t.isObjectPattern(node)) {
for (const property of node.properties)
markTarget(
t.isRestElement(property) ? property.argument : property.value,
mutation,
);
}
};
yield* traverseJavaScriptAstSteps(program, {
enter: (node, parent) => {

View File

@ -60,23 +60,7 @@ export class SemanticPropertyPathCoverage {
/** Retain a path only when no previously retained prefix covers it. */
retain(path: JavaScriptSemanticPropertyPath): boolean {
const pending = [{ node: this.#root, offset: 0 }];
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) break;
if (current.node.terminal) return false;
const key = path[current.offset];
if (key === undefined) continue;
if (key !== null && typeof key !== "object") {
const exact = current.node.exact.get(String(key));
if (exact !== undefined)
pending.push({ node: exact, offset: current.offset + 1 });
}
for (const selected of current.node.selectors.values()) {
if (keyCovers(selected.key, key))
pending.push({ node: selected.node, offset: current.offset + 1 });
}
}
if (this.covers(path)) return false;
let node = this.#root;
for (const key of path) {
if (key !== null && typeof key !== "object") {
@ -102,6 +86,28 @@ export class SemanticPropertyPathCoverage {
node.selectors.clear();
return true;
}
/** Query retained prefixes without changing the coverage index. */
covers(path: JavaScriptSemanticPropertyPath): boolean {
const pending = [{ node: this.#root, offset: 0 }];
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) break;
if (current.node.terminal) return true;
const key = path[current.offset];
if (key === undefined) continue;
if (key !== null && typeof key !== "object") {
const exact = current.node.exact.get(String(key));
if (exact !== undefined)
pending.push({ node: exact, offset: current.offset + 1 });
}
for (const selected of current.node.selectors.values()) {
if (keyCovers(selected.key, key))
pending.push({ node: selected.node, offset: current.offset + 1 });
}
}
return false;
}
}
const keyCovers = (

View File

@ -1,4 +1,8 @@
import * as t from "@babel/types";
import {
ELECTRON_IDENTITY_LIMITATION,
ELECTRON_MODULE,
} from "./javascriptElectronMemberWrites.js";
import {
completeSemanticSteps,
@ -114,6 +118,12 @@ const finalizeStaticAnalysis = (
: [
"One or more static keys, expressions, or Electron boundary values were dynamic and remain unknown.",
]),
...(accumulator.references.some(
({ value }) =>
value.specifier !== null && ELECTRON_MODULE.test(value.specifier),
)
? [ELECTRON_IDENTITY_LIMITATION]
: []),
"JavaScript syntax was parsed as data and was never evaluated.",
];
return {

View File

@ -24,6 +24,8 @@ import {
import { parseJavaScriptApplicationGraph } from "../../../src/domain/javascript/javascriptApplicationGraph.js";
import { z } from "zod";
import { ELECTRON_IDENTITY_LIMITATION } from "../../../src/domain/javascript/javascriptElectronMemberWrites.js";
const execute = promisify(execFile);
const requireFixture = createRequire(import.meta.url);
const temporary: string[] = [];
@ -201,6 +203,254 @@ const analyzeThroughStdioMcp = async (
}
};
describe("Electron alias identity CLI and MCP", () => {
it.each([
{
name: "CommonJS binding chains",
esm: false,
main: 'const { BrowserWindow, ipcMain } = require("electron"); const First = BrowserWindow; const Win = First; const ipc = ipcMain;',
preload:
'const { contextBridge, ipcRenderer } = require("electron"); const First = contextBridge; const bridge = First; const ipc = ipcRenderer;',
},
{
name: "static namespace members",
esm: false,
main: 'const electron = require("electron"); const copied = electron; copied.unrelated = {}; const Win = copied["BrowserWindow"]; const ipc = copied.ipcMain;',
preload:
'const electron = require("electron"); const copied = electron; const bridge = copied.contextBridge; const ipc = copied["ipcRenderer"];',
},
{
name: "namespace destructuring",
esm: false,
main: 'const electron = require("electron"); const { BrowserWindow: Win, ipcMain: ipc } = electron;',
preload:
'const electron = require("electron"); const { contextBridge: bridge, ipcRenderer: ipc } = electron;',
},
{
name: "unwritten let and var bindings",
esm: false,
main: 'const { BrowserWindow, ipcMain } = require("electron"); let Win = BrowserWindow; var ipc = ipcMain;',
preload:
'const { contextBridge, ipcRenderer } = require("electron"); var bridge = contextBridge; let ipc = ipcRenderer;',
},
{
name: "ESM binding aliases",
esm: true,
main: 'import { BrowserWindow, ipcMain } from "electron"; const Win = BrowserWindow; const ipc = ipcMain;',
preload:
'import { contextBridge, ipcRenderer } from "electron"; const bridge = contextBridge; const ipc = ipcRenderer;',
},
])(
"recovers Electron boundaries through $name in CLI and MCP (#1656)",
async ({ main, preload, esm }) => {
const root = await createTestTempDirectory("rea-electron-alias-cli-");
temporary.push(root);
await Promise.all([
writeFile(
join(root, "package.json"),
JSON.stringify({
name: "alias-repro",
version: "1.0.0",
main: "main.js",
type: esm ? "module" : "commonjs",
}),
),
writeFile(
join(root, "main.js"),
`${main}\n${esm ? 'import { fileURLToPath } from "node:url";' : 'const path = require("node:path");'}\nnew Win({ webPreferences: { preload: ${esm ? 'fileURLToPath(new URL("./preload.js", import.meta.url))' : 'path.join(__dirname, "preload.js")'} } });\nipc.handle("ch:ping", async () => "pong");\nfunction shadow(Win, ipc) { new Win(); ipc.handle("ch:decoy", () => null); }\n`,
),
writeFile(
join(root, "preload.js"),
`${preload}\nbridge.exposeInMainWorld("api", { ping: () => ipc.invoke("ch:ping") });\nfunction shadow(bridge, ipc) { bridge.exposeInMainWorld("decoy", {}); ipc.invoke("ch:decoy"); }\n`,
),
]);
const cli = z
.object({
normalized_result: javascriptApplicationAnalysisResultSchema,
})
.parse(
await runCli(["analyze-javascript-application", root, "--json"]),
).normalized_result;
const mcp = await analyzeThroughStdioMcp(root);
expect(cli.summary).toMatchObject({
browser_windows: 1,
explicit_web_preferences: 1,
preload_entrypoints: 1,
context_bridge_apis: 1,
exposed_api_members: 1,
ipc: {
operations: 2,
literal_channels: 1,
main_handlers: 1,
renderer_transmissions: 1,
paired_renderer_transmissions: 1,
},
});
expect(mcp.summary).toEqual(cli.summary);
for (const result of [cli, mcp]) {
expect(result.limitations).toContain(ELECTRON_IDENTITY_LIMITATION);
expect(result.graph.nodes).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "ipc-channel",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
channel: "ch:ping",
operation: "handle",
}),
evidence: expect.objectContaining({
authority: "ast-static-analysis",
location: expect.objectContaining({
available: true,
value: expect.objectContaining({
kind: "source-range",
source: "main.js",
}),
}),
}),
}),
expect.objectContaining({
properties: expect.objectContaining({
channel: "ch:ping",
operation: "invoke",
}),
evidence: expect.objectContaining({
authority: "ast-static-analysis",
location: expect.objectContaining({
available: true,
value: expect.objectContaining({
kind: "source-range",
source: "preload.js",
}),
}),
}),
}),
]),
}),
]),
);
expect(result.graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({ relation: "handles" }),
expect.objectContaining({ relation: "invokes" }),
expect.objectContaining({ relation: "loads" }),
expect.objectContaining({ relation: "exposes" }),
]),
);
}
},
);
});
describe("Electron unavailable identity CLI and MCP", () => {
it.each([
{
name: "reassigned aliases",
main: 'const { BrowserWindow, ipcMain } = require("electron"); let Win = BrowserWindow; let ipc = ipcMain; Win = class {}; ipc = { handle() {} };',
preload:
'const { contextBridge, ipcRenderer } = require("electron"); let bridge = contextBridge; let ipc = ipcRenderer; bridge = { exposeInMainWorld() {} }; ipc = { invoke() {} };',
},
{
name: "conditional initializers",
main: 'const { BrowserWindow, ipcMain } = require("electron"); if (enabled) { var Win = BrowserWindow; var ipc = ipcMain; }',
preload:
'const { contextBridge, ipcRenderer } = require("electron"); if (enabled) { var bridge = contextBridge; var ipc = ipcRenderer; }',
},
{
name: "dynamic property selections",
main: 'const electron = require("electron"); const Win = electron[windowKey]; const ipc = electron[ipcKey];',
preload:
'const electron = require("electron"); const bridge = electron[bridgeKey]; const ipc = electron[ipcKey];',
},
{
name: "overwritten namespace exports",
main: 'const electron = require("electron"); const copied = electron; copied.BrowserWindow = class {}; electron.ipcMain = { handle() {} }; const Win = electron.BrowserWindow; const ipc = copied.ipcMain;',
preload:
'const electron = require("electron"); electron.contextBridge = { exposeInMainWorld() {} }; electron.ipcRenderer = { invoke() {} }; const bridge = electron.contextBridge; const ipc = electron.ipcRenderer;',
},
{
name: "overwritten API methods",
main: 'const electron = require("electron"); const ipc = electron.ipcMain; ipc.handle = () => null; const Win = electron[windowKey];',
preload:
'const electron = require("electron"); const bridge = electron.contextBridge; delete bridge.exposeInMainWorld; const ipc = electron.ipcRenderer; ipc.invoke++;',
},
{
name: "direct require namespace writes",
main: 'require("electron").BrowserWindow = class {}; require("electron").ipcMain = {handle() {}}; const {BrowserWindow: Win, ipcMain: ipc} = require("electron");',
preload:
'require("electron").contextBridge = {}; require("electron").ipcRenderer = {}; const {contextBridge: bridge, ipcRenderer: ipc} = require("electron");',
},
{
name: "unknown namespace write keys",
main: 'const electron = require("electron"); electron[unknownKey] = {}; const {BrowserWindow: Win, ipcMain: ipc} = electron;',
preload:
'const electron = require("electron"); electron[unknownKey] = {}; const {contextBridge: bridge, ipcRenderer: ipc} = electron;',
},
{
name: "destructuring assignment targets",
main: 'const electron = require("electron"); ({ BrowserWindow: electron.BrowserWindow, ipcMain: electron.ipcMain } = replacement); const {BrowserWindow: Win, ipcMain: ipc} = electron;',
preload:
'const electron = require("electron"); [electron.contextBridge, electron.ipcRenderer] = replacements; const {contextBridge: bridge, ipcRenderer: ipc} = electron;',
},
{
name: "loop assignment targets",
main: 'const electron = require("electron"); for ({ BrowserWindow: electron.BrowserWindow, ipcMain: electron.ipcMain } of replacements) {} const {BrowserWindow: Win, ipcMain: ipc} = electron;',
preload:
'const electron = require("electron"); for (electron.contextBridge in replacements) {} for (electron.ipcRenderer of replacements) {} const {contextBridge: bridge, ipcRenderer: ipc} = electron;',
},
{
name: "literal keys containing dots",
main: 'const electron = require("electron"); const Win = electron["fake.BrowserWindow"]; const ipc = electron["fake.ipcMain"]; electron["ipcMain.handle"]("ch:literal", () => null);',
preload:
'const electron = require("electron"); const bridge = electron["fake.contextBridge"]; const ipc = electron["fake.ipcRenderer"];',
},
])(
"keeps Electron identity unresolved for $name in CLI and MCP (#1656)",
async ({ main, preload }) => {
const root = await createTestTempDirectory("rea-electron-alias-unknown-");
temporary.push(root);
await Promise.all([
writeFile(
join(root, "package.json"),
JSON.stringify({
name: "alias-unknown",
version: "1.0.0",
main: "main.js",
}),
),
writeFile(
join(root, "main.js"),
`${main}\nnew Win(); ipc.handle("ch:ping", () => "pong");\n`,
),
writeFile(
join(root, "preload.js"),
`${preload}\nbridge.exposeInMainWorld("api", { ping: () => ipc.invoke("ch:ping") });\n`,
),
]);
const cli = z
.object({
normalized_result: javascriptApplicationAnalysisResultSchema,
})
.parse(
await runCli(["analyze-javascript-application", root, "--json"]),
).normalized_result;
const mcp = await analyzeThroughStdioMcp(root);
for (const result of [cli, mcp]) {
expect(result.summary).toMatchObject({
browser_windows: 0,
context_bridge_apis: 0,
ipc: { operations: 0 },
});
expect(
result.graph.nodes.some(({ kind }) => kind === "ipc-channel"),
).toBe(false);
expect(result.limitations).toContain(ELECTRON_IDENTITY_LIMITATION);
}
},
);
});
describe("JavaScript application path CLI", () => {
it("accepts a relative local application path and preserves canonical Evidence identity", async () => {
const root = await createTestTempDirectory("rea-relative-application-cli-");

View File

@ -67,7 +67,10 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
`new BrowserWindow({webPreferences:{${preferences}}});`,
);
await Promise.all([
writeFile(join(root, "main.js"), `${sourceLines.join("\n")}\n`),
writeFile(
join(root, "main.js"),
`${sourceLines.join("\n")}\nimport { BrowserWindow } from "electron";\n`,
),
writeFile(join(root, "first.js"), 'throw new Error("inert preload");\n'),
writeFile(join(root, "last.js"), 'throw new Error("inert preload");\n'),
]);

View File

@ -218,7 +218,10 @@ describe("production stdio runtime", () => {
record.level >= 50,
);
expect(fatal).toEqual([]);
}, 15_000);
// Full catalog discovery and first-call SDK schema compilation take about
// 8–9 seconds on one local CPU; the 15-second budget repeatedly expired in
// parallel CI shards. Keep a finite budget for the complete workflow.
}, 30_000);
it.runIf(process.platform === "darwin")(
"serves a database-kind initial target without a fatal record on macOS",

View File

@ -164,8 +164,14 @@ const windowPreloadNodes = (result: Reconstruction) =>
const reconstructSource = async (source: string): Promise<Reconstruction> => {
const root = await createTestTempDirectory("rea-electron-option-keys-");
await Promise.all([
writeFile(join(root, "package.json"), JSON.stringify({ main: "main.js" })),
writeFile(join(root, "main.js"), source),
writeFile(
join(root, "package.json"),
JSON.stringify({ main: "main.js", type: "module" }),
),
writeFile(
join(root, "main.js"),
`${source}\nimport { BrowserWindow, utilityProcess } from "electron";\n`,
),
writeFile(join(root, "preload.js"), "void 0;"),
writeFile(join(root, "worker.js"), "void 0;"),
]);