@timber-js/app 0.2.0-alpha.34 → 0.2.0-alpha.35

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 (230) hide show
  1. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  2. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  3. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  4. package/dist/_chunks/{debug-B3Gypr3D.js → debug-ECi_61pb.js} +1 -1
  5. package/dist/_chunks/{debug-B3Gypr3D.js.map → debug-ECi_61pb.js.map} +1 -1
  6. package/dist/_chunks/define-cookie-w5GWm_bL.js +93 -0
  7. package/dist/_chunks/define-cookie-w5GWm_bL.js.map +1 -0
  8. package/dist/_chunks/error-boundary-TYEQJZ1-.js +211 -0
  9. package/dist/_chunks/error-boundary-TYEQJZ1-.js.map +1 -0
  10. package/dist/_chunks/{format-RyoGQL74.js → format-cX7wzEp2.js} +2 -2
  11. package/dist/_chunks/{format-RyoGQL74.js.map → format-cX7wzEp2.js.map} +1 -1
  12. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  13. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  14. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  16. package/dist/_chunks/{request-context-BQUC8PHn.js → request-context-CZz_T0Bc.js} +40 -71
  17. package/dist/_chunks/request-context-CZz_T0Bc.js.map +1 -0
  18. package/dist/_chunks/segment-context-Dpq2XOKg.js +34 -0
  19. package/dist/_chunks/segment-context-Dpq2XOKg.js.map +1 -0
  20. package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
  21. package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
  22. package/dist/_chunks/{tracing-CemImE6h.js → tracing-BPyIzIdu.js} +2 -2
  23. package/dist/_chunks/{tracing-CemImE6h.js.map → tracing-BPyIzIdu.js.map} +1 -1
  24. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
  25. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
  26. package/dist/_chunks/wrappers-C1SN725w.js +331 -0
  27. package/dist/_chunks/wrappers-C1SN725w.js.map +1 -0
  28. package/dist/cache/index.js +1 -1
  29. package/dist/client/error-boundary.d.ts +10 -1
  30. package/dist/client/error-boundary.d.ts.map +1 -1
  31. package/dist/client/error-boundary.js +1 -125
  32. package/dist/client/index.d.ts +2 -2
  33. package/dist/client/index.d.ts.map +1 -1
  34. package/dist/client/index.js +193 -90
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/link.d.ts +8 -8
  37. package/dist/client/link.d.ts.map +1 -1
  38. package/dist/client/navigation-context.d.ts +2 -2
  39. package/dist/client/router.d.ts +25 -3
  40. package/dist/client/router.d.ts.map +1 -1
  41. package/dist/client/rsc-fetch.d.ts +23 -2
  42. package/dist/client/rsc-fetch.d.ts.map +1 -1
  43. package/dist/client/segment-cache.d.ts +1 -1
  44. package/dist/client/segment-cache.d.ts.map +1 -1
  45. package/dist/client/stale-reload.d.ts +15 -0
  46. package/dist/client/stale-reload.d.ts.map +1 -1
  47. package/dist/client/top-loader.d.ts +1 -1
  48. package/dist/client/top-loader.d.ts.map +1 -1
  49. package/dist/client/use-params.d.ts +2 -2
  50. package/dist/client/use-params.d.ts.map +1 -1
  51. package/dist/client/use-query-states.d.ts +1 -1
  52. package/dist/codec.d.ts +21 -0
  53. package/dist/codec.d.ts.map +1 -0
  54. package/dist/cookies/define-cookie.d.ts +33 -12
  55. package/dist/cookies/define-cookie.d.ts.map +1 -1
  56. package/dist/cookies/index.js +1 -81
  57. package/dist/index.d.ts +87 -12
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +346 -210
  60. package/dist/index.js.map +1 -1
  61. package/dist/params/define.d.ts +76 -0
  62. package/dist/params/define.d.ts.map +1 -0
  63. package/dist/params/index.d.ts +8 -0
  64. package/dist/params/index.d.ts.map +1 -0
  65. package/dist/params/index.js +104 -0
  66. package/dist/params/index.js.map +1 -0
  67. package/dist/plugins/adapter-build.d.ts.map +1 -1
  68. package/dist/plugins/build-manifest.d.ts.map +1 -1
  69. package/dist/plugins/client-chunks.d.ts +32 -0
  70. package/dist/plugins/client-chunks.d.ts.map +1 -0
  71. package/dist/plugins/entries.d.ts.map +1 -1
  72. package/dist/plugins/routing.d.ts.map +1 -1
  73. package/dist/plugins/server-bundle.d.ts.map +1 -1
  74. package/dist/plugins/static-build.d.ts.map +1 -1
  75. package/dist/routing/codegen.d.ts +2 -2
  76. package/dist/routing/codegen.d.ts.map +1 -1
  77. package/dist/routing/index.js +1 -1
  78. package/dist/routing/scanner.d.ts.map +1 -1
  79. package/dist/routing/status-file-lint.d.ts +2 -1
  80. package/dist/routing/status-file-lint.d.ts.map +1 -1
  81. package/dist/routing/types.d.ts +6 -4
  82. package/dist/routing/types.d.ts.map +1 -1
  83. package/dist/rsc-runtime/rsc.d.ts +1 -1
  84. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  85. package/dist/search-params/codecs.d.ts +1 -1
  86. package/dist/search-params/define.d.ts +153 -0
  87. package/dist/search-params/define.d.ts.map +1 -0
  88. package/dist/search-params/index.d.ts +4 -5
  89. package/dist/search-params/index.d.ts.map +1 -1
  90. package/dist/search-params/index.js +3 -474
  91. package/dist/search-params/registry.d.ts +1 -1
  92. package/dist/search-params/wrappers.d.ts +53 -0
  93. package/dist/search-params/wrappers.d.ts.map +1 -0
  94. package/dist/server/access-gate.d.ts +4 -0
  95. package/dist/server/access-gate.d.ts.map +1 -1
  96. package/dist/server/action-encryption.d.ts +76 -0
  97. package/dist/server/action-encryption.d.ts.map +1 -0
  98. package/dist/server/action-handler.d.ts.map +1 -1
  99. package/dist/server/als-registry.d.ts +4 -4
  100. package/dist/server/als-registry.d.ts.map +1 -1
  101. package/dist/server/build-manifest.d.ts +2 -2
  102. package/dist/server/early-hints.d.ts +13 -5
  103. package/dist/server/early-hints.d.ts.map +1 -1
  104. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  105. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  106. package/dist/server/flight-injection-state.d.ts +78 -0
  107. package/dist/server/flight-injection-state.d.ts.map +1 -0
  108. package/dist/server/form-data.d.ts +29 -0
  109. package/dist/server/form-data.d.ts.map +1 -1
  110. package/dist/server/html-injectors.d.ts.map +1 -1
  111. package/dist/server/index.d.ts +1 -1
  112. package/dist/server/index.d.ts.map +1 -1
  113. package/dist/server/index.js +1819 -1629
  114. package/dist/server/index.js.map +1 -1
  115. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  116. package/dist/server/pipeline.d.ts.map +1 -1
  117. package/dist/server/request-context.d.ts +28 -40
  118. package/dist/server/request-context.d.ts.map +1 -1
  119. package/dist/server/route-element-builder.d.ts +7 -0
  120. package/dist/server/route-element-builder.d.ts.map +1 -1
  121. package/dist/server/route-matcher.d.ts +2 -2
  122. package/dist/server/route-matcher.d.ts.map +1 -1
  123. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  124. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  125. package/dist/server/slot-resolver.d.ts.map +1 -1
  126. package/dist/server/ssr-entry.d.ts.map +1 -1
  127. package/dist/server/ssr-render.d.ts +3 -0
  128. package/dist/server/ssr-render.d.ts.map +1 -1
  129. package/dist/server/tree-builder.d.ts +12 -8
  130. package/dist/server/tree-builder.d.ts.map +1 -1
  131. package/dist/server/types.d.ts +1 -3
  132. package/dist/server/types.d.ts.map +1 -1
  133. package/dist/server/version-skew.d.ts +61 -0
  134. package/dist/server/version-skew.d.ts.map +1 -0
  135. package/dist/shims/navigation-client.d.ts +1 -1
  136. package/dist/shims/navigation-client.d.ts.map +1 -1
  137. package/dist/shims/navigation.d.ts +1 -1
  138. package/dist/shims/navigation.d.ts.map +1 -1
  139. package/dist/utils/state-machine.d.ts +80 -0
  140. package/dist/utils/state-machine.d.ts.map +1 -0
  141. package/package.json +12 -8
  142. package/src/client/browser-entry.ts +55 -13
  143. package/src/client/error-boundary.tsx +18 -1
  144. package/src/client/index.ts +9 -1
  145. package/src/client/link.tsx +9 -9
  146. package/src/client/navigation-context.ts +2 -2
  147. package/src/client/router.ts +102 -55
  148. package/src/client/rsc-fetch.ts +63 -2
  149. package/src/client/segment-cache.ts +1 -1
  150. package/src/client/stale-reload.ts +28 -0
  151. package/src/client/top-loader.tsx +2 -2
  152. package/src/client/use-params.ts +3 -3
  153. package/src/client/use-query-states.ts +1 -1
  154. package/src/codec.ts +21 -0
  155. package/src/cookies/define-cookie.ts +69 -18
  156. package/src/index.ts +255 -65
  157. package/src/params/define.ts +260 -0
  158. package/src/params/index.ts +28 -0
  159. package/src/plugins/adapter-build.ts +6 -0
  160. package/src/plugins/build-manifest.ts +11 -0
  161. package/src/plugins/client-chunks.ts +65 -0
  162. package/src/plugins/entries.ts +3 -6
  163. package/src/plugins/routing.ts +40 -14
  164. package/src/plugins/server-bundle.ts +32 -1
  165. package/src/plugins/shims.ts +1 -1
  166. package/src/plugins/static-build.ts +8 -4
  167. package/src/routing/codegen.ts +109 -88
  168. package/src/routing/scanner.ts +55 -6
  169. package/src/routing/status-file-lint.ts +2 -1
  170. package/src/routing/types.ts +7 -4
  171. package/src/rsc-runtime/rsc.ts +2 -0
  172. package/src/search-params/codecs.ts +1 -1
  173. package/src/search-params/define.ts +504 -0
  174. package/src/search-params/index.ts +12 -18
  175. package/src/search-params/registry.ts +1 -1
  176. package/src/search-params/wrappers.ts +85 -0
  177. package/src/server/access-gate.tsx +38 -8
  178. package/src/server/action-encryption.ts +144 -0
  179. package/src/server/action-handler.ts +16 -0
  180. package/src/server/als-registry.ts +4 -4
  181. package/src/server/build-manifest.ts +4 -4
  182. package/src/server/early-hints.ts +36 -15
  183. package/src/server/error-boundary-wrapper.ts +57 -14
  184. package/src/server/flight-injection-state.ts +152 -0
  185. package/src/server/form-data.ts +76 -0
  186. package/src/server/html-injectors.ts +42 -26
  187. package/src/server/index.ts +2 -4
  188. package/src/server/node-stream-transforms.ts +68 -41
  189. package/src/server/pipeline.ts +98 -26
  190. package/src/server/request-context.ts +49 -124
  191. package/src/server/route-element-builder.ts +102 -99
  192. package/src/server/route-matcher.ts +2 -2
  193. package/src/server/rsc-entry/error-renderer.ts +3 -2
  194. package/src/server/rsc-entry/index.ts +26 -11
  195. package/src/server/rsc-entry/rsc-payload.ts +2 -2
  196. package/src/server/rsc-entry/ssr-renderer.ts +4 -4
  197. package/src/server/slot-resolver.ts +204 -206
  198. package/src/server/ssr-entry.ts +3 -1
  199. package/src/server/ssr-render.ts +3 -0
  200. package/src/server/tree-builder.ts +84 -48
  201. package/src/server/types.ts +1 -3
  202. package/src/server/version-skew.ts +104 -0
  203. package/src/shims/navigation-client.ts +1 -1
  204. package/src/shims/navigation.ts +1 -1
  205. package/src/utils/state-machine.ts +111 -0
  206. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  207. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  208. package/dist/_chunks/request-context-BQUC8PHn.js.map +0 -1
  209. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  210. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  211. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  212. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  213. package/dist/client/error-boundary.js.map +0 -1
  214. package/dist/cookies/index.js.map +0 -1
  215. package/dist/plugins/dynamic-transform.d.ts +0 -72
  216. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  217. package/dist/search-params/analyze.d.ts +0 -54
  218. package/dist/search-params/analyze.d.ts.map +0 -1
  219. package/dist/search-params/builtin-codecs.d.ts +0 -105
  220. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  221. package/dist/search-params/create.d.ts +0 -106
  222. package/dist/search-params/create.d.ts.map +0 -1
  223. package/dist/search-params/index.js.map +0 -1
  224. package/dist/server/prerender.d.ts +0 -77
  225. package/dist/server/prerender.d.ts.map +0 -1
  226. package/src/plugins/dynamic-transform.ts +0 -161
  227. package/src/search-params/analyze.ts +0 -192
  228. package/src/search-params/builtin-codecs.ts +0 -228
  229. package/src/search-params/create.ts +0 -321
  230. package/src/server/prerender.ts +0 -139
