@timber-js/app 0.2.0-alpha.57 → 0.2.0-alpha.59

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 (188) hide show
  1. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -1
  2. package/dist/_chunks/define-D5STJpIr.js +121 -0
  3. package/dist/_chunks/define-D5STJpIr.js.map +1 -0
  4. package/dist/_chunks/define-TK8C1M3x.js.map +1 -1
  5. package/dist/_chunks/{define-cookie-k9btcEfI.js → define-cookie-DtAavax4.js} +4 -4
  6. package/dist/_chunks/define-cookie-DtAavax4.js.map +1 -0
  7. package/dist/_chunks/{error-boundary-B9vT_YK_.js → error-boundary-DpZJBCqh.js} +1 -1
  8. package/dist/_chunks/{error-boundary-B9vT_YK_.js.map → error-boundary-DpZJBCqh.js.map} +1 -1
  9. package/dist/_chunks/interception-Cey5DCGr.js.map +1 -1
  10. package/dist/_chunks/{request-context-0h-6Voad.js → request-context-0wfZsnhh.js} +3 -1
  11. package/dist/_chunks/request-context-0wfZsnhh.js.map +1 -0
  12. package/dist/_chunks/{segment-context-Bmugn-ao.js → segment-context-CyaM1mrD.js} +1 -1
  13. package/dist/_chunks/{segment-context-Bmugn-ao.js.map → segment-context-CyaM1mrD.js.map} +1 -1
  14. package/dist/_chunks/{stale-reload-Db4wqE46.js → stale-reload-DKN3aXxR.js} +1 -1
  15. package/dist/_chunks/{stale-reload-Db4wqE46.js.map → stale-reload-DKN3aXxR.js.map} +1 -1
  16. package/dist/_chunks/{tracing-JI4cYUdz.js → tracing-VYETCQsg.js} +1 -1
  17. package/dist/_chunks/{tracing-JI4cYUdz.js.map → tracing-VYETCQsg.js.map} +1 -1
  18. package/dist/_chunks/use-query-states-wEXY2JQB.js.map +1 -1
  19. package/dist/_chunks/{wrappers-C9XPg7-U.js → wrappers-BaG1bnM3.js} +1 -1
  20. package/dist/_chunks/{wrappers-C9XPg7-U.js.map → wrappers-BaG1bnM3.js.map} +1 -1
  21. package/dist/cache/index.js +1 -1
  22. package/dist/cache/index.js.map +1 -1
  23. package/dist/client/error-boundary.js +1 -1
  24. package/dist/client/error-reconstituter.d.ts +54 -0
  25. package/dist/client/error-reconstituter.d.ts.map +1 -0
  26. package/dist/client/form.d.ts +2 -2
  27. package/dist/client/form.d.ts.map +1 -1
  28. package/dist/client/index.d.ts +1 -1
  29. package/dist/client/index.d.ts.map +1 -1
  30. package/dist/client/index.js +4 -4
  31. package/dist/client/index.js.map +1 -1
  32. package/dist/client/link.d.ts +1 -1
  33. package/dist/client/link.d.ts.map +1 -1
  34. package/dist/client/segment-outlet.d.ts +63 -0
  35. package/dist/client/segment-outlet.d.ts.map +1 -0
  36. package/dist/client/use-params.d.ts +1 -1
  37. package/dist/client/use-params.d.ts.map +1 -1
  38. package/dist/client/use-query-states.d.ts +1 -1
  39. package/dist/client/use-query-states.d.ts.map +1 -1
  40. package/dist/cookies/define-cookie.d.ts +3 -3
  41. package/dist/cookies/define-cookie.d.ts.map +1 -1
  42. package/dist/cookies/index.js +1 -1
  43. package/dist/index.d.ts +15 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +173 -6
  46. package/dist/index.js.map +1 -1
  47. package/dist/params/define.d.ts +25 -1
  48. package/dist/params/define.d.ts.map +1 -1
  49. package/dist/params/index.d.ts +5 -5
  50. package/dist/params/index.d.ts.map +1 -1
  51. package/dist/params/index.js +2 -103
  52. package/dist/plugins/adapter-build.d.ts +1 -1
  53. package/dist/plugins/adapter-build.d.ts.map +1 -1
  54. package/dist/plugins/build-manifest.d.ts +2 -2
  55. package/dist/plugins/build-manifest.d.ts.map +1 -1
  56. package/dist/plugins/build-report.d.ts +3 -3
  57. package/dist/plugins/build-report.d.ts.map +1 -1
  58. package/dist/plugins/content.d.ts +1 -1
  59. package/dist/plugins/content.d.ts.map +1 -1
  60. package/dist/plugins/dev-browser-logs.d.ts +84 -0
  61. package/dist/plugins/dev-browser-logs.d.ts.map +1 -0
  62. package/dist/plugins/dev-logs.d.ts +1 -1
  63. package/dist/plugins/dev-logs.d.ts.map +1 -1
  64. package/dist/plugins/dev-server.d.ts +1 -1
  65. package/dist/plugins/dev-server.d.ts.map +1 -1
  66. package/dist/plugins/entries.d.ts +1 -1
  67. package/dist/plugins/entries.d.ts.map +1 -1
  68. package/dist/plugins/fonts.d.ts +2 -2
  69. package/dist/plugins/fonts.d.ts.map +1 -1
  70. package/dist/plugins/mdx.d.ts +1 -1
  71. package/dist/plugins/mdx.d.ts.map +1 -1
  72. package/dist/plugins/routing.d.ts +1 -1
  73. package/dist/plugins/routing.d.ts.map +1 -1
  74. package/dist/plugins/shims.d.ts +8 -5
  75. package/dist/plugins/shims.d.ts.map +1 -1
  76. package/dist/plugins/static-build.d.ts +1 -1
  77. package/dist/plugins/static-build.d.ts.map +1 -1
  78. package/dist/search-params/define.d.ts +1 -1
  79. package/dist/search-params/define.d.ts.map +1 -1
  80. package/dist/search-params/index.d.ts +1 -1
  81. package/dist/search-params/index.d.ts.map +1 -1
  82. package/dist/search-params/index.js +1 -1
  83. package/dist/server/actions.d.ts +1 -1
  84. package/dist/server/actions.d.ts.map +1 -1
  85. package/dist/server/als-registry.d.ts +7 -0
  86. package/dist/server/als-registry.d.ts.map +1 -1
  87. package/dist/server/deny-renderer.d.ts.map +1 -1
  88. package/dist/server/fallback-error.d.ts +3 -3
  89. package/dist/server/fallback-error.d.ts.map +1 -1
  90. package/dist/server/index.js +4 -4
  91. package/dist/server/index.js.map +1 -1
  92. package/dist/server/pipeline-interception.d.ts +1 -1
  93. package/dist/server/pipeline-interception.d.ts.map +1 -1
  94. package/dist/server/pipeline.d.ts +2 -2
  95. package/dist/server/pipeline.d.ts.map +1 -1
  96. package/dist/server/request-context.d.ts.map +1 -1
  97. package/dist/server/rsc-entry/api-handler.d.ts +2 -2
  98. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  99. package/dist/server/rsc-entry/error-renderer.d.ts +13 -13
  100. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  101. package/dist/server/rsc-entry/helpers.d.ts +2 -2
  102. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  103. package/dist/server/rsc-entry/index.d.ts +2 -2
  104. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  105. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -3
  106. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  107. package/dist/server/rsc-entry/rsc-stream.d.ts +1 -1
  108. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  109. package/dist/server/rsc-entry/ssr-bridge.d.ts +1 -1
  110. package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
  111. package/dist/server/rsc-entry/ssr-renderer.d.ts +4 -4
  112. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  113. package/dist/server/status-code-resolver.d.ts +1 -1
  114. package/dist/server/status-code-resolver.d.ts.map +1 -1
  115. package/dist/server/stream-utils.d.ts +36 -0
  116. package/dist/server/stream-utils.d.ts.map +1 -0
  117. package/dist/server/tree-builder.d.ts +1 -1
  118. package/dist/server/tree-builder.d.ts.map +1 -1
  119. package/dist/shims/font-google.d.ts +1 -1
  120. package/dist/shims/font-google.d.ts.map +1 -1
  121. package/package.json +1 -4
  122. package/src/cache/timber-cache.ts +1 -1
  123. package/src/client/browser-entry.ts +7 -8
  124. package/src/client/error-reconstituter.tsx +65 -0
  125. package/src/client/form.tsx +2 -2
  126. package/src/client/index.ts +2 -2
  127. package/src/client/link.tsx +2 -2
  128. package/src/client/segment-outlet.tsx +86 -0
  129. package/src/client/use-params.ts +1 -1
  130. package/src/client/use-query-states.ts +2 -2
  131. package/src/cookies/define-cookie.ts +9 -9
  132. package/src/index.ts +17 -0
  133. package/src/params/define.ts +61 -1
  134. package/src/params/index.ts +5 -5
  135. package/src/plugins/adapter-build.ts +2 -2
  136. package/src/plugins/build-manifest.ts +2 -2
  137. package/src/plugins/build-report.ts +3 -3
  138. package/src/plugins/cache-transform.ts +1 -1
  139. package/src/plugins/content.ts +1 -1
  140. package/src/plugins/dev-browser-logs.ts +274 -0
  141. package/src/plugins/dev-logs.ts +1 -1
  142. package/src/plugins/dev-server.ts +3 -3
  143. package/src/plugins/entries.ts +1 -1
  144. package/src/plugins/fonts.ts +9 -9
  145. package/src/plugins/mdx.ts +1 -1
  146. package/src/plugins/routing.ts +6 -6
  147. package/src/plugins/server-action-exports.ts +1 -1
  148. package/src/plugins/shims.ts +19 -39
  149. package/src/plugins/static-build.ts +2 -2
  150. package/src/routing/scanner.ts +1 -1
  151. package/src/routing/status-file-lint.ts +1 -1
  152. package/src/search-params/define.ts +2 -2
  153. package/src/search-params/index.ts +1 -1
  154. package/src/server/action-client.ts +1 -1
  155. package/src/server/action-handler.ts +1 -1
  156. package/src/server/actions.ts +1 -1
  157. package/src/server/als-registry.ts +7 -0
  158. package/src/server/deny-renderer.ts +3 -2
  159. package/src/server/error-boundary-wrapper.ts +1 -1
  160. package/src/server/fallback-error.ts +6 -6
  161. package/src/server/pipeline-interception.ts +1 -1
  162. package/src/server/pipeline.ts +2 -2
  163. package/src/server/primitives.ts +1 -1
  164. package/src/server/request-context.ts +7 -1
  165. package/src/server/route-element-builder.ts +1 -1
  166. package/src/server/rsc-entry/api-handler.ts +8 -8
  167. package/src/server/rsc-entry/error-renderer.ts +120 -185
  168. package/src/server/rsc-entry/helpers.ts +2 -2
  169. package/src/server/rsc-entry/index.ts +42 -38
  170. package/src/server/rsc-entry/rsc-payload.ts +6 -6
  171. package/src/server/rsc-entry/rsc-stream.ts +6 -6
  172. package/src/server/rsc-entry/ssr-bridge.ts +2 -2
  173. package/src/server/rsc-entry/ssr-renderer.ts +16 -12
  174. package/src/server/slot-resolver.ts +2 -2
  175. package/src/server/ssr-entry.ts +3 -3
  176. package/src/server/status-code-resolver.ts +1 -1
  177. package/src/server/stream-utils.ts +209 -0
  178. package/src/server/tree-builder.ts +1 -1
  179. package/src/shims/font-google.ts +1 -1
  180. package/dist/_chunks/define-cookie-k9btcEfI.js.map +0 -1
  181. package/dist/_chunks/request-context-0h-6Voad.js.map +0 -1
  182. package/dist/params/index.js.map +0 -1
  183. package/dist/server/rsc-entry/ssr-error-bridge.d.ts +0 -12
  184. package/dist/server/rsc-entry/ssr-error-bridge.d.ts.map +0 -1
  185. package/dist/server/ssr-error-entry.d.ts +0 -65
  186. package/dist/server/ssr-error-entry.d.ts.map +0 -1
  187. package/src/server/rsc-entry/ssr-error-bridge.ts +0 -20
  188. package/src/server/ssr-error-entry.ts +0 -237
