Merge pull request #3798 from code-yeongyu/fix/node-runtime-compat
fix(bun-spawn-shim): eliminate globalThis.Bun top-level destructures (#3797)
This commit is contained in:
+2
-1
@@ -22,7 +22,8 @@
|
|||||||
"./schema.json": "./dist/oh-my-opencode.schema.json"
|
"./schema.json": "./dist/oh-my-opencode.schema.json"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi --external zod && tsc --emitDeclarationOnly && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi && bun run build:schema",
|
"build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi --external zod && bun run build:node-require-shim && tsc --emitDeclarationOnly && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi && bun run build:schema",
|
||||||
|
"build:node-require-shim": "bun run script/patch-node-require-shim.ts",
|
||||||
"build:all": "bun run build && bun run build:binaries",
|
"build:all": "bun run build && bun run build:binaries",
|
||||||
"build:binaries": "bun run script/build-binaries.ts",
|
"build:binaries": "bun run script/build-binaries.ts",
|
||||||
"build:schema": "bun run script/build-schema.ts",
|
"build:schema": "bun run script/build-schema.ts",
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs"
|
||||||
|
import { dirname, join } from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
|
||||||
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const DIST_PATH = join(SCRIPT_DIR, "..", "dist", "index.js")
|
||||||
|
const IMPORT_LINE = 'import { createRequire as __omoCreateRequire } from "node:module";'
|
||||||
|
const BUN_REQUIRE_LINE = "var __require = import.meta.require;"
|
||||||
|
const NODE_SAFE_REQUIRE_LINE = 'var __require = typeof import.meta.require === "function" ? import.meta.require : __omoCreateRequire(import.meta.url);'
|
||||||
|
|
||||||
|
const original = readFileSync(DIST_PATH, "utf-8")
|
||||||
|
|
||||||
|
if (original.includes(NODE_SAFE_REQUIRE_LINE)) {
|
||||||
|
console.log("Node/Electron require shim already present in dist/index.js, skipping.")
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!original.includes(BUN_REQUIRE_LINE)) {
|
||||||
|
throw new Error(`Expected Bun require helper not found in ${DIST_PATH}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const patched = original.replace(BUN_REQUIRE_LINE, `${IMPORT_LINE}\n${NODE_SAFE_REQUIRE_LINE}`)
|
||||||
|
|
||||||
|
writeFileSync(DIST_PATH, patched, "utf-8")
|
||||||
|
console.log("Patched Node/Electron require shim in dist/index.js")
|
||||||
@@ -2,9 +2,10 @@ import fs from "node:fs/promises"
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
|
||||||
import type { TeamModeConfig } from "./manager"
|
import type { TeamModeConfig } from "./manager"
|
||||||
|
import { spawn as bunSpawn } from "../../../shared/bun-spawn-shim"
|
||||||
|
|
||||||
async function runGit(args: string[]): Promise<{ code: number; stderr: string }> {
|
async function runGit(args: string[]): Promise<{ code: number; stderr: string }> {
|
||||||
const process = Bun.spawn({ cmd: ["git", ...args], stdout: "pipe", stderr: "pipe" })
|
const process = bunSpawn({ cmd: ["git", ...args], stdout: "pipe", stderr: "pipe" })
|
||||||
const [exitCode, stderrText] = await Promise.all([process.exited, new Response(process.stderr).text()])
|
const [exitCode, stderrText] = await Promise.all([process.exited, new Response(process.stderr).text()])
|
||||||
return { code: exitCode, stderr: stderrText }
|
return { code: exitCode, stderr: stderrText }
|
||||||
}
|
}
|
||||||
@@ -12,7 +13,7 @@ async function runGit(args: string[]): Promise<{ code: number; stderr: string }>
|
|||||||
export async function removeWorktree(worktreePath: string): Promise<void> {
|
export async function removeWorktree(worktreePath: string): Promise<void> {
|
||||||
await fs.rm(worktreePath, { recursive: true, force: true })
|
await fs.rm(worktreePath, { recursive: true, force: true })
|
||||||
|
|
||||||
const rootLookup = await Bun.spawn({
|
const rootLookup = bunSpawn({
|
||||||
cmd: ["git", "-C", worktreePath, "rev-parse", "--show-superproject-working-tree"],
|
cmd: ["git", "-C", worktreePath, "rev-parse", "--show-superproject-working-tree"],
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { spawn as bunSpawn } from "../../../shared/bun-spawn-shim"
|
||||||
|
|
||||||
export type TeamModeConfig = {
|
export type TeamModeConfig = {
|
||||||
worktreeBaseDir?: string
|
worktreeBaseDir?: string
|
||||||
@@ -16,7 +17,7 @@ function countParentSegments(spec: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runGit(args: string[], cwd?: string): Promise<{ code: number; stderr: string }> {
|
async function runGit(args: string[], cwd?: string): Promise<{ code: number; stderr: string }> {
|
||||||
const process = Bun.spawn({ cmd: ["git", ...args], cwd, stdout: "pipe", stderr: "pipe" })
|
const process = bunSpawn({ cmd: ["git", ...args], cwd, stdout: "pipe", stderr: "pipe" })
|
||||||
const [exitCode, stderrBytes] = await Promise.all([process.exited, new Response(process.stderr).text()])
|
const [exitCode, stderrBytes] = await Promise.all([process.exited, new Response(process.stderr).text()])
|
||||||
return { code: exitCode, stderr: stderrBytes }
|
return { code: exitCode, stderr: stderrBytes }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../../shared/bun-spawn-shim"
|
||||||
import type { WindowState, TmuxPaneInfo } from "./types"
|
import type { WindowState, TmuxPaneInfo } from "./types"
|
||||||
import { parsePaneStateOutput } from "./pane-state-parser"
|
import { parsePaneStateOutput } from "./pane-state-parser"
|
||||||
import { getTmuxPath } from "../../tools/interactive-bash/tmux-path-resolver"
|
import { getTmuxPath } from "../../tools/interactive-bash/tmux-path-resolver"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../../shared/bun-spawn-shim"
|
||||||
import { createRequire } from "module"
|
import { createRequire } from "module"
|
||||||
import { dirname, join } from "path"
|
import { dirname, join } from "path"
|
||||||
import { existsSync } from "fs"
|
import { existsSync } from "fs"
|
||||||
|
|||||||
@@ -33,6 +33,21 @@ export function getDefaultSoundPath(platform: Platform): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ShellCommand = Promise<unknown> & {
|
||||||
|
quiet?: () => Promise<unknown>
|
||||||
|
nothrow?: () => ShellCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runQuietNothrow(command: ShellCommand): Promise<void> {
|
||||||
|
const safeCommand = typeof command.nothrow === "function" ? command.nothrow() : command
|
||||||
|
if (typeof safeCommand.quiet === "function") {
|
||||||
|
await safeCommand.quiet()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await safeCommand
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendSessionNotification(
|
export async function sendSessionNotification(
|
||||||
ctx: PluginInput,
|
ctx: PluginInput,
|
||||||
platform: Platform,
|
platform: Platform,
|
||||||
@@ -72,14 +87,14 @@ export async function sendSessionNotification(
|
|||||||
|
|
||||||
const escapedTitle = escapeAppleScriptText(title)
|
const escapedTitle = escapeAppleScriptText(title)
|
||||||
const escapedMessage = escapeAppleScriptText(message)
|
const escapedMessage = escapeAppleScriptText(message)
|
||||||
await ctx.$`${osascriptPath} -e ${"display notification \"" + escapedMessage + "\" with title \"" + escapedTitle + "\""}`.nothrow().quiet()
|
await runQuietNothrow(ctx.$`${osascriptPath} -e ${"display notification \"" + escapedMessage + "\" with title \"" + escapedTitle + "\""}`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "linux": {
|
case "linux": {
|
||||||
const notifySendPath = await getNotifySendPath()
|
const notifySendPath = await getNotifySendPath()
|
||||||
if (!notifySendPath) return
|
if (!notifySendPath) return
|
||||||
|
|
||||||
await ctx.$`${notifySendPath} ${title} ${message} 2>/dev/null`.nothrow().quiet()
|
await runQuietNothrow(ctx.$`${notifySendPath} ${title} ${message} 2>/dev/null`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "win32": {
|
case "win32": {
|
||||||
@@ -87,7 +102,7 @@ export async function sendSessionNotification(
|
|||||||
if (!powershellPath) return
|
if (!powershellPath) return
|
||||||
|
|
||||||
const toastScript = buildWindowsToastScript(title, message)
|
const toastScript = buildWindowsToastScript(title, message)
|
||||||
await ctx.$`${powershellPath} -Command ${toastScript}`.nothrow().quiet()
|
await runQuietNothrow(ctx.$`${powershellPath} -Command ${toastScript}`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,17 +117,17 @@ export async function playSessionNotificationSound(
|
|||||||
case "darwin": {
|
case "darwin": {
|
||||||
const afplayPath = await getAfplayPath()
|
const afplayPath = await getAfplayPath()
|
||||||
if (!afplayPath) return
|
if (!afplayPath) return
|
||||||
ctx.$`${afplayPath} ${soundPath}`.nothrow().quiet()
|
await runQuietNothrow(ctx.$`${afplayPath} ${soundPath}`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "linux": {
|
case "linux": {
|
||||||
const paplayPath = await getPaplayPath()
|
const paplayPath = await getPaplayPath()
|
||||||
if (paplayPath) {
|
if (paplayPath) {
|
||||||
ctx.$`${paplayPath} ${soundPath} 2>/dev/null`.nothrow().quiet()
|
await runQuietNothrow(ctx.$`${paplayPath} ${soundPath} 2>/dev/null`)
|
||||||
} else {
|
} else {
|
||||||
const aplayPath = await getAplayPath()
|
const aplayPath = await getAplayPath()
|
||||||
if (aplayPath) {
|
if (aplayPath) {
|
||||||
ctx.$`${aplayPath} ${soundPath} 2>/dev/null`.nothrow().quiet()
|
await runQuietNothrow(ctx.$`${aplayPath} ${soundPath} 2>/dev/null`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -121,7 +136,7 @@ export async function playSessionNotificationSound(
|
|||||||
const powershellPath = await getPowershellPath()
|
const powershellPath = await getPowershellPath()
|
||||||
if (!powershellPath) return
|
if (!powershellPath) return
|
||||||
const escaped = escapePowerShellSingleQuotedText(soundPath)
|
const escaped = escapePowerShellSingleQuotedText(soundPath)
|
||||||
ctx.$`${powershellPath} -Command ${"(New-Object Media.SoundPlayer '" + escaped + "').PlaySync()"}`.nothrow().quiet()
|
await runQuietNothrow(ctx.$`${powershellPath} -Command ${"(New-Object Media.SoundPlayer '" + escaped + "').PlaySync()"}`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../shared/bun-spawn-shim"
|
||||||
import { validateGatewayUrl } from "./gateway-url-validation"
|
import { validateGatewayUrl } from "./gateway-url-validation"
|
||||||
import type { OpenClawGateway, WakeResult } from "./types"
|
import type { OpenClawGateway, WakeResult } from "./types"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { readFileSync } from "fs"
|
import { readFileSync } from "fs"
|
||||||
import { spawn } from "bun"
|
import { spawn } from "../shared/bun-spawn-shim"
|
||||||
|
|
||||||
export const REPLY_LISTENER_DAEMON_IDENTITY_MARKER = "--openclaw-reply-listener-daemon"
|
export const REPLY_LISTENER_DAEMON_IDENTITY_MARKER = "--openclaw-reply-listener-daemon"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../shared/bun-spawn-shim"
|
||||||
import {
|
import {
|
||||||
createReplyListenerDaemonEnv,
|
createReplyListenerDaemonEnv,
|
||||||
REPLY_LISTENER_DAEMON_IDENTITY_MARKER,
|
REPLY_LISTENER_DAEMON_IDENTITY_MARKER,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../shared/bun-spawn-shim"
|
||||||
|
|
||||||
export function getCurrentTmuxSession(): string | null {
|
export function getCurrentTmuxSession(): string | null {
|
||||||
const env = process.env.TMUX
|
const env = process.env.TMUX
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
||||||
import * as path from "node:path";
|
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 { validateArchiveEntries, type ArchiveEntry } from "./archive-entry-validator";
|
||||||
import { extractZip } from "./zip-extractor";
|
import { extractZip } from "./zip-extractor";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
|
import { spawn, spawnSync } from "./bun-spawn-shim"
|
||||||
|
|
||||||
|
describe("bun-spawn-shim", () => {
|
||||||
|
test("#given array command #when spawn exits successfully #then exited resolves to zero", async () => {
|
||||||
|
const proc = spawn(["bun", "--version"], { stdout: "pipe", stderr: "pipe" })
|
||||||
|
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0)
|
||||||
|
expect(proc.exitCode).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given piped stdout #when spawn writes output #then stdout is readable", async () => {
|
||||||
|
const proc = spawn(["bun", "--print", "'shim-ok'"], { stdout: "pipe", stderr: "pipe" })
|
||||||
|
|
||||||
|
const [exitCode, stdout] = await Promise.all([
|
||||||
|
proc.exited,
|
||||||
|
new Response(proc.stdout).text(),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0)
|
||||||
|
expect(stdout.trim()).toBe("shim-ok")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given detached object command #when spawn starts #then process exposes daemon controls", async () => {
|
||||||
|
const proc = spawn({
|
||||||
|
cmd: ["bun", "--print", "'detached-ok'"],
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
detached: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.unref()
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0)
|
||||||
|
expect(typeof proc.ref).toBe("function")
|
||||||
|
expect(typeof proc.unref).toBe("function")
|
||||||
|
expect(proc.pid).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given stdio tuple #when spawn runs #then ignored streams are still safe to read", async () => {
|
||||||
|
const proc = spawn({
|
||||||
|
cmd: ["bun", "--print", "'ignored'"],
|
||||||
|
stdio: ["ignore", "ignore", "ignore"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
const stdout = await new Response(proc.stdout).text()
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0)
|
||||||
|
expect(stdout).toBe("")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given spawnSync command #when it writes output #then stdout and exit code match", () => {
|
||||||
|
const result = spawnSync(["bun", "--print", "'sync-ok'"], { stdout: "pipe", stderr: "pipe" })
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.stdout).toBeDefined()
|
||||||
|
expect(Buffer.from(result.stdout!).toString().trim()).toBe("sync-ok")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given spawnSync command #when it completes #then result.pid is a positive number", () => {
|
||||||
|
const result = spawnSync(["bun", "--version"], { stdout: "pipe", stderr: "pipe" })
|
||||||
|
|
||||||
|
expect(result.pid).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given default stdio #when child reads stdin #then it does not hang waiting for input", async () => {
|
||||||
|
const proc = spawn(["cat"], { stdout: "pipe", stderr: "pipe" })
|
||||||
|
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given missing executable #when spawn invoked #then the error is surfaced to the caller", async () => {
|
||||||
|
let observedError: unknown
|
||||||
|
try {
|
||||||
|
const proc = spawn(["__omo-shim-missing-binary__"], { stdout: "pipe", stderr: "pipe" })
|
||||||
|
await proc.exited
|
||||||
|
} catch (error) {
|
||||||
|
observedError = error
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(observedError).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process"
|
||||||
|
import { Readable, Writable } from "node:stream"
|
||||||
|
|
||||||
|
type AnyRecord = Record<string, unknown>
|
||||||
|
type StdioMode = "pipe" | "inherit" | "ignore"
|
||||||
|
type StdioTuple = [StdioMode, StdioMode, StdioMode]
|
||||||
|
|
||||||
|
export interface SpawnOptions {
|
||||||
|
cmd?: string[]
|
||||||
|
cwd?: string
|
||||||
|
env?: NodeJS.ProcessEnv
|
||||||
|
stdin?: StdioMode
|
||||||
|
stdout?: StdioMode
|
||||||
|
stderr?: StdioMode
|
||||||
|
stdio?: StdioTuple
|
||||||
|
detached?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpawnedProcess {
|
||||||
|
readonly exitCode: number | null
|
||||||
|
readonly exited: Promise<number>
|
||||||
|
readonly stdout: ReadableStream<Uint8Array>
|
||||||
|
readonly stderr: ReadableStream<Uint8Array>
|
||||||
|
readonly stdin: NodeJS.WritableStream
|
||||||
|
readonly pid: number | undefined
|
||||||
|
kill(signal?: NodeJS.Signals): void
|
||||||
|
ref(): void
|
||||||
|
unref(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpawnSyncResult {
|
||||||
|
readonly exitCode: number
|
||||||
|
readonly stdout: Buffer | undefined
|
||||||
|
readonly stderr: Buffer | undefined
|
||||||
|
readonly success: boolean
|
||||||
|
readonly pid: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type BunSpawnRuntime = {
|
||||||
|
spawn(command: string[], options?: SpawnOptions): SpawnedProcess
|
||||||
|
spawn(options: SpawnOptions & { cmd: string[] }): SpawnedProcess
|
||||||
|
spawnSync(command: string[], options?: SpawnOptions): SpawnSyncResult
|
||||||
|
spawnSync(options: SpawnOptions & { cmd: string[] }): SpawnSyncResult
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtime = globalThis as typeof globalThis & { Bun?: BunSpawnRuntime }
|
||||||
|
const IS_BUN = typeof runtime.Bun !== "undefined"
|
||||||
|
|
||||||
|
function emptyReadableStream(): ReadableStream<Uint8Array> {
|
||||||
|
return new ReadableStream<Uint8Array>({
|
||||||
|
start(controller) {
|
||||||
|
controller.close()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toReadableStream(stream: NodeJS.ReadableStream | null): ReadableStream<Uint8Array> {
|
||||||
|
if (!stream) return emptyReadableStream()
|
||||||
|
|
||||||
|
return Readable.toWeb(stream as Readable) as ReadableStream<Uint8Array>
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyWritableStream(): Writable {
|
||||||
|
return new Writable({
|
||||||
|
write(_chunk, _encoding, callback) {
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCommand(cmdOrOpts: unknown, optsArg?: unknown): { cmd: string[]; opts: SpawnOptions } {
|
||||||
|
const isObj = !Array.isArray(cmdOrOpts)
|
||||||
|
const opts = isObj ? (cmdOrOpts as SpawnOptions) : ((optsArg ?? {}) as SpawnOptions)
|
||||||
|
|
||||||
|
return {
|
||||||
|
cmd: isObj ? ((cmdOrOpts as AnyRecord).cmd as string[]) : (cmdOrOpts as string[]),
|
||||||
|
opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStdio(options: SpawnOptions): StdioTuple {
|
||||||
|
if (options.stdio) return options.stdio
|
||||||
|
|
||||||
|
return [options.stdin ?? "ignore", options.stdout ?? "pipe", options.stderr ?? "inherit"]
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapNodeProcess(proc: ReturnType<typeof nodeSpawn>): SpawnedProcess {
|
||||||
|
let exitCode: number | null = null
|
||||||
|
const exited = new Promise<number>((resolve, reject) => {
|
||||||
|
proc.on("exit", (code) => {
|
||||||
|
exitCode = code ?? 1
|
||||||
|
resolve(exitCode)
|
||||||
|
})
|
||||||
|
proc.on("error", (error) => {
|
||||||
|
if (exitCode === null) {
|
||||||
|
exitCode = 1
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
get exitCode() {
|
||||||
|
return exitCode
|
||||||
|
},
|
||||||
|
exited,
|
||||||
|
stdout: toReadableStream(proc.stdout),
|
||||||
|
stderr: toReadableStream(proc.stderr),
|
||||||
|
stdin: proc.stdin ?? emptyWritableStream(),
|
||||||
|
kill(signal?: NodeJS.Signals) {
|
||||||
|
if (proc.killed || exitCode !== null) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
proc.kill(signal)
|
||||||
|
} catch (error) {
|
||||||
|
if (!String(error).includes("kill")) throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pid: proc.pid,
|
||||||
|
ref() {
|
||||||
|
proc.ref()
|
||||||
|
},
|
||||||
|
unref() {
|
||||||
|
proc.unref()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawn(command: string[], options?: SpawnOptions): SpawnedProcess
|
||||||
|
export function spawn(options: SpawnOptions & { cmd: string[] }): SpawnedProcess
|
||||||
|
export function spawn(cmdOrOpts: unknown, opts?: unknown): SpawnedProcess {
|
||||||
|
if (IS_BUN) return runtime.Bun!.spawn(cmdOrOpts as string[] & SpawnOptions & { cmd: string[] }, opts as SpawnOptions)
|
||||||
|
|
||||||
|
const { cmd, opts: options } = resolveCommand(cmdOrOpts, opts)
|
||||||
|
const [bin, ...args] = cmd
|
||||||
|
const proc = nodeSpawn(bin, args, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
env: options.env,
|
||||||
|
stdio: resolveStdio(options),
|
||||||
|
detached: options.detached,
|
||||||
|
})
|
||||||
|
|
||||||
|
return wrapNodeProcess(proc)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnSync(command: string[], options?: SpawnOptions): SpawnSyncResult
|
||||||
|
export function spawnSync(options: SpawnOptions & { cmd: string[] }): SpawnSyncResult
|
||||||
|
export function spawnSync(cmdOrOpts: unknown, opts?: unknown): SpawnSyncResult {
|
||||||
|
if (IS_BUN) return runtime.Bun!.spawnSync(cmdOrOpts as string[] & SpawnOptions & { cmd: string[] }, opts as SpawnOptions)
|
||||||
|
|
||||||
|
const { cmd, opts: options } = resolveCommand(cmdOrOpts, opts)
|
||||||
|
const [bin, ...args] = cmd
|
||||||
|
const result = nodeSpawnSync(bin, args, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
env: options.env,
|
||||||
|
stdio: resolveStdio(options),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
exitCode: result.status ?? 1,
|
||||||
|
stdout: result.stdout ?? undefined,
|
||||||
|
stderr: result.stderr ?? undefined,
|
||||||
|
success: (result.status ?? 1) === 0,
|
||||||
|
pid: result.pid ?? -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { existsSync } from "node:fs"
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
|
const DIST_INDEX = "dist/index.js"
|
||||||
|
const GLOBAL_BUN_DESTRUCTURE = /^\s*(?:var|let|const)\s*\{[^}]*\}\s*=\s*globalThis\.Bun/gm
|
||||||
|
const TOP_LEVEL_REQUIRE_CALL = "__require("
|
||||||
|
|
||||||
|
describe("dist bundle Bun globals", () => {
|
||||||
|
test.skipIf(!existsSync(DIST_INDEX))("#given dist bundle #when scanned #then no globalThis.Bun destructures remain", async () => {
|
||||||
|
const dist = await Bun.file(DIST_INDEX).text()
|
||||||
|
|
||||||
|
const matches = dist.match(GLOBAL_BUN_DESTRUCTURE) ?? []
|
||||||
|
|
||||||
|
expect(matches).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test.skipIf(!existsSync(DIST_INDEX))("#given dist bundle #when scanned #then no top-level __require call remains", async () => {
|
||||||
|
const dist = await Bun.file(DIST_INDEX).text()
|
||||||
|
const offending: string[] = []
|
||||||
|
let depth = 0
|
||||||
|
|
||||||
|
for (const [index, line] of dist.split("\n").entries()) {
|
||||||
|
if (depth === 0 && line.includes(TOP_LEVEL_REQUIRE_CALL)) {
|
||||||
|
offending.push(`${index + 1}: ${line.trim()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const char of line) {
|
||||||
|
if (char === "{") {
|
||||||
|
depth += 1
|
||||||
|
} else if (char === "}") {
|
||||||
|
depth -= 1
|
||||||
|
if (depth < 0) depth = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(offending).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test.skipIf(!existsSync(DIST_INDEX))("#given dist bundle #when imported under node --input-type=module #then it loads without error", async () => {
|
||||||
|
const node = Bun.which("node")
|
||||||
|
if (!node) return
|
||||||
|
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: [node, "--input-type=module", "-e", "await import('./dist/index.js'); console.log('node-esm-load-ok')"],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
|
||||||
|
const stdout = await new Response(proc.stdout).text()
|
||||||
|
const stderr = await new Response(proc.stderr).text()
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
|
||||||
|
expect({
|
||||||
|
exitCode,
|
||||||
|
stdout: stdout.trim(),
|
||||||
|
stderr: stderr.trim(),
|
||||||
|
}).toEqual({
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: "node-esm-load-ok",
|
||||||
|
stderr: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 { spawn as nodeSpawn, type ChildProcess } from "node:child_process"
|
||||||
import { Readable } from "node:stream"
|
import { Readable } from "node:stream"
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export function spawnWithWindowsHide(command: string[], options: SpawnOptions):
|
|||||||
const proc = nodeSpawn(cmd, args, {
|
const proc = nodeSpawn(cmd, args, {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
env: options.env,
|
env: options.env,
|
||||||
stdio: [options.stdin ?? "pipe", options.stdout ?? "pipe", options.stderr ?? "pipe"],
|
stdio: [options.stdin ?? "ignore", options.stdout ?? "pipe", options.stderr ?? "inherit"],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
shell: true,
|
shell: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../../bun-spawn-shim"
|
||||||
import type { TmuxLayout } from "../../../config/schema"
|
import type { TmuxLayout } from "../../../config/schema"
|
||||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
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"
|
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||||
|
|
||||||
export interface PaneDimensions {
|
export interface PaneDimensions {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../../bun-spawn-shim"
|
||||||
import type { TmuxConfig } from "../../../config/schema"
|
import type { TmuxConfig } from "../../../config/schema"
|
||||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||||
import type { SpawnPaneResult } from "../types"
|
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 type { TmuxConfig } from "../../../config/schema"
|
||||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||||
import type { SpawnPaneResult } from "../types"
|
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 type { TmuxConfig } from "../../../config/schema"
|
||||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||||
import type { SpawnPaneResult } from "../types"
|
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 type { TmuxConfig } from "../../../config/schema"
|
||||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||||
import type { SpawnPaneResult } from "../types"
|
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"
|
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"
|
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(
|
export async function readZipSymlinkTarget(
|
||||||
archivePath: string,
|
archivePath: string,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../bun-spawn-shim"
|
||||||
|
|
||||||
import type { ArchiveEntry } from "../archive-entry-validator"
|
import type { ArchiveEntry } from "../archive-entry-validator"
|
||||||
import { log } from "../logger"
|
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 type { ArchiveEntry } from "../archive-entry-validator"
|
||||||
import { readZipSymlinkTarget } from "./read-zip-symlink-target"
|
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 { release } from "os"
|
||||||
|
|
||||||
import { validateArchiveEntries } from "./archive-entry-validator"
|
import { validateArchiveEntries } from "./archive-entry-validator"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../../shared/bun-spawn-shim"
|
||||||
import { existsSync } from "fs"
|
import { existsSync } from "fs"
|
||||||
import {
|
import {
|
||||||
getSgCliPath,
|
getSgCliPath,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolve } from "node:path"
|
import { resolve } from "node:path"
|
||||||
import { spawn } from "bun"
|
import { spawn } from "../../shared/bun-spawn-shim"
|
||||||
import {
|
import {
|
||||||
resolveGrepCli,
|
resolveGrepCli,
|
||||||
type GrepBackend,
|
type GrepBackend,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../../shared/bun-spawn-shim"
|
||||||
import {
|
import {
|
||||||
resolveGrepCli,
|
resolveGrepCli,
|
||||||
type ResolvedCli,
|
type ResolvedCli,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
|
import { spawn as bunSpawn } from "../../shared/bun-spawn-shim"
|
||||||
|
|
||||||
interface FormatterConfig {
|
interface FormatterConfig {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -106,7 +107,7 @@ export async function runFormattersForFile(
|
|||||||
const cmd = buildFormatterCommand(formatter.command, filePath)
|
const cmd = buildFormatterCommand(formatter.command, filePath)
|
||||||
try {
|
try {
|
||||||
log("[formatter-trigger] Running formatter", { command: cmd, file: filePath })
|
log("[formatter-trigger] Running formatter", { command: cmd, file: filePath })
|
||||||
const proc = Bun.spawn(cmd, {
|
const proc = bunSpawn(cmd, {
|
||||||
cwd: directory,
|
cwd: directory,
|
||||||
env: { ...process.env, ...formatter.environment },
|
env: { ...process.env, ...formatter.environment },
|
||||||
stdout: "ignore",
|
stdout: "ignore",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../../shared/bun-spawn-shim"
|
||||||
|
|
||||||
let tmuxPath: string | null = null
|
let tmuxPath: string | null = null
|
||||||
let initPromise: Promise<string | null> | null = null
|
let initPromise: Promise<string | null> | null = null
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn as bunSpawn } from "bun"
|
import { spawn as bunSpawn } from "../../shared/bun-spawn-shim"
|
||||||
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process"
|
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process"
|
||||||
import { existsSync, statSync } from "fs"
|
import { existsSync, statSync } from "fs"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
|
|||||||
Reference in New Issue
Block a user