@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
@@ -10,24 +10,25 @@
10
10
 
11
11
  import type { Plugin, ViteDevServer } from 'vite';
12
12
  import { writeFile, mkdir } from 'node:fs/promises';
13
- import { existsSync, readFileSync } from 'node:fs';
14
13
  import { join } from 'node:path';
15
14
  import { scanRoutes } from '../routing/scanner.js';
16
15
  import { generateRouteMap } from '../routing/codegen.js';
17
- import { collectInterceptionRewrites } from '../routing/interception.js';
18
16
  import {
19
17
  buildDirToRouteMap,
20
18
  writeSegmentTypes,
21
19
  type SegmentInfo,
22
20
  } from '../routing/segment-codegen.js';
23
21
  import { validateSchemaAgainstRoutes } from '../routing/schema-validation.js';
24
-
22
+ import {
23
+ generateManifestModule,
24
+ generateSearchParamsRegistryModule,
25
+ generateSchemaModule,
26
+ } from '../routing/manifest-codegen.js';
25
27
  import {
26
28
  lintConventions,
27
29
  checkAppDirExists,
28
30
  formatConventionWarnings,
29
31
  } from '../routing/convention-lint.js';
30
- import type { RouteTree, SegmentNode, RouteFile } from '../routing/types.js';
31
32
  import type { PluginContext } from '../plugin-context.js';
32
33
 
33
34
  const VIRTUAL_MODULE_ID = 'virtual:timber-route-manifest';
@@ -413,347 +414,3 @@ function invalidateManifest(server: ViteDevServer): void {
413
414
  // Trigger HMR full reload — route changes are structural
414
415
  server.hot.send({ type: 'full-reload' });
415
416
  }
