diff --git a/src/presets/bun/runtime/bun.ts b/src/presets/bun/runtime/bun.ts index f9983c0c7e..04975f8461 100644 --- a/src/presets/bun/runtime/bun.ts +++ b/src/presets/bun/runtime/bun.ts @@ -1,4 +1,32 @@ 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; +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); + } + } +} + +// @ts-expect-error -- intentional global override for compat +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..c78461eb78 --- /dev/null +++ b/test/fixture/server/routes/bun-direct-stream.ts @@ -0,0 +1,15 @@ +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, + }; +}; diff --git a/test/presets/bun.test.ts b/test/presets/bun.test.ts index 1b5bbe556e..df25396498 100644 --- a/test/presets/bun.test.ts +++ b/test/presets/bun.test.ts @@ -1,7 +1,7 @@ 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; @@ -25,5 +25,10 @@ describe.runIf(hasBun)("nitro:preset:bun", async () => { 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); + }); }); });