@@ -1,33 +1,36 @@
1
1
  /**
2
2
  * RSC Error & No-Match Renderers — handles error pages and 404s.
3
3
  *
4
- * Error pages use two rendering paths depending on file format:
5
- * - TSX/JSX ('use client'): SSR-only render via Fizz (bypasses RSC Flight)
6
- * - MDX (server component): RSC SSR (plain props, no Error serialization issue)
4
+ * All error pages (TSX and MDX) render through the RSC → SSR pipeline:
5
+ * - TSX ('use client'): Error is converted to SerializableError, wrapped
6
+ * with ErrorReconstituter to reconstitute a real Error on the client.
7
+ * - MDX (server component): Receives plain props ({ status }), no wrapper needed.
7
8
  *
8
- * The SSR-only path exists because Error objects are not RSC-serializable.
9
- * React Flight throws "Only plain objects can be passed to Client Components"
10
- * when Error instances are passed as props to 'use client' components.
9
+ * This unified approach replaces the former SSR-only bypass for TSX error
10
+ * pages. See design/spike-TIM-565-unify-error-pages.md for the full analysis.
11
11
  *
12
12
  * Design docs: 10-error-handling.md §"Three-Tier Error Page Rendering"
13
13
  */
14
14
 
15
15
  import { createElement } from 'react';