416
-
417
- /**
418
- * TIM-830: Walk the route tree and collect every page that exports a
419
- * `searchParams` definition, then emit a module that statically imports
420
- * each export and registers it into the shared search-params registry.
421
- *
422
- * Keys use the un-interpolated route pattern (e.g. '/products/[id]') so
423
- * <Link> can look up the definition from the original `href` prop before
424
- * segmentParams are interpolated.
425
- */
426
- /** Schema file extensions to check, in priority order. */
427
- const SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
428
-
429
- /**
430
- * TIM-931: Generate the virtual:timber-schema module.
431
- *
432
- * When app/schema.{ts,tsx,js,jsx} exists, generates a module that
433
- * imports the schema and resolves its segmentParams codecs into a
434
- * bare-name codec map for runtime coercion.
435
- *
436
- * When no schema exists, exports null.
437
- */
438
- function generateSchemaModule(appDir: string): string {
439
- let schemaPath: string | null = null;
440
- for (const ext of SCHEMA_EXTENSIONS) {
441
- const candidate = join(appDir, `schema${ext}`);
442
- if (existsSync(candidate)) {
443
- schemaPath = candidate;
444
- break;
445
- }
446
- }
447
-
448
- if (!schemaPath) {
449
- return [
450
- '// Auto-generated schema loader — no app/schema.ts found.',
451
- 'export default null;',
452
- ].join('\n');
453
- }
454
-
455
- return [
456
- '// Auto-generated schema loader — do not edit.',
457
- '// Generated by timber-routing plugin (TIM-931).',
458
- '',
459
- `import schema from ${JSON.stringify(schemaPath)};`,
460
- `import { resolveSchemaCodecs } from '@timber-js/app/params';`,
461
- '',
462
- 'const globalCodecs = schema?.segmentParams',
463
- ' ? resolveSchemaCodecs(schema.segmentParams)',
464
- ' : null;',
465
- '',
466
- 'export default globalCodecs;',
467
- ].join('\n');
468
- }
469
-
470
- function generateSearchParamsRegistryModule(tree: RouteTree): string {
471
- const entries: Array<{ urlPath: string; filePath: string }> = [];
472
-
473
- function walk(node: SegmentNode): void {
474
- // params.ts is the canonical location for `export const searchParams`;
475
- // page.tsx is the legacy fallback (see codegen.ts `collectRoutes`).
476
- if (node.page) {
477
- if (node.params && fileHasSearchParamsExport(node.params.filePath)) {
478
- entries.push({ urlPath: node.urlPath, filePath: node.params.filePath });
479
- } else if (fileHasSearchParamsExport(node.page.filePath)) {
480
- entries.push({ urlPath: node.urlPath, filePath: node.page.filePath });
481
- }
482
- }
483
- for (const child of node.children) walk(child);
484
- for (const slot of Object.values(node.slots)) walk(slot);
485
- }
486
- walk(tree.root);
487
-
488
- // Deterministic order for stable output / snapshots
489
- entries.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
490
-
491
- const lines: string[] = [];
492
- lines.push('// Auto-generated search-params registry — do not edit.');
493
- lines.push('// Generated by timber-routing plugin (TIM-830).');
494
- lines.push('');
495
- lines.push("import { registerSearchParams } from '@timber-js/app/search-params';");
496
- lines.push('');
497
-
498
- // Static imports — Vite/Rolldown tree-shakes the page component body out
499
- // of this chunk, leaving only the `searchParams` named export.
500
- entries.forEach((entry, i) => {
501
- lines.push(`import { searchParams as r${i} } from ${JSON.stringify(entry.filePath)};`);
502
- });
503
- lines.push('');
504
-
505
- entries.forEach((entry, i) => {
506
- lines.push(`registerSearchParams(${JSON.stringify(entry.urlPath)}, r${i});`);
507
- });
508
- lines.push('');
509
-
510
- // Exporting an empty object keeps the module a real ESM module even
511
- // when there are zero entries (no top-level imports in that case).
512
- lines.push('export {};');
513
- lines.push('');
514
-
515
- return lines.join('\n');
516
- }
517
-
518
- /**
519
- * Lightweight check for a `searchParams` named export. Mirrors
520
- * `fileHasExport('searchParams')` in routing/codegen.ts so we stay
521
- * in sync with what the type codegen detects.
522
- */
523
- function fileHasSearchParamsExport(filePath: string): boolean {
524
- try {
525
- if (!existsSync(filePath)) return false;
526
- const content = readFileSync(filePath, 'utf-8');
527
- return (
528
- /export\s+(const|let|var)\s+searchParams\b/.test(content) ||
529
- /export\s*\{[^}]*\bsearchParams\b[^}]*\}/.test(content)
530
- );
531
- } catch {
532
- return false;
533
- }
534
- }
535
-
536
- /**
537
- * Generate the virtual module source code for the route manifest.
538
- *
539
- * The output is a default-exported object containing the serialized route tree.
540
- * All file references use absolute paths (required for virtual modules).
541
- */
542
- function generateManifestModule(tree: RouteTree, viteRoot: string): string {
543
- const imports: string[] = [];
544
- let importIndex = 0;
545
-
546
- /**
547
- * Create a lazy import expression for a route file.
548
- * Returns the import variable name.
549
- */
550
- function addImport(file: RouteFile): string {
551
- const varName = `_route${importIndex++}`;
552
- imports.push(`const ${varName} = () => import("${file.filePath}");`);
553
- return varName;
554
- }
555
-
556
- /**
557
- * Serialize a segment node to a JS object literal.
558
- */
559
- function serializeNode(node: SegmentNode, indent: string): string {
560
- const nextIndent = indent + ' ';
561
- const parts: string[] = [];
562
-
563
- parts.push(`${nextIndent}segmentName: ${JSON.stringify(node.segmentName)},`);
564
- parts.push(`${nextIndent}segmentType: ${JSON.stringify(node.segmentType)},`);
565
- parts.push(`${nextIndent}urlPath: ${JSON.stringify(node.urlPath)},`);
566
-
567
- if (node.paramName) {
568
- parts.push(`${nextIndent}paramName: ${JSON.stringify(node.paramName)},`);
569
- }
570
-
571
- if (node.interceptionMarker) {
572
- parts.push(`${nextIndent}interceptionMarker: ${JSON.stringify(node.interceptionMarker)},`);
573
- }
574
- if (node.interceptedSegmentName) {
575
- parts.push(
576
- `${nextIndent}interceptedSegmentName: ${JSON.stringify(node.interceptedSegmentName)},`
577
- );
578
- }
579
-
580
- // File conventions — absolute paths as lazy imports
581
- if (node.page) {
582
- const v = addImport(node.page);
583
- parts.push(
584
- `${nextIndent}page: { load: ${v}, filePath: ${JSON.stringify(node.page.filePath)} },`
585
- );
586
- }
587
- if (node.layout) {
588
- const v = addImport(node.layout);
589
- parts.push(
590
- `${nextIndent}layout: { load: ${v}, filePath: ${JSON.stringify(node.layout.filePath)} },`
591
- );
592
- }
593
- if (node.middleware) {
594
- const v = addImport(node.middleware);
595
- parts.push(
596
- `${nextIndent}middleware: { load: ${v}, filePath: ${JSON.stringify(node.middleware.filePath)} },`
597
- );
598
- }
599
- if (node.access) {
600
- const v = addImport(node.access);
601
- parts.push(
602
- `${nextIndent}access: { load: ${v}, filePath: ${JSON.stringify(node.access.filePath)} },`
603
- );
604
- }
605
- if (node.route) {
606
- const v = addImport(node.route);
607
- parts.push(
608
- `${nextIndent}route: { load: ${v}, filePath: ${JSON.stringify(node.route.filePath)} },`
609
- );
610
- }
611
- if (node.error) {
612
- const v = addImport(node.error);
613
- parts.push(
614
- `${nextIndent}error: { load: ${v}, filePath: ${JSON.stringify(node.error.filePath)} },`
615
- );
616
- }
617
- if (node.default) {
618
- const v = addImport(node.default);
619
- parts.push(
620
- `${nextIndent}default: { load: ${v}, filePath: ${JSON.stringify(node.default.filePath)} },`
621
- );
622
- }
623
- if (node.denied) {
624
- const v = addImport(node.denied);
625
- parts.push(
626
- `${nextIndent}denied: { load: ${v}, filePath: ${JSON.stringify(node.denied.filePath)} },`
627
- );
628
- }
629
- if (node.params) {
630
- const v = addImport(node.params);
631
- parts.push(
632
- `${nextIndent}params: { load: ${v}, filePath: ${JSON.stringify(node.params.filePath)} },`
633
- );
634
- }
635
- // searchParams is now a named export from page.tsx, not a separate file.
636
- // The page module's searchParams export is loaded via the page's lazy import.
637
- // Runtime registration happens in the route loader using the page module.
638
-
639
- // Status-code files
640
- if (node.statusFiles && Object.keys(node.statusFiles).length > 0) {
641
- const statusEntries: string[] = [];
642
- for (const [code, file] of Object.entries(node.statusFiles)) {
643
- const v = addImport(file);
644
- statusEntries.push(
645
- `${nextIndent} ${JSON.stringify(code)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`
646
- );
647
- }
648
- parts.push(`${nextIndent}statusFiles: {\n${statusEntries.join(',\n')}\n${nextIndent}},`);
649
- }
650
-
651
- // JSON status-code files
652
- if (node.jsonStatusFiles && Object.keys(node.jsonStatusFiles).length > 0) {
653
- const jsonEntries: string[] = [];
654
- for (const [code, file] of Object.entries(node.jsonStatusFiles)) {
655
- const v = addImport(file);
656
- jsonEntries.push(
657
- `${nextIndent} ${JSON.stringify(code)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`
658
- );
659
- }
660
- parts.push(`${nextIndent}jsonStatusFiles: {\n${jsonEntries.join(',\n')}\n${nextIndent}},`);
661
- }
662
-
663
- // Legacy status files
664
- if (node.legacyStatusFiles && Object.keys(node.legacyStatusFiles).length > 0) {
665
- const legacyEntries: string[] = [];
666
- for (const [name, file] of Object.entries(node.legacyStatusFiles)) {
667
- const v = addImport(file);
668
- legacyEntries.push(
669
- `${nextIndent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`
670
- );
671
- }
672
- parts.push(
673
- `${nextIndent}legacyStatusFiles: {\n${legacyEntries.join(',\n')}\n${nextIndent}},`
674
- );
675
- }
676
-
677
- // Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.)
678
- if (node.metadataRoutes && Object.keys(node.metadataRoutes).length > 0) {
679
- const metaEntries: string[] = [];
680
- for (const [name, file] of Object.entries(node.metadataRoutes)) {
681
- const v = addImport(file);
682
- metaEntries.push(
683
- `${nextIndent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`
684
- );
685
- }
686
- parts.push(`${nextIndent}metadataRoutes: {\n${metaEntries.join(',\n')}\n${nextIndent}},`);
687
- }
688
-
689
- // Children
690
- if (node.children.length > 0) {
691
- const childNodes = node.children.map((c) => serializeNode(c, nextIndent));
692
- parts.push(`${nextIndent}children: [\n${childNodes.join(',\n')}\n${nextIndent}],`);
693
- } else {
694
- parts.push(`${nextIndent}children: [],`);
695
- }
696
-
697
- // Parallel slots
698
- const slotKeys = Object.keys(node.slots);
699
- if (slotKeys.length > 0) {
700
- const slotEntries: string[] = [];
701
- for (const slotName of slotKeys) {
702
- const slotNode = node.slots[slotName]!;
703
- slotEntries.push(
704
- `${nextIndent} ${JSON.stringify(slotName)}: ${serializeNode(slotNode, nextIndent + ' ')}`
705
- );
706
- }
707
- parts.push(`${nextIndent}slots: {\n${slotEntries.join(',\n')}\n${nextIndent}},`);
708
- } else {
709
- parts.push(`${nextIndent}slots: {},`);
710
- }
711
-
712
- return `${indent}{\n${parts.join('\n')}\n${indent}}`;
713
- }
714
-
715
- const rootSerialized = serializeNode(tree.root, ' ');
716
-
717
- // Proxy file
718
- let proxyLine = '';
719
- if (tree.proxy) {
720
- const v = addImport(tree.proxy);
721
- proxyLine = ` proxy: { load: ${v}, filePath: ${JSON.stringify(tree.proxy.filePath)} },`;
722
- }
723
-
724
- // Global error page (Tier 2)
725
- let globalErrorLine = '';
726
- if (tree.globalError) {
727
- const v = addImport(tree.globalError);
728
- globalErrorLine = ` globalError: { load: ${v}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
729
- }
730
-
731
- // Interception rewrites — computed at build time from the route tree.
732
- // Only interceptedPattern and interceptingPrefix are needed at runtime.
733
- const rewrites = collectInterceptionRewrites(tree.root);
734
- const rewritesLine =
735
- rewrites.length > 0
736
- ? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({ interceptedPattern: r.interceptedPattern, interceptingPrefix: r.interceptingPrefix })))},`
737
- : '';
738
-
739
- const code = [
740
- '// Auto-generated route manifest — do not edit.',
741
- '// Generated by timber-routing plugin.',
742
- '',
743
- ...imports,
744
- '',
745
- 'const manifest = {',
746
- ` viteRoot: ${JSON.stringify(viteRoot)},`,
747
- proxyLine,
748
- globalErrorLine,
749
- rewritesLine,
750
- ` root: ${rootSerialized},`,
751
- '};',
752
- '',
753
- 'export default manifest;',
754
- ]
755
- .filter((line) => line !== undefined && line !== '')
756
- .join('\n');
757
-
758
- return code;
759
- }
@@ -21,6 +21,10 @@ export interface RouteEntry {
21
21
  searchParamsPagePath?: string;
22
22
  /** Whether this is an API route (route.ts) vs page route */
23
23
  isApiRoute: boolean;
24
+ /** Whether this entry lives inside a parallel slot (@name). Slot entries
25
+ * share their parent's urlPath — urlPath-keyed emissions dedupe them,
26
+ * preferring the non-slot entry. segmentPath-keyed emissions keep all. */
27
+ isSlotRoute?: boolean;
24
28
  /** Whether a global schema (app/schema.ts) exists for this app */
25
29
  hasSchema?: boolean;
26
30
  }
@@ -8,7 +8,7 @@
8
8
  * No runtime overhead — purely static type generation.
9
9
  */
10
10
 
11
- import { existsSync, readFileSync } from 'node:fs';
11
+ import { existsSync } from 'node:fs';
12
12
  import { join, relative } from 'node:path';
13
13
  import type { RouteTree, SegmentNode } from './types.js';
14
14
  import type { ParamEntry, RouteEntry } from './codegen-types.js';
@@ -18,6 +18,7 @@ import {
18
18
  emitResolveSegmentFieldHelper,
19
19
  formatSearchParamsType,
20
20
  } from './codegen-shared.js';
21
+ import { readFileCached } from './file-cache.js';
21
22
  import { formatLinkCatchAllOverloads, formatTypedLinkOverloads } from './link-codegen.js';
22
23
 
23
24
  /** Options for route map generation. */
@@ -48,7 +49,7 @@ export function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}):
48
49
  const hasSchema = options.hasSchema ?? detectSchema(options.appDir);
