@timber-js/app 0.2.0-alpha.121 → 0.2.0-alpha.123

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.
Files changed (137) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{actions-Dy8vOjFt.js → actions-ZlcKx0H9.js} +5 -5
  3. package/dist/_chunks/actions-ZlcKx0H9.js.map +1 -0
  4. package/dist/_chunks/{build-output-helper-B1n0sUqj.js → build-output-helper-DYOWsPjZ.js} +47 -6
  5. package/dist/_chunks/build-output-helper-DYOWsPjZ.js.map +1 -0
  6. package/dist/_chunks/cli-schema-sync-4HcQIziG.js +2 -0
  7. package/dist/_chunks/{cli-schema-sync-ByZPGGmS.js → cli-schema-sync-VcCf1TFY.js} +17 -3
  8. package/dist/_chunks/{cli-schema-sync-ByZPGGmS.js.map → cli-schema-sync-VcCf1TFY.js.map} +1 -1
  9. package/dist/_chunks/{define-CPVQaWbx.js → define-C0V_eKiW.js} +6 -18
  10. package/dist/_chunks/define-C0V_eKiW.js.map +1 -0
  11. package/dist/_chunks/{handler-store-CcwNX-gk.js → handler-store-bIAstrxQ.js} +1 -1
  12. package/dist/_chunks/{handler-store-CcwNX-gk.js.map → handler-store-bIAstrxQ.js.map} +1 -1
  13. package/dist/_chunks/{merge-search-params-az6ZiBeU.js → merge-search-params-Cm1j99HC.js} +1 -1
  14. package/dist/_chunks/{merge-search-params-az6ZiBeU.js.map → merge-search-params-Cm1j99HC.js.map} +1 -1
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-DESG8uk5.js} +32 -19
  16. package/dist/_chunks/metadata-routes-DESG8uk5.js.map +1 -0
  17. package/dist/_chunks/{navigation-root-DoRTeBm5.js → navigation-root-Ds_hDtRW.js} +2 -2
  18. package/dist/_chunks/{navigation-root-DoRTeBm5.js.map → navigation-root-Ds_hDtRW.js.map} +1 -1
  19. package/dist/_chunks/{resolve-schema-Bf8eC6q6.js → resolve-schema-DU2nbVOi.js} +5 -1
  20. package/dist/_chunks/resolve-schema-DU2nbVOi.js.map +1 -0
  21. package/dist/_chunks/{router-ref-CpvjBqdu.js → router-ref-B2-2kIIC.js} +2 -2
  22. package/dist/_chunks/{router-ref-CpvjBqdu.js.map → router-ref-B2-2kIIC.js.map} +1 -1
  23. package/dist/_chunks/{segment-classify-CnfLLvir.js → segment-classify-BjfuctV2.js} +1 -1
  24. package/dist/_chunks/{segment-classify-CnfLLvir.js.map → segment-classify-BjfuctV2.js.map} +1 -1
  25. package/dist/_chunks/{segment-context-EsfQpS--.js → segment-context-Bg0pFbLT.js} +1 -1
  26. package/dist/_chunks/{segment-context-EsfQpS--.js.map → segment-context-Bg0pFbLT.js.map} +1 -1
  27. package/dist/_chunks/{ssr-data-m9-cNEIw.js → ssr-data-DVX8S7vj.js} +1 -1
  28. package/dist/_chunks/{ssr-data-m9-cNEIw.js.map → ssr-data-DVX8S7vj.js.map} +1 -1
  29. package/dist/_chunks/{use-segment-params-CtIAjqK0.js → use-segment-params-Du7u25cK.js} +3 -8
  30. package/dist/_chunks/{use-segment-params-CtIAjqK0.js.map → use-segment-params-Du7u25cK.js.map} +1 -1
  31. package/dist/_chunks/{validation-BMU5MPzP.js → validation-AfEFM5ze.js} +1 -1
  32. package/dist/_chunks/{validation-BMU5MPzP.js.map → validation-AfEFM5ze.js.map} +1 -1
  33. package/dist/_chunks/{walkers-DNp9tHyV.js → walkers-tkw7sDlj.js} +12 -9
  34. package/dist/_chunks/walkers-tkw7sDlj.js.map +1 -0
  35. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  36. package/dist/adapters/cloudflare.js +1 -1
  37. package/dist/adapters/nitro.js +1 -1
  38. package/dist/cache/index.js +1 -1
  39. package/dist/cli-schema-sync.d.ts.map +1 -1
  40. package/dist/cli.js +1 -1
  41. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  42. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  43. package/dist/client/error-boundary.js +2 -2
  44. package/dist/client/index.d.ts +1 -0
  45. package/dist/client/index.d.ts.map +1 -1
  46. package/dist/client/index.js +48 -15
  47. package/dist/client/index.js.map +1 -1
  48. package/dist/client/internal.js +67 -32
  49. package/dist/client/internal.js.map +1 -1
  50. package/dist/client/link.d.ts +9 -5
  51. package/dist/client/link.d.ts.map +1 -1
  52. package/dist/client/router.d.ts.map +1 -1
  53. package/dist/client/rsc-fetch.d.ts +8 -0
  54. package/dist/client/rsc-fetch.d.ts.map +1 -1
  55. package/dist/client/shallow-url.d.ts +10 -0
  56. package/dist/client/shallow-url.d.ts.map +1 -0
  57. package/dist/cookies/index.js +2 -2
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +102 -10
  60. package/dist/index.js.map +1 -1
  61. package/dist/params/index.js +1 -1
  62. package/dist/params/resolve-schema.d.ts.map +1 -1
  63. package/dist/plugins/build-manifest.d.ts.map +1 -1
  64. package/dist/plugins/cloudflare-wasm.d.ts +18 -0
  65. package/dist/plugins/cloudflare-wasm.d.ts.map +1 -0
  66. package/dist/plugins/dev-server.d.ts.map +1 -1
  67. package/dist/routing/codegen-shared.d.ts +5 -4
  68. package/dist/routing/codegen-shared.d.ts.map +1 -1
  69. package/dist/routing/index.js +3 -3
  70. package/dist/segment-params/define.d.ts +0 -6
  71. package/dist/segment-params/define.d.ts.map +1 -1
  72. package/dist/segment-params/index.js +1 -1
  73. package/dist/server/actions.d.ts +3 -8
  74. package/dist/server/actions.d.ts.map +1 -1
  75. package/dist/server/build-manifest.d.ts +6 -0
  76. package/dist/server/build-manifest.d.ts.map +1 -1
  77. package/dist/server/index.js +1 -1
  78. package/dist/server/internal.d.ts +1 -1
  79. package/dist/server/internal.d.ts.map +1 -1
  80. package/dist/server/internal.js +15 -6
  81. package/dist/server/internal.js.map +1 -1
  82. package/dist/server/metadata-routes.d.ts +23 -7
  83. package/dist/server/metadata-routes.d.ts.map +1 -1
  84. package/dist/server/pipeline-phases.d.ts.map +1 -1
  85. package/dist/server/request-context.d.ts.map +1 -1
  86. package/dist/server/route-element-builder.d.ts +1 -1
  87. package/dist/server/route-element-builder.d.ts.map +1 -1
  88. package/dist/server/route-matcher.d.ts +4 -2
  89. package/dist/server/route-matcher.d.ts.map +1 -1
  90. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  91. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  92. package/dist/server/ssr-entry.d.ts.map +1 -1
  93. package/dist/server/tree-match.d.ts +12 -4
  94. package/dist/server/tree-match.d.ts.map +1 -1
  95. package/dist/server/types.d.ts +2 -2
  96. package/package.json +6 -7
  97. package/src/adapters/build-output-helper.ts +67 -5
  98. package/src/cli-schema-sync.ts +25 -4
  99. package/src/cli.ts +0 -0
  100. package/src/client/browser-entry/action-dispatch.ts +9 -10
  101. package/src/client/browser-entry/index.ts +1 -1
  102. package/src/client/browser-entry/post-hydration.ts +12 -6
  103. package/src/client/index.ts +1 -5
  104. package/src/client/link.tsx +34 -21
  105. package/src/client/router.ts +74 -25
  106. package/src/client/rsc-fetch.ts +20 -5
  107. package/src/client/shallow-url.ts +22 -0
  108. package/src/index.ts +2 -0
  109. package/src/params/resolve-schema.ts +15 -0
  110. package/src/plugins/build-manifest.ts +57 -5
  111. package/src/plugins/cloudflare-wasm.ts +103 -0
  112. package/src/plugins/dev-server.ts +4 -2
  113. package/src/plugins/routing.ts +1 -1
  114. package/src/routing/codegen-shared.ts +8 -5
  115. package/src/routing/codegen.ts +5 -0
  116. package/src/routing/link-codegen.ts +1 -1
  117. package/src/segment-params/define.ts +2 -29
  118. package/src/server/actions.ts +4 -14
  119. package/src/server/build-manifest.ts +7 -0
  120. package/src/server/internal.ts +7 -1
  121. package/src/server/metadata-routes.ts +44 -19
  122. package/src/server/pipeline-phases.ts +8 -0
  123. package/src/server/request-context.ts +0 -4
  124. package/src/server/route-element-builder.ts +57 -11
  125. package/src/server/route-matcher.ts +81 -49
  126. package/src/server/rsc-entry/index.ts +7 -1
  127. package/src/server/rsc-entry/render-route.ts +7 -1
  128. package/src/server/ssr-entry.ts +6 -0
  129. package/src/server/tree-match.ts +23 -11
  130. package/src/server/types.ts +2 -2
  131. package/dist/_chunks/actions-Dy8vOjFt.js.map +0 -1
  132. package/dist/_chunks/build-output-helper-B1n0sUqj.js.map +0 -1
  133. package/dist/_chunks/cli-schema-sync-DHQLjFu3.js +0 -2
  134. package/dist/_chunks/define-CPVQaWbx.js.map +0 -1
  135. package/dist/_chunks/metadata-routes-Cjmvi3rQ.js.map +0 -1
  136. package/dist/_chunks/resolve-schema-Bf8eC6q6.js.map +0 -1
  137. package/dist/_chunks/walkers-DNp9tHyV.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"actions-Dy8vOjFt.js","names":[],"sources":["../../src/server/cookie-parsing.ts","../../src/server/cookie-context.ts","../../src/server/request-context.ts","../../src/server/waituntil-bridge.ts","../../src/server/primitives.ts","../../src/server/form-data.ts","../../src/server/actions.ts"],"sourcesContent":["/**\n * Cookie parsing and serialization helpers — pure string ↔ structure\n * functions with no ALS dependency. Split out of `cookie-context.ts`\n * (TIM-853) so the API surface and the wire-format codecs can each be\n * read on their own.\n *\n * The functions in this module are total over arbitrary input. They\n * never throw and never call `assertValid*` (the security validators\n * live in the API surface — `cookie-context.ts` invokes them at every\n * jar entry point so the smuggling-primitive invariant from TIM-868\n * is enforced regardless of which path produced the bytes).\n */\n\nimport type { CookieEntry } from './als-registry.js';\nimport type { CookieOptions } from './cookie-context.js';\n\n/**\n * Parse a Cookie header string into a Map of name → value pairs.\n * Follows RFC 6265 §4.2.1: cookies are semicolon-separated key=value pairs.\n *\n * Values are auto-decoded with `decodeURIComponent` so they round-trip\n * losslessly with `getCookies().set()` (which auto-encodes). Malformed\n * `%`-escapes from third-party cookies fall back to the raw byte sequence\n * — the parser must be total over arbitrary inbound headers, including\n * non-conforming values from other servers, browser extensions, etc.\n */\nexport function parseCookieHeader(header: string): Map<string, string> {\n const map = new Map<string, string>();\n if (!header) return map;\n\n for (const pair of header.split(';')) {\n const eqIndex = pair.indexOf('=');\n if (eqIndex === -1) continue;\n const name = pair.slice(0, eqIndex).trim();\n const value = pair.slice(eqIndex + 1).trim();\n if (name) {\n map.set(name, safeDecodeCookieValue(value));\n }\n }\n\n return map;\n}\n\n/**\n * Decode a single cookie value with `decodeURIComponent`, falling back to\n * the raw byte sequence if the input contains a malformed `%`-escape.\n *\n * Used by both `parseCookieHeader` (incoming Cookie: header) and the\n * `setRaw` forwarding path (outgoing Set-Cookie from upstream services).\n * Total — never throws.\n */\nexport function safeDecodeCookieValue(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\n}\n\n/** Serialize a CookieEntry into a Set-Cookie header value. */\nexport function serializeCookieEntry(entry: CookieEntry): string {\n const parts = [`${entry.name}=${entry.value}`];\n const opts = entry.options;\n\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.path) parts.push(`Path=${opts.path}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.httpOnly) parts.push('HttpOnly');\n if (opts.secure) parts.push('Secure');\n if (opts.sameSite) {\n parts.push(`SameSite=${opts.sameSite.charAt(0).toUpperCase()}${opts.sameSite.slice(1)}`);\n }\n if (opts.partitioned) parts.push('Partitioned');\n\n return parts.join('; ');\n}\n\n/**\n * Parse a raw `Set-Cookie` header string into name, value, and options.\n * Handles all standard attributes: Path, Domain, Max-Age, Expires,\n * SameSite, Secure, HttpOnly, Partitioned.\n *\n * Does NOT apply DEFAULT_COOKIE_OPTIONS — the caller decides whether\n * to merge defaults (e.g. `set()` does, but `setRaw()` should preserve\n * the original header's intent).\n */\nexport function parseSetCookie(\n header: string\n): { name: string; value: string; options: CookieOptions } | null {\n const segments = header.split(';');\n const nameValue = segments[0];\n const eqIdx = nameValue.indexOf('=');\n if (eqIdx <= 0) return null;\n\n const name = nameValue.slice(0, eqIdx).trim();\n const value = nameValue.slice(eqIdx + 1).trim();\n const options: CookieOptions = {};\n\n for (let i = 1; i < segments.length; i++) {\n const seg = segments[i].trim();\n if (!seg) continue;\n const [attrName, ...rest] = seg.split('=');\n const key = attrName.trim().toLowerCase();\n const val = rest.join('=').trim();\n switch (key) {\n case 'path':\n options.path = val || '/';\n break;\n case 'domain':\n options.domain = val;\n break;\n case 'max-age':\n options.maxAge = Number(val);\n break;\n case 'expires':\n options.expires = new Date(val);\n break;\n case 'samesite':\n options.sameSite = val.toLowerCase() as 'strict' | 'lax' | 'none';\n break;\n case 'secure':\n options.secure = true;\n break;\n case 'httponly':\n options.httpOnly = true;\n break;\n case 'partitioned':\n options.partitioned = true;\n break;\n }\n }\n\n return { name, value, options };\n}\n","/**\n * Cookie Context — per-request cookie API and on-the-wire helpers.\n *\n * Split out of `request-context.ts` (TIM-853) so the cookie subsystem\n * — encoding contract, options grammar, parser, serializer, RYW map,\n * and rerender seed — lives in one file. The headers/scope/params APIs\n * stay in `request-context.ts` and call into this module via the\n * exported helpers.\n *\n * See design/29-cookies.md for the encoding contract and read-your-own-\n * writes semantics. See ONGOING_SECURITY.md H-3 (TIM-868) for the\n * smuggling primitive that the encoding contract closes.\n */\n\nimport { requestContextAls, type RequestContextStore } from './als-registry.js';\nimport { isDebug } from './debug.js';\nimport { assertValidCookieName, assertValidCookieValue } from '../cookies/validation.js';\nimport {\n parseCookieHeader,\n parseSetCookie,\n safeDecodeCookieValue,\n serializeCookieEntry,\n} from './cookie-parsing.js';\n\n// Re-export the validators so framework-internal consumers and tests can\n// import the canonical implementation from the same module that hosts\n// `getCookies()`. The pure shared module lives in `cookies/validation.ts`\n// so the client `useCookie` hook can use the same checks without pulling\n// in the server ALS code.\nexport { assertValidCookieName, assertValidCookieValue };\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns a cookie accessor for the current request.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n *\n * Read methods (.get, .has, .getAll) are always available and reflect\n * read-your-own-writes from .set() calls in the same request.\n *\n * Mutation methods (.set, .delete, .clear) are only available in mutable\n * contexts (middleware.ts, server actions, route.ts handlers). Calling them\n * in read-only contexts (access.ts, server components) throws.\n *\n * This is the escape hatch for direct cookie jar operations. For typed\n * cookie access, use `defineCookie()` instead.\n *\n * See design/29-cookies.md\n */\nexport function getCookieJar(): RequestCookies {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getCookieJar() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n\n // Parse cookies lazily on first access\n if (!store.parsedCookies) {\n store.parsedCookies = parseCookieHeader(store.cookieHeader);\n }\n\n const map = store.parsedCookies;\n return {\n get(name: string): string | undefined {\n return map.get(name);\n },\n has(name: string): boolean {\n return map.has(name);\n },\n getAll(): Array<{ name: string; value: string }> {\n return Array.from(map.entries()).map(([name, value]) => ({ name, value }));\n },\n get size(): number {\n return map.size;\n },\n\n set(name: string, value: string, options?: SetCookieOptions): void {\n assertMutable(store, 'set');\n // Validate the name first — names cannot be URL-encoded (RFC 7230\n // token grammar is strict), so a bad name is always a bug.\n assertValidCookieName(name);\n // Type guard with a guiding error. The single most common mistake\n // is passing a non-string (object, number, Date) and expecting the\n // framework to JSON-encode. Point developers at jsonCookieCodec.\n if (typeof value !== 'string') {\n throw new Error(\n `[timber] getCookieJar().set(${JSON.stringify(name)}, …): value must be a string, got ${typeof value}.\\n` +\n ` To store a JSON-serializable value, use defineCookie + jsonCookieCodec:\\n` +\n `\\n` +\n ` import { defineCookie, jsonCookieCodec } from '@timber-js/app/cookies';\\n` +\n `\\n` +\n ` export const ${name}Cookie = defineCookie(${JSON.stringify(name)}, {\\n` +\n ` codec: jsonCookieCodec(),\\n` +\n ` });\\n` +\n `\\n` +\n ` ${name}Cookie.set(value);\\n` +\n `\\n` +\n ` See design/29-cookies.md §\"Typed Cookies with Schema Validation\".`\n );\n }\n // Encode the value so the on-the-wire bytes always satisfy\n // RFC 6265 §4.1.1 cookie-octet. encodeURIComponent's output is a\n // strict subset of cookie-octet (only `A-Z a-z 0-9 ! ' ( ) * - . _\n // ~ %`), so the encoded form can never carry the H-3 smuggling\n // primitive — `;` becomes `%3B`, CR/LF become `%0D`/`%0A`, etc.\n // The round-trip is lossless: parseCookieHeader auto-decodes on\n // read, so `cookies().get(name)` returns exactly `value`. See\n // ONGOING_SECURITY.md H-3 (TIM-868) and design/29-cookies.md\n // §\"Encoding Contract\".\n //\n // The `{ raw: true }` opt-out skips the encoder for callers who\n // need exact byte control (e.g. forwarding pre-encoded cookies\n // from an upstream service). The opt-out goes through the strict\n // cookie-octet validator instead — the smuggling primitive cannot\n // sneak in via the escape hatch.\n const raw = options?.raw === true;\n const wireValue = raw ? value : encodeURIComponent(value);\n if (raw) {\n assertValidCookieValue(name, wireValue);\n }\n if (store.flushed) {\n if (isDebug()) {\n console.warn(\n `[timber] warn: getCookieJar().set('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be sent. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n // Strip the framework-only `raw` flag before persisting — it is\n // not an HTTP cookie attribute and must not leak into the jar.\n const { raw: _raw, ...attributeOptions } = options ?? {};\n void _raw;\n const opts = { ...DEFAULT_COOKIE_OPTIONS, ...attributeOptions };\n store.cookieJar.set(name, { name, value: wireValue, options: opts });\n // Read-your-own-writes: store the DECODED logical value so that\n // subsequent `cookies().get(name)` in the same request returns\n // exactly what the developer wrote — never the encoded form.\n // For `{ raw: true }`, the wire form IS the logical form.\n map.set(name, raw ? wireValue : value);\n },\n\n setFromHeaders(headers: Headers): void {\n assertMutable(store, 'setFromHeaders');\n if (store.flushed) {\n console.warn(\n `[timber] warn: getCookieJar().setFromHeaders() called after response headers were committed.\\n` +\n ` The cookies will NOT be sent. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n return;\n }\n // Headers.getSetCookie() returns individual Set-Cookie strings,\n // avoiding the fragile comma-splitting that raw .get() requires.\n for (const raw of headers.getSetCookie()) {\n const parsed = parseSetCookie(raw);\n if (parsed) {\n // Use setRaw to preserve the original header's attributes without\n // merging DEFAULT_COOKIE_OPTIONS (parseSetCookie intentionally\n // does not apply defaults — see its doc comment).\n setRaw(store, map, parsed.name, parsed.value, parsed.options);\n }\n }\n },\n\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void {\n assertMutable(store, 'delete');\n // Validate the name even though delete writes an empty value — a\n // smuggled name (`;` or CR/LF) would corrupt the Set-Cookie header\n // we emit. See ONGOING_SECURITY.md H-3 (TIM-868).\n assertValidCookieName(name);\n if (store.flushed) {\n if (isDebug()) {\n console.warn(\n `[timber] warn: getCookieJar().delete('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be deleted. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n const opts: CookieOptions = {\n ...DEFAULT_COOKIE_OPTIONS,\n ...options,\n maxAge: 0,\n expires: new Date(0),\n };\n store.cookieJar.set(name, { name, value: '', options: opts });\n // Remove from read view\n map.delete(name);\n },\n\n clear(): void {\n assertMutable(store, 'clear');\n if (store.flushed) return;\n // Delete every incoming cookie\n for (const name of Array.from(map.keys())) {\n store.cookieJar.set(name, {\n name,\n value: '',\n options: { ...DEFAULT_COOKIE_OPTIONS, maxAge: 0, expires: new Date(0) },\n });\n }\n map.clear();\n },\n\n toString(): string {\n // Re-encode values when serializing as a Cookie header — the\n // RYW map holds decoded logical values, but a Cookie header has\n // to satisfy `cookie-octet`. Mirror the auto-encode contract on\n // `set()` so toString() round-trips losslessly with parseCookieHeader.\n return Array.from(map.entries())\n .map(([name, value]) => `${name}=${encodeURIComponent(value)}`)\n .join('; ');\n },\n };\n}\n\n/**\n * Returns the value of a single cookie, or undefined if absent.\n *\n * @internal — not part of the public API. Use `defineCookie().get()` or `getCookieJar().get()` instead.\n */\nexport function getCookie(name: string): string | undefined {\n const jar = getCookieJar();\n return jar.get(name);\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\n/**\n * Per-call options for `getCookies().set()`. Extends the persistent\n * `CookieOptions` (HTTP cookie attributes) with framework-only flags\n * that are NOT serialized into the Set-Cookie header.\n *\n * The `raw` flag is the escape hatch for the auto-encoding contract.\n * See design/29-cookies.md §\"Encoding Contract\".\n */\nexport interface SetCookieOptions extends CookieOptions {\n /**\n * Skip the framework's `encodeURIComponent` pass and store the value\n * verbatim. The value is then validated against the strict RFC 6265\n * §4.1.1 `cookie-octet` grammar — the H-3 smuggling primitive cannot\n * sneak in via this opt-out.\n *\n * Use this when forwarding a cookie value that is already in its\n * intended on-the-wire form (e.g. mirroring an upstream service's\n * Set-Cookie). Default: `false` (auto-encode).\n */\n raw?: boolean;\n}\n\n/** Options for setting a cookie. See design/29-cookies.md. */\nexport interface CookieOptions {\n /** Domain scope. Default: omitted (current domain only). */\n domain?: string;\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Expiration date. Mutually exclusive with maxAge. */\n expires?: Date;\n /** Max age in seconds. Mutually exclusive with expires. */\n maxAge?: number;\n /** Prevent client-side JS access. Default: true. */\n httpOnly?: boolean;\n /** Only send over HTTPS. Default: true. */\n secure?: boolean;\n /** Cross-site request policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Partitioned (CHIPS) — isolate cookie per top-level site. Default: false. */\n partitioned?: boolean;\n}\n\n/**\n * Cookie accessor returned by `getCookies()`.\n *\n * Read methods are always available. Mutation methods throw in read-only\n * contexts (access.ts, server components).\n */\nexport interface RequestCookies {\n /** Get a cookie value by name. Returns undefined if not present. */\n get(name: string): string | undefined;\n /** Check if a cookie exists. */\n has(name: string): boolean;\n /** Get all cookies as an array of { name, value } pairs. */\n getAll(): Array<{ name: string; value: string }>;\n /** Number of cookies. */\n readonly size: number;\n /**\n * Set a cookie. Only available in mutable contexts (middleware, actions,\n * route handlers).\n *\n * The value is auto-encoded with `encodeURIComponent` so the on-the-wire\n * bytes always satisfy RFC 6265 §4.1.1 cookie-octet — `cookies().get()`\n * returns the same logical value the developer wrote. Pass `{ raw: true }`\n * to skip the encoder; the raw path validates against the strict\n * cookie-octet grammar instead. See design/29-cookies.md §\"Encoding\n * Contract\" and ONGOING_SECURITY.md H-3 (TIM-868).\n */\n set(name: string, value: string, options?: SetCookieOptions): void;\n /**\n * Copy all `Set-Cookie` headers from a `Headers` object.\n * Parses each header and forwards name, value, and all attributes\n * (path, domain, max-age, expires, sameSite, secure, httpOnly, partitioned).\n *\n * Useful when forwarding cookies from an internal `fetch()` or auth handler:\n * ```ts\n * const response = await auth.handler(req);\n * getCookies().then(c => c.setFromHeaders(response.headers));\n * ```\n */\n setFromHeaders(headers: Headers): void;\n /** Delete a cookie. Only available in mutable contexts. */\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void;\n /** Delete all cookies. Only available in mutable contexts. */\n clear(): void;\n /** Serialize cookies as a Cookie header string. */\n toString(): string;\n}\n\nconst DEFAULT_COOKIE_OPTIONS: CookieOptions = {\n path: '/',\n httpOnly: true,\n secure: true,\n sameSite: 'lax',\n};\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Per-request seed map of cookie name → value, registered out-of-band so the\n * pipeline's `runWithRequestContext` call can pick it up without changing\n * the function's calling convention. Stored in a WeakMap keyed by the\n * synthetic Request object built for the rerender path, so the seed lives\n * exactly as long as the request and is collected with it.\n *\n * The seed exists to eliminate the parse/serialize round-trip on the no-JS\n * form-rerender path — see ONGOING_SECURITY.md H-3 (TIM-868). The action's\n * post-mutation RYW snapshot is threaded directly into the rerender scope's\n * `parsedCookies` map, bypassing `parseCookieHeader` entirely.\n */\nconst seededRequestCookies = new WeakMap<Request, Map<string, string>>();\n\n/**\n * Register a pre-parsed cookie map to use as the request context seed for\n * the next `runWithRequestContext(req, …)` call with this exact `req`.\n *\n * Used by the no-JS form-rerender dispatcher in\n * `rsc-entry/wrap-action-dispatch.ts` to thread the action's post-mutation\n * cookie state into the rerender scope without serializing back through a\n * `Cookie:` header. See TIM-868 / TIM-837.\n *\n * @internal — framework use only.\n */\nexport function seedRequestCookies(req: Request, cookies: Map<string, string>): void {\n // Defensive copy: callers must not be able to mutate the rerender scope's\n // map after seeding, and the rerender scope must not mutate the snapshot\n // we hand back to other observers.\n seededRequestCookies.set(req, new Map(cookies));\n}\n\n/**\n * Pop the seed (if any) for `req` and return it. Called from\n * `runWithRequestContext` exactly once per request — the seed is consumed\n * eagerly so it cannot leak into a hypothetical future re-use of the same\n * Request reference.\n *\n * @internal — framework use only.\n */\nexport function consumeSeededCookies(req: Request): Map<string, string> | undefined {\n const seed = seededRequestCookies.get(req);\n if (seed) seededRequestCookies.delete(req);\n return seed;\n}\n\n/**\n * Build a Map of cookie name → value reflecting the current request's\n * read-your-own-writes state. Includes incoming cookies plus any\n * mutations from getCookies().set() / getCookies().delete() in the same request.\n *\n * Used by SSR renderers to populate NavContext.cookies so that\n * useCookie()'s server snapshot matches the actual response state.\n *\n * See design/29-cookies.md §\"Read-Your-Own-Writes\"\n * See design/triage/TIM-441-cookie-api-triage.md §4\n */\nexport function getCookiesForSsr(): Map<string, string> {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] getCookiesForSsr() called outside of a request context.');\n }\n\n // Trigger lazy parsing if not yet done\n if (!store.parsedCookies) {\n store.parsedCookies = parseCookieHeader(store.cookieHeader);\n }\n\n // The parsedCookies map already reflects read-your-own-writes:\n // - getCookies().set() updates the map via map.set(name, value)\n // - getCookies().delete() removes from the map via map.delete(name)\n // Return a copy so callers can't mutate the internal map.\n return new Map(store.parsedCookies);\n}\n\n/**\n * Collect all Set-Cookie headers from the cookie jar.\n * Called by the framework at flush time to apply cookies to the response.\n *\n * Returns an array of serialized Set-Cookie header values.\n */\nexport function getSetCookieHeaders(): string[] {\n const store = requestContextAls.getStore();\n if (!store) return [];\n return Array.from(store.cookieJar.values()).map(serializeCookieEntry);\n}\n\n// ─── Cookie Helpers ───────────────────────────────────────────────────────\n\n/** Throw if cookie mutation is attempted in a read-only context. */\nfunction assertMutable(store: RequestContextStore, method: string): void {\n if (!store.mutableContext) {\n throw new Error(\n `[timber] getCookieJar().${method}() cannot be called in this context.\\n` +\n ` Set cookies in middleware.ts, server actions, or route.ts handlers.`\n );\n }\n}\n\n/**\n * Write a cookie to the jar WITHOUT merging DEFAULT_COOKIE_OPTIONS.\n * Used by setFromHeaders to preserve the original header's attributes exactly.\n *\n * For deletion cookies (maxAge=0), the jar entry is still created so the\n * Set-Cookie header is emitted, but the cookie is NOT added to the read map\n * (it would be misleading — the cookie is being deleted).\n */\nfunction setRaw(\n store: RequestContextStore,\n readMap: Map<string, string>,\n name: string,\n value: string,\n options: CookieOptions\n): void {\n // setRaw is the forwarding path for upstream Set-Cookie headers\n // (`getCookies().setFromHeaders(response.headers)`). The value comes\n // out of `parseSetCookie` in its on-the-wire form — already encoded\n // by whoever produced it — so we re-emit it verbatim into our jar.\n // We DO validate against the strict cookie-octet grammar, both as\n // defense-in-depth against a malicious upstream and to keep the\n // ONGOING_SECURITY.md H-3 invariant intact at every entry point into\n // the cookie jar / RYW map.\n assertValidCookieName(name);\n assertValidCookieValue(name, value);\n store.cookieJar.set(name, { name, value, options });\n // Deletion cookies (Max-Age=0) should not appear in the read map.\n if (options.maxAge === 0) {\n readMap.delete(name);\n } else {\n // Decode for the RYW map so consumers see the same logical bytes\n // they would see if the upstream cookie had arrived on the next\n // request. Mirrors `parseCookieHeader`'s auto-decode.\n readMap.set(name, safeDecodeCookieValue(value));\n }\n}\n","/**\n * Request Context — per-request ALS store for headers, search params,\n * segment params, and request scope lifecycle.\n *\n * Follows the same pattern as tracing.ts: a module-level AsyncLocalStorage\n * instance, public accessor functions that throw outside request scope,\n * and a framework-internal `runWithRequestContext()` to establish scope.\n *\n * Cookie state lives in `cookie-context.ts` (split out in TIM-853). The\n * scope set up here owns the cookie jar / parsedCookies fields on the\n * store, but the cookie API and helpers are in the cookie module.\n *\n * See design/04-authorization.md §\"AccessContext does not include cookies or headers\"\n * and design/11-platform.md §\"AsyncLocalStorage\".\n */\n\nimport { requestContextAls, type RequestContextStore } from './als-registry.js';\nimport { _setGetSearchParamsFn } from '../search-params/define.js';\nimport { _setGetSegmentParamsFn } from '../segment-params/define.js';\nimport { consumeSeededCookies } from './cookie-context.js';\nimport { isDebug } from './debug.js';\n\n// Re-export the ALS for framework-internal consumers that need direct access.\nexport { requestContextAls };\n\n// No fallback needed — we use enterWith() instead of run() to ensure\n// the ALS context persists for the entire request lifecycle including\n// async stream consumption by React's renderToReadableStream.\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns a read-only view of the current request's headers.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n */\nexport function getHeaders(): ReadonlyHeaders {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getHeaders() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.headers;\n}\n\n/**\n * Returns the value of a single request header, or undefined if absent.\n *\n * Thin wrapper over `getHeaders().get(name)` for the common case where\n * you need exactly one header.\n *\n * @internal — not part of the public API. Use `getHeaders().get(name)` instead.\n */\nexport function getHeader(name: string): string | undefined {\n const headers = getHeaders();\n return headers.get(name) ?? undefined;\n}\n\n/**\n * Returns the current request's raw URLSearchParams.\n *\n * @internal — not part of the public API. Use `defineSearchParams().get()` instead.\n */\nexport function getSearchParams(): URLSearchParams {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getSearchParams() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.searchParams;\n}\n\n// Eagerly register getSearchParams with the search-params module so\n// searchParams.get() can call it without a dynamic import.\n// Dynamic imports lose ALS context in React's RSC Flight renderer,\n// breaking getSearchParams() in parallel slot pages. See TIM-523.\n_setGetSearchParamsFn(getSearchParams);\n\n// Eagerly register getSegmentParams with the segment-params module so\n// segmentParams.get() can call it without a dynamic import.\n// Same pattern as search params — dynamic imports lose ALS context. See TIM-523.\n_setGetSegmentParamsFn(getSegmentParams);\n\n/**\n * Returns the current request's coerced segment params.\n *\n * The optional `segmentPath` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value. Pass a segment path\n * (e.g. `'/(browse)/[artistSlug]/[year]'`) to narrow the return type to the\n * exact params shape for that segment.\n *\n * Without an argument, returns all coerced params with optional types.\n *\n * See design/41-global-params.md §Function Signatures\n */\nexport function getSegmentParams(segmentPath?: string): Record<string, string | string[]> {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getSegmentParams() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n if (!store.segmentParams) {\n throw new Error(\n '[timber] getSegmentParams() called before route matching completed. ' +\n 'Segment params are not available until after the route is matched.'\n );\n }\n\n // Check slot params map first — parallel slots may have different param\n // types than the main route (e.g. catch-all [..year] → string[] vs\n // dynamic [year] → string). If the requested segment path matches a\n // stored slot, return its params merged with the main route's.\n if (segmentPath && store.slotParamsMap?.has(segmentPath)) {\n const slotParams = store.slotParamsMap.get(segmentPath)!;\n // Merge: main params as base, slot-specific params override.\n // This ensures params from segments above the slot (that the slot\n // doesn't re-derive) are still available.\n return { ...store.segmentParams, ...slotParams };\n }\n\n // Dev-mode validation: warn when the segment path expects dynamic\n // segments that don't exist on the actual matched segment. This catches\n // bugs like passing a child segment's $segment to a parent layout.\n // Skip for slot paths — they're validated against their own chain.\n // See design/41-global-params.md §Runtime Validation\n if (segmentPath && isDebug() && store.matchedSegmentPath) {\n const expected = extractDynamicSegments(segmentPath);\n const actual = extractDynamicSegments(store.matchedSegmentPath);\n const missing = expected.filter((s) => !actual.includes(s));\n\n if (missing.length > 0) {\n console.warn(\n `[timber] getSegmentParams('${segmentPath}') called but current segment is '${store.matchedSegmentPath}'\\n` +\n ` Missing params: ${missing.join(', ')}\\n` +\n ` These will be undefined at runtime despite the type annotation.`\n );\n }\n }\n\n return store.segmentParams;\n}\n\n/**\n * Set the segment params on the current request context.\n * Called by the pipeline after route matching and param coercion.\n *\n * @internal — framework use only\n */\nexport function setSegmentParams(params: Record<string, string | string[]>): void {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] setSegmentParams() called outside of a request context.');\n }\n store.segmentParams = params;\n}\n\n/**\n * Store per-slot coerced segment params in the current request context.\n * Called by the route element builder when resolving parallel slots.\n *\n * @param segmentPath — The slot's full tree path (e.g. '/(browse)/@shows/[artistSlug]/[...year]')\n * @param params — The slot's coerced segment params\n * @internal — framework use only\n */\nexport function setSlotParams(\n segmentPath: string,\n params: Record<string, string | string[]>\n): void {\n const store = requestContextAls.getStore();\n if (!store) return; // non-throwing — optional diagnostic\n if (!store.slotParamsMap) store.slotParamsMap = new Map();\n store.slotParamsMap.set(segmentPath, params);\n}\n\n/**\n * Store the matched segment path (e.g. '/(browse)/[artistSlug]/[year]') for\n * dev-mode validation in getSegmentParams().\n *\n * @internal — framework use only\n */\nexport function setMatchedSegmentPath(segmentPath: string): void {\n const store = requestContextAls.getStore();\n if (!store) return; // non-throwing — optional diagnostic\n store.matchedSegmentPath = segmentPath;\n}\n\n/**\n * Extract dynamic segment names from a route pattern.\n * '/[artistSlug]/[year]' → ['artistSlug', 'year']\n * '/docs/[...slug]' → ['slug']\n */\nfunction extractDynamicSegments(route: string): string[] {\n const segments: string[] = [];\n // Match [paramName], [...paramName], [[...paramName]]\n const re = /\\[{1,2}\\.{0,3}(\\w+)\\]{1,2}/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(route)) !== null) {\n segments.push(m[1]);\n }\n return segments;\n}\n\n/**\n * Returns the raw search string from the current request URL (e.g. \"?foo=bar\").\n * Synchronous — safe for use in `redirect()` which throws synchronously.\n *\n * Returns empty string if called outside a request context (non-throwing for\n * use in redirect's optional preserveSearchParams path).\n *\n * @internal — used by redirect() for preserveSearchParams support.\n */\nexport function getRequestSearchString(): string {\n const store = requestContextAls.getStore();\n return store?.searchString ?? '';\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\n/**\n * Read-only Headers interface. The standard Headers class is mutable;\n * this type narrows it to read-only methods. The underlying object is\n * still a Headers instance, but user code should not mutate it.\n */\nexport type ReadonlyHeaders = Pick<\n Headers,\n 'get' | 'has' | 'entries' | 'keys' | 'values' | 'forEach' | typeof Symbol.iterator\n>;\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Run a callback within a request context. Used by the pipeline to establish\n * per-request ALS scope so that `getHeaders()` and `getCookies()` work.\n *\n * If the request was previously registered via `seedRequestCookies`, the\n * resulting context's `parsedCookies` map is initialized from the seed and\n * the raw `cookieHeader` is left empty — `parseCookieHeader` is never called\n * for that request. This is the no-JS form-rerender path. See TIM-868.\n *\n * @param req - The incoming Request object.\n * @param fn - The function to run within the request context.\n */\nexport function runWithRequestContext<T>(req: Request, fn: () => T): T {\n const originalCopy = new Headers(req.headers);\n const parsedUrl = new URL(req.url);\n const seed = consumeSeededCookies(req);\n const store: RequestContextStore = {\n headers: freezeHeaders(req.headers),\n originalHeaders: originalCopy,\n // When seeded, leave the raw header empty — parseCookieHeader is the\n // exact code path the smuggling primitive abused, and lazy parsing is\n // gated on `parsedCookies` being undefined.\n cookieHeader: seed ? '' : (req.headers.get('cookie') ?? ''),\n parsedCookies: seed,\n searchParams: parsedUrl.searchParams,\n searchString: parsedUrl.search,\n cookieJar: new Map(),\n flushed: false,\n mutableContext: false,\n };\n return requestContextAls.run(store, fn);\n}\n\n/**\n * Enable cookie mutation for the current context. Called by the framework\n * when entering middleware.ts, server actions, or route.ts handlers.\n *\n * See design/29-cookies.md §\"Context Tracking\"\n */\nexport function setMutableCookieContext(mutable: boolean): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.mutableContext = mutable;\n }\n}\n\n/**\n * Mark the response as flushed (headers committed). After this point,\n * cookie mutations log a warning instead of throwing.\n *\n * See design/29-cookies.md §\"Streaming Constraint: Post-Flush Cookie Warning\"\n */\nexport function markResponseFlushed(): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.flushed = true;\n }\n}\n\n/**\n * Apply middleware-injected request headers to the current request context.\n *\n * Called by the pipeline after middleware.ts runs. Merges overlay headers\n * on top of the original request headers so downstream code (access.ts,\n * server components, server actions) sees them via `getHeaders()`.\n *\n * The original request headers are never mutated — a new frozen Headers\n * object is created with the overlay applied on top.\n *\n * See design/07-routing.md §\"Request Header Injection\"\n */\nexport function applyRequestHeaderOverlay(overlay: Headers): void {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] applyRequestHeaderOverlay() called outside of a request context.');\n }\n\n // Check if the overlay has any headers — skip if empty\n let hasOverlay = false;\n overlay.forEach(() => {\n hasOverlay = true;\n });\n if (!hasOverlay) return;\n\n // Merge: start with original headers, overlay on top\n const merged = new Headers(store.originalHeaders);\n overlay.forEach((value, key) => {\n merged.set(key, value);\n });\n store.headers = freezeHeaders(merged);\n}\n\n// ─── Read-Only Headers ────────────────────────────────────────────────────\n\nconst MUTATING_METHODS = new Set(['set', 'append', 'delete']);\n\n/**\n * Wrap a Headers object in a Proxy that throws on mutating methods.\n * Object.freeze doesn't work on Headers (native internal slots), so we\n * intercept property access and reject set/append/delete at runtime.\n *\n * Read methods (get, has, entries, etc.) must be bound to the underlying\n * Headers instance because they access private #headersList slots.\n */\nfunction freezeHeaders(source: Headers): Headers {\n const copy = new Headers(source);\n return new Proxy(copy, {\n get(target, prop) {\n if (typeof prop === 'string' && MUTATING_METHODS.has(prop)) {\n return () => {\n throw new Error(\n `[timber] getHeaders() returns a read-only Headers object. ` +\n `Calling .${prop}() is not allowed. ` +\n `Use ctx.requestHeaders in middleware to inject headers for downstream components.`\n );\n };\n }\n const value = Reflect.get(target, prop);\n // Bind methods to the real Headers instance so private slot access works\n if (typeof value === 'function') {\n return value.bind(target);\n }\n return value;\n },\n });\n}\n","/**\n * Per-request waitUntil bridge — ALS bridge for platform adapters.\n *\n * The generated entry point (Nitro, Cloudflare) wraps the handler with\n * `runWithWaitUntil`, binding the platform's lifecycle extension function\n * (e.g., h3's `event.waitUntil()` or CF's `ctx.waitUntil()`) for the\n * request duration. The `waitUntil()` primitive reads from this ALS to\n * dispatch background work to the correct platform API.\n *\n * Design doc: design/11-platform.md §\"waitUntil()\"\n */\n\nimport { waitUntilAls } from './als-registry.js';\n\n/**\n * Run a function with a per-request waitUntil handler installed.\n *\n * Called by generated entry points (Nitro node-server/bun, Cloudflare)\n * to bind the platform's lifecycle extension for the request duration.\n */\nexport function runWithWaitUntil<T>(\n waitUntilFn: (promise: Promise<unknown>) => void,\n fn: () => T\n): T {\n return waitUntilAls.run(waitUntilFn, fn);\n}\n\n/**\n * Get the current request's waitUntil function, if available.\n *\n * Returns undefined when no platform adapter has installed a waitUntil\n * handler for the current request (e.g., on platforms that don't support\n * lifecycle extension, or outside a request context).\n */\nexport function getWaitUntil(): ((promise: Promise<unknown>) => void) | undefined {\n return waitUntilAls.getStore();\n}\n","// Server-side primitives: deny, redirect, redirectExternal, RenderError, waitUntil, SsrStreamError\n//\n// These are the core runtime signals that components, middleware, and access gates\n// use to control request flow. See design/10-error-handling.md.\n\nimport type { JsonSerializable } from './types.js';\nimport { getWaitUntil as _getWaitUntil } from './waituntil-bridge.js';\nimport { isDebug } from './debug.js';\nimport { getRequestSearchString } from './request-context.js';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.js';\n\n// ─── Dev-mode validation ────────────────────────────────────────────────────\n\n/**\n * Check if a value is JSON-serializable without data loss.\n * Returns a description of the first non-serializable value found, or null if OK.\n *\n * @internal Exported for testing only.\n */\nexport function findNonSerializable(value: unknown, path = 'data'): string | null {\n if (value === null || value === undefined) return null;\n\n switch (typeof value) {\n case 'string':\n case 'number':\n case 'boolean':\n return null;\n case 'bigint':\n return `${path} contains a BigInt — BigInt throws in JSON.stringify`;\n case 'function':\n return `${path} is a function — functions are not JSON-serializable`;\n case 'symbol':\n return `${path} is a symbol — symbols are not JSON-serializable`;\n case 'object':\n break;\n default:\n return `${path} has unsupported type \"${typeof value}\"`;\n }\n\n if (value instanceof Date) {\n return `${path} is a Date — Dates silently coerce to strings in JSON.stringify`;\n }\n if (value instanceof Map) {\n return `${path} is a Map — Maps serialize as {} in JSON.stringify (data loss)`;\n }\n if (value instanceof Set) {\n return `${path} is a Set — Sets serialize as {} in JSON.stringify (data loss)`;\n }\n if (value instanceof RegExp) {\n return `${path} is a RegExp — RegExps serialize as {} in JSON.stringify`;\n }\n if (value instanceof Error) {\n return `${path} is an Error — Errors serialize as {} in JSON.stringify`;\n }\n\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const result = findNonSerializable(value[i], `${path}[${i}]`);\n if (result) return result;\n }\n return null;\n }\n\n // Plain object — only Object.prototype is safe. Null-prototype objects\n // (Object.create(null)) survive JSON.stringify but React Flight rejects\n // them with \"Classes or null prototypes are not supported\", so the\n // pre-flush deny path (renderDenyPage → renderToReadableStream) would throw.\n const proto = Object.getPrototypeOf(value);\n if (proto === null) {\n return `${path} is a null-prototype object — React Flight rejects null prototypes`;\n }\n if (proto !== Object.prototype) {\n const name = (value as object).constructor?.name ?? 'unknown';\n return `${path} is a ${name} instance — class instances may lose data in JSON.stringify`;\n }\n\n for (const key of Object.keys(value as Record<string, unknown>)) {\n const result = findNonSerializable((value as Record<string, unknown>)[key], `${path}.${key}`);\n if (result) return result;\n }\n return null;\n}\n\n/**\n * Emit a dev-mode warning if data is not JSON-serializable.\n * No-op in production.\n */\nfunction warnIfNotSerializable(data: unknown, callerName: string): void {\n if (!isDebug()) return;\n if (data === undefined) return;\n\n const issue = findNonSerializable(data);\n if (issue) {\n console.warn(\n `[timber] ${callerName}: ${issue}. ` +\n 'Data passed to deny() or RenderError must be JSON-serializable because ' +\n 'the post-flush path uses JSON.stringify, not React Flight.'\n );\n }\n}\n\n// ─── DenySignal ─────────────────────────────────────────────────────────────\n\n/**\n * Render-phase signal thrown by `deny()`. Caught by the framework to produce\n * the correct HTTP status code (segment context) or graceful degradation (slot context).\n */\nexport class DenySignal extends Error {\n readonly status: number;\n readonly data: JsonSerializable | undefined;\n\n constructor(status: number, data?: JsonSerializable) {\n super(`Access denied with status ${status}`);\n this.name = 'DenySignal';\n this.status = status;\n this.data = data;\n }\n\n /**\n * Extract the file that called deny() from the stack trace.\n * Returns a short path (e.g. \"app/auth/access.ts\") or undefined if\n * the stack can't be parsed. Dev-only — used for dev log output.\n */\n get sourceFile(): string | undefined {\n if (!this.stack) return undefined;\n const frames = this.stack.split('\\n');\n // Skip the Error line and the deny() frame — the caller is the 3rd line.\n // Stack format: \" at FnName (file:line:col)\" or \" at file:line:col\"\n for (let i = 2; i < frames.length; i++) {\n const frame = frames[i];\n if (!frame) continue;\n // Skip framework internals\n if (frame.includes('primitives.ts') || frame.includes('node_modules')) continue;\n // Extract file path from the frame\n const match =\n frame.match(/\\(([^)]+?)(?::\\d+:\\d+)\\)/) ?? frame.match(/at\\s+([^\\s]+?)(?::\\d+:\\d+)/);\n if (match?.[1]) {\n // Shorten to app-relative path\n const full = match[1];\n const appIdx = full.indexOf('/app/');\n return appIdx >= 0 ? full.slice(appIdx + 1) : full;\n }\n }\n return undefined;\n }\n}\n\n/** Options for deny() when using the object form. */\nexport interface DenyOptions {\n /** HTTP status code (4xx or 5xx). Default: 403. */\n status?: number;\n /** Human-readable message (logged server-side, not sent to client). */\n message?: string;\n /** JSON-serializable data passed as `dangerouslyPassData` prop to status-code files. */\n data?: JsonSerializable;\n}\n\n/**\n * Universal denial/error primitive. Throws a `DenySignal` that the framework catches.\n *\n * - In segment context (outside Suspense): produces HTTP status code\n * - In slot context: graceful degradation → denied.tsx → default.tsx → null\n * - Inside Suspense (hold window): promoted to pre-flush behavior\n * - Inside Suspense (after flush): error boundary + noindex meta\n *\n * Supports both positional and object signatures:\n * ```ts\n * deny() // 403 (default)\n * deny(404) // 404\n * deny(503, { retry: true }) // 503 with data\n * deny({ status: 503, message: 'Maintenance' }) // object form\n * ```\n *\n * Accepts any 4xx or 5xx status code. This replaces the need for\n * `throw new RenderError(...)` in user code — RenderError is now an\n * internal pipeline detail.\n *\n * @param statusOrOptions - Status code (number) or options object. Default: 403.\n * @param data - Optional JSON-serializable data (positional form only).\n */\nexport function deny(statusOrOptions?: number | DenyOptions, data?: JsonSerializable): never {\n let status: number;\n let resolvedData: JsonSerializable | undefined;\n\n if (typeof statusOrOptions === 'object' && statusOrOptions !== null) {\n status = statusOrOptions.status ?? 403;\n resolvedData = statusOrOptions.data;\n } else {\n status = statusOrOptions ?? 403;\n resolvedData = data;\n }\n\n if (status < 400 || status > 599) {\n throw new Error(`deny() requires a 4xx or 5xx status code, got ${status}.`);\n }\n warnIfNotSerializable(resolvedData, 'deny()');\n throw new DenySignal(status, resolvedData);\n}\n\n/**\n * @deprecated Use `deny(404)` instead.\n * Kept for internal use by the Next.js shim layer.\n * @internal\n */\nexport function notFound(): never {\n deny(404);\n}\n\n/**\n * Next.js redirect type discriminator.\n *\n * Provided for API compatibility with libraries that import `RedirectType`\n * from `next/navigation`. In timber, `redirect()` always uses `replace`\n * semantics (no history entry for the redirect itself).\n */\nexport const RedirectType = {\n push: 'push',\n replace: 'replace',\n} as const;\n\n// ─── RedirectSignal ─────────────────────────────────────────────────────────\n\n/**\n * Render-phase signal thrown by `redirect()` and `redirectExternal()`.\n * Caught by the framework to produce a 3xx response or client-side navigation.\n */\nexport class RedirectSignal extends Error {\n readonly location: string;\n readonly status: number;\n\n constructor(location: string, status: number) {\n super(`Redirect to ${location}`);\n this.name = 'RedirectSignal';\n this.location = location;\n this.status = status;\n }\n}\n\n/** Pattern matching absolute URLs: http(s):// or protocol-relative // */\nconst ABSOLUTE_URL_RE = /^(?:[a-zA-Z][a-zA-Z\\d+\\-.]*:|\\/\\/)/;\n\n/**\n * Options for redirect() — alternative to passing a bare status code.\n */\nexport interface RedirectOptions {\n /** HTTP redirect status code (3xx). Defaults to 302 (or 308 when `permanent: true`). */\n status?: number;\n /**\n * When true, defaults the status to 308 (Permanent Redirect, preserves HTTP method).\n * If `status` is also provided, `status` takes precedence.\n *\n * @example\n * redirect('/new-path', { permanent: true }); // 308\n * redirect('/new-path', { permanent: true, status: 301 }); // 301\n */\n permanent?: boolean;\n /**\n * Preserve search params from the current request URL on the redirect target.\n *\n * - `true` — preserve ALL current search params (target params take precedence)\n * - `string[]` — preserve only the named params (e.g. `['private', 'token']`)\n *\n * Target path's own query params always take precedence over preserved ones.\n */\n preserveSearchParams?: true | string[];\n}\n\n/**\n * Redirect to a relative path. Rejects absolute and protocol-relative URLs.\n * Use `redirectExternal()` for external redirects with an allow-list.\n *\n * @param path - Relative path (e.g. '/login', 'settings', '/login?returnTo=/dash')\n * @param statusOrOptions - HTTP status code (3xx, default 302) or options object.\n *\n * @example\n * // Simple redirect\n * redirect('/login');\n *\n * // With status code\n * redirect('/login', 301);\n *\n * // With preserved search params\n * redirect(`/docs/${version}/${slug}`, { preserveSearchParams: ['foo'] });\n */\nexport function redirect(path: string, statusOrOptions?: number | RedirectOptions): never {\n let status: number;\n let preserveSearchParams: true | string[] | undefined;\n\n if (typeof statusOrOptions === 'number') {\n status = statusOrOptions;\n } else if (statusOrOptions) {\n // Explicit status wins. Otherwise permanent: true → 308, default → 302.\n status = statusOrOptions.status ?? (statusOrOptions.permanent ? 308 : 302);\n preserveSearchParams = statusOrOptions.preserveSearchParams;\n } else {\n status = 302;\n }\n\n if (status < 300 || status > 399) {\n throw new Error(`redirect() requires a 3xx status code, got ${status}.`);\n }\n if (ABSOLUTE_URL_RE.test(path)) {\n throw new Error(\n `redirect() only accepts relative URLs. Got absolute URL: \"${path}\". ` +\n 'Use redirectExternal(url, allowList) for external redirects.'\n );\n }\n\n let resolvedPath = path;\n if (preserveSearchParams) {\n const currentSearch = getRequestSearchString();\n resolvedPath = mergePreservedSearchParams(path, currentSearch, preserveSearchParams);\n }\n\n throw new RedirectSignal(resolvedPath, status);\n}\n\n/**\n * @deprecated Use `redirect(path, { permanent: true })` instead.\n * Kept for internal use by the Next.js shim layer.\n * @internal\n */\nexport function permanentRedirect(path: string, options?: Omit<RedirectOptions, 'status'>): never {\n redirect(path, { permanent: true, ...options });\n}\n\n/**\n * Redirect to an external URL. The hostname must be in the provided allow-list.\n *\n * @param url - Absolute URL to redirect to.\n * @param allowList - Array of allowed hostnames (e.g. ['example.com', 'auth.example.com']).\n * @param status - HTTP redirect status code (3xx). Defaults to 302.\n */\nexport function redirectExternal(url: string, allowList: string[], status: number = 302): never {\n if (status < 300 || status > 399) {\n throw new Error(`redirectExternal() requires a 3xx status code, got ${status}.`);\n }\n\n let hostname: string;\n try {\n hostname = new URL(url).hostname;\n } catch {\n throw new Error(`redirectExternal() received an invalid URL: \"${url}\"`);\n }\n\n if (!allowList.includes(hostname)) {\n throw new Error(\n `redirectExternal() target \"${hostname}\" is not in the allow-list. ` +\n `Allowed: [${allowList.join(', ')}]`\n );\n }\n\n throw new RedirectSignal(url, status);\n}\n\n// ─── RenderError ────────────────────────────────────────────────────────────\n\n/**\n * Typed digest that crosses the RSC → client boundary.\n * The `code` identifies the error class; `data` carries JSON-serializable context.\n */\nexport interface RenderErrorDigest<\n TCode extends string = string,\n TData extends JsonSerializable = JsonSerializable,\n> {\n code: TCode;\n data: TData;\n}\n\n/**\n * Typed throw for render-phase errors that carry structured context to error boundaries.\n *\n * The `digest` (code + data) is serialized into the RSC stream separately from the\n * Error instance — only the digest crosses the RSC → client boundary.\n *\n * @example\n * ```ts\n * throw new RenderError('PRODUCT_NOT_FOUND', {\n * title: 'Product not found',\n * resourceId: params.id,\n * })\n * ```\n */\nexport class RenderError<\n TCode extends string = string,\n TData extends JsonSerializable = JsonSerializable,\n> extends Error {\n readonly code: TCode;\n readonly digest: RenderErrorDigest<TCode, TData>;\n readonly status: number;\n\n constructor(code: TCode, data: TData, options?: { status?: number }) {\n super(`RenderError: ${code}`);\n this.name = 'RenderError';\n this.code = code;\n this.digest = { code, data };\n\n warnIfNotSerializable(data, 'RenderError');\n\n const status = options?.status ?? 500;\n if (status < 400 || status > 599) {\n throw new Error(`RenderError status must be 4xx or 5xx, got ${status}.`);\n }\n this.status = status;\n }\n}\n\n// ─── waitUntil ──────────────────────────────────────────────────────────────\n\n/** Minimal interface for adapters that support background work. */\nexport interface WaitUntilAdapter {\n waitUntil?(promise: Promise<unknown>): void;\n}\n\n// Intentional per-app singleton — warn-once flag that persists for the\n// lifetime of the process/isolate. Not per-request; do not migrate to ALS.\nlet _waitUntilWarned = false;\n\n/**\n * Register a promise to be kept alive after the response is sent.\n * Maps to `ctx.waitUntil()` on Cloudflare Workers and similar platforms.\n *\n * In production, the platform adapter installs a per-request waitUntil\n * function via ALS (see waituntil-bridge.ts). This function checks the\n * ALS bridge first, then falls back to the legacy adapter argument.\n *\n * If neither is available, a warning is logged once and the promise is\n * left to resolve (or reject) without being tracked.\n *\n * @param promise - The background work to keep alive.\n * @param adapter - Optional legacy adapter (prefer ALS bridge in production).\n */\nexport function waitUntil(promise: Promise<unknown>, adapter?: WaitUntilAdapter): void {\n // Check ALS bridge first (installed by generated entry points)\n const alsFn = _getWaitUntil();\n if (alsFn) {\n alsFn(promise);\n return;\n }\n\n // Fall back to legacy adapter argument\n if (adapter && typeof adapter.waitUntil === 'function') {\n adapter.waitUntil(promise);\n return;\n }\n\n if (!_waitUntilWarned) {\n _waitUntilWarned = true;\n console.warn(\n '[timber] waitUntil() is not supported by the current adapter. ' +\n 'Background work will not be tracked. This warning is shown once.'\n );\n }\n}\n\n/**\n * Reset the waitUntil warning state. Exported for testing only.\n * @internal\n */\nexport function _resetWaitUntilWarning(): void {\n _waitUntilWarned = false;\n}\n\n// ─── SsrStreamError ─────────────────────────────────────────────────────────\n\n/**\n * Error thrown when SSR's renderToReadableStream fails due to an error\n * in the decoded RSC stream (e.g., uncontained slot errors).\n *\n * The RSC entry checks for this error type in its catch block to avoid\n * re-executing server components via renderDenyPage. Instead, it renders\n * a bare deny/error page without layout wrapping.\n *\n * Defined in primitives.ts (not ssr-entry.ts) because ssr-entry.ts imports\n * react-dom/server which cannot be loaded in the RSC environment.\n */\nexport class SsrStreamError extends Error {\n constructor(\n message: string,\n public readonly cause: unknown\n ) {\n super(message);\n this.name = 'SsrStreamError';\n }\n}\n","/**\n * FormData preprocessing — schema-agnostic conversion of FormData to typed objects.\n *\n * FormData is all strings. Schema validation expects typed values. This module\n * bridges the gap with intelligent coercion that runs *before* schema validation.\n *\n * Inspired by zod-form-data, but schema-agnostic — works with any Standard Schema\n * library (Zod, Valibot, ArkType).\n *\n * See design/08-forms-and-actions.md §\"parseFormData() and coerce helpers\"\n */\n\n// ─── parseFormData ───────────────────────────────────────────────────────\n\n/**\n * Convert FormData into a plain object with intelligent coercion.\n *\n * Handles:\n * - **Duplicate keys → arrays**: `tags=js&tags=ts` → `{ tags: [\"js\", \"ts\"] }`\n * - **Nested dot-paths**: `user.name=Alice` → `{ user: { name: \"Alice\" } }`\n * - **Empty strings → undefined**: Enables `.optional()` semantics in schemas\n * - **Empty Files → undefined**: File inputs with no selection become `undefined`\n * - **Strips `$ACTION_*` fields**: React's internal hidden fields are excluded\n */\nexport function parseFormData(formData: FormData): Record<string, unknown> {\n const flat: Record<string, unknown> = {};\n\n for (const key of new Set(formData.keys())) {\n // Skip React internal fields\n if (key.startsWith('$ACTION_')) continue;\n\n const values = formData.getAll(key);\n const processed = values.map(normalizeValue);\n\n if (processed.length === 1) {\n flat[key] = processed[0];\n } else {\n // Filter out undefined entries from multi-value fields\n flat[key] = processed.filter((v) => v !== undefined);\n }\n }\n\n // Expand dot-notation paths into nested objects\n return expandDotPaths(flat);\n}\n\n/**\n * Normalize a single FormData entry value.\n * - Empty strings → undefined (enables .optional() semantics)\n * - Empty File objects (no selection) → undefined\n * - Everything else passes through as-is\n */\nfunction normalizeValue(value: FormDataEntryValue): unknown {\n if (typeof value === 'string') {\n return value === '' ? undefined : value;\n }\n\n // File input with no selection: browsers submit a File with name=\"\" and size=0\n if (value instanceof File && value.size === 0 && value.name === '') {\n return undefined;\n }\n\n return value;\n}\n\n/**\n * Expand dot-notation keys into nested objects.\n * `{ \"user.name\": \"Alice\", \"user.age\": \"30\" }` → `{ user: { name: \"Alice\", age: \"30\" } }`\n *\n * Keys without dots are left as-is. Bracket notation (e.g. `items[0]`) is NOT\n * supported — use dot notation (`items.0`) instead.\n */\nfunction expandDotPaths(flat: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n let hasDotPaths = false;\n\n // First pass: check if any keys have dots\n for (const key of Object.keys(flat)) {\n if (key.includes('.')) {\n hasDotPaths = true;\n break;\n }\n }\n\n // Fast path: no dot-notation keys, return as-is\n if (!hasDotPaths) return flat;\n\n for (const [key, value] of Object.entries(flat)) {\n if (!key.includes('.')) {\n result[key] = value;\n continue;\n }\n\n const parts = key.split('.');\n let current: Record<string, unknown> = result;\n\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (current[part] === undefined || current[part] === null) {\n current[part] = {};\n }\n // If current[part] is not an object (e.g., a string from a non-dotted key),\n // the dot-path takes precedence\n if (typeof current[part] !== 'object' || current[part] instanceof File) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n\n current[parts[parts.length - 1]] = value;\n }\n\n return result;\n}\n\n// ─── Coercion Helpers ────────────────────────────────────────────────────\n\n/**\n * Schema-agnostic coercion primitives for common FormData patterns.\n *\n * These are plain transform functions — they compose with any schema library's\n * `transform`/`preprocess` pipeline:\n *\n * ```ts\n * // Zod\n * z.preprocess(coerce.number, z.number())\n * // Valibot\n * v.pipe(v.unknown(), v.transform(coerce.number), v.number())\n * ```\n */\nexport const coerce = {\n /**\n * Coerce a string to a number.\n * - `\"42\"` → `42`\n * - `\"3.14\"` → `3.14`\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Non-numeric strings → `undefined` (schema validation will catch this)\n */\n number(value: unknown): number | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value === 'number') return value;\n if (typeof value !== 'string') return undefined;\n const num = Number(value);\n if (Number.isNaN(num)) return undefined;\n return num;\n },\n\n /**\n * Coerce a checkbox value to a boolean.\n * HTML checkboxes submit \"on\" when checked and are absent when unchecked.\n * - `\"on\"` / any truthy string → `true`\n * - `undefined` / `null` / `\"\"` → `false`\n */\n checkbox(value: unknown): boolean {\n if (value === undefined || value === null || value === '') return false;\n if (typeof value === 'boolean') return value;\n // Any non-empty string (typically \"on\") is true\n return typeof value === 'string' && value.length > 0;\n },\n\n /**\n * Parse a JSON string into an object.\n * - Valid JSON string → parsed object\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Invalid JSON → `undefined` (schema validation will catch this)\n */\n json(value: unknown): unknown {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return undefined;\n }\n },\n\n /**\n * Coerce a date string to a Date object.\n * Handles `<input type=\"date\">` (`\"2024-01-15\"`), `<input type=\"datetime-local\">`\n * (`\"2024-01-15T10:30\"`), and full ISO 8601 strings.\n * - Valid date string → `Date`\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Invalid date strings → `undefined` (schema validation will catch this)\n * - Impossible dates that `new Date()` silently normalizes (e.g. Feb 31) → `undefined`\n */\n date(value: unknown): Date | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (value instanceof Date) return value;\n if (typeof value !== 'string') return undefined;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return undefined;\n\n // Overflow detection: extract Y/M/D from the input string and verify\n // they match the parsed Date components. new Date('2024-02-31') silently\n // normalizes to March 2nd — we reject such inputs.\n const ymdMatch = value.match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n if (ymdMatch) {\n const inputYear = Number(ymdMatch[1]);\n const inputMonth = Number(ymdMatch[2]);\n const inputDay = Number(ymdMatch[3]);\n\n // Use UTC methods for date-only and Z-suffixed strings to avoid\n // timezone offset shifting the day. For datetime-local (no Z suffix),\n // the Date constructor parses in local time, so use local methods.\n const isUTC = value.length === 10 || value.endsWith('Z');\n const parsedYear = isUTC ? date.getUTCFullYear() : date.getFullYear();\n const parsedMonth = isUTC ? date.getUTCMonth() + 1 : date.getMonth() + 1;\n const parsedDay = isUTC ? date.getUTCDate() : date.getDate();\n\n if (inputYear !== parsedYear || inputMonth !== parsedMonth || inputDay !== parsedDay) {\n return undefined;\n }\n }\n\n return date;\n },\n\n /**\n * Create a File coercion function with optional size and mime type validation.\n * Returns the File if valid, `undefined` otherwise.\n *\n * ```ts\n * // Basic — just checks it's a real File\n * z.preprocess(coerce.file(), z.instanceof(File))\n *\n * // With constraints\n * z.preprocess(\n * coerce.file({ maxSize: 5 * 1024 * 1024, accept: ['image/png', 'image/jpeg'] }),\n * z.instanceof(File)\n * )\n * ```\n */\n file(options?: { maxSize?: number; accept?: string[] }): (value: unknown) => File | undefined {\n return (value: unknown): File | undefined => {\n if (value === undefined || value === null || value === '') return undefined;\n if (!(value instanceof File)) return undefined;\n\n // Empty file input (no selection): browsers submit File with name=\"\" and size=0\n if (value.size === 0 && value.name === '') return undefined;\n\n if (options?.maxSize !== undefined && value.size > options.maxSize) {\n return undefined;\n }\n\n if (options?.accept !== undefined && !options.accept.includes(value.type)) {\n return undefined;\n }\n\n return value;\n };\n },\n};\n","/**\n * Server action primitives: revalidatePath, revalidateTag, and the action handler.\n *\n * - revalidatePath(path) re-renders the route at that path and returns the RSC\n * flight payload for inline reconciliation.\n * - revalidateTag(tag) invalidates timber.cache entries by tag.\n *\n * Both are callable from anywhere on the server — actions, API routes, handlers.\n *\n * The action handler processes incoming action requests, validates CSRF,\n * enforces body limits, executes the action, and returns the response\n * (with piggybacked RSC payload if revalidatePath was called).\n *\n * See design/08-forms-and-actions.md\n */\n\nimport { getCacheHandler } from '../cache/handler-store';\nimport { RedirectSignal } from './primitives';\nimport { withSpan } from './tracing';\nimport { revalidationAls, type RevalidationState } from './als-registry.js';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/** Result of rendering a revalidation — element tree before RSC serialization. */\nexport interface RevalidationResult {\n /** React element tree (pre-serialization — passed to renderToReadableStream). */\n element: unknown;\n /** Resolved head elements for metadata. */\n headElements: unknown[];\n}\n\n/** Renderer function that builds a React element tree for a given path. */\nexport type RevalidateRenderer = (path: string) => Promise<RevalidationResult>;\n\n// Re-export the type from the registry for public API consumers.\nexport type { RevalidationState } from './als-registry.js';\n\n/** Options for creating the action handler. */\nexport interface ActionHandlerConfig {\n /** Renderer for producing RSC payloads during revalidation. */\n renderer?: RevalidateRenderer;\n}\n\n/** Result of handling a server action request. */\nexport interface ActionHandlerResult {\n /** The action's return value (serialized). */\n actionResult: unknown;\n /** Revalidation result if revalidatePath was called (element tree, not yet serialized). */\n revalidation?: RevalidationResult;\n /** Redirect location if a RedirectSignal was thrown during revalidation. */\n redirectTo?: string;\n /** Redirect status code. */\n redirectStatus?: number;\n}\n\n// ─── Revalidation State ──────────────────────────────────────────────────\n\n// Per-request revalidation state stored in AsyncLocalStorage (from als-registry.ts).\n// This ensures concurrent requests never share or overwrite each other's state\n// (the previous module-level global was vulnerable to cross-request pollution).\n\n/**\n * Set the revalidation state for the current action execution.\n * @internal — kept for test compatibility; prefer executeAction() which uses ALS.\n */\nexport function _setRevalidationState(state: RevalidationState): void {\n // Enter ALS scope — this is only used by tests that call revalidatePath/Tag\n // directly without going through executeAction().\n revalidationAls.enterWith(state);\n}\n\n/**\n * Clear the revalidation state after action execution.\n * @internal — kept for test compatibility.\n */\nexport function _clearRevalidationState(): void {\n revalidationAls.enterWith(undefined as unknown as RevalidationState);\n}\n\n/**\n * Get the current revalidation state. Throws if called outside an action context.\n * @internal\n */\nfunction getRevalidationState(): RevalidationState {\n const state = revalidationAls.getStore();\n if (!state) {\n throw new Error(\n 'revalidatePath/revalidateTag called outside of a server action context. ' +\n 'These functions can only be called during action execution.'\n );\n }\n return state;\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────\n\n/**\n * Re-render the route at `path` and include the RSC flight payload in the\n * action response. The client reconciles inline — no separate fetch needed.\n *\n * Can be called from server actions, API routes, or any server-side context.\n *\n * @param path - The path to re-render (e.g. '/dashboard', '/todos').\n */\nexport function revalidatePath(path: string): void {\n const state = getRevalidationState();\n if (!state.paths.includes(path)) {\n state.paths.push(path);\n }\n}\n\n/**\n * Invalidate all timber.cache entries tagged with `tag`.\n * Does not return a payload — the next request for an invalidated entry re-executes.\n *\n * @param tag - The cache tag to invalidate (e.g. 'products', 'user:123').\n */\nexport function revalidateTag(tag: string): void {\n const state = getRevalidationState();\n if (!state.tags.includes(tag)) {\n state.tags.push(tag);\n }\n}\n\n// ─── Action Handler ──────────────────────────────────────────────────────\n\n/**\n * Execute a server action and process revalidation.\n *\n * 1. Sets up revalidation state\n * 2. Calls the action function\n * 3. Processes revalidateTag calls (invalidates cache entries)\n * 4. Processes revalidatePath calls (re-renders and captures RSC payload)\n * 5. Returns the action result + optional RSC payload\n *\n * @param actionFn - The server action function to execute.\n * @param args - Arguments to pass to the action.\n * @param config - Handler configuration (cache handler, renderer).\n */\nexport async function executeAction(\n actionFn: (...args: unknown[]) => Promise<unknown>,\n args: unknown[],\n config: ActionHandlerConfig = {},\n spanMeta?: { actionFile?: string; actionName?: string }\n): Promise<ActionHandlerResult> {\n const state: RevalidationState = { paths: [], tags: [] };\n let actionResult: unknown;\n let redirectTo: string | undefined;\n let redirectStatus: number | undefined;\n\n // Run the action inside ALS scope so revalidatePath/Tag resolve to this\n // request's state object — concurrent requests each get their own scope.\n await revalidationAls.run(state, async () => {\n try {\n actionResult = await withSpan(\n 'timber.action',\n {\n ...(spanMeta?.actionFile ? { 'timber.action_file': spanMeta.actionFile } : {}),\n ...(spanMeta?.actionName ? { 'timber.action_name': spanMeta.actionName } : {}),\n },\n () => actionFn(...args)\n );\n } catch (error) {\n if (error instanceof RedirectSignal) {\n redirectTo = error.location;\n redirectStatus = error.status;\n } else {\n throw error;\n }\n }\n });\n\n // Process tag invalidation via the module-level cache handler singleton.\n // setCacheHandler() is called at boot from rsc-entry when timber.config.ts\n // provides a cacheHandler; otherwise falls back to in-memory LRU (TIM-599).\n if (state.tags.length > 0) {\n const handler = getCacheHandler();\n await Promise.all(state.tags.map((tag) => handler.invalidate({ tag })));\n }\n\n // Process path revalidation — build element tree (not yet serialized)\n let revalidation: RevalidationResult | undefined;\n if (state.paths.length > 0 && config.renderer) {\n // For now, render the first revalidated path.\n // Multiple paths could be supported via multipart streaming in the future.\n const path = state.paths[0];\n try {\n revalidation = await config.renderer(path);\n } catch (renderError) {\n if (renderError instanceof RedirectSignal) {\n // Revalidation triggered a redirect (e.g., session expired)\n redirectTo = renderError.location;\n redirectStatus = renderError.status;\n } else {\n // Log but don't fail the action — revalidation is best-effort\n console.error('[timber] revalidatePath render failed:', renderError);\n }\n }\n }\n\n return {\n actionResult,\n revalidation,\n ...(redirectTo ? { redirectTo, redirectStatus } : {}),\n };\n}\n\n/**\n * Build an HTTP Response for a no-JS form submission.\n * Standard POST → 302 redirect pattern.\n *\n * @param redirectPath - Where to redirect after the action executes.\n */\nexport function buildNoJsResponse(redirectPath: string, status: number = 302): Response {\n return new Response(null, {\n status,\n headers: { Location: redirectPath },\n });\n}\n\n/**\n * Detect whether the incoming request is an RSC action request (with JS)\n * or a plain HTML form POST (no JS).\n *\n * RSC action requests use Accept: text/x-component or Content-Type: text/x-component.\n */\nexport function isRscActionRequest(req: Request): boolean {\n const accept = req.headers.get('Accept') ?? '';\n const contentType = req.headers.get('Content-Type') ?? '';\n return accept.includes('text/x-component') || contentType.includes('text/x-component');\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-output-helper-B1n0sUqj.js","names":[],"sources":["../../src/adapters/shared.ts","../../src/adapters/build-output-helper.ts"],"sourcesContent":["// Shared adapter utilities — leaf module with zero deps on server/, client/,\n// or any Vite-dependent code.\n//\n// Adapters are loaded by Node at Vite startup time, before Vite's module\n// resolver is available. This module uses only Node built-ins and can be\n// imported by both adapters via a relative path.\n//\n// IMPORTANT: Do NOT add imports from ../server/, ../client/, ../plugins/,\n// or any module that transitively depends on Vite. A test in\n// tests/adapters/shared-no-forbidden-imports.test.ts enforces this.\n\n// ─── Static asset cache headers ──────────────────────────────────────────\n\n/** Cache-Control value for hashed (immutable) assets. */\nexport const IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\n\n/** Cache-Control value for unhashed static assets. */\nexport const STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\n/**\n * Generate a `_headers` file for static asset cache control.\n *\n * The `_headers` file is a platform convention supported by Cloudflare Workers\n * Static Assets, Cloudflare Pages, and Netlify. It maps URL patterns to\n * HTTP response headers.\n *\n * Vite places all hashed chunks under `/assets/` — these get immutable caching.\n * Everything else (favicon.ico, robots.txt, etc.) gets a shorter cache.\n */\nexport function 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","// Shared build-output helper for adapter buildOutput() methods.\n//\n// Steps 1–5 of every adapter's buildOutput are platform-agnostic:\n// 1. Copy client assets to the public/static directory\n// 2. Write _headers file for static asset cache control\n// 3. Copy RSC + SSR server bundles\n// 4. Write manifest-init module (if present)\n//\n// Only the final steps (entry codegen, platform config) vary per adapter.\n//\n// IMPORTANT: This module must remain a leaf — no imports from ../server/,\n// ../client/, or Vite-dependent code. See adapters/shared.ts header.\n\nimport { writeFile, mkdir, cp } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { generateHeadersFile } from './shared.js';\nimport type { TimberConfig } from './types';\n\n/** Options for the shared build output steps. */\nexport interface BuildOutputBaseOptions {\n /** Resolved timber config. */\n config: TimberConfig;\n /** Root build directory (e.g. `.timber/build`). */\n buildDir: string;\n /** Adapter-specific output directory (e.g. `.timber/build/nitro`). */\n outDir: string;\n /**\n * Name of the public/static directory within outDir.\n * Nitro uses 'public', Cloudflare uses 'static'.\n */\n publicDirName: string;\n}\n\n/**\n * Run the platform-agnostic build output steps shared by all adapters:\n *\n * 1. Create the output directory\n * 2. Copy client assets to the public/static directory\n * 3. Write the `_headers` file for static asset cache control\n * 4. Copy RSC and SSR server bundles into the output directory\n * 5. Write the manifest-init module if present\n *\n * Returns the resolved public directory path for further adapter-specific use.\n */\nexport async function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise<string> {\n const { config, buildDir, outDir, publicDirName } = opts;\n\n // 1. Create the output directory.\n await mkdir(outDir, { recursive: true });\n\n // 2. Copy client assets to public/static 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, publicDirName);\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 // 3. Write _headers file for static asset cache control.\n await writeFile(join(publicDir, '_headers'), generateHeadersFile());\n\n // 4. Copy RSC + SSR server bundles into the output directory.\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 // 5. Write manifest-init module if present.\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n return publicDir;\n}\n"],"mappings":";;;;AAcA,IAAa,kBAAkB;;AAG/B,IAAa,eAAe;;;;;;;;;;;AAY5B,SAAgB,sBAA8B;AAC5C,QAAO;;;;mBAIU,gBAAgB;;;mBAGhB,aAAa;;;;;;;;;;;;;;;;ACOhC,eAAsB,oBAAoB,MAA+C;CACvF,MAAM,EAAE,QAAQ,UAAU,QAAQ,kBAAkB;AAGpD,OAAM,MAAM,QAAQ,EAAE,WAAW,MAAM,CAAC;CAKxC,MAAM,YAAY,KAAK,UAAU,SAAS;CAC1C,MAAM,YAAY,KAAK,QAAQ,cAAc;AAC7C,OAAM,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;AAC3C,OAAM,GAAG,WAAW,WAAW;EAC7B,WAAW;EACX,QAAQ,OAAO,4BAA4B,QAAgB,CAAC,IAAI,SAAS,MAAM,GAAG,KAAA;EACnF,CAAC,CAAC,YAAY,GAEb;AAGF,OAAM,UAAU,KAAK,WAAW,WAAW,EAAE,qBAAqB,CAAC;AAGnE,OAAM,GAAG,KAAK,UAAU,MAAM,EAAE,KAAK,QAAQ,MAAM,EAAE,EAAE,WAAW,MAAM,CAAC;AACzE,OAAM,GAAG,KAAK,UAAU,MAAM,EAAE,KAAK,QAAQ,MAAM,EAAE,EAAE,WAAW,MAAM,CAAC,CAAC,YAAY,GAAG;AAGzF,KAAI,OAAO,aACT,OAAM,UAAU,KAAK,QAAQ,2BAA2B,EAAE,OAAO,aAAa;AAGhF,QAAO"}