16
- import { renderToReadableStream } from '#/rsc-runtime/rsc.js';
17
-
18
- import type { RouteMatch } from '#/server/pipeline.js';
19
- import { logRenderError } from '#/server/logger.js';
20
- import type { ManifestSegmentNode } from '#/server/route-matcher.js';
21
- import { DenySignal, RenderError } from '#/server/primitives.js';
22
- import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
23
- import { flightInitScript } from '#/server/flight-scripts.js';
24
- import { renderDenyPage } from '#/server/deny-renderer.js';
25
- import type { LayoutEntry } from '#/server/deny-renderer.js';
26
- import type { NavContext } from '#/server/ssr-entry.js';
16
+ import { renderToReadableStream } from '../../rsc-runtime/rsc.js';
17
+
18
+ import type { RouteMatch } from '../pipeline.js';
19
+ import { logRenderError } from '../logger.js';
20
+ import type { ManifestSegmentNode } from '../route-matcher.js';
21
+ import { DenySignal, RenderError } from '../primitives.js';
22
+ import type { ClientBootstrapConfig } from '../html-injectors.js';
23
+ import { flightInitScript } from '../flight-scripts.js';
24
+ import { renderDenyPage } from '../deny-renderer.js';
25
+ import type { LayoutEntry } from '../deny-renderer.js';
26
+ import type { NavContext } from '../ssr-entry.js';
27
27
  import { createDebugChannelSink } from './helpers.js';
28
- import { getCookiesForSsr } from '#/server/request-context.js';
28
+ import { getCookiesForSsr } from '../request-context.js';
29
29
  import { callSsr } from './ssr-bridge.js';
30
- import { callSsrErrorPage } from './ssr-error-bridge.js';
30
+ import { teeWithErrorPropagation } from '../stream-utils.js';
31
+ import { isDevMode } from '../debug.js';
32
+ import { ErrorReconstituter } from '../../client/error-reconstituter.js';
33
+ import type { SerializableError } from '../../client/error-reconstituter.js';
31
34
 
32
35
  /**
33
36
  * A manifest file reference with lazy import and path.
@@ -55,14 +58,12 @@ function isMdxFile(filePath: string): boolean {
55
58
  * Result of walking the segment chain for an error file.
56
59
  */
57
60
  interface ErrorFileResolution {
58
- /** The loaded component (for MDX RSC path) */
61
+ /** The loaded component */
59
62
  component: (...args: unknown[]) => unknown;
60
63
  /** Index of the segment where the file was found */
61
64
  segmentIndex: number;
62
65
  /** Whether the file is MDX (server component) */
63
66
  isMdx: boolean;
64
- /** File path for SSR-only import */
65
- filePath: string;
66
67
  }
