@timber-js/app 0.2.0-alpha.165 → 0.2.0-alpha.167
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-CSDD6x7U.js → actions-TSxpXLHJ.js} +3 -3
- package/dist/_chunks/{actions-CSDD6x7U.js.map → actions-TSxpXLHJ.js.map} +1 -1
- package/dist/_chunks/{cache-api-eb1gydM7.js → cache-api-DzpQQOEx.js} +53 -49
- package/dist/_chunks/{cache-api-eb1gydM7.js.map → cache-api-DzpQQOEx.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-mGfRbjh2.js → cli-schema-sync-wX-i90Og.js} +6 -4
- package/dist/_chunks/cli-schema-sync-wX-i90Og.js.map +1 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
- package/dist/_chunks/{define-CFmvb4Bt.js → define-COtkxMRT.js} +16 -28
- package/dist/_chunks/define-COtkxMRT.js.map +1 -0
- package/dist/_chunks/{define-Bssfp6ot.js → define-c4au4I9R.js} +2 -2
- package/dist/_chunks/{define-Bssfp6ot.js.map → define-c4au4I9R.js.map} +1 -1
- package/dist/_chunks/{logger-B_O6-mdJ.js → logger-t3uxAmbX.js} +3 -3
- package/dist/_chunks/logger-t3uxAmbX.js.map +1 -0
- package/dist/_chunks/{plugin-context-BnaiU_cF.js → plugin-context---kTF5v8.js} +2 -2
- package/dist/_chunks/{plugin-context-BnaiU_cF.js.map → plugin-context---kTF5v8.js.map} +1 -1
- package/dist/_chunks/{resolve-schema-3iUvBV5T.js → resolve-schema-Dz3fcFUo.js} +2 -2
- package/dist/_chunks/{resolve-schema-3iUvBV5T.js.map → resolve-schema-Dz3fcFUo.js.map} +1 -1
- package/dist/_chunks/{schema-bridge-BY3QLBL7.js → schema-bridge-DT_Tn0Xf.js} +2 -2
- package/dist/_chunks/{schema-bridge-BY3QLBL7.js.map → schema-bridge-DT_Tn0Xf.js.map} +1 -1
- package/dist/_chunks/{use-query-states-CbeQmext.js → use-query-states-DFvWd-EA.js} +65 -7
- package/dist/_chunks/use-query-states-DFvWd-EA.js.map +1 -0
- package/dist/_chunks/{walkers-BL3MCMgO.js → walkers-Cfwvl-UC.js} +3 -3
- package/dist/_chunks/{walkers-BL3MCMgO.js.map → walkers-Cfwvl-UC.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.d.ts +12 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -461
- package/dist/adapters/nitro.js +1 -1
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/cache-api.d.ts +33 -11
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -0
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/internal.js +139 -115
- package/dist/client/internal.js.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/logs.d.ts +15 -1
- package/dist/dev-tools/logs.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +138 -45
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +8 -8
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +35 -4
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +3 -7
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +2 -2
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +3 -1
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +12 -7
- package/dist/server/internal.js.map +1 -1
- package/dist/server/route-element-builder.d.ts +9 -0
- package/dist/server/route-element-builder.d.ts.map +1 -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 +8 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/docs/api/32-api-cache.mdx +4 -4
- package/docs/api/33-api-search-params.mdx +13 -0
- package/docs/learn/00-introduction.mdx +1 -2
- package/docs/learn/09-caching.mdx +5 -5
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/50-ai-agent-instructions.mdx +5 -5
- package/package.json +10 -8
- package/src/adapters/cloudflare.ts +63 -25
- package/src/adapters/types.ts +2 -0
- package/src/cache/cache-api.ts +84 -84
- package/src/cli.ts +0 -0
- package/src/client/browser-entry/post-hydration.ts +16 -9
- package/src/client/history.ts +11 -0
- package/src/client/router.ts +212 -206
- package/src/client/use-query-states.ts +99 -7
- package/src/dev-tools/logs.ts +119 -34
- package/src/index.ts +14 -1
- package/src/plugins/adapter-build.ts +1 -0
- package/src/plugins/cache.ts +45 -30
- package/src/routing/scanner.ts +7 -0
- package/src/schema-bridge.ts +4 -3
- package/src/search-params/define.ts +57 -51
- package/src/search-params/wrappers.ts +17 -26
- package/src/server/als-registry.ts +7 -0
- package/src/server/deny-boundary.ts +6 -3
- package/src/server/route-element-builder.ts +49 -8
- package/src/server/rsc-entry/render-route.ts +3 -1
- package/src/server/rsc-entry/rsc-payload.ts +21 -4
- package/src/server/rsc-entry/rsc-stream.ts +8 -0
- package/src/server/stream-utils.ts +8 -6
- package/LICENSE +0 -8
- package/dist/_chunks/cli-schema-sync-mGfRbjh2.js.map +0 -1
- package/dist/_chunks/define-CFmvb4Bt.js.map +0 -1
- package/dist/_chunks/logger-B_O6-mdJ.js.map +0 -1
- package/dist/_chunks/use-query-states-CbeQmext.js.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { useQueryStates as clientUseQueryStates } from '../client/use-query-states.js';
|
|
14
|
-
import { fromSchema, isStandardSchema, isCodec
|
|
14
|
+
import { fromSchema, isStandardSchema, isCodec } from '../schema-bridge.js';
|
|
15
15
|
import type { StandardSchemaV1 } from '../schema-bridge.js';
|
|
16
16
|
import type { Codec } from '../codec.js';
|
|
17
17
|
|
|
@@ -165,9 +165,43 @@ export type { StandardSchemaV1 } from '../schema-bridge.js';
|
|
|
165
165
|
// Type-level helpers
|
|
166
166
|
// ---------------------------------------------------------------------------
|
|
167
167
|
|
|
168
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Extract a Standard Schema's declared *input* type from its optional
|
|
170
|
+
* `~standard.types` property (part of the Standard Schema spec; Zod, Valibot,
|
|
171
|
+
* and ArkType all declare it at the type level). Falls back to `never` when
|
|
172
|
+
* the schema doesn't declare types (e.g. hand-written schemas): with no
|
|
173
|
+
* metadata we can't know whether the schema handles `undefined`, so InferField
|
|
174
|
+
* widens conservatively rather than risk a type lie.
|
|
175
|
+
*/
|
|
176
|
+
type InferSchemaInput<V> = V extends { '~standard': { types?: infer TS } }
|
|
177
|
+
? [NonNullable<TS>] extends [{ input: infer I }]
|
|
178
|
+
? I
|
|
179
|
+
: never
|
|
180
|
+
: never;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Infer the output type from either a SearchParamCodec or a StandardSchemaV1.
|
|
184
|
+
*
|
|
185
|
+
* Schemas whose input type rejects `undefined` (e.g. bare `z.string()`, whose
|
|
186
|
+
* input is `string`) are implicitly optional: the URL might not contain the
|
|
187
|
+
* param, and fromSchema returns `undefined` when the schema rejects absent
|
|
188
|
+
* input and has no default. The output type widens to `T | undefined` so the
|
|
189
|
+
* type doesn't lie. Schemas that accept `undefined` input (`.optional()`,
|
|
190
|
+
* `.default()`) keep their declared output type.
|
|
191
|
+
*
|
|
192
|
+
* Limitation: `z.coerce.*` schemas declare input `unknown`, which accepts
|
|
193
|
+
* `undefined` at the type level — so a coerce schema without `.default()`
|
|
194
|
+
* keeps its narrow output type even though absent input yields `undefined`
|
|
195
|
+
* at runtime. Add `.default()` to coerce schemas for accurate types.
|
|
196
|
+
*/
|
|
169
197
|
export type InferField<V> =
|
|
170
|
-
V extends SearchParamCodec<infer T>
|
|
198
|
+
V extends SearchParamCodec<infer T>
|
|
199
|
+
? T
|
|
200
|
+
: V extends StandardSchemaV1<infer T>
|
|
201
|
+
? undefined extends InferSchemaInput<V>
|
|
202
|
+
? T
|
|
203
|
+
: T | undefined
|
|
204
|
+
: never;
|
|
171
205
|
|
|
172
206
|
/** Acceptable field value for defineSearchParams: a codec or a Standard Schema. */
|
|
173
207
|
export type SearchParamField<T = unknown> = SearchParamCodec<T> | StandardSchemaV1<T>;
|
|
@@ -198,9 +232,17 @@ function normalizeRaw(
|
|
|
198
232
|
* Compute the serialized default value for a codec. Used for
|
|
199
233
|
* default-omission: when serialize(value) === serialize(parse(undefined)),
|
|
200
234
|
* the field is omitted from the URL.
|
|
235
|
+
*
|
|
236
|
+
* Codecs are documented to return a default rather than throw, but a
|
|
237
|
+
* hand-written codec that throws on absent input must not turn definition
|
|
238
|
+
* into a crash — treat its default as null (nothing to omit).
|
|
201
239
|
*/
|
|
202
240
|
function getDefaultSerialized<T>(codec: SearchParamCodec<T>): string | null {
|
|
203
|
-
|
|
241
|
+
try {
|
|
242
|
+
return codec.serialize(codec.parse(undefined));
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
204
246
|
}
|
|
205
247
|
|
|
206
248
|
// isStandardSchema and isCodec are imported from schema-bridge.ts.
|
|
@@ -218,22 +260,11 @@ function resolveField(
|
|
|
218
260
|
return { codec: value, urlKey: value.urlKey };
|
|
219
261
|
}
|
|
220
262
|
|
|
221
|
-
// Auto-detect Standard Schema
|
|
263
|
+
// Auto-detect Standard Schema. Schemas that reject undefined input and
|
|
264
|
+
// have no default are implicitly optional: fromSchema returns undefined
|
|
265
|
+
// for absent params, and InferField widens the output type to
|
|
266
|
+
// T | undefined. design/23-search-params.md §"Implicit Optionality"
|
|
222
267
|
if (isStandardSchema(value)) {
|
|
223
|
-
// Validate that the schema handles absent params (undefined input).
|
|
224
|
-
// Search params are optional — the URL might not contain the param.
|
|
225
|
-
// If the schema rejects undefined and has no default, the codec would
|
|
226
|
-
// return `undefined as T`, making the type lie. Catch this at definition
|
|
227
|
-
// time instead. design/23-search-params.md §"Default Validation"
|
|
228
|
-
const absentCheck = validateSync(value, undefined);
|
|
229
|
-
if (absentCheck.issues) {
|
|
230
|
-
throw new Error(
|
|
231
|
-
`[timber] defineSearchParams: field '${fieldName}' has no default value and rejects undefined input.\n` +
|
|
232
|
-
` Search params are optional — the URL might not contain ?${fieldName}=anything.\n` +
|
|
233
|
-
` Add .default() to your schema: z.coerce.number().int().min(1).default(1)\n` +
|
|
234
|
-
` Or use .optional() if the field is truly optional.`
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
268
|
return { codec: fromSchema(value) };
|
|
238
269
|
}
|
|
239
270
|
|
|
@@ -244,25 +275,6 @@ function resolveField(
|
|
|
244
275
|
);
|
|
245
276
|
}
|
|
246
277
|
|
|
247
|
-
/**
|
|
248
|
-
* Validate that all codecs handle absent params (parse(undefined) doesn't throw).
|
|
249
|
-
* Catches schemas that throw on missing input. `undefined` and `null` are both
|
|
250
|
-
* valid defaults — `undefined` is correct for optional fields (e.g., `z.string().optional()`).
|
|
251
|
-
*/
|
|
252
|
-
function validateDefaults(codecMap: Record<string, SearchParamCodec<unknown>>): void {
|
|
253
|
-
for (const [key, codec] of Object.entries(codecMap)) {
|
|
254
|
-
try {
|
|
255
|
-
codec.parse(undefined);
|
|
256
|
-
} catch {
|
|
257
|
-
throw new Error(
|
|
258
|
-
`[timber] defineSearchParams: field '${key}' throws when the param is absent.\n` +
|
|
259
|
-
` Search params are optional — the URL might not contain ?${key}=anything.\n` +
|
|
260
|
-
` Add .default() or .optional() to your schema, or wrap with withDefault().`
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
278
|
// ---------------------------------------------------------------------------
|
|
267
279
|
// Factory
|
|
268
280
|
// ---------------------------------------------------------------------------
|
|
@@ -297,9 +309,13 @@ function validateDefaults(codecMap: Record<string, SearchParamCodec<unknown>>):
|
|
|
297
309
|
* )
|
|
298
310
|
* ```
|
|
299
311
|
*/
|
|
300
|
-
export function defineSearchParams<
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
export function defineSearchParams<
|
|
313
|
+
S extends StandardSchemaV1<Record<string, unknown>> & {
|
|
314
|
+
shape: Record<string, StandardSchemaV1<unknown>>;
|
|
315
|
+
},
|
|
316
|
+
>(
|
|
317
|
+
schema: S
|
|
318
|
+
): SearchParamsDefinition<{ [K in keyof S['shape'] & string]: InferField<S['shape'][K]> }>;
|
|
303
319
|
|
|
304
320
|
/**
|
|
305
321
|
* Overload: accept a map of codecs and/or Standard Schema objects.
|
|
@@ -357,9 +373,6 @@ function defineSearchParamsFromMap(
|
|
|
357
373
|
}
|
|
358
374
|
}
|
|
359
375
|
|
|
360
|
-
// Validate that all codecs handle absent params
|
|
361
|
-
validateDefaults(resolvedCodecs);
|
|
362
|
-
|
|
363
376
|
return buildDefinition(resolvedCodecs as unknown as CodecMap<Record<string, unknown>>, urlKeys);
|
|
364
377
|
}
|
|
365
378
|
|
|
@@ -484,11 +497,6 @@ function buildDefinition<T extends Record<string, unknown>>(
|
|
|
484
497
|
// Merge URL keys: base keys + new codec urlKeys from withUrlKey
|
|
485
498
|
const combinedUrlKeys: Record<string, string> = { ...urlKeys, ...newUrlKeys };
|
|
486
499
|
|
|
487
|
-
// Same definition-time guard as defineSearchParams — a throw-on-absent
|
|
488
|
-
// codec added via extend() gets the friendly guidance error, not a
|
|
489
|
-
// crash on first parse (TIM-1066).
|
|
490
|
-
validateDefaults(combinedCodecs as Record<string, SearchParamCodec<unknown>>);
|
|
491
|
-
|
|
492
500
|
return buildDefinition<Combined>(combinedCodecs, combinedUrlKeys);
|
|
493
501
|
}
|
|
494
502
|
|
|
@@ -515,8 +523,6 @@ function buildDefinition<T extends Record<string, unknown>>(
|
|
|
515
523
|
}
|
|
516
524
|
}
|
|
517
525
|
|
|
518
|
-
validateDefaults(pickedCodecs);
|
|
519
|
-
|
|
520
526
|
return buildDefinition<Pick<T, K>>(
|
|
521
527
|
pickedCodecs as unknown as CodecMap<Pick<T, K>>,
|
|
522
528
|
pickedUrlKeys
|
|
@@ -8,24 +8,13 @@
|
|
|
8
8
|
* Design doc: design/23-search-params.md
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
): SearchParamCodec<T> {
|
|
19
|
-
const absentCheck = validateSync(schema, undefined);
|
|
20
|
-
if (absentCheck.issues) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
`[timber] withUrlKey: schema for '${fieldHint}' has no default value and rejects undefined input.\n` +
|
|
23
|
-
` Search params are optional — the URL might not contain ?${fieldHint}=anything.\n` +
|
|
24
|
-
` Add .default() to your schema or use .optional().`
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
return fromSchema(schema);
|
|
28
|
-
}
|
|
11
|
+
import type {
|
|
12
|
+
InferField,
|
|
13
|
+
SearchParamCodec,
|
|
14
|
+
SearchParamCodecWithUrlKey,
|
|
15
|
+
SearchParamField,
|
|
16
|
+
} from './define.js';
|
|
17
|
+
import { isCodec, isStandardSchema, fromSchema } from '../schema-bridge.js';
|
|
29
18
|
|
|
30
19
|
// ---------------------------------------------------------------------------
|
|
31
20
|
// withDefault
|
|
@@ -90,17 +79,19 @@ export function withDefault<T>(
|
|
|
90
79
|
* withUrlKey(withDefault(parseAsInteger, 1), 'p')
|
|
91
80
|
* ```
|
|
92
81
|
*/
|
|
93
|
-
export function withUrlKey<
|
|
94
|
-
codecOrSchema:
|
|
82
|
+
export function withUrlKey<F extends SearchParamField>(
|
|
83
|
+
codecOrSchema: F,
|
|
95
84
|
urlKey: string
|
|
96
|
-
): SearchParamCodecWithUrlKey<
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
//
|
|
85
|
+
): SearchParamCodecWithUrlKey<InferField<F>> {
|
|
86
|
+
type T = InferField<F>;
|
|
87
|
+
// Auto-detect Standard Schema (Zod, Valibot, ArkType) and wrap. Schemas
|
|
88
|
+
// that reject undefined input are implicitly optional — fromSchema returns
|
|
89
|
+
// undefined for absent params, and InferField widens the type to include
|
|
90
|
+
// undefined. design/23-search-params.md §"Implicit Optionality"
|
|
100
91
|
const codec: SearchParamCodec<T> = isCodec(codecOrSchema)
|
|
101
|
-
? codecOrSchema
|
|
92
|
+
? (codecOrSchema as SearchParamCodec<T>)
|
|
102
93
|
: isStandardSchema(codecOrSchema)
|
|
103
|
-
?
|
|
94
|
+
? (fromSchema(codecOrSchema) as SearchParamCodec<T>)
|
|
104
95
|
: (codecOrSchema as SearchParamCodec<T>);
|
|
105
96
|
return {
|
|
106
97
|
parse: codec.parse.bind(codec),
|
|
@@ -127,6 +127,13 @@ export interface RequestContextStore {
|
|
|
127
127
|
* See TIM-666.
|
|
128
128
|
*/
|
|
129
129
|
denyStatus?: number;
|
|
130
|
+
/**
|
|
131
|
+
* Callback fired when setDenyStatus() is called. Used by shellSettled
|
|
132
|
+
* to short-circuit when any component catches a DenySignal in-tree,
|
|
133
|
+
* regardless of which component caught it (AccessGate, TracedLayout,
|
|
134
|
+
* or PageDenyBoundary). See TIM-1208.
|
|
135
|
+
*/
|
|
136
|
+
onDenyStatus?: () => void;
|
|
130
137
|
/**
|
|
131
138
|
* Dev-only: getter for the current request's RSC debug components.
|
|
132
139
|
* Set by renderRoute() so onPipelineError can include component tree
|
|
@@ -262,6 +262,7 @@ export async function PageDenyBoundary({
|
|
|
262
262
|
Page,
|
|
263
263
|
route,
|
|
264
264
|
denyPages,
|
|
265
|
+
onSettled,
|
|
265
266
|
}: {
|
|
266
267
|
/** The page server component function. */
|
|
267
268
|
Page: (...args: unknown[]) => unknown;
|
|
@@ -269,10 +270,10 @@ export async function PageDenyBoundary({
|
|
|
269
270
|
route: string;
|
|
270
271
|
/** Deny page fallback chain from the segment chain. */
|
|
271
272
|
denyPages: DenyPageEntry[];
|
|
273
|
+
/** Called when the page component finishes (success or deny). See TIM-1045. */
|
|
274
|
+
onSettled?: () => void;
|
|
272
275
|
}): Promise<React.ReactElement> {
|
|
273
276
|
try {
|
|
274
|
-
// Call the page as an async function — same as React Flight does.
|
|
275
|
-
// Wrap in OTEL span for tracing (replaces the TracedPage wrapper).
|
|
276
277
|
const result = await withSpan('timber.page', { 'timber.route': route }, () => Page({}));
|
|
277
278
|
return result as React.ReactElement;
|
|
278
279
|
} catch (error: unknown) {
|
|
@@ -283,8 +284,9 @@ export async function PageDenyBoundary({
|
|
|
283
284
|
return denyElement;
|
|
284
285
|
}
|
|
285
286
|
}
|
|
286
|
-
// Non-deny errors (RedirectSignal, runtime errors) propagate normally.
|
|
287
287
|
throw error;
|
|
288
|
+
} finally {
|
|
289
|
+
onSettled?.();
|
|
288
290
|
}
|
|
289
291
|
}
|
|
290
292
|
|
|
@@ -299,6 +301,7 @@ export function setDenyStatus(status: number): void {
|
|
|
299
301
|
const store = requestContextAls.getStore();
|
|
300
302
|
if (store) {
|
|
301
303
|
store.denyStatus = status;
|
|
304
|
+
store.onDenyStatus?.();
|
|
302
305
|
}
|
|
303
306
|
}
|
|
304
307
|
|
|
@@ -34,6 +34,7 @@ function getDevNonce(): string {
|
|
|
34
34
|
}
|
|
35
35
|
import { DenySignal, RedirectSignal } from './primitives.js';
|
|
36
36
|
import { AccessGate } from './access-gate.js';
|
|
37
|
+
import { requestContextAls } from './als-registry.js';
|
|
37
38
|
import {
|
|
38
39
|
PageDenyBoundary,
|
|
39
40
|
buildDenyPageChain,
|
|
@@ -177,6 +178,15 @@ export interface RouteElementResult {
|
|
|
177
178
|
* See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
178
179
|
*/
|
|
179
180
|
skippedSegments: string[];
|
|
181
|
+
/**
|
|
182
|
+
* Resolves when every deny-capable shell component (PageDenyBoundary and
|
|
183
|
+
* each TracedLayout with a deny page chain) has finished executing, OR
|
|
184
|
+
* immediately when any of them catches a DenySignal in-tree (the HTTP
|
|
185
|
+
* status is known at that point; inner components may never run).
|
|
186
|
+
* Undefined when the route has no deny-capable server components.
|
|
187
|
+
* See TIM-1045 (pages), TIM-1208 (layouts).
|
|
188
|
+
*/
|
|
189
|
+
shellSettled?: Promise<void>;
|
|
180
190
|
}
|
|
181
191
|
|
|
182
192
|
// ─── Module Processing Helpers ─────────────────────────────────────────────
|
|
@@ -462,12 +472,24 @@ export async function buildRouteElement(
|
|
|
462
472
|
// Build element tree: page wrapped in layouts (innermost to outermost)
|
|
463
473
|
const h = createElement as (...args: unknown[]) => React.ReactElement;
|
|
464
474
|
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
475
|
+
// Shell settlement tracking (TIM-1045 pages, TIM-1208 layouts).
|
|
476
|
+
// ChildSegmentProvider is 'use client', so React Flight eagerly
|
|
477
|
+
// serializes both the layout and child content as independent subtrees —
|
|
478
|
+
// the page can settle before an outer layout. We need all deny-capable
|
|
479
|
+
// components to settle before committing the HTTP status.
|
|
480
|
+
//
|
|
481
|
+
// Each tracked component gets its own deferred. shellSettled resolves
|
|
482
|
+
// when ALL settle (Promise.all) OR when any in-tree deny fires
|
|
483
|
+
// (short-circuit via onDenyStatus — covers AccessGate, TracedLayout,
|
|
484
|
+
// and PageDenyBoundary without threading callbacks into each).
|
|
485
|
+
const shellPromises: Promise<void>[] = [];
|
|
486
|
+
let resolveShortCircuit: (() => void) | undefined;
|
|
487
|
+
const trackShellComponent = (): (() => void) => {
|
|
488
|
+
let resolve!: () => void;
|
|
489
|
+
shellPromises.push(new Promise<void>((r) => (resolve = r)));
|
|
490
|
+
return resolve;
|
|
491
|
+
};
|
|
492
|
+
|
|
471
493
|
// Build the page element.
|
|
472
494
|
// Server component pages are wrapped in PageDenyBoundary which calls
|
|
473
495
|
// them as async functions and catches DenySignal — rendering the deny
|
|
@@ -482,11 +504,12 @@ export async function buildRouteElement(
|
|
|
482
504
|
if (isClientReference(PageComponent)) {
|
|
483
505
|
element = h(PageComponent, {});
|
|
484
506
|
} else if (leafDenyPages && leafDenyPages.length > 0) {
|
|
485
|
-
|
|
507
|
+
const onSettled = trackShellComponent();
|
|
486
508
|
element = h(PageDenyBoundary, {
|
|
487
509
|
Page: PageComponent,
|
|
488
510
|
route: match.segments[leafIndex]?.urlPath ?? '/',
|
|
489
511
|
denyPages: leafDenyPages,
|
|
512
|
+
onSettled,
|
|
490
513
|
});
|
|
491
514
|
} else {
|
|
492
515
|
// Server component page WITHOUT deny page chain — trace only
|
|
@@ -694,6 +717,8 @@ export async function buildRouteElement(
|
|
|
694
717
|
// fallback path. See TIM-668, design/04-authorization.md.
|
|
695
718
|
const layoutComponentRef = layoutComponent;
|
|
696
719
|
const layoutDenyPages = denyPageChains.get(i);
|
|
720
|
+
const hasLayoutDenyChain = !!layoutDenyPages && layoutDenyPages.length > 0;
|
|
721
|
+
const onLayoutSettled = hasLayoutDenyChain ? trackShellComponent() : undefined;
|
|
697
722
|
const TracedLayout = async (props: Record<string, unknown>) => {
|
|
698
723
|
try {
|
|
699
724
|
return await withSpan('timber.layout', { 'timber.segment': segmentId }, () =>
|
|
@@ -707,8 +732,11 @@ export async function buildRouteElement(
|
|
|
707
732
|
return denyElement;
|
|
708
733
|
}
|
|
709
734
|
}
|
|
710
|
-
// Non-deny errors (RedirectSignal, runtime errors) propagate normally
|
|
735
|
+
// Non-deny errors (RedirectSignal, runtime errors) propagate normally
|
|
736
|
+
// to onError — signalDetected / renderError handling covers those.
|
|
711
737
|
throw error;
|
|
738
|
+
} finally {
|
|
739
|
+
onLayoutSettled?.();
|
|
712
740
|
}
|
|
713
741
|
};
|
|
714
742
|
layoutElement = h(TracedLayout, {
|
|
@@ -776,11 +804,24 @@ export async function buildRouteElement(
|
|
|
776
804
|
element = replaceOutermostSegmentOutlet(element, outermostSegmentProvider);
|
|
777
805
|
}
|
|
778
806
|
|
|
807
|
+
// Compose shellSettled: resolves when ALL deny-capable components settle
|
|
808
|
+
// (Promise.all) OR immediately when any component calls setDenyStatus()
|
|
809
|
+
// (short-circuit via onDenyStatus — covers AccessGate, TracedLayout, and
|
|
810
|
+
// PageDenyBoundary). See TIM-1045, TIM-1208.
|
|
811
|
+
let shellSettled: Promise<void> | undefined;
|
|
812
|
+
if (shellPromises.length > 0) {
|
|
813
|
+
const shortCircuit = new Promise<void>((r) => (resolveShortCircuit = r));
|
|
814
|
+
const store = requestContextAls.getStore();
|
|
815
|
+
if (store) store.onDenyStatus = () => resolveShortCircuit?.();
|
|
816
|
+
shellSettled = Promise.race([Promise.all(shellPromises).then(() => {}), shortCircuit]);
|
|
817
|
+
}
|
|
818
|
+
|
|
779
819
|
return {
|
|
780
820
|
element,
|
|
781
821
|
layoutComponents,
|
|
782
822
|
segments,
|
|
783
823
|
deferSuspenseFor,
|
|
784
824
|
skippedSegments,
|
|
825
|
+
shellSettled,
|
|
785
826
|
};
|
|
786
827
|
}
|
|
@@ -137,7 +137,8 @@ export async function renderRoute(
|
|
|
137
137
|
desc: 'build element tree',
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
const { element, layoutComponents, deferSuspenseFor, skippedSegments } =
|
|
140
|
+
const { element, layoutComponents, deferSuspenseFor, skippedSegments, shellSettled } =
|
|
141
|
+
routeResult;
|
|
141
142
|
|
|
142
143
|
// Build head HTML for injection into the SSR output.
|
|
143
144
|
// Collects CSS, fonts, and modulepreload from the build manifest for matched segments.
|
|
@@ -191,6 +192,7 @@ export async function renderRoute(
|
|
|
191
192
|
// Render to RSC Flight stream with signal tracking.
|
|
192
193
|
const _rscStart = performance.now();
|
|
193
194
|
const { rscStream, signals, getDebugComponents } = renderRscStream(element, req);
|
|
195
|
+
if (shellSettled) signals.shellSettled = shellSettled;
|
|
194
196
|
|
|
195
197
|
// Store the debug components getter in ALS so onPipelineError can
|
|
196
198
|
// include component tree context for render-phase errors (dev mode only).
|
|
@@ -88,11 +88,28 @@ export async function buildRscPayloadResponse(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// If data arrived first, still check signals — they may have fired
|
|
91
|
-
// concurrently.
|
|
92
|
-
//
|
|
91
|
+
// concurrently. Wait for all deny-capable shell components (layouts
|
|
92
|
+
// with deny chains + page) to settle so async deny() calls preceded
|
|
93
|
+
// by I/O are captured. See TIM-1045, TIM-1208.
|
|
93
94
|
if (first.type === 'data' && !signals.redirectSignal && !signals.denySignal) {
|
|
94
|
-
//
|
|
95
|
-
|
|
95
|
+
// Sync deny (AccessGate pre-pass replay) sets getDenyStatus() during
|
|
96
|
+
// renderToReadableStream — before the first chunk is read. Skip the
|
|
97
|
+
// wait entirely: components below the denied gate never execute.
|
|
98
|
+
const { getDenyStatus: earlyDenyCheck } = await import('../deny-boundary.js');
|
|
99
|
+
if (signals.shellSettled && !earlyDenyCheck()) {
|
|
100
|
+
// Wait for all deny-capable shell components to finish, or for an
|
|
101
|
+
// in-tree layout deny to short-circuit (setDenyStatus already
|
|
102
|
+
// called), or for a signal to escape to onError. TIM-1045, TIM-1208.
|
|
103
|
+
await Promise.race([signals.shellSettled, signalDetected]);
|
|
104
|
+
// Shell components' finally blocks resolve before React Flight's
|
|
105
|
+
// onError fires. Yield one macrotask so onError can set
|
|
106
|
+
// signals.redirectSignal / signals.renderError before we check.
|
|
107
|
+
await new Promise<void>((r) => setTimeout(r, 0));
|
|
108
|
+
} else if (!earlyDenyCheck()) {
|
|
109
|
+
// No deny-capable shell components (client component page, no deny
|
|
110
|
+
// chain) — brief yield to let in-flight microtask rejections complete.
|
|
111
|
+
await new Promise<void>((r) => setTimeout(r, 0));
|
|
112
|
+
}
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
// Detach the callback — no longer needed after this point.
|
|
@@ -53,6 +53,14 @@ export interface RenderSignals {
|
|
|
53
53
|
lastUnhandledError: unknown | null;
|
|
54
54
|
/** Callback fired when a redirect or deny signal is captured in onError. */
|
|
55
55
|
onSignal?: () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves when every deny-capable shell component (PageDenyBoundary +
|
|
58
|
+
* TracedLayouts with deny chains) has settled, OR immediately when any
|
|
59
|
+
* catches a DenySignal in-tree (status is known). Set by renderRoute
|
|
60
|
+
* after element-tree construction. Awaited by buildRscPayloadResponse
|
|
61
|
+
* before committing HTTP status. See TIM-1045, TIM-1208.
|
|
62
|
+
*/
|
|
63
|
+
shellSettled?: Promise<void>;
|
|
56
64
|
/** Callback fired when an unhandled error (non-signal) is captured. Used
|
|
57
65
|
* to halt Flight data injection into the inline stream. */
|
|
58
66
|
onUnhandledError?: () => void;
|
|
@@ -228,12 +228,14 @@ export function wrapResponseWithStreamTiming(
|
|
|
228
228
|
let cancelled = false;
|
|
229
229
|
|
|
230
230
|
function emitLog(): void {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
`
|
|
235
|
-
|
|
236
|
-
|
|
231
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
232
|
+
const closeMs = Math.round(performance.now() - startTime);
|
|
233
|
+
console.debug(
|
|
234
|
+
`[timber] stream-lifecycle: firstChunk=${firstChunkMs}ms lastChunk=${lastChunkMs}ms ` +
|
|
235
|
+
`close=${closeMs}ms maxGap=${maxGapMs}ms chunks=${chunks} bytes=${bytes}` +
|
|
236
|
+
(cancelled ? ' cancelled=true' : '')
|
|
237
|
+
);
|
|
238
|
+
}
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
const reader = body.getReader();
|
package/LICENSE
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
DONTFUCKINGUSE LICENSE
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Daniel Saewitz
|
|
4
|
-
|
|
5
|
-
This software may not be used, copied, modified, merged, published,
|
|
6
|
-
distributed, sublicensed, or sold by anyone other than the copyright holder.
|
|
7
|
-
|
|
8
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli-schema-sync-mGfRbjh2.js","names":[],"sources":["../../src/routing/scanner.ts","../../src/cli-schema-sync.ts"],"sourcesContent":["/**\n * Route discovery scanner.\n *\n * Pure function: (appDir, config) → RouteTree\n *\n * Scans the app/ directory and builds a segment tree recognizing all\n * timber.js file conventions. Does NOT handle request matching — this\n * is discovery only.\n */\n\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, extname, basename } from 'node:path';\nimport type {\n RouteTree,\n SegmentNode,\n SegmentType,\n RouteFile,\n ScannerConfig,\n InterceptionMarker,\n} from './types.js';\nimport { classifySegment } from './segment-classify.js';\nimport { DEFAULT_PAGE_EXTENSIONS } from './types.js';\nimport { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.js';\nimport { swallow } from '../server/logger.js';\nimport { ENCODED_SEPARATOR_RE, NULL_BYTE_RE } from '../server/canonicalize.js';\n\n/**\n * File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)\n */\nconst PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'error', 'default', 'denied']);\n\n/**\n * Legacy compat status-code files.\n * Maps legacy file name → HTTP status code for the fallback chain.\n * See design/10-error-handling.md §\"Fallback Chain\".\n */\nconst LEGACY_STATUS_FILES: Record<string, number> = {\n 'not-found': 404,\n 'forbidden': 403,\n 'unauthorized': 401,\n};\n\n/**\n * File convention names that are always .ts/.tsx (never .mdx etc.)\n */\nconst FIXED_CONVENTIONS = new Set(['middleware', 'access', 'route', 'params']);\n\n/**\n * Status-code file patterns:\n * - Exact 3-digit codes: 401.tsx, 429.tsx, 503.tsx\n * - Category catch-alls: 4xx.tsx, 5xx.tsx\n */\nconst STATUS_CODE_PATTERN = /^(\\d{3}|[45]xx)$/;\n\n/**\n * Scan the app/ directory and build the route tree.\n *\n * @param appDir - Absolute path to the app/ directory\n * @param config - Scanner configuration\n * @returns The complete route tree\n */\nexport function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTree {\n const pageExtensions = config.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;\n const extSet = new Set(pageExtensions);\n\n const tree: RouteTree = {\n root: createSegmentNode('', 'static', '/'),\n };\n\n // Check for proxy.ts at app root\n const proxyFile = findFixedFile(appDir, 'proxy');\n if (proxyFile) {\n tree.proxy = proxyFile;\n }\n\n // Check for global-error.{tsx,ts,jsx,js} at app root.\n // Tier 2 error page — renders standalone (no layouts) when no segment-level\n // error file is found. See design/10-error-handling.md §\"Tier 2\".\n const globalErrorFile = findPageExtFile(appDir, 'global-error', extSet);\n if (globalErrorFile) {\n tree.globalError = globalErrorFile;\n }\n\n // Scan the root directory's files\n scanSegmentFiles(appDir, tree.root, extSet);\n\n // Scan children recursively\n scanChildren(appDir, tree.root, extSet);\n\n // Validate: detect route group collisions (different groups producing pages at the same URL)\n validateRouteGroupCollisions(tree.root);\n\n // Validate: detect duplicate param names in nested dynamic segments\n // e.g., /[id]/items/[id] — same param name in ancestor and descendant\n validateDuplicateParamNames(tree.root);\n\n // Validate: detect same param name used with different bracket forms\n // e.g., [id] in one route and [...id] in another\n validateUniqueBracketForms(tree.root);\n\n return tree;\n}\n\n/**\n * Create an empty segment node.\n */\nfunction createSegmentNode(\n segmentName: string,\n segmentType: SegmentType,\n urlPath: string,\n paramName?: string,\n interceptionMarker?: InterceptionMarker,\n interceptedSegmentName?: string\n): SegmentNode {\n return {\n segmentName,\n segmentType,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName,\n children: [],\n slots: {},\n };\n}\n\n/**\n * Compute the URL path for a child segment given its parent's URL path.\n * Route groups, slots, and intercepting routes do NOT add URL depth.\n */\nfunction computeUrlPath(parentUrlPath: string, dirName: string, segmentType: SegmentType): string {\n // Groups, slots, and intercepting routes don't add to URL path\n if (segmentType === 'group' || segmentType === 'slot' || segmentType === 'intercepting') {\n return parentUrlPath;\n }\n\n const parentPath = parentUrlPath === '/' ? '' : parentUrlPath;\n return `${parentPath}/${dirName}`;\n}\n\n/**\n * Scan a directory for file conventions and populate the segment node.\n */\nfunction scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanSegmentFiles: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n // Skip directories — handled by scanChildren\n try {\n if (statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const ext = extname(entry).slice(1); // remove leading dot\n const name = basename(entry, `.${ext}`);\n\n // Page-extension conventions (page, layout, error, default, denied)\n if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {\n const key = name as 'page' | 'layout' | 'error' | 'default' | 'denied';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Fixed conventions (middleware, access, route, params) — only\n // .ts/.tsx/.js/.jsx, never pageExtensions like .mdx. The regex is\n // anchored: mts/cts/mjs/cjs (and stray tail matches like `.foots`)\n // are NOT recognized. See design/07-routing.md §\"Page Extensions\".\n if (FIXED_CONVENTIONS.has(name) && /^[jt]sx?$/.test(ext)) {\n const key = name as 'middleware' | 'access' | 'route' | 'params';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // JSON status-code files (401.json, 4xx.json, 503.json, 5xx.json)\n // Recognized regardless of pageExtensions — .json is a data format, not a page extension.\n if (STATUS_CODE_PATTERN.test(name) && ext === 'json') {\n if (!node.jsonStatusFiles) {\n node.jsonStatusFiles = {};\n }\n node.jsonStatusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Status-code files (401.tsx, 4xx.tsx, 503.tsx, 5xx.tsx)\n if (STATUS_CODE_PATTERN.test(name) && extSet.has(ext)) {\n if (!node.statusFiles) {\n node.statusFiles = {};\n }\n const existing = node.statusFiles[name];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node.statusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Legacy compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx)\n if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {\n if (!node.legacyStatusFiles) {\n node.legacyStatusFiles = {};\n }\n const existing = node.legacyStatusFiles[name];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node.legacyStatusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)\n // Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.\n // When both exist for the same base name, dynamic takes precedence.\n // Two files of the SAME kind (static+static or dynamic+dynamic) are a\n // build error — picking one by readdir order is platform-dependent.\n // See design/16-metadata.md §\"Metadata Routes\"\n const metaInfo = classifyMetadataRoute(entry);\n if (metaInfo) {\n if (!node.metadataRoutes) {\n node.metadataRoutes = {};\n }\n const existing = node.metadataRoutes[name];\n if (existing) {\n const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);\n const newIsDynamic = isDynamicMetadataExtension(name, ext);\n if (existingIsDynamic === newIsDynamic) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n // Dynamic > static precedence: overwrite only when the new file is dynamic.\n if (newIsDynamic) {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n } else {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n }\n }\n\n // Validate: route.ts + page.* is a hard build error\n if (node.route && node.page) {\n throw new Error(\n `Build error: route.ts and page.* cannot coexist in the same segment.\\n` +\n ` route.ts: ${node.route.filePath}\\n` +\n ` page: ${node.page.filePath}\\n` +\n `A URL is either an API endpoint or a rendered page, not both.`\n );\n }\n}\n\n/**\n * Throw a build error for two files claiming the same convention slot in\n * one segment (e.g. page.tsx + page.jsx). Which file readdir returns first\n * is platform-dependent (APFS sorts entries, ext4 does not) — silently\n * picking one would let macOS dev and Linux CI serve different\n * implementations of the same route.\n */\nfunction throwDuplicateConvention(\n convention: string,\n firstPath: string,\n secondPath: string\n): never {\n throw new Error(\n `Build error: duplicate '${convention}' files in the same route segment.\\n` +\n ` File 1: ${firstPath}\\n` +\n ` File 2: ${secondPath}\\n` +\n `Each segment may define '${convention}' at most once. Remove one of the files.`\n );\n}\n\n/**\n * Recursively scan child directories and build the segment tree.\n */\nfunction scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanChildren: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n try {\n if (!statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n // Reject directories with encoded path delimiters or null bytes.\n // These can cause route collisions when decoded at the URL boundary.\n // See design/13-security.md §\"Encoded separators rejected\" and §\"Null bytes rejected\".\n if (ENCODED_SEPARATOR_RE.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded path delimiter (%2F or %5C).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded separators in directory names cause route collisions when decoded. ` +\n `Rename the directory to remove the encoded delimiter.`\n );\n }\n if (NULL_BYTE_RE.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded null byte (%00).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded null bytes in directory names are not allowed. ` +\n `Rename the directory to remove the null byte encoding.`\n );\n }\n\n const { type, paramName, interceptionMarker, interceptedSegmentName } = classifySegment(entry);\n\n // Skip private folders — underscore-prefixed dirs are excluded from routing\n if (type === 'private') continue;\n\n const urlPath = computeUrlPath(parentNode.urlPath, entry, type);\n const childNode = createSegmentNode(\n entry,\n type,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName\n );\n\n // Scan this segment's files\n scanSegmentFiles(fullPath, childNode, extSet);\n\n // Recurse into subdirectories\n scanChildren(fullPath, childNode, extSet);\n\n // Attach to parent: slots go into slots record, everything else is a child\n if (type === 'slot') {\n const slotName = entry.slice(1); // remove @\n parentNode.slots[slotName] = childNode;\n } else {\n parentNode.children.push(childNode);\n }\n }\n}\n\n/**\n * Validate that route groups don't produce conflicting pages/routes at the same URL path.\n *\n * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim\n * /login — the scanner must detect and reject this at build time.\n *\n * Each parallel slot is validated as its OWN collision space: a slot's pages\n * intentionally share URL paths with the parent tree (that's the point of\n * parallel routes), but two route groups INSIDE one slot claiming the same\n * URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.\n */\nfunction validateRouteGroupCollisions(root: SegmentNode): void {\n // Map from urlPath → { filePath, source } for the first page/route seen at that path\n collectRoutableLeaves(root, new Map(), '');\n}\n\n/**\n * Walk the segment tree and collect all routable leaves (page or route files),\n * throwing on collision. Each slot recurses with a FRESH collision map —\n * slots are parallel routes that intentionally share URL paths with their\n * parent tree, but collisions within a single slot are real errors.\n */\nfunction collectRoutableLeaves(\n node: SegmentNode,\n seen: Map<string, { filePath: string; segmentPath: string }>,\n segmentPath: string\n): void {\n // Intercepting routes are resolved by the slot resolver, not URL matching,\n // and their computed urlPaths intentionally mirror other routes (e.g. two\n // interceptors in one slot both produce /gallery/[id]). Skip them entirely.\n if (node.segmentType === 'intercepting') return;\n\n const currentPath = segmentPath\n ? `${segmentPath}/${node.segmentName}`\n : node.segmentName || '(root)';\n\n const routableFile = node.page ?? node.route;\n if (routableFile) {\n const existing = seen.get(node.urlPath);\n if (existing) {\n throw new Error(\n `Build error: route collision — multiple route groups produce a page/route at the same URL path.\\n` +\n ` URL path: ${node.urlPath}\\n` +\n ` File 1: ${existing.filePath} (via ${existing.segmentPath})\\n` +\n ` File 2: ${routableFile.filePath} (via ${currentPath})\\n` +\n `Each URL path must map to exactly one page or route handler. ` +\n `Rename or move one of the conflicting files.`\n );\n }\n seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });\n }\n\n // Recurse into children\n for (const child of node.children) {\n collectRoutableLeaves(child, seen, currentPath);\n }\n\n // Recurse into slots — each slot gets a fresh map (its own collision space)\n for (const slotNode of Object.values(node.slots)) {\n collectRoutableLeaves(slotNode, new Map(), currentPath);\n }\n}\n\n/**\n * Validate that no route chain contains duplicate dynamic param names.\n *\n * Example violation:\n * app/[id]/items/[id]/page.tsx — 'id' appears twice in the ancestor chain.\n *\n * Route groups are transparent — params accumulate through them.\n * Slots are independent — duplicate detection does NOT cross slot boundaries.\n *\n * See design/07-routing.md §\"Duplicate Param Name Detection\"\n */\nfunction validateDuplicateParamNames(root: SegmentNode): void {\n walkForDuplicateParams(root, new Map());\n}\n\n/**\n * Recursively walk the segment tree, tracking seen param names → segment paths.\n * Throws on the first duplicate found.\n */\nfunction walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): void {\n // If this node introduces a param name, check for duplicates\n if (node.paramName) {\n const existing = seen.get(node.paramName);\n if (existing) {\n throw new Error(\n `[timber] Duplicate param name '${node.paramName}' in route chain.\\n` +\n ` First defined at: ${existing}\\n` +\n ` Duplicate at: ${node.urlPath || '/'}\\n` +\n ` Rename one of the segments to avoid ambiguity.`\n );\n }\n // Add to seen for descendants (use a new Map to avoid polluting siblings)\n seen = new Map(seen);\n seen.set(node.paramName, node.urlPath || '/');\n }\n\n // Recurse into children (they inherit the accumulated params)\n for (const child of node.children) {\n walkForDuplicateParams(child, seen);\n }\n\n // Slots are independent parallel routes — start fresh param tracking\n // (a slot's params don't conflict with the main route's params)\n for (const slotNode of Object.values(node.slots)) {\n walkForDuplicateParams(slotNode, new Map(seen));\n }\n}\n\n/**\n * Validate that each param name uses a single bracket form across the entire route tree.\n *\n * Different bracket forms produce different types:\n * [id] → string\n * [...id] → string[]\n * [[...id]] → string[] | undefined\n *\n * If the same param name appears with multiple forms, the no-argument\n * getSegmentParams() / useSegmentParams() return type becomes ambiguous.\n *\n * Same name + same form across different routes is fine and expected.\n */\nfunction validateUniqueBracketForms(root: SegmentNode): void {\n const forms = new Map<string, { segmentType: SegmentType; urlPath: string }>();\n collectBracketForms(root, forms);\n}\n\nfunction collectBracketForms(\n node: SegmentNode,\n forms: Map<string, { segmentType: SegmentType; urlPath: string }>\n): void {\n if (node.paramName && node.segmentType !== 'static' && node.segmentType !== 'group') {\n const existing = forms.get(node.paramName);\n if (existing && existing.segmentType !== node.segmentType) {\n const bracketExamples: Record<string, string> = {\n 'dynamic': `[${node.paramName}]`,\n 'catch-all': `[...${node.paramName}]`,\n 'optional-catch-all': `[[...${node.paramName}]]`,\n };\n throw new Error(\n `[timber] Param name '${node.paramName}' uses multiple bracket forms.\\n` +\n ` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\\n` +\n ` ${bracketExamples[node.segmentType] ?? node.segmentType} at: ${node.urlPath || '/'}\\n` +\n ` Each param name must use a single bracket form across all routes.\\n` +\n ` Rename one of the segments to fix this (e.g., [${node.paramName}] → [${node.paramName}Id]).`\n );\n }\n if (!existing) {\n forms.set(node.paramName, {\n segmentType: node.segmentType,\n urlPath: node.urlPath || '/',\n });\n }\n }\n\n for (const child of node.children) {\n collectBracketForms(child, forms);\n }\n for (const slotNode of Object.values(node.slots)) {\n collectBracketForms(slotNode, forms);\n }\n}\n\n/**\n * Find a fixed-extension file (proxy.ts) in a directory.\n */\nfunction findFixedFile(dirPath: string, name: string): RouteFile | undefined {\n for (const ext of ['ts', 'tsx']) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n\n/**\n * Find a file using the configured page extensions (tsx, ts, jsx, js, mdx, etc.).\n * Used for app-root conventions like global-error that aren't per-segment.\n */\nfunction findPageExtFile(\n dirPath: string,\n name: string,\n extSet: Set<string>\n): RouteFile | undefined {\n for (const ext of extSet) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n","/**\n * `timber schema sync` — scan filesystem for dynamic segments, append\n * missing entries with `codec.string` defaults, and remove stale entries\n * whose segments no longer exist in the filesystem.\n *\n * Design doc: design/41-global-params.md §Auto-Scaffolding\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { scanRoutes } from './routing/scanner.js';\nimport type { SegmentNode } from './routing/types.js';\nimport { resolveAppDir } from './plugin-context.js';\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from the route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegments(appDir: string): string[] {\n const tree = scanRoutes(appDir);\n const segments = new Set<string>();\n walkSegments(tree.root, segments);\n return [...segments].sort();\n}\n\nfunction walkSegments(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName) {\n // Reconstruct bracket key from segmentName (already has brackets)\n segments.add(node.segmentName);\n }\n for (const child of node.children) {\n walkSegments(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkSegments(slot, segments);\n }\n}\n\n/**\n * Replace comment bodies (and optionally string contents) with spaces,\n * preserving length and newlines so every index maps 1:1 to the original.\n * Lets the structural scanning below ignore `segmentParams` mentions and\n * braces inside comments/strings without a TS parser (fine pre-alpha,\n * see TIM-1066).\n *\n * With `maskStrings`, string contents are blanked too — except literal\n * 'segmentParams' strings, kept so a quoted property key stays findable.\n */\nfunction maskSource(content: string, maskStrings: boolean): string {\n const out = content.split('');\n const n = content.length;\n let i = 0;\n\n const blank = (from: number, to: number): void => {\n for (let j = from; j < to; j++) {\n if (content[j] !== '\\n') out[j] = ' ';\n }\n };\n\n while (i < n) {\n const ch = content[i];\n const next = content[i + 1];\n if (ch === '/' && next === '/') {\n const start = i;\n while (i < n && content[i] !== '\\n') i++;\n blank(start, i);\n } else if (ch === '/' && next === '*') {\n const start = i;\n i += 2;\n while (i < n && !(content[i] === '*' && content[i + 1] === '/')) i++;\n i = Math.min(i + 2, n);\n blank(start, i);\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n const start = i;\n i++;\n while (i < n && content[i] !== ch) {\n if (content[i] === '\\\\') i++;\n i++;\n }\n const end = i; // at closing quote (or n if unterminated)\n if (maskStrings && content.slice(start + 1, end) !== 'segmentParams') {\n blank(start + 1, end);\n }\n i++;\n } else {\n i++;\n }\n }\n return out.join('');\n}\n\n/**\n * Locate the `segmentParams: { ... }` block in a schema file.\n * Returns the indices of the opening and matching closing brace, or null\n * if no segmentParams property is found. Comments and strings are masked\n * first, so a JSDoc example mentioning `segmentParams: { ... }` above the\n * real defineSchema call is not mistaken for the block, and braces inside\n * comments/strings don't skew the depth count. The search is anchored to\n * the `defineSchema(` call when present, so helper objects with their own\n * segmentParams property earlier in the file are skipped.\n */\nexport function findSegmentParamsBlock(\n content: string\n): { openBrace: number; closeBrace: number } | null {\n const masked = maskSource(content, true);\n\n const defineCall = /\\bdefineSchema\\s*\\(/.exec(masked);\n const keyRe = /(['\"]?)segmentParams\\1\\s*:\\s*\\{/g;\n keyRe.lastIndex = defineCall ? defineCall.index + defineCall[0].length : 0;\n const keyMatch = keyRe.exec(masked);\n if (!keyMatch) return null;\n\n const openBrace = keyMatch.index + keyMatch[0].length - 1;\n let depth = 1;\n let i = openBrace + 1;\n while (i < masked.length && depth > 0) {\n if (masked[i] === '{') depth++;\n else if (masked[i] === '}') depth--;\n i++;\n }\n if (depth > 0) return null;\n // i now points to just after the closing brace\n return { openBrace, closeBrace: i - 1 };\n}\n\n/**\n * Parse existing bracket keys from an app/schema.ts file.\n * Scoped to the segmentParams: { ... } block — bracket keys elsewhere in\n * the file (unrelated objects, comments above defineSchema) are ignored.\n * Comments inside the block are masked, so a commented-out entry does not\n * count as registered.\n *\n * The key charset matches the route classifier (segment-classify.ts):\n * any character except `]`, so hyphenated params like '[my-param]' parse.\n */\nexport function parseExistingSchemaKeys(content: string): string[] {\n const block = findSegmentParamsBlock(content);\n if (!block) return [];\n\n const blockContent = maskSource(content, false).slice(block.openBrace, block.closeBrace);\n const keys: string[] = [];\n // Match quoted bracket keys like '[artistSlug]', '[...slug]', '[[...topic]]'\n const re = /['\"](\\[{1,2}(?:\\.{3})?[^\\]]+\\]{1,2})['\"]\\s*:/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(blockContent)) !== null) {\n keys.push(m[1]);\n }\n return keys;\n}\n\n/** Schema file extensions to check. */\nconst SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n\n/**\n * Find the schema file path if it exists.\n */\nexport function findSchemaFile(appDir: string): string | undefined {\n for (const ext of SCHEMA_EXTENSIONS) {\n const p = join(appDir, `schema${ext}`);\n if (existsSync(p)) return p;\n }\n return undefined;\n}\n\nexport function defaultCodecForSegment(seg: string): string {\n if (seg.startsWith('[[...')) return 'codec.optionalCatchAll(codec.string)';\n if (seg.startsWith('[...')) return 'codec.stringArray';\n return 'codec.string';\n}\n\n/**\n * Generate the content for a new app/schema.ts with the given segment keys.\n */\nfunction generateNewSchema(segments: string[]): string {\n const lines: string[] = [];\n lines.push(\"import { defineSchema } from '@timber-js/app/params';\");\n lines.push(\"import { codec } from '@timber-js/app/codec';\");\n lines.push('');\n lines.push('export default defineSchema({');\n lines.push(' segmentParams: {');\n for (const seg of segments) {\n lines.push(` '${seg}': ${defaultCodecForSegment(seg)},`);\n }\n lines.push(' },');\n lines.push('});');\n lines.push('');\n return lines.join('\\n');\n}\n\n/**\n * Append missing segment entries to an existing schema file.\n * Inserts new entries before the closing `}` of segmentParams.\n */\nconst CODEC_IMPORT = \"import { codec } from '@timber-js/app/codec';\";\n\nfunction hasCodecImport(content: string): boolean {\n return /import\\s+\\{[^}]*\\bcodec\\b[^}]*\\}\\s+from\\s+['\"]@timber-js\\/app\\/codec['\"]/.test(content);\n}\n\nfunction ensureCodecImport(content: string): string {\n if (hasCodecImport(content)) return content;\n\n // Insert after the last existing import statement\n const lines = content.split('\\n');\n let lastImportIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n if (/^\\s*import\\s/.test(lines[i])) lastImportIndex = i;\n }\n\n if (lastImportIndex === -1) {\n return CODEC_IMPORT + '\\n' + content;\n }\n\n lines.splice(lastImportIndex + 1, 0, CODEC_IMPORT);\n return lines.join('\\n');\n}\n\nfunction appendToSchema(content: string, newSegments: string[]): string {\n content = ensureCodecImport(content);\n\n const block = findSegmentParamsBlock(content);\n if (!block) {\n throw new Error(\n '[timber] Could not find segmentParams in schema file. ' +\n 'Expected a defineSchema({ segmentParams: { ... } }) call.'\n );\n }\n const { openBrace, closeBrace } = block;\n\n // Detect indentation from existing entries\n const existingLine = content.slice(openBrace, closeBrace).match(/^(\\s+)['\"]\\[/m);\n const indent = existingLine ? existingLine[1] : ' ';\n\n // Build insertion text\n const insertion = newSegments\n .map((seg) => `${indent}'${seg}': ${defaultCodecForSegment(seg)},`)\n .join('\\n');\n\n // Insert before the closing brace\n const before = content.slice(0, closeBrace);\n const after = content.slice(closeBrace);\n\n // Add newline if the content before doesn't end with one\n const separator = before.trimEnd().endsWith(',') ? '\\n' : '\\n';\n\n return before + separator + insertion + '\\n' + after;\n}\n\n/**\n * Remove entries from the segmentParams block whose keys match `staleSegments`.\n * Removes the entire entry including trailing comma/whitespace. Scoped to\n * the segmentParams block — identical keys in unrelated objects elsewhere\n * in the file are left untouched.\n */\nfunction removeFromSchema(content: string, staleSegments: string[]): string {\n const block = findSegmentParamsBlock(content);\n if (!block) return content;\n\n let blockContent = content.slice(block.openBrace, block.closeBrace);\n // Key matching and the entry walk run on a comment-masked copy (same\n // length, indices align) so commented-out entries are never removed.\n // Both copies are spliced in sync to keep indices valid.\n let blockMasked = maskSource(blockContent, false);\n\n for (const seg of staleSegments) {\n const escaped = seg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const keyRe = new RegExp(`^[ \\t]*['\"]${escaped}['\"][ \\t]*:`, 'gm');\n let m: RegExpExecArray | null;\n while ((m = keyRe.exec(blockMasked)) !== null) {\n const start = m.index;\n let i = m.index + m[0].length;\n let depth = 0;\n let inString: string | null = null;\n\n while (i < blockMasked.length) {\n const ch = blockMasked[i];\n if (inString) {\n if (ch === '\\\\') {\n i++;\n } else if (ch === inString) {\n inString = null;\n }\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch;\n } else if (ch === '(' || ch === '{' || ch === '[') {\n depth++;\n } else if (ch === ')' || ch === '}' || ch === ']') {\n depth--;\n } else if (depth === 0 && ch === ',') {\n i++;\n break;\n } else if (depth === 0 && (ch === '\\n' || ch === '}')) {\n break;\n }\n i++;\n }\n\n // Consume trailing newline\n if (i < blockMasked.length && blockMasked[i] === '\\n') i++;\n\n blockContent = blockContent.slice(0, start) + blockContent.slice(i);\n blockMasked = blockMasked.slice(0, start) + blockMasked.slice(i);\n keyRe.lastIndex = start;\n }\n }\n\n return content.slice(0, block.openBrace) + blockContent + content.slice(block.closeBrace);\n}\n\nexport interface SchemaSyncResult {\n /** Segments found in the filesystem. */\n filesystemSegments: string[];\n /** Segments already defined in app/schema.ts. */\n existingSegments: string[];\n /** Segments that were added. */\n addedSegments: string[];\n /** Segments that were removed (no longer in filesystem). */\n removedSegments: string[];\n /** Whether a new schema file was created. */\n created: boolean;\n}\n\n/**\n * Run the schema sync: scan filesystem, diff against schema, append missing.\n */\nexport function runSchemaSync(projectRoot: string): SchemaSyncResult {\n const appDir = resolveAppDir(projectRoot);\n\n const filesystemSegments = collectDynamicSegments(appDir);\n\n const schemaPath = findSchemaFile(appDir);\n\n if (filesystemSegments.length === 0 && !schemaPath) {\n return {\n filesystemSegments: [],\n existingSegments: [],\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n if (!schemaPath) {\n // Create new schema file\n const newPath = join(appDir, 'schema.ts');\n writeFileSync(newPath, generateNewSchema(filesystemSegments), 'utf-8');\n return {\n filesystemSegments,\n existingSegments: [],\n addedSegments: filesystemSegments,\n removedSegments: [],\n created: true,\n };\n }\n\n // Read existing schema and diff\n const content = readFileSync(schemaPath, 'utf-8');\n const existingSegments = parseExistingSchemaKeys(content);\n const missing = filesystemSegments.filter((seg) => !existingSegments.includes(seg));\n const stale = existingSegments.filter((seg) => !filesystemSegments.includes(seg));\n\n if (missing.length === 0 && stale.length === 0) {\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n let updated = content;\n if (stale.length > 0) {\n updated = removeFromSchema(updated, stale);\n }\n if (missing.length > 0) {\n updated = appendToSchema(updated, missing);\n }\n writeFileSync(schemaPath, updated, 'utf-8');\n\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: missing,\n removedSegments: stale,\n created: false,\n };\n}\n"],"mappings":"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"}
|