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:
@@ -1,6 +1,6 @@
|
||||
import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { spawn } from "bun";
|
||||
import { spawn } from "./bun-spawn-shim";
|
||||
import { validateArchiveEntries, type ArchiveEntry } from "./archive-entry-validator";
|
||||
import { extractZip } from "./zip-extractor";
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Node/Electron-compatible spawn shim.
|
||||
*
|
||||
* Replaces direct `import { spawn } from "bun"` throughout the codebase so
|
||||
* that `bun build --target bun` no longer emits top-level
|
||||
* var { spawn } = globalThis.Bun;
|
||||
* destructures that crash on Node/Electron (where globalThis.Bun is undefined).
|
||||
*
|
||||
* On real Bun runtime: delegates straight to Bun.spawn / Bun.spawnSync.
|
||||
* On Node/Electron: backed by node:child_process (via static ESM imports
|
||||
* which are safe in both runtimes).
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process"
|
||||
import { Readable } from "node:stream"
|
||||
|
||||
const IS_BUN = typeof globalThis.Bun !== "undefined"
|
||||
|
||||
function _resolveCmd(cmdOrOpts: any, optsArg?: any): { cmd: string[]; opts: any } {
|
||||
const isObj = !Array.isArray(cmdOrOpts)
|
||||
return {
|
||||
cmd: isObj ? (cmdOrOpts as any).cmd : (cmdOrOpts as string[]),
|
||||
opts: isObj ? cmdOrOpts : (optsArg ?? {}),
|
||||
}
|
||||
}
|
||||
|
||||
function _wrapNodeProc(proc: ReturnType<typeof nodeSpawn>): any {
|
||||
let code: number | null = null
|
||||
const exited = new Promise<number>((resolve) => {
|
||||
proc.on("exit", (c) => { code = c ?? 1; resolve(code) })
|
||||
proc.on("error", () => { if (code === null) { code = 1; resolve(1) } })
|
||||
})
|
||||
return {
|
||||
get exitCode() { return code },
|
||||
exited,
|
||||
stdout: proc.stdout ? Readable.toWeb(proc.stdout) as ReadableStream<Uint8Array> : undefined,
|
||||
stderr: proc.stderr ? Readable.toWeb(proc.stderr) as ReadableStream<Uint8Array> : undefined,
|
||||
stdin: proc.stdin,
|
||||
kill(s?: NodeJS.Signals) { try { proc.kill(s) } catch {} },
|
||||
pid: proc.pid,
|
||||
}
|
||||
}
|
||||
|
||||
export function spawn(cmdOrOpts: any, opts?: any): any {
|
||||
if (IS_BUN) return (globalThis.Bun as any).spawn(cmdOrOpts, opts)
|
||||
const { cmd, opts: o } = _resolveCmd(cmdOrOpts, opts)
|
||||
const [bin, ...args] = cmd
|
||||
const proc = nodeSpawn(bin, args, {
|
||||
cwd: o.cwd as string | undefined,
|
||||
env: o.env as NodeJS.ProcessEnv | undefined,
|
||||
stdio: [(o.stdin ?? "pipe") as any, (o.stdout ?? "pipe") as any, (o.stderr ?? "pipe") as any],
|
||||
})
|
||||
return _wrapNodeProc(proc)
|
||||
}
|
||||
|
||||
export function spawnSync(cmdOrOpts: any, _opts?: any): any {
|
||||
if (IS_BUN) return (globalThis.Bun as any).spawnSync(cmdOrOpts)
|
||||
const { cmd, opts: o } = _resolveCmd(cmdOrOpts)
|
||||
const [bin, ...args] = cmd
|
||||
const r = nodeSpawnSync(bin, args, {
|
||||
cwd: o.cwd as string | undefined,
|
||||
env: o.env as NodeJS.ProcessEnv | undefined,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
})
|
||||
return { exitCode: r.status ?? 1, stdout: r.stdout, stderr: r.stderr, success: (r.status ?? 1) === 0, pid: -1 }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn as bunSpawn } from "bun"
|
||||
import { spawn as bunSpawn } from "./bun-spawn-shim"
|
||||
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process"
|
||||
import { Readable } from "node:stream"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "bun"
|
||||
import { spawn } from "../../bun-spawn-shim"
|
||||
import type { TmuxLayout } from "../../../config/schema"
|
||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "bun"
|
||||
import { spawn } from "../../bun-spawn-shim"
|
||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||
|
||||
export interface PaneDimensions {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "bun"
|
||||
import { spawn } from "../../bun-spawn-shim"
|
||||
import type { TmuxConfig } from "../../../config/schema"
|
||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||
import type { SpawnPaneResult } from "../types"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "bun"
|
||||
import { spawn } from "../../bun-spawn-shim"
|
||||
import type { TmuxConfig } from "../../../config/schema"
|
||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||
import type { SpawnPaneResult } from "../types"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "bun"
|
||||
import { spawn } from "../../bun-spawn-shim"
|
||||
import type { TmuxConfig } from "../../../config/schema"
|
||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||
import type { SpawnPaneResult } from "../types"
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { spawn } from "bun"
|
||||
export { spawn } from "../../bun-spawn-shim"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "bun"
|
||||
import { spawn } from "../../bun-spawn-shim"
|
||||
import type { TmuxConfig } from "../../../config/schema"
|
||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||
import type { SpawnPaneResult } from "../types"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "bun"
|
||||
import { spawn } from "../bun-spawn-shim"
|
||||
|
||||
import type { ArchiveEntry } from "../archive-entry-validator"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn, spawnSync } from "bun"
|
||||
import { spawn, spawnSync } from "../bun-spawn-shim"
|
||||
|
||||
import type { ArchiveEntry } from "../archive-entry-validator"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "bun"
|
||||
import { spawn } from "../bun-spawn-shim"
|
||||
|
||||
export async function readZipSymlinkTarget(
|
||||
archivePath: string,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "bun"
|
||||
import { spawn } from "../bun-spawn-shim"
|
||||
|
||||
import type { ArchiveEntry } from "../archive-entry-validator"
|
||||
import { log } from "../logger"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn, spawnSync } from "bun"
|
||||
import { spawn, spawnSync } from "../bun-spawn-shim"
|
||||
|
||||
import type { ArchiveEntry } from "../archive-entry-validator"
|
||||
import { readZipSymlinkTarget } from "./read-zip-symlink-target"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn, spawnSync } from "bun"
|
||||
import { spawn, spawnSync } from "./bun-spawn-shim"
|
||||
import { release } from "os"
|
||||
|
||||
import { validateArchiveEntries } from "./archive-entry-validator"
|
||||
|
||||
Reference in New Issue
Block a user