@timber-js/app 0.2.0-alpha.110 → 0.2.0-alpha.111

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 (156) hide show
  1. package/dist/_chunks/{actions-D6gyxlGQ.js → actions-ILhDUzES.js} +46 -10
  2. package/dist/_chunks/actions-ILhDUzES.js.map +1 -0
  3. package/dist/_chunks/{build-output-helper-5TK2Emwp.js → build-output-helper-DhxiT18k.js} +1 -1
  4. package/dist/_chunks/{build-output-helper-5TK2Emwp.js.map → build-output-helper-DhxiT18k.js.map} +1 -1
  5. package/dist/_chunks/cli-schema-sync-BvvuK8Rp.js +137 -0
  6. package/dist/_chunks/cli-schema-sync-BvvuK8Rp.js.map +1 -0
  7. package/dist/_chunks/cli-schema-sync-i2OEL2-p.js +2 -0
  8. package/dist/_chunks/codec-registry-V7ARyeLN.js +21 -0
  9. package/dist/_chunks/codec-registry-V7ARyeLN.js.map +1 -0
  10. package/dist/_chunks/{define-Dn98bPbb.js → define-8zQ8Vm7x.js} +3 -3
  11. package/dist/_chunks/{define-Dn98bPbb.js.map → define-8zQ8Vm7x.js.map} +1 -1
  12. package/dist/_chunks/{define-BflNsrwD.js → define-a0CCE6Nx.js} +3 -3
  13. package/dist/_chunks/{define-BflNsrwD.js.map → define-a0CCE6Nx.js.map} +1 -1
  14. package/dist/_chunks/{format-CYBGxKtc.js → format-CR6v1pWE.js} +1 -1
  15. package/dist/_chunks/{format-CYBGxKtc.js.map → format-CR6v1pWE.js.map} +1 -1
  16. package/dist/_chunks/{handler-store-B_df5Npe.js → handler-store-iqnt2ghN.js} +1 -1
  17. package/dist/_chunks/{handler-store-B_df5Npe.js.map → handler-store-iqnt2ghN.js.map} +1 -1
  18. package/dist/_chunks/{merge-search-params-Cm_KIWDX.js → merge-search-params-CwxJzBLS.js} +1 -1
  19. package/dist/_chunks/{merge-search-params-Cm_KIWDX.js.map → merge-search-params-CwxJzBLS.js.map} +1 -1
  20. package/dist/_chunks/{navigation-root-2ypaZQDx.js → navigation-root-CY3pRMxF.js} +2 -2
  21. package/dist/_chunks/{navigation-root-2ypaZQDx.js.map → navigation-root-CY3pRMxF.js.map} +1 -1
  22. package/dist/_chunks/{registry-DB93kT5c.js → registry-Dp_hJL4s.js} +1 -1
  23. package/dist/_chunks/{registry-DB93kT5c.js.map → registry-Dp_hJL4s.js.map} +1 -1
  24. package/dist/_chunks/resolve-schema-DO8B0mO9.js +65 -0
  25. package/dist/_chunks/resolve-schema-DO8B0mO9.js.map +1 -0
  26. package/dist/_chunks/{router-ref-TkUdmqtM.js → router-ref-sektkr-t.js} +2 -2
  27. package/dist/_chunks/{router-ref-TkUdmqtM.js.map → router-ref-sektkr-t.js.map} +1 -1
  28. package/dist/_chunks/scanner-DmfoB7iK.js +336 -0
  29. package/dist/_chunks/scanner-DmfoB7iK.js.map +1 -0
  30. package/dist/_chunks/{schema-bridge-C3xl_vfb.js → schema-bridge-eL5vQDSA.js} +1 -1
  31. package/dist/_chunks/{schema-bridge-C3xl_vfb.js.map → schema-bridge-eL5vQDSA.js.map} +1 -1
  32. package/dist/_chunks/{segment-context-Dc-GbSTe.js → segment-context-EsfQpS--.js} +1 -1
  33. package/dist/_chunks/{segment-context-Dc-GbSTe.js.map → segment-context-EsfQpS--.js.map} +1 -1
  34. package/dist/_chunks/{ssr-data-DzuI0bIV.js → ssr-data-BO5OvEvg.js} +1 -1
  35. package/dist/_chunks/{ssr-data-DzuI0bIV.js.map → ssr-data-BO5OvEvg.js.map} +1 -1
  36. package/dist/_chunks/{stale-reload-D6FlD3xn.js → stale-reload-BCPRG0Cv.js} +1 -1
  37. package/dist/_chunks/{stale-reload-D6FlD3xn.js.map → stale-reload-BCPRG0Cv.js.map} +1 -1
  38. package/dist/_chunks/tracing-DvKLzCFa.js.map +1 -1
  39. package/dist/_chunks/{use-query-states-B-cLl8mx.js → use-query-states-D9SBlAfp.js} +2 -2
  40. package/dist/_chunks/{use-query-states-B-cLl8mx.js.map → use-query-states-D9SBlAfp.js.map} +1 -1
  41. package/dist/_chunks/{use-segment-params-07V1TIxL.js → use-segment-params-BSt_O2cb.js} +2 -2
  42. package/dist/_chunks/{use-segment-params-07V1TIxL.js.map → use-segment-params-BSt_O2cb.js.map} +1 -1
  43. package/dist/_chunks/{validation-DfyuMoK2.js → validation-Bm3Je-09.js} +1 -1
  44. package/dist/_chunks/{validation-DfyuMoK2.js.map → validation-Bm3Je-09.js.map} +1 -1
  45. package/dist/_chunks/{walkers-DQn5NNrf.js → walkers-DGTmJT0f.js} +109 -353
  46. package/dist/_chunks/walkers-DGTmJT0f.js.map +1 -0
  47. package/dist/adapters/cloudflare.js +1 -1
  48. package/dist/adapters/nitro.js +1 -1
  49. package/dist/cache/index.js +3 -2
  50. package/dist/cache/index.js.map +1 -1
  51. package/dist/cache/sizeof.d.ts.map +1 -1
  52. package/dist/cli-schema-sync.d.ts +37 -0
  53. package/dist/cli-schema-sync.d.ts.map +1 -0
  54. package/dist/cli.d.ts +8 -1
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +34 -3
  57. package/dist/cli.js.map +1 -1
  58. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  59. package/dist/client/error-boundary.js +5 -3
  60. package/dist/client/error-boundary.js.map +1 -1
  61. package/dist/client/index.js +27 -9
  62. package/dist/client/index.js.map +1 -1
  63. package/dist/client/internal.js +7 -7
  64. package/dist/client/link.d.ts.map +1 -1
  65. package/dist/codec.js +1 -1
  66. package/dist/cookies/index.js +3 -3
  67. package/dist/dev-tools/browser-logs.d.ts.map +1 -1
  68. package/dist/dev-tools/error-page.d.ts.map +1 -1
  69. package/dist/dev-tools/logs.d.ts.map +1 -1
  70. package/dist/dev-tools/overlay.d.ts +1 -1
  71. package/dist/dev-tools/overlay.d.ts.map +1 -1
  72. package/dist/index.d.ts +1 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +98 -16
  75. package/dist/index.js.map +1 -1
  76. package/dist/params/codec-registry.d.ts +26 -0
  77. package/dist/params/codec-registry.d.ts.map +1 -0
  78. package/dist/params/define-schema.d.ts +52 -0
  79. package/dist/params/define-schema.d.ts.map +1 -0
  80. package/dist/params/index.d.ts +5 -0
  81. package/dist/params/index.d.ts.map +1 -0
  82. package/dist/params/index.js +33 -0
  83. package/dist/params/index.js.map +1 -0
  84. package/dist/params/resolve-schema.d.ts +34 -0
  85. package/dist/params/resolve-schema.d.ts.map +1 -0
  86. package/dist/plugins/mdx.d.ts.map +1 -1
  87. package/dist/plugins/routing.d.ts.map +1 -1
  88. package/dist/routing/codegen-shared.d.ts +8 -0
  89. package/dist/routing/codegen-shared.d.ts.map +1 -1
  90. package/dist/routing/codegen-types.d.ts +5 -0
  91. package/dist/routing/codegen-types.d.ts.map +1 -1
  92. package/dist/routing/codegen.d.ts +5 -0
  93. package/dist/routing/codegen.d.ts.map +1 -1
  94. package/dist/routing/index.js +2 -1
  95. package/dist/routing/link-codegen.d.ts +2 -2
  96. package/dist/routing/link-codegen.d.ts.map +1 -1
  97. package/dist/search-params/index.js +3 -3
  98. package/dist/segment-params/index.js +1 -1
  99. package/dist/server/als-registry.d.ts +7 -0
  100. package/dist/server/als-registry.d.ts.map +1 -1
  101. package/dist/server/coalesce-tick.d.ts +40 -0
  102. package/dist/server/coalesce-tick.d.ts.map +1 -0
  103. package/dist/server/fallback-error.d.ts.map +1 -1
  104. package/dist/server/html-injectors.d.ts.map +1 -1
  105. package/dist/server/index.js +2 -2
  106. package/dist/server/internal.js +30 -4
  107. package/dist/server/internal.js.map +1 -1
  108. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  109. package/dist/server/param-coercion.d.ts +12 -2
  110. package/dist/server/param-coercion.d.ts.map +1 -1
  111. package/dist/server/pipeline-phases.d.ts.map +1 -1
  112. package/dist/server/pipeline.d.ts +18 -0
  113. package/dist/server/pipeline.d.ts.map +1 -1
  114. package/dist/server/request-context.d.ts +17 -3
  115. package/dist/server/request-context.d.ts.map +1 -1
  116. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  117. package/dist/server/rsc-entry/index.d.ts +8 -14
  118. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  119. package/package.json +11 -6
  120. package/src/cache/sizeof.ts +3 -1
  121. package/src/cli-schema-sync.ts +209 -0
  122. package/src/cli.ts +49 -2
  123. package/src/client/browser-entry/action-dispatch.ts +15 -5
  124. package/src/client/browser-entry/index.ts +6 -0
  125. package/src/client/error-boundary.tsx +2 -2
  126. package/src/client/link.tsx +25 -1
  127. package/src/dev-tools/browser-logs.ts +5 -2
  128. package/src/dev-tools/error-page.ts +3 -6
  129. package/src/dev-tools/logs.ts +6 -2
  130. package/src/dev-tools/overlay.ts +18 -5
  131. package/src/index.ts +5 -0
  132. package/src/params/codec-registry.ts +34 -0
  133. package/src/params/define-schema.ts +72 -0
  134. package/src/params/index.ts +11 -0
  135. package/src/params/resolve-schema.ts +88 -0
  136. package/src/plugins/dev-server.ts +24 -16
  137. package/src/plugins/mdx.ts +5 -1
  138. package/src/plugins/routing.ts +107 -2
  139. package/src/routing/codegen-shared.ts +11 -0
  140. package/src/routing/codegen-types.ts +5 -0
  141. package/src/routing/codegen.ts +129 -21
  142. package/src/routing/link-codegen.ts +19 -5
  143. package/src/server/als-registry.ts +7 -0
  144. package/src/server/coalesce-tick.ts +75 -0
  145. package/src/server/fallback-error.ts +3 -3
  146. package/src/server/html-injectors.ts +11 -22
  147. package/src/server/node-stream-transforms.ts +6 -16
  148. package/src/server/param-coercion.ts +55 -2
  149. package/src/server/pipeline-phases.ts +8 -0
  150. package/src/server/pipeline.ts +19 -0
  151. package/src/server/request-context.ts +58 -3
  152. package/src/server/rsc-entry/helpers.ts +3 -2
  153. package/src/server/rsc-entry/index.ts +28 -35
  154. package/LICENSE +0 -8
  155. package/dist/_chunks/actions-D6gyxlGQ.js.map +0 -1
  156. package/dist/_chunks/walkers-DQn5NNrf.js.map +0 -1
