@timber-js/app 0.2.0-alpha.204 → 0.2.0-alpha.205

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 (104) hide show
  1. package/dist/_chunks/{actions-HUdJADAD.js → actions-jNrXdzgs.js} +4 -3
  2. package/dist/_chunks/{actions-HUdJADAD.js.map → actions-jNrXdzgs.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
  4. package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
  5. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
  6. package/dist/_chunks/{cache-api-CAPbZTga.js → cache-api-CUD9Ezaq.js} +3 -2
  7. package/dist/_chunks/{cache-api-CAPbZTga.js.map → cache-api-CUD9Ezaq.js.map} +1 -1
  8. package/dist/_chunks/{chains-C9Y7Oyno.js → chains-BjGKq9Je.js} +2 -2
  9. package/dist/_chunks/{chains-C9Y7Oyno.js.map → chains-BjGKq9Je.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-BzMGuIH6.js → cli-check-3WE_GyoL.js} +3 -3
  11. package/dist/_chunks/{cli-check-BzMGuIH6.js.map → cli-check-3WE_GyoL.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-DnXqcIIj.js → cli-schema-sync-D3iIZjku.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-DnXqcIIj.js.map → cli-schema-sync-D3iIZjku.js.map} +1 -1
  14. package/dist/_chunks/{cloudflare-DxX1SU0g.js → cloudflare-BFb__LYG.js} +3 -3
  15. package/dist/_chunks/{cloudflare-DxX1SU0g.js.map → cloudflare-BFb__LYG.js.map} +1 -1
  16. package/dist/_chunks/{convention-lint-DHOFvX5s.js → convention-lint-BnO5TyHD.js} +2 -2
  17. package/dist/_chunks/{convention-lint-DHOFvX5s.js.map → convention-lint-BnO5TyHD.js.map} +1 -1
  18. package/dist/_chunks/{dev-server-BV01iyUs.js → dev-server-Dbdgx2dD.js} +8 -195
  19. package/dist/_chunks/dev-server-Dbdgx2dD.js.map +1 -0
  20. package/dist/_chunks/{error-boundary-BQKxl6EX.js → error-boundary-BndF-3Td.js} +2 -2
  21. package/dist/_chunks/{error-boundary-BQKxl6EX.js.map → error-boundary-BndF-3Td.js.map} +1 -1
  22. package/dist/_chunks/{graph-cache-Cv3njEH8.js → graph-cache-CP4GEmf9.js} +2 -2
  23. package/dist/_chunks/{graph-cache-Cv3njEH8.js.map → graph-cache-CP4GEmf9.js.map} +1 -1
  24. package/dist/_chunks/{live-graph-DCLQzV6m.js → live-graph-BM2UbbNx.js} +3 -3
  25. package/dist/_chunks/{live-graph-DCLQzV6m.js.map → live-graph-BM2UbbNx.js.map} +1 -1
  26. package/dist/_chunks/logger-k2DQ4EUf.js +506 -0
  27. package/dist/_chunks/logger-k2DQ4EUf.js.map +1 -0
  28. package/dist/_chunks/{poison-scan-CX_NaNDi.js → poison-scan-g83aAXhs.js} +2 -2
  29. package/dist/_chunks/{poison-scan-CX_NaNDi.js.map → poison-scan-g83aAXhs.js.map} +1 -1
  30. package/dist/_chunks/{logger-DqJ2VoAY.js → primitives-DLAnvsrk.js} +17 -518
  31. package/dist/_chunks/primitives-DLAnvsrk.js.map +1 -0
  32. package/dist/_chunks/{scanner-B_tnqFcF.js → scanner-BhNruzv0.js} +3 -3
  33. package/dist/_chunks/{scanner-B_tnqFcF.js.map → scanner-BhNruzv0.js.map} +1 -1
  34. package/dist/_chunks/{walkers-Cm3PC5JT.js → walkers-BNsswm-h.js} +2 -2
  35. package/dist/_chunks/{walkers-Cm3PC5JT.js.map → walkers-BNsswm-h.js.map} +1 -1
  36. package/dist/adapters/cloudflare-dev.js +1 -1
  37. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  38. package/dist/adapters/cloudflare.js +1 -1
  39. package/dist/analyze/crawl-entry.js +2 -2
  40. package/dist/analyze/graph-command.js +3 -3
  41. package/dist/cache/index.js +1 -1
  42. package/dist/cli.js +3 -3
  43. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  44. package/dist/client/error-boundary.js +1 -1
  45. package/dist/client/internal.js +1 -1
  46. package/dist/dev-tools/index.d.ts +1 -1
  47. package/dist/dev-tools/index.d.ts.map +1 -1
  48. package/dist/dev-tools/overlay.d.ts +1 -21
  49. package/dist/dev-tools/overlay.d.ts.map +1 -1
  50. package/dist/index.js +6 -7
  51. package/dist/index.js.map +1 -1
  52. package/dist/plugins/dev-server.d.ts.map +1 -1
  53. package/dist/routing/index.js +2 -2
  54. package/dist/server/als-registry.d.ts +0 -7
  55. package/dist/server/als-registry.d.ts.map +1 -1
  56. package/dist/server/deny-renderer.d.ts +2 -6
  57. package/dist/server/deny-renderer.d.ts.map +1 -1
  58. package/dist/server/index.js +3 -2
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/internal.js +180 -7
  61. package/dist/server/internal.js.map +1 -1
  62. package/dist/server/pipeline.d.ts +1 -13
  63. package/dist/server/pipeline.d.ts.map +1 -1
  64. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  65. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  66. package/dist/server/rsc-entry/helpers.d.ts +0 -38
  67. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  68. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  69. package/dist/server/rsc-entry/render-route.d.ts +0 -1
  70. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  71. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  72. package/dist/server/rsc-entry/rsc-stream.d.ts +1 -7
  73. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  74. package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -1
  75. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  76. package/dist/server/slot-resolver.d.ts.map +1 -1
  77. package/dist/server/slot-subtree-contain.d.ts +1 -1
  78. package/dist/server/slot-subtree-contain.d.ts.map +1 -1
  79. package/package.json +1 -1
  80. package/src/client/browser-entry/rsc-stream.ts +1 -82
  81. package/src/dev-tools/index.ts +0 -2
  82. package/src/dev-tools/overlay.ts +1 -65
  83. package/src/plugins/dev-server.ts +4 -45
  84. package/src/server/als-registry.ts +0 -8
  85. package/src/server/deny-renderer.ts +0 -7
  86. package/src/server/pipeline.ts +1 -14
  87. package/src/server/rsc-entry/deny-fallback.ts +1 -10
  88. package/src/server/rsc-entry/error-renderer.ts +1 -11
  89. package/src/server/rsc-entry/helpers.ts +0 -92
  90. package/src/server/rsc-entry/index.ts +1 -14
  91. package/src/server/rsc-entry/render-route.ts +2 -23
  92. package/src/server/rsc-entry/rsc-payload.ts +1 -2
  93. package/src/server/rsc-entry/rsc-stream.ts +3 -29
  94. package/src/server/rsc-entry/ssr-renderer.ts +2 -15
  95. package/src/server/slot-resolver.ts +7 -29
  96. package/src/server/slot-subtree-contain.ts +22 -14
  97. package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
  98. package/dist/_chunks/csp-nonce-hOGniaG4.js +0 -227
  99. package/dist/_chunks/csp-nonce-hOGniaG4.js.map +0 -1
  100. package/dist/_chunks/dev-server-BV01iyUs.js.map +0 -1
  101. package/dist/_chunks/logger-DqJ2VoAY.js.map +0 -1
  102. package/dist/dev-tools/debug-channel.d.ts +0 -55
  103. package/dist/dev-tools/debug-channel.d.ts.map +0 -1
  104. package/src/dev-tools/debug-channel.ts +0 -151
@@ -23,7 +23,6 @@ import { getCspNonce } from '../csp-nonce.ts';
23
23
  import { renderDenyPage, renderDenyPageAsRsc } from '../deny-renderer.ts';
24
24
  import type { DefaultPageRenderer, LayoutEntry } from '../deny-renderer.ts';
25
25
  import type { NavContext } from '../ssr-bridge-types.ts';
26
- import { createDebugChannelSink } from './helpers.ts';
27
26
  import { isRscRequest } from '../../shared/rsc-media-type.ts';
28
27
  import { withPublishedParams } from '../publish-params.ts';
29
28
  import { getCookiesForSsr } from '../cookie-context.ts';
@@ -291,7 +290,6 @@ async function renderErrorPageViaRsc(
291
290
  onError(err: unknown) {
292
291
  logRenderError({ method: req.method, path: url.pathname, error: err });
293
292
  },
294
- debugChannel: createDebugChannelSink(),
295
293
  });
