@timber-js/app 0.2.0-alpha.169 → 0.2.0-alpha.170

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 (59) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{canonicalize-P41GR6tY.js → canonicalize-Du3o_ptW.js} +30 -12
  3. package/dist/_chunks/canonicalize-Du3o_ptW.js.map +1 -0
  4. package/dist/_chunks/{cli-schema-sync-B73L6pMq.js → cli-schema-sync-B5FDplGI.js} +55 -7
  5. package/dist/_chunks/cli-schema-sync-B5FDplGI.js.map +1 -0
  6. package/dist/_chunks/{segment-classify-CDDRVKs7.js → segment-classify-Byy425ng.js} +57 -18
  7. package/dist/_chunks/segment-classify-Byy425ng.js.map +1 -0
  8. package/dist/_chunks/{walkers-CoOC8Hga.js → walkers-DCoE-LJf.js} +23 -9
  9. package/dist/_chunks/walkers-DCoE-LJf.js.map +1 -0
  10. package/dist/cli-schema-sync.d.ts.map +1 -1
  11. package/dist/cli.js +1 -1
  12. package/dist/client/index.js +5 -2
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/link.d.ts.map +1 -1
  15. package/dist/index.js +26 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/routing/index.js +3 -3
  18. package/dist/routing/link-codegen.d.ts.map +1 -1
  19. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  20. package/dist/routing/scanner.d.ts.map +1 -1
  21. package/dist/routing/schema-validation.d.ts.map +1 -1
  22. package/dist/routing/segment-classify.d.ts +8 -2
  23. package/dist/routing/segment-classify.d.ts.map +1 -1
  24. package/dist/routing/types.d.ts +4 -0
  25. package/dist/routing/types.d.ts.map +1 -1
  26. package/dist/server/internal.js +13 -4
  27. package/dist/server/internal.js.map +1 -1
  28. package/dist/server/metadata-routes.d.ts +24 -4
  29. package/dist/server/metadata-routes.d.ts.map +1 -1
  30. package/dist/server/pipeline-interception.d.ts.map +1 -1
  31. package/dist/server/pipeline-phases.d.ts.map +1 -1
  32. package/dist/server/route-element-builder.d.ts.map +1 -1
  33. package/dist/server/route-matcher.d.ts.map +1 -1
  34. package/dist/server/sitemap-generator.d.ts.map +1 -1
  35. package/docs/more/01-advanced-routing.mdx +35 -1
  36. package/docs/more/04-metadata-and-fonts.mdx +2 -2
  37. package/package.json +7 -8
  38. package/src/cli-schema-sync.ts +15 -2
  39. package/src/cli.ts +0 -0
  40. package/src/client/link.tsx +4 -1
  41. package/src/routing/codegen.ts +13 -1
  42. package/src/routing/link-codegen.ts +15 -12
  43. package/src/routing/manifest-codegen.ts +6 -0
  44. package/src/routing/scanner.ts +85 -3
  45. package/src/routing/schema-validation.ts +7 -1
  46. package/src/routing/segment-classify.ts +49 -14
  47. package/src/routing/types.ts +4 -0
  48. package/src/server/metadata-routes.ts +71 -10
  49. package/src/server/pipeline-interception.ts +14 -1
  50. package/src/server/pipeline-phases.ts +7 -1
  51. package/src/server/route-element-builder.ts +12 -3
  52. package/src/server/route-matcher.ts +16 -2
  53. package/src/server/sitemap-generator.ts +5 -4
  54. package/src/server/slot-resolver.ts +7 -1
  55. package/src/server/tree-match.ts +46 -4
  56. package/dist/_chunks/canonicalize-P41GR6tY.js.map +0 -1
  57. package/dist/_chunks/cli-schema-sync-B73L6pMq.js.map +0 -1
  58. package/dist/_chunks/segment-classify-CDDRVKs7.js.map +0 -1
  59. package/dist/_chunks/walkers-CoOC8Hga.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walkers-DCoE-LJf.js","names":[],"sources":["../../src/routing/codegen-shared.ts","../../src/routing/file-cache.ts","../../src/routing/export-detect.ts","../../src/routing/link-codegen.ts","../../src/routing/codegen.ts","../../src/routing/schema-validation.ts","../../src/routing/interception.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 the param's base type when\n * schema doesn't declare this param (schemas are opt-in per key).\n *\n * Uses `infer` to extract the codec value rather than direct indexing\n * because TypeScript doesn't narrow index validity inside a conditional\n * type's true branch — `typeof schema.segmentParams[K]` produces TS2538\n * even when the condition proves K is a valid key. In .d.ts files that\n * error is suppressed and the type silently becomes `any`.\n */\nexport function buildSchemaParamType(p: ParamEntry, fallback?: string): string {\n const key = JSON.stringify(p.bracketName);\n const fb = fallback ?? p.type;\n return `typeof schema['segmentParams'] extends Record<${key}, infer V> ? InferCodecType<V> : ${fb}`;\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","import { readFileSync, statSync } from 'node:fs';\n\nconst cache = new Map<string, { mtimeMs: number; size: number; content: string }>();\n\n/**\n * Read a file's text content, returning a cached copy when the file\n * hasn't been modified since the last read (checked via mtime + size).\n *\n * Falls back to a fresh read on any stat/read error.\n */\nexport function readFileCached(filePath: string): string {\n try {\n const stat = statSync(filePath);\n const entry = cache.get(filePath);\n if (entry && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.content;\n const content = readFileSync(filePath, 'utf-8');\n cache.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, content });\n return content;\n } catch {\n cache.delete(filePath);\n return readFileSync(filePath, 'utf-8');\n }\n}\n","/**\n * AST-based export detection for route files.\n *\n * Uses Vite's `parseAst` (backed by oxc) to precisely detect named and\n * default exports, replacing the regex-based approach that was fragile\n * with TypeScript syntax (type exports, `as` aliases, comments, etc.).\n */\n\nimport { existsSync } from 'node:fs';\nimport { parseAst } from 'vite';\nimport { readFileCached } from './file-cache.js';\n\ninterface AstNode {\n type: string;\n exportKind?: string;\n declaration?: AstNode;\n specifiers?: Array<{\n exported?: { name: string };\n local?: { name: string };\n exportKind?: string;\n }>;\n id?: { name: string };\n declarations?: Array<{ id?: { name: string } }>;\n}\n\ninterface ProgramNode {\n body: AstNode[];\n}\n\nfunction tryParse(source: string): ProgramNode | null {\n try {\n return parseAst(source, { lang: 'tsx' }) as unknown as ProgramNode;\n } catch {\n return null;\n }\n}\n\n/**\n * Collect all named export identifiers from a parsed program.\n * Handles: `export function X`, `export const X`, `export { X }`,\n * `export { X } from '...'`, `export { Y as X }`.\n */\nfunction collectNamedExports(program: ProgramNode): Set<string> {\n const names = new Set<string>();\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration') continue;\n // Skip `export type ...` declarations — they're erased at runtime\n if (stmt.exportKind === 'type') continue;\n\n if (stmt.declaration) {\n if (stmt.declaration.id?.name) {\n names.add(stmt.declaration.id.name);\n }\n if (stmt.declaration.declarations) {\n for (const decl of stmt.declaration.declarations) {\n if (decl.id?.name) names.add(decl.id.name);\n }\n }\n }\n\n if (stmt.specifiers) {\n for (const spec of stmt.specifiers) {\n // Skip `export { type X }` — per-specifier type exports\n if (spec.exportKind === 'type') continue;\n if (spec.exported?.name && spec.exported.name !== 'default') {\n names.add(spec.exported.name);\n }\n }\n }\n }\n return names;\n}\n\n/**\n * Check whether a program has a default export.\n * Handles: `export default ...`, `export { X as default }`,\n * `export { default } from '...'`.\n */\nfunction hasDefaultExport(program: ProgramNode): boolean {\n for (const stmt of program.body) {\n if (stmt.type === 'ExportDefaultDeclaration') return true;\n if (stmt.type === 'ExportNamedDeclaration' && stmt.specifiers) {\n for (const spec of stmt.specifiers) {\n if (spec.exported?.name === 'default') return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Check whether a program has any bare `export * from '...'` declarations.\n * Excludes `export * as X from '...'` (namespace re-exports) since those\n * create a namespace object, not individual top-level exports.\n */\nfunction hasStarExport(program: ProgramNode): boolean {\n return program.body.some(\n (stmt) =>\n stmt.type === 'ExportAllDeclaration' && !(stmt as any).exported && stmt.exportKind !== 'type'\n );\n}\n\n/**\n * Check if a file exports a specific named export.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasExport(filePath: string, exportName: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return collectNamedExports(program).has(exportName);\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a file has any of the given named exports.\n * Returns the set of matching export names.\n */\nexport function fileHasAnyExport(filePath: string, exportNames: readonly string[]): Set<string> {\n const matches = new Set<string>();\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return matches;\n const exports = collectNamedExports(program);\n for (const name of exportNames) {\n if (exports.has(name)) matches.add(name);\n }\n } catch {\n // Graceful fallback: return empty set\n }\n return matches;\n}\n\n/**\n * Check if a file has a default export.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasDefaultExport(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return hasDefaultExport(program);\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a file has any `export * from '...'` declarations.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasStarExport(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return hasStarExport(program);\n } catch {\n return false;\n }\n}\n\n/**\n * Parsed value of `export const prerender` from a route file.\n *\n * - `true` / `false` — literal boolean\n * - `{ ttl?, tags? }` — ISR options object (prerender: true is implied)\n * - `undefined` — no `export const prerender` found\n *\n * See design/45-cache-lifetimes.md §\"Route-Level Static\".\n */\nexport type PrerenderExportValue = boolean | { ttl?: number; tags?: string[] };\n\nfunction extractLiteralValue(node: AstNode): unknown {\n if (!node) return undefined;\n const n = node as any;\n if (n.type === 'Literal') return n.value;\n if (n.type === 'UnaryExpression' && n.operator === '-' && n.argument?.type === 'Literal') {\n return -(n.argument.value as number);\n }\n if (n.type === 'ArrayExpression') {\n const elements = n.elements as AstNode[] | undefined;\n if (!elements) return undefined;\n const result: unknown[] = [];\n for (const el of elements) {\n const v = extractLiteralValue(el);\n if (v === undefined) return undefined;\n result.push(v);\n }\n return result;\n }\n return undefined;\n}\n\nfunction extractObjectLiteral(node: AstNode): Record<string, unknown> | undefined {\n if ((node as any).type !== 'ObjectExpression') return undefined;\n const props = (node as any).properties as AstNode[] | undefined;\n if (!props) return undefined;\n const result: Record<string, unknown> = {};\n for (const prop of props) {\n const p = prop as any;\n if (p.type === 'SpreadElement') return undefined;\n if (p.type !== 'Property' || p.computed) return undefined;\n const key =\n p.key?.type === 'Identifier'\n ? p.key.name\n : p.key?.type === 'Literal'\n ? String(p.key.value)\n : undefined;\n if (!key) return undefined;\n const value = extractLiteralValue(p.value);\n if (value === undefined) return undefined;\n result[key] = value;\n }\n return result;\n}\n\n/**\n * Extract the value of `export const prerender` from a file.\n * Returns undefined if the file doesn't export `prerender` or can't be parsed.\n *\n * Supports:\n * export const prerender = true;\n * export const prerender = false;\n * export const prerender = { ttl: 3600, tags: ['docs'] };\n */\nexport function getPrerenderExport(filePath: string): PrerenderExportValue | undefined {\n if (!existsSync(filePath)) return undefined;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return undefined;\n\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration') continue;\n if (stmt.exportKind === 'type') continue;\n if (!stmt.declaration) continue;\n const decl = stmt.declaration;\n if (decl.type !== 'VariableDeclaration') continue;\n const declarations = decl.declarations as\n | Array<{\n id?: { name?: string };\n init?: AstNode;\n }>\n | undefined;\n if (!declarations) continue;\n for (const d of declarations) {\n if (d.id?.name !== 'prerender' || !d.init) continue;\n const init = d.init as any;\n if (init.type === 'Literal' && typeof init.value === 'boolean') {\n return init.value;\n }\n if (init.type === 'ObjectExpression') {\n const obj = extractObjectLiteral(init);\n if (!obj) return undefined;\n const result: { ttl?: number; tags?: string[] } = {};\n if ('ttl' in obj && typeof obj.ttl === 'number') result.ttl = obj.ttl;\n if (\n 'tags' in obj &&\n Array.isArray(obj.tags) &&\n obj.tags.every((t: unknown) => typeof t === 'string')\n ) {\n result.tags = obj.tags as string[];\n }\n return result;\n }\n return undefined;\n }\n }\n return undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check if a file starts with a specific directive (e.g. \"use client\").\n * Directives are string literal expression statements at the top of the file.\n */\nexport function fileHasDirective(filePath: string, directive: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n for (const stmt of program.body) {\n if (stmt.type !== 'ExpressionStatement') break;\n const expr = (stmt as any).expression;\n if (expr?.type !== 'Literal' || typeof expr.value !== 'string') break;\n if (expr.value === directive) return true;\n }\n return false;\n } catch {\n return false;\n }\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';\nimport { classifyUrlSegment } from './segment-classify.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 const seg = classifyUrlSegment(part);\n switch (seg.kind) {\n case 'catch-all':\n case 'optional-catch-all':\n // eslint-disable-next-line no-template-curly-in-string -- codegen output\n return '${string}';\n case 'dynamic': {\n const prefix = seg.prefix ?? '';\n const suffix = seg.suffix ?? '';\n // eslint-disable-next-line no-template-curly-in-string -- codegen output\n return `${prefix}\\${string}${suffix}`;\n }\n default:\n return part;\n }\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, fallback)\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 /* eslint-disable no-template-curly-in-string -- codegen output */\n const externalHref =\n '`http://${string}` | `https://${string}` | `mailto:${string}` | `tel:${string}` | `ftp://${string}` | `//${string}` | `#${string}` | `?${string}`';\n /* eslint-enable no-template-curly-in-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 } 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 { fileHasExport } from './export-detect.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, false);\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 insideSlot: 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: bracketNameForSegment(node),\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 isSlotRoute: insideSlot,\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, insideSlot);\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, true);\n }\n}\n\n/**\n * Dedupe route entries by urlPath for urlPath-keyed emissions (the Routes\n * interface, useQueryStates overloads, and typed Link variants).\n *\n * Parallel-slot pages share their parent's urlPath; emitting one entry per\n * slot page produced duplicate interface keys (TS2300/TS2717 under\n * skipLibCheck:false) and duplicate overloads. The non-slot entry is the\n * canonical one; when only slot entries exist (parent has no page), the\n * lexicographically smallest segmentPath wins so output is independent of\n * slot scan order. A slot's searchParams definition is carried onto the\n * merged entry when the canonical entry has none — never silently dropped.\n *\n * segmentPath-keyed emissions (getSegmentParams, useSegmentParams,\n * TimberSegmentParams) must NOT use this — segmentPaths are already unique\n * and slot entries feed those APIs directly.\n */\nfunction dedupeRoutesByUrlPath(routes: RouteEntry[]): RouteEntry[] {\n const byPath = new Map<string, RouteEntry>();\n for (const route of routes) {\n const existing = byPath.get(route.urlPath);\n if (!existing) {\n byPath.set(route.urlPath, route);\n continue;\n }\n\n let winner = existing;\n let loser = route;\n const routeWins =\n (existing.isSlotRoute && !route.isSlotRoute) ||\n (!!existing.isSlotRoute === !!route.isSlotRoute &&\n route.segmentPath.localeCompare(existing.segmentPath) < 0);\n if (routeWins) {\n winner = route;\n loser = existing;\n }\n\n if (!winner.hasSearchParams && loser.hasSearchParams) {\n winner = {\n ...winner,\n hasSearchParams: true,\n searchParamsPagePath: loser.searchParamsPagePath,\n };\n }\n byPath.set(route.urlPath, winner);\n }\n return [...byPath.values()];\n}\n\n/**\n * Determine the TypeScript type for a segment's param.\n */\n/**\n * Derive the bracket key for schema/codegen lookup.\n * For bare segments like `[id]` this is the segmentName itself.\n * For affixed segments like `img-[id].png` this is `[id]` — the bare bracket form.\n */\nfunction bracketNameForSegment(node: SegmentNode): string {\n if (node.paramPrefix || node.paramSuffix) {\n return `[${node.paramName}]`;\n }\n return node.segmentName;\n}\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 * 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 // Generic constraint enforces that all values are `true` — if any value is\n // an error tuple, TS reports a real diagnostic instead of silently computing it.\n lines.push(\n 'type _EnforceSchemaValues<_T extends Record<string, true> = _AssertSchemaValues> = true;'\n );\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 // urlPath-keyed emissions use the deduped list (slot pages share their\n // parent's urlPath); segmentPath-keyed emissions below keep all entries.\n const uniqueByUrlPath = dedupeRoutesByUrlPath(routes);\n\n lines.push(\"declare module '@timber-js/app' {\");\n lines.push(' interface Routes {');\n\n for (const route of uniqueByUrlPath) {\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 // useQueryStates + typed Link are urlPath-keyed → deduped list.\n const pageRoutes = uniqueByUrlPath.filter((r) => !r.isApiRoute);\n // getSegmentParams / useSegmentParams are segmentPath-keyed → full list,\n // so slot segments keep their own typed overloads.\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 * 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 { readFileCached } from './file-cache.js';\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 // Use bare bracket form for schema keys — affixed segments like\n // `img-[id].png` register as `[id]`, matching runtime codec lookup.\n if (classified.kind === 'dynamic' && (classified.prefix || classified.suffix)) {\n segments.add(`[${classified.name}]`);\n } else {\n segments.add(node.segmentName);\n }\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 = readFileCached(schemaPath);\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 * 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\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(interceptingNode, interceptedUrlBase, parentUrlPath, rewrites);\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 rewrites: InterceptionRewrite[]\n): void {\n if (node.page) {\n rewrites.push({\n interceptedPattern: interceptedUrlPath,\n interceptingPrefix,\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(child, childUrl, interceptingPrefix, rewrites);\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 * 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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cli-schema-sync.d.ts","sourceRoot":"","sources":["../src/cli-schema-sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAK/D;AAoED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,GACd;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAoBlD;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAajE;AAKD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI1D;AA6ID,MAAM,WAAW,gBAAgB;IAC/B,wCAAwC;IACxC,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,iDAAiD;IACjD,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,gCAAgC;IAChC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,4DAA4D;IAC5D,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CA8DnE"}
1
+ {"version":3,"file":"cli-schema-sync.d.ts","sourceRoot":"","sources":["../src/cli-schema-sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAoBH;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAK/D;AAqED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,GACd;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAoBlD;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAajE;AAKD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI1D;AA6ID,MAAM,WAAW,gBAAgB;IAC/B,wCAAwC;IACxC,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,iDAAiD;IACjD,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,gCAAgC;IAChC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,4DAA4D;IAC5D,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CA8DnE"}
package/dist/cli.js CHANGED
@@ -116,7 +116,7 @@ async function runCheck(options) {
116
116
  async function runSchema(args) {
117
117
  const subcommand = args[0];
118
118
  if (subcommand !== "sync") throw new Error(`Unknown schema subcommand: ${subcommand ?? "(none)"}. Usage: timber schema sync`);
119
- const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync-B73L6pMq.js").then((n) => n.t);
119
+ const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync-B5FDplGI.js").then((n) => n.t);
120
120
  const result = runSchemaSync(process.cwd());
121
121
  if (result.filesystemSegments.length === 0) {
122
122
  console.log("[timber] No dynamic segments found in app/.");
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { n as classifyUrlSegment } from "../_chunks/segment-classify-CDDRVKs7.js";
2
+ import { n as classifyUrlSegment } from "../_chunks/segment-classify-Byy425ng.js";
3
3
  import { t as getSearchParamsDefinition } from "../_chunks/registry-DbJPKoBp.js";
4
4
  import { n as getSsrData } from "../_chunks/ssr-data-BOWsq18U.js";
5
5
  import { n as useSegmentParams, o as useNavigationContext } from "../_chunks/use-segment-params-D1CzlgBo.js";
@@ -116,7 +116,10 @@ function resolveSegment(seg, params, pattern) {
116
116
  if (Array.isArray(value)) throw new Error(`<Link> param "${seg.name}" expected a string but received an array for pattern "${pattern}".`);
117
117
  const codec = getLinkCodec(`[${seg.name}]`);
118
118
  const str = codec ? codec.serialize(value) ?? String(value) : String(value);
119
- return encodeURIComponent(str);
119
+ const encoded = encodeURIComponent(str);
120
+ const prefix = seg.prefix ?? "";
121
+ const suffix = seg.suffix ?? "";
122
+ return prefix + encoded + suffix;
120
123
  }
121
124
  }
122
125
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-link-status.ts","../../src/client/link.tsx","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/navigation-api.ts","../../src/client/shallow-url.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx"],"sourcesContent":["'use client';\n\n// useLinkStatus — returns { isPending: true } while the nearest parent <Link>'s\n// navigation is in flight. No arguments — scoped via React context.\n// See design/19-client-navigation.md §\"useLinkStatus()\"\n\nimport { useContext, createContext } from 'react';\n\nexport interface LinkStatus {\n isPending: boolean;\n}\n\n/**\n * React context provided by <Link>. Holds the pending status\n * for that specific link's navigation.\n */\nexport const LinkStatusContext = createContext<LinkStatus>({ isPending: false });\n\n/**\n * Returns `{ isPending: true }` while the nearest parent `<Link>` component's\n * navigation is in flight. Must be used inside a `<Link>` component's children.\n *\n * Unlike `usePendingNavigation()` which is global, this hook is scoped to\n * the nearest parent `<Link>` — only the link the user clicked shows pending.\n *\n * ```tsx\n * 'use client'\n * import { Link, useLinkStatus } from '@timber-js/app/client'\n *\n * function Hint() {\n * const { isPending } = useLinkStatus()\n * return <span className={isPending ? 'opacity-50' : ''} />\n * }\n *\n * export function NavLink({ href, children }) {\n * return (\n * <Link href={href}>\n * {children} <Hint />\n * </Link>\n * )\n * }\n * ```\n */\nexport function useLinkStatus(): LinkStatus {\n return useContext(LinkStatusContext);\n}\n","'use client';\n\n// Link component — client-side navigation with progressive enhancement\n// See design/19-client-navigation.md § Progressive Enhancement\n//\n// Without JavaScript, <Link> renders as a plain <a> tag — standard browser\n// navigation. With JavaScript, the Link component's onClick handler triggers\n// RSC-based client navigation via the router.\n//\n// Each Link owns its own click handler — no global event delegation.\n// This keeps navigation within React's component tree, ensuring pending\n// state (useLinkStatus) updates atomically with the navigation.\n//\n// Typed Link: design/09-typescript.md §\"Typed Link\"\n// - href validated against known routes (via codegen overloads, not runtime)\n// - params prop typed per-route, URL interpolated at runtime\n// - searchParams prop serialized via SearchParamsDefinition\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport {\n useTransition,\n type AnchorHTMLAttributes,\n type ReactNode,\n type MouseEvent as ReactMouseEvent,\n} from 'react';\nimport type { SearchParamsDefinition } from '../search-params/define.js';\nimport { getSearchParamsDefinition } from '../search-params/registry.js';\nimport type { LinkFunction } from './index.js';\nimport { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';\nimport {\n validateNavigationHref as validateLinkHref,\n isInternalHref,\n} from '../shared/href-validation.js';\nimport { LinkStatusContext } from './use-link-status.js';\nimport { getRouterOrNull } from './router-ref.js';\nimport { getSsrData } from './ssr-data.js';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.js';\nimport { getLinkCodec } from '../params/codec-registry.js';\nimport type { LinkStatus } from './use-link-status.js';\n\nconst LINK_PENDING: LinkStatus = { isPending: true };\nconst LINK_IDLE: LinkStatus = { isPending: false };\n\n// ─── Current Search Params ────────────────────────────────────────\n\n/**\n * Read the current URL's search string without requiring a React hook.\n * On the client, reads window.location.search. During SSR, reads from\n * the request context (getSsrData). Returns empty string if unavailable.\n */\nfunction getCurrentSearch(): string {\n if (typeof window !== 'undefined') return window.location.search;\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type OnNavigateEvent = {\n preventDefault: () => void;\n};\n\nexport type OnNavigateHandler = (e: OnNavigateEvent) => void;\n\n/**\n * Base props shared by all Link variants.\n *\n * Exported so the public `LinkFunction` interface (declared in\n * `./index.ts`, where module augmentation can merge into it) can\n * compose this without duplication.\n */\nexport interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** Prefetch the RSC payload on hover */\n prefetch?: boolean;\n /**\n * Scroll to top on navigation. Defaults to true.\n * Set to false for tabbed interfaces where content changes within a fixed layout.\n */\n scroll?: boolean;\n /**\n * Preserve search params from the current URL across navigation.\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 * Useful for route-group gating where a search param (e.g. `?private=access`)\n * must persist across internal navigations. The target href's own search params\n * always take precedence over preserved ones.\n *\n * During SSR, reads search params from the request context. On the client,\n * reads from the current URL and updates reactively when the URL changes.\n */\n preserveSearchParams?: true | string[];\n /**\n * Called before client-side navigation commits. Call `e.preventDefault()`\n * to cancel the default navigation — the caller is then responsible for\n * navigating (e.g. via `router.push()`).\n *\n * Only fires for client-side SPA navigations, not full page loads.\n * Has no effect during SSR.\n */\n onNavigate?: OnNavigateHandler;\n children?: ReactNode;\n}\n\n// ─── Typed Link Props ────────────────────────────────────────────\n\n/**\n * Widen server-side string params to string | number for Link convenience.\n * Exported for use by codegen-generated overloads.\n */\nexport type LinkSegmentParams<T> = {\n [K in keyof T]: [string] extends [T[K]] ? string | number : T[K];\n};\n\n// ─── External Href Types ─────────────────────────────────────────\n//\n// `ExternalHref` and the public `LinkFunction` interface live in\n// `./index.ts` rather than this file. They MUST be originally declared\n// in the same module that the codegen augments (`@timber-js/app/client`)\n// so that codegen-generated per-route call signatures merge with the\n// same interface that types the `Link` constant. Re-exporting an\n// interface via `export type {}` does NOT participate in module\n// augmentation merging — only originally-declared interfaces do.\n// See TIM-624.\n\n// ─── searchParams prop shapes ────────────────────────────────────\n//\n// Per-route Link overloads (emitted by codegen) pass the flat values shape:\n// searchParams={{ page: 2, q: 'boots' }}\n// The framework looks up the route's SearchParamsDefinition from the\n// search-params registry at runtime (see TIM-830).\n//\n// The catch-all overload in client/index.ts (external/computed hrefs)\n// additionally accepts the legacy wrapped shape:\n// searchParams={{ definition: def, values: { page: 2 } }}\n// because there is no way to look up a definition from a computed string.\n//\n// `resolveHref` discriminates at runtime by presence of a `definition` key.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ParamValue = string | number | string[] | { toString(): string; [key: string]: any };\n\ntype WrappedSearchParamsProp = {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n};\ntype FlatSearchParamsProp = Record<string, unknown>;\ntype LinkSearchParamsProp = WrappedSearchParamsProp | FlatSearchParamsProp;\n\n/**\n * Runtime-only loose props used internally by the Link implementation.\n * Not exposed to callers — the public API uses LinkFunction.\n */\ninterface LinkRuntimeProps extends LinkBaseProps {\n href: string;\n segmentParams?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n}\n\n// Legacy exports for backward compat (used by buildLinkProps, tests, etc.)\nexport type LinkPropsWithHref = LinkBaseProps & {\n href: string;\n segmentParams?: never;\n searchParams?: LinkSearchParamsProp;\n};\nexport type LinkPropsWithParams = LinkRuntimeProps & {\n segmentParams: Record<string, ParamValue>;\n};\nexport type LinkProps = LinkRuntimeProps;\n\nexport { validateLinkHref, isInternalHref };\n\n// ─── URL Interpolation ──────────────────────────────────────────\n\n/**\n * Interpolate dynamic segments in a route pattern with actual values.\n * e.g. interpolateParams(\"/products/[id]\", { id: \"123\" }) → \"/products/123\"\n *\n * Supports:\n * - [param] → single segment\n * - [...param] → catch-all (joined with /)\n * - [[...param]] → optional catch-all (omitted if undefined/empty)\n */\n/**\n * Parse a route pattern's path portion into classified segments.\n * Exported for testing. Uses the shared character-based classifier.\n */\nexport function parseSegments(pattern: string): UrlSegment[] {\n return pattern.split('/').filter(Boolean).map(classifyUrlSegment);\n}\n\n/**\n * Resolve a single classified segment into its string representation.\n * Returns null for optional catch-all with no value (filtered out before join).\n *\n * When schema codecs are registered (via virtual:timber-schema), uses\n * codec.serialize() for URL construction instead of plain String().\n */\nfunction resolveSegment(\n seg: UrlSegment,\n params: Record<string, ParamValue>,\n pattern: string\n): string | null {\n switch (seg.kind) {\n case 'static':\n return seg.value;\n\n case 'optional-catch-all': {\n const value = params[seg.name];\n if (value === undefined || (Array.isArray(value) && value.length === 0)) {\n return null;\n }\n const codec = getLinkCodec(`[[...${seg.name}]]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) return null;\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'catch-all': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(\n `<Link> missing required catch-all param \"${seg.name}\" for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[...${seg.name}]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" codec returned null for pattern \"${pattern}\".`\n );\n }\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n if (segments.length === 0) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" must have at least one segment for pattern \"${pattern}\".`\n );\n }\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'dynamic': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(`<Link> missing required param \"${seg.name}\" for pattern \"${pattern}\".`);\n }\n if (Array.isArray(value)) {\n throw new Error(\n `<Link> param \"${seg.name}\" expected a string but received an array for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[${seg.name}]`);\n const str = codec ? (codec.serialize(value) ?? String(value)) : String(value);\n return encodeURIComponent(str);\n }\n }\n}\n\n/**\n * Split a URL pattern into the path portion and any trailing ?query/#hash suffix.\n * Uses URL parsing for correctness rather than manual index arithmetic.\n */\nfunction splitPatternSuffix(pattern: string): [path: string, suffix: string] {\n if (!pattern.includes('?') && !pattern.includes('#')) {\n return [pattern, ''];\n }\n const url = new URL(pattern, 'http://x');\n const suffix = url.search + url.hash;\n const path = pattern.slice(0, pattern.length - suffix.length);\n return [path, suffix];\n}\n\nexport function interpolateParams(pattern: string, params: Record<string, ParamValue>): string {\n const [pathPart, suffix] = splitPatternSuffix(pattern);\n\n const resolved = parseSegments(pathPart)\n .map((seg) => resolveSegment(seg, params, pattern))\n .filter((s): s is string => s !== null);\n return ('/' + resolved.join('/') || '/') + suffix;\n}\n\n// ─── Resolve Href ───────────────────────────────────────────────\n\n/**\n * Resolve the final href string from Link props.\n *\n * Handles:\n * - params interpolation into route patterns\n * - searchParams serialization via SearchParamsDefinition\n * - Validation that searchParams and inline query strings are exclusive\n */\n/**\n * Runtime discriminator: treat `searchParams` as the legacy wrapped shape\n * only when it literally has a `definition` key. Everything else is the\n * flat `Partial<T>` values shape (TIM-830).\n */\nfunction isWrappedSearchParamsProp(sp: LinkSearchParamsProp): sp is WrappedSearchParamsProp {\n return 'definition' in sp;\n}\n\nexport function resolveHref(\n href: string,\n params?: Record<string, ParamValue>,\n searchParams?: LinkSearchParamsProp\n): string {\n let resolvedPath = href;\n\n // Interpolate params if provided\n if (params) {\n resolvedPath = interpolateParams(href, params);\n }\n\n // Serialize searchParams if provided\n if (searchParams) {\n // Validate: searchParams prop and inline query string are mutually exclusive\n if (resolvedPath.includes('?')) {\n throw new Error(\n '<Link> received both a searchParams prop and a query string in href. ' +\n 'These are mutually exclusive — use one or the other.'\n );\n }\n\n let definition: SearchParamsDefinition<Record<string, unknown>> | undefined;\n let values: Record<string, unknown>;\n\n if (isWrappedSearchParamsProp(searchParams)) {\n // Legacy wrapped shape — used by the catch-all overload for\n // computed/external hrefs where no route lookup is possible.\n definition = searchParams.definition;\n values = searchParams.values;\n } else {\n // Flat shape (TIM-830): look up the definition from the runtime\n // registry using the un-interpolated href pattern (e.g. '/products/[id]').\n // The search-params registry is populated eagerly at startup by the\n // virtual:timber-search-params-registry module generated by the\n // timber-routing Vite plugin.\n definition = getSearchParamsDefinition(href) as\n | SearchParamsDefinition<Record<string, unknown>>\n | undefined;\n values = searchParams;\n }\n\n if (definition) {\n const qs = definition.serialize(values);\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n } else {\n // No registered definition — serialize flat object as plain query params.\n const usp = new URLSearchParams();\n for (const [key, val] of Object.entries(values)) {\n if (val === undefined || val === null) continue;\n if (Array.isArray(val)) {\n for (const item of val) usp.append(key, String(item));\n } else {\n usp.set(key, String(val));\n }\n }\n const qs = usp.toString();\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n }\n }\n\n return resolvedPath;\n}\n\n// ─── Build Props ─────────────────────────────────────────────────\n\ninterface LinkOutputProps {\n href: string;\n}\n\n/**\n * Build the HTML attributes for a Link. Separated from the component\n * for testability — the component just spreads these onto an <a>.\n */\nexport function buildLinkProps(\n props: Pick<LinkPropsWithHref, 'href'> & {\n params?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n }\n): LinkOutputProps {\n const resolvedHref = resolveHref(props.href, props.params, props.searchParams);\n validateLinkHref(resolvedHref);\n return { href: resolvedHref };\n}\n\n// ─── Click Handler ───────────────────────────────────────────────\n\n/**\n * Should this click be intercepted for SPA navigation?\n *\n * Returns false (pass through to browser) when:\n * - Modified keys are held (Ctrl, Meta, Shift, Alt) — open in new tab\n * - The click is not the primary button\n * - The event was already prevented by a parent handler\n * - The link has target=\"_blank\" or similar\n * - The link has a download attribute\n * - The href is external\n */\nfunction shouldInterceptClick(\n event: ReactMouseEvent<HTMLAnchorElement>,\n resolvedHref: string\n): boolean {\n if (event.button !== 0) return false;\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;\n if (event.defaultPrevented) return false;\n\n const anchor = event.currentTarget;\n if (anchor.target && anchor.target !== '_self') return false;\n if (anchor.hasAttribute('download')) return false;\n\n if (!isInternalHref(resolvedHref)) return false;\n\n return true;\n}\n\n// ─── Link Component ──────────────────────────────────────────────\n\n/**\n * Navigation link with progressive enhancement.\n *\n * Renders as a plain `<a>` tag — works without JavaScript. When the client\n * runtime is active, the Link's onClick handler triggers RSC-based client\n * navigation via the router. No global event delegation — each Link owns\n * its own click handling.\n *\n * Supports typed routes via the Routes interface (populated by codegen).\n * At runtime:\n * - `segmentParams` prop interpolates dynamic segments in the href pattern\n * - `searchParams` prop serializes query parameters via a SearchParamsDefinition\n *\n * Typed via the LinkFunction callable interface. The base call signature\n * forbids segmentParams; per-route signatures are added by codegen via\n * interface merging. See TIM-624.\n */\n// Cast to LinkFunction — the callable interface provides the public type,\n// but the implementation destructures LinkRuntimeProps internally.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const Link: LinkFunction = function LinkImpl(props: any) {\n const {\n href,\n prefetch,\n scroll,\n segmentParams,\n searchParams,\n preserveSearchParams,\n onNavigate,\n onClick: userOnClick,\n onMouseEnter: userOnMouseEnter,\n children,\n ...rest\n } = props as LinkRuntimeProps;\n const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });\n\n // ─── Per-link pending state ─────────────────────────────────────────\n // Each Link owns a useTransition. On click, it wraps\n // router.navigate() in startTransition — only this Link re-renders\n // for isPending, zero siblings touched. The transition tracks the\n // full navigation lifecycle (fetch + render + commit).\n const [isPending, startTransition] = useTransition();\n const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;\n\n // Preserve search params from the current URL when requested.\n // useSearchParams() works during both SSR (reads from request context)\n // and on the client (reads from window.location, reactive to URL changes).\n // We read current search params directly to avoid unconditional hook calls.\n // On the client, window.location.search is always current; during SSR,\n // getSsrData() provides the request's search params.\n const internal = isInternalHref(baseHref);\n\n // Only preserve search params for internal links — leaking current\n // page params (tokens, UTM, etc.) to external domains is a data leak.\n const resolvedHref =\n preserveSearchParams && internal\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : baseHref;\n\n // ─── Click handler ───────────────────────────────────────────\n // Each Link component owns its click handling. The router is\n // accessed via the singleton ref — during SSR, getRouterOrNull()\n // returns null and onClick is a no-op (the <a> works as a plain link).\n const handleClick = internal\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n // Call user's onClick first (e.g., analytics)\n userOnClick?.(event);\n\n if (!shouldInterceptClick(event, resolvedHref)) return;\n\n // Call onNavigate if provided — allows caller to cancel\n if (onNavigate) {\n let prevented = false;\n onNavigate({\n preventDefault: () => {\n prevented = true;\n },\n });\n if (prevented) {\n event.preventDefault();\n return;\n }\n }\n\n const router = getRouterOrNull();\n if (!router) return;\n\n const navHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref;\n\n const resolved = new URL(navHref, window.location.href);\n\n if (\n resolved.pathname === window.location.pathname &&\n resolved.search === window.location.search &&\n resolved.hash\n ) {\n return;\n }\n\n event.preventDefault();\n\n const shouldScroll = scroll !== false;\n // Keep the #fragment — the router commits it to the address bar and\n // scrolls to the matching element after render. The hash is stripped\n // from the RSC fetch URL inside the router (TIM-1035).\n const absoluteHref = resolved.pathname + resolved.search + resolved.hash;\n\n startTransition(async () => {\n await router.navigate(absoluteHref, { scroll: shouldScroll });\n });\n }\n : userOnClick; // External links — just pass through user's onClick\n\n // ─── Hover prefetch ──────────────────────────────────────────\n const handleMouseEnter =\n internal && prefetch\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n userOnMouseEnter?.(event);\n const router = getRouterOrNull();\n if (router) {\n const prefetchHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref;\n const resolved = new URL(prefetchHref, window.location.href);\n router.prefetch(resolved.pathname + resolved.search);\n }\n }\n : userOnMouseEnter;\n\n return (\n <a {...rest} href={resolvedHref} onClick={handleClick} onMouseEnter={handleMouseEnter}>\n <LinkStatusContext.Provider value={linkStatus}>{children}</LinkStatusContext.Provider>\n </a>\n );\n};\n","/**\n * useRouter() — client-side hook for programmatic navigation.\n *\n * Returns a router instance with push, replace, refresh, back, forward,\n * and prefetch methods. Compatible with Next.js's `useRouter()` from\n * `next/navigation` (App Router).\n *\n * This wraps timber's internal RouterInstance in the Next.js-compatible\n * AppRouterInstance shape that ecosystem libraries expect.\n *\n * NOTE: Unlike Next.js, these methods do NOT wrap navigation in\n * startTransition. In Next.js, router state is React state (useReducer)\n * so startTransition defers the update and provides isPending tracking.\n * In timber, navigation calls reactRoot.render() which is a root-level\n * render — startTransition has no effect on root renders.\n *\n * Navigation state (params, pathname) is delivered atomically via\n * NavigationContext embedded in the element tree passed to\n * reactRoot.render(). See design/19-client-navigation.md §\"NavigationContext\".\n *\n * For loading UI during navigation, use:\n * - useLinkStatus() — per-link pending indicator (inside <Link>)\n * - usePendingNavigation() — global navigation pending state\n */\n\nimport { getRouterOrNull } from './router-ref.js';\nimport { validateNavigationHref } from '../shared/href-validation.js';\n\nexport interface AppRouterInstance {\n /** Navigate to a URL, pushing a new history entry */\n push(href: string, options?: { scroll?: boolean }): void;\n /** Navigate to a URL, replacing the current history entry */\n replace(href: string, options?: { scroll?: boolean }): void;\n /** Refresh the current page (re-fetch RSC payload) */\n refresh(): void;\n /** Navigate back in history */\n back(): void;\n /** Navigate forward in history */\n forward(): void;\n /** Prefetch an RSC payload for a URL */\n prefetch(href: string): void;\n}\n\n/**\n * Get a router instance for programmatic navigation.\n *\n * Compatible with Next.js's `useRouter()` from `next/navigation`.\n *\n * Methods lazily resolve the global router when invoked (during user\n * interaction) rather than capturing it at render time. This is critical\n * because during hydration, React synchronously executes component render\n * functions *before* the router is bootstrapped in browser-entry.ts.\n * If we eagerly captured the router during render, components would get\n * a null reference and be stuck with silent no-ops forever.\n *\n * Returns safe no-ops during SSR or before bootstrap. The `typeof window`\n * check is insufficient because Vite's client SSR environment defines\n * `window`, so we use a try/catch on getRouter() — but only at method\n * invocation time, not at render time.\n */\nexport function useRouter(): AppRouterInstance {\n return {\n push(href: string, options?: { scroll?: boolean }) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error(\n '[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.'\n );\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll });\n },\n replace(href: string, options?: { scroll?: boolean }) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().replace() called but router is not initialized.');\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll, replace: true });\n },\n refresh() {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().refresh() called but router is not initialized.');\n }\n return;\n }\n void router.refresh();\n },\n back() {\n if (typeof window !== 'undefined') window.history.back();\n },\n forward() {\n if (typeof window !== 'undefined') window.history.forward();\n },\n prefetch(href: string) {\n const router = getRouterOrNull();\n if (!router) return; // Silent — prefetch failure is non-fatal\n router.prefetch(href);\n },\n };\n}\n","/**\n * usePathname() — client-side hook for reading the current pathname.\n *\n * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').\n * Updates when client-side navigation changes the URL.\n *\n * On the client, reads from NavigationContext which is updated atomically\n * with the RSC tree render. This replaces the previous useSyncExternalStore\n * approach which only subscribed to popstate events — meaning usePathname()\n * did NOT re-render on forward navigation (pushState). The context approach\n * fixes this: pathname updates in the same render pass as the new tree.\n *\n * During SSR, reads the request pathname from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { getSsrData } from './ssr-data.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n/**\n * Read the current URL pathname.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). During SSR, reads from the\n * ALS-backed SSR data context. Falls back to window.location.pathname\n * when called outside a React component (e.g., in tests).\n */\nexport function usePathname(): string {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.pathname;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to SSR/fallback below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n const ssrData = getSsrData();\n if (ssrData) return ssrData.pathname ?? '/';\n\n // Final fallback: window.location (tests, edge cases).\n if (typeof window !== 'undefined') return window.location.pathname;\n return '/';\n}\n","/**\n * Navigation API integration — progressive enhancement for client navigation.\n *\n * When the Navigation API (`window.navigation`) is available, this module\n * provides an intercept-based navigation model that replaces the separate\n * popstate + click handler approach with a single navigate event listener.\n *\n * Key benefits:\n * - Intercepts ALL navigations (link clicks, form submissions, back/forward)\n * - Built-in AbortSignal per navigation (auto-aborts in-flight fetches)\n * - Per-entry state via NavigationHistoryEntry.getState()\n * - navigation.transition for progress tracking\n *\n * When unavailable, all functions are no-ops and the History API fallback\n * in browser-entry.ts handles navigation.\n *\n * See design/19-client-navigation.md\n */\n\nimport { isHardNavigating } from './navigation-root.js';\n\n// ─── Feature Detection ───────────────────────────────────────────\n\n/**\n * Returns true if the Navigation API is available in the current environment.\n * Feature-detected at runtime — no polyfill.\n */\nexport function hasNavigationApi(): boolean {\n return typeof window !== 'undefined' && 'navigation' in window && window.navigation != null;\n}\n\n/**\n * Get the Navigation API instance. Returns null if unavailable.\n */\nexport function getNavigationApi(): Navigation | null {\n if (!hasNavigationApi()) return null;\n return window.navigation;\n}\n\n// ─── Navigation API Controller ───────────────────────────────────\n\n/**\n * Callbacks for the Navigation API event handler.\n *\n * When the Navigation API intercepts a navigation, it delegates to these\n * callbacks which run the RSC fetch + render pipeline.\n */\nexport interface NavigationApiCallbacks {\n /**\n * Handle a push/replace navigation intercepted by the Navigation API.\n * This covers both Link <a> clicks (user-initiated) and external\n * navigations (plain <a> tags, programmatic).\n * The Navigation API handles the URL update via event.intercept().\n */\n onExternalNavigate: (\n url: string,\n options: { replace: boolean; signal: AbortSignal; scroll?: boolean }\n ) => Promise<void>;\n\n /**\n * Handle a traversal (back/forward button). The Navigation API intercepts\n * the traversal and delegates to us for RSC replay/fetch.\n */\n onTraverse: (url: string, scrollY: number, signal: AbortSignal) => Promise<void>;\n\n /**\n * Called when a shallow URL update is intercepted (e.g., nuqs with\n * shallow: true, or replaceUrl). The URL has already been committed —\n * this callback syncs NavigationContext.search so useSearchParams()\n * reflects the new value without a full router navigation.\n */\n onShallowNavigate?: (url: string) => void;\n}\n\n/**\n * Controller returned by setupNavigationApi. Provides methods to\n * coordinate between the router and the navigate event listener.\n */\nexport interface NavigationApiController {\n /**\n * Set the router-navigating flag. When `true`, the next navigate event\n * (from pushState/replaceState) is recognized as router-initiated. The\n * handler still intercepts it — but ties the browser's native loading\n * state to a deferred promise instead of running the RSC pipeline again.\n *\n * This means `navigation.transition` is active for the full duration of\n * every router-initiated navigation, giving the browser a native loading\n * indicator (tab spinner, address bar) aligned with the TopLoader.\n *\n * Must be called synchronously around pushState/replaceState:\n * controller.setRouterNavigating(true);\n * history.pushState(...); // navigate event fires, intercepted\n * controller.setRouterNavigating(false); // flag off, deferred stays open\n */\n setRouterNavigating: (value: boolean) => void;\n\n /**\n * Resolve the deferred promise created by setRouterNavigating(true),\n * clearing the browser's native loading state. Call this when the\n * navigation fully completes — aligned with when the TopLoader's\n * pendingUrl clears (same finally block in router.navigate).\n */\n completeRouterNavigation: () => void;\n\n /**\n * Initiate a navigation via the Navigation API (`navigation.navigate()`).\n * Unlike `history.pushState()`, this fires the navigate event BEFORE\n * committing the URL — allowing Chrome to show its native loading\n * indicator while the intercept handler runs.\n *\n * Must be called with setRouterNavigating(true) active so the handler\n * recognizes it as router-initiated and uses the deferred promise.\n */\n navigate: (url: string, replace: boolean) => void;\n\n /**\n * Save scroll position into the current navigation entry's state.\n * Uses navigation.updateCurrentEntry() for per-entry scroll storage.\n */\n saveScrollPosition: (scrollY: number) => void;\n\n /**\n * Check if the Navigation API has an active transition.\n * Returns the transition object if available, null otherwise.\n */\n hasActiveTransition: () => boolean;\n\n /** Remove the navigate event listener. */\n cleanup: () => void;\n}\n\n/**\n * Set up the Navigation API navigate event listener.\n *\n * Intercepts same-origin navigations and delegates to the provided callbacks.\n * Router-initiated navigations (pushState from router.navigate) are detected\n * via a synchronous flag and NOT intercepted — the router already handles them.\n *\n * Returns a controller for coordinating with the router.\n */\nexport function setupNavigationApi(callbacks: NavigationApiCallbacks): NavigationApiController {\n const nav = getNavigationApi()!;\n\n let routerNavigating = false;\n\n // Deferred promise for router-initiated navigations. Created when\n // setRouterNavigating(true) is called, resolved by completeRouterNavigation().\n // The navigate event handler intercepts with this promise so the browser's\n // native loading state (tab spinner) stays active until the navigation\n // completes — aligned with TopLoader's pendingUrl lifecycle.\n let routerNavDeferred: { promise: Promise<void>; resolve: () => void } | null = null;\n\n function handleNavigate(event: NavigateEvent): void {\n // Skip non-interceptable navigations (cross-origin, etc.)\n if (!event.canIntercept) return;\n\n // Hard navigation guard: when the router has triggered a full page\n // load (500 error, version skew), skip interception entirely so the\n // browser performs the MPA navigation. Without this guard, setting\n // window.location.href fires a navigate event that we'd intercept,\n // running the RSC pipeline again → 500 → window.location.href →\n // navigate event → infinite loop.\n // See design/19-client-navigation.md §\"Hard Navigation Guard\"\n if (isHardNavigating()) return;\n\n // Skip download requests\n if (event.downloadRequest) return;\n\n // Skip blob: URLs — these are almost always downloads or object-URL\n // navigations initiated by the host page (e.g., generated files, PDFs).\n // The RSC pipeline cannot handle them, and intercepting would break\n // the download/open behavior the host page expects.\n if (event.destination.url.startsWith('blob:')) return;\n\n // Skip hash-only changes — let the browser handle scroll-to-anchor\n if (event.hashChange) return;\n\n // Shallow URL updates (e.g., nuqs search param changes). The navigation\n // only changes the URL — no server round trip needed. Intercept with a\n // no-op handler so the Navigation API commits the URL change without\n // triggering a full page navigation (which is the default if we don't\n // intercept). The info property is the Navigation API's built-in\n // per-navigation metadata — no side-channel flags needed.\n const info = event.info as { shallow?: boolean } | null | undefined;\n if (info?.shallow) {\n event.intercept({\n handler: () => Promise.resolve(),\n focusReset: 'manual',\n scroll: 'manual',\n });\n callbacks.onShallowNavigate?.(event.destination.url);\n return;\n }\n\n // Skip form submissions with a body (POST/PUT/etc.). These need the\n // browser's native form handling to send the request body to the server.\n // Intercepting would convert them into GET RSC navigations, dropping\n // the form data. Server actions use fetch() directly (not form navigation),\n // so they are unaffected by this check.\n if (event.formData) return;\n\n // Skip cross-origin (defense-in-depth — canIntercept covers this)\n const destUrl = new URL(event.destination.url);\n if (destUrl.origin !== location.origin) return;\n\n // Router-initiated navigation (Link click → router.navigate → pushState).\n // The router is already running the RSC pipeline — don't run it again.\n // Instead, intercept with the deferred promise so the browser's native\n // loading state tracks the navigation's full lifecycle. This aligns the\n // tab spinner / address bar indicator with the TopLoader.\n if (routerNavigating && routerNavDeferred) {\n event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n handler: () => routerNavDeferred!.promise,\n });\n return;\n }\n\n // Skip reload navigations — let the browser handle full page reload\n if (event.navigationType === 'reload') return;\n\n const url = destUrl.pathname + destUrl.search;\n\n if (event.navigationType === 'traverse') {\n // Back/forward button — intercept and delegate to router.\n // Read scroll position from the destination entry's state.\n const entryState = event.destination.getState() as\n | { scrollY?: number; timber?: boolean }\n | null\n | undefined;\n const scrollY = entryState && typeof entryState.scrollY === 'number' ? entryState.scrollY : 0;\n\n event.intercept({\n // Manual scroll — we handle scroll restoration ourselves\n // via afterPaint (same as the History API path).\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onTraverse(url, scrollY, event.signal);\n },\n });\n } else if (event.navigationType === 'push' || event.navigationType === 'replace') {\n // Push/replace — a Link <a> click or an external navigation\n // (plain <a> tag, programmatic).\n\n // Save the departing page's scroll position BEFORE event.intercept()\n // commits the URL change. Once intercept() is called, currentEntry\n // switches to the new (destination) entry — any updateCurrentEntry()\n // call after that would save to the wrong entry.\n // See: router.navigate() also calls saveNavigationEntryScroll(), but\n // for Navigation API <a> click navigations (where Link does NOT call\n // router.navigate directly), the router's save runs inside the\n // intercept handler — too late, currentEntry has already switched.\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY: window.scrollY },\n });\n } catch {\n // Ignore — entry may be disposed\n }\n\n event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n // Keep the #fragment — router.navigate splits it back off for the\n // RSC fetch and scrolls to the matching element after render\n // (TIM-1035). Traversals above stay hash-less: history-stack keys\n // use pathname + search.\n await callbacks.onExternalNavigate(url + destUrl.hash, {\n replace: event.navigationType === 'replace',\n signal: event.signal,\n scroll: undefined,\n });\n },\n });\n }\n }\n\n nav.addEventListener('navigate', handleNavigate);\n\n return {\n setRouterNavigating(value: boolean): void {\n routerNavigating = value;\n if (value) {\n // Create a new deferred promise. The navigate event handler will\n // intercept and tie the browser's loading state to this promise.\n let resolve!: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n routerNavDeferred = { promise, resolve };\n } else {\n // Flag off — but DON'T resolve the deferred here. The navigation\n // is still in flight (RSC fetch + render). completeRouterNavigation()\n // resolves it when the navigation fully completes.\n routerNavigating = false;\n }\n },\n\n completeRouterNavigation(): void {\n if (routerNavDeferred) {\n routerNavDeferred.resolve();\n routerNavDeferred = null;\n }\n },\n\n navigate(url: string, replace: boolean): void {\n // Use navigation.navigate() instead of history.pushState().\n // This fires the navigate event BEFORE committing the URL,\n // which lets Chrome show its native loading indicator while\n // the intercept handler (deferred promise) is pending.\n // history.pushState() commits the URL synchronously, so Chrome\n // sees the navigation as already complete and skips the indicator.\n nav.navigate(url, {\n history: replace ? 'replace' : 'push',\n });\n },\n\n saveScrollPosition(scrollY: number): void {\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY },\n });\n } catch {\n // Ignore errors — updateCurrentEntry may throw if entry is disposed\n }\n },\n\n hasActiveTransition(): boolean {\n return nav.transition != null;\n },\n\n cleanup(): void {\n nav.removeEventListener('navigate', handleNavigate);\n },\n };\n}\n","/**\n * Shallow URL replacement — update the browser URL bar without triggering\n * RSC navigation, TopLoader, or any server round-trip.\n *\n * Uses the Navigation API's `info: { shallow: true }` when available (Chrome),\n * which the navigate event handler intercepts with a no-op handler. Falls back\n * to raw `history.replaceState` (Safari/Firefox — no navigate event fired).\n */\n\nimport { getNavigationApi } from './navigation-api.js';\n\nexport function replaceUrl(url: string): void {\n const nav = getNavigationApi();\n if (nav) {\n nav.navigate(url, {\n history: 'replace',\n info: { shallow: true },\n });\n } else {\n history.replaceState(history.state, '', url);\n }\n}\n","/**\n * useSelectedLayoutSegment / useSelectedLayoutSegments — client-side hooks\n * for reading the active segment(s) below the current layout.\n *\n * These hooks are used by navigation UIs to highlight active sections.\n * They match Next.js's API from next/navigation.\n *\n * How they work:\n * 1. Each layout is wrapped with a SegmentProvider that records its depth\n * (the URL segments from root to that layout level).\n * 2. The hooks read the current URL pathname via usePathname().\n * 3. They compare the layout's segment depth against the full URL segments\n * to determine which child segments are \"selected\" below.\n *\n * Example: For URL \"/dashboard/settings/profile\"\n * - Root layout (depth 0, segments: ['']): selected segment = \"dashboard\"\n * - Dashboard layout (depth 1, segments: ['', 'dashboard']): selected = \"settings\"\n * - Settings layout (depth 2, segments: ['', 'dashboard', 'settings']): selected = \"profile\"\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { useSegmentContext } from './segment-context.js';\nimport { usePathname } from './use-pathname.js';\n\n/**\n * Split a pathname into URL segments.\n * \"/\" → [\"\"]\n * \"/dashboard\" → [\"\", \"dashboard\"]\n * \"/dashboard/settings\" → [\"\", \"dashboard\", \"settings\"]\n */\nexport function pathnameToSegments(pathname: string): string[] {\n return pathname.split('/');\n}\n\n/**\n * Pure function: compute the selected child segment given a layout's segment\n * depth and the current URL pathname.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns the active child segment one level below, or null if at the leaf\n */\nexport function getSelectedSegment(\n contextSegments: string[] | null,\n pathname: string\n): string | null {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments[1] || null;\n }\n\n const depth = contextSegments.length;\n return urlSegments[depth] || null;\n}\n\n/**\n * Pure function: compute all selected segments below a layout's depth.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns all active segments below the layout\n */\nexport function getSelectedSegments(contextSegments: string[] | null, pathname: string): string[] {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments.slice(1).filter(Boolean);\n }\n\n const depth = contextSegments.length;\n return urlSegments.slice(depth).filter(Boolean);\n}\n\n/**\n * Returns the active child segment one level below the layout where this\n * hook is called. Returns `null` if the layout is the leaf (no child segment).\n *\n * Compatible with Next.js's `useSelectedLayoutSegment()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegment(parallelRouteKey?: string): string | null {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegment(context?.segments ?? null, pathname);\n}\n\n/**\n * Returns all active segments below the layout where this hook is called.\n * Returns an empty array if the layout is the leaf (no child segments).\n *\n * Compatible with Next.js's `useSelectedLayoutSegments()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegments(parallelRouteKey?: string): string[] {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegments(context?.segments ?? null, pathname);\n}\n","/**\n * Client-side form utilities for server actions.\n *\n * Exports a typed `useActionState` that understands the action builder's result shape.\n * Result is typed to:\n * { data: T } | { validationErrors: Record<string, string[]> } | { serverError: { code, data? } } | null\n *\n * The action builder emits a function that satisfies both the direct call signature\n * and React's `(prevState, formData) => Promise<State>` contract.\n *\n * See design/08-forms-and-actions.md §\"Client-Side Form Mechanics\"\n */\n\nimport { useActionState as reactUseActionState, useTransition } from 'react';\nimport type { ActionFn, ActionResult, InputHint, ValidationErrors } from '../server/action-client';\nimport type { FormFlashData } from '../server/form-flash';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/**\n * The action function type accepted by useActionState.\n * Must satisfy React's (prevState, formData) => Promise<State> contract.\n */\nexport type UseActionStateFn<TData> = (\n prevState: ActionResult<TData> | null,\n formData: FormData\n) => Promise<ActionResult<TData>>;\n\n/**\n * Return type of useActionState.\n * [result, formAction, isPending, errors]\n * The 4th element is auto-derived from result via useFormErrors logic.\n */\nexport type UseActionStateReturn<TData> = [\n result: ActionResult<TData> | null,\n formAction: (formData: FormData) => void,\n isPending: boolean,\n errors: FormErrorsResult,\n];\n\n// ─── useActionState ──────────────────────────────────────────────────────\n\n/**\n * Typed wrapper around React 19's `useActionState` that understands\n * the timber action builder's result shape.\n *\n * @param action - A server action created with createActionClient or a raw 'use server' function.\n * @param initialState - Initial state, typically `null`. Pass `getFormFlash()` for no-JS\n * progressive enhancement — the flash seeds the initial state so the form has a\n * single source of truth for both with-JS and no-JS paths.\n * @param permalink - Optional permalink for progressive enhancement (no-JS fallback URL).\n *\n * @example\n * ```tsx\n * 'use client'\n * import { useActionState } from '@timber-js/app/client'\n * import { createTodo } from './actions'\n *\n * export function NewTodoForm({ flash }) {\n * const [result, action, isPending] = useActionState(createTodo, flash)\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {result?.validationErrors?.title && <p>{result.validationErrors.title}</p>}\n * <button disabled={isPending}>Add</button>\n * </form>\n * )\n * }\n * ```\n */\nexport function useActionState<TData>(\n action: UseActionStateFn<TData>,\n initialState: ActionResult<TData> | FormFlashData | null,\n permalink?: string\n): UseActionStateReturn<TData> {\n // FormFlashData is structurally compatible with ActionResult at runtime —\n // the cast satisfies React's generic inference which would otherwise widen TData.\n const [result, formAction, isPending] = reactUseActionState(\n action,\n initialState as ActionResult<TData> | null,\n permalink\n );\n const errors = deriveFormErrors(result);\n return [result, formAction, isPending, errors];\n}\n\n// ─── useFormAction ───────────────────────────────────────────────────────\n\n/**\n * Hook for calling a server action imperatively (not via a form).\n * Returns [execute, isPending] where execute accepts the input directly.\n *\n * @example\n * ```tsx\n * const [deleteTodo, isPending] = useFormAction(deleteTodoAction)\n * <button onClick={() => deleteTodo({ id: todo.id })} disabled={isPending}>\n * Delete\n * </button>\n * ```\n */\nexport function useFormAction<TData = unknown, TInput = unknown>(\n action: ActionFn<TData, TInput> | ((input: TInput) => Promise<ActionResult<TData>>)\n): [\n (\n ...args: undefined extends TInput ? [input?: InputHint<TInput>] : [input: InputHint<TInput>]\n ) => Promise<ActionResult<TData>>,\n boolean,\n] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: InputHint<TInput>): Promise<ActionResult<TData>> => {\n return new Promise((resolve) => {\n startTransition(async () => {\n const result = await (action as (input: InputHint<TInput>) => Promise<ActionResult<TData>>)(\n input as InputHint<TInput>\n );\n resolve(result);\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── Form error extraction ────────────────────────────────────────────────\n\n/** Return type of the errors element in useActionState. */\nexport interface FormErrorsResult {\n /** Per-field validation errors keyed by field name. */\n fieldErrors: Record<string, string[]>;\n /** Form-level errors (from `_root` key). */\n formErrors: string[];\n /** Server error if the action threw an ActionError. */\n serverError: { code: string; data?: Record<string, unknown> } | null;\n /** Whether any errors are present. */\n hasErrors: boolean;\n /** Get the first error message for a field, or null. */\n getFieldError: (field: string) => string | null;\n}\n\n/**\n * Derive FormErrorsResult from an action result.\n * Used internally by useActionState 4th tuple element.\n * @internal — exported for test access only.\n */\nexport function deriveFormErrors<TData>(\n result:\n | ActionResult<TData>\n | {\n validationErrors?: ValidationErrors;\n serverError?: { code: string; data?: Record<string, unknown> };\n }\n | null\n): FormErrorsResult {\n const empty: FormErrorsResult = {\n fieldErrors: {},\n formErrors: [],\n serverError: null,\n hasErrors: false,\n getFieldError: () => null,\n };\n\n if (!result) return empty;\n\n const validationErrors = result.validationErrors as ValidationErrors | undefined;\n const serverError = result.serverError as\n | { code: string; data?: Record<string, unknown> }\n | undefined;\n\n if (!validationErrors && !serverError) return empty;\n\n // Separate _root (form-level) errors from field errors\n const fieldErrors: Record<string, string[]> = {};\n const formErrors: string[] = [];\n\n if (validationErrors) {\n for (const [key, messages] of Object.entries(validationErrors)) {\n if (key === '_root') {\n formErrors.push(...messages);\n } else {\n fieldErrors[key] = messages;\n }\n }\n }\n\n const hasErrors =\n Object.keys(fieldErrors).length > 0 || formErrors.length > 0 || serverError != null;\n\n return {\n fieldErrors,\n formErrors,\n serverError: serverError ?? null,\n hasErrors,\n getFieldError(field: string): string | null {\n const errs = fieldErrors[field];\n return errs && errs.length > 0 ? errs[0] : null;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-link-status.ts","../../src/client/link.tsx","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/navigation-api.ts","../../src/client/shallow-url.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx"],"sourcesContent":["'use client';\n\n// useLinkStatus — returns { isPending: true } while the nearest parent <Link>'s\n// navigation is in flight. No arguments — scoped via React context.\n// See design/19-client-navigation.md §\"useLinkStatus()\"\n\nimport { useContext, createContext } from 'react';\n\nexport interface LinkStatus {\n isPending: boolean;\n}\n\n/**\n * React context provided by <Link>. Holds the pending status\n * for that specific link's navigation.\n */\nexport const LinkStatusContext = createContext<LinkStatus>({ isPending: false });\n\n/**\n * Returns `{ isPending: true }` while the nearest parent `<Link>` component's\n * navigation is in flight. Must be used inside a `<Link>` component's children.\n *\n * Unlike `usePendingNavigation()` which is global, this hook is scoped to\n * the nearest parent `<Link>` — only the link the user clicked shows pending.\n *\n * ```tsx\n * 'use client'\n * import { Link, useLinkStatus } from '@timber-js/app/client'\n *\n * function Hint() {\n * const { isPending } = useLinkStatus()\n * return <span className={isPending ? 'opacity-50' : ''} />\n * }\n *\n * export function NavLink({ href, children }) {\n * return (\n * <Link href={href}>\n * {children} <Hint />\n * </Link>\n * )\n * }\n * ```\n */\nexport function useLinkStatus(): LinkStatus {\n return useContext(LinkStatusContext);\n}\n","'use client';\n\n// Link component — client-side navigation with progressive enhancement\n// See design/19-client-navigation.md § Progressive Enhancement\n//\n// Without JavaScript, <Link> renders as a plain <a> tag — standard browser\n// navigation. With JavaScript, the Link component's onClick handler triggers\n// RSC-based client navigation via the router.\n//\n// Each Link owns its own click handler — no global event delegation.\n// This keeps navigation within React's component tree, ensuring pending\n// state (useLinkStatus) updates atomically with the navigation.\n//\n// Typed Link: design/09-typescript.md §\"Typed Link\"\n// - href validated against known routes (via codegen overloads, not runtime)\n// - params prop typed per-route, URL interpolated at runtime\n// - searchParams prop serialized via SearchParamsDefinition\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport {\n useTransition,\n type AnchorHTMLAttributes,\n type ReactNode,\n type MouseEvent as ReactMouseEvent,\n} from 'react';\nimport type { SearchParamsDefinition } from '../search-params/define.js';\nimport { getSearchParamsDefinition } from '../search-params/registry.js';\nimport type { LinkFunction } from './index.js';\nimport { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';\nimport {\n validateNavigationHref as validateLinkHref,\n isInternalHref,\n} from '../shared/href-validation.js';\nimport { LinkStatusContext } from './use-link-status.js';\nimport { getRouterOrNull } from './router-ref.js';\nimport { getSsrData } from './ssr-data.js';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.js';\nimport { getLinkCodec } from '../params/codec-registry.js';\nimport type { LinkStatus } from './use-link-status.js';\n\nconst LINK_PENDING: LinkStatus = { isPending: true };\nconst LINK_IDLE: LinkStatus = { isPending: false };\n\n// ─── Current Search Params ────────────────────────────────────────\n\n/**\n * Read the current URL's search string without requiring a React hook.\n * On the client, reads window.location.search. During SSR, reads from\n * the request context (getSsrData). Returns empty string if unavailable.\n */\nfunction getCurrentSearch(): string {\n if (typeof window !== 'undefined') return window.location.search;\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type OnNavigateEvent = {\n preventDefault: () => void;\n};\n\nexport type OnNavigateHandler = (e: OnNavigateEvent) => void;\n\n/**\n * Base props shared by all Link variants.\n *\n * Exported so the public `LinkFunction` interface (declared in\n * `./index.ts`, where module augmentation can merge into it) can\n * compose this without duplication.\n */\nexport interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** Prefetch the RSC payload on hover */\n prefetch?: boolean;\n /**\n * Scroll to top on navigation. Defaults to true.\n * Set to false for tabbed interfaces where content changes within a fixed layout.\n */\n scroll?: boolean;\n /**\n * Preserve search params from the current URL across navigation.\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 * Useful for route-group gating where a search param (e.g. `?private=access`)\n * must persist across internal navigations. The target href's own search params\n * always take precedence over preserved ones.\n *\n * During SSR, reads search params from the request context. On the client,\n * reads from the current URL and updates reactively when the URL changes.\n */\n preserveSearchParams?: true | string[];\n /**\n * Called before client-side navigation commits. Call `e.preventDefault()`\n * to cancel the default navigation — the caller is then responsible for\n * navigating (e.g. via `router.push()`).\n *\n * Only fires for client-side SPA navigations, not full page loads.\n * Has no effect during SSR.\n */\n onNavigate?: OnNavigateHandler;\n children?: ReactNode;\n}\n\n// ─── Typed Link Props ────────────────────────────────────────────\n\n/**\n * Widen server-side string params to string | number for Link convenience.\n * Exported for use by codegen-generated overloads.\n */\nexport type LinkSegmentParams<T> = {\n [K in keyof T]: [string] extends [T[K]] ? string | number : T[K];\n};\n\n// ─── External Href Types ─────────────────────────────────────────\n//\n// `ExternalHref` and the public `LinkFunction` interface live in\n// `./index.ts` rather than this file. They MUST be originally declared\n// in the same module that the codegen augments (`@timber-js/app/client`)\n// so that codegen-generated per-route call signatures merge with the\n// same interface that types the `Link` constant. Re-exporting an\n// interface via `export type {}` does NOT participate in module\n// augmentation merging — only originally-declared interfaces do.\n// See TIM-624.\n\n// ─── searchParams prop shapes ────────────────────────────────────\n//\n// Per-route Link overloads (emitted by codegen) pass the flat values shape:\n// searchParams={{ page: 2, q: 'boots' }}\n// The framework looks up the route's SearchParamsDefinition from the\n// search-params registry at runtime (see TIM-830).\n//\n// The catch-all overload in client/index.ts (external/computed hrefs)\n// additionally accepts the legacy wrapped shape:\n// searchParams={{ definition: def, values: { page: 2 } }}\n// because there is no way to look up a definition from a computed string.\n//\n// `resolveHref` discriminates at runtime by presence of a `definition` key.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ParamValue = string | number | string[] | { toString(): string; [key: string]: any };\n\ntype WrappedSearchParamsProp = {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n};\ntype FlatSearchParamsProp = Record<string, unknown>;\ntype LinkSearchParamsProp = WrappedSearchParamsProp | FlatSearchParamsProp;\n\n/**\n * Runtime-only loose props used internally by the Link implementation.\n * Not exposed to callers — the public API uses LinkFunction.\n */\ninterface LinkRuntimeProps extends LinkBaseProps {\n href: string;\n segmentParams?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n}\n\n// Legacy exports for backward compat (used by buildLinkProps, tests, etc.)\nexport type LinkPropsWithHref = LinkBaseProps & {\n href: string;\n segmentParams?: never;\n searchParams?: LinkSearchParamsProp;\n};\nexport type LinkPropsWithParams = LinkRuntimeProps & {\n segmentParams: Record<string, ParamValue>;\n};\nexport type LinkProps = LinkRuntimeProps;\n\nexport { validateLinkHref, isInternalHref };\n\n// ─── URL Interpolation ──────────────────────────────────────────\n\n/**\n * Interpolate dynamic segments in a route pattern with actual values.\n * e.g. interpolateParams(\"/products/[id]\", { id: \"123\" }) → \"/products/123\"\n *\n * Supports:\n * - [param] → single segment\n * - [...param] → catch-all (joined with /)\n * - [[...param]] → optional catch-all (omitted if undefined/empty)\n */\n/**\n * Parse a route pattern's path portion into classified segments.\n * Exported for testing. Uses the shared character-based classifier.\n */\nexport function parseSegments(pattern: string): UrlSegment[] {\n return pattern.split('/').filter(Boolean).map(classifyUrlSegment);\n}\n\n/**\n * Resolve a single classified segment into its string representation.\n * Returns null for optional catch-all with no value (filtered out before join).\n *\n * When schema codecs are registered (via virtual:timber-schema), uses\n * codec.serialize() for URL construction instead of plain String().\n */\nfunction resolveSegment(\n seg: UrlSegment,\n params: Record<string, ParamValue>,\n pattern: string\n): string | null {\n switch (seg.kind) {\n case 'static':\n return seg.value;\n\n case 'optional-catch-all': {\n const value = params[seg.name];\n if (value === undefined || (Array.isArray(value) && value.length === 0)) {\n return null;\n }\n const codec = getLinkCodec(`[[...${seg.name}]]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) return null;\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'catch-all': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(\n `<Link> missing required catch-all param \"${seg.name}\" for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[...${seg.name}]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" codec returned null for pattern \"${pattern}\".`\n );\n }\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n if (segments.length === 0) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" must have at least one segment for pattern \"${pattern}\".`\n );\n }\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'dynamic': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(`<Link> missing required param \"${seg.name}\" for pattern \"${pattern}\".`);\n }\n if (Array.isArray(value)) {\n throw new Error(\n `<Link> param \"${seg.name}\" expected a string but received an array for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[${seg.name}]`);\n const str = codec ? (codec.serialize(value) ?? String(value)) : String(value);\n const encoded = encodeURIComponent(str);\n const prefix = seg.prefix ?? '';\n const suffix = seg.suffix ?? '';\n return prefix + encoded + suffix;\n }\n }\n}\n\n/**\n * Split a URL pattern into the path portion and any trailing ?query/#hash suffix.\n * Uses URL parsing for correctness rather than manual index arithmetic.\n */\nfunction splitPatternSuffix(pattern: string): [path: string, suffix: string] {\n if (!pattern.includes('?') && !pattern.includes('#')) {\n return [pattern, ''];\n }\n const url = new URL(pattern, 'http://x');\n const suffix = url.search + url.hash;\n const path = pattern.slice(0, pattern.length - suffix.length);\n return [path, suffix];\n}\n\nexport function interpolateParams(pattern: string, params: Record<string, ParamValue>): string {\n const [pathPart, suffix] = splitPatternSuffix(pattern);\n\n const resolved = parseSegments(pathPart)\n .map((seg) => resolveSegment(seg, params, pattern))\n .filter((s): s is string => s !== null);\n return ('/' + resolved.join('/') || '/') + suffix;\n}\n\n// ─── Resolve Href ───────────────────────────────────────────────\n\n/**\n * Resolve the final href string from Link props.\n *\n * Handles:\n * - params interpolation into route patterns\n * - searchParams serialization via SearchParamsDefinition\n * - Validation that searchParams and inline query strings are exclusive\n */\n/**\n * Runtime discriminator: treat `searchParams` as the legacy wrapped shape\n * only when it literally has a `definition` key. Everything else is the\n * flat `Partial<T>` values shape (TIM-830).\n */\nfunction isWrappedSearchParamsProp(sp: LinkSearchParamsProp): sp is WrappedSearchParamsProp {\n return 'definition' in sp;\n}\n\nexport function resolveHref(\n href: string,\n params?: Record<string, ParamValue>,\n searchParams?: LinkSearchParamsProp\n): string {\n let resolvedPath = href;\n\n // Interpolate params if provided\n if (params) {\n resolvedPath = interpolateParams(href, params);\n }\n\n // Serialize searchParams if provided\n if (searchParams) {\n // Validate: searchParams prop and inline query string are mutually exclusive\n if (resolvedPath.includes('?')) {\n throw new Error(\n '<Link> received both a searchParams prop and a query string in href. ' +\n 'These are mutually exclusive — use one or the other.'\n );\n }\n\n let definition: SearchParamsDefinition<Record<string, unknown>> | undefined;\n let values: Record<string, unknown>;\n\n if (isWrappedSearchParamsProp(searchParams)) {\n // Legacy wrapped shape — used by the catch-all overload for\n // computed/external hrefs where no route lookup is possible.\n definition = searchParams.definition;\n values = searchParams.values;\n } else {\n // Flat shape (TIM-830): look up the definition from the runtime\n // registry using the un-interpolated href pattern (e.g. '/products/[id]').\n // The search-params registry is populated eagerly at startup by the\n // virtual:timber-search-params-registry module generated by the\n // timber-routing Vite plugin.\n definition = getSearchParamsDefinition(href) as\n | SearchParamsDefinition<Record<string, unknown>>\n | undefined;\n values = searchParams;\n }\n\n if (definition) {\n const qs = definition.serialize(values);\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n } else {\n // No registered definition — serialize flat object as plain query params.\n const usp = new URLSearchParams();\n for (const [key, val] of Object.entries(values)) {\n if (val === undefined || val === null) continue;\n if (Array.isArray(val)) {\n for (const item of val) usp.append(key, String(item));\n } else {\n usp.set(key, String(val));\n }\n }\n const qs = usp.toString();\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n }\n }\n\n return resolvedPath;\n}\n\n// ─── Build Props ─────────────────────────────────────────────────\n\ninterface LinkOutputProps {\n href: string;\n}\n\n/**\n * Build the HTML attributes for a Link. Separated from the component\n * for testability — the component just spreads these onto an <a>.\n */\nexport function buildLinkProps(\n props: Pick<LinkPropsWithHref, 'href'> & {\n params?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n }\n): LinkOutputProps {\n const resolvedHref = resolveHref(props.href, props.params, props.searchParams);\n validateLinkHref(resolvedHref);\n return { href: resolvedHref };\n}\n\n// ─── Click Handler ───────────────────────────────────────────────\n\n/**\n * Should this click be intercepted for SPA navigation?\n *\n * Returns false (pass through to browser) when:\n * - Modified keys are held (Ctrl, Meta, Shift, Alt) — open in new tab\n * - The click is not the primary button\n * - The event was already prevented by a parent handler\n * - The link has target=\"_blank\" or similar\n * - The link has a download attribute\n * - The href is external\n */\nfunction shouldInterceptClick(\n event: ReactMouseEvent<HTMLAnchorElement>,\n resolvedHref: string\n): boolean {\n if (event.button !== 0) return false;\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;\n if (event.defaultPrevented) return false;\n\n const anchor = event.currentTarget;\n if (anchor.target && anchor.target !== '_self') return false;\n if (anchor.hasAttribute('download')) return false;\n\n if (!isInternalHref(resolvedHref)) return false;\n\n return true;\n}\n\n// ─── Link Component ──────────────────────────────────────────────\n\n/**\n * Navigation link with progressive enhancement.\n *\n * Renders as a plain `<a>` tag — works without JavaScript. When the client\n * runtime is active, the Link's onClick handler triggers RSC-based client\n * navigation via the router. No global event delegation — each Link owns\n * its own click handling.\n *\n * Supports typed routes via the Routes interface (populated by codegen).\n * At runtime:\n * - `segmentParams` prop interpolates dynamic segments in the href pattern\n * - `searchParams` prop serializes query parameters via a SearchParamsDefinition\n *\n * Typed via the LinkFunction callable interface. The base call signature\n * forbids segmentParams; per-route signatures are added by codegen via\n * interface merging. See TIM-624.\n */\n// Cast to LinkFunction — the callable interface provides the public type,\n// but the implementation destructures LinkRuntimeProps internally.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const Link: LinkFunction = function LinkImpl(props: any) {\n const {\n href,\n prefetch,\n scroll,\n segmentParams,\n searchParams,\n preserveSearchParams,\n onNavigate,\n onClick: userOnClick,\n onMouseEnter: userOnMouseEnter,\n children,\n ...rest\n } = props as LinkRuntimeProps;\n const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });\n\n // ─── Per-link pending state ─────────────────────────────────────────\n // Each Link owns a useTransition. On click, it wraps\n // router.navigate() in startTransition — only this Link re-renders\n // for isPending, zero siblings touched. The transition tracks the\n // full navigation lifecycle (fetch + render + commit).\n const [isPending, startTransition] = useTransition();\n const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;\n\n // Preserve search params from the current URL when requested.\n // useSearchParams() works during both SSR (reads from request context)\n // and on the client (reads from window.location, reactive to URL changes).\n // We read current search params directly to avoid unconditional hook calls.\n // On the client, window.location.search is always current; during SSR,\n // getSsrData() provides the request's search params.\n const internal = isInternalHref(baseHref);\n\n // Only preserve search params for internal links — leaking current\n // page params (tokens, UTM, etc.) to external domains is a data leak.\n const resolvedHref =\n preserveSearchParams && internal\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : baseHref;\n\n // ─── Click handler ───────────────────────────────────────────\n // Each Link component owns its click handling. The router is\n // accessed via the singleton ref — during SSR, getRouterOrNull()\n // returns null and onClick is a no-op (the <a> works as a plain link).\n const handleClick = internal\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n // Call user's onClick first (e.g., analytics)\n userOnClick?.(event);\n\n if (!shouldInterceptClick(event, resolvedHref)) return;\n\n // Call onNavigate if provided — allows caller to cancel\n if (onNavigate) {\n let prevented = false;\n onNavigate({\n preventDefault: () => {\n prevented = true;\n },\n });\n if (prevented) {\n event.preventDefault();\n return;\n }\n }\n\n const router = getRouterOrNull();\n if (!router) return;\n\n const navHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref;\n\n const resolved = new URL(navHref, window.location.href);\n\n if (\n resolved.pathname === window.location.pathname &&\n resolved.search === window.location.search &&\n resolved.hash\n ) {\n return;\n }\n\n event.preventDefault();\n\n const shouldScroll = scroll !== false;\n // Keep the #fragment — the router commits it to the address bar and\n // scrolls to the matching element after render. The hash is stripped\n // from the RSC fetch URL inside the router (TIM-1035).\n const absoluteHref = resolved.pathname + resolved.search + resolved.hash;\n\n startTransition(async () => {\n await router.navigate(absoluteHref, { scroll: shouldScroll });\n });\n }\n : userOnClick; // External links — just pass through user's onClick\n\n // ─── Hover prefetch ──────────────────────────────────────────\n const handleMouseEnter =\n internal && prefetch\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n userOnMouseEnter?.(event);\n const router = getRouterOrNull();\n if (router) {\n const prefetchHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref;\n const resolved = new URL(prefetchHref, window.location.href);\n router.prefetch(resolved.pathname + resolved.search);\n }\n }\n : userOnMouseEnter;\n\n return (\n <a {...rest} href={resolvedHref} onClick={handleClick} onMouseEnter={handleMouseEnter}>\n <LinkStatusContext.Provider value={linkStatus}>{children}</LinkStatusContext.Provider>\n </a>\n );\n};\n","/**\n * useRouter() — client-side hook for programmatic navigation.\n *\n * Returns a router instance with push, replace, refresh, back, forward,\n * and prefetch methods. Compatible with Next.js's `useRouter()` from\n * `next/navigation` (App Router).\n *\n * This wraps timber's internal RouterInstance in the Next.js-compatible\n * AppRouterInstance shape that ecosystem libraries expect.\n *\n * NOTE: Unlike Next.js, these methods do NOT wrap navigation in\n * startTransition. In Next.js, router state is React state (useReducer)\n * so startTransition defers the update and provides isPending tracking.\n * In timber, navigation calls reactRoot.render() which is a root-level\n * render — startTransition has no effect on root renders.\n *\n * Navigation state (params, pathname) is delivered atomically via\n * NavigationContext embedded in the element tree passed to\n * reactRoot.render(). See design/19-client-navigation.md §\"NavigationContext\".\n *\n * For loading UI during navigation, use:\n * - useLinkStatus() — per-link pending indicator (inside <Link>)\n * - usePendingNavigation() — global navigation pending state\n */\n\nimport { getRouterOrNull } from './router-ref.js';\nimport { validateNavigationHref } from '../shared/href-validation.js';\n\nexport interface AppRouterInstance {\n /** Navigate to a URL, pushing a new history entry */\n push(href: string, options?: { scroll?: boolean }): void;\n /** Navigate to a URL, replacing the current history entry */\n replace(href: string, options?: { scroll?: boolean }): void;\n /** Refresh the current page (re-fetch RSC payload) */\n refresh(): void;\n /** Navigate back in history */\n back(): void;\n /** Navigate forward in history */\n forward(): void;\n /** Prefetch an RSC payload for a URL */\n prefetch(href: string): void;\n}\n\n/**\n * Get a router instance for programmatic navigation.\n *\n * Compatible with Next.js's `useRouter()` from `next/navigation`.\n *\n * Methods lazily resolve the global router when invoked (during user\n * interaction) rather than capturing it at render time. This is critical\n * because during hydration, React synchronously executes component render\n * functions *before* the router is bootstrapped in browser-entry.ts.\n * If we eagerly captured the router during render, components would get\n * a null reference and be stuck with silent no-ops forever.\n *\n * Returns safe no-ops during SSR or before bootstrap. The `typeof window`\n * check is insufficient because Vite's client SSR environment defines\n * `window`, so we use a try/catch on getRouter() — but only at method\n * invocation time, not at render time.\n */\nexport function useRouter(): AppRouterInstance {\n return {\n push(href: string, options?: { scroll?: boolean }) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error(\n '[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.'\n );\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll });\n },\n replace(href: string, options?: { scroll?: boolean }) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().replace() called but router is not initialized.');\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll, replace: true });\n },\n refresh() {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().refresh() called but router is not initialized.');\n }\n return;\n }\n void router.refresh();\n },\n back() {\n if (typeof window !== 'undefined') window.history.back();\n },\n forward() {\n if (typeof window !== 'undefined') window.history.forward();\n },\n prefetch(href: string) {\n const router = getRouterOrNull();\n if (!router) return; // Silent — prefetch failure is non-fatal\n router.prefetch(href);\n },\n };\n}\n","/**\n * usePathname() — client-side hook for reading the current pathname.\n *\n * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').\n * Updates when client-side navigation changes the URL.\n *\n * On the client, reads from NavigationContext which is updated atomically\n * with the RSC tree render. This replaces the previous useSyncExternalStore\n * approach which only subscribed to popstate events — meaning usePathname()\n * did NOT re-render on forward navigation (pushState). The context approach\n * fixes this: pathname updates in the same render pass as the new tree.\n *\n * During SSR, reads the request pathname from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { getSsrData } from './ssr-data.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n/**\n * Read the current URL pathname.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). During SSR, reads from the\n * ALS-backed SSR data context. Falls back to window.location.pathname\n * when called outside a React component (e.g., in tests).\n */\nexport function usePathname(): string {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.pathname;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to SSR/fallback below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n const ssrData = getSsrData();\n if (ssrData) return ssrData.pathname ?? '/';\n\n // Final fallback: window.location (tests, edge cases).\n if (typeof window !== 'undefined') return window.location.pathname;\n return '/';\n}\n","/**\n * Navigation API integration — progressive enhancement for client navigation.\n *\n * When the Navigation API (`window.navigation`) is available, this module\n * provides an intercept-based navigation model that replaces the separate\n * popstate + click handler approach with a single navigate event listener.\n *\n * Key benefits:\n * - Intercepts ALL navigations (link clicks, form submissions, back/forward)\n * - Built-in AbortSignal per navigation (auto-aborts in-flight fetches)\n * - Per-entry state via NavigationHistoryEntry.getState()\n * - navigation.transition for progress tracking\n *\n * When unavailable, all functions are no-ops and the History API fallback\n * in browser-entry.ts handles navigation.\n *\n * See design/19-client-navigation.md\n */\n\nimport { isHardNavigating } from './navigation-root.js';\n\n// ─── Feature Detection ───────────────────────────────────────────\n\n/**\n * Returns true if the Navigation API is available in the current environment.\n * Feature-detected at runtime — no polyfill.\n */\nexport function hasNavigationApi(): boolean {\n return typeof window !== 'undefined' && 'navigation' in window && window.navigation != null;\n}\n\n/**\n * Get the Navigation API instance. Returns null if unavailable.\n */\nexport function getNavigationApi(): Navigation | null {\n if (!hasNavigationApi()) return null;\n return window.navigation;\n}\n\n// ─── Navigation API Controller ───────────────────────────────────\n\n/**\n * Callbacks for the Navigation API event handler.\n *\n * When the Navigation API intercepts a navigation, it delegates to these\n * callbacks which run the RSC fetch + render pipeline.\n */\nexport interface NavigationApiCallbacks {\n /**\n * Handle a push/replace navigation intercepted by the Navigation API.\n * This covers both Link <a> clicks (user-initiated) and external\n * navigations (plain <a> tags, programmatic).\n * The Navigation API handles the URL update via event.intercept().\n */\n onExternalNavigate: (\n url: string,\n options: { replace: boolean; signal: AbortSignal; scroll?: boolean }\n ) => Promise<void>;\n\n /**\n * Handle a traversal (back/forward button). The Navigation API intercepts\n * the traversal and delegates to us for RSC replay/fetch.\n */\n onTraverse: (url: string, scrollY: number, signal: AbortSignal) => Promise<void>;\n\n /**\n * Called when a shallow URL update is intercepted (e.g., nuqs with\n * shallow: true, or replaceUrl). The URL has already been committed —\n * this callback syncs NavigationContext.search so useSearchParams()\n * reflects the new value without a full router navigation.\n */\n onShallowNavigate?: (url: string) => void;\n}\n\n/**\n * Controller returned by setupNavigationApi. Provides methods to\n * coordinate between the router and the navigate event listener.\n */\nexport interface NavigationApiController {\n /**\n * Set the router-navigating flag. When `true`, the next navigate event\n * (from pushState/replaceState) is recognized as router-initiated. The\n * handler still intercepts it — but ties the browser's native loading\n * state to a deferred promise instead of running the RSC pipeline again.\n *\n * This means `navigation.transition` is active for the full duration of\n * every router-initiated navigation, giving the browser a native loading\n * indicator (tab spinner, address bar) aligned with the TopLoader.\n *\n * Must be called synchronously around pushState/replaceState:\n * controller.setRouterNavigating(true);\n * history.pushState(...); // navigate event fires, intercepted\n * controller.setRouterNavigating(false); // flag off, deferred stays open\n */\n setRouterNavigating: (value: boolean) => void;\n\n /**\n * Resolve the deferred promise created by setRouterNavigating(true),\n * clearing the browser's native loading state. Call this when the\n * navigation fully completes — aligned with when the TopLoader's\n * pendingUrl clears (same finally block in router.navigate).\n */\n completeRouterNavigation: () => void;\n\n /**\n * Initiate a navigation via the Navigation API (`navigation.navigate()`).\n * Unlike `history.pushState()`, this fires the navigate event BEFORE\n * committing the URL — allowing Chrome to show its native loading\n * indicator while the intercept handler runs.\n *\n * Must be called with setRouterNavigating(true) active so the handler\n * recognizes it as router-initiated and uses the deferred promise.\n */\n navigate: (url: string, replace: boolean) => void;\n\n /**\n * Save scroll position into the current navigation entry's state.\n * Uses navigation.updateCurrentEntry() for per-entry scroll storage.\n */\n saveScrollPosition: (scrollY: number) => void;\n\n /**\n * Check if the Navigation API has an active transition.\n * Returns the transition object if available, null otherwise.\n */\n hasActiveTransition: () => boolean;\n\n /** Remove the navigate event listener. */\n cleanup: () => void;\n}\n\n/**\n * Set up the Navigation API navigate event listener.\n *\n * Intercepts same-origin navigations and delegates to the provided callbacks.\n * Router-initiated navigations (pushState from router.navigate) are detected\n * via a synchronous flag and NOT intercepted — the router already handles them.\n *\n * Returns a controller for coordinating with the router.\n */\nexport function setupNavigationApi(callbacks: NavigationApiCallbacks): NavigationApiController {\n const nav = getNavigationApi()!;\n\n let routerNavigating = false;\n\n // Deferred promise for router-initiated navigations. Created when\n // setRouterNavigating(true) is called, resolved by completeRouterNavigation().\n // The navigate event handler intercepts with this promise so the browser's\n // native loading state (tab spinner) stays active until the navigation\n // completes — aligned with TopLoader's pendingUrl lifecycle.\n let routerNavDeferred: { promise: Promise<void>; resolve: () => void } | null = null;\n\n function handleNavigate(event: NavigateEvent): void {\n // Skip non-interceptable navigations (cross-origin, etc.)\n if (!event.canIntercept) return;\n\n // Hard navigation guard: when the router has triggered a full page\n // load (500 error, version skew), skip interception entirely so the\n // browser performs the MPA navigation. Without this guard, setting\n // window.location.href fires a navigate event that we'd intercept,\n // running the RSC pipeline again → 500 → window.location.href →\n // navigate event → infinite loop.\n // See design/19-client-navigation.md §\"Hard Navigation Guard\"\n if (isHardNavigating()) return;\n\n // Skip download requests\n if (event.downloadRequest) return;\n\n // Skip blob: URLs — these are almost always downloads or object-URL\n // navigations initiated by the host page (e.g., generated files, PDFs).\n // The RSC pipeline cannot handle them, and intercepting would break\n // the download/open behavior the host page expects.\n if (event.destination.url.startsWith('blob:')) return;\n\n // Skip hash-only changes — let the browser handle scroll-to-anchor\n if (event.hashChange) return;\n\n // Shallow URL updates (e.g., nuqs search param changes). The navigation\n // only changes the URL — no server round trip needed. Intercept with a\n // no-op handler so the Navigation API commits the URL change without\n // triggering a full page navigation (which is the default if we don't\n // intercept). The info property is the Navigation API's built-in\n // per-navigation metadata — no side-channel flags needed.\n const info = event.info as { shallow?: boolean } | null | undefined;\n if (info?.shallow) {\n event.intercept({\n handler: () => Promise.resolve(),\n focusReset: 'manual',\n scroll: 'manual',\n });\n callbacks.onShallowNavigate?.(event.destination.url);\n return;\n }\n\n // Skip form submissions with a body (POST/PUT/etc.). These need the\n // browser's native form handling to send the request body to the server.\n // Intercepting would convert them into GET RSC navigations, dropping\n // the form data. Server actions use fetch() directly (not form navigation),\n // so they are unaffected by this check.\n if (event.formData) return;\n\n // Skip cross-origin (defense-in-depth — canIntercept covers this)\n const destUrl = new URL(event.destination.url);\n if (destUrl.origin !== location.origin) return;\n\n // Router-initiated navigation (Link click → router.navigate → pushState).\n // The router is already running the RSC pipeline — don't run it again.\n // Instead, intercept with the deferred promise so the browser's native\n // loading state tracks the navigation's full lifecycle. This aligns the\n // tab spinner / address bar indicator with the TopLoader.\n if (routerNavigating && routerNavDeferred) {\n event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n handler: () => routerNavDeferred!.promise,\n });\n return;\n }\n\n // Skip reload navigations — let the browser handle full page reload\n if (event.navigationType === 'reload') return;\n\n const url = destUrl.pathname + destUrl.search;\n\n if (event.navigationType === 'traverse') {\n // Back/forward button — intercept and delegate to router.\n // Read scroll position from the destination entry's state.\n const entryState = event.destination.getState() as\n | { scrollY?: number; timber?: boolean }\n | null\n | undefined;\n const scrollY = entryState && typeof entryState.scrollY === 'number' ? entryState.scrollY : 0;\n\n event.intercept({\n // Manual scroll — we handle scroll restoration ourselves\n // via afterPaint (same as the History API path).\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onTraverse(url, scrollY, event.signal);\n },\n });\n } else if (event.navigationType === 'push' || event.navigationType === 'replace') {\n // Push/replace — a Link <a> click or an external navigation\n // (plain <a> tag, programmatic).\n\n // Save the departing page's scroll position BEFORE event.intercept()\n // commits the URL change. Once intercept() is called, currentEntry\n // switches to the new (destination) entry — any updateCurrentEntry()\n // call after that would save to the wrong entry.\n // See: router.navigate() also calls saveNavigationEntryScroll(), but\n // for Navigation API <a> click navigations (where Link does NOT call\n // router.navigate directly), the router's save runs inside the\n // intercept handler — too late, currentEntry has already switched.\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY: window.scrollY },\n });\n } catch {\n // Ignore — entry may be disposed\n }\n\n event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n // Keep the #fragment — router.navigate splits it back off for the\n // RSC fetch and scrolls to the matching element after render\n // (TIM-1035). Traversals above stay hash-less: history-stack keys\n // use pathname + search.\n await callbacks.onExternalNavigate(url + destUrl.hash, {\n replace: event.navigationType === 'replace',\n signal: event.signal,\n scroll: undefined,\n });\n },\n });\n }\n }\n\n nav.addEventListener('navigate', handleNavigate);\n\n return {\n setRouterNavigating(value: boolean): void {\n routerNavigating = value;\n if (value) {\n // Create a new deferred promise. The navigate event handler will\n // intercept and tie the browser's loading state to this promise.\n let resolve!: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n routerNavDeferred = { promise, resolve };\n } else {\n // Flag off — but DON'T resolve the deferred here. The navigation\n // is still in flight (RSC fetch + render). completeRouterNavigation()\n // resolves it when the navigation fully completes.\n routerNavigating = false;\n }\n },\n\n completeRouterNavigation(): void {\n if (routerNavDeferred) {\n routerNavDeferred.resolve();\n routerNavDeferred = null;\n }\n },\n\n navigate(url: string, replace: boolean): void {\n // Use navigation.navigate() instead of history.pushState().\n // This fires the navigate event BEFORE committing the URL,\n // which lets Chrome show its native loading indicator while\n // the intercept handler (deferred promise) is pending.\n // history.pushState() commits the URL synchronously, so Chrome\n // sees the navigation as already complete and skips the indicator.\n nav.navigate(url, {\n history: replace ? 'replace' : 'push',\n });\n },\n\n saveScrollPosition(scrollY: number): void {\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY },\n });\n } catch {\n // Ignore errors — updateCurrentEntry may throw if entry is disposed\n }\n },\n\n hasActiveTransition(): boolean {\n return nav.transition != null;\n },\n\n cleanup(): void {\n nav.removeEventListener('navigate', handleNavigate);\n },\n };\n}\n","/**\n * Shallow URL replacement — update the browser URL bar without triggering\n * RSC navigation, TopLoader, or any server round-trip.\n *\n * Uses the Navigation API's `info: { shallow: true }` when available (Chrome),\n * which the navigate event handler intercepts with a no-op handler. Falls back\n * to raw `history.replaceState` (Safari/Firefox — no navigate event fired).\n */\n\nimport { getNavigationApi } from './navigation-api.js';\n\nexport function replaceUrl(url: string): void {\n const nav = getNavigationApi();\n if (nav) {\n nav.navigate(url, {\n history: 'replace',\n info: { shallow: true },\n });\n } else {\n history.replaceState(history.state, '', url);\n }\n}\n","/**\n * useSelectedLayoutSegment / useSelectedLayoutSegments — client-side hooks\n * for reading the active segment(s) below the current layout.\n *\n * These hooks are used by navigation UIs to highlight active sections.\n * They match Next.js's API from next/navigation.\n *\n * How they work:\n * 1. Each layout is wrapped with a SegmentProvider that records its depth\n * (the URL segments from root to that layout level).\n * 2. The hooks read the current URL pathname via usePathname().\n * 3. They compare the layout's segment depth against the full URL segments\n * to determine which child segments are \"selected\" below.\n *\n * Example: For URL \"/dashboard/settings/profile\"\n * - Root layout (depth 0, segments: ['']): selected segment = \"dashboard\"\n * - Dashboard layout (depth 1, segments: ['', 'dashboard']): selected = \"settings\"\n * - Settings layout (depth 2, segments: ['', 'dashboard', 'settings']): selected = \"profile\"\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { useSegmentContext } from './segment-context.js';\nimport { usePathname } from './use-pathname.js';\n\n/**\n * Split a pathname into URL segments.\n * \"/\" → [\"\"]\n * \"/dashboard\" → [\"\", \"dashboard\"]\n * \"/dashboard/settings\" → [\"\", \"dashboard\", \"settings\"]\n */\nexport function pathnameToSegments(pathname: string): string[] {\n return pathname.split('/');\n}\n\n/**\n * Pure function: compute the selected child segment given a layout's segment\n * depth and the current URL pathname.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns the active child segment one level below, or null if at the leaf\n */\nexport function getSelectedSegment(\n contextSegments: string[] | null,\n pathname: string\n): string | null {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments[1] || null;\n }\n\n const depth = contextSegments.length;\n return urlSegments[depth] || null;\n}\n\n/**\n * Pure function: compute all selected segments below a layout's depth.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns all active segments below the layout\n */\nexport function getSelectedSegments(contextSegments: string[] | null, pathname: string): string[] {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments.slice(1).filter(Boolean);\n }\n\n const depth = contextSegments.length;\n return urlSegments.slice(depth).filter(Boolean);\n}\n\n/**\n * Returns the active child segment one level below the layout where this\n * hook is called. Returns `null` if the layout is the leaf (no child segment).\n *\n * Compatible with Next.js's `useSelectedLayoutSegment()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegment(parallelRouteKey?: string): string | null {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegment(context?.segments ?? null, pathname);\n}\n\n/**\n * Returns all active segments below the layout where this hook is called.\n * Returns an empty array if the layout is the leaf (no child segments).\n *\n * Compatible with Next.js's `useSelectedLayoutSegments()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegments(parallelRouteKey?: string): string[] {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegments(context?.segments ?? null, pathname);\n}\n","/**\n * Client-side form utilities for server actions.\n *\n * Exports a typed `useActionState` that understands the action builder's result shape.\n * Result is typed to:\n * { data: T } | { validationErrors: Record<string, string[]> } | { serverError: { code, data? } } | null\n *\n * The action builder emits a function that satisfies both the direct call signature\n * and React's `(prevState, formData) => Promise<State>` contract.\n *\n * See design/08-forms-and-actions.md §\"Client-Side Form Mechanics\"\n */\n\nimport { useActionState as reactUseActionState, useTransition } from 'react';\nimport type { ActionFn, ActionResult, InputHint, ValidationErrors } from '../server/action-client';\nimport type { FormFlashData } from '../server/form-flash';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/**\n * The action function type accepted by useActionState.\n * Must satisfy React's (prevState, formData) => Promise<State> contract.\n */\nexport type UseActionStateFn<TData> = (\n prevState: ActionResult<TData> | null,\n formData: FormData\n) => Promise<ActionResult<TData>>;\n\n/**\n * Return type of useActionState.\n * [result, formAction, isPending, errors]\n * The 4th element is auto-derived from result via useFormErrors logic.\n */\nexport type UseActionStateReturn<TData> = [\n result: ActionResult<TData> | null,\n formAction: (formData: FormData) => void,\n isPending: boolean,\n errors: FormErrorsResult,\n];\n\n// ─── useActionState ──────────────────────────────────────────────────────\n\n/**\n * Typed wrapper around React 19's `useActionState` that understands\n * the timber action builder's result shape.\n *\n * @param action - A server action created with createActionClient or a raw 'use server' function.\n * @param initialState - Initial state, typically `null`. Pass `getFormFlash()` for no-JS\n * progressive enhancement — the flash seeds the initial state so the form has a\n * single source of truth for both with-JS and no-JS paths.\n * @param permalink - Optional permalink for progressive enhancement (no-JS fallback URL).\n *\n * @example\n * ```tsx\n * 'use client'\n * import { useActionState } from '@timber-js/app/client'\n * import { createTodo } from './actions'\n *\n * export function NewTodoForm({ flash }) {\n * const [result, action, isPending] = useActionState(createTodo, flash)\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {result?.validationErrors?.title && <p>{result.validationErrors.title}</p>}\n * <button disabled={isPending}>Add</button>\n * </form>\n * )\n * }\n * ```\n */\nexport function useActionState<TData>(\n action: UseActionStateFn<TData>,\n initialState: ActionResult<TData> | FormFlashData | null,\n permalink?: string\n): UseActionStateReturn<TData> {\n // FormFlashData is structurally compatible with ActionResult at runtime —\n // the cast satisfies React's generic inference which would otherwise widen TData.\n const [result, formAction, isPending] = reactUseActionState(\n action,\n initialState as ActionResult<TData> | null,\n permalink\n );\n const errors = deriveFormErrors(result);\n return [result, formAction, isPending, errors];\n}\n\n// ─── useFormAction ───────────────────────────────────────────────────────\n\n/**\n * Hook for calling a server action imperatively (not via a form).\n * Returns [execute, isPending] where execute accepts the input directly.\n *\n * @example\n * ```tsx\n * const [deleteTodo, isPending] = useFormAction(deleteTodoAction)\n * <button onClick={() => deleteTodo({ id: todo.id })} disabled={isPending}>\n * Delete\n * </button>\n * ```\n */\nexport function useFormAction<TData = unknown, TInput = unknown>(\n action: ActionFn<TData, TInput> | ((input: TInput) => Promise<ActionResult<TData>>)\n): [\n (\n ...args: undefined extends TInput ? [input?: InputHint<TInput>] : [input: InputHint<TInput>]\n ) => Promise<ActionResult<TData>>,\n boolean,\n] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: InputHint<TInput>): Promise<ActionResult<TData>> => {\n return new Promise((resolve) => {\n startTransition(async () => {\n const result = await (action as (input: InputHint<TInput>) => Promise<ActionResult<TData>>)(\n input as InputHint<TInput>\n );\n resolve(result);\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── Form error extraction ────────────────────────────────────────────────\n\n/** Return type of the errors element in useActionState. */\nexport interface FormErrorsResult {\n /** Per-field validation errors keyed by field name. */\n fieldErrors: Record<string, string[]>;\n /** Form-level errors (from `_root` key). */\n formErrors: string[];\n /** Server error if the action threw an ActionError. */\n serverError: { code: string; data?: Record<string, unknown> } | null;\n /** Whether any errors are present. */\n hasErrors: boolean;\n /** Get the first error message for a field, or null. */\n getFieldError: (field: string) => string | null;\n}\n\n/**\n * Derive FormErrorsResult from an action result.\n * Used internally by useActionState 4th tuple element.\n * @internal — exported for test access only.\n */\nexport function deriveFormErrors<TData>(\n result:\n | ActionResult<TData>\n | {\n validationErrors?: ValidationErrors;\n serverError?: { code: string; data?: Record<string, unknown> };\n }\n | null\n): FormErrorsResult {\n const empty: FormErrorsResult = {\n fieldErrors: {},\n formErrors: [],\n serverError: null,\n hasErrors: false,\n getFieldError: () => null,\n };\n\n if (!result) return empty;\n\n const validationErrors = result.validationErrors as ValidationErrors | undefined;\n const serverError = result.serverError as\n | { code: string; data?: Record<string, unknown> }\n | undefined;\n\n if (!validationErrors && !serverError) return empty;\n\n // Separate _root (form-level) errors from field errors\n const fieldErrors: Record<string, string[]> = {};\n const formErrors: string[] = [];\n\n if (validationErrors) {\n for (const [key, messages] of Object.entries(validationErrors)) {\n if (key === '_root') {\n formErrors.push(...messages);\n } else {\n fieldErrors[key] = messages;\n }\n }\n }\n\n const hasErrors =\n Object.keys(fieldErrors).length > 0 || formErrors.length > 0 || serverError != null;\n\n return {\n fieldErrors,\n formErrors,\n serverError: serverError ?? null,\n hasErrors,\n getFieldError(field: string): string | null {\n const errs = fieldErrors[field];\n return errs && errs.length > 0 ? errs[0] : null;\n },\n };\n}\n"],"mappings":"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"}