diff --git a/src/presets/bun/runtime/bun.ts b/src/presets/bun/runtime/bun.ts index f9983c0c7e..96582343da 100644 --- a/src/presets/bun/runtime/bun.ts +++ b/src/presets/bun/runtime/bun.ts @@ -1,4 +1,28 @@ import "#nitro/virtual/polyfills"; + +// React 19's server.edge.js uses ReadableStream({ type: "direct", ... }), a +// Cloudflare Workers extension. Bun follows the web spec strictly and throws +// ERR_INVALID_ARG_VALUE for unknown `type` values. Strip it before it reaches +// Bun's constructor so prerendering works without switching to the node preset. +// Using class extends preserves the prototype chain so instanceof checks work correctly. +const _OriginalReadableStream = globalThis.ReadableStream; +// @ts-expect-error -- TypeScript cannot resolve overloaded ReadableStream constructor generics via class extends +class _PatchedReadableStream extends _OriginalReadableStream { + constructor( + underlyingSource?: UnderlyingDefaultSource | UnderlyingByteSource, + strategy?: QueuingStrategy + ) { + if (underlyingSource && (underlyingSource as Record).type === "direct") { + const { type: _type, ...rest } = underlyingSource as Record; + super(rest as UnderlyingDefaultSource, strategy); + } else { + super(underlyingSource as UnderlyingDefaultSource, strategy); + } + } +} + +globalThis.ReadableStream = _PatchedReadableStream; + import type { ServerRequest } from "srvx"; import { serve } from "srvx/bun"; import wsAdapter from "crossws/adapters/bun"; diff --git a/test/fixture/server/routes/bun-direct-stream.ts b/test/fixture/server/routes/bun-direct-stream.ts new file mode 100644 index 0000000000..0341ce47ac --- /dev/null +++ b/test/fixture/server/routes/bun-direct-stream.ts @@ -0,0 +1,16 @@ +export default () => { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + // @ts-expect-error - direct is a Cloudflare extension + type: "direct", + start(controller) { + controller.enqueue(encoder.encode("bun-direct")); + controller.close(); + }, + }); + + return { + isStream: stream instanceof ReadableStream, + hasCorrectPrototype: Object.getPrototypeOf(stream) === ReadableStream.prototype, + }; +}; diff --git a/test/presets/bun.test.ts b/test/presets/bun.test.ts index 1b5bbe556e..a3f6c5afba 100644 --- a/test/presets/bun.test.ts +++ b/test/presets/bun.test.ts @@ -1,29 +1,39 @@ import { execa, execaCommandSync } from "execa"; import { getRandomPort, waitForPort } from "get-port-please"; import { resolve } from "pathe"; -import { describe } from "vitest"; +import { describe, it, expect } from "vitest"; import { setupTest, testNitro } from "../tests.ts"; const hasBun = execaCommandSync("bun --version", { stdio: "ignore", reject: false }).exitCode === 0; describe.runIf(hasBun)("nitro:preset:bun", async () => { const ctx = await setupTest("bun"); - testNitro(ctx, async () => { - const port = await getRandomPort(); - process.env.PORT = String(port); - execa("bun", [resolve(ctx.outDir, "server/index.mjs")], { - stdio: "inherit", - }); - ctx.server = { - url: `http://127.0.0.1:${port}`, - close: () => { - // p.kill() - }, - } as any; - await waitForPort(port); - return async ({ url, ...opts }) => { - const res = await ctx.fetch(url, opts); - return res; - }; - }); + testNitro( + ctx, + async () => { + const port = await getRandomPort(); + process.env.PORT = String(port); + execa("bun", [resolve(ctx.outDir, "server/index.mjs")], { + stdio: "inherit", + }); + ctx.server = { + url: `http://127.0.0.1:${port}`, + close: () => { + // p.kill() + }, + } as any; + await waitForPort(port); + return async ({ url, ...opts }) => { + const res = await ctx.fetch(url, opts); + return res; + }; + }, + (ctx, callHandler) => { + it("bun: ReadableStream polyfill works", async () => { + const { data } = await callHandler({ url: "/bun-direct-stream" }); + expect(data.isStream).toBe(true); + expect(data.hasCorrectPrototype).toBe(true); + }); + } + ); });