@timber-js/app 0.2.0-alpha.200 → 0.2.0-alpha.202

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 (143) hide show
  1. package/bin/timber.mjs +15 -1
  2. package/dist/_chunks/{actions-d1hCqnU3.js → actions-HUdJADAD.js} +3 -3
  3. package/dist/_chunks/{actions-d1hCqnU3.js.map → actions-HUdJADAD.js.map} +1 -1
  4. package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
  5. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  6. package/dist/_chunks/{cache-api-ByagcC-J.js → cache-api-CAPbZTga.js} +2 -2
  7. package/dist/_chunks/{cache-api-ByagcC-J.js.map → cache-api-CAPbZTga.js.map} +1 -1
  8. package/dist/_chunks/{chains-Bpb0W4ax.js → chains-CBNA0Ozj.js} +3 -3
  9. package/dist/_chunks/{chains-Bpb0W4ax.js.map → chains-CBNA0Ozj.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-D6VolrDV.js → cli-check-BzMGuIH6.js} +3 -3
  11. package/dist/_chunks/{cli-check-D6VolrDV.js.map → cli-check-BzMGuIH6.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js → cli-schema-sync-DnXqcIIj.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js.map → cli-schema-sync-DnXqcIIj.js.map} +1 -1
  14. package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-DxX1SU0g.js} +3 -3
  15. package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-DxX1SU0g.js.map} +1 -1
  16. package/dist/_chunks/{convention-lint-fRkwVwEH.js → convention-lint-DHOFvX5s.js} +5 -95
  17. package/dist/_chunks/convention-lint-DHOFvX5s.js.map +1 -0
  18. package/dist/_chunks/csp-nonce-hOGniaG4.js +227 -0
  19. package/dist/_chunks/csp-nonce-hOGniaG4.js.map +1 -0
  20. package/dist/_chunks/dev-server-DioP7tkQ.js +2288 -0
  21. package/dist/_chunks/dev-server-DioP7tkQ.js.map +1 -0
  22. package/dist/_chunks/{error-boundary-BfPHZjm0.js → error-boundary-BQKxl6EX.js} +11 -17
  23. package/dist/_chunks/{error-boundary-BfPHZjm0.js.map → error-boundary-BQKxl6EX.js.map} +1 -1
  24. package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-Cv3njEH8.js} +2 -2
  25. package/dist/_chunks/{graph-cache-CP4GEmf9.js.map → graph-cache-Cv3njEH8.js.map} +1 -1
  26. package/dist/_chunks/{live-graph-D_2D32Ad.js → live-graph-VjHFF5EV.js} +3 -3
  27. package/dist/_chunks/{live-graph-D_2D32Ad.js.map → live-graph-VjHFF5EV.js.map} +1 -1
  28. package/dist/_chunks/{logger-uLBuGKDI.js → logger-DqJ2VoAY.js} +450 -458
  29. package/dist/_chunks/logger-DqJ2VoAY.js.map +1 -0
  30. package/dist/_chunks/{segment-keys-lqtdookO.js → metadata-routes-DSDjM_hJ.js} +2 -61
  31. package/dist/_chunks/metadata-routes-DSDjM_hJ.js.map +1 -0
  32. package/dist/_chunks/{poison-scan-Bm9Yyqk9.js → poison-scan-lEbz4pQE.js} +2 -2
  33. package/dist/_chunks/{poison-scan-Bm9Yyqk9.js.map → poison-scan-lEbz4pQE.js.map} +1 -1
  34. package/dist/_chunks/{scanner-AiazgH_f.js → scanner-B_tnqFcF.js} +37 -137
  35. package/dist/_chunks/scanner-B_tnqFcF.js.map +1 -0
  36. package/dist/_chunks/segment-keys-D5hu1hz4.js +62 -0
  37. package/dist/_chunks/segment-keys-D5hu1hz4.js.map +1 -0
  38. package/dist/_chunks/tree-match-CdbvYTBz.js +122 -0
  39. package/dist/_chunks/tree-match-CdbvYTBz.js.map +1 -0
  40. package/dist/_chunks/{walkers-B6XUtmqK.js → walkers-Cm3PC5JT.js} +2 -2
  41. package/dist/_chunks/{walkers-B6XUtmqK.js.map → walkers-Cm3PC5JT.js.map} +1 -1
  42. package/dist/adapters/cloudflare-dev.js +1 -1
  43. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  44. package/dist/adapters/cloudflare.js +1 -1
  45. package/dist/analyze/crawl-entry.js +2 -2
  46. package/dist/analyze/graph-command.js +3 -3
  47. package/dist/cache/index.js +1 -1
  48. package/dist/cli.d.ts +7 -2
  49. package/dist/cli.d.ts.map +1 -1
  50. package/dist/cli.js +44 -7
  51. package/dist/cli.js.map +1 -1
  52. package/dist/client/browser-dev.d.ts +5 -1
  53. package/dist/client/browser-dev.d.ts.map +1 -1
  54. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  55. package/dist/client/browser-entry/action-queue.d.ts +20 -1
  56. package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
  57. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  58. package/dist/client/error-boundary.d.ts +3 -15
  59. package/dist/client/error-boundary.d.ts.map +1 -1
  60. package/dist/client/error-boundary.js +1 -1
  61. package/dist/client/error-reconstituter.d.ts +4 -4
  62. package/dist/client/error-reconstituter.d.ts.map +1 -1
  63. package/dist/client/internal.js +1 -1
  64. package/dist/dev-tools/debug-channel.d.ts +55 -0
  65. package/dist/dev-tools/debug-channel.d.ts.map +1 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +302 -2245
  68. package/dist/index.js.map +1 -1
  69. package/dist/plugins/dev-server.d.ts +7 -0
  70. package/dist/plugins/dev-server.d.ts.map +1 -1
  71. package/dist/plugins/mdx.d.ts.map +1 -1
  72. package/dist/routing/convention-lint.d.ts.map +1 -1
  73. package/dist/routing/index.js +2 -2
  74. package/dist/routing/scanner.d.ts +4 -4
  75. package/dist/routing/scanner.d.ts.map +1 -1
  76. package/dist/server/access-gate.d.ts +2 -2
  77. package/dist/server/deny-boundary.d.ts +3 -5
  78. package/dist/server/deny-boundary.d.ts.map +1 -1
  79. package/dist/server/deny-renderer.d.ts +0 -1
  80. package/dist/server/deny-renderer.d.ts.map +1 -1
  81. package/dist/server/error-boundary-wrapper.d.ts +5 -12
  82. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  83. package/dist/server/index.js +2 -2
  84. package/dist/server/internal.js +20 -195
  85. package/dist/server/internal.js.map +1 -1
  86. package/dist/server/pipeline.d.ts +8 -0
  87. package/dist/server/pipeline.d.ts.map +1 -1
  88. package/dist/server/primitives.d.ts +15 -13
  89. package/dist/server/primitives.d.ts.map +1 -1
  90. package/dist/server/rsc-entry/error-renderer.d.ts +3 -7
  91. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  92. package/dist/server/rsc-entry/helpers.d.ts +6 -27
  93. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  94. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  95. package/dist/server/rsc-entry/render-route.d.ts +1 -0
  96. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  97. package/dist/server/rsc-entry/rsc-stream.d.ts +4 -1
  98. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  99. package/dist/server/rsc-entry/ssr-renderer.d.ts +1 -0
  100. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  101. package/dist/server/slot-resolver.d.ts.map +1 -1
  102. package/dist/server/slot-subtree-contain.d.ts +1 -1
  103. package/dist/server/slot-subtree-contain.d.ts.map +1 -1
  104. package/docs/api/30-api-server.mdx +5 -5
  105. package/docs/api/34-api-config.mdx +2 -2
  106. package/docs/api/36-cli.mdx +1 -1
  107. package/docs/learn/12-error-handling.mdx +7 -8
  108. package/package.json +2 -3
  109. package/src/cli.ts +62 -8
  110. package/src/client/browser-dev.ts +35 -3
  111. package/src/client/browser-entry/action-dispatch.ts +12 -0
  112. package/src/client/browser-entry/action-queue.ts +70 -8
  113. package/src/client/browser-entry/rsc-stream.ts +78 -22
  114. package/src/client/error-boundary.tsx +13 -39
  115. package/src/client/error-reconstituter.tsx +5 -5
  116. package/src/dev-tools/debug-channel.ts +151 -0
  117. package/src/index.ts +8 -12
  118. package/src/plugins/dev-server.ts +58 -7
  119. package/src/plugins/mdx.ts +2 -1
  120. package/src/routing/convention-lint.ts +4 -111
  121. package/src/routing/scanner.ts +33 -22
  122. package/src/server/access-gate.tsx +3 -3
  123. package/src/server/deny-boundary.ts +5 -23
  124. package/src/server/deny-renderer.ts +4 -7
  125. package/src/server/error-boundary-wrapper.ts +11 -35
  126. package/src/server/pipeline.ts +9 -0
  127. package/src/server/primitives.ts +20 -26
  128. package/src/server/rsc-entry/error-renderer.ts +15 -43
  129. package/src/server/rsc-entry/helpers.ts +10 -67
  130. package/src/server/rsc-entry/index.ts +1 -0
  131. package/src/server/rsc-entry/render-route.ts +12 -1
  132. package/src/server/rsc-entry/rsc-stream.ts +27 -24
  133. package/src/server/rsc-entry/ssr-renderer.ts +7 -1
  134. package/src/server/slot-resolver.ts +29 -7
  135. package/src/server/slot-subtree-contain.ts +13 -4
  136. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
  137. package/dist/_chunks/convention-lint-fRkwVwEH.js.map +0 -1
  138. package/dist/_chunks/logger-uLBuGKDI.js.map +0 -1
  139. package/dist/_chunks/scanner-AiazgH_f.js.map +0 -1
  140. package/dist/_chunks/segment-keys-lqtdookO.js.map +0 -1
  141. package/dist/server/utils/mdx-file.d.ts +0 -17
  142. package/dist/server/utils/mdx-file.d.ts.map +0 -1
  143. package/src/server/utils/mdx-file.ts +0 -22
