From 8d8843d1e94eebb05c61b810acd11e40c8bd4d7a Mon Sep 17 00:00:00 2001 From: Deepak kudi Date: Thu, 4 Jun 2026 11:25:02 +0530 Subject: [PATCH] fix(vite): support Bun-only dev server --- src/build/vite/env.ts | 20 +++++++++----- src/dev/server.ts | 10 ++++++- src/runtime/internal/vite/dev-worker.mjs | 32 +++++++++++++++++++--- test/unit/vite-dev-worker.test.ts | 34 ++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 test/unit/vite-dev-worker.test.ts diff --git a/src/build/vite/env.ts b/src/build/vite/env.ts index 46dbf92032..26fc25364f 100644 --- a/src/build/vite/env.ts +++ b/src/build/vite/env.ts @@ -167,9 +167,7 @@ export async function reloadEnvRunner(ctx: NitroPluginContext) { } async function _loadRunner(ctx: NitroPluginContext, manager: RunnerManager) { - const runnerName = (ctx.nitro!.options.devServer.runner || - process.env.NITRO_DEV_RUNNER || - "node-worker") as RunnerName; + const runnerName = getDevRunnerName(ctx); const entry = resolve(runtimeDir, "internal/vite/dev-worker.mjs"); let runner; if (runnerName === "miniflare") { @@ -233,9 +231,7 @@ export function nitroServiceProxy(): VitePlugin { // workerd-based runners (miniflare) cannot handle CJS externals via import(), // so all dependencies must be processed through Vite's transform pipeline. function _isWorkerdRunner(ctx: NitroPluginContext): boolean { - const runnerName = - ctx.nitro!.options.devServer.runner || process.env.NITRO_DEV_RUNNER || "node-worker"; - return runnerName === "miniflare"; + return getDevRunnerName(ctx) === "miniflare"; } function tryResolve(id: string) { @@ -249,3 +245,15 @@ function tryResolve(id: string) { }); return resolved || id; } + +function getDevRunnerName(ctx: NitroPluginContext): RunnerName { + return (ctx.nitro!.options.devServer.runner || + process.env.NITRO_DEV_RUNNER || + getDefaultDevRunnerName()) as RunnerName; +} + +function getDefaultDevRunnerName(): RunnerName { + return typeof (globalThis as typeof globalThis & { Bun?: unknown }).Bun === "undefined" + ? "node-worker" + : "bun-process"; +} diff --git a/src/dev/server.ts b/src/dev/server.ts index a6b5e33d87..f151522c5b 100644 --- a/src/dev/server.ts +++ b/src/dev/server.ts @@ -176,7 +176,9 @@ export class NitroDevServer extends NitroDevApp implements RunnerRPCHooks { async #reload() { const runnerName = - this.nitro.options.devServer.runner || process.env.NITRO_DEV_RUNNER || "node-worker"; + this.nitro.options.devServer.runner || + process.env.NITRO_DEV_RUNNER || + getDefaultDevRunnerName(); const runner = await loadRunner(runnerName as RunnerName, { name: `Nitro_${this.#workerIdCtr++}`, data: { entry: this.#entry, ...this.#workerData }, @@ -248,3 +250,9 @@ export class NitroDevServer extends NitroDevApp implements RunnerRPCHooks { // #endregion } + +function getDefaultDevRunnerName(): RunnerName { + return typeof (globalThis as typeof globalThis & { Bun?: unknown }).Bun === "undefined" + ? "node-worker" + : "bun-process"; +} diff --git a/src/runtime/internal/vite/dev-worker.mjs b/src/runtime/internal/vite/dev-worker.mjs index 0cf82a1736..f91d20747e 100644 --- a/src/runtime/internal/vite/dev-worker.mjs +++ b/src/runtime/internal/vite/dev-worker.mjs @@ -148,13 +148,15 @@ globalThis.__transform_html__ = async function (html) { // ----- Exports (env-runner AppEntry) ----- -export function fetch(req) { +export async function fetch(req) { const viteEnv = req?.headers.get("x-vite-env") || "nitro"; - const env = envs[viteEnv]; + const env = await waitForViteEnv(viteEnv); if (!env) { - return renderError(req, httpError(500, `Unknown vite environment "${viteEnv}"`)); + return normalizeBunResponse( + await renderError(req, httpError(500, `Unknown vite environment "${viteEnv}"`)) + ); } - return env.fetch(req); + return normalizeBunResponse(await env.fetch(req)); } export function upgrade(context) { @@ -211,6 +213,28 @@ function httpError(status, message) { return error; } +async function waitForViteEnv(name) { + let env = envs[name]; + for (let i = 0; i < 5 && !env; i++) { + await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, i))); + env = envs[name]; + } + return env; +} + +export function normalizeBunResponse(response) { + if (typeof Bun === "undefined") { + return response; + } + const headers = new Headers(response.headers); + headers.delete("content-length"); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + async function renderError(req, error) { if (req.headers.get("accept")?.includes("application/json")) { return new Response( diff --git a/test/unit/vite-dev-worker.test.ts b/test/unit/vite-dev-worker.test.ts new file mode 100644 index 0000000000..469d811565 --- /dev/null +++ b/test/unit/vite-dev-worker.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from "vitest"; + +const { normalizeBunResponse } = await import("../../src/runtime/internal/vite/dev-worker.mjs"); + +describe("vite dev worker", () => { + it("leaves responses untouched outside Bun", () => { + const response = new Response("ok", { + headers: { "content-length": "2" }, + }); + + expect(normalizeBunResponse(response)).toBe(response); + }); + + it("drops explicit content length before Bun serves the response", async () => { + vi.stubGlobal("Bun", {}); + try { + const response = normalizeBunResponse( + new Response("ok", { + headers: { + "content-length": "2", + "content-type": "text/plain", + }, + }) + ); + + expect(response).toBeInstanceOf(Response); + expect(response.headers.has("content-length")).toBe(false); + expect(response.headers.get("content-type")).toBe("text/plain"); + expect(await response.text()).toBe("ok"); + } finally { + vi.unstubAllGlobals(); + } + }); +});