@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
@@ -48,8 +48,6 @@ export interface TreeBuilderConfig {
48
48
  segments: SegmentNode[];
49
49
  /** Route params extracted by the matcher (catch-all segments produce string[]). */
50
50
  params: Record<string, string | string[]>;
51
- /** Parsed search params (typed or URLSearchParams). */
52
- searchParams: unknown;
53
51
  /** Loads a route file's module. */
54
52
  loadModule: ModuleLoader;
55
53
  /** React.createElement or equivalent. */
@@ -77,9 +75,8 @@ export interface TreeBuilderConfig {
77
75
  * (backward compat for tree-builder.ts which doesn't run a pre-render pass).
78
76
  */
79
77
  export interface AccessGateProps {
80
- accessFn: (ctx: { params: Record<string, string | string[]>; searchParams: unknown }) => unknown;
78
+ accessFn: (ctx: { params: Record<string, string | string[]> }) => unknown;
81
79
  params: Record<string, string | string[]>;
82
- searchParams: unknown;
83
80
  /** Segment name for dev logging (e.g. "authenticated", "dashboard"). */
84
81
  segmentName?: string;
85
82
  /**
@@ -98,12 +95,20 @@ export interface AccessGateProps {
98
95
  /**
99
96
  * Framework-injected slot access gate component.
100
97
  * On denial, renders denied.tsx → default.tsx → null instead of failing the page.
98
+ *
99
+ * DeniedComponent is passed instead of a pre-built element so that
100
+ * SlotAccessGate can forward DenySignal.data as dangerouslyPassData
101
+ * and slotName as the slot prop after catching the signal.
101
102
  */
102
103
  export interface SlotAccessGateProps {
103
- accessFn: (ctx: { params: Record<string, string | string[]>; searchParams: unknown }) => unknown;
104
+ accessFn: (ctx: { params: Record<string, string | string[]> }) => unknown;
104
105
  params: Record<string, string | string[]>;
105
- searchParams: unknown;
106
- deniedFallback: ReactElement | null;
106
+ /** The denied.tsx component (not a pre-built element). null if no denied.tsx exists. */
107
+ DeniedComponent: ((...args: unknown[]) => unknown) | null;
108
+ /** Slot directory name without @ prefix (e.g. "admin", "sidebar"). */
109
+ slotName: string;
110
+ /** createElement function for building elements dynamically. */
111
+ createElement: CreateElement;
107
112
  defaultFallback: ReactElement | null;
108
113
  children: ReactElement;
109
114
  }
@@ -113,7 +118,8 @@ export interface SlotAccessGateProps {
113
118
  * Wraps content with status-code error boundary handling.
114
119
  */
115
120
  export interface ErrorBoundaryProps {
116
- fallbackComponent: ReactElement | null;
121
+ fallbackComponent?: ReactElement | null;
122
+ fallbackElement?: ReactElement | null;
117
123
  status?: number;
118
124
  children: ReactElement;
119
125
  }
@@ -143,8 +149,7 @@ export interface TreeBuildResult {
143
149
  * Parallel slots are resolved at each layout level and composed as named props.
144
150
  */
145
151
  export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeBuildResult> {
146
- const { segments, params, searchParams, loadModule, createElement, errorBoundaryComponent } =
147
- config;
152
+ const { segments, params, loadModule, createElement, errorBoundaryComponent } = config;
148
153
 
149
154
  if (segments.length === 0) {
150
155
  throw new Error('[timber] buildElementTree: empty segment chain');
@@ -168,8 +173,8 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
168
173
  );
169
174
  }
170
175
 
171
- // Build the page element with params and searchParams props
172
- let element: ReactElement = createElement(PageComponent, { params, searchParams });
176
+ // Build the page element with params prop
177
+ let element: ReactElement = createElement(PageComponent, { params });
173
178
 
174
179
  // Build tree bottom-up: wrap page, then walk segments from leaf to root
175
180
  for (let i = segments.length - 1; i >= 0; i--) {
@@ -191,7 +196,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
191
196
  element = createElement('timber:access-gate', {
192
197
  accessFn,
193
198
  params,
194
- searchParams,
195
199
  segmentName: segment.segmentName,
196
200
  children: element,
197
201
  } satisfies AccessGateProps);
@@ -212,7 +216,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
212
216
  slotProps[slotName] = await buildSlotElement(
213
217
  slotNode,
214
218
  params,
215
- searchParams,
216
219
  loadModule,
217
220
  createElement,
218
221
  errorBoundaryComponent
@@ -223,7 +226,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
223
226
  element = createElement(LayoutComponent, {
224
227
  ...slotProps,
225
228
  params,
226
- searchParams,
227
229
  children: element,
228
230
  });
229
231
  }
@@ -244,7 +246,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
244
246
  async function buildSlotElement(
245
247
  slotNode: SegmentNode,
246
248
  params: Record<string, string | string[]>,
247
- searchParams: unknown,
248
249
  loadModule: ModuleLoader,
249
250
  createElement: CreateElement,
250
251
  errorBoundaryComponent: unknown
@@ -261,10 +262,10 @@ async function buildSlotElement(
261
262
 
262
263
  // If no page, render default.tsx or null
263
264
  if (!PageComponent) {
264
- return DefaultComponent ? createElement(DefaultComponent, { params, searchParams }) : null;
265
+ return DefaultComponent ? createElement(DefaultComponent, { params }) : null;
265
266
  }
266
267
 
267
- let element: ReactElement = createElement(PageComponent, { params, searchParams });
268
+ let element: ReactElement = createElement(PageComponent, { params });
268
269
 
269
270
  // Wrap in error boundaries
270
271
  element = await wrapWithErrorBoundaries(
@@ -280,27 +281,20 @@ async function buildSlotElement(
280
281
  const accessModule = await loadModule(slotNode.access);
281
282
  const accessFn = accessModule.default as SlotAccessGateProps['accessFn'];
282
283
 
283
- // Load denied.tsx
284
+ // Load denied.tsx — pass component (not pre-built element) so
285
+ // SlotAccessGate can forward DenySignal.data dynamically. See TIM-488.
284
286
  const deniedModule = slotNode.denied ? await loadModule(slotNode.denied) : null;
285
- const DeniedComponent = deniedModule?.default as
286
- | ((...args: unknown[]) => ReactElement)
287
- | undefined;
288
-
289
- const deniedFallback = DeniedComponent
290
- ? createElement(DeniedComponent, {
291
- slot: slotNode.segmentName.replace(/^@/, ''),
292
- dangerouslyPassData: undefined,
293
- })
294
- : null;
295
- const defaultFallback = DefaultComponent
296
- ? createElement(DefaultComponent, { params, searchParams })
297
- : null;
287
+ const DeniedComponent =
288
+ (deniedModule?.default as ((...args: unknown[]) => ReactElement) | undefined) ?? null;
289
+
290
+ const defaultFallback = DefaultComponent ? createElement(DefaultComponent, { params }) : null;
298
291
 
299
292
  element = createElement('timber:slot-access-gate', {
300
293
  accessFn,
301
294
  params,
302
- searchParams,
303
- deniedFallback,
295
+ DeniedComponent,
296
+ slotName: slotNode.segmentName.replace(/^@/, ''),
297
+ createElement,
304
298
  defaultFallback,
305
299
  children: element,
306
300
  } satisfies SlotAccessGateProps);
@@ -311,6 +305,19 @@ async function buildSlotElement(
311
305
 
312
306
  // ─── Error Boundary Wrapping ─────────────────────────────────────────────────
313
307
 
308
+ /** MDX/markdown extensions — these are server components that cannot be passed as function props. */
309
+ const MDX_EXTENSIONS = new Set(['mdx', 'md']);
310
+
311
+ /**
312
+ * Check if a route file is an MDX/markdown file based on its extension.
313
+ * MDX components are server components by default and cannot cross the
314
+ * RSC→client boundary as function props. They must be pre-rendered as
315
+ * elements and passed as fallbackElement instead of fallbackComponent.
316
+ */
317
+ function isMdxFile(file: RouteFile): boolean {
318
+ return MDX_EXTENSIONS.has(file.extension);
319
+ }
320
+
314
321
  /**
315
322
  * Wrap an element with error boundaries from a segment's status-code files.
316
323
  *
@@ -320,6 +327,12 @@ async function buildSlotElement(
320
327
  * 3. error.tsx (general error boundary)
321
328
  *
322
329
  * This creates the fallback chain described in design/10-error-handling.md.
330
+ *
331
+ * MDX status files are server components and cannot be passed as function
332
+ * props to TimberErrorBoundary (a 'use client' component). Instead, they
333
+ * are pre-rendered as elements and passed as fallbackElement. The error
334
+ * boundary renders the element directly when an error is caught.
335
+ * See TIM-503.
323
336
  */
324
337
  async function wrapWithErrorBoundaries(
325
338
  segment: SegmentNode,
@@ -340,11 +353,18 @@ async function wrapWithErrorBoundaries(
340
353
  const mod = await loadModule(file);
341
354
  const Component = mod.default;
342
355
  if (Component) {
343
- element = createElement(errorBoundaryComponent, {
344
- fallbackComponent: Component,
345
- status,
346
- children: element,
347
- } satisfies ErrorBoundaryProps);
356
+ const boundaryProps = isMdxFile(file)
357
+ ? ({
358
+ fallbackElement: createElement(Component, { status }),
359
+ status,
360
+ children: element,
361
+ } satisfies ErrorBoundaryProps)
362
+ : ({
363
+ fallbackComponent: Component,
364
+ status,
365
+ children: element,
366
+ } satisfies ErrorBoundaryProps);
367
+ element = createElement(errorBoundaryComponent, boundaryProps);
348
368
  }
349
369
  }
350
370
  }
@@ -356,25 +376,41 @@ async function wrapWithErrorBoundaries(
356
376
  const mod = await loadModule(file);
357
377
  const Component = mod.default;
358
378
  if (Component) {
359
- element = createElement(errorBoundaryComponent, {
360
- fallbackComponent: Component,
361
- status: key === '4xx' ? 400 : 500, // category marker
362
- children: element,
363
- } satisfies ErrorBoundaryProps);
379
+ const categoryStatus = key === '4xx' ? 400 : 500;
380
+ const boundaryProps = isMdxFile(file)
381
+ ? ({
382
+ fallbackElement: createElement(Component, {}),
383
+ status: categoryStatus,
384
+ children: element,
385
+ } satisfies ErrorBoundaryProps)
386
+ : ({
387
+ fallbackComponent: Component,
388
+ status: categoryStatus,
389
+ children: element,
390
+ } satisfies ErrorBoundaryProps);
391
+ element = createElement(errorBoundaryComponent, boundaryProps);
364
392
  }
365
393
  }
366
394
  }
367
395
  }
368
396
 
369
397
  // Wrap with error.tsx (outermost — catches anything not matched by status files)
398
+ // Note: error.tsx/error.mdx receives { error, digest, reset } props.
399
+ // MDX error files are pre-rendered without those props (they're static content).
370
400
  if (segment.error) {
371
401
  const errorModule = await loadModule(segment.error);
372
402
  const ErrorComponent = errorModule.default;
373
403
  if (ErrorComponent) {
374
- element = createElement(errorBoundaryComponent, {
375
- fallbackComponent: ErrorComponent,
376
- children: element,
377
- } satisfies ErrorBoundaryProps);
404
+ const boundaryProps = isMdxFile(segment.error)
405
+ ? ({
406
+ fallbackElement: createElement(ErrorComponent, {}),
407
+ children: element,
408
+ } satisfies ErrorBoundaryProps)
409
+ : ({
410
+ fallbackComponent: ErrorComponent,
411
+ children: element,
412
+ } satisfies ErrorBoundaryProps);
413
+ element = createElement(errorBoundaryComponent, boundaryProps);
378
414
  }
379
415
  }
380
416
 
@@ -22,8 +22,7 @@ export interface MiddlewareContext {
22
22
  req: Request;
23
23
  requestHeaders: Headers;
24
24
  headers: Headers;
25
- params: Record<string, string | string[]>;
26
- searchParams: unknown;
25
+ segmentParams: Record<string, string | string[]>;
27
26
  /** Declare early hints for critical resources. Appends Link headers. */
28
27
  earlyHints: (hints: EarlyHint[]) => void;
29
28
  }
@@ -37,7 +36,6 @@ export interface RouteContext {
37
36
 
38
37
  export interface AccessContext {
39
38
  params: Record<string, string | string[]>;
40
- searchParams: unknown;
41
39
  }
42
40
 
43
41
  export interface Metadata {
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Version Skew Detection — graceful recovery when stale clients hit new deployments.
3
+ *
4
+ * When a new version of the app is deployed, clients with open tabs still have
5
+ * the old JavaScript bundle. Without version skew handling, these stale clients
6
+ * will experience:
7
+ *
8
+ * 1. Server action calls that crash (action IDs are content-hashed)
9
+ * 2. Chunk load failures (old filenames gone from CDN)
10
+ * 3. RSC payload mismatches (component references differ between builds)
11
+ *
12
+ * This module implements deployment ID comparison:
13
+ * - A per-build deployment ID is generated at build time (see build-manifest.ts)
14
+ * - The client sends it via `X-Timber-Deployment-Id` header on every RSC/action request
15
+ * - The server compares it against the current build's ID
16
+ * - On mismatch: signal the client to reload (not crash)
17
+ *
18
+ * The deployment ID is always-on in production. Dev mode skips the check
19
+ * (HMR handles code updates without full reloads).
20
+ *
21
+ * See design/25-production-deployments.md, TIM-446
22
+ */
23
+
24
+ // ─── Constants ───────────────────────────────────────────────────
25
+
26
+ /** Header sent by the client with every RSC/action request. */
27
+ export const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';
28
+
29
+ /** Response header that signals the client to do a full page reload. */
30
+ export const RELOAD_HEADER = 'X-Timber-Reload';
31
+
32
+ // ─── Deployment ID ───────────────────────────────────────────────
33
+
34
+ /**
35
+ * The current build's deployment ID. Set at startup from the manifest init
36
+ * module (globalThis.__TIMBER_DEPLOYMENT_ID__). Null in dev mode.
37
+ */
38
+ let currentDeploymentId: string | null = null;
39
+
40
+ /**
41
+ * Set the current deployment ID. Called once at server startup from the
42
+ * manifest init module. In dev mode this is never called (deployment ID
43
+ * checks are skipped).
44
+ */
45
+ export function setDeploymentId(id: string): void {
46
+ currentDeploymentId = id;
47
+ }
48
+
49
+ /**
50
+ * Get the current deployment ID. Returns null in dev mode.
51
+ */
52
+ export function getDeploymentId(): string | null {
53
+ return currentDeploymentId;
54
+ }
55
+
56
+ // ─── Skew Detection ──────────────────────────────────────────────
57
+
58
+ /** Result of a version skew check. */
59
+ export interface SkewCheckResult {
60
+ /** Whether the client's deployment ID matches the server's. */
61
+ ok: boolean;
62
+ /** The client's deployment ID (null if header not sent — e.g., initial page load). */
63
+ clientId: string | null;
64
+ }
65
+
66
+ /**
67
+ * Check if a request's deployment ID matches the current build.
68
+ *
69
+ * Returns `{ ok: true }` when:
70
+ * - Dev mode (no deployment ID set — HMR handles updates)
71
+ * - No deployment ID header (initial page load, non-RSC request)
72
+ * - Deployment IDs match
73
+ *
74
+ * Returns `{ ok: false }` when:
75
+ * - Client sends a deployment ID that differs from the current build
76
+ */
77
+ export function checkVersionSkew(req: Request): SkewCheckResult {
78
+ // Dev mode — no deployment ID checks (HMR handles updates)
79
+ if (!currentDeploymentId) {
80
+ return { ok: true, clientId: null };
81
+ }
82
+
83
+ const clientId = req.headers.get(DEPLOYMENT_ID_HEADER);
84
+
85
+ // No header — initial page load or non-RSC request. Always OK.
86
+ if (!clientId) {
87
+ return { ok: true, clientId: null };
88
+ }
89
+
90
+ // Compare deployment IDs
91
+ if (clientId === currentDeploymentId) {
92
+ return { ok: true, clientId };
93
+ }
94
+
95
+ return { ok: false, clientId };
96
+ }
97
+
98
+ /**
99
+ * Apply version skew reload headers to a response.
100
+ * Sets X-Timber-Reload: 1 to signal the client to do a full page reload.
101
+ */
102
+ export function applyReloadHeaders(headers: Headers): void {
103
+ headers.set(RELOAD_HEADER, '1');
104
+ }
@@ -18,7 +18,7 @@
18
18
 
19
19
  // Hooks — imported from the public barrel for module singleton consistency.
20
20
  export {
21
- useParams,
21
+ useSegmentParams,
22
22
  usePathname,
23
23
  useSearchParams,
24
24
  useRouter,
@@ -8,7 +8,7 @@
8
8
 
9
9
  // Hooks (client-side — imported from public barrel for module singleton)
10
10
  export {
11
- useParams,
11
+ useSegmentParams,
12
12
  usePathname,
13
13
  useSearchParams,
14
14
  useRouter,
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Tiny typed state machine utility.
3
+ *
4
+ * Enforces discriminated-union states, typed transitions with runtime
5
+ * guards, subscribe for external store integration, and assertPhase
6
+ * for function entry guards.
7
+ *
8
+ * Designed for 3–5 state consumers (stream transforms, client navigation,
9
+ * build phase sequencing). No async, no hierarchy, no history.
10
+ *
11
+ * Performance: send() is one object lookup + one function call.
12
+ * Equivalent cost to a boolean check after V8 inlining.
13
+ */
14
+
15
+ /** A state machine instance with typed state and events. */
16
+ export interface Machine<TState extends { phase: string }, TEvent extends { type: string }> {
17
+ /** Current state (discriminated union — narrowed by phase). */
18
+ readonly state: TState;
19
+
20
+ /** Transition with runtime guard. Throws on invalid source+event pair. */
21
+ send(event: TEvent): void;
22
+
23
+ /** Subscribe to state changes. Returns unsubscribe function. */
24
+ subscribe(listener: (state: TState) => void): () => void;
25
+
26
+ /** Throw if not in the expected phase. Entry guard for functions. */
27
+ assertPhase<P extends TState['phase']>(phase: P): void;
28
+ }
29
+
30
+ /**
31
+ * Transition map: `transitions[phase][eventType]` returns the next state.
32
+ *
33
+ * Each handler receives the current state (narrowed by phase context)
34
+ * and the event, returning the new state.
35
+ */
36
+ export type TransitionMap<TState extends { phase: string }, TEvent extends { type: string }> = {
37
+ [P in TState['phase']]?: {
38
+ [E in TEvent['type']]?: (
39
+ state: Extract<TState, { phase: P }>,
40
+ event: Extract<TEvent, { type: E }>
41
+ ) => TState;
42
+ };
43
+ };
44
+
45
+ export interface MachineConfig<TState extends { phase: string }, TEvent extends { type: string }> {
46
+ initial: TState;
47
+ transitions: TransitionMap<TState, TEvent>;
48
+ /** Fires after every valid transition. Use for side effects. */
49
+ onTransition?: (prev: TState, next: TState, event: TEvent) => void;
50
+ }
51
+
52
+ /**
53
+ * Create a state machine from a config.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * type State = { phase: 'idle' } | { phase: 'running'; pid: number };
58
+ * type Event = { type: 'START'; pid: number } | { type: 'STOP' };
59
+ *
60
+ * const m = createMachine<State, Event>({
61
+ * initial: { phase: 'idle' },
62
+ * transitions: {
63
+ * idle: { START: (_s, e) => ({ phase: 'running', pid: e.pid }) },
64
+ * running: { STOP: () => ({ phase: 'idle' }) },
65
+ * },
66
+ * });
67
+ * ```
68
+ */
69
+ export function createMachine<TState extends { phase: string }, TEvent extends { type: string }>(
70
+ config: MachineConfig<TState, TEvent>
71
+ ): Machine<TState, TEvent> {
72
+ let current: TState = config.initial;
73
+ const listeners = new Set<(state: TState) => void>();
74
+
75
+ return {
76
+ get state() {
77
+ return current;
78
+ },
79
+
80
+ send(event: TEvent): void {
81
+ const phaseHandlers = config.transitions[current.phase as TState['phase']];
82
+ const handler = phaseHandlers?.[event.type as TEvent['type']] as
83
+ | ((state: TState, event: TEvent) => TState)
84
+ | undefined;
85
+
86
+ if (!handler) {
87
+ throw new Error(`[state-machine] Invalid transition: ${current.phase} + ${event.type}`);
88
+ }
89
+
90
+ const prev = current;
91
+ current = handler(prev, event);
92
+
93
+ config.onTransition?.(prev, current, event);
94
+
95
+ for (const listener of listeners) {
96
+ listener(current);
97
+ }
98
+ },
99
+
100
+ subscribe(listener: (state: TState) => void): () => void {
101
+ listeners.add(listener);
102
+ return () => listeners.delete(listener);
103
+ },
104
+
105
+ assertPhase<P extends TState['phase']>(phase: P): void {
106
+ if (current.phase !== phase) {
107
+ throw new Error(`[state-machine] Expected phase "${phase}", got "${current.phase}"`);
108
+ }
109
+ },
110
+ };
111
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"als-registry-B7DbZ2hS.js","names":[],"sources":["../../src/server/als-registry.ts"],"sourcesContent":["/**\n * Centralized AsyncLocalStorage registry for server-side per-request state.\n *\n * ALL ALS instances used by the server framework live here. Individual\n * modules (request-context.ts, tracing.ts, actions.ts, etc.) import from\n * this registry and re-export public accessor functions.\n *\n * Why: ALS instances require singleton semantics — if two copies of the\n * same ALS exist (one from a relative import, one from a barrel import),\n * one module writes to its copy and another reads from an empty copy.\n * Centralizing ALS creation in a single module eliminates this class of bug.\n *\n * The `timber-shims` plugin ensures `@timber-js/app/server` resolves to\n * src/ in RSC and SSR environments, so all import paths converge here.\n *\n * DO NOT create ALS instances outside this file. If you need a new ALS,\n * add it here and import from `./als-registry.js` in the consuming module.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\n\n// ─── Request Context ──────────────────────────────────────────────────────\n// Used by: request-context.ts (headers(), cookies(), searchParams())\n// Design doc: design/04-authorization.md\n\n/** @internal — import via request-context.ts public API */\nexport const requestContextAls = new AsyncLocalStorage<RequestContextStore>();\n\nexport interface RequestContextStore {\n /** Incoming request headers (read-only view). */\n headers: Headers;\n /** Raw cookie header string, parsed lazily into a Map on first access. */\n cookieHeader: string;\n /** Lazily-parsed cookie map (mutable — reflects write-overlay from set()). */\n parsedCookies?: Map<string, string>;\n /** Original (pre-overlay) frozen headers, kept for overlay merging. */\n originalHeaders: Headers;\n /**\n * Promise resolving to the route's typed search params (when search-params.ts\n * exists) or to the raw URLSearchParams. Stored as a Promise so the framework\n * can later support partial pre-rendering where param resolution is deferred.\n */\n searchParamsPromise: Promise<URLSearchParams | Record<string, unknown>>;\n /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */\n cookieJar: Map<string, CookieEntry>;\n /** Whether the response has flushed (headers committed). */\n flushed: boolean;\n /** Whether the current context allows cookie mutation. */\n mutableContext: boolean;\n}\n\n/** A single outgoing cookie entry in the cookie jar. */\nexport interface CookieEntry {\n name: string;\n value: string;\n options: import('./request-context.js').CookieOptions;\n}\n\n// ─── Tracing ──────────────────────────────────────────────────────────────\n// Used by: tracing.ts (traceId(), spanId())\n// Design doc: design/17-logging.md\n\nexport interface TraceStore {\n /** 32-char lowercase hex trace ID (OTEL or UUID fallback). */\n traceId: string;\n /** OTEL span ID if available, undefined otherwise. */\n spanId?: string;\n}\n\n/** @internal — import via tracing.ts public API */\nexport const traceAls = new AsyncLocalStorage<TraceStore>();\n\n// ─── Server-Timing ────────────────────────────────────────────────────────\n// Used by: server-timing.ts (recordTiming(), withTiming())\n// Design doc: (dev-only performance instrumentation)\n\nexport interface TimingStore {\n entries: import('./server-timing.js').TimingEntry[];\n}\n\n/** @internal — import via server-timing.ts public API */\nexport const timingAls = new AsyncLocalStorage<TimingStore>();\n\n// ─── Revalidation ─────────────────────────────────────────────────────────\n// Used by: actions.ts (revalidatePath(), revalidateTag())\n// Design doc: design/08-forms-and-actions.md\n\nexport interface RevalidationState {\n /** Paths to re-render (populated by revalidatePath calls). */\n paths: string[];\n /** Tags to invalidate (populated by revalidateTag calls). */\n tags: string[];\n}\n\n/** @internal — import via actions.ts public API */\nexport const revalidationAls = new AsyncLocalStorage<RevalidationState>();\n\n// ─── Form Flash ───────────────────────────────────────────────────────────\n// Used by: form-flash.ts (getFormFlash())\n// Design doc: design/08-forms-and-actions.md §\"No-JS Error Round-Trip\"\n\n/** @internal — import via form-flash.ts public API */\nexport const formFlashAls = new AsyncLocalStorage<import('./form-flash.js').FormFlashData>();\n\n// ─── Early Hints Sender ──────────────────────────────────────────────────\n// Used by: early-hints-sender.ts (sendEarlyHints103())\n// Design doc: design/02-rendering-pipeline.md §\"Early Hints (103)\"\n\n/** Function that sends Link header values as a 103 Early Hints response. */\nexport type EarlyHintsSenderFn = (links: string[]) => void;\n\n/** @internal — import via early-hints-sender.ts public API */\nexport const earlyHintsSenderAls = new AsyncLocalStorage<EarlyHintsSenderFn>();\n\n// ─── waitUntil Bridge ────────────────────────────────────────────────────\n// Used by: waituntil-bridge.ts (waitUntil())\n// Design doc: design/11-platform.md §\"waitUntil()\"\n\n/** Function that extends the request lifecycle with a background promise. */\nexport type WaitUntilFn = (promise: Promise<unknown>) => void;\n\n/** @internal — import via waituntil-bridge.ts public API */\nexport const waitUntilAls = new AsyncLocalStorage<WaitUntilFn>();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,oBAAoB,IAAI,mBAAwC;;AA4C7E,IAAa,WAAW,IAAI,mBAA+B;;AAW3D,IAAa,YAAY,IAAI,mBAAgC;;AAc7D,IAAa,kBAAkB,IAAI,mBAAsC;;AAOzE,IAAa,eAAe,IAAI,mBAA4D;;AAU5F,IAAa,sBAAsB,IAAI,mBAAuC;;AAU9E,IAAa,eAAe,IAAI,mBAAgC"}