296
294
 
297
295
  const [ssrStream, inlineStream] = teeWithErrorPropagation(rscStream);
@@ -402,14 +400,7 @@ export async function renderNoMatchPage(
402
400
  const match: RouteMatch = { segments, segmentParams: {}, middlewareChain: [] };
403
401
 
404
402
  if (isRscRequest(req)) {
405
- return renderDenyPageAsRsc(
406
- deny,
407
- segments,
408
- layoutComponents,
409
- req,
410
- responseHeaders,
411
- createDebugChannelSink
412
- );
403
+ return renderDenyPageAsRsc(deny, segments, layoutComponents, req, responseHeaders);
413
404
  }
414
405
 
415
406
  return renderDenyPage(
@@ -420,7 +411,6 @@ export async function renderNoMatchPage(
420
411
  match,
421
412
  responseHeaders,
422
413
  clientBootstrap,
423
- createDebugChannelSink,
424
414
  callSsr,
425
415
  defaultPage
426
416
  );
@@ -5,104 +5,12 @@
5
5
  */
6
6
 
7
7
  import type { ManifestSegmentNode } from '../route-matcher.ts';
8
- import { swallow } from '../logger.ts';
9
8
  import { computeSegmentKeys, computeSegmentTreePaths } from '../../routing/segment-keys.ts';
10
9
  import type { SegmentInfo } from '../../shared/segment-info.ts';
11
10
  import type { SlotSkipEntry } from '../slot-resolver.ts';
12
11
  import { isClientReference } from '../utils/element-type.ts';
13
12
  import { isStaticRequestDependent } from '../request-dep.ts';
14
13
 
15
- /**
16
- * No-op — returns undefined so no debugChannel is passed to Flight.
17
- * Debug rows ride the main stream inline (React's default behavior).
18
- *
19
- * Production Flight emits no debug rows. In dev, inline debug rows
20
- * are handled by the Flight client for owner stack resolution.
21
- *
22
- * A writable-only debugChannel triggers a react-server-dom-webpack
23
- * >=19.3.0 Flight client regression (owner/stack refs parsed after
24
- * stream close produce "Connection closed" errors).
25
- */
26
- export function createDebugChannelSink(): undefined {
27
- return undefined;
28
- }
29
-
30
- // ─── Debug Channel Collector (dev mode only) ────────────────────────────
31
-
32
- /**
33
- * Parsed component debug info extracted from the Flight debug channel.
34
- *
35
- * Contains only component names, environment labels, and stack frames —
36
- * never source code or props. See design/13-security.md §"Server source
37
- * never reaches the client".
38
- */
39
- export interface DebugComponentEntry {
40
- name: string;
41
- env: string | null;
42
- key: string | null;
43
- stack: unknown[] | null;
44
- }
45
-
46
- /**
47
- * Parse React Flight debug rows into component entries.
48
- *
49
- * The Flight debug channel writes rows in `hexId:json\n` format. Each row
50
- * with a JSON object containing a `name` field is a component debug info
51
- * entry. Rows without `name` (timing rows, reference rows like `D"$id"`)
52
- * are skipped.
53
- *
54
- * Security: `props` are explicitly stripped from parsed entries — they may
55
- * contain rendered output or user data. Only `name`, `env`, `key`, and
56
- * `stack` are retained.
57
- */
58
- export function parseDebugRows(text: string): DebugComponentEntry[] {
59
- if (!text) return [];
60
-
61
- const entries: DebugComponentEntry[] = [];
62
- const lines = text.split('\n');
63
-
64
- for (const line of lines) {
65
- if (!line) continue;
66
-
67
- // Flight row format: hexId:payload
68
- const colonIdx = line.indexOf(':');
69
- if (colonIdx === -1) continue;
70
-
71
- let payload = line.slice(colonIdx + 1);
72
-
73
- // React Flight debug rows may have a type-tag prefix before the JSON
74
- // object (e.g., "D{...}" for debug info rows). Strip the single-char
75
- // tag so the JSON parser sees the object. Non-JSON payloads like
76
- // D"$a" reference rows are caught by the JSON.parse try/catch below.
77
- // See TIM-556.
78
- if (payload.length > 0 && payload[0] !== '{' && payload[0] !== '[' && payload[0] !== '"') {
79
- payload = payload.slice(1);
80
- }
81
-
82
- // Skip payloads that still aren't JSON objects after tag stripping
83
- if (!payload.startsWith('{')) continue;
84
-
85
- try {
86
- const parsed = JSON.parse(payload);
87
- if (typeof parsed !== 'object' || parsed === null) continue;
88
- if (typeof parsed.name !== 'string') continue;
89
-
90
- // Strip props — may contain source-derived data or user data.
91
- // Only retain: name, env, key, stack.
92
- entries.push({
93
- name: parsed.name,
94
- env: parsed.env ?? null,
95
- key: parsed.key ?? null,
96
- stack: Array.isArray(parsed.stack) ? parsed.stack : null,
97
- });
98
- } catch (e) {
99
- swallow(e, 'malformed JSON in RSC client reference row');
100
- }
101
- }
102
-
103
- return entries;
104
- }
105
-
106
14
  /**
107
15
  * Build segment metadata for the X-Timber-Segments response header.
108
16
  * Describes the rendered segment chain with request-dependency status,
@@ -60,7 +60,6 @@ import { renderNoMatchPage } from './error-renderer.ts';
60
60
  import { renderRoute } from './render-route.ts';
61
61
  import { isDebug, isDevMode, setDebugFromConfig } from '../debug.ts';
62
62
  import { setSourceMapCallback } from '../dev-source-map.ts';
63
- import { requestContextAls } from '../als-registry.ts';
64
63
  import { capturePrebuiltPayloads } from '../prebuilt-builder.ts';
65
64
  import { setPrebuiltPayloadSource } from '../prebuilt/payload-source.ts';
66
65
  import { setRscFunctions, setComponentRefreshTimeout } from '../prebuilt-runtime.ts';
@@ -97,10 +96,6 @@ export function setDevSourceMapHandler(handler: (error: Error) => void): void {
97
96
  setSourceMapCallback(handler);
98
97
  }
99
98
 
100
- // Dev-only: debug components getter is stored per-request in ALS
101
- // (RequestContextStore.debugComponentsGetter) to avoid cross-request
102
- // race conditions. See TIM-557.
103
-
104
99
  /**
105
100
  * Create the RSC request handler from the route manifest.
106
101
  *
@@ -299,7 +294,6 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
299
294
  rootSegment: manifest.root,
300
295
  buildManifest: typedBuildManifest,
301
296
  globalError: manifest.globalError,
302
- devDebugRegistry: pipelineConfig.devDebugRegistry,
303
297
  },
304
298
  interception
305
299
  );
@@ -351,14 +345,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
351
345
  onPipelineError: isDev
352
346
  ? (error: Error, phase: string) => {
353
347
  if (pipelineConfig.devPipelineErrorHandler) {
354
- // For render-phase errors, include RSC debug component data
355
- // from the Flight debug channel (if available from the current request).
356
- const store = requestContextAls.getStore();
357
- const debugComponents =
358
- phase === 'render' && store?.debugComponentsGetter
359
- ? store.debugComponentsGetter()
360
- : undefined;
361
- pipelineConfig.devPipelineErrorHandler(error, phase, debugComponents);
348
+ pipelineConfig.devPipelineErrorHandler(error, phase);
362
349
  }
363
350
  }
364
351
  : undefined,
@@ -35,13 +35,12 @@ import { buildRouteElement, NoPageComponentError } from '../route-element-builde
35
35
  import { ParamCoercionError } from '../param-coercion.ts';
36
36
  import type { ManifestSegmentNode } from '../route-matcher.ts';
37
37
  import { recordTiming } from '../server-timing.ts';
38
- import { requestContextAls } from '../als-registry.ts';
39
38
  import { parseClientStateTree } from '../state-tree-diff.ts';
40
39
 
41
40
  import { getFirstDeniedSegmentIndex } from '../deny-boundary.ts';
42
41
  import { handleApiRoute } from './api-handler.ts';
43
42
  import { renderErrorPage, renderNoMatchPage } from './error-renderer.ts';
44
- import { buildRedirectResponse, createDebugChannelSink } from './helpers.ts';
43
+ import { buildRedirectResponse } from './helpers.ts';
45
44
  import { isRscRequest } from '../../shared/rsc-media-type.ts';
46
45
  import { buildRscPayloadResponse } from './rsc-payload.ts';
47
46
  import { renderRscStream } from './rsc-stream.ts';
@@ -60,7 +59,6 @@ export interface RenderRouteDeps {
60
59
  rootSegment: ManifestSegmentNode;
61
60
  buildManifest: BuildManifest;
62
61
  globalError?: { load: () => Promise<unknown>; filePath: string };
63
- devDebugRegistry?: import('../../dev-tools/debug-channel.ts').DebugChannelRegistry;
64
62
  }
65
63
 
66
64
  /**
@@ -199,24 +197,8 @@ export async function renderRoute(
199
197
 
200
198
  // Render to RSC Flight stream with signal tracking.
201
199
  const _rscStart = performance.now();
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
- );
200
+ const { rscStream, signals } = renderRscStream(payload, req, leafDenyChain);
212
201
 
213
- // Store the debug components getter in ALS so onPipelineError can
214
- // include component tree context for render-phase errors (dev mode only).
215
- // Per-request via ALS — no cross-request race. See TIM-557.
216
- const alsStore = requestContextAls.getStore();
217
- if (alsStore) {
218
- alsStore.debugComponentsGetter = getDebugComponents;
219
- }
220
202
  recordTiming({
221
203
  name: 'rsc-init',
222
204
  dur: Math.round(performance.now() - _rscStart),
@@ -239,7 +221,6 @@ export async function renderRoute(
239
221
  layoutComponents as LayoutEntry[],
240
222
  req,
241
223
  responseHeaders,
242
- createDebugChannelSink,
243
224
  getFirstDeniedSegmentIndex()
244
225
  );
245
226
  }
@@ -251,7 +232,6 @@ export async function renderRoute(
251
232
  match,
252
233
  responseHeaders,
253
234
  clientBootstrap,
254
- createDebugChannelSink,
255
235
  callSsr,
256
236
  undefined,
257
237
  getFirstDeniedSegmentIndex()
@@ -310,6 +290,5 @@ export async function renderRoute(
310
290
  deferSuspenseFor,
311
291
  globalError,
312
292
  slotSkipInfo,
313
- debugRequestId,
314
293
  });
315
294
  }
@@ -26,7 +26,7 @@ import type { LayoutComponentEntry } from '../route-element-builder.ts';
26
26
  import type { ManifestSegmentNode } from '../route-matcher.ts';
27
27
  import type { SlotSkipEntry } from '../slot-resolver.ts';
28
28
 
29
- import { buildRedirectResponse, buildSegmentInfo, createDebugChannelSink } from './helpers.ts';
29
+ import { buildRedirectResponse, buildSegmentInfo } from './helpers.ts';
30
30
  import { RSC_CONTENT_TYPE } from '../../shared/rsc-media-type.ts';
31
31
 
32
32
  import { rscErrorEnvelope } from '../rsc-error-envelope.ts';
@@ -178,7 +178,6 @@ export async function buildRscPayloadResponse(
178
178
  layoutComponents as LayoutEntry[],
179
179
  req,
180
180
  responseHeaders,
181
- createDebugChannelSink,
182
181
  getDeniedIdx()
183
182
  );
184
183
  }
@@ -27,8 +27,7 @@ import {
27
27
  } from '../primitives.ts';
28
28
  import { checkAndWarnRscPropError } from '../rsc-prop-warnings.ts';
29
29
 
30
- import { isAbortError, type DebugComponentEntry } from './helpers.ts';
31
- import type { DebugChannelRegistry } from '../../dev-tools/debug-channel.ts';
30
+ import { isAbortError } from './helpers.ts';
32
31
  import { isDebug } from '../debug.ts';
33
32
  import { isDevMode } from '../debug.ts';
34
33
 
@@ -78,10 +77,6 @@ export interface RenderSignals {
78
77
  export interface RscStreamResult {
79
78
  rscStream: ReadableStream<Uint8Array> | undefined;
80
79
  signals: RenderSignals;
81
- /** Dev-only: server component debug info from the Flight debug channel. */
82
- getDebugComponents?: () => DebugComponentEntry[];
83
- /** Dev-only: request ID for the debug channel HMR transport. */
84
- debugRequestId?: string;
85
80
  }
86
81
 
87
82
  /**
@@ -98,8 +93,7 @@ export interface RscStreamResult {
98
93
  export function renderRscStream(
99
94
  payload: RscPayloadRoot,
100
95
  req: Request,
101
- leafDenyChain?: DenyPageEntry[],
102
- devDebugRegistry?: DebugChannelRegistry
96
+ leafDenyChain?: DenyPageEntry[]
103
97
  ): RscStreamResult {
104
98
  const signals: RenderSignals = {
105
99
  denySignal: null,
@@ -111,14 +105,6 @@ export function renderRscStream(
111
105
 
112
106
  let rscStream: ReadableStream<Uint8Array> | undefined;
113
107
 
114
- // No debugChannel — debug rows ride the main RSC stream inline.
115
- // Production Flight emits none. In dev, the browser Flight client
116
- // handles inline debug rows for owner stack resolution (React's
117
- // default behavior). A writable-only debugChannel triggers a
118
- // react-server-dom-webpack >=19.3.0 Flight client regression.
119
- const debugChannel = undefined;
120
- const debugRequestId = undefined;
121
-
122
108
  try {
123
109
  rscStream = renderToReadableStream(
124
110
  payload,
@@ -243,7 +229,6 @@ export function renderRscStream(
243
229
  errorId,
244
230
  });
245
231
  },
246
- debugChannel,
247
232
  },
248
233
  {
249
234
  onClientReference(info: { id: string; name: string; deps: unknown }) {
@@ -272,16 +257,5 @@ export function renderRscStream(
272
257
  }
273
258
  }
274
259
 
275
- return {
276
- rscStream,
277
- signals,
278
- // In production, the debug channel collector's getComponents is available.
279
- // In dev mode, debug rows ride the main stream (no separate channel), so
280
- // this is undefined — the error overlay uses stack traces directly.
281
- getDebugComponents:
282
- debugChannel && 'getComponents' in debugChannel
283
- ? (debugChannel as { getComponents: () => DebugComponentEntry[] }).getComponents
284
- : undefined,
285
- debugRequestId,
286
- };
260
+ return { rscStream, signals };
287
261
  }
@@ -27,12 +27,7 @@ import type { SlotSkipEntry } from '../slot-resolver.ts';
27
27
  import type { NavContext } from '../ssr-bridge-types.ts';
28
28
 
29
29
  import { htmlEscapeJsonString } from '../flight-scripts.ts';
30
- import {
31
- buildRedirectResponse,
32
- buildSegmentInfo,
33
- createDebugChannelSink,
34
- isAbortError,
35
- } from './helpers.ts';
30
+ import { buildRedirectResponse, buildSegmentInfo, isAbortError } from './helpers.ts';
36
31
  import { getCookiesForSsr } from '../cookie-context.ts';
37
32
  import { getDenyStatus, getFirstDeniedSegmentIndex } from '../deny-boundary.ts';
38
33
  import { renderErrorPage, type GlobalErrorFile } from './error-renderer.ts';
@@ -105,7 +100,6 @@ interface SsrRenderOptions {
105
100
  /** Tier 2 global-error.tsx file, if present in app/. */
106
101
  globalError?: GlobalErrorFile;
107
102
  slotSkipInfo?: SlotSkipEntry[];
108
- debugRequestId?: string;
109
103
  }