@@ -28,12 +28,24 @@ import { DenySignal, RedirectSignal } from './primitives.js';
28
28
  import { AccessGate } from './access-gate.js';
29
29
  import { resolveSlotElement } from './slot-resolver.js';
30
30
  import { SegmentProvider } from '#/client/segment-context.js';
31
- import { setParsedSearchParams } from './request-context.js';
32
- import type { SearchParamsDefinition } from '#/search-params/create.js';
31
+
33
32
  import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
34
33
  import type { InterceptionContext } from './pipeline.js';
35
34
  import { shouldSkipSegment } from './state-tree-diff.js';
36
35
 
36
+ // ─── Param Coercion Error ─────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Thrown when a defineSegmentParams codec's parse() fails.
40
+ * The pipeline catches this and responds with 404.
41
+ */
42
+ export class ParamCoercionError extends Error {
43
+ constructor(message: string) {
44
+ super(message);
45
+ this.name = 'ParamCoercionError';
46
+ }
47
+ }
48
+
37
49
  // ─── Types ────────────────────────────────────────────────────────────────
38
50
 
39
51
  /** Head element for client-side metadata updates. */
@@ -84,6 +96,62 @@ export class RouteSignalWithContext extends Error {
84
96
  }
85
97
  }
86
98
 
99
+ // ─── Module Processing Helpers ─────────────────────────────────────────────
100
+
101
+ /**
102
+ * Reject the legacy `generateMetadata` export with a helpful migration message.
103
+ * Throws if the module exports `generateMetadata` instead of `metadata`.
104
+ */
105
+ function rejectLegacyGenerateMetadata(mod: Record<string, unknown>, filePath: string): void {
106
+ if ('generateMetadata' in mod) {
107
+ throw new Error(
108
+ `${filePath}: "generateMetadata" is not a valid export. ` +
109
+ `Export an async function named "metadata" instead.\n\n` +
110
+ ` // Before\n` +
111
+ ` export async function generateMetadata({ params }) { ... }\n\n` +
112
+ ` // After\n` +
113
+ ` export async function metadata({ params }) { ... }`
114
+ );
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Extract and resolve metadata from a module (layout or page).
120
+ * Handles both static metadata objects and async metadata functions.
121
+ * Returns the resolved Metadata, or null if none exported.
122
+ */
123
+ async function extractMetadata(
124
+ mod: Record<string, unknown>,
125
+ segment: ManifestSegmentNode,
126
+ paramsPromise: Promise<Record<string, string | string[]>>
127
+ ): Promise<Metadata | null> {
128
+ if (typeof mod.metadata === 'function') {
129
+ type MetadataFn = (props: Record<string, unknown>) => Promise<Metadata>;
130
+ return (
131
+ (await withSpan(
132
+ 'timber.metadata',
133
+ { 'timber.segment': segment.segmentName ?? segment.urlPath },
134
+ () => (mod.metadata as MetadataFn)({ params: paramsPromise })
135
+ )) ?? null
136
+ );
137
+ }
138
+ if (mod.metadata) {
139
+ return mod.metadata as Metadata;
140
+ }
141
+ return null;
142
+ }
143
+
144
+ /**
145
+ * Extract `deferSuspenseFor` from a module and return the maximum
146
+ * of the current value and the module's value.
147
+ */
148
+ function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number): number {
149
+ if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > current) {
150
+ return mod.deferSuspenseFor;
151
+ }
152
+ return current;
153
+ }
154
+
87
155
  // ─── Builder ──────────────────────────────────────────────────────────────