67
68
 
68
69
  /**
@@ -91,7 +92,6 @@ async function resolveErrorFile(
91
92
  component: mod.default as (...args: unknown[]) => unknown,
92
93
  segmentIndex: i,
93
94
  isMdx: isMdxFile(specificFile.filePath),
94
- filePath: specificFile.filePath,
95
95
  };
96
96
  }
97
97
  }
@@ -105,7 +105,6 @@ async function resolveErrorFile(
105
105
  component: mod.default as (...args: unknown[]) => unknown,
106
106
  segmentIndex: i,
107
107
  isMdx: isMdxFile(categoryFile.filePath),
108
- filePath: categoryFile.filePath,
109
108
  };
110
109
  }
111
110
  }
@@ -119,7 +118,6 @@ async function resolveErrorFile(
119
118
  component: mod.default as (...args: unknown[]) => unknown,
120
119
  segmentIndex: i,
121
120
  isMdx: isMdxFile(segment.error.filePath),
122
- filePath: segment.error.filePath,
123
121
  };
124
122
  }
125
123
  }
@@ -140,12 +138,28 @@ function getLayoutsForErrorFile(
140
138
  return layoutComponents.filter((lc) => resolvedSegments.has(lc.segment));
141
139
  }
142
140
 
141
+ /**
142
+ * Convert an error to a SerializableError for RSC Flight serialization.
143
+ * Stack traces are stripped in production (gated by isDevMode).
144
+ *
145
+ * See design/13-security.md §"Debug Flag Security Boundary" — uses isDevMode(),
146
+ * not isDebug(), because this data crosses the RSC → client boundary.
147
+ */
148
+ function toSerializableError(error: unknown): SerializableError {
149
+ const err = error instanceof Error ? error : new Error(String(error));
150
+ return {
151
+ message: err.message,
152
+ name: err.name,
153
+ ...(isDevMode() && err.stack ? { stack: err.stack } : {}),
154
+ };
155
+ }
156
+
143
157
  /**
144
158
  * Render an error page for unhandled throws or RenderError outside Suspense.
145
159
  *
146
- * Uses two rendering paths:
147
- * - TSX/JSX: SSR-only (bypass RSC, render directly through Fizz)
148
- * - MDX: RSC SSR (server components need Flight, but props are plain)
160
+ * All error pages (TSX and MDX) go through RSC → SSR:
161
+ * - TSX: ErrorReconstituter wrapper converts SerializableError real Error
162
+ * - MDX: Plain props ({ status }), no wrapper needed
149
163
  */
