Skip to content
Merged
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
7 changes: 7 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/lib/db/adapters/runtimeRequire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/turbopack-runtime-boundaries-12074.test.ts
Original file line number Diff line number Diff line change
@@ -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\(/);
});