110
104
 
111
105
  /**
@@ -198,12 +192,7 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
198
192
  clientBootstrap.preloadLinks +
199
193
  // Initialize __timber_f in <head> so it exists before any streaming
200
194
  // chunk scripts arrive in <body>. See flight-scripts.ts, LOCAL-415.
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
- : ''),
195
+ (clientJsDisabled ? '' : flightInitScript(nonce)),
207
196
  getHeadSegmentScript: clientJsDisabled
208
197
  ? undefined
209
198
  : () =>
@@ -250,7 +239,6 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
250
239
  match,
251
240
  responseHeaders,
252
241
  clientBootstrap,
253
- createDebugChannelSink,
254
242
  callSsr,
255
243
  undefined,
256
244
  resolvedDeniedIndex()
@@ -437,7 +425,6 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
437
425
  match,
438
426
  responseHeaders,
439
427
  clientBootstrap,
440
- createDebugChannelSink,
441
428
  callSsr,
442
429
  undefined,
443
430
  resolvedDeniedIndex()
@@ -156,12 +156,7 @@ 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(
160
- rendered as React.ReactNode,
161
- denyFallback,
162
- undefined,
163
- slotNode.segmentName
164
- );
159
+ return containSlotSubtree(rendered as React.ReactNode, denyFallback);
165
160
  } catch (error) {
166
161
  // RedirectSignal must propagate — the pipeline handles redirects
167
162
  // at the top level. Swallowing it here would silently return
@@ -194,20 +189,12 @@ export async function resolveSlotElement(
194
189
  element,
195
190
  h,
196
191
  scope,
197
- denyFallback,
198
- slotNode.segmentName
192
+ denyFallback
199
193
  );
200
194
 
201
195
  // Wrap with slot root's layout — INSIDE the access gate, so the layout
202
196
  // server component never executes when access.ts denies. See TIM-1074.
203
- element = await wrapWithLayout(
204
- slotNode,
205
- element,
206
- h,
207
- scope,
208
- denyFallback,
209
- slotNode.segmentName
210
- );
197
+ element = await wrapWithLayout(slotNode, element, h, scope, denyFallback);
211
198
 
212
199
  // Wrap in SlotAccessGate if slot root has access.ts. The gate is
213
200
  // OUTSIDE the layout, matching the segment model in
@@ -303,8 +290,7 @@ async function wrapWithIntermediateSegments(
303
290
  element: React.ReactElement,
304
291
  h: CreateElementFn,
305
292
  scope?: SlotParamScope,
306
- denyFallback?: React.ReactNode,
307
- slotName?: string
293
+ denyFallback?: React.ReactNode
308
294
  ): Promise<React.ReactElement> {
309
295
  for (let i = chain.length - 1; i > 0; i--) {
310
296
  const seg = chain[i];
@@ -314,7 +300,7 @@ async function wrapWithIntermediateSegments(
314
300
  errorBoundaryComponent: TimberErrorBoundary,
315
301
  isSlotBoundary: true,
316
302
  });
317
- element = await wrapWithLayout(seg, element, h, scope, denyFallback, slotName);
303
+ element = await wrapWithLayout(seg, element, h, scope, denyFallback);
318
304
  element = await wrapWithAccessGate(seg, element, h, scope);
319
305
  }
320
306
  return element;
@@ -355,16 +341,10 @@ function ScopedSlotLayout(props: {
355
341
  layout: (props: Record<string, unknown>) => unknown;
356
342
  scope: SlotParamScope;
357
343
  denyFallback: React.ReactNode;
358
- slotName: string | undefined;
359
344
  children: React.ReactElement;
360
345
  }): unknown {
361
346
  const rendered = props.scope(() => props.layout({ children: props.children }));
362
- return containSlotSubtree(
363
- rendered as React.ReactNode,
364
- props.denyFallback,
365
- props.children,
366
- props.slotName
367
- );
347
+ return containSlotSubtree(rendered as React.ReactNode, props.denyFallback, props.children);
368
348
  }
369
349
 
370
350
  /**
@@ -382,8 +362,7 @@ async function wrapWithLayout(
382
362
  element: React.ReactElement,
383
363
  h: CreateElementFn,
384
364
  scope?: SlotParamScope,
385
- denyFallback: React.ReactNode = null,
386
- slotName?: string
365
+ denyFallback: React.ReactNode = null
387
366
  ): Promise<React.ReactElement> {
388
367
  if (!node.layout) return element;
389
368
  const Layout = await loadComponent(node.layout);
@@ -394,7 +373,6 @@ async function wrapWithLayout(
394
373
  layout: Layout as (props: Record<string, unknown>) => unknown,
395
374
  scope,
396
375
  denyFallback,
397
- slotName,
398
376
  children: element,
399
377
  });
400
378
  }
@@ -65,10 +65,6 @@ 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;
72
68
  }
73
69
 
74
70
  function isThenable(value: unknown): value is Thenable<unknown> {
@@ -160,7 +156,7 @@ function ContainedSlotComponent(props: {
160
156
  fallback: ReactNode;
161
157
  }): ReactNode | Promise<ReactNode> {
162
158
  const { component: Component, componentProps, fallback } = props;
163
- const containment: Containment = { fallback, opaque: undefined, wrapCount: 0 };
159
+ const containment: Containment = { fallback, opaque: undefined };
164
160
  let result: unknown;
165
161
  try {
166
162
  result = Component(componentProps);
@@ -208,16 +204,13 @@ function walk(node: unknown, c: Containment, seen: WeakSet<object>): unknown {
208
204
  const { type, key, props } = node;
209
205
  const component = type === ContainedSlotComponent ? undefined : serverComponentOf(type);
210
206
  if (component) {
211
- const effectiveKey = key ?? (c.slotName ? `${c.slotName}:${c.wrapCount++}` : null);
212
207
  const wrapped = {
213
208
  component,
214
209
  componentProps: props as Record<string, unknown>,
215
210
  fallback: c.fallback,
211
+ ...(key !== null ? { key } : undefined),
216
212
  };
217
- return createElement(
218
- ContainedSlotComponent,
219
- effectiveKey === null ? wrapped : { ...wrapped, key: effectiveKey }
220
- );
213
+ return createElement(ContainedSlotComponent, wrapped);
221
214
  }
222
215
  // Host elements, fragments, Suspense, client references, providers, and
223
216
  // wrappers this walk already made: keep the type, contain whatever server
@@ -225,7 +218,23 @@ function walk(node: unknown, c: Containment, seen: WeakSet<object>): unknown {
225
218
  // slots).
226
219
  const nextProps = walk(props, c, seen);
227
220
  if (nextProps === props) return node;
228
- return createElement(type, key === null ? nextProps : { ...(nextProps as object), key });
221
+ // JSX compiles `<Flex><A /><B /></Flex>` to separate createElement
222
+ // arguments that React does not key-validate, but the walk produces
223
+ // `{ children: [A, B] }` inside props — an array that React treats as
224
+ // dynamic and warns about missing keys. For 2+ children, spread them as
225
+ // separate arguments to restore the JSX calling convention. For 0–1
226
+ // children, pass the array as a single child argument to preserve the
227
+ // array shape — spreading `[x]` would collapse it to a bare element,
228
+ // breaking components that call `children.map()`.
229
+ const { children, ...rest } = nextProps as Record<string, unknown>;
230
+ const propsWithKey = key === null ? rest : { ...rest, key };
231
+ if (Array.isArray(children) && children.length > 1) {
232
+ return createElement(type, propsWithKey, ...(children as ReactNode[]));
233
+ }
234
+ if (children !== undefined) {
235
+ return createElement(type, propsWithKey, children as ReactNode);
236
+ }
237
+ return createElement(type, propsWithKey);
229
238
  }
230
239
 
231
240
  if (isPlainObject(node)) {
@@ -257,8 +266,7 @@ function walk(node: unknown, c: Containment, seen: WeakSet<object>): unknown {
257
266
  export function containSlotSubtree(
258
267
  tree: ReactNode,
259
268
  fallback: ReactNode,
260
- opaque?: ReactNode,
261
- slotName?: string
269
+ opaque?: ReactNode
262
270
  ): ReactNode {
263
- return walk(tree, { fallback, opaque, slotName, wrapCount: 0 }, new WeakSet()) as ReactNode;
271
+ return walk(tree, { fallback, opaque }, new WeakSet()) as ReactNode;
264
272
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-manifest-DWppEdLB.js","names":[],"sources":["../../src/server/build-manifest.ts"],"sourcesContent":["/**\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":";;;;;;;;;;;;;;AAgEA,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"}