88
156
 
89
157
  /**
@@ -126,87 +194,34 @@ export async function buildRouteElement(
126
194
  segment,
127
195
  });
128
196
  }
129
- // Reject legacy generateMetadata export — use `export async function metadata()` instead
130
- if ('generateMetadata' in mod) {
131
- const filePath = segment.layout.filePath ?? segment.urlPath;
132
- throw new Error(
133
- `${filePath}: "generateMetadata" is not a valid export. ` +
134
- `Export an async function named "metadata" instead.\n\n` +
135
- ` // Before\n` +
136
- ` export async function generateMetadata({ params }) { ... }\n\n` +
137
- ` // After\n` +
138
- ` export async function metadata({ params }) { ... }`
139
- );
140
- }
141
- // Unified metadata export: static object or async function
142
- if (typeof mod.metadata === 'function') {
143
- type MetadataFn = (props: Record<string, unknown>) => Promise<Metadata>;
144
- const generated = await withSpan(
145
- 'timber.metadata',
146
- { 'timber.segment': segment.segmentName ?? segment.urlPath },
147
- () => (mod.metadata as MetadataFn)({ params: paramsPromise })
148
- );
149
- if (generated) {
150
- metadataEntries.push({ metadata: generated, isPage: false });
151
- }
152
- } else if (mod.metadata) {
153
- metadataEntries.push({ metadata: mod.metadata as Metadata, isPage: false });
154
- }
155
- // deferSuspenseFor hold window — max across all segments
156
- if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > deferSuspenseFor) {
157
- deferSuspenseFor = mod.deferSuspenseFor;
197
+
198
+ // Param coercion is handled in the pipeline (Stage 2c) before
199
+ // middleware and rendering. See coerceSegmentParams() in pipeline.ts.
200
+
201
+ rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
202
+ const layoutMetadata = await extractMetadata(mod, segment, paramsPromise);
203
+ if (layoutMetadata) {
204
+ metadataEntries.push({ metadata: layoutMetadata, isPage: false });
158
205
  }
206
+ deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
159
207
  }
160
208
 
161
209
  // Load page (leaf segment only)
162
210
  if (isLeaf && segment.page) {
163
- // Load and apply search-params.ts definition before rendering so
164
- // searchParams() from @timber-js/app/server returns parsed typed values.
165
- if (segment.searchParams) {
166
- const spMod = (await segment.searchParams.load()) as {
167
- default?: SearchParamsDefinition<Record<string, unknown>>;
168
- };
169
- if (spMod.default) {
170
- const rawSearchParams = new URL(req.url).searchParams;
171
- const parsed = spMod.default.parse(rawSearchParams);
172
- setParsedSearchParams(parsed);
173
- }
174
- }
175
-
176
211
  const mod = (await segment.page.load()) as Record<string, unknown>;
212
+
213
+ // Param coercion is handled in the pipeline (Stage 2c) before
214
+ // middleware and rendering. See coerceSegmentParams() in pipeline.ts.
215
+
177
216
  if (mod.default) {
178
217
  PageComponent = mod.default as (...args: unknown[]) => unknown;
179
218
  }
180
- // Reject legacy generateMetadata export — use `export async function metadata()` instead
181
- if ('generateMetadata' in mod) {
182
- const filePath = segment.page.filePath ?? segment.urlPath;
183
- throw new Error(
184
- `${filePath}: "generateMetadata" is not a valid export. ` +
185
- `Export an async function named "metadata" instead.\n\n` +
186
- ` // Before\n` +
187
- ` export async function generateMetadata({ params }) { ... }\n\n` +
188
- ` // After\n` +
189
- ` export async function metadata({ params }) { ... }`
190
- );
191
- }
192
- // Unified metadata export: static object or async function
193
- if (typeof mod.metadata === 'function') {
194
- type MetadataFn = (props: Record<string, unknown>) => Promise<Metadata>;
195
- const generated = await withSpan(
196
- 'timber.metadata',
197
- { 'timber.segment': segment.segmentName ?? segment.urlPath },
198
- () => (mod.metadata as MetadataFn)({ params: paramsPromise })
199
- );
200
- if (generated) {
201
- metadataEntries.push({ metadata: generated, isPage: true });
202
- }
203
- } else if (mod.metadata) {
204
- metadataEntries.push({ metadata: mod.metadata as Metadata, isPage: true });
205
- }
206
- // deferSuspenseFor hold window — max across all segments
207
- if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > deferSuspenseFor) {
208
- deferSuspenseFor = mod.deferSuspenseFor;
219
+ rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
220
+ const pageMetadata = await extractMetadata(mod, segment, paramsPromise);
221
+ if (pageMetadata) {
222
+ metadataEntries.push({ metadata: pageMetadata, isPage: true });
209
223
  }
224
+ deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
210
225
  }
211
226
  }
212
227
 
@@ -227,7 +242,7 @@ export async function buildRouteElement(
227
242
  if (segment.access) {
228
243
  const accessMod = (await segment.access.load()) as Record<string, unknown>;
229
244
  const accessFn = accessMod.default as
230
- | ((ctx: { params: Record<string, string | string[]>; searchParams: unknown }) => unknown)
245
+ | ((ctx: { params: Record<string, string | string[]> }) => unknown)
231
246
  | undefined;
232
247
  if (accessFn) {
233
248
  try {
@@ -236,7 +251,7 @@ export async function buildRouteElement(
236
251
  { 'timber.segment': segment.segmentName ?? 'unknown' },
237
252
  async () => {
238
253
  try {
239
- await accessFn({ params: match.params, searchParams: {} });
254
+ await accessFn({ params: match.params });
240
255
  await setSpanAttribute('timber.result', 'pass');
241
256
  accessVerdicts.set(si, 'pass');
242
257
  } catch (error) {
@@ -304,7 +319,6 @@ export async function buildRouteElement(
304
319
 
305
320
  let element = h(TracedPage, {
306
321
  params: paramsPromise,
307
- searchParams: {},
308
322
  });
309
323
 
310
324
  // Build a lookup of layout components by segment for O(1) access.
@@ -380,13 +394,12 @@ export async function buildRouteElement(
380
394
  if (segment.access) {
381
395
  const accessMod = (await segment.access.load()) as Record<string, unknown>;
382
396
  const accessFn = accessMod.default as
383
- | ((ctx: { params: Record<string, string | string[]>; searchParams: unknown }) => unknown)
397
+ | ((ctx: { params: Record<string, string | string[]> }) => unknown)
384
398
  | undefined;
385
399
  if (accessFn) {
386
400
  element = h(AccessGate, {
387
401
  accessFn,
388
402
  params: match.params,
389
- searchParams: {},
390
403
  segmentName: segment.segmentName,
391
404
  verdict: accessVerdicts.get(i),
392
405
  children: element,
@@ -412,31 +425,22 @@ export async function buildRouteElement(
412
425
  const segmentPath = segment.urlPath.split('/');
413
426
  const parallelRouteKeys = Object.keys(segment.slots ?? {});
414
427
 
415
- // Wrap the layout component in an OTEL span.
416
- // For route groups, urlPath is "/" (groups don't add URL segments), so
417
- // include the directory name to distinguish e.g. "layout /(pre-release)"
418
- // from the root "layout /".
419
- const segmentForSpan = segment;
420
- const layoutComponentForSpan = layoutComponent;
421
- const segmentLabel =
422
- segmentForSpan.segmentType === 'group'
423
- ? `${segmentForSpan.urlPath === '/' ? '' : segmentForSpan.urlPath}/${segmentForSpan.segmentName}`
424
- : segmentForSpan.urlPath;
425
- const TracedLayout = async (props: Record<string, unknown>) => {
426
- return withSpan('timber.layout', { 'timber.segment': segmentLabel }, () =>
427
- (layoutComponentForSpan as (props: Record<string, unknown>) => unknown)(props)
428
- );
429
- };
430
-
431
- // segmentId uniquely identifies this segment for client-side element
432
- // caching. For route groups, urlPath is shared with the parent (both "/"),
433
- // so we include the group name to distinguish them. Without this, the
434
- // segment merger's element cache would conflate root and group elements.
428
+ // For route groups, urlPath is shared with the parent (both "/"),
429
+ // so include the group name to distinguish them. Used for both OTEL
430
+ // span labels and client-side element caching (segmentId).
435
431
  const segmentId =
436
432
  segment.segmentType === 'group'
437
433
  ? `${segment.urlPath === '/' ? '' : segment.urlPath}/${segment.segmentName}`
438
434
  : segment.urlPath;
439
435
 
436
+ // Wrap the layout component in an OTEL span
437
+ const layoutComponentRef = layoutComponent;
438
+ const TracedLayout = async (props: Record<string, unknown>) => {
439
+ return withSpan('timber.layout', { 'timber.segment': segmentId }, () =>
440
+ (layoutComponentRef as (props: Record<string, unknown>) => unknown)(props)
441
+ );
442
+ };
443
+
440
444
  element = h(SegmentProvider, {
441
445
  segments: segmentPath,
442
446
  segmentId,
@@ -444,7 +448,6 @@ export async function buildRouteElement(
444
448
  children: h(TracedLayout, {
445
449
  ...slotProps,
446
450
  params: paramsPromise,
447
- searchParams: {},
448
451
  children: element,
449
452
  }),
450
453
  });
@@ -50,14 +50,14 @@ export interface ManifestSegmentNode {
50
50
  middleware?: ManifestFile;
51
51
  access?: ManifestFile;
52
52
  route?: ManifestFile;
53
+ /** params.ts — isomorphic convention file for segmentParams + searchParams definitions. */
54
+ params?: ManifestFile;
53
55
  error?: ManifestFile;
