fix(javascript): follow bundler namespace interop for Electron APIs (#1827)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-11 06:28:19 -07:00
committed by GitHub
parent 91f843bf38
commit 6ac5c36503
5 changed files with 187 additions and 14 deletions
@@ -5,6 +5,7 @@ import {
resolveSemanticBindingState,
semanticResolutionBlocked,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
import {
semanticStaticPropertyKey,
@@ -58,3 +59,73 @@ export const semanticRequireOrigin = (
call: current,
};
};
// esbuild/Rolldown, TypeScript, and Babel emit these helpers for namespace
// imports of CommonJS modules; each returns an object exposing every own
// member of its operand, so member paths through the result are unchanged.
// Helper bodies are not evaluated: recognition trusts a single program-scope
// function definition under one of these emitted names.
const NAMESPACE_INTEROP_HELPERS = new Set([
"__importStar",
"__toESM",
"_interopRequireWildcard",
]);
/** Return the module operand of a locally declared namespace interop helper. */
export const semanticNamespaceInteropOperand = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): t.Expression | undefined => {
const call = unwrapJavaScriptExpression(node).node;
if (
!t.isCallExpression(call) ||
!t.isIdentifier(call.callee) ||
!NAMESPACE_INTEROP_HELPERS.has(call.callee.name) ||
semanticResolutionBlocked(state, call.callee, call.callee.name)
)
return undefined;
const helper = resolveSemanticBindingState(
state,
call.callee,
call.callee.name,
);
const [operand] = call.arguments;
return helper !== undefined &&
emittedInteropHelper(helper, state) &&
t.isExpression(operand)
? operand
: undefined;
};
// Emitters declare each helper once at program scope as a function, an
// arrow/function expression, or `(this && this.name) || <function>`.
const emittedInteropHelper = (
helper: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
): boolean => {
const [initializer] = helper.initializers;
if (
initializer === undefined ||
helper.initializers.length !== 1 ||
helper.definitions.length !== 1 ||
(helper.kind !== "function" && helper.kind !== "variable") ||
state.scopesById.get(helper.scopeId)?.kind !== "program"
)
return false;
const value = unwrapJavaScriptExpression(initializer.node).node;
const emitted =
t.isLogicalExpression(value, { operator: "||" }) &&
t.isLogicalExpression(unwrapJavaScriptExpression(value.left).node, {
operator: "&&",
})
? unwrapJavaScriptExpression(value.right).node
: value;
return (
t.isFunctionDeclaration(emitted) ||
t.isFunctionExpression(emitted) ||
t.isArrowFunctionExpression(emitted) ||
(t.isCallExpression(emitted) &&
(t.isFunctionExpression(emitted.callee) ||
t.isArrowFunctionExpression(emitted.callee)))
);
};