49
50
 
50
51
  const routes: RouteEntry[] = [];
51
- collectRoutes(tree.root, [], [], '', routes, hasSchema);
52
+ collectRoutes(tree.root, [], [], '', routes, hasSchema, false);
52
53
 
53
54
  // Sort routes alphabetically for deterministic output
54
55
  routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
@@ -82,7 +83,8 @@ function collectRoutes(
82
83
  ancestorParamsFiles: string[],
83
84
  parentTreePath: string,
84
85
  routes: RouteEntry[],
85
- hasSchema: boolean
86
+ hasSchema: boolean,
87
+ insideSlot: boolean
86
88
  ): void {
87
89
  // Build the tree path (includes groups, slots — uniquely identifies this segment).
88
90
  // Root node has empty segmentName, producing treePath = ''.
@@ -156,6 +158,7 @@ function collectRoutes(
156
158
  params: resolvedParams,
157
159
  hasSearchParams: false,
158
160
  isApiRoute,
161
+ isSlotRoute: insideSlot,
159
162
  hasSchema,
160
163
  };
161
164
 
@@ -175,15 +178,63 @@ function collectRoutes(
175
178
 
176
179
  // Recurse into children
177
180
  for (const child of node.children) {
178
- collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema);
181
+ collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema, insideSlot);
179
182
  }