@@ -1,336 +1,5 @@
1
- import { r as DEFAULT_PAGE_EXTENSIONS, t as classifySegment } from "./segment-classify-CnfLLvir.js";
2
- import { a as isDynamicMetadataExtension, n as classifyMetadataRoute } from "./metadata-routes-Cjmvi3rQ.js";
3
- import { h as swallow } from "./logger-DDmK5skc.js";
4
- import { basename, extname, join, posix, relative } from "node:path";
5
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
- //#region src/routing/scanner.ts
7
- /**
8
- * Route discovery scanner.
9
- *
10
- * Pure function: (appDir, config) → RouteTree
11
- *
12
- * Scans the app/ directory and builds a segment tree recognizing all
13
- * timber.js file conventions. Does NOT handle request matching — this
14
- * is discovery only.
15
- */
16
- /**
17
- * Pattern matching encoded path delimiters that must be rejected during route discovery.
18
- * %2F / %2f (forward slash) and %5C / %5c (backslash) can cause route collisions
19
- * when decoded. See design/13-security.md §"Encoded separators rejected".
20
- */
21
- var ENCODED_SEPARATOR_PATTERN = /%(?:2[fF]|5[cC])/;
22
- /**
23
- * Pattern matching encoded null bytes (%00) that must be rejected.
24
- * See design/13-security.md §"Null bytes rejected".
25
- */
26
- var ENCODED_NULL_PATTERN = /%00/;
27
- /**
28
- * File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)
29
- */
30
- var PAGE_EXT_CONVENTIONS = new Set([
31
- "page",
32
- "layout",
33
- "error",
34
- "default",
35
- "denied"
36
- ]);
37
- /**
38
- * Legacy compat status-code files.
39
- * Maps legacy file name → HTTP status code for the fallback chain.
40
- * See design/10-error-handling.md §"Fallback Chain".
41
- */
42
- var LEGACY_STATUS_FILES = {
43
- "not-found": 404,
44
- "forbidden": 403,
45
- "unauthorized": 401
46
- };
47
- /**
48
- * File convention names that are always .ts/.tsx (never .mdx etc.)
49
- */
50
- var FIXED_CONVENTIONS = new Set([
51
- "middleware",
52
- "access",
53
- "route",
54
- "params"
55
- ]);
56
- /**
57
- * Status-code file patterns:
58
- * - Exact 3-digit codes: 401.tsx, 429.tsx, 503.tsx
59
- * - Category catch-alls: 4xx.tsx, 5xx.tsx
60
- */
61
- var STATUS_CODE_PATTERN = /^(\d{3}|[45]xx)$/;
62
- /**
63
- * Scan the app/ directory and build the route tree.
64
- *
65
- * @param appDir - Absolute path to the app/ directory
66
- * @param config - Scanner configuration
67
- * @returns The complete route tree
68
- */
69
- function scanRoutes(appDir, config = {}) {
70
- const pageExtensions = config.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;
71
- const extSet = new Set(pageExtensions);
72
- const tree = { root: createSegmentNode("", "static", "/") };
73
- const proxyFile = findFixedFile(appDir, "proxy");
74
- if (proxyFile) tree.proxy = proxyFile;
75
- const globalErrorFile = findPageExtFile(appDir, "global-error", extSet);
76
- if (globalErrorFile) tree.globalError = globalErrorFile;
77
- scanSegmentFiles(appDir, tree.root, extSet);
78
- scanChildren(appDir, tree.root, extSet);
79
- validateRouteGroupCollisions(tree.root);
80
- validateDuplicateParamNames(tree.root);
81
- return tree;
82
- }
83
- /**
84
- * Create an empty segment node.
85
- */
86
- function createSegmentNode(segmentName, segmentType, urlPath, paramName, interceptionMarker, interceptedSegmentName) {
87
- return {
88
- segmentName,
89
- segmentType,
90
- urlPath,
91
- paramName,
92
- interceptionMarker,
93
- interceptedSegmentName,
94
- children: [],
95
- slots: {}
96
- };
97
- }
98
- /**
99
- * Compute the URL path for a child segment given its parent's URL path.
100
- * Route groups, slots, and intercepting routes do NOT add URL depth.
101
- */
102
- function computeUrlPath(parentUrlPath, dirName, segmentType) {
103
- if (segmentType === "group" || segmentType === "slot" || segmentType === "intercepting") return parentUrlPath;
104
- return `${parentUrlPath === "/" ? "" : parentUrlPath}/${dirName}`;
105
- }
106
- /**
107
- * Scan a directory for file conventions and populate the segment node.
108
- */
109
- function scanSegmentFiles(dirPath, node, extSet) {
110
- let entries;
111
- try {
112
- entries = readdirSync(dirPath);
113
- } catch (err) {
114
- swallow(err, `scanSegmentFiles: unreadable directory ${dirPath}`, { level: "warn" });
115
- return;
116
- }
117
- for (const entry of entries) {
118
- const fullPath = join(dirPath, entry);
119
- try {
120
- if (statSync(fullPath).isDirectory()) continue;
121
- } catch {
122
- continue;
123
- }
124
- const ext = extname(entry).slice(1);
125
- const name = basename(entry, `.${ext}`);
126
- if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {
127
- const file = {
128
- filePath: fullPath,
129
- extension: ext
130
- };
131
- switch (name) {
132
- case "page":
133
- node.page = file;
134
- break;
135
- case "layout":
136
- node.layout = file;
137
- break;
138
- case "error":
139
- node.error = file;
140
- break;
141
- case "default":
142
- node.default = file;
143
- break;
144
- case "denied":
145
- node.denied = file;
146
- break;
147
- }
148
- continue;
149
- }
150
- if (FIXED_CONVENTIONS.has(name) && /\.?[jt]sx?$/.test(ext)) {
151
- const file = {
152
- filePath: fullPath,
153
- extension: ext
154
- };
155
- switch (name) {
156
- case "middleware":
157
- node.middleware = file;
158
- break;
159
- case "access":
160
- node.access = file;
161
- break;
162
- case "route":
163
- node.route = file;
164
- break;
165
- case "params":
166
- node.params = file;
167
- break;
168
- }
169
- continue;
170
- }
171
- if (STATUS_CODE_PATTERN.test(name) && ext === "json") {
172
- if (!node.jsonStatusFiles) node.jsonStatusFiles = {};
173
- node.jsonStatusFiles[name] = {
174
- filePath: fullPath,
175
- extension: ext
176
- };
177
- continue;
178
- }
179
- if (STATUS_CODE_PATTERN.test(name) && extSet.has(ext)) {
180
- if (!node.statusFiles) node.statusFiles = {};
181
- node.statusFiles[name] = {
182
- filePath: fullPath,
183
- extension: ext
184
- };
185
- continue;
186
- }
187
- if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {
188
- if (!node.legacyStatusFiles) node.legacyStatusFiles = {};
189
- node.legacyStatusFiles[name] = {
190
- filePath: fullPath,
191
- extension: ext
192
- };
193
- continue;
194
- }
195
- if (classifyMetadataRoute(entry)) {
196
- if (!node.metadataRoutes) node.metadataRoutes = {};
197
- const existing = node.metadataRoutes[name];
198
- if (existing) {
199
- const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);
200
- if (isDynamicMetadataExtension(name, ext) || !existingIsDynamic) node.metadataRoutes[name] = {
201
- filePath: fullPath,
202
- extension: ext
203
- };
204
- } else node.metadataRoutes[name] = {
205
- filePath: fullPath,
206
- extension: ext
207
- };
208
- }
209
- }
210
- if (node.route && node.page) throw new Error(`Build error: route.ts and page.* cannot coexist in the same segment.\n route.ts: ${node.route.filePath}\n page: ${node.page.filePath}\nA URL is either an API endpoint or a rendered page, not both.`);
211
- }
212
- /**
213
- * Recursively scan child directories and build the segment tree.
214
- */
215
- function scanChildren(dirPath, parentNode, extSet) {
216
- let entries;
217
- try {
218
- entries = readdirSync(dirPath);
219
- } catch (err) {
220
- swallow(err, `scanChildren: unreadable directory ${dirPath}`, { level: "warn" });
221
- return;
222
- }
223
- for (const entry of entries) {
224
- const fullPath = join(dirPath, entry);
225
- try {
226
- if (!statSync(fullPath).isDirectory()) continue;
227
- } catch {
228
- continue;
229
- }
230
- if (ENCODED_SEPARATOR_PATTERN.test(entry)) throw new Error(`Build error: directory name contains an encoded path delimiter (%%2F or %%5C).\n Directory: ${fullPath}\nEncoded separators in directory names cause route collisions when decoded. Rename the directory to remove the encoded delimiter.`);
231
- if (ENCODED_NULL_PATTERN.test(entry)) throw new Error(`Build error: directory name contains an encoded null byte (%%00).\n Directory: ${fullPath}\nEncoded null bytes in directory names are not allowed. Rename the directory to remove the null byte encoding.`);
232
- const { type, paramName, interceptionMarker, interceptedSegmentName } = classifySegment(entry);
233
- if (type === "private") continue;
234
- const childNode = createSegmentNode(entry, type, computeUrlPath(parentNode.urlPath, entry, type), paramName, interceptionMarker, interceptedSegmentName);
235
- scanSegmentFiles(fullPath, childNode, extSet);
236
- scanChildren(fullPath, childNode, extSet);
237
- if (type === "slot") {
238
- const slotName = entry.slice(1);
239
- parentNode.slots[slotName] = childNode;
240
- } else parentNode.children.push(childNode);
241
- }
242
- }
243
- /**
244
- * Validate that route groups don't produce conflicting pages/routes at the same URL path.
245
- *
246
- * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim
247
- * /login — the scanner must detect and reject this at build time.
248
- *
249
- * Parallel slots are excluded from collision detection — they intentionally coexist at
250
- * the same URL path as their parent (that's the whole point of parallel routes).
251
- */
252
- function validateRouteGroupCollisions(root) {
253
- collectRoutableLeaves(root, /* @__PURE__ */ new Map(), "", false);
254
- }
255
- /**
256
- * Walk the segment tree and collect all routable leaves (page or route files),
257
- * throwing on collision. Slots are tracked in their own collision space since
258
- * they are parallel routes that intentionally share URL paths with their parent.
259
- */
260
- function collectRoutableLeaves(node, seen, segmentPath, insideSlot) {
261
- const currentPath = segmentPath ? `${segmentPath}/${node.segmentName}` : node.segmentName || "(root)";
262
- if (!insideSlot) {
263
- const routableFile = node.page ?? node.route;
264
- if (routableFile) {
265
- const existing = seen.get(node.urlPath);
266
- if (existing) throw new Error(`Build error: route collision — multiple route groups produce a page/route at the same URL path.\n URL path: ${node.urlPath}\n File 1: ${existing.filePath} (via ${existing.segmentPath})\n File 2: ${routableFile.filePath} (via ${currentPath})\nEach URL path must map to exactly one page or route handler. Rename or move one of the conflicting files.`);
267
- seen.set(node.urlPath, {
268
- filePath: routableFile.filePath,
269
- segmentPath: currentPath
270
- });
271
- }
272
- }
273
- for (const child of node.children) collectRoutableLeaves(child, seen, currentPath, insideSlot);
274
- for (const slotNode of Object.values(node.slots)) collectRoutableLeaves(slotNode, seen, currentPath, true);
275
- }
276
- /**
277
- * Validate that no route chain contains duplicate dynamic param names.
278
- *
279
- * Example violation:
280
- * app/[id]/items/[id]/page.tsx — 'id' appears twice in the ancestor chain.
281
- *
282
- * Route groups are transparent — params accumulate through them.
283
- * Slots are independent — duplicate detection does NOT cross slot boundaries.
284
- *
285
- * See design/07-routing.md §"Duplicate Param Name Detection"
286
- */
287
- function validateDuplicateParamNames(root) {
288
- walkForDuplicateParams(root, /* @__PURE__ */ new Map());
289
- }
290
- /**
291
- * Recursively walk the segment tree, tracking seen param names → segment paths.
292
- * Throws on the first duplicate found.
293
- */
294
- function walkForDuplicateParams(node, seen) {
295
- if (node.paramName) {
296
- const existing = seen.get(node.paramName);
297
- if (existing) throw new Error(`[timber] Duplicate param name '${node.paramName}' in route chain.\n First defined at: ${existing}\n Duplicate at: ${node.urlPath || "/"}\n Rename one of the segments to avoid ambiguity.`);
298
- seen = new Map(seen);
299
- seen.set(node.paramName, node.urlPath || "/");
300
- }
301
- for (const child of node.children) walkForDuplicateParams(child, seen);
302
- for (const slotNode of Object.values(node.slots)) walkForDuplicateParams(slotNode, new Map(seen));
303
- }
304
- /**
305
- * Find a fixed-extension file (proxy.ts) in a directory.
306
- */
307
- function findFixedFile(dirPath, name) {
308
- for (const ext of ["ts", "tsx"]) {
309
- const fullPath = join(dirPath, `${name}.${ext}`);
310
- try {
311
- if (statSync(fullPath).isFile()) return {
312
- filePath: fullPath,
313
- extension: ext
314
- };
315
- } catch {}
316
- }
317
- }
318
- /**
319
- * Find a file using the configured page extensions (tsx, ts, jsx, js, mdx, etc.).
320
- * Used for app-root conventions like global-error that aren't per-segment.
321
- */
322
- function findPageExtFile(dirPath, name, extSet) {
323
- for (const ext of extSet) {
324
- const fullPath = join(dirPath, `${name}.${ext}`);
325
- try {
326
- if (statSync(fullPath).isFile()) return {
327
- filePath: fullPath,
328
- extension: ext
329
- };
330
- } catch {}
331
- }
332
- }
333
- //#endregion
1
+ import { join, posix, relative } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
334
3
  //#region src/routing/codegen-shared.ts
