@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/LICENSE +190 -0
- package/README.md +184 -0
- package/dist/build.d.ts +57 -0
- package/dist/build.js +1382 -0
- package/dist/bundler.d.ts +20 -0
- package/dist/bundler.js +991 -0
- package/dist/cache-handler.d.ts +32 -0
- package/dist/cache-handler.js +100 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +247 -0
- package/dist/manifest.d.ts +40 -0
- package/dist/manifest.js +27 -0
- package/dist/worker-entry.d.ts +133 -0
- package/dist/worker-entry.js +7734 -0
- package/package.json +64 -0
- package/src/shims/als-polyfill.js +7 -0
- package/src/shims/critters.js +7 -0
- package/src/shims/empty.js +2 -0
- package/src/shims/env.js +3 -0
- package/src/shims/fast-set-immediate.js +285 -0
- package/src/shims/fs.js +225 -0
- package/src/shims/http.js +240 -0
- package/src/shims/image-optimizer.js +18 -0
- package/src/shims/load-manifest.js +123 -0
- package/src/shims/opentelemetry.js +229 -0
- package/src/shims/sharp.js +12 -0
- package/src/shims/sqlite3-binding.js +517 -0
- package/src/shims/track-module-loading.js +68 -0
- package/src/shims/vm.js +49 -0
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
|