54
56
  default?: ManifestFile;
55
57
  denied?: ManifestFile;
56
- searchParams?: ManifestFile;
57
58
  statusFiles?: Record<string, ManifestFile>;
58
59
  jsonStatusFiles?: Record<string, ManifestFile>;
59
60
  legacyStatusFiles?: Record<string, ManifestFile>;
60
- prerender?: ManifestFile;
61
61
  /** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */
62
62
  metadataRoutes?: Record<string, ManifestFile>;
63
63
 
@@ -15,7 +15,8 @@ import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
15
15
  import { renderDenyPage } from '#/server/deny-renderer.js';
16
16
  import type { LayoutEntry } from '#/server/deny-renderer.js';
17
17
  import type { NavContext } from '#/server/ssr-entry.js';
18
- import { createDebugChannelSink, parseCookiesFromHeader } from './helpers.js';
18
+ import { createDebugChannelSink } from './helpers.js';
19
+ import { getCookiesForSsr } from '#/server/request-context.js';
19
20
  import { callSsr } from './ssr-bridge.js';
20
21
 
21
22
  /**
@@ -127,7 +128,7 @@ export async function renderErrorPage(
127
128
  headHtml: '',
128
129
  bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
129
130
  rscStream: inlineStream,
130
- cookies: parseCookiesFromHeader(req.headers.get('cookie') ?? ''),
131
+ cookies: getCookiesForSsr(),
131
132
  };
132
133
 
133
134
  return callSsr(ssrStream, navContext);
@@ -47,7 +47,11 @@ import { buildClientScripts } from '#/server/html-injectors.js';
47
47
  import type { InterceptionContext, PipelineConfig, RouteMatch } from '#/server/pipeline.js';
48
48
  import { createPipeline } from '#/server/pipeline.js';
49
49
  import { DenySignal, RedirectSignal } from '#/server/primitives.js';
50
- import { buildRouteElement, RouteSignalWithContext } from '#/server/route-element-builder.js';
50
+ import {
51
+ buildRouteElement,
52
+ RouteSignalWithContext,
53
+ ParamCoercionError,
54
+ } from '#/server/route-element-builder.js';
51
55
  import type { ManifestSegmentNode } from '#/server/route-matcher.js';
52
56
  import { createMetadataRouteMatcher, createRouteMatcher } from '#/server/route-matcher.js';
53
57
  import { initDevTracing } from '#/server/tracing.js';
@@ -112,13 +116,15 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
112
116
  // See design/17-logging.md §"register() — Server Startup"
113
117
  await loadInstrumentation(loadUserInstrumentation);
114
118
 
115
- // Initialize cookie signing secrets from config (design/29-cookies.md §"Signed Cookies")
116
- const cookieSecrets = (runtimeConfig as Record<string, unknown>).cookieSecrets as
117
- | string[]
119
+ // Initialize deployment ID for version skew detection (TIM-446).
120
+ // The manifest init module sets globalThis.__TIMBER_DEPLOYMENT_ID__ at startup.
121
+ // In dev mode this is undefined — skew checks are skipped.
122
+ const deploymentId = (globalThis as Record<string, unknown>).__TIMBER_DEPLOYMENT_ID__ as
123
+ | string
118
124
  | undefined;
119
- if (cookieSecrets?.length) {
120
- const { setCookieSecrets } = await import('#/server/request-context.js');
121
- setCookieSecrets(cookieSecrets);
125
+ if (deploymentId) {
126
+ const { setDeploymentId } = await import('#/server/version-skew.js');
127
+ setDeploymentId(deploymentId);
122
128
  }
123
129
 
124
130
  const matchRoute = createRouteMatcher(manifest);
@@ -211,7 +217,8 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
211
217
  responseHeaders,
212
218
  clientBootstrap,
213
219
  clientJsDisabled,
214
- interception
220
+ interception,
221
+ manifest.root
215
222
  );
216
223
  },
217
224
  renderNoMatch: async (req: Request, responseHeaders: Headers) => {
@@ -304,7 +311,8 @@ async function renderRoute(
304
311
  responseHeaders: Headers,
305
312
  clientBootstrap: ClientBootstrapConfig,
306
313
  clientJsDisabled: boolean,
307
- interception?: InterceptionContext
314
+ interception?: InterceptionContext,
315
+ rootSegment?: ManifestSegmentNode
308
316
  ): Promise<Response> {
309
317
  const segments = match.segments as unknown as ManifestSegmentNode[];
310
318
  const leaf = segments[segments.length - 1];
@@ -359,9 +367,16 @@ async function renderRoute(
359
367
  return buildRedirectResponse(_req, signal, responseHeaders);
360
368
  }
361
369
  }
362
- // No PageComponent found
370
+ // Param coercion failed — render the custom 404 page (status files / not-found).
371
+ // Previously returned a bare Response(null, { status: 404 }) which bypassed
372
+ // custom not-found pages. Now routes through renderNoMatchPage so apps with
373
+ // 404.tsx / not-found status files render their custom page.
374
+ if (error instanceof ParamCoercionError) {
375
+ return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
376
+ }
377
+ // No PageComponent found — same treatment as param coercion: render custom 404.
363
378
  if (error instanceof Error && error.message.startsWith('No page component')) {
364
- return new Response(null, { status: 404 });
379
+ return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
365
380
  }
366
381
  throw error;
367
382
  }
@@ -155,8 +155,8 @@ export async function buildRscPayloadResponse(
155
155
  responseHeaders.set('X-Timber-Skipped-Segments', JSON.stringify(skippedSegments));
156
156
  }
157
157
 
158
- // Send route params so the client can populate useParams() after
159
- // SPA navigation. Without this, useParams() returns {}.
158
+ // Send route params so the client can populate useSegmentParams() after
159
+ // SPA navigation. Without this, useSegmentParams() returns {}.
160
160
  if (Object.keys(match.params).length > 0) {
161
161
  responseHeaders.set('X-Timber-Params', JSON.stringify(match.params));
162
162
  }
@@ -26,8 +26,8 @@ import {
26
26
  buildSegmentInfo,
27
27
  createDebugChannelSink,
28
28
  isAbortError,
29
- parseCookiesFromHeader,
30
29
  } from './helpers.js';
30
+ import { getCookiesForSsr } from '#/server/request-context.js';
31
31
  import { renderErrorPage } from './error-renderer.js';
32
32
  import { callSsr } from './ssr-bridge.js';
33
33
  import type { RenderSignals } from './rsc-stream.js';
@@ -92,8 +92,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
92
92
  ? ''
93
93
  : `<script>self.__timber_segments=${JSON.stringify(buildSegmentInfo(segments, layoutComponents))}</script>`;
94
94
 
95
- // Embed route params in HTML so useParams() works on initial hydration.
96
- // Without this, useParams() returns {} until the first client navigation.
95
+ // Embed route params in HTML so useSegmentParams() works on initial hydration.
96
+ // Without this, useSegmentParams() returns {} until the first client navigation.
97
97
  const paramsScript =
98
98
  clientJsDisabled || Object.keys(match.params).length === 0
99
99
  ? ''
@@ -111,7 +111,7 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
111
111
  rscStream: clientJsDisabled ? undefined : inlineStream,
112
112
  deferSuspenseFor: deferSuspenseFor > 0 ? deferSuspenseFor : undefined,
113
113
  signal: req.signal,
114
- cookies: parseCookiesFromHeader(req.headers.get('cookie') ?? ''),
114
+ cookies: getCookiesForSsr(),
115
115
  };
116
116
 
117
117
  // Helper: check if render-phase signals were captured and return the