fix(bun-spawn-shim): eliminate globalThis.Bun top-level destructures for Electron/Node compat

Root cause: bun build --target bun inlines top-level
  var { spawn } = globalThis.Bun;
for every file that contains 'import { spawn } from "bun"'. On Node/Electron
where globalThis.Bun is undefined, this crashes with
  Cannot destructure property 'spawn' of 'globalThis.Bun' as it is undefined.
26 source files had this import; the bundled output had 25 top-level destructures.

Fix:
- Add src/shared/bun-spawn-shim.ts: a thin wrapper that
  - delegates to Bun.spawn/spawnSync when globalThis.Bun is present (real Bun)
  - falls back to static ESM imports of node:child_process otherwise
  - uses static 'import { spawn } from "node:child_process"' so Bun bundler
    does NOT emit any globalThis.Bun destructures for this module
- Replace all 26 'from "bun"' spawn/spawnSync imports with relative paths to shim
- Replace 4 direct Bun.spawn() call sites with shim's spawn()
- Remove src/electron-compat.ts and script/prepend-electron-shim.ts (no longer needed)
- Update src/electron-compat.test.ts to assert 0 top-level globalThis.Bun destructures

Verification: grep -c '} = globalThis.Bun;' dist/index.js → 0 (was 25)
All 5921 tests pass (1 pre-existing timeout failure unrelated to this change).

Fixes #3797
This commit is contained in:
YeonGyu-Kim
2026-05-05 22:41:45 +09:00
parent f178ea3207
commit 3ddc757b15
35 changed files with 117 additions and 191 deletions
-65
View File
@@ -1,65 +0,0 @@
#!/usr/bin/env bun
/**
* Prepend the Electron/Node compat shim to dist/index.js.
*
* When bundled with --target bun, Bun inlines top-level
* var { spawn } = globalThis.Bun;
* statements from its own internal modules. These crash on Node/Electron
* because globalThis.Bun is undefined there.
*
* Since Bun's bundler does not guarantee that a side-effect import at the
* top of src/index.ts will appear before all other module top-level code,
* we post-process the bundle and manually prepend the shim.
*/
import { readFileSync, writeFileSync } from "node:fs"
import { join } from "node:path"
const DIST_PATH = join(import.meta.dir, "..", "dist", "index.js")
const SHIM = `// [omo] Electron/Node compat shim — prepended by script/prepend-electron-shim.ts
// Populates globalThis.Bun before any top-level destructure fires.
if (!globalThis.Bun) {
const _cp = await import("node:child_process");
const _Readable = (await import("node:stream")).Readable;
function _mkproc(p) {
let c = null;
const exited = new Promise(r => {
p.on("exit", code => { c = code ?? 1; r(c); });
p.on("error", () => { if (c === null) { c = 1; r(1); } });
});
return { get exitCode() { return c; }, exited,
stdout: p.stdout ? _Readable.toWeb(p.stdout) : undefined,
stderr: p.stderr ? _Readable.toWeb(p.stderr) : undefined,
stdin: p.stdin, kill(s) { try { p.kill(s); } catch {} }, pid: p.pid };
}
function _spawn(cmdOrOpts, opts) {
const isObj = !Array.isArray(cmdOrOpts);
const cmd = isObj ? cmdOrOpts.cmd : cmdOrOpts;
const o = isObj ? cmdOrOpts : (opts || {});
const [bin, ...args] = cmd;
return _mkproc(_cp.spawn(bin, args, { cwd: o.cwd, env: o.env,
stdio: [o.stdin||"pipe", o.stdout||"pipe", o.stderr||"pipe"] }));
}
function _spawnSync(cmdOrOpts) {
const isObj = !Array.isArray(cmdOrOpts);
const cmd = isObj ? cmdOrOpts.cmd : cmdOrOpts;
const o = isObj ? cmdOrOpts : {};
const [bin, ...args] = cmd;
const r = _cp.spawnSync(bin, args, { cwd: o.cwd, env: o.env, stdio: ["pipe","pipe","pipe"] });
return { exitCode: r.status ?? 1, stdout: r.stdout, stderr: r.stderr };
}
globalThis.Bun = { spawn: _spawn, spawnSync: _spawnSync, env: process.env, version: "0.0.0-node-shim" };
}
`
const original = readFileSync(DIST_PATH, "utf-8")
// Avoid double-prepend
if (original.includes("[omo] Electron/Node compat shim")) {
console.log("Shim already present in dist/index.js, skipping.")
process.exit(0)
}
writeFileSync(DIST_PATH, SHIM + original, "utf-8")
console.log(`✓ Prepended Electron/Node compat shim to dist/index.js`)