diff --git a/next.config.mjs b/next.config.mjs index a1a3fd5d7bb4..d668d6bc3baa 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -286,6 +286,9 @@ const nextConfig = { // (better-sqlite3 → node:sqlite → sql.js). Next traces sql-wasm.js but can // omit the runtime sql-wasm.wasm asset from the standalone bundle. "./node_modules/sql.js/dist/sql-wasm.wasm", + // tiktoken is server-externalized below so Node selects its CommonJS entry. + // That entry reads the tokenizer WASM beside itself at runtime. + "./node_modules/tiktoken/tiktoken_bg.wasm", ], }, outputFileTracingExcludes: { @@ -339,6 +342,10 @@ const nextConfig = { "tough-cookie", "@ngrok/ngrok", "@huggingface/transformers", + // The ESM entry imports tiktoken_bg.wasm as a module. Turbopack can compile + // that graph but omits the runtime asset, making provider routes fail during + // module evaluation. Keep Node's CommonJS loader and colocated WASM intact. + "tiktoken", // copilot-m365-web.ts imports 'ws' as a client-side WebSocket. When bundled, // ws cannot resolve its 'bufferutil' native addon (frame masking) and throws // TypeError: b.mask is not a function on the first outgoing frame, causing diff --git a/src/lib/db/adapters/runtimeRequire.ts b/src/lib/db/adapters/runtimeRequire.ts index e3e3b0e1c750..ad441eaac250 100644 --- a/src/lib/db/adapters/runtimeRequire.ts +++ b/src/lib/db/adapters/runtimeRequire.ts @@ -5,13 +5,17 @@ * The standalone server is emitted as CommonJS chunks and externalizes the * native database packages. Keep those requests as static `require()` calls so * webpack preserves the external boundary. Development and tests run as ESM, - * where `require` is unavailable; the `createRequire(import.meta.url)` fallback - * handles those callers. + * where `require` is unavailable; the fallback anchors resolution to the real + * process entrypoint so Turbopack cannot replace it with an in-bundle resolver. */ import * as nodeModule from "node:module"; +const esmRequire = nodeModule.createRequire(process.argv[1] || process.cwd()); + function esmRuntimeRequire(specifier: string): unknown { - return nodeModule.createRequire(import.meta.url)(specifier); + // Reflect keeps the optional request dynamic. A direct call is rewritten by + // Turbopack and fails at runtime as "Cannot find module as expression is too dynamic". + return Reflect.apply(esmRequire, undefined, [specifier]); } export function runtimeRequire(specifier: string): unknown { diff --git a/tests/unit/turbopack-runtime-boundaries-12074.test.ts b/tests/unit/turbopack-runtime-boundaries-12074.test.ts new file mode 100644 index 000000000000..449ea78c5d6e --- /dev/null +++ b/tests/unit/turbopack-runtime-boundaries-12074.test.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { pathToFileURL } from "node:url"; + +const repoRoot = process.cwd(); + +test("#12074: Turbopack keeps the WASM-backed tiktoken package external", async () => { + const configUrl = `${pathToFileURL(path.join(repoRoot, "next.config.mjs")).href}?phase5=${Date.now()}`; + const { default: nextConfig } = await import(configUrl); + + assert.ok( + new Set(nextConfig.serverExternalPackages).has("tiktoken"), + "bundling tiktoken selects its ESM WASM entry and makes /api/providers fail with Missing tiktoken_bg.wasm" + ); + assert.ok( + nextConfig.outputFileTracingIncludes?.["/*"]?.includes( + "./node_modules/tiktoken/tiktoken_bg.wasm" + ), + "the external tokenizer must retain its runtime WASM asset in standalone output" + ); +}); + +test("#12074: the ESM SQLite loader stays outside Turbopack's module resolver", () => { + const source = fs.readFileSync( + path.join(repoRoot, "src/lib/db/adapters/runtimeRequire.ts"), + "utf8" + ); + + assert.doesNotMatch(source, /createRequire\(import\.meta\.url\)/); + assert.match(source, /createRequire\(process\.argv\[1\] \|\| process\.cwd\(\)\)/); + assert.match(source, /Reflect\.apply\(/); +});