@rangojs/router 0.0.0-experimental.68 → 0.0.0-experimental.6c70a2ab
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 +112 -17
- package/dist/vite/index.js +1456 -467
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +54 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +45 -0
- package/skills/layout/SKILL.md +24 -0
- package/skills/links/SKILL.md +234 -16
- package/skills/loader/SKILL.md +70 -3
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/parallel/SKILL.md +68 -0
- package/skills/rango/SKILL.md +26 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +48 -0
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +9 -1
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +80 -5
- package/src/browser/navigation-client.ts +64 -13
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +58 -12
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +70 -18
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +17 -4
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +8 -1
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/types.ts +19 -0
- package/src/build/route-trie.ts +52 -25
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +87 -175
- package/src/href-client.ts +4 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +40 -9
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +62 -36
- package/src/route-definition/dsl-helpers.ts +175 -23
- package/src/route-definition/helpers-types.ts +63 -14
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-types.ts +7 -0
- package/src/router/handler-context.ts +21 -38
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/cache-lookup.ts +2 -1
- package/src/router/match-result.ts +101 -4
- package/src/router/middleware-types.ts +14 -25
- package/src/router/middleware.ts +54 -7
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/revalidation.ts +15 -1
- package/src/router/segment-resolution/fresh.ts +13 -0
- package/src/router/segment-resolution/revalidation.ts +135 -101
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +16 -8
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +10 -0
- package/src/rsc/server-action.ts +4 -0
- package/src/rsc/types.ts +6 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +71 -70
- package/src/server/context.ts +26 -3
- package/src/server/request-context.ts +10 -42
- package/src/ssr/index.tsx +5 -1
- package/src/types/handler-context.ts +12 -39
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +30 -4
- package/src/urls/response-types.ts +2 -10
- package/src/use-loader.tsx +4 -1
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +31 -3
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +172 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +52 -28
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +540 -372
- package/src/vite/plugins/performance-tracks.ts +17 -9
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +49 -14
- package/src/vite/router-discovery.ts +558 -53
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
package/src/reverse.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtractParams } from "./types.js";
|
|
2
2
|
import type { SearchSchema, ResolveSearchSchema } from "./search-params.js";
|
|
3
3
|
import { serializeSearchParams } from "./search-params.js";
|
|
4
|
+
import { substitutePatternParams } from "./router/substitute-pattern-params.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Sanitize prefix string by removing leading slash
|
|
@@ -218,6 +219,64 @@ export type ExtractLocalRoutes<TPatterns> = TPatterns extends {
|
|
|
218
219
|
? TPatterns
|
|
219
220
|
: Record<string, string>;
|
|
220
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Params accepted by `useReverse(routes)`. The route's own params are
|
|
224
|
+
* required, and additional string keys are permitted so callers can
|
|
225
|
+
* override values that would otherwise be auto-filled from the matched
|
|
226
|
+
* route's `useParams()` (e.g. an enclosing `:tenantId` mount segment).
|
|
227
|
+
*/
|
|
228
|
+
export type LocalReverseParams<TPattern extends string> =
|
|
229
|
+
ExtractParams<TPattern> & {
|
|
230
|
+
readonly [extra: string]: string | undefined;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Type-safe local reverse function with dot-prefixed names only.
|
|
235
|
+
*
|
|
236
|
+
* Returned by `useReverse(routes)` on the client. The route map is the
|
|
237
|
+
* exposure boundary (a generated `routes` from a `urls()` module) and the
|
|
238
|
+
* scope is implicit from that import — there is no global namespace, so
|
|
239
|
+
* names must be dot-prefixed to mirror `ctx.reverse(".name")`.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* const reverse = useReverse(blogRoutes);
|
|
244
|
+
* reverse(".index"); // ✓ no params
|
|
245
|
+
* reverse(".post", { postId: "hello" }); // ✓ with params
|
|
246
|
+
* reverse(".search", {}, { q: "hi" }); // ✓ with search schema
|
|
247
|
+
* reverse(".typo"); // ✗ compile error
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
export type LocalReverseFunction<TLocalRoutes> = {
|
|
251
|
+
/**
|
|
252
|
+
* Dot-prefixed local route without params
|
|
253
|
+
*/
|
|
254
|
+
<TName extends keyof TLocalRoutes & string>(
|
|
255
|
+
name: IsEmptyObject<
|
|
256
|
+
ExtractParams<RoutePatternFor<TLocalRoutes, TName>>
|
|
257
|
+
> extends true
|
|
258
|
+
? `.${TName}`
|
|
259
|
+
: never,
|
|
260
|
+
): string;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Dot-prefixed local route with params
|
|
264
|
+
*/
|
|
265
|
+
<TName extends keyof TLocalRoutes & string>(
|
|
266
|
+
name: `.${TName}`,
|
|
267
|
+
params: LocalReverseParams<RoutePatternFor<TLocalRoutes, TName>>,
|
|
268
|
+
): string;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Dot-prefixed local route with params and search
|
|
272
|
+
*/
|
|
273
|
+
<TName extends keyof TLocalRoutes & string>(
|
|
274
|
+
name: `.${TName}`,
|
|
275
|
+
params: LocalReverseParams<RoutePatternFor<TLocalRoutes, TName>>,
|
|
276
|
+
search: ResolveSearchSchema<ExtractSearchSchema<TLocalRoutes, TName>>,
|
|
277
|
+
): string;
|
|
278
|
+
};
|
|
279
|
+
|
|
221
280
|
/**
|
|
222
281
|
* Extract the response data type for a named route from a UrlPatterns instance.
|
|
223
282
|
* Re-exported from urls.ts for consumer convenience.
|
|
@@ -301,42 +360,9 @@ export function createReverse<TRoutes extends Record<string, string>>(
|
|
|
301
360
|
throw new Error(`Unknown route: ${name}`);
|
|
302
361
|
}
|
|
303
362
|
|
|
304
|
-
let result =
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// Strip constraint syntax: :param(a|b) -> use "param" as key
|
|
308
|
-
// Optional params (:param?) are omitted when not provided
|
|
309
|
-
let hadOmittedOptional = false;
|
|
310
|
-
result = result.replace(
|
|
311
|
-
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
|
|
312
|
-
(_, key, _constraint, optional) => {
|
|
313
|
-
const value = params[key];
|
|
314
|
-
if (value === undefined) {
|
|
315
|
-
hadOmittedOptional = true;
|
|
316
|
-
return "";
|
|
317
|
-
}
|
|
318
|
-
return encodeURIComponent(value);
|
|
319
|
-
},
|
|
320
|
-
);
|
|
321
|
-
// Second pass: required params (no trailing ?)
|
|
322
|
-
result = result.replace(
|
|
323
|
-
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
|
|
324
|
-
(_, key) => {
|
|
325
|
-
const value = params[key];
|
|
326
|
-
if (value === undefined) {
|
|
327
|
-
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
328
|
-
}
|
|
329
|
-
return encodeURIComponent(value);
|
|
330
|
-
},
|
|
331
|
-
);
|
|
332
|
-
// Clean up slashes only when an optional param was actually omitted,
|
|
333
|
-
// so intentional trailing-slash patterns like "/blog/" are preserved.
|
|
334
|
-
if (hadOmittedOptional) {
|
|
335
|
-
const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
336
|
-
result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
337
|
-
if (hadTrailingSlash && !result.endsWith("/")) result += "/";
|
|
338
|
-
}
|
|
339
|
-
}
|
|
363
|
+
let result = params
|
|
364
|
+
? substitutePatternParams(pattern, params, name)
|
|
365
|
+
: pattern;
|
|
340
366
|
|
|
341
367
|
// Append search params as query string
|
|
342
368
|
if (search) {
|
|
@@ -55,6 +55,9 @@ const hasRoutesInItem = (item: AllUseItems): boolean => {
|
|
|
55
55
|
if (item.type === "layout" && item.uses) {
|
|
56
56
|
return item.uses.some((child) => hasRoutesInItem(child));
|
|
57
57
|
}
|
|
58
|
+
if (item.type === "middleware" && item.uses) {
|
|
59
|
+
return item.uses.some((child) => hasRoutesInItem(child));
|
|
60
|
+
}
|
|
58
61
|
return false;
|
|
59
62
|
};
|
|
60
63
|
|
|
@@ -301,6 +304,15 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
301
304
|
return { name: namespace, type: "cache" } as CacheItem;
|
|
302
305
|
}
|
|
303
306
|
|
|
307
|
+
// Inside a loader() use() callback, only the direct form — cache()/cache(opts)/
|
|
308
|
+
// cache("profile") — writes cache config to the loader entry. The wrapper
|
|
309
|
+
// form creates a structural cache boundary with its own children scope, which
|
|
310
|
+
// has no effect on the loader and would silently no-op.
|
|
311
|
+
invariant(
|
|
312
|
+
!(ctx.parent && (ctx.parent as any).type === "loader"),
|
|
313
|
+
"cache() wrapper form is not valid inside loader() use(). Use cache({...}) without children to configure the loader's cache.",
|
|
314
|
+
);
|
|
315
|
+
|
|
304
316
|
// With children: create a cache entry (like layout with caching semantics)
|
|
305
317
|
const namespace = `${ctx.namespace}.${cacheIndex}`;
|
|
306
318
|
const cacheShortCode = store.getShortCode("cache");
|
|
@@ -353,10 +365,37 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
353
365
|
return { name: namespace, type: "cache", uses: result } as CacheItem;
|
|
354
366
|
};
|
|
355
367
|
|
|
356
|
-
const middleware: RouteHelpers<any, any>["middleware"] = (...
|
|
368
|
+
const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
369
|
+
// Four call forms:
|
|
370
|
+
// middleware(fn) — single fn, sibling
|
|
371
|
+
// middleware(fn, () => [...]) — single fn, wrapping
|
|
372
|
+
// middleware([fn1, fn2]) — array, sibling
|
|
373
|
+
// middleware([fn1, fn2], () => [...]) — array, wrapping
|
|
374
|
+
const isArray = Array.isArray(args[0]);
|
|
375
|
+
|
|
376
|
+
// Reject the removed variadic form before executing anything.
|
|
377
|
+
// middleware(fn1, fn2, fn3) — 3+ args, always wrong.
|
|
378
|
+
// middleware(fn1, fn2) where fn2 is a middleware fn (length >= 1), not a
|
|
379
|
+
// children callback (length === 0) — legacy two-fn form, reject early.
|
|
380
|
+
if (
|
|
381
|
+
args.length > 2 ||
|
|
382
|
+
(!isArray &&
|
|
383
|
+
args.length === 2 &&
|
|
384
|
+
typeof args[1] === "function" &&
|
|
385
|
+
args[1].length > 0)
|
|
386
|
+
) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
"middleware() no longer accepts variadic arguments. " +
|
|
389
|
+
"Use middleware([fn1, fn2, ...]) instead of middleware(fn1, fn2, ...).",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const fns: MiddlewareFn<any>[] = isArray ? args[0] : [args[0]];
|
|
394
|
+
const children: (() => any[]) | undefined =
|
|
395
|
+
typeof args[1] === "function" ? args[1] : undefined;
|
|
396
|
+
|
|
357
397
|
// Prevent "use cache" functions from being used as middleware.
|
|
358
|
-
|
|
359
|
-
for (const f of fn) {
|
|
398
|
+
for (const f of fns) {
|
|
360
399
|
if (isCachedFunction(f)) {
|
|
361
400
|
throw new Error(
|
|
362
401
|
`A "use cache" function cannot be used as middleware. ` +
|
|
@@ -367,17 +406,80 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...fn) => {
|
|
|
367
406
|
}
|
|
368
407
|
}
|
|
369
408
|
|
|
370
|
-
const
|
|
409
|
+
const store = getContext();
|
|
410
|
+
const ctx = store.getStore();
|
|
371
411
|
if (!ctx) throw new Error("middleware() must be called inside map()");
|
|
372
412
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
413
|
+
if (!children) {
|
|
414
|
+
// Sibling mode: attach to parent entry
|
|
415
|
+
const parent = ctx.parent;
|
|
416
|
+
if (!parent || !("middleware" in parent)) {
|
|
417
|
+
invariant(false, "No parent entry available for middleware()");
|
|
418
|
+
}
|
|
419
|
+
const name = `$${store.getNextIndex("middleware")}`;
|
|
420
|
+
parent.middleware.push(...fns);
|
|
421
|
+
return { name, type: "middleware" } as MiddlewareItem;
|
|
377
422
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
423
|
+
|
|
424
|
+
// Wrapping mode: create a transparent layout that carries the middleware
|
|
425
|
+
const mwIndex = store.getNextIndex("middleware");
|
|
426
|
+
const namespace = `${ctx.namespace}.${mwIndex}`;
|
|
427
|
+
|
|
428
|
+
const urlPrefix = getUrlPrefix();
|
|
429
|
+
const entry = {
|
|
430
|
+
id: namespace,
|
|
431
|
+
shortCode: store.getShortCode("layout"),
|
|
432
|
+
type: "layout",
|
|
433
|
+
parent: ctx.parent,
|
|
434
|
+
handler: RootLayout,
|
|
435
|
+
loading: undefined,
|
|
436
|
+
middleware: [...fns],
|
|
437
|
+
revalidate: [],
|
|
438
|
+
errorBoundary: [],
|
|
439
|
+
notFoundBoundary: [],
|
|
440
|
+
layout: [],
|
|
441
|
+
parallel: {},
|
|
442
|
+
intercept: [],
|
|
443
|
+
loader: [],
|
|
444
|
+
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
445
|
+
} as EntryData;
|
|
446
|
+
|
|
447
|
+
// Run children callback. If the second arg was actually a middleware fn
|
|
448
|
+
// (old variadic form: middleware(mw1, mw2)), this will return a non-array
|
|
449
|
+
// and the invariant below gives a clear migration error.
|
|
450
|
+
const rawResult = store.run(namespace, entry, children);
|
|
451
|
+
|
|
452
|
+
invariant(
|
|
453
|
+
Array.isArray(rawResult),
|
|
454
|
+
"middleware(fn, children) expects the second argument to return an array of use items. " +
|
|
455
|
+
"To pass multiple middleware, use middleware([fn1, fn2]).",
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const result = rawResult.flat(3);
|
|
459
|
+
|
|
460
|
+
invariant(
|
|
461
|
+
result.every((item: any) => isValidUseItem(item)),
|
|
462
|
+
`middleware() children callback must return an array of use items [${namespace}]`,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const hasRoutes =
|
|
466
|
+
result &&
|
|
467
|
+
Array.isArray(result) &&
|
|
468
|
+
result.some((item) => item != null && hasRoutesInItem(item));
|
|
469
|
+
|
|
470
|
+
if (!hasRoutes) {
|
|
471
|
+
const parent = ctx.parent;
|
|
472
|
+
if (parent && "layout" in parent) {
|
|
473
|
+
entry.parent = null;
|
|
474
|
+
parent.layout.push(entry);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
name: namespace,
|
|
480
|
+
type: "middleware",
|
|
481
|
+
uses: result,
|
|
482
|
+
} as MiddlewareItem;
|
|
381
483
|
};
|
|
382
484
|
|
|
383
485
|
const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
@@ -398,13 +500,25 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
398
500
|
|
|
399
501
|
const namespace = `${ctx.namespace}.$${store.getNextIndex("parallel")}`;
|
|
400
502
|
|
|
401
|
-
// Unwrap
|
|
503
|
+
// Unwrap slot values. A slot value can be:
|
|
504
|
+
// - a Handler / ReactNode (legacy form)
|
|
505
|
+
// - a Static() definition (build-time only)
|
|
506
|
+
// - a slot descriptor `{ handler, use? }` for slot-local overrides
|
|
507
|
+
// The descriptor's `use` runs after the broadcast `use` for that slot,
|
|
508
|
+
// so single-assignment items like `loading()` placed there win without
|
|
509
|
+
// affecting siblings.
|
|
402
510
|
const unwrappedSlots: Record<string, any> = {};
|
|
511
|
+
const slotLocalUses: Record<string, (() => any[]) | undefined> = {};
|
|
403
512
|
let hasStaticSlot = false;
|
|
404
513
|
const staticSlotIds: Record<string, string> = {};
|
|
405
|
-
for (const [slotName,
|
|
514
|
+
for (const [slotName, rawSlot] of Object.entries(
|
|
406
515
|
slots as Record<string, any>,
|
|
407
516
|
)) {
|
|
517
|
+
let slotHandler: any = rawSlot;
|
|
518
|
+
if (isSlotDescriptor(rawSlot)) {
|
|
519
|
+
slotHandler = rawSlot.handler;
|
|
520
|
+
slotLocalUses[slotName] = rawSlot.use;
|
|
521
|
+
}
|
|
408
522
|
if (isStaticHandler(slotHandler)) {
|
|
409
523
|
hasStaticSlot = true;
|
|
410
524
|
unwrappedSlots[slotName] = slotHandler.handler;
|
|
@@ -471,13 +585,25 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
471
585
|
}),
|
|
472
586
|
} satisfies EntryData;
|
|
473
587
|
|
|
474
|
-
// Per-slot
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
588
|
+
// Per-slot merge order (narrowest-scope-wins for single-assignment items
|
|
589
|
+
// like loading()):
|
|
590
|
+
// 1. handler.use — defaults baked into the handler
|
|
591
|
+
// 2. shared `use` — broadcast at the parallel() call site
|
|
592
|
+
// 3. slot-local `use` — per-slot override via `{ handler, use }` descriptor
|
|
593
|
+
// Items that accumulate (loader, middleware, revalidate, …) compose
|
|
594
|
+
// across all three layers regardless of order.
|
|
595
|
+
const rawSlot = (slots as Record<string, any>)[slotName];
|
|
596
|
+
const slotHandlerForUse = isSlotDescriptor(rawSlot)
|
|
597
|
+
? rawSlot.handler
|
|
598
|
+
: rawSlot;
|
|
599
|
+
const slotHandlerUse = resolveHandlerUse(slotHandlerForUse);
|
|
600
|
+
const slotLocalUse = slotLocalUses[slotName];
|
|
601
|
+
const explicitUse = combineExplicitUses(use, slotLocalUse);
|
|
602
|
+
const slotMergedUse = mergeHandlerUse(
|
|
603
|
+
slotHandlerUse,
|
|
604
|
+
explicitUse,
|
|
605
|
+
"parallel",
|
|
606
|
+
);
|
|
481
607
|
if (slotMergedUse) {
|
|
482
608
|
const result = store.run(namespace, slotEntry, slotMergedUse)?.flat(3);
|
|
483
609
|
invariant(
|
|
@@ -491,6 +617,28 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
491
617
|
return { name: namespace, type: "parallel" } as ParallelItem;
|
|
492
618
|
};
|
|
493
619
|
|
|
620
|
+
function isSlotDescriptor(
|
|
621
|
+
value: unknown,
|
|
622
|
+
): value is { handler: unknown; use?: () => any[] } {
|
|
623
|
+
return (
|
|
624
|
+
typeof value === "object" &&
|
|
625
|
+
value !== null &&
|
|
626
|
+
!("__brand" in value) &&
|
|
627
|
+
"handler" in value &&
|
|
628
|
+
typeof (value as any).handler !== "undefined"
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function combineExplicitUses(
|
|
633
|
+
sharedUse: (() => any[]) | undefined,
|
|
634
|
+
slotLocalUse: (() => any[]) | undefined,
|
|
635
|
+
): (() => any[]) | undefined {
|
|
636
|
+
if (!sharedUse && !slotLocalUse) return undefined;
|
|
637
|
+
if (!slotLocalUse) return sharedUse;
|
|
638
|
+
if (!sharedUse) return slotLocalUse;
|
|
639
|
+
return () => [...sharedUse(), ...slotLocalUse()];
|
|
640
|
+
}
|
|
641
|
+
|
|
494
642
|
/**
|
|
495
643
|
* Intercept helper - defines an intercepting route for soft navigation
|
|
496
644
|
*/
|
|
@@ -611,8 +759,12 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
611
759
|
revalidate: [] as ShouldRevalidateFn<any, any>[],
|
|
612
760
|
};
|
|
613
761
|
|
|
614
|
-
//
|
|
615
|
-
|
|
762
|
+
// Merge handler.use defaults (attached to the loader definition) with explicit use
|
|
763
|
+
const handlerUseFn = resolveHandlerUse(loaderDef);
|
|
764
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "loader");
|
|
765
|
+
|
|
766
|
+
// If any use callback is in effect, run it to collect revalidation rules and cache config
|
|
767
|
+
if (mergedUse) {
|
|
616
768
|
// Temporarily set context for revalidate()/cache() calls to target this loader
|
|
617
769
|
const originalParent = ctx.parent;
|
|
618
770
|
// Create a temporary "parent" with type "loader" so cache() can detect it.
|
|
@@ -625,7 +777,7 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
625
777
|
};
|
|
626
778
|
ctx.parent = tempParent as EntryData;
|
|
627
779
|
|
|
628
|
-
const result =
|
|
780
|
+
const result = mergedUse()?.flat(3);
|
|
629
781
|
|
|
630
782
|
// Copy cache config only if cache() was called during the use() callback.
|
|
631
783
|
// The spread from originalParent may carry an inherited .cache from
|
|
@@ -123,7 +123,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
123
123
|
* "@main": async (ctx) => <MainContent data={ctx.use(DataLoader)} />,
|
|
124
124
|
* })
|
|
125
125
|
*
|
|
126
|
-
* // With loaders and loading states
|
|
126
|
+
* // With loaders and loading states (broadcast to every slot)
|
|
127
127
|
* parallel({
|
|
128
128
|
* "@analytics": AnalyticsPanel,
|
|
129
129
|
* "@metrics": MetricsPanel,
|
|
@@ -131,12 +131,36 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
131
131
|
* loader(DashboardLoader),
|
|
132
132
|
* loading(<DashboardSkeleton />),
|
|
133
133
|
* ])
|
|
134
|
+
*
|
|
135
|
+
* // Per-slot scoped use via slot descriptor — for single-assignment items
|
|
136
|
+
* // like loading() that should not broadcast to siblings.
|
|
137
|
+
* parallel({
|
|
138
|
+
* "@meta": MetaSlot,
|
|
139
|
+
* "@sidebar": {
|
|
140
|
+
* handler: SidebarSlot,
|
|
141
|
+
* use: () => [loading(<SidebarSkeleton />)],
|
|
142
|
+
* },
|
|
143
|
+
* })
|
|
134
144
|
* ```
|
|
135
145
|
* @param slots - Object with slot names (prefixed with @) mapped to handlers
|
|
146
|
+
* or `{ handler, use? }` slot descriptors.
|
|
136
147
|
* @param use - Optional callback for loaders, loading, revalidate, etc.
|
|
148
|
+
* Items here apply to every slot in the call (broadcast).
|
|
149
|
+
* For per-slot single-assignment items, use the slot descriptor's
|
|
150
|
+
* own `use` callback — slot-local items run after the broadcast,
|
|
151
|
+
* so they take precedence on `loading()` and other last-write-wins
|
|
152
|
+
* fields.
|
|
137
153
|
*/
|
|
138
154
|
parallel: <
|
|
139
|
-
TSlots extends Record
|
|
155
|
+
TSlots extends Record<
|
|
156
|
+
`@${string}`,
|
|
157
|
+
| Handler<any, any, TEnv>
|
|
158
|
+
| ReactNode
|
|
159
|
+
| {
|
|
160
|
+
handler: Handler<any, any, TEnv> | ReactNode;
|
|
161
|
+
use?: () => UseItems<ParallelUseItem>;
|
|
162
|
+
}
|
|
163
|
+
>,
|
|
140
164
|
>(
|
|
141
165
|
slots: TSlots,
|
|
142
166
|
use?: () => UseItems<ParallelUseItem>,
|
|
@@ -182,21 +206,41 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
182
206
|
): InterceptItem;
|
|
183
207
|
};
|
|
184
208
|
/**
|
|
185
|
-
* Attach middleware to the current route/layout
|
|
209
|
+
* Attach middleware to the current route/layout, or wrap child segments
|
|
210
|
+
*
|
|
211
|
+
* **Sibling mode** — attaches middleware to the parent entry:
|
|
186
212
|
* ```typescript
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
213
|
+
* layout(<DashboardShell />, () => [
|
|
214
|
+
* middleware(authMiddleware),
|
|
215
|
+
* middleware([authMiddleware, loggingMiddleware]),
|
|
216
|
+
* path("/", DashboardPage),
|
|
217
|
+
* ])
|
|
218
|
+
* ```
|
|
193
219
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
220
|
+
* **Wrapping mode** — scopes middleware to the children only:
|
|
221
|
+
* ```typescript
|
|
222
|
+
* middleware(authMiddleware, () => [
|
|
223
|
+
* path("/dashboard", DashboardPage),
|
|
224
|
+
* path("/settings", SettingsPage),
|
|
225
|
+
* ])
|
|
226
|
+
*
|
|
227
|
+
* middleware([authMiddleware, loggingMiddleware], () => [
|
|
228
|
+
* path("/admin", AdminPage),
|
|
229
|
+
* ])
|
|
196
230
|
* ```
|
|
197
|
-
* @param fns - One or more middleware functions to execute in order
|
|
198
231
|
*/
|
|
199
|
-
middleware:
|
|
232
|
+
middleware: {
|
|
233
|
+
(fn: MiddlewareFn<TEnv>): MiddlewareItem;
|
|
234
|
+
(
|
|
235
|
+
fn: MiddlewareFn<TEnv>,
|
|
236
|
+
children: () => UseItems<LayoutUseItem>,
|
|
237
|
+
): MiddlewareItem;
|
|
238
|
+
(fns: MiddlewareFn<TEnv>[]): MiddlewareItem;
|
|
239
|
+
(
|
|
240
|
+
fns: MiddlewareFn<TEnv>[],
|
|
241
|
+
children: () => UseItems<LayoutUseItem>,
|
|
242
|
+
): MiddlewareItem;
|
|
243
|
+
};
|
|
200
244
|
/**
|
|
201
245
|
* Control when a segment should revalidate during navigation
|
|
202
246
|
* ```typescript
|
|
@@ -215,7 +259,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
215
259
|
* ({ defaultShouldRevalidate: true })
|
|
216
260
|
* )
|
|
217
261
|
* ```
|
|
218
|
-
* @param fn - Function
|
|
262
|
+
* @param fn - Function returning either:
|
|
263
|
+
* - `boolean` (hard decision — short-circuits the chain),
|
|
264
|
+
* - `{ defaultShouldRevalidate: boolean }` (soft — updates the suggestion
|
|
265
|
+
* for downstream revalidators),
|
|
266
|
+
* - or nothing / `null` / `undefined` (defer — leaves the suggestion
|
|
267
|
+
* unchanged and continues to the next revalidator).
|
|
219
268
|
*/
|
|
220
269
|
revalidate: (fn: ShouldRevalidateFn<any, TEnv>) => RevalidateItem;
|
|
221
270
|
/**
|
|
@@ -21,6 +21,10 @@ export function resolveHandlerUse(handler: unknown): (() => any[]) | undefined {
|
|
|
21
21
|
if (isStaticHandler(handler)) {
|
|
22
22
|
return (handler as any).use;
|
|
23
23
|
}
|
|
24
|
+
// Loader definitions from createLoader() — branded objects with optional .use
|
|
25
|
+
if (typeof handler === "object" && (handler as any).__brand === "loader") {
|
|
26
|
+
return (handler as any).use;
|
|
27
|
+
}
|
|
24
28
|
// Plain handler function
|
|
25
29
|
if (typeof handler === "function") {
|
|
26
30
|
return (handler as any).use;
|
|
@@ -99,6 +103,8 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
|
|
|
99
103
|
"when",
|
|
100
104
|
"transition",
|
|
101
105
|
]),
|
|
106
|
+
// LoaderUseItem — only revalidate + cache can attach to a loader entry
|
|
107
|
+
loader: new Set(["revalidate", "cache"]),
|
|
102
108
|
};
|
|
103
109
|
|
|
104
110
|
/**
|
package/src/route-types.ts
CHANGED
|
@@ -176,6 +176,13 @@ export type IncludeItem = {
|
|
|
176
176
|
>;
|
|
177
177
|
/** Root scope flag for dot-local reverse resolution */
|
|
178
178
|
rootScoped?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Positional include scope token composed from the parent scope plus this
|
|
181
|
+
* include's sibling index (`${parentScope}I${idx}`). Applied to direct-
|
|
182
|
+
* descendant shortCodes during lazy evaluation so routes inside the
|
|
183
|
+
* include cannot collide with siblings declared outside it.
|
|
184
|
+
*/
|
|
185
|
+
includeScope?: string;
|
|
179
186
|
};
|
|
180
187
|
[IncludeBrand]: void;
|
|
181
188
|
};
|
|
@@ -18,6 +18,8 @@ import { isInsideCacheScope } from "../server/context.js";
|
|
|
18
18
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
19
19
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
20
20
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
21
|
+
import { substitutePatternParams } from "./substitute-pattern-params.js";
|
|
22
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Strip internal _rsc* query params from a URL.
|
|
@@ -158,48 +160,14 @@ export function createReverseFunction(
|
|
|
158
160
|
);
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
let result = pattern;
|
|
162
|
-
|
|
163
163
|
// Merge current request params as defaults, explicit params override
|
|
164
164
|
const effectiveParams = currentParams
|
|
165
165
|
? { ...currentParams, ...hrefParams }
|
|
166
166
|
: hrefParams;
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let hadOmittedOptional = false;
|
|
172
|
-
// First pass: optional params (trailing ?)
|
|
173
|
-
result = result.replace(
|
|
174
|
-
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
|
|
175
|
-
(_, key) => {
|
|
176
|
-
const value = effectiveParams[key];
|
|
177
|
-
if (value === undefined) {
|
|
178
|
-
hadOmittedOptional = true;
|
|
179
|
-
return "";
|
|
180
|
-
}
|
|
181
|
-
return encodeURIComponent(value);
|
|
182
|
-
},
|
|
183
|
-
);
|
|
184
|
-
// Second pass: required params (no trailing ?)
|
|
185
|
-
result = result.replace(
|
|
186
|
-
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
|
|
187
|
-
(_, key) => {
|
|
188
|
-
const value = effectiveParams[key];
|
|
189
|
-
if (value === undefined) {
|
|
190
|
-
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
191
|
-
}
|
|
192
|
-
return encodeURIComponent(value);
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
// Clean up slashes only when an optional param was actually omitted,
|
|
196
|
-
// so intentional trailing-slash patterns like "/blog/" are preserved.
|
|
197
|
-
if (hadOmittedOptional) {
|
|
198
|
-
const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
199
|
-
result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
200
|
-
if (hadTrailingSlash && !result.endsWith("/")) result += "/";
|
|
201
|
-
}
|
|
202
|
-
}
|
|
168
|
+
let result = effectiveParams
|
|
169
|
+
? substitutePatternParams(pattern, effectiveParams, name)
|
|
170
|
+
: pattern;
|
|
203
171
|
|
|
204
172
|
// Append search params as query string
|
|
205
173
|
if (search) {
|
|
@@ -278,8 +246,12 @@ export function createHandlerContext<TEnv>(
|
|
|
278
246
|
search: searchSchema ? resolvedSearchParams : {},
|
|
279
247
|
pathname,
|
|
280
248
|
url,
|
|
281
|
-
originalUrl: new URL(request.url),
|
|
249
|
+
originalUrl: requestContext?.originalUrl ?? new URL(request.url),
|
|
282
250
|
env: bindings,
|
|
251
|
+
waitUntil: requestContext
|
|
252
|
+
? requestContext.waitUntil.bind(requestContext)
|
|
253
|
+
: fireAndForgetWaitUntil,
|
|
254
|
+
executionContext: requestContext?.executionContext,
|
|
283
255
|
_variables: variables,
|
|
284
256
|
get: ((keyOrVar: any) => {
|
|
285
257
|
// Read-time guard: non-cacheable var inside cache() → throw.
|
|
@@ -384,6 +356,12 @@ export function createPrerenderContext<TEnv>(
|
|
|
384
356
|
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
385
357
|
);
|
|
386
358
|
},
|
|
359
|
+
// Build-time prerender has no live request. waitUntil is a true no-op
|
|
360
|
+
// (running fn() here would fire side effects during build, which is
|
|
361
|
+
// incorrect — these are meant to outlive the live response).
|
|
362
|
+
// executionContext is absent for the same reason.
|
|
363
|
+
waitUntil: () => {},
|
|
364
|
+
executionContext: undefined,
|
|
387
365
|
_variables: variables,
|
|
388
366
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
389
367
|
set: ((keyOrVar: any, value: any) => {
|
|
@@ -473,6 +451,11 @@ export function createStaticContext<TEnv>(
|
|
|
473
451
|
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
474
452
|
);
|
|
475
453
|
},
|
|
454
|
+
// Static() handlers have no live request. waitUntil is a true no-op
|
|
455
|
+
// (running fn() here would fire side effects during build, which is
|
|
456
|
+
// incorrect). executionContext is absent for the same reason.
|
|
457
|
+
waitUntil: () => {},
|
|
458
|
+
executionContext: undefined,
|
|
476
459
|
_variables: variables,
|
|
477
460
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
478
461
|
set: ((keyOrVar: any, value: any) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registerRouteMap } from "../route-map-builder.js";
|
|
2
2
|
import { extractStaticPrefix } from "./pattern-matching.js";
|
|
3
3
|
import {
|
|
4
|
-
EntryData,
|
|
4
|
+
type EntryData,
|
|
5
5
|
RSCRouterContext,
|
|
6
6
|
runWithPrefixes,
|
|
7
7
|
getIsolatedLazyParent,
|
|
@@ -125,9 +125,8 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
125
125
|
// Merge captured counters from include() to maintain consistent
|
|
126
126
|
// shortCode indices with sibling entries from pattern extraction
|
|
127
127
|
const lazyCounters: Record<string, number> = {};
|
|
128
|
-
if (lazyContext
|
|
129
|
-
const
|
|
130
|
-
for (const [key, value] of Object.entries(captured)) {
|
|
128
|
+
if (lazyContext?.counters) {
|
|
129
|
+
for (const [key, value] of Object.entries(lazyContext.counters)) {
|
|
131
130
|
lazyCounters[key] = value;
|
|
132
131
|
}
|
|
133
132
|
}
|
|
@@ -141,8 +140,9 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
141
140
|
namespace: "lazy",
|
|
142
141
|
parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
|
|
143
142
|
counters: lazyCounters,
|
|
144
|
-
cacheProfiles:
|
|
145
|
-
rootScoped:
|
|
143
|
+
cacheProfiles: lazyContext?.cacheProfiles,
|
|
144
|
+
rootScoped: lazyContext?.rootScoped,
|
|
145
|
+
includeScope: lazyContext?.includeScope,
|
|
146
146
|
},
|
|
147
147
|
() => {
|
|
148
148
|
// Run the lazy patterns handler with the original context prefixes
|