diff --git a/packages/vinext/src/server/server-globals.ts b/packages/vinext/src/server/server-globals.ts index 6681f62d3..7c5cbc5e7 100644 --- a/packages/vinext/src/server/server-globals.ts +++ b/packages/vinext/src/server/server-globals.ts @@ -15,6 +15,12 @@ const PHASE_PRODUCTION_BUILD = "phase-production-build"; type BrowserGlobalName = "window" | "document"; +type ConsoleTaskLike = { name: string; run: (fn: () => T) => T }; + +type ConsoleWithCreateTask = typeof console & { + createTask?: unknown; +}; + function clearBrowserGlobal(name: BrowserGlobalName): void { const descriptor = Object.getOwnPropertyDescriptor(globalThis, name); @@ -40,6 +46,30 @@ function clearBrowserGlobal(name: BrowserGlobalName): void { } } +/** + * Fall back to a synchronous passthrough when `console.createTask` is inert. + * + * workerd's console exposes `createTask` but throws "not implemented" when it + * is called, and React's development builds call it at module initialization + * and during rendering (scheduler task tracing). The combination crashes every + * server environment on Workers, so probe the runtime implementation once and + * replace it with a passthrough when it cannot execute. A working + * implementation (Node's task tracer) is left untouched. + */ +function installCreateTaskFallback(): void { + const existing = (console as ConsoleWithCreateTask).createTask; + if (typeof existing !== "function") return; + + try { + (existing as (name: string) => ConsoleTaskLike)("vinext:createTask-probe").run(() => {}); + } catch { + (console as ConsoleWithCreateTask).createTask = (name: string): ConsoleTaskLike => ({ + name, + run: (fn) => fn(), + }); + } +} + export function installServerGlobals(): void { clearBrowserGlobal("window"); clearBrowserGlobal("document"); @@ -55,6 +85,8 @@ export function installServerGlobals(): void { }); } + installCreateTaskFallback(); + const nextPhaseDescriptor = Object.getOwnPropertyDescriptor(globalThis, "__VINEXT_NEXT_PHASE"); if (!nextPhaseDescriptor || nextPhaseDescriptor.configurable) { Object.defineProperty(globalThis, "__VINEXT_NEXT_PHASE", { diff --git a/tests/edge-globals.test.ts b/tests/edge-globals.test.ts index befd2c9dc..ff3444bfe 100644 --- a/tests/edge-globals.test.ts +++ b/tests/edge-globals.test.ts @@ -10,7 +10,7 @@ * that do `new AsyncLocalStorage()` without an import fail with * ReferenceError: AsyncLocalStorage is not defined. */ -import { describe, expect, it } from "vite-plus/test"; +import { afterEach, describe, expect, it } from "vite-plus/test"; import { AsyncLocalStorage as NodeAsyncLocalStorage } from "node:async_hooks"; import { installServerGlobals } from "../packages/vinext/src/server/server-globals.js"; @@ -165,3 +165,60 @@ describe("edge runtime globals", () => { } }); }); + +type TaskLike = { name: string; run: (fn: () => T) => T }; + +describe("console.createTask fallback", () => { + const originalDescriptor = Object.getOwnPropertyDescriptor(console, "createTask"); + + afterEach(() => { + if (originalDescriptor) { + Object.defineProperty(console, "createTask", originalDescriptor); + } + }); + + it("replaces a createTask implementation that throws when called", () => { + // workerd exposes console.createTask but throws "not implemented" on call. + Object.defineProperty(console, "createTask", { + configurable: true, + writable: true, + value: () => { + throw new Error("not implemented"); + }, + }); + + installServerGlobals(); + + const createTask = (console as unknown as { createTask: (name: string) => TaskLike }) + .createTask; + const task = createTask("render"); + expect(task.name).toBe("render"); + expect(task.run(() => 42)).toBe(42); + }); + + it("leaves a working createTask implementation untouched", () => { + const working = (name: string): TaskLike => ({ name, run: (fn) => fn() }); + Object.defineProperty(console, "createTask", { + configurable: true, + writable: true, + value: working, + }); + + installServerGlobals(); + + expect((console as { createTask: unknown }).createTask).toBe(working); + }); + + it("does not add createTask when the runtime does not expose it", () => { + const consoleWithoutCreateTask = console as unknown as Record; + const saved = consoleWithoutCreateTask["createTask"]; + delete consoleWithoutCreateTask["createTask"]; + + try { + installServerGlobals(); + expect("createTask" in (console as object)).toBe(false); + } finally { + consoleWithoutCreateTask["createTask"] = saved; + } + }); +});