180
183
 
181
184
  // Recurse into slots (they share the parent's URL path, but may have their own pages)
182
185
  for (const slot of Object.values(node.slots)) {
183
- collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema);
186
+ collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema, true);
184
187
  }
185
188
  }
186
189
 
190
+ /**
191
+ * Dedupe route entries by urlPath for urlPath-keyed emissions (the Routes
192
+ * interface, useQueryStates overloads, and typed Link variants).
193
+ *
194
+ * Parallel-slot pages share their parent's urlPath; emitting one entry per
195
+ * slot page produced duplicate interface keys (TS2300/TS2717 under
196
+ * skipLibCheck:false) and duplicate overloads. The non-slot entry is the
197
+ * canonical one; when only slot entries exist (parent has no page), the
198
+ * lexicographically smallest segmentPath wins so output is independent of
199
+ * slot scan order. A slot's searchParams definition is carried onto the
200
+ * merged entry when the canonical entry has none — never silently dropped.
201
+ *
202
+ * segmentPath-keyed emissions (getSegmentParams, useSegmentParams,
203
+ * TimberSegmentParams) must NOT use this — segmentPaths are already unique
204
+ * and slot entries feed those APIs directly.
205
+ */
206
+ function dedupeRoutesByUrlPath(routes: RouteEntry[]): RouteEntry[] {
207
+ const byPath = new Map<string, RouteEntry>();
208
+ for (const route of routes) {
209
+ const existing = byPath.get(route.urlPath);
210
+ if (!existing) {
211
+ byPath.set(route.urlPath, route);
212
+ continue;
213
+ }
214
+
215
+ let winner = existing;
216
+ let loser = route;
217
+ const routeWins =
218
+ (existing.isSlotRoute && !route.isSlotRoute) ||
219
+ (!!existing.isSlotRoute === !!route.isSlotRoute &&
220
+ route.segmentPath.localeCompare(existing.segmentPath) < 0);
221
+ if (routeWins) {
222
+ winner = route;
223
+ loser = existing;
224
+ }
225
+
226
+ if (!winner.hasSearchParams && loser.hasSearchParams) {
227
+ winner = {
228
+ ...winner,
229
+ hasSearchParams: true,
230
+ searchParamsPagePath: loser.searchParamsPagePath,
231
+ };
232
+ }
233
+ byPath.set(route.urlPath, winner);
234
+ }
235
+ return [...byPath.values()];
236
+ }
237
+
187
238
  /**
188
239
  * Determine the TypeScript type for a segment's param.
189
240
  */
