From f21aa406db5c06ff037f1632ecd19c297e5cf7be Mon Sep 17 00:00:00 2001 From: DoSun Date: Fri, 15 May 2026 10:31:59 +0800 Subject: [PATCH] fix: resolve serve command hanging in Node.js runtime - Add Bun.serve polyfill using Node.js http.createServer to bridge Hono fetch handler - Add Bun.semver polyfill delegating to npm semver package - Add Bun.version and Bun.env stubs - Fix childSpawn.ts to only pass -d/--feature flags for .ts/.tsx source files - Make serve() async to correctly resolve port when --port 0 (random allocation) --- build.ts | 77 +++++++++++++++++++++++++++++++++++++++- src/main.tsx | 2 +- src/server/childSpawn.ts | 13 +++---- 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/build.ts b/build.ts index c8159e0ac..fceb03b98 100644 --- a/build.ts +++ b/build.ts @@ -243,7 +243,82 @@ if (typeof globalThis.Bun === "undefined") { } return h; } - globalThis.Bun = { which, $, hash }; + // Bun.serve polyfill — bridges Hono fetch handler to Node.js http. + // Returns a Promise that resolves with the actual port after 'listening' event. + const { createServer: _createHttpServer } = await import("http"); + async function serve(options) { + return new Promise((resolve) => { + const server = _createHttpServer((req, res) => { + const host = req.headers.host || (options.hostname + ":" + (options.port || 0)); + const url = new URL(req.url || "/", "http://" + host); + const headers = new Headers(); + const rawHeaders = req.headers; + for (const key of Object.keys(rawHeaders)) { + const val = rawHeaders[key]; + if (val != null) headers.set(key, Array.isArray(val) ? val.join(", ") : val); + } + const chunks = []; + req.on("data", (c) => chunks.push(c)); + req.on("end", () => { + const hasBody = req.method !== "GET" && req.method !== "HEAD" && chunks.length > 0; + const request = new Request(url.toString(), { + method: req.method || "GET", + headers, + body: hasBody ? Buffer.concat(chunks) : undefined, + }); + Promise.resolve(options.fetch(request)) + .then((response) => { + res.statusCode = response.status; + response.headers.forEach((v, k) => res.setHeader(k, v)); + if (response.body) { + const reader = response.body.getReader(); + const pump = () => reader.read().then(({ done, value }) => { + if (done) { res.end(); return; } + res.write(value); + pump(); + }).catch(() => res.end()); + pump(); + } else { + res.end(); + } + }) + .catch((err) => { + if (!res.headersSent) res.statusCode = 500; + res.end(); + }); + }); + }); + server.once("listening", () => { + const addr = server.address(); + const actualPort = typeof addr === "object" && addr ? addr.port : options.port || 0; + resolve({ + port: actualPort, + hostname: options.hostname, + stop: () => server.close(), + }); + }); + server.listen(options.port || 0, options.hostname || "0.0.0.0"); + }); + } + // Bun.semver — delegate to npm semver package + const { createRequire: _createRequire } = await import("module"); + const _nodeRequire = _createRequire(import.meta.url); + let _semver = null; + function getSemver() { + if (!_semver) _semver = _nodeRequire("semver"); + return _semver; + } + const semverPolyfill = { + order: (a, b) => { + const r = getSemver().compare(a, b, { loose: true }); + return r > 0 ? 1 : r < 0 ? -1 : 0; + }, + satisfies: (version, range) => getSemver().satisfies(version, range, { loose: true }), + }; + // Bun.version & env stubs + const version = "1.2.0"; + const env = process.env; + globalThis.Bun = { which, $, hash, serve, semver: semverPolyfill, version, env }; } import "./cli.js" ` diff --git a/src/main.tsx b/src/main.tsx index b43794b24..dc4dcd613 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5996,7 +5996,7 @@ async function run(): Promise { }); await sessionManager.init(); - const server = startServer(config, sessionManager, eventBus); + const server = await startServer(config, sessionManager, eventBus); const actualPort = server.port ?? config.port; printBanner(config, undefined, actualPort); diff --git a/src/server/childSpawn.ts b/src/server/childSpawn.ts index 343624ddc..d32d8e245 100644 --- a/src/server/childSpawn.ts +++ b/src/server/childSpawn.ts @@ -37,12 +37,13 @@ export async function saveChildSpawnPrefix(): Promise { const { execPath, scriptArgs } = getChildSpawnArgs() let defineArgs: string[] = [] let featureArgs: string[] = [] - // Only pass --feature/define flags when running as a script (dev mode). - // In compiled standalone executable mode scriptArgs is empty, and feature - // flags are already baked in at compile time — passing --feature to the - // child process would cause "unknown option" errors. - const isScriptMode = scriptArgs.length > 0 - if (isScriptMode) { + // Only pass --feature/define flags when running TypeScript source files. + // In compiled builds (dist/cli-node.js, standalone binary) feature flags are + // already baked in at compile time — passing -d/--feature to the child process + // would cause "unknown option" errors in Node.js or standalone mode. + // Dev mode: .ts/.tsx source files need -d flags. Build: .js files don't. + const isRunningSourceFile = scriptArgs.length > 0 && (scriptArgs[0].endsWith('.tsx') || scriptArgs[0].endsWith('.ts')) + if (isRunningSourceFile) { try { const definesMod = await import('../../scripts/defines.js') as { getMacroDefines: () => Record; DEFAULT_BUILD_FEATURES: readonly string[] } const defines = definesMod.getMacroDefines()