@solcreek/adapter-creek 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build.js ADDED
@@ -0,0 +1,1382 @@
1
+ /**
2
+ * Core build handler for Creek's Next.js adapter.
3
+ *
4
+ * Called by Next.js after build completes via onBuildComplete().
5
+ * With --webpack, .next/server/ contains standard CJS that esbuild
6
+ * can bundle directly (unlike Turbopack's custom chunked format).
7
+ *
8
+ * Note: onBuildComplete runs BEFORE standalone output is generated
9
+ * (Next.js source: build/index.js:2544-2581), so we cannot rely on
10
+ * .next/standalone/. Instead we import directly from .next/server/.
11
+ */
12
+ import * as fs from "node:fs/promises";
13
+ import * as path from "node:path";
14
+ import { generateWorkerEntry } from "./worker-entry.js";
15
+ import { bundleForWorkers } from "./bundler.js";
16
+ import { writeManifest } from "./manifest.js";
17
+ const OUTPUT_DIR = ".creek/adapter-output";
18
+ export async function handleBuild(ctx) {
19
+ const outputDir = path.join(ctx.projectDir, OUTPUT_DIR);
20
+ const assetsDir = path.join(outputDir, "assets");
21
+ const serverDir = path.join(outputDir, "server");
22
+ await fs.rm(outputDir, { recursive: true, force: true });
23
+ await fs.mkdir(assetsDir, { recursive: true });
24
+ await fs.mkdir(serverDir, { recursive: true });
25
+ console.log(`\n [Creek Adapter] Preparing deployment output...`);
26
+ // Step 1: Collect static files (including public/* and edge-chunks/*)
27
+ const assetCount = await collectStaticFiles(ctx.outputs, assetsDir, ctx.projectDir, ctx.distDir, ctx.buildId);
28
+ console.log(` [Creek Adapter] ${assetCount} static files collected`);
29
+ // Step 2: Collect WASM files from all outputs.
30
+ // Middleware (both \`ctx.outputs.middleware\` and the \`edgeRuntime\` variant)
31
+ // ships its own wasmAssets separate from the page/route outputs — tests
32
+ // like \`edge-can-use-wasm-files\` import a user-provided \`.wasm\` from
33
+ // middleware. Skipping middleware here produced a \`.creek/\` output
34
+ // with zero wasm siblings and workerd threw at runtime:
35
+ // \`dynamically loading WebAssembly is not supported ... chunk
36
+ // 'chunks/src_add_0656eb_.wasm'\`.
37
+ const wasmFiles = new Map();
38
+ for (const outputs of [
39
+ ctx.outputs.appPages,
40
+ ctx.outputs.appRoutes,
41
+ ctx.outputs.pages,
42
+ ctx.outputs.pagesApi,
43
+ ]) {
44
+ for (const output of outputs) {
45
+ if (output.wasmAssets) {
46
+ for (const [name, absPath] of Object.entries(output.wasmAssets)) {
47
+ wasmFiles.set(name, absPath);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ // Middleware ships its own wasmAssets separate from the page/route
53
+ // outputs. \`edge-can-use-wasm-files\` imports a user-provided \`.wasm\`
54
+ // from middleware. Skipping this produced zero wasm siblings in the
55
+ // \`.creek/\` output and workerd threw at runtime with
56
+ // \`dynamically loading WebAssembly is not supported ...
57
+ // chunk 'chunks/src_add_0656eb_.wasm'\`.
58
+ const mwOutput = ctx.outputs.middleware;
59
+ if (mwOutput?.wasmAssets) {
60
+ for (const [name, absPath] of Object.entries(mwOutput.wasmAssets)) {
61
+ wasmFiles.set(name, absPath);
62
+ }
63
+ }
64
+ // Scan \`output.assets\` for \`.wasm\` files too. Next.js's adapter API
65
+ // puts Turbopack-known wasm in \`wasmAssets\` (above), but libraries
66
+ // like \`@vercel/og\`'s node-runtime variant load \`resvg.wasm\` /
67
+ // \`yoga.wasm\` via \`fs.readFileSync\` — those files show up in
68
+ // \`output.assets\` only (file-tracing result). workerd rejects
69
+ // \`WebAssembly.instantiate(bytes)\` — we need the bytes precompiled
70
+ // into a \`WebAssembly.Module\` at bundle time. Feed these wasms
71
+ // through the same CompiledWasm pipeline so the runtime instantiate
72
+ // override can swap bytes → pre-compiled Module by length.
73
+ // Fixes next/og node-runtime path (og-api \`/og-node\`,
74
+ // use-cache-metadata-route-handler opengraph/icon tests).
75
+ for (const outputs of [
76
+ ctx.outputs.appPages,
77
+ ctx.outputs.appRoutes,
78
+ ctx.outputs.pages,
79
+ ctx.outputs.pagesApi,
80
+ ]) {
81
+ for (const output of outputs) {
82
+ const assets = output.assets;
83
+ if (!assets)
84
+ continue;
85
+ for (const [outPath, srcPath] of Object.entries(assets)) {
86
+ if (!outPath.endsWith(".wasm"))
87
+ continue;
88
+ // Use basename as the wasm \`name\` so collisions across sources
89
+ // don't produce colliding destination files. The
90
+ // xxh3-content-hashing step below key by content anyway.
91
+ const name = path.basename(outPath);
92
+ if (!wasmFiles.has(name))
93
+ wasmFiles.set(name, srcPath);
94
+ }
95
+ }
96
+ }
97
+ // Step 3: Collect manifests from .next/ for embedding in the worker.
98
+ // Next.js route modules call loadManifest() which uses fs.readFileSync().
99
+ // CF Workers doesn't have fs, so we embed all manifests and shim the loader.
100
+ const manifests = await collectManifests(ctx.distDir);
101
+ console.log(` [Creek Adapter] ${Object.keys(manifests).length} manifests embedded`);
102
+ // Step 3a-bis: Compute xxh3-128 hex for every wasm file. Turbopack's
103
+ // edge bundles access each wasm via \`globalThis.wasm_<hex>\` where
104
+ // \`<hex>\` is xxh3_128(wasm_content). At worker init we need to import
105
+ // the wasm (as CompiledWasm via wrangler rules) and mirror it onto
106
+ // globalThis under the expected name.
107
+ const wasmHashToFilename = new Map();
108
+ // Byte length → bundled wasm filename. Used at runtime by the
109
+ // \`WebAssembly.instantiate\` patch to swap byte-based calls
110
+ // (which workerd rejects as "Wasm code generation disallowed")
111
+ // for the pre-compiled CompiledWasm module wrangler bundled.
112
+ const wasmLengthToFilename = new Map();
113
+ try {
114
+ const { xxh3 } = await import("@node-rs/xxhash");
115
+ for (const [name, absPath] of wasmFiles) {
116
+ try {
117
+ const bytes = await fs.readFile(absPath);
118
+ const hex = xxh3.xxh128(bytes).toString(16).padStart(32, "0");
119
+ const destName = name.endsWith(".wasm") ? name : name + ".wasm";
120
+ console.log(` wasm: name=${name} dest=${destName} xxh3=${hex} bytes=${bytes.byteLength}`);
121
+ wasmHashToFilename.set(hex, destName);
122
+ wasmLengthToFilename.set(bytes.byteLength, destName);
123
+ }
124
+ catch { }
125
+ }
126
+ if (wasmHashToFilename.size > 0) {
127
+ console.log(` [Creek Adapter] ${wasmHashToFilename.size} wasm edge var mappings computed`);
128
+ }
129
+ }
130
+ catch { }
131
+ // Step 3b: Collect prerender entries for ISR cache seeding.
132
+ // Each prerender with a fallback file gets seeded into the cache at startup.
133
+ const fallbackShellRoutes = await collectFallbackShellRoutes(ctx.distDir);
134
+ const prerenderEntries = await collectPrerenderEntries(ctx.outputs, fallbackShellRoutes);
135
+ if (prerenderEntries.length > 0) {
136
+ console.log(` [Creek Adapter] ${prerenderEntries.length} prerender entries for cache seeding`);
137
+ }
138
+ // Step 3c: Extract \`'use cache'\` entries from every prerender's postponedState.
139
+ // Keyed by bracket-form shell pathname so the worker can apply them ONLY to
140
+ // requests matching that shell — mirrors Next.js's request-scoped RDC and
141
+ // keeps e.g. /with-suspense/* build-time values out of /without-suspense/*
142
+ // requests that expect fresh runtime renders.
143
+ const composableCacheSeedsByShell = await collectComposableCacheSeeds(ctx.outputs, fallbackShellRoutes);
144
+ const composableCacheSeedEntries = Array.from(composableCacheSeedsByShell.entries());
145
+ const composableCacheSeedCount = composableCacheSeedEntries.reduce((n, [, seeds]) => n + seeds.length, 0);
146
+ if (composableCacheSeedCount > 0) {
147
+ console.log(` [Creek Adapter] ${composableCacheSeedCount} composable cache seeds across ${composableCacheSeedEntries.length} shells`);
148
+ }
149
+ // Find Turbopack runtime for static import (triggers chunk bundling)
150
+ let turbopackRuntimePath;
151
+ try {
152
+ const ssrChunksDir = path.join(ctx.distDir, "server", "chunks", "ssr");
153
+ const files = await fs.readdir(ssrChunksDir);
154
+ const runtimeFile = files.find((f) => f.includes("[turbopack]_runtime"));
155
+ if (runtimeFile) {
156
+ turbopackRuntimePath = path.join(ssrChunksDir, runtimeFile);
157
+ }
158
+ }
159
+ catch { }
160
+ // Create no-op instrumentation.js if missing — Next.js tries to require()
161
+ // this at runtime, and CF Workers throws a generic error for dynamic require
162
+ // of missing modules. The error code doesn't match ENOENT/MODULE_NOT_FOUND
163
+ // that Next.js expects, causing an unhandled rejection.
164
+ //
165
+ // We also track whether the file represents a REAL user instrumentation
166
+ // (not our no-op) so the worker entry can statically import it and invoke
167
+ // \`register()\` at startup. Next.js's \`getInstrumentationModule\` uses
168
+ // \`__require\` with a dynamic path — workerd rejects that with
169
+ // "Dynamic require ... is not supported", the registration promise resolves
170
+ // undefined, and no user instrumentation ever runs. Side effects like
171
+ // \`experimental.clientTraceMetadata\` meta-tag injection silently disappear.
172
+ // Static-importing here gets the user module into the bundle and gives us
173
+ // a concrete handle to hand back to Next.js at runtime.
174
+ const instrumentationPath = path.join(ctx.distDir, "server", "instrumentation.js");
175
+ let userInstrumentationPath;
176
+ try {
177
+ const existing = await fs.readFile(instrumentationPath, "utf-8");
178
+ if (existing.trim().length > 0 && !/^\s*module\.exports\s*=\s*\{\s*\}\s*;?\s*$/.test(existing)) {
179
+ userInstrumentationPath = instrumentationPath;
180
+ }
181
+ }
182
+ catch {
183
+ await fs.writeFile(instrumentationPath, "module.exports = {};");
184
+ }
185
+ // Step 3c: Find edge middleware registration chunk.
186
+ // Turbopack generates TWO edge-wrapper files:
187
+ // 1. turbopack-..._edge-wrapper (modulePath — Turbopack runtime, imported by worker)
188
+ // 2. node_modules_..._edge-wrapper (contains _ENTRIES registration + module loader)
189
+ // File 2 is NOT referenced by modulePath, so we need to import it explicitly.
190
+ //
191
+ // Webpack builds take a different path: the middleware output's `assets`
192
+ // include `server/edge-runtime-webpack.js`, a tiny IIFE that installs a
193
+ // `webpackChunk_N_E.push` hook. Without importing it BEFORE `middleware.js`,
194
+ // the chunk push becomes a plain Array.push and the entry chunk never
195
+ // evaluates — `_ENTRIES["middleware_middleware"]` stays undefined and all
196
+ // middleware rewrites silently no-op (reproduces as search-params 404s).
197
+ let edgeRegistrationChunkPath;
198
+ let edgeRuntimeModuleIds = [];
199
+ let edgeOtherChunkPaths = [];
200
+ let webpackEdgeRuntimePath;
201
+ let webpackEdgeBootstrapPath;
202
+ if (ctx.outputs.middleware?.edgeRuntime) {
203
+ const mwAssets = ctx.outputs.middleware.assets || {};
204
+ for (const [rel, abs] of Object.entries(mwAssets)) {
205
+ if (/(^|\/)server\/edge-runtime-webpack\.js$/.test(rel)) {
206
+ webpackEdgeRuntimePath = abs;
207
+ console.log(` [Creek Adapter] Webpack edge runtime: ${path.basename(abs)}`);
208
+ break;
209
+ }
210
+ }
211
+ // Webpack's middleware.js ends with `(_ENTRIES="u"<typeof _ENTRIES?{}:
212
+ // _ENTRIES).middleware_middleware=b` — a bare assignment that needs
213
+ // `_ENTRIES` to resolve to a writable global. esbuild bundles the file as
214
+ // strict-mode ESM, so the bare identifier throws ReferenceError unless
215
+ // `globalThis._ENTRIES` already exists. This bootstrap file ensures it
216
+ // does; importing it before the runtime/middleware imports runs it first
217
+ // (imports evaluate in declaration order).
218
+ if (webpackEdgeRuntimePath) {
219
+ const bootstrapPath = path.join(ctx.distDir, "server", "creek-edge-bootstrap.js");
220
+ await fs.writeFile(bootstrapPath, "globalThis._ENTRIES = globalThis._ENTRIES || {};\n");
221
+ webpackEdgeBootstrapPath = bootstrapPath;
222
+ }
223
+ try {
224
+ const edgeChunksDir = path.join(ctx.distDir, "server", "edge", "chunks");
225
+ const files = await fs.readdir(edgeChunksDir);
226
+ for (const f of files) {
227
+ if (f.includes("edge-wrapper") && !f.endsWith(".map") && !f.startsWith("turbopack-")) {
228
+ const content = await fs.readFile(path.join(edgeChunksDir, f), "utf-8");
229
+ if (content.includes("_ENTRIES")) {
230
+ edgeRegistrationChunkPath = path.join(edgeChunksDir, f);
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ // Extract runtimeModuleIds and otherChunks from the Turbopack runtime chunk.
236
+ for (const f of files) {
237
+ if (f.startsWith("turbopack-") && f.includes("edge-wrapper") && !f.endsWith(".map")) {
238
+ const content = await fs.readFile(path.join(edgeChunksDir, f), "utf-8");
239
+ const idsMatch = content.match(/runtimeModuleIds:\s*\[([0-9,\s]+)\]/);
240
+ if (idsMatch) {
241
+ edgeRuntimeModuleIds = idsMatch[1].split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
242
+ }
243
+ // Extract otherChunks paths — these need to be imported so their
244
+ // module factories are registered in the Turbopack module registry.
245
+ // Note: can't use [^\]] because chunk paths contain literal ] (e.g., [root-of-the-server])
246
+ const chunksMatch = content.match(/otherChunks:\s*\[((?:"[^"]*"(?:,\s*)?)*)\]/);
247
+ if (chunksMatch) {
248
+ const chunkPaths = chunksMatch[1].match(/"([^"]+)"/g);
249
+ if (chunkPaths) {
250
+ for (const raw of chunkPaths) {
251
+ const rel = raw.replace(/"/g, "");
252
+ const absPath = await resolveEdgeOtherChunkPath(ctx.distDir, rel);
253
+ if (!absPath)
254
+ continue;
255
+ await addEdgeChunkImportPath(edgeOtherChunkPaths, absPath);
256
+ console.log(` [Creek Adapter] Edge otherChunk: ${path.basename(absPath)}`);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ // Middleware handler extraction is handled by the Turbopack runtime
263
+ // patching and otherChunks import above. The edge runtime's _ENTRIES
264
+ // registration works via runtimeModuleIds push (see __initEdgeModules).
265
+ }
266
+ catch { }
267
+ }
268
+ // Step 3d: Extract runtimeModuleIds for edge handlers.
269
+ // Log edge handler info for debugging
270
+ for (const outputs2 of [ctx.outputs.appPages, ctx.outputs.appRoutes, ctx.outputs.pages, ctx.outputs.pagesApi]) {
271
+ for (const output of outputs2) {
272
+ if (output.runtime === "edge") {
273
+ }
274
+ }
275
+ }
276
+ // Each edge page/route has its own Turbopack edge-wrapper with runtimeModuleIds.
277
+ // We read the wrapper files to extract the module IDs and attach them to outputs.
278
+ for (const outputs of [ctx.outputs.appPages, ctx.outputs.appRoutes, ctx.outputs.pages, ctx.outputs.pagesApi]) {
279
+ for (const output of outputs) {
280
+ if (output.runtime === "edge" && output.edgeRuntime?.modulePath) {
281
+ try {
282
+ const content = await fs.readFile(output.edgeRuntime.modulePath, "utf-8");
283
+ const idsMatch = content.match(/runtimeModuleIds:\s*\[([0-9,\s]+)\]/);
284
+ if (idsMatch) {
285
+ const ids = idsMatch[1].split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
286
+ if (ids.length > 0) {
287
+ output.edgeRuntime.runtimeModuleId = ids[0];
288
+ }
289
+ }
290
+ // Also find and import otherChunks for this edge handler
291
+ const chunksMatch = content.match(/otherChunks:\s*\[((?:"[^"]*"(?:,\s*)?)*)\]/);
292
+ if (chunksMatch) {
293
+ const chunkPaths = chunksMatch[1].match(/"([^"]+)"/g);
294
+ if (chunkPaths) {
295
+ for (const raw of chunkPaths) {
296
+ const rel = raw.replace(/"/g, "");
297
+ const absPath = await resolveEdgeOtherChunkPath(ctx.distDir, rel);
298
+ if (!absPath)
299
+ continue;
300
+ await addEdgeChunkImportPath(edgeOtherChunkPaths, absPath);
301
+ }
302
+ }
303
+ }
304
+ }
305
+ catch { }
306
+ }
307
+ }
308
+ }
309
+ // Turbopack edge wrappers do not enumerate every transitive chunk in
310
+ // otherChunks. Preload the full edge chunk directory so module factories
311
+ // for builtins like global-error are always registered before execution.
312
+ try {
313
+ const edgeChunksDir = path.join(ctx.distDir, "server", "edge", "chunks");
314
+ const chunkPaths = await collectJsFilesRecursive(edgeChunksDir);
315
+ for (const chunkPath of chunkPaths) {
316
+ await addEdgeChunkImportPath(edgeOtherChunkPaths, chunkPath);
317
+ }
318
+ }
319
+ catch { }
320
+ // Also preload node-side SSR chunks. Turbopack emits the server actions
321
+ // registry (module 3103 on a representative build — the one that maps
322
+ // action hex → handler fn) into \`.next/server/chunks/ssr/\` regardless
323
+ // of runtime, but edge-wrappers don't list those paths in their
324
+ // \`otherChunks\`. Without this, edge routes that invoke a server action
325
+ // throw \`Module N was instantiated because it was required from module M,
326
+ // but the module factory is not available\` at request time. Factory
327
+ // registration is side-effect free (just pushes onto globalThis.TURBOPACK),
328
+ // so preloading is safe — any Node-only factory body only runs if
329
+ // someone actually requires that specific module.
330
+ try {
331
+ const nodeChunksDir = path.join(ctx.distDir, "server", "chunks");
332
+ const chunkPaths = await collectJsFilesRecursive(nodeChunksDir);
333
+ for (const chunkPath of chunkPaths) {
334
+ await addEdgeChunkImportPath(edgeOtherChunkPaths, chunkPath);
335
+ }
336
+ }
337
+ catch { }
338
+ // Step 3e: Collect non-code user files (data.json, etc.) that route
339
+ // handlers may read at runtime via fs.readFileSync. Next.js's adapter API
340
+ // exposes these per-output as `output.assets` (the result of file tracing).
341
+ // We embed them in __USER_FILES so the fs shim can serve them in workerd.
342
+ const userFiles = await collectUserFiles(ctx.outputs);
343
+ if (Object.keys(userFiles).length > 0) {
344
+ console.log(` [Creek Adapter] ${Object.keys(userFiles).length} user data files embedded`);
345
+ }
346
+ // Step 4: Generate worker entry
347
+ console.log(" [Creek Adapter] Scanning external modules...");
348
+ const externalModules = await collectExternalizedModules(ctx.distDir);
349
+ if (externalModules.length > 0) {
350
+ console.log(` [Creek Adapter] ${externalModules.length} external modules preloaded`);
351
+ }
352
+ console.log(" [Creek Adapter] Generating worker entry...");
353
+ const workerSource = generateWorkerEntry({
354
+ buildId: ctx.buildId,
355
+ routing: ctx.routing,
356
+ outputs: ctx.outputs,
357
+ basePath: ctx.config.basePath || "",
358
+ assetPrefix: ctx.config.assetPrefix || "",
359
+ i18n: ctx.config.i18n || null,
360
+ config: { trailingSlash: !!ctx.config.trailingSlash },
361
+ manifests,
362
+ userFiles,
363
+ prerenderEntries,
364
+ composableCacheSeedsByShell: composableCacheSeedEntries,
365
+ wasmHashToFilename: Array.from(wasmHashToFilename.entries()),
366
+ wasmLengthToFilename: Array.from(wasmLengthToFilename.entries()),
367
+ externalModules,
368
+ turbopackRuntimePath,
369
+ edgeRegistrationChunkPath,
370
+ edgeRuntimeModuleIds,
371
+ edgeOtherChunkPaths,
372
+ webpackEdgeRuntimePath,
373
+ webpackEdgeBootstrapPath,
374
+ userInstrumentationPath,
375
+ });
376
+ // Step 4: Bundle with esbuild
377
+ console.log(" [Creek Adapter] Bundling worker...");
378
+ const serverFiles = await bundleForWorkers({
379
+ workerSource,
380
+ outputDir: serverDir,
381
+ serverAssets: new Map(),
382
+ wasmFiles,
383
+ distDir: ctx.distDir,
384
+ repoRoot: ctx.repoRoot,
385
+ standaloneDir: ctx.distDir,
386
+ });
387
+ const totalSize = await getTotalSize(serverDir, serverFiles);
388
+ console.log(` [Creek Adapter] Worker bundled: ${serverFiles.length} files (${formatSize(totalSize)})`);
389
+ // Step 5: Write deploy manifest
390
+ await writeManifest(outputDir, {
391
+ buildId: ctx.buildId,
392
+ nextVersion: ctx.nextVersion,
393
+ entrypoint: "worker.js",
394
+ serverFiles,
395
+ hasMiddleware: !!ctx.outputs.middleware,
396
+ hasPrerender: ctx.outputs.prerenders.length > 0,
397
+ });
398
+ // Phase 2a (experimental, opt-in via \`CREEK_MULTI_WORKER=1\`): emit a
399
+ // 3-worker output that reuses the single-worker bundle as the
400
+ // node-runtime worker. The dispatcher forwards unconditionally to it
401
+ // via a service binding. Phase 2b will carve middleware + routing
402
+ // into the dispatcher and Phase 2c produces a proper edge-runtime
403
+ // bundle. Staying opt-in means enterprise customers get multi-runtime
404
+ // isolation + fluid-compute-ready topology, while hobbyist tiers keep
405
+ // the lean single-worker path.
406
+ if (process.env.CREEK_MULTI_WORKER === "1") {
407
+ await emitMultiWorker(ctx, outputDir, serverDir);
408
+ }
409
+ console.log(` [Creek Adapter] Output ready: ${OUTPUT_DIR}/`);
410
+ }
411
+ function classifyHandlersByRuntime(outputs) {
412
+ const counts = { nodejs: 0, edge: 0 };
413
+ for (const bucket of [outputs.appPages, outputs.appRoutes, outputs.pages, outputs.pagesApi]) {
414
+ for (const h of bucket) {
415
+ if (h.runtime === "edge")
416
+ counts.edge += 1;
417
+ else
418
+ counts.nodejs += 1;
419
+ }
420
+ }
421
+ return counts;
422
+ }
423
+ function buildRouteRuntimeMap(outputs) {
424
+ const literals = [];
425
+ const patterns = [];
426
+ const pushHandler = (p, runtime) => {
427
+ if (p.includes("[")) {
428
+ patterns.push({ pattern: bracketPathToRegexSource(p), runtime });
429
+ }
430
+ else {
431
+ literals.push({ pathname: p, runtime });
432
+ }
433
+ };
434
+ for (const bucket of [
435
+ outputs.appPages,
436
+ outputs.appRoutes,
437
+ outputs.pages,
438
+ outputs.pagesApi,
439
+ ]) {
440
+ for (const h of bucket) {
441
+ const runtime = h.runtime === "edge" ? "edge" : "nodejs";
442
+ pushHandler(h.pathname, runtime);
443
+ }
444
+ }
445
+ // Patterns with more path segments match first so \`/users/[id]\` beats
446
+ // \`/[...catchall]\`. Same heuristic the runtime routing layer uses.
447
+ patterns.sort((a, b) => {
448
+ const as = (a.pattern ?? "").split("/").length;
449
+ const bs = (b.pattern ?? "").split("/").length;
450
+ return bs - as;
451
+ });
452
+ return [...literals, ...patterns];
453
+ }
454
+ /** Convert \`/[slug]/cache\` → \`^/([^/]+)/cache$\` (RegExp source string). */
455
+ function bracketPathToRegexSource(pathname) {
456
+ let re = "";
457
+ let i = 0;
458
+ while (i < pathname.length) {
459
+ const ch = pathname[i];
460
+ if (ch === "[") {
461
+ const end = pathname.indexOf("]", i);
462
+ if (end === -1) {
463
+ re += "\\[";
464
+ i += 1;
465
+ continue;
466
+ }
467
+ const inner = pathname.slice(i + 1, end);
468
+ if (inner.startsWith("...")) {
469
+ re += "(.+)";
470
+ }
471
+ else if (inner.startsWith("[...") && inner.endsWith("]")) {
472
+ re += "(.*)";
473
+ }
474
+ else {
475
+ re += "([^/]+)";
476
+ }
477
+ i = end + 1;
478
+ continue;
479
+ }
480
+ if (/[.*+?^${}()|\\]/.test(ch))
481
+ re += "\\" + ch;
482
+ else
483
+ re += ch;
484
+ i += 1;
485
+ }
486
+ return "^" + re + "$";
487
+ }
488
+ /**
489
+ * Emit the 3-worker multi-runtime layout: a dispatcher + node-runtime +
490
+ * edge-runtime. Phase 2a reuses the single-worker bundle as the
491
+ * node-runtime worker's \`worker.js\` — the dispatcher just forwards
492
+ * every request via a service binding. This validates the real
493
+ * end-to-end plumbing (service binding preserves body + headers +
494
+ * streaming for genuine Next.js responses) before Phase 2b moves
495
+ * middleware/routing up into the dispatcher.
496
+ *
497
+ * Opt-in via \`CREEK_MULTI_WORKER=1\`. The single-worker bundle in
498
+ * \`server/\` stays untouched so existing deploys are unaffected.
499
+ */
500
+ async function emitMultiWorker(ctx, outputDir, serverDir) {
501
+ const runtimes = classifyHandlersByRuntime(ctx.outputs);
502
+ const routeRuntimeMap = buildRouteRuntimeMap(ctx.outputs);
503
+ console.log(` [Creek Adapter] Multi-worker build: nodejs=${runtimes.nodejs} edge=${runtimes.edge} (routeRuntimeMap entries: ${routeRuntimeMap.length})`);
504
+ const dispatcherDir = path.join(outputDir, "dispatcher");
505
+ const nodeDir = path.join(outputDir, "node-runtime");
506
+ const edgeDir = path.join(outputDir, "edge-runtime");
507
+ for (const d of [dispatcherDir, nodeDir, edgeDir]) {
508
+ await fs.mkdir(d, { recursive: true });
509
+ }
510
+ // --- node-runtime worker = existing single-worker bundle (Phase 2a) ---
511
+ //
512
+ // Copy server/worker.js + server/wrangler.toml into node-runtime/ so
513
+ // the real Next.js handler runs there. In Phase 2b this worker will
514
+ // receive pre-routed, pre-middleware requests from the dispatcher via
515
+ // x-creek-* headers and skip its own middleware/resolveRoutes.
516
+ const singleWorkerJs = path.join(serverDir, "worker.js");
517
+ const nodeWorkerJs = path.join(nodeDir, "worker.js");
518
+ await fs.copyFile(singleWorkerJs, nodeWorkerJs);
519
+ // Mirror the server/wrangler.toml into node-runtime/ but rename
520
+ // \`name\` so the service binding target matches (\`creek-node-runtime\`)
521
+ // and bump the \`assets.directory\` so it still resolves to the shared
522
+ // \`../assets/\`.
523
+ const singleWranglerToml = await fs.readFile(path.join(serverDir, "wrangler.toml"), "utf-8");
524
+ const nodeWranglerToml = singleWranglerToml.replace(/name = "creek"/, 'name = "creek-node-runtime"');
525
+ await fs.writeFile(path.join(nodeDir, "wrangler.toml"), nodeWranglerToml);
526
+ // --- dispatcher worker: thin forwarder (Phase 2a) ---
527
+ //
528
+ // Unconditional forward to NODE_WORKER until Phase 2b splits by
529
+ // runtime label. The \`x-creek-from\` header gives us a probe for
530
+ // ensuring every request passed through the dispatcher.
531
+ // Build-time map embedded as a JS constant so the dispatcher can pick
532
+ // NODE_WORKER vs EDGE_WORKER without re-parsing routes-manifest.json.
533
+ // Phase 2b step 1: the map is emitted and compiled into regex at worker
534
+ // init but not yet used for dispatch — that lands in the next commit
535
+ // once edge-runtime carries a real bundle.
536
+ const routeRuntimeMapLiteral = JSON.stringify(routeRuntimeMap);
537
+ const dispatcherSource = [
538
+ "// Creek adapter — dispatcher worker (Phase 2b step 3).",
539
+ "// Picks NODE_WORKER or EDGE_WORKER based on the build-time",
540
+ "// route→runtime map. Unknown pathnames (no handler match) fall",
541
+ "// through to NODE_WORKER as a conservative default — matches the",
542
+ "// single-worker behaviour where every unclaimed request goes to",
543
+ "// the one bundle. Edge bundle split follows in Phase 2c.",
544
+ "",
545
+ "const RAW_ROUTE_RUNTIME_MAP = " + routeRuntimeMapLiteral + ";",
546
+ "const ROUTE_RUNTIME_LITERALS = new Map();",
547
+ "const ROUTE_RUNTIME_PATTERNS = [];",
548
+ "for (const entry of RAW_ROUTE_RUNTIME_MAP) {",
549
+ " if (typeof entry.pathname === \"string\") {",
550
+ " ROUTE_RUNTIME_LITERALS.set(entry.pathname, entry.runtime);",
551
+ " } else if (typeof entry.pattern === \"string\") {",
552
+ " try { ROUTE_RUNTIME_PATTERNS.push([new RegExp(entry.pattern), entry.runtime]); } catch {}",
553
+ " }",
554
+ "}",
555
+ "",
556
+ "function pickRuntimeForPathname(pathname) {",
557
+ " const literal = ROUTE_RUNTIME_LITERALS.get(pathname);",
558
+ " if (literal) return literal;",
559
+ " for (const [re, rt] of ROUTE_RUNTIME_PATTERNS) {",
560
+ " if (re.test(pathname)) return rt;",
561
+ " }",
562
+ " return \"nodejs\";",
563
+ "}",
564
+ "",
565
+ "export default {",
566
+ " async fetch(request, env, ctx) {",
567
+ " const url = new URL(request.url);",
568
+ " const runtime = pickRuntimeForPathname(url.pathname);",
569
+ " const target = runtime === \"edge\" ? env.EDGE_WORKER : env.NODE_WORKER;",
570
+ " const resp = await target.fetch(request);",
571
+ " const headers = new Headers(resp.headers);",
572
+ ' headers.set("x-creek-from", "dispatcher");',
573
+ ' headers.set("x-creek-route-runtime", runtime);',
574
+ " return new Response(resp.body, {",
575
+ " status: resp.status,",
576
+ " statusText: resp.statusText,",
577
+ " headers,",
578
+ " });",
579
+ " },",
580
+ "};",
581
+ "",
582
+ ].join("\n");
583
+ await fs.writeFile(path.join(dispatcherDir, "worker.js"), dispatcherSource);
584
+ await fs.writeFile(path.join(dispatcherDir, "wrangler.toml"), [
585
+ "# Generated by adapter-creek (multi-worker, Phase 2a).",
586
+ 'name = "creek-dispatcher"',
587
+ 'main = "worker.js"',
588
+ 'compatibility_date = "2026-03-23"',
589
+ 'compatibility_flags = ["nodejs_compat"]',
590
+ "",
591
+ "[[services]]",
592
+ 'binding = "NODE_WORKER"',
593
+ 'service = "creek-node-runtime"',
594
+ "",
595
+ "[[services]]",
596
+ 'binding = "EDGE_WORKER"',
597
+ 'service = "creek-edge-runtime"',
598
+ "",
599
+ ].join("\n"));
600
+ // --- edge-runtime worker = same bundle as node-runtime (Phase 2b) ---
601
+ //
602
+ // Step 2: give edge-runtime the full adapter bundle so it can serve a
603
+ // request identically to node-runtime once the dispatcher starts
604
+ // routing edge handlers there. Bundle optimisation (stripping nodejs-
605
+ // only handler imports) lands in Phase 2c — for now we prioritise
606
+ // functional correctness of the dispatch topology over bundle size.
607
+ const edgeWorkerJs = path.join(edgeDir, "worker.js");
608
+ await fs.copyFile(singleWorkerJs, edgeWorkerJs);
609
+ const edgeWranglerToml = singleWranglerToml.replace(/name = "creek"/, 'name = "creek-edge-runtime"');
610
+ await fs.writeFile(path.join(edgeDir, "wrangler.toml"), edgeWranglerToml);
611
+ console.log(` [Creek Adapter] Multi-worker emitted: dispatcher + node-runtime + edge-runtime`);
612
+ }
613
+ // True for static-file entries that represent a pre-rendered HTML page
614
+ // (e.g. \`/about\`, \`/catch-all/[...slug]\`) rather than a real asset like
615
+ // \`/_next/static/foo.js\`. We can't just check `path.extname()`: a Next.js
616
+ // dynamic segment like `[...slug]` contains dots and `extname` returns
617
+ // `.slug]`, which would misclassify catch-all pages as assets and skip the
618
+ // `index.html` rewrite below — leaving the served file at \`/catch-all/[...slug]\`
619
+ // where the worker can't find it.
620
+ function isStaticHtmlPage(pathname) {
621
+ if (pathname.startsWith("/_next/"))
622
+ return false;
623
+ if (pathname.includes("["))
624
+ return true;
625
+ return !path.extname(pathname);
626
+ }
627
+ // Inject `data-dpl-id="<buildId>"` into the `<html>` tag of static HTML
628
+ // files at build time. The Pages Router client reads this attribute on
629
+ // page load to populate `globalThis.NEXT_DEPLOYMENT_ID`, then sends
630
+ // `x-deployment-id: <buildId>` on subsequent /_next/data/* fetches. The
631
+ // worker runtime always responds with `x-nextjs-deployment-id: <buildId>`,
632
+ // so the client's skew-protection check matches and client-side
633
+ // navigation stays soft. Without this injection, static pages (/404,
634
+ // /error, /about) don't have the attribute — because nextConfig.deploymentId
635
+ // is usually unset in test fixtures — and client-transition tests in
636
+ // middleware-general hard-reload on every push, wiping `window.beforeNav`.
637
+ // Dynamically rendered pages get the attribute through Next.js's own
638
+ // `createHtmlDataDplIdTransformStream`, which we enable at runtime by
639
+ // patching `__SERVER_FILES_MANIFEST.config.deploymentId` in worker-entry.
640
+ async function copyHtmlWithDplId(src, dest, buildId) {
641
+ const content = await fs.readFile(src, "utf8");
642
+ // Skip if already has the attribute (e.g. upstream nextConfig.deploymentId
643
+ // was set at build time), otherwise insert right after `<html`.
644
+ let patched = content;
645
+ if (!content.includes("data-dpl-id")) {
646
+ patched = content.replace(/<html(?=[\s>])/, `<html data-dpl-id="${buildId}"`);
647
+ }
648
+ await fs.writeFile(dest, patched);
649
+ }
650
+ // \`fs.mkdir(..., {recursive:true})\` throws ENOTDIR if any parent of the
651
+ // target path exists as a file. Next.js's adapter API emits
652
+ // interception-route prerenders that collide with regular routes: e.g.
653
+ // \`/test-nested\` lands as an HTML file at \`assets/test-nested\`, then a
654
+ // subsequent \`/(.)test-nested/deeper\` wants to create
655
+ // \`assets/(.)test-nested/deeper/\` — but a prior loop iteration may have
656
+ // created \`assets/(.)test-nested\` as a FILE. Recover: return false so
657
+ // the caller skips this entry. Interception routes are dynamic and
658
+ // will be served via the worker, so skipping the prerender copy is safe.
659
+ // Fixes \`Build error: ENOTDIR: not a directory, mkdir '.../(.)foo/bar'\`
660
+ // on interception-dynamic-segment + parallel-routes fixtures.
661
+ async function safeMkdirForDest(destPath, label) {
662
+ try {
663
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
664
+ return true;
665
+ }
666
+ catch (err) {
667
+ // ENOTDIR: a parent in the path exists as a file → cannot create dir
668
+ // beneath it. (Seen when \`(.)foo/bar\` is processed after
669
+ // \`(.)foo\` got written as a file — \`(.)foo\` then blocks
670
+ // \`(.)foo/bar\`.)
671
+ // EEXIST: \`fs.mkdir({recursive:true})\` only silences EEXIST when the
672
+ // target is already a directory. If \`(.)foo\` was written as
673
+ // a file first and we later try to \`mkdir assets/(.)foo\` to
674
+ // host a sibling, node throws EEXIST instead of ENOTDIR
675
+ // depending on which parent the conflict lands on.
676
+ if (err && (err.code === "ENOTDIR" || err.code === "EEXIST")) {
677
+ if (label) {
678
+ console.error("[adapter-creek] skip (path conflicts with a file sibling):", label);
679
+ }
680
+ return false;
681
+ }
682
+ throw err;
683
+ }
684
+ }
685
+ async function collectStaticFiles(outputs, assetsDir, projectDir, distDir, buildId) {
686
+ let count = 0;
687
+ const allPathnames = new Set(outputs.staticFiles.map((f) => f.pathname));
688
+ for (const file of outputs.staticFiles) {
689
+ let destRelative = file.pathname;
690
+ const isHtml = isStaticHtmlPage(destRelative);
691
+ if (isHtml) {
692
+ // Pre-rendered HTML pages (e.g. /, /about, /404, /catch-all/[...slug]).
693
+ // Store as <pathname>/index.html so CF Workers Assets serves them correctly.
694
+ destRelative = path.join(destRelative, "index.html");
695
+ }
696
+ const destPath = path.join(assetsDir, destRelative);
697
+ if (!(await safeMkdirForDest(destPath, destRelative)))
698
+ continue;
699
+ try {
700
+ if (isHtml && buildId) {
701
+ await copyHtmlWithDplId(file.filePath, destPath, buildId);
702
+ }
703
+ else {
704
+ await fs.copyFile(file.filePath, destPath);
705
+ }
706
+ count++;
707
+ }
708
+ catch { }
709
+ }
710
+ for (const prerender of outputs.prerenders) {
711
+ if (prerender.fallback?.filePath) {
712
+ // Prerender source files: \`.html\` for APP_PAGE / Pages Router,
713
+ // \`.body\` for APP_ROUTE (opengraph-image, icon, sitemap, etc. — often
714
+ // binary). Misclassifying a binary \`.body\` as HTML routes it through
715
+ // \`copyHtmlWithDplId\` which reads as UTF-8 and corrupts PNG/JPEG
716
+ // bytes into \`0xef 0xbf 0xbd\` replacement chars. Trust the source
717
+ // extension over the pathname — \`/opengraph-image\` has no dot but
718
+ // maps to \`opengraph-image.body\`.
719
+ const isBinary = prerender.fallback.filePath.endsWith(".body");
720
+ const isHtml = !isBinary && isStaticHtmlPage(prerender.pathname);
721
+ let destRelative = prerender.pathname;
722
+ if (isHtml) {
723
+ destRelative = destRelative + "/index.html";
724
+ }
725
+ const destPath = path.join(assetsDir, destRelative);
726
+ if (!(await safeMkdirForDest(destPath, destRelative)))
727
+ continue;
728
+ try {
729
+ if (isHtml && buildId) {
730
+ await copyHtmlWithDplId(prerender.fallback.filePath, destPath, buildId);
731
+ }
732
+ else {
733
+ await fs.copyFile(prerender.fallback.filePath, destPath);
734
+ }
735
+ count++;
736
+ }
737
+ catch { }
738
+ }
739
+ }
740
+ // Edge asset bindings (\`.next/server/edge-chunks/asset_*\`). Edge routes
741
+ // that do \`fetch(new URL('../../assets/foo', import.meta.url))\` get
742
+ // rewritten by next's middleware-asset-loader to \`fetch('blob:foo')\` at
743
+ // build time, with the actual bytes emitted to
744
+ // \`.next/server/edge-chunks/asset_foo\`. The upstream edge sandbox has a
745
+ // \`fetchInlineAsset\` shim that intercepts \`blob:\` URLs and reads the
746
+ // file from disk; since CF Workers have no fs access, we copy the
747
+ // chunks into the static assets binding under \`/_next/edge-chunks/\`
748
+ // and the runtime fetch wrapper maps \`blob:NAME\` →
749
+ // \`/_next/edge-chunks/asset_NAME\`.
750
+ if (distDir) {
751
+ const edgeChunksDir = path.join(distDir, "server", "edge-chunks");
752
+ try {
753
+ const entries = await fs.readdir(edgeChunksDir, { withFileTypes: true });
754
+ for (const entry of entries) {
755
+ if (!entry.isFile())
756
+ continue;
757
+ const srcPath = path.join(edgeChunksDir, entry.name);
758
+ const destPath = path.join(assetsDir, "_next", "edge-chunks", entry.name);
759
+ if (!(await safeMkdirForDest(destPath, entry.name)))
760
+ continue;
761
+ try {
762
+ await fs.copyFile(srcPath, destPath);
763
+ count++;
764
+ }
765
+ catch { }
766
+ }
767
+ }
768
+ catch {
769
+ // No edge-chunks dir — no edge asset bindings in this build.
770
+ }
771
+ }
772
+ // Turbopack edge assets (\`.next/server/edge/assets/*\`). These are files
773
+ // referenced by edge route handlers via
774
+ // \`fetch(new URL('./asset.ttf', import.meta.url))\`. Turbopack rewrites
775
+ // the URL to \`blob:server/edge/assets/<hashed-filename>\` at build time;
776
+ // Next.js's edge sandbox uses \`fetchInlineAsset\` to translate the blob URL
777
+ // back to a filesystem read. CF Workers have no fs, so we mirror the
778
+ // files into the static assets binding under \`/_next/edge-assets/<name>\`
779
+ // and the runtime fetch wrapper in worker-entry.ts maps
780
+ // \`blob:server/edge/assets/<name>\` → \`/_next/edge-assets/<name>\`.
781
+ // \`next/og\` custom fonts (and any other \`new URL(..., import.meta.url)\`
782
+ // edge asset) goes through this path.
783
+ if (distDir) {
784
+ const edgeAssetsDir = path.join(distDir, "server", "edge", "assets");
785
+ try {
786
+ const entries = await fs.readdir(edgeAssetsDir, { withFileTypes: true });
787
+ for (const entry of entries) {
788
+ if (!entry.isFile())
789
+ continue;
790
+ const srcPath = path.join(edgeAssetsDir, entry.name);
791
+ const destPath = path.join(assetsDir, "_next", "edge-assets", entry.name);
792
+ if (!(await safeMkdirForDest(destPath, entry.name)))
793
+ continue;
794
+ try {
795
+ await fs.copyFile(srcPath, destPath);
796
+ count++;
797
+ }
798
+ catch { }
799
+ }
800
+ }
801
+ catch {
802
+ // No edge assets dir — no Turbopack edge assets in this build.
803
+ }
804
+ }
805
+ // Public files (\`<projectDir>/public/*\`). Next.js's adapter API does not
806
+ // expose these via outputs.staticFiles (only \`_next/static/*\` lands there),
807
+ // so we walk the directory ourselves and copy each file to the deployment
808
+ // assets root. Without this, root-level scripts like \`/test1.js\` and
809
+ // \`/favicon.ico\` 404 because the worker has nothing to serve.
810
+ if (projectDir) {
811
+ const publicDir = path.join(projectDir, "public");
812
+ try {
813
+ await fs.access(publicDir);
814
+ const walk = async (dir) => {
815
+ const entries = await fs.readdir(dir, { withFileTypes: true });
816
+ for (const entry of entries) {
817
+ const srcPath = path.join(dir, entry.name);
818
+ if (entry.isDirectory()) {
819
+ await walk(srcPath);
820
+ continue;
821
+ }
822
+ if (!entry.isFile())
823
+ continue;
824
+ const relativeFromPublic = path.relative(publicDir, srcPath);
825
+ const destPath = path.join(assetsDir, relativeFromPublic);
826
+ if (!(await safeMkdirForDest(destPath, relativeFromPublic)))
827
+ continue;
828
+ try {
829
+ await fs.copyFile(srcPath, destPath);
830
+ count++;
831
+ }
832
+ catch { }
833
+ }
834
+ };
835
+ await walk(publicDir);
836
+ }
837
+ catch {
838
+ // No public dir — skip silently
839
+ }
840
+ }
841
+ return count;
842
+ }
843
+ async function addEdgeChunkImportPath(paths, absPath) {
844
+ const importPath = await getSafeEdgeImportPath(absPath);
845
+ if (!paths.includes(importPath)) {
846
+ paths.push(importPath);
847
+ }
848
+ }
849
+ /**
850
+ * Resolve a chunk reference from a Turbopack edge-wrapper's \`otherChunks\`
851
+ * list. Turbopack emits heterogeneous path forms within the same list:
852
+ * - absolute paths: \`/abs/path/.next/server/chunks/foo.js\`
853
+ * - edge-relative: \`chunks/ssr/foo.js\` → \`{distDir}/server/edge/{rel}\`
854
+ * - dist-relative: \`server/chunks/foo.js\` → \`{distDir}/{rel}\`
855
+ *
856
+ * The third form is the one that broke Server Actions — Turbopack emits
857
+ * the server-actions registry chunk (module 3103) into
858
+ * \`.next/server/chunks/ssr/\` but edge-wrappers reference it via
859
+ * \`server/chunks/...\`. Before this fix, all three forms were blindly joined
860
+ * to \`{distDir}/server/edge\`, producing non-existent paths for forms 1 + 3.
861
+ * The chunk was never imported, its module factories never registered, and
862
+ * any request that invoked a server action threw
863
+ * \`Module 3103 was instantiated ... but the module factory is not available\`.
864
+ *
865
+ * Returns null if no candidate exists on disk.
866
+ */
867
+ async function resolveEdgeOtherChunkPath(distDir, rel) {
868
+ const candidates = [];
869
+ if (path.isAbsolute(rel)) {
870
+ candidates.push(rel);
871
+ }
872
+ else {
873
+ candidates.push(path.join(distDir, "server", "edge", rel));
874
+ candidates.push(path.join(distDir, rel));
875
+ }
876
+ for (const cand of candidates) {
877
+ try {
878
+ await fs.access(cand);
879
+ return cand;
880
+ }
881
+ catch { }
882
+ }
883
+ return null;
884
+ }
885
+ async function getSafeEdgeImportPath(absPath) {
886
+ if (!absPath.includes("[") && !absPath.includes("]")) {
887
+ return absPath;
888
+ }
889
+ const dir = path.dirname(absPath);
890
+ const base = path.basename(absPath).replace(/\[/g, "_").replace(/\]/g, "_");
891
+ const safePath = path.join(dir, base);
892
+ try {
893
+ const content = await fs.readFile(absPath, "utf-8");
894
+ await fs.writeFile(safePath, content);
895
+ return safePath;
896
+ }
897
+ catch {
898
+ return absPath;
899
+ }
900
+ }
901
+ async function collectJsFilesRecursive(dir) {
902
+ const entries = await fs.readdir(dir, { withFileTypes: true });
903
+ const files = [];
904
+ for (const entry of entries) {
905
+ const absPath = path.join(dir, entry.name);
906
+ if (entry.isDirectory()) {
907
+ files.push(...await collectJsFilesRecursive(absPath));
908
+ continue;
909
+ }
910
+ if (entry.isFile() && entry.name.endsWith(".js") && !entry.name.endsWith(".js.map")) {
911
+ files.push(absPath);
912
+ }
913
+ }
914
+ return files;
915
+ }
916
+ async function getTotalSize(dir, files) {
917
+ let total = 0;
918
+ for (const f of files) {
919
+ try {
920
+ const stat = await fs.stat(path.join(dir, f));
921
+ total += stat.size;
922
+ }
923
+ catch { }
924
+ }
925
+ return total;
926
+ }
927
+ /**
928
+ * Collect all JSON manifests from .next/ for embedding in the worker.
929
+ * Returns a map of absolute path → file content string.
930
+ */
931
+ async function collectManifests(distDir) {
932
+ const manifests = {};
933
+ // Recursively find all .json files in .next/ and .next/server/
934
+ async function walk(dir) {
935
+ let entries;
936
+ try {
937
+ entries = await fs.readdir(dir, { withFileTypes: true });
938
+ }
939
+ catch {
940
+ return;
941
+ }
942
+ for (const entry of entries) {
943
+ const fullPath = path.join(dir, entry.name);
944
+ if (entry.isDirectory()) {
945
+ // Skip large top-level directories that don't contain manifests.
946
+ // Match on full relative path, not just name: a user route like
947
+ // \`app/static/[slug]\` is a legitimate app directory whose
948
+ // \`page_client-reference-manifest.js\` must be collected so the
949
+ // Flight renderer can resolve its client components at request time
950
+ // — skipping by basename alone drops those manifests and surfaces
951
+ // as "Could not find the module ... in the React Client Manifest"
952
+ // on middleware rewrites into that route.
953
+ const rel = path.relative(distDir, fullPath);
954
+ if (rel === "static" || rel === "cache" || rel === "server/chunks" || rel === "server/edge-chunks")
955
+ continue;
956
+ await walk(fullPath);
957
+ }
958
+ else if (entry.name === "BUILD_ID" || entry.name === "package.json") {
959
+ manifests[fullPath] = await fs.readFile(fullPath, "utf-8").catch(() => "");
960
+ }
961
+ else if (entry.name.endsWith(".json") || entry.name.endsWith(".js")) {
962
+ // Skip non-essential files that bloat the worker entry:
963
+ // - .nft.json (file tracing, not needed at runtime)
964
+ // - .segments files
965
+ // - page.js / route.js (handler code, imported separately)
966
+ // - client/route handler code (imported separately)
967
+ if (entry.name.endsWith(".nft.json"))
968
+ continue;
969
+ if (entry.name.endsWith(".segments"))
970
+ continue;
971
+ if (entry.name === "page.js" || entry.name === "route.js")
972
+ continue;
973
+ try {
974
+ const stat = await fs.stat(fullPath);
975
+ // Skip files > 512KB (not manifests)
976
+ if (stat.size < 512_000) {
977
+ manifests[fullPath] = await fs.readFile(fullPath, "utf-8");
978
+ }
979
+ }
980
+ catch { }
981
+ }
982
+ }
983
+ }
984
+ await walk(distDir);
985
+ // Also collect the required-server-files manifest
986
+ const reqServerFiles = path.join(distDir, "required-server-files.json");
987
+ try {
988
+ manifests[reqServerFiles] = await fs.readFile(reqServerFiles, "utf-8");
989
+ }
990
+ catch { }
991
+ return manifests;
992
+ }
993
+ /**
994
+ * Collect non-code user files that route handlers may read via fs.readFileSync.
995
+ *
996
+ * Walks every output's `assets` map (Next.js file-trace results), filters out
997
+ * node_modules and code files, and reads the remainder into two maps:
998
+ * - text files (.json, .txt, .yaml, etc.) are kept as utf-8 strings in the
999
+ * `text` map, indexed by fileOutputPath (relative to outputFileTracingRoot).
1000
+ * - binary files (fonts, images, wasm, etc.) are base64-encoded into the
1001
+ * `binary` map with a `__CREEK_B64__` prefix so the fs shim can detect
1002
+ * them and decode to Uint8Array on read. This supports patterns like
1003
+ * `next/og` Node-runtime route handlers that do
1004
+ * `fs.readFile(join(cwd, 'assets/foo.ttf'))`.
1005
+ *
1006
+ * Size cap (2MB total across both maps) prevents accidentally bloating the
1007
+ * worker bundle when a project has large data assets — the user can hit it
1008
+ * explicitly to force a different deployment strategy.
1009
+ */
1010
+ async function collectUserFiles(outputs) {
1011
+ const TEXT_EXTENSIONS = new Set([
1012
+ ".json", ".txt", ".yaml", ".yml", ".md", ".csv", ".xml",
1013
+ ".html", ".htm", ".sql", ".graphql", ".gql", ".env",
1014
+ ]);
1015
+ const BINARY_EXTENSIONS = new Set([
1016
+ ".ttf", ".otf", ".woff", ".woff2", ".eot",
1017
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".ico",
1018
+ ".svg", ".wasm", ".pdf",
1019
+ // SQLite databases — our sqlite3 shim (via sql.js) reads these at
1020
+ // request time when getStaticProps fallback runs on the worker.
1021
+ ".sqlite", ".sqlite3", ".db",
1022
+ ]);
1023
+ const MAX_TOTAL_BYTES = 10 * 1024 * 1024; // 10MB cap (base64 inflates binary)
1024
+ const files = {};
1025
+ let totalBytes = 0;
1026
+ const allOutputs = [
1027
+ ...outputs.appPages,
1028
+ ...outputs.appRoutes,
1029
+ ...outputs.pages,
1030
+ ...outputs.pagesApi,
1031
+ ];
1032
+ for (const output of allOutputs) {
1033
+ const assets = output.assets;
1034
+ if (!assets)
1035
+ continue;
1036
+ for (const [fileOutputPath, sourceFile] of Object.entries(assets)) {
1037
+ // Skip already-collected (different outputs may share the same asset)
1038
+ if (files[fileOutputPath])
1039
+ continue;
1040
+ const ext = path.extname(fileOutputPath).toLowerCase();
1041
+ const isDeclarationFile = fileOutputPath.endsWith(".d.ts");
1042
+ const isText = TEXT_EXTENSIONS.has(ext) || isDeclarationFile;
1043
+ const isBinary = BINARY_EXTENSIONS.has(ext);
1044
+ if (!isText && !isBinary)
1045
+ continue;
1046
+ // Skip JS/TS dependencies from node_modules — esbuild already bundles
1047
+ // those. But keep BINARY assets even when they live in node_modules:
1048
+ // \`next/og\` (node runtime) reads \`@vercel/og/resvg.wasm\` +
1049
+ // \`Geist-Regular.ttf\` via \`fs.readFileSync(fileURLToPath(...))\` at
1050
+ // request time — those bytes have to be available through our fs
1051
+ // shim or the route handler 500s. Same for other libs that ship
1052
+ // fonts/wasm as sibling assets.
1053
+ // Fixes og-api node-runtime (\`/og-node\`) and
1054
+ // use-cache-metadata-route-handler opengraph/icon image tests.
1055
+ if (fileOutputPath.includes("node_modules/") && !isBinary && !isDeclarationFile)
1056
+ continue;
1057
+ try {
1058
+ if (isText) {
1059
+ const content = await fs.readFile(sourceFile, "utf-8");
1060
+ if (totalBytes + content.length > MAX_TOTAL_BYTES) {
1061
+ console.warn(` [Creek Adapter] User-files size cap reached (${MAX_TOTAL_BYTES} bytes); skipping ${fileOutputPath}`);
1062
+ continue;
1063
+ }
1064
+ files[fileOutputPath] = content;
1065
+ totalBytes += content.length;
1066
+ }
1067
+ else {
1068
+ // Binary path: base64-encode with a sentinel prefix so the fs
1069
+ // shim can detect and decode.
1070
+ const buffer = await fs.readFile(sourceFile);
1071
+ const encoded = "__CREEK_B64__" + buffer.toString("base64");
1072
+ if (totalBytes + encoded.length > MAX_TOTAL_BYTES) {
1073
+ console.warn(` [Creek Adapter] User-files size cap reached (${MAX_TOTAL_BYTES} bytes); skipping ${fileOutputPath}`);
1074
+ continue;
1075
+ }
1076
+ files[fileOutputPath] = encoded;
1077
+ totalBytes += encoded.length;
1078
+ }
1079
+ }
1080
+ catch {
1081
+ // Skip files we can't read — they may have been excluded by tracing
1082
+ }
1083
+ }
1084
+ }
1085
+ return files;
1086
+ }
1087
+ async function collectFallbackShellRoutes(distDir) {
1088
+ try {
1089
+ const raw = await fs.readFile(path.join(distDir, "prerender-manifest.json"), "utf-8");
1090
+ const manifest = JSON.parse(raw);
1091
+ const dynamicRoutes = manifest?.dynamicRoutes && typeof manifest.dynamicRoutes === "object"
1092
+ ? manifest.dynamicRoutes
1093
+ : {};
1094
+ const out = new Set();
1095
+ for (const [pathname, entry] of Object.entries(dynamicRoutes)) {
1096
+ if (typeof pathname === "string" &&
1097
+ typeof entry?.fallback === "string" &&
1098
+ entry.fallback.length > 0) {
1099
+ out.add(pathname);
1100
+ }
1101
+ }
1102
+ return out;
1103
+ }
1104
+ catch {
1105
+ return null;
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Collect prerender entries from build outputs for App Router PPR/cache seeding.
1110
+ * Pages Router prerenders are served from assets and don't need to be embedded
1111
+ * in the worker bundle.
1112
+ */
1113
+ async function collectPrerenderEntries(outputs, fallbackShellRoutes) {
1114
+ const entries = [];
1115
+ // The Next adapter emits one \`prerenders\` entry per output file — including
1116
+ // \`.rsc\` sidecars and \`.segments/*.segment.rsc\` fragments. Those aren't
1117
+ // standalone page cache entries, they're assets fetched via the page seed.
1118
+ // Filter to \`.html\` fallbacks so the prerender map only indexes actual
1119
+ // page keys (e.g. \`/memory-pressure/30\`, not \`/memory-pressure/30.rsc\`).
1120
+ for (const prerender of outputs.prerenders) {
1121
+ const fallback = prerender.fallback;
1122
+ if (!fallback?.filePath || !fallback.filePath.endsWith(".html"))
1123
+ continue;
1124
+ const hasPostponedState = typeof fallback.postponedState === "string" &&
1125
+ fallback.postponedState.length > 0;
1126
+ const hasPprHeaders = !!prerender.pprChain?.headers;
1127
+ const isPprChain = hasPostponedState || hasPprHeaders;
1128
+ // Skip bracket-form fallback shells — they're handled via the
1129
+ // \`__CREEK_POSTPONED_BY_SHELL\` regex map, not as direct page seeds.
1130
+ if (!isPprChain && prerender.pathname.includes("["))
1131
+ continue;
1132
+ try {
1133
+ const stat = await fs.stat(fallback.filePath).catch(() => null);
1134
+ const metaPath = fallback.filePath.replace(/\.(html|body)$/, ".meta");
1135
+ const meta = await fs.readFile(metaPath, "utf-8")
1136
+ .then((raw) => JSON.parse(raw))
1137
+ .catch(() => null);
1138
+ // Never inline HTML into the worker bundle — it can be multi-MB
1139
+ // (e.g. memory-pressure pages are ~2MB each). \`__creekSeededAppPageEntry\`
1140
+ // fetches HTML and RSC from the assets bucket at request time. The seed
1141
+ // only carries the metadata needed to reconstruct the cache entry shape.
1142
+ entries.push({
1143
+ pathname: prerender.pathname,
1144
+ html: "",
1145
+ postponedState: fallback.postponedState,
1146
+ allowsFallbackShellResume: fallbackShellRoutes
1147
+ ? fallbackShellRoutes.has(prerender.pathname)
1148
+ : undefined,
1149
+ initialRevalidate: fallback.initialRevalidate,
1150
+ initialStatus: fallback.initialStatus,
1151
+ initialHeaders: fallback.initialHeaders,
1152
+ initialExpiration: fallback.initialExpiration,
1153
+ pprHeaders: prerender.pprChain?.headers,
1154
+ lastModified: stat?.mtimeMs,
1155
+ segmentPaths: Array.isArray(meta?.segmentPaths) ? meta.segmentPaths : undefined,
1156
+ metaHeaders: meta?.headers && typeof meta.headers === "object"
1157
+ ? meta.headers
1158
+ : undefined,
1159
+ });
1160
+ }
1161
+ catch {
1162
+ // Skip prerenders whose fallback file can't be read
1163
+ }
1164
+ }
1165
+ return entries;
1166
+ }
1167
+ /**
1168
+ * Extract \`'use cache'\` entries from every prerender's postponedState.
1169
+ *
1170
+ * Next.js serializes postponedState as \`<len>:<postponedString><base64ZlibBlob>\`
1171
+ * where the tail is a zlib-compressed JSON containing the cache, fetch, and
1172
+ * encryptedBoundArgs stores. We decompress and pull out the cache entries so
1173
+ * the worker can hand them to CreekComposableCacheHandler at init — meaning
1174
+ * root-layout \`'use cache'\` values computed at build time ("buildtime"
1175
+ * sentinel) survive into runtime GETs without a full PPR resume.
1176
+ *
1177
+ * Duplicate cache keys (e.g. root layout shared across many shells) dedupe
1178
+ * naturally by Map key — the last seen wins, which is fine since they're
1179
+ * semantically identical.
1180
+ */
1181
+ async function collectComposableCacheSeeds(outputs, fallbackShellRoutes) {
1182
+ const zlib = await import("node:zlib");
1183
+ // Map bracket-form pathname → its cache entries. Gating by shell prevents
1184
+ // seeds from one prerender's request-scoped RDC from bleeding into
1185
+ // unrelated requests (e.g. \`/with-suspense/*\`'s build-time "buildtime"
1186
+ // leaking into \`/without-suspense/*\` where the test expects "runtime").
1187
+ const byShell = new Map();
1188
+ for (const prerender of outputs.prerenders) {
1189
+ if (prerender.pathname.includes("[") &&
1190
+ fallbackShellRoutes &&
1191
+ !fallbackShellRoutes.has(prerender.pathname)) {
1192
+ continue;
1193
+ }
1194
+ const postponed = prerender.fallback?.postponedState;
1195
+ if (typeof postponed !== "string" || postponed.length === 0)
1196
+ continue;
1197
+ const m = postponed.match(/^(\d+):/);
1198
+ if (!m)
1199
+ continue;
1200
+ const prefixLen = m[0].length;
1201
+ const postponedLen = parseInt(m[1], 10);
1202
+ const cacheBlob = postponed.slice(prefixLen + postponedLen);
1203
+ if (!cacheBlob || cacheBlob === "null")
1204
+ continue;
1205
+ try {
1206
+ const buf = Buffer.from(cacheBlob, "base64");
1207
+ const inflated = zlib.inflateSync(buf, { maxOutputLength: 200 * 1024 * 1024 });
1208
+ const json = JSON.parse(inflated.toString("utf-8"));
1209
+ const cacheStore = json?.store?.cache;
1210
+ if (!cacheStore || typeof cacheStore !== "object")
1211
+ continue;
1212
+ const shellSeeds = [];
1213
+ for (const [key, serialized] of Object.entries(cacheStore)) {
1214
+ if (!serialized?.entry)
1215
+ continue;
1216
+ const e = serialized.entry;
1217
+ shellSeeds.push({
1218
+ key,
1219
+ value: e.value ?? "",
1220
+ tags: Array.isArray(e.tags) ? e.tags : [],
1221
+ stale: typeof e.stale === "number" ? e.stale : 0,
1222
+ timestamp: typeof e.timestamp === "number" ? e.timestamp : Date.now(),
1223
+ expire: typeof e.expire === "number" ? e.expire : Number.MAX_SAFE_INTEGER,
1224
+ revalidate: typeof e.revalidate === "number" ? e.revalidate : Number.MAX_SAFE_INTEGER,
1225
+ });
1226
+ }
1227
+ if (shellSeeds.length > 0)
1228
+ byShell.set(prerender.pathname, shellSeeds);
1229
+ }
1230
+ catch { }
1231
+ }
1232
+ return byShell;
1233
+ }
1234
+ /**
1235
+ * Scan built Turbopack chunks for the \`await e.y("<specifier>")\`
1236
+ * externalImport calls Turbopack emits for modules it can't bundle
1237
+ * (Node-specific libs like \`@vercel/og/index.node.js\`). workerd refuses
1238
+ * to resolve those at runtime, so we collect the specifiers and
1239
+ * statically \`import\` them from our worker entry instead — wrangler
1240
+ * then bundles them, and our patched externalImport returns the cached
1241
+ * module from \`globalThis.__CREEK_EXT_MODS\`.
1242
+ */
1243
+ async function collectExternalizedModules(distDir) {
1244
+ const found = new Set();
1245
+ const scanDir = async (dir) => {
1246
+ try {
1247
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1248
+ for (const e of entries) {
1249
+ const full = path.join(dir, e.name);
1250
+ if (e.isDirectory())
1251
+ await scanDir(full);
1252
+ else if (e.name.endsWith(".js")) {
1253
+ try {
1254
+ const content = await fs.readFile(full, "utf-8");
1255
+ const matches = content.matchAll(/\w+\.y\("([^"]+)"\)/g);
1256
+ for (const m of matches)
1257
+ found.add(m[1]);
1258
+ }
1259
+ catch { }
1260
+ }
1261
+ }
1262
+ }
1263
+ catch { }
1264
+ };
1265
+ await scanDir(path.join(distDir, "server", "chunks"));
1266
+ await scanDir(path.join(distDir, "server", "edge", "chunks"));
1267
+ const projectRoot = path.dirname(distDir);
1268
+ return Promise.all(Array.from(found).map(async (id) => ({
1269
+ id,
1270
+ importSpecifier: await resolveExternalImportSpecifier(projectRoot, id),
1271
+ })));
1272
+ }
1273
+ function stripTurbopackPackageAlias(specifier) {
1274
+ const parts = specifier.split("/");
1275
+ const packageIndex = specifier.startsWith("@") ? 1 : 0;
1276
+ const packageName = parts[packageIndex];
1277
+ if (!packageName)
1278
+ return specifier;
1279
+ const match = /^(.*)-[0-9a-f]{16}$/i.exec(packageName);
1280
+ if (!match || !match[1])
1281
+ return specifier;
1282
+ parts[packageIndex] = match[1];
1283
+ return parts.join("/");
1284
+ }
1285
+ function splitPackageSpecifier(specifier) {
1286
+ if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("node:"))
1287
+ return null;
1288
+ const parts = specifier.split("/");
1289
+ if (specifier.startsWith("@")) {
1290
+ if (parts.length < 2 || !parts[0] || !parts[1])
1291
+ return null;
1292
+ return {
1293
+ packageName: `${parts[0]}/${parts[1]}`,
1294
+ subpath: parts.length > 2 ? `./${parts.slice(2).join("/")}` : ".",
1295
+ };
1296
+ }
1297
+ if (!parts[0])
1298
+ return null;
1299
+ return {
1300
+ packageName: parts[0],
1301
+ subpath: parts.length > 1 ? `./${parts.slice(1).join("/")}` : ".",
1302
+ };
1303
+ }
1304
+ async function resolveExternalImportSpecifier(projectRoot, runtimeSpecifier) {
1305
+ const unaliased = stripTurbopackPackageAlias(runtimeSpecifier);
1306
+ const split = splitPackageSpecifier(unaliased);
1307
+ if (!split)
1308
+ return unaliased;
1309
+ const packageJsonPath = path.join(projectRoot, "node_modules", ...split.packageName.split("/"), "package.json");
1310
+ let packageJson;
1311
+ try {
1312
+ packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
1313
+ }
1314
+ catch {
1315
+ return unaliased;
1316
+ }
1317
+ const exportTarget = resolvePackageExportTarget(packageJson.exports, split.subpath);
1318
+ if (exportTarget && !exportTarget.startsWith(".") && !exportTarget.startsWith("/")) {
1319
+ return unaliased;
1320
+ }
1321
+ if (exportTarget) {
1322
+ return path.join(path.dirname(packageJsonPath), exportTarget);
1323
+ }
1324
+ if (split.subpath === "." && typeof packageJson.module === "string") {
1325
+ return path.join(path.dirname(packageJsonPath), packageJson.module);
1326
+ }
1327
+ if (split.subpath === "." && typeof packageJson.main === "string") {
1328
+ return path.join(path.dirname(packageJsonPath), packageJson.main);
1329
+ }
1330
+ return unaliased;
1331
+ }
1332
+ function resolvePackageExportTarget(exportsField, subpath) {
1333
+ if (!exportsField)
1334
+ return null;
1335
+ if (typeof exportsField === "string" || Array.isArray(exportsField)) {
1336
+ return subpath === "." ? pickConditionalExportTarget(exportsField) : null;
1337
+ }
1338
+ if (typeof exportsField !== "object")
1339
+ return null;
1340
+ const exportsObj = exportsField;
1341
+ const hasSubpathKeys = Object.keys(exportsObj).some((key) => key === "." || key.startsWith("./"));
1342
+ const selected = hasSubpathKeys ? exportsObj[subpath] : subpath === "." ? exportsField : undefined;
1343
+ return pickConditionalExportTarget(selected);
1344
+ }
1345
+ function pickConditionalExportTarget(value) {
1346
+ if (typeof value === "string")
1347
+ return value;
1348
+ if (Array.isArray(value)) {
1349
+ for (const item of value) {
1350
+ const target = pickConditionalExportTarget(item);
1351
+ if (target)
1352
+ return target;
1353
+ }
1354
+ return null;
1355
+ }
1356
+ if (!value || typeof value !== "object")
1357
+ return null;
1358
+ const conditions = value;
1359
+ for (const condition of ["node", "import", "module", "default"]) {
1360
+ if (Object.prototype.hasOwnProperty.call(conditions, condition)) {
1361
+ const target = pickConditionalExportTarget(conditions[condition]);
1362
+ if (target)
1363
+ return target;
1364
+ }
1365
+ }
1366
+ for (const [condition, targetValue] of Object.entries(conditions)) {
1367
+ if (condition === "types" || condition === "browser" || condition === "require")
1368
+ continue;
1369
+ const target = pickConditionalExportTarget(targetValue);
1370
+ if (target)
1371
+ return target;
1372
+ }
1373
+ return null;
1374
+ }
1375
+ function formatSize(bytes) {
1376
+ if (bytes < 1024)
1377
+ return `${bytes}B`;
1378
+ if (bytes < 1024 * 1024)
1379
+ return `${Math.round(bytes / 1024)}KB`;
1380
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
1381
+ }
1382
+ //# sourceMappingURL=build.js.map