Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/vinext/src/server/server-globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ const PHASE_PRODUCTION_BUILD = "phase-production-build";

type BrowserGlobalName = "window" | "document";

type ConsoleTaskLike = { name: string; run: <T>(fn: () => T) => T };

type ConsoleWithCreateTask = typeof console & {
createTask?: unknown;
};

function clearBrowserGlobal(name: BrowserGlobalName): void {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, name);

Expand All @@ -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");
Expand All @@ -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", {
Expand Down
59 changes: 58 additions & 1 deletion tests/edge-globals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -165,3 +165,60 @@ describe("edge runtime globals", () => {
}
});
});

type TaskLike = { name: string; run: <T>(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<string, unknown>;
const saved = consoleWithoutCreateTask["createTask"];
delete consoleWithoutCreateTask["createTask"];

try {
installServerGlobals();
expect("createTask" in (console as object)).toBe(false);
} finally {
consoleWithoutCreateTask["createTask"] = saved;
}
});
});
Loading