@timber-js/app 0.2.0-alpha.160 → 0.2.0-alpha.163
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/_chunks/{actions-C9yAuoX3.js → actions-cjklt63G.js} +7 -4
- package/dist/_chunks/{actions-C9yAuoX3.js.map → actions-cjklt63G.js.map} +1 -1
- package/dist/_chunks/{cache-api-swbefQ6F.js → cache-api-CzYUlgXA.js} +2 -2
- package/dist/_chunks/{cache-api-swbefQ6F.js.map → cache-api-CzYUlgXA.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-BhYDz_-x.js → cli-schema-sync-NfLbLnDw.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-BhYDz_-x.js.map → cli-schema-sync-NfLbLnDw.js.map} +1 -1
- package/dist/_chunks/{logger-CPoQkGK6.js → logger-B_O6-mdJ.js} +7 -45
- package/dist/_chunks/logger-B_O6-mdJ.js.map +1 -0
- package/dist/_chunks/{walkers-c8aReVgo.js → walkers-9mz9T7mb.js} +2 -2
- package/dist/_chunks/{walkers-c8aReVgo.js.map → walkers-9mz9T7mb.js.map} +1 -1
- package/dist/adapters/cloudflare.d.ts +13 -13
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +44 -46
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +1 -8
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/types.d.ts +0 -6
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/child-segment-context.d.ts +22 -0
- package/dist/client/child-segment-context.d.ts.map +1 -0
- package/dist/client/child-segment-outlet.d.ts +18 -0
- package/dist/client/child-segment-outlet.d.ts.map +1 -0
- package/dist/client/child-segment-provider.d.ts +21 -0
- package/dist/client/child-segment-provider.d.ts.map +1 -0
- package/dist/client/internal.js +1 -2
- package/dist/client/internal.js.map +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +5 -1
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.js +256 -9
- package/dist/index.js.map +1 -1
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/callsite-ast.d.ts +27 -5
- package/dist/plugins/callsite-ast.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/prebuilt-capture.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +1 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +85 -10
- package/dist/server/internal.js.map +1 -1
- package/dist/server/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/prebuilt-runtime.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +5 -12
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +7 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +2 -2
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +26 -3
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/adapters/cloudflare.ts +41 -63
- package/src/adapters/nitro.ts +0 -11
- package/src/adapters/types.ts +0 -7
- package/src/client/child-segment-context.ts +40 -0
- package/src/client/child-segment-outlet.tsx +25 -0
- package/src/client/child-segment-provider.tsx +27 -0
- package/src/client/segment-cache.ts +4 -5
- package/src/client/use-cookie.ts +11 -1
- package/src/plugins/cache.ts +44 -1
- package/src/plugins/callsite-ast.ts +310 -5
- package/src/plugins/fonts.ts +8 -0
- package/src/plugins/prebuilt-capture.ts +7 -0
- package/src/plugins/shims.ts +24 -0
- package/src/server/actions.ts +10 -1
- package/src/server/als-registry.ts +1 -0
- package/src/server/index.ts +1 -1
- package/src/server/prebuilt-builder.ts +120 -42
- package/src/server/prebuilt-runtime.ts +24 -1
- package/src/server/primitives.ts +5 -20
- package/src/server/route-element-builder.ts +113 -79
- package/src/server/rsc-entry/helpers.ts +12 -10
- package/src/server/rsc-entry/index.ts +40 -40
- package/src/server/rsc-entry/render-route.ts +6 -2
- package/src/server/rsc-entry/rsc-payload.ts +2 -2
- package/src/server/server-only-guard-noop.js +3 -0
- package/src/server/state-tree-diff.ts +49 -4
- package/dist/_chunks/logger-CPoQkGK6.js.map +0 -1
|
@@ -101,6 +101,10 @@ function cloudflare(options = {}) {
|
|
|
101
101
|
}
|
|
102
102
|
const prebuiltData = await readPrebuiltData(buildDir);
|
|
103
103
|
await writeFile(join(outDir, "_timber-prebuilt-source.js"), generatePrebuiltSourceModule(prebuiltData));
|
|
104
|
+
await rm(join(outDir, "prebuilt"), {
|
|
105
|
+
recursive: true,
|
|
106
|
+
force: true
|
|
107
|
+
}).catch(() => {});
|
|
104
108
|
await writeFile(join(outDir, "_timber-runtime-init.js"), generateRuntimeInitModule());
|
|
105
109
|
const workerEntry = generateWorkerEntry(outDir, outDir, !!config.manifestInit, hasWorkerHandlers);
|
|
106
110
|
await writeFile(join(outDir, "_worker.js"), workerEntry);
|
|
@@ -219,41 +223,47 @@ ${hasWorkerHandlers ? " ...workerHandlers,\n" : ""} async fetch(request, env,
|
|
|
219
223
|
`;
|
|
220
224
|
}
|
|
221
225
|
/**
|
|
222
|
-
* Discover prebuilt flight payload files from the build directory.
|
|
226
|
+
* Discover and read prebuilt flight payload files from the build directory.
|
|
223
227
|
* Returns null when no prebuilt artifacts exist (no cache.component usage).
|
|
224
228
|
*
|
|
225
229
|
* @internal Exported for testing.
|
|
226
230
|
*/
|
|
227
231
|
async function readPrebuiltData(buildDir) {
|
|
228
232
|
const prebuiltDir = join(buildDir, "prebuilt");
|
|
233
|
+
let manifestJson;
|
|
229
234
|
try {
|
|
230
|
-
await readFile(join(prebuiltDir, "manifest.json"));
|
|
235
|
+
manifestJson = await readFile(join(prebuiltDir, "manifest.json"), "utf-8");
|
|
231
236
|
} catch {
|
|
232
237
|
return null;
|
|
233
238
|
}
|
|
234
|
-
const
|
|
239
|
+
const payloads = [];
|
|
235
240
|
try {
|
|
236
241
|
const componentDirs = await readdir(prebuiltDir, { withFileTypes: true });
|
|
237
242
|
for (const dir of componentDirs) {
|
|
238
243
|
if (!dir.isDirectory()) continue;
|
|
239
244
|
const files = await readdir(join(prebuiltDir, dir.name));
|
|
240
|
-
for (const file of files) if (file.endsWith(".flight"))
|
|
245
|
+
for (const file of files) if (file.endsWith(".flight")) {
|
|
246
|
+
const filePath = `${dir.name}/${file}`;
|
|
247
|
+
const bytes = await readFile(join(prebuiltDir, filePath));
|
|
248
|
+
payloads.push({
|
|
249
|
+
file: filePath,
|
|
250
|
+
bytes
|
|
251
|
+
});
|
|
252
|
+
}
|
|
241
253
|
}
|
|
242
254
|
} catch {}
|
|
243
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
manifestJson,
|
|
257
|
+
payloads
|
|
258
|
+
};
|
|
244
259
|
}
|
|
245
260
|
/**
|
|
246
261
|
* Generate the prebuilt payload source module for Cloudflare Workers.
|
|
247
262
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* `find_additional_modules: true` with Data/Text rules.
|
|
253
|
-
*
|
|
254
|
-
* This is a separate ESM module so it evaluates before the RSC handler —
|
|
255
|
-
* static imports are hoisted, guaranteeing `globalThis.__TIMBER_PREBUILT_SOURCE__`
|
|
256
|
-
* is set before `createRequestHandler()` reads it.
|
|
263
|
+
* Inlines manifest JSON and flight payload bytes directly into the module
|
|
264
|
+
* so they're part of the worker's JS bundle — no external Text/Data module
|
|
265
|
+
* imports needed. This avoids depending on wrangler's find_additional_modules
|
|
266
|
+
* to discover non-JS files, which doesn't reliably resolve Text/Data imports.
|
|
257
267
|
*
|
|
258
268
|
* @internal Exported for testing.
|
|
259
269
|
*/
|
|
@@ -265,21 +275,22 @@ globalThis.__TIMBER_PREBUILT_SOURCE__ = {
|
|
|
265
275
|
async readPayload() { return null },
|
|
266
276
|
}
|
|
267
277
|
`;
|
|
268
|
-
const imports = [];
|
|
269
278
|
const entries = [];
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
imports.push(`import _p${i} from './prebuilt/${file}'`);
|
|
274
|
-
entries.push(` ${JSON.stringify(file)}: new Uint8Array(_p${i})`);
|
|
279
|
+
for (const { file, bytes } of data.payloads) {
|
|
280
|
+
const b64 = bytes.toString("base64");
|
|
281
|
+
entries.push(` ${JSON.stringify(file)}: ${JSON.stringify(b64)}`);
|
|
275
282
|
}
|
|
276
283
|
return `// Generated by @timber-js/app/adapters/cloudflare
|
|
277
|
-
// Prebuilt payload source —
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
${imports.join("\n")}
|
|
284
|
+
// Prebuilt payload source — inlines cache.component flight payloads
|
|
285
|
+
// directly into the worker bundle (TIM-1162).
|
|
286
|
+
const _manifest = ${data.manifestJson.trim()}
|
|
281
287
|
|
|
282
|
-
|
|
288
|
+
function _decode(b64) {
|
|
289
|
+
const bin = atob(b64)
|
|
290
|
+
const buf = new Uint8Array(bin.length)
|
|
291
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i)
|
|
292
|
+
return buf
|
|
293
|
+
}
|
|
283
294
|
|
|
284
295
|
const _payloads = {
|
|
285
296
|
${entries.join(",\n")}
|
|
@@ -287,7 +298,10 @@ ${entries.join(",\n")}
|
|
|
287
298
|
|
|
288
299
|
globalThis.__TIMBER_PREBUILT_SOURCE__ = {
|
|
289
300
|
async loadManifest() { return _manifest },
|
|
290
|
-
async readPayload(file) {
|
|
301
|
+
async readPayload(file) {
|
|
302
|
+
const b64 = _payloads[file]
|
|
303
|
+
return b64 ? _decode(b64) : null
|
|
304
|
+
},
|
|
291
305
|
}
|
|
292
306
|
`;
|
|
293
307
|
}
|
|
@@ -388,26 +402,17 @@ function isPlainObject(val) {
|
|
|
388
402
|
}
|
|
389
403
|
/** @internal Exported for testing. */
|
|
390
404
|
function generateWranglerConfig(config, options) {
|
|
391
|
-
const compatDate = options.compatibilityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
392
|
-
const flags = options.compatibilityFlags ?? ["nodejs_compat"];
|
|
393
|
-
const PREBUILT_MODULE_RULES = [{
|
|
394
|
-
type: "Text",
|
|
395
|
-
globs: ["prebuilt/**/*.json"]
|
|
396
|
-
}, {
|
|
397
|
-
type: "Data",
|
|
398
|
-
globs: ["prebuilt/**/*.flight"]
|
|
399
|
-
}];
|
|
400
405
|
const base = {
|
|
401
406
|
name: "timber-app",
|
|
402
407
|
main: "_worker.js",
|
|
403
|
-
compatibility_date:
|
|
404
|
-
compatibility_flags:
|
|
408
|
+
compatibility_date: options.compatibilityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
409
|
+
compatibility_flags: options.compatibilityFlags ?? ["nodejs_compat"],
|
|
405
410
|
no_bundle: true,
|
|
406
411
|
find_additional_modules: true,
|
|
407
412
|
rules: [{
|
|
408
413
|
type: "ESModule",
|
|
409
414
|
globs: ["**/*.js"]
|
|
410
|
-
}
|
|
415
|
+
}],
|
|
411
416
|
assets: {
|
|
412
417
|
directory: "./static",
|
|
413
418
|
binding: "ASSETS"
|
|
@@ -419,14 +424,7 @@ function generateWranglerConfig(config, options) {
|
|
|
419
424
|
...base,
|
|
420
425
|
...bindingsConfig
|
|
421
426
|
};
|
|
422
|
-
if (options.wrangler)
|
|
423
|
-
const result = shallowDeepMerge(merged, options.wrangler);
|
|
424
|
-
if (options.wrangler.rules) {
|
|
425
|
-
const userRules = result.rules;
|
|
426
|
-
for (const required of PREBUILT_MODULE_RULES) if (!userRules.some((r) => r.type === required.type && JSON.stringify(r.globs) === JSON.stringify(required.globs))) userRules.push(required);
|
|
427
|
-
}
|
|
428
|
-
return result;
|
|
429
|
-
}
|
|
427
|
+
if (options.wrangler) return shallowDeepMerge(merged, options.wrangler);
|
|
430
428
|
return merged;
|
|
431
429
|
}
|
|
432
430
|
/** @internal Exported for testing. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.js","names":[],"sources":["../../src/adapters/cloudflare.ts"],"sourcesContent":["// Cloudflare Workers adapter\n//\n// Primary deployment target. Generates a Workers-compatible entry point\n// and wrangler.jsonc configuration. See design/11-platform.md §\"Cloudflare Workers\".\n\nimport { writeFile, readFile, cp, rm, readdir } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\nimport { runSharedBuildSteps } from './build-output-helper.js';\n\n// ─── Bindings passthrough ─────────────────────────────────────────────────\n// ALS stores the env object per-request so server components and middleware\n// can access KV, D1, DO, R2, Queues, etc. via getCloudflareBindings().\n// No global fallback — if called outside a request, it throws.\n// See design/11-platform.md §\"Platform Target\" and design/25-production-deployments.md.\n//\n// The ALS is stored on globalThis via Symbol.for so it survives Vite's\n// module instance split in dev mode. The Vite host process (where\n// cloudflareDevBindings → runWithBindings runs) and the RSC module runner\n// (where getCloudflareBindings runs) load separate instances of this file,\n// each with their own module-level variables. globalThis + Symbol.for\n// ensures both share the same ALS — same pattern React uses for\n// Symbol.for('react.element').\n\nconst BINDINGS_ALS_KEY = Symbol.for('timber:cf-bindings-als');\n\nfunction getBindingsAls(): AsyncLocalStorage<Record<string, unknown>> {\n let als = (globalThis as any)[BINDINGS_ALS_KEY] as\n | AsyncLocalStorage<Record<string, unknown>>\n | undefined;\n if (!als) {\n als = new AsyncLocalStorage<Record<string, unknown>>();\n (globalThis as any)[BINDINGS_ALS_KEY] = als;\n }\n return als;\n}\n\n/**\n * Get Cloudflare Worker bindings for the current request.\n *\n * Returns the `env` object passed to the Worker's `fetch` handler,\n * giving direct access to KV, D1, Durable Objects, R2, Queues, and\n * any other bindings configured in `wrangler.jsonc`.\n *\n * Must be called within a request context (server component, middleware,\n * server action). Throws outside a request.\n *\n * @example\n * ```ts\n * import { getCloudflareBindings } from '@timber-js/app/adapters/cloudflare'\n *\n * export default async function Page() {\n * const { MY_KV, MY_DB } = getCloudflareBindings()\n * const data = await MY_KV.get('key')\n * return <div>{data}</div>\n * }\n * ```\n */\nexport function getCloudflareBindings<T = Record<string, unknown>>(): T {\n const env = getBindingsAls().getStore();\n if (!env) {\n throw new Error(\n 'getCloudflareBindings() called outside a Cloudflare Workers request context. ' +\n 'It can only be called from server components, middleware, or server actions ' +\n 'when running on the Cloudflare adapter.'\n );\n }\n return env as T;\n}\n\n/**\n * Run a function with Cloudflare bindings available via getCloudflareBindings().\n * @internal Used by cloudflareDevBindings() to wrap dev-server requests.\n */\nexport function runWithBindings<T>(env: Record<string, unknown>, fn: () => T): T {\n return getBindingsAls().run(env, fn);\n}\n\n// ─── Binding configuration types ──────────────────────────────────────────────\n// Declarative binding config that maps to wrangler.jsonc binding sections.\n// See design/35-cloudflare-primitives.md §\"Binding Declarations in Adapter Config\".\n\n/** KV namespace binding. `id` defaults to `''` (filled in by wrangler or dashboard). */\nexport interface CloudflareKVBinding {\n name: string;\n id?: string;\n}\n\n/** D1 database binding. `database_id` is required (from Cloudflare dashboard). */\nexport interface CloudflareD1Binding {\n name: string;\n database_id: string;\n}\n\n/** R2 bucket binding. `bucket_name` must match the bucket created in Cloudflare. */\nexport interface CloudflareR2Binding {\n name: string;\n bucket_name: string;\n}\n\n/** Queue producer binding. `queue` is the queue name in Cloudflare. */\nexport interface CloudflareQueueProducer {\n name: string;\n queue: string;\n}\n\n/** Queue consumer declaration. Attached to the worker, not a binding name. */\nexport interface CloudflareQueueConsumer {\n queue: string;\n max_batch_size?: number;\n max_retries?: number;\n dead_letter_queue?: string;\n}\n\n/** Durable Object binding. `script_name` is for external DO references. */\nexport interface CloudflareDurableObjectBinding {\n name: string;\n class_name: string;\n script_name?: string;\n}\n\n/**\n * Declarative Cloudflare bindings configuration.\n *\n * These are converted to the appropriate wrangler.jsonc sections\n * (`kv_namespaces`, `d1_databases`, `r2_buckets`, `queues`, `durable_objects`).\n * The `wrangler` escape hatch overrides any generated binding sections\n * if there's a conflict.\n *\n * @example\n * ```ts\n * cloudflare({\n * bindings: {\n * kv: [{ name: 'TIMBER_CACHE' }],\n * d1: [{ name: 'MY_DB', database_id: 'xxxx' }],\n * r2: [{ name: 'MY_BUCKET', bucket_name: 'my-bucket' }],\n * queues: {\n * producers: [{ name: 'EMAIL_QUEUE', queue: 'email-queue' }],\n * consumers: [{ queue: 'email-queue', max_batch_size: 10 }],\n * },\n * durableObjects: [{ name: 'MY_DO', class_name: 'MyDurableObject' }],\n * },\n * })\n * ```\n */\nexport interface CloudflareBindings {\n kv?: CloudflareKVBinding[];\n d1?: CloudflareD1Binding[];\n r2?: CloudflareR2Binding[];\n queues?: {\n producers?: CloudflareQueueProducer[];\n consumers?: CloudflareQueueConsumer[];\n };\n durableObjects?: CloudflareDurableObjectBinding[];\n}\n\n/** Options for the Cloudflare Workers adapter. */\nexport interface CloudflareAdapterOptions {\n /**\n * Cloudflare compatibility date.\n * @default Current date in YYYY-MM-DD format at build time.\n */\n compatibilityDate?: string;\n\n /**\n * Additional compatibility flags.\n * @default ['nodejs_compat']\n */\n compatibilityFlags?: string[];\n\n /**\n * Declarative Cloudflare bindings. Generates the appropriate\n * `kv_namespaces`, `d1_databases`, `r2_buckets`, `queues`, and\n * `durable_objects` sections in wrangler.jsonc.\n *\n * If both `bindings` and `wrangler` specify the same section,\n * `wrangler` wins (it's the escape hatch).\n */\n bindings?: CloudflareBindings;\n\n /**\n * Custom wrangler.jsonc fields to merge.\n * Overrides generated values (including bindings-generated sections).\n */\n wrangler?: Record<string, unknown>;\n\n /**\n * Path to a module that exports additional Worker handlers (queue, scheduled, email, etc.).\n * The module is imported in the generated `_worker.js` and its named exports are\n * spread into the default export alongside `fetch`.\n *\n * The module receives `(batch/event, env, ctx)` — standard Cloudflare handler signatures.\n * It does NOT have access to timber's request pipeline (no ALS, no getCloudflareBindings).\n * Use `env` directly for bindings.\n *\n * @example\n * ```ts\n * // timber.config.ts\n * adapter: cloudflare({ workerHandlers: './src/worker-handlers.ts' })\n *\n * // src/worker-handlers.ts\n * export async function queue(batch, env) { ... }\n * export async function scheduled(controller, env, ctx) { ... }\n * ```\n */\n workerHandlers?: string;\n}\n\n/**\n * Create a Cloudflare Workers adapter.\n *\n * @example\n * ```ts\n * import { cloudflare } from '@timber-js/app/adapters/cloudflare'\n *\n * export default {\n * output: 'server',\n * adapter: cloudflare(),\n * }\n * ```\n */\nexport function cloudflare(options: CloudflareAdapterOptions = {}): TimberPlatformAdapter {\n return {\n name: 'cloudflare',\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'cloudflare');\n\n // Clean stale prebuilt artifacts BEFORE the shared copy step.\n // The shared step uses cp (merge, not replace), so old .flight files\n // from removed/renamed components would persist and get picked up by\n // find_additional_modules. Also clean static/prebuilt/ from pre-TIM-1162\n // builds where prebuilt was copied into static assets.\n await rm(join(outDir, 'prebuilt'), { recursive: true, force: true }).catch(() => {});\n await rm(join(outDir, 'static', 'prebuilt'), { recursive: true, force: true }).catch(\n () => {}\n );\n\n // Steps 1–5: shared across all adapters (mkdir, copy client/rsc/ssr,\n // write _headers, write manifest-init).\n await runSharedBuildSteps({\n config,\n buildDir,\n outDir,\n publicDirName: 'static',\n });\n\n // Compile optional worker handlers (queue, scheduled, etc.)\n // Uses Vite's build API to bundle the TypeScript source into ESM.\n let hasWorkerHandlers = false;\n if (options.workerHandlers) {\n const handlersEntry = join(process.cwd(), options.workerHandlers);\n const { build: viteBuild } = await import('vite');\n await viteBuild({\n configFile: false,\n logLevel: 'info',\n build: {\n lib: {\n entry: handlersEntry,\n formats: ['es'],\n fileName: () => '_worker-handlers.js',\n },\n outDir,\n emptyOutDir: false,\n minify: false,\n rollupOptions: {\n external: [/^node:/, /^cloudflare:/],\n },\n },\n });\n hasWorkerHandlers = true;\n }\n\n // Embed prebuilt flight payloads directly into the worker module\n // (TIM-1162). Previously, prebuilt/ was copied into static assets and\n // read via env.ASSETS.fetch() — but that made .flight files and\n // manifest.json publicly fetchable, bypassing middleware and access\n // gates. Now payloads are imported as native wrangler Data/Text modules\n // from outDir/prebuilt/ (outside static/), keeping them server-side only.\n const prebuiltData = await readPrebuiltData(buildDir);\n await writeFile(\n join(outDir, '_timber-prebuilt-source.js'),\n generatePrebuiltSourceModule(prebuiltData)\n );\n\n // Write the runtime init module — sets process.env.TIMBER_RUNTIME\n // BEFORE the RSC entry evaluates (ESM import order). Without this,\n // createRequestHandler's auto-wire for WorkersCachePurge would not\n // detect the Cloudflare runtime (TIM-1170).\n await writeFile(join(outDir, '_timber-runtime-init.js'), generateRuntimeInitModule());\n\n // Generate the Workers entry point\n const hasManifestInit = !!config.manifestInit;\n const workerEntry = generateWorkerEntry(outDir, outDir, hasManifestInit, hasWorkerHandlers);\n await writeFile(join(outDir, '_worker.js'), workerEntry);\n\n // Generate wrangler.jsonc\n const wranglerConfig = generateWranglerConfig(config, options);\n await writeFile(join(outDir, 'wrangler.jsonc'), JSON.stringify(wranglerConfig, null, 2));\n },\n\n async preview(_config: TimberConfig, buildDir: string) {\n const cmd = generatePreviewCommand(buildDir);\n await spawnPreviewProcess(cmd.command, cmd.args, cmd.cwd);\n },\n\n // No adapter-level waitUntil: on Workers there is no ambient\n // ExecutionContext outside a request, so support is provided\n // per-request by the generated _worker.js, which bridges\n // ctx.waitUntil() into the ALS via runWithWaitUntil().\n // See design/11-platform.md §\"waitUntil()\".\n };\n}\n\n// ─── Exported helpers (used by tests and build) ─────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateWorkerEntry(\n buildDir: string,\n outDir: string,\n hasManifestInit = false,\n hasWorkerHandlers = false\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // The Vite RSC plugin outputs it to rsc/index.js.\n let rscEntryRelative = relative(outDir, join(buildDir, 'rsc', 'index.js'));\n // Ensure the import path starts with ./ for ESM compatibility\n if (!rscEntryRelative.startsWith('.')) {\n rscEntryRelative = './' + rscEntryRelative;\n }\n\n // Build manifest init must be imported before the RSC handler so that\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.\n // ESM guarantees imports are evaluated in order.\n const manifestImport = hasManifestInit ? \"import './_timber-manifest-init.js'\\n\" : '';\n\n // Optional additional Worker handlers (queue, scheduled, email, etc.)\n // Compiled by buildOutput via Vite's build API into _worker-handlers.js.\n // The module's named exports are spread into the default export alongside fetch.\n const handlersImport = hasWorkerHandlers\n ? \"import * as workerHandlers from './_worker-handlers.js'\\n\"\n : '';\n const handlersSpread = hasWorkerHandlers ? ' ...workerHandlers,\\n' : '';\n\n return `// Generated by @timber-js/app/adapters/cloudflare\n// Do not edit — this file is regenerated on each build.\n\nimport { AsyncLocalStorage } from 'node:async_hooks'\n// Side-effect imports: evaluated IN ORDER before the _worker.js body runs.\n// _timber-runtime-init.js sets TIMBER_RUNTIME before createRequestHandler()\n// runs (during the RSC entry's top-level await) so the auto-wire for\n// WorkersCachePurge can detect the Cloudflare runtime (TIM-1170).\n// _timber-prebuilt-source.js sets globalThis.__TIMBER_PREBUILT_SOURCE__\n// for cache.component payloads.\nimport './_timber-runtime-init.js'\nimport './_timber-prebuilt-source.js'\n${manifestImport}import handler, { runWithWaitUntil, runWithWorkersCachePurge } from '${rscEntryRelative}'\n${handlersImport}\n\n// Bind Cloudflare env to ALS so getCloudflareBindings() works at runtime.\n// Uses the same Symbol.for key as getCloudflareBindings() reads from.\nconst ALS_KEY = Symbol.for('timber:cf-bindings-als')\nlet bindingsAls = globalThis[ALS_KEY]\nif (!bindingsAls) {\n bindingsAls = new AsyncLocalStorage()\n globalThis[ALS_KEY] = bindingsAls\n}\n\n// Register timber's platform tracer so pipeline phase spans (timber.proxy,\n// timber.middleware, timber.render, timber.access, timber.action, …) appear\n// as native Cloudflare spans in the Observability dashboard, nested under\n// the auto-instrumented fetch/KV/D1 spans. Requires observability.traces\n// enabled in wrangler.jsonc (generated by default).\n//\n// Lazily imported and guarded: runtimes without the custom spans API\n// (older compatibility dates, older local workerd) fall back to OTEL-only\n// emission. withSpan() in the framework composes both channels.\nconst TRACER_KEY = Symbol.for('timber:platform-tracer')\nlet tracerInit\nfunction initPlatformTracer() {\n tracerInit ??= import('cloudflare:workers')\n .then(({ tracing }) => {\n if (tracing && typeof tracing.enterSpan === 'function') {\n globalThis[TRACER_KEY] ??= {\n enterSpan: (name, fn) => tracing.enterSpan(name, fn),\n }\n }\n })\n .catch(() => {})\n return tracerInit\n}\n\nexport default {\n${handlersSpread} async fetch(request, env, ctx) {\n // One-time on first request; resolved-promise await thereafter.\n await initPlatformTracer()\n\n // Edge-cache GET/HEAD responses via the Cache API. RSC navigation\n // requests (Accept: text/x-component) are excluded — they vary by\n // X-Timber-State-Tree which the cache key doesn't capture.\n const isRsc = request.headers.get('Accept')?.includes('text/x-component')\n const reqCc = request.headers.get('Cache-Control') || ''\n const bypassCache = reqCc.includes('no-cache') || reqCc.includes('max-age=0')\n || request.headers.has('Pragma')\n const cacheable = !isRsc && !bypassCache\n && (request.method === 'GET' || request.method === 'HEAD')\n const cache = cacheable && typeof caches !== 'undefined' ? caches.default : null\n if (cache) {\n const cached = await cache.match(request)\n if (cached) return cached\n }\n\n // Bridge ctx.cache into the per-request ALS so WorkersCachePurge can\n // call ctx.cache.purge({ tags }) during cache.invalidate/revalidateTag.\n // Workers Cache is available on all Cloudflare plans — no API token needed.\n const workersCacheCtx = ctx.cache && typeof ctx.cache.purge === 'function' ? ctx.cache : null\n const runHandler = () =>\n runWithWaitUntil((promise) => ctx.waitUntil(promise), () => handler(request))\n const response = await bindingsAls.run(env, () =>\n workersCacheCtx\n ? runWithWorkersCachePurge(workersCacheCtx, runHandler)\n : runHandler()\n )\n\n // Cache public responses at the edge. A response is edge-cacheable if\n // it has 'public' or 's-maxage' (a shared-cache TTL directive) and no\n // 'no-store' or 'private'. Cloudflare's cache.put() only accepts GET\n // requests and rejects 206, so only store GET 200s. HEAD requests\n // benefit from cache.match() which handles HEAD→GET matching automatically.\n // See design/25-production-deployments.md §\"CDN / Edge Cache\".\n if (cache && request.method === 'GET' && response.status === 200) {\n const cc = response.headers.get('Cache-Control')\n const vary = response.headers.get('Vary')\n if (cc && !cc.includes('no-store') && !cc.includes('private')\n && (cc.includes('public') || cc.includes('s-maxage'))\n && vary !== '*' && !response.headers.has('Set-Cookie')) {\n ctx.waitUntil(cache.put(request, response.clone()))\n }\n }\n\n return response\n }\n}\n`;\n}\n\n/** Build-time prebuilt file list read from `.timber/dist/prebuilt/`. */\nexport interface PrebuiltModuleData {\n /** Payload file paths relative to prebuilt/ (e.g. \"comp_abc/deadbeef.flight\"). */\n payloadFiles: string[];\n}\n\n/**\n * Discover prebuilt flight payload files from the build directory.\n * Returns null when no prebuilt artifacts exist (no cache.component usage).\n *\n * @internal Exported for testing.\n */\nexport async function readPrebuiltData(buildDir: string): Promise<PrebuiltModuleData | null> {\n const prebuiltDir = join(buildDir, 'prebuilt');\n try {\n await readFile(join(prebuiltDir, 'manifest.json'));\n } catch {\n return null;\n }\n const payloadFiles: string[] = [];\n try {\n const componentDirs = await readdir(prebuiltDir, { withFileTypes: true });\n for (const dir of componentDirs) {\n if (!dir.isDirectory()) continue;\n const files = await readdir(join(prebuiltDir, dir.name));\n for (const file of files) {\n if (file.endsWith('.flight')) {\n payloadFiles.push(`${dir.name}/${file}`);\n }\n }\n }\n } catch {\n // No subdirectories — manifest exists but no payloads\n }\n return { payloadFiles };\n}\n\n/**\n * Generate the prebuilt payload source module for Cloudflare Workers.\n *\n * Imports prebuilt flight payloads as native wrangler Data/Text modules\n * from the worker's module graph (NOT from static assets — TIM-1162).\n * Files live at `outDir/prebuilt/` which is outside `static/` and thus\n * not publicly fetchable. Wrangler discovers them via\n * `find_additional_modules: true` with Data/Text rules.\n *\n * This is a separate ESM module so it evaluates before the RSC handler —\n * static imports are hoisted, guaranteeing `globalThis.__TIMBER_PREBUILT_SOURCE__`\n * is set before `createRequestHandler()` reads it.\n *\n * @internal Exported for testing.\n */\nexport function generatePrebuiltSourceModule(data: PrebuiltModuleData | null): string {\n if (!data) {\n return `// Generated by @timber-js/app/adapters/cloudflare\n// No prebuilt artifacts — all cache.component wrappers render dynamically.\nglobalThis.__TIMBER_PREBUILT_SOURCE__ = {\n async loadManifest() { return null },\n async readPayload() { return null },\n}\n`;\n }\n\n const imports: string[] = [];\n const entries: string[] = [];\n\n imports.push(`import _manifestText from './prebuilt/manifest.json'`);\n\n for (let i = 0; i < data.payloadFiles.length; i++) {\n const file = data.payloadFiles[i];\n imports.push(`import _p${i} from './prebuilt/${file}'`);\n entries.push(` ${JSON.stringify(file)}: new Uint8Array(_p${i})`);\n }\n\n return `// Generated by @timber-js/app/adapters/cloudflare\n// Prebuilt payload source — imports cache.component flight payloads as\n// native wrangler modules from the worker bundle (TIM-1162). Files live\n// at outDir/prebuilt/, outside static/, so they are not publicly fetchable.\n${imports.join('\\n')}\n\nconst _manifest = JSON.parse(_manifestText)\n\nconst _payloads = {\n${entries.join(',\\n')}\n}\n\nglobalThis.__TIMBER_PREBUILT_SOURCE__ = {\n async loadManifest() { return _manifest },\n async readPayload(file) { return _payloads[file] ?? null },\n}\n`;\n}\n\n/**\n * Generate the runtime init module that sets TIMBER_RUNTIME before the\n * RSC entry evaluates. Imported as a side-effect module in _worker.js.\n *\n * @internal Exported for testing.\n */\nexport function generateRuntimeInitModule(): string {\n return `// Generated by @timber-js/app/adapters/cloudflare\n// Sets TIMBER_RUNTIME before the RSC handler evaluates (TIM-1170).\n// Must be a separate module imported before rsc/index.js so that ESM\n// evaluation order guarantees the env var is set when\n// createRequestHandler() runs its auto-wire logic.\nglobalThis.process ??= { env: {} }\nprocess.env.TIMBER_RUNTIME = 'cloudflare'\n`;\n}\n\n/** Wrangler binding sections generated from `CloudflareBindings`. */\nexport interface WranglerBindingsConfig {\n kv_namespaces?: Array<{ binding: string; id: string }>;\n d1_databases?: Array<{ binding: string; database_id: string }>;\n r2_buckets?: Array<{ binding: string; bucket_name: string }>;\n queues?: {\n producers?: Array<{ binding: string; queue: string }>;\n consumers?: Array<Record<string, unknown>>;\n };\n durable_objects?: {\n bindings: Array<Record<string, string>>;\n };\n}\n\n/**\n * Convert declarative `CloudflareBindings` into wrangler.jsonc binding sections.\n *\n * Maps:\n * - `kv` → `kv_namespaces`\n * - `d1` → `d1_databases`\n * - `r2` → `r2_buckets`\n * - `queues` → `queues` (producers + consumers)\n * - `durableObjects` → `durable_objects`\n *\n * Empty arrays are omitted from the output.\n *\n * @internal Exported for testing.\n */\nexport function generateBindingsConfig(\n bindings: CloudflareBindings | undefined\n): WranglerBindingsConfig {\n if (!bindings) return {};\n\n const result: WranglerBindingsConfig = {};\n\n // KV namespaces\n if (bindings.kv && bindings.kv.length > 0) {\n result.kv_namespaces = bindings.kv.map((kv) => ({\n binding: kv.name,\n id: kv.id ?? '',\n }));\n }\n\n // D1 databases\n if (bindings.d1 && bindings.d1.length > 0) {\n result.d1_databases = bindings.d1.map((d1) => ({\n binding: d1.name,\n database_id: d1.database_id,\n }));\n }\n\n // R2 buckets\n if (bindings.r2 && bindings.r2.length > 0) {\n result.r2_buckets = bindings.r2.map((r2) => ({\n binding: r2.name,\n bucket_name: r2.bucket_name,\n }));\n }\n\n // Queues (producers and/or consumers)\n if (bindings.queues) {\n const queues: Record<string, unknown> = {};\n if (bindings.queues.producers && bindings.queues.producers.length > 0) {\n queues.producers = bindings.queues.producers.map((p) => ({\n binding: p.name,\n queue: p.queue,\n }));\n }\n if (bindings.queues.consumers && bindings.queues.consumers.length > 0) {\n queues.consumers = bindings.queues.consumers.map((c) => {\n const consumer: Record<string, unknown> = { queue: c.queue };\n if (c.max_batch_size !== undefined) consumer.max_batch_size = c.max_batch_size;\n if (c.max_retries !== undefined) consumer.max_retries = c.max_retries;\n if (c.dead_letter_queue !== undefined) consumer.dead_letter_queue = c.dead_letter_queue;\n return consumer;\n });\n }\n if (Object.keys(queues).length > 0) {\n result.queues = queues;\n }\n }\n\n // Durable Objects\n if (bindings.durableObjects && bindings.durableObjects.length > 0) {\n result.durable_objects = {\n bindings: bindings.durableObjects.map((dobj) => {\n const entry: Record<string, string> = {\n name: dobj.name,\n class_name: dobj.class_name,\n };\n if (dobj.script_name) entry.script_name = dobj.script_name;\n return entry;\n }),\n };\n }\n\n return result;\n}\n\n/**\n * One-level deep merge: for each key in `override`, if both the base and\n * override values are plain objects (not arrays, not null), merge their\n * keys with override winning on conflicts. Everything else (primitives,\n * arrays, null) is replaced outright — matching the intuitive behavior\n * where `kv_namespaces: [...]` replaces fully but\n * `durable_objects: { migrations }` merges with generated `bindings`.\n */\nfunction shallowDeepMerge(\n base: Record<string, unknown>,\n override: Record<string, unknown>\n): Record<string, unknown> {\n const result = { ...base };\n for (const key of Object.keys(override)) {\n const baseVal = result[key];\n const overVal = override[key];\n if (isPlainObject(baseVal) && isPlainObject(overVal)) {\n result[key] = { ...baseVal, ...overVal };\n } else {\n result[key] = overVal;\n }\n }\n return result;\n}\n\n/** True for `{}` literals — false for arrays, null, Date, etc. */\nfunction isPlainObject(val: unknown): val is Record<string, unknown> {\n return val !== null && typeof val === 'object' && !Array.isArray(val);\n}\n\n/** @internal Exported for testing. */\nexport function generateWranglerConfig(\n config: TimberConfig,\n options: CloudflareAdapterOptions\n): Record<string, unknown> {\n const compatDate = options.compatibilityDate ?? new Date().toISOString().slice(0, 10);\n\n const flags = options.compatibilityFlags ?? ['nodejs_compat'];\n\n const PREBUILT_MODULE_RULES = [\n { type: 'Text' as const, globs: ['prebuilt/**/*.json'] },\n { type: 'Data' as const, globs: ['prebuilt/**/*.flight'] },\n ];\n\n const base: Record<string, unknown> = {\n name: 'timber-app',\n main: '_worker.js',\n compatibility_date: compatDate,\n compatibility_flags: flags,\n // The build output is already fully bundled by Vite — skip wrangler's\n // esbuild pass to avoid issues with top-level await and module format.\n no_bundle: true,\n find_additional_modules: true,\n rules: [{ type: 'ESModule', globs: ['**/*.js'] }, ...PREBUILT_MODULE_RULES],\n assets: {\n directory: './static',\n binding: 'ASSETS',\n },\n // Native trace destination — required for timber's pipeline spans\n // (emitted via tracing.enterSpan in the generated _worker.js) to appear\n // in the Cloudflare Observability dashboard. Gates traces only; does\n // not enable Workers Logs. Override via the `wrangler` escape hatch:\n // wrangler: { observability: { traces: { enabled: false } } }.\n observability: {\n traces: { enabled: true },\n },\n };\n\n // Layer 1: merge bindings-generated sections into base\n const bindingsConfig = generateBindingsConfig(options.bindings);\n const merged = { ...base, ...bindingsConfig };\n\n // Layer 2: wrangler escape hatch with deep merge for nested plain objects.\n // A shallow spread would replace entire sections like durable_objects and\n // queues — e.g. adding migrations via wrangler would drop the generated\n // durable_objects.bindings. Deep merge preserves generated keys while\n // letting wrangler override on actual key-level conflicts.\n if (options.wrangler) {\n const result = shallowDeepMerge(merged, options.wrangler);\n // Ensure required prebuilt module rules survive a user `rules` override.\n // Without this, `wrangler: { rules: [...] }` replaces the entire array\n // and drops the Text/Data rules that _timber-prebuilt-source.js needs.\n if (options.wrangler.rules) {\n const userRules = result.rules as Array<Record<string, unknown>>;\n for (const required of PREBUILT_MODULE_RULES) {\n const exists = userRules.some(\n (r) =>\n r.type === required.type && JSON.stringify(r.globs) === JSON.stringify(required.globs)\n );\n if (!exists) userRules.push(required);\n }\n }\n return result;\n }\n\n return merged;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Command descriptor for preview — testable without spawning a process. */\nexport interface PreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generatePreviewCommand(buildDir: string): PreviewCommand {\n const cfDir = join(buildDir, 'cloudflare');\n return {\n command: 'wrangler',\n args: ['dev', '--local', '--config', join(cfDir, 'wrangler.jsonc')],\n cwd: cfDir,\n };\n}\n\n/**\n * Spawn a long-running preview process and pipe stdio to the parent.\n * Resolves when the process exits.\n */\nfunction spawnPreviewProcess(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Cloudflare Workers type stubs ───────────────────────────────────────────\n// Local type declarations so this file compiles without @cloudflare/workers-types.\n// These are NOT declared globally — that would clash with @cloudflare/workers-types\n// when users install it. Instead, they're module-scoped and used only within\n// this file's function signatures.\n\n/** @internal Minimal stub — use @cloudflare/workers-types for full types. */\nexport interface CfExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n passThroughOnException(): void;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cloudflare.js","names":[],"sources":["../../src/adapters/cloudflare.ts"],"sourcesContent":["// Cloudflare Workers adapter\n//\n// Primary deployment target. Generates a Workers-compatible entry point\n// and wrangler.jsonc configuration. See design/11-platform.md §\"Cloudflare Workers\".\n\nimport { writeFile, readFile, cp, rm, readdir } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\nimport { runSharedBuildSteps } from './build-output-helper.js';\n\n// ─── Bindings passthrough ─────────────────────────────────────────────────\n// ALS stores the env object per-request so server components and middleware\n// can access KV, D1, DO, R2, Queues, etc. via getCloudflareBindings().\n// No global fallback — if called outside a request, it throws.\n// See design/11-platform.md §\"Platform Target\" and design/25-production-deployments.md.\n//\n// The ALS is stored on globalThis via Symbol.for so it survives Vite's\n// module instance split in dev mode. The Vite host process (where\n// cloudflareDevBindings → runWithBindings runs) and the RSC module runner\n// (where getCloudflareBindings runs) load separate instances of this file,\n// each with their own module-level variables. globalThis + Symbol.for\n// ensures both share the same ALS — same pattern React uses for\n// Symbol.for('react.element').\n\nconst BINDINGS_ALS_KEY = Symbol.for('timber:cf-bindings-als');\n\nfunction getBindingsAls(): AsyncLocalStorage<Record<string, unknown>> {\n let als = (globalThis as any)[BINDINGS_ALS_KEY] as\n | AsyncLocalStorage<Record<string, unknown>>\n | undefined;\n if (!als) {\n als = new AsyncLocalStorage<Record<string, unknown>>();\n (globalThis as any)[BINDINGS_ALS_KEY] = als;\n }\n return als;\n}\n\n/**\n * Get Cloudflare Worker bindings for the current request.\n *\n * Returns the `env` object passed to the Worker's `fetch` handler,\n * giving direct access to KV, D1, Durable Objects, R2, Queues, and\n * any other bindings configured in `wrangler.jsonc`.\n *\n * Must be called within a request context (server component, middleware,\n * server action). Throws outside a request.\n *\n * @example\n * ```ts\n * import { getCloudflareBindings } from '@timber-js/app/adapters/cloudflare'\n *\n * export default async function Page() {\n * const { MY_KV, MY_DB } = getCloudflareBindings()\n * const data = await MY_KV.get('key')\n * return <div>{data}</div>\n * }\n * ```\n */\nexport function getCloudflareBindings<T = Record<string, unknown>>(): T {\n const env = getBindingsAls().getStore();\n if (!env) {\n throw new Error(\n 'getCloudflareBindings() called outside a Cloudflare Workers request context. ' +\n 'It can only be called from server components, middleware, or server actions ' +\n 'when running on the Cloudflare adapter.'\n );\n }\n return env as T;\n}\n\n/**\n * Run a function with Cloudflare bindings available via getCloudflareBindings().\n * @internal Used by cloudflareDevBindings() to wrap dev-server requests.\n */\nexport function runWithBindings<T>(env: Record<string, unknown>, fn: () => T): T {\n return getBindingsAls().run(env, fn);\n}\n\n// ─── Binding configuration types ──────────────────────────────────────────────\n// Declarative binding config that maps to wrangler.jsonc binding sections.\n// See design/35-cloudflare-primitives.md §\"Binding Declarations in Adapter Config\".\n\n/** KV namespace binding. `id` defaults to `''` (filled in by wrangler or dashboard). */\nexport interface CloudflareKVBinding {\n name: string;\n id?: string;\n}\n\n/** D1 database binding. `database_id` is required (from Cloudflare dashboard). */\nexport interface CloudflareD1Binding {\n name: string;\n database_id: string;\n}\n\n/** R2 bucket binding. `bucket_name` must match the bucket created in Cloudflare. */\nexport interface CloudflareR2Binding {\n name: string;\n bucket_name: string;\n}\n\n/** Queue producer binding. `queue` is the queue name in Cloudflare. */\nexport interface CloudflareQueueProducer {\n name: string;\n queue: string;\n}\n\n/** Queue consumer declaration. Attached to the worker, not a binding name. */\nexport interface CloudflareQueueConsumer {\n queue: string;\n max_batch_size?: number;\n max_retries?: number;\n dead_letter_queue?: string;\n}\n\n/** Durable Object binding. `script_name` is for external DO references. */\nexport interface CloudflareDurableObjectBinding {\n name: string;\n class_name: string;\n script_name?: string;\n}\n\n/**\n * Declarative Cloudflare bindings configuration.\n *\n * These are converted to the appropriate wrangler.jsonc sections\n * (`kv_namespaces`, `d1_databases`, `r2_buckets`, `queues`, `durable_objects`).\n * The `wrangler` escape hatch overrides any generated binding sections\n * if there's a conflict.\n *\n * @example\n * ```ts\n * cloudflare({\n * bindings: {\n * kv: [{ name: 'TIMBER_CACHE' }],\n * d1: [{ name: 'MY_DB', database_id: 'xxxx' }],\n * r2: [{ name: 'MY_BUCKET', bucket_name: 'my-bucket' }],\n * queues: {\n * producers: [{ name: 'EMAIL_QUEUE', queue: 'email-queue' }],\n * consumers: [{ queue: 'email-queue', max_batch_size: 10 }],\n * },\n * durableObjects: [{ name: 'MY_DO', class_name: 'MyDurableObject' }],\n * },\n * })\n * ```\n */\nexport interface CloudflareBindings {\n kv?: CloudflareKVBinding[];\n d1?: CloudflareD1Binding[];\n r2?: CloudflareR2Binding[];\n queues?: {\n producers?: CloudflareQueueProducer[];\n consumers?: CloudflareQueueConsumer[];\n };\n durableObjects?: CloudflareDurableObjectBinding[];\n}\n\n/** Options for the Cloudflare Workers adapter. */\nexport interface CloudflareAdapterOptions {\n /**\n * Cloudflare compatibility date.\n * @default Current date in YYYY-MM-DD format at build time.\n */\n compatibilityDate?: string;\n\n /**\n * Additional compatibility flags.\n * @default ['nodejs_compat']\n */\n compatibilityFlags?: string[];\n\n /**\n * Declarative Cloudflare bindings. Generates the appropriate\n * `kv_namespaces`, `d1_databases`, `r2_buckets`, `queues`, and\n * `durable_objects` sections in wrangler.jsonc.\n *\n * If both `bindings` and `wrangler` specify the same section,\n * `wrangler` wins (it's the escape hatch).\n */\n bindings?: CloudflareBindings;\n\n /**\n * Custom wrangler.jsonc fields to merge.\n * Overrides generated values (including bindings-generated sections).\n */\n wrangler?: Record<string, unknown>;\n\n /**\n * Path to a module that exports additional Worker handlers (queue, scheduled, email, etc.).\n * The module is imported in the generated `_worker.js` and its named exports are\n * spread into the default export alongside `fetch`.\n *\n * The module receives `(batch/event, env, ctx)` — standard Cloudflare handler signatures.\n * It does NOT have access to timber's request pipeline (no ALS, no getCloudflareBindings).\n * Use `env` directly for bindings.\n *\n * @example\n * ```ts\n * // timber.config.ts\n * adapter: cloudflare({ workerHandlers: './src/worker-handlers.ts' })\n *\n * // src/worker-handlers.ts\n * export async function queue(batch, env) { ... }\n * export async function scheduled(controller, env, ctx) { ... }\n * ```\n */\n workerHandlers?: string;\n}\n\n/**\n * Create a Cloudflare Workers adapter.\n *\n * @example\n * ```ts\n * import { cloudflare } from '@timber-js/app/adapters/cloudflare'\n *\n * export default {\n * output: 'server',\n * adapter: cloudflare(),\n * }\n * ```\n */\nexport function cloudflare(options: CloudflareAdapterOptions = {}): TimberPlatformAdapter {\n return {\n name: 'cloudflare',\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'cloudflare');\n\n // Clean stale prebuilt artifacts from prior builds.\n await rm(join(outDir, 'prebuilt'), { recursive: true, force: true }).catch(() => {});\n await rm(join(outDir, 'static', 'prebuilt'), { recursive: true, force: true }).catch(\n () => {}\n );\n\n // Steps 1–5: shared across all adapters (mkdir, copy client/rsc/ssr,\n // write _headers, write manifest-init).\n await runSharedBuildSteps({\n config,\n buildDir,\n outDir,\n publicDirName: 'static',\n });\n\n // Compile optional worker handlers (queue, scheduled, etc.)\n // Uses Vite's build API to bundle the TypeScript source into ESM.\n let hasWorkerHandlers = false;\n if (options.workerHandlers) {\n const handlersEntry = join(process.cwd(), options.workerHandlers);\n const { build: viteBuild } = await import('vite');\n await viteBuild({\n configFile: false,\n logLevel: 'info',\n build: {\n lib: {\n entry: handlersEntry,\n formats: ['es'],\n fileName: () => '_worker-handlers.js',\n },\n outDir,\n emptyOutDir: false,\n minify: false,\n rollupOptions: {\n external: [/^node:/, /^cloudflare:/],\n },\n },\n });\n hasWorkerHandlers = true;\n }\n\n // Inline prebuilt flight payloads into the worker JS module (TIM-1162).\n // Data is read from buildDir/prebuilt/ and embedded as literals —\n // no external Text/Data module imports needed.\n const prebuiltData = await readPrebuiltData(buildDir);\n await writeFile(\n join(outDir, '_timber-prebuilt-source.js'),\n generatePrebuiltSourceModule(prebuiltData)\n );\n // Remove the copied prebuilt/ dir — data is inlined, files aren't needed.\n await rm(join(outDir, 'prebuilt'), { recursive: true, force: true }).catch(() => {});\n\n // Write the runtime init module — sets process.env.TIMBER_RUNTIME\n // BEFORE the RSC entry evaluates (ESM import order). Without this,\n // createRequestHandler's auto-wire for WorkersCachePurge would not\n // detect the Cloudflare runtime (TIM-1170).\n await writeFile(join(outDir, '_timber-runtime-init.js'), generateRuntimeInitModule());\n\n // Generate the Workers entry point\n const hasManifestInit = !!config.manifestInit;\n const workerEntry = generateWorkerEntry(outDir, outDir, hasManifestInit, hasWorkerHandlers);\n await writeFile(join(outDir, '_worker.js'), workerEntry);\n\n // Generate wrangler.jsonc\n const wranglerConfig = generateWranglerConfig(config, options);\n await writeFile(join(outDir, 'wrangler.jsonc'), JSON.stringify(wranglerConfig, null, 2));\n },\n\n async preview(_config: TimberConfig, buildDir: string) {\n const cmd = generatePreviewCommand(buildDir);\n await spawnPreviewProcess(cmd.command, cmd.args, cmd.cwd);\n },\n\n // No adapter-level waitUntil: on Workers there is no ambient\n // ExecutionContext outside a request, so support is provided\n // per-request by the generated _worker.js, which bridges\n // ctx.waitUntil() into the ALS via runWithWaitUntil().\n // See design/11-platform.md §\"waitUntil()\".\n };\n}\n\n// ─── Exported helpers (used by tests and build) ─────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateWorkerEntry(\n buildDir: string,\n outDir: string,\n hasManifestInit = false,\n hasWorkerHandlers = false\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // The Vite RSC plugin outputs it to rsc/index.js.\n let rscEntryRelative = relative(outDir, join(buildDir, 'rsc', 'index.js'));\n // Ensure the import path starts with ./ for ESM compatibility\n if (!rscEntryRelative.startsWith('.')) {\n rscEntryRelative = './' + rscEntryRelative;\n }\n\n // Build manifest init must be imported before the RSC handler so that\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.\n // ESM guarantees imports are evaluated in order.\n const manifestImport = hasManifestInit ? \"import './_timber-manifest-init.js'\\n\" : '';\n\n // Optional additional Worker handlers (queue, scheduled, email, etc.)\n // Compiled by buildOutput via Vite's build API into _worker-handlers.js.\n // The module's named exports are spread into the default export alongside fetch.\n const handlersImport = hasWorkerHandlers\n ? \"import * as workerHandlers from './_worker-handlers.js'\\n\"\n : '';\n const handlersSpread = hasWorkerHandlers ? ' ...workerHandlers,\\n' : '';\n\n return `// Generated by @timber-js/app/adapters/cloudflare\n// Do not edit — this file is regenerated on each build.\n\nimport { AsyncLocalStorage } from 'node:async_hooks'\n// Side-effect imports: evaluated IN ORDER before the _worker.js body runs.\n// _timber-runtime-init.js sets TIMBER_RUNTIME before createRequestHandler()\n// runs (during the RSC entry's top-level await) so the auto-wire for\n// WorkersCachePurge can detect the Cloudflare runtime (TIM-1170).\n// _timber-prebuilt-source.js sets globalThis.__TIMBER_PREBUILT_SOURCE__\n// for cache.component payloads.\nimport './_timber-runtime-init.js'\nimport './_timber-prebuilt-source.js'\n${manifestImport}import handler, { runWithWaitUntil, runWithWorkersCachePurge } from '${rscEntryRelative}'\n${handlersImport}\n\n// Bind Cloudflare env to ALS so getCloudflareBindings() works at runtime.\n// Uses the same Symbol.for key as getCloudflareBindings() reads from.\nconst ALS_KEY = Symbol.for('timber:cf-bindings-als')\nlet bindingsAls = globalThis[ALS_KEY]\nif (!bindingsAls) {\n bindingsAls = new AsyncLocalStorage()\n globalThis[ALS_KEY] = bindingsAls\n}\n\n// Register timber's platform tracer so pipeline phase spans (timber.proxy,\n// timber.middleware, timber.render, timber.access, timber.action, …) appear\n// as native Cloudflare spans in the Observability dashboard, nested under\n// the auto-instrumented fetch/KV/D1 spans. Requires observability.traces\n// enabled in wrangler.jsonc (generated by default).\n//\n// Lazily imported and guarded: runtimes without the custom spans API\n// (older compatibility dates, older local workerd) fall back to OTEL-only\n// emission. withSpan() in the framework composes both channels.\nconst TRACER_KEY = Symbol.for('timber:platform-tracer')\nlet tracerInit\nfunction initPlatformTracer() {\n tracerInit ??= import('cloudflare:workers')\n .then(({ tracing }) => {\n if (tracing && typeof tracing.enterSpan === 'function') {\n globalThis[TRACER_KEY] ??= {\n enterSpan: (name, fn) => tracing.enterSpan(name, fn),\n }\n }\n })\n .catch(() => {})\n return tracerInit\n}\n\nexport default {\n${handlersSpread} async fetch(request, env, ctx) {\n // One-time on first request; resolved-promise await thereafter.\n await initPlatformTracer()\n\n // Edge-cache GET/HEAD responses via the Cache API. RSC navigation\n // requests (Accept: text/x-component) are excluded — they vary by\n // X-Timber-State-Tree which the cache key doesn't capture.\n const isRsc = request.headers.get('Accept')?.includes('text/x-component')\n const reqCc = request.headers.get('Cache-Control') || ''\n const bypassCache = reqCc.includes('no-cache') || reqCc.includes('max-age=0')\n || request.headers.has('Pragma')\n const cacheable = !isRsc && !bypassCache\n && (request.method === 'GET' || request.method === 'HEAD')\n const cache = cacheable && typeof caches !== 'undefined' ? caches.default : null\n if (cache) {\n const cached = await cache.match(request)\n if (cached) return cached\n }\n\n // Bridge ctx.cache into the per-request ALS so WorkersCachePurge can\n // call ctx.cache.purge({ tags }) during cache.invalidate/revalidateTag.\n // Workers Cache is available on all Cloudflare plans — no API token needed.\n const workersCacheCtx = ctx.cache && typeof ctx.cache.purge === 'function' ? ctx.cache : null\n const runHandler = () =>\n runWithWaitUntil((promise) => ctx.waitUntil(promise), () => handler(request))\n const response = await bindingsAls.run(env, () =>\n workersCacheCtx\n ? runWithWorkersCachePurge(workersCacheCtx, runHandler)\n : runHandler()\n )\n\n // Cache public responses at the edge. A response is edge-cacheable if\n // it has 'public' or 's-maxage' (a shared-cache TTL directive) and no\n // 'no-store' or 'private'. Cloudflare's cache.put() only accepts GET\n // requests and rejects 206, so only store GET 200s. HEAD requests\n // benefit from cache.match() which handles HEAD→GET matching automatically.\n // See design/25-production-deployments.md §\"CDN / Edge Cache\".\n if (cache && request.method === 'GET' && response.status === 200) {\n const cc = response.headers.get('Cache-Control')\n const vary = response.headers.get('Vary')\n if (cc && !cc.includes('no-store') && !cc.includes('private')\n && (cc.includes('public') || cc.includes('s-maxage'))\n && vary !== '*' && !response.headers.has('Set-Cookie')) {\n ctx.waitUntil(cache.put(request, response.clone()))\n }\n }\n\n return response\n }\n}\n`;\n}\n\n/** Build-time prebuilt data read from `.timber/dist/prebuilt/`. */\nexport interface PrebuiltModuleData {\n /** Raw manifest.json content. */\n manifestJson: string;\n /** Payload file paths and their raw bytes. */\n payloads: Array<{ file: string; bytes: Buffer }>;\n}\n\n/**\n * Discover and read prebuilt flight payload files from the build directory.\n * Returns null when no prebuilt artifacts exist (no cache.component usage).\n *\n * @internal Exported for testing.\n */\nexport async function readPrebuiltData(buildDir: string): Promise<PrebuiltModuleData | null> {\n const prebuiltDir = join(buildDir, 'prebuilt');\n let manifestJson: string;\n try {\n manifestJson = await readFile(join(prebuiltDir, 'manifest.json'), 'utf-8');\n } catch {\n return null;\n }\n const payloads: PrebuiltModuleData['payloads'] = [];\n try {\n const componentDirs = await readdir(prebuiltDir, { withFileTypes: true });\n for (const dir of componentDirs) {\n if (!dir.isDirectory()) continue;\n const files = await readdir(join(prebuiltDir, dir.name));\n for (const file of files) {\n if (file.endsWith('.flight')) {\n const filePath = `${dir.name}/${file}`;\n const bytes = await readFile(join(prebuiltDir, filePath));\n payloads.push({ file: filePath, bytes });\n }\n }\n }\n } catch {\n // No subdirectories — manifest exists but no payloads\n }\n return { manifestJson, payloads };\n}\n\n/**\n * Generate the prebuilt payload source module for Cloudflare Workers.\n *\n * Inlines manifest JSON and flight payload bytes directly into the module\n * so they're part of the worker's JS bundle — no external Text/Data module\n * imports needed. This avoids depending on wrangler's find_additional_modules\n * to discover non-JS files, which doesn't reliably resolve Text/Data imports.\n *\n * @internal Exported for testing.\n */\nexport function generatePrebuiltSourceModule(data: PrebuiltModuleData | null): string {\n if (!data) {\n return `// Generated by @timber-js/app/adapters/cloudflare\n// No prebuilt artifacts — all cache.component wrappers render dynamically.\nglobalThis.__TIMBER_PREBUILT_SOURCE__ = {\n async loadManifest() { return null },\n async readPayload() { return null },\n}\n`;\n }\n\n const entries: string[] = [];\n for (const { file, bytes } of data.payloads) {\n const b64 = bytes.toString('base64');\n entries.push(` ${JSON.stringify(file)}: ${JSON.stringify(b64)}`);\n }\n\n return `// Generated by @timber-js/app/adapters/cloudflare\n// Prebuilt payload source — inlines cache.component flight payloads\n// directly into the worker bundle (TIM-1162).\nconst _manifest = ${data.manifestJson.trim()}\n\nfunction _decode(b64) {\n const bin = atob(b64)\n const buf = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i)\n return buf\n}\n\nconst _payloads = {\n${entries.join(',\\n')}\n}\n\nglobalThis.__TIMBER_PREBUILT_SOURCE__ = {\n async loadManifest() { return _manifest },\n async readPayload(file) {\n const b64 = _payloads[file]\n return b64 ? _decode(b64) : null\n },\n}\n`;\n}\n\n/**\n * Generate the runtime init module that sets TIMBER_RUNTIME before the\n * RSC entry evaluates. Imported as a side-effect module in _worker.js.\n *\n * @internal Exported for testing.\n */\nexport function generateRuntimeInitModule(): string {\n return `// Generated by @timber-js/app/adapters/cloudflare\n// Sets TIMBER_RUNTIME before the RSC handler evaluates (TIM-1170).\n// Must be a separate module imported before rsc/index.js so that ESM\n// evaluation order guarantees the env var is set when\n// createRequestHandler() runs its auto-wire logic.\nglobalThis.process ??= { env: {} }\nprocess.env.TIMBER_RUNTIME = 'cloudflare'\n`;\n}\n\n/** Wrangler binding sections generated from `CloudflareBindings`. */\nexport interface WranglerBindingsConfig {\n kv_namespaces?: Array<{ binding: string; id: string }>;\n d1_databases?: Array<{ binding: string; database_id: string }>;\n r2_buckets?: Array<{ binding: string; bucket_name: string }>;\n queues?: {\n producers?: Array<{ binding: string; queue: string }>;\n consumers?: Array<Record<string, unknown>>;\n };\n durable_objects?: {\n bindings: Array<Record<string, string>>;\n };\n}\n\n/**\n * Convert declarative `CloudflareBindings` into wrangler.jsonc binding sections.\n *\n * Maps:\n * - `kv` → `kv_namespaces`\n * - `d1` → `d1_databases`\n * - `r2` → `r2_buckets`\n * - `queues` → `queues` (producers + consumers)\n * - `durableObjects` → `durable_objects`\n *\n * Empty arrays are omitted from the output.\n *\n * @internal Exported for testing.\n */\nexport function generateBindingsConfig(\n bindings: CloudflareBindings | undefined\n): WranglerBindingsConfig {\n if (!bindings) return {};\n\n const result: WranglerBindingsConfig = {};\n\n // KV namespaces\n if (bindings.kv && bindings.kv.length > 0) {\n result.kv_namespaces = bindings.kv.map((kv) => ({\n binding: kv.name,\n id: kv.id ?? '',\n }));\n }\n\n // D1 databases\n if (bindings.d1 && bindings.d1.length > 0) {\n result.d1_databases = bindings.d1.map((d1) => ({\n binding: d1.name,\n database_id: d1.database_id,\n }));\n }\n\n // R2 buckets\n if (bindings.r2 && bindings.r2.length > 0) {\n result.r2_buckets = bindings.r2.map((r2) => ({\n binding: r2.name,\n bucket_name: r2.bucket_name,\n }));\n }\n\n // Queues (producers and/or consumers)\n if (bindings.queues) {\n const queues: Record<string, unknown> = {};\n if (bindings.queues.producers && bindings.queues.producers.length > 0) {\n queues.producers = bindings.queues.producers.map((p) => ({\n binding: p.name,\n queue: p.queue,\n }));\n }\n if (bindings.queues.consumers && bindings.queues.consumers.length > 0) {\n queues.consumers = bindings.queues.consumers.map((c) => {\n const consumer: Record<string, unknown> = { queue: c.queue };\n if (c.max_batch_size !== undefined) consumer.max_batch_size = c.max_batch_size;\n if (c.max_retries !== undefined) consumer.max_retries = c.max_retries;\n if (c.dead_letter_queue !== undefined) consumer.dead_letter_queue = c.dead_letter_queue;\n return consumer;\n });\n }\n if (Object.keys(queues).length > 0) {\n result.queues = queues;\n }\n }\n\n // Durable Objects\n if (bindings.durableObjects && bindings.durableObjects.length > 0) {\n result.durable_objects = {\n bindings: bindings.durableObjects.map((dobj) => {\n const entry: Record<string, string> = {\n name: dobj.name,\n class_name: dobj.class_name,\n };\n if (dobj.script_name) entry.script_name = dobj.script_name;\n return entry;\n }),\n };\n }\n\n return result;\n}\n\n/**\n * One-level deep merge: for each key in `override`, if both the base and\n * override values are plain objects (not arrays, not null), merge their\n * keys with override winning on conflicts. Everything else (primitives,\n * arrays, null) is replaced outright — matching the intuitive behavior\n * where `kv_namespaces: [...]` replaces fully but\n * `durable_objects: { migrations }` merges with generated `bindings`.\n */\nfunction shallowDeepMerge(\n base: Record<string, unknown>,\n override: Record<string, unknown>\n): Record<string, unknown> {\n const result = { ...base };\n for (const key of Object.keys(override)) {\n const baseVal = result[key];\n const overVal = override[key];\n if (isPlainObject(baseVal) && isPlainObject(overVal)) {\n result[key] = { ...baseVal, ...overVal };\n } else {\n result[key] = overVal;\n }\n }\n return result;\n}\n\n/** True for `{}` literals — false for arrays, null, Date, etc. */\nfunction isPlainObject(val: unknown): val is Record<string, unknown> {\n return val !== null && typeof val === 'object' && !Array.isArray(val);\n}\n\n/** @internal Exported for testing. */\nexport function generateWranglerConfig(\n config: TimberConfig,\n options: CloudflareAdapterOptions\n): Record<string, unknown> {\n const compatDate = options.compatibilityDate ?? new Date().toISOString().slice(0, 10);\n\n const flags = options.compatibilityFlags ?? ['nodejs_compat'];\n\n const base: Record<string, unknown> = {\n name: 'timber-app',\n main: '_worker.js',\n compatibility_date: compatDate,\n compatibility_flags: flags,\n // The build output is already fully bundled by Vite — skip wrangler's\n // esbuild pass to avoid issues with top-level await and module format.\n no_bundle: true,\n find_additional_modules: true,\n rules: [{ type: 'ESModule', globs: ['**/*.js'] }],\n assets: {\n directory: './static',\n binding: 'ASSETS',\n },\n // Native trace destination — required for timber's pipeline spans\n // (emitted via tracing.enterSpan in the generated _worker.js) to appear\n // in the Cloudflare Observability dashboard. Gates traces only; does\n // not enable Workers Logs. Override via the `wrangler` escape hatch:\n // wrangler: { observability: { traces: { enabled: false } } }.\n observability: {\n traces: { enabled: true },\n },\n };\n\n // Layer 1: merge bindings-generated sections into base\n const bindingsConfig = generateBindingsConfig(options.bindings);\n const merged = { ...base, ...bindingsConfig };\n\n // Layer 2: wrangler escape hatch with deep merge for nested plain objects.\n // A shallow spread would replace entire sections like durable_objects and\n // queues — e.g. adding migrations via wrangler would drop the generated\n // durable_objects.bindings. Deep merge preserves generated keys while\n // letting wrangler override on actual key-level conflicts.\n if (options.wrangler) {\n return shallowDeepMerge(merged, options.wrangler);\n }\n\n return merged;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Command descriptor for preview — testable without spawning a process. */\nexport interface PreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generatePreviewCommand(buildDir: string): PreviewCommand {\n const cfDir = join(buildDir, 'cloudflare');\n return {\n command: 'wrangler',\n args: ['dev', '--local', '--config', join(cfDir, 'wrangler.jsonc')],\n cwd: cfDir,\n };\n}\n\n/**\n * Spawn a long-running preview process and pipe stdio to the parent.\n * Resolves when the process exits.\n */\nfunction spawnPreviewProcess(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Cloudflare Workers type stubs ───────────────────────────────────────────\n// Local type declarations so this file compiles without @cloudflare/workers-types.\n// These are NOT declared globally — that would clash with @cloudflare/workers-types\n// when users install it. Instead, they're module-scoped and used only within\n// this file's function signatures.\n\n/** @internal Minimal stub — use @cloudflare/workers-types for full types. */\nexport interface CfExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n passThroughOnException(): void;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAOnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,aAAa,GACb,SAAS,GACT,cAAc,GACd,YAAY,GACZ,aAAa,GACb,iBAAiB,GACjB,aAAa,GACb,KAAK,CAAC;AAEV,2CAA2C;AAC3C,UAAU,YAAY;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAgFD,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,
|
|
1
|
+
{"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAOnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,aAAa,GACb,SAAS,GACT,cAAc,GACd,YAAY,GACZ,aAAa,GACb,iBAAiB,GACjB,aAAa,GACb,KAAK,CAAC;AAEV,2CAA2C;AAC3C,UAAU,YAAY;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAgFD,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,CA6D9E;AAID,sCAAsC;AACtC,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,EACnB,QAAQ,UAAO,GACd,MAAM,CAmFR;AAOD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CA+QnF;AAED,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,sCAAsC;AACtC,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,mBAAmB,GAAG,IAAI,CAY5B;AA+DD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAEjE"}
|
package/dist/adapters/nitro.js
CHANGED
|
@@ -106,8 +106,7 @@ var PRESET_CONFIGS = {
|
|
|
106
106
|
function nitro(options = {}) {
|
|
107
107
|
const preset = options.preset ?? "node-server";
|
|
108
108
|
const compress = options.compress ?? true;
|
|
109
|
-
|
|
110
|
-
const pendingPromises = [];
|
|
109
|
+
PRESET_CONFIGS[preset];
|
|
111
110
|
return {
|
|
112
111
|
name: `nitro-${preset}`,
|
|
113
112
|
async buildOutput(config, buildDir) {
|
|
@@ -133,12 +132,6 @@ function nitro(options = {}) {
|
|
|
133
132
|
const scriptPath = join(buildDir, "nitro", "_preview-server.mjs");
|
|
134
133
|
await writeFile(scriptPath, previewScript);
|
|
135
134
|
await spawnNitroPreview(preset === "bun" ? "bun" : "node", [scriptPath], join(buildDir, "nitro"));
|
|
136
|
-
} : void 0,
|
|
137
|
-
waitUntil: presetConfig.supportsWaitUntil ? (promise) => {
|
|
138
|
-
const tracked = promise.catch((err) => {
|
|
139
|
-
console.error("[timber] waitUntil promise rejected:", err);
|
|
140
|
-
});
|
|
141
|
-
pendingPromises.push(tracked);
|
|
142
135
|
} : void 0
|
|
143
136
|
};
|
|
144
137
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/compress-module.ts","../../src/adapters/nitro.ts"],"sourcesContent":["// Compiles server/compress.ts into a standalone ESM module (_compress.mjs)\n// for self-hosted deployments. The adapter writes this to the build output\n// during buildOutput(). It's imported by the Nitro entry and preview server\n// at runtime.\n//\n// By compiling the actual TypeScript source rather than maintaining a\n// hand-written string template, any fix to server/compress.ts automatically\n// flows to the production module. See design/18-build-system.md (\"entry\n// modules are real TypeScript files, not codegen strings\").\n\nimport { transformWithOxc } from 'vite';\n// Vite's ?raw import embeds the file as a string at build time — works\n// in both dev (Vite resolves the TS source) and the published package\n// (the string is inlined into the compiled adapter entry).\nimport compressSource from '../server/compress.ts?raw';\n\n/**\n * Compile server/compress.ts into a standalone ESM module.\n *\n * Uses Vite's OXC transform to strip TypeScript types from the\n * embedded source. The result is written to `_compress.mjs` during\n * buildOutput.\n *\n * @internal Exported for testing.\n */\nexport async function generateCompressModule(): Promise<string> {\n const result = await transformWithOxc(compressSource, 'compress.ts', {\n lang: 'ts',\n });\n\n return (\n '// Generated by @timber-js/app — compiled from server/compress.ts.\\n' +\n '// Do not edit — this file is regenerated on each build.\\n' +\n result.code\n );\n}\n","// Nitro adapter — multi-platform deployment\n//\n// Covers everything except Cloudflare Workers: Node.js, Bun, Vercel,\n// Netlify, AWS Lambda, Deno Deploy, Azure Functions. Nitro handles\n// compression, graceful shutdown, static file serving, and platform quirks.\n// See design/11-platform.md and design/25-production-deployments.md.\n\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\nimport { generateCompressModule } from './compress-module.js';\nimport { IMMUTABLE_CACHE } from './shared.js';\nimport { runSharedBuildSteps } from './build-output-helper.js';\n\n// ─── Presets ─────────────────────────────────────────────────────────────────\n\n/**\n * Supported Nitro deployment presets.\n *\n * Each preset maps to a Nitro deployment target. The adapter generates\n * the appropriate configuration and entry point for the selected platform.\n */\nexport type NitroPreset =\n | 'vercel'\n | 'vercel-edge'\n | 'netlify'\n | 'netlify-edge'\n | 'aws-lambda'\n | 'deno-deploy'\n | 'azure-functions'\n | 'node-server'\n | 'bun';\n\n/** Preset-specific Nitro configuration. */\ninterface PresetConfig {\n /** Nitro preset name passed to the Nitro build. */\n nitroPreset: string;\n /** Output directory name within the build dir. */\n outputDir: string;\n /** Whether the runtime supports waitUntil. */\n supportsWaitUntil: boolean;\n /** Whether the runtime supports application-level 103 Early Hints. */\n supportsEarlyHints: boolean;\n /** Value for TIMBER_RUNTIME env var. See design/25-production-deployments.md. */\n runtimeName: string;\n /** Additional nitro.config fields for this preset. */\n extraConfig?: Record<string, unknown>;\n}\n\nconst PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {\n 'vercel': {\n nitroPreset: 'vercel',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel',\n extraConfig: { vercel: { functions: { maxDuration: 30 } } },\n },\n 'vercel-edge': {\n nitroPreset: 'vercel-edge',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel-edge',\n },\n 'netlify': {\n nitroPreset: 'netlify',\n outputDir: '.netlify/functions-internal',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'netlify',\n },\n 'netlify-edge': {\n nitroPreset: 'netlify-edge',\n outputDir: '.netlify/edge-functions',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'netlify-edge',\n },\n 'aws-lambda': {\n nitroPreset: 'aws-lambda',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'aws-lambda',\n },\n 'deno-deploy': {\n nitroPreset: 'deno-deploy',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'deno-deploy',\n },\n 'azure-functions': {\n nitroPreset: 'azure-functions',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'azure-functions',\n },\n 'node-server': {\n nitroPreset: 'node-server',\n outputDir: '.output',\n supportsWaitUntil: true,\n // Disabled by default: most node-server deployments sit behind a\n // reverse proxy (nginx, caddy, traefik) that doesn't support 103\n // Early Hints over HTTP/1.1. Sending 103 causes nginx to intermittently\n // treat the response as an error and retry after proxy_connect_timeout\n // (~5s stalls on ~23% of requests). Link headers on the 200 response\n // are the safe fallback — Cloudflare and other CDNs convert them to\n // 103 automatically at the edge (over HTTP/2+ to the browser).\n supportsEarlyHints: false,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n supportsWaitUntil: true,\n // Disabled for same reason as node-server — reverse proxies choke on 103.\n // Link headers on the 200 response are converted to 103 by CDNs.\n supportsEarlyHints: false,\n runtimeName: 'bun',\n },\n};\n\n// ─── Options ─────────────────────────────────────────────────────────────────\n\n/** Options for the Nitro adapter. */\nexport interface NitroAdapterOptions {\n /**\n * Deployment preset. Determines the target platform.\n * @default 'node-server'\n */\n preset?: NitroPreset;\n\n /**\n * Enable application-level gzip compression for HTML and RSC responses.\n *\n * When `true` (default), the origin compresses responses using the Web\n * `CompressionStream` API. This is useful for self-hosted deployments\n * where no reverse proxy or CDN handles compression.\n *\n * Set to `false` when deploying behind a reverse proxy (nginx, caddy)\n * or CDN (Cloudflare, Fastly, Vercel) that compresses at the edge.\n * Disabling origin compression saves CPU on the Node.js event loop —\n * compressing 1MB+ streaming HTML responses takes 10-15ms of main\n * thread time per request, directly reducing throughput under load.\n *\n * @default true\n */\n compress?: boolean;\n\n /**\n * Additional Nitro configuration to merge into the generated config.\n * Overrides default values for the selected preset.\n */\n nitroConfig?: Record<string, unknown>;\n}\n\n// ─── Adapter ─────────────────────────────────────────────────────────────────\n\n/**\n * Create a Nitro-based adapter for multi-platform deployment.\n *\n * Nitro abstracts deployment targets — the same timber.js app can deploy\n * to Vercel, Netlify, AWS, Deno Deploy, or Azure by changing the preset.\n *\n * @example\n * ```ts\n * import { nitro } from '@timber-js/app/adapters/nitro'\n *\n * export default {\n * output: 'server',\n * adapter: nitro({ preset: 'vercel' }),\n * }\n * ```\n */\nexport function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter {\n const preset = options.preset ?? 'node-server';\n const compress = options.compress ?? true;\n const presetConfig = PRESET_CONFIGS[preset];\n const pendingPromises: Promise<unknown>[] = [];\n\n return {\n name: `nitro-${preset}`,\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'nitro');\n\n // Steps 1–5: shared across all adapters (mkdir, copy client/rsc/ssr,\n // write _headers, write manifest-init).\n await runSharedBuildSteps({\n config,\n buildDir,\n outDir,\n publicDirName: 'public',\n });\n\n // Write the compression helper module for runtime use.\n // See design/25-production-deployments.md — self-hosted deployments\n // need application-level compression (Cloudflare handles it at the edge).\n await writeFile(join(outDir, '_compress.mjs'), await generateCompressModule());\n\n // Prepend the manifest assignment directly into the RSC entry so\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set before any module reads it.\n // This must be top-level code, not an import, because rollup tree-shakes\n // side-effect-only globalThis assignments from imported modules.\n if (config.manifestInit) {\n const rscEntry = join(outDir, 'rsc', 'index.js');\n const rscContent = await readFile(rscEntry, 'utf-8');\n await writeFile(rscEntry, `${config.manifestInit}\\n${rscContent}`);\n }\n\n // Generate the Nitro entry point (imports from ./rsc/ within nitro dir)\n const entry = generateNitroEntry(buildDir, outDir, preset, compress);\n await writeFile(join(outDir, 'entry.ts'), entry);\n\n // Run the Nitro build to produce a production-ready server bundle.\n // The output goes to dist/nitro/.output/server/index.mjs (for node-server preset).\n // Config is passed programmatically — no nitro.config.ts file needed.\n await runNitroBuild(outDir, preset, options.nitroConfig);\n },\n\n // Only presets that produce a locally-runnable server get preview().\n // Serverless presets (vercel, netlify, aws-lambda, etc.) have no\n // local runtime — Vite's built-in preview is the fallback.\n preview: LOCALLY_PREVIEWABLE.has(preset)\n ? async (_config: TimberConfig, buildDir: string) => {\n // Generate a standalone preview server that uses Node's built-in\n // HTTP server. The Nitro entry.ts can't be run directly because\n // it imports h3 (a Nitro dependency not available at runtime).\n const previewScript = generatePreviewScript(buildDir, preset);\n const scriptPath = join(buildDir, 'nitro', '_preview-server.mjs');\n await writeFile(scriptPath, previewScript);\n\n const command = preset === 'bun' ? 'bun' : 'node';\n await spawnNitroPreview(command, [scriptPath], join(buildDir, 'nitro'));\n }\n : undefined,\n\n waitUntil: presetConfig.supportsWaitUntil\n ? (promise: Promise<unknown>) => {\n const tracked = promise.catch((err) => {\n console.error('[timber] waitUntil promise rejected:', err);\n });\n pendingPromises.push(tracked);\n }\n : undefined,\n };\n}\n\n// ─── Entry Generation ────────────────────────────────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateNitroEntry(\n buildDir: string,\n outDir: string,\n preset: NitroPreset,\n compress = true\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // rsc/ is copied into the nitro dir so the import is local.\n // The manifest init is prepended to rsc/index.js before the nitro build,\n // so globalThis.__TIMBER_BUILD_MANIFEST__ is set before any code reads it.\n const serverEntryRelative = './rsc/index.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;\n const supportsWaitUntil = PRESET_CONFIGS[preset].supportsWaitUntil;\n\n // On node-server and bun, wrap the handler with ALS so the pipeline\n // can send 103 Early Hints via res.writeEarlyHints(). Other presets\n // either don't support 103 or handle it at the CDN level.\n //\n // For presets that support waitUntil, bridge h3's event.waitUntil()\n // to timber's waitUntil() primitive via the ALS bridge.\n // See design/11-platform.md §\"waitUntil()\".\n let handlerCall: string;\n if (earlyHints && supportsWaitUntil) {\n handlerCall = ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const callHandler = () => handler(webRequest)\n let wrappedHandler = earlyHintsSender\n ? () => runWithEarlyHintsSender(earlyHintsSender, callHandler)\n : callHandler\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, wrappedHandler)\n : await wrappedHandler()`;\n } else if (earlyHints) {\n handlerCall = ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const webResponse = earlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest)`;\n } else if (supportsWaitUntil) {\n handlerCall = ` const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, () => handler(webRequest))\n : await handler(webRequest)`;\n } else {\n handlerCall = ` const webResponse = await handler(webRequest)`;\n }\n\n // Import runWithWaitUntil only when the preset supports it.\n const waitUntilImport = supportsWaitUntil ? ', runWithWaitUntil' : '';\n\n const compressImport = compress ? \"import { compressResponse } from './_compress.mjs'\\n\" : '';\n const compressCall = compress ? 'compressResponse(webRequest, webResponse)' : 'webResponse';\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'\n${compressImport}\n// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.\n// See design/25-production-deployments.md §\"TIMBER_RUNTIME\".\nprocess.env.TIMBER_RUNTIME = '${runtimeName}'\n\n// Nitro's index.mjs wraps this import with defineLazyEventHandler, which\n// already handles the event handler protocol. We export a plain async\n// function — no defineEventHandler wrapper needed.\n// Nitro bundles h3 into its internal _libs/ directory but doesn't make\n// it available as a bare specifier in the output's node_modules.\nexport default async function timberHandler(event) {\n // h3 v2: event.req is the Web Request\n const webRequest = event.req\n${handlerCall}\n return ${compressCall}\n}\n`;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Presets that produce a locally-runnable server entry. */\nconst LOCALLY_PREVIEWABLE = new Set<NitroPreset>(['node-server', 'bun']);\n\n/**\n * Generate a standalone preview server script that uses Node's built-in\n * HTTP server. This bypasses Nitro entirely — the Nitro entry.ts imports\n * h3 which isn't available outside a Nitro build. For local preview we\n * just need to serve static files and route requests to the RSC handler.\n *\n * @internal Exported for testing.\n */\nexport function generatePreviewScript(buildDir: string, preset: NitroPreset): string {\n const rscEntryRelative = relative(join(buildDir, 'nitro'), join(buildDir, 'rsc', 'index.js'));\n const rscEntry = rscEntryRelative.startsWith('.') ? rscEntryRelative : './' + rscEntryRelative;\n const publicDir = './public';\n const manifestInitPath = './_timber-manifest-init.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n\n return `// Generated by @timber-js/app — standalone preview server.\n// Uses Node's built-in HTTP server to serve static assets and route\n// dynamic requests through the RSC handler. No Nitro/h3 dependency.\n\nimport { createServer } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\n\n// Set runtime before importing the handler.\nprocess.env.TIMBER_RUNTIME = '${runtimeName}';\n\n// Load the build manifest if it exists.\nconst __dirname = fileURLToPath(new URL('.', import.meta.url));\nconst manifestPath = join(__dirname, '${manifestInitPath}');\nif (existsSync(manifestPath)) {\n await import('${manifestInitPath}');\n}\n\n// Import the RSC handler (default export is the fetch-like handler).\nconst { default: handler, runWithEarlyHintsSender } = await import('${rscEntry}');\n\n// Import compression helper for self-hosted response compression.\nconst { compressResponse } = await import('./_compress.mjs');\n\nconst MIME_TYPES = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.mjs': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.otf': 'font/otf',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.webm': 'video/webm',\n '.mp4': 'video/mp4',\n '.txt': 'text/plain',\n '.xml': 'application/xml',\n '.wasm': 'application/wasm',\n};\n\nconst publicDir = join(__dirname, '${publicDir}');\n\n// Port resolution (TIM-842):\n// - Default to 3000\n// - If PORT env var is set, honor it strictly (fail loudly on conflict)\n// - Otherwise auto-bump from 3000 until a free port is found\n// Mirrors the dev server behavior so timber dev/preview/production all\n// behave the same way around port selection.\nconst envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : null;\nconst portIsExplicit = envPort != null && Number.isFinite(envPort) && envPort > 0;\nconst startPort = portIsExplicit ? envPort : 3000;\nconst host = process.env.HOST || process.env.HOSTNAME || 'localhost';\n\n// Set after listenWithBump() resolves so request handlers can build\n// absolute URLs from the actual bound port (which may differ from\n// startPort after an auto-bump).\nlet boundPort = startPort;\n\nconst server = createServer(async (req, res) => {\n // Scheme from the socket's TLS state — a hardcoded http:// breaks CSRF\n // origin comparison and absolute-URL construction when TLS is terminated\n // at this server (TIM-1073; mirrors the dev-server fix in TIM-1067).\n // X-Forwarded-Proto is intentionally not consulted here: on a direct\n // connection it would be attacker-controlled. The CSRF layer honors it\n // separately for reverse-proxy deployments.\n const scheme = req.socket?.encrypted ? 'https' : 'http';\n const url = new URL(req.url || '/', \\`\\${scheme}://\\${host}:\\${boundPort}\\`);\n\n // Try serving static files from the public directory first.\n const filePath = join(publicDir, url.pathname);\n // Prevent path traversal.\n if (filePath.startsWith(publicDir)) {\n try {\n const fileStat = await stat(filePath);\n if (fileStat.isFile()) {\n const ext = extname(filePath);\n const contentType = MIME_TYPES[ext] || 'application/octet-stream';\n const body = await readFile(filePath);\n // Hashed assets get immutable cache, others get short cache.\n const isImmutable =\n url.pathname.startsWith('/assets/') || url.pathname.startsWith('/_timber/fonts/');\n const cacheControl = isImmutable\n ? 'public, max-age=31536000, immutable'\n : 'public, max-age=3600, must-revalidate';\n res.writeHead(200, {\n 'Content-Type': contentType,\n 'Content-Length': body.length,\n 'Cache-Control': cacheControl,\n });\n res.end(body);\n return;\n }\n } catch {\n // File not found — fall through to the RSC handler.\n }\n }\n\n // Convert Node request to Web Request.\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n }\n\n let body = undefined;\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n body = await new Promise((resolve) => {\n const chunks = [];\n req.on('data', (chunk) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks)));\n });\n }\n\n const webRequest = new Request(url.href, {\n method: req.method,\n headers,\n body,\n duplex: body ? 'half' : undefined,\n });\n\n try {\n // Support 103 Early Hints when available.\n const earlyHintsSender = (typeof res.writeEarlyHints === 'function')\n ? (links) => { try { res.writeEarlyHints({ link: links }); } catch {} }\n : undefined;\n\n const rawResponse = earlyHintsSender && runWithEarlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest);\n\n // Compress the response for self-hosted deployments.\n const webResponse = compressResponse(webRequest, rawResponse);\n\n // Write the response back to the Node response.\n //\n // Set-Cookie needs special handling: Headers.entries() joins multiple\n // Set-Cookie values with \", \" into a single entry, but each cookie must\n // be its own header per RFC 6265 §4.1. Object.fromEntries() would then\n // collapse them into one malformed Set-Cookie header that browsers reject\n // (or only honor partially). Use getSetCookie() to preserve individual\n // values, and pass them as an array — Node's writeHead accepts\n // Record<string, string | string[]> and emits one header per array entry.\n //\n // Without this, EVERY cookie set by the framework on Node—/Nitro deployments\n // is silently dropped: defineCookie().setCookie() in actions, middleware\n // cookie writes, the framework's own session cookies, all of it. See\n // LOCAL-741.\n const responseHeaders = {};\n webResponse.headers.forEach((value, key) => {\n if (key.toLowerCase() !== 'set-cookie') {\n responseHeaders[key] = value;\n }\n });\n const setCookies = webResponse.headers.getSetCookie();\n if (setCookies.length > 0) {\n responseHeaders['set-cookie'] = setCookies;\n }\n res.writeHead(webResponse.status, responseHeaders);\n\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n\n // Cancel the reader when the client disconnects. This causes any\n // pending reader.read() to reject, breaking the pump loop. Critical\n // for SSE and other infinite streams — without this, disconnected\n // clients leak readers.\n let clientDisconnected = false;\n const onClose = () => {\n clientDisconnected = true;\n reader.cancel('Client disconnected').catch(() => {});\n };\n res.on('close', onClose);\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n } catch (err) {\n // reader.cancel() from the close handler causes read() to reject.\n // This is expected on client disconnect — not an error.\n if (!clientDisconnected) {\n throw err;\n }\n } finally {\n res.off('close', onClose);\n reader.releaseLock();\n if (!res.writableEnded) {\n res.end();\n }\n }\n } else {\n res.end();\n }\n } catch (err) {\n console.error('[timber preview] Request error:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n }\n res.end('Internal Server Error');\n }\n});\n\nfunction listenWithBump(port, attempt = 0) {\n return new Promise((resolveListen, rejectListen) => {\n const onError = (err) => {\n server.removeListener('listening', onListening);\n if (err && err.code === 'EADDRINUSE' && !portIsExplicit && attempt < 100) {\n if (attempt === 0) {\n console.log(\\` [timber] Port \\${port} in use, trying \\${port + 1}...\\`);\n }\n listenWithBump(port + 1, attempt + 1).then(resolveListen, rejectListen);\n return;\n }\n rejectListen(err);\n };\n const onListening = () => {\n server.removeListener('error', onError);\n resolveListen(port);\n };\n server.once('error', onError);\n server.once('listening', onListening);\n server.listen(port, host);\n });\n}\n\ntry {\n boundPort = await listenWithBump(startPort);\n if (boundPort !== startPort) {\n console.log();\n console.log(\\` [timber] Port \\${startPort} in use, using \\${boundPort}\\`);\n }\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${boundPort}\\`);\n console.log();\n} catch (err) {\n if (err && err.code === 'EADDRINUSE') {\n console.error(\\`[timber] Port \\${startPort} is already in use. \\` +\n 'Set PORT to a free port, or unset PORT to auto-pick.');\n } else {\n console.error('[timber] Failed to start preview server:', err);\n }\n process.exit(1);\n}\n`;\n}\n\n/** Command descriptor for Nitro preview — testable without spawning. */\nexport interface NitroPreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroPreviewCommand(\n buildDir: string,\n preset: NitroPreset\n): NitroPreviewCommand | null {\n if (!LOCALLY_PREVIEWABLE.has(preset)) return null;\n\n const nitroDir = join(buildDir, 'nitro');\n const entryPath = join(nitroDir, 'entry.ts');\n\n const command = preset === 'bun' ? 'bun' : 'node';\n return {\n command,\n args: [entryPath],\n cwd: nitroDir,\n };\n}\n\n/**\n * Run the Nitro production build using the programmatic API.\n * Uses dynamic import so nitro is only loaded at build time.\n * Externalizes the timber RSC/SSR output — those files are pre-built\n * by timber and have internal references that nitro's bundler can't follow.\n */\nasync function runNitroBuild(\n nitroDir: string,\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): Promise<void> {\n const presetConfig = PRESET_CONFIGS[preset];\n const {\n createNitro,\n build: nitroBuild,\n prepare,\n copyPublicAssets,\n } = await import('nitro/builder');\n\n const nitro = await createNitro({\n rootDir: nitroDir,\n preset: presetConfig.nitroPreset,\n // Use renderer.entry so Nitro wraps our handler with its server runtime\n // (HTTP server, static file serving, graceful shutdown, etc.).\n // Using `entry` directly would bypass the Nitro server runtime.\n renderer: { handler: join(nitroDir, 'entry.ts') },\n output: { dir: join(nitroDir, presetConfig.outputDir) },\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n '/_timber/fonts/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n // Don't bundle the timber RSC/SSR build output — it has its own\n // internal file references that nitro's bundler can't follow.\n // Mark them as external so rollup leaves the imports as-is.\n rollupConfig: {\n external: [/\\.\\.\\/rsc\\//],\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n });\n\n await prepare(nitro);\n await copyPublicAssets(nitro);\n await nitroBuild(nitro);\n await nitro.close();\n}\n\n/** Spawn a Nitro preview process and pipe stdio. */\nfunction spawnNitroPreview(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Get the preset configuration for a given preset name.\n * @internal Exported for testing.\n */\nexport function getPresetConfig(preset: NitroPreset): PresetConfig {\n return PRESET_CONFIGS[preset];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/compress-module.ts","../../src/adapters/nitro.ts"],"sourcesContent":["// Compiles server/compress.ts into a standalone ESM module (_compress.mjs)\n// for self-hosted deployments. The adapter writes this to the build output\n// during buildOutput(). It's imported by the Nitro entry and preview server\n// at runtime.\n//\n// By compiling the actual TypeScript source rather than maintaining a\n// hand-written string template, any fix to server/compress.ts automatically\n// flows to the production module. See design/18-build-system.md (\"entry\n// modules are real TypeScript files, not codegen strings\").\n\nimport { transformWithOxc } from 'vite';\n// Vite's ?raw import embeds the file as a string at build time — works\n// in both dev (Vite resolves the TS source) and the published package\n// (the string is inlined into the compiled adapter entry).\nimport compressSource from '../server/compress.ts?raw';\n\n/**\n * Compile server/compress.ts into a standalone ESM module.\n *\n * Uses Vite's OXC transform to strip TypeScript types from the\n * embedded source. The result is written to `_compress.mjs` during\n * buildOutput.\n *\n * @internal Exported for testing.\n */\nexport async function generateCompressModule(): Promise<string> {\n const result = await transformWithOxc(compressSource, 'compress.ts', {\n lang: 'ts',\n });\n\n return (\n '// Generated by @timber-js/app — compiled from server/compress.ts.\\n' +\n '// Do not edit — this file is regenerated on each build.\\n' +\n result.code\n );\n}\n","// Nitro adapter — multi-platform deployment\n//\n// Covers everything except Cloudflare Workers: Node.js, Bun, Vercel,\n// Netlify, AWS Lambda, Deno Deploy, Azure Functions. Nitro handles\n// compression, graceful shutdown, static file serving, and platform quirks.\n// See design/11-platform.md and design/25-production-deployments.md.\n\nimport { writeFile, readFile } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\nimport { generateCompressModule } from './compress-module.js';\nimport { IMMUTABLE_CACHE } from './shared.js';\nimport { runSharedBuildSteps } from './build-output-helper.js';\n\n// ─── Presets ─────────────────────────────────────────────────────────────────\n\n/**\n * Supported Nitro deployment presets.\n *\n * Each preset maps to a Nitro deployment target. The adapter generates\n * the appropriate configuration and entry point for the selected platform.\n */\nexport type NitroPreset =\n | 'vercel'\n | 'vercel-edge'\n | 'netlify'\n | 'netlify-edge'\n | 'aws-lambda'\n | 'deno-deploy'\n | 'azure-functions'\n | 'node-server'\n | 'bun';\n\n/** Preset-specific Nitro configuration. */\ninterface PresetConfig {\n /** Nitro preset name passed to the Nitro build. */\n nitroPreset: string;\n /** Output directory name within the build dir. */\n outputDir: string;\n /** Whether the runtime supports waitUntil. */\n supportsWaitUntil: boolean;\n /** Whether the runtime supports application-level 103 Early Hints. */\n supportsEarlyHints: boolean;\n /** Value for TIMBER_RUNTIME env var. See design/25-production-deployments.md. */\n runtimeName: string;\n /** Additional nitro.config fields for this preset. */\n extraConfig?: Record<string, unknown>;\n}\n\nconst PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {\n 'vercel': {\n nitroPreset: 'vercel',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel',\n extraConfig: { vercel: { functions: { maxDuration: 30 } } },\n },\n 'vercel-edge': {\n nitroPreset: 'vercel-edge',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel-edge',\n },\n 'netlify': {\n nitroPreset: 'netlify',\n outputDir: '.netlify/functions-internal',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'netlify',\n },\n 'netlify-edge': {\n nitroPreset: 'netlify-edge',\n outputDir: '.netlify/edge-functions',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'netlify-edge',\n },\n 'aws-lambda': {\n nitroPreset: 'aws-lambda',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'aws-lambda',\n },\n 'deno-deploy': {\n nitroPreset: 'deno-deploy',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'deno-deploy',\n },\n 'azure-functions': {\n nitroPreset: 'azure-functions',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'azure-functions',\n },\n 'node-server': {\n nitroPreset: 'node-server',\n outputDir: '.output',\n supportsWaitUntil: true,\n // Disabled by default: most node-server deployments sit behind a\n // reverse proxy (nginx, caddy, traefik) that doesn't support 103\n // Early Hints over HTTP/1.1. Sending 103 causes nginx to intermittently\n // treat the response as an error and retry after proxy_connect_timeout\n // (~5s stalls on ~23% of requests). Link headers on the 200 response\n // are the safe fallback — Cloudflare and other CDNs convert them to\n // 103 automatically at the edge (over HTTP/2+ to the browser).\n supportsEarlyHints: false,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n supportsWaitUntil: true,\n // Disabled for same reason as node-server — reverse proxies choke on 103.\n // Link headers on the 200 response are converted to 103 by CDNs.\n supportsEarlyHints: false,\n runtimeName: 'bun',\n },\n};\n\n// ─── Options ─────────────────────────────────────────────────────────────────\n\n/** Options for the Nitro adapter. */\nexport interface NitroAdapterOptions {\n /**\n * Deployment preset. Determines the target platform.\n * @default 'node-server'\n */\n preset?: NitroPreset;\n\n /**\n * Enable application-level gzip compression for HTML and RSC responses.\n *\n * When `true` (default), the origin compresses responses using the Web\n * `CompressionStream` API. This is useful for self-hosted deployments\n * where no reverse proxy or CDN handles compression.\n *\n * Set to `false` when deploying behind a reverse proxy (nginx, caddy)\n * or CDN (Cloudflare, Fastly, Vercel) that compresses at the edge.\n * Disabling origin compression saves CPU on the Node.js event loop —\n * compressing 1MB+ streaming HTML responses takes 10-15ms of main\n * thread time per request, directly reducing throughput under load.\n *\n * @default true\n */\n compress?: boolean;\n\n /**\n * Additional Nitro configuration to merge into the generated config.\n * Overrides default values for the selected preset.\n */\n nitroConfig?: Record<string, unknown>;\n}\n\n// ─── Adapter ─────────────────────────────────────────────────────────────────\n\n/**\n * Create a Nitro-based adapter for multi-platform deployment.\n *\n * Nitro abstracts deployment targets — the same timber.js app can deploy\n * to Vercel, Netlify, AWS, Deno Deploy, or Azure by changing the preset.\n *\n * @example\n * ```ts\n * import { nitro } from '@timber-js/app/adapters/nitro'\n *\n * export default {\n * output: 'server',\n * adapter: nitro({ preset: 'vercel' }),\n * }\n * ```\n */\nexport function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter {\n const preset = options.preset ?? 'node-server';\n const compress = options.compress ?? true;\n const presetConfig = PRESET_CONFIGS[preset];\n return {\n name: `nitro-${preset}`,\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'nitro');\n\n // Steps 1–5: shared across all adapters (mkdir, copy client/rsc/ssr,\n // write _headers, write manifest-init).\n await runSharedBuildSteps({\n config,\n buildDir,\n outDir,\n publicDirName: 'public',\n });\n\n // Write the compression helper module for runtime use.\n // See design/25-production-deployments.md — self-hosted deployments\n // need application-level compression (Cloudflare handles it at the edge).\n await writeFile(join(outDir, '_compress.mjs'), await generateCompressModule());\n\n // Prepend the manifest assignment directly into the RSC entry so\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set before any module reads it.\n // This must be top-level code, not an import, because rollup tree-shakes\n // side-effect-only globalThis assignments from imported modules.\n if (config.manifestInit) {\n const rscEntry = join(outDir, 'rsc', 'index.js');\n const rscContent = await readFile(rscEntry, 'utf-8');\n await writeFile(rscEntry, `${config.manifestInit}\\n${rscContent}`);\n }\n\n // Generate the Nitro entry point (imports from ./rsc/ within nitro dir)\n const entry = generateNitroEntry(buildDir, outDir, preset, compress);\n await writeFile(join(outDir, 'entry.ts'), entry);\n\n // Run the Nitro build to produce a production-ready server bundle.\n // The output goes to dist/nitro/.output/server/index.mjs (for node-server preset).\n // Config is passed programmatically — no nitro.config.ts file needed.\n await runNitroBuild(outDir, preset, options.nitroConfig);\n },\n\n // Only presets that produce a locally-runnable server get preview().\n // Serverless presets (vercel, netlify, aws-lambda, etc.) have no\n // local runtime — Vite's built-in preview is the fallback.\n preview: LOCALLY_PREVIEWABLE.has(preset)\n ? async (_config: TimberConfig, buildDir: string) => {\n // Generate a standalone preview server that uses Node's built-in\n // HTTP server. The Nitro entry.ts can't be run directly because\n // it imports h3 (a Nitro dependency not available at runtime).\n const previewScript = generatePreviewScript(buildDir, preset);\n const scriptPath = join(buildDir, 'nitro', '_preview-server.mjs');\n await writeFile(scriptPath, previewScript);\n\n const command = preset === 'bun' ? 'bun' : 'node';\n await spawnNitroPreview(command, [scriptPath], join(buildDir, 'nitro'));\n }\n : undefined,\n };\n}\n\n// ─── Entry Generation ────────────────────────────────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateNitroEntry(\n buildDir: string,\n outDir: string,\n preset: NitroPreset,\n compress = true\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // rsc/ is copied into the nitro dir so the import is local.\n // The manifest init is prepended to rsc/index.js before the nitro build,\n // so globalThis.__TIMBER_BUILD_MANIFEST__ is set before any code reads it.\n const serverEntryRelative = './rsc/index.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;\n const supportsWaitUntil = PRESET_CONFIGS[preset].supportsWaitUntil;\n\n // On node-server and bun, wrap the handler with ALS so the pipeline\n // can send 103 Early Hints via res.writeEarlyHints(). Other presets\n // either don't support 103 or handle it at the CDN level.\n //\n // For presets that support waitUntil, bridge h3's event.waitUntil()\n // to timber's waitUntil() primitive via the ALS bridge.\n // See design/11-platform.md §\"waitUntil()\".\n let handlerCall: string;\n if (earlyHints && supportsWaitUntil) {\n handlerCall = ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const callHandler = () => handler(webRequest)\n let wrappedHandler = earlyHintsSender\n ? () => runWithEarlyHintsSender(earlyHintsSender, callHandler)\n : callHandler\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, wrappedHandler)\n : await wrappedHandler()`;\n } else if (earlyHints) {\n handlerCall = ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const webResponse = earlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest)`;\n } else if (supportsWaitUntil) {\n handlerCall = ` const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, () => handler(webRequest))\n : await handler(webRequest)`;\n } else {\n handlerCall = ` const webResponse = await handler(webRequest)`;\n }\n\n // Import runWithWaitUntil only when the preset supports it.\n const waitUntilImport = supportsWaitUntil ? ', runWithWaitUntil' : '';\n\n const compressImport = compress ? \"import { compressResponse } from './_compress.mjs'\\n\" : '';\n const compressCall = compress ? 'compressResponse(webRequest, webResponse)' : 'webResponse';\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'\n${compressImport}\n// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.\n// See design/25-production-deployments.md §\"TIMBER_RUNTIME\".\nprocess.env.TIMBER_RUNTIME = '${runtimeName}'\n\n// Nitro's index.mjs wraps this import with defineLazyEventHandler, which\n// already handles the event handler protocol. We export a plain async\n// function — no defineEventHandler wrapper needed.\n// Nitro bundles h3 into its internal _libs/ directory but doesn't make\n// it available as a bare specifier in the output's node_modules.\nexport default async function timberHandler(event) {\n // h3 v2: event.req is the Web Request\n const webRequest = event.req\n${handlerCall}\n return ${compressCall}\n}\n`;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Presets that produce a locally-runnable server entry. */\nconst LOCALLY_PREVIEWABLE = new Set<NitroPreset>(['node-server', 'bun']);\n\n/**\n * Generate a standalone preview server script that uses Node's built-in\n * HTTP server. This bypasses Nitro entirely — the Nitro entry.ts imports\n * h3 which isn't available outside a Nitro build. For local preview we\n * just need to serve static files and route requests to the RSC handler.\n *\n * @internal Exported for testing.\n */\nexport function generatePreviewScript(buildDir: string, preset: NitroPreset): string {\n const rscEntryRelative = relative(join(buildDir, 'nitro'), join(buildDir, 'rsc', 'index.js'));\n const rscEntry = rscEntryRelative.startsWith('.') ? rscEntryRelative : './' + rscEntryRelative;\n const publicDir = './public';\n const manifestInitPath = './_timber-manifest-init.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n\n return `// Generated by @timber-js/app — standalone preview server.\n// Uses Node's built-in HTTP server to serve static assets and route\n// dynamic requests through the RSC handler. No Nitro/h3 dependency.\n\nimport { createServer } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\n\n// Set runtime before importing the handler.\nprocess.env.TIMBER_RUNTIME = '${runtimeName}';\n\n// Load the build manifest if it exists.\nconst __dirname = fileURLToPath(new URL('.', import.meta.url));\nconst manifestPath = join(__dirname, '${manifestInitPath}');\nif (existsSync(manifestPath)) {\n await import('${manifestInitPath}');\n}\n\n// Import the RSC handler (default export is the fetch-like handler).\nconst { default: handler, runWithEarlyHintsSender } = await import('${rscEntry}');\n\n// Import compression helper for self-hosted response compression.\nconst { compressResponse } = await import('./_compress.mjs');\n\nconst MIME_TYPES = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.mjs': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.otf': 'font/otf',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.webm': 'video/webm',\n '.mp4': 'video/mp4',\n '.txt': 'text/plain',\n '.xml': 'application/xml',\n '.wasm': 'application/wasm',\n};\n\nconst publicDir = join(__dirname, '${publicDir}');\n\n// Port resolution (TIM-842):\n// - Default to 3000\n// - If PORT env var is set, honor it strictly (fail loudly on conflict)\n// - Otherwise auto-bump from 3000 until a free port is found\n// Mirrors the dev server behavior so timber dev/preview/production all\n// behave the same way around port selection.\nconst envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : null;\nconst portIsExplicit = envPort != null && Number.isFinite(envPort) && envPort > 0;\nconst startPort = portIsExplicit ? envPort : 3000;\nconst host = process.env.HOST || process.env.HOSTNAME || 'localhost';\n\n// Set after listenWithBump() resolves so request handlers can build\n// absolute URLs from the actual bound port (which may differ from\n// startPort after an auto-bump).\nlet boundPort = startPort;\n\nconst server = createServer(async (req, res) => {\n // Scheme from the socket's TLS state — a hardcoded http:// breaks CSRF\n // origin comparison and absolute-URL construction when TLS is terminated\n // at this server (TIM-1073; mirrors the dev-server fix in TIM-1067).\n // X-Forwarded-Proto is intentionally not consulted here: on a direct\n // connection it would be attacker-controlled. The CSRF layer honors it\n // separately for reverse-proxy deployments.\n const scheme = req.socket?.encrypted ? 'https' : 'http';\n const url = new URL(req.url || '/', \\`\\${scheme}://\\${host}:\\${boundPort}\\`);\n\n // Try serving static files from the public directory first.\n const filePath = join(publicDir, url.pathname);\n // Prevent path traversal.\n if (filePath.startsWith(publicDir)) {\n try {\n const fileStat = await stat(filePath);\n if (fileStat.isFile()) {\n const ext = extname(filePath);\n const contentType = MIME_TYPES[ext] || 'application/octet-stream';\n const body = await readFile(filePath);\n // Hashed assets get immutable cache, others get short cache.\n const isImmutable =\n url.pathname.startsWith('/assets/') || url.pathname.startsWith('/_timber/fonts/');\n const cacheControl = isImmutable\n ? 'public, max-age=31536000, immutable'\n : 'public, max-age=3600, must-revalidate';\n res.writeHead(200, {\n 'Content-Type': contentType,\n 'Content-Length': body.length,\n 'Cache-Control': cacheControl,\n });\n res.end(body);\n return;\n }\n } catch {\n // File not found — fall through to the RSC handler.\n }\n }\n\n // Convert Node request to Web Request.\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n }\n\n let body = undefined;\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n body = await new Promise((resolve) => {\n const chunks = [];\n req.on('data', (chunk) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks)));\n });\n }\n\n const webRequest = new Request(url.href, {\n method: req.method,\n headers,\n body,\n duplex: body ? 'half' : undefined,\n });\n\n try {\n // Support 103 Early Hints when available.\n const earlyHintsSender = (typeof res.writeEarlyHints === 'function')\n ? (links) => { try { res.writeEarlyHints({ link: links }); } catch {} }\n : undefined;\n\n const rawResponse = earlyHintsSender && runWithEarlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest);\n\n // Compress the response for self-hosted deployments.\n const webResponse = compressResponse(webRequest, rawResponse);\n\n // Write the response back to the Node response.\n //\n // Set-Cookie needs special handling: Headers.entries() joins multiple\n // Set-Cookie values with \", \" into a single entry, but each cookie must\n // be its own header per RFC 6265 §4.1. Object.fromEntries() would then\n // collapse them into one malformed Set-Cookie header that browsers reject\n // (or only honor partially). Use getSetCookie() to preserve individual\n // values, and pass them as an array — Node's writeHead accepts\n // Record<string, string | string[]> and emits one header per array entry.\n //\n // Without this, EVERY cookie set by the framework on Node—/Nitro deployments\n // is silently dropped: defineCookie().setCookie() in actions, middleware\n // cookie writes, the framework's own session cookies, all of it. See\n // LOCAL-741.\n const responseHeaders = {};\n webResponse.headers.forEach((value, key) => {\n if (key.toLowerCase() !== 'set-cookie') {\n responseHeaders[key] = value;\n }\n });\n const setCookies = webResponse.headers.getSetCookie();\n if (setCookies.length > 0) {\n responseHeaders['set-cookie'] = setCookies;\n }\n res.writeHead(webResponse.status, responseHeaders);\n\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n\n // Cancel the reader when the client disconnects. This causes any\n // pending reader.read() to reject, breaking the pump loop. Critical\n // for SSE and other infinite streams — without this, disconnected\n // clients leak readers.\n let clientDisconnected = false;\n const onClose = () => {\n clientDisconnected = true;\n reader.cancel('Client disconnected').catch(() => {});\n };\n res.on('close', onClose);\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n } catch (err) {\n // reader.cancel() from the close handler causes read() to reject.\n // This is expected on client disconnect — not an error.\n if (!clientDisconnected) {\n throw err;\n }\n } finally {\n res.off('close', onClose);\n reader.releaseLock();\n if (!res.writableEnded) {\n res.end();\n }\n }\n } else {\n res.end();\n }\n } catch (err) {\n console.error('[timber preview] Request error:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n }\n res.end('Internal Server Error');\n }\n});\n\nfunction listenWithBump(port, attempt = 0) {\n return new Promise((resolveListen, rejectListen) => {\n const onError = (err) => {\n server.removeListener('listening', onListening);\n if (err && err.code === 'EADDRINUSE' && !portIsExplicit && attempt < 100) {\n if (attempt === 0) {\n console.log(\\` [timber] Port \\${port} in use, trying \\${port + 1}...\\`);\n }\n listenWithBump(port + 1, attempt + 1).then(resolveListen, rejectListen);\n return;\n }\n rejectListen(err);\n };\n const onListening = () => {\n server.removeListener('error', onError);\n resolveListen(port);\n };\n server.once('error', onError);\n server.once('listening', onListening);\n server.listen(port, host);\n });\n}\n\ntry {\n boundPort = await listenWithBump(startPort);\n if (boundPort !== startPort) {\n console.log();\n console.log(\\` [timber] Port \\${startPort} in use, using \\${boundPort}\\`);\n }\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${boundPort}\\`);\n console.log();\n} catch (err) {\n if (err && err.code === 'EADDRINUSE') {\n console.error(\\`[timber] Port \\${startPort} is already in use. \\` +\n 'Set PORT to a free port, or unset PORT to auto-pick.');\n } else {\n console.error('[timber] Failed to start preview server:', err);\n }\n process.exit(1);\n}\n`;\n}\n\n/** Command descriptor for Nitro preview — testable without spawning. */\nexport interface NitroPreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroPreviewCommand(\n buildDir: string,\n preset: NitroPreset\n): NitroPreviewCommand | null {\n if (!LOCALLY_PREVIEWABLE.has(preset)) return null;\n\n const nitroDir = join(buildDir, 'nitro');\n const entryPath = join(nitroDir, 'entry.ts');\n\n const command = preset === 'bun' ? 'bun' : 'node';\n return {\n command,\n args: [entryPath],\n cwd: nitroDir,\n };\n}\n\n/**\n * Run the Nitro production build using the programmatic API.\n * Uses dynamic import so nitro is only loaded at build time.\n * Externalizes the timber RSC/SSR output — those files are pre-built\n * by timber and have internal references that nitro's bundler can't follow.\n */\nasync function runNitroBuild(\n nitroDir: string,\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): Promise<void> {\n const presetConfig = PRESET_CONFIGS[preset];\n const {\n createNitro,\n build: nitroBuild,\n prepare,\n copyPublicAssets,\n } = await import('nitro/builder');\n\n const nitro = await createNitro({\n rootDir: nitroDir,\n preset: presetConfig.nitroPreset,\n // Use renderer.entry so Nitro wraps our handler with its server runtime\n // (HTTP server, static file serving, graceful shutdown, etc.).\n // Using `entry` directly would bypass the Nitro server runtime.\n renderer: { handler: join(nitroDir, 'entry.ts') },\n output: { dir: join(nitroDir, presetConfig.outputDir) },\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n '/_timber/fonts/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n // Don't bundle the timber RSC/SSR build output — it has its own\n // internal file references that nitro's bundler can't follow.\n // Mark them as external so rollup leaves the imports as-is.\n rollupConfig: {\n external: [/\\.\\.\\/rsc\\//],\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n });\n\n await prepare(nitro);\n await copyPublicAssets(nitro);\n await nitroBuild(nitro);\n await nitro.close();\n}\n\n/** Spawn a Nitro preview process and pipe stdio. */\nfunction spawnNitroPreview(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Get the preset configuration for a given preset name.\n * @internal Exported for testing.\n */\nexport function getPresetConfig(preset: NitroPreset): PresetConfig {\n return PRESET_CONFIGS[preset];\n}\n"],"mappings":"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"}
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -32,12 +32,6 @@ export interface TimberPlatformAdapter {
|
|
|
32
32
|
* Falls back to the built-in Node.js preview server if not provided.
|
|
33
33
|
*/
|
|
34
34
|
preview?(config: TimberConfig, buildDir: string): Promise<void>;
|
|
35
|
-
/**
|
|
36
|
-
* Register a promise to be kept alive after the response is sent.
|
|
37
|
-
* Maps to platform-specific lifecycle extension (e.g. ctx.waitUntil()
|
|
38
|
-
* on Cloudflare Workers). Undefined if the platform doesn't support it.
|
|
39
|
-
*/
|
|
40
|
-
waitUntil?(promise: Promise<unknown>): void;
|
|
41
35
|
/**
|
|
42
36
|
* Send 103 Early Hints to the client before the final response.
|
|
43
37
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;;OAGG;IACH,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhE
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;;OAGG;IACH,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhE;;;;;;;;;OASG;IACH,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACxC"}
|
package/dist/cache/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { _ as setDefaultSingleflightTimeout, f as MemoryCacheHandler, g as setCacheHandler, h as getCacheHandler, m as RedisCacheHandler, p as TagAwareCacheHandler, t as cache } from "../_chunks/cache-api-
|
|
1
|
+
import { _ as setDefaultSingleflightTimeout, f as MemoryCacheHandler, g as setCacheHandler, h as getCacheHandler, m as RedisCacheHandler, p as TagAwareCacheHandler, t as cache } from "../_chunks/cache-api-CzYUlgXA.js";
|
|
2
2
|
import { t as estimateByteSize } from "../_chunks/sizeof-lXffQ1Vr.js";
|
|
3
3
|
export { MemoryCacheHandler, RedisCacheHandler, TagAwareCacheHandler, cache, estimateByteSize, getCacheHandler, setCacheHandler, setDefaultSingleflightTimeout };
|
package/dist/cli.js
CHANGED
|
@@ -115,7 +115,7 @@ async function runCheck(options) {
|
|
|
115
115
|
async function runSchema(args) {
|
|
116
116
|
const subcommand = args[0];
|
|
117
117
|
if (subcommand !== "sync") throw new Error(`Unknown schema subcommand: ${subcommand ?? "(none)"}. Usage: timber schema sync`);
|
|
118
|
-
const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync-
|
|
118
|
+
const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync-NfLbLnDw.js").then((n) => n.t);
|
|
119
119
|
const result = runSchemaSync(process.cwd());
|
|
120
120
|
if (result.filesystemSegments.length === 0) {
|
|
121
121
|
console.log("[timber] No dynamic segments found in app/.");
|