@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.
- package/bin/timber.mjs +15 -1
- package/dist/_chunks/{actions-d1hCqnU3.js → actions-HUdJADAD.js} +3 -3
- package/dist/_chunks/{actions-d1hCqnU3.js.map → actions-HUdJADAD.js.map} +1 -1
- package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/{cache-api-ByagcC-J.js → cache-api-CAPbZTga.js} +2 -2
- package/dist/_chunks/{cache-api-ByagcC-J.js.map → cache-api-CAPbZTga.js.map} +1 -1
- package/dist/_chunks/{chains-Bpb0W4ax.js → chains-CBNA0Ozj.js} +3 -3
- package/dist/_chunks/{chains-Bpb0W4ax.js.map → chains-CBNA0Ozj.js.map} +1 -1
- package/dist/_chunks/{cli-check-D6VolrDV.js → cli-check-BzMGuIH6.js} +3 -3
- package/dist/_chunks/{cli-check-D6VolrDV.js.map → cli-check-BzMGuIH6.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js → cli-schema-sync-DnXqcIIj.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js.map → cli-schema-sync-DnXqcIIj.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-DxX1SU0g.js} +3 -3
- package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-DxX1SU0g.js.map} +1 -1
- package/dist/_chunks/{convention-lint-fRkwVwEH.js → convention-lint-DHOFvX5s.js} +5 -95
- package/dist/_chunks/convention-lint-DHOFvX5s.js.map +1 -0
- package/dist/_chunks/csp-nonce-hOGniaG4.js +227 -0
- package/dist/_chunks/csp-nonce-hOGniaG4.js.map +1 -0
- package/dist/_chunks/dev-server-DioP7tkQ.js +2288 -0
- package/dist/_chunks/dev-server-DioP7tkQ.js.map +1 -0
- package/dist/_chunks/{error-boundary-BfPHZjm0.js → error-boundary-BQKxl6EX.js} +11 -17
- package/dist/_chunks/{error-boundary-BfPHZjm0.js.map → error-boundary-BQKxl6EX.js.map} +1 -1
- package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-Cv3njEH8.js} +2 -2
- package/dist/_chunks/{graph-cache-CP4GEmf9.js.map → graph-cache-Cv3njEH8.js.map} +1 -1
- package/dist/_chunks/{live-graph-D_2D32Ad.js → live-graph-VjHFF5EV.js} +3 -3
- package/dist/_chunks/{live-graph-D_2D32Ad.js.map → live-graph-VjHFF5EV.js.map} +1 -1
- package/dist/_chunks/{logger-uLBuGKDI.js → logger-DqJ2VoAY.js} +450 -458
- package/dist/_chunks/logger-DqJ2VoAY.js.map +1 -0
- package/dist/_chunks/{segment-keys-lqtdookO.js → metadata-routes-DSDjM_hJ.js} +2 -61
- package/dist/_chunks/metadata-routes-DSDjM_hJ.js.map +1 -0
- package/dist/_chunks/{poison-scan-Bm9Yyqk9.js → poison-scan-lEbz4pQE.js} +2 -2
- package/dist/_chunks/{poison-scan-Bm9Yyqk9.js.map → poison-scan-lEbz4pQE.js.map} +1 -1
- package/dist/_chunks/{scanner-AiazgH_f.js → scanner-B_tnqFcF.js} +37 -137
- package/dist/_chunks/scanner-B_tnqFcF.js.map +1 -0
- package/dist/_chunks/segment-keys-D5hu1hz4.js +62 -0
- package/dist/_chunks/segment-keys-D5hu1hz4.js.map +1 -0
- package/dist/_chunks/tree-match-CdbvYTBz.js +122 -0
- package/dist/_chunks/tree-match-CdbvYTBz.js.map +1 -0
- package/dist/_chunks/{walkers-B6XUtmqK.js → walkers-Cm3PC5JT.js} +2 -2
- package/dist/_chunks/{walkers-B6XUtmqK.js.map → walkers-Cm3PC5JT.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +3 -3
- package/dist/cache/index.js +1 -1
- package/dist/cli.d.ts +7 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +44 -7
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-dev.d.ts +5 -1
- package/dist/client/browser-dev.d.ts.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +20 -1
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +3 -15
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/error-reconstituter.d.ts +4 -4
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/internal.js +1 -1
- package/dist/dev-tools/debug-channel.d.ts +55 -0
- package/dist/dev-tools/debug-channel.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +302 -2245
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts +7 -0
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/scanner.d.ts +4 -4
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +2 -2
- package/dist/server/deny-boundary.d.ts +3 -5
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +0 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +5 -12
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +20 -195
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline.d.ts +8 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +15 -13
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +3 -7
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +6 -27
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +1 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +1 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +1 -1
- package/dist/server/slot-subtree-contain.d.ts.map +1 -1
- package/docs/api/30-api-server.mdx +5 -5
- package/docs/api/34-api-config.mdx +2 -2
- package/docs/api/36-cli.mdx +1 -1
- package/docs/learn/12-error-handling.mdx +7 -8
- package/package.json +2 -3
- package/src/cli.ts +62 -8
- package/src/client/browser-dev.ts +35 -3
- package/src/client/browser-entry/action-dispatch.ts +12 -0
- package/src/client/browser-entry/action-queue.ts +70 -8
- package/src/client/browser-entry/rsc-stream.ts +78 -22
- package/src/client/error-boundary.tsx +13 -39
- package/src/client/error-reconstituter.tsx +5 -5
- package/src/dev-tools/debug-channel.ts +151 -0
- package/src/index.ts +8 -12
- package/src/plugins/dev-server.ts +58 -7
- package/src/plugins/mdx.ts +2 -1
- package/src/routing/convention-lint.ts +4 -111
- package/src/routing/scanner.ts +33 -22
- package/src/server/access-gate.tsx +3 -3
- package/src/server/deny-boundary.ts +5 -23
- package/src/server/deny-renderer.ts +4 -7
- package/src/server/error-boundary-wrapper.ts +11 -35
- package/src/server/pipeline.ts +9 -0
- package/src/server/primitives.ts +20 -26
- package/src/server/rsc-entry/error-renderer.ts +15 -43
- package/src/server/rsc-entry/helpers.ts +10 -67
- package/src/server/rsc-entry/index.ts +1 -0
- package/src/server/rsc-entry/render-route.ts +12 -1
- package/src/server/rsc-entry/rsc-stream.ts +27 -24
- package/src/server/rsc-entry/ssr-renderer.ts +7 -1
- package/src/server/slot-resolver.ts +29 -7
- package/src/server/slot-subtree-contain.ts +13 -4
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +0 -1
- package/dist/_chunks/logger-uLBuGKDI.js.map +0 -1
- package/dist/_chunks/scanner-AiazgH_f.js.map +0 -1
- package/dist/_chunks/segment-keys-lqtdookO.js.map +0 -1
- package/dist/server/utils/mdx-file.d.ts +0 -17
- package/dist/server/utils/mdx-file.d.ts.map +0 -1
- 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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
-
*
|
|
264
|
-
*
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
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(): {
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
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
|
|
132
|
-
//
|
|
133
|
-
//
|
|
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
|
|
137
|
-
|
|
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
|
-
//
|
|
283
|
-
//
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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"}
|