@timber-js/app 0.2.0-alpha.190 → 0.2.0-alpha.191
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-35jnMdeJ.js → actions-BjbbNRFN.js} +3 -3
- package/dist/_chunks/{actions-35jnMdeJ.js.map → actions-BjbbNRFN.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -1
- package/dist/_chunks/{cache-api-DjNrIWRR.js → cache-api-DllJ-Lyw.js} +2 -2
- package/dist/_chunks/{cache-api-DjNrIWRR.js.map → cache-api-DllJ-Lyw.js.map} +1 -1
- package/dist/_chunks/{cli-check-CpmN7Nh-.js → cli-check-DJZDc22E.js} +3 -3
- package/dist/_chunks/{cli-check-CpmN7Nh-.js.map → cli-check-DJZDc22E.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-CGMp_Psg.js → cli-schema-sync-D4_AZVUb.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-CGMp_Psg.js.map → cli-schema-sync-D4_AZVUb.js.map} +1 -1
- package/dist/_chunks/{convention-lint-kXsgc_-7.js → convention-lint-CpteIpTm.js} +2 -2
- package/dist/_chunks/{convention-lint-kXsgc_-7.js.map → convention-lint-CpteIpTm.js.map} +1 -1
- package/dist/_chunks/{logger-D8xJZXIN.js → logger-CH7IcMmg.js} +1 -16
- package/dist/_chunks/{logger-D8xJZXIN.js.map → logger-CH7IcMmg.js.map} +1 -1
- package/dist/_chunks/{scanner-C8b0Gcw3.js → scanner-CAietmj4.js} +2 -2
- package/dist/_chunks/{scanner-C8b0Gcw3.js.map → scanner-CAietmj4.js.map} +1 -1
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -1
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -1
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -1
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -1
- package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -1
- package/dist/_chunks/{walkers-RzN6AFjr.js → walkers-BsVLmD1S.js} +2 -2
- package/dist/_chunks/{walkers-RzN6AFjr.js.map → walkers-BsVLmD1S.js.map} +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/internal.js.map +1 -1
- package/dist/client/params-context.d.ts +2 -1
- package/dist/client/params-context.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +2 -1
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +3 -2
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/use-segment-params.d.ts +5 -4
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/routing/index.js +2 -2
- package/dist/routing/segment-keys.d.ts +2 -1
- package/dist/routing/segment-keys.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +10 -5
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +2 -1
- package/dist/server/chain-url-parts.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +3 -3
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +40 -18
- package/dist/server/metadata-collector.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +2 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +3 -2
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt/cache-key.d.ts +2 -1
- package/dist/server/prebuilt/cache-key.d.ts.map +1 -1
- package/dist/server/prebuilt/payload-source.d.ts +2 -1
- package/dist/server/prebuilt/payload-source.d.ts.map +1 -1
- package/dist/server/prebuilt/synthetic-store.d.ts +2 -1
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
- package/dist/server/prebuilt-builder.d.ts +4 -3
- package/dist/server/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/prebuilt-runtime.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +6 -5
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts +2 -1
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +5 -3
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/types.d.ts +2 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/shared/als-slots.d.ts +3 -2
- package/dist/shared/als-slots.d.ts.map +1 -1
- package/dist/shared/param-value.d.ts +25 -1
- package/dist/shared/param-value.d.ts.map +1 -1
- package/dist/shared/payload-root.d.ts +2 -1
- package/dist/shared/payload-root.d.ts.map +1 -1
- package/dist/shared/slot-params.d.ts +4 -3
- package/dist/shared/slot-params.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/params-context.ts +2 -2
- package/src/client/ssr-data.ts +2 -1
- package/src/client/state.ts +3 -2
- package/src/client/use-segment-params.ts +6 -5
- package/src/routing/segment-keys.ts +5 -2
- package/src/server/action-handler.ts +106 -8
- package/src/server/als-registry.ts +10 -5
- package/src/server/chain-url-parts.ts +2 -1
- package/src/server/index.ts +4 -0
- package/src/server/metadata-collector.ts +114 -40
- package/src/server/param-coercion.ts +7 -7
- package/src/server/pipeline-phases.ts +3 -1
- package/src/server/pipeline.ts +3 -2
- package/src/server/prebuilt/cache-key.ts +2 -1
- package/src/server/prebuilt/payload-source.ts +2 -1
- package/src/server/prebuilt/synthetic-store.ts +2 -1
- package/src/server/prebuilt-builder.ts +6 -5
- package/src/server/prebuilt-runtime.ts +5 -3
- package/src/server/request-context.ts +6 -8
- package/src/server/route-element-builder.ts +18 -40
- package/src/server/sitemap-generator.ts +7 -9
- package/src/server/slot-resolver.ts +2 -1
- package/src/server/ssr-bridge-types.ts +6 -3
- package/src/server/types.ts +2 -1
- package/src/shared/als-slots.ts +3 -1
- package/src/shared/param-value.ts +44 -7
- package/src/shared/payload-root.ts +2 -1
- package/src/shared/slot-params.ts +11 -12
package/src/server/pipeline.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* and design/17-logging.md §"Production Logging"
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import type { CoercedParams } from '../shared/param-value.js';
|
|
18
19
|
import type { ProxyExport } from './proxy.js';
|
|
19
20
|
import type { MiddlewareFn } from './middleware-runner.js';
|
|
20
21
|
import { runWithTimingCollector, getServerTimingHeader } from './server-timing.js';
|
|
@@ -54,8 +55,8 @@ import { isDebug } from './debug.js';
|
|
|
54
55
|
export interface RouteMatch {
|
|
55
56
|
/** The matched segment chain from root to leaf. */
|
|
56
57
|
segments: ManifestSegmentNode[];
|
|
57
|
-
/** Extracted segment params (catch-all segments produce string[]). */
|
|
58
|
-
segmentParams:
|
|
58
|
+
/** Extracted segment params (catch-all segments produce string[]). Post-coercion, values may be non-string (TIM-1347). */
|
|
59
|
+
segmentParams: CoercedParams;
|
|
59
60
|
/**
|
|
60
61
|
* Raw segment params before codec coercion. Always string or string[].
|
|
61
62
|
* Used by slot resolution to reconstruct URL parts — coerced params may
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
43
|
import { createHash } from 'node:crypto';
|
|
44
|
+
import type { CoercedParams } from '../../shared/param-value.js';
|
|
44
45
|
|
|
45
46
|
class UnkeyableValueError extends Error {}
|
|
46
47
|
|
|
@@ -260,7 +261,7 @@ export function splitSlotProps(
|
|
|
260
261
|
*/
|
|
261
262
|
export function computeComponentCacheKey(
|
|
262
263
|
props: Record<string, unknown>,
|
|
263
|
-
params:
|
|
264
|
+
params: CoercedParams
|
|
264
265
|
): string | null {
|
|
265
266
|
let canonical: string;
|
|
266
267
|
try {
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { initTagIndex, resetTagIndex } from './overlay.js';
|
|
20
|
+
import type { CoercedParams } from '../../shared/param-value.js';
|
|
20
21
|
|
|
21
22
|
export interface PrebuiltManifestEntry {
|
|
22
23
|
/** Segment params this entry was captured under. */
|
|
23
|
-
params:
|
|
24
|
+
params: CoercedParams;
|
|
24
25
|
/** Payload path relative to the prebuilt/ directory. */
|
|
25
26
|
file: string;
|
|
26
27
|
/** Payload size in bytes (build report / diagnostics). */
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import type { RequestContextStore } from '../als-registry.js';
|
|
38
|
+
import type { CoercedParams } from '../../shared/param-value.js';
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Thrown when a prerendered component touches per-request context at
|
|
@@ -79,7 +80,7 @@ export interface BuildTimeStoreHandle {
|
|
|
79
80
|
*/
|
|
80
81
|
export function createBuildTimeStore(
|
|
81
82
|
componentId: string,
|
|
82
|
-
segmentParams:
|
|
83
|
+
segmentParams: CoercedParams
|
|
83
84
|
): BuildTimeStoreHandle {
|
|
84
85
|
const violations: BuildTimeContextError[] = [];
|
|
85
86
|
const handle: BuildTimeStoreHandle = {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
import { createElement } from 'react';
|
|
46
46
|
|
|
47
47
|
import { requestContextAls } from './als-registry.js';
|
|
48
|
+
import type { CoercedParams } from '../shared/param-value.js';
|
|
48
49
|
import { ParamCoercionError } from './route-element-builder.js';
|
|
49
50
|
import { SlotOutlet } from '../client/slot-outlet.js';
|
|
50
51
|
import { coerceSegmentParams } from './param-coercion.js';
|
|
@@ -76,8 +77,8 @@ export interface CapturedEntry {
|
|
|
76
77
|
componentId: string;
|
|
77
78
|
/** `computeComponentCacheKey(callSiteProps, coercedParams)` — names the .flight file. */
|
|
78
79
|
cacheKey: string;
|
|
79
|
-
/** The
|
|
80
|
-
params:
|
|
80
|
+
/** The param combo — manifest metadata, not the hash input. */
|
|
81
|
+
params: CoercedParams;
|
|
81
82
|
/** Call-site props used for this entry (empty object for zero-prop usages). */
|
|
82
83
|
props?: Record<string, unknown>;
|
|
83
84
|
bytes: Uint8Array;
|
|
@@ -91,7 +92,7 @@ export interface CapturedEntry {
|
|
|
91
92
|
export interface CaptureIssue {
|
|
92
93
|
componentId: string;
|
|
93
94
|
routeUrlPath: string;
|
|
94
|
-
params:
|
|
95
|
+
params: CoercedParams;
|
|
95
96
|
message: string;
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -219,7 +220,7 @@ async function loadLazyChunk(chunkFile: string): Promise<void> {
|
|
|
219
220
|
|
|
220
221
|
// ─── Param enumeration ────────────────────────────────────────────────────
|
|
221
222
|
|
|
222
|
-
type ParamCombo =
|
|
223
|
+
type ParamCombo = CoercedParams;
|
|
223
224
|
|
|
224
225
|
/**
|
|
225
226
|
* Resolve the param combos to capture for a route. Static routes have
|
|
@@ -333,7 +334,7 @@ interface EntryOutcome {
|
|
|
333
334
|
async function renderEntry(
|
|
334
335
|
registration: PrebuiltRegistration,
|
|
335
336
|
props: Record<string, unknown>,
|
|
336
|
-
coercedParams:
|
|
337
|
+
coercedParams: CoercedParams,
|
|
337
338
|
timeoutMs: number,
|
|
338
339
|
isLayout: boolean
|
|
339
340
|
): Promise<EntryOutcome> {
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
import { createElement } from 'react';
|
|
37
37
|
|
|
38
38
|
import { requestContextAls, type RequestContextStore } from './als-registry.js';
|
|
39
|
+
import type { CoercedParams } from '../shared/param-value.js';
|
|
40
|
+
import type { SlotParamsRecord } from '../shared/slot-params.js';
|
|
39
41
|
import { computeComponentCacheKey, splitSlotProps, tryCanonicalize } from './prebuilt/cache-key.js';
|
|
40
42
|
import {
|
|
41
43
|
isChildrenSlotOutletElement,
|
|
@@ -172,9 +174,9 @@ const registry = new Map<string, PrebuiltRegistration>();
|
|
|
172
174
|
* aliases, …) is unkeyable here too, and this guard can never throw.
|
|
173
175
|
*/
|
|
174
176
|
function resolveKeyParams(store: {
|
|
175
|
-
segmentParams?:
|
|
176
|
-
slotParams?:
|
|
177
|
-
}):
|
|
177
|
+
segmentParams?: CoercedParams;
|
|
178
|
+
slotParams?: SlotParamsRecord;
|
|
179
|
+
}): CoercedParams | null {
|
|
178
180
|
const main = store.segmentParams ?? {};
|
|
179
181
|
const slots = store.slotParams;
|
|
180
182
|
if (!slots || Object.keys(slots).length === 0) return main;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* and design/11-platform.md §"AsyncLocalStorage".
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import type { CoercedParams } from '../shared/param-value.js';
|
|
17
18
|
import { requestContextAls, type RequestContextStore } from './als-registry.js';
|
|
18
19
|
import { _setGetSearchParamsFn, _setGetSegmentParamsFn } from '../shared/als-slots.js';
|
|
19
20
|
import { consumeSeededCookies } from './cookie-context.js';
|
|
@@ -101,7 +102,7 @@ _setGetSegmentParamsFn(getSegmentParams);
|
|
|
101
102
|
*
|
|
102
103
|
* See design/41-global-params.md §Function Signatures
|
|
103
104
|
*/
|
|
104
|
-
export function getSegmentParams(segmentPath?: string):
|
|
105
|
+
export function getSegmentParams(segmentPath?: string): CoercedParams {
|
|
105
106
|
const store = requestContextAls.getStore();
|
|
106
107
|
if (!store) {
|
|
107
108
|
throw new Error(
|
|
@@ -152,7 +153,7 @@ export function getSegmentParams(segmentPath?: string): Record<string, string |
|
|
|
152
153
|
*
|
|
153
154
|
* @internal — framework use only
|
|
154
155
|
*/
|
|
155
|
-
export function setSegmentParams(params:
|
|
156
|
+
export function setSegmentParams(params: CoercedParams): void {
|
|
156
157
|
const store = requestContextAls.getStore();
|
|
157
158
|
if (!store) {
|
|
158
159
|
throw new Error('[timber] setSegmentParams() called outside of a request context.');
|
|
@@ -168,10 +169,7 @@ export function setSegmentParams(params: Record<string, string | string[]>): voi
|
|
|
168
169
|
* @param params — The slot's coerced segment params
|
|
169
170
|
* @internal — framework use only
|
|
170
171
|
*/
|
|
171
|
-
export function setSlotParams(
|
|
172
|
-
segmentPath: string,
|
|
173
|
-
params: Record<string, string | string[]>
|
|
174
|
-
): void {
|
|
172
|
+
export function setSlotParams(segmentPath: string, params: CoercedParams): void {
|
|
175
173
|
const store = requestContextAls.getStore();
|
|
176
174
|
if (!store) return; // non-throwing — optional diagnostic
|
|
177
175
|
if (!store.slotParams) store.slotParams = Object.create(null) as SlotParamsRecord;
|
|
@@ -203,7 +201,7 @@ export function setSlotParams(
|
|
|
203
201
|
*/
|
|
204
202
|
export function runWithSlotSegmentParams<T>(
|
|
205
203
|
slotSegmentPath: string | undefined,
|
|
206
|
-
slotParams:
|
|
204
|
+
slotParams: CoercedParams,
|
|
207
205
|
fn: () => T
|
|
208
206
|
): T {
|
|
209
207
|
const store = requestContextAls.getStore();
|
|
@@ -230,7 +228,7 @@ export function runWithSlotSegmentParams<T>(
|
|
|
230
228
|
*
|
|
231
229
|
* @internal — framework use only. See TIM-1285.
|
|
232
230
|
*/
|
|
233
|
-
export function getSegmentParamsForClient():
|
|
231
|
+
export function getSegmentParamsForClient(): CoercedParams {
|
|
234
232
|
return requestContextAls.getStore()?.segmentParams ?? {};
|
|
235
233
|
}
|
|
236
234
|
|
|
@@ -220,34 +220,6 @@ function rejectLegacyGenerateMetadata(mod: Record<string, unknown>, filePath: st
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/**
|
|
224
|
-
* Extract and resolve metadata from a module (layout or page).
|
|
225
|
-
* Handles both static metadata objects and async metadata functions.
|
|
226
|
-
* Returns the resolved Metadata, or null if none exported.
|
|
227
|
-
*
|
|
228
|
-
* Metadata functions no longer receive { params } — they access params
|
|
229
|
-
* via getSegmentParams() from ALS, same as page/layout components.
|
|
230
|
-
*/
|
|
231
|
-
async function extractMetadata(
|
|
232
|
-
mod: Record<string, unknown>,
|
|
233
|
-
segment: ManifestSegmentNode
|
|
234
|
-
): Promise<Metadata | null> {
|
|
235
|
-
if (typeof mod.metadata === 'function') {
|
|
236
|
-
type MetadataFn = () => Promise<Metadata>;
|
|
237
|
-
return (
|
|
238
|
-
(await withSpan(
|
|
239
|
-
'timber.metadata',
|
|
240
|
-
{ 'timber.segment': segment.segmentName ?? segment.urlPath },
|
|
241
|
-
() => (mod.metadata as MetadataFn)()
|
|
242
|
-
)) ?? null
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
if (mod.metadata) {
|
|
246
|
-
return mod.metadata as Metadata;
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
223
|
/**
|
|
252
224
|
* Extract `deferSuspenseFor` from a module and return the maximum
|
|
253
225
|
* of the current value and the module's value.
|
|
@@ -446,18 +418,24 @@ export async function buildRouteElement(
|
|
|
446
418
|
element = h(TracedPage, {});
|
|
447
419
|
}
|
|
448
420
|
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
421
|
+
// Place MetadataHead as a SIBLING of the page, not a wrapper. React
|
|
422
|
+
// renders siblings concurrently — the page starts immediately while
|
|
423
|
+
// MetadataHead awaits metadata promises. Float hoists the head
|
|
424
|
+
// elements to <head> regardless of tree position. Both resolve before
|
|
425
|
+
// onShellReady (outside Suspense), but they overlap.
|
|
426
|
+
// Placed at the page level so metadata is always re-rendered on SPA
|
|
427
|
+
// navigation (never skipped by segment tree diffing).
|
|
428
|
+
// See TIM-1363, TIM-1367, design/16-metadata.md.
|
|
429
|
+
element = h(
|
|
430
|
+
Fragment,
|
|
431
|
+
null,
|
|
432
|
+
h(MetadataHead, {
|
|
433
|
+
segments,
|
|
434
|
+
requestUrl: req.url,
|
|
435
|
+
metadataRouteHashes,
|
|
436
|
+
}),
|
|
437
|
+
element
|
|
438
|
+
);
|
|
461
439
|
|
|
462
440
|
// Build a lookup of layout components by segment for O(1) access.
|
|
463
441
|
const layoutBySegment = new Map(
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { serializeSitemap, serializeSitemapIndex } from './pipeline-metadata.js';
|
|
18
18
|
import type { SegmentNode } from '../routing/types.js';
|
|
19
|
+
import type { CoercedParams } from '../shared/param-value.js';
|
|
19
20
|
|
|
20
21
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
21
22
|
|
|
@@ -229,26 +230,23 @@ export async function resolveSitemapUrls(
|
|
|
229
230
|
*
|
|
230
231
|
* @returns The resolved path, or null if params are missing
|
|
231
232
|
*/
|
|
232
|
-
export function resolvePathWithParams(
|
|
233
|
-
urlPath: string,
|
|
234
|
-
params: Record<string, string | string[]>
|
|
235
|
-
): string | null {
|
|
233
|
+
export function resolvePathWithParams(urlPath: string, params: CoercedParams): string | null {
|
|
236
234
|
let resolved = urlPath;
|
|
237
235
|
|
|
238
236
|
// Optional catch-all: [[...param]]
|
|
239
237
|
resolved = resolved.replace(/\[\[\.\.\.([^\]]+)\]\]/g, (_, name) => {
|
|
240
238
|
const value = params[name];
|
|
241
239
|
if (value === undefined || value === '') return '';
|
|
242
|
-
if (Array.isArray(value)) return value.join('/');
|
|
243
|
-
return value;
|
|
240
|
+
if (Array.isArray(value)) return value.map(String).join('/');
|
|
241
|
+
return String(value);
|
|
244
242
|
});
|
|
245
243
|
|
|
246
244
|
// Catch-all: [...param]
|
|
247
245
|
resolved = resolved.replace(/\[\.\.\.([^\]]+)\]/g, (_, name) => {
|
|
248
246
|
const value = params[name];
|
|
249
247
|
if (value === undefined) return '';
|
|
250
|
-
if (Array.isArray(value)) return value.join('/');
|
|
251
|
-
return value;
|
|
248
|
+
if (Array.isArray(value)) return value.map(String).join('/');
|
|
249
|
+
return String(value);
|
|
252
250
|
});
|
|
253
251
|
|
|
254
252
|
// Single dynamic: [param] (possibly with affixes like img-[id].png)
|
|
@@ -260,7 +258,7 @@ export function resolvePathWithParams(
|
|
|
260
258
|
missingRequired = true;
|
|
261
259
|
return match; // keep original to avoid malformed URL
|
|
262
260
|
}
|
|
263
|
-
const v = Array.isArray(value) ? (value[0] ?? '') : value;
|
|
261
|
+
const v = Array.isArray(value) ? String(value[0] ?? '') : String(value);
|
|
264
262
|
return prefix + v + suffix;
|
|
265
263
|
});
|
|
266
264
|
|
|
@@ -28,6 +28,7 @@ import { logRenderError } from './logger.js';
|
|
|
28
28
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
29
29
|
import { setSlotParams, runWithSlotSegmentParams } from './request-context.js';
|
|
30
30
|
import { coerceSlotParams } from './param-coercion.js';
|
|
31
|
+
import type { CoercedParams } from '../shared/param-value.js';
|
|
31
32
|
import { matchUrlParts } from './tree-match.js';
|
|
32
33
|
import { extractUrlParts, consumedPartsThroughSegment } from './chain-url-parts.js';
|
|
33
34
|
import { findInterceptingMatch } from './slot-interception.js';
|
|
@@ -391,7 +392,7 @@ export interface SlotMatchResult {
|
|
|
391
392
|
page: NonNullable<ManifestSegmentNode['page']>;
|
|
392
393
|
/** The full chain of slot nodes traversed (slot root → … → leaf with page). */
|
|
393
394
|
chain: ManifestSegmentNode[];
|
|
394
|
-
/** Params collected from the slot's segment chain (may differ from main route params). */
|
|
395
|
+
/** Params collected from the slot's segment chain (pre-coercion, may differ from main route params). */
|
|
395
396
|
slotParams: Record<string, string | string[]>;
|
|
396
397
|
}
|
|
397
398
|
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
* and `CLAUDE.md` §"RSC and SSR Are Separate Vite Environments".
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
import type { CoercedParams } from '../shared/param-value.js';
|
|
33
|
+
import type { SlotParamsRecord } from '../shared/slot-params.js';
|
|
34
|
+
|
|
32
35
|
/**
|
|
33
36
|
* Navigation context passed from the RSC environment to SSR.
|
|
34
37
|
*
|
|
@@ -38,8 +41,8 @@
|
|
|
38
41
|
export interface NavContext {
|
|
39
42
|
/** The requested pathname */
|
|
40
43
|
pathname: string;
|
|
41
|
-
/** Extracted route segment params
|
|
42
|
-
segmentParams:
|
|
44
|
+
/** Extracted route segment params. Post-coercion, values may be non-string (TIM-1347). */
|
|
45
|
+
segmentParams: CoercedParams;
|
|
43
46
|
/** Search params from the URL */
|
|
44
47
|
searchParams: Record<string, string>;
|
|
45
48
|
/**
|
|
@@ -47,7 +50,7 @@ export interface NavContext {
|
|
|
47
50
|
* rendered no slot with params of its own. Feeds `useSegmentParams(path)`
|
|
48
51
|
* during SSR and, via `__timber_slot_params`, after hydration (TIM-1285).
|
|
49
52
|
*/
|
|
50
|
-
slotParams?:
|
|
53
|
+
slotParams?: SlotParamsRecord;
|
|
51
54
|
/** The committed HTTP status code */
|
|
52
55
|
statusCode: number;
|
|
53
56
|
/** Response headers from middleware/proxy */
|
package/src/server/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Server-side type definitions
|
|
2
2
|
|
|
3
|
+
import type { CoercedParams } from '../shared/param-value.js';
|
|
3
4
|
import type { EarlyHint } from './early-hints.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -22,7 +23,7 @@ export interface MiddlewareContext {
|
|
|
22
23
|
req: Request;
|
|
23
24
|
requestHeaders: Headers;
|
|
24
25
|
headers: Headers;
|
|
25
|
-
segmentParams:
|
|
26
|
+
segmentParams: CoercedParams;
|
|
26
27
|
/** Declare early hints for critical resources. Appends Link headers. */
|
|
27
28
|
earlyHints: (hints: EarlyHint[]) => void;
|
|
28
29
|
}
|
package/src/shared/als-slots.ts
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
* design/41-global-params.md.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
import type { CoercedParams } from './param-value.js';
|
|
35
|
+
|
|
34
36
|
interface AlsSlot<T> {
|
|
35
37
|
/** Register the server-side getter. Called once at module-init time. */
|
|
36
38
|
set(fn: () => T): void;
|
|
@@ -56,7 +58,7 @@ const searchParamsSlot = createAlsSlot<URLSearchParams>(
|
|
|
56
58
|
'Use searchParams.useQueryStates() on the client.'
|
|
57
59
|
);
|
|
58
60
|
|
|
59
|
-
const segmentParamsSlot = createAlsSlot<
|
|
61
|
+
const segmentParamsSlot = createAlsSlot<CoercedParams>(
|
|
60
62
|
'[timber] segmentParams.get() is only available on the server. ' +
|
|
61
63
|
'Use segmentParams.useSegmentParams() on the client.'
|
|
62
64
|
);
|
|
@@ -62,6 +62,43 @@
|
|
|
62
62
|
* See design/41-global-params.md §"Transport".
|
|
63
63
|
*/
|
|
64
64
|
|
|
65
|
+
// ─── Post-coercion param types ──────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A single segment param value after codec coercion and normalization.
|
|
69
|
+
*
|
|
70
|
+
* Before coercion, every param is `string | string[]` (the raw URL part).
|
|
71
|
+
* After `app/schema.ts` codecs run, a param can be any value in the
|
|
72
|
+
* `normalizeParamValue` domain below. This type replaces the previous
|
|
73
|
+
* `string | string[]` declaration, which was a lie whenever a codec
|
|
74
|
+
* produced a non-string value (TIM-1347).
|
|
75
|
+
*
|
|
76
|
+
* The typed accessor `getSegmentParams(SEGMENT_PATH)` resolves through
|
|
77
|
+
* the schema and returns the exact type the codec declares. This type
|
|
78
|
+
* covers the untyped/no-argument form and internal plumbing.
|
|
79
|
+
*/
|
|
80
|
+
export type CoercedParamValue =
|
|
81
|
+
| string
|
|
82
|
+
| number
|
|
83
|
+
| boolean
|
|
84
|
+
| bigint
|
|
85
|
+
| null
|
|
86
|
+
| undefined
|
|
87
|
+
| Date
|
|
88
|
+
| Map<CoercedParamValue, CoercedParamValue>
|
|
89
|
+
| Set<CoercedParamValue>
|
|
90
|
+
| CoercedParamValue[]
|
|
91
|
+
| { [key: string]: CoercedParamValue };
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A record of coerced segment params, keyed by param name.
|
|
95
|
+
*
|
|
96
|
+
* This is the post-coercion type for `segmentParams` throughout the
|
|
97
|
+
* framework. Before coercion (route matcher output, `rawSegmentParams`),
|
|
98
|
+
* the type is `Record<string, string | string[]>`.
|
|
99
|
+
*/
|
|
100
|
+
export type CoercedParams = Record<string, CoercedParamValue>;
|
|
101
|
+
|
|
65
102
|
/** Types the canonical form rebuilds rather than rejects. */
|
|
66
103
|
const REBUILT = ['Date', 'Map', 'Set'] as const;
|
|
67
104
|
|
|
@@ -88,16 +125,16 @@ function describe(value: object): string {
|
|
|
88
125
|
* is rejected. That is one rule for two things — a shared reference and a cycle
|
|
89
126
|
* are both "this object again" — and it is what keeps the result a tree.
|
|
90
127
|
*/
|
|
91
|
-
export function normalizeParamValue(value: unknown, path = ''):
|
|
128
|
+
export function normalizeParamValue(value: unknown, path = ''): CoercedParamValue {
|
|
92
129
|
return normalize(value, path, new Set());
|
|
93
130
|
}
|
|
94
131
|
|
|
95
|
-
function normalize(value: unknown, path: string, seen: Set<object>):
|
|
132
|
+
function normalize(value: unknown, path: string, seen: Set<object>): CoercedParamValue {
|
|
96
133
|
if (value === null) return null;
|
|
97
134
|
|
|
98
135
|
const kind = typeof value;
|
|
99
136
|
if (kind === 'string' || kind === 'number' || kind === 'boolean' || kind === 'bigint') {
|
|
100
|
-
return value;
|
|
137
|
+
return value as string | number | boolean | bigint;
|
|
101
138
|
}
|
|
102
139
|
if (kind === 'undefined') return undefined;
|
|
103
140
|
if (kind === 'function') {
|
|
@@ -124,7 +161,7 @@ function normalize(value: unknown, path: string, seen: Set<object>): unknown {
|
|
|
124
161
|
// Read by index, so holes become `undefined` — on both sides, which is
|
|
125
162
|
// the point. A subclass and any own properties are left behind for the
|
|
126
163
|
// same reason: the clone is a plain, dense array either way.
|
|
127
|
-
const out:
|
|
164
|
+
const out: CoercedParamValue[] = [];
|
|
128
165
|
for (let index = 0; index < object.length; index++) {
|
|
129
166
|
out.push(normalize((object as unknown[])[index], `${path}[${index}]`, seen));
|
|
130
167
|
}
|
|
@@ -138,7 +175,7 @@ function normalize(value: unknown, path: string, seen: Set<object>): unknown {
|
|
|
138
175
|
}
|
|
139
176
|
|
|
140
177
|
if (object instanceof Map) {
|
|
141
|
-
const out = new Map<
|
|
178
|
+
const out = new Map<CoercedParamValue, CoercedParamValue>();
|
|
142
179
|
for (const [key, entry] of object) {
|
|
143
180
|
out.set(
|
|
144
181
|
normalize(key, `${path}<key>`, seen),
|
|
@@ -149,7 +186,7 @@ function normalize(value: unknown, path: string, seen: Set<object>): unknown {
|
|
|
149
186
|
}
|
|
150
187
|
|
|
151
188
|
if (object instanceof Set) {
|
|
152
|
-
const out = new Set<
|
|
189
|
+
const out = new Set<CoercedParamValue>();
|
|
153
190
|
let index = 0;
|
|
154
191
|
for (const entry of object) out.add(normalize(entry, `${path}[${index++}]`, seen));
|
|
155
192
|
return out;
|
|
@@ -171,7 +208,7 @@ function normalize(value: unknown, path: string, seen: Set<object>): unknown {
|
|
|
171
208
|
// so dropping it here keeps the two sides identical. `__proto__` is skipped
|
|
172
209
|
// rather than copied: it has a language-level setter that would change the
|
|
173
210
|
// prototype chain of the copy (TIM-655, TIM-855, TIM-873).
|
|
174
|
-
const out: Record<string,
|
|
211
|
+
const out: Record<string, CoercedParamValue> = Object.create(null);
|
|
175
212
|
for (const key of Object.keys(object)) {
|
|
176
213
|
if (key === '__proto__') continue;
|
|
177
214
|
out[key] = normalize(
|
|
@@ -32,12 +32,13 @@
|
|
|
32
32
|
* helpers here — producer and consumer do not each describe the payload.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
import type { CoercedParams } from './param-value.js';
|
|
35
36
|
import type { SlotParamsRecord } from './slot-params.js';
|
|
36
37
|
|
|
37
38
|
/** The params published beside a tree. */
|
|
38
39
|
export interface PublishedParams {
|
|
39
40
|
/** The main route's coerced params. */
|
|
40
|
-
params:
|
|
41
|
+
params: CoercedParams;
|
|
41
42
|
/** Per-slot params, or null when the route rendered no slots with params. */
|
|
42
43
|
slotParams: SlotParamsRecord | null;
|
|
43
44
|
}
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* See design/41-global-params.md §"Params in a parallel slot".
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import type { CoercedParams } from './param-value.js';
|
|
22
|
+
|
|
21
23
|
/** Slot tree path → that slot's own coerced params. */
|
|
22
|
-
export type SlotParamsRecord = Record<string,
|
|
24
|
+
export type SlotParamsRecord = Record<string, CoercedParams>;
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* The params a slot sees: the main route's as a base, the slot's own on top.
|
|
@@ -37,9 +39,9 @@ export type SlotParamsRecord = Record<string, Record<string, string | string[]>>
|
|
|
37
39
|
* by merging. See design/13-security.md #36c.
|
|
38
40
|
*/
|
|
39
41
|
export function mergeSlotParams(
|
|
40
|
-
mainParams:
|
|
41
|
-
slotParams:
|
|
42
|
-
):
|
|
42
|
+
mainParams: CoercedParams,
|
|
43
|
+
slotParams: CoercedParams
|
|
44
|
+
): CoercedParams {
|
|
43
45
|
return Object.assign(Object.create(null), mainParams, slotParams);
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -56,10 +58,10 @@ export function mergeSlotParams(
|
|
|
56
58
|
* resolve to an inherited member of `Object.prototype`.
|
|
57
59
|
*/
|
|
58
60
|
export function resolveSegmentParams(
|
|
59
|
-
mainParams:
|
|
61
|
+
mainParams: CoercedParams,
|
|
60
62
|
slotParams: SlotParamsRecord | null | undefined,
|
|
61
63
|
segmentPath: string | undefined
|
|
62
|
-
):
|
|
64
|
+
): CoercedParams {
|
|
63
65
|
if (!segmentPath || !slotParams || !Object.hasOwn(slotParams, segmentPath)) return mainParams;
|
|
64
66
|
return cachedMerge(mainParams, slotParams, segmentPath);
|
|
65
67
|
}
|
|
@@ -83,16 +85,13 @@ export function resolveSegmentParams(
|
|
|
83
85
|
* A `WeakMap` chain rather than a `Map`, so a superseded navigation's records
|
|
84
86
|
* and their merges become collectable as soon as nothing else holds them.
|
|
85
87
|
*/
|
|
86
|
-
const mergeCache = new WeakMap<
|
|
87
|
-
object,
|
|
88
|
-
WeakMap<object, Map<string, Record<string, string | string[]>>>
|
|
89
|
-
>();
|
|
88
|
+
const mergeCache = new WeakMap<object, WeakMap<object, Map<string, CoercedParams>>>();
|
|
90
89
|
|
|
91
90
|
function cachedMerge(
|
|
92
|
-
mainParams:
|
|
91
|
+
mainParams: CoercedParams,
|
|
93
92
|
slotParams: SlotParamsRecord,
|
|
94
93
|
segmentPath: string
|
|
95
|
-
):
|
|
94
|
+
): CoercedParams {
|
|
96
95
|
let bySlotMap = mergeCache.get(mainParams);
|
|
97
96
|
if (!bySlotMap) {
|
|
98
97
|
bySlotMap = new WeakMap();
|