@@ -1,13 +1,9 @@
1
1
  /**
2
2
  * RSC Error & No-Match Renderers — handles error pages and 404s.
3
3
  *
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.
8
- *
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.
4
+ * Error pages are 'use client' components rendered through the RSC → SSR
5
+ * pipeline: the Error is converted to SerializableError, wrapped with
6
+ * ErrorReconstituter to reconstitute a real Error on the client.
11
7
  *
12
8
  * Design docs: 10-error-handling.md §"Three-Tier Error Page Rendering"
13
9
  */
@@ -38,7 +34,6 @@ import { ErrorReconstituter } from '../../client/error-reconstituter.tsx';
38
34
  import type { SerializableError } from '../../client/error-reconstituter.tsx';
39
35
  import { loadModule } from '../safe-load.ts';
40
36
  import { sourceMapError } from '../dev-source-map.ts';
41
- import { isMdxFilePath as isMdxFile } from '../utils/mdx-file.ts';
42
37
  import { defaultStatusPageResponse } from '../default-status-page.ts';
43
38
  import { rscErrorEnvelope } from '../rsc-error-envelope.ts';
44
39
 
@@ -60,8 +55,6 @@ interface ErrorFileResolution {
60
55
  component: (...args: unknown[]) => unknown;
61
56
  /** Index of the segment where the file was found */
62
57
  segmentIndex: number;
63
- /** Whether the file is MDX (server component) */
64
- isMdx: boolean;
65
58
  }
66
59
 
