@timber-js/app 0.2.0-alpha.204 → 0.2.0-alpha.206
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/dist/_chunks/{actions-HUdJADAD.js → actions-jNrXdzgs.js} +4 -3
- package/dist/_chunks/{actions-HUdJADAD.js.map → actions-jNrXdzgs.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
- package/dist/_chunks/{cache-api-CAPbZTga.js → cache-api-CUD9Ezaq.js} +3 -2
- package/dist/_chunks/{cache-api-CAPbZTga.js.map → cache-api-CUD9Ezaq.js.map} +1 -1
- package/dist/_chunks/{chains-C9Y7Oyno.js → chains-B4bgOl2g.js} +2 -2
- package/dist/_chunks/{chains-C9Y7Oyno.js.map → chains-B4bgOl2g.js.map} +1 -1
- package/dist/_chunks/{cli-check-BzMGuIH6.js → cli-check-BqOylm-3.js} +3 -3
- package/dist/_chunks/{cli-check-BzMGuIH6.js.map → cli-check-BqOylm-3.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DnXqcIIj.js → cli-schema-sync--LZKs8Zn.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DnXqcIIj.js.map → cli-schema-sync--LZKs8Zn.js.map} +1 -1
- package/dist/_chunks/{cloudflare-DxX1SU0g.js → cloudflare-BFb__LYG.js} +3 -3
- package/dist/_chunks/{cloudflare-DxX1SU0g.js.map → cloudflare-BFb__LYG.js.map} +1 -1
- package/dist/_chunks/{convention-lint-DHOFvX5s.js → convention-lint-BnO5TyHD.js} +2 -2
- package/dist/_chunks/{convention-lint-DHOFvX5s.js.map → convention-lint-BnO5TyHD.js.map} +1 -1
- package/dist/_chunks/{dev-server-BV01iyUs.js → dev-server-BzBxgYAV.js} +9 -196
- package/dist/_chunks/dev-server-BzBxgYAV.js.map +1 -0
- package/dist/_chunks/{error-boundary-BQKxl6EX.js → error-boundary-BndF-3Td.js} +2 -2
- package/dist/_chunks/{error-boundary-BQKxl6EX.js.map → error-boundary-BndF-3Td.js.map} +1 -1
- package/dist/_chunks/{graph-cache-Cv3njEH8.js → graph-cache-CP4GEmf9.js} +2 -2
- package/dist/_chunks/{graph-cache-Cv3njEH8.js.map → graph-cache-CP4GEmf9.js.map} +1 -1
- package/dist/_chunks/{live-graph-DCLQzV6m.js → live-graph-Bm5WTvcr.js} +3 -3
- package/dist/_chunks/{live-graph-DCLQzV6m.js.map → live-graph-Bm5WTvcr.js.map} +1 -1
- package/dist/_chunks/logger-k2DQ4EUf.js +506 -0
- package/dist/_chunks/logger-k2DQ4EUf.js.map +1 -0
- package/dist/_chunks/{poison-scan-CX_NaNDi.js → poison-scan-DG5eI2_z.js} +2 -2
- package/dist/_chunks/{poison-scan-CX_NaNDi.js.map → poison-scan-DG5eI2_z.js.map} +1 -1
- package/dist/_chunks/{logger-DqJ2VoAY.js → primitives-DLAnvsrk.js} +17 -518
- package/dist/_chunks/primitives-DLAnvsrk.js.map +1 -0
- package/dist/_chunks/{scanner-B_tnqFcF.js → scanner-BQ5sPIJ7.js} +4 -4
- package/dist/_chunks/{scanner-B_tnqFcF.js.map → scanner-BQ5sPIJ7.js.map} +1 -1
- package/dist/_chunks/{tree-match-CdbvYTBz.js → tree-match-6yihorhk.js} +49 -2
- package/dist/_chunks/tree-match-6yihorhk.js.map +1 -0
- package/dist/_chunks/{walkers-Cm3PC5JT.js → walkers-CN2N_bA-.js} +2 -2
- package/dist/_chunks/{walkers-Cm3PC5JT.js.map → walkers-CN2N_bA-.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.js +3 -3
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/internal.js +1 -1
- package/dist/dev-tools/index.d.ts +1 -1
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/dev-tools/overlay.d.ts +1 -21
- package/dist/dev-tools/overlay.d.ts.map +1 -1
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/als-registry.d.ts +0 -7
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +2 -6
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +180 -7
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline.d.ts +1 -13
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -38
- 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 +0 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +1 -7
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -1
- 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/dist/server/tree-match.d.ts +10 -0
- package/dist/server/tree-match.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry/rsc-stream.ts +1 -82
- package/src/dev-tools/index.ts +0 -2
- package/src/dev-tools/overlay.ts +1 -65
- package/src/plugins/dev-server.ts +4 -45
- package/src/server/als-registry.ts +0 -8
- package/src/server/deny-renderer.ts +0 -7
- package/src/server/pipeline.ts +1 -14
- package/src/server/route-matcher.ts +73 -20
- package/src/server/rsc-entry/deny-fallback.ts +1 -10
- package/src/server/rsc-entry/error-renderer.ts +1 -11
- package/src/server/rsc-entry/helpers.ts +0 -92
- package/src/server/rsc-entry/index.ts +1 -14
- package/src/server/rsc-entry/render-route.ts +2 -23
- package/src/server/rsc-entry/rsc-payload.ts +1 -2
- package/src/server/rsc-entry/rsc-stream.ts +3 -29
- package/src/server/rsc-entry/ssr-renderer.ts +2 -15
- package/src/server/slot-resolver.ts +7 -29
- package/src/server/slot-subtree-contain.ts +22 -14
- package/src/server/tree-match.ts +250 -18
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
- package/dist/_chunks/csp-nonce-hOGniaG4.js +0 -227
- package/dist/_chunks/csp-nonce-hOGniaG4.js.map +0 -1
- package/dist/_chunks/dev-server-BV01iyUs.js.map +0 -1
- package/dist/_chunks/logger-DqJ2VoAY.js.map +0 -1
- package/dist/_chunks/tree-match-CdbvYTBz.js.map +0 -1
- package/dist/dev-tools/debug-channel.d.ts +0 -55
- package/dist/dev-tools/debug-channel.d.ts.map +0 -1
- package/src/dev-tools/debug-channel.ts +0 -151
|
@@ -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
|
|
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
|
-
|
|
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
|
|
271
|
+
return walk(tree, { fallback, opaque }, new WeakSet()) as ReactNode;
|
|
264
272
|
}
|
package/src/server/tree-match.ts
CHANGED
|
@@ -66,6 +66,27 @@ export function matchUrlParts<TFile>(
|
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Yield every match for the given URL parts, in priority order.
|
|
71
|
+
*
|
|
72
|
+
* Same matching semantics as `matchUrlParts` — static > affixed-dynamic >
|
|
73
|
+
* bare-dynamic > catch-all > optional-catch-all — but instead of returning
|
|
74
|
+
* the first match, it yields all of them. Used by the metadata route matcher,
|
|
75
|
+
* which needs to try alternate owner segments when the first match doesn't
|
|
76
|
+
* own the requested metadata file.
|
|
77
|
+
*/
|
|
78
|
+
export function* matchUrlPartsAll<TFile>(
|
|
79
|
+
root: SegmentNode<TFile>,
|
|
80
|
+
parts: string[],
|
|
81
|
+
options?: TreeMatchOptions
|
|
82
|
+
): Generator<TreeMatchResult<TFile>> {
|
|
83
|
+
const chain: SegmentNode<TFile>[] = [];
|
|
84
|
+
const params: Record<string, string | string[]> = {};
|
|
85
|
+
const requireTerminal = options?.requireTerminal ?? true;
|
|
86
|
+
|
|
87
|
+
yield* matchNodeAll(root, parts, 0, chain, params, requireTerminal);
|
|
88
|
+
}
|
|
89
|
+
|
|
69
90
|
// ─── Internal ─────────────────────────────────────────────────────────────
|
|
70
91
|
|
|
71
92
|
/**
|
|
@@ -107,6 +128,59 @@ function isTerminal<TFile>(node: SegmentNode<TFile>): boolean {
|
|
|
107
128
|
return !!(node.page || node.route);
|
|
108
129
|
}
|
|
109
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Specificity tier for a segment. Lower = more specific. Matches the
|
|
133
|
+
* priority order in `matchNode`: static > affixed-dynamic > bare-dynamic >
|
|
134
|
+
* catch-all > optional-catch-all.
|
|
135
|
+
*/
|
|
136
|
+
function segmentTier<TFile>(node: SegmentNode<TFile>): number {
|
|
137
|
+
if (node.segmentType === 'static') return 0;
|
|
138
|
+
if (node.segmentType === 'dynamic' && (node.paramPrefix || node.paramSuffix)) return 1;
|
|
139
|
+
if (node.segmentType === 'dynamic') return 2;
|
|
140
|
+
if (node.segmentType === 'catch-all') return 3;
|
|
141
|
+
if (node.segmentType === 'optional-catch-all') return 4;
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Score a bare-dynamic candidate by running `matchNode` against its subtree
|
|
147
|
+
* with the remaining URL parts, then reading the tier of each segment in
|
|
148
|
+
* the matched chain. Returns a tier array that is compared lexicographically
|
|
149
|
+
* — the candidate whose chain has a lower tier at the first differing
|
|
150
|
+
* position is more specific.
|
|
151
|
+
*
|
|
152
|
+
* Delegates entirely to `matchNode`, so every segment type and priority
|
|
153
|
+
* rule is inherited automatically — no second copy to drift.
|
|
154
|
+
*/
|
|
155
|
+
function subtreeMatchScore<TFile>(
|
|
156
|
+
node: SegmentNode<TFile>,
|
|
157
|
+
parts: string[],
|
|
158
|
+
index: number,
|
|
159
|
+
requireTerminal: boolean
|
|
160
|
+
): number[] {
|
|
161
|
+
const chain: SegmentNode<TFile>[] = [];
|
|
162
|
+
const params: Record<string, string | string[]> = {};
|
|
163
|
+
if (!matchNode(node, parts, index, chain, params, requireTerminal)) return [];
|
|
164
|
+
// Skip the first entry (the candidate node itself — it's the bare-dynamic
|
|
165
|
+
// we're scoring, which is the same tier for all candidates).
|
|
166
|
+
return chain.slice(1).map((seg) => segmentTier(seg));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Compare two tier arrays lexicographically. Negative means `a` is more
|
|
171
|
+
* specific (should be tried first).
|
|
172
|
+
*/
|
|
173
|
+
function compareTierArrays(a: number[], b: number[]): number {
|
|
174
|
+
const len = Math.max(a.length, b.length);
|
|
175
|
+
for (let i = 0; i < len; i++) {
|
|
176
|
+
// A shorter match is less specific — missing entries treated as worst tier.
|
|
177
|
+
const ta = i < a.length ? a[i] : 5;
|
|
178
|
+
const tb = i < b.length ? b[i] : 5;
|
|
179
|
+
if (ta !== tb) return ta - tb;
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
110
184
|
/** Combined affix length for specificity ordering. */
|
|
111
185
|
function affixLength<TFile>(node: SegmentNode<TFile>): number {
|
|
112
186
|
return (node.paramPrefix?.length ?? 0) + (node.paramSuffix?.length ?? 0);
|
|
@@ -206,32 +280,177 @@ function matchNode<TFile>(
|
|
|
206
280
|
}
|
|
207
281
|
|
|
208
282
|
// 3. Bare dynamic segments ([param])
|
|
283
|
+
// When multiple bare dynamics compete (common with route groups that
|
|
284
|
+
// flatten into the same parent), sort by subtree specificity so a
|
|
285
|
+
// branch containing a static match for the NEXT URL part is tried
|
|
286
|
+
// first. Without this, the winner depends on children order and
|
|
287
|
+
// renaming a route group can flip it — the exact shape the collision
|
|
288
|
+
// validator exists to reject.
|
|
289
|
+
const bareDynamic = effective.filter(
|
|
290
|
+
({ child }) =>
|
|
291
|
+
child.segmentType === 'dynamic' && child.paramName && !child.paramPrefix && !child.paramSuffix
|
|
292
|
+
);
|
|
293
|
+
if (bareDynamic.length > 1 && index + 1 < parts.length) {
|
|
294
|
+
const scores = new Map<SegmentNode<TFile>, number[]>();
|
|
295
|
+
for (const { child } of bareDynamic) {
|
|
296
|
+
scores.set(child, subtreeMatchScore(child, parts, index + 1, requireTerminal));
|
|
297
|
+
}
|
|
298
|
+
bareDynamic.sort((a, b) => compareTierArrays(scores.get(a.child)!, scores.get(b.child)!));
|
|
299
|
+
}
|
|
300
|
+
for (const { child, groupChain } of bareDynamic) {
|
|
301
|
+
chain.push(...groupChain);
|
|
302
|
+
const prev = params[child.paramName!];
|
|
303
|
+
params[child.paramName!] = part;
|
|
304
|
+
if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
// Backtrack
|
|
308
|
+
chain.length -= groupChain.length;
|
|
309
|
+
if (prev !== undefined) {
|
|
310
|
+
params[child.paramName!] = prev;
|
|
311
|
+
} else {
|
|
312
|
+
delete params[child.paramName!];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 4. Catch-all segments ([...param]) — must be terminal unless the
|
|
317
|
+
// caller passed requireTerminal:false (metadata route matching needs to
|
|
318
|
+
// find the owning segment even when it has no page).
|
|
209
319
|
for (const { child, groupChain } of effective) {
|
|
210
320
|
if (
|
|
211
|
-
child.segmentType === '
|
|
321
|
+
child.segmentType === 'catch-all' &&
|
|
212
322
|
child.paramName &&
|
|
213
|
-
!child
|
|
214
|
-
!child.paramSuffix
|
|
323
|
+
(!requireTerminal || isTerminal(child))
|
|
215
324
|
) {
|
|
216
|
-
chain.push(...groupChain);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
325
|
+
chain.push(...groupChain, child);
|
|
326
|
+
params[child.paramName] = parts.slice(index);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 5. Optional catch-all segments ([[...param]]) — same terminal rule
|
|
332
|
+
for (const { child, groupChain } of effective) {
|
|
333
|
+
if (
|
|
334
|
+
child.segmentType === 'optional-catch-all' &&
|
|
335
|
+
child.paramName &&
|
|
336
|
+
(!requireTerminal || isTerminal(child))
|
|
337
|
+
) {
|
|
338
|
+
chain.push(...groupChain, child);
|
|
339
|
+
params[child.paramName] = parts.slice(index);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
chain.pop();
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Generator twin of `matchNode` — yields every match instead of returning
|
|
350
|
+
* the first. Each yield snapshots `chain` and `params` so the caller gets
|
|
351
|
+
* independent results. After yielding, backtracking continues to find the
|
|
352
|
+
* next match.
|
|
353
|
+
*/
|
|
354
|
+
function* matchNodeAll<TFile>(
|
|
355
|
+
node: SegmentNode<TFile>,
|
|
356
|
+
parts: string[],
|
|
357
|
+
index: number,
|
|
358
|
+
chain: SegmentNode<TFile>[],
|
|
359
|
+
params: Record<string, string | string[]>,
|
|
360
|
+
requireTerminal: boolean
|
|
361
|
+
): Generator<TreeMatchResult<TFile>> {
|
|
362
|
+
chain.push(node);
|
|
363
|
+
|
|
364
|
+
if (index >= parts.length) {
|
|
365
|
+
if (!requireTerminal || isTerminal(node)) {
|
|
366
|
+
yield { chain: [...chain], params: { ...params } };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const child of node.children) {
|
|
370
|
+
if (child.segmentType === 'group') {
|
|
371
|
+
yield* matchNodeAll(child, parts, index, chain, params, requireTerminal);
|
|
221
372
|
}
|
|
222
|
-
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const effective = collectEffectiveChildren(node);
|
|
376
|
+
for (const { child, groupChain } of effective) {
|
|
377
|
+
if (child.segmentType === 'optional-catch-all' && (!requireTerminal || isTerminal(child))) {
|
|
378
|
+
chain.push(...groupChain, child);
|
|
379
|
+
yield { chain: [...chain], params: { ...params } };
|
|
380
|
+
chain.length -= groupChain.length + 1;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
chain.pop();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const part = parts[index];
|
|
389
|
+
const effective = collectEffectiveChildren(node);
|
|
390
|
+
|
|
391
|
+
// 1. Static segments
|
|
392
|
+
for (const { child, groupChain } of effective) {
|
|
393
|
+
if (child.segmentType === 'static' && child.segmentName === part) {
|
|
394
|
+
chain.push(...groupChain);
|
|
395
|
+
yield* matchNodeAll(child, parts, index + 1, chain, params, requireTerminal);
|
|
396
|
+
chain.length -= groupChain.length;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 2. Affixed dynamic segments
|
|
401
|
+
const affixed = effective.filter(
|
|
402
|
+
({ child }) =>
|
|
403
|
+
child.segmentType === 'dynamic' && child.paramName && (child.paramPrefix || child.paramSuffix)
|
|
404
|
+
);
|
|
405
|
+
affixed.sort((a, b) => affixLength(b.child) - affixLength(a.child));
|
|
406
|
+
for (const { child, groupChain } of affixed) {
|
|
407
|
+
const prefix = child.paramPrefix ?? '';
|
|
408
|
+
const suffix = child.paramSuffix ?? '';
|
|
409
|
+
if (
|
|
410
|
+
part.length > prefix.length + suffix.length &&
|
|
411
|
+
(!prefix || part.startsWith(prefix)) &&
|
|
412
|
+
(!suffix || part.endsWith(suffix))
|
|
413
|
+
) {
|
|
414
|
+
const value = part.slice(prefix.length, part.length - (suffix.length || 0));
|
|
415
|
+
chain.push(...groupChain);
|
|
416
|
+
const prev = params[child.paramName!];
|
|
417
|
+
params[child.paramName!] = value;
|
|
418
|
+
yield* matchNodeAll(child, parts, index + 1, chain, params, requireTerminal);
|
|
223
419
|
chain.length -= groupChain.length;
|
|
224
420
|
if (prev !== undefined) {
|
|
225
|
-
params[child.paramName] = prev;
|
|
421
|
+
params[child.paramName!] = prev;
|
|
226
422
|
} else {
|
|
227
|
-
delete params[child.paramName];
|
|
423
|
+
delete params[child.paramName!];
|
|
228
424
|
}
|
|
229
425
|
}
|
|
230
426
|
}
|
|
231
427
|
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
428
|
+
// 3. Bare dynamic segments
|
|
429
|
+
const bareDynamic = effective.filter(
|
|
430
|
+
({ child }) =>
|
|
431
|
+
child.segmentType === 'dynamic' && child.paramName && !child.paramPrefix && !child.paramSuffix
|
|
432
|
+
);
|
|
433
|
+
if (bareDynamic.length > 1 && index + 1 < parts.length) {
|
|
434
|
+
const scores = new Map<SegmentNode<TFile>, number[]>();
|
|
435
|
+
for (const { child } of bareDynamic) {
|
|
436
|
+
scores.set(child, subtreeMatchScore(child, parts, index + 1, requireTerminal));
|
|
437
|
+
}
|
|
438
|
+
bareDynamic.sort((a, b) => compareTierArrays(scores.get(a.child)!, scores.get(b.child)!));
|
|
439
|
+
}
|
|
440
|
+
for (const { child, groupChain } of bareDynamic) {
|
|
441
|
+
chain.push(...groupChain);
|
|
442
|
+
const prev = params[child.paramName!];
|
|
443
|
+
params[child.paramName!] = part;
|
|
444
|
+
yield* matchNodeAll(child, parts, index + 1, chain, params, requireTerminal);
|
|
445
|
+
chain.length -= groupChain.length;
|
|
446
|
+
if (prev !== undefined) {
|
|
447
|
+
params[child.paramName!] = prev;
|
|
448
|
+
} else {
|
|
449
|
+
delete params[child.paramName!];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 4. Catch-all segments
|
|
235
454
|
for (const { child, groupChain } of effective) {
|
|
236
455
|
if (
|
|
237
456
|
child.segmentType === 'catch-all' &&
|
|
@@ -239,12 +458,19 @@ function matchNode<TFile>(
|
|
|
239
458
|
(!requireTerminal || isTerminal(child))
|
|
240
459
|
) {
|
|
241
460
|
chain.push(...groupChain, child);
|
|
461
|
+
const prev = params[child.paramName];
|
|
242
462
|
params[child.paramName] = parts.slice(index);
|
|
243
|
-
|
|
463
|
+
yield { chain: [...chain], params: { ...params } };
|
|
464
|
+
chain.length -= groupChain.length + 1;
|
|
465
|
+
if (prev !== undefined) {
|
|
466
|
+
params[child.paramName] = prev;
|
|
467
|
+
} else {
|
|
468
|
+
delete params[child.paramName];
|
|
469
|
+
}
|
|
244
470
|
}
|
|
245
471
|
}
|
|
246
472
|
|
|
247
|
-
// 5. Optional catch-all segments
|
|
473
|
+
// 5. Optional catch-all segments
|
|
248
474
|
for (const { child, groupChain } of effective) {
|
|
249
475
|
if (
|
|
250
476
|
child.segmentType === 'optional-catch-all' &&
|
|
@@ -252,11 +478,17 @@ function matchNode<TFile>(
|
|
|
252
478
|
(!requireTerminal || isTerminal(child))
|
|
253
479
|
) {
|
|
254
480
|
chain.push(...groupChain, child);
|
|
481
|
+
const prev = params[child.paramName];
|
|
255
482
|
params[child.paramName] = parts.slice(index);
|
|
256
|
-
|
|
483
|
+
yield { chain: [...chain], params: { ...params } };
|
|
484
|
+
chain.length -= groupChain.length + 1;
|
|
485
|
+
if (prev !== undefined) {
|
|
486
|
+
params[child.paramName] = prev;
|
|
487
|
+
} else {
|
|
488
|
+
delete params[child.paramName];
|
|
489
|
+
}
|
|
257
490
|
}
|
|
258
491
|
}
|
|
259
492
|
|
|
260
493
|
chain.pop();
|
|
261
|
-
return false;
|
|
262
494
|
}
|
|
@@ -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"}
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { r as requestContextAls } from "./als-registry-C6kcfprT.js";
|
|
2
|
-
import { R as getSetCookieHeaders, m as setLogger, q as getTraceId } from "./logger-DqJ2VoAY.js";
|
|
3
|
-
import { r as isRscRequest } from "./rsc-media-type-DDc7duTD.js";
|
|
4
|
-
//#region src/server/instrumentation.ts
|
|
5
|
-
/**
|
|
6
|
-
* Instrumentation — loads and runs the user's instrumentation.ts file.
|
|
7
|
-
*
|
|
8
|
-
* instrumentation.ts is a file convention at the project root that exports:
|
|
9
|
-
* - register() — called once at server startup, before the first request
|
|
10
|
-
* - onRequestError() — called for every unhandled server error
|
|
11
|
-
* - logger — any object with info/warn/error/debug methods
|
|
12
|
-
*
|
|
13
|
-
* See design/17-logging.md §"instrumentation.ts — The Entry Point"
|
|
14
|
-
*/
|
|
15
|
-
var _initialized = false;
|
|
16
|
-
var _onRequestError = null;
|
|
17
|
-
/**
|
|
18
|
-
* Load and initialize the user's instrumentation.ts module.
|
|
19
|
-
*
|
|
20
|
-
* - Awaits register() before returning (server blocks on this).
|
|
21
|
-
* - Picks up the logger export and wires it into the framework logger.
|
|
22
|
-
* - Stores onRequestError for later invocation.
|
|
23
|
-
*
|
|
24
|
-
* @param loader - Function that dynamically imports the user's instrumentation module.
|
|
25
|
-
* Returns null if no instrumentation.ts exists.
|
|
26
|
-
*/
|
|
27
|
-
async function loadInstrumentation(loader) {
|
|
28
|
-
if (_initialized) return;
|
|
29
|
-
_initialized = true;
|
|
30
|
-
let mod;
|
|
31
|
-
try {
|
|
32
|
-
mod = await loader();
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error("[timber] Failed to load instrumentation.ts:", error);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (!mod) return;
|
|
38
|
-
if (mod.logger && typeof mod.logger.info === "function") setLogger(mod.logger);
|
|
39
|
-
if (typeof mod.onRequestError === "function") _onRequestError = mod.onRequestError;
|
|
40
|
-
if (typeof mod.register === "function") try {
|
|
41
|
-
await mod.register();
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error("[timber] instrumentation.ts register() threw:", error);
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Call the user's onRequestError hook. Catches and logs any errors thrown
|
|
49
|
-
* by the hook itself — it must not affect the response.
|
|
50
|
-
*/
|
|
51
|
-
async function callOnRequestError(error, request, context) {
|
|
52
|
-
if (!_onRequestError) return;
|
|
53
|
-
try {
|
|
54
|
-
await _onRequestError(error, request, context);
|
|
55
|
-
} catch (hookError) {
|
|
56
|
-
console.error("[timber] onRequestError hook threw:", hookError);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Check if onRequestError is registered.
|
|
61
|
-
*/
|
|
62
|
-
function hasOnRequestError() {
|
|
63
|
-
return _onRequestError !== null;
|
|
64
|
-
}
|
|
65
|
-
//#endregion
|
|
66
|
-
//#region src/server/pipeline-helpers.ts
|
|
67
|
-
/**
|
|
68
|
-
* Build a proxy resolver closure from the declared source. Called exactly
|
|
69
|
-
* once at `createPipeline` setup time, so the hot path sees only the branch
|
|
70
|
-
* that corresponds to this pipeline's configured variant.
|
|
71
|
-
*
|
|
72
|
-
* Returns `null` when the app has no proxy.ts — the hot path short-circuits
|
|
73
|
-
* around `runProxyPhase` entirely in that case.
|
|
74
|
-
*
|
|
75
|
-
* Accepts the sugar form (a bare `ProxyExport` — function or function array)
|
|
76
|
-
* and normalises it to the static variant. Functions and arrays are
|
|
77
|
-
* structurally distinct from the tagged `{ kind: 'lazy', loader }` object,
|
|
78
|
-
* so discrimination is unambiguous.
|
|
79
|
-
*/
|
|
80
|
-
function makeProxyResolver(proxy) {
|
|
81
|
-
if (proxy === void 0) return null;
|
|
82
|
-
if (typeof proxy === "function" || Array.isArray(proxy)) {
|
|
83
|
-
const exp = proxy;
|
|
84
|
-
return () => exp;
|
|
85
|
-
}
|
|
86
|
-
if (proxy.kind === "static") {
|
|
87
|
-
const exp = proxy.export;
|
|
88
|
-
return () => exp;
|
|
89
|
-
}
|
|
90
|
-
const loader = proxy.loader;
|
|
91
|
-
return async () => (await loader()).default;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Apply all Set-Cookie headers from the cookie jar to a Headers object.
|
|
95
|
-
* Each cookie gets its own Set-Cookie header per RFC 6265 §4.1.
|
|
96
|
-
*/
|
|
97
|
-
function applyCookieJar(headers) {
|
|
98
|
-
for (const value of getSetCookieHeaders()) headers.append("Set-Cookie", value);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Merge framework-managed response headers onto a terminal response without
|
|
102
|
-
* overwriting headers the terminal response already set itself.
|
|
103
|
-
*/
|
|
104
|
-
function mergeMissingHeaders(target, source) {
|
|
105
|
-
const existingKeys = new Set([...target.keys()].map((key) => key.toLowerCase()));
|
|
106
|
-
for (const [key, value] of source.entries()) if (!existingKeys.has(key.toLowerCase())) target.append(key, value);
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Clone a Response into a fresh one whose header bag is guaranteed mutable.
|
|
110
|
-
*
|
|
111
|
-
* `Response.redirect()` and some platform-level passthrough responses (notably
|
|
112
|
-
* on Cloudflare Workers) return objects with frozen header bags. Calling
|
|
113
|
-
* `.set()` or `.append()` on them throws `TypeError: immutable`, which the
|
|
114
|
-
* pipeline can hit when it appends Set-Cookie or Server-Timing entries.
|
|
115
|
-
*
|
|
116
|
-
* The pipeline calls this at the producer sites where user-controlled
|
|
117
|
-
* responses enter the framework — `outcomeToResponse` for all phase outcomes,
|
|
118
|
-
* and `handleRequest` for metadata-route and auto-sitemap user handlers — so
|
|
119
|
-
* downstream code can write headers without runtime feature-detection.
|
|
120
|
-
*
|
|
121
|
-
* The clone is unconditional. This is a deliberate trade: we avoid a
|
|
122
|
-
* try/catch + thrown `TypeError` on every request (the previous probe-based
|
|
123
|
-
* approach paid that cost on the hot path) and accept one cheap Response
|
|
124
|
-
* rewrap at the framework boundary instead.
|
|
125
|
-
*/
|
|
126
|
-
function cloneWithMutableHeaders(response) {
|
|
127
|
-
return new Response(response.body, {
|
|
128
|
-
status: response.status,
|
|
129
|
-
statusText: response.statusText,
|
|
130
|
-
headers: new Headers(response.headers)
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Build a redirect Response from a RedirectSignal.
|
|
135
|
-
*
|
|
136
|
-
* For RSC payload requests (client navigation), returns 204 + X-Timber-Redirect
|
|
137
|
-
* so the client router can perform a soft SPA redirect. A raw 302 would be
|
|
138
|
-
* turned into an opaque redirect by fetch({redirect:'manual'}), crashing
|
|
139
|
-
* createFromFetch. See design/19-client-navigation.md.
|
|
140
|
-
*/
|
|
141
|
-
function buildRedirectResponse(signal, req, headers) {
|
|
142
|
-
if (isRscRequest(req)) {
|
|
143
|
-
headers.set("X-Timber-Redirect", signal.location);
|
|
144
|
-
return new Response(null, {
|
|
145
|
-
status: 204,
|
|
146
|
-
headers
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
headers.set("Location", signal.location);
|
|
150
|
-
return new Response(null, {
|
|
151
|
-
status: signal.status,
|
|
152
|
-
headers
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Fire the user's onRequestError hook with request context.
|
|
157
|
-
* Extracts request info from the Request object and calls the instrumentation hook.
|
|
158
|
-
*/
|
|
159
|
-
async function fireOnRequestError(error, req, phase) {
|
|
160
|
-
const url = new URL(req.url);
|
|
161
|
-
const headersObj = {};
|
|
162
|
-
req.headers.forEach((v, k) => {
|
|
163
|
-
headersObj[k] = v;
|
|
164
|
-
});
|
|
165
|
-
await callOnRequestError(error, {
|
|
166
|
-
method: req.method,
|
|
167
|
-
path: url.pathname,
|
|
168
|
-
headers: headersObj
|
|
169
|
-
}, {
|
|
170
|
-
phase,
|
|
171
|
-
routePath: url.pathname,
|
|
172
|
-
routeType: "page",
|
|
173
|
-
traceId: getTraceId()
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
//#endregion
|
|
177
|
-
//#region src/server/csp-nonce.ts
|
|
178
|
-
/**
|
|
179
|
-
* CSP nonce — per-request, generated on demand.
|
|
180
|
-
*
|
|
181
|
-
* timber emits several inline `<script>` tags per HTML response — React's
|
|
182
|
-
* bootstrap script and Fizz runtime, the Flight init/chunk scripts, the
|
|
183
|
-
* segment-tree script, and the post-flush noindex script. Under a strict
|
|
184
|
-
* `Content-Security-Policy` (`script-src 'nonce-…' 'strict-dynamic'`) every
|
|
185
|
-
* one of them needs the request's nonce or the browser refuses to run it.
|
|
186
|
-
*
|
|
187
|
-
* The app opts in by reading `ctx.nonce` in `middleware.ts` and putting it
|
|
188
|
-
* in the CSP header it sets through `ctx.headers`. The first read generates
|
|
189
|
-
* the value and stores it on the request context; the render pipeline reads
|
|
190
|
-
* it back with `getCspNonce()` and stamps it on every inline script. A
|
|
191
|
-
* request whose middleware never reads `ctx.nonce` has no nonce, and its
|
|
192
|
-
* markup is byte-identical to a render without this feature. timber does
|
|
193
|
-
* not parse the CSP header — the app owns the policy, timber owns the
|
|
194
|
-
* markup, and `ctx.nonce` is the one value both are built from.
|
|
195
|
-
*
|
|
196
|
-
* `proxy.ts` cannot participate: it only sees the finished response from
|
|
197
|
-
* `next()`, after the markup exists.
|
|
198
|
-
*
|
|
199
|
-
* See design/13-security.md §"Content Security Policy" (TIM-1414).
|
|
200
|
-
*/
|
|
201
|
-
/**
|
|
202
|
-
* Generate a CSP nonce: 128 random bits, base64. Matches CSP's
|
|
203
|
-
* `base64-value` grammar, so it is valid in the header verbatim and safe
|
|
204
|
-
* inside a double-quoted HTML attribute without escaping.
|
|
205
|
-
*/
|
|
206
|
-
function generateNonce() {
|
|
207
|
-
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
208
|
-
let binary = "";
|
|
209
|
-
for (const b of bytes) binary += String.fromCharCode(b);
|
|
210
|
-
return btoa(binary);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* The request's CSP nonce, creating it on first call. Backs the
|
|
214
|
-
* `ctx.nonce` getter on `MiddlewareContext`. Outside a request context
|
|
215
|
-
* (tests, build-time rendering) a fresh value is returned but not
|
|
216
|
-
* retained, since there is no request for it to belong to.
|
|
217
|
-
*/
|
|
218
|
-
function requestCspNonce() {
|
|
219
|
-
const store = requestContextAls.getStore();
|
|
220
|
-
if (!store) return generateNonce();
|
|
221
|
-
store.cspNonce ??= generateNonce();
|
|
222
|
-
return store.cspNonce;
|
|
223
|
-
}
|
|
224
|
-
//#endregion
|
|
225
|
-
export { fireOnRequestError as a, callOnRequestError as c, cloneWithMutableHeaders as i, hasOnRequestError as l, applyCookieJar as n, makeProxyResolver as o, buildRedirectResponse as r, mergeMissingHeaders as s, requestCspNonce as t, loadInstrumentation as u };
|
|
226
|
-
|
|
227
|
-
//# sourceMappingURL=csp-nonce-hOGniaG4.js.map
|