@@ -207,7 +258,7 @@ function paramTypeForSegment(segmentType: string): ParamEntry['type'] {
207
258
  function fileHasExport(filePath: string, exportName: string): boolean {
208
259
  if (!existsSync(filePath)) return false;
209
260
  try {
210
- const content = readFileSync(filePath, 'utf-8');
261
+ const content = readFileCached(filePath);
211
262
  const e = exportName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
212
263
  return (
213
264
  new RegExp(`export\\s+(const|let|var)\\s+${e}\\b`).test(content) ||
@@ -366,10 +417,14 @@ function formatDeclarationFile(
366
417
  lines.push('');
367
418
  }
368
419
 
420
+ // urlPath-keyed emissions use the deduped list (slot pages share their
421
+ // parent's urlPath); segmentPath-keyed emissions below keep all entries.
422
+ const uniqueByUrlPath = dedupeRoutesByUrlPath(routes);
423
+
369
424
  lines.push("declare module '@timber-js/app' {");
370
425
  lines.push(' interface Routes {');
371
426
 
372
- for (const route of routes) {
427
+ for (const route of uniqueByUrlPath) {
373
428
  const paramsType = formatParamsType(route.params, importBase, hasSchema);
374
429
  const searchParamsType = formatSearchParamsType(route, importBase);
375
430
 
@@ -393,7 +448,10 @@ function formatDeclarationFile(
393
448
  lines.push('}');
394
449
  lines.push('');
395
450
 
396
- const pageRoutes = routes.filter((r) => !r.isApiRoute);
451
+ // useQueryStates + typed Link are urlPath-keyed → deduped list.
452
+ const pageRoutes = uniqueByUrlPath.filter((r) => !r.isApiRoute);
453
+ // getSegmentParams / useSegmentParams are segmentPath-keyed → full list,
454
+ // so slot segments keep their own typed overloads.
397
455
  const dynamicRoutes = routes.filter((r) => r.params.length > 0);
398
456
 
399
457
  // Generate @timber-js/app/server augmentation — typed getSegmentParams
@@ -11,9 +11,10 @@
11
11
  * Design doc: 07-routing.md, 10-error-handling.md
12
12
  */
13
13
 
14
- import { readFileSync, existsSync } from 'node:fs';
14
+ import { existsSync } from 'node:fs';
15
15
  import type { RouteTree, SegmentNode } from './types.js';
16
16
  import { swallow } from '../server/logger.js';
17
+ import { readFileCached } from './file-cache.js';
17
18
 
18
19
  // ─── Types ──────────────────────────────────────────────────────────────────
19
20
 
@@ -141,7 +142,7 @@ function checkRouteExports(node: SegmentNode, warnings: ConventionWarning[]): vo
141
142
  if (node.route) {
142
143
  const filePath = node.route.filePath;
143
144
  try {
144
- const source = readFileSync(filePath, 'utf-8');
145
+ const source = readFileCached(filePath);
145
146
  if (!EXPORT_PATTERN.test(source)) {
146
147
  // Check if there's a default export (common mistake)
147
148
  const hasDefaultExport = /export\s+default\b/.test(source);
@@ -265,7 +266,7 @@ function checkFileDefaultExport(
265
266
  warnings: ConventionWarning[]
266
267
  ): void {
267
268
  try {
268
- const source = readFileSync(filePath, 'utf-8');
269
+ const source = readFileCached(filePath);
269
270
  if (!DEFAULT_EXPORT_PATTERN.test(source)) {
270
271
  warnings.push({
271
272
  id: `NO_DEFAULT_EXPORT:${filePath}`,
@@ -0,0 +1,23 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+
3
+ const cache = new Map<string, { mtimeMs: number; size: number; content: string }>();
4
+
5
+ /**
6
+ * Read a file's text content, returning a cached copy when the file
7
+ * hasn't been modified since the last read (checked via mtime + size).
8
+ *
9
+ * Falls back to a fresh read on any stat/read error.
10
+ */
11
+ export function readFileCached(filePath: string): string {
12
+ try {
13
+ const stat = statSync(filePath);
14
+ const entry = cache.get(filePath);
15
+ if (entry && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.content;
16
+ const content = readFileSync(filePath, 'utf-8');
17
+ cache.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, content });
18
+ return content;
19
+ } catch {
20
+ cache.delete(filePath);
21
+ return readFileSync(filePath, 'utf-8');
22
+ }
23
+ }
@@ -32,11 +32,6 @@ export interface InterceptionRewrite {
32
32
  * E.g., "/feed" for a (.)photo/[id] inside /feed/@modal/.
33
33
  */
34
34
  interceptingPrefix: string;
35
- /**
36
- * Segments chain from root → intercepting leaf. Used to build the element
37
- * tree when the interception is active.
38
- */
39
- segmentPath: SegmentNode[];
40
35
  }
41
36
 
42
37
  /**
@@ -94,13 +89,7 @@ function collectFromInterceptingNode(
94
89
  interceptedBase === '/' ? `/${segmentName}` : `${interceptedBase}/${segmentName}`;
95
90
 
96
91
  // Find all leaf pages in the intercepting sub-tree
97
- collectLeavesWithRewrites(
98
- interceptingNode,
99
- interceptedUrlBase,
100
- parentUrlPath,
101
- [...ancestors, interceptingNode],
102
- rewrites
103
- );
92
+ collectLeavesWithRewrites(interceptingNode, interceptedUrlBase, parentUrlPath, rewrites);
104
93
  }
105
94
 
106
95
  /**
@@ -111,14 +100,12 @@ function collectLeavesWithRewrites(
111
100
  node: SegmentNode,
112
101
  interceptedUrlPath: string,
113
102
  interceptingPrefix: string,
114
- segmentPath: SegmentNode[],
115
103
  rewrites: InterceptionRewrite[]
116
104
  ): void {
117
105
  if (node.page) {
118
106
  rewrites.push({
119
107
  interceptedPattern: interceptedUrlPath,
120
108
  interceptingPrefix,
121
- segmentPath: [...segmentPath],
122
109
  });
123
110
  }
124
111
 
@@ -127,13 +114,7 @@ function collectLeavesWithRewrites(
127
114
  child.segmentType === 'group'
128
115
  ? interceptedUrlPath
129
116
  : `${interceptedUrlPath}/${child.segmentName}`;
130
- collectLeavesWithRewrites(
131
- child,
132
- childUrl,
133
- interceptingPrefix,
134
- [...segmentPath, child],
135
- rewrites
136
- );
117
+ collectLeavesWithRewrites(child, childUrl, interceptingPrefix, rewrites);
137
118
  }
138
119
  }
139
120