@@ -1,2 +0,0 @@
1
- import { i as runSchemaSync } from "./cli-schema-sync-ByZPGGmS.js";
2
- export { runSchemaSync };
@@ -1 +0,0 @@
1
- {"version":3,"file":"define-CPVQaWbx.js","names":[],"sources":["../../src/segment-params/define.ts"],"sourcesContent":["/**\n * defineSegmentParams — factory for typed route param coercion.\n *\n * Creates a ParamsDefinition that coerces raw string params from the\n * URL into typed values. Used by exporting from params.ts (segment-level)\n * convention file.\n *\n * Reuses the shared Codec<T> protocol with Standard Schema auto-detection,\n * same pattern as defineSearchParams. Runtime constraints are stricter:\n * - serialize must return string (not null — path segments can't be omitted)\n * - parse throwing → 404 (invalid param value)\n *\n * Design doc: design/07a-route-params-triage.md\n */\n\nimport type { Codec } from '../codec.js';\nimport {\n type StandardSchemaV1,\n validateSync,\n isStandardSchema,\n isCodec,\n} from '../schema-bridge.js';\n\n// ---------------------------------------------------------------------------\n// Server-only ALS reference for .get()\n// ---------------------------------------------------------------------------\n\n// Same pattern as search-params: eagerly registered at server startup\n// to avoid dynamic imports that lose ALS context. See TIM-523.\nlet _getSegmentParamsFn: (() => Record<string, string | string[]>) | undefined;\nlet _useSegmentParamsHook: (() => Record<string, string | string[]>) | undefined;\n\n/**\n * Register the client useSegmentParams hook.\n * Called by client entry at module load time to avoid require() at call time.\n * @internal\n */\nexport function _registerUseSegmentParams(hook: () => Record<string, string | string[]>): void {\n _useSegmentParamsHook = hook;\n}\n\n// Self-register on the client when the barrel hasn't been imported yet.\n// Handles the edge case where a client component imports only from params.ts\n// without importing anything from @timber-js/app/client (which runs the\n// registration). The dynamic import resolves before React hydration in Vite.\nif (typeof window !== 'undefined' && !_useSegmentParamsHook) {\n import('../client/use-segment-params.js').then((mod) => {\n if (!_useSegmentParamsHook) {\n _useSegmentParamsHook = mod.useSegmentParams;\n }\n });\n}\n\n/**\n * Register the getSegmentParams function. Called once at module load time\n * from request-context.ts to avoid dynamic import at call time.\n * @internal\n */\nexport function _setGetSegmentParamsFn(fn: () => Record<string, string | string[]>): void {\n _getSegmentParamsFn = fn;\n}\n\nfunction getSegmentParamsFromAls(): Record<string, string | string[]> {\n if (!_getSegmentParamsFn) {\n throw new Error(\n '[timber] segmentParams.get() is only available on the server. ' +\n 'Use segmentParams.useSegmentParams() on the client.'\n );\n }\n return _getSegmentParamsFn();\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from a Codec or StandardSchemaV1. */\nexport type InferParamField<V> =\n V extends Codec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineParams: a Codec or a Standard Schema. */\nexport type ParamField<T = unknown> = Codec<T> | StandardSchemaV1<T>;\n\nexport type { StandardSchemaV1 };\n\n/**\n * A typed route params definition.\n *\n * Returned by defineParams(). Provides parse (string → typed) and\n * serialize (typed → string) for each declared param.\n */\nexport interface ParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw string params into typed values. Throws on invalid values. */\n parse(raw: Record<string, string | string[]>): T;\n\n /** Serialize typed values back to strings for URL construction. */\n serialize(values: T): Record<string, string>;\n\n /**\n * Get typed segment params from the current request context (ALS).\n *\n * Server-only, sync.\n *\n * ```ts\n * // app/products/[id]/params.ts\n * export const segmentParams = defineSegmentParams({ id: z.coerce.number() })\n *\n * // app/products/[id]/page.tsx\n * import { segmentParams } from './params'\n * export default function Page() {\n * const { id } = segmentParams.get() // id: number\n * }\n * ```\n */\n get(): T;\n\n /**\n * Client hook for accessing typed segment params reactively.\n *\n * ```tsx\n * 'use client'\n * import { segmentParams } from './params'\n * export function ProductHeader() {\n * const { id } = segmentParams.useSegmentParams()\n * }\n * ```\n */\n useSegmentParams(): T;\n\n /** Read-only codec map. */\n codecs: { [K in keyof T]: Codec<T[K]> };\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a Standard Schema into a Codec for route params.\n *\n * Unlike fromSchema for search params:\n * - Parse throws on failure (no fallback to default)\n * - Serialize returns string (not null)\n */\nfunction fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Route params are always strings (single segment) or string[] (catch-all)\n const input = Array.isArray(value) ? value : value;\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // For route params, parse failure means the param is invalid → throw\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n // Catch-all segments produce arrays — join with '/' for path reconstruction\n if (Array.isArray(value)) {\n return value.join('/');\n }\n return String(value);\n },\n };\n}\n\n/**\n * Resolve a field value to a Codec. Auto-detects Standard Schema objects.\n */\nfunction resolveField(fieldName: string, value: ParamField): Codec<unknown> {\n if (isCodec(value)) {\n return value;\n }\n\n if (isStandardSchema(value)) {\n return fromParamSchema(fieldName, value);\n }\n\n throw new Error(\n `[timber] defineSegmentParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that no codec's serialize returns null.\n * Route params are structural — they must produce a valid path segment.\n */\nfunction validateSerialize(codecMap: Record<string, Codec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n // Test serialize with a sample parsed value to check for null\n // We can't exhaustively test, but we can check that serialize(parse(\"test\"))\n // doesn't return null for a basic input.\n try {\n const testValue = codec.parse('test');\n const serialized = codec.serialize(testValue);\n if (serialized === null) {\n throw new Error(\n `[timber] defineSegmentParams: field '${key}' codec.serialize() returned null.\\n` +\n ` Route params are path segments — they cannot be omitted.\\n` +\n ` Ensure serialize() always returns a string.`\n );\n }\n } catch (e) {\n // parse('test') may throw for strict codecs (e.g., number-only).\n // That's fine — it means the codec validates. We only care about\n // serialize returning null, which we can't test without a valid value.\n if (e instanceof Error && e.message.includes('returned null')) {\n throw e;\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a ParamsDefinition from a map of codecs and/or Standard Schema objects.\n *\n * ```ts\n * // app/products/[id]/layout.tsx\n * import { defineSegmentParams } from '@timber-js/app/segment-params'\n * import { z } from 'zod/v4'\n *\n * export const segmentParams = defineSegmentParams({\n * id: z.coerce.number().int().positive(),\n * })\n * ```\n */\nexport function defineSegmentParams<C extends Record<string, ParamField>>(\n codecs: C\n): ParamsDefinition<{ [K in keyof C]: InferParamField<C[K]> }> {\n type T = { [K in keyof C]: InferParamField<C[K]> };\n\n const resolvedCodecs: Record<string, Codec<unknown>> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n resolvedCodecs[key] = resolveField(key, value as ParamField);\n }\n\n // Validate that serialize doesn't return null\n validateSerialize(resolvedCodecs);\n\n // ---- parse ----\n function parse(raw: Record<string, string | string[]>): T {\n const result: Record<string, unknown> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const rawValue = raw[key];\n // Skip params not present in the URL at this route depth.\n // This supports TIM-834 inheritance: a parent params.ts can declare\n // codecs for descendant segments. At shallower depths those params\n // don't exist yet — skip them instead of passing undefined to the codec.\n if (rawValue === undefined) continue;\n result[key] = codec.parse(rawValue);\n }\n\n return result as T;\n }\n\n // ---- serialize ----\n function serialize(values: T): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const serialized = codec.serialize(values[key as keyof T] as unknown);\n if (serialized === null) {\n throw new Error(\n `[timber] params.serialize: field '${key}' serialized to null. ` +\n `Route params must produce a valid path segment.`\n );\n }\n result[key] = serialized;\n }\n\n return result;\n }\n\n // ---- get ----\n // ALS-backed: reads segment params from the current request context.\n // Server-only, sync.\n //\n // The pipeline already coerces params via coerceSegmentParams() which\n // calls parse() and stores typed values in ALS via setSegmentParams().\n // We return those directly instead of re-parsing, because codecs may\n // not be idempotent (e.g., a codec that only accepts raw strings would\n // throw if given an already-parsed value). See TIM-574.\n function get(): T {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] segmentParams.get() is server-only. ' +\n 'Use segmentParams.useSegmentParams() on the client.'\n );\n }\n const params = getSegmentParamsFromAls();\n // params are already coerced by the pipeline — return as-is.\n return params as unknown as T;\n }\n\n // ---- useSegmentParams ----\n // Client hook that reads typed params from the navigation context.\n // Delegates to the existing useSegmentParams hook from use-segment-params.ts.\n function useSegmentParams(): T {\n if (!_useSegmentParamsHook) {\n throw new Error(\n '[timber] segmentParams.useSegmentParams() requires @timber-js/app/client to be loaded. ' +\n 'This hook can only be used in client components.'\n );\n }\n const raw = _useSegmentParamsHook();\n // Params are already coerced by the server pipeline before being sent\n // to the client — return as-is. Re-parsing would break non-idempotent\n // codecs (e.g., z.coerce.number() on an already-parsed number). See TIM-574.\n return raw as unknown as T;\n }\n\n const definition: ParamsDefinition<T> = {\n parse,\n serialize,\n get,\n useSegmentParams,\n codecs: resolvedCodecs as { [K in keyof T]: Codec<T[K]> },\n };\n\n return definition;\n}\n"],"mappings":";;AA6BA,IAAI;AACJ,IAAI;;;;;;AAOJ,SAAgB,0BAA0B,MAAqD;AAC7F,yBAAwB;;AAO1B,IAAI,OAAO,WAAW,eAAe,CAAC,sBACpC,QAAO,oCAAA,MAAA,MAAA,EAAA,EAAA,CAAmC,MAAM,QAAQ;AACtD,KAAI,CAAC,sBACH,yBAAwB,IAAI;EAE9B;;;;;;AAQJ,SAAgB,uBAAuB,IAAmD;AACxF,uBAAsB;;AAGxB,SAAS,0BAA6D;AACpE,KAAI,CAAC,oBACH,OAAM,IAAI,MACR,oHAED;AAEH,QAAO,qBAAqB;;;;;;;;;AA2E9B,SAAS,gBAAmB,WAAmB,QAAuC;AACpF,QAAO;EACL,MAAM,OAAyC;GAI7C,MAAM,SAAS,aAAa,QAFd,MAAM,QAAQ,MAAM,GAAG,QAAQ,MAEH;AAC1C,OAAI,CAAC,OAAO,OACV,QAAO,OAAO;GAIhB,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;AAC/D,SAAM,IAAI,MAAM,mBAAmB,UAAU,qBAAqB,WAAW;;EAG/E,UAAU,OAAyB;AACjC,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;AAGT,OAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,IAAI;AAExB,UAAO,OAAO,MAAM;;EAEvB;;;;;AAMH,SAAS,aAAa,WAAmB,OAAmC;AAC1E,KAAI,QAAQ,MAAM,CAChB,QAAO;AAGT,KAAI,iBAAiB,MAAM,CACzB,QAAO,gBAAgB,WAAW,MAAM;AAG1C,OAAM,IAAI,MACR,wCAAwC,UAAU,uJAGnD;;;;;;AAOH,SAAS,kBAAkB,UAAgD;AACzE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,CAIjD,KAAI;EACF,MAAM,YAAY,MAAM,MAAM,OAAO;AAErC,MADmB,MAAM,UAAU,UAAU,KAC1B,KACjB,OAAM,IAAI,MACR,wCAAwC,IAAI,+IAG7C;UAEI,GAAG;AAIV,MAAI,aAAa,SAAS,EAAE,QAAQ,SAAS,gBAAgB,CAC3D,OAAM;;;;;;;;;;;;;;;;AAuBd,SAAgB,oBACd,QAC6D;CAG7D,MAAM,iBAAiD,EAAE;AAEzD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,gBAAe,OAAO,aAAa,KAAK,MAAoB;AAI9D,mBAAkB,eAAe;CAGjC,SAAS,MAAM,KAA2C;EACxD,MAAM,SAAkC,EAAE;AAE1C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,WAAW,IAAI;AAKrB,OAAI,aAAa,KAAA,EAAW;AAC5B,UAAO,OAAO,MAAM,MAAM,SAAS;;AAGrC,SAAO;;CAIT,SAAS,UAAU,QAAmC;EACpD,MAAM,SAAiC,EAAE;AAEzC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,aAAa,MAAM,UAAU,OAAO,KAA2B;AACrE,OAAI,eAAe,KACjB,OAAM,IAAI,MACR,qCAAqC,IAAI,uEAE1C;AAEH,UAAO,OAAO;;AAGhB,SAAO;;CAYT,SAAS,MAAS;AAChB,MAAI,OAAO,WAAW,YACpB,OAAM,IAAI,MACR,mGAED;AAIH,SAFe,yBAAyB;;CAQ1C,SAAS,mBAAsB;AAC7B,MAAI,CAAC,sBACH,OAAM,IAAI,MACR,0IAED;AAMH,SAJY,uBAAuB;;AAerC,QARwC;EACtC;EACA;EACA;EACA;EACA,QAAQ;EACT"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"metadata-routes-Cjmvi3rQ.js","names":[],"sources":["../../src/server/metadata-routes.ts"],"sourcesContent":["/**\n * Metadata route classification for timber.js.\n *\n * Metadata routes are file-based endpoints that generate well-known URLs for\n * crawlers and browsers (sitemap.xml, robots.txt, OG images, etc.).\n *\n * These routes run through proxy.ts but NOT through middleware.ts or access.ts —\n * they are public endpoints by nature.\n *\n * See design/16-metadata.md §\"Metadata Routes\"\n */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Classification of a metadata route file. */\nexport interface MetadataRouteInfo {\n /** The metadata route type. */\n type: MetadataRouteType;\n /** The content type to serve this route with. */\n contentType: string;\n /** Whether this route can appear in nested segments (not just app root). */\n nestable: boolean;\n}\n\nexport type MetadataRouteType =\n | 'sitemap'\n | 'robots'\n | 'manifest'\n | 'favicon'\n | 'icon'\n | 'opengraph-image'\n | 'twitter-image'\n | 'apple-icon';\n\n// ─── Convention Table ────────────────────────────────────────────────────────\n\n/**\n * All recognized metadata route file conventions.\n *\n * Each entry maps a base file name (without extension) to its route info.\n * The extensions determine whether the file is static or dynamic.\n *\n * Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg\n * Dynamic extensions: .ts, .tsx\n */\nexport const METADATA_ROUTE_CONVENTIONS: Record<\n string,\n {\n type: MetadataRouteType;\n contentType: string;\n nestable: boolean;\n staticExtensions: string[];\n dynamicExtensions: string[];\n /** The URL path this file serves at (relative to segment). */\n servePath: string;\n }\n> = {\n 'sitemap': {\n type: 'sitemap',\n contentType: 'application/xml',\n nestable: true,\n staticExtensions: ['xml'],\n dynamicExtensions: ['ts'],\n servePath: 'sitemap.xml',\n },\n 'robots': {\n type: 'robots',\n contentType: 'text/plain',\n nestable: false,\n staticExtensions: ['txt'],\n dynamicExtensions: ['ts'],\n servePath: 'robots.txt',\n },\n 'manifest': {\n type: 'manifest',\n contentType: 'application/manifest+json',\n nestable: false,\n staticExtensions: ['json'],\n dynamicExtensions: ['ts'],\n servePath: 'manifest.webmanifest',\n },\n 'favicon': {\n type: 'favicon',\n contentType: 'image/x-icon',\n nestable: false,\n staticExtensions: ['ico'],\n dynamicExtensions: [],\n servePath: 'favicon.ico',\n },\n 'icon': {\n type: 'icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg', 'svg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'icon',\n },\n 'opengraph-image': {\n type: 'opengraph-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'opengraph-image',\n },\n 'twitter-image': {\n type: 'twitter-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'twitter-image',\n },\n 'apple-icon': {\n type: 'apple-icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'apple-icon',\n },\n};\n\n// ─── MIME Type Resolution ─────────────────────────────────────────────────────\n\n/**\n * Map of file extensions to MIME types for static metadata route files.\n * Used to resolve the generic `image/*` content type for static image files.\n */\nconst EXTENSION_MIME_TYPES: Record<string, string> = {\n xml: 'application/xml',\n txt: 'text/plain',\n json: 'application/json',\n ico: 'image/x-icon',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n svg: 'image/svg+xml',\n webp: 'image/webp',\n};\n\n/**\n * Resolve the concrete MIME type for a static metadata route file.\n *\n * For generic content types like `image/*`, this resolves to the actual\n * MIME type based on the file extension (e.g. `image/png` for `.png`).\n *\n * @param conventionContentType - The content type from the convention table (may be generic like `image/*`)\n * @param extension - The file extension without leading dot (e.g. \"png\", \"xml\")\n * @returns The resolved MIME type\n */\nexport function resolveStaticContentType(conventionContentType: string, extension: string): string {\n if (conventionContentType.includes('*')) {\n return EXTENSION_MIME_TYPES[extension] ?? 'application/octet-stream';\n }\n return conventionContentType;\n}\n\n/**\n * Check if a file extension represents a static (non-code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"xml\", \"png\", \"ts\")\n * @returns true if this is a static file, false if dynamic or unrecognized\n */\nexport function isStaticMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.staticExtensions.includes(extension);\n}\n\n/**\n * Check if a file extension represents a dynamic (code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"ts\", \"tsx\")\n * @returns true if this is a dynamic file, false if static or unrecognized\n */\nexport function isDynamicMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.dynamicExtensions.includes(extension);\n}\n\n// ─── Classification ──────────────────────────────────────────────────────────\n\n/**\n * Classify a file name as a metadata route, or return null if it's not one.\n *\n * @param fileName - The full file name including extension (e.g. \"sitemap.xml\", \"icon.tsx\")\n * @returns Classification info, or null if not a metadata route\n */\nexport function classifyMetadataRoute(fileName: string): MetadataRouteInfo | null {\n const dotIndex = fileName.lastIndexOf('.');\n if (dotIndex === -1) return null;\n\n const baseName = fileName.slice(0, dotIndex);\n const ext = fileName.slice(dotIndex + 1);\n\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return null;\n\n const isStatic = convention.staticExtensions.includes(ext);\n const isDynamic = convention.dynamicExtensions.includes(ext);\n\n if (!isStatic && !isDynamic) return null;\n\n return {\n type: convention.type,\n contentType: convention.contentType,\n nestable: convention.nestable,\n };\n}\n\n/**\n * Get the serve path for a metadata route type.\n *\n * @param type - The metadata route type\n * @returns The URL path fragment this route serves at\n */\nexport function getMetadataRouteServePath(type: MetadataRouteType): string {\n for (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.type === type) return convention.servePath;\n }\n throw new Error(`[timber] Unknown metadata route type: ${type}`);\n}\n\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns An object with tag/attrs for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(\n type: MetadataRouteType,\n href: string\n): { rel: string; href: string; type?: string } | null {\n switch (type) {\n case 'icon':\n return { rel: 'icon', href };\n case 'apple-icon':\n return { rel: 'apple-touch-icon', href };\n case 'manifest':\n return { rel: 'manifest', href };\n default:\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;AA6CA,IAAa,6BAWT;CACF,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,UAAU;EACR,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,YAAY;EACV,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO;EAC1B,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,EAAE;EACrB,WAAW;EACZ;CACD,QAAQ;EACN,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB;GAAC;GAAO;GAAO;GAAM;EACvC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,mBAAmB;EACjB,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,iBAAiB;EACf,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,cAAc;EACZ,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACF;;;;;;;;AAyDD,SAAgB,2BAA2B,UAAkB,WAA4B;CACvF,MAAM,aAAa,2BAA2B;AAC9C,KAAI,CAAC,WAAY,QAAO;AACxB,QAAO,WAAW,kBAAkB,SAAS,UAAU;;;;;;;;AAWzD,SAAgB,sBAAsB,UAA4C;CAChF,MAAM,WAAW,SAAS,YAAY,IAAI;AAC1C,KAAI,aAAa,GAAI,QAAO;CAE5B,MAAM,WAAW,SAAS,MAAM,GAAG,SAAS;CAC5C,MAAM,MAAM,SAAS,MAAM,WAAW,EAAE;CAExC,MAAM,aAAa,2BAA2B;AAC9C,KAAI,CAAC,WAAY,QAAO;CAExB,MAAM,WAAW,WAAW,iBAAiB,SAAS,IAAI;CAC1D,MAAM,YAAY,WAAW,kBAAkB,SAAS,IAAI;AAE5D,KAAI,CAAC,YAAY,CAAC,UAAW,QAAO;AAEpC,QAAO;EACL,MAAM,WAAW;EACjB,aAAa,WAAW;EACxB,UAAU,WAAW;EACtB;;;;;;;;AASH,SAAgB,0BAA0B,MAAiC;AACzE,MAAK,MAAM,cAAc,OAAO,OAAO,2BAA2B,CAChE,KAAI,WAAW,SAAS,KAAM,QAAO,WAAW;AAElD,OAAM,IAAI,MAAM,yCAAyC,OAAO;;;;;;;;;;AAWlE,SAAgB,yBACd,MACA,MACqD;AACrD,SAAQ,MAAR;EACE,KAAK,OACH,QAAO;GAAE,KAAK;GAAQ;GAAM;EAC9B,KAAK,aACH,QAAO;GAAE,KAAK;GAAoB;GAAM;EAC1C,KAAK,WACH,QAAO;GAAE,KAAK;GAAY;GAAM;EAClC,QACE,QAAO"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolve-schema-Bf8eC6q6.js","names":[],"sources":["../../src/params/resolve-schema.ts"],"sourcesContent":["/**\n * Schema resolution — resolves the global schema's segmentParams into\n * a runtime-ready codec map keyed by bracket name (e.g. '[id]').\n *\n * Bracket keys are preserved so that `[id]`, `[...id]`, and `[[...id]]`\n * remain distinct entries. The coercion layer reconstructs bracket keys\n * from matched segment metadata to look up the correct codec.\n *\n * Design doc: design/41-global-params.md §Pipeline Integration\n */\n\nimport type { Codec } from '../codec.js';\nimport { isCodec, isStandardSchema, validateSync } from '../schema-bridge.js';\nimport type { StandardSchemaV1 } from '../schema-bridge.js';\n\n/**\n * Wrap a Standard Schema into a Codec for route params.\n * Parse throws on failure (invalid param → 404). Serialize returns string.\n */\nfunction fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n const result = validateSync(schema, value);\n if (!result.issues) {\n return result.value;\n }\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n serialize(value: T): string | null {\n if (value === null || value === undefined) return null;\n if (Array.isArray(value)) return value.join('/');\n return String(value);\n },\n };\n}\n\n/**\n * Resolve a schema entry to a Codec. Accepts Codec<T> or StandardSchemaV1<T>.\n */\nfunction resolveCodec(fieldName: string, value: unknown): Codec<unknown> {\n if (isCodec(value)) return value;\n if (isStandardSchema(value)) return fromParamSchema(fieldName, value);\n throw new Error(`[timber] Schema field '${fieldName}' is not a valid codec or Standard Schema.`);\n}\n\n/**\n * Reconstruct the bracket key for a segment from its type and param name.\n * Used by the coercion layer to look up codecs in the bracket-keyed map.\n *\n * 'dynamic' + 'id' → '[id]'\n * 'catch-all' + 'slug' → '[...slug]'\n * 'optional-catch-all' + 'slug' → '[[...slug]]'\n */\nexport function toBracketKey(segmentType: string, paramName: string): string {\n switch (segmentType) {\n case 'catch-all':\n return `[...${paramName}]`;\n case 'optional-catch-all':\n return `[[...${paramName}]]`;\n default:\n return `[${paramName}]`;\n }\n}\n\nconst CATCH_ALL_RE = /^\\[\\.\\.\\..+\\]$/;\nconst OPTIONAL_CATCH_ALL_RE = /^\\[\\[\\.\\.\\..+\\]\\]$/;\n\nfunction isCatchAllKey(key: string): boolean {\n return CATCH_ALL_RE.test(key) || OPTIONAL_CATCH_ALL_RE.test(key);\n}\n\n/**\n * Resolve the global schema's segmentParams into a map of bracket-keyed\n * param name → resolved Codec.\n *\n * Keys are preserved exactly as written in `app/schema.ts` so that\n * `[id]`, `[...id]`, and `[[...id]]` remain distinct entries.\n * The coercion layer in `param-coercion.ts` reconstructs bracket keys\n * from matched segment metadata to look up the correct codec.\n *\n * Input: { '[id]': z.coerce.number(), '[...slug]': codec.string }\n * Output: { '[id]': Codec<number>, '[...slug]': Codec<string> }\n */\nexport function resolveSchemaCodecs(\n segmentParams: Record<string, unknown>\n): Record<string, Codec<unknown>> {\n const resolved: Record<string, Codec<unknown>> = Object.create(null);\n\n for (const [bracketName, value] of Object.entries(segmentParams)) {\n if (isCatchAllKey(bracketName) && isCodec(value) && !('__catchAll' in value)) {\n throw new Error(\n `[timber] Schema error: '${bracketName}' is a catch-all segment and requires an array codec ` +\n `(e.g. codec.catchAll(codec.string)). Got a scalar codec.`\n );\n }\n resolved[bracketName] = resolveCodec(bracketName, value);\n }\n\n return resolved;\n}\n"],"mappings":";;;;;;AAmBA,SAAS,gBAAmB,WAAmB,QAAuC;AACpF,QAAO;EACL,MAAM,OAAyC;GAC7C,MAAM,SAAS,aAAa,QAAQ,MAAM;AAC1C,OAAI,CAAC,OAAO,OACV,QAAO,OAAO;GAEhB,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;AAC/D,SAAM,IAAI,MAAM,mBAAmB,UAAU,qBAAqB,WAAW;;EAE/E,UAAU,OAAyB;AACjC,OAAI,UAAU,QAAQ,UAAU,KAAA,EAAW,QAAO;AAClD,OAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,MAAM,KAAK,IAAI;AAChD,UAAO,OAAO,MAAM;;EAEvB;;;;;AAMH,SAAS,aAAa,WAAmB,OAAgC;AACvE,KAAI,QAAQ,MAAM,CAAE,QAAO;AAC3B,KAAI,iBAAiB,MAAM,CAAE,QAAO,gBAAgB,WAAW,MAAM;AACrE,OAAM,IAAI,MAAM,0BAA0B,UAAU,4CAA4C;;;;;;;;;;AAWlG,SAAgB,aAAa,aAAqB,WAA2B;AAC3E,SAAQ,aAAR;EACE,KAAK,YACH,QAAO,OAAO,UAAU;EAC1B,KAAK,qBACH,QAAO,QAAQ,UAAU;EAC3B,QACE,QAAO,IAAI,UAAU;;;AAI3B,IAAM,eAAe;AACrB,IAAM,wBAAwB;AAE9B,SAAS,cAAc,KAAsB;AAC3C,QAAO,aAAa,KAAK,IAAI,IAAI,sBAAsB,KAAK,IAAI;;;;;;;;;;;;;;AAelE,SAAgB,oBACd,eACgC;CAChC,MAAM,WAA2C,OAAO,OAAO,KAAK;AAEpE,MAAK,MAAM,CAAC,aAAa,UAAU,OAAO,QAAQ,cAAc,EAAE;AAChE,MAAI,cAAc,YAAY,IAAI,QAAQ,MAAM,IAAI,EAAE,gBAAgB,OACpE,OAAM,IAAI,MACR,2BAA2B,YAAY,+GAExC;AAEH,WAAS,eAAe,aAAa,aAAa,MAAM;;AAG1D,QAAO"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"walkers-DNp9tHyV.js","names":[],"sources":["../../src/routing/codegen-shared.ts","../../src/routing/link-codegen.ts","../../src/routing/codegen.ts","../../src/routing/interception.ts","../../src/routing/schema-validation.ts","../../src/routing/walkers.ts"],"sourcesContent":["/**\n * Shared codegen helpers — import-path computation, codec chain type\n * builder, and searchParams type formatter.\n *\n * Extracted from `codegen.ts` so `link-codegen.ts` can use the same\n * helpers without a cyclic import.\n */\n\nimport { relative, posix } from 'node:path';\nimport type { ParamEntry, RouteEntry } from './codegen-types.js';\n\n/**\n * Compute a relative import specifier for a codec/page file, stripping\n * the .ts/.tsx extension and resolving against the codegen output dir.\n */\nexport function codecImportPath(codecFilePath: string, importBase: string | undefined): string {\n const absPath = codecFilePath.replace(/\\.(ts|tsx)$/, '');\n if (importBase) {\n return './' + relative(importBase, absPath).replace(/\\\\/g, '/');\n }\n return './' + posix.basename(absPath);\n}\n\n/** Name of the shared helper type emitted at the top of the .d.ts. */\nexport const RESOLVE_SEGMENT_FIELD_TYPE_NAME = '_TimberResolveSegmentField';\n\n/**\n * Helper type emitted once at the top of the generated `.d.ts` and\n * referenced by every codec-chain conditional. Without this shared\n * helper, the inline expansion would duplicate the fallback branch on\n * every step and grow O(2^N) in chain depth (a single deep nested route\n * could blow up the file size and TS performance). With the helper,\n * each step reuses the named type and growth is O(N).\n *\n * The helper is a 2-arg conditional: given a typeof import expression\n * (`Def`), a key (`K`), and a fallback (`F`), it returns `T[K]` if\n * `Def extends ParamsDefinition<T>` and `K extends keyof T`, otherwise\n * `F`. The codec chain composes calls to this helper.\n */\nexport function emitResolveSegmentFieldHelper(): string {\n return [\n `type ${RESOLVE_SEGMENT_FIELD_TYPE_NAME}<Def, K extends string, F> =`,\n ` Def extends import('@timber-js/app/segment-params').ParamsDefinition<infer T>`,\n ` ? K extends keyof T ? T[K] : F`,\n ` : F;`,\n ].join('\\n');\n}\n\n/**\n * Build a TypeScript type expression that resolves a single param's\n * codec by walking a chain of params.ts files in priority order.\n *\n * Each entry in the chain emits one application of the shared\n * `_TimberResolveSegmentField` helper type. Composing N applications\n * grows linearly with chain depth (O(N) characters), unlike an inline\n * conditional that would duplicate the fallback in each branch and\n * grow O(2^N).\n *\n * The closest match (position 0 in the chain) is checked first; if its\n * `segmentParams` definition declares the key, its inferred type wins.\n * Otherwise we fall through to the next entry, and finally to the\n * provided fallback. See TIM-834.\n */\nexport function buildCodecChainType(\n p: ParamEntry,\n importBase: string | undefined,\n fallback: string\n): string {\n const files = p.codecFilePaths;\n if (!files || files.length === 0) return fallback;\n const key = JSON.stringify(p.name);\n // Compose helper applications inside-out so the closest entry\n // (files[0]) ends up as the OUTERMOST application. Each application\n // adds a constant-size wrapper around the running fallback.\n let inner = fallback;\n for (let i = files.length - 1; i >= 0; i--) {\n const importPath = codecImportPath(files[i], importBase);\n inner = `${RESOLVE_SEGMENT_FIELD_TYPE_NAME}<(typeof import('${importPath}'))['segmentParams'], ${key}, ${inner}>`;\n }\n return inner;\n}\n\n/**\n * Build a TypeScript type expression for a single param using the global\n * schema's InferCodecType. Falls back to `fallback` when schema doesn't\n * declare this param.\n *\n * Generates: `InferCodecType<typeof schema.segmentParams['[paramName]']>`\n */\nexport function buildSchemaParamType(p: ParamEntry): string {\n return `InferCodecType<typeof schema.segmentParams[${JSON.stringify(p.bracketName)}]>`;\n}\n\n/**\n * Format the searchParams type for a route entry.\n *\n * When a page.tsx (or params.ts) exports searchParams, we reference its\n * inferred type via an import type. The import path is relative to\n * `importBase` (the directory where the .d.ts will be written). When\n * importBase is undefined, falls back to a bare relative path.\n */\nexport function formatSearchParamsType(route: RouteEntry, importBase?: string): string {\n if (route.hasSearchParams && route.searchParamsPagePath) {\n const importPath = codecImportPath(route.searchParamsPagePath, importBase);\n // Extract the type from the named 'searchParams' export of the page module.\n return `(typeof import('${importPath}'))['searchParams'] extends import('@timber-js/app/search-params').SearchParamsDefinition<infer T> ? T : never`;\n }\n return '{}';\n}\n","/**\n * Typed `<Link>` codegen — interface augmentation generation.\n *\n * Extracted from `codegen.ts` to keep that file under the project's\n * 500-line cap. This module owns everything that emits the\n * `interface LinkFunction { ... }` augmentation blocks in the generated\n * `.timber/timber-routes.d.ts`.\n *\n * Two augmentation blocks are emitted:\n *\n * 1. **Per-route discriminated union** (`formatTypedLinkOverloads`) —\n * one call signature whose props is a union keyed on `href`. TS\n * narrows by literal href and reports prop errors against the\n * matched variant. See TIM-835 and `design/09-typescript.md`.\n *\n * 2. **Catch-all overloads** (`formatLinkCatchAllOverloads`) — external\n * href literals (`http://`, `mailto:`, etc.) and a computed-string\n * `<H extends string>` signature for runtime-computed paths.\n *\n * Block ordering is critical for error UX: per-route is emitted FIRST\n * so that, after TS's \"later overload set ordered first\" merge rule,\n * the discriminated union ends up LAST in resolution order — the\n * overload TS reports against on failure.\n */\n\nimport type { ParamEntry, RouteEntry } from './codegen-types.js';\nimport {\n buildCodecChainType,\n buildSchemaParamType,\n formatSearchParamsType,\n} from './codegen-shared.js';\n\n/** Shared Link base-props type literal used in every emitted call signature. */\nexport const LINK_BASE_PROPS_TYPE =\n \"Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & { prefetch?: boolean; scroll?: boolean; preserveSearchParams?: true | string[]; onNavigate?: import('./client/link.js').OnNavigateHandler; children?: import('react').ReactNode }\";\n\n/**\n * Build a TypeScript template literal pattern for a dynamic route.\n * e.g. '/products/[id]' → '/products/${string}'\n * '/blog/[...slug]' → '/blog/${string}'\n * '/docs/[[...path]]' → '/docs/${string}' (also matches /docs)\n * '/[org]/[repo]' → '/${string}/${string}'\n */\nexport function buildResolvedPattern(route: RouteEntry): string | null {\n const parts = route.urlPath.split('/');\n const templateParts = parts.map((part) => {\n if (part.startsWith('[[...') && part.endsWith(']]')) {\n // Optional catch-all — matches any trailing path\n return '${string}';\n }\n if (part.startsWith('[...') && part.endsWith(']')) {\n // Catch-all — matches one or more segments\n return '${string}';\n }\n if (part.startsWith('[') && part.endsWith(']')) {\n // Dynamic segment\n return '${string}';\n }\n return part;\n });\n return templateParts.join('/');\n}\n\n/**\n * Format the segmentParams type for Link overloads.\n *\n * Link params accept `string | number` for single dynamic segments\n * (convenience — values are stringified at runtime). Catch-all and\n * optional catch-all remain `string[]` / `string[] | undefined`.\n *\n * When the segment's params chain (TIM-834) declares a typed codec, the\n * inferred type from the codec wins via a nested conditional.\n */\nexport function formatLinkParamsType(\n params: ParamEntry[],\n importBase?: string,\n hasSchema?: boolean\n): string {\n if (params.length === 0) {\n return '{}';\n }\n\n const fields = params.map((p) => {\n const fallback = p.type === 'string' ? 'string | number' : p.type;\n const codecType = hasSchema\n ? buildSchemaParamType(p)\n : buildCodecChainType(p, importBase, fallback);\n return `${p.name}: ${codecType}`;\n });\n return `{ ${fields.join('; ')} }`;\n}\n\n/**\n * Catch-all call signatures for `<Link>` — external hrefs and computed\n * `string` variables. Emitted from codegen in a SEPARATE\n * `declare module` block (declared AFTER the per-route block) so the TS\n * \"later overload set ordered first\" rule places these catch-all\n * signatures ahead of per-route in resolution order, leaving per-route\n * as the final overload whose error message is reported on failure.\n *\n * The conditional `string extends H ? ... : never` protection preserves\n * TIM-624's guarantee that unknown internal path literals don't match\n * the catch-all — typos like `<Link href=\"/typo\" />` still error.\n */\nexport function formatLinkCatchAllOverloads(): string[] {\n const lines: string[] = [];\n const baseProps = LINK_BASE_PROPS_TYPE;\n\n // TIM-830: the catch-all signatures accept EITHER the legacy\n // `{ definition, values }` wrapper OR a flat `Record<string, unknown>`\n // values object. External/computed hrefs can't be looked up in the\n // runtime registry, so the wrapped form is still the reliable path;\n // the flat form is kept permissive for callers migrating from typed-\n // route hrefs to computed strings. `resolveHref` discriminates at\n // runtime via the presence of a `definition` key.\n const catchAllSearchParams =\n '{ definition: SearchParamsDefinition<Record<string, unknown>>; values: Record<string, unknown> } | Record<string, unknown>';\n\n // ExternalHref inlined here rather than referenced as an exported\n // alias so the generated .d.ts stands alone without source imports.\n const externalHref =\n '`http://${string}` | `https://${string}` | `mailto:${string}` | `tel:${string}` | `ftp://${string}` | `//${string}` | `#${string}` | `?${string}`';\n\n lines.push(' // Typed Link overloads — catch-all (block 2 / emitted second)');\n lines.push(' interface LinkFunction {');\n\n // (1) External/literal-protocol hrefs.\n //\n // TIM-833: `segmentParams` is permissively typed as\n // `Record<string, unknown>` (not `never`) so generic wrappers that\n // forward a `LinkProps`-shaped object can compile when the wrapper\n // happens to bottom out at an external href. There are no dynamic\n // segments to interpolate for an external href, so this prop is\n // ignored at runtime — accepting an open-shape object instead of\n // forbidding it removes a footgun without changing behavior.\n lines.push(` (props: ${baseProps} & {`);\n lines.push(` href: ${externalHref}`);\n lines.push(` segmentParams?: Record<string, unknown>`);\n lines.push(` searchParams?: ${catchAllSearchParams}`);\n lines.push(` }): import('react').JSX.Element`);\n\n // (2) Computed/variable href — non-literal `string` only.\n //\n // `string extends H` is true only when H is the wide `string` type,\n // not a specific literal. For literal hrefs, this overload must NOT\n // be selectable so the per-route discriminated union (block 1) is\n // the resolution target.\n //\n // TIM-833 follow-up: the previous shape used `string extends H ? {...} : never`\n // on the WHOLE props type. For literal H, that collapsed `props` to\n // `never`, and JSX type inference then read `(never).children` as\n // `never` — producing the misleading\n // `'children' prop expects type 'never' which requires multiple\n // children, but only a single child was provided` (TS2745) error,\n // which buried the actual prop-mismatch message under a noise diagnostic.\n //\n // Fix: move the `: never` from the whole props onto just the `href`\n // field. The overload remains uncallable for literal H (since `\"/x\"`\n // is not assignable to `never`), but `children` retains its real\n // `ReactNode` type so JSX inference no longer collapses to never.\n // The diagnostic surface becomes: TS reports the per-route block's\n // error chain (the helpful \"Type 'number' is not assignable to type\n // 'string'\" message) WITHOUT a leading TS2745 noise line.\n lines.push(` <H extends string>(`);\n lines.push(` props: ${baseProps} & {`);\n lines.push(` href: string extends H ? H : never`);\n lines.push(` segmentParams?: Record<string, string | number | string[]>`);\n lines.push(` searchParams?: ${catchAllSearchParams}`);\n lines.push(` }`);\n lines.push(` ): import('react').JSX.Element`);\n\n lines.push(' }');\n return lines;\n}\n\n/**\n * Generate typed per-route Link call signatures via LinkFunction\n * interface merging.\n *\n * TIM-835: This emits a SINGLE call signature whose props is a\n * discriminated union keyed on `href`. TypeScript narrows the union by\n * the literal `href` at the call site, then checks the rest of the\n * props against the matched variant. When `segmentParams` or\n * `searchParams` is wrong, TS reports the error against the matched\n * variant — naming the user's actual `href` and the offending field.\n *\n * Before TIM-835, this function emitted N separate per-route overloads\n * (one per route, sometimes two for dynamic routes). When ALL overloads\n * failed, TS would pick an arbitrary failed overload (heuristically the\n * \"last tried\") to render the diagnostic, often pointing at a route\n * completely unrelated to the one the user wrote. The discriminated\n * union sidesteps overload-resolution heuristics entirely.\n *\n * Open property shapes (`Record<string, unknown>` instead of `never`)\n * for `segmentParams` on routes that don't declare them keep generic\n * `LinkProps`-spreading wrappers compiling. (Aligned with TIM-833.)\n *\n * TIM-832: this function still emits the per-route block FIRST and the\n * catch-all block follows, so per-route remains the LAST overload set in\n * resolution order — the one TS reports against on failure. With a\n * discriminated union there is only one call signature in this block,\n * so the resolved-template-vs-pattern ordering reduces to placing the\n * pattern variant before the resolved-template variant inside the union.\n */\nexport function formatTypedLinkOverloads(\n routes: RouteEntry[],\n importBase?: string,\n hasSchema?: boolean\n): string[] {\n const lines: string[] = [];\n const baseProps = LINK_BASE_PROPS_TYPE;\n\n // Build the union variants. Each route contributes one variant for the\n // pattern href (e.g. '/products/[id]') and, for dynamic routes, an\n // additional variant for the resolved-template href (e.g.\n // `/products/${string}`).\n //\n // The PATTERN variant must be listed BEFORE the resolved-template\n // variant for the same route so that when the user writes the literal\n // pattern href (`<Link href=\"/products/[id]\" />`), TS narrows to the\n // pattern variant (which carries the typed `segmentParams` shape)\n // rather than the looser resolved-template variant. Without this\n // ordering, TS would silently match the resolved-template variant\n // first — swallowing typed-segmentParams type errors.\n const variants: string[] = [];\n for (const route of routes) {\n const hasDynamicParams = route.params.length > 0;\n // For routes with no dynamic params, accept an absent OR open-shape\n // segmentParams. `Record<string, unknown>` keeps generic spreads\n // compiling and gives a readable error if a non-object is passed.\n const paramsProp = hasDynamicParams\n ? `segmentParams: ${formatLinkParamsType(route.params, importBase, hasSchema)}`\n : 'segmentParams?: Record<string, unknown>';\n\n const searchParamsType = route.hasSearchParams\n ? formatSearchParamsType(route, importBase)\n : null;\n // TIM-830: pattern href uses the FLAT `Partial<T>` shape — the\n // runtime registry is keyed by the un-interpolated pattern.\n //\n // TIM-833: when the route has NO searchParams definition, use\n // `Record<string, unknown>` (open shape) instead of\n // `Record<string, never>` so generic `LinkProps`-spreading wrappers\n // compile. The trade-off vs the original TIM-835 strict shape is\n // that excess searchParams keys on a no-def route no longer raise\n // a type error — but those keys are runtime no-ops anyway, and\n // wrapper compatibility is the more common pain point. Routes WITH\n // a searchParams definition still get strict `Partial<T>` typing.\n const patternSearchParamsProp = searchParamsType\n ? `searchParams?: Partial<${searchParamsType}>`\n : 'searchParams?: Record<string, unknown>';\n\n // Pattern variant FIRST (more specific).\n variants.push(\n `${baseProps} & { href: '${route.urlPath}'; ${paramsProp}; ${patternSearchParamsProp} }`\n );\n\n // Resolved-template variant SECOND (looser, matches interpolated hrefs).\n if (hasDynamicParams) {\n const templatePattern = buildResolvedPattern(route);\n if (templatePattern) {\n // TIM-830: resolved-template href keeps the WRAPPED\n // `{ definition, values }` shape — the registry can't be looked\n // up by an already-interpolated href.\n //\n // TIM-833: searchParams falls back to `Record<string, unknown>`\n // (was `Record<string, never>`) when the route has no\n // definition, mirroring the pattern-variant change above. The\n // resolved-template `segmentParams` is intentionally KEPT as\n // `Record<string, never>` below to preserve TIM-835's\n // pattern-variant typo detection: loosening segmentParams here\n // would let the resolved-template variant shadow the pattern\n // variant on literal pattern hrefs and silently swallow keyed\n // typos like `<Link href=\"/[id]\" segmentParams={{ idz: 1 }} />`.\n const resolvedSearchParamsProp = searchParamsType\n ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }`\n : 'searchParams?: Record<string, unknown>';\n // TIM-835: `Record<string, never>` for segmentParams forbids ANY\n // provided keys on the resolved-template variant. Without this,\n // the resolved-template would shadow the pattern variant for\n // literal pattern hrefs (`<Link href=\"/products/[id]\" segmentParams={...} />`)\n // because both variants accept the literal `'/products/[id]'`\n // and the looser variant would silently swallow segmentParams\n // type errors.\n variants.push(\n `${baseProps} & { href: \\`${templatePattern}\\`; segmentParams?: Record<string, never>; ${resolvedSearchParamsProp} }`\n );\n }\n }\n }\n\n lines.push(' interface LinkFunction {');\n if (variants.length === 0) {\n // No page routes — emit nothing. The catch-all block in the second\n // augmentation still provides external/computed-string signatures.\n lines.push(' }');\n return lines;\n }\n lines.push(' (');\n lines.push(' props:');\n for (const variant of variants) {\n lines.push(` | (${variant})`);\n }\n lines.push(` ): import('react').JSX.Element`);\n lines.push(' }');\n\n return lines;\n}\n","/**\n * Route map codegen.\n *\n * Walks the scanned RouteTree and generates a TypeScript declaration file\n * mapping every route to its params and searchParams shapes.\n *\n * This runs at build time and in dev (regenerated on file changes).\n * No runtime overhead — purely static type generation.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport type { RouteTree, SegmentNode } from './types.js';\nimport type { ParamEntry, RouteEntry } from './codegen-types.js';\nimport {\n buildCodecChainType,\n buildSchemaParamType,\n emitResolveSegmentFieldHelper,\n formatSearchParamsType,\n} from './codegen-shared.js';\nimport { formatLinkCatchAllOverloads, formatTypedLinkOverloads } from './link-codegen.js';\n\n/** Options for route map generation. */\nexport interface CodegenOptions {\n /** Absolute path to the app/ directory. Required for page searchParams detection. */\n appDir?: string;\n /**\n * Absolute path to the directory where the .d.ts file will be written.\n * Used to compute correct relative import paths for page files.\n * Defaults to appDir when not provided (preserves backward compat for tests).\n */\n outputDir?: string;\n /**\n * Whether a global schema (app/schema.ts) exists.\n * Auto-detected from appDir when not explicitly set.\n */\n hasSchema?: boolean;\n}\n\n/**\n * Generate a TypeScript declaration file string from a scanned route tree.\n *\n * The output is a `declare module '@timber-js/app'` block containing the Routes\n * interface that maps every route path to its params and searchParams shape.\n */\nexport function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}): string {\n // Auto-detect schema from appDir when not explicitly set\n const hasSchema = options.hasSchema ?? detectSchema(options.appDir);\n\n const routes: RouteEntry[] = [];\n collectRoutes(tree.root, [], [], '', routes, hasSchema);\n\n // Sort routes alphabetically for deterministic output\n routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));\n\n // When outputDir differs from appDir, import paths must be relative to outputDir\n const importBase = options.outputDir ?? options.appDir;\n\n return formatDeclarationFile(routes, importBase, hasSchema, options.appDir);\n}\n\n/** Schema file extensions to check, in priority order. */\nconst SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n\n/**\n * Detect whether app/schema.{ts,tsx,js,jsx} exists.\n */\nfunction detectSchema(appDir: string | undefined): boolean {\n if (!appDir) return false;\n return SCHEMA_EXTENSIONS.some((ext) => existsSync(join(appDir, `schema${ext}`)));\n}\n\n/**\n * Recursively walk the segment tree and collect route entries.\n *\n * A route entry is created for any segment that has a `page` or `route` file.\n * Params accumulate from ancestor dynamic segments.\n */\nfunction collectRoutes(\n node: SegmentNode,\n ancestorParams: ParamEntry[],\n ancestorParamsFiles: string[],\n parentTreePath: string,\n routes: RouteEntry[],\n hasSchema: boolean\n): void {\n // Build the tree path (includes groups, slots — uniquely identifies this segment).\n // Root node has empty segmentName, producing treePath = ''.\n const treePath = node.segmentName ? `${parentTreePath}/${node.segmentName}` : parentTreePath;\n // TIM-834: Identify this segment's own params.ts (if it has a\n // segmentParams export). The full chain of params.ts files in the\n // route ancestry is threaded down via `ancestorParamsFiles`; codec\n // resolution for each ParamEntry is deferred until leaf time so that\n // descendant params.ts files can override ancestor codecs (closest-\n // to-leaf wins, matching the runtime semantics of\n // coerceSegmentParams which walks segments top-down and overwrites\n // earlier coercions).\n const ownParamsFile =\n node.params && fileHasExport(node.params.filePath, 'segmentParams')\n ? node.params.filePath\n : undefined;\n\n // Accumulate params from this segment. We attach `codecFilePaths`\n // later (at leaf time) using the FULL chain so descendant overrides\n // are visible. The legacy layout/page fallback is recorded now\n // because it is per-segment (and does not participate in inheritance).\n const params = [...ancestorParams];\n if (node.paramName) {\n const legacyFallback = ownParamsFile ? undefined : findLegacyParamsExport(node);\n params.push({\n name: node.paramName,\n bracketName: node.segmentName,\n type: paramTypeForSegment(node.segmentType),\n // Codec chain populated at leaf time. We carry the per-segment\n // legacy fallback (if any) so leaf-time resolution can fall back\n // to it when no params.ts in the chain declares this key.\n legacyCodecFilePath: legacyFallback,\n });\n }\n\n // Extend the chain for descendants of this segment.\n const nextAncestorFiles = ownParamsFile\n ? [...ancestorParamsFiles, ownParamsFile]\n : ancestorParamsFiles;\n\n // Check if this segment is a leaf route (has page or route file)\n const isPage = !!node.page;\n const isApiRoute = !!node.route;\n\n if (isPage || isApiRoute) {\n // TIM-834 P1 fix: at LEAF time, the full chain of params.ts files\n // (root-to-leaf) is known. Resolve every ParamEntry's\n // `codecFilePaths` to the chain in LEAF-FIRST order so the\n // closest-to-leaf entry is checked first — matching runtime\n // closest-wins semantics. The chain is shared by all params in the\n // route, so we compute it once.\n const leafFirstChain = nextAncestorFiles.length > 0 ? [...nextAncestorFiles].reverse() : [];\n const resolvedParams: ParamEntry[] = params.map((p) => {\n const codecFilePaths =\n leafFirstChain.length > 0\n ? leafFirstChain\n : p.legacyCodecFilePath\n ? [p.legacyCodecFilePath]\n : undefined;\n return {\n name: p.name,\n bracketName: p.bracketName,\n type: p.type,\n codecFilePaths,\n };\n });\n\n const entry: RouteEntry = {\n urlPath: node.urlPath,\n segmentPath: treePath || '/',\n params: resolvedParams,\n hasSearchParams: false,\n isApiRoute,\n hasSchema,\n };\n\n // Detect searchParams export from params.ts (primary) or page.tsx (fallback)\n if (isPage) {\n if (node.params && fileHasExport(node.params.filePath, 'searchParams')) {\n entry.hasSearchParams = true;\n entry.searchParamsPagePath = node.params.filePath;\n } else if (node.page && fileHasExport(node.page.filePath, 'searchParams')) {\n entry.hasSearchParams = true;\n entry.searchParamsPagePath = node.page.filePath;\n }\n }\n\n routes.push(entry);\n }\n\n // Recurse into children\n for (const child of node.children) {\n collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema);\n }\n\n // Recurse into slots (they share the parent's URL path, but may have their own pages)\n for (const slot of Object.values(node.slots)) {\n collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema);\n }\n}\n\n/**\n * Determine the TypeScript type for a segment's param.\n */\nfunction paramTypeForSegment(segmentType: string): ParamEntry['type'] {\n switch (segmentType) {\n case 'catch-all':\n return 'string[]';\n case 'optional-catch-all':\n return 'string[] | undefined';\n default:\n return 'string';\n }\n}\n\n/**\n * Check if a file exports a specific named export.\n *\n * Uses a lightweight regex check — not full AST parsing. The actual type\n * extraction happens via the TypeScript compiler in the generated .d.ts.\n */\nfunction fileHasExport(filePath: string, exportName: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const content = readFileSync(filePath, 'utf-8');\n const e = exportName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return (\n new RegExp(`export\\\\s+(const|let|var)\\\\s+${e}\\\\b`).test(content) ||\n new RegExp(`export\\\\s*\\\\{[^}]*\\\\b${e}\\\\b[^}]*\\\\}`).test(content)\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Find a legacy `segmentParams` export on layout.tsx or page.tsx.\n *\n * Backward-compat shim: TIM-508 made params.ts the canonical location\n * for `segmentParams`. Layout/page exports are still accepted for the\n * OWN segment only (not inherited by descendants — see TIM-834).\n */\nfunction findLegacyParamsExport(node: SegmentNode): string | undefined {\n if (node.layout && fileHasExport(node.layout.filePath, 'segmentParams')) {\n return node.layout.filePath;\n }\n if (node.page && fileHasExport(node.page.filePath, 'segmentParams')) {\n return node.page.filePath;\n }\n return undefined;\n}\n\n/**\n * Emit the schema-based type exports: TimberSegmentParams, TimberRoute,\n * AllSegmentParams, RoutesWithParams.\n *\n * These types use InferCodecType references so they stay in sync with\n * the schema without hardcoding resolved types.\n *\n * Design doc: design/41-global-params.md §Codegen Output\n */\nfunction formatSchemaTypes(routes: RouteEntry[]): string[] {\n const lines: string[] = [];\n\n lines.push('/** Union of all valid route patterns. */');\n lines.push('export type TimberRoute = keyof TimberSegmentParams;');\n lines.push('');\n lines.push('/** Maps each segment path to its accumulated segment params. */');\n lines.push('export interface TimberSegmentParams {');\n\n for (const route of routes) {\n if (route.params.length === 0) {\n lines.push(` '${route.segmentPath}': {};`);\n } else {\n lines.push(` '${route.segmentPath}': {`);\n for (const p of route.params) {\n lines.push(` ${p.name}: ${buildSchemaParamType(p)};`);\n }\n lines.push(' };');\n }\n }\n\n lines.push('}');\n lines.push('');\n\n // AllSegmentParams — flat union of all param types across all routes\n lines.push('/** All segment params (union of all route params). */');\n lines.push('export type AllSegmentParams = {');\n lines.push(\n ' [K in keyof TimberSegmentParams as TimberSegmentParams[K] extends Record<string, never>'\n );\n lines.push(' ? never');\n lines.push(' : K]: TimberSegmentParams[K];');\n lines.push('} extends infer U');\n lines.push(' ? { [K in keyof U[keyof U]]: U[keyof U][K] }');\n lines.push(' : never;');\n lines.push('');\n\n // RoutesWithParams — filter routes by required param keys\n lines.push(\n '/** Filters TimberRoute to routes whose accumulated params include all listed keys. */'\n );\n lines.push('export type RoutesWithParams<K extends keyof AllSegmentParams> = {');\n lines.push(' [R in TimberRoute]: K extends keyof TimberSegmentParams[R] ? R : never;');\n lines.push('}[TimberRoute];');\n\n return lines;\n}\n\n/**\n * Format the collected routes into a TypeScript declaration file.\n */\nfunction formatDeclarationFile(\n routes: RouteEntry[],\n importBase?: string,\n hasSchema?: boolean,\n appDir?: string\n): string {\n const lines: string[] = [];\n\n lines.push('// This file is auto-generated by timber.js route map codegen.');\n lines.push('// Do not edit manually. Regenerated on build and in dev mode.');\n lines.push('');\n // export {} makes this file a module, so all declare module blocks are\n // augmentations rather than ambient replacements. Without this, the\n // declare module blocks would replace the original module types entirely\n // (removing exports like bindUseQueryStates that aren't listed here).\n lines.push('export {};');\n lines.push('');\n\n if (hasSchema) {\n // Schema-based codegen: import schema and InferCodecType\n // Compute the schema import path relative to the output directory\n const schemaImport =\n appDir && importBase\n ? './' + relative(importBase, join(appDir, 'schema')).replace(/\\\\/g, '/')\n : '../app/schema';\n lines.push(`import type schema from '${schemaImport}';`);\n lines.push(\"import type { InferCodecType, SegmentParamCodec } from '@timber-js/app/params';\");\n lines.push('');\n // Collect all unique bracket keys from routes for key validation\n const allBracketKeys = new Set<string>();\n for (const route of routes) {\n for (const p of route.params) {\n allBracketKeys.add(p.bracketName);\n }\n }\n\n // Augment the ValidDynamicSegmentKeysRegistry so defineSchema constrains keys\n // to known filesystem segments. Unknown keys produce inline TS errors.\n if (allBracketKeys.size > 0) {\n lines.push(\"declare module '@timber-js/app/params' {\");\n lines.push(' interface ValidDynamicSegmentKeysRegistry {');\n for (const key of [...allBracketKeys].sort()) {\n lines.push(` ${JSON.stringify(key)}: true;`);\n }\n lines.push(' }');\n lines.push('}');\n lines.push('');\n }\n // Type-level validation: each schema value must be a Codec or Standard Schema.\n // Running `pnpm run typecheck` will surface errors for invalid values.\n lines.push('type _AssertSchemaValues = {');\n lines.push(\n \" [K in keyof typeof schema['segmentParams']]: typeof schema['segmentParams'][K] extends SegmentParamCodec ? true : ['Error: value for', K, 'is not a valid Codec or Standard Schema'];\"\n );\n lines.push('};');\n lines.push('');\n }\n\n if (!hasSchema) {\n // TIM-834 P2: emit the shared codec-resolution helper type ONCE so the\n // per-param chain conditionals reference it instead of inlining the\n // fallback in both branches (which grows O(2^N) in chain depth).\n lines.push(emitResolveSegmentFieldHelper());\n lines.push('');\n }\n\n lines.push(\"declare module '@timber-js/app' {\");\n lines.push(' interface Routes {');\n\n for (const route of routes) {\n const paramsType = formatParamsType(route.params, importBase, hasSchema);\n const searchParamsType = formatSearchParamsType(route, importBase);\n\n lines.push(` '${route.urlPath}': {`);\n lines.push(` segmentParams: ${paramsType}`);\n lines.push(` searchParams: ${searchParamsType}`);\n lines.push(` }`);\n }\n\n lines.push(' }');\n\n // When a global schema exists, emit segment param types inside the\n // @timber-js/app module augmentation so that declare module blocks\n // for @timber-js/app/server and @timber-js/app/client can reference\n // AllSegmentParams via `import type { AllSegmentParams } from '@timber-js/app'`.\n if (hasSchema) {\n lines.push('');\n lines.push(...formatSchemaTypes(routes).map((l) => ` ${l}`));\n }\n\n lines.push('}');\n lines.push('');\n\n const pageRoutes = routes.filter((r) => !r.isApiRoute);\n const dynamicRoutes = routes.filter((r) => r.params.length > 0);\n\n // Generate @timber-js/app/server augmentation — typed getSegmentParams\n if (dynamicRoutes.length > 0) {\n lines.push(\"declare module '@timber-js/app/server' {\");\n if (hasSchema) {\n lines.push(\" import type { AllSegmentParams } from '@timber-js/app'\");\n }\n for (const route of dynamicRoutes) {\n const paramsType = formatParamsType(route.params, importBase, hasSchema);\n lines.push(\n ` export function getSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`\n );\n }\n lines.push(\n hasSchema\n ? ' export function getSegmentParams(): Partial<AllSegmentParams>'\n : ' export function getSegmentParams(): Record<string, string | string[]>'\n );\n lines.push('}');\n lines.push('');\n }\n\n // Generate overloads for @timber-js/app/client\n\n if (dynamicRoutes.length > 0 || pageRoutes.length > 0) {\n lines.push(\"declare module '@timber-js/app/client' {\");\n lines.push(\n \" import type { SearchParamsDefinition, SetParams, QueryStatesOptions, SearchParamCodec } from '@timber-js/app/search-params'\"\n );\n if (hasSchema) {\n lines.push(\" import type { AllSegmentParams } from '@timber-js/app'\");\n }\n lines.push('');\n\n // useSegmentParams overloads\n if (dynamicRoutes.length > 0) {\n for (const route of dynamicRoutes) {\n const paramsType = formatParamsType(route.params, importBase, hasSchema);\n lines.push(\n ` export function useSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`\n );\n }\n lines.push(\n hasSchema\n ? ' export function useSegmentParams(): Partial<AllSegmentParams>'\n : ' export function useSegmentParams(): Record<string, string | string[]>'\n );\n lines.push('');\n }\n\n // useQueryStates overloads\n if (pageRoutes.length > 0) {\n lines.push(...formatUseQueryStatesOverloads(pageRoutes, importBase));\n lines.push('');\n }\n\n // Typed Link overloads — per-route with DIRECT types (no conditionals).\n // Direct types preserve TypeScript's excess property checking.\n //\n // TIM-832: per-route and catch-all are emitted as TWO separate\n // augmentation blocks. Per TS's merging rule \"later overload sets\n // ordered first\", the catch-all block (declared SECOND in this file)\n // ends up FIRST in the merged call-signature list at resolution time,\n // which puts the per-route block LAST — so its error message is the\n // one TypeScript reports when no overload matches. This gives users a\n // clear prop-mismatch error (e.g. \"'string | undefined' is not\n // assignable to 'string | number' on id\") instead of the old\n // confusing \"Type 'string' is not assignable to type 'never'\" cascade.\n if (pageRoutes.length > 0) {\n lines.push(' // Typed Link overloads — per-route (block 1 / emitted first)');\n lines.push(...formatTypedLinkOverloads(pageRoutes, importBase, hasSchema));\n lines.push('');\n }\n\n lines.push('}');\n lines.push('');\n }\n\n // TIM-832: catch-all block — emitted as a SEPARATE `declare module`\n // augmentation so TS's \"later overload set first\" rule orders it ahead\n // of the per-route block above at resolution time, leaving per-route as\n // the \"last overload\" whose error TypeScript reports.\n lines.push(\"declare module '@timber-js/app/client' {\");\n lines.push(\" import type { SearchParamsDefinition } from '@timber-js/app/search-params'\");\n lines.push('');\n lines.push(...formatLinkCatchAllOverloads());\n lines.push('}');\n lines.push('');\n\n return lines.join('\\n');\n}\n\n/**\n * Format the params type for a route entry.\n */\nfunction formatParamsType(params: ParamEntry[], importBase?: string, hasSchema?: boolean): string {\n if (params.length === 0) {\n return '{}';\n }\n\n const fields = params.map((p) => {\n const codecType = hasSchema\n ? buildSchemaParamType(p)\n : buildCodecChainType(p, importBase, p.type);\n return `${p.name}: ${codecType}`;\n });\n return `{ ${fields.join('; ')} }`;\n}\n\n/**\n * Generate useQueryStates overloads.\n *\n * For each page route:\n * - Routes with search-params.ts get a typed overload returning the inferred T\n * - Routes without search-params.ts get an overload returning [{}, SetParams<{}>]\n *\n * A fallback overload for standalone codecs (existing API) is emitted last.\n */\nfunction formatUseQueryStatesOverloads(routes: RouteEntry[], importBase?: string): string[] {\n const lines: string[] = [];\n\n for (const route of routes) {\n const searchParamsType = route.hasSearchParams\n ? formatSearchParamsType(route, importBase)\n : '{}';\n lines.push(\n ` export function useQueryStates<R extends '${route.urlPath}'>(route: R, options?: QueryStatesOptions): [${searchParamsType}, SetParams<${searchParamsType}>]`\n );\n }\n\n // Fallback: standalone codecs (existing API)\n lines.push(\n ' export function useQueryStates<T extends Record<string, unknown>>(codecs: { [K in keyof T]: SearchParamCodec<T[K]> }, options?: QueryStatesOptions): [T, SetParams<T>]'\n );\n\n return lines;\n}\n\n// Link overload formatters and helpers (`formatTypedLinkOverloads`,\n// `formatLinkCatchAllOverloads`, `formatLinkParamsType`,\n// `buildResolvedPattern`, `LINK_BASE_PROPS_TYPE`) were extracted to\n// `./link-codegen.ts` (TIM-835) to keep this file under the 500-line\n// cap. They are imported at the top of this file.\n","/**\n * Intercepting route utilities.\n *\n * Computes rewrite rules from the route tree that enable intercepting routes\n * to conditionally render when navigating via client-side (soft) navigation.\n *\n * The mechanism: at build time, each intercepting route directory generates a\n * conditional rewrite. On soft navigation, the client sends an `X-Timber-URL`\n * header with the current pathname. The server checks if any rewrite's source\n * (the intercepted URL) matches the target pathname AND the header matches\n * the intercepting route's parent URL. If both match, the intercepting route\n * renders instead of the normal route.\n *\n * On hard navigation (no header), no rewrite matches, and the normal route\n * renders.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\n\nimport type { SegmentNode, InterceptionMarker } from './types.js';\n\n/** A conditional rewrite rule generated from an intercepting route. */\nexport interface InterceptionRewrite {\n /**\n * The URL pattern that this rewrite intercepts (the target of navigation).\n * E.g., \"/photo/[id]\" for a (.)photo/[id] interception.\n */\n interceptedPattern: string;\n /**\n * The URL prefix that the client must be navigating FROM for this rewrite\n * to apply. Matched against the X-Timber-URL header.\n * E.g., \"/feed\" for a (.)photo/[id] inside /feed/@modal/.\n */\n interceptingPrefix: string;\n /**\n * Segments chain from root → intercepting leaf. Used to build the element\n * tree when the interception is active.\n */\n segmentPath: SegmentNode[];\n}\n\n/**\n * Collect all interception rewrite rules from the route tree.\n *\n * Walks the tree recursively. For each intercepting segment, computes the\n * intercepted URL based on the marker and the segment's position.\n */\nexport function collectInterceptionRewrites(root: SegmentNode): InterceptionRewrite[] {\n const rewrites: InterceptionRewrite[] = [];\n walkForInterceptions(root, [root], rewrites);\n return rewrites;\n}\n\n/**\n * Recursively walk the segment tree to find intercepting routes.\n */\nfunction walkForInterceptions(\n node: SegmentNode,\n ancestors: SegmentNode[],\n rewrites: InterceptionRewrite[]\n): void {\n // Check children\n for (const child of node.children) {\n if (child.segmentType === 'intercepting' && child.interceptionMarker) {\n // Found an intercepting route — collect rewrites from its sub-tree\n collectFromInterceptingNode(child, ancestors, rewrites);\n } else {\n walkForInterceptions(child, [...ancestors, child], rewrites);\n }\n }\n\n // Check slots (intercepting routes are typically inside slots like @modal)\n for (const slot of Object.values(node.slots)) {\n walkForInterceptions(slot, ancestors, rewrites);\n }\n}\n\n/**\n * For an intercepting segment, find all leaf pages in its sub-tree and\n * generate rewrite rules for each.\n */\nfunction collectFromInterceptingNode(\n interceptingNode: SegmentNode,\n ancestors: SegmentNode[],\n rewrites: InterceptionRewrite[]\n): void {\n const marker = interceptingNode.interceptionMarker!;\n const segmentName = interceptingNode.interceptedSegmentName!;\n\n // Compute the intercepted URL base based on the marker\n const parentUrlPath = ancestors[ancestors.length - 1].urlPath;\n const interceptedBase = computeInterceptedBase(parentUrlPath, marker);\n const interceptedUrlBase =\n interceptedBase === '/' ? `/${segmentName}` : `${interceptedBase}/${segmentName}`;\n\n // Find all leaf pages in the intercepting sub-tree\n collectLeavesWithRewrites(\n interceptingNode,\n interceptedUrlBase,\n parentUrlPath,\n [...ancestors, interceptingNode],\n rewrites\n );\n}\n\n/**\n * Recursively find leaf pages in an intercepting sub-tree and generate\n * rewrite rules for each.\n */\nfunction collectLeavesWithRewrites(\n node: SegmentNode,\n interceptedUrlPath: string,\n interceptingPrefix: string,\n segmentPath: SegmentNode[],\n rewrites: InterceptionRewrite[]\n): void {\n if (node.page) {\n rewrites.push({\n interceptedPattern: interceptedUrlPath,\n interceptingPrefix,\n segmentPath: [...segmentPath],\n });\n }\n\n for (const child of node.children) {\n const childUrl =\n child.segmentType === 'group'\n ? interceptedUrlPath\n : `${interceptedUrlPath}/${child.segmentName}`;\n collectLeavesWithRewrites(\n child,\n childUrl,\n interceptingPrefix,\n [...segmentPath, child],\n rewrites\n );\n }\n}\n\n/**\n * Compute the base URL that an intercepting route intercepts, given the\n * parent's URL path and the interception marker.\n *\n * - (.) — same level: parent's URL path\n * - (..) — one level up: parent's parent URL path\n * - (...) — root level: /\n * - (..)(..) — two levels up: parent's grandparent URL path\n *\n * Level counting operates on URL path segments, NOT filesystem directories.\n * Route groups and parallel slots are already excluded from urlPath (they\n * don't add URL depth), so (..) correctly climbs visible segments. This\n * avoids the Vinext bug where path.dirname() on filesystem paths would\n * waste climbs on invisible route groups.\n */\nfunction computeInterceptedBase(parentUrlPath: string, marker: InterceptionMarker): string {\n switch (marker) {\n case '(.)':\n return parentUrlPath;\n case '(..)': {\n const parts = parentUrlPath.split('/').filter(Boolean);\n parts.pop();\n return parts.length === 0 ? '/' : `/${parts.join('/')}`;\n }\n case '(...)':\n return '/';\n case '(..)(..)': {\n const parts = parentUrlPath.split('/').filter(Boolean);\n parts.pop();\n parts.pop();\n return parts.length === 0 ? '/' : `/${parts.join('/')}`;\n }\n }\n}\n","/**\n * Schema key/value validation against the filesystem route tree.\n *\n * Runs at codegen time (build + dev) to catch mismatches between\n * app/schema.ts keys and the actual dynamic segments on disk.\n *\n * Design doc: design/41-global-params.md\n */\n\nimport { readFileSync } from 'node:fs';\nimport type { RouteTree, SegmentNode } from './types.js';\nimport { findSchemaFile, parseExistingSchemaKeys } from '../cli-schema-sync.js';\nimport { classifyUrlSegment } from './segment-classify.js';\n\nexport interface SchemaWarning {\n type: 'stale-key' | 'missing-key' | 'bracket-mismatch';\n key: string;\n message: string;\n filesystemKey?: string;\n}\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from a route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegmentsFromTree(tree: RouteTree): Set<string> {\n const segments = new Set<string>();\n walkTree(tree.root, segments);\n return segments;\n}\n\nfunction walkTree(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName && node.segmentName) {\n const classified = classifyUrlSegment(node.segmentName);\n if (classified.kind !== 'static') {\n segments.add(node.segmentName);\n }\n }\n for (const child of node.children) {\n walkTree(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkTree(slot, segments);\n }\n}\n\n/**\n * Extract the param name from a bracket key.\n * '[id]' → 'id', '[...slug]' → 'slug', '[[...topic]]' → 'topic'\n */\nfunction extractParamName(bracketKey: string): string | null {\n const seg = classifyUrlSegment(bracketKey);\n if (seg.kind === 'static') return null;\n return seg.name;\n}\n\n/**\n * Validate schema keys against the filesystem route tree.\n *\n * Returns warnings for:\n * - Stale keys: schema keys that don't match any filesystem segment\n * - Bracket form mismatches: same param name, different bracket form\n * - Missing keys: filesystem segments not registered in schema (informational)\n */\nexport function validateSchemaAgainstRoutes(tree: RouteTree, appDir: string): SchemaWarning[] {\n const schemaPath = findSchemaFile(appDir);\n if (!schemaPath) return [];\n\n const content = readFileSync(schemaPath, 'utf-8');\n const schemaKeys = parseExistingSchemaKeys(content);\n\n if (schemaKeys.length === 0) return [];\n\n const filesystemSegments = collectDynamicSegmentsFromTree(tree);\n\n return validateKeys(schemaKeys, filesystemSegments);\n}\n\n/**\n * Core validation: compare schema keys against filesystem segments.\n * Exported separately for testing without filesystem dependencies.\n */\nexport function validateKeys(\n schemaKeys: string[],\n filesystemSegments: Set<string>\n): SchemaWarning[] {\n const warnings: SchemaWarning[] = [];\n\n // Build a map of param name → bracket form for filesystem segments\n const fsParamToKey = new Map<string, string>();\n for (const fsKey of filesystemSegments) {\n const name = extractParamName(fsKey);\n if (name) {\n fsParamToKey.set(name, fsKey);\n }\n }\n\n // Build a set of schema param names for the missing-key check\n const schemaParamNames = new Set<string>();\n\n for (const schemaKey of schemaKeys) {\n const schemaParamName = extractParamName(schemaKey);\n if (!schemaParamName) continue;\n\n schemaParamNames.add(schemaParamName);\n\n if (filesystemSegments.has(schemaKey)) {\n // Exact match — no issue\n continue;\n }\n\n // Check if the param name exists but with a different bracket form\n const fsKey = fsParamToKey.get(schemaParamName);\n if (fsKey) {\n warnings.push({\n type: 'bracket-mismatch',\n key: schemaKey,\n filesystemKey: fsKey,\n message:\n `Schema key '${schemaKey}' does not match filesystem bracket form '${fsKey}'. ` +\n `Update the schema key to '${fsKey}'.`,\n });\n } else {\n warnings.push({\n type: 'stale-key',\n key: schemaKey,\n message:\n `Schema key '${schemaKey}' does not match any dynamic segment in the filesystem. ` +\n `Remove it from app/schema.ts or create the corresponding route segment.`,\n });\n }\n }\n\n // Check for filesystem segments not in schema (informational)\n for (const fsKey of filesystemSegments) {\n const name = extractParamName(fsKey);\n if (name && !schemaParamNames.has(name)) {\n warnings.push({\n type: 'missing-key',\n key: fsKey,\n message:\n `Dynamic segment '${fsKey}' has no codec in app/schema.ts (defaults to string). ` +\n `Run: timber schema sync`,\n });\n }\n }\n\n return warnings;\n}\n","/**\n * Shared route-tree walkers (TIM-848).\n *\n * Tiny helpers that walk a `SegmentNode<TFile>` tree generically. Both the\n * build-time tree (`SegmentNode<RouteFile>`) and the runtime manifest\n * tree (`SegmentNode<ManifestFile>`) flow through these helpers because\n * the walker only reads the structural fields shared by both shapes\n * (`children`, `slots`, `page`, `route`, `urlPath`).\n *\n * Before this module, three near-identical `collectRoutes` functions\n * lived in `plugins/dev-404-page.ts`, `plugins/build-report.ts`, and\n * `routing/codegen.ts`. The codegen one is special-purpose (it\n * accumulates `ParamEntry[]` and resolves codec chains) and stays\n * local; the other two now share `collectLeafRoutes` from this file.\n */\n\nimport type { SegmentNode } from './types.js';\n\n/** A leaf route discovered while walking the segment tree. */\nexport interface LeafRoute<TFile> {\n /** URL path of the leaf (root is \"/\"). */\n urlPath: string;\n /** Segment chain from root to this leaf, inclusive. */\n segments: SegmentNode<TFile>[];\n /** The page file at this leaf, if any. */\n page?: TFile;\n /** The route handler file at this leaf, if any. */\n route?: TFile;\n}\n\n/** Options for `collectLeafRoutes`. */\nexport interface CollectLeafRoutesOptions {\n /**\n * If true, recurse into parallel slots and emit slot leaves alongside\n * the main route tree. Defaults to `false` because slots render\n * alongside their parent at the same URL and are not separately\n * URL-addressable. The build report excludes slots; route-listing\n * UIs that want to show \"all leaves with a page handler\" can opt in.\n */\n includeSlots?: boolean;\n}\n\n/**\n * Walk a segment tree and collect every leaf with a `page` or `route`\n * handler. Generic over `TFile` so it works on both the build-time\n * scanner output and the runtime manifest tree.\n *\n * - Pages and route handlers at the same URL produce two distinct\n * entries (the build report deduplicates by URL afterward).\n * - Parallel slots are skipped unless `includeSlots: true` (slots\n * share their parent's URL and are not addressable on their own).\n * - Result is sorted by `urlPath` for deterministic output.\n */\nexport function collectLeafRoutes<TFile>(\n root: SegmentNode<TFile>,\n options: CollectLeafRoutesOptions = {}\n): LeafRoute<TFile>[] {\n const { includeSlots = false } = options;\n const result: LeafRoute<TFile>[] = [];\n walk(root, [], result, includeSlots);\n result.sort((a, b) => a.urlPath.localeCompare(b.urlPath));\n return result;\n}\n\nfunction walk<TFile>(\n node: SegmentNode<TFile>,\n chain: SegmentNode<TFile>[],\n result: LeafRoute<TFile>[],\n includeSlots: boolean\n): void {\n const currentChain = [...chain, node];\n const path = node.urlPath || '/';\n\n if (node.page) {\n result.push({ urlPath: path, segments: currentChain, page: node.page });\n }\n if (node.route) {\n result.push({ urlPath: path, segments: currentChain, route: node.route });\n }\n\n for (const child of node.children) {\n walk(child, currentChain, result, includeSlots);\n }\n\n if (includeSlots) {\n for (const slotNode of Object.values(node.slots)) {\n walk(slotNode, currentChain, result, includeSlots);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,gBAAgB,eAAuB,YAAwC;CAC7F,MAAM,UAAU,cAAc,QAAQ,eAAe,GAAG;AACxD,KAAI,WACF,QAAO,OAAO,SAAS,YAAY,QAAQ,CAAC,QAAQ,OAAO,IAAI;AAEjE,QAAO,OAAO,MAAM,SAAS,QAAQ;;;AAIvC,IAAa,kCAAkC;;;;;;;;;;;;;;AAe/C,SAAgB,gCAAwC;AACtD,QAAO;EACL,QAAQ,gCAAgC;EACxC;EACA;EACA;EACD,CAAC,KAAK,KAAK;;;;;;;;;;;;;;;;;AAkBd,SAAgB,oBACd,GACA,YACA,UACQ;CACR,MAAM,QAAQ,EAAE;AAChB,KAAI,CAAC,SAAS,MAAM,WAAW,EAAG,QAAO;CACzC,MAAM,MAAM,KAAK,UAAU,EAAE,KAAK;CAIlC,IAAI,QAAQ;AACZ,MAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,IAErC,SAAQ,GAAG,gCAAgC,mBADxB,gBAAgB,MAAM,IAAI,WAAW,CACiB,wBAAwB,IAAI,IAAI,MAAM;AAEjH,QAAO;;;;;;;;;AAUT,SAAgB,qBAAqB,GAAuB;AAC1D,QAAO,8CAA8C,KAAK,UAAU,EAAE,YAAY,CAAC;;;;;;;;;;AAWrF,SAAgB,uBAAuB,OAAmB,YAA6B;AACrF,KAAI,MAAM,mBAAmB,MAAM,qBAGjC,QAAO,mBAFY,gBAAgB,MAAM,sBAAsB,WAAW,CAErC;AAEvC,QAAO;;;;;AC1ET,IAAa,uBACX;;;;;;;;AASF,SAAgB,qBAAqB,OAAkC;AAiBrE,QAhBc,MAAM,QAAQ,MAAM,IAAI,CACV,KAAK,SAAS;AACxC,MAAI,KAAK,WAAW,QAAQ,IAAI,KAAK,SAAS,KAAK,CAEjD,QAAO;AAET,MAAI,KAAK,WAAW,OAAO,IAAI,KAAK,SAAS,IAAI,CAE/C,QAAO;AAET,MAAI,KAAK,WAAW,IAAI,IAAI,KAAK,SAAS,IAAI,CAE5C,QAAO;AAET,SAAO;GACP,CACmB,KAAK,IAAI;;;;;;;;;;;;AAahC,SAAgB,qBACd,QACA,YACA,WACQ;AACR,KAAI,OAAO,WAAW,EACpB,QAAO;AAUT,QAAO,KAPQ,OAAO,KAAK,MAAM;EAC/B,MAAM,WAAW,EAAE,SAAS,WAAW,oBAAoB,EAAE;EAC7D,MAAM,YAAY,YACd,qBAAqB,EAAE,GACvB,oBAAoB,GAAG,YAAY,SAAS;AAChD,SAAO,GAAG,EAAE,KAAK,IAAI;GACrB,CACiB,KAAK,KAAK,CAAC;;;;;;;;;;;;;;AAehC,SAAgB,8BAAwC;CACtD,MAAM,QAAkB,EAAE;CAC1B,MAAM,YAAY;CASlB,MAAM,uBACJ;CAIF,MAAM,eACJ;AAEF,OAAM,KAAK,mEAAmE;AAC9E,OAAM,KAAK,6BAA6B;AAWxC,OAAM,KAAK,eAAe,UAAU,MAAM;AAC1C,OAAM,KAAK,eAAe,eAAe;AACzC,OAAM,KAAK,gDAAgD;AAC3D,OAAM,KAAK,wBAAwB,uBAAuB;AAC1D,OAAM,KAAK,sCAAsC;AAwBjD,OAAM,KAAK,0BAA0B;AACrC,OAAM,KAAK,gBAAgB,UAAU,MAAM;AAC3C,OAAM,KAAK,6CAA6C;AACxD,OAAM,KAAK,qEAAqE;AAChF,OAAM,KAAK,0BAA0B,uBAAuB;AAC5D,OAAM,KAAK,UAAU;AACrB,OAAM,KAAK,qCAAqC;AAEhD,OAAM,KAAK,MAAM;AACjB,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT,SAAgB,yBACd,QACA,YACA,WACU;CACV,MAAM,QAAkB,EAAE;CAC1B,MAAM,YAAY;CAclB,MAAM,WAAqB,EAAE;AAC7B,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,mBAAmB,MAAM,OAAO,SAAS;EAI/C,MAAM,aAAa,mBACf,kBAAkB,qBAAqB,MAAM,QAAQ,YAAY,UAAU,KAC3E;EAEJ,MAAM,mBAAmB,MAAM,kBAC3B,uBAAuB,OAAO,WAAW,GACzC;EAYJ,MAAM,0BAA0B,mBAC5B,0BAA0B,iBAAiB,KAC3C;AAGJ,WAAS,KACP,GAAG,UAAU,cAAc,MAAM,QAAQ,KAAK,WAAW,IAAI,wBAAwB,IACtF;AAGD,MAAI,kBAAkB;GACpB,MAAM,kBAAkB,qBAAqB,MAAM;AACnD,OAAI,iBAAiB;IAcnB,MAAM,2BAA2B,mBAC7B,uDAAuD,iBAAiB,qBAAqB,iBAAiB,OAC9G;AAQJ,aAAS,KACP,GAAG,UAAU,eAAe,gBAAgB,6CAA6C,yBAAyB,IACnH;;;;AAKP,OAAM,KAAK,6BAA6B;AACxC,KAAI,SAAS,WAAW,GAAG;AAGzB,QAAM,KAAK,MAAM;AACjB,SAAO;;AAET,OAAM,KAAK,QAAQ;AACnB,OAAM,KAAK,eAAe;AAC1B,MAAK,MAAM,WAAW,SACpB,OAAM,KAAK,cAAc,QAAQ,GAAG;AAEtC,OAAM,KAAK,qCAAqC;AAChD,OAAM,KAAK,MAAM;AAEjB,QAAO;;;;;;;;;;;;;;;;;;;ACrQT,SAAgB,iBAAiB,MAAiB,UAA0B,EAAE,EAAU;CAEtF,MAAM,YAAY,QAAQ,aAAa,aAAa,QAAQ,OAAO;CAEnE,MAAM,SAAuB,EAAE;AAC/B,eAAc,KAAK,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,QAAQ,UAAU;AAGvD,QAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,QAAQ,CAAC;AAKzD,QAAO,sBAAsB,QAFV,QAAQ,aAAa,QAAQ,QAEC,WAAW,QAAQ,OAAO;;;AAI7E,IAAM,oBAAoB;CAAC;CAAO;CAAQ;CAAO;CAAO;;;;AAKxD,SAAS,aAAa,QAAqC;AACzD,KAAI,CAAC,OAAQ,QAAO;AACpB,QAAO,kBAAkB,MAAM,QAAQ,WAAW,KAAK,QAAQ,SAAS,MAAM,CAAC,CAAC;;;;;;;;AASlF,SAAS,cACP,MACA,gBACA,qBACA,gBACA,QACA,WACM;CAGN,MAAM,WAAW,KAAK,cAAc,GAAG,eAAe,GAAG,KAAK,gBAAgB;CAS9E,MAAM,gBACJ,KAAK,UAAU,cAAc,KAAK,OAAO,UAAU,gBAAgB,GAC/D,KAAK,OAAO,WACZ,KAAA;CAMN,MAAM,SAAS,CAAC,GAAG,eAAe;AAClC,KAAI,KAAK,WAAW;EAClB,MAAM,iBAAiB,gBAAgB,KAAA,IAAY,uBAAuB,KAAK;AAC/E,SAAO,KAAK;GACV,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,MAAM,oBAAoB,KAAK,YAAY;GAI3C,qBAAqB;GACtB,CAAC;;CAIJ,MAAM,oBAAoB,gBACtB,CAAC,GAAG,qBAAqB,cAAc,GACvC;CAGJ,MAAM,SAAS,CAAC,CAAC,KAAK;CACtB,MAAM,aAAa,CAAC,CAAC,KAAK;AAE1B,KAAI,UAAU,YAAY;EAOxB,MAAM,iBAAiB,kBAAkB,SAAS,IAAI,CAAC,GAAG,kBAAkB,CAAC,SAAS,GAAG,EAAE;EAC3F,MAAM,iBAA+B,OAAO,KAAK,MAAM;GACrD,MAAM,iBACJ,eAAe,SAAS,IACpB,iBACA,EAAE,sBACA,CAAC,EAAE,oBAAoB,GACvB,KAAA;AACR,UAAO;IACL,MAAM,EAAE;IACR,aAAa,EAAE;IACf,MAAM,EAAE;IACR;IACD;IACD;EAEF,MAAM,QAAoB;GACxB,SAAS,KAAK;GACd,aAAa,YAAY;GACzB,QAAQ;GACR,iBAAiB;GACjB;GACA;GACD;AAGD,MAAI;OACE,KAAK,UAAU,cAAc,KAAK,OAAO,UAAU,eAAe,EAAE;AACtE,UAAM,kBAAkB;AACxB,UAAM,uBAAuB,KAAK,OAAO;cAChC,KAAK,QAAQ,cAAc,KAAK,KAAK,UAAU,eAAe,EAAE;AACzE,UAAM,kBAAkB;AACxB,UAAM,uBAAuB,KAAK,KAAK;;;AAI3C,SAAO,KAAK,MAAM;;AAIpB,MAAK,MAAM,SAAS,KAAK,SACvB,eAAc,OAAO,QAAQ,mBAAmB,UAAU,QAAQ,UAAU;AAI9E,MAAK,MAAM,QAAQ,OAAO,OAAO,KAAK,MAAM,CAC1C,eAAc,MAAM,QAAQ,mBAAmB,UAAU,QAAQ,UAAU;;;;;AAO/E,SAAS,oBAAoB,aAAyC;AACpE,SAAQ,aAAR;EACE,KAAK,YACH,QAAO;EACT,KAAK,qBACH,QAAO;EACT,QACE,QAAO;;;;;;;;;AAUb,SAAS,cAAc,UAAkB,YAA6B;AACpE,KAAI,CAAC,WAAW,SAAS,CAAE,QAAO;AAClC,KAAI;EACF,MAAM,UAAU,aAAa,UAAU,QAAQ;EAC/C,MAAM,IAAI,WAAW,QAAQ,uBAAuB,OAAO;AAC3D,SACE,IAAI,OAAO,gCAAgC,EAAE,KAAK,CAAC,KAAK,QAAQ,IAChE,IAAI,OAAO,wBAAwB,EAAE,aAAa,CAAC,KAAK,QAAQ;SAE5D;AACN,SAAO;;;;;;;;;;AAWX,SAAS,uBAAuB,MAAuC;AACrE,KAAI,KAAK,UAAU,cAAc,KAAK,OAAO,UAAU,gBAAgB,CACrE,QAAO,KAAK,OAAO;AAErB,KAAI,KAAK,QAAQ,cAAc,KAAK,KAAK,UAAU,gBAAgB,CACjE,QAAO,KAAK,KAAK;;;;;;;;;;;AAcrB,SAAS,kBAAkB,QAAgC;CACzD,MAAM,QAAkB,EAAE;AAE1B,OAAM,KAAK,4CAA4C;AACvD,OAAM,KAAK,uDAAuD;AAClE,OAAM,KAAK,GAAG;AACd,OAAM,KAAK,mEAAmE;AAC9E,OAAM,KAAK,yCAAyC;AAEpD,MAAK,MAAM,SAAS,OAClB,KAAI,MAAM,OAAO,WAAW,EAC1B,OAAM,KAAK,MAAM,MAAM,YAAY,QAAQ;MACtC;AACL,QAAM,KAAK,MAAM,MAAM,YAAY,MAAM;AACzC,OAAK,MAAM,KAAK,MAAM,OACpB,OAAM,KAAK,OAAO,EAAE,KAAK,IAAI,qBAAqB,EAAE,CAAC,GAAG;AAE1D,QAAM,KAAK,OAAO;;AAItB,OAAM,KAAK,IAAI;AACf,OAAM,KAAK,GAAG;AAGd,OAAM,KAAK,yDAAyD;AACpE,OAAM,KAAK,mCAAmC;AAC9C,OAAM,KACJ,4FACD;AACD,OAAM,KAAK,cAAc;AACzB,OAAM,KAAK,oCAAoC;AAC/C,OAAM,KAAK,oBAAoB;AAC/B,OAAM,KAAK,iDAAiD;AAC5D,OAAM,KAAK,aAAa;AACxB,OAAM,KAAK,GAAG;AAGd,OAAM,KACJ,yFACD;AACD,OAAM,KAAK,qEAAqE;AAChF,OAAM,KAAK,4EAA4E;AACvF,OAAM,KAAK,kBAAkB;AAE7B,QAAO;;;;;AAMT,SAAS,sBACP,QACA,YACA,WACA,QACQ;CACR,MAAM,QAAkB,EAAE;AAE1B,OAAM,KAAK,iEAAiE;AAC5E,OAAM,KAAK,iEAAiE;AAC5E,OAAM,KAAK,GAAG;AAKd,OAAM,KAAK,aAAa;AACxB,OAAM,KAAK,GAAG;AAEd,KAAI,WAAW;EAGb,MAAM,eACJ,UAAU,aACN,OAAO,SAAS,YAAY,KAAK,QAAQ,SAAS,CAAC,CAAC,QAAQ,OAAO,IAAI,GACvE;AACN,QAAM,KAAK,4BAA4B,aAAa,IAAI;AACxD,QAAM,KAAK,kFAAkF;AAC7F,QAAM,KAAK,GAAG;EAEd,MAAM,iCAAiB,IAAI,KAAa;AACxC,OAAK,MAAM,SAAS,OAClB,MAAK,MAAM,KAAK,MAAM,OACpB,gBAAe,IAAI,EAAE,YAAY;AAMrC,MAAI,eAAe,OAAO,GAAG;AAC3B,SAAM,KAAK,2CAA2C;AACtD,SAAM,KAAK,gDAAgD;AAC3D,QAAK,MAAM,OAAO,CAAC,GAAG,eAAe,CAAC,MAAM,CAC1C,OAAM,KAAK,OAAO,KAAK,UAAU,IAAI,CAAC,SAAS;AAEjD,SAAM,KAAK,MAAM;AACjB,SAAM,KAAK,IAAI;AACf,SAAM,KAAK,GAAG;;AAIhB,QAAM,KAAK,+BAA+B;AAC1C,QAAM,KACJ,0LACD;AACD,QAAM,KAAK,KAAK;AAChB,QAAM,KAAK,GAAG;;AAGhB,KAAI,CAAC,WAAW;AAId,QAAM,KAAK,+BAA+B,CAAC;AAC3C,QAAM,KAAK,GAAG;;AAGhB,OAAM,KAAK,oCAAoC;AAC/C,OAAM,KAAK,uBAAuB;AAElC,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,aAAa,iBAAiB,MAAM,QAAQ,YAAY,UAAU;EACxE,MAAM,mBAAmB,uBAAuB,OAAO,WAAW;AAElE,QAAM,KAAK,QAAQ,MAAM,QAAQ,MAAM;AACvC,QAAM,KAAK,wBAAwB,aAAa;AAChD,QAAM,KAAK,uBAAuB,mBAAmB;AACrD,QAAM,KAAK,QAAQ;;AAGrB,OAAM,KAAK,MAAM;AAMjB,KAAI,WAAW;AACb,QAAM,KAAK,GAAG;AACd,QAAM,KAAK,GAAG,kBAAkB,OAAO,CAAC,KAAK,MAAM,KAAK,IAAI,CAAC;;AAG/D,OAAM,KAAK,IAAI;AACf,OAAM,KAAK,GAAG;CAEd,MAAM,aAAa,OAAO,QAAQ,MAAM,CAAC,EAAE,WAAW;CACtD,MAAM,gBAAgB,OAAO,QAAQ,MAAM,EAAE,OAAO,SAAS,EAAE;AAG/D,KAAI,cAAc,SAAS,GAAG;AAC5B,QAAM,KAAK,2CAA2C;AACtD,MAAI,UACF,OAAM,KAAK,2DAA2D;AAExE,OAAK,MAAM,SAAS,eAAe;GACjC,MAAM,aAAa,iBAAiB,MAAM,QAAQ,YAAY,UAAU;AACxE,SAAM,KACJ,oDAAoD,MAAM,YAAY,MAAM,aAC7E;;AAEH,QAAM,KACJ,YACI,oEACA,0EACL;AACD,QAAM,KAAK,IAAI;AACf,QAAM,KAAK,GAAG;;AAKhB,KAAI,cAAc,SAAS,KAAK,WAAW,SAAS,GAAG;AACrD,QAAM,KAAK,2CAA2C;AACtD,QAAM,KACJ,gIACD;AACD,MAAI,UACF,OAAM,KAAK,2DAA2D;AAExE,QAAM,KAAK,GAAG;AAGd,MAAI,cAAc,SAAS,GAAG;AAC5B,QAAK,MAAM,SAAS,eAAe;IACjC,MAAM,aAAa,iBAAiB,MAAM,QAAQ,YAAY,UAAU;AACxE,UAAM,KACJ,oDAAoD,MAAM,YAAY,MAAM,aAC7E;;AAEH,SAAM,KACJ,YACI,oEACA,0EACL;AACD,SAAM,KAAK,GAAG;;AAIhB,MAAI,WAAW,SAAS,GAAG;AACzB,SAAM,KAAK,GAAG,8BAA8B,YAAY,WAAW,CAAC;AACpE,SAAM,KAAK,GAAG;;AAehB,MAAI,WAAW,SAAS,GAAG;AACzB,SAAM,KAAK,kEAAkE;AAC7E,SAAM,KAAK,GAAG,yBAAyB,YAAY,YAAY,UAAU,CAAC;AAC1E,SAAM,KAAK,GAAG;;AAGhB,QAAM,KAAK,IAAI;AACf,QAAM,KAAK,GAAG;;AAOhB,OAAM,KAAK,2CAA2C;AACtD,OAAM,KAAK,+EAA+E;AAC1F,OAAM,KAAK,GAAG;AACd,OAAM,KAAK,GAAG,6BAA6B,CAAC;AAC5C,OAAM,KAAK,IAAI;AACf,OAAM,KAAK,GAAG;AAEd,QAAO,MAAM,KAAK,KAAK;;;;;AAMzB,SAAS,iBAAiB,QAAsB,YAAqB,WAA6B;AAChG,KAAI,OAAO,WAAW,EACpB,QAAO;AAST,QAAO,KANQ,OAAO,KAAK,MAAM;EAC/B,MAAM,YAAY,YACd,qBAAqB,EAAE,GACvB,oBAAoB,GAAG,YAAY,EAAE,KAAK;AAC9C,SAAO,GAAG,EAAE,KAAK,IAAI;GACrB,CACiB,KAAK,KAAK,CAAC;;;;;;;;;;;AAYhC,SAAS,8BAA8B,QAAsB,YAA+B;CAC1F,MAAM,QAAkB,EAAE;AAE1B,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,mBAAmB,MAAM,kBAC3B,uBAAuB,OAAO,WAAW,GACzC;AACJ,QAAM,KACJ,+CAA+C,MAAM,QAAQ,+CAA+C,iBAAiB,cAAc,iBAAiB,IAC7J;;AAIH,OAAM,KACJ,2KACD;AAED,QAAO;;;;;;;;;;ACheT,SAAgB,4BAA4B,MAA0C;CACpF,MAAM,WAAkC,EAAE;AAC1C,sBAAqB,MAAM,CAAC,KAAK,EAAE,SAAS;AAC5C,QAAO;;;;;AAMT,SAAS,qBACP,MACA,WACA,UACM;AAEN,MAAK,MAAM,SAAS,KAAK,SACvB,KAAI,MAAM,gBAAgB,kBAAkB,MAAM,mBAEhD,6BAA4B,OAAO,WAAW,SAAS;KAEvD,sBAAqB,OAAO,CAAC,GAAG,WAAW,MAAM,EAAE,SAAS;AAKhE,MAAK,MAAM,QAAQ,OAAO,OAAO,KAAK,MAAM,CAC1C,sBAAqB,MAAM,WAAW,SAAS;;;;;;AAQnD,SAAS,4BACP,kBACA,WACA,UACM;CACN,MAAM,SAAS,iBAAiB;CAChC,MAAM,cAAc,iBAAiB;CAGrC,MAAM,gBAAgB,UAAU,UAAU,SAAS,GAAG;CACtD,MAAM,kBAAkB,uBAAuB,eAAe,OAAO;AAKrE,2BACE,kBAJA,oBAAoB,MAAM,IAAI,gBAAgB,GAAG,gBAAgB,GAAG,eAMpE,eACA,CAAC,GAAG,WAAW,iBAAiB,EAChC,SACD;;;;;;AAOH,SAAS,0BACP,MACA,oBACA,oBACA,aACA,UACM;AACN,KAAI,KAAK,KACP,UAAS,KAAK;EACZ,oBAAoB;EACpB;EACA,aAAa,CAAC,GAAG,YAAY;EAC9B,CAAC;AAGJ,MAAK,MAAM,SAAS,KAAK,SAKvB,2BACE,OAJA,MAAM,gBAAgB,UAClB,qBACA,GAAG,mBAAmB,GAAG,MAAM,eAInC,oBACA,CAAC,GAAG,aAAa,MAAM,EACvB,SACD;;;;;;;;;;;;;;;;;AAmBL,SAAS,uBAAuB,eAAuB,QAAoC;AACzF,SAAQ,QAAR;EACE,KAAK,MACH,QAAO;EACT,KAAK,QAAQ;GACX,MAAM,QAAQ,cAAc,MAAM,IAAI,CAAC,OAAO,QAAQ;AACtD,SAAM,KAAK;AACX,UAAO,MAAM,WAAW,IAAI,MAAM,IAAI,MAAM,KAAK,IAAI;;EAEvD,KAAK,QACH,QAAO;EACT,KAAK,YAAY;GACf,MAAM,QAAQ,cAAc,MAAM,IAAI,CAAC,OAAO,QAAQ;AACtD,SAAM,KAAK;AACX,SAAM,KAAK;AACX,UAAO,MAAM,WAAW,IAAI,MAAM,IAAI,MAAM,KAAK,IAAI;;;;;;;;;;;;;;;;;;AChJ3D,SAAgB,+BAA+B,MAA8B;CAC3E,MAAM,2BAAW,IAAI,KAAa;AAClC,UAAS,KAAK,MAAM,SAAS;AAC7B,QAAO;;AAGT,SAAS,SAAS,MAAmB,UAA6B;AAChE,KAAI,KAAK,aAAa,KAAK;MACN,mBAAmB,KAAK,YAAY,CACxC,SAAS,SACtB,UAAS,IAAI,KAAK,YAAY;;AAGlC,MAAK,MAAM,SAAS,KAAK,SACvB,UAAS,OAAO,SAAS;AAE3B,MAAK,MAAM,QAAQ,OAAO,OAAO,KAAK,MAAM,CAC1C,UAAS,MAAM,SAAS;;;;;;AAQ5B,SAAS,iBAAiB,YAAmC;CAC3D,MAAM,MAAM,mBAAmB,WAAW;AAC1C,KAAI,IAAI,SAAS,SAAU,QAAO;AAClC,QAAO,IAAI;;;;;;;;;;AAWb,SAAgB,4BAA4B,MAAiB,QAAiC;CAC5F,MAAM,aAAa,eAAe,OAAO;AACzC,KAAI,CAAC,WAAY,QAAO,EAAE;CAG1B,MAAM,aAAa,wBADH,aAAa,YAAY,QAAQ,CACE;AAEnD,KAAI,WAAW,WAAW,EAAG,QAAO,EAAE;AAItC,QAAO,aAAa,YAFO,+BAA+B,KAAK,CAEZ;;;;;;AAOrD,SAAgB,aACd,YACA,oBACiB;CACjB,MAAM,WAA4B,EAAE;CAGpC,MAAM,+BAAe,IAAI,KAAqB;AAC9C,MAAK,MAAM,SAAS,oBAAoB;EACtC,MAAM,OAAO,iBAAiB,MAAM;AACpC,MAAI,KACF,cAAa,IAAI,MAAM,MAAM;;CAKjC,MAAM,mCAAmB,IAAI,KAAa;AAE1C,MAAK,MAAM,aAAa,YAAY;EAClC,MAAM,kBAAkB,iBAAiB,UAAU;AACnD,MAAI,CAAC,gBAAiB;AAEtB,mBAAiB,IAAI,gBAAgB;AAErC,MAAI,mBAAmB,IAAI,UAAU,CAEnC;EAIF,MAAM,QAAQ,aAAa,IAAI,gBAAgB;AAC/C,MAAI,MACF,UAAS,KAAK;GACZ,MAAM;GACN,KAAK;GACL,eAAe;GACf,SACE,eAAe,UAAU,4CAA4C,MAAM,+BAC9C,MAAM;GACtC,CAAC;MAEF,UAAS,KAAK;GACZ,MAAM;GACN,KAAK;GACL,SACE,eAAe,UAAU;GAE5B,CAAC;;AAKN,MAAK,MAAM,SAAS,oBAAoB;EACtC,MAAM,OAAO,iBAAiB,MAAM;AACpC,MAAI,QAAQ,CAAC,iBAAiB,IAAI,KAAK,CACrC,UAAS,KAAK;GACZ,MAAM;GACN,KAAK;GACL,SACE,oBAAoB,MAAM;GAE7B,CAAC;;AAIN,QAAO;;;;;;;;;;;;;;;AC9FT,SAAgB,kBACd,MACA,UAAoC,EAAE,EAClB;CACpB,MAAM,EAAE,eAAe,UAAU;CACjC,MAAM,SAA6B,EAAE;AACrC,MAAK,MAAM,EAAE,EAAE,QAAQ,aAAa;AACpC,QAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,QAAQ,CAAC;AACzD,QAAO;;AAGT,SAAS,KACP,MACA,OACA,QACA,cACM;CACN,MAAM,eAAe,CAAC,GAAG,OAAO,KAAK;CACrC,MAAM,OAAO,KAAK,WAAW;AAE7B,KAAI,KAAK,KACP,QAAO,KAAK;EAAE,SAAS;EAAM,UAAU;EAAc,MAAM,KAAK;EAAM,CAAC;AAEzE,KAAI,KAAK,MACP,QAAO,KAAK;EAAE,SAAS;EAAM,UAAU;EAAc,OAAO,KAAK;EAAO,CAAC;AAG3E,MAAK,MAAM,SAAS,KAAK,SACvB,MAAK,OAAO,cAAc,QAAQ,aAAa;AAGjD,KAAI,aACF,MAAK,MAAM,YAAY,OAAO,OAAO,KAAK,MAAM,CAC9C,MAAK,UAAU,cAAc,QAAQ,aAAa"}