@rangojs/router 0.0.0-experimental.61 → 0.0.0-experimental.63
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/README.md +61 -8
- package/dist/bin/rango.js +2 -1
- package/dist/vite/index.js +144 -62
- package/dist/vite/index.js.bak +5448 -0
- package/package.json +14 -15
- package/skills/prerender/SKILL.md +110 -68
- package/src/__internal.ts +1 -1
- package/src/build/generate-manifest.ts +3 -6
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +8 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +2 -0
- package/src/route-definition/dsl-helpers.ts +37 -18
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +11 -0
- package/src/router/handler-context.ts +22 -5
- package/src/router/match-api.ts +2 -8
- package/src/router/match-middleware/cache-lookup.ts +2 -6
- package/src/router/prerender-match.ts +104 -8
- package/src/router/router-interfaces.ts +4 -0
- package/src/router/segment-resolution/fresh.ts +7 -2
- package/src/router/segment-resolution/revalidation.ts +10 -5
- package/src/router.ts +9 -1
- package/src/server/context.ts +5 -1
- package/src/static-handler.ts +18 -6
- package/src/types/handler-context.ts +12 -2
- package/src/types/route-entry.ts +1 -1
- package/src/urls/path-helper-types.ts +5 -1
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/response-types.ts +16 -6
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/prerender-collection.ts +14 -1
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/rango.ts +2 -1
- package/src/vite/router-discovery.ts +153 -34
- package/src/vite/utils/prerender-utils.ts +2 -0
package/src/prerender.ts
CHANGED
|
@@ -36,6 +36,7 @@ import type { Handle } from "./handle.js";
|
|
|
36
36
|
import type { ContextVar } from "./context-var.js";
|
|
37
37
|
import type { ReverseFunction } from "./reverse.js";
|
|
38
38
|
import type { DefaultReverseRouteMap } from "./types/global-namespace.js";
|
|
39
|
+
import type { UseItems, HandlerUseItem } from "./route-types.js";
|
|
39
40
|
import { isCachedFunction } from "./cache/taint.js";
|
|
40
41
|
|
|
41
42
|
// -- Named route resolution types -------------------------------------------
|
|
@@ -105,13 +106,6 @@ type ResolvePrerenderParams<
|
|
|
105
106
|
// -- Types ------------------------------------------------------------------
|
|
106
107
|
|
|
107
108
|
export interface PrerenderOptions {
|
|
108
|
-
/**
|
|
109
|
-
* Keep handler in server bundle for live fallback (default: false).
|
|
110
|
-
* false: handler replaced with stub, source-only APIs excluded from bundle.
|
|
111
|
-
* true: handler stays in bundle, unknown params render live at request time.
|
|
112
|
-
*/
|
|
113
|
-
passthrough?: boolean;
|
|
114
|
-
|
|
115
109
|
/**
|
|
116
110
|
* Maximum number of param sets to render in parallel (default: 1).
|
|
117
111
|
* Only applies to dynamic Prerender handlers with getParams().
|
|
@@ -131,8 +125,8 @@ export interface PrerenderOptions {
|
|
|
131
125
|
|
|
132
126
|
/**
|
|
133
127
|
* Context passed to Prerender() handlers at build time.
|
|
134
|
-
* Has a synthetic URL from getParams, params, and
|
|
135
|
-
* No request,
|
|
128
|
+
* Has a synthetic URL from getParams, params, pathname, and optionally env.
|
|
129
|
+
* No request, headers, cookies.
|
|
136
130
|
*/
|
|
137
131
|
export interface BuildContext<TParams> {
|
|
138
132
|
/** Params extracted from the route pattern (populated from getParams). */
|
|
@@ -141,6 +135,23 @@ export interface BuildContext<TParams> {
|
|
|
141
135
|
/** True during build-time pre-rendering, false during passthrough live render. */
|
|
142
136
|
build: true;
|
|
143
137
|
|
|
138
|
+
/**
|
|
139
|
+
* True when running in Vite dev mode (on-demand prerender), false during
|
|
140
|
+
* production `vite build`. Use this to branch on runtime mode without
|
|
141
|
+
* changing build semantics.
|
|
142
|
+
*/
|
|
143
|
+
dev: boolean;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build-time environment bindings (KV, D1, etc.) supplied by the Vite plugin.
|
|
147
|
+
* Only available when `buildEnv` is configured in rango() options.
|
|
148
|
+
* Throws with a clear error if not configured.
|
|
149
|
+
*
|
|
150
|
+
* This is NOT the live request env — it is shared across all prerender
|
|
151
|
+
* invocations for the build.
|
|
152
|
+
*/
|
|
153
|
+
env: DefaultEnv;
|
|
154
|
+
|
|
144
155
|
/** Read a variable set by getParams or a parent handler. */
|
|
145
156
|
get: {
|
|
146
157
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
@@ -173,8 +184,8 @@ export interface BuildContext<TParams> {
|
|
|
173
184
|
|
|
174
185
|
/**
|
|
175
186
|
* Signal that this param set should not produce a local prerender artifact.
|
|
176
|
-
* At runtime the handler runs
|
|
177
|
-
* with `
|
|
187
|
+
* At runtime the live handler runs instead. Only valid on routes wrapped
|
|
188
|
+
* with `Passthrough()`.
|
|
178
189
|
*/
|
|
179
190
|
passthrough: () => PrerenderPassthroughResult;
|
|
180
191
|
}
|
|
@@ -187,6 +198,17 @@ export interface StaticBuildContext {
|
|
|
187
198
|
/** Always true for Static handlers at build time. */
|
|
188
199
|
build: true;
|
|
189
200
|
|
|
201
|
+
/**
|
|
202
|
+
* True when running in Vite dev mode, false during production build.
|
|
203
|
+
*/
|
|
204
|
+
dev: boolean;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build-time environment bindings supplied by the Vite plugin.
|
|
208
|
+
* Only available when `buildEnv` is configured in rango() options.
|
|
209
|
+
*/
|
|
210
|
+
env: DefaultEnv;
|
|
211
|
+
|
|
190
212
|
/** Read a variable (available for type consistency with BuildContext). */
|
|
191
213
|
get: {
|
|
192
214
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
@@ -214,6 +236,17 @@ export interface GetParamsContext {
|
|
|
214
236
|
/** Always true during build-time getParams execution. */
|
|
215
237
|
build: true;
|
|
216
238
|
|
|
239
|
+
/**
|
|
240
|
+
* True when running in Vite dev mode, false during production build.
|
|
241
|
+
*/
|
|
242
|
+
dev: boolean;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Build-time environment bindings supplied by the Vite plugin.
|
|
246
|
+
* Only available when `buildEnv` is configured in rango() options.
|
|
247
|
+
*/
|
|
248
|
+
env: DefaultEnv;
|
|
249
|
+
|
|
217
250
|
/** Set a variable that will be available to each handler invocation via ctx.get(). */
|
|
218
251
|
set: {
|
|
219
252
|
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
@@ -224,23 +257,6 @@ export interface GetParamsContext {
|
|
|
224
257
|
reverse: BuildReverseFunction;
|
|
225
258
|
}
|
|
226
259
|
|
|
227
|
-
/**
|
|
228
|
-
* Context type for passthrough Prerender handlers.
|
|
229
|
-
*
|
|
230
|
-
* When `passthrough: true`, the handler runs both at build time and at request
|
|
231
|
-
* time. The context is a full `HandlerContext` with `build: boolean`:
|
|
232
|
-
* - `ctx.build === true`: build-time, env/request/res throw at runtime
|
|
233
|
-
* - `ctx.build === false`: live request, full context available
|
|
234
|
-
*
|
|
235
|
-
* For `passthrough: false` (default), handlers receive `BuildContext` only.
|
|
236
|
-
*/
|
|
237
|
-
export type PrerenderPassthroughContext<
|
|
238
|
-
TParams = {},
|
|
239
|
-
TEnv = DefaultEnv,
|
|
240
|
-
> = HandlerContext<TParams, TEnv> & {
|
|
241
|
-
passthrough: () => PrerenderPassthroughResult;
|
|
242
|
-
};
|
|
243
|
-
|
|
244
260
|
export interface PrerenderHandlerDefinition<
|
|
245
261
|
TParams extends Record<string, any> = any,
|
|
246
262
|
> {
|
|
@@ -253,6 +269,8 @@ export interface PrerenderHandlerDefinition<
|
|
|
253
269
|
getParams?: (ctx: GetParamsContext) => Promise<TParams[]> | TParams[];
|
|
254
270
|
/** Pre-render options. */
|
|
255
271
|
options?: PrerenderOptions;
|
|
272
|
+
/** Composable default DSL items merged when the handler is mounted. */
|
|
273
|
+
use?: () => UseItems<HandlerUseItem>;
|
|
256
274
|
}
|
|
257
275
|
|
|
258
276
|
// -- Overloads --------------------------------------------------------------
|
|
@@ -263,7 +281,7 @@ export interface PrerenderHandlerDefinition<
|
|
|
263
281
|
// Explicit params work as before:
|
|
264
282
|
// Prerender<{ slug: string }> → params = { slug: string }
|
|
265
283
|
|
|
266
|
-
// Overload 1: Static handler
|
|
284
|
+
// Overload 1: Static handler (build-time only)
|
|
267
285
|
export function Prerender<
|
|
268
286
|
T extends
|
|
269
287
|
| keyof DefaultPrerenderRouteMap
|
|
@@ -273,34 +291,15 @@ export function Prerender<
|
|
|
273
291
|
>(
|
|
274
292
|
handler: (
|
|
275
293
|
ctx: BuildContext<ResolvePrerenderParams<T, TRouteMap>>,
|
|
276
|
-
) => ReactNode | Promise<ReactNode>,
|
|
277
|
-
options?: PrerenderOptions & { passthrough?: false },
|
|
278
|
-
__injectedId?: string,
|
|
279
|
-
): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
|
|
280
|
-
|
|
281
|
-
// Overload 2: Static handler, passthrough (build + live — full HandlerContext)
|
|
282
|
-
export function Prerender<
|
|
283
|
-
T extends
|
|
284
|
-
| keyof DefaultPrerenderRouteMap
|
|
285
|
-
| `.${keyof TRouteMap & string}`
|
|
286
|
-
| Record<string, any> = {},
|
|
287
|
-
TRouteMap extends {} = DefaultPrerenderRouteMap,
|
|
288
|
-
TEnv = DefaultEnv,
|
|
289
|
-
>(
|
|
290
|
-
handler: (
|
|
291
|
-
ctx: PrerenderPassthroughContext<
|
|
292
|
-
ResolvePrerenderParams<T, TRouteMap>,
|
|
293
|
-
TEnv
|
|
294
|
-
>,
|
|
295
294
|
) =>
|
|
296
295
|
| ReactNode
|
|
297
296
|
| PrerenderPassthroughResult
|
|
298
297
|
| Promise<ReactNode | PrerenderPassthroughResult>,
|
|
299
|
-
options
|
|
298
|
+
options?: PrerenderOptions,
|
|
300
299
|
__injectedId?: string,
|
|
301
300
|
): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
|
|
302
301
|
|
|
303
|
-
// Overload
|
|
302
|
+
// Overload 2: Dynamic handler (build-time only)
|
|
304
303
|
export function Prerender<
|
|
305
304
|
T extends
|
|
306
305
|
| keyof DefaultPrerenderRouteMap
|
|
@@ -315,35 +314,11 @@ export function Prerender<
|
|
|
315
314
|
| ResolvePrerenderParams<T, TRouteMap>[],
|
|
316
315
|
handler: (
|
|
317
316
|
ctx: BuildContext<ResolvePrerenderParams<T, TRouteMap>>,
|
|
318
|
-
) => ReactNode | Promise<ReactNode>,
|
|
319
|
-
options?: PrerenderOptions & { passthrough?: false },
|
|
320
|
-
__injectedId?: string,
|
|
321
|
-
): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
|
|
322
|
-
|
|
323
|
-
// Overload 4: Dynamic handler, passthrough (build + live — full HandlerContext)
|
|
324
|
-
export function Prerender<
|
|
325
|
-
T extends
|
|
326
|
-
| keyof DefaultPrerenderRouteMap
|
|
327
|
-
| `.${keyof TRouteMap & string}`
|
|
328
|
-
| Record<string, any>,
|
|
329
|
-
TRouteMap extends {} = DefaultPrerenderRouteMap,
|
|
330
|
-
TEnv = DefaultEnv,
|
|
331
|
-
>(
|
|
332
|
-
getParams: (
|
|
333
|
-
ctx: GetParamsContext,
|
|
334
|
-
) =>
|
|
335
|
-
| Promise<ResolvePrerenderParams<T, TRouteMap>[]>
|
|
336
|
-
| ResolvePrerenderParams<T, TRouteMap>[],
|
|
337
|
-
handler: (
|
|
338
|
-
ctx: PrerenderPassthroughContext<
|
|
339
|
-
ResolvePrerenderParams<T, TRouteMap>,
|
|
340
|
-
TEnv
|
|
341
|
-
>,
|
|
342
317
|
) =>
|
|
343
318
|
| ReactNode
|
|
344
319
|
| PrerenderPassthroughResult
|
|
345
320
|
| Promise<ReactNode | PrerenderPassthroughResult>,
|
|
346
|
-
options
|
|
321
|
+
options?: PrerenderOptions,
|
|
347
322
|
__injectedId?: string,
|
|
348
323
|
): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
|
|
349
324
|
|
|
@@ -422,7 +397,7 @@ export function Prerender<TParams extends Record<string, any>>(
|
|
|
422
397
|
/**
|
|
423
398
|
* Sentinel returned by `ctx.passthrough()` to signal that a specific param set
|
|
424
399
|
* should not produce a local prerender artifact. The build skips writing the
|
|
425
|
-
* entry; at runtime the
|
|
400
|
+
* entry; at runtime the Passthrough live handler runs instead.
|
|
426
401
|
*/
|
|
427
402
|
export const PRERENDER_PASSTHROUGH: Readonly<{
|
|
428
403
|
__brand: "prerenderPassthrough";
|
|
@@ -446,7 +421,7 @@ export function isPrerenderPassthrough(
|
|
|
446
421
|
);
|
|
447
422
|
}
|
|
448
423
|
|
|
449
|
-
// -- Type
|
|
424
|
+
// -- Type guards ------------------------------------------------------------
|
|
450
425
|
|
|
451
426
|
/**
|
|
452
427
|
* Type guard to check if a value is a PrerenderHandlerDefinition.
|
|
@@ -461,3 +436,89 @@ export function isPrerenderHandler(
|
|
|
461
436
|
(value as { __brand: unknown }).__brand === "prerenderHandler"
|
|
462
437
|
);
|
|
463
438
|
}
|
|
439
|
+
|
|
440
|
+
// -- Passthrough wrapper ----------------------------------------------------
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* A prerender route with a live fallback handler for unknown params at runtime.
|
|
444
|
+
*
|
|
445
|
+
* Wraps a `Prerender(...)` definition with a separate handler that runs at
|
|
446
|
+
* request time for params not covered by `getParams()`.
|
|
447
|
+
*
|
|
448
|
+
* - Build time: `prerenderDef` provides getParams + build handler.
|
|
449
|
+
* - Runtime: `liveHandler` runs for unknown params with full HandlerContext.
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```ts
|
|
453
|
+
* const BlogPrerender = Prerender(
|
|
454
|
+
* async () => [{ slug: "getting-started" }, { slug: "api-reference" }],
|
|
455
|
+
* async (ctx) => <BlogPost slug={ctx.params.slug} />,
|
|
456
|
+
* );
|
|
457
|
+
*
|
|
458
|
+
* // In route definition:
|
|
459
|
+
* path("/blog/:slug", Passthrough(BlogPrerender, async (ctx) => {
|
|
460
|
+
* const post = await ctx.env.DB.get(ctx.params.slug);
|
|
461
|
+
* return <BlogPost slug={ctx.params.slug} post={post} />;
|
|
462
|
+
* }))
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
export interface PassthroughHandlerDefinition<
|
|
466
|
+
TParams extends Record<string, any> = any,
|
|
467
|
+
TEnv = DefaultEnv,
|
|
468
|
+
> {
|
|
469
|
+
readonly __brand: "passthroughHandler";
|
|
470
|
+
/** The underlying prerender definition (build-time rendering). */
|
|
471
|
+
prerenderDef: PrerenderHandlerDefinition<TParams>;
|
|
472
|
+
/** Live handler for runtime fallback on unknown params. */
|
|
473
|
+
liveHandler: (
|
|
474
|
+
ctx: HandlerContext<TParams, TEnv>,
|
|
475
|
+
) => ReactNode | Promise<ReactNode> | Response | Promise<Response>;
|
|
476
|
+
/** Composable default DSL items merged when the handler is mounted. */
|
|
477
|
+
use?: () => UseItems<HandlerUseItem>;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function Passthrough<
|
|
481
|
+
TParams extends Record<string, any>,
|
|
482
|
+
TEnv = DefaultEnv,
|
|
483
|
+
>(
|
|
484
|
+
prerenderDef: PrerenderHandlerDefinition<TParams>,
|
|
485
|
+
liveHandler: (
|
|
486
|
+
ctx: HandlerContext<TParams, TEnv>,
|
|
487
|
+
) => ReactNode | Promise<ReactNode> | Response | Promise<Response>,
|
|
488
|
+
): PassthroughHandlerDefinition<TParams, TEnv>;
|
|
489
|
+
|
|
490
|
+
// Implementation
|
|
491
|
+
export function Passthrough<
|
|
492
|
+
TParams extends Record<string, any>,
|
|
493
|
+
TEnv = DefaultEnv,
|
|
494
|
+
>(
|
|
495
|
+
prerenderDef: PrerenderHandlerDefinition<TParams>,
|
|
496
|
+
liveHandler: (
|
|
497
|
+
ctx: HandlerContext<TParams, TEnv>,
|
|
498
|
+
) => ReactNode | Promise<ReactNode> | Response | Promise<Response>,
|
|
499
|
+
): PassthroughHandlerDefinition<TParams, TEnv> {
|
|
500
|
+
if (!isPrerenderHandler(prerenderDef)) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
"[rsc-router] Passthrough: first argument must be a Prerender() definition.",
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
__brand: "passthroughHandler" as const,
|
|
507
|
+
prerenderDef,
|
|
508
|
+
liveHandler,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Type guard to check if a value is a PassthroughHandlerDefinition.
|
|
514
|
+
*/
|
|
515
|
+
export function isPassthroughHandler(
|
|
516
|
+
value: unknown,
|
|
517
|
+
): value is PassthroughHandlerDefinition {
|
|
518
|
+
return (
|
|
519
|
+
typeof value === "object" &&
|
|
520
|
+
value !== null &&
|
|
521
|
+
"__brand" in value &&
|
|
522
|
+
(value as { __brand: unknown }).__brand === "passthroughHandler"
|
|
523
|
+
);
|
|
524
|
+
}
|
package/src/reverse.ts
CHANGED
|
@@ -332,7 +332,9 @@ export function createReverse<TRoutes extends Record<string, string>>(
|
|
|
332
332
|
// Clean up slashes only when an optional param was actually omitted,
|
|
333
333
|
// so intentional trailing-slash patterns like "/blog/" are preserved.
|
|
334
334
|
if (hadOmittedOptional) {
|
|
335
|
+
const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
335
336
|
result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
337
|
+
if (hadTrailingSlash && !result.endsWith("/")) result += "/";
|
|
336
338
|
}
|
|
337
339
|
}
|
|
338
340
|
|
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
UseItems,
|
|
38
38
|
} from "../route-types.js";
|
|
39
39
|
import type { RouteHelpers } from "./helpers-types.js";
|
|
40
|
+
import { resolveHandlerUse, mergeHandlerUse } from "./resolve-handler-use.js";
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Check if an item contains routes (directly or inside nested structures like cache).
|
|
@@ -447,15 +448,6 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
447
448
|
: {}),
|
|
448
449
|
} satisfies EntryData;
|
|
449
450
|
|
|
450
|
-
// Run use callback if provided to collect loaders, revalidate, loading
|
|
451
|
-
if (use && typeof use === "function") {
|
|
452
|
-
const result = store.run(namespace, entry, use)?.flat(3);
|
|
453
|
-
invariant(
|
|
454
|
-
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
455
|
-
`parallel() use() callback must return an array of use items [${namespace}]`,
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
451
|
for (const slotName of slotNames) {
|
|
460
452
|
const slotEntry = {
|
|
461
453
|
...entry,
|
|
@@ -478,6 +470,22 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
478
470
|
staticHandlerIds: undefined,
|
|
479
471
|
}),
|
|
480
472
|
} satisfies EntryData;
|
|
473
|
+
|
|
474
|
+
// Per-slot: handler.use defaults first, then explicit use second.
|
|
475
|
+
// This matches the "defaults first, overrides second" rule used by
|
|
476
|
+
// path(), layout(), and intercept(). Each slot's handler.use is
|
|
477
|
+
// scoped to its own entry (no cross-slot bleed).
|
|
478
|
+
const slotHandler = (slots as Record<string, any>)[slotName];
|
|
479
|
+
const slotHandlerUse = resolveHandlerUse(slotHandler);
|
|
480
|
+
const slotMergedUse = mergeHandlerUse(slotHandlerUse, use, "parallel");
|
|
481
|
+
if (slotMergedUse) {
|
|
482
|
+
const result = store.run(namespace, slotEntry, slotMergedUse)?.flat(3);
|
|
483
|
+
invariant(
|
|
484
|
+
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
485
|
+
`parallel() use() callback must return an array of use items [${namespace}]`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
481
489
|
ctx.parent.parallel[slotName] = slotEntry;
|
|
482
490
|
}
|
|
483
491
|
return { name: namespace, type: "parallel" } as ParallelItem;
|
|
@@ -527,8 +535,12 @@ const intercept = (
|
|
|
527
535
|
when: [], // Selector conditions for conditional interception
|
|
528
536
|
};
|
|
529
537
|
|
|
530
|
-
//
|
|
531
|
-
|
|
538
|
+
// Merge handler.use defaults with explicit use
|
|
539
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
540
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "intercept");
|
|
541
|
+
|
|
542
|
+
// Run merged use callback to collect loaders, revalidate, middleware, etc.
|
|
543
|
+
if (mergedUse) {
|
|
532
544
|
// Create a temporary parent context for the use() callback
|
|
533
545
|
// so that middleware, loader, revalidate attach to the intercept entry
|
|
534
546
|
const originalParent = ctx.parent;
|
|
@@ -555,7 +567,7 @@ const intercept = (
|
|
|
555
567
|
};
|
|
556
568
|
ctx.parent = tempParent as EntryData;
|
|
557
569
|
|
|
558
|
-
const result =
|
|
570
|
+
const result = mergedUse()?.flat(3);
|
|
559
571
|
|
|
560
572
|
// Restore original parent
|
|
561
573
|
ctx.parent = originalParent;
|
|
@@ -752,7 +764,7 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
752
764
|
shortCode: store.getShortCode("route"),
|
|
753
765
|
type: "route",
|
|
754
766
|
parent: ctx.parent,
|
|
755
|
-
handler,
|
|
767
|
+
handler: handler as unknown as Handler<any, any, any>,
|
|
756
768
|
loading: undefined, // Allow loading() to attach loading state
|
|
757
769
|
middleware: [],
|
|
758
770
|
revalidate: [],
|
|
@@ -771,9 +783,12 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
771
783
|
);
|
|
772
784
|
/* Register route entry */
|
|
773
785
|
ctx.manifest.set(name, entry);
|
|
786
|
+
/* Merge handler.use defaults with explicit use */
|
|
787
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
788
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
|
|
774
789
|
/* Run use and attach handlers */
|
|
775
|
-
if (
|
|
776
|
-
const result = store.run(namespace, entry,
|
|
790
|
+
if (mergedUse) {
|
|
791
|
+
const result = store.run(namespace, entry, mergedUse)?.flat(3);
|
|
777
792
|
invariant(
|
|
778
793
|
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
779
794
|
`route() use() callback must return an array of use items [${namespace}]`,
|
|
@@ -834,10 +849,14 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
834
849
|
(handler as any).$$routePrefix = ctx.namePrefix;
|
|
835
850
|
}
|
|
836
851
|
|
|
837
|
-
//
|
|
852
|
+
// Merge handler.use defaults with explicit use
|
|
853
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
854
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "layout");
|
|
855
|
+
|
|
856
|
+
// Run merged use callback if present
|
|
838
857
|
let result: AllUseItems[] | undefined;
|
|
839
|
-
if (
|
|
840
|
-
result = store.run(namespace, entry,
|
|
858
|
+
if (mergedUse) {
|
|
859
|
+
result = store.run(namespace, entry, mergedUse)?.flat(3);
|
|
841
860
|
|
|
842
861
|
invariant(
|
|
843
862
|
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { AllUseItems } from "../route-types.js";
|
|
2
|
+
import { isPrerenderHandler, isPassthroughHandler } from "../prerender.js";
|
|
3
|
+
import { isStaticHandler } from "../static-handler.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract the .use callback from any handler shape.
|
|
7
|
+
*
|
|
8
|
+
* Checks definition brands first (objects with __brand), then plain functions.
|
|
9
|
+
* ReactNode handlers return undefined (no .use possible).
|
|
10
|
+
*/
|
|
11
|
+
export function resolveHandlerUse(handler: unknown): (() => any[]) | undefined {
|
|
12
|
+
if (handler == null) return undefined;
|
|
13
|
+
|
|
14
|
+
// Check branded definitions first — they're objects but also have typeof "object"
|
|
15
|
+
if (isPassthroughHandler(handler)) {
|
|
16
|
+
return (handler as any).use;
|
|
17
|
+
}
|
|
18
|
+
if (isPrerenderHandler(handler)) {
|
|
19
|
+
return (handler as any).use;
|
|
20
|
+
}
|
|
21
|
+
if (isStaticHandler(handler)) {
|
|
22
|
+
return (handler as any).use;
|
|
23
|
+
}
|
|
24
|
+
// Plain handler function
|
|
25
|
+
if (typeof handler === "function") {
|
|
26
|
+
return (handler as any).use;
|
|
27
|
+
}
|
|
28
|
+
// ReactNode or other — no .use
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Allowed item types per mount site.
|
|
34
|
+
* Mirrors the RouteUseItem / ParallelUseItem / InterceptUseItem / LayoutUseItem unions
|
|
35
|
+
* from route-types.ts for runtime validation.
|
|
36
|
+
*/
|
|
37
|
+
const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
|
|
38
|
+
path: new Set([
|
|
39
|
+
"layout",
|
|
40
|
+
"parallel",
|
|
41
|
+
"intercept",
|
|
42
|
+
"middleware",
|
|
43
|
+
"revalidate",
|
|
44
|
+
"loader",
|
|
45
|
+
"loading",
|
|
46
|
+
"errorBoundary",
|
|
47
|
+
"notFoundBoundary",
|
|
48
|
+
"cache",
|
|
49
|
+
"transition",
|
|
50
|
+
]),
|
|
51
|
+
// Response routes (path.json, path.text, etc.) — mirrors ResponseRouteUseItem
|
|
52
|
+
response: new Set(["middleware", "cache"]),
|
|
53
|
+
route: new Set([
|
|
54
|
+
"layout",
|
|
55
|
+
"parallel",
|
|
56
|
+
"intercept",
|
|
57
|
+
"middleware",
|
|
58
|
+
"revalidate",
|
|
59
|
+
"loader",
|
|
60
|
+
"loading",
|
|
61
|
+
"errorBoundary",
|
|
62
|
+
"notFoundBoundary",
|
|
63
|
+
"cache",
|
|
64
|
+
"transition",
|
|
65
|
+
]),
|
|
66
|
+
// layout allows AllUseItems — no validation needed, but included for completeness
|
|
67
|
+
layout: new Set([
|
|
68
|
+
"layout",
|
|
69
|
+
"route",
|
|
70
|
+
"middleware",
|
|
71
|
+
"revalidate",
|
|
72
|
+
"parallel",
|
|
73
|
+
"intercept",
|
|
74
|
+
"loader",
|
|
75
|
+
"loading",
|
|
76
|
+
"errorBoundary",
|
|
77
|
+
"notFoundBoundary",
|
|
78
|
+
"cache",
|
|
79
|
+
"transition",
|
|
80
|
+
"include",
|
|
81
|
+
]),
|
|
82
|
+
parallel: new Set([
|
|
83
|
+
"revalidate",
|
|
84
|
+
"loader",
|
|
85
|
+
"loading",
|
|
86
|
+
"errorBoundary",
|
|
87
|
+
"notFoundBoundary",
|
|
88
|
+
"transition",
|
|
89
|
+
]),
|
|
90
|
+
intercept: new Set([
|
|
91
|
+
"middleware",
|
|
92
|
+
"revalidate",
|
|
93
|
+
"loader",
|
|
94
|
+
"loading",
|
|
95
|
+
"errorBoundary",
|
|
96
|
+
"notFoundBoundary",
|
|
97
|
+
"layout",
|
|
98
|
+
"route",
|
|
99
|
+
"when",
|
|
100
|
+
"transition",
|
|
101
|
+
]),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate that items from handler.use() are valid for the given mount site.
|
|
106
|
+
* Throws a descriptive error if any item is not allowed.
|
|
107
|
+
*/
|
|
108
|
+
export function validateHandlerUseItems(
|
|
109
|
+
items: AllUseItems[],
|
|
110
|
+
mountSite: string,
|
|
111
|
+
): void {
|
|
112
|
+
const allowed = MOUNT_SITE_ALLOWED_TYPES[mountSite];
|
|
113
|
+
if (!allowed) return;
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
if (item == null) continue;
|
|
116
|
+
if (!allowed.has((item as any).type)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`handler.use() returned ${(item as any).type}() which is not valid inside ${mountSite}(). ` +
|
|
119
|
+
`Allowed types: ${[...allowed].join(", ")}.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create a merged use callback from handler.use and explicit use.
|
|
127
|
+
* handler.use items come first (defaults), explicit items second (overrides).
|
|
128
|
+
* Returns undefined if both are absent.
|
|
129
|
+
*/
|
|
130
|
+
export function mergeHandlerUse(
|
|
131
|
+
handlerUse: (() => any[]) | undefined,
|
|
132
|
+
explicitUse: (() => any[]) | undefined,
|
|
133
|
+
mountSite: string,
|
|
134
|
+
): (() => any[]) | undefined {
|
|
135
|
+
if (!handlerUse && !explicitUse) return undefined;
|
|
136
|
+
if (!handlerUse) return explicitUse;
|
|
137
|
+
if (!explicitUse) {
|
|
138
|
+
return () => {
|
|
139
|
+
const items = handlerUse().flat(3);
|
|
140
|
+
validateHandlerUseItems(items, mountSite);
|
|
141
|
+
return items;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return () => {
|
|
145
|
+
const hItems = handlerUse().flat(3);
|
|
146
|
+
validateHandlerUseItems(hItems, mountSite);
|
|
147
|
+
return [...hItems, ...explicitUse()];
|
|
148
|
+
};
|
|
149
|
+
}
|
package/src/route-types.ts
CHANGED
|
@@ -257,3 +257,14 @@ export type LoaderUseItem = RevalidateItem | CacheItem;
|
|
|
257
257
|
* runtime via .flat(3).
|
|
258
258
|
*/
|
|
259
259
|
export type UseItems<T> = (T | readonly T[])[];
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Union of all items that handler.use() may return.
|
|
263
|
+
* A handler doesn't know its mount site at definition time, so the type
|
|
264
|
+
* is intentionally broad — validation happens per-mount-site at runtime.
|
|
265
|
+
*/
|
|
266
|
+
export type HandlerUseItem =
|
|
267
|
+
| RouteUseItem
|
|
268
|
+
| LayoutUseItem
|
|
269
|
+
| ParallelUseItem
|
|
270
|
+
| InterceptUseItem;
|