67
60
  /**
@@ -95,7 +88,6 @@ async function resolveErrorFile(
95
88
  return {
96
89
  component: mod.default as (...args: unknown[]) => unknown,
97
90
  segmentIndex: i,
98
- isMdx: isMdxFile(specificFile.filePath),
99
91
  };
100
92
  }
101
93
  }
@@ -108,7 +100,6 @@ async function resolveErrorFile(
108
100
  return {
109
101
  component: mod.default as (...args: unknown[]) => unknown,
110
102
  segmentIndex: i,
111
- isMdx: isMdxFile(categoryFile.filePath),
112
103
  };
113
104
  }
114
105
  }
@@ -121,7 +112,6 @@ async function resolveErrorFile(
121
112
  return {
122
113
  component: mod.default as (...args: unknown[]) => unknown,
123
114
  segmentIndex: i,
124
- isMdx: isMdxFile(segment.error.filePath),
125
115
  };
126
116
  }
127
117
  }
@@ -236,7 +226,7 @@ export async function renderErrorPage(
236
226
  return defaultStatusPageResponse(status, responseHeaders);
237
227
  }
238
228
 
239
- const { component: errorComponent, segmentIndex, isMdx } = resolution;
229
+ const { component: errorComponent, segmentIndex } = resolution;
240
230
  const layoutsToWrap = getLayoutsForErrorFile(
241
231
  segments,
242
232
  layoutComponents,
@@ -247,7 +237,6 @@ export async function renderErrorPage(
247
237
  return renderErrorPageViaRsc(
248
238
  error,
249
239
  errorComponent,
250
- isMdx,
251
240
  status,
252
241
  layoutsToWrap,
253
242
  req,
@@ -260,14 +249,12 @@ export async function renderErrorPage(
260
249
  /**
261
250
  * Render an error page through the RSC → SSR pipeline.
262
251
  *
263
- * Handles both TSX and MDX error pages:
264
- * - TSX: Wrapped with ErrorReconstituter to reconstitute Error on client
265
- * - MDX: Receives plain props ({ status }), rendered as server component
252
+ * Error pages are 'use client' components wrapped with ErrorReconstituter
253
+ * to reconstitute a real Error instance on the client.
266
254
  */
