@timber-js/app 0.2.0-alpha.146 → 0.2.0-alpha.148

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 (297) hide show
  1. package/dist/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
  2. package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
  3. package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
  4. package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
  5. package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
  6. package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
  7. package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-DJDdyEae.js} +157 -313
  8. package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
  9. package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
  10. package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
  11. package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
  12. package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
  13. package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
  14. package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
  15. package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
  16. package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
  17. package/dist/_chunks/logger-CM0clugc.js.map +1 -0
  18. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
  19. package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
  20. package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
  21. package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
  22. package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
  23. package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
  24. package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
  25. package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
  26. package/dist/_chunks/tree-match-D2l830j2.js +102 -0
  27. package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
  28. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
  29. package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-DvWDj453.js} +154 -88
  30. package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
  31. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  32. package/dist/adapters/cloudflare-kv-cache.js +36 -14
  33. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  34. package/dist/adapters/compress-module.d.ts +5 -4
  35. package/dist/adapters/compress-module.d.ts.map +1 -1
  36. package/dist/adapters/nitro.d.ts +1 -3
  37. package/dist/adapters/nitro.d.ts.map +1 -1
  38. package/dist/adapters/nitro.js +14 -134
  39. package/dist/adapters/nitro.js.map +1 -1
  40. package/dist/cache/cache-api.d.ts.map +1 -1
  41. package/dist/cache/index.d.ts +3 -1
  42. package/dist/cache/index.d.ts.map +1 -1
  43. package/dist/cache/index.js +20 -235
  44. package/dist/cache/index.js.map +1 -1
  45. package/dist/cache/invalidation-epoch.d.ts +43 -0
  46. package/dist/cache/invalidation-epoch.d.ts.map +1 -0
  47. package/dist/cache/redis-handler.d.ts +13 -0
  48. package/dist/cache/redis-handler.d.ts.map +1 -1
  49. package/dist/cache/timber-cache.d.ts +17 -9
  50. package/dist/cache/timber-cache.d.ts.map +1 -1
  51. package/dist/cli-schema-sync.d.ts +21 -1
  52. package/dist/cli-schema-sync.d.ts.map +1 -1
  53. package/dist/cli.d.ts +12 -0
  54. package/dist/cli.d.ts.map +1 -1
  55. package/dist/cli.js +26 -31
  56. package/dist/cli.js.map +1 -1
  57. package/dist/client/browser-entry/hmr.d.ts.map +1 -1
  58. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  59. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  60. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  61. package/dist/client/error-boundary.d.ts.map +1 -1
  62. package/dist/client/error-boundary.js +13 -4
  63. package/dist/client/error-boundary.js.map +1 -1
  64. package/dist/client/error-reconstituter.d.ts +8 -1
  65. package/dist/client/error-reconstituter.d.ts.map +1 -1
  66. package/dist/client/history.d.ts +10 -17
  67. package/dist/client/history.d.ts.map +1 -1
  68. package/dist/client/index.js +1 -1
  69. package/dist/client/index.js.map +1 -1
  70. package/dist/client/internal.js +30 -33
  71. package/dist/client/internal.js.map +1 -1
  72. package/dist/client/navigation-api.d.ts +9 -2
  73. package/dist/client/navigation-api.d.ts.map +1 -1
  74. package/dist/client/navigation-context.d.ts +0 -11
  75. package/dist/client/navigation-context.d.ts.map +1 -1
  76. package/dist/client/navigation-root.d.ts +0 -5
  77. package/dist/client/navigation-root.d.ts.map +1 -1
  78. package/dist/client/router.d.ts.map +1 -1
  79. package/dist/client/rsc-fetch.d.ts.map +1 -1
  80. package/dist/client/use-search-params.d.ts +10 -5
  81. package/dist/client/use-search-params.d.ts.map +1 -1
  82. package/dist/codec.js +1 -1
  83. package/dist/config-validation.d.ts.map +1 -1
  84. package/dist/cookies/define-cookie.d.ts.map +1 -1
  85. package/dist/cookies/index.js +2 -4
  86. package/dist/cookies/index.js.map +1 -1
  87. package/dist/dev-tools/dev-page-shell.d.ts +1 -0
  88. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
  89. package/dist/dev-tools/hydration-overlay.d.ts +11 -0
  90. package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
  91. package/dist/dev-tools/index.d.ts +1 -2
  92. package/dist/dev-tools/index.d.ts.map +1 -1
  93. package/dist/fonts/ast-walk.d.ts +68 -0
  94. package/dist/fonts/ast-walk.d.ts.map +1 -0
  95. package/dist/fonts/ast.d.ts +23 -11
  96. package/dist/fonts/ast.d.ts.map +1 -1
  97. package/dist/fonts/bundle.d.ts +3 -1
  98. package/dist/fonts/bundle.d.ts.map +1 -1
  99. package/dist/fonts/dev-middleware.d.ts +1 -1
  100. package/dist/fonts/dev-middleware.d.ts.map +1 -1
  101. package/dist/fonts/fallbacks.d.ts +0 -7
  102. package/dist/fonts/fallbacks.d.ts.map +1 -1
  103. package/dist/fonts/google.d.ts +4 -2
  104. package/dist/fonts/google.d.ts.map +1 -1
  105. package/dist/fonts/local.d.ts +3 -3
  106. package/dist/fonts/local.d.ts.map +1 -1
  107. package/dist/fonts/paths.d.ts +43 -0
  108. package/dist/fonts/paths.d.ts.map +1 -0
  109. package/dist/fonts/pipeline.d.ts +7 -21
  110. package/dist/fonts/pipeline.d.ts.map +1 -1
  111. package/dist/fonts/transform.d.ts.map +1 -1
  112. package/dist/fonts/virtual-modules.d.ts +1 -18
  113. package/dist/fonts/virtual-modules.d.ts.map +1 -1
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +609 -406
  116. package/dist/index.js.map +1 -1
  117. package/dist/params/index.js +1 -1
  118. package/dist/params/resolve-schema.d.ts.map +1 -1
  119. package/dist/plugin-context.d.ts +28 -5
  120. package/dist/plugin-context.d.ts.map +1 -1
  121. package/dist/plugins/cache.d.ts +13 -5
  122. package/dist/plugins/cache.d.ts.map +1 -1
  123. package/dist/plugins/dev-server.d.ts.map +1 -1
  124. package/dist/plugins/fonts.d.ts +1 -2
  125. package/dist/plugins/fonts.d.ts.map +1 -1
  126. package/dist/plugins/routing.d.ts.map +1 -1
  127. package/dist/routing/codegen-types.d.ts +4 -0
  128. package/dist/routing/codegen-types.d.ts.map +1 -1
  129. package/dist/routing/codegen.d.ts.map +1 -1
  130. package/dist/routing/convention-lint.d.ts.map +1 -1
  131. package/dist/routing/file-cache.d.ts +8 -0
  132. package/dist/routing/file-cache.d.ts.map +1 -0
  133. package/dist/routing/index.js +2 -2
  134. package/dist/routing/interception.d.ts +0 -5
  135. package/dist/routing/interception.d.ts.map +1 -1
  136. package/dist/routing/manifest-codegen.d.ts +32 -0
  137. package/dist/routing/manifest-codegen.d.ts.map +1 -0
  138. package/dist/routing/scanner.d.ts.map +1 -1
  139. package/dist/schema-bridge.d.ts +15 -0
  140. package/dist/schema-bridge.d.ts.map +1 -1
  141. package/dist/search-params/define.d.ts.map +1 -1
  142. package/dist/search-params/index.js +7 -3
  143. package/dist/search-params/index.js.map +1 -1
  144. package/dist/search-params/wrappers.d.ts.map +1 -1
  145. package/dist/segment-params/define.d.ts.map +1 -1
  146. package/dist/segment-params/index.js +1 -1
  147. package/dist/server/access-gate.d.ts.map +1 -1
  148. package/dist/server/actions.d.ts +0 -14
  149. package/dist/server/actions.d.ts.map +1 -1
  150. package/dist/server/build-manifest.d.ts +0 -29
  151. package/dist/server/build-manifest.d.ts.map +1 -1
  152. package/dist/server/canonicalize.d.ts +10 -0
  153. package/dist/server/canonicalize.d.ts.map +1 -1
  154. package/dist/server/deny-boundary.d.ts +27 -2
  155. package/dist/server/deny-boundary.d.ts.map +1 -1
  156. package/dist/server/deny-renderer.d.ts +1 -1
  157. package/dist/server/deny-renderer.d.ts.map +1 -1
  158. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  159. package/dist/server/html-injector-core.d.ts +8 -0
  160. package/dist/server/html-injector-core.d.ts.map +1 -1
  161. package/dist/server/html-injectors.d.ts.map +1 -1
  162. package/dist/server/index.js +2 -2
  163. package/dist/server/internal.d.ts +1 -5
  164. package/dist/server/internal.d.ts.map +1 -1
  165. package/dist/server/internal.js +238 -349
  166. package/dist/server/internal.js.map +1 -1
  167. package/dist/server/node-stream-transforms.d.ts +0 -11
  168. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  169. package/dist/server/render-utils.d.ts +25 -0
  170. package/dist/server/render-utils.d.ts.map +1 -0
  171. package/dist/server/route-element-builder.d.ts +11 -25
  172. package/dist/server/route-element-builder.d.ts.map +1 -1
  173. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  174. package/dist/server/rsc-entry/helpers.d.ts +2 -18
  175. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  179. package/dist/server/ssr-render.d.ts.map +1 -1
  180. package/dist/server/status-code-resolver.d.ts +6 -0
  181. package/dist/server/status-code-resolver.d.ts.map +1 -1
  182. package/dist/server/utils/mdx-file.d.ts +15 -0
  183. package/dist/server/utils/mdx-file.d.ts.map +1 -0
  184. package/dist/shared/href-validation.d.ts.map +1 -1
  185. package/dist/shims/image.d.ts +15 -15
  186. package/package.json +3 -3
  187. package/src/adapters/cloudflare-kv-cache.ts +56 -19
  188. package/src/adapters/compress-module.ts +27 -118
  189. package/src/adapters/nitro.ts +3 -41
  190. package/src/adapters/raw-imports.d.ts +5 -0
  191. package/src/cache/cache-api.ts +9 -1
  192. package/src/cache/index.ts +5 -2
  193. package/src/cache/invalidation-epoch.ts +93 -0
  194. package/src/cache/redis-handler.ts +33 -2
  195. package/src/cache/timber-cache.ts +109 -53
  196. package/src/cli-schema-sync.ts +123 -30
  197. package/src/cli.ts +33 -37
  198. package/src/client/browser-entry/hmr.ts +0 -11
  199. package/src/client/browser-entry/hydrate.ts +39 -5
  200. package/src/client/browser-entry/post-hydration.ts +6 -15
  201. package/src/client/browser-entry/router-init.ts +98 -3
  202. package/src/client/error-boundary.tsx +14 -0
  203. package/src/client/error-reconstituter.tsx +15 -1
  204. package/src/client/history.ts +10 -24
  205. package/src/client/navigation-api.ts +14 -9
  206. package/src/client/navigation-context.ts +1 -21
  207. package/src/client/navigation-root.tsx +0 -8
  208. package/src/client/router.ts +18 -8
  209. package/src/client/rsc-fetch.ts +7 -2
  210. package/src/client/use-search-params.ts +10 -5
  211. package/src/config-validation.ts +33 -25
  212. package/src/cookies/define-cookie.ts +2 -18
  213. package/src/dev-tools/dev-page-shell.ts +6 -2
  214. package/src/dev-tools/hydration-overlay.ts +158 -0
  215. package/src/dev-tools/index.ts +3 -16
  216. package/src/fonts/ast-walk.ts +235 -0
  217. package/src/fonts/ast.ts +91 -249
  218. package/src/fonts/bundle.ts +16 -9
  219. package/src/fonts/dev-middleware.ts +16 -5
  220. package/src/fonts/fallbacks.ts +0 -23
  221. package/src/fonts/google.ts +7 -3
  222. package/src/fonts/local.ts +7 -6
  223. package/src/fonts/paths.ts +58 -0
  224. package/src/fonts/pipeline.ts +27 -48
  225. package/src/fonts/transform.ts +27 -0
  226. package/src/fonts/virtual-modules.ts +1 -57
  227. package/src/index.ts +5 -11
  228. package/src/params/resolve-schema.ts +2 -34
  229. package/src/plugin-context.ts +95 -17
  230. package/src/plugins/cache.ts +64 -17
  231. package/src/plugins/dev-server.ts +61 -9
  232. package/src/plugins/fonts.ts +21 -6
  233. package/src/plugins/routing.ts +5 -348
  234. package/src/routing/codegen-types.ts +4 -0
  235. package/src/routing/codegen.ts +66 -8
  236. package/src/routing/convention-lint.ts +4 -3
  237. package/src/routing/file-cache.ts +23 -0
  238. package/src/routing/interception.ts +2 -21
  239. package/src/routing/manifest-codegen.ts +290 -0
  240. package/src/routing/scanner.ts +85 -80
  241. package/src/routing/schema-validation.ts +2 -2
  242. package/src/schema-bridge.ts +63 -2
  243. package/src/search-params/define.ts +34 -2
  244. package/src/search-params/wrappers.ts +21 -3
  245. package/src/segment-params/define.ts +2 -62
  246. package/src/server/access-gate.tsx +8 -0
  247. package/src/server/actions.ts +8 -31
  248. package/src/server/build-manifest.ts +0 -56
  249. package/src/server/canonicalize.ts +6 -3
  250. package/src/server/deny-boundary.ts +106 -13
  251. package/src/server/deny-renderer.ts +46 -10
  252. package/src/server/error-boundary-wrapper.ts +1 -14
  253. package/src/server/html-injector-core.ts +19 -0
  254. package/src/server/html-injectors.ts +14 -4
  255. package/src/server/internal.ts +1 -18
  256. package/src/server/node-stream-transforms.ts +5 -72
  257. package/src/server/render-utils.ts +31 -0
  258. package/src/server/route-element-builder.ts +77 -47
  259. package/src/server/route-matcher.ts +22 -6
  260. package/src/server/rsc-entry/error-renderer.ts +2 -12
  261. package/src/server/rsc-entry/helpers.ts +6 -34
  262. package/src/server/rsc-entry/index.ts +11 -0
  263. package/src/server/rsc-entry/render-route.ts +7 -9
  264. package/src/server/rsc-entry/rsc-payload.ts +2 -1
  265. package/src/server/rsc-entry/ssr-renderer.ts +2 -2
  266. package/src/server/ssr-render.ts +1 -11
  267. package/src/server/status-code-resolver.ts +3 -2
  268. package/src/server/tree-match.ts +15 -5
  269. package/src/server/utils/mdx-file.ts +22 -0
  270. package/src/shared/href-validation.ts +8 -0
  271. package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
  272. package/dist/_chunks/cli-schema-sync-DH-M8Poy.js +0 -2
  273. package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
  274. package/dist/_chunks/define-DMVhsipD.js.map +0 -1
  275. package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
  276. package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
  277. package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
  278. package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
  279. package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
  280. package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
  281. package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
  282. package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
  283. package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
  284. package/dist/_chunks/warnings-CNPXaypM.js +0 -125
  285. package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
  286. package/dist/client/link-pending-store.d.ts +0 -65
  287. package/dist/client/link-pending-store.d.ts.map +0 -1
  288. package/dist/client/nav-link-store.d.ts +0 -27
  289. package/dist/client/nav-link-store.d.ts.map +0 -1
  290. package/dist/dev-tools/warnings.d.ts +0 -109
  291. package/dist/dev-tools/warnings.d.ts.map +0 -1
  292. package/dist/server/flush.d.ts +0 -74
  293. package/dist/server/flush.d.ts.map +0 -1
  294. package/src/client/link-pending-store.ts +0 -116
  295. package/src/client/nav-link-store.ts +0 -37
  296. package/src/dev-tools/warnings.ts +0 -238
  297. package/src/server/flush.ts +0 -183
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Route manifest codegen — generates virtual module source for the route
3
+ * manifest, search-params registry, and global schema.
4
+ *
5
+ * Extracted from plugins/routing.ts to keep it under the 500-line budget.
6
+ * See design/18-build-system.md §"Virtual Modules".
7
+ *
8
+ * Task: TIM-1071 item 8
9
+ */
10
+
11
+ import { existsSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { collectInterceptionRewrites } from './interception.js';
14
+ import type { RouteTree, SegmentNode, RouteFile } from './types.js';
15
+ import { readFileCached } from './file-cache.js';
16
+
17
+ // ── Single-file conventions ─────────────────────────────────────────────
18
+ // Each entry maps to a node property that holds a single RouteFile.
19
+ // Order matches the original serializeNode output for byte-identical diffs.
20
+ const SINGLE_FILE_KEYS = [
21
+ 'page',
22
+ 'layout',
23
+ 'middleware',
24
+ 'access',
25
+ 'route',
26
+ 'error',
27
+ 'default',
28
+ 'denied',
29
+ 'params',
30
+ ] as const;
31
+
32
+ // ── Record-shaped conventions ───────────────────────────────────────────
33
+ // Each entry maps to a node property that holds Record<string, RouteFile>.
34
+ const RECORD_FILE_KEYS = [
35
+ 'statusFiles',
36
+ 'jsonStatusFiles',
37
+ 'legacyStatusFiles',
38
+ 'metadataRoutes',
39
+ ] as const;
40
+
41
+ const SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
42
+
43
+ // ── Manifest generation ─────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Generate the virtual module source for the route manifest.
47
+ *
48
+ * The output is a default-exported object containing the serialized route tree.
49
+ * All file references use absolute paths (required for virtual modules).
50
+ */
51
+ export function generateManifestModule(tree: RouteTree, viteRoot: string): string {
52
+ const imports: string[] = [];
53
+ let importIndex = 0;
54
+
55
+ function addImport(file: RouteFile): string {
56
+ const varName = `_route${importIndex++}`;
57
+ imports.push(`const ${varName} = () => import("${file.filePath}");`);
58
+ return varName;
59
+ }
60
+
61
+ function serializeRouteFile(key: string, file: RouteFile, indent: string): string {
62
+ const v = addImport(file);
63
+ return `${indent}${key}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} },`;
64
+ }
65
+
66
+ function serializeRouteFileRecord(
67
+ key: string,
68
+ record: Record<string, RouteFile>,
69
+ indent: string
70
+ ): string | null {
71
+ const entries = Object.entries(record);
72
+ if (entries.length === 0) return null;
73
+ const inner = entries.map(([name, file]) => {
74
+ const v = addImport(file);
75
+ return `${indent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`;
76
+ });
77
+ return `${indent}${key}: {\n${inner.join(',\n')}\n${indent}},`;
78
+ }
79
+
80
+ function serializeNode(node: SegmentNode, indent: string): string {
81
+ const nextIndent = indent + ' ';
82
+ const parts: string[] = [];
83
+
84
+ parts.push(`${nextIndent}segmentName: ${JSON.stringify(node.segmentName)},`);
85
+ parts.push(`${nextIndent}segmentType: ${JSON.stringify(node.segmentType)},`);
86
+ parts.push(`${nextIndent}urlPath: ${JSON.stringify(node.urlPath)},`);
87
+
88
+ if (node.paramName) {
89
+ parts.push(`${nextIndent}paramName: ${JSON.stringify(node.paramName)},`);
90
+ }
91
+
92
+ if (node.interceptionMarker) {
93
+ parts.push(`${nextIndent}interceptionMarker: ${JSON.stringify(node.interceptionMarker)},`);
94
+ }
95
+ if (node.interceptedSegmentName) {
96
+ parts.push(
97
+ `${nextIndent}interceptedSegmentName: ${JSON.stringify(node.interceptedSegmentName)},`
98
+ );
99
+ }
100
+
101
+ // Single-file conventions — table-driven to avoid copy-paste drift
102
+ for (const key of SINGLE_FILE_KEYS) {
103
+ const file = node[key];
104
+ if (file) {
105
+ parts.push(serializeRouteFile(key, file, nextIndent));
106
+ }
107
+ }
108
+
109
+ // searchParams is now a named export from page.tsx, not a separate file.
110
+ // The page module's searchParams export is loaded via the page's lazy import.
111
+ // Runtime registration happens in the route loader using the page module.
112
+
113
+ // Record-shaped conventions — table-driven
114
+ for (const key of RECORD_FILE_KEYS) {
115
+ const record = node[key];
116
+ if (record) {
117
+ const line = serializeRouteFileRecord(key, record, nextIndent);
118
+ if (line) parts.push(line);
119
+ }
120
+ }
121
+
122
+ // Children
123
+ if (node.children.length > 0) {
124
+ const childNodes = node.children.map((c) => serializeNode(c, nextIndent));
125
+ parts.push(`${nextIndent}children: [\n${childNodes.join(',\n')}\n${nextIndent}],`);
126
+ } else {
127
+ parts.push(`${nextIndent}children: [],`);
128
+ }
129
+
130
+ // Parallel slots
131
+ const slotKeys = Object.keys(node.slots);
132
+ if (slotKeys.length > 0) {
133
+ const slotEntries: string[] = [];
134
+ for (const slotName of slotKeys) {
135
+ const slotNode = node.slots[slotName]!;
136
+ slotEntries.push(
137
+ `${nextIndent} ${JSON.stringify(slotName)}: ${serializeNode(slotNode, nextIndent + ' ')}`
138
+ );
139
+ }
140
+ parts.push(`${nextIndent}slots: {\n${slotEntries.join(',\n')}\n${nextIndent}},`);
141
+ } else {
142
+ parts.push(`${nextIndent}slots: {},`);
143
+ }
144
+
145
+ return `${indent}{\n${parts.join('\n')}\n${indent}}`;
146
+ }
147
+
148
+ const rootSerialized = serializeNode(tree.root, ' ');
149
+
150
+ let proxyLine = '';
151
+ if (tree.proxy) {
152
+ const v = addImport(tree.proxy);
153
+ proxyLine = ` proxy: { load: ${v}, filePath: ${JSON.stringify(tree.proxy.filePath)} },`;
154
+ }
155
+
156
+ let globalErrorLine = '';
157
+ if (tree.globalError) {
158
+ const v = addImport(tree.globalError);
159
+ globalErrorLine = ` globalError: { load: ${v}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
160
+ }
161
+
162
+ const rewrites = collectInterceptionRewrites(tree.root);
163
+ const rewritesLine =
164
+ rewrites.length > 0
165
+ ? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({ interceptedPattern: r.interceptedPattern, interceptingPrefix: r.interceptingPrefix })))},`
166
+ : '';
167
+
168
+ const code = [
169
+ '// Auto-generated route manifest — do not edit.',
170
+ '// Generated by timber-routing plugin.',
171
+ '',
172
+ ...imports,
173
+ '',
174
+ 'const manifest = {',
175
+ ` viteRoot: ${JSON.stringify(viteRoot)},`,
176
+ proxyLine,
177
+ globalErrorLine,
178
+ rewritesLine,
179
+ ` root: ${rootSerialized},`,
180
+ '};',
181
+ '',
182
+ 'export default manifest;',
183
+ ]
184
+ .filter((line) => line !== undefined && line !== '')
185
+ .join('\n');
186
+
187
+ return code;
188
+ }
189
+
190
+ // ── Search-params registry ──────────────────────────────────────────────
191
+
192
+ /**
193
+ * Walk the route tree and collect every page that exports a
194
+ * `searchParams` definition, then emit a module that statically imports
195
+ * each export and registers it into the shared search-params registry.
196
+ */
197
+ export function generateSearchParamsRegistryModule(tree: RouteTree): string {
198
+ const entries: Array<{ urlPath: string; filePath: string }> = [];
199
+
200
+ function walk(node: SegmentNode): void {
201
+ if (node.page) {
202
+ if (node.params && fileHasSearchParamsExport(node.params.filePath)) {
203
+ entries.push({ urlPath: node.urlPath, filePath: node.params.filePath });
204
+ } else if (fileHasSearchParamsExport(node.page.filePath)) {
205
+ entries.push({ urlPath: node.urlPath, filePath: node.page.filePath });
206
+ }
207
+ }
208
+ for (const child of node.children) walk(child);
209
+ for (const slot of Object.values(node.slots)) walk(slot);
210
+ }
211
+ walk(tree.root);
212
+
213
+ entries.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
214
+
215
+ const lines: string[] = [];
216
+ lines.push('// Auto-generated search-params registry — do not edit.');
217
+ lines.push('// Generated by timber-routing plugin (TIM-830).');
218
+ lines.push('');
219
+ lines.push("import { registerSearchParams } from '@timber-js/app/search-params';");
220
+ lines.push('');
221
+
222
+ entries.forEach((entry, i) => {
223
+ lines.push(`import { searchParams as r${i} } from ${JSON.stringify(entry.filePath)};`);
224
+ });
225
+ lines.push('');
226
+
227
+ entries.forEach((entry, i) => {
228
+ lines.push(`registerSearchParams(${JSON.stringify(entry.urlPath)}, r${i});`);
229
+ });
230
+ lines.push('');
231
+
232
+ lines.push('export {};');
233
+ lines.push('');
234
+
235
+ return lines.join('\n');
236
+ }
237
+
238
+ function fileHasSearchParamsExport(filePath: string): boolean {
239
+ try {
240
+ if (!existsSync(filePath)) return false;
241
+ const content = readFileCached(filePath);
242
+ return (
243
+ /export\s+(const|let|var)\s+searchParams\b/.test(content) ||
244
+ /export\s*\{[^}]*\bsearchParams\b[^}]*\}/.test(content)
245
+ );
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+
251
+ // ── Schema module ───────────────────────────────────────────────────────
252
+
253
+ /**
254
+ * Generate the virtual:timber-schema module.
255
+ *
256
+ * When app/schema.{ts,tsx,js,jsx} exists, generates a module that
257
+ * imports the schema and resolves its segmentParams codecs into a
258
+ * bare-name codec map for runtime coercion.
259
+ */
260
+ export function generateSchemaModule(appDir: string): string {
261
+ let schemaPath: string | null = null;
262
+ for (const ext of SCHEMA_EXTENSIONS) {
263
+ const candidate = join(appDir, `schema${ext}`);
264
+ if (existsSync(candidate)) {
265
+ schemaPath = candidate;
266
+ break;
267
+ }
268
+ }
269
+
270
+ if (!schemaPath) {
271
+ return [
272
+ '// Auto-generated schema loader — no app/schema.ts found.',
273
+ 'export default null;',
274
+ ].join('\n');
275
+ }
276
+
277
+ return [
278
+ '// Auto-generated schema loader — do not edit.',
279
+ '// Generated by timber-routing plugin (TIM-931).',
280
+ '',
281
+ `import schema from ${JSON.stringify(schemaPath)};`,
282
+ `import { resolveSchemaCodecs } from '@timber-js/app/params';`,
283
+ '',
284
+ 'const globalCodecs = schema?.segmentParams',
285
+ ' ? resolveSchemaCodecs(schema.segmentParams)',
286
+ ' : null;',
287
+ '',
288
+ 'export default globalCodecs;',
289
+ ].join('\n');
290
+ }
@@ -22,19 +22,7 @@ import { classifySegment } from './segment-classify.js';
22
22
  import { DEFAULT_PAGE_EXTENSIONS } from './types.js';
23
23
  import { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.js';
24
24
  import { swallow } from '../server/logger.js';
25
-
26
- /**
27
- * Pattern matching encoded path delimiters that must be rejected during route discovery.
28
- * %2F / %2f (forward slash) and %5C / %5c (backslash) can cause route collisions
29
- * when decoded. See design/13-security.md §"Encoded separators rejected".
30
- */
31
- const ENCODED_SEPARATOR_PATTERN = /%(?:2[fF]|5[cC])/;
32
-
33
- /**
34
- * Pattern matching encoded null bytes (%00) that must be rejected.
35
- * See design/13-security.md §"Null bytes rejected".
36
- */
37
- const ENCODED_NULL_PATTERN = /%00/;
25
+ import { ENCODED_SEPARATOR_RE, NULL_BYTE_RE } from '../server/canonicalize.js';
38
26
 
39
27
  /**
40
28
  * File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)
@@ -177,44 +165,26 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
177
165
 
178
166
  // Page-extension conventions (page, layout, error, default, denied)
179
167
  if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {
180
- const file: RouteFile = { filePath: fullPath, extension: ext };
181
- switch (name) {
182
- case 'page':
183
- node.page = file;
184
- break;
185
- case 'layout':
186
- node.layout = file;
187
- break;
188
- case 'error':
189
- node.error = file;
190
- break;
191
- case 'default':
192
- node.default = file;
193
- break;
194
- case 'denied':
195
- node.denied = file;
196
- break;
168
+ const key = name as 'page' | 'layout' | 'error' | 'default' | 'denied';
169
+ const existing = node[key];
170
+ if (existing) {
171
+ throwDuplicateConvention(name, existing.filePath, fullPath);
197
172
  }
173
+ node[key] = { filePath: fullPath, extension: ext };
198
174
  continue;
199
175
  }
200
176
 
201
- // Fixed conventions (middleware, access, route) — always .ts or .tsx
202
- if (FIXED_CONVENTIONS.has(name) && /\.?[jt]sx?$/.test(ext)) {
203
- const file: RouteFile = { filePath: fullPath, extension: ext };
204
- switch (name) {
205
- case 'middleware':
206
- node.middleware = file;
207
- break;
208
- case 'access':
209
- node.access = file;
210
- break;
211
- case 'route':
212
- node.route = file;
213
- break;
214
- case 'params':
215
- node.params = file;
216
- break;
177
+ // Fixed conventions (middleware, access, route, params) — only
178
+ // .ts/.tsx/.js/.jsx, never pageExtensions like .mdx. The regex is
179
+ // anchored: mts/cts/mjs/cjs (and stray tail matches like `.foots`)
180
+ // are NOT recognized. See design/07-routing.md §"Page Extensions".
181
+ if (FIXED_CONVENTIONS.has(name) && /^[jt]sx?$/.test(ext)) {
182
+ const key = name as 'middleware' | 'access' | 'route' | 'params';
183
+ const existing = node[key];
184
+ if (existing) {
185
+ throwDuplicateConvention(name, existing.filePath, fullPath);
217
186
  }
187
+ node[key] = { filePath: fullPath, extension: ext };
218
188
  continue;
219
189
  }
220
190
 
@@ -233,6 +203,10 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
233
203
  if (!node.statusFiles) {
234
204
  node.statusFiles = {};
235
205
  }
206
+ const existing = node.statusFiles[name];
207
+ if (existing) {
208
+ throwDuplicateConvention(name, existing.filePath, fullPath);
209
+ }
236
210
  node.statusFiles[name] = { filePath: fullPath, extension: ext };
237
211
  continue;
238
212
  }
@@ -242,6 +216,10 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
242
216
  if (!node.legacyStatusFiles) {
243
217
  node.legacyStatusFiles = {};
244
218
  }
219
+ const existing = node.legacyStatusFiles[name];
220
+ if (existing) {
221
+ throwDuplicateConvention(name, existing.filePath, fullPath);
222
+ }
245
223
  node.legacyStatusFiles[name] = { filePath: fullPath, extension: ext };
246
224
  continue;
247
225
  }
@@ -249,6 +227,8 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
249
227
  // Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)
250
228
  // Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.
251
229
  // When both exist for the same base name, dynamic takes precedence.
230
+ // Two files of the SAME kind (static+static or dynamic+dynamic) are a
231
+ // build error — picking one by readdir order is platform-dependent.
252
232
  // See design/16-metadata.md §"Metadata Routes"
253
233
  const metaInfo = classifyMetadataRoute(entry);
254
234
  if (metaInfo) {
@@ -257,11 +237,13 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
257
237
  }
258
238
  const existing = node.metadataRoutes[name];
259
239
  if (existing) {
260
- // Dynamic > static precedence: only overwrite if the new file is dynamic
261
- // or the existing file is static (dynamic always wins).
262
240
  const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);
263
241
  const newIsDynamic = isDynamicMetadataExtension(name, ext);
264
- if (newIsDynamic || !existingIsDynamic) {
242
+ if (existingIsDynamic === newIsDynamic) {
243
+ throwDuplicateConvention(name, existing.filePath, fullPath);
244
+ }
245
+ // Dynamic > static precedence: overwrite only when the new file is dynamic.
246
+ if (newIsDynamic) {
265
247
  node.metadataRoutes[name] = { filePath: fullPath, extension: ext };
266
248
  }
267
249
  } else {
@@ -281,6 +263,26 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
281
263
  }
282
264
  }
283
265
 
266
+ /**
267
+ * Throw a build error for two files claiming the same convention slot in
268
+ * one segment (e.g. page.tsx + page.jsx). Which file readdir returns first
269
+ * is platform-dependent (APFS sorts entries, ext4 does not) — silently
270
+ * picking one would let macOS dev and Linux CI serve different
271
+ * implementations of the same route.
272
+ */
273
+ function throwDuplicateConvention(
274
+ convention: string,
275
+ firstPath: string,
276
+ secondPath: string
277
+ ): never {
278
+ throw new Error(
279
+ `Build error: duplicate '${convention}' files in the same route segment.\n` +
280
+ ` File 1: ${firstPath}\n` +
281
+ ` File 2: ${secondPath}\n` +
282
+ `Each segment may define '${convention}' at most once. Remove one of the files.`
283
+ );
284
+ }
285
+
284
286
  /**
285
287
  * Recursively scan child directories and build the segment tree.
286
288
  */
@@ -305,17 +307,17 @@ function scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<stri
305
307
  // Reject directories with encoded path delimiters or null bytes.
306
308
  // These can cause route collisions when decoded at the URL boundary.
307
309
  // See design/13-security.md §"Encoded separators rejected" and §"Null bytes rejected".
308
- if (ENCODED_SEPARATOR_PATTERN.test(entry)) {
310
+ if (ENCODED_SEPARATOR_RE.test(entry)) {
309
311
  throw new Error(
310
- `Build error: directory name contains an encoded path delimiter (%%2F or %%5C).\n` +
312
+ `Build error: directory name contains an encoded path delimiter (%2F or %5C).\n` +
311
313
  ` Directory: ${fullPath}\n` +
312
314
  `Encoded separators in directory names cause route collisions when decoded. ` +
313
315
  `Rename the directory to remove the encoded delimiter.`
314
316
  );
315
317
  }
316
- if (ENCODED_NULL_PATTERN.test(entry)) {
318
+ if (NULL_BYTE_RE.test(entry)) {
317
319
  throw new Error(
318
- `Build error: directory name contains an encoded null byte (%%00).\n` +
320
+ `Build error: directory name contains an encoded null byte (%00).\n` +
319
321
  ` Directory: ${fullPath}\n` +
320
322
  `Encoded null bytes in directory names are not allowed. ` +
321
323
  `Rename the directory to remove the null byte encoding.`
@@ -359,57 +361,60 @@ function scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<stri
359
361
  * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim
360
362
  * /login — the scanner must detect and reject this at build time.
361
363
  *
362
- * Parallel slots are excluded from collision detection — they intentionally coexist at
363
- * the same URL path as their parent (that's the whole point of parallel routes).
364
+ * Each parallel slot is validated as its OWN collision space: a slot's pages
365
+ * intentionally share URL paths with the parent tree (that's the point of
366
+ * parallel routes), but two route groups INSIDE one slot claiming the same
367
+ * URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.
364
368
  */
365
369
  function validateRouteGroupCollisions(root: SegmentNode): void {
366
370
  // Map from urlPath → { filePath, source } for the first page/route seen at that path
367
- const seen = new Map<string, { filePath: string; segmentPath: string }>();
368
- collectRoutableLeaves(root, seen, '', false);
371
+ collectRoutableLeaves(root, new Map(), '');
369
372
  }
370
373
 
371
374
  /**
372
375
  * Walk the segment tree and collect all routable leaves (page or route files),
373
- * throwing on collision. Slots are tracked in their own collision space since
374
- * they are parallel routes that intentionally share URL paths with their parent.
376
+ * throwing on collision. Each slot recurses with a FRESH collision map —
377
+ * slots are parallel routes that intentionally share URL paths with their
378
+ * parent tree, but collisions within a single slot are real errors.
375
379
  */
376
380
  function collectRoutableLeaves(
377
381
  node: SegmentNode,
378
382
  seen: Map<string, { filePath: string; segmentPath: string }>,
379
- segmentPath: string,
380
- insideSlot: boolean
383
+ segmentPath: string
381
384
  ): void {
385
+ // Intercepting routes are resolved by the slot resolver, not URL matching,
386
+ // and their computed urlPaths intentionally mirror other routes (e.g. two
387
+ // interceptors in one slot both produce /gallery/[id]). Skip them entirely.
388
+ if (node.segmentType === 'intercepting') return;
389
+
382
390
  const currentPath = segmentPath
383
391
  ? `${segmentPath}/${node.segmentName}`
384
392
  : node.segmentName || '(root)';
385
393
 
386
- // Only check collisions for non-slot pages — slots intentionally share URL paths
387
- if (!insideSlot) {
388
- const routableFile = node.page ?? node.route;
389
- if (routableFile) {
390
- const existing = seen.get(node.urlPath);
391
- if (existing) {
392
- throw new Error(
393
- `Build error: route collision — multiple route groups produce a page/route at the same URL path.\n` +
394
- ` URL path: ${node.urlPath}\n` +
395
- ` File 1: ${existing.filePath} (via ${existing.segmentPath})\n` +
396
- ` File 2: ${routableFile.filePath} (via ${currentPath})\n` +
397
- `Each URL path must map to exactly one page or route handler. ` +
398
- `Rename or move one of the conflicting files.`
399
- );
400
- }
401
- seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });
394
+ const routableFile = node.page ?? node.route;
395
+ if (routableFile) {
396
+ const existing = seen.get(node.urlPath);
397
+ if (existing) {
398
+ throw new Error(
399
+ `Build error: route collision — multiple route groups produce a page/route at the same URL path.\n` +
400
+ ` URL path: ${node.urlPath}\n` +
401
+ ` File 1: ${existing.filePath} (via ${existing.segmentPath})\n` +
402
+ ` File 2: ${routableFile.filePath} (via ${currentPath})\n` +
403
+ `Each URL path must map to exactly one page or route handler. ` +
404
+ `Rename or move one of the conflicting files.`
405
+ );
402
406
  }
407
+ seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });
403
408
  }
404
409
 
405
410
  // Recurse into children
406
411
  for (const child of node.children) {
407
- collectRoutableLeaves(child, seen, currentPath, insideSlot);
412
+ collectRoutableLeaves(child, seen, currentPath);
408
413
  }
409
414
 
410
- // Recurse into slots — each slot is its own parallel route space
415
+ // Recurse into slots — each slot gets a fresh map (its own collision space)
411
416
  for (const slotNode of Object.values(node.slots)) {
412
- collectRoutableLeaves(slotNode, seen, currentPath, true);
417
+ collectRoutableLeaves(slotNode, new Map(), currentPath);
413
418
  }
414
419
  }
415
420
 
@@ -7,7 +7,7 @@
7
7
  * Design doc: design/41-global-params.md
8
8
  */
9
9
 
10
- import { readFileSync } from 'node:fs';
10
+ import { readFileCached } from './file-cache.js';
11
11
  import type { RouteTree, SegmentNode } from './types.js';
12
12
  import { findSchemaFile, parseExistingSchemaKeys } from '../cli-schema-sync.js';
13
13
  import { classifyUrlSegment } from './segment-classify.js';
@@ -66,7 +66,7 @@ export function validateSchemaAgainstRoutes(tree: RouteTree, appDir: string): Sc
66
66
  const schemaPath = findSchemaFile(appDir);
67
67
  if (!schemaPath) return [];
68
68
 
69
- const content = readFileSync(schemaPath, 'utf-8');
69
+ const content = readFileCached(schemaPath);
70
70
  const schemaKeys = parseExistingSchemaKeys(content);
71
71
 
72
72
  if (schemaKeys.length === 0) return [];
@@ -85,6 +85,63 @@ export function isCodec(value: unknown): value is Codec<unknown> {
85
85
  );
86
86
  }
87
87
 
88
+ // ---------------------------------------------------------------------------
89
+ // fromParamSchema — bridge from Standard Schema to Codec<T> for route params
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * Bridge a Standard Schema to a Codec for route params.
94
+ * Parse throws on failure (invalid param → 404). Serialize returns string.
95
+ */
96
+ export function fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {
97
+ return {
98
+ parse(value: string | string[] | undefined): T {
99
+ const result = validateSync(schema, value);
100
+ if (!result.issues) {
101
+ return result.value;
102
+ }
103
+ const messages = result.issues.map((i) => i.message).join(', ');
104
+ throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);
105
+ },
106
+ serialize(value: T): string | null {
107
+ if (value === null || value === undefined) return null;
108
+ if (Array.isArray(value)) return value.join('/');
109
+ return String(value);
110
+ },
111
+ };
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // resolveCodecOrSchema — generic resolver for any codec-or-schema field
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Resolve a field value to a Codec. Accepts Codec<T>, StandardSchemaV1<T>,
120
+ * or (for route params) auto-wraps via fromParamSchema.
121
+ *
122
+ * @param fieldName - used in error messages
123
+ * @param value - the codec or schema to resolve
124
+ * @param mode - 'param' uses fromParamSchema (throws on parse failure),
125
+ * 'search' uses fromSchema (falls back to default on failure)
126
+ */
127
+ export function resolveCodecOrSchema(
128
+ fieldName: string,
129
+ value: unknown,
130
+ mode: 'param' | 'search' = 'search'
131
+ ): Codec<unknown> {
132
+ if (isCodec(value)) return value;
133
+ if (isStandardSchema(value)) {
134
+ return mode === 'param'
135
+ ? fromParamSchema(fieldName, value)
136
+ : (fromSchema(value) as Codec<unknown>);
137
+ }
138
+ throw new Error(
139
+ `[timber] Field '${fieldName}' is not a valid codec or Standard Schema. ` +
140
+ `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +
141
+ `(Zod, Valibot, ArkType).`
142
+ );
143
+ }
144
+
88
145
  // ---------------------------------------------------------------------------
89
146
  // fromSchema — bridge from Standard Schema to Codec<T>
90
147
  // ---------------------------------------------------------------------------
@@ -119,13 +176,17 @@ export function fromSchema<T>(schema: StandardSchemaV1<T>): Codec<T> {
119
176
  return result.value;
120
177
  }
121
178
 
122
- // On failure, try parsing undefined to get the default
179
+ // On failure, try parsing undefined to get the default.
180
+ // Re-validate each time so factory defaults (e.g. .default(() => []))
181
+ // produce fresh values.
123
182
  const defaultResult = validateSync(schema, undefined);
124
183
  if (!defaultResult.issues) {
125
184
  return defaultResult.value;
126
185
  }
127
186
 
128
- // No default available — return undefined (codec design choice)
187
+ // No default available — return undefined (codec design choice).
188
+ // Callers like defineSearchParams validate this at definition time
189
+ // via validateSchemaDefaults() to catch it early.
129
190
  return undefined as T;
130
191
  },
131
192