150
164
  export async function renderErrorPage(
151
165
  error: unknown,
@@ -158,8 +172,6 @@ export async function renderErrorPage(
158
172
  clientBootstrap: ClientBootstrapConfig,
159
173
  globalError?: GlobalErrorFile
160
174
  ): Promise<Response> {
161
- const h = createElement as (...args: unknown[]) => React.ReactElement;
162
-
163
175
  // Walk segments from leaf to root to find the error component
164
176
  const resolution = await resolveErrorFile(status, segments);
165
177
 
@@ -181,28 +193,14 @@ export async function renderErrorPage(
181
193
  return new Response(null, { status, headers: responseHeaders });
182
194
  }
183
195
 
184
- const { component: errorComponent, segmentIndex, isMdx, filePath } = resolution;
196
+ const { component: errorComponent, segmentIndex, isMdx } = resolution;
185
197
  const layoutsToWrap = getLayoutsForErrorFile(segments, layoutComponents, segmentIndex);
186
198
 
187
- // MDX error pages go through RSC → SSR (server components, plain props)
188
- if (isMdx) {
189
- return renderErrorPageViaMdx(
190
- errorComponent,
191
- status,
192
- layoutsToWrap,
193
- req,
194
- match,
195
- responseHeaders,
196
- clientBootstrap,
197
- h
198
- );
199
- }
200
-
201
- // TSX error pages: SSR-only render (bypass RSC Flight)
202
- return renderErrorPageViaSsrOnly(
199
+ return renderErrorPageViaRsc(
203
200
  error,
201
+ errorComponent,
202
+ isMdx,
204
203
  status,
205
- filePath,
206
204
  layoutsToWrap,
207
205
  req,
208
206
  match,
@@ -212,77 +210,47 @@ export async function renderErrorPage(
212
210
  }
213
211
 
214
212
  /**
215
- * TSX error page: SSR-only render, bypasses RSC Flight entirely.
213
+ * Render an error page through the RSC SSR pipeline.
216
214
  *
217
- * Passes component file paths to the SSR environment which imports them
218
- * (resolving 'use client' references to actual modules) and builds the
219
- * element tree for Fizz. Error objects are passed as-is Fizz has no
220
- * serialization constraint on props.
215
+ * Handles both TSX and MDX error pages:
216
+ * - TSX: Wrapped with ErrorReconstituter to reconstitute Error on client
217
+ * - MDX: Receives plain props ({ status }), rendered as server component
221
218
  */
222
- async function renderErrorPageViaSsrOnly(
219
+ async function renderErrorPageViaRsc(
223
220
  error: unknown,
221
+ errorComponent: (...args: unknown[]) => unknown,
222
+ isMdx: boolean,
224
223
  status: number,
225
- errorFilePath: string,
226
224
  layoutsToWrap: LayoutEntry[],
227
225
  req: Request,
228
226
  match: RouteMatch,
229
227
  responseHeaders: Headers,
230
228
  clientBootstrap: ClientBootstrapConfig
231
229
  ): Promise<Response> {
232
- // Build digest prop for RenderError, null for unhandled errors
233
- const digest =
234
- error instanceof RenderError ? { code: error.code, data: error.digest.data } : null;
235
-
236
- // Collect layout file paths (root → leaf order) for SSR-env import.
237
- // Layouts have filePaths on their segment's layout ManifestFile.
238
- const layoutPaths: string[] = [];
239
- for (const lc of layoutsToWrap) {
240
- if (lc.segment.layout) {
241
- layoutPaths.push(lc.segment.layout.filePath);
242
- }
243
- }
230
+ const h = createElement as (...args: unknown[]) => React.ReactElement;
231
+ const url = new URL(req.url);
244
232
 
245
- // Render directly through Fizz in the SSR environment — no RSC Flight stream.
246
- // Do NOT inject flightInitScript() here: this path bypasses RSC Flight
247
- // entirely, so there are no data rows to consume. Injecting the [0]
248
- // bootstrap signal causes the client to call createFromReadableStream
249
- // on an empty stream, which fails. See TIM-552.
250
- return callSsrErrorPage({
251
- pathname: new URL(req.url).pathname,
252
- params: match.params,
253
- searchParams: Object.fromEntries(new URL(req.url).searchParams),
254
- statusCode: status,
255
- responseHeaders,
256
- headHtml: '',
257
- bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
258
- signal: req.signal,
259
- cookies: getCookiesForSsr(),
260
- errorProps: {
261
- error: error instanceof Error ? error : new Error(String(error)),
233
+ let element: React.ReactElement;
234
+
235
+ if (isMdx) {
236
+ // MDX error pages receive plain props no Error serialization issue
237
+ element = h(errorComponent, { status });
238
+ } else {
239
+ // TSX error pages: wrap with ErrorReconstituter for Error reconstitution.
240
+ // The ErrorReconstituter is a 'use client' component that receives
241
+ // SerializableError (plain object) and reconstitutes a real Error
242
+ // before passing to the user's error component.
243
+ const serializedError = toSerializableError(error);
244
+ const digest =
245
+ error instanceof RenderError ? { code: error.code, data: error.digest.data } : null;
246
+
247
+ element = h(ErrorReconstituter, {
248
+ error: serializedError,
262
249
  digest,
263
250
  reset: undefined,
264
- },
265
- errorComponentPath: errorFilePath,
266
- layoutPaths,
267
- });
268
- }
269
-
270
- /**
271
- * MDX error page: RSC → SSR render. MDX is a server component and needs
272
- * the Flight pipeline, but it receives plain props (no Error object).
273
- */
274
- async function renderErrorPageViaMdx(
275
- errorComponent: (...args: unknown[]) => unknown,
276
- status: number,
277
- layoutsToWrap: LayoutEntry[],
278
- req: Request,
279
- match: RouteMatch,
280
- responseHeaders: Headers,
281
- clientBootstrap: ClientBootstrapConfig,
282
- h: (...args: unknown[]) => React.ReactElement
283
- ): Promise<Response> {
284
- // MDX error pages receive {} or { status } — no Error object
285
- let element = h(errorComponent, { status });
251
+ component: errorComponent,
252
+ });
253
+ }
286
254
 
287
255
  // Wrap in layouts
288
256
  for (let i = layoutsToWrap.length - 1; i >= 0; i--) {
@@ -290,39 +258,43 @@ async function renderErrorPageViaMdx(
290
258
  element = h(component, null, element);
291
259
  }
292
260
 
293
- // Render through RSC → SSR (same as deny pages)
294
- const rscStream = renderToReadableStream(element, {
295
- onError(err: unknown) {
296
- logRenderError({ method: req.method, path: new URL(req.url).pathname, error: err });
297
- },
298
- debugChannel: createDebugChannelSink(),
299
- });
300
-
301
- const [ssrStream, inlineStream] = rscStream.tee();
302
-
303
- const navContext: NavContext = {
304
- pathname: new URL(req.url).pathname,
305
- params: match.params,
306
- searchParams: Object.fromEntries(new URL(req.url).searchParams),
307
- statusCode: status,
308
- responseHeaders,
309
- headHtml: flightInitScript(),
310
- bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
311
- rscStream: inlineStream,
312
- cookies: getCookiesForSsr(),
313
- };
261
+ // Render through RSC → SSR (same path as deny pages)
262
+ try {
263
+ const rscStream = renderToReadableStream(element, {
264
+ onError(err: unknown) {
265
+ logRenderError({ method: req.method, path: url.pathname, error: err });
266
+ },
267
+ debugChannel: createDebugChannelSink(),
268
+ });
269
+
270
+ const [ssrStream, inlineStream] = teeWithErrorPropagation(rscStream);
314
271
 
315
- return callSsr(ssrStream, navContext);
272
+ const navContext: NavContext = {
273
+ pathname: url.pathname,
274
+ params: match.params,
275
+ searchParams: Object.fromEntries(url.searchParams),
276
+ statusCode: status,
277
+ responseHeaders,
278
+ headHtml: flightInitScript(),
279
+ bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
280
+ rscStream: inlineStream,
281
+ cookies: getCookiesForSsr(),
282
+ };
283
+
284
+ return callSsr(ssrStream, navContext);
285
+ } catch (renderError) {
286
+ // Error page itself failed to render — fall through to bare response.
287
+ // This guards against recursive errors in user error page code.
288
+ logRenderError({ method: req.method, path: url.pathname, error: renderError });
289
+ return new Response(null, { status, headers: responseHeaders });
290
+ }
316
291
  }
317
292
 
318
293
  /**
319
294
  * Tier 2 — Render global-error.tsx as a standalone full-page replacement.
320
295
  *
321
296
  * No layout wrapping. The component must provide its own <html> and <body>.
322
- * Uses SSR-only render (bypasses RSC Flight) since global-error.tsx is
323
- * a 'use client' component receiving { error, digest, reset }.
324
- *
325
- * MDX global-error files go through RSC → SSR with plain props.
297
+ * All formats (TSX and MDX) go through RSC → SSR.
326
298
  *
327
299
  * See design/10-error-handling.md §"Tier 2 — Global Error Page"
328
300
  */
@@ -335,63 +307,26 @@ async function renderGlobalErrorPage(
335
307
  responseHeaders: Headers,
336
308
  clientBootstrap: ClientBootstrapConfig
337
309
  ): Promise<Response> {
338
- const h = createElement as (...args: unknown[]) => React.ReactElement;
339
-
340
- if (isMdxFile(globalError.filePath)) {
341
- // MDX global-error: RSC → SSR with plain props (no Error object)
342
- const mod = (await globalError.load()) as Record<string, unknown>;
343
- if (!mod.default) {
344
- return new Response(null, { status, headers: responseHeaders });
345
- }
346
- const component = mod.default as (...args: unknown[]) => unknown;
347
- const element = h(component, { status });
348
-
349
- const rscStream = renderToReadableStream(element, {
350
- onError(err: unknown) {
351
- logRenderError({ method: req.method, path: new URL(req.url).pathname, error: err });
352
- },
353
- debugChannel: createDebugChannelSink(),
354
- });
355
-
356
- const [ssrStream, inlineStream] = rscStream.tee();
357
-
358
- const navContext: NavContext = {
359
- pathname: new URL(req.url).pathname,
360
- params: match.params,
361
- searchParams: Object.fromEntries(new URL(req.url).searchParams),
362
- statusCode: status,
363
- responseHeaders,
364
- headHtml: flightInitScript(),
365
- bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
366
- rscStream: inlineStream,
367
- cookies: getCookiesForSsr(),
368
- };
369
-
370
- return callSsr(ssrStream, navContext);
310
+ const mod = (await globalError.load()) as Record<string, unknown>;
311
+ if (!mod.default) {
312
+ return new Response(null, { status, headers: responseHeaders });
371
313
  }
372
314
 
373
- // TSX/JSX global-error: SSR-only render, no layouts
374
- const digest =
375
- error instanceof RenderError ? { code: error.code, data: error.digest.data } : null;
315
+ const component = mod.default as (...args: unknown[]) => unknown;
316
+ const isMdx = isMdxFile(globalError.filePath);
376
317
 
377
- return callSsrErrorPage({
378
- pathname: new URL(req.url).pathname,
379
- params: match.params,
380
- searchParams: Object.fromEntries(new URL(req.url).searchParams),
381
- statusCode: status,
318
+ // Reuse the unified RSC path with no layouts
319
+ return renderErrorPageViaRsc(
320
+ error,
321
+ component,
322
+ isMdx,
323
+ status,
324
+ [], // No layouts — global-error is standalone
325
+ req,
326
+ match,
382
327
  responseHeaders,
383
- headHtml: '',
384
- bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
385
- signal: req.signal,
386
- cookies: getCookiesForSsr(),
387
- errorProps: {
388
- error: error instanceof Error ? error : new Error(String(error)),
389
- digest,
390
- reset: undefined,
391
- },
392
- errorComponentPath: globalError.filePath,
393
- layoutPaths: [], // No layouts — global-error is standalone
394
- });
328
+ clientBootstrap
329
+ );
395
330
  }
396
331
 
397
332
  /**
@@ -4,8 +4,8 @@
4
4
  * Small, stateless functions used across the RSC entry modules.
5
5
  */
6
6
 
7
- import type { ManifestSegmentNode } from '#/server/route-matcher.js';
8
- import { RedirectSignal } from '#/server/primitives.js';
7
+ import type { ManifestSegmentNode } from '../route-matcher.js';
8
+ import { RedirectSignal } from '../primitives.js';
9
9
 
10
10
  /** RSC content type for client navigation payload requests. */
11
11
  export const RSC_CONTENT_TYPE = 'text/x-component';
@@ -24,38 +24,38 @@ import buildManifest from 'virtual:timber-build-manifest';
24
24
  // @ts-expect-error — virtual module provided by timber-entries plugin
25
25
  import loadUserInstrumentation from 'virtual:timber-instrumentation';
26
26
 
27
- import type { FormRerender } from '#/server/action-handler.js';
28
- import { handleActionRequest, isActionRequest } from '#/server/action-handler.js';
29
- import type { BodyLimitsConfig } from '#/server/body-limits.js';
30
- import type { BuildManifest } from '#/server/build-manifest.js';
27
+ import type { FormRerender } from '../action-handler.js';
28
+ import { handleActionRequest, isActionRequest } from '../action-handler.js';
29
+ import type { BodyLimitsConfig } from '../body-limits.js';
30
+ import type { BuildManifest } from '../build-manifest.js';
31
31
  import {
32
32
  buildFontPreloadTags,
33
33
  buildModulepreloadTags,
34
34
  collectRouteFonts,
35
35
  collectRouteModulepreloads,
36
- } from '#/server/build-manifest.js';
37
- import type { LayoutEntry } from '#/server/deny-renderer.js';
38
- import { renderDenyPage, renderDenyPageAsRsc } from '#/server/deny-renderer.js';
39
- import { resolveLogMode } from '#/server/dev-logger.js';
40
- import { sendEarlyHints103 } from '#/server/early-hints-sender.js';
41
- import { collectEarlyHintHeaders } from '#/server/early-hints.js';
42
- import { runWithFormFlash } from '#/server/form-flash.js';
43
- import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
44
- import { buildClientScripts } from '#/server/html-injectors.js';
45
- import type { InterceptionContext, PipelineConfig, RouteMatch } from '#/server/pipeline.js';
46
- import { createPipeline, coerceSegmentParams } from '#/server/pipeline.js';
47
- import { DenySignal, RedirectSignal } from '#/server/primitives.js';
36
+ } from '../build-manifest.js';
37
+ import type { LayoutEntry } from '../deny-renderer.js';
38
+ import { renderDenyPage, renderDenyPageAsRsc } from '../deny-renderer.js';
39
+ import { resolveLogMode } from '../dev-logger.js';
40
+ import { sendEarlyHints103 } from '../early-hints-sender.js';
41
+ import { collectEarlyHintHeaders } from '../early-hints.js';
42
+ import { runWithFormFlash } from '../form-flash.js';
43
+ import type { ClientBootstrapConfig } from '../html-injectors.js';
44
+ import { buildClientScripts } from '../html-injectors.js';
45
+ import type { InterceptionContext, PipelineConfig, RouteMatch } from '../pipeline.js';
46
+ import { createPipeline, coerceSegmentParams } from '../pipeline.js';
47
+ import { DenySignal, RedirectSignal } from '../primitives.js';
48
48
  import {
49
49
  buildRouteElement,
50
50
  RouteSignalWithContext,
51
51
  ParamCoercionError,
52
- } from '#/server/route-element-builder.js';
53
- import type { ManifestSegmentNode } from '#/server/route-matcher.js';
54
- import { createMetadataRouteMatcher, createRouteMatcher } from '#/server/route-matcher.js';
55
- import { initDevTracing } from '#/server/tracing.js';
52
+ } from '../route-element-builder.js';
53
+ import type { ManifestSegmentNode } from '../route-matcher.js';
54
+ import { createMetadataRouteMatcher, createRouteMatcher } from '../route-matcher.js';
55
+ import { initDevTracing } from '../tracing.js';
56
56
 
57
- import { renderFallbackError as renderFallback } from '#/server/fallback-error.js';
58
- import { loadInstrumentation } from '#/server/instrumentation.js';
57
+ import { renderFallbackError as renderFallback } from '../fallback-error.js';
58
+ import { loadInstrumentation } from '../instrumentation.js';
59
59
  import { handleApiRoute } from './api-handler.js';
60
60
  import { renderErrorPage, renderNoMatchPage } from './error-renderer.js';
61
61
  import {
@@ -65,13 +65,14 @@ import {
65
65
  isRscPayloadRequest,
66
66
  type DebugComponentEntry,
67
67
  } from './helpers.js';
68
- import { parseClientStateTree } from '#/server/state-tree-diff.js';
68
+ import { parseClientStateTree } from '../state-tree-diff.js';
69
69
  import { buildRscPayloadResponse } from './rsc-payload.js';
70
70
  import { renderRscStream } from './rsc-stream.js';
71
71
  import { renderSsrResponse } from './ssr-renderer.js';
72
72
  import { callSsr } from './ssr-bridge.js';
73
- import { isDebug, isDevMode, setDebugFromConfig } from '#/server/debug.js';
74
- import { recordTiming } from '#/server/server-timing.js';
73
+ import { isDebug, isDevMode, setDebugFromConfig } from '../debug.js';
74
+ import { recordTiming } from '../server-timing.js';
75
+ import { requestContextAls } from '../als-registry.js';
75
76
 
76
77
  /**
77
78
  * Resolve the Server-Timing mode from timber.config.ts.
@@ -113,11 +114,9 @@ export function setDevPipelineErrorHandler(
113
114
  _devPipelineErrorHandler = handler;
114
115
  }
115
116
 
116
- // Dev-only: holds a getter for the current request's RSC debug components.
117
- // Updated on each renderRscStream call so the onPipelineError callback can
118
- // include component tree context for render-phase errors. This is request-
119
- // scoped by convention — each renderRoute call sets it before returning.
120
- let _lastDebugComponentsGetter: (() => DebugComponentEntry[]) | undefined;
117
+ // Dev-only: debug components getter is stored per-request in ALS
118
+ // (RequestContextStore.debugComponentsGetter) to avoid cross-request
119
+ // race conditions. See TIM-557.
121
120
 
122
121
  /**
123
122
  * Create the RSC request handler from the route manifest.
@@ -139,7 +138,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
139
138
  | string
140
139
  | undefined;
141
140
  if (deploymentId) {
142
- const { setDeploymentId } = await import('#/server/version-skew.js');
141
+ const { setDeploymentId } = await import('../version-skew.js');
143
142
  setDeploymentId(deploymentId);
144
143
  }
145
144
 
@@ -251,9 +250,10 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
251
250
  if (_devPipelineErrorHandler) {
252
251
  // For render-phase errors, include RSC debug component data
253
252
  // from the Flight debug channel (if available from the current request).
253
+ const store = requestContextAls.getStore();
254
254
  const debugComponents =
255
- phase === 'render' && _lastDebugComponentsGetter
256
- ? _lastDebugComponentsGetter()
255
+ phase === 'render' && store?.debugComponentsGetter
256
+ ? store.debugComponentsGetter()
257
257
  : undefined;
258
258
  _devPipelineErrorHandler(error, phase, debugComponents);
259
259
  }
@@ -487,9 +487,13 @@ async function renderRoute(
487
487
  const _rscStart = performance.now();
488
488
  const { rscStream, signals, getDebugComponents } = renderRscStream(element, _req);
489
489
 
490
- // Store the debug components getter so onPipelineError can include
491
- // component tree context for render-phase errors (dev mode only).
492
- _lastDebugComponentsGetter = getDebugComponents;
490
+ // Store the debug components getter in ALS so onPipelineError can
491
+ // include component tree context for render-phase errors (dev mode only).
492
+ // Per-request via ALS — no cross-request race. See TIM-557.
493
+ const alsStore = requestContextAls.getStore();
494
+ if (alsStore) {
495
+ alsStore.debugComponentsGetter = getDebugComponents;
496
+ }
493
497
  recordTiming({
494
498
  name: 'rsc-init',
495
499
  dur: Math.round(performance.now() - _rscStart),
@@ -580,10 +584,10 @@ async function renderRoute(
580
584
 
581
585
  // Re-export for generated entry points (e.g., Nitro node-server/bun) to wrap
582
586
  // the handler with per-request 103 Early Hints sender via ALS.
583
- export { runWithEarlyHintsSender } from '#/server/early-hints-sender.js';
587
+ export { runWithEarlyHintsSender } from '../early-hints-sender.js';
584
588
 
585
589
  // Re-export for generated entry points to wrap the handler with per-request
586
590
  // waitUntil support via ALS. See design/11-platform.md §"waitUntil()".
587
- export { runWithWaitUntil } from '#/server/waituntil-bridge.js';
591
+ export { runWithWaitUntil } from '../waituntil-bridge.js';
588
592
 
589
593
  export default await createRequestHandler(routeManifest, config);
@@ -9,12 +9,12 @@
9
9
  * 16-metadata.md §"Head Elements"
10
10
  */
11
11
 
12
- import type { LayoutEntry } from '#/server/deny-renderer.js';
13
- import { renderDenyPageAsRsc } from '#/server/deny-renderer.js';
14
- import type { RouteMatch } from '#/server/pipeline.js';
15
- import type { RedirectSignal } from '#/server/primitives.js';
16
- import type { HeadElement, LayoutComponentEntry } from '#/server/route-element-builder.js';
17
- import type { ManifestSegmentNode } from '#/server/route-matcher.js';
12
+ import type { LayoutEntry } from '../deny-renderer.js';
13
+ import { renderDenyPageAsRsc } from '../deny-renderer.js';
14
+ import type { RouteMatch } from '../pipeline.js';
15
+ import type { RedirectSignal } from '../primitives.js';
16
+ import type { HeadElement, LayoutComponentEntry } from '../route-element-builder.js';
17
+ import type { ManifestSegmentNode } from '../route-matcher.js';
18
18
 
19
19
  import {
20
20
  buildRedirectResponse,
@@ -10,13 +10,13 @@
10
10
  * 13-security.md §"Errors don't leak"
11
11
  */
12
12
 
13
- import { renderToReadableStream } from '#/rsc-runtime/rsc.js';
13
+ import { renderToReadableStream } from '../../rsc-runtime/rsc.js';
14
14
 
15
15
  import { randomUUID } from 'node:crypto';
16
16
 
17
- import { logRenderError } from '#/server/logger.js';
18
- import { DenySignal, RedirectSignal, RenderError } from '#/server/primitives.js';
19
- import { checkAndWarnRscPropError } from '#/server/rsc-prop-warnings.js';
17
+ import { logRenderError } from '../logger.js';
18
+ import { DenySignal, RedirectSignal, RenderError } from '../primitives.js';
19
+ import { checkAndWarnRscPropError } from '../rsc-prop-warnings.js';
20
20
 
21
21
  import {
22
22
  createDebugChannelSink,
@@ -24,8 +24,8 @@ import {
24
24
  isAbortError,
25
25
  type DebugComponentEntry,
26
26
  } from './helpers.js';
27
- import { isDebug } from '#/server/debug.js';
28
- import { isDevMode } from '#/server/debug.js';
27
+ import { isDebug } from '../debug.js';
28
+ import { isDevMode } from '../debug.js';
29
29
 
30
30
  /**
31
31
  * Mutable signal state captured during RSC rendering.
@@ -4,13 +4,13 @@
4
4
 
5
5
  /// <reference types="@vitejs/plugin-rsc/types" />
6
6
 
7
- import type { NavContext } from '#/server/ssr-entry.js';
7
+ import type { NavContext } from '../ssr-entry.js';
8
8
 
9
9
  export async function callSsr(
10
10
  rscStream: ReadableStream<Uint8Array>,
11
11
  navContext: NavContext
12
12
  ): Promise<Response> {
13
- const ssrEntry = await import.meta.viteRsc.import<typeof import('#/server/ssr-entry.js')>(
13
+ const ssrEntry = await import.meta.viteRsc.import<typeof import('../ssr-entry.js')>(
14
14
  '../ssr-entry.js',
15
15
  { environment: 'ssr' }
16
16
  );