@timber-js/app 0.1.40 → 0.1.41

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.
@@ -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;AAsBnE;;;;;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;AAuED,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,CA0F9E;AAID,sCAAsC;AACtC,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,EACnB,eAAe,UAAQ,GACtB,MAAM,CAyCR;AAED,sCAAsC;AACtC,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAyBR;AAOD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAsKnF;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;AAyDD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAEjE"}
1
+ {"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAsBnE;;;;;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;AAuED,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,CA0F9E;AAID,sCAAsC;AACtC,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,EACnB,eAAe,UAAQ,GACtB,MAAM,CA8CR;AAED,sCAAsC;AACtC,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAyBR;AAOD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAsKnF;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;AAyDD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAEjE"}
@@ -242,28 +242,31 @@ function nitro(options = {}) {
242
242
  }
243
243
  /** @internal Exported for testing. */
244
244
  function generateNitroEntry(buildDir, outDir, preset, hasManifestInit = false) {
245
+ const serverEntryRelative = "./rsc/index.js";
246
+ const runtimeName = PRESET_CONFIGS[preset].runtimeName;
247
+ const handlerCall = PRESET_CONFIGS[preset].supportsEarlyHints ? ` const nodeRes = event.node?.res
248
+ const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')
249
+ ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }
250
+ : undefined
251
+
252
+ const webResponse = earlyHintsSender
253
+ ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))
254
+ : await handler(webRequest)` : ` const webResponse = await handler(webRequest)`;
245
255
  return `// Generated by @timber-js/app/adapters/nitro
246
256
  // Do not edit — this file is regenerated on each build.
247
257
 
248
- import { defineEventHandler } from 'nitro/h3'
249
- import handler, { runWithEarlyHintsSender } from './rsc/index.js'
258
+ ${hasManifestInit ? "import './_timber-manifest-init.js'\n" : ""}import { defineEventHandler } from 'nitro/h3'
259
+ import handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'
250
260
  import { compressResponse } from './_compress.mjs'
251
261
 
252
262
  // Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
253
263
  // See design/25-production-deployments.md §"TIMBER_RUNTIME".
254
- process.env.TIMBER_RUNTIME = '${PRESET_CONFIGS[preset].runtimeName}'
264
+ process.env.TIMBER_RUNTIME = '${runtimeName}'
255
265
 
256
266
  export default defineEventHandler(async (event) => {
257
267
  // h3 v2: event.req is the Web Request
258
268
  const webRequest = event.req
259
- ${PRESET_CONFIGS[preset].supportsEarlyHints ? ` const nodeRes = event.node?.res
260
- const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')
261
- ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }
262
- : undefined
263
-
264
- const webResponse = earlyHintsSender
265
- ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))
266
- : await handler(webRequest)` : ` const webResponse = await handler(webRequest)`}
269
+ ${handlerCall}
267
270
  return compressResponse(webRequest, webResponse)
268
271
  })
269
272
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/compress-module.ts","../../src/adapters/nitro.ts"],"sourcesContent":["// Generated compression module template for self-hosted deployments.\n//\n// This file generates a standalone ESM module (_compress.mjs) that is\n// written to the build output during adapter buildOutput(). It's imported\n// by the Nitro entry and preview server at runtime.\n//\n// Uses CompressionStream (Web API) for gzip and node:zlib for brotli.\n// Cloudflare Workers don't need this — the edge auto-compresses.\n//\n// See design/25-production-deployments.md.\n\n/**\n * Generate a standalone ESM module that exports compressResponse().\n *\n * Written to `_compress.mjs` during buildOutput. Imported by the Nitro entry\n * and preview server.\n *\n * @internal Exported for testing.\n */\nexport function generateCompressModule(): string {\n return `// Generated by @timber-js/app — response compression for self-hosted deployments.\n// Do not edit — this file is regenerated on each build.\n// Uses CompressionStream (Web API) for gzip, node:zlib for brotli.\n\nimport { createBrotliCompress, constants as zlibConstants } from 'node:zlib';\nimport { Readable } from 'node:stream';\n\nconst COMPRESSIBLE_TYPES = new Set([\n 'text/html', 'text/css', 'text/plain', 'text/xml', 'text/javascript',\n 'text/x-component', 'application/json', 'application/javascript',\n 'application/xml', 'application/xhtml+xml', 'application/rss+xml',\n 'application/atom+xml', 'image/svg+xml',\n]);\n\nconst NO_COMPRESS_STATUSES = new Set([204, 304]);\n\nfunction negotiateEncoding(acceptEncoding) {\n if (!acceptEncoding) return null;\n const tokens = acceptEncoding.split(',').map(s => s.split(';')[0].trim().toLowerCase());\n if (tokens.includes('br')) return 'br';\n if (tokens.includes('gzip')) return 'gzip';\n return null;\n}\n\nfunction shouldCompress(response) {\n if (!response.body) return false;\n if (NO_COMPRESS_STATUSES.has(response.status)) return false;\n if (response.headers.has('Content-Encoding')) return false;\n const contentType = response.headers.get('Content-Type');\n if (!contentType) return false;\n const mimeType = contentType.split(';')[0].trim().toLowerCase();\n if (mimeType === 'text/event-stream') return false;\n return COMPRESSIBLE_TYPES.has(mimeType);\n}\n\nfunction compressWithGzip(body) {\n return body.pipeThrough(new CompressionStream('gzip'));\n}\n\nfunction compressWithBrotli(body) {\n const brotli = createBrotliCompress({\n params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 4 },\n });\n const reader = body.getReader();\n const pump = async () => {\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) { brotli.end(); return; }\n if (!brotli.write(value)) {\n await new Promise(resolve => brotli.once('drain', resolve));\n }\n }\n } catch (err) {\n brotli.destroy(err instanceof Error ? err : new Error(String(err)));\n }\n };\n pump();\n return Readable.toWeb(brotli);\n}\n\nexport function compressResponse(request, response) {\n if (!shouldCompress(response)) return response;\n const acceptEncoding = request.headers.get('Accept-Encoding') || '';\n const encoding = negotiateEncoding(acceptEncoding);\n if (!encoding) return response;\n const compressedBody = encoding === 'br'\n ? compressWithBrotli(response.body)\n : compressWithGzip(response.body);\n const headers = new Headers(response.headers);\n headers.set('Content-Encoding', encoding);\n headers.delete('Content-Length');\n const existingVary = headers.get('Vary');\n if (existingVary) {\n if (!existingVary.toLowerCase().includes('accept-encoding')) {\n headers.set('Vary', existingVary + ', Accept-Encoding');\n }\n } else {\n headers.set('Vary', 'Accept-Encoding');\n }\n return new Response(compressedBody, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\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, mkdir, cp } 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';\n// Inlined from server/asset-headers.ts — adapters are loaded by Node at\n// Vite startup time, before Vite's module resolver is available, so cross-\n// directory .ts imports don't resolve.\nconst IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\nconst STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\nfunction generateHeadersFile(): string {\n return `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/*\n Cache-Control: ${STATIC_CACHE}\n`;\n}\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 supportsEarlyHints: true,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: true,\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 * 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 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 await mkdir(outDir, { recursive: true });\n\n // Copy client assets to public directory.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const publicDir = join(outDir, 'public');\n await mkdir(publicDir, { recursive: true });\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: config.clientJavascriptDisabled ? (src: string) => !src.endsWith('.js') : undefined,\n }).catch(() => {\n // Client dir may not exist when client JavaScript is disabled\n });\n\n // Write _headers file for platforms that support it (Netlify, etc.).\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n await writeFile(join(publicDir, '_headers'), generateHeadersFile());\n\n // Write the build manifest init module (if manifest data was produced).\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\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'), generateCompressModule());\n\n // Copy rsc/ssr build output into the nitro dir so imports stay local\n // during the Nitro bundling step (avoids broken relative paths in output).\n await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });\n await cp(join(buildDir, 'ssr'), join(outDir, 'ssr'), { recursive: true }).catch(() => {});\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);\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 hasManifestInit = false,\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\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 const handlerCall = earlyHints\n ? ` 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 : ` const webResponse = await handler(webRequest)`;\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport { defineEventHandler } from 'nitro/h3'\nimport handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'\nimport { compressResponse } from './_compress.mjs'\n\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\nexport default defineEventHandler(async (event) => {\n // h3 v2: event.req is the Web Request\n const webRequest = event.req\n${handlerCall}\n return compressResponse(webRequest, webResponse)\n})\n`;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroConfig(\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): string {\n const presetConfig = PRESET_CONFIGS[preset];\n\n const config: Record<string, unknown> = {\n preset: presetConfig.nitroPreset,\n entry: './entry.ts',\n output: { dir: presetConfig.outputDir },\n // Static asset cache headers — hashed assets are immutable, others get 1h.\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n };\n\n const configJson = JSON.stringify(config, null, 2);\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport { defineNitroConfig } from 'nitro/config'\n\nexport default defineNitroConfig(${configJson})\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}');\nconst port = parseInt(process.env.PORT || '3000', 10);\nconst host = process.env.HOST || process.env.HOSTNAME || 'localhost';\n\nconst server = createServer(async (req, res) => {\n const url = new URL(req.url || '/', \\`http://\\${host}:\\${port}\\`);\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 cacheControl = url.pathname.startsWith('/assets/')\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 res.writeHead(webResponse.status, Object.fromEntries(webResponse.headers.entries()));\n\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n const pump = async () => {\n while (true) {\n const { done, value } = await reader.read();\n if (done) { res.end(); return; }\n res.write(value);\n }\n };\n await pump();\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\nserver.listen(port, host, () => {\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${port}\\`);\n console.log();\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 { createNitro, build: nitroBuild, prepare, copyPublicAssets } = await import('nitro');\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: { entry: join(nitroDir, 'entry.ts') },\n output: { dir: join(nitroDir, presetConfig.outputDir) },\n routeRules: {\n '/assets/**': { 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":";;;;;;;;;;;;AAmBA,SAAgB,yBAAiC;AAC/C,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACLT,IAAM,kBAAkB;AACxB,IAAM,eAAe;AAErB,SAAS,sBAA8B;AACrC,QAAO;;;;mBAIU,gBAAgB;;;mBAGhB,aAAa;;;AAuChC,IAAM,iBAAoD;CACxD,UAAU;EACR,aAAa;EACb,WAAW;EACX,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACb,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,IAAI,EAAE,EAAE;EAC5D;CACD,eAAe;EACb,aAAa;EACb,WAAW;EACX,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACd;CACD,WAAW;EACT,aAAa;EACb,WAAW;EACX,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACd;CACD,gBAAgB;EACd,aAAa;EACb,WAAW;EACX,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACd;CACD,cAAc;EACZ,aAAa;EACb,WAAW;EACX,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACd;CACD,eAAe;EACb,aAAa;EACb,WAAW;EACX,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACd;CACD,mBAAmB;EACjB,aAAa;EACb,WAAW;EACX,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACd;CACD,eAAe;EACb,aAAa;EACb,WAAW;EACX,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACd;CACD,OAAO;EACL,aAAa;EACb,WAAW;EACX,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACd;CACF;;;;;;;;;;;;;;;;;AAqCD,SAAgB,MAAM,UAA+B,EAAE,EAAyB;CAC9E,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,eAAe,eAAe;CACpC,MAAM,kBAAsC,EAAE;AAE9C,QAAO;EACL,MAAM,SAAS;EAEf,MAAM,YAAY,QAAsB,UAAkB;GACxD,MAAM,SAAS,KAAK,UAAU,QAAQ;AACtC,SAAM,MAAM,QAAQ,EAAE,WAAW,MAAM,CAAC;GAKxC,MAAM,YAAY,KAAK,UAAU,SAAS;GAC1C,MAAM,YAAY,KAAK,QAAQ,SAAS;AACxC,SAAM,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;AAC3C,SAAM,GAAG,WAAW,WAAW;IAC7B,WAAW;IACX,QAAQ,OAAO,4BAA4B,QAAgB,CAAC,IAAI,SAAS,MAAM,GAAG,KAAA;IACnF,CAAC,CAAC,YAAY,GAEb;AAIF,SAAM,UAAU,KAAK,WAAW,WAAW,EAAE,qBAAqB,CAAC;AAGnE,OAAI,OAAO,aACT,OAAM,UAAU,KAAK,QAAQ,2BAA2B,EAAE,OAAO,aAAa;AAMhF,SAAM,UAAU,KAAK,QAAQ,gBAAgB,EAAE,wBAAwB,CAAC;AAIxE,SAAM,GAAG,KAAK,UAAU,MAAM,EAAE,KAAK,QAAQ,MAAM,EAAE,EAAE,WAAW,MAAM,CAAC;AACzE,SAAM,GAAG,KAAK,UAAU,MAAM,EAAE,KAAK,QAAQ,MAAM,EAAE,EAAE,WAAW,MAAM,CAAC,CAAC,YAAY,GAAG;AAMzF,OAAI,OAAO,cAAc;IACvB,MAAM,WAAW,KAAK,QAAQ,OAAO,WAAW;IAChD,MAAM,aAAa,MAAM,SAAS,UAAU,QAAQ;AACpD,UAAM,UAAU,UAAU,GAAG,OAAO,aAAa,IAAI,aAAa;;GAIpE,MAAM,QAAQ,mBAAmB,UAAU,QAAQ,OAAO;AAC1D,SAAM,UAAU,KAAK,QAAQ,WAAW,EAAE,MAAM;AAKhD,SAAM,cAAc,QAAQ,QAAQ,QAAQ,YAAY;;EAM1D,SAAS,oBAAoB,IAAI,OAAO,GACpC,OAAO,SAAuB,aAAqB;GAIjD,MAAM,gBAAgB,sBAAsB,UAAU,OAAO;GAC7D,MAAM,aAAa,KAAK,UAAU,SAAS,sBAAsB;AACjE,SAAM,UAAU,YAAY,cAAc;AAG1C,SAAM,kBADU,WAAW,QAAQ,QAAQ,QACV,CAAC,WAAW,EAAE,KAAK,UAAU,QAAQ,CAAC;MAEzE,KAAA;EAEJ,WAAW,aAAa,qBACnB,YAA8B;GAC7B,MAAM,UAAU,QAAQ,OAAO,QAAQ;AACrC,YAAQ,MAAM,wCAAwC,IAAI;KAC1D;AACF,mBAAgB,KAAK,QAAQ;MAE/B,KAAA;EACL;;;AAMH,SAAgB,mBACd,UACA,QACA,QACA,kBAAkB,OACV;AAuBR,QAAO;;;;;;;;;gCAjBa,eAAe,QAAQ,YA0BD;;;;;EAzBvB,eAAe,QAAQ,qBAMtC;;;;;;;mCAQA,kDAgBQ;;;;;;AAOd,SAAgB,oBACd,QACA,YACQ;CACR,MAAM,eAAe,eAAe;CAEpC,MAAM,SAAkC;EACtC,QAAQ,aAAa;EACrB,OAAO;EACP,QAAQ,EAAE,KAAK,aAAa,WAAW;EAGvC,YAAY,EACV,cAAc,EAAE,SAAS,EAAE,iBAAiB,iBAAiB,EAAE,EAChE;EACD,GAAG,aAAa;EAChB,GAAG;EACJ;AAID,QAAO;;;;;mCAFY,KAAK,UAAU,QAAQ,MAAM,EAAE,CAON;;;;AAO9C,IAAM,sBAAsB,IAAI,IAAiB,CAAC,eAAe,MAAM,CAAC;;;;;;;;;AAUxE,SAAgB,sBAAsB,UAAkB,QAA6B;CACnF,MAAM,mBAAmB,SAAS,KAAK,UAAU,QAAQ,EAAE,KAAK,UAAU,OAAO,WAAW,CAAC;CAC7F,MAAM,WAAW,iBAAiB,WAAW,IAAI,GAAG,mBAAmB,OAAO;CAC9E,MAAM,YAAY;CAClB,MAAM,mBAAmB;AAGzB,QAAO;;;;;;;;;;;gCAFa,eAAe,QAAQ,YAaD;;;;wCAIJ,iBAAiB;;kBAEvC,iBAAiB;;;;sEAImC,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCA8B1C,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsH/C,SAAgB,4BACd,UACA,QAC4B;AAC5B,KAAI,CAAC,oBAAoB,IAAI,OAAO,CAAE,QAAO;CAE7C,MAAM,WAAW,KAAK,UAAU,QAAQ;CACxC,MAAM,YAAY,KAAK,UAAU,WAAW;AAG5C,QAAO;EACL,SAFc,WAAW,QAAQ,QAAQ;EAGzC,MAAM,CAAC,UAAU;EACjB,KAAK;EACN;;;;;;;;AASH,eAAe,cACb,UACA,QACA,YACe;CACf,MAAM,eAAe,eAAe;CACpC,MAAM,EAAE,aAAa,OAAO,YAAY,SAAS,qBAAqB,MAAM,OAAO;CAEnF,MAAM,QAAQ,MAAM,YAAY;EAC9B,SAAS;EACT,QAAQ,aAAa;EAIrB,UAAU,EAAE,OAAO,KAAK,UAAU,WAAW,EAAE;EAC/C,QAAQ,EAAE,KAAK,KAAK,UAAU,aAAa,UAAU,EAAE;EACvD,YAAY,EACV,cAAc,EAAE,SAAS,EAAE,iBAAiB,iBAAiB,EAAE,EAChE;EAID,cAAc,EACZ,UAAU,CAAC,cAAc,EAC1B;EACD,GAAG,aAAa;EAChB,GAAG;EACJ,CAAC;AAEF,OAAM,QAAQ,MAAM;AACpB,OAAM,iBAAiB,MAAM;AAC7B,OAAM,WAAW,MAAM;AACvB,OAAM,MAAM,OAAO;;;AAIrB,SAAS,kBAAkB,SAAiB,MAAgB,KAA4B;AACtF,QAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,QAAQ,SAAS,SAAS,MAAM,EAAE,KAAK,GAAG,QAAQ;AACtD,OAAI,IAAK,QAAO,IAAI;OACf,UAAS;IACd;AACF,QAAM,QAAQ,KAAK,QAAQ,OAAO;AAClC,QAAM,QAAQ,KAAK,QAAQ,OAAO;GAClC;;;;;;AASJ,SAAgB,gBAAgB,QAAmC;AACjE,QAAO,eAAe"}
1
+ {"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/compress-module.ts","../../src/adapters/nitro.ts"],"sourcesContent":["// Generated compression module template for self-hosted deployments.\n//\n// This file generates a standalone ESM module (_compress.mjs) that is\n// written to the build output during adapter buildOutput(). It's imported\n// by the Nitro entry and preview server at runtime.\n//\n// Uses CompressionStream (Web API) for gzip and node:zlib for brotli.\n// Cloudflare Workers don't need this — the edge auto-compresses.\n//\n// See design/25-production-deployments.md.\n\n/**\n * Generate a standalone ESM module that exports compressResponse().\n *\n * Written to `_compress.mjs` during buildOutput. Imported by the Nitro entry\n * and preview server.\n *\n * @internal Exported for testing.\n */\nexport function generateCompressModule(): string {\n return `// Generated by @timber-js/app — response compression for self-hosted deployments.\n// Do not edit — this file is regenerated on each build.\n// Uses CompressionStream (Web API) for gzip, node:zlib for brotli.\n\nimport { createBrotliCompress, constants as zlibConstants } from 'node:zlib';\nimport { Readable } from 'node:stream';\n\nconst COMPRESSIBLE_TYPES = new Set([\n 'text/html', 'text/css', 'text/plain', 'text/xml', 'text/javascript',\n 'text/x-component', 'application/json', 'application/javascript',\n 'application/xml', 'application/xhtml+xml', 'application/rss+xml',\n 'application/atom+xml', 'image/svg+xml',\n]);\n\nconst NO_COMPRESS_STATUSES = new Set([204, 304]);\n\nfunction negotiateEncoding(acceptEncoding) {\n if (!acceptEncoding) return null;\n const tokens = acceptEncoding.split(',').map(s => s.split(';')[0].trim().toLowerCase());\n if (tokens.includes('br')) return 'br';\n if (tokens.includes('gzip')) return 'gzip';\n return null;\n}\n\nfunction shouldCompress(response) {\n if (!response.body) return false;\n if (NO_COMPRESS_STATUSES.has(response.status)) return false;\n if (response.headers.has('Content-Encoding')) return false;\n const contentType = response.headers.get('Content-Type');\n if (!contentType) return false;\n const mimeType = contentType.split(';')[0].trim().toLowerCase();\n if (mimeType === 'text/event-stream') return false;\n return COMPRESSIBLE_TYPES.has(mimeType);\n}\n\nfunction compressWithGzip(body) {\n return body.pipeThrough(new CompressionStream('gzip'));\n}\n\nfunction compressWithBrotli(body) {\n const brotli = createBrotliCompress({\n params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 4 },\n });\n const reader = body.getReader();\n const pump = async () => {\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) { brotli.end(); return; }\n if (!brotli.write(value)) {\n await new Promise(resolve => brotli.once('drain', resolve));\n }\n }\n } catch (err) {\n brotli.destroy(err instanceof Error ? err : new Error(String(err)));\n }\n };\n pump();\n return Readable.toWeb(brotli);\n}\n\nexport function compressResponse(request, response) {\n if (!shouldCompress(response)) return response;\n const acceptEncoding = request.headers.get('Accept-Encoding') || '';\n const encoding = negotiateEncoding(acceptEncoding);\n if (!encoding) return response;\n const compressedBody = encoding === 'br'\n ? compressWithBrotli(response.body)\n : compressWithGzip(response.body);\n const headers = new Headers(response.headers);\n headers.set('Content-Encoding', encoding);\n headers.delete('Content-Length');\n const existingVary = headers.get('Vary');\n if (existingVary) {\n if (!existingVary.toLowerCase().includes('accept-encoding')) {\n headers.set('Vary', existingVary + ', Accept-Encoding');\n }\n } else {\n headers.set('Vary', 'Accept-Encoding');\n }\n return new Response(compressedBody, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\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, mkdir, cp } 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';\n// Inlined from server/asset-headers.ts — adapters are loaded by Node at\n// Vite startup time, before Vite's module resolver is available, so cross-\n// directory .ts imports don't resolve.\nconst IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\nconst STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\nfunction generateHeadersFile(): string {\n return `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/*\n Cache-Control: ${STATIC_CACHE}\n`;\n}\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 supportsEarlyHints: true,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: true,\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 * 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 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 await mkdir(outDir, { recursive: true });\n\n // Copy client assets to public directory.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const publicDir = join(outDir, 'public');\n await mkdir(publicDir, { recursive: true });\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: config.clientJavascriptDisabled ? (src: string) => !src.endsWith('.js') : undefined,\n }).catch(() => {\n // Client dir may not exist when client JavaScript is disabled\n });\n\n // Write _headers file for platforms that support it (Netlify, etc.).\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n await writeFile(join(publicDir, '_headers'), generateHeadersFile());\n\n // Write the build manifest init module (if manifest data was produced).\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\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'), generateCompressModule());\n\n // Copy rsc/ssr build output into the nitro dir so imports stay local\n // during the Nitro bundling step (avoids broken relative paths in output).\n await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });\n await cp(join(buildDir, 'ssr'), join(outDir, 'ssr'), { recursive: true }).catch(() => {});\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);\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 hasManifestInit = false,\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\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 const handlerCall = earlyHints\n ? ` 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 : ` const webResponse = await handler(webRequest)`;\n\n // Build manifest init must be imported before the 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 return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\n${manifestImport}import { defineEventHandler } from 'nitro/h3'\nimport handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'\nimport { compressResponse } from './_compress.mjs'\n\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\nexport default defineEventHandler(async (event) => {\n // h3 v2: event.req is the Web Request\n const webRequest = event.req\n${handlerCall}\n return compressResponse(webRequest, webResponse)\n})\n`;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroConfig(\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): string {\n const presetConfig = PRESET_CONFIGS[preset];\n\n const config: Record<string, unknown> = {\n preset: presetConfig.nitroPreset,\n entry: './entry.ts',\n output: { dir: presetConfig.outputDir },\n // Static asset cache headers — hashed assets are immutable, others get 1h.\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n };\n\n const configJson = JSON.stringify(config, null, 2);\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport { defineNitroConfig } from 'nitro/config'\n\nexport default defineNitroConfig(${configJson})\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}');\nconst port = parseInt(process.env.PORT || '3000', 10);\nconst host = process.env.HOST || process.env.HOSTNAME || 'localhost';\n\nconst server = createServer(async (req, res) => {\n const url = new URL(req.url || '/', \\`http://\\${host}:\\${port}\\`);\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 cacheControl = url.pathname.startsWith('/assets/')\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 res.writeHead(webResponse.status, Object.fromEntries(webResponse.headers.entries()));\n\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n const pump = async () => {\n while (true) {\n const { done, value } = await reader.read();\n if (done) { res.end(); return; }\n res.write(value);\n }\n };\n await pump();\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\nserver.listen(port, host, () => {\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${port}\\`);\n console.log();\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 { createNitro, build: nitroBuild, prepare, copyPublicAssets } = await import('nitro');\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: { entry: join(nitroDir, 'entry.ts') },\n output: { dir: join(nitroDir, presetConfig.outputDir) },\n routeRules: {\n '/assets/**': { 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 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/client/link-navigate-interceptor.tsx","../../src/client/use-link-status.ts","../../src/client/navigation-context.ts","../../src/client/link-status-provider.tsx","../../src/client/link.tsx","../../src/client/segment-cache.ts","../../src/client/history.ts","../../src/client/use-params.ts","../../src/client/router.ts","../../src/client/use-navigation-pending.ts","../../src/client/router-ref.ts","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/use-search-params.ts","../../src/client/segment-context.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx"],"sourcesContent":["'use client';\n\n// LinkNavigateInterceptor — client component that stores an onNavigate callback\n// on the parent <a> element so the delegated click handler in browser-entry.ts\n// can invoke it before triggering SPA navigation.\n//\n// See design/19-client-navigation.md, TIM-167\n\nimport { useRef, useEffect, type ReactNode } from 'react';\n\n/** Symbol used to store the onNavigate callback on anchor elements. */\nexport const ON_NAVIGATE_KEY = '__timberOnNavigate' as const;\n\nexport type OnNavigateEvent = {\n preventDefault: () => void;\n};\n\nexport type OnNavigateHandler = (e: OnNavigateEvent) => void;\n\n/**\n * Augment HTMLAnchorElement with the optional onNavigate property.\n * Used by browser-entry.ts handleLinkClick to check for the callback.\n */\ndeclare global {\n interface HTMLAnchorElement {\n [ON_NAVIGATE_KEY]?: OnNavigateHandler;\n }\n}\n\n/**\n * Client component rendered inside <Link> that attaches the onNavigate\n * callback to the closest <a> ancestor via a DOM property. The callback\n * is cleaned up on unmount.\n *\n * Renders no extra DOM — just a transparent wrapper.\n */\nexport function LinkNavigateInterceptor({\n onNavigate,\n children,\n}: {\n onNavigate: OnNavigateHandler;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLSpanElement>(null);\n\n useEffect(() => {\n const anchor = ref.current?.closest('a');\n if (!anchor) return;\n anchor[ON_NAVIGATE_KEY] = onNavigate;\n return () => {\n delete anchor[ON_NAVIGATE_KEY];\n };\n }, [onNavigate]);\n\n // Use a <span> with display:contents to avoid affecting layout.\n // The ref lets us walk up to the parent <a> in the effect.\n return (\n <span ref={ref} style={{ display: 'contents' }}>\n {children}\n </span>\n );\n}\n","'use client';\n\n// useLinkStatus — returns { pending: true } while the nearest parent <Link>'s\n// navigation is in flight. No arguments — scoped via React context.\n// See design/19-client-navigation.md §\"useLinkStatus()\"\n\nimport { useContext, createContext } from 'react';\n\nexport interface LinkStatus {\n pending: boolean;\n}\n\n/**\n * React context provided by <Link>. Holds the pending status\n * for that specific link's navigation.\n */\nexport const LinkStatusContext = createContext<LinkStatus>({ pending: false });\n\n/**\n * Returns `{ pending: true }` while the nearest parent `<Link>` component's\n * navigation is in flight. Must be used inside a `<Link>` component's children.\n *\n * Unlike `useNavigationPending()` which is global, this hook is scoped to\n * the nearest parent `<Link>` — only the link the user clicked shows pending.\n *\n * ```tsx\n * 'use client'\n * import { Link, useLinkStatus } from '@timber-js/app/client'\n *\n * function Hint() {\n * const { pending } = useLinkStatus()\n * return <span className={pending ? 'opacity-50' : ''} />\n * }\n *\n * export function NavLink({ href, children }) {\n * return (\n * <Link href={href}>\n * {children} <Hint />\n * </Link>\n * )\n * }\n * ```\n */\nexport function useLinkStatus(): LinkStatus {\n return useContext(LinkStatusContext);\n}\n","/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current route params and pathname, updated atomically\n * with the RSC tree on each navigation. This replaces the previous\n * useSyncExternalStore approach for useParams() and usePathname(),\n * which suffered from a timing gap: the new tree could commit before\n * the external store re-renders fired, causing a frame where both\n * old and new active states were visible simultaneously.\n *\n * By wrapping the RSC payload element in NavigationProvider inside\n * renderRoot(), the context value and the element tree are passed to\n * reactRoot.render() in the same call — atomic by construction.\n * All consumers (useParams, usePathname) see the new values in the\n * same render pass as the new tree.\n *\n * During SSR, no NavigationProvider is mounted. Hooks fall back to\n * the ALS-backed getSsrData() for per-request isolation.\n *\n * IMPORTANT: createContext and useContext are NOT available in the RSC\n * environment (React Server Components use a stripped-down React).\n * The context is lazily initialized on first access, and all functions\n * that depend on these APIs are safe to call from any environment —\n * they return null or no-op when the APIs aren't available.\n *\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport React, { createElement, type ReactNode } from 'react';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\nexport interface NavigationState {\n params: Record<string, string | string[]>;\n pathname: string;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy context initialization\n// ---------------------------------------------------------------------------\n\n/**\n * The context is created lazily to avoid calling createContext at module\n * level. In the RSC environment, React.createContext doesn't exist —\n * calling it at import time would crash the server.\n */\nlet _context: React.Context<NavigationState | null> | undefined;\n\nfunction getOrCreateContext(): React.Context<NavigationState | null> | undefined {\n if (_context !== undefined) return _context;\n // createContext may not exist in the RSC environment\n if (typeof React.createContext === 'function') {\n _context = React.createContext<NavigationState | null>(null);\n }\n return _context;\n}\n\n/**\n * Read the navigation context. Returns null during SSR (no provider)\n * or in the RSC environment (no context available).\n * Internal — used by useParams() and usePathname().\n */\nexport function useNavigationContext(): NavigationState | null {\n const ctx = getOrCreateContext();\n if (!ctx) return null;\n // useContext may not exist in the RSC environment — caller wraps in try/catch\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n// ---------------------------------------------------------------------------\n// Provider component\n// ---------------------------------------------------------------------------\n\nexport interface NavigationProviderProps {\n value: NavigationState;\n children?: ReactNode;\n}\n\n/**\n * Wraps children with NavigationContext.Provider.\n *\n * Used in browser-entry.ts renderRoot to wrap the RSC payload element\n * so that navigation state updates atomically with the tree render.\n */\nexport function NavigationProvider({ value, children }: NavigationProviderProps): React.ReactElement {\n const ctx = getOrCreateContext();\n if (!ctx) {\n // RSC environment — no context available. Return children as-is.\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state for renderRoot to read\n// ---------------------------------------------------------------------------\n\n/**\n * Module-level navigation state. Updated by the router before calling\n * renderRoot(). The renderRoot callback reads this to create the\n * NavigationProvider with the correct values.\n *\n * This is NOT used by hooks directly — hooks read from React context.\n * This exists only as a communication channel between the router\n * (which knows the new nav state) and renderRoot (which wraps the element).\n */\nlet _currentNavState: NavigationState = { params: {}, pathname: '/' };\n\nexport function setNavigationState(state: NavigationState): void {\n _currentNavState = state;\n}\n\nexport function getNavigationState(): NavigationState {\n return _currentNavState;\n}\n\n// ---------------------------------------------------------------------------\n// Pending Navigation Context (same module for singleton guarantee)\n// ---------------------------------------------------------------------------\n\n/**\n * Separate context for the in-flight navigation URL. Provided by\n * TransitionRoot (useOptimistic state), consumed by LinkStatusProvider\n * and useNavigationPending.\n *\n * Lives in this module (not a separate file) to guarantee singleton\n * identity across chunks. The `'use client'` LinkStatusProvider and\n * the non-directive TransitionRoot both import from this module —\n * if they were in separate files, the bundler could duplicate the\n * module-level context variable across chunks.\n */\nlet _pendingContext: React.Context<string | null> | undefined;\n\nfunction getOrCreatePendingContext(): React.Context<string | null> | undefined {\n if (_pendingContext !== undefined) return _pendingContext;\n if (typeof React.createContext === 'function') {\n _pendingContext = React.createContext<string | null>(null);\n }\n return _pendingContext;\n}\n\n/**\n * Read the pending navigation URL from context.\n * Returns null during SSR (no provider) or in the RSC environment.\n */\nexport function usePendingNavigationUrl(): string | null {\n const ctx = getOrCreatePendingContext();\n if (!ctx) return null;\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n/**\n * Provider for the pending navigation URL. Wraps children with\n * the pending context Provider.\n */\nexport function PendingNavigationProvider({\n value,\n children,\n}: {\n value: string | null;\n children?: ReactNode;\n}): React.ReactElement {\n const ctx = getOrCreatePendingContext();\n if (!ctx) {\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n","'use client';\n\n// LinkStatusProvider — client component that provides per-link pending status\n// via React context. Used inside <Link> to power useLinkStatus().\n//\n// Reads pendingUrl from PendingNavigationContext (provided by TransitionRoot).\n// The pending URL is set as an URGENT update at navigation start (shows\n// immediately) and cleared inside startTransition when the new tree commits\n// (atomic with params/pathname). This eliminates both:\n// 1. The delay before showing the spinner (urgent update, not deferred)\n// 2. The gap between spinner disappearing and active state updating (same commit)\n\nimport type { ReactNode } from 'react';\nimport { LinkStatusContext, type LinkStatus } from './use-link-status.js';\nimport { usePendingNavigationUrl } from './navigation-context.js';\n\nconst NOT_PENDING: LinkStatus = { pending: false };\nconst IS_PENDING: LinkStatus = { pending: true };\n\n/**\n * Client component that reads the pending URL from PendingNavigationContext\n * and provides a scoped LinkStatusContext to children. Renders no extra DOM —\n * just a context provider around children.\n */\nexport function LinkStatusProvider({ href, children }: { href: string; children?: ReactNode }) {\n const pendingUrl = usePendingNavigationUrl();\n const status = pendingUrl === href ? IS_PENDING : NOT_PENDING;\n\n return <LinkStatusContext.Provider value={status}>{children}</LinkStatusContext.Provider>;\n}\n","'use client';\n\n// Link component — client-side navigation with progressive enhancement\n// See design/19-client-navigation.md § Progressive Enhancement\n//\n// Without JavaScript, <Link> renders as a plain <a> tag — standard browser\n// navigation. With JavaScript, the client runtime intercepts clicks on links\n// marked with data-timber-link, fetches RSC payloads, and reconciles the DOM.\n//\n// Typed Link: design/09-typescript.md §\"Typed Link\"\n// - href validated against known routes (via codegen overloads, not runtime)\n// - params prop typed per-route, URL interpolated at runtime\n// - searchParams prop serialized via SearchParamsDefinition\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport type { AnchorHTMLAttributes, ReactNode } from 'react';\nimport type { SearchParamsDefinition } from '#/search-params/create.js';\nimport type { OnNavigateHandler } from './link-navigate-interceptor.js';\nimport { LinkNavigateInterceptor } from './link-navigate-interceptor.js';\nimport { LinkStatusProvider } from './link-status-provider.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/**\n * Base props shared by all Link variants.\n */\ninterface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** Prefetch the RSC payload on hover */\n prefetch?: boolean;\n /**\n * Scroll to top on navigation. Defaults to true.\n * Set to false for tabbed interfaces where content changes within a fixed layout.\n */\n scroll?: boolean;\n /**\n * Called before client-side navigation commits. Call `e.preventDefault()`\n * to cancel the default navigation — the caller is then responsible for\n * navigating (e.g. via `router.push()`).\n *\n * Only fires for client-side SPA navigations, not full page loads.\n * Has no effect during SSR.\n */\n onNavigate?: OnNavigateHandler;\n children?: ReactNode;\n}\n\n/**\n * Link with a fully-resolved string href.\n * When using a string href with params already interpolated,\n * the params prop is not available.\n */\nexport interface LinkPropsWithHref extends LinkBaseProps {\n href: string;\n params?: never;\n /**\n * Typed search params — serialized via the route's SearchParamsDefinition.\n * Mutually exclusive with an inline query string in href.\n */\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n}\n\n/**\n * Link with a route pattern + params for interpolation.\n * e.g. <Link href=\"/products/[id]\" params={{ id: \"123\" }}>\n * <Link href=\"/products/[id]\" params={{ id: 123 }}>\n */\nexport interface LinkPropsWithParams extends LinkBaseProps {\n /** Route pattern with dynamic segments (e.g. \"/products/[id]\") */\n href: string;\n /**\n * Dynamic segment values to interpolate into the href.\n * Single dynamic segments accept string | number (numbers are stringified).\n * Catch-all segments accept string[].\n */\n params: Record<string, string | number | string[]>;\n /**\n * Typed search params — serialized via the route's SearchParamsDefinition.\n */\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n}\n\nexport type LinkProps = LinkPropsWithHref | LinkPropsWithParams;\n\n// ─── Dangerous URL Scheme Detection ──────────────────────────────\n\n/**\n * Reject dangerous URL schemes that could execute script.\n * Security: design/13-security.md § Link scheme injection (test #9)\n */\nconst DANGEROUS_SCHEMES = /^\\s*(javascript|data|vbscript):/i;\n\nexport function validateLinkHref(href: string): void {\n if (DANGEROUS_SCHEMES.test(href)) {\n throw new Error(\n `<Link> received a dangerous href: \"${href}\". ` +\n 'javascript:, data:, and vbscript: URLs are not allowed.'\n );\n }\n}\n\n// ─── Internal Link Detection ─────────────────────────────────────\n\n/** Returns true if the href is an internal path (not an external URL) */\nfunction isInternalHref(href: string): boolean {\n // Relative paths, root-relative paths, and hash links are internal\n if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {\n return true;\n }\n // Anything with a protocol scheme is external\n if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {\n return false;\n }\n // Bare relative paths (e.g., \"dashboard\") are internal\n return true;\n}\n\n// ─── URL Interpolation ──────────────────────────────────────────\n\n/**\n * Interpolate dynamic segments in a route pattern with actual values.\n * e.g. interpolateParams(\"/products/[id]\", { id: \"123\" }) → \"/products/123\"\n *\n * Supports:\n * - [param] → single segment\n * - [...param] → catch-all (joined with /)\n * - [[...param]] → optional catch-all (omitted if undefined/empty)\n */\nexport function interpolateParams(\n pattern: string,\n params: Record<string, string | number | string[]>\n): string {\n return (\n pattern\n .replace(\n /\\[\\[\\.\\.\\.(\\w+)\\]\\]|\\[\\.\\.\\.(\\w+)\\]|\\[(\\w+)\\]/g,\n (_match, optionalCatchAll, catchAll, single) => {\n if (optionalCatchAll) {\n const value = params[optionalCatchAll];\n if (value === undefined || (Array.isArray(value) && value.length === 0)) {\n return '';\n }\n const segments = Array.isArray(value) ? value : [value];\n return segments.map(encodeURIComponent).join('/');\n }\n\n if (catchAll) {\n const value = params[catchAll];\n if (value === undefined) {\n throw new Error(\n `<Link> missing required catch-all param \"${catchAll}\" for pattern \"${pattern}\".`\n );\n }\n const segments = Array.isArray(value) ? value : [value];\n if (segments.length === 0) {\n throw new Error(\n `<Link> catch-all param \"${catchAll}\" must have at least one segment for pattern \"${pattern}\".`\n );\n }\n return segments.map(encodeURIComponent).join('/');\n }\n\n // single dynamic segment\n const value = params[single];\n if (value === undefined) {\n throw new Error(`<Link> missing required param \"${single}\" for pattern \"${pattern}\".`);\n }\n if (Array.isArray(value)) {\n throw new Error(\n `<Link> param \"${single}\" expected a string but received an array for pattern \"${pattern}\".`\n );\n }\n // Accept numbers — coerce to string for URL interpolation\n return encodeURIComponent(String(value));\n }\n )\n // Clean up trailing slash from empty optional catch-all\n .replace(/\\/+$/, '') || '/'\n );\n}\n\n// ─── Resolve Href ───────────────────────────────────────────────\n\n/**\n * Resolve the final href string from Link props.\n *\n * Handles:\n * - params interpolation into route patterns\n * - searchParams serialization via SearchParamsDefinition\n * - Validation that searchParams and inline query strings are exclusive\n */\nexport function resolveHref(\n href: string,\n params?: Record<string, string | number | string[]>,\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n }\n): string {\n let resolvedPath = href;\n\n // Interpolate params if provided\n if (params) {\n resolvedPath = interpolateParams(href, params);\n }\n\n // Serialize searchParams if provided\n if (searchParams) {\n // Validate: searchParams prop and inline query string are mutually exclusive\n if (resolvedPath.includes('?')) {\n throw new Error(\n '<Link> received both a searchParams prop and a query string in href. ' +\n 'These are mutually exclusive — use one or the other.'\n );\n }\n\n const qs = searchParams.definition.serialize(searchParams.values);\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n }\n\n return resolvedPath;\n}\n\n// ─── Build Props ─────────────────────────────────────────────────\n\ninterface LinkOutputProps {\n 'href': string;\n 'data-timber-link'?: boolean;\n 'data-timber-prefetch'?: boolean;\n 'data-timber-scroll'?: string;\n}\n\n/**\n * Build the HTML attributes for a Link. Separated from the component\n * for testability — the component just spreads these onto an <a>.\n */\nexport function buildLinkProps(\n props: Pick<LinkPropsWithHref, 'href' | 'prefetch' | 'scroll'> & {\n params?: Record<string, string | number | string[]>;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n }\n): LinkOutputProps {\n const resolvedHref = resolveHref(props.href, props.params, props.searchParams);\n\n validateLinkHref(resolvedHref);\n\n const output: LinkOutputProps = { href: resolvedHref };\n const internal = isInternalHref(resolvedHref);\n\n if (internal) {\n output['data-timber-link'] = true;\n\n if (props.prefetch) {\n output['data-timber-prefetch'] = true;\n }\n\n if (props.scroll === false) {\n output['data-timber-scroll'] = 'false';\n }\n }\n\n return output;\n}\n\n// ─── Link Component ──────────────────────────────────────────────\n\n/**\n * Navigation link with progressive enhancement.\n *\n * Renders as a plain `<a>` tag — works without JavaScript. When the client\n * runtime is active, it intercepts clicks on links marked with\n * `data-timber-link` to perform RSC-based client navigation.\n *\n * Supports typed routes via codegen overloads. At runtime:\n * - `params` prop interpolates dynamic segments in the href pattern\n * - `searchParams` prop serializes query parameters via a SearchParamsDefinition\n */\nexport function Link({\n href,\n prefetch,\n scroll,\n params,\n searchParams,\n onNavigate,\n children,\n ...rest\n}: LinkProps) {\n const linkProps = buildLinkProps({ href, prefetch, scroll, params, searchParams });\n\n const inner = <LinkStatusProvider href={linkProps.href}>{children}</LinkStatusProvider>;\n\n return (\n <a {...rest} {...linkProps}>\n {onNavigate ? (\n <LinkNavigateInterceptor onNavigate={onNavigate}>{inner}</LinkNavigateInterceptor>\n ) : (\n inner\n )}\n </a>\n );\n}\n","// Segment Cache — stores the mounted segment tree and prefetched payloads\n// See design/19-client-navigation.md for architecture details.\n\nimport type { HeadElement } from './head';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** A prefetched RSC result with optional head elements and segment metadata. */\nexport interface PrefetchResult {\n payload: unknown;\n headElements: HeadElement[] | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo?: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useParams(). */\n params?: Record<string, string | string[]> | null;\n}\n\n/**\n * A node in the client-side segment tree. Each node represents a mounted\n * layout or page segment with its RSC flight payload.\n */\nexport interface SegmentNode {\n /** The segment's URL pattern (e.g., \"/\", \"/dashboard\", \"/projects/[id]\") */\n segment: string;\n /** The RSC flight payload for this segment (opaque to the cache) */\n payload: unknown;\n /** Whether the segment is async (async layouts always re-render on navigation) */\n isAsync: boolean;\n /** Child segments keyed by segment path */\n children: Map<string, SegmentNode>;\n}\n\n/**\n * Serialized state tree sent via X-Timber-State-Tree header.\n * Only sync segments are included — async segments always re-render.\n */\nexport interface StateTree {\n segments: string[];\n}\n\n// ─── Segment Cache ───────────────────────────────────────────────\n\n/**\n * Maintains the client-side segment tree representing currently mounted\n * layouts and pages. Used for navigation reconciliation — the router diffs\n * new routes against this tree to determine which segments to re-fetch.\n */\nexport class SegmentCache {\n private root: SegmentNode | undefined;\n\n get(segment: string): SegmentNode | undefined {\n if (segment === '/' || segment === this.root?.segment) {\n return this.root;\n }\n return undefined;\n }\n\n set(segment: string, node: SegmentNode): void {\n if (segment === '/' || !this.root) {\n this.root = node;\n }\n }\n\n clear(): void {\n this.root = undefined;\n }\n\n /**\n * Serialize the mounted segment tree for the X-Timber-State-Tree header.\n * Only includes sync segments — async segments are excluded because the\n * server must always re-render them (they may depend on request context).\n *\n * This is a performance optimization only, NOT a security boundary.\n * The server always runs all access.ts files regardless of the state tree.\n */\n serializeStateTree(): StateTree {\n const segments: string[] = [];\n if (this.root) {\n collectSyncSegments(this.root, segments);\n }\n return { segments };\n }\n}\n\n/** Recursively collect sync segment paths from the tree */\nfunction collectSyncSegments(node: SegmentNode, out: string[]): void {\n if (!node.isAsync) {\n out.push(node.segment);\n }\n for (const child of node.children.values()) {\n collectSyncSegments(child, out);\n }\n}\n\n// ─── Segment Tree Builder ────────────────────────────────────────\n\n/**\n * Segment metadata from the server, sent via X-Timber-Segments header.\n * Describes a rendered segment's path and whether it's async.\n */\nexport interface SegmentInfo {\n path: string;\n isAsync: boolean;\n}\n\n/**\n * Build a SegmentNode tree from flat segment metadata.\n *\n * Takes an ordered list of segment descriptors (root → leaf) from the\n * server's X-Timber-Segments header and constructs the hierarchical\n * tree structure that SegmentCache expects.\n *\n * Each segment is nested as a child of the previous one, forming a\n * linear chain from root to leaf. The leaf segment (page) is excluded\n * from the tree — pages are never cached across navigations.\n */\nexport function buildSegmentTree(segments: SegmentInfo[]): SegmentNode | undefined {\n // Need at least a root segment to build a tree\n if (segments.length === 0) return undefined;\n\n // Exclude the leaf (page) — pages always re-render on navigation.\n // Only layouts are cached in the segment tree.\n const layouts = segments.length > 1 ? segments.slice(0, -1) : segments;\n\n let root: SegmentNode | undefined;\n let parent: SegmentNode | undefined;\n\n for (const info of layouts) {\n const node: SegmentNode = {\n segment: info.path,\n payload: null,\n isAsync: info.isAsync,\n children: new Map(),\n };\n\n if (!root) {\n root = node;\n }\n\n if (parent) {\n parent.children.set(info.path, node);\n }\n\n parent = node;\n }\n\n return root;\n}\n\n// ─── Prefetch Cache ──────────────────────────────────────────────\n\ninterface PrefetchEntry {\n result: PrefetchResult;\n expiresAt: number;\n}\n\n/**\n * Short-lived cache for hover-triggered prefetches. Entries expire after\n * 30 seconds. When a link is clicked, the prefetched payload is consumed\n * (moved to the history stack) and removed from this cache.\n *\n * timber.js does NOT prefetch on viewport intersection — only explicit\n * hover on <Link prefetch> triggers a prefetch.\n */\nexport class PrefetchCache {\n private static readonly TTL_MS = 30_000;\n private entries = new Map<string, PrefetchEntry>();\n\n set(url: string, result: PrefetchResult): void {\n this.entries.set(url, {\n result,\n expiresAt: Date.now() + PrefetchCache.TTL_MS,\n });\n }\n\n get(url: string): PrefetchResult | undefined {\n const entry = this.entries.get(url);\n if (!entry) return undefined;\n if (Date.now() >= entry.expiresAt) {\n this.entries.delete(url);\n return undefined;\n }\n return entry.result;\n }\n\n /** Get and remove the entry (used when navigation consumes a prefetch) */\n consume(url: string): PrefetchResult | undefined {\n const result = this.get(url);\n if (result !== undefined) {\n this.entries.delete(url);\n }\n return result;\n }\n}\n","// History Stack — stores RSC payloads by URL for instant back/forward navigation\n// See design/19-client-navigation.md § History Stack\n\nimport type { HeadElement } from './head';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface HistoryEntry {\n /** The complete segment tree payload at the time of navigation */\n payload: unknown;\n /** Resolved head elements for this page (title, meta tags). Null for SSR'd initial page. */\n headElements?: HeadElement[] | null;\n /** Route params for this page (for useParams). Null for SSR'd initial page. */\n params?: Record<string, string | string[]> | null;\n}\n\n// ─── History Stack ───────────────────────────────────────────────\n\n/**\n * Session-lived history stack keyed by URL. Enables instant back/forward\n * navigation without a server roundtrip.\n *\n * On forward navigation, the new page's payload is pushed onto the stack.\n * On popstate, the cached payload is replayed instantly.\n *\n * Scroll positions are stored in history.state (browser History API),\n * not in this stack — see design/19-client-navigation.md §Scroll Restoration.\n *\n * Entries persist for the session duration (no expiry) and are cleared\n * when the tab is closed — matching browser back-button behavior.\n */\nexport class HistoryStack {\n private entries = new Map<string, HistoryEntry>();\n\n push(url: string, entry: HistoryEntry): void {\n this.entries.set(url, entry);\n }\n\n get(url: string): HistoryEntry | undefined {\n return this.entries.get(url);\n }\n\n has(url: string): boolean {\n return this.entries.has(url);\n }\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['params']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * During SSR, params are read from the ALS-backed SSR data context\n * (populated by ssr-entry.ts) to ensure correct per-request isolation\n * across concurrent requests with streaming Suspense.\n *\n * Reactivity: On the client, useParams() reads from NavigationContext\n * which is updated atomically with the RSC tree render. This replaces\n * the previous useSyncExternalStore approach that suffered from a\n * timing gap between tree render and store notification — causing\n * preserved layout components to briefly show stale active state.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { Routes } from '#/index.js';\nimport { getSsrData } from './ssr-data.js';\nimport { currentParams, _setCurrentParams, paramsListeners } from './state.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n// ---------------------------------------------------------------------------\n// Module-level subscribe/notify pattern — kept for backward compat and tests\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to params changes.\n * Retained for backward compatibility with tests that verify the\n * subscribe/notify contract. On the client, useParams() reads from\n * NavigationContext instead.\n */\nexport function subscribe(callback: () => void): () => void {\n paramsListeners.add(callback);\n return () => paramsListeners.delete(callback);\n}\n\n/**\n * Get the current params snapshot (module-level fallback).\n * Used by tests and by the hook when called outside a React component.\n */\nexport function getSnapshot(): Record<string, string | string[]> {\n return currentParams;\n}\n\n// ---------------------------------------------------------------------------\n// Framework API — called by the segment router on each navigation\n// ---------------------------------------------------------------------------\n\n/**\n * Set the current route params in the module-level store.\n *\n * Called by the router on each navigation. This updates the fallback\n * snapshot used by tests and by the hook when called outside a React\n * component (no NavigationContext available).\n *\n * On the client, the primary reactivity path is NavigationContext —\n * the router calls setNavigationState() then renderRoot() which wraps\n * the element in NavigationProvider. setCurrentParams is still called\n * for the module-level fallback.\n *\n * During SSR, params are also available via getSsrData().params\n * (ALS-backed).\n */\nexport function setCurrentParams(params: Record<string, string | string[]>): void {\n _setCurrentParams(params);\n}\n\n/**\n * Notify all legacy subscribers that params have changed.\n *\n * Retained for backward compatibility with tests. On the client,\n * the NavigationContext + renderRoot pattern replaces this — params\n * update atomically with the tree render, so explicit notification\n * is no longer needed.\n */\nexport function notifyParamsListeners(): void {\n for (const listener of paramsListeners) {\n listener();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public hook\n// ---------------------------------------------------------------------------\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). This ensures params update\n * atomically with the RSC tree — no timing gap.\n *\n * During SSR, reads from the ALS-backed SSR data context to ensure\n * per-request isolation across concurrent requests with streaming Suspense.\n *\n * When called outside a React component (e.g., in test assertions),\n * falls back to the module-level snapshot.\n *\n * @overload Typed — when a known route path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useParams<R extends keyof Routes>(route: R): Routes[R]['params'];\nexport function useParams(route?: string): Record<string, string | string[]>;\nexport function useParams(_route?: string): Record<string, string | string[]> {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n // When called outside a React component, useContext throws — caught below.\n try {\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.params;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to module-level snapshot below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n // Falls back to module-level currentParams for tests.\n return getSsrData()?.params ?? currentParams;\n}\n","// Segment Router — manages client-side navigation and RSC payload fetching\n// See design/19-client-navigation.md for the full architecture.\n\nimport { SegmentCache, PrefetchCache, buildSegmentTree } from './segment-cache';\nimport type { SegmentInfo } from './segment-cache';\nimport { HistoryStack } from './history';\nimport type { HeadElement } from './head';\nimport { setCurrentParams } from './use-params.js';\nimport { setNavigationState } from './navigation-context.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface NavigationOptions {\n /** Set to false to prevent scroll-to-top on forward navigation */\n scroll?: boolean;\n /** Use replaceState instead of pushState (replaces current history entry) */\n replace?: boolean;\n}\n\n/**\n * Function that decodes an RSC Flight stream into a React element tree.\n * In production: createFromFetch from @vitejs/plugin-rsc/browser.\n * In tests: a mock that returns the raw payload.\n */\nexport type RscDecoder = (fetchPromise: Promise<Response>) => unknown;\n\n/**\n * Function that renders a decoded RSC element tree into the DOM.\n * In production: reactRoot.render(element).\n * In tests: a no-op or mock.\n */\nexport type RootRenderer = (element: unknown) => void;\n\n/**\n * Platform dependencies injected for testability. In production these\n * map to browser APIs; in tests they're replaced with mocks.\n */\nexport interface RouterDeps {\n fetch: (url: string, init: RequestInit) => Promise<Response>;\n pushState: (data: unknown, unused: string, url: string) => void;\n replaceState: (data: unknown, unused: string, url: string) => void;\n scrollTo: (x: number, y: number) => void;\n getCurrentUrl: () => string;\n getScrollY: () => number;\n /** Decode RSC Flight stream into React elements. If not provided, raw response text is stored. */\n decodeRsc?: RscDecoder;\n /** Render decoded RSC tree into the DOM. If not provided, rendering is a no-op. */\n renderRoot?: RootRenderer;\n /**\n * Schedule a callback after the next paint. In the browser, this is\n * requestAnimationFrame + setTimeout(0) to run after React commits.\n * In tests, this runs the callback synchronously.\n */\n afterPaint?: (callback: () => void) => void;\n /** Apply resolved head elements (title, meta tags) to the DOM after navigation. */\n applyHead?: (elements: HeadElement[]) => void;\n /**\n * Run a navigation inside a React transition with optimistic pending URL.\n * The pending URL shows immediately (useOptimistic urgent update) and\n * reverts when the transition commits (atomic with the new tree).\n *\n * The `perform` callback receives a `wrapPayload` function to wrap the\n * decoded RSC payload with NavigationProvider + NuqsAdapter before\n * TransitionRoot sets it as the new element.\n *\n * If not provided (tests), the router falls back to renderRoot.\n */\n navigateTransition?: (\n pendingUrl: string,\n perform: (wrapPayload: (payload: unknown) => unknown) => Promise<unknown>,\n ) => Promise<void>;\n}\n\n/** Result of fetching an RSC payload — includes head elements and segment metadata. */\ninterface FetchResult {\n payload: unknown;\n headElements: HeadElement[] | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useParams(). */\n params: Record<string, string | string[]> | null;\n}\n\nexport interface RouterInstance {\n /** Navigate to a new URL (forward navigation) */\n navigate(url: string, options?: NavigationOptions): Promise<void>;\n /** Full re-render of the current URL — no state tree sent */\n refresh(): Promise<void>;\n /** Handle a popstate event (back/forward button). scrollY is read from history.state. */\n handlePopState(url: string, scrollY?: number): Promise<void>;\n /** Whether a navigation is currently in flight */\n isPending(): boolean;\n /** The URL currently being navigated to, or null if idle */\n getPendingUrl(): string | null;\n /** Subscribe to pending state changes */\n onPendingChange(listener: (pending: boolean) => void): () => void;\n /** Prefetch an RSC payload for a URL (used by Link hover) */\n prefetch(url: string): void;\n /**\n * Apply a piggybacked revalidation payload from a server action response.\n * Renders the element tree and updates head elements without a server fetch.\n * See design/08-forms-and-actions.md §\"Single-Roundtrip Revalidation\".\n */\n applyRevalidation(element: unknown, headElements: HeadElement[] | null): void;\n /**\n * Populate the segment cache from server-provided segment metadata.\n * Called on initial hydration with segment info embedded in the HTML.\n */\n initSegmentCache(segments: SegmentInfo[]): void;\n /** The segment cache (exposed for tests and <Link> prefetch) */\n segmentCache: SegmentCache;\n /** The prefetch cache (exposed for tests and <Link> prefetch) */\n prefetchCache: PrefetchCache;\n /** The history stack (exposed for tests) */\n historyStack: HistoryStack;\n}\n\n/**\n * Thrown when an RSC payload response contains X-Timber-Redirect header.\n * Caught in navigate() to trigger a soft router navigation to the redirect target.\n */\nclass RedirectError extends Error {\n readonly redirectUrl: string;\n constructor(url: string) {\n super(`Server redirect to ${url}`);\n this.redirectUrl = url;\n }\n}\n\n/**\n * Check if an error is an abort error (connection closed / fetch aborted).\n * Browsers throw DOMException with name 'AbortError' when a fetch is aborted.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof DOMException && error.name === 'AbortError') return true;\n if (error instanceof Error && error.name === 'AbortError') return true;\n return false;\n}\n\n// ─── RSC Fetch ───────────────────────────────────────────────────\n\nconst RSC_CONTENT_TYPE = 'text/x-component';\n\n/**\n * Generate a short random cache-busting ID (5 chars, a-z0-9).\n * Matches the format Next.js uses for _rsc params.\n */\nfunction generateCacheBustId(): string {\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n let id = '';\n for (let i = 0; i < 5; i++) {\n id += chars[(Math.random() * 36) | 0];\n }\n return id;\n}\n\n/**\n * Append a `_rsc=<id>` query parameter to the URL.\n * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML\n * for RSC navigation requests and signals that this is an RSC fetch.\n */\nfunction appendRscParam(url: string): string {\n const separator = url.includes('?') ? '&' : '?';\n return `${url}${separator}_rsc=${generateCacheBustId()}`;\n}\n\nfunction buildRscHeaders(\n stateTree: { segments: string[] } | undefined,\n currentUrl?: string\n): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: RSC_CONTENT_TYPE,\n };\n if (stateTree) {\n headers['X-Timber-State-Tree'] = JSON.stringify(stateTree);\n }\n // Send current URL for intercepting route resolution.\n // The server uses this to determine if an intercepting route should\n // render instead of the actual target route (modal pattern).\n // See design/07-routing.md §\"Intercepting Routes\"\n if (currentUrl) {\n headers['X-Timber-URL'] = currentUrl;\n }\n return headers;\n}\n\n/**\n * Extract head elements from the X-Timber-Head response header.\n * Returns null if the header is missing or malformed.\n */\nfunction extractHeadElements(response: Response): HeadElement[] | null {\n const header = response.headers.get('X-Timber-Head');\n if (!header) return null;\n try {\n return JSON.parse(decodeURIComponent(header));\n } catch {\n return null;\n }\n}\n\n/**\n * Extract segment metadata from the X-Timber-Segments response header.\n * Returns null if the header is missing or malformed.\n *\n * Format: JSON array of {path, isAsync} objects describing the rendered\n * segment chain from root to leaf. Used to populate the client-side\n * segment cache for state tree diffing on subsequent navigations.\n */\nfunction extractSegmentInfo(response: Response): SegmentInfo[] | null {\n const header = response.headers.get('X-Timber-Segments');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n return null;\n }\n}\n\n/**\n * Extract route params from the X-Timber-Params response header.\n * Returns null if the header is missing or malformed.\n *\n * Used to populate useParams() after client-side navigation.\n */\nfunction extractParams(response: Response): Record<string, string | string[]> | null {\n const header = response.headers.get('X-Timber-Params');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n return null;\n }\n}\n\n/**\n * Fetch an RSC payload from the server. If a decodeRsc function is provided,\n * the response is decoded into a React element tree via createFromFetch.\n * Otherwise, the raw response text is returned (test mode).\n *\n * Also extracts head elements from the X-Timber-Head response header\n * so the client can update document.title and <meta> tags after navigation.\n */\nasync function fetchRscPayload(\n url: string,\n deps: RouterDeps,\n stateTree?: { segments: string[] },\n currentUrl?: string\n): Promise<FetchResult> {\n const rscUrl = appendRscParam(url);\n const headers = buildRscHeaders(stateTree, currentUrl);\n if (deps.decodeRsc) {\n // Production path: use createFromFetch for streaming RSC decoding.\n // createFromFetch takes a Promise<Response> and progressively parses\n // the RSC Flight stream as chunks arrive.\n //\n // Intercept the response to read X-Timber-Head before createFromFetch\n // consumes the body. Reading headers does NOT consume the body stream.\n const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual' });\n let headElements: HeadElement[] | null = null;\n let segmentInfo: SegmentInfo[] | null = null;\n let params: Record<string, string | string[]> | null = null;\n const wrappedPromise = fetchPromise.then((response) => {\n // Detect server-side redirects. The server returns 204 + X-Timber-Redirect\n // for RSC payload requests instead of a raw 302, because fetch with\n // redirect: \"manual\" turns 302s into opaque redirects (status 0, null body)\n // which crashes createFromFetch when it tries to read the body stream.\n const redirectLocation =\n response.headers.get('X-Timber-Redirect') ||\n (response.status >= 300 && response.status < 400 ? response.headers.get('Location') : null);\n if (redirectLocation) {\n throw new RedirectError(redirectLocation);\n }\n headElements = extractHeadElements(response);\n segmentInfo = extractSegmentInfo(response);\n params = extractParams(response);\n return response;\n });\n // Await so headElements/segmentInfo/params are populated before we return.\n // Also await the decoded payload — createFromFetch returns a thenable\n // that resolves to the React element tree.\n await wrappedPromise;\n const payload = await deps.decodeRsc(wrappedPromise);\n return { payload, headElements, segmentInfo, params };\n }\n // Test/fallback path: return raw text\n const response = await deps.fetch(rscUrl, { headers, redirect: 'manual' });\n // Check for redirect in test path too\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('Location');\n if (location) {\n throw new RedirectError(location);\n }\n }\n return {\n payload: await response.text(),\n headElements: extractHeadElements(response),\n segmentInfo: extractSegmentInfo(response),\n params: extractParams(response),\n };\n}\n\n// ─── Router Factory ──────────────────────────────────────────────\n\n/**\n * Create a router instance. In production, called once at app hydration\n * with real browser APIs. In tests, called with mock dependencies.\n */\nexport function createRouter(deps: RouterDeps): RouterInstance {\n const segmentCache = new SegmentCache();\n const prefetchCache = new PrefetchCache();\n const historyStack = new HistoryStack();\n\n let pending = false;\n let pendingUrl: string | null = null;\n const pendingListeners = new Set<(pending: boolean) => void>();\n\n function setPending(value: boolean, url?: string): void {\n const newPendingUrl = value && url ? url : null;\n if (pending === value && pendingUrl === newPendingUrl) return;\n pending = value;\n pendingUrl = newPendingUrl;\n // Notify external store listeners (non-React consumers).\n // React-facing pending state is handled by useOptimistic in\n // TransitionRoot via navigateTransition — not this function.\n for (const listener of pendingListeners) {\n listener(value);\n }\n }\n\n /** Update the segment cache from server-provided segment metadata. */\n function updateSegmentCache(segmentInfo: SegmentInfo[] | null | undefined): void {\n if (!segmentInfo || segmentInfo.length === 0) return;\n const tree = buildSegmentTree(segmentInfo);\n if (tree) {\n segmentCache.set('/', tree);\n }\n }\n\n /** Render a decoded RSC payload into the DOM if a renderer is available. */\n function renderPayload(payload: unknown): void {\n if (deps.renderRoot) {\n deps.renderRoot(payload);\n }\n }\n\n /**\n * Update navigation state (params + pathname) for the next render.\n *\n * Sets both the module-level fallback (for tests and SSR) and the\n * navigation context state (read by renderRoot to wrap the element\n * in NavigationProvider). The context update is atomic with the tree\n * render — both are passed to reactRoot.render() in the same call.\n */\n function updateNavigationState(\n params: Record<string, string | string[]> | null | undefined,\n url: string\n ): void {\n const resolvedParams = params ?? {};\n // Module-level fallback for tests (no NavigationProvider) and SSR\n setCurrentParams(resolvedParams);\n // Navigation context — read by renderRoot to wrap the RSC element\n const pathname = url.startsWith('http')\n ? new URL(url).pathname\n : url.split('?')[0] || '/';\n setNavigationState({ params: resolvedParams, pathname });\n }\n\n /**\n * Render a payload via navigateTransition (production) or renderRoot (tests).\n * The perform callback should fetch data, update state, and return the payload.\n * In production, the entire callback runs inside a React transition with\n * useOptimistic for the pending URL. In tests, the payload is rendered directly.\n */\n async function renderViaTransition(\n pendingUrl: string,\n perform: () => Promise<FetchResult>,\n ): Promise<HeadElement[] | null> {\n if (deps.navigateTransition) {\n let headElements: HeadElement[] | null = null;\n await deps.navigateTransition(pendingUrl, async (wrapPayload) => {\n const result = await perform();\n headElements = result.headElements;\n return wrapPayload(result.payload);\n });\n return headElements;\n }\n // Fallback: no transition (tests, no React tree)\n const result = await perform();\n renderPayload(result.payload);\n return result.headElements;\n }\n\n /** Apply head elements (title, meta tags) to the DOM if available. */\n function applyHead(elements: HeadElement[] | null | undefined): void {\n if (elements && deps.applyHead) {\n deps.applyHead(elements);\n }\n }\n\n /** Run a callback after the next paint (after React commit). */\n function afterPaint(callback: () => void): void {\n if (deps.afterPaint) {\n deps.afterPaint(callback);\n } else {\n callback();\n }\n }\n\n /**\n * Core navigation logic shared between the transition and fallback paths.\n * Fetches the RSC payload, updates all state, and returns the result.\n */\n async function performNavigationFetch(\n url: string,\n options: { replace: boolean },\n ): Promise<FetchResult> {\n // Check prefetch cache first. PrefetchResult has optional segmentInfo/params\n // fields — normalize to null for FetchResult compatibility.\n const prefetched = prefetchCache.consume(url);\n let result: FetchResult | undefined = prefetched\n ? {\n payload: prefetched.payload,\n headElements: prefetched.headElements,\n segmentInfo: prefetched.segmentInfo ?? null,\n params: prefetched.params ?? null,\n }\n : undefined;\n\n if (result === undefined) {\n // Fetch RSC payload with state tree for partial rendering.\n // Send current URL for intercepting route resolution (modal pattern).\n const stateTree = segmentCache.serializeStateTree();\n const rawCurrentUrl = deps.getCurrentUrl();\n const currentUrl = rawCurrentUrl.startsWith('http')\n ? new URL(rawCurrentUrl).pathname\n : new URL(rawCurrentUrl, 'http://localhost').pathname;\n result = await fetchRscPayload(url, deps, stateTree, currentUrl);\n }\n\n // Update the browser history — replace mode overwrites the current entry\n if (options.replace) {\n deps.replaceState({ timber: true, scrollY: 0 }, '', url);\n } else {\n deps.pushState({ timber: true, scrollY: 0 }, '', url);\n }\n\n // Store the payload in the history stack\n historyStack.push(url, {\n payload: result.payload,\n headElements: result.headElements,\n params: result.params,\n });\n\n // Update the segment cache with the new route's segment tree.\n updateSegmentCache(result.segmentInfo);\n\n // Update navigation state (params + pathname) before rendering.\n updateNavigationState(result.params, url);\n\n return result;\n }\n\n async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {\n const scroll = options.scroll !== false;\n const replace = options.replace === true;\n\n // Capture the departing page's scroll position for scroll={false} preservation.\n const currentScrollY = deps.getScrollY();\n\n // Save the departing page's scroll position in history.state before\n // pushing a new entry. This ensures back/forward navigation can restore\n // the correct scroll position from the browser's per-entry state.\n deps.replaceState({ timber: true, scrollY: currentScrollY }, '', deps.getCurrentUrl());\n\n setPending(true, url);\n\n try {\n const headElements = await renderViaTransition(url, () =>\n performNavigationFetch(url, { replace }),\n );\n\n // Update document.title and <meta> tags with the new page's metadata\n applyHead(headElements);\n\n // Notify nuqs adapter (and any other listeners) that navigation completed.\n window.dispatchEvent(new Event('timber:navigation-end'));\n\n // Scroll-to-top on forward navigation, or restore captured position\n // for scroll={false}. React's render() on the document root can reset\n // scroll during DOM reconciliation, so all scroll must be actively managed.\n afterPaint(() => {\n if (scroll) {\n deps.scrollTo(0, 0);\n } else {\n deps.scrollTo(0, currentScrollY);\n }\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n } catch (error) {\n // Server-side redirect during RSC fetch → soft router navigation.\n if (error instanceof RedirectError) {\n setPending(false);\n await navigate(error.redirectUrl, { replace: true });\n return;\n }\n // Abort errors are not application errors — swallow silently.\n if (isAbortError(error)) return;\n throw error;\n } finally {\n setPending(false);\n }\n }\n\n async function refresh(): Promise<void> {\n const currentUrl = deps.getCurrentUrl();\n\n setPending(true, currentUrl);\n\n try {\n const headElements = await renderViaTransition(currentUrl, async () => {\n // No state tree sent — server renders the complete RSC payload\n const result = await fetchRscPayload(currentUrl, deps);\n historyStack.push(currentUrl, {\n payload: result.payload,\n headElements: result.headElements,\n params: result.params,\n });\n updateSegmentCache(result.segmentInfo);\n updateNavigationState(result.params, currentUrl);\n return result;\n });\n\n applyHead(headElements);\n } finally {\n setPending(false);\n }\n }\n\n async function handlePopState(url: string, scrollY: number = 0): Promise<void> {\n // Scroll position is read from history.state by the caller (browser-entry.ts)\n // and passed in. This is more reliable than tracking scroll per-URL in memory\n // because the browser maintains per-entry state even with duplicate URLs.\n const entry = historyStack.get(url);\n\n if (entry && entry.payload !== null) {\n // Replay cached payload — no server roundtrip\n updateNavigationState(entry.params, url);\n renderPayload(entry.payload);\n applyHead(entry.headElements);\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n } else {\n // No cached payload — fetch from server.\n // This happens when navigating back to the initial SSR'd page\n // (its payload is null since it was rendered via SSR, not RSC fetch)\n // or when the entry doesn't exist at all.\n setPending(true, url);\n try {\n const headElements = await renderViaTransition(url, async () => {\n const stateTree = segmentCache.serializeStateTree();\n const result = await fetchRscPayload(url, deps, stateTree);\n updateSegmentCache(result.segmentInfo);\n updateNavigationState(result.params, url);\n historyStack.push(url, {\n payload: result.payload,\n headElements: result.headElements,\n params: result.params,\n });\n return result;\n });\n\n applyHead(headElements);\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n } finally {\n setPending(false);\n }\n }\n }\n\n /**\n * Prefetch an RSC payload for a URL and store it in the prefetch cache.\n * Called on hover of <Link prefetch> elements.\n */\n function prefetch(url: string): void {\n // Don't prefetch if already cached\n if (prefetchCache.get(url) !== undefined) return;\n if (historyStack.has(url)) return;\n\n // Fire-and-forget fetch\n const stateTree = segmentCache.serializeStateTree();\n void fetchRscPayload(url, deps, stateTree).then(\n (result) => {\n prefetchCache.set(url, result);\n },\n () => {\n // Prefetch failure is non-fatal — navigation will fetch fresh\n }\n );\n }\n\n return {\n navigate,\n refresh,\n handlePopState,\n isPending: () => pending,\n getPendingUrl: () => pendingUrl,\n onPendingChange(listener) {\n pendingListeners.add(listener);\n return () => pendingListeners.delete(listener);\n },\n prefetch,\n applyRevalidation(element: unknown, headElements: HeadElement[] | null): void {\n // Render the piggybacked element tree from a server action response.\n // Updates the current history entry with the fresh payload and applies\n // head elements — same as refresh() but without a server fetch.\n const currentUrl = deps.getCurrentUrl();\n historyStack.push(currentUrl, {\n payload: element,\n headElements,\n });\n renderPayload(element);\n applyHead(headElements);\n },\n initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),\n segmentCache,\n prefetchCache,\n historyStack,\n };\n}\n","// useNavigationPending — returns true while an RSC navigation is in flight.\n// See design/19-client-navigation.md §\"useNavigationPending()\"\n//\n// Reads from PendingNavigationContext (provided by TransitionRoot) so the\n// pending state shows immediately (urgent update) and clears atomically\n// with the new tree (same startTransition commit).\n\nimport { usePendingNavigationUrl } from './navigation-context.js';\n\n/**\n * Returns true while an RSC navigation is in flight.\n *\n * The pending state is true from the moment the RSC fetch starts until\n * React reconciliation completes. This includes the fetch itself,\n * RSC stream parsing, and React tree reconciliation.\n *\n * It does NOT include Suspense streaming after the shell — only the\n * initial shell reconciliation.\n *\n * ```tsx\n * 'use client'\n * import { useNavigationPending } from '@timber-js/app/client'\n *\n * export function NavBar() {\n * const isPending = useNavigationPending()\n * return (\n * <nav className={isPending ? 'opacity-50' : ''}>\n * <Link href=\"/dashboard\">Dashboard</Link>\n * </nav>\n * )\n * }\n * ```\n */\nexport function useNavigationPending(): boolean {\n const pendingUrl = usePendingNavigationUrl();\n // During SSR or outside PendingNavigationProvider, no navigation is pending\n return pendingUrl !== null;\n}\n","// Global router reference — shared between browser-entry and client hooks.\n//\n// Delegates to client/state.ts for the actual module-level variable.\n// This ensures singleton semantics regardless of import path — all\n// callers converge on the same state.ts instance via the barrel.\n//\n// See design/18-build-system.md §\"Module Singleton Strategy\"\n\nimport type { RouterInstance } from './router.js';\nimport { globalRouter, _setGlobalRouter } from './state.js';\n\n/**\n * Set the global router instance. Called once during bootstrap.\n */\nexport function setGlobalRouter(router: RouterInstance): void {\n _setGlobalRouter(router);\n}\n\n/**\n * Get the global router instance. Throws if called before bootstrap.\n * Used by client-side hooks (useNavigationPending, etc.)\n */\nexport function getRouter(): RouterInstance {\n if (!globalRouter) {\n throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');\n }\n return globalRouter;\n}\n\n/**\n * Get the global router instance or null if not yet initialized.\n * Used by useRouter() methods to avoid silent failures — callers\n * can log a meaningful warning instead of silently no-oping.\n */\nexport function getRouterOrNull(): RouterInstance | null {\n return globalRouter;\n}\n\n/**\n * Reset the global router to null. Used only in tests to isolate\n * module-level state between test cases.\n * @internal\n */\nexport function resetGlobalRouter(): void {\n _setGlobalRouter(null);\n}\n","/**\n * useRouter() — client-side hook for programmatic navigation.\n *\n * Returns a router instance with push, replace, refresh, back, forward,\n * and prefetch methods. Compatible with Next.js's `useRouter()` from\n * `next/navigation` (App Router).\n *\n * This wraps timber's internal RouterInstance in the Next.js-compatible\n * AppRouterInstance shape that ecosystem libraries expect.\n *\n * NOTE: Unlike Next.js, these methods do NOT wrap navigation in\n * startTransition. In Next.js, router state is React state (useReducer)\n * so startTransition defers the update and provides isPending tracking.\n * In timber, navigation calls reactRoot.render() which is a root-level\n * render — startTransition has no effect on root renders.\n *\n * Navigation state (params, pathname) is delivered atomically via\n * NavigationContext embedded in the element tree passed to\n * reactRoot.render(). See design/19-client-navigation.md §\"NavigationContext\".\n *\n * For loading UI during navigation, use:\n * - useLinkStatus() — per-link pending indicator (inside <Link>)\n * - useNavigationPending() — global navigation pending state\n */\n\nimport { getRouterOrNull } from './router-ref.js';\n\nexport interface AppRouterInstance {\n /** Navigate to a URL, pushing a new history entry */\n push(href: string, options?: { scroll?: boolean }): void;\n /** Navigate to a URL, replacing the current history entry */\n replace(href: string, options?: { scroll?: boolean }): void;\n /** Refresh the current page (re-fetch RSC payload) */\n refresh(): void;\n /** Navigate back in history */\n back(): void;\n /** Navigate forward in history */\n forward(): void;\n /** Prefetch an RSC payload for a URL */\n prefetch(href: string): void;\n}\n\n/**\n * Get a router instance for programmatic navigation.\n *\n * Compatible with Next.js's `useRouter()` from `next/navigation`.\n *\n * Methods lazily resolve the global router when invoked (during user\n * interaction) rather than capturing it at render time. This is critical\n * because during hydration, React synchronously executes component render\n * functions *before* the router is bootstrapped in browser-entry.ts.\n * If we eagerly captured the router during render, components would get\n * a null reference and be stuck with silent no-ops forever.\n *\n * Returns safe no-ops during SSR or before bootstrap. The `typeof window`\n * check is insufficient because Vite's client SSR environment defines\n * `window`, so we use a try/catch on getRouter() — but only at method\n * invocation time, not at render time.\n */\nexport function useRouter(): AppRouterInstance {\n return {\n push(href: string, options?: { scroll?: boolean }) {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.');\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll });\n },\n replace(href: string, options?: { scroll?: boolean }) {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().replace() called but router is not initialized.');\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll, replace: true });\n },\n refresh() {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().refresh() called but router is not initialized.');\n }\n return;\n }\n void router.refresh();\n },\n back() {\n if (typeof window !== 'undefined') window.history.back();\n },\n forward() {\n if (typeof window !== 'undefined') window.history.forward();\n },\n prefetch(href: string) {\n const router = getRouterOrNull();\n if (!router) return; // Silent — prefetch failure is non-fatal\n router.prefetch(href);\n },\n };\n}\n","/**\n * usePathname() — client-side hook for reading the current pathname.\n *\n * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').\n * Updates when client-side navigation changes the URL.\n *\n * On the client, reads from NavigationContext which is updated atomically\n * with the RSC tree render. This replaces the previous useSyncExternalStore\n * approach which only subscribed to popstate events — meaning usePathname()\n * did NOT re-render on forward navigation (pushState). The context approach\n * fixes this: pathname updates in the same render pass as the new tree.\n *\n * During SSR, reads the request pathname from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { getSsrData } from './ssr-data.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n/**\n * Read the current URL pathname.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). During SSR, reads from the\n * ALS-backed SSR data context. Falls back to window.location.pathname\n * when called outside a React component (e.g., in tests).\n */\nexport function usePathname(): string {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n try {\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.pathname;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to SSR/fallback below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n const ssrData = getSsrData();\n if (ssrData) return ssrData.pathname ?? '/';\n\n // Final fallback: window.location (tests, edge cases).\n if (typeof window !== 'undefined') return window.location.pathname;\n return '/';\n}\n","/**\n * useSearchParams() — client-side hook for reading URL search params.\n *\n * Returns a read-only URLSearchParams instance reflecting the current\n * URL's query string. Updates when client-side navigation changes the URL.\n *\n * This is a thin wrapper over window.location.search, provided for\n * Next.js API compatibility (libraries like nuqs import useSearchParams\n * from next/navigation).\n *\n * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard\n * URLSearchParams. Mutation methods (set, delete, append) work on the\n * local copy but do NOT affect the URL — use the router or nuqs for that.\n *\n * During SSR, reads the request search params from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\nimport { cachedSearch, cachedSearchParams, _setCachedSearch } from './state.js';\n\nfunction getSearch(): string {\n if (typeof window !== 'undefined') return window.location.search;\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction getServerSearch(): string {\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction subscribe(callback: () => void): () => void {\n window.addEventListener('popstate', callback);\n return () => window.removeEventListener('popstate', callback);\n}\n\n// Cache the last search string and its parsed URLSearchParams to avoid\n// creating a new object on every render when the URL hasn't changed.\n// State lives in client/state.ts for singleton guarantees.\n\nfunction getSearchParams(): URLSearchParams {\n const search = getSearch();\n if (search !== cachedSearch) {\n const params = new URLSearchParams(search);\n _setCachedSearch(search, params);\n return params;\n }\n return cachedSearchParams;\n}\n\nfunction getServerSearchParams(): URLSearchParams {\n const data = getSsrData();\n return data ? new URLSearchParams(data.searchParams) : new URLSearchParams();\n}\n\n/**\n * Read the current URL search params.\n *\n * Compatible with Next.js's `useSearchParams()` from `next/navigation`.\n */\nexport function useSearchParams(): URLSearchParams {\n // useSyncExternalStore needs a primitive snapshot for comparison.\n // We use the raw search string as the snapshot, then return the\n // parsed URLSearchParams.\n useSyncExternalStore(subscribe, getSearch, getServerSearch);\n return typeof window !== 'undefined' ? getSearchParams() : getServerSearchParams();\n}\n","/**\n * Segment Context — provides layout segment position for useSelectedLayoutSegment hooks.\n *\n * Each layout in the segment tree is wrapped with a SegmentProvider that stores\n * the URL segments from root to the current layout level. The hooks read this\n * context to determine which child segments are active below the calling layout.\n *\n * The context value is intentionally minimal: just the segment path array and\n * parallel route keys. No internal cache details are exposed.\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { createContext, useContext, createElement, useMemo } from 'react';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface SegmentContextValue {\n /** URL segments from root to this layout (e.g. ['', 'dashboard', 'settings']) */\n segments: string[];\n /** Parallel route slot keys available at this layout level (e.g. ['sidebar', 'modal']) */\n parallelRouteKeys: string[];\n}\n\n// ─── Context ─────────────────────────────────────────────────────\n\nconst SegmentContext = createContext<SegmentContextValue | null>(null);\n\n/** Read the segment context. Returns null if no provider is above this component. */\nexport function useSegmentContext(): SegmentContextValue | null {\n return useContext(SegmentContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface SegmentProviderProps {\n segments: string[];\n parallelRouteKeys: string[];\n children: React.ReactNode;\n}\n\n/**\n * Wraps each layout to provide segment position context.\n * Injected by rsc-entry.ts during element tree construction.\n */\nexport function SegmentProvider({ segments, parallelRouteKeys, children }: SegmentProviderProps) {\n const value = useMemo(\n () => ({ segments, parallelRouteKeys }),\n // segments and parallelRouteKeys are static per layout — they don't change\n // across navigations. The layout's position in the tree is fixed.\n // Intentionally using derived keys — segments/parallelRouteKeys are static per layout\n [segments.join('/'), parallelRouteKeys.join(',')]\n );\n return createElement(SegmentContext.Provider, { value }, children);\n}\n","/**\n * useSelectedLayoutSegment / useSelectedLayoutSegments — client-side hooks\n * for reading the active segment(s) below the current layout.\n *\n * These hooks are used by navigation UIs to highlight active sections.\n * They match Next.js's API from next/navigation.\n *\n * How they work:\n * 1. Each layout is wrapped with a SegmentProvider that records its depth\n * (the URL segments from root to that layout level).\n * 2. The hooks read the current URL pathname via usePathname().\n * 3. They compare the layout's segment depth against the full URL segments\n * to determine which child segments are \"selected\" below.\n *\n * Example: For URL \"/dashboard/settings/profile\"\n * - Root layout (depth 0, segments: ['']): selected segment = \"dashboard\"\n * - Dashboard layout (depth 1, segments: ['', 'dashboard']): selected = \"settings\"\n * - Settings layout (depth 2, segments: ['', 'dashboard', 'settings']): selected = \"profile\"\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { useSegmentContext } from './segment-context.js';\nimport { usePathname } from './use-pathname.js';\n\n/**\n * Split a pathname into URL segments.\n * \"/\" → [\"\"]\n * \"/dashboard\" → [\"\", \"dashboard\"]\n * \"/dashboard/settings\" → [\"\", \"dashboard\", \"settings\"]\n */\nexport function pathnameToSegments(pathname: string): string[] {\n return pathname.split('/');\n}\n\n/**\n * Pure function: compute the selected child segment given a layout's segment\n * depth and the current URL pathname.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns the active child segment one level below, or null if at the leaf\n */\nexport function getSelectedSegment(\n contextSegments: string[] | null,\n pathname: string\n): string | null {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments[1] || null;\n }\n\n const depth = contextSegments.length;\n return urlSegments[depth] || null;\n}\n\n/**\n * Pure function: compute all selected segments below a layout's depth.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns all active segments below the layout\n */\nexport function getSelectedSegments(contextSegments: string[] | null, pathname: string): string[] {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments.slice(1).filter(Boolean);\n }\n\n const depth = contextSegments.length;\n return urlSegments.slice(depth).filter(Boolean);\n}\n\n/**\n * Returns the active child segment one level below the layout where this\n * hook is called. Returns `null` if the layout is the leaf (no child segment).\n *\n * Compatible with Next.js's `useSelectedLayoutSegment()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegment(parallelRouteKey?: string): string | null {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegment(context?.segments ?? null, pathname);\n}\n\n/**\n * Returns all active segments below the layout where this hook is called.\n * Returns an empty array if the layout is the leaf (no child segments).\n *\n * Compatible with Next.js's `useSelectedLayoutSegments()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegments(parallelRouteKey?: string): string[] {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegments(context?.segments ?? null, pathname);\n}\n","/**\n * Client-side form utilities for server actions.\n *\n * Exports a typed `useActionState` that understands the action builder's result shape.\n * Result is typed to:\n * { data: T } | { validationErrors: Record<string, string[]> } | { serverError: { code, data? } } | null\n *\n * The action builder emits a function that satisfies both the direct call signature\n * and React's `(prevState, formData) => Promise<State>` contract.\n *\n * See design/08-forms-and-actions.md §\"Client-Side Form Mechanics\"\n */\n\nimport { useActionState as reactUseActionState, useTransition } from 'react';\nimport type { ActionResult, ValidationErrors } from '#/server/action-client';\nimport type { FormFlashData } from '#/server/form-flash';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/**\n * The action function type accepted by useActionState.\n * Must satisfy React's (prevState, formData) => Promise<State> contract.\n */\nexport type UseActionStateFn<TData> = (\n prevState: ActionResult<TData> | null,\n formData: FormData\n) => Promise<ActionResult<TData>>;\n\n/**\n * Return type of useActionState — matches React 19's useActionState return.\n * [result, formAction, isPending]\n */\nexport type UseActionStateReturn<TData> = [\n result: ActionResult<TData> | null,\n formAction: (formData: FormData) => void,\n isPending: boolean,\n];\n\n// ─── useActionState ──────────────────────────────────────────────────────\n\n/**\n * Typed wrapper around React 19's `useActionState` that understands\n * the timber action builder's result shape.\n *\n * @param action - A server action created with createActionClient or a raw 'use server' function.\n * @param initialState - Initial state, typically `null`. Pass `getFormFlash()` for no-JS\n * progressive enhancement — the flash seeds the initial state so the form has a\n * single source of truth for both with-JS and no-JS paths.\n * @param permalink - Optional permalink for progressive enhancement (no-JS fallback URL).\n *\n * @example\n * ```tsx\n * 'use client'\n * import { useActionState } from '@timber-js/app/client'\n * import { createTodo } from './actions'\n *\n * export function NewTodoForm({ flash }) {\n * const [result, action, isPending] = useActionState(createTodo, flash)\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {result?.validationErrors?.title && <p>{result.validationErrors.title}</p>}\n * <button disabled={isPending}>Add</button>\n * </form>\n * )\n * }\n * ```\n */\nexport function useActionState<TData>(\n action: UseActionStateFn<TData>,\n initialState: ActionResult<TData> | FormFlashData | null,\n permalink?: string\n): UseActionStateReturn<TData> {\n // FormFlashData is structurally compatible with ActionResult at runtime —\n // the cast satisfies React's generic inference which would otherwise widen TData.\n return reactUseActionState(action, initialState as ActionResult<TData> | null, permalink);\n}\n\n// ─── useFormAction ───────────────────────────────────────────────────────\n\n/**\n * Hook for calling a server action imperatively (not via a form).\n * Returns [execute, isPending] where execute accepts the input directly.\n *\n * @example\n * ```tsx\n * const [deleteTodo, isPending] = useFormAction(deleteTodoAction)\n * <button onClick={() => deleteTodo({ id: todo.id })} disabled={isPending}>\n * Delete\n * </button>\n * ```\n */\nexport function useFormAction<TData>(\n action: (input: unknown) => Promise<ActionResult<TData>>\n): [(input?: unknown) => Promise<ActionResult<TData>>, boolean] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: unknown): Promise<ActionResult<TData>> => {\n return new Promise((resolve) => {\n startTransition(async () => {\n const result = await action(input);\n resolve(result);\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── useFormErrors ──────────────────────────────────────────────────────\n\n/** Return type of useFormErrors(). */\nexport interface FormErrorsResult {\n /** Per-field validation errors keyed by field name. */\n fieldErrors: Record<string, string[]>;\n /** Form-level errors (from `_root` key). */\n formErrors: string[];\n /** Server error if the action threw an ActionError. */\n serverError: { code: string; data?: Record<string, unknown> } | null;\n /** Whether any errors are present. */\n hasErrors: boolean;\n /** Get the first error message for a field, or null. */\n getFieldError: (field: string) => string | null;\n}\n\n/**\n * Extract per-field and form-level errors from an ActionResult.\n *\n * Pure function (no internal hooks) — follows React naming convention\n * since it's used in render. Accepts the result from `useActionState`\n * or flash data from `getFormFlash()`.\n *\n * @example\n * ```tsx\n * const [result, action, isPending] = useActionState(createTodo, null)\n * const errors = useFormErrors(result)\n *\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {errors.getFieldError('title') && <p>{errors.getFieldError('title')}</p>}\n * {errors.formErrors.map(e => <p key={e}>{e}</p>)}\n * </form>\n * )\n * ```\n */\nexport function useFormErrors<TData>(\n result:\n | ActionResult<TData>\n | {\n validationErrors?: ValidationErrors;\n serverError?: { code: string; data?: Record<string, unknown> };\n }\n | null\n): FormErrorsResult {\n const empty: FormErrorsResult = {\n fieldErrors: {},\n formErrors: [],\n serverError: null,\n hasErrors: false,\n getFieldError: () => null,\n };\n\n if (!result) return empty;\n\n const validationErrors = result.validationErrors as ValidationErrors | undefined;\n const serverError = result.serverError as\n | { code: string; data?: Record<string, unknown> }\n | undefined;\n\n if (!validationErrors && !serverError) return empty;\n\n // Separate _root (form-level) errors from field errors\n const fieldErrors: Record<string, string[]> = {};\n const formErrors: string[] = [];\n\n if (validationErrors) {\n for (const [key, messages] of Object.entries(validationErrors)) {\n if (key === '_root') {\n formErrors.push(...messages);\n } else {\n fieldErrors[key] = messages;\n }\n }\n }\n\n const hasErrors =\n Object.keys(fieldErrors).length > 0 || formErrors.length > 0 || serverError != null;\n\n return {\n fieldErrors,\n formErrors,\n serverError: serverError ?? null,\n hasErrors,\n getFieldError(field: string): string | null {\n const errs = fieldErrors[field];\n return errs && errs.length > 0 ? errs[0] : null;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/client/link-navigate-interceptor.tsx","../../src/client/use-link-status.ts","../../src/client/navigation-context.ts","../../src/client/link-status-provider.tsx","../../src/client/link.tsx","../../src/client/segment-cache.ts","../../src/client/history.ts","../../src/client/use-params.ts","../../src/client/router.ts","../../src/client/use-navigation-pending.ts","../../src/client/router-ref.ts","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/use-search-params.ts","../../src/client/segment-context.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx"],"sourcesContent":["'use client';\n\n// LinkNavigateInterceptor — client component that stores an onNavigate callback\n// on the parent <a> element so the delegated click handler in browser-entry.ts\n// can invoke it before triggering SPA navigation.\n//\n// See design/19-client-navigation.md, TIM-167\n\nimport { useRef, useEffect, type ReactNode } from 'react';\n\n/** Symbol used to store the onNavigate callback on anchor elements. */\nexport const ON_NAVIGATE_KEY = '__timberOnNavigate' as const;\n\nexport type OnNavigateEvent = {\n preventDefault: () => void;\n};\n\nexport type OnNavigateHandler = (e: OnNavigateEvent) => void;\n\n/**\n * Augment HTMLAnchorElement with the optional onNavigate property.\n * Used by browser-entry.ts handleLinkClick to check for the callback.\n */\ndeclare global {\n interface HTMLAnchorElement {\n [ON_NAVIGATE_KEY]?: OnNavigateHandler;\n }\n}\n\n/**\n * Client component rendered inside <Link> that attaches the onNavigate\n * callback to the closest <a> ancestor via a DOM property. The callback\n * is cleaned up on unmount.\n *\n * Renders no extra DOM — just a transparent wrapper.\n */\nexport function LinkNavigateInterceptor({\n onNavigate,\n children,\n}: {\n onNavigate: OnNavigateHandler;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLSpanElement>(null);\n\n useEffect(() => {\n const anchor = ref.current?.closest('a');\n if (!anchor) return;\n anchor[ON_NAVIGATE_KEY] = onNavigate;\n return () => {\n delete anchor[ON_NAVIGATE_KEY];\n };\n }, [onNavigate]);\n\n // Use a <span> with display:contents to avoid affecting layout.\n // The ref lets us walk up to the parent <a> in the effect.\n return (\n <span ref={ref} style={{ display: 'contents' }}>\n {children}\n </span>\n );\n}\n","'use client';\n\n// useLinkStatus — returns { pending: true } while the nearest parent <Link>'s\n// navigation is in flight. No arguments — scoped via React context.\n// See design/19-client-navigation.md §\"useLinkStatus()\"\n\nimport { useContext, createContext } from 'react';\n\nexport interface LinkStatus {\n pending: boolean;\n}\n\n/**\n * React context provided by <Link>. Holds the pending status\n * for that specific link's navigation.\n */\nexport const LinkStatusContext = createContext<LinkStatus>({ pending: false });\n\n/**\n * Returns `{ pending: true }` while the nearest parent `<Link>` component's\n * navigation is in flight. Must be used inside a `<Link>` component's children.\n *\n * Unlike `useNavigationPending()` which is global, this hook is scoped to\n * the nearest parent `<Link>` — only the link the user clicked shows pending.\n *\n * ```tsx\n * 'use client'\n * import { Link, useLinkStatus } from '@timber-js/app/client'\n *\n * function Hint() {\n * const { pending } = useLinkStatus()\n * return <span className={pending ? 'opacity-50' : ''} />\n * }\n *\n * export function NavLink({ href, children }) {\n * return (\n * <Link href={href}>\n * {children} <Hint />\n * </Link>\n * )\n * }\n * ```\n */\nexport function useLinkStatus(): LinkStatus {\n return useContext(LinkStatusContext);\n}\n","'use client';\n\n/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current route params and pathname, updated atomically\n * with the RSC tree on each navigation. This replaces the previous\n * useSyncExternalStore approach for useParams() and usePathname(),\n * which suffered from a timing gap: the new tree could commit before\n * the external store re-renders fired, causing a frame where both\n * old and new active states were visible simultaneously.\n *\n * By wrapping the RSC payload element in NavigationProvider inside\n * renderRoot(), the context value and the element tree are passed to\n * reactRoot.render() in the same call — atomic by construction.\n * All consumers (useParams, usePathname) see the new values in the\n * same render pass as the new tree.\n *\n * During SSR, no NavigationProvider is mounted. Hooks fall back to\n * the ALS-backed getSsrData() for per-request isolation.\n *\n * IMPORTANT: createContext and useContext are NOT available in the RSC\n * environment (React Server Components use a stripped-down React).\n * The context is lazily initialized on first access, and all functions\n * that depend on these APIs are safe to call from any environment —\n * they return null or no-op when the APIs aren't available.\n *\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport React, { createElement, type ReactNode } from 'react';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\nexport interface NavigationState {\n params: Record<string, string | string[]>;\n pathname: string;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy context initialization\n// ---------------------------------------------------------------------------\n\n/**\n * The context is created lazily to avoid calling createContext at module\n * level. In the RSC environment, React.createContext doesn't exist —\n * calling it at import time would crash the server.\n */\nlet _context: React.Context<NavigationState | null> | undefined;\n\nfunction getOrCreateContext(): React.Context<NavigationState | null> | undefined {\n if (_context !== undefined) return _context;\n // createContext may not exist in the RSC environment\n if (typeof React.createContext === 'function') {\n _context = React.createContext<NavigationState | null>(null);\n }\n return _context;\n}\n\n/**\n * Read the navigation context. Returns null during SSR (no provider)\n * or in the RSC environment (no context available).\n * Internal — used by useParams() and usePathname().\n */\nexport function useNavigationContext(): NavigationState | null {\n const ctx = getOrCreateContext();\n if (!ctx) return null;\n // useContext may not exist in the RSC environment — caller wraps in try/catch\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n// ---------------------------------------------------------------------------\n// Provider component\n// ---------------------------------------------------------------------------\n\nexport interface NavigationProviderProps {\n value: NavigationState;\n children?: ReactNode;\n}\n\n/**\n * Wraps children with NavigationContext.Provider.\n *\n * Used in browser-entry.ts renderRoot to wrap the RSC payload element\n * so that navigation state updates atomically with the tree render.\n */\nexport function NavigationProvider({ value, children }: NavigationProviderProps): React.ReactElement {\n const ctx = getOrCreateContext();\n if (!ctx) {\n // RSC environment — no context available. Return children as-is.\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state for renderRoot to read\n// ---------------------------------------------------------------------------\n\n/**\n * Module-level navigation state. Updated by the router before calling\n * renderRoot(). The renderRoot callback reads this to create the\n * NavigationProvider with the correct values.\n *\n * This is NOT used by hooks directly — hooks read from React context.\n * This exists only as a communication channel between the router\n * (which knows the new nav state) and renderRoot (which wraps the element).\n */\nlet _currentNavState: NavigationState = { params: {}, pathname: '/' };\n\nexport function setNavigationState(state: NavigationState): void {\n _currentNavState = state;\n}\n\nexport function getNavigationState(): NavigationState {\n return _currentNavState;\n}\n\n// ---------------------------------------------------------------------------\n// Pending Navigation Context (same module for singleton guarantee)\n// ---------------------------------------------------------------------------\n\n/**\n * Separate context for the in-flight navigation URL. Provided by\n * TransitionRoot (useOptimistic state), consumed by LinkStatusProvider\n * and useNavigationPending.\n *\n * Lives in this module (not a separate file) to guarantee singleton\n * identity across chunks. The `'use client'` LinkStatusProvider and\n * the non-directive TransitionRoot both import from this module —\n * if they were in separate files, the bundler could duplicate the\n * module-level context variable across chunks.\n */\nlet _pendingContext: React.Context<string | null> | undefined;\n\nfunction getOrCreatePendingContext(): React.Context<string | null> | undefined {\n if (_pendingContext !== undefined) return _pendingContext;\n if (typeof React.createContext === 'function') {\n _pendingContext = React.createContext<string | null>(null);\n }\n return _pendingContext;\n}\n\n/**\n * Read the pending navigation URL from context.\n * Returns null during SSR (no provider) or in the RSC environment.\n */\nexport function usePendingNavigationUrl(): string | null {\n const ctx = getOrCreatePendingContext();\n if (!ctx) return null;\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n/**\n * Provider for the pending navigation URL. Wraps children with\n * the pending context Provider.\n */\nexport function PendingNavigationProvider({\n value,\n children,\n}: {\n value: string | null;\n children?: ReactNode;\n}): React.ReactElement {\n const ctx = getOrCreatePendingContext();\n if (!ctx) {\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n","'use client';\n\n// LinkStatusProvider — client component that provides per-link pending status\n// via React context. Used inside <Link> to power useLinkStatus().\n//\n// Reads pendingUrl from PendingNavigationContext (provided by TransitionRoot).\n// The pending URL is set as an URGENT update at navigation start (shows\n// immediately) and cleared inside startTransition when the new tree commits\n// (atomic with params/pathname). This eliminates both:\n// 1. The delay before showing the spinner (urgent update, not deferred)\n// 2. The gap between spinner disappearing and active state updating (same commit)\n\nimport type { ReactNode } from 'react';\nimport { LinkStatusContext, type LinkStatus } from './use-link-status.js';\nimport { usePendingNavigationUrl } from './navigation-context.js';\n\nconst NOT_PENDING: LinkStatus = { pending: false };\nconst IS_PENDING: LinkStatus = { pending: true };\n\n/**\n * Client component that reads the pending URL from PendingNavigationContext\n * and provides a scoped LinkStatusContext to children. Renders no extra DOM —\n * just a context provider around children.\n */\nexport function LinkStatusProvider({ href, children }: { href: string; children?: ReactNode }) {\n const pendingUrl = usePendingNavigationUrl();\n const status = pendingUrl === href ? IS_PENDING : NOT_PENDING;\n\n return <LinkStatusContext.Provider value={status}>{children}</LinkStatusContext.Provider>;\n}\n","'use client';\n\n// Link component — client-side navigation with progressive enhancement\n// See design/19-client-navigation.md § Progressive Enhancement\n//\n// Without JavaScript, <Link> renders as a plain <a> tag — standard browser\n// navigation. With JavaScript, the client runtime intercepts clicks on links\n// marked with data-timber-link, fetches RSC payloads, and reconciles the DOM.\n//\n// Typed Link: design/09-typescript.md §\"Typed Link\"\n// - href validated against known routes (via codegen overloads, not runtime)\n// - params prop typed per-route, URL interpolated at runtime\n// - searchParams prop serialized via SearchParamsDefinition\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport type { AnchorHTMLAttributes, ReactNode } from 'react';\nimport type { SearchParamsDefinition } from '#/search-params/create.js';\nimport type { OnNavigateHandler } from './link-navigate-interceptor.js';\nimport { LinkNavigateInterceptor } from './link-navigate-interceptor.js';\nimport { LinkStatusProvider } from './link-status-provider.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/**\n * Base props shared by all Link variants.\n */\ninterface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** Prefetch the RSC payload on hover */\n prefetch?: boolean;\n /**\n * Scroll to top on navigation. Defaults to true.\n * Set to false for tabbed interfaces where content changes within a fixed layout.\n */\n scroll?: boolean;\n /**\n * Called before client-side navigation commits. Call `e.preventDefault()`\n * to cancel the default navigation — the caller is then responsible for\n * navigating (e.g. via `router.push()`).\n *\n * Only fires for client-side SPA navigations, not full page loads.\n * Has no effect during SSR.\n */\n onNavigate?: OnNavigateHandler;\n children?: ReactNode;\n}\n\n/**\n * Link with a fully-resolved string href.\n * When using a string href with params already interpolated,\n * the params prop is not available.\n */\nexport interface LinkPropsWithHref extends LinkBaseProps {\n href: string;\n params?: never;\n /**\n * Typed search params — serialized via the route's SearchParamsDefinition.\n * Mutually exclusive with an inline query string in href.\n */\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n}\n\n/**\n * Link with a route pattern + params for interpolation.\n * e.g. <Link href=\"/products/[id]\" params={{ id: \"123\" }}>\n * <Link href=\"/products/[id]\" params={{ id: 123 }}>\n */\nexport interface LinkPropsWithParams extends LinkBaseProps {\n /** Route pattern with dynamic segments (e.g. \"/products/[id]\") */\n href: string;\n /**\n * Dynamic segment values to interpolate into the href.\n * Single dynamic segments accept string | number (numbers are stringified).\n * Catch-all segments accept string[].\n */\n params: Record<string, string | number | string[]>;\n /**\n * Typed search params — serialized via the route's SearchParamsDefinition.\n */\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n}\n\nexport type LinkProps = LinkPropsWithHref | LinkPropsWithParams;\n\n// ─── Dangerous URL Scheme Detection ──────────────────────────────\n\n/**\n * Reject dangerous URL schemes that could execute script.\n * Security: design/13-security.md § Link scheme injection (test #9)\n */\nconst DANGEROUS_SCHEMES = /^\\s*(javascript|data|vbscript):/i;\n\nexport function validateLinkHref(href: string): void {\n if (DANGEROUS_SCHEMES.test(href)) {\n throw new Error(\n `<Link> received a dangerous href: \"${href}\". ` +\n 'javascript:, data:, and vbscript: URLs are not allowed.'\n );\n }\n}\n\n// ─── Internal Link Detection ─────────────────────────────────────\n\n/** Returns true if the href is an internal path (not an external URL) */\nfunction isInternalHref(href: string): boolean {\n // Relative paths, root-relative paths, and hash links are internal\n if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {\n return true;\n }\n // Anything with a protocol scheme is external\n if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {\n return false;\n }\n // Bare relative paths (e.g., \"dashboard\") are internal\n return true;\n}\n\n// ─── URL Interpolation ──────────────────────────────────────────\n\n/**\n * Interpolate dynamic segments in a route pattern with actual values.\n * e.g. interpolateParams(\"/products/[id]\", { id: \"123\" }) → \"/products/123\"\n *\n * Supports:\n * - [param] → single segment\n * - [...param] → catch-all (joined with /)\n * - [[...param]] → optional catch-all (omitted if undefined/empty)\n */\nexport function interpolateParams(\n pattern: string,\n params: Record<string, string | number | string[]>\n): string {\n return (\n pattern\n .replace(\n /\\[\\[\\.\\.\\.(\\w+)\\]\\]|\\[\\.\\.\\.(\\w+)\\]|\\[(\\w+)\\]/g,\n (_match, optionalCatchAll, catchAll, single) => {\n if (optionalCatchAll) {\n const value = params[optionalCatchAll];\n if (value === undefined || (Array.isArray(value) && value.length === 0)) {\n return '';\n }\n const segments = Array.isArray(value) ? value : [value];\n return segments.map(encodeURIComponent).join('/');\n }\n\n if (catchAll) {\n const value = params[catchAll];\n if (value === undefined) {\n throw new Error(\n `<Link> missing required catch-all param \"${catchAll}\" for pattern \"${pattern}\".`\n );\n }\n const segments = Array.isArray(value) ? value : [value];\n if (segments.length === 0) {\n throw new Error(\n `<Link> catch-all param \"${catchAll}\" must have at least one segment for pattern \"${pattern}\".`\n );\n }\n return segments.map(encodeURIComponent).join('/');\n }\n\n // single dynamic segment\n const value = params[single];\n if (value === undefined) {\n throw new Error(`<Link> missing required param \"${single}\" for pattern \"${pattern}\".`);\n }\n if (Array.isArray(value)) {\n throw new Error(\n `<Link> param \"${single}\" expected a string but received an array for pattern \"${pattern}\".`\n );\n }\n // Accept numbers — coerce to string for URL interpolation\n return encodeURIComponent(String(value));\n }\n )\n // Clean up trailing slash from empty optional catch-all\n .replace(/\\/+$/, '') || '/'\n );\n}\n\n// ─── Resolve Href ───────────────────────────────────────────────\n\n/**\n * Resolve the final href string from Link props.\n *\n * Handles:\n * - params interpolation into route patterns\n * - searchParams serialization via SearchParamsDefinition\n * - Validation that searchParams and inline query strings are exclusive\n */\nexport function resolveHref(\n href: string,\n params?: Record<string, string | number | string[]>,\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n }\n): string {\n let resolvedPath = href;\n\n // Interpolate params if provided\n if (params) {\n resolvedPath = interpolateParams(href, params);\n }\n\n // Serialize searchParams if provided\n if (searchParams) {\n // Validate: searchParams prop and inline query string are mutually exclusive\n if (resolvedPath.includes('?')) {\n throw new Error(\n '<Link> received both a searchParams prop and a query string in href. ' +\n 'These are mutually exclusive — use one or the other.'\n );\n }\n\n const qs = searchParams.definition.serialize(searchParams.values);\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n }\n\n return resolvedPath;\n}\n\n// ─── Build Props ─────────────────────────────────────────────────\n\ninterface LinkOutputProps {\n 'href': string;\n 'data-timber-link'?: boolean;\n 'data-timber-prefetch'?: boolean;\n 'data-timber-scroll'?: string;\n}\n\n/**\n * Build the HTML attributes for a Link. Separated from the component\n * for testability — the component just spreads these onto an <a>.\n */\nexport function buildLinkProps(\n props: Pick<LinkPropsWithHref, 'href' | 'prefetch' | 'scroll'> & {\n params?: Record<string, string | number | string[]>;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n }\n): LinkOutputProps {\n const resolvedHref = resolveHref(props.href, props.params, props.searchParams);\n\n validateLinkHref(resolvedHref);\n\n const output: LinkOutputProps = { href: resolvedHref };\n const internal = isInternalHref(resolvedHref);\n\n if (internal) {\n output['data-timber-link'] = true;\n\n if (props.prefetch) {\n output['data-timber-prefetch'] = true;\n }\n\n if (props.scroll === false) {\n output['data-timber-scroll'] = 'false';\n }\n }\n\n return output;\n}\n\n// ─── Link Component ──────────────────────────────────────────────\n\n/**\n * Navigation link with progressive enhancement.\n *\n * Renders as a plain `<a>` tag — works without JavaScript. When the client\n * runtime is active, it intercepts clicks on links marked with\n * `data-timber-link` to perform RSC-based client navigation.\n *\n * Supports typed routes via codegen overloads. At runtime:\n * - `params` prop interpolates dynamic segments in the href pattern\n * - `searchParams` prop serializes query parameters via a SearchParamsDefinition\n */\nexport function Link({\n href,\n prefetch,\n scroll,\n params,\n searchParams,\n onNavigate,\n children,\n ...rest\n}: LinkProps) {\n const linkProps = buildLinkProps({ href, prefetch, scroll, params, searchParams });\n\n const inner = <LinkStatusProvider href={linkProps.href}>{children}</LinkStatusProvider>;\n\n return (\n <a {...rest} {...linkProps}>\n {onNavigate ? (\n <LinkNavigateInterceptor onNavigate={onNavigate}>{inner}</LinkNavigateInterceptor>\n ) : (\n inner\n )}\n </a>\n );\n}\n","// Segment Cache — stores the mounted segment tree and prefetched payloads\n// See design/19-client-navigation.md for architecture details.\n\nimport type { HeadElement } from './head';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** A prefetched RSC result with optional head elements and segment metadata. */\nexport interface PrefetchResult {\n payload: unknown;\n headElements: HeadElement[] | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo?: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useParams(). */\n params?: Record<string, string | string[]> | null;\n}\n\n/**\n * A node in the client-side segment tree. Each node represents a mounted\n * layout or page segment with its RSC flight payload.\n */\nexport interface SegmentNode {\n /** The segment's URL pattern (e.g., \"/\", \"/dashboard\", \"/projects/[id]\") */\n segment: string;\n /** The RSC flight payload for this segment (opaque to the cache) */\n payload: unknown;\n /** Whether the segment is async (async layouts always re-render on navigation) */\n isAsync: boolean;\n /** Child segments keyed by segment path */\n children: Map<string, SegmentNode>;\n}\n\n/**\n * Serialized state tree sent via X-Timber-State-Tree header.\n * Only sync segments are included — async segments always re-render.\n */\nexport interface StateTree {\n segments: string[];\n}\n\n// ─── Segment Cache ───────────────────────────────────────────────\n\n/**\n * Maintains the client-side segment tree representing currently mounted\n * layouts and pages. Used for navigation reconciliation — the router diffs\n * new routes against this tree to determine which segments to re-fetch.\n */\nexport class SegmentCache {\n private root: SegmentNode | undefined;\n\n get(segment: string): SegmentNode | undefined {\n if (segment === '/' || segment === this.root?.segment) {\n return this.root;\n }\n return undefined;\n }\n\n set(segment: string, node: SegmentNode): void {\n if (segment === '/' || !this.root) {\n this.root = node;\n }\n }\n\n clear(): void {\n this.root = undefined;\n }\n\n /**\n * Serialize the mounted segment tree for the X-Timber-State-Tree header.\n * Only includes sync segments — async segments are excluded because the\n * server must always re-render them (they may depend on request context).\n *\n * This is a performance optimization only, NOT a security boundary.\n * The server always runs all access.ts files regardless of the state tree.\n */\n serializeStateTree(): StateTree {\n const segments: string[] = [];\n if (this.root) {\n collectSyncSegments(this.root, segments);\n }\n return { segments };\n }\n}\n\n/** Recursively collect sync segment paths from the tree */\nfunction collectSyncSegments(node: SegmentNode, out: string[]): void {\n if (!node.isAsync) {\n out.push(node.segment);\n }\n for (const child of node.children.values()) {\n collectSyncSegments(child, out);\n }\n}\n\n// ─── Segment Tree Builder ────────────────────────────────────────\n\n/**\n * Segment metadata from the server, sent via X-Timber-Segments header.\n * Describes a rendered segment's path and whether it's async.\n */\nexport interface SegmentInfo {\n path: string;\n isAsync: boolean;\n}\n\n/**\n * Build a SegmentNode tree from flat segment metadata.\n *\n * Takes an ordered list of segment descriptors (root → leaf) from the\n * server's X-Timber-Segments header and constructs the hierarchical\n * tree structure that SegmentCache expects.\n *\n * Each segment is nested as a child of the previous one, forming a\n * linear chain from root to leaf. The leaf segment (page) is excluded\n * from the tree — pages are never cached across navigations.\n */\nexport function buildSegmentTree(segments: SegmentInfo[]): SegmentNode | undefined {\n // Need at least a root segment to build a tree\n if (segments.length === 0) return undefined;\n\n // Exclude the leaf (page) — pages always re-render on navigation.\n // Only layouts are cached in the segment tree.\n const layouts = segments.length > 1 ? segments.slice(0, -1) : segments;\n\n let root: SegmentNode | undefined;\n let parent: SegmentNode | undefined;\n\n for (const info of layouts) {\n const node: SegmentNode = {\n segment: info.path,\n payload: null,\n isAsync: info.isAsync,\n children: new Map(),\n };\n\n if (!root) {\n root = node;\n }\n\n if (parent) {\n parent.children.set(info.path, node);\n }\n\n parent = node;\n }\n\n return root;\n}\n\n// ─── Prefetch Cache ──────────────────────────────────────────────\n\ninterface PrefetchEntry {\n result: PrefetchResult;\n expiresAt: number;\n}\n\n/**\n * Short-lived cache for hover-triggered prefetches. Entries expire after\n * 30 seconds. When a link is clicked, the prefetched payload is consumed\n * (moved to the history stack) and removed from this cache.\n *\n * timber.js does NOT prefetch on viewport intersection — only explicit\n * hover on <Link prefetch> triggers a prefetch.\n */\nexport class PrefetchCache {\n private static readonly TTL_MS = 30_000;\n private entries = new Map<string, PrefetchEntry>();\n\n set(url: string, result: PrefetchResult): void {\n this.entries.set(url, {\n result,\n expiresAt: Date.now() + PrefetchCache.TTL_MS,\n });\n }\n\n get(url: string): PrefetchResult | undefined {\n const entry = this.entries.get(url);\n if (!entry) return undefined;\n if (Date.now() >= entry.expiresAt) {\n this.entries.delete(url);\n return undefined;\n }\n return entry.result;\n }\n\n /** Get and remove the entry (used when navigation consumes a prefetch) */\n consume(url: string): PrefetchResult | undefined {\n const result = this.get(url);\n if (result !== undefined) {\n this.entries.delete(url);\n }\n return result;\n }\n}\n","// History Stack — stores RSC payloads by URL for instant back/forward navigation\n// See design/19-client-navigation.md § History Stack\n\nimport type { HeadElement } from './head';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface HistoryEntry {\n /** The complete segment tree payload at the time of navigation */\n payload: unknown;\n /** Resolved head elements for this page (title, meta tags). Null for SSR'd initial page. */\n headElements?: HeadElement[] | null;\n /** Route params for this page (for useParams). Null for SSR'd initial page. */\n params?: Record<string, string | string[]> | null;\n}\n\n// ─── History Stack ───────────────────────────────────────────────\n\n/**\n * Session-lived history stack keyed by URL. Enables instant back/forward\n * navigation without a server roundtrip.\n *\n * On forward navigation, the new page's payload is pushed onto the stack.\n * On popstate, the cached payload is replayed instantly.\n *\n * Scroll positions are stored in history.state (browser History API),\n * not in this stack — see design/19-client-navigation.md §Scroll Restoration.\n *\n * Entries persist for the session duration (no expiry) and are cleared\n * when the tab is closed — matching browser back-button behavior.\n */\nexport class HistoryStack {\n private entries = new Map<string, HistoryEntry>();\n\n push(url: string, entry: HistoryEntry): void {\n this.entries.set(url, entry);\n }\n\n get(url: string): HistoryEntry | undefined {\n return this.entries.get(url);\n }\n\n has(url: string): boolean {\n return this.entries.has(url);\n }\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['params']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * During SSR, params are read from the ALS-backed SSR data context\n * (populated by ssr-entry.ts) to ensure correct per-request isolation\n * across concurrent requests with streaming Suspense.\n *\n * Reactivity: On the client, useParams() reads from NavigationContext\n * which is updated atomically with the RSC tree render. This replaces\n * the previous useSyncExternalStore approach that suffered from a\n * timing gap between tree render and store notification — causing\n * preserved layout components to briefly show stale active state.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { Routes } from '#/index.js';\nimport { getSsrData } from './ssr-data.js';\nimport { currentParams, _setCurrentParams, paramsListeners } from './state.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n// ---------------------------------------------------------------------------\n// Module-level subscribe/notify pattern — kept for backward compat and tests\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to params changes.\n * Retained for backward compatibility with tests that verify the\n * subscribe/notify contract. On the client, useParams() reads from\n * NavigationContext instead.\n */\nexport function subscribe(callback: () => void): () => void {\n paramsListeners.add(callback);\n return () => paramsListeners.delete(callback);\n}\n\n/**\n * Get the current params snapshot (module-level fallback).\n * Used by tests and by the hook when called outside a React component.\n */\nexport function getSnapshot(): Record<string, string | string[]> {\n return currentParams;\n}\n\n// ---------------------------------------------------------------------------\n// Framework API — called by the segment router on each navigation\n// ---------------------------------------------------------------------------\n\n/**\n * Set the current route params in the module-level store.\n *\n * Called by the router on each navigation. This updates the fallback\n * snapshot used by tests and by the hook when called outside a React\n * component (no NavigationContext available).\n *\n * On the client, the primary reactivity path is NavigationContext —\n * the router calls setNavigationState() then renderRoot() which wraps\n * the element in NavigationProvider. setCurrentParams is still called\n * for the module-level fallback.\n *\n * During SSR, params are also available via getSsrData().params\n * (ALS-backed).\n */\nexport function setCurrentParams(params: Record<string, string | string[]>): void {\n _setCurrentParams(params);\n}\n\n/**\n * Notify all legacy subscribers that params have changed.\n *\n * Retained for backward compatibility with tests. On the client,\n * the NavigationContext + renderRoot pattern replaces this — params\n * update atomically with the tree render, so explicit notification\n * is no longer needed.\n */\nexport function notifyParamsListeners(): void {\n for (const listener of paramsListeners) {\n listener();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public hook\n// ---------------------------------------------------------------------------\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). This ensures params update\n * atomically with the RSC tree — no timing gap.\n *\n * During SSR, reads from the ALS-backed SSR data context to ensure\n * per-request isolation across concurrent requests with streaming Suspense.\n *\n * When called outside a React component (e.g., in test assertions),\n * falls back to the module-level snapshot.\n *\n * @overload Typed — when a known route path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useParams<R extends keyof Routes>(route: R): Routes[R]['params'];\nexport function useParams(route?: string): Record<string, string | string[]>;\nexport function useParams(_route?: string): Record<string, string | string[]> {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n // When called outside a React component, useContext throws — caught below.\n try {\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.params;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to module-level snapshot below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n // Falls back to module-level currentParams for tests.\n return getSsrData()?.params ?? currentParams;\n}\n","// Segment Router — manages client-side navigation and RSC payload fetching\n// See design/19-client-navigation.md for the full architecture.\n\nimport { SegmentCache, PrefetchCache, buildSegmentTree } from './segment-cache';\nimport type { SegmentInfo } from './segment-cache';\nimport { HistoryStack } from './history';\nimport type { HeadElement } from './head';\nimport { setCurrentParams } from './use-params.js';\nimport { setNavigationState } from './navigation-context.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface NavigationOptions {\n /** Set to false to prevent scroll-to-top on forward navigation */\n scroll?: boolean;\n /** Use replaceState instead of pushState (replaces current history entry) */\n replace?: boolean;\n}\n\n/**\n * Function that decodes an RSC Flight stream into a React element tree.\n * In production: createFromFetch from @vitejs/plugin-rsc/browser.\n * In tests: a mock that returns the raw payload.\n */\nexport type RscDecoder = (fetchPromise: Promise<Response>) => unknown;\n\n/**\n * Function that renders a decoded RSC element tree into the DOM.\n * In production: reactRoot.render(element).\n * In tests: a no-op or mock.\n */\nexport type RootRenderer = (element: unknown) => void;\n\n/**\n * Platform dependencies injected for testability. In production these\n * map to browser APIs; in tests they're replaced with mocks.\n */\nexport interface RouterDeps {\n fetch: (url: string, init: RequestInit) => Promise<Response>;\n pushState: (data: unknown, unused: string, url: string) => void;\n replaceState: (data: unknown, unused: string, url: string) => void;\n scrollTo: (x: number, y: number) => void;\n getCurrentUrl: () => string;\n getScrollY: () => number;\n /** Decode RSC Flight stream into React elements. If not provided, raw response text is stored. */\n decodeRsc?: RscDecoder;\n /** Render decoded RSC tree into the DOM. If not provided, rendering is a no-op. */\n renderRoot?: RootRenderer;\n /**\n * Schedule a callback after the next paint. In the browser, this is\n * requestAnimationFrame + setTimeout(0) to run after React commits.\n * In tests, this runs the callback synchronously.\n */\n afterPaint?: (callback: () => void) => void;\n /** Apply resolved head elements (title, meta tags) to the DOM after navigation. */\n applyHead?: (elements: HeadElement[]) => void;\n /**\n * Run a navigation inside a React transition with optimistic pending URL.\n * The pending URL shows immediately (useOptimistic urgent update) and\n * reverts when the transition commits (atomic with the new tree).\n *\n * The `perform` callback receives a `wrapPayload` function to wrap the\n * decoded RSC payload with NavigationProvider + NuqsAdapter before\n * TransitionRoot sets it as the new element.\n *\n * If not provided (tests), the router falls back to renderRoot.\n */\n navigateTransition?: (\n pendingUrl: string,\n perform: (wrapPayload: (payload: unknown) => unknown) => Promise<unknown>,\n ) => Promise<void>;\n}\n\n/** Result of fetching an RSC payload — includes head elements and segment metadata. */\ninterface FetchResult {\n payload: unknown;\n headElements: HeadElement[] | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo: SegmentInfo[] | null;\n /** Route params from X-Timber-Params header for populating useParams(). */\n params: Record<string, string | string[]> | null;\n}\n\nexport interface RouterInstance {\n /** Navigate to a new URL (forward navigation) */\n navigate(url: string, options?: NavigationOptions): Promise<void>;\n /** Full re-render of the current URL — no state tree sent */\n refresh(): Promise<void>;\n /** Handle a popstate event (back/forward button). scrollY is read from history.state. */\n handlePopState(url: string, scrollY?: number): Promise<void>;\n /** Whether a navigation is currently in flight */\n isPending(): boolean;\n /** The URL currently being navigated to, or null if idle */\n getPendingUrl(): string | null;\n /** Subscribe to pending state changes */\n onPendingChange(listener: (pending: boolean) => void): () => void;\n /** Prefetch an RSC payload for a URL (used by Link hover) */\n prefetch(url: string): void;\n /**\n * Apply a piggybacked revalidation payload from a server action response.\n * Renders the element tree and updates head elements without a server fetch.\n * See design/08-forms-and-actions.md §\"Single-Roundtrip Revalidation\".\n */\n applyRevalidation(element: unknown, headElements: HeadElement[] | null): void;\n /**\n * Populate the segment cache from server-provided segment metadata.\n * Called on initial hydration with segment info embedded in the HTML.\n */\n initSegmentCache(segments: SegmentInfo[]): void;\n /** The segment cache (exposed for tests and <Link> prefetch) */\n segmentCache: SegmentCache;\n /** The prefetch cache (exposed for tests and <Link> prefetch) */\n prefetchCache: PrefetchCache;\n /** The history stack (exposed for tests) */\n historyStack: HistoryStack;\n}\n\n/**\n * Thrown when an RSC payload response contains X-Timber-Redirect header.\n * Caught in navigate() to trigger a soft router navigation to the redirect target.\n */\nclass RedirectError extends Error {\n readonly redirectUrl: string;\n constructor(url: string) {\n super(`Server redirect to ${url}`);\n this.redirectUrl = url;\n }\n}\n\n/**\n * Check if an error is an abort error (connection closed / fetch aborted).\n * Browsers throw DOMException with name 'AbortError' when a fetch is aborted.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof DOMException && error.name === 'AbortError') return true;\n if (error instanceof Error && error.name === 'AbortError') return true;\n return false;\n}\n\n// ─── RSC Fetch ───────────────────────────────────────────────────\n\nconst RSC_CONTENT_TYPE = 'text/x-component';\n\n/**\n * Generate a short random cache-busting ID (5 chars, a-z0-9).\n * Matches the format Next.js uses for _rsc params.\n */\nfunction generateCacheBustId(): string {\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n let id = '';\n for (let i = 0; i < 5; i++) {\n id += chars[(Math.random() * 36) | 0];\n }\n return id;\n}\n\n/**\n * Append a `_rsc=<id>` query parameter to the URL.\n * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML\n * for RSC navigation requests and signals that this is an RSC fetch.\n */\nfunction appendRscParam(url: string): string {\n const separator = url.includes('?') ? '&' : '?';\n return `${url}${separator}_rsc=${generateCacheBustId()}`;\n}\n\nfunction buildRscHeaders(\n stateTree: { segments: string[] } | undefined,\n currentUrl?: string\n): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: RSC_CONTENT_TYPE,\n };\n if (stateTree) {\n headers['X-Timber-State-Tree'] = JSON.stringify(stateTree);\n }\n // Send current URL for intercepting route resolution.\n // The server uses this to determine if an intercepting route should\n // render instead of the actual target route (modal pattern).\n // See design/07-routing.md §\"Intercepting Routes\"\n if (currentUrl) {\n headers['X-Timber-URL'] = currentUrl;\n }\n return headers;\n}\n\n/**\n * Extract head elements from the X-Timber-Head response header.\n * Returns null if the header is missing or malformed.\n */\nfunction extractHeadElements(response: Response): HeadElement[] | null {\n const header = response.headers.get('X-Timber-Head');\n if (!header) return null;\n try {\n return JSON.parse(decodeURIComponent(header));\n } catch {\n return null;\n }\n}\n\n/**\n * Extract segment metadata from the X-Timber-Segments response header.\n * Returns null if the header is missing or malformed.\n *\n * Format: JSON array of {path, isAsync} objects describing the rendered\n * segment chain from root to leaf. Used to populate the client-side\n * segment cache for state tree diffing on subsequent navigations.\n */\nfunction extractSegmentInfo(response: Response): SegmentInfo[] | null {\n const header = response.headers.get('X-Timber-Segments');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n return null;\n }\n}\n\n/**\n * Extract route params from the X-Timber-Params response header.\n * Returns null if the header is missing or malformed.\n *\n * Used to populate useParams() after client-side navigation.\n */\nfunction extractParams(response: Response): Record<string, string | string[]> | null {\n const header = response.headers.get('X-Timber-Params');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n return null;\n }\n}\n\n/**\n * Fetch an RSC payload from the server. If a decodeRsc function is provided,\n * the response is decoded into a React element tree via createFromFetch.\n * Otherwise, the raw response text is returned (test mode).\n *\n * Also extracts head elements from the X-Timber-Head response header\n * so the client can update document.title and <meta> tags after navigation.\n */\nasync function fetchRscPayload(\n url: string,\n deps: RouterDeps,\n stateTree?: { segments: string[] },\n currentUrl?: string\n): Promise<FetchResult> {\n const rscUrl = appendRscParam(url);\n const headers = buildRscHeaders(stateTree, currentUrl);\n if (deps.decodeRsc) {\n // Production path: use createFromFetch for streaming RSC decoding.\n // createFromFetch takes a Promise<Response> and progressively parses\n // the RSC Flight stream as chunks arrive.\n //\n // Intercept the response to read X-Timber-Head before createFromFetch\n // consumes the body. Reading headers does NOT consume the body stream.\n const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual' });\n let headElements: HeadElement[] | null = null;\n let segmentInfo: SegmentInfo[] | null = null;\n let params: Record<string, string | string[]> | null = null;\n const wrappedPromise = fetchPromise.then((response) => {\n // Detect server-side redirects. The server returns 204 + X-Timber-Redirect\n // for RSC payload requests instead of a raw 302, because fetch with\n // redirect: \"manual\" turns 302s into opaque redirects (status 0, null body)\n // which crashes createFromFetch when it tries to read the body stream.\n const redirectLocation =\n response.headers.get('X-Timber-Redirect') ||\n (response.status >= 300 && response.status < 400 ? response.headers.get('Location') : null);\n if (redirectLocation) {\n throw new RedirectError(redirectLocation);\n }\n headElements = extractHeadElements(response);\n segmentInfo = extractSegmentInfo(response);\n params = extractParams(response);\n return response;\n });\n // Await so headElements/segmentInfo/params are populated before we return.\n // Also await the decoded payload — createFromFetch returns a thenable\n // that resolves to the React element tree.\n await wrappedPromise;\n const payload = await deps.decodeRsc(wrappedPromise);\n return { payload, headElements, segmentInfo, params };\n }\n // Test/fallback path: return raw text\n const response = await deps.fetch(rscUrl, { headers, redirect: 'manual' });\n // Check for redirect in test path too\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('Location');\n if (location) {\n throw new RedirectError(location);\n }\n }\n return {\n payload: await response.text(),\n headElements: extractHeadElements(response),\n segmentInfo: extractSegmentInfo(response),\n params: extractParams(response),\n };\n}\n\n// ─── Router Factory ──────────────────────────────────────────────\n\n/**\n * Create a router instance. In production, called once at app hydration\n * with real browser APIs. In tests, called with mock dependencies.\n */\nexport function createRouter(deps: RouterDeps): RouterInstance {\n const segmentCache = new SegmentCache();\n const prefetchCache = new PrefetchCache();\n const historyStack = new HistoryStack();\n\n let pending = false;\n let pendingUrl: string | null = null;\n const pendingListeners = new Set<(pending: boolean) => void>();\n\n function setPending(value: boolean, url?: string): void {\n const newPendingUrl = value && url ? url : null;\n if (pending === value && pendingUrl === newPendingUrl) return;\n pending = value;\n pendingUrl = newPendingUrl;\n // Notify external store listeners (non-React consumers).\n // React-facing pending state is handled by useOptimistic in\n // TransitionRoot via navigateTransition — not this function.\n for (const listener of pendingListeners) {\n listener(value);\n }\n }\n\n /** Update the segment cache from server-provided segment metadata. */\n function updateSegmentCache(segmentInfo: SegmentInfo[] | null | undefined): void {\n if (!segmentInfo || segmentInfo.length === 0) return;\n const tree = buildSegmentTree(segmentInfo);\n if (tree) {\n segmentCache.set('/', tree);\n }\n }\n\n /** Render a decoded RSC payload into the DOM if a renderer is available. */\n function renderPayload(payload: unknown): void {\n if (deps.renderRoot) {\n deps.renderRoot(payload);\n }\n }\n\n /**\n * Update navigation state (params + pathname) for the next render.\n *\n * Sets both the module-level fallback (for tests and SSR) and the\n * navigation context state (read by renderRoot to wrap the element\n * in NavigationProvider). The context update is atomic with the tree\n * render — both are passed to reactRoot.render() in the same call.\n */\n function updateNavigationState(\n params: Record<string, string | string[]> | null | undefined,\n url: string\n ): void {\n const resolvedParams = params ?? {};\n // Module-level fallback for tests (no NavigationProvider) and SSR\n setCurrentParams(resolvedParams);\n // Navigation context — read by renderRoot to wrap the RSC element\n const pathname = url.startsWith('http')\n ? new URL(url).pathname\n : url.split('?')[0] || '/';\n setNavigationState({ params: resolvedParams, pathname });\n }\n\n /**\n * Render a payload via navigateTransition (production) or renderRoot (tests).\n * The perform callback should fetch data, update state, and return the payload.\n * In production, the entire callback runs inside a React transition with\n * useOptimistic for the pending URL. In tests, the payload is rendered directly.\n */\n async function renderViaTransition(\n pendingUrl: string,\n perform: () => Promise<FetchResult>,\n ): Promise<HeadElement[] | null> {\n if (deps.navigateTransition) {\n let headElements: HeadElement[] | null = null;\n await deps.navigateTransition(pendingUrl, async (wrapPayload) => {\n const result = await perform();\n headElements = result.headElements;\n return wrapPayload(result.payload);\n });\n return headElements;\n }\n // Fallback: no transition (tests, no React tree)\n const result = await perform();\n renderPayload(result.payload);\n return result.headElements;\n }\n\n /** Apply head elements (title, meta tags) to the DOM if available. */\n function applyHead(elements: HeadElement[] | null | undefined): void {\n if (elements && deps.applyHead) {\n deps.applyHead(elements);\n }\n }\n\n /** Run a callback after the next paint (after React commit). */\n function afterPaint(callback: () => void): void {\n if (deps.afterPaint) {\n deps.afterPaint(callback);\n } else {\n callback();\n }\n }\n\n /**\n * Core navigation logic shared between the transition and fallback paths.\n * Fetches the RSC payload, updates all state, and returns the result.\n */\n async function performNavigationFetch(\n url: string,\n options: { replace: boolean },\n ): Promise<FetchResult> {\n // Check prefetch cache first. PrefetchResult has optional segmentInfo/params\n // fields — normalize to null for FetchResult compatibility.\n const prefetched = prefetchCache.consume(url);\n let result: FetchResult | undefined = prefetched\n ? {\n payload: prefetched.payload,\n headElements: prefetched.headElements,\n segmentInfo: prefetched.segmentInfo ?? null,\n params: prefetched.params ?? null,\n }\n : undefined;\n\n if (result === undefined) {\n // Fetch RSC payload with state tree for partial rendering.\n // Send current URL for intercepting route resolution (modal pattern).\n const stateTree = segmentCache.serializeStateTree();\n const rawCurrentUrl = deps.getCurrentUrl();\n const currentUrl = rawCurrentUrl.startsWith('http')\n ? new URL(rawCurrentUrl).pathname\n : new URL(rawCurrentUrl, 'http://localhost').pathname;\n result = await fetchRscPayload(url, deps, stateTree, currentUrl);\n }\n\n // Update the browser history — replace mode overwrites the current entry\n if (options.replace) {\n deps.replaceState({ timber: true, scrollY: 0 }, '', url);\n } else {\n deps.pushState({ timber: true, scrollY: 0 }, '', url);\n }\n\n // Store the payload in the history stack\n historyStack.push(url, {\n payload: result.payload,\n headElements: result.headElements,\n params: result.params,\n });\n\n // Update the segment cache with the new route's segment tree.\n updateSegmentCache(result.segmentInfo);\n\n // Update navigation state (params + pathname) before rendering.\n updateNavigationState(result.params, url);\n\n return result;\n }\n\n async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {\n const scroll = options.scroll !== false;\n const replace = options.replace === true;\n\n // Capture the departing page's scroll position for scroll={false} preservation.\n const currentScrollY = deps.getScrollY();\n\n // Save the departing page's scroll position in history.state before\n // pushing a new entry. This ensures back/forward navigation can restore\n // the correct scroll position from the browser's per-entry state.\n deps.replaceState({ timber: true, scrollY: currentScrollY }, '', deps.getCurrentUrl());\n\n setPending(true, url);\n\n try {\n const headElements = await renderViaTransition(url, () =>\n performNavigationFetch(url, { replace }),\n );\n\n // Update document.title and <meta> tags with the new page's metadata\n applyHead(headElements);\n\n // Notify nuqs adapter (and any other listeners) that navigation completed.\n window.dispatchEvent(new Event('timber:navigation-end'));\n\n // Scroll-to-top on forward navigation, or restore captured position\n // for scroll={false}. React's render() on the document root can reset\n // scroll during DOM reconciliation, so all scroll must be actively managed.\n afterPaint(() => {\n if (scroll) {\n deps.scrollTo(0, 0);\n } else {\n deps.scrollTo(0, currentScrollY);\n }\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n } catch (error) {\n // Server-side redirect during RSC fetch → soft router navigation.\n if (error instanceof RedirectError) {\n setPending(false);\n await navigate(error.redirectUrl, { replace: true });\n return;\n }\n // Abort errors are not application errors — swallow silently.\n if (isAbortError(error)) return;\n throw error;\n } finally {\n setPending(false);\n }\n }\n\n async function refresh(): Promise<void> {\n const currentUrl = deps.getCurrentUrl();\n\n setPending(true, currentUrl);\n\n try {\n const headElements = await renderViaTransition(currentUrl, async () => {\n // No state tree sent — server renders the complete RSC payload\n const result = await fetchRscPayload(currentUrl, deps);\n historyStack.push(currentUrl, {\n payload: result.payload,\n headElements: result.headElements,\n params: result.params,\n });\n updateSegmentCache(result.segmentInfo);\n updateNavigationState(result.params, currentUrl);\n return result;\n });\n\n applyHead(headElements);\n } finally {\n setPending(false);\n }\n }\n\n async function handlePopState(url: string, scrollY: number = 0): Promise<void> {\n // Scroll position is read from history.state by the caller (browser-entry.ts)\n // and passed in. This is more reliable than tracking scroll per-URL in memory\n // because the browser maintains per-entry state even with duplicate URLs.\n const entry = historyStack.get(url);\n\n if (entry && entry.payload !== null) {\n // Replay cached payload — no server roundtrip\n updateNavigationState(entry.params, url);\n renderPayload(entry.payload);\n applyHead(entry.headElements);\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n } else {\n // No cached payload — fetch from server.\n // This happens when navigating back to the initial SSR'd page\n // (its payload is null since it was rendered via SSR, not RSC fetch)\n // or when the entry doesn't exist at all.\n setPending(true, url);\n try {\n const headElements = await renderViaTransition(url, async () => {\n const stateTree = segmentCache.serializeStateTree();\n const result = await fetchRscPayload(url, deps, stateTree);\n updateSegmentCache(result.segmentInfo);\n updateNavigationState(result.params, url);\n historyStack.push(url, {\n payload: result.payload,\n headElements: result.headElements,\n params: result.params,\n });\n return result;\n });\n\n applyHead(headElements);\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n } finally {\n setPending(false);\n }\n }\n }\n\n /**\n * Prefetch an RSC payload for a URL and store it in the prefetch cache.\n * Called on hover of <Link prefetch> elements.\n */\n function prefetch(url: string): void {\n // Don't prefetch if already cached\n if (prefetchCache.get(url) !== undefined) return;\n if (historyStack.has(url)) return;\n\n // Fire-and-forget fetch\n const stateTree = segmentCache.serializeStateTree();\n void fetchRscPayload(url, deps, stateTree).then(\n (result) => {\n prefetchCache.set(url, result);\n },\n () => {\n // Prefetch failure is non-fatal — navigation will fetch fresh\n }\n );\n }\n\n return {\n navigate,\n refresh,\n handlePopState,\n isPending: () => pending,\n getPendingUrl: () => pendingUrl,\n onPendingChange(listener) {\n pendingListeners.add(listener);\n return () => pendingListeners.delete(listener);\n },\n prefetch,\n applyRevalidation(element: unknown, headElements: HeadElement[] | null): void {\n // Render the piggybacked element tree from a server action response.\n // Updates the current history entry with the fresh payload and applies\n // head elements — same as refresh() but without a server fetch.\n const currentUrl = deps.getCurrentUrl();\n historyStack.push(currentUrl, {\n payload: element,\n headElements,\n });\n renderPayload(element);\n applyHead(headElements);\n },\n initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),\n segmentCache,\n prefetchCache,\n historyStack,\n };\n}\n","// useNavigationPending — returns true while an RSC navigation is in flight.\n// See design/19-client-navigation.md §\"useNavigationPending()\"\n//\n// Reads from PendingNavigationContext (provided by TransitionRoot) so the\n// pending state shows immediately (urgent update) and clears atomically\n// with the new tree (same startTransition commit).\n\nimport { usePendingNavigationUrl } from './navigation-context.js';\n\n/**\n * Returns true while an RSC navigation is in flight.\n *\n * The pending state is true from the moment the RSC fetch starts until\n * React reconciliation completes. This includes the fetch itself,\n * RSC stream parsing, and React tree reconciliation.\n *\n * It does NOT include Suspense streaming after the shell — only the\n * initial shell reconciliation.\n *\n * ```tsx\n * 'use client'\n * import { useNavigationPending } from '@timber-js/app/client'\n *\n * export function NavBar() {\n * const isPending = useNavigationPending()\n * return (\n * <nav className={isPending ? 'opacity-50' : ''}>\n * <Link href=\"/dashboard\">Dashboard</Link>\n * </nav>\n * )\n * }\n * ```\n */\nexport function useNavigationPending(): boolean {\n const pendingUrl = usePendingNavigationUrl();\n // During SSR or outside PendingNavigationProvider, no navigation is pending\n return pendingUrl !== null;\n}\n","// Global router reference — shared between browser-entry and client hooks.\n//\n// Delegates to client/state.ts for the actual module-level variable.\n// This ensures singleton semantics regardless of import path — all\n// callers converge on the same state.ts instance via the barrel.\n//\n// See design/18-build-system.md §\"Module Singleton Strategy\"\n\nimport type { RouterInstance } from './router.js';\nimport { globalRouter, _setGlobalRouter } from './state.js';\n\n/**\n * Set the global router instance. Called once during bootstrap.\n */\nexport function setGlobalRouter(router: RouterInstance): void {\n _setGlobalRouter(router);\n}\n\n/**\n * Get the global router instance. Throws if called before bootstrap.\n * Used by client-side hooks (useNavigationPending, etc.)\n */\nexport function getRouter(): RouterInstance {\n if (!globalRouter) {\n throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');\n }\n return globalRouter;\n}\n\n/**\n * Get the global router instance or null if not yet initialized.\n * Used by useRouter() methods to avoid silent failures — callers\n * can log a meaningful warning instead of silently no-oping.\n */\nexport function getRouterOrNull(): RouterInstance | null {\n return globalRouter;\n}\n\n/**\n * Reset the global router to null. Used only in tests to isolate\n * module-level state between test cases.\n * @internal\n */\nexport function resetGlobalRouter(): void {\n _setGlobalRouter(null);\n}\n","/**\n * useRouter() — client-side hook for programmatic navigation.\n *\n * Returns a router instance with push, replace, refresh, back, forward,\n * and prefetch methods. Compatible with Next.js's `useRouter()` from\n * `next/navigation` (App Router).\n *\n * This wraps timber's internal RouterInstance in the Next.js-compatible\n * AppRouterInstance shape that ecosystem libraries expect.\n *\n * NOTE: Unlike Next.js, these methods do NOT wrap navigation in\n * startTransition. In Next.js, router state is React state (useReducer)\n * so startTransition defers the update and provides isPending tracking.\n * In timber, navigation calls reactRoot.render() which is a root-level\n * render — startTransition has no effect on root renders.\n *\n * Navigation state (params, pathname) is delivered atomically via\n * NavigationContext embedded in the element tree passed to\n * reactRoot.render(). See design/19-client-navigation.md §\"NavigationContext\".\n *\n * For loading UI during navigation, use:\n * - useLinkStatus() — per-link pending indicator (inside <Link>)\n * - useNavigationPending() — global navigation pending state\n */\n\nimport { getRouterOrNull } from './router-ref.js';\n\nexport interface AppRouterInstance {\n /** Navigate to a URL, pushing a new history entry */\n push(href: string, options?: { scroll?: boolean }): void;\n /** Navigate to a URL, replacing the current history entry */\n replace(href: string, options?: { scroll?: boolean }): void;\n /** Refresh the current page (re-fetch RSC payload) */\n refresh(): void;\n /** Navigate back in history */\n back(): void;\n /** Navigate forward in history */\n forward(): void;\n /** Prefetch an RSC payload for a URL */\n prefetch(href: string): void;\n}\n\n/**\n * Get a router instance for programmatic navigation.\n *\n * Compatible with Next.js's `useRouter()` from `next/navigation`.\n *\n * Methods lazily resolve the global router when invoked (during user\n * interaction) rather than capturing it at render time. This is critical\n * because during hydration, React synchronously executes component render\n * functions *before* the router is bootstrapped in browser-entry.ts.\n * If we eagerly captured the router during render, components would get\n * a null reference and be stuck with silent no-ops forever.\n *\n * Returns safe no-ops during SSR or before bootstrap. The `typeof window`\n * check is insufficient because Vite's client SSR environment defines\n * `window`, so we use a try/catch on getRouter() — but only at method\n * invocation time, not at render time.\n */\nexport function useRouter(): AppRouterInstance {\n return {\n push(href: string, options?: { scroll?: boolean }) {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.');\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll });\n },\n replace(href: string, options?: { scroll?: boolean }) {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().replace() called but router is not initialized.');\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll, replace: true });\n },\n refresh() {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().refresh() called but router is not initialized.');\n }\n return;\n }\n void router.refresh();\n },\n back() {\n if (typeof window !== 'undefined') window.history.back();\n },\n forward() {\n if (typeof window !== 'undefined') window.history.forward();\n },\n prefetch(href: string) {\n const router = getRouterOrNull();\n if (!router) return; // Silent — prefetch failure is non-fatal\n router.prefetch(href);\n },\n };\n}\n","/**\n * usePathname() — client-side hook for reading the current pathname.\n *\n * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').\n * Updates when client-side navigation changes the URL.\n *\n * On the client, reads from NavigationContext which is updated atomically\n * with the RSC tree render. This replaces the previous useSyncExternalStore\n * approach which only subscribed to popstate events — meaning usePathname()\n * did NOT re-render on forward navigation (pushState). The context approach\n * fixes this: pathname updates in the same render pass as the new tree.\n *\n * During SSR, reads the request pathname from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { getSsrData } from './ssr-data.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n/**\n * Read the current URL pathname.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). During SSR, reads from the\n * ALS-backed SSR data context. Falls back to window.location.pathname\n * when called outside a React component (e.g., in tests).\n */\nexport function usePathname(): string {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n try {\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.pathname;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to SSR/fallback below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n const ssrData = getSsrData();\n if (ssrData) return ssrData.pathname ?? '/';\n\n // Final fallback: window.location (tests, edge cases).\n if (typeof window !== 'undefined') return window.location.pathname;\n return '/';\n}\n","/**\n * useSearchParams() — client-side hook for reading URL search params.\n *\n * Returns a read-only URLSearchParams instance reflecting the current\n * URL's query string. Updates when client-side navigation changes the URL.\n *\n * This is a thin wrapper over window.location.search, provided for\n * Next.js API compatibility (libraries like nuqs import useSearchParams\n * from next/navigation).\n *\n * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard\n * URLSearchParams. Mutation methods (set, delete, append) work on the\n * local copy but do NOT affect the URL — use the router or nuqs for that.\n *\n * During SSR, reads the request search params from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\nimport { cachedSearch, cachedSearchParams, _setCachedSearch } from './state.js';\n\nfunction getSearch(): string {\n if (typeof window !== 'undefined') return window.location.search;\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction getServerSearch(): string {\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction subscribe(callback: () => void): () => void {\n window.addEventListener('popstate', callback);\n return () => window.removeEventListener('popstate', callback);\n}\n\n// Cache the last search string and its parsed URLSearchParams to avoid\n// creating a new object on every render when the URL hasn't changed.\n// State lives in client/state.ts for singleton guarantees.\n\nfunction getSearchParams(): URLSearchParams {\n const search = getSearch();\n if (search !== cachedSearch) {\n const params = new URLSearchParams(search);\n _setCachedSearch(search, params);\n return params;\n }\n return cachedSearchParams;\n}\n\nfunction getServerSearchParams(): URLSearchParams {\n const data = getSsrData();\n return data ? new URLSearchParams(data.searchParams) : new URLSearchParams();\n}\n\n/**\n * Read the current URL search params.\n *\n * Compatible with Next.js's `useSearchParams()` from `next/navigation`.\n */\nexport function useSearchParams(): URLSearchParams {\n // useSyncExternalStore needs a primitive snapshot for comparison.\n // We use the raw search string as the snapshot, then return the\n // parsed URLSearchParams.\n useSyncExternalStore(subscribe, getSearch, getServerSearch);\n return typeof window !== 'undefined' ? getSearchParams() : getServerSearchParams();\n}\n","/**\n * Segment Context — provides layout segment position for useSelectedLayoutSegment hooks.\n *\n * Each layout in the segment tree is wrapped with a SegmentProvider that stores\n * the URL segments from root to the current layout level. The hooks read this\n * context to determine which child segments are active below the calling layout.\n *\n * The context value is intentionally minimal: just the segment path array and\n * parallel route keys. No internal cache details are exposed.\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { createContext, useContext, createElement, useMemo } from 'react';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface SegmentContextValue {\n /** URL segments from root to this layout (e.g. ['', 'dashboard', 'settings']) */\n segments: string[];\n /** Parallel route slot keys available at this layout level (e.g. ['sidebar', 'modal']) */\n parallelRouteKeys: string[];\n}\n\n// ─── Context ─────────────────────────────────────────────────────\n\nconst SegmentContext = createContext<SegmentContextValue | null>(null);\n\n/** Read the segment context. Returns null if no provider is above this component. */\nexport function useSegmentContext(): SegmentContextValue | null {\n return useContext(SegmentContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface SegmentProviderProps {\n segments: string[];\n parallelRouteKeys: string[];\n children: React.ReactNode;\n}\n\n/**\n * Wraps each layout to provide segment position context.\n * Injected by rsc-entry.ts during element tree construction.\n */\nexport function SegmentProvider({ segments, parallelRouteKeys, children }: SegmentProviderProps) {\n const value = useMemo(\n () => ({ segments, parallelRouteKeys }),\n // segments and parallelRouteKeys are static per layout — they don't change\n // across navigations. The layout's position in the tree is fixed.\n // Intentionally using derived keys — segments/parallelRouteKeys are static per layout\n [segments.join('/'), parallelRouteKeys.join(',')]\n );\n return createElement(SegmentContext.Provider, { value }, children);\n}\n","/**\n * useSelectedLayoutSegment / useSelectedLayoutSegments — client-side hooks\n * for reading the active segment(s) below the current layout.\n *\n * These hooks are used by navigation UIs to highlight active sections.\n * They match Next.js's API from next/navigation.\n *\n * How they work:\n * 1. Each layout is wrapped with a SegmentProvider that records its depth\n * (the URL segments from root to that layout level).\n * 2. The hooks read the current URL pathname via usePathname().\n * 3. They compare the layout's segment depth against the full URL segments\n * to determine which child segments are \"selected\" below.\n *\n * Example: For URL \"/dashboard/settings/profile\"\n * - Root layout (depth 0, segments: ['']): selected segment = \"dashboard\"\n * - Dashboard layout (depth 1, segments: ['', 'dashboard']): selected = \"settings\"\n * - Settings layout (depth 2, segments: ['', 'dashboard', 'settings']): selected = \"profile\"\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { useSegmentContext } from './segment-context.js';\nimport { usePathname } from './use-pathname.js';\n\n/**\n * Split a pathname into URL segments.\n * \"/\" → [\"\"]\n * \"/dashboard\" → [\"\", \"dashboard\"]\n * \"/dashboard/settings\" → [\"\", \"dashboard\", \"settings\"]\n */\nexport function pathnameToSegments(pathname: string): string[] {\n return pathname.split('/');\n}\n\n/**\n * Pure function: compute the selected child segment given a layout's segment\n * depth and the current URL pathname.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns the active child segment one level below, or null if at the leaf\n */\nexport function getSelectedSegment(\n contextSegments: string[] | null,\n pathname: string\n): string | null {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments[1] || null;\n }\n\n const depth = contextSegments.length;\n return urlSegments[depth] || null;\n}\n\n/**\n * Pure function: compute all selected segments below a layout's depth.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns all active segments below the layout\n */\nexport function getSelectedSegments(contextSegments: string[] | null, pathname: string): string[] {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments.slice(1).filter(Boolean);\n }\n\n const depth = contextSegments.length;\n return urlSegments.slice(depth).filter(Boolean);\n}\n\n/**\n * Returns the active child segment one level below the layout where this\n * hook is called. Returns `null` if the layout is the leaf (no child segment).\n *\n * Compatible with Next.js's `useSelectedLayoutSegment()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegment(parallelRouteKey?: string): string | null {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegment(context?.segments ?? null, pathname);\n}\n\n/**\n * Returns all active segments below the layout where this hook is called.\n * Returns an empty array if the layout is the leaf (no child segments).\n *\n * Compatible with Next.js's `useSelectedLayoutSegments()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegments(parallelRouteKey?: string): string[] {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegments(context?.segments ?? null, pathname);\n}\n","/**\n * Client-side form utilities for server actions.\n *\n * Exports a typed `useActionState` that understands the action builder's result shape.\n * Result is typed to:\n * { data: T } | { validationErrors: Record<string, string[]> } | { serverError: { code, data? } } | null\n *\n * The action builder emits a function that satisfies both the direct call signature\n * and React's `(prevState, formData) => Promise<State>` contract.\n *\n * See design/08-forms-and-actions.md §\"Client-Side Form Mechanics\"\n */\n\nimport { useActionState as reactUseActionState, useTransition } from 'react';\nimport type { ActionResult, ValidationErrors } from '#/server/action-client';\nimport type { FormFlashData } from '#/server/form-flash';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/**\n * The action function type accepted by useActionState.\n * Must satisfy React's (prevState, formData) => Promise<State> contract.\n */\nexport type UseActionStateFn<TData> = (\n prevState: ActionResult<TData> | null,\n formData: FormData\n) => Promise<ActionResult<TData>>;\n\n/**\n * Return type of useActionState — matches React 19's useActionState return.\n * [result, formAction, isPending]\n */\nexport type UseActionStateReturn<TData> = [\n result: ActionResult<TData> | null,\n formAction: (formData: FormData) => void,\n isPending: boolean,\n];\n\n// ─── useActionState ──────────────────────────────────────────────────────\n\n/**\n * Typed wrapper around React 19's `useActionState` that understands\n * the timber action builder's result shape.\n *\n * @param action - A server action created with createActionClient or a raw 'use server' function.\n * @param initialState - Initial state, typically `null`. Pass `getFormFlash()` for no-JS\n * progressive enhancement — the flash seeds the initial state so the form has a\n * single source of truth for both with-JS and no-JS paths.\n * @param permalink - Optional permalink for progressive enhancement (no-JS fallback URL).\n *\n * @example\n * ```tsx\n * 'use client'\n * import { useActionState } from '@timber-js/app/client'\n * import { createTodo } from './actions'\n *\n * export function NewTodoForm({ flash }) {\n * const [result, action, isPending] = useActionState(createTodo, flash)\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {result?.validationErrors?.title && <p>{result.validationErrors.title}</p>}\n * <button disabled={isPending}>Add</button>\n * </form>\n * )\n * }\n * ```\n */\nexport function useActionState<TData>(\n action: UseActionStateFn<TData>,\n initialState: ActionResult<TData> | FormFlashData | null,\n permalink?: string\n): UseActionStateReturn<TData> {\n // FormFlashData is structurally compatible with ActionResult at runtime —\n // the cast satisfies React's generic inference which would otherwise widen TData.\n return reactUseActionState(action, initialState as ActionResult<TData> | null, permalink);\n}\n\n// ─── useFormAction ───────────────────────────────────────────────────────\n\n/**\n * Hook for calling a server action imperatively (not via a form).\n * Returns [execute, isPending] where execute accepts the input directly.\n *\n * @example\n * ```tsx\n * const [deleteTodo, isPending] = useFormAction(deleteTodoAction)\n * <button onClick={() => deleteTodo({ id: todo.id })} disabled={isPending}>\n * Delete\n * </button>\n * ```\n */\nexport function useFormAction<TData>(\n action: (input: unknown) => Promise<ActionResult<TData>>\n): [(input?: unknown) => Promise<ActionResult<TData>>, boolean] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: unknown): Promise<ActionResult<TData>> => {\n return new Promise((resolve) => {\n startTransition(async () => {\n const result = await action(input);\n resolve(result);\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── useFormErrors ──────────────────────────────────────────────────────\n\n/** Return type of useFormErrors(). */\nexport interface FormErrorsResult {\n /** Per-field validation errors keyed by field name. */\n fieldErrors: Record<string, string[]>;\n /** Form-level errors (from `_root` key). */\n formErrors: string[];\n /** Server error if the action threw an ActionError. */\n serverError: { code: string; data?: Record<string, unknown> } | null;\n /** Whether any errors are present. */\n hasErrors: boolean;\n /** Get the first error message for a field, or null. */\n getFieldError: (field: string) => string | null;\n}\n\n/**\n * Extract per-field and form-level errors from an ActionResult.\n *\n * Pure function (no internal hooks) — follows React naming convention\n * since it's used in render. Accepts the result from `useActionState`\n * or flash data from `getFormFlash()`.\n *\n * @example\n * ```tsx\n * const [result, action, isPending] = useActionState(createTodo, null)\n * const errors = useFormErrors(result)\n *\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {errors.getFieldError('title') && <p>{errors.getFieldError('title')}</p>}\n * {errors.formErrors.map(e => <p key={e}>{e}</p>)}\n * </form>\n * )\n * ```\n */\nexport function useFormErrors<TData>(\n result:\n | ActionResult<TData>\n | {\n validationErrors?: ValidationErrors;\n serverError?: { code: string; data?: Record<string, unknown> };\n }\n | null\n): FormErrorsResult {\n const empty: FormErrorsResult = {\n fieldErrors: {},\n formErrors: [],\n serverError: null,\n hasErrors: false,\n getFieldError: () => null,\n };\n\n if (!result) return empty;\n\n const validationErrors = result.validationErrors as ValidationErrors | undefined;\n const serverError = result.serverError as\n | { code: string; data?: Record<string, unknown> }\n | undefined;\n\n if (!validationErrors && !serverError) return empty;\n\n // Separate _root (form-level) errors from field errors\n const fieldErrors: Record<string, string[]> = {};\n const formErrors: string[] = [];\n\n if (validationErrors) {\n for (const [key, messages] of Object.entries(validationErrors)) {\n if (key === '_root') {\n formErrors.push(...messages);\n } else {\n fieldErrors[key] = messages;\n }\n }\n }\n\n const hasErrors =\n Object.keys(fieldErrors).length > 0 || formErrors.length > 0 || serverError != null;\n\n return {\n fieldErrors,\n formErrors,\n serverError: serverError ?? null,\n hasErrors,\n getFieldError(field: string): string | null {\n const errs = fieldErrors[field];\n return errs && errs.length > 0 ? errs[0] : null;\n },\n };\n}\n"],"mappings":";;;;;;;;;AAWA,IAAa,kBAAkB;;;;;;;;AAyB/B,SAAgB,wBAAwB,EACtC,YACA,YAIC;CACD,MAAM,MAAM,OAAwB,KAAK;AAEzC,iBAAgB;EACd,MAAM,SAAS,IAAI,SAAS,QAAQ,IAAI;AACxC,MAAI,CAAC,OAAQ;AACb,SAAO,mBAAmB;AAC1B,eAAa;AACX,UAAO,OAAO;;IAEf,CAAC,WAAW,CAAC;AAIhB,QACE,oBAAC,QAAD;EAAW;EAAK,OAAO,EAAE,SAAS,YAAY;EAC3C;EACI,CAAA;;;;;;;;AC3CX,IAAa,oBAAoB,cAA0B,EAAE,SAAS,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;AA2B9E,SAAgB,gBAA4B;AAC1C,QAAO,WAAW,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACMtC,IAAI;AAEJ,SAAS,qBAAwE;AAC/E,KAAI,aAAa,KAAA,EAAW,QAAO;AAEnC,KAAI,OAAO,MAAM,kBAAkB,WACjC,YAAW,MAAM,cAAsC,KAAK;AAE9D,QAAO;;;;;;;AAQT,SAAgB,uBAA+C;CAC7D,MAAM,MAAM,oBAAoB;AAChC,KAAI,CAAC,IAAK,QAAO;AAEjB,KAAI,OAAO,MAAM,eAAe,WAAY,QAAO;AACnD,QAAO,MAAM,WAAW,IAAI;;;;;;;;AAkB9B,SAAgB,mBAAmB,EAAE,OAAO,YAAyD;CACnG,MAAM,MAAM,oBAAoB;AAChC,KAAI,CAAC,IAEH,QAAO;AAET,QAAO,cAAc,IAAI,UAAU,EAAE,OAAO,EAAE,SAAS;;;;;;;;;;;AAgBzD,IAAI,mBAAoC;CAAE,QAAQ,EAAE;CAAE,UAAU;CAAK;AAErE,SAAgB,mBAAmB,OAA8B;AAC/D,oBAAmB;;AAGrB,SAAgB,qBAAsC;AACpD,QAAO;;;;;;;;;;;;;AAkBT,IAAI;AAEJ,SAAS,4BAAsE;AAC7E,KAAI,oBAAoB,KAAA,EAAW,QAAO;AAC1C,KAAI,OAAO,MAAM,kBAAkB,WACjC,mBAAkB,MAAM,cAA6B,KAAK;AAE5D,QAAO;;;;;;AAOT,SAAgB,0BAAyC;CACvD,MAAM,MAAM,2BAA2B;AACvC,KAAI,CAAC,IAAK,QAAO;AACjB,KAAI,OAAO,MAAM,eAAe,WAAY,QAAO;AACnD,QAAO,MAAM,WAAW,IAAI;;;;AC1I9B,IAAM,cAA0B,EAAE,SAAS,OAAO;AAClD,IAAM,aAAyB,EAAE,SAAS,MAAM;;;;;;AAOhD,SAAgB,mBAAmB,EAAE,MAAM,YAAoD;CAE7F,MAAM,SADa,yBAAyB,KACd,OAAO,aAAa;AAElD,QAAO,oBAAC,kBAAkB,UAAnB;EAA4B,OAAO;EAAS;EAAsC,CAAA;;;;;;;;ACoE3F,IAAM,oBAAoB;AAE1B,SAAgB,iBAAiB,MAAoB;AACnD,KAAI,kBAAkB,KAAK,KAAK,CAC9B,OAAM,IAAI,MACR,sCAAsC,KAAK,4DAE5C;;;AAOL,SAAS,eAAe,MAAuB;AAE7C,KAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW,IAAI,CACtE,QAAO;AAGT,KAAI,uBAAuB,KAAK,KAAK,CACnC,QAAO;AAGT,QAAO;;;;;;;;;;;AAcT,SAAgB,kBACd,SACA,QACQ;AACR,QACE,QACG,QACC,mDACC,QAAQ,kBAAkB,UAAU,WAAW;AAC9C,MAAI,kBAAkB;GACpB,MAAM,QAAQ,OAAO;AACrB,OAAI,UAAU,KAAA,KAAc,MAAM,QAAQ,MAAM,IAAI,MAAM,WAAW,EACnE,QAAO;AAGT,WADiB,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,MAAM,EACvC,IAAI,mBAAmB,CAAC,KAAK,IAAI;;AAGnD,MAAI,UAAU;GACZ,MAAM,QAAQ,OAAO;AACrB,OAAI,UAAU,KAAA,EACZ,OAAM,IAAI,MACR,4CAA4C,SAAS,iBAAiB,QAAQ,IAC/E;GAEH,MAAM,WAAW,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,MAAM;AACvD,OAAI,SAAS,WAAW,EACtB,OAAM,IAAI,MACR,2BAA2B,SAAS,gDAAgD,QAAQ,IAC7F;AAEH,UAAO,SAAS,IAAI,mBAAmB,CAAC,KAAK,IAAI;;EAInD,MAAM,QAAQ,OAAO;AACrB,MAAI,UAAU,KAAA,EACZ,OAAM,IAAI,MAAM,kCAAkC,OAAO,iBAAiB,QAAQ,IAAI;AAExF,MAAI,MAAM,QAAQ,MAAM,CACtB,OAAM,IAAI,MACR,iBAAiB,OAAO,yDAAyD,QAAQ,IAC1F;AAGH,SAAO,mBAAmB,OAAO,MAAM,CAAC;GAE3C,CAEA,QAAQ,QAAQ,GAAG,IAAI;;;;;;;;;;AAc9B,SAAgB,YACd,MACA,QACA,cAIQ;CACR,IAAI,eAAe;AAGnB,KAAI,OACF,gBAAe,kBAAkB,MAAM,OAAO;AAIhD,KAAI,cAAc;AAEhB,MAAI,aAAa,SAAS,IAAI,CAC5B,OAAM,IAAI,MACR,4HAED;EAGH,MAAM,KAAK,aAAa,WAAW,UAAU,aAAa,OAAO;AACjE,MAAI,GACF,gBAAe,GAAG,aAAa,GAAG;;AAItC,QAAO;;;;;;AAgBT,SAAgB,eACd,OAOiB;CACjB,MAAM,eAAe,YAAY,MAAM,MAAM,MAAM,QAAQ,MAAM,aAAa;AAE9E,kBAAiB,aAAa;CAE9B,MAAM,SAA0B,EAAE,MAAM,cAAc;AAGtD,KAFiB,eAAe,aAAa,EAE/B;AACZ,SAAO,sBAAsB;AAE7B,MAAI,MAAM,SACR,QAAO,0BAA0B;AAGnC,MAAI,MAAM,WAAW,MACnB,QAAO,wBAAwB;;AAInC,QAAO;;;;;;;;;;;;;AAgBT,SAAgB,KAAK,EACnB,MACA,UACA,QACA,QACA,cACA,YACA,UACA,GAAG,QACS;CACZ,MAAM,YAAY,eAAe;EAAE;EAAM;EAAU;EAAQ;EAAQ;EAAc,CAAC;CAElF,MAAM,QAAQ,oBAAC,oBAAD;EAAoB,MAAM,UAAU;EAAO;EAA8B,CAAA;AAEvF,QACE,oBAAC,KAAD;EAAG,GAAI;EAAM,GAAI;YACd,aACC,oBAAC,yBAAD;GAAqC;aAAa;GAAgC,CAAA,GAElF;EAEA,CAAA;;;;;;;;;ACtQR,IAAa,eAAb,MAA0B;CACxB;CAEA,IAAI,SAA0C;AAC5C,MAAI,YAAY,OAAO,YAAY,KAAK,MAAM,QAC5C,QAAO,KAAK;;CAKhB,IAAI,SAAiB,MAAyB;AAC5C,MAAI,YAAY,OAAO,CAAC,KAAK,KAC3B,MAAK,OAAO;;CAIhB,QAAc;AACZ,OAAK,OAAO,KAAA;;;;;;;;;;CAWd,qBAAgC;EAC9B,MAAM,WAAqB,EAAE;AAC7B,MAAI,KAAK,KACP,qBAAoB,KAAK,MAAM,SAAS;AAE1C,SAAO,EAAE,UAAU;;;;AAKvB,SAAS,oBAAoB,MAAmB,KAAqB;AACnE,KAAI,CAAC,KAAK,QACR,KAAI,KAAK,KAAK,QAAQ;AAExB,MAAK,MAAM,SAAS,KAAK,SAAS,QAAQ,CACxC,qBAAoB,OAAO,IAAI;;;;;;;;;;;;;AA0BnC,SAAgB,iBAAiB,UAAkD;AAEjF,KAAI,SAAS,WAAW,EAAG,QAAO,KAAA;CAIlC,MAAM,UAAU,SAAS,SAAS,IAAI,SAAS,MAAM,GAAG,GAAG,GAAG;CAE9D,IAAI;CACJ,IAAI;AAEJ,MAAK,MAAM,QAAQ,SAAS;EAC1B,MAAM,OAAoB;GACxB,SAAS,KAAK;GACd,SAAS;GACT,SAAS,KAAK;GACd,0BAAU,IAAI,KAAK;GACpB;AAED,MAAI,CAAC,KACH,QAAO;AAGT,MAAI,OACF,QAAO,SAAS,IAAI,KAAK,MAAM,KAAK;AAGtC,WAAS;;AAGX,QAAO;;;;;;;;;;AAkBT,IAAa,gBAAb,MAAa,cAAc;CACzB,OAAwB,SAAS;CACjC,0BAAkB,IAAI,KAA4B;CAElD,IAAI,KAAa,QAA8B;AAC7C,OAAK,QAAQ,IAAI,KAAK;GACpB;GACA,WAAW,KAAK,KAAK,GAAG,cAAc;GACvC,CAAC;;CAGJ,IAAI,KAAyC;EAC3C,MAAM,QAAQ,KAAK,QAAQ,IAAI,IAAI;AACnC,MAAI,CAAC,MAAO,QAAO,KAAA;AACnB,MAAI,KAAK,KAAK,IAAI,MAAM,WAAW;AACjC,QAAK,QAAQ,OAAO,IAAI;AACxB;;AAEF,SAAO,MAAM;;;CAIf,QAAQ,KAAyC;EAC/C,MAAM,SAAS,KAAK,IAAI,IAAI;AAC5B,MAAI,WAAW,KAAA,EACb,MAAK,QAAQ,OAAO,IAAI;AAE1B,SAAO;;;;;;;;;;;;;;;;;;AChKX,IAAa,eAAb,MAA0B;CACxB,0BAAkB,IAAI,KAA2B;CAEjD,KAAK,KAAa,OAA2B;AAC3C,OAAK,QAAQ,IAAI,KAAK,MAAM;;CAG9B,IAAI,KAAuC;AACzC,SAAO,KAAK,QAAQ,IAAI,IAAI;;CAG9B,IAAI,KAAsB;AACxB,SAAO,KAAK,QAAQ,IAAI,IAAI;;;;;;;;;;;;;;;;;;;;ACoChC,SAAgB,iBAAiB,QAAiD;AAChF,mBAAkB,OAAO;;AA2C3B,SAAgB,UAAU,QAAoD;AAI5E,KAAI;EACF,MAAM,aAAa,sBAAsB;AACzC,MAAI,eAAe,KACjB,QAAO,WAAW;SAEd;AAOR,QAAO,YAAY,EAAE,UAAU;;;;;;;;AClBjC,IAAM,gBAAN,cAA4B,MAAM;CAChC;CACA,YAAY,KAAa;AACvB,QAAM,sBAAsB,MAAM;AAClC,OAAK,cAAc;;;;;;;AAQvB,SAAS,aAAa,OAAyB;AAC7C,KAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAAc,QAAO;AACzE,KAAI,iBAAiB,SAAS,MAAM,SAAS,aAAc,QAAO;AAClE,QAAO;;AAKT,IAAM,mBAAmB;;;;;AAMzB,SAAS,sBAA8B;CACrC,MAAM,QAAQ;CACd,IAAI,KAAK;AACT,MAAK,IAAI,IAAI,GAAG,IAAI,GAAG,IACrB,OAAM,MAAO,KAAK,QAAQ,GAAG,KAAM;AAErC,QAAO;;;;;;;AAQT,SAAS,eAAe,KAAqB;AAE3C,QAAO,GAAG,MADQ,IAAI,SAAS,IAAI,GAAG,MAAM,IAClB,OAAO,qBAAqB;;AAGxD,SAAS,gBACP,WACA,YACwB;CACxB,MAAM,UAAkC,EACtC,QAAQ,kBACT;AACD,KAAI,UACF,SAAQ,yBAAyB,KAAK,UAAU,UAAU;AAM5D,KAAI,WACF,SAAQ,kBAAkB;AAE5B,QAAO;;;;;;AAOT,SAAS,oBAAoB,UAA0C;CACrE,MAAM,SAAS,SAAS,QAAQ,IAAI,gBAAgB;AACpD,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI;AACF,SAAO,KAAK,MAAM,mBAAmB,OAAO,CAAC;SACvC;AACN,SAAO;;;;;;;;;;;AAYX,SAAS,mBAAmB,UAA0C;CACpE,MAAM,SAAS,SAAS,QAAQ,IAAI,oBAAoB;AACxD,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI;AACF,SAAO,KAAK,MAAM,OAAO;SACnB;AACN,SAAO;;;;;;;;;AAUX,SAAS,cAAc,UAA8D;CACnF,MAAM,SAAS,SAAS,QAAQ,IAAI,kBAAkB;AACtD,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI;AACF,SAAO,KAAK,MAAM,OAAO;SACnB;AACN,SAAO;;;;;;;;;;;AAYX,eAAe,gBACb,KACA,MACA,WACA,YACsB;CACtB,MAAM,SAAS,eAAe,IAAI;CAClC,MAAM,UAAU,gBAAgB,WAAW,WAAW;AACtD,KAAI,KAAK,WAAW;EAOlB,MAAM,eAAe,KAAK,MAAM,QAAQ;GAAE;GAAS,UAAU;GAAU,CAAC;EACxE,IAAI,eAAqC;EACzC,IAAI,cAAoC;EACxC,IAAI,SAAmD;EACvD,MAAM,iBAAiB,aAAa,MAAM,aAAa;GAKrD,MAAM,mBACJ,SAAS,QAAQ,IAAI,oBAAoB,KACxC,SAAS,UAAU,OAAO,SAAS,SAAS,MAAM,SAAS,QAAQ,IAAI,WAAW,GAAG;AACxF,OAAI,iBACF,OAAM,IAAI,cAAc,iBAAiB;AAE3C,kBAAe,oBAAoB,SAAS;AAC5C,iBAAc,mBAAmB,SAAS;AAC1C,YAAS,cAAc,SAAS;AAChC,UAAO;IACP;AAIF,QAAM;AAEN,SAAO;GAAE,SADO,MAAM,KAAK,UAAU,eAAe;GAClC;GAAc;GAAa;GAAQ;;CAGvD,MAAM,WAAW,MAAM,KAAK,MAAM,QAAQ;EAAE;EAAS,UAAU;EAAU,CAAC;AAE1E,KAAI,SAAS,UAAU,OAAO,SAAS,SAAS,KAAK;EACnD,MAAM,WAAW,SAAS,QAAQ,IAAI,WAAW;AACjD,MAAI,SACF,OAAM,IAAI,cAAc,SAAS;;AAGrC,QAAO;EACL,SAAS,MAAM,SAAS,MAAM;EAC9B,cAAc,oBAAoB,SAAS;EAC3C,aAAa,mBAAmB,SAAS;EACzC,QAAQ,cAAc,SAAS;EAChC;;;;;;AASH,SAAgB,aAAa,MAAkC;CAC7D,MAAM,eAAe,IAAI,cAAc;CACvC,MAAM,gBAAgB,IAAI,eAAe;CACzC,MAAM,eAAe,IAAI,cAAc;CAEvC,IAAI,UAAU;CACd,IAAI,aAA4B;CAChC,MAAM,mCAAmB,IAAI,KAAiC;CAE9D,SAAS,WAAW,OAAgB,KAAoB;EACtD,MAAM,gBAAgB,SAAS,MAAM,MAAM;AAC3C,MAAI,YAAY,SAAS,eAAe,cAAe;AACvD,YAAU;AACV,eAAa;AAIb,OAAK,MAAM,YAAY,iBACrB,UAAS,MAAM;;;CAKnB,SAAS,mBAAmB,aAAqD;AAC/E,MAAI,CAAC,eAAe,YAAY,WAAW,EAAG;EAC9C,MAAM,OAAO,iBAAiB,YAAY;AAC1C,MAAI,KACF,cAAa,IAAI,KAAK,KAAK;;;CAK/B,SAAS,cAAc,SAAwB;AAC7C,MAAI,KAAK,WACP,MAAK,WAAW,QAAQ;;;;;;;;;;CAY5B,SAAS,sBACP,QACA,KACM;EACN,MAAM,iBAAiB,UAAU,EAAE;AAEnC,mBAAiB,eAAe;AAKhC,qBAAmB;GAAE,QAAQ;GAAgB,UAH5B,IAAI,WAAW,OAAO,GACnC,IAAI,IAAI,IAAI,CAAC,WACb,IAAI,MAAM,IAAI,CAAC,MAAM;GAC8B,CAAC;;;;;;;;CAS1D,eAAe,oBACb,YACA,SAC+B;AAC/B,MAAI,KAAK,oBAAoB;GAC3B,IAAI,eAAqC;AACzC,SAAM,KAAK,mBAAmB,YAAY,OAAO,gBAAgB;IAC/D,MAAM,SAAS,MAAM,SAAS;AAC9B,mBAAe,OAAO;AACtB,WAAO,YAAY,OAAO,QAAQ;KAClC;AACF,UAAO;;EAGT,MAAM,SAAS,MAAM,SAAS;AAC9B,gBAAc,OAAO,QAAQ;AAC7B,SAAO,OAAO;;;CAIhB,SAAS,UAAU,UAAkD;AACnE,MAAI,YAAY,KAAK,UACnB,MAAK,UAAU,SAAS;;;CAK5B,SAAS,WAAW,UAA4B;AAC9C,MAAI,KAAK,WACP,MAAK,WAAW,SAAS;MAEzB,WAAU;;;;;;CAQd,eAAe,uBACb,KACA,SACsB;EAGtB,MAAM,aAAa,cAAc,QAAQ,IAAI;EAC7C,IAAI,SAAkC,aAClC;GACE,SAAS,WAAW;GACpB,cAAc,WAAW;GACzB,aAAa,WAAW,eAAe;GACvC,QAAQ,WAAW,UAAU;GAC9B,GACD,KAAA;AAEJ,MAAI,WAAW,KAAA,GAAW;GAGxB,MAAM,YAAY,aAAa,oBAAoB;GACnD,MAAM,gBAAgB,KAAK,eAAe;AAI1C,YAAS,MAAM,gBAAgB,KAAK,MAAM,WAHvB,cAAc,WAAW,OAAO,GAC/C,IAAI,IAAI,cAAc,CAAC,WACvB,IAAI,IAAI,eAAe,mBAAmB,CAAC,SACiB;;AAIlE,MAAI,QAAQ,QACV,MAAK,aAAa;GAAE,QAAQ;GAAM,SAAS;GAAG,EAAE,IAAI,IAAI;MAExD,MAAK,UAAU;GAAE,QAAQ;GAAM,SAAS;GAAG,EAAE,IAAI,IAAI;AAIvD,eAAa,KAAK,KAAK;GACrB,SAAS,OAAO;GAChB,cAAc,OAAO;GACrB,QAAQ,OAAO;GAChB,CAAC;AAGF,qBAAmB,OAAO,YAAY;AAGtC,wBAAsB,OAAO,QAAQ,IAAI;AAEzC,SAAO;;CAGT,eAAe,SAAS,KAAa,UAA6B,EAAE,EAAiB;EACnF,MAAM,SAAS,QAAQ,WAAW;EAClC,MAAM,UAAU,QAAQ,YAAY;EAGpC,MAAM,iBAAiB,KAAK,YAAY;AAKxC,OAAK,aAAa;GAAE,QAAQ;GAAM,SAAS;GAAgB,EAAE,IAAI,KAAK,eAAe,CAAC;AAEtF,aAAW,MAAM,IAAI;AAErB,MAAI;AAMF,aALqB,MAAM,oBAAoB,WAC7C,uBAAuB,KAAK,EAAE,SAAS,CAAC,CACzC,CAGsB;AAGvB,UAAO,cAAc,IAAI,MAAM,wBAAwB,CAAC;AAKxD,oBAAiB;AACf,QAAI,OACF,MAAK,SAAS,GAAG,EAAE;QAEnB,MAAK,SAAS,GAAG,eAAe;AAElC,WAAO,cAAc,IAAI,MAAM,yBAAyB,CAAC;KACzD;WACK,OAAO;AAEd,OAAI,iBAAiB,eAAe;AAClC,eAAW,MAAM;AACjB,UAAM,SAAS,MAAM,aAAa,EAAE,SAAS,MAAM,CAAC;AACpD;;AAGF,OAAI,aAAa,MAAM,CAAE;AACzB,SAAM;YACE;AACR,cAAW,MAAM;;;CAIrB,eAAe,UAAyB;EACtC,MAAM,aAAa,KAAK,eAAe;AAEvC,aAAW,MAAM,WAAW;AAE5B,MAAI;AAcF,aAbqB,MAAM,oBAAoB,YAAY,YAAY;IAErE,MAAM,SAAS,MAAM,gBAAgB,YAAY,KAAK;AACtD,iBAAa,KAAK,YAAY;KAC5B,SAAS,OAAO;KAChB,cAAc,OAAO;KACrB,QAAQ,OAAO;KAChB,CAAC;AACF,uBAAmB,OAAO,YAAY;AACtC,0BAAsB,OAAO,QAAQ,WAAW;AAChD,WAAO;KACP,CAEqB;YACf;AACR,cAAW,MAAM;;;CAIrB,eAAe,eAAe,KAAa,UAAkB,GAAkB;EAI7E,MAAM,QAAQ,aAAa,IAAI,IAAI;AAEnC,MAAI,SAAS,MAAM,YAAY,MAAM;AAEnC,yBAAsB,MAAM,QAAQ,IAAI;AACxC,iBAAc,MAAM,QAAQ;AAC5B,aAAU,MAAM,aAAa;AAC7B,oBAAiB;AACf,SAAK,SAAS,GAAG,QAAQ;AACzB,WAAO,cAAc,IAAI,MAAM,yBAAyB,CAAC;KACzD;SACG;AAKL,cAAW,MAAM,IAAI;AACrB,OAAI;AAcF,cAbqB,MAAM,oBAAoB,KAAK,YAAY;KAE9D,MAAM,SAAS,MAAM,gBAAgB,KAAK,MADxB,aAAa,oBAAoB,CACO;AAC1D,wBAAmB,OAAO,YAAY;AACtC,2BAAsB,OAAO,QAAQ,IAAI;AACzC,kBAAa,KAAK,KAAK;MACrB,SAAS,OAAO;MAChB,cAAc,OAAO;MACrB,QAAQ,OAAO;MAChB,CAAC;AACF,YAAO;MACP,CAEqB;AACvB,qBAAiB;AACf,UAAK,SAAS,GAAG,QAAQ;AACzB,YAAO,cAAc,IAAI,MAAM,yBAAyB,CAAC;MACzD;aACM;AACR,eAAW,MAAM;;;;;;;;CASvB,SAAS,SAAS,KAAmB;AAEnC,MAAI,cAAc,IAAI,IAAI,KAAK,KAAA,EAAW;AAC1C,MAAI,aAAa,IAAI,IAAI,CAAE;AAItB,kBAAgB,KAAK,MADR,aAAa,oBAAoB,CACT,CAAC,MACxC,WAAW;AACV,iBAAc,IAAI,KAAK,OAAO;WAE1B,GAGP;;AAGH,QAAO;EACL;EACA;EACA;EACA,iBAAiB;EACjB,qBAAqB;EACrB,gBAAgB,UAAU;AACxB,oBAAiB,IAAI,SAAS;AAC9B,gBAAa,iBAAiB,OAAO,SAAS;;EAEhD;EACA,kBAAkB,SAAkB,cAA0C;GAI5E,MAAM,aAAa,KAAK,eAAe;AACvC,gBAAa,KAAK,YAAY;IAC5B,SAAS;IACT;IACD,CAAC;AACF,iBAAc,QAAQ;AACtB,aAAU,aAAa;;EAEzB,mBAAmB,aAA4B,mBAAmB,SAAS;EAC3E;EACA;EACA;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACvlBH,SAAgB,uBAAgC;AAG9C,QAFmB,yBAAyB,KAEtB;;;;;;;ACtBxB,SAAgB,gBAAgB,QAA8B;AAC5D,kBAAiB,OAAO;;;;;;AAO1B,SAAgB,YAA4B;AAC1C,KAAI,CAAC,aACH,OAAM,IAAI,MAAM,8EAA8E;AAEhG,QAAO;;;;;;;AAQT,SAAgB,kBAAyC;AACvD,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACwBT,SAAgB,YAA+B;AAC7C,QAAO;EACL,KAAK,MAAc,SAAgC;GACjD,MAAM,SAAS,iBAAiB;AAChC,OAAI,CAAC,QAAQ;AACX,QAAA,QAAA,IAAA,aAA6B,cAC3B,SAAQ,MAAM,sGAAsG;AAEtH;;AAEG,UAAO,SAAS,MAAM,EAAE,QAAQ,SAAS,QAAQ,CAAC;;EAEzD,QAAQ,MAAc,SAAgC;GACpD,MAAM,SAAS,iBAAiB;AAChC,OAAI,CAAC,QAAQ;AACX,QAAA,QAAA,IAAA,aAA6B,cAC3B,SAAQ,MAAM,uEAAuE;AAEvF;;AAEG,UAAO,SAAS,MAAM;IAAE,QAAQ,SAAS;IAAQ,SAAS;IAAM,CAAC;;EAExE,UAAU;GACR,MAAM,SAAS,iBAAiB;AAChC,OAAI,CAAC,QAAQ;AACX,QAAA,QAAA,IAAA,aAA6B,cAC3B,SAAQ,MAAM,uEAAuE;AAEvF;;AAEG,UAAO,SAAS;;EAEvB,OAAO;AACL,OAAI,OAAO,WAAW,YAAa,QAAO,QAAQ,MAAM;;EAE1D,UAAU;AACR,OAAI,OAAO,WAAW,YAAa,QAAO,QAAQ,SAAS;;EAE7D,SAAS,MAAc;GACrB,MAAM,SAAS,iBAAiB;AAChC,OAAI,CAAC,OAAQ;AACb,UAAO,SAAS,KAAK;;EAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACzEH,SAAgB,cAAsB;AAGpC,KAAI;EACF,MAAM,aAAa,sBAAsB;AACzC,MAAI,eAAe,KACjB,QAAO,WAAW;SAEd;CAMR,MAAM,UAAU,YAAY;AAC5B,KAAI,QAAS,QAAO,QAAQ,YAAY;AAGxC,KAAI,OAAO,WAAW,YAAa,QAAO,OAAO,SAAS;AAC1D,QAAO;;;;;;;;;;;;;;;;;;;;;;;;ACvBT,SAAS,YAAoB;AAC3B,KAAI,OAAO,WAAW,YAAa,QAAO,OAAO,SAAS;CAC1D,MAAM,OAAO,YAAY;AACzB,KAAI,CAAC,KAAM,QAAO;CAElB,MAAM,MADK,IAAI,gBAAgB,KAAK,aAAa,CAClC,UAAU;AACzB,QAAO,MAAM,IAAI,QAAQ;;AAG3B,SAAS,kBAA0B;CACjC,MAAM,OAAO,YAAY;AACzB,KAAI,CAAC,KAAM,QAAO;CAElB,MAAM,MADK,IAAI,gBAAgB,KAAK,aAAa,CAClC,UAAU;AACzB,QAAO,MAAM,IAAI,QAAQ;;AAG3B,SAAS,UAAU,UAAkC;AACnD,QAAO,iBAAiB,YAAY,SAAS;AAC7C,cAAa,OAAO,oBAAoB,YAAY,SAAS;;AAO/D,SAAS,kBAAmC;CAC1C,MAAM,SAAS,WAAW;AAC1B,KAAI,WAAW,cAAc;EAC3B,MAAM,SAAS,IAAI,gBAAgB,OAAO;AAC1C,mBAAiB,QAAQ,OAAO;AAChC,SAAO;;AAET,QAAO;;AAGT,SAAS,wBAAyC;CAChD,MAAM,OAAO,YAAY;AACzB,QAAO,OAAO,IAAI,gBAAgB,KAAK,aAAa,GAAG,IAAI,iBAAiB;;;;;;;AAQ9E,SAAgB,kBAAmC;AAIjD,sBAAqB,WAAW,WAAW,gBAAgB;AAC3D,QAAO,OAAO,WAAW,cAAc,iBAAiB,GAAG,uBAAuB;;;;;;;;;;;;;;;;AChDpF,IAAM,iBAAiB,cAA0C,KAAK;;AAGtE,SAAgB,oBAAgD;AAC9D,QAAO,WAAW,eAAe;;;;;;AAenC,SAAgB,gBAAgB,EAAE,UAAU,mBAAmB,YAAkC;CAC/F,MAAM,QAAQ,eACL;EAAE;EAAU;EAAmB,GAItC,CAAC,SAAS,KAAK,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC,CAClD;AACD,QAAO,cAAc,eAAe,UAAU,EAAE,OAAO,EAAE,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACtBpE,SAAgB,mBAAmB,UAA4B;AAC7D,QAAO,SAAS,MAAM,IAAI;;;;;;;;;;AAW5B,SAAgB,mBACd,iBACA,UACe;CACf,MAAM,cAAc,mBAAmB,SAAS;AAEhD,KAAI,CAAC,gBACH,QAAO,YAAY,MAAM;AAI3B,QAAO,YADO,gBAAgB,WACD;;;;;;;;;AAU/B,SAAgB,oBAAoB,iBAAkC,UAA4B;CAChG,MAAM,cAAc,mBAAmB,SAAS;AAEhD,KAAI,CAAC,gBACH,QAAO,YAAY,MAAM,EAAE,CAAC,OAAO,QAAQ;CAG7C,MAAM,QAAQ,gBAAgB;AAC9B,QAAO,YAAY,MAAM,MAAM,CAAC,OAAO,QAAQ;;;;;;;;;;;;AAajD,SAAgB,yBAAyB,kBAA0C;CAEjF,MAAM,UAAU,mBAAmB;CACnC,MAAM,WAAW,aAAa;AAC9B,QAAO,mBAAmB,SAAS,YAAY,MAAM,SAAS;;;;;;;;;;;;AAahE,SAAgB,0BAA0B,kBAAqC;CAE7E,MAAM,UAAU,mBAAmB;CACnC,MAAM,WAAW,aAAa;AAC9B,QAAO,oBAAoB,SAAS,YAAY,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxCjE,SAAgB,eACd,QACA,cACA,WAC6B;AAG7B,QAAO,iBAAoB,QAAQ,cAA4C,UAAU;;;;;;;;;;;;;;AAiB3F,SAAgB,cACd,QAC8D;CAC9D,MAAM,CAAC,WAAW,mBAAmB,eAAe;CAEpD,MAAM,WAAW,UAAkD;AACjE,SAAO,IAAI,SAAS,YAAY;AAC9B,mBAAgB,YAAY;AAE1B,YADe,MAAM,OAAO,MAAM,CACnB;KACf;IACF;;AAGJ,QAAO,CAAC,SAAS,UAAU;;;;;;;;;;;;;;;;;;;;;;;AAwC7B,SAAgB,cACd,QAOkB;CAClB,MAAM,QAA0B;EAC9B,aAAa,EAAE;EACf,YAAY,EAAE;EACd,aAAa;EACb,WAAW;EACX,qBAAqB;EACtB;AAED,KAAI,CAAC,OAAQ,QAAO;CAEpB,MAAM,mBAAmB,OAAO;CAChC,MAAM,cAAc,OAAO;AAI3B,KAAI,CAAC,oBAAoB,CAAC,YAAa,QAAO;CAG9C,MAAM,cAAwC,EAAE;CAChD,MAAM,aAAuB,EAAE;AAE/B,KAAI,iBACF,MAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,iBAAiB,CAC5D,KAAI,QAAQ,QACV,YAAW,KAAK,GAAG,SAAS;KAE5B,aAAY,OAAO;CAKzB,MAAM,YACJ,OAAO,KAAK,YAAY,CAAC,SAAS,KAAK,WAAW,SAAS,KAAK,eAAe;AAEjF,QAAO;EACL;EACA;EACA,aAAa,eAAe;EAC5B;EACA,cAAc,OAA8B;GAC1C,MAAM,OAAO,YAAY;AACzB,UAAO,QAAQ,KAAK,SAAS,IAAI,KAAK,KAAK;;EAE9C"}
@@ -1 +1 @@
1
- {"version":3,"file":"navigation-context.d.ts","sourceRoot":"","sources":["../../src/client/navigation-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAM7D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAsBD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,eAAe,GAAG,IAAI,CAM7D;AAMD,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CAOnG;AAiBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAE/D;AAED,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AA2BD;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAKvD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,GAAG,KAAK,CAAC,YAAY,CAMrB"}
1
+ {"version":3,"file":"navigation-context.d.ts","sourceRoot":"","sources":["../../src/client/navigation-context.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAM7D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAsBD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,eAAe,GAAG,IAAI,CAM7D;AAMD,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CAOnG;AAiBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAE/D;AAED,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AA2BD;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAKvD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,GAAG,KAAK,CAAC,YAAY,CAMrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -288,10 +288,15 @@ export function generateNitroEntry(
288
288
  : await handler(webRequest)`
289
289
  : ` const webResponse = await handler(webRequest)`;
290
290
 
291
+ // Build manifest init must be imported before the handler so that
292
+ // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.
293
+ // ESM guarantees imports are evaluated in order.
294
+ const manifestImport = hasManifestInit ? "import './_timber-manifest-init.js'\n" : '';
295
+
291
296
  return `// Generated by @timber-js/app/adapters/nitro
292
297
  // Do not edit — this file is regenerated on each build.
293
298
 
294
- import { defineEventHandler } from 'nitro/h3'
299
+ ${manifestImport}import { defineEventHandler } from 'nitro/h3'
295
300
  import handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'
296
301
  import { compressResponse } from './_compress.mjs'
297
302
 
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  /**
2
4
  * NavigationContext — React context for navigation state.
3
5
  *