267
255
  async function renderErrorPageViaRsc(
268
256
  error: unknown,
269
257
  errorComponent: (...args: unknown[]) => unknown,
270
- isMdx: boolean,
271
258
  status: number,
272
259
  layoutsToWrap: LayoutEntry[],
273
260
  req: Request,
@@ -278,26 +265,15 @@ async function renderErrorPageViaRsc(
278
265
  const h = createElement as (...args: unknown[]) => React.ReactElement;
279
266
  const url = new URL(req.url);
280
267
 
281
- let element: React.ReactElement;
282
-
283
- if (isMdx) {
284
- // MDX error pages receive plain props — no Error serialization issue
285
- element = h(errorComponent, { status });
286
- } else {
287
- // TSX error pages: wrap with ErrorReconstituter for Error reconstitution.
288
- // The ErrorReconstituter is a 'use client' component that receives
289
- // SerializableError (plain object) and reconstitutes a real Error
290
- // before passing to the user's error component.
291
- const serializedError = toSerializableError(error);
292
- const digest = isRenderError(error) ? { code: error.code, data: error.digest.data } : null;
293
-
294
- element = h(ErrorReconstituter, {
295
- error: serializedError,
296
- digest,
297
- reset: undefined,
298
- component: errorComponent,
299
- });
300
- }
268
+ const serializedError = toSerializableError(error);
269
+ const digest = isRenderError(error) ? { code: error.code, data: error.digest.data } : null;
270
+
271
+ let element: React.ReactElement = h(ErrorReconstituter, {
272
+ error: serializedError,
273
+ digest,
274
+ reset: undefined,
275
+ component: errorComponent,
276
+ });
301
277
 
302
278
  // Wrap in layouts
303
279
  for (let i = layoutsToWrap.length - 1; i >= 0; i--) {
@@ -358,7 +334,6 @@ async function renderErrorPageViaRsc(
358
334
  * Tier 2 — Render global-error.tsx as a standalone full-page replacement.
359
335
  *
360
336
  * No layout wrapping. The component must provide its own <html> and <body>.
361
- * All formats (TSX and MDX) go through RSC → SSR.
362
337
  *
363
338
  * See design/10-error-handling.md §"Tier 2 — Global Error Page"
364
339
  */
@@ -380,13 +355,10 @@ async function renderGlobalErrorPage(
380
355
  }
381
356
 
382
357
  const component = mod.default as (...args: unknown[]) => unknown;
383
- const isMdx = isMdxFile(globalError.filePath);
384
358
 
385
- // Reuse the unified RSC path with no layouts
386
359
  return renderErrorPageViaRsc(
387
360
  error,
388
361
  component,
389
- isMdx,
390
362
  status,
391
363
  [], // No layouts — global-error is standalone
392
364
  req,
@@ -15,22 +15,21 @@ import { isStaticRequestDependent } from '../request-dep.ts';
15
15
  /**
16
16
  * Create a debug channel sink that discards all debug data.
17
17
  *
18
- * React Flight's dev mode serializes server component source code as `$E`
19
- * entries for DevTools. Without a separate debugChannel, this data is
20
- * embedded inline in the main RSC stream — leaking source code to the
21
- * browser. By providing a debug channel, debug data goes to a separate
22
- * stream that we drain and discard.
18
+ * Used in production to divert debug rows ($E source entries, component
19
+ * info) away from the main Flight stream. In dev mode, the debug channel
20
+ * is omitted entirely so debug rows (owner stacks) ride the main stream
21
+ * and reach the browser for console warning resolution.
23
22
  *
24
- * See design/13-security.md §"Server component source leak"
23
+ * Only provides `writable` — omitting `readable` avoids Flight's
24
+ * keepDebugAlive mode which holds deferred debug refs open per request.
25
25
  */
26
- export function createDebugChannelSink(): { readable: ReadableStream; writable: WritableStream } {
26
+ export function createDebugChannelSink(): { writable: WritableStream } {
27
27
  const sink = new TransformStream();
28
28
  // Drain the readable side so the writable never back-pressures.
29
29
  sink.readable.pipeTo(new WritableStream()).catch(() => {});
30
- return {
31
- readable: new ReadableStream(), // no commands to send to Flight
32
- writable: sink.writable,
33
- };
30
+ // Only writable — omitting `readable` avoids Flight's keepDebugAlive
31
+ // mode which holds deferred debug refs open per request.
32
+ return { writable: sink.writable };
34
33
  }
35
34
 
36
35
  // ─── Debug Channel Collector (dev mode only) ────────────────────────────
@@ -49,62 +48,6 @@ export interface DebugComponentEntry {
49
48
  stack: unknown[] | null;
50
49
  }
51
50
 
52
- /**
53
- * A debug channel that collects Flight debug rows instead of discarding them.
54
- *
55
- * Used in dev mode to capture server component tree information for the
56
- * Vite error overlay. The collector provides the same `{ readable, writable }`
57
- * shape as the discard sink, plus methods to retrieve collected data.
58
- *
59
- * Security: only component names, environments, and stack frames are
60
- * extracted — props and source code are stripped. In production builds,
61
- * use `createDebugChannelSink()` instead (this function is never called).
62
- */
63
- export interface DebugChannelCollector {
64
- readable: ReadableStream;
65
- writable: WritableStream;
66
- /** Get the raw collected text from the debug channel. */
67
- getCollectedText(): string;
68
- /** Get parsed component entries (names, stacks — no props or source). */
69
- getComponents(): DebugComponentEntry[];
70
- }
71
-
72
- export function createDebugChannelCollector(): DebugChannelCollector {
73
- const chunks: string[] = [];
74
- const decoder = new TextDecoder();
75
-
76
- const sink = new TransformStream();
77
-
78
- // Collect chunks from the readable side instead of discarding them.
79
- sink.readable
80
- .pipeTo(
81
- new WritableStream({
82
- write(chunk: Uint8Array) {
83
- chunks.push(decoder.decode(chunk, { stream: true }));
84
- },
85
- close() {
86
- // Flush any remaining bytes in the decoder
87
- const remaining = decoder.decode();
88
- if (remaining) chunks.push(remaining);
89
- },
90
- })
91
- )
92
- .catch(() => {
93
- // Stream abort — request cancelled. Not an error.
94
- });
95
-
96
- return {
97
- readable: new ReadableStream(), // no commands to send to Flight
98
- writable: sink.writable,
99
- getCollectedText() {
100
- return chunks.join('');
101
- },
102
- getComponents() {
103
- return parseDebugRows(chunks.join(''));
104
- },
105
- };
106
- }
107
-
108
51
  /**
109
52
  * Parse React Flight debug rows into component entries.
110
53
  *
@@ -299,6 +299,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
299
299
  rootSegment: manifest.root,
300
300
  buildManifest: typedBuildManifest,
301
301
  globalError: manifest.globalError,
302
+ devDebugRegistry: pipelineConfig.devDebugRegistry,
302
303
  },
303
304
  interception
304
305
  );
@@ -60,6 +60,7 @@ export interface RenderRouteDeps {
60
60
  rootSegment: ManifestSegmentNode;
61
61
  buildManifest: BuildManifest;
62
62
  globalError?: { load: () => Promise<unknown>; filePath: string };
63
+ devDebugRegistry?: import('../../dev-tools/debug-channel.ts').DebugChannelRegistry;
63
64
  }
64
65
 
65
66
  /**
@@ -198,7 +199,16 @@ export async function renderRoute(
198
199
 
199
200
  // Render to RSC Flight stream with signal tracking.
200
201
  const _rscStart = performance.now();
201
- const { rscStream, signals, getDebugComponents } = renderRscStream(payload, req, leafDenyChain);
202
+ // Only wire the HMR debug transport for HTML requests. RSC payload
203
+ // requests (client navigations) don't serve HTML with __timber_debug_id,
204
+ // so the browser can't subscribe — skip to avoid orphaned sessions.
205
+ const debugRegistry = isRscRequest(req) || clientJsDisabled ? undefined : deps.devDebugRegistry;
206
+ const { rscStream, signals, getDebugComponents, debugRequestId } = renderRscStream(
207
+ payload,
208
+ req,
209
+ leafDenyChain,
210
+ debugRegistry
211
+ );
202
212
 
203
213
  // Store the debug components getter in ALS so onPipelineError can
204
214
  // include component tree context for render-phase errors (dev mode only).
@@ -300,5 +310,6 @@ export async function renderRoute(
300
310
  deferSuspenseFor,
301
311
  globalError,
302
312
  slotSkipInfo,
313
+ debugRequestId,
303
314
  });
304
315
  }
@@ -27,12 +27,8 @@ import {
27
27
  } from '../primitives.ts';
28
28
  import { checkAndWarnRscPropError } from '../rsc-prop-warnings.ts';
29
29
 
30
- import {
31
- createDebugChannelSink,
32
- createDebugChannelCollector,
33
- isAbortError,
34
- type DebugComponentEntry,
35
- } from './helpers.ts';
30
+ import { createDebugChannelSink, isAbortError, type DebugComponentEntry } from './helpers.ts';
31
+ import { createDevDebugChannel, type DebugChannelRegistry } from '../../dev-tools/debug-channel.ts';
36
32
  import { isDebug } from '../debug.ts';
37
33
  import { isDevMode } from '../debug.ts';
38
34
 
@@ -84,6 +80,8 @@ export interface RscStreamResult {
84
80
  signals: RenderSignals;
85
81
  /** Dev-only: server component debug info from the Flight debug channel. */
86
82
  getDebugComponents?: () => DebugComponentEntry[];
83
+ /** Dev-only: request ID for the debug channel HMR transport. */
84
+ debugRequestId?: string;
87
85
  }
88
86
 
89
87
  /**
@@ -100,7 +98,8 @@ export interface RscStreamResult {
100
98
  export function renderRscStream(
101
99
  payload: RscPayloadRoot,
102
100
  req: Request,
103
- leafDenyChain?: DenyPageEntry[]
101
+ leafDenyChain?: DenyPageEntry[],
102
+ devDebugRegistry?: DebugChannelRegistry
104
103
  ): RscStreamResult {
105
104
  const signals: RenderSignals = {
106
105
  denySignal: null,
@@ -112,9 +111,18 @@ export function renderRscStream(
112
111
 
113
112
  let rscStream: ReadableStream<Uint8Array> | undefined;
114
113
 
115
- // In dev mode, collect debug channel data for the error overlay.
116
- // In production, use the discard sink (no overhead).
117
- const debugChannel = isDevMode() ? createDebugChannelCollector() : createDebugChannelSink();
114
+ // Always provide a debugChannel to prevent $E source entries (which
115
+ // may contain bound-arg secrets) from leaking into the main stream.
116
+ //
117
+ // In dev mode with an HMR transport: use createDevDebugChannel which
118
+ // pipes debug data to the browser via WebSocket for owner stack
119
+ // resolution. In production (or dev without transport): use a sink
120
+ // that discards everything.
121
+ const debugChannel = devDebugRegistry
122
+ ? createDevDebugChannel(devDebugRegistry)
123
+ : createDebugChannelSink();
124
+ const debugRequestId =
125
+ 'requestId' in debugChannel ? (debugChannel as { requestId: string }).requestId : undefined;
118
126
 
119
127
  try {
120
128
  rscStream = renderToReadableStream(
@@ -128,20 +136,13 @@ export function renderRscStream(
128
136
  if (isDenySignal(error)) {
129
137
  // Late addressing (TIM-1450): a nested-component deny escapes
130
138
  // all in-tree catch sites and arrives here without owners.
131
- // Resolve it against the leaf deny chain — when the first match
132
- // is non-MDX, stamp the full candidate list so the client can
133
- // find the first reachable owner via its ancestry context.
139
+ // Resolve it against the leaf deny chain and stamp the full
140
+ // candidate list so the client can find the first reachable
141
+ // owner via its ancestry context.
134
142
  if (!error.owners && leafDenyChain) {
135
143
  const entries = matchDenyEntries(leafDenyChain, error.status);
136
- if (entries.length > 0 && !entries[0].isMdx) {
137
- // Only non-MDX entries are addressable — MDX boundaries hold
138
- // pre-rendered fallbackElements without runtime props. TIM-1451
139
- // will widen exact-code MDX; until then they stay on the
140
- // re-render fallback path.
141
- const nonMdx = entries.filter((e) => !e.isMdx);
142
- if (nonMdx.length > 0) {
143
- error.owners = nonMdx.map((e) => e.ownerKey);
144
- }
144
+ if (entries.length > 0) {
145
+ error.owners = entries.map((e) => e.ownerKey);
145
146
  }
146
147
  }
147
148
  // First-wins: two concurrent denies from separately serialized
@@ -279,11 +280,13 @@ export function renderRscStream(
279
280
  return {
280
281
  rscStream,
281
282
  signals,
282
- // Expose the debug channel collector's getComponents in dev mode.
283
- // The caller can retrieve component tree info when handling errors.
283
+ // In production, the debug channel collector's getComponents is available.
284
+ // In dev mode, debug rows ride the main stream (no separate channel), so
285
+ // this is undefined — the error overlay uses stack traces directly.
284
286
  getDebugComponents:
285
287
  'getComponents' in debugChannel
286
288
  ? (debugChannel as { getComponents: () => DebugComponentEntry[] }).getComponents
287
289
  : undefined,
290
+ debugRequestId,
288
291
  };
289
292
  }
@@ -105,6 +105,7 @@ interface SsrRenderOptions {
105
105
  /** Tier 2 global-error.tsx file, if present in app/. */
106
106
  globalError?: GlobalErrorFile;
107
107
  slotSkipInfo?: SlotSkipEntry[];
108
+ debugRequestId?: string;
108
109
  }
109
110
 
110
111
  /**
@@ -197,7 +198,12 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
197
198
  clientBootstrap.preloadLinks +
198
199
  // Initialize __timber_f in <head> so it exists before any streaming
199
200
  // chunk scripts arrive in <body>. See flight-scripts.ts, LOCAL-415.
200
- (clientJsDisabled ? '' : flightInitScript(nonce)),
201
+ (clientJsDisabled ? '' : flightInitScript(nonce)) +
202
+ // Embed the debug channel request ID so the browser can subscribe
203
+ // to the correct HMR debug stream for owner stack resolution.
204
+ (opts.debugRequestId && !clientJsDisabled
205
+ ? `<script${nonceAttr(nonce)}>self.__timber_debug_id=${htmlEscapeJsonString(JSON.stringify(opts.debugRequestId))}</script>`
206
+ : ''),
201
207
  getHeadSegmentScript: clientJsDisabled
202
208
  ? undefined
203
209
  : () =>
@@ -156,7 +156,12 @@ export async function resolveSlotElement(
156
156
  const rendered = await scope(() =>
157
157
  (SlotPage as (props: Record<string, unknown>) => unknown)(props)
158
158
  );
159
- return containSlotSubtree(rendered as React.ReactNode, denyFallback);
159
+ return containSlotSubtree(
160
+ rendered as React.ReactNode,
161
+ denyFallback,
162
+ undefined,
163
+ slotNode.segmentName
164
+ );
160
165
  } catch (error) {
161
166
  // RedirectSignal must propagate — the pipeline handles redirects
162
167
  // at the top level. Swallowing it here would silently return
@@ -189,12 +194,20 @@ export async function resolveSlotElement(
189
194
  element,
190
195
  h,
191
196
  scope,
192
- denyFallback
197
+ denyFallback,
198
+ slotNode.segmentName
193
199
  );
194
200
 
195
201
  // Wrap with slot root's layout — INSIDE the access gate, so the layout
196
202
  // server component never executes when access.ts denies. See TIM-1074.
197
- element = await wrapWithLayout(slotNode, element, h, scope, denyFallback);
203
+ element = await wrapWithLayout(
204
+ slotNode,
205
+ element,
206
+ h,
207
+ scope,
208
+ denyFallback,
209
+ slotNode.segmentName
210
+ );
198
211
 
199
212
  // Wrap in SlotAccessGate if slot root has access.ts. The gate is
200
213
  // OUTSIDE the layout, matching the segment model in
@@ -290,7 +303,8 @@ async function wrapWithIntermediateSegments(
290
303
  element: React.ReactElement,
291
304
  h: CreateElementFn,
292
305
  scope?: SlotParamScope,
293
- denyFallback?: React.ReactNode
306
+ denyFallback?: React.ReactNode,
307
+ slotName?: string
294
308
  ): Promise<React.ReactElement> {
295
309
  for (let i = chain.length - 1; i > 0; i--) {
296
310
  const seg = chain[i];
@@ -300,7 +314,7 @@ async function wrapWithIntermediateSegments(
300
314
  errorBoundaryComponent: TimberErrorBoundary,
301
315
  isSlotBoundary: true,
302
316
  });
303
- element = await wrapWithLayout(seg, element, h, scope, denyFallback);
317
+ element = await wrapWithLayout(seg, element, h, scope, denyFallback, slotName);
304
318
  element = await wrapWithAccessGate(seg, element, h, scope);
305
319
  }
306
320
  return element;
@@ -341,10 +355,16 @@ function ScopedSlotLayout(props: {
341
355
  layout: (props: Record<string, unknown>) => unknown;
342
356
  scope: SlotParamScope;
343
357
  denyFallback: React.ReactNode;
358
+ slotName: string | undefined;
344
359
  children: React.ReactElement;
345
360
  }): unknown {
346
361
  const rendered = props.scope(() => props.layout({ children: props.children }));
347
- return containSlotSubtree(rendered as React.ReactNode, props.denyFallback, props.children);
362
+ return containSlotSubtree(
363
+ rendered as React.ReactNode,
364
+ props.denyFallback,
365
+ props.children,
366
+ props.slotName
367
+ );
348
368
  }
349
369
 
350
370
  /**
@@ -362,7 +382,8 @@ async function wrapWithLayout(
362
382
  element: React.ReactElement,
363
383
  h: CreateElementFn,
364
384
  scope?: SlotParamScope,
365
- denyFallback: React.ReactNode = null
385
+ denyFallback: React.ReactNode = null,
386
+ slotName?: string
366
387
  ): Promise<React.ReactElement> {
367
388
  if (!node.layout) return element;
368
389
  const Layout = await loadComponent(node.layout);
@@ -373,6 +394,7 @@ async function wrapWithLayout(
373
394
  layout: Layout as (props: Record<string, unknown>) => unknown,
374
395
  scope,
375
396
  denyFallback,
397
+ slotName,
376
398
  children: element,
377
399
  });
378
400
  }
@@ -65,6 +65,10 @@ interface Containment {
65
65
  * so the level below is not wrapped (and re-walked) a second time.
66
66
  */
67
67
  opaque: unknown;
68
+ /** Slot name (e.g. `@artists`) used as a key prefix for wrapped elements. */
69
+ slotName?: string;
70
+ /** Monotonic counter for disambiguating same-name sibling components. */
71
+ wrapCount: number;
68
72
  }
69
73
 
70
74
  function isThenable(value: unknown): value is Thenable<unknown> {
@@ -156,7 +160,7 @@ function ContainedSlotComponent(props: {
156
160
  fallback: ReactNode;
157
161
  }): ReactNode | Promise<ReactNode> {
158
162
  const { component: Component, componentProps, fallback } = props;
159
- const containment: Containment = { fallback, opaque: undefined };
163
+ const containment: Containment = { fallback, opaque: undefined, wrapCount: 0 };
160
164
  let result: unknown;
161
165
  try {
162
166
  result = Component(componentProps);
@@ -204,12 +208,16 @@ function walk(node: unknown, c: Containment, seen: WeakSet<object>): unknown {
204
208
  const { type, key, props } = node;
205
209
  const component = type === ContainedSlotComponent ? undefined : serverComponentOf(type);
206
210
  if (component) {
211
+ const effectiveKey = key ?? (c.slotName ? `${c.slotName}:${c.wrapCount++}` : null);
207
212
  const wrapped = {
208
213
  component,
209
214
  componentProps: props as Record<string, unknown>,
210
215
  fallback: c.fallback,
211
216
  };
212
- return createElement(ContainedSlotComponent, key === null ? wrapped : { ...wrapped, key });
217
+ return createElement(
218
+ ContainedSlotComponent,
219
+ effectiveKey === null ? wrapped : { ...wrapped, key: effectiveKey }
220
+ );
213
221
  }
214
222
  // Host elements, fragments, Suspense, client references, providers, and
215
223
  // wrappers this walk already made: keep the type, contain whatever server
@@ -249,7 +257,8 @@ function walk(node: unknown, c: Containment, seen: WeakSet<object>): unknown {
249
257
  export function containSlotSubtree(
250
258
  tree: ReactNode,
251
259
  fallback: ReactNode,
252
- opaque?: ReactNode
260
+ opaque?: ReactNode,
261
+ slotName?: string
253
262
  ): ReactNode {
254
- return walk(tree, { fallback, opaque }, new WeakSet()) as ReactNode;
263
+ return walk(tree, { fallback, opaque, slotName, wrapCount: 0 }, new WeakSet()) as ReactNode;
255
264
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-manifest-DTmSGLRz.js","names":[],"sources":["../../src/server/csp-nonce.ts","../../src/server/build-manifest.ts"],"sourcesContent":["/**\n * CSP nonce — per-request, generated on demand.\n *\n * timber emits several inline `<script>` tags per HTML response — React's\n * bootstrap script and Fizz runtime, the Flight init/chunk scripts, the\n * segment-tree script, and the post-flush noindex script. Under a strict\n * `Content-Security-Policy` (`script-src 'nonce-…' 'strict-dynamic'`) every\n * one of them needs the request's nonce or the browser refuses to run it.\n *\n * The app opts in by reading `ctx.nonce` in `middleware.ts` and putting it\n * in the CSP header it sets through `ctx.headers`. The first read generates\n * the value and stores it on the request context; the render pipeline reads\n * it back with `getCspNonce()` and stamps it on every inline script. A\n * request whose middleware never reads `ctx.nonce` has no nonce, and its\n * markup is byte-identical to a render without this feature. timber does\n * not parse the CSP header — the app owns the policy, timber owns the\n * markup, and `ctx.nonce` is the one value both are built from.\n *\n * `proxy.ts` cannot participate: it only sees the finished response from\n * `next()`, after the markup exists.\n *\n * See design/13-security.md §\"Content Security Policy\" (TIM-1414).\n */\n\nimport { requestContextAls } from './als-registry.ts';\n\n/**\n * Generate a CSP nonce: 128 random bits, base64. Matches CSP's\n * `base64-value` grammar, so it is valid in the header verbatim and safe\n * inside a double-quoted HTML attribute without escaping.\n */\nfunction generateNonce(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n let binary = '';\n for (const b of bytes) binary += String.fromCharCode(b);\n return btoa(binary);\n}\n\n/**\n * The request's CSP nonce, creating it on first call. Backs the\n * `ctx.nonce` getter on `MiddlewareContext`. Outside a request context\n * (tests, build-time rendering) a fresh value is returned but not\n * retained, since there is no request for it to belong to.\n */\nexport function requestCspNonce(): string {\n const store = requestContextAls.getStore();\n if (!store) return generateNonce();\n store.cspNonce ??= generateNonce();\n return store.cspNonce;\n}\n\n/**\n * The request's CSP nonce if middleware asked for one, else `undefined` —\n * in which case the pipeline emits bare `<script>` tags.\n */\nexport function getCspNonce(): string | undefined {\n return requestContextAls.getStore()?.cspNonce;\n}\n\n/**\n * The ` nonce=\"…\"` attribute fragment for a timber-generated `<script>` tag,\n * or `''` when the request has no nonce. Callers interpolate it directly\n * after the tag name: `` `<script${nonceAttr(nonce)}>` ``.\n *\n * The value comes from `generateNonce` and is base64, so no attribute\n * escaping is required.\n */\nexport function nonceAttr(nonce: string | undefined): string {\n return nonce ? ` nonce=\"${nonce}\"` : '';\n}\n","/**\n * Build manifest types and utilities for CSS and JS asset tracking.\n *\n * The build manifest maps route segment file paths to their output\n * chunks from Vite's client build. This enables:\n * - <link rel=\"stylesheet\"> injection in HTML <head>\n * - <script type=\"module\"> with hashed URLs in production\n * - <link rel=\"modulepreload\"> for client chunk dependencies\n * - Link preload headers for Early Hints (103)\n *\n * In dev mode, Vite's HMR client handles CSS/JS injection, so the build\n * manifest is empty. In production, it's populated from Vite's\n * .vite/manifest.json after the client build.\n *\n * Design docs: 18-build-system.md §\"Build Manifest\", 02-rendering-pipeline.md §\"Early Hints\"\n */\n\n/** A font asset entry in the build manifest. */\nexport interface ManifestFontEntry {\n /** URL path to the font file (e.g. `/_timber/fonts/inter-latin-400-abc123.woff2`). */\n href: string;\n /** Font format (e.g. `woff2`). */\n format: string;\n /** Crossorigin attribute — always `anonymous` for fonts. */\n crossOrigin: string;\n}\n\n/** Build manifest mapping input file paths to output asset URLs. */\nexport interface BuildManifest {\n /** Map from input file path (relative to project root) to output CSS URLs. */\n css: Record<string, string[]>;\n /** Map from input file path to output JS chunk URL (hashed filename). */\n js: Record<string, string>;\n /** Map from input file path to transitive JS dependency URLs for modulepreload. */\n modulepreload: Record<string, string[]>;\n /** Map from input file path to font assets used by that module. */\n fonts: Record<string, ManifestFontEntry[]>;\n /**\n * Cache-busting hashes for metadata route files (opengraph-image, etc.).\n * Map from file path (relative to project root) to short content hash.\n * In dev mode this is empty — a startup nonce is used instead.\n */\n metadataRouteHashes?: Record<string, string>;\n /**\n * Static request-dependency map from the Vite plugin's module-graph analysis.\n * Map from segment entry file path to whether it's request-dependent.\n * Used by buildSegmentInfo() for X-Timber-Segments in production.\n */\n requestDep?: Record<string, boolean>;\n}\n\n/**\n * Empty build manifest used in dev mode, and the single source of truth for\n * what an empty manifest contains.\n *\n * `virtual:timber-build-manifest` serializes this constant into its generated\n * source rather than restating it — one literal for the dev module, one for the\n * production module's `globalThis.__TIMBER_BUILD_MANIFEST__ ?? …` fallback.\n * They were three hand-written objects that had already drifted: this one and\n * the dev literal were missing `requestDep` while the production fallback had\n * it, so a reader who grepped the built bundle for `requestDep` found the one\n * copy that mentioned it and read the fallback as the shipped map (TIM-1300).\n * A field added here now reaches every copy.\n */\nexport const EMPTY_BUILD_MANIFEST: BuildManifest = {\n css: {},\n js: {},\n modulepreload: {},\n fonts: {},\n metadataRouteHashes: {},\n requestDep: {},\n};\n\n/** Segment shape expected by collectRouteCss (matches ManifestSegmentNode). */\ninterface SegmentWithFiles {\n layout?: { filePath: string };\n page?: { filePath: string };\n}\n\n/**\n * Collect all CSS files needed for a matched route's segment chain.\n *\n * Walks segments root → leaf, collecting CSS for each layout and page.\n * Deduplicates while preserving order (root layout CSS first).\n */\nexport function collectRouteCss(segments: SegmentWithFiles[], manifest: BuildManifest): string[] {\n const seen = new Set<string>();\n const result: string[] = [];\n\n for (const segment of segments) {\n for (const file of [segment.layout, segment.page]) {\n if (!file) continue;\n const cssFiles = manifest.css[file.filePath];\n if (!cssFiles) continue;\n for (const url of cssFiles) {\n if (!seen.has(url)) {\n seen.add(url);\n result.push(url);\n }\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generate <link rel=\"stylesheet\"> tags for CSS URLs.\n *\n * Returns an HTML string to prepend to headHtml for injection\n * via injectHead() before </head>.\n */\nexport function buildCssLinkTags(cssUrls: string[]): string {\n // Emit only <link rel=\"stylesheet\"> — no preload tags. CSS preloading\n // is handled by 103 Early Hints (Link header) which fires before the\n // HTML stream. Float also emits `<link rel=\"stylesheet\" data-precedence>`\n // at the top of <head> during Fizz rendering — the browser discovers\n // CSS from that tag. A <link rel=\"preload\"> here would arrive *after*\n // Float's stylesheet tag and trigger \"Preload was ignored\" warnings.\n return cssUrls.map((url) => `<link rel=\"stylesheet\" href=\"${url}\">`).join('');\n}\n\n// ─── Font utilities ──────────────────────────────────────────────────────\n\n/**\n * Collect all font entries needed for a matched route's segment chain.\n *\n * Walks segments root → leaf, collecting fonts for each layout and page.\n * Deduplicates by href while preserving order.\n */\nexport function collectRouteFonts(\n segments: SegmentWithFiles[],\n manifest: BuildManifest\n): ManifestFontEntry[] {\n const seen = new Set<string>();\n const result: ManifestFontEntry[] = [];\n\n for (const segment of segments) {\n for (const file of [segment.layout, segment.page]) {\n if (!file) continue;\n const fonts = manifest.fonts[file.filePath];\n if (!fonts) continue;\n for (const entry of fonts) {\n if (!seen.has(entry.href)) {\n seen.add(entry.href);\n result.push(entry);\n }\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generate <link rel=\"preload\"> tags for font assets.\n *\n * Font preloads use `as=font` and always include `crossorigin` (required\n * for font preloads even for same-origin resources per the spec).\n */\nexport function buildFontPreloadTags(fonts: ManifestFontEntry[]): string {\n return fonts\n .map(\n (f) =>\n `<link rel=\"preload\" href=\"${f.href}\" as=\"font\" type=\"font/${f.format}\" crossorigin=\"${f.crossOrigin}\">`\n )\n .join('');\n}\n\n// ─── JS chunk utilities ──────────────────────────────────────────────────\n\n/**\n * Collect modulepreload URLs for a matched route's segment chain.\n *\n * Walks segments root → leaf, collecting transitive JS dependencies\n * for each layout and page. Deduplicates across segments.\n */\nexport function collectRouteModulepreloads(\n segments: SegmentWithFiles[],\n manifest: BuildManifest\n): string[] {\n const seen = new Set<string>();\n const result: string[] = [];\n\n for (const segment of segments) {\n for (const file of [segment.layout, segment.page]) {\n if (!file) continue;\n const preloads = manifest.modulepreload[file.filePath];\n if (!preloads) continue;\n for (const url of preloads) {\n if (!seen.has(url)) {\n seen.add(url);\n result.push(url);\n }\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generate <link rel=\"modulepreload\"> tags for JS dependency URLs.\n *\n * Modulepreload hints tell the browser to fetch and parse JS modules\n * before they're needed, reducing waterfall latency for dynamic imports.\n */\nexport function buildModulepreloadTags(urls: string[]): string {\n return urls.map((url) => `<link rel=\"modulepreload\" href=\"${url}\">`).join('');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAS,gBAAwB;CAC/B,MAAM,QAAQ,OAAO,gCAAgB,IAAI,WAAW,EAAE,CAAC;CACvD,IAAI,SAAS;CACb,KAAK,MAAM,KAAK,OAAO,UAAU,OAAO,aAAa,CAAC;CACtD,OAAO,KAAK,MAAM;AACpB;;;;;;;AAQA,SAAgB,kBAA0B;CACxC,MAAM,QAAQ,kBAAkB,SAAS;CACzC,IAAI,CAAC,OAAO,OAAO,cAAc;CACjC,MAAM,aAAa,cAAc;CACjC,OAAO,MAAM;AACf;;;;;;;;;;;;;;;;ACeA,IAAa,uBAAsC;CACjD,KAAK,CAAC;CACN,IAAI,CAAC;CACL,eAAe,CAAC;CAChB,OAAO,CAAC;CACR,qBAAqB,CAAC;CACtB,YAAY,CAAC;AACf;;;;;;;AAcA,SAAgB,gBAAgB,UAA8B,UAAmC;CAC/F,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,WAAW,UACpB,KAAK,MAAM,QAAQ,CAAC,QAAQ,QAAQ,QAAQ,IAAI,GAAG;EACjD,IAAI,CAAC,MAAM;EACX,MAAM,WAAW,SAAS,IAAI,KAAK;EACnC,IAAI,CAAC,UAAU;EACf,KAAK,MAAM,OAAO,UAChB,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG;GAClB,KAAK,IAAI,GAAG;GACZ,OAAO,KAAK,GAAG;EACjB;CAEJ;CAGF,OAAO;AACT;;;;;;;AA0BA,SAAgB,kBACd,UACA,UACqB;CACrB,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAA8B,CAAC;CAErC,KAAK,MAAM,WAAW,UACpB,KAAK,MAAM,QAAQ,CAAC,QAAQ,QAAQ,QAAQ,IAAI,GAAG;EACjD,IAAI,CAAC,MAAM;EACX,MAAM,QAAQ,SAAS,MAAM,KAAK;EAClC,IAAI,CAAC,OAAO;EACZ,KAAK,MAAM,SAAS,OAClB,IAAI,CAAC,KAAK,IAAI,MAAM,IAAI,GAAG;GACzB,KAAK,IAAI,MAAM,IAAI;GACnB,OAAO,KAAK,KAAK;EACnB;CAEJ;CAGF,OAAO;AACT;;;;;;;AAyBA,SAAgB,2BACd,UACA,UACU;CACV,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,WAAW,UACpB,KAAK,MAAM,QAAQ,CAAC,QAAQ,QAAQ,QAAQ,IAAI,GAAG;EACjD,IAAI,CAAC,MAAM;EACX,MAAM,WAAW,SAAS,cAAc,KAAK;EAC7C,IAAI,CAAC,UAAU;EACf,KAAK,MAAM,OAAO,UAChB,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG;GAClB,KAAK,IAAI,GAAG;GACZ,OAAO,KAAK,GAAG;EACjB;CAEJ;CAGF,OAAO;AACT"}