* fix(javascript): follow bundler namespace interop for Electron APIs
Rolldown/tsdown emit `let electron = require("electron"); electron =
__toESM(electron);` and esbuild/tsc emit `__toESM(require("electron"))`
or `__importStar(require("electron"))` for namespace imports. The
reassignment or the helper call hid the Electron origin, so bundled
main processes reported zero BrowserWindows and preloads.
Locally declared, never reassigned interop helpers recognized by name
now pass the module through, and a binding's rewrap of its own value is
not treated as a reassignment. Other writes stay unresolved.
Fixes #1802
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(javascript): require emitted interop helper definitions
Only trust an interop helper name when it has a single program-scope
function definition, and state that helper bodies are not evaluated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
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;
|
|
|
|
/**
|
|
* Electron's CommonJS exports define no `default` member, so a leading
|
|
* `default` segment can only denote the module object itself, as an ES module
|
|
* default import (`import electron from "electron"`) binds it.
|
|
*/
|
|
export const electronExportPath = (
|
|
importedPath: readonly string[],
|
|
): readonly string[] =>
|
|
importedPath[0] === "default" ? importedPath.slice(1) : importedPath;
|
|
|
|
/** Static identity recovery cannot establish the absence of Electron usage. */
|
|
export const ELECTRON_IDENTITY_LIMITATION =
|
|
"Electron API identity follows proven lexical module aliases, including results of namespace interop helpers (__toESM, __importStar, _interopRequireWildcard) recognized by name and a single program-scope function definition; helper bodies are not evaluated. Other 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;
|
|
}
|