335
4
  /**
336
5
  * Shared codegen helpers — import-path computation, codec chain type
@@ -395,6 +64,16 @@ function buildCodecChainType(p, importBase, fallback) {
395
64
  return inner;
396
65
  }
397
66
  /**
67
+ * Build a TypeScript type expression for a single param using the global
68
+ * schema's InferCodecType. Falls back to `fallback` when schema doesn't
69
+ * declare this param.
70
+ *
71
+ * Generates: `InferCodecType<typeof schema.segmentParams['[paramName]']>`
72
+ */
73
+ function buildSchemaParamType(p) {
74
+ return `InferCodecType<typeof schema.segmentParams[${JSON.stringify(p.bracketName)}]>`;
75
+ }
76
+ /**
398
77
  * Format the searchParams type for a route entry.
399
78
  *
400
79
  * When a page.tsx (or params.ts) exports searchParams, we reference its
@@ -435,10 +114,11 @@ function buildResolvedPattern(route) {
435
114
  * When the segment's params chain (TIM-834) declares a typed codec, the
436
115
  * inferred type from the codec wins via a nested conditional.
437
116
  */
438
- function formatLinkParamsType(params, importBase) {
117
+ function formatLinkParamsType(params, importBase, hasSchema) {
439
118
  if (params.length === 0) return "{}";
440
119
  return `{ ${params.map((p) => {
441
- const codecType = buildCodecChainType(p, importBase, p.type === "string" ? "string | number" : p.type);
120
+ const fallback = p.type === "string" ? "string | number" : p.type;
121
+ const codecType = hasSchema ? buildSchemaParamType(p) : buildCodecChainType(p, importBase, fallback);
442
122
  return `${p.name}: ${codecType}`;
443
123
  }).join("; ")} }`;
444
124
  }
@@ -505,13 +185,13 @@ function formatLinkCatchAllOverloads() {
505
185
  * so the resolved-template-vs-pattern ordering reduces to placing the
506
186
  * pattern variant before the resolved-template variant inside the union.
507
187
  */
508
- function formatTypedLinkOverloads(routes, importBase) {
188
+ function formatTypedLinkOverloads(routes, importBase, hasSchema) {
509
189
  const lines = [];
510
190
  const baseProps = LINK_BASE_PROPS_TYPE;
511
191
  const variants = [];
512
192
  for (const route of routes) {
513
193
  const hasDynamicParams = route.params.length > 0;
514
- const paramsProp = hasDynamicParams ? `segmentParams: ${formatLinkParamsType(route.params, importBase)}` : "segmentParams?: Record<string, unknown>";
194
+ const paramsProp = hasDynamicParams ? `segmentParams: ${formatLinkParamsType(route.params, importBase, hasSchema)}` : "segmentParams?: Record<string, unknown>";
515
195
  const searchParamsType = route.hasSearchParams ? formatSearchParamsType(route, importBase) : null;
516
196
  const patternSearchParamsProp = searchParamsType ? `searchParams?: Partial<${searchParamsType}>` : "searchParams?: Record<string, unknown>";
517
197
  variants.push(`${baseProps} & { href: '${route.urlPath}'; ${paramsProp}; ${patternSearchParamsProp} }`);
@@ -553,10 +233,25 @@ function formatTypedLinkOverloads(routes, importBase) {
553
233
  * interface that maps every route path to its params and searchParams shape.
554
234
  */
555
235
  function generateRouteMap(tree, options = {}) {
236
+ const hasSchema = options.hasSchema ?? detectSchema(options.appDir);
556
237
  const routes = [];
557
- collectRoutes(tree.root, [], [], routes);
238
+ collectRoutes(tree.root, [], [], routes, hasSchema);
558
239
  routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
559
- return formatDeclarationFile(routes, options.outputDir ?? options.appDir);
240
+ return formatDeclarationFile(routes, options.outputDir ?? options.appDir, hasSchema);
241
+ }
242
+ /** Schema file extensions to check, in priority order. */
243
+ var SCHEMA_EXTENSIONS = [
244
+ ".ts",
245
+ ".tsx",
246
+ ".js",
247
+ ".jsx"
248
+ ];
249
+ /**
250
+ * Detect whether app/schema.{ts,tsx,js,jsx} exists.
251
+ */
252
+ function detectSchema(appDir) {
253
+ if (!appDir) return false;
254
+ return SCHEMA_EXTENSIONS.some((ext) => existsSync(join(appDir, `schema${ext}`)));
560
255
  }
561
256
  /**
562
257
  * Recursively walk the segment tree and collect route entries.
@@ -564,13 +259,14 @@ function generateRouteMap(tree, options = {}) {
564
259
  * A route entry is created for any segment that has a `page` or `route` file.
565
260
  * Params accumulate from ancestor dynamic segments.
566
261
  */
567
- function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
262
+ function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes, hasSchema) {
568
263
  const ownParamsFile = node.params && fileHasExport(node.params.filePath, "segmentParams") ? node.params.filePath : void 0;
569
264
  const params = [...ancestorParams];
570
265
  if (node.paramName) {
571
266
  const legacyFallback = ownParamsFile ? void 0 : findLegacyParamsExport(node);
572
267
  params.push({
573
268
  name: node.paramName,
269
+ bracketName: node.segmentName,
574
270
  type: paramTypeForSegment(node.segmentType),
575
271
  legacyCodecFilePath: legacyFallback
576
272
  });
@@ -584,6 +280,7 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
584
280
  const codecFilePaths = leafFirstChain.length > 0 ? leafFirstChain : p.legacyCodecFilePath ? [p.legacyCodecFilePath] : void 0;
585
281
  return {
586
282
  name: p.name,
283
+ bracketName: p.bracketName,
587
284
  type: p.type,
588
285
  codecFilePaths
589
286
  };
@@ -592,7 +289,8 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
592
289
  urlPath: node.urlPath,
593
290
  params: resolvedParams,
594
291
  hasSearchParams: false,
595
- isApiRoute
292
+ isApiRoute,
293
+ hasSchema
596
294
  };
597
295
  if (isPage) {
598
296
  if (node.params && fileHasExport(node.params.filePath, "searchParams")) {
@@ -605,8 +303,8 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
605
303
  }
606
304
  routes.push(entry);
607
305
  }
608
- for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, routes);
609
- for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, routes);
306
+ for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, routes, hasSchema);
307
+ for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, routes, hasSchema);
610
308
  }
611
309
  /**
612
310
  * Determine the TypeScript type for a segment's param.
@@ -646,21 +344,69 @@ function findLegacyParamsExport(node) {
646
344
  if (node.page && fileHasExport(node.page.filePath, "segmentParams")) return node.page.filePath;
647
345
  }
648
346
  /**
347
+ * Emit the schema-based type exports: TimberSegmentParams, TimberRoute,
348
+ * AllSegmentParams, RoutesWithParams.
349
+ *
350
+ * These types use InferCodecType references so they stay in sync with
351
+ * the schema without hardcoding resolved types.
352
+ *
353
+ * Design doc: design/41-global-params.md §Codegen Output
354
+ */
355
+ function formatSchemaTypes(routes) {
356
+ const lines = [];
357
+ lines.push("/** Union of all valid route patterns. */");
358
+ lines.push("export type TimberRoute = keyof TimberSegmentParams;");
359
+ lines.push("");
360
+ lines.push("/** Maps each route pattern to its accumulated segment params. */");
361
+ lines.push("export interface TimberSegmentParams {");
362
+ for (const route of routes) if (route.params.length === 0) lines.push(` '${route.urlPath}': {};`);
363
+ else {
364
+ lines.push(` '${route.urlPath}': {`);
365
+ for (const p of route.params) lines.push(` ${p.name}: ${buildSchemaParamType(p)};`);
366
+ lines.push(" };");
367
+ }
368
+ lines.push("}");
369
+ lines.push("");
370
+ lines.push("/** All segment params (union of all route params). */");
371
+ lines.push("export type AllSegmentParams = {");
372
+ lines.push(" [K in keyof TimberSegmentParams as TimberSegmentParams[K] extends Record<string, never>");
373
+ lines.push(" ? never");
374
+ lines.push(" : K]: TimberSegmentParams[K];");
375
+ lines.push("} extends infer U");
376
+ lines.push(" ? { [K in keyof U[keyof U]]: U[keyof U][K] }");
377
+ lines.push(" : never;");
378
+ lines.push("");
379
+ lines.push("/** Filters TimberRoute to routes whose accumulated params include all listed keys. */");
380
+ lines.push("export type RoutesWithParams<K extends keyof AllSegmentParams> = {");
381
+ lines.push(" [R in TimberRoute]: K extends keyof TimberSegmentParams[R] ? R : never;");
382
+ lines.push("}[TimberRoute];");
383
+ return lines;
384
+ }
385
+ /**
649
386
  * Format the collected routes into a TypeScript declaration file.
650
387
  */
651
- function formatDeclarationFile(routes, importBase) {
388
+ function formatDeclarationFile(routes, importBase, hasSchema) {
652
389
  const lines = [];
653
390
  lines.push("// This file is auto-generated by timber.js route map codegen.");
654
391
  lines.push("// Do not edit manually. Regenerated on build and in dev mode.");
655
392
  lines.push("");
656
393
  lines.push("export {};");
657
394
  lines.push("");
658
- lines.push(emitResolveSegmentFieldHelper());
659
- lines.push("");
395
+ if (hasSchema) {
396
+ lines.push("import type schema from '../app/schema';");
397
+ lines.push("import type { InferCodecType } from '@timber-js/app/params';");
398
+ lines.push("");
399
+ lines.push(...formatSchemaTypes(routes));
400
+ lines.push("");
401
+ }
402
+ if (!hasSchema) {
403
+ lines.push(emitResolveSegmentFieldHelper());
404
+ lines.push("");
405
+ }
660
406
  lines.push("declare module '@timber-js/app' {");
661
407
  lines.push(" interface Routes {");
662
408
  for (const route of routes) {
663
- const paramsType = formatParamsType(route.params, importBase);
409
+ const paramsType = formatParamsType(route.params, importBase, hasSchema);
664
410
  const searchParamsType = formatSearchParamsType(route, importBase);
665
411
  lines.push(` '${route.urlPath}': {`);
666
412
  lines.push(` segmentParams: ${paramsType}`);
@@ -672,13 +418,23 @@ function formatDeclarationFile(routes, importBase) {
672
418
  lines.push("");
673
419
  const pageRoutes = routes.filter((r) => !r.isApiRoute);
674
420
  const dynamicRoutes = routes.filter((r) => r.params.length > 0);
421
+ if (dynamicRoutes.length > 0) {
422
+ lines.push("declare module '@timber-js/app/server' {");
423
+ for (const route of dynamicRoutes) {
424
+ const paramsType = formatParamsType(route.params, importBase, hasSchema);
425
+ lines.push(` export function getSegmentParams(route: '${route.urlPath}'): ${paramsType}`);
426
+ }
427
+ lines.push(" export function getSegmentParams(): Record<string, string | string[]>");
428
+ lines.push("}");
429
+ lines.push("");
430
+ }
675
431
  if (dynamicRoutes.length > 0 || pageRoutes.length > 0) {
676
432
  lines.push("declare module '@timber-js/app/client' {");
677
433
  lines.push(" import type { SearchParamsDefinition, SetParams, QueryStatesOptions, SearchParamCodec } from '@timber-js/app/search-params'");
678
434
  lines.push("");
679
435
  if (dynamicRoutes.length > 0) {
680
436
  for (const route of dynamicRoutes) {
681
- const paramsType = formatParamsType(route.params, importBase);
437
+ const paramsType = formatParamsType(route.params, importBase, hasSchema);
682
438
  lines.push(` export function useSegmentParams(route: '${route.urlPath}'): ${paramsType}`);
683
439
  }
684
440
  lines.push(" export function useSegmentParams(): Record<string, string | string[]>");
@@ -690,7 +446,7 @@ function formatDeclarationFile(routes, importBase) {
690
446
  }
691
447
  if (pageRoutes.length > 0) {
692
448
  lines.push(" // Typed Link overloads — per-route (block 1 / emitted first)");
693
- lines.push(...formatTypedLinkOverloads(pageRoutes, importBase));
449
+ lines.push(...formatTypedLinkOverloads(pageRoutes, importBase, hasSchema));
694
450
  lines.push("");
695
451
  }
696
452
  lines.push("}");
@@ -707,10 +463,10 @@ function formatDeclarationFile(routes, importBase) {
707
463
  /**
708
464
  * Format the params type for a route entry.
709
465
  */
710
- function formatParamsType(params, importBase) {
466
+ function formatParamsType(params, importBase, hasSchema) {
711
467
  if (params.length === 0) return "{}";
712
468
  return `{ ${params.map((p) => {
713
- const codecType = buildCodecChainType(p, importBase, p.type);
469
+ const codecType = hasSchema ? buildSchemaParamType(p) : buildCodecChainType(p, importBase, p.type);
714
470
  return `${p.name}: ${codecType}`;
715
471
  }).join("; ")} }`;
716
472
  }
@@ -845,6 +601,6 @@ function walk(node, chain, result, includeSlots) {
845
601
  if (includeSlots) for (const slotNode of Object.values(node.slots)) walk(slotNode, currentChain, result, includeSlots);
846
602
  }
847
603
  //#endregion
848
- export { scanRoutes as i, collectInterceptionRewrites as n, generateRouteMap as r, collectLeafRoutes as t };
604
+ export { collectInterceptionRewrites as n, generateRouteMap as r, collectLeafRoutes as t };
849
605
 
850
- //# sourceMappingURL=walkers-DQn5NNrf.js.map
606
+ //# sourceMappingURL=walkers-DGTmJT0f.js.map