@rangojs/router 0.0.0-experimental.29 → 0.0.0-experimental.2a0dea97
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/AGENTS.md +4 -0
- package/README.md +78 -19
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +853 -435
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +22 -4
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +71 -21
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +24 -22
- package/skills/route/SKILL.md +56 -2
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +33 -21
- package/src/__internal.ts +92 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +125 -16
- package/src/browser/navigation-client.ts +142 -57
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +94 -17
- package/src/browser/prefetch/cache.ts +82 -12
- package/src/browser/prefetch/fetch.ts +98 -27
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +88 -9
- package/src/browser/react/NavigationProvider.tsx +40 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +134 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +72 -10
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +55 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +50 -24
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -0
- package/src/client.tsx +6 -66
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/index.rsc.ts +6 -36
- package/src/index.ts +50 -43
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +25 -1
- package/src/route-definition/dsl-helpers.ts +224 -37
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +111 -25
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +156 -21
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +125 -190
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +104 -10
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +16 -22
- package/src/router/middleware.ts +24 -30
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +198 -20
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +438 -300
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/types.ts +1 -0
- package/src/router.ts +59 -6
- package/src/rsc/handler.ts +472 -372
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +14 -2
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/server-action.ts +8 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +9 -1
- package/src/segment-content-promise.ts +33 -0
- package/src/segment-system.tsx +164 -23
- package/src/server/context.ts +140 -14
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -28
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +149 -49
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +6 -0
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +128 -74
- package/src/vite/discovery/state.ts +13 -6
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +163 -211
- package/src/vite/router-discovery.ts +178 -45
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +37 -5
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -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).
|
|
@@ -54,6 +55,9 @@ const hasRoutesInItem = (item: AllUseItems): boolean => {
|
|
|
54
55
|
if (item.type === "layout" && item.uses) {
|
|
55
56
|
return item.uses.some((child) => hasRoutesInItem(child));
|
|
56
57
|
}
|
|
58
|
+
if (item.type === "middleware" && item.uses) {
|
|
59
|
+
return item.uses.some((child) => hasRoutesInItem(child));
|
|
60
|
+
}
|
|
57
61
|
return false;
|
|
58
62
|
};
|
|
59
63
|
|
|
@@ -282,7 +286,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
282
286
|
errorBoundary: [],
|
|
283
287
|
notFoundBoundary: [],
|
|
284
288
|
layout: [],
|
|
285
|
-
parallel:
|
|
289
|
+
parallel: {},
|
|
286
290
|
intercept: [],
|
|
287
291
|
loader: [],
|
|
288
292
|
...(cacheUrlPrefix ? { mountPath: cacheUrlPrefix } : {}),
|
|
@@ -320,7 +324,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
320
324
|
errorBoundary: [],
|
|
321
325
|
notFoundBoundary: [],
|
|
322
326
|
layout: [],
|
|
323
|
-
parallel:
|
|
327
|
+
parallel: {},
|
|
324
328
|
intercept: [],
|
|
325
329
|
loader: [],
|
|
326
330
|
...(cacheUrlPrefix2 ? { mountPath: cacheUrlPrefix2 } : {}),
|
|
@@ -352,10 +356,37 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
352
356
|
return { name: namespace, type: "cache", uses: result } as CacheItem;
|
|
353
357
|
};
|
|
354
358
|
|
|
355
|
-
const middleware: RouteHelpers<any, any>["middleware"] = (...
|
|
359
|
+
const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
360
|
+
// Four call forms:
|
|
361
|
+
// middleware(fn) — single fn, sibling
|
|
362
|
+
// middleware(fn, () => [...]) — single fn, wrapping
|
|
363
|
+
// middleware([fn1, fn2]) — array, sibling
|
|
364
|
+
// middleware([fn1, fn2], () => [...]) — array, wrapping
|
|
365
|
+
const isArray = Array.isArray(args[0]);
|
|
366
|
+
|
|
367
|
+
// Reject the removed variadic form before executing anything.
|
|
368
|
+
// middleware(fn1, fn2, fn3) — 3+ args, always wrong.
|
|
369
|
+
// middleware(fn1, fn2) where fn2 is a middleware fn (length >= 1), not a
|
|
370
|
+
// children callback (length === 0) — legacy two-fn form, reject early.
|
|
371
|
+
if (
|
|
372
|
+
args.length > 2 ||
|
|
373
|
+
(!isArray &&
|
|
374
|
+
args.length === 2 &&
|
|
375
|
+
typeof args[1] === "function" &&
|
|
376
|
+
args[1].length > 0)
|
|
377
|
+
) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
"middleware() no longer accepts variadic arguments. " +
|
|
380
|
+
"Use middleware([fn1, fn2, ...]) instead of middleware(fn1, fn2, ...).",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const fns: MiddlewareFn<any>[] = isArray ? args[0] : [args[0]];
|
|
385
|
+
const children: (() => any[]) | undefined =
|
|
386
|
+
typeof args[1] === "function" ? args[1] : undefined;
|
|
387
|
+
|
|
356
388
|
// Prevent "use cache" functions from being used as middleware.
|
|
357
|
-
|
|
358
|
-
for (const f of fn) {
|
|
389
|
+
for (const f of fns) {
|
|
359
390
|
if (isCachedFunction(f)) {
|
|
360
391
|
throw new Error(
|
|
361
392
|
`A "use cache" function cannot be used as middleware. ` +
|
|
@@ -366,17 +397,80 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...fn) => {
|
|
|
366
397
|
}
|
|
367
398
|
}
|
|
368
399
|
|
|
369
|
-
const
|
|
400
|
+
const store = getContext();
|
|
401
|
+
const ctx = store.getStore();
|
|
370
402
|
if (!ctx) throw new Error("middleware() must be called inside map()");
|
|
371
403
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
404
|
+
if (!children) {
|
|
405
|
+
// Sibling mode: attach to parent entry
|
|
406
|
+
const parent = ctx.parent;
|
|
407
|
+
if (!parent || !("middleware" in parent)) {
|
|
408
|
+
invariant(false, "No parent entry available for middleware()");
|
|
409
|
+
}
|
|
410
|
+
const name = `$${store.getNextIndex("middleware")}`;
|
|
411
|
+
parent.middleware.push(...fns);
|
|
412
|
+
return { name, type: "middleware" } as MiddlewareItem;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Wrapping mode: create a transparent layout that carries the middleware
|
|
416
|
+
const mwIndex = store.getNextIndex("middleware");
|
|
417
|
+
const namespace = `${ctx.namespace}.${mwIndex}`;
|
|
418
|
+
|
|
419
|
+
const urlPrefix = getUrlPrefix();
|
|
420
|
+
const entry = {
|
|
421
|
+
id: namespace,
|
|
422
|
+
shortCode: store.getShortCode("layout"),
|
|
423
|
+
type: "layout",
|
|
424
|
+
parent: ctx.parent,
|
|
425
|
+
handler: RootLayout,
|
|
426
|
+
loading: undefined,
|
|
427
|
+
middleware: [...fns],
|
|
428
|
+
revalidate: [],
|
|
429
|
+
errorBoundary: [],
|
|
430
|
+
notFoundBoundary: [],
|
|
431
|
+
layout: [],
|
|
432
|
+
parallel: {},
|
|
433
|
+
intercept: [],
|
|
434
|
+
loader: [],
|
|
435
|
+
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
436
|
+
} as EntryData;
|
|
437
|
+
|
|
438
|
+
// Run children callback. If the second arg was actually a middleware fn
|
|
439
|
+
// (old variadic form: middleware(mw1, mw2)), this will return a non-array
|
|
440
|
+
// and the invariant below gives a clear migration error.
|
|
441
|
+
const rawResult = store.run(namespace, entry, children);
|
|
442
|
+
|
|
443
|
+
invariant(
|
|
444
|
+
Array.isArray(rawResult),
|
|
445
|
+
"middleware(fn, children) expects the second argument to return an array of use items. " +
|
|
446
|
+
"To pass multiple middleware, use middleware([fn1, fn2]).",
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const result = rawResult.flat(3);
|
|
450
|
+
|
|
451
|
+
invariant(
|
|
452
|
+
result.every((item: any) => isValidUseItem(item)),
|
|
453
|
+
`middleware() children callback must return an array of use items [${namespace}]`,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const hasRoutes =
|
|
457
|
+
result &&
|
|
458
|
+
Array.isArray(result) &&
|
|
459
|
+
result.some((item) => item != null && hasRoutesInItem(item));
|
|
460
|
+
|
|
461
|
+
if (!hasRoutes) {
|
|
462
|
+
const parent = ctx.parent;
|
|
463
|
+
if (parent && "layout" in parent) {
|
|
464
|
+
entry.parent = null;
|
|
465
|
+
parent.layout.push(entry);
|
|
466
|
+
}
|
|
376
467
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
name: namespace,
|
|
471
|
+
type: "middleware",
|
|
472
|
+
uses: result,
|
|
473
|
+
} as MiddlewareItem;
|
|
380
474
|
};
|
|
381
475
|
|
|
382
476
|
const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
@@ -393,15 +487,29 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
393
487
|
"parallel() cannot be nested inside another parallel()",
|
|
394
488
|
);
|
|
395
489
|
|
|
490
|
+
const slotNames = Object.keys(slots as Record<string, any>) as `@${string}`[];
|
|
491
|
+
|
|
396
492
|
const namespace = `${ctx.namespace}.$${store.getNextIndex("parallel")}`;
|
|
397
493
|
|
|
398
|
-
// Unwrap
|
|
494
|
+
// Unwrap slot values. A slot value can be:
|
|
495
|
+
// - a Handler / ReactNode (legacy form)
|
|
496
|
+
// - a Static() definition (build-time only)
|
|
497
|
+
// - a slot descriptor `{ handler, use? }` for slot-local overrides
|
|
498
|
+
// The descriptor's `use` runs after the broadcast `use` for that slot,
|
|
499
|
+
// so single-assignment items like `loading()` placed there win without
|
|
500
|
+
// affecting siblings.
|
|
399
501
|
const unwrappedSlots: Record<string, any> = {};
|
|
502
|
+
const slotLocalUses: Record<string, (() => any[]) | undefined> = {};
|
|
400
503
|
let hasStaticSlot = false;
|
|
401
504
|
const staticSlotIds: Record<string, string> = {};
|
|
402
|
-
for (const [slotName,
|
|
505
|
+
for (const [slotName, rawSlot] of Object.entries(
|
|
403
506
|
slots as Record<string, any>,
|
|
404
507
|
)) {
|
|
508
|
+
let slotHandler: any = rawSlot;
|
|
509
|
+
if (isSlotDescriptor(rawSlot)) {
|
|
510
|
+
slotHandler = rawSlot.handler;
|
|
511
|
+
slotLocalUses[slotName] = rawSlot.use;
|
|
512
|
+
}
|
|
405
513
|
if (isStaticHandler(slotHandler)) {
|
|
406
514
|
hasStaticSlot = true;
|
|
407
515
|
unwrappedSlots[slotName] = slotHandler.handler;
|
|
@@ -431,7 +539,7 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
431
539
|
errorBoundary: [],
|
|
432
540
|
notFoundBoundary: [],
|
|
433
541
|
layout: [],
|
|
434
|
-
parallel:
|
|
542
|
+
parallel: {},
|
|
435
543
|
intercept: [],
|
|
436
544
|
loader: [],
|
|
437
545
|
...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
|
|
@@ -445,19 +553,83 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
445
553
|
: {}),
|
|
446
554
|
} satisfies EntryData;
|
|
447
555
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
556
|
+
for (const slotName of slotNames) {
|
|
557
|
+
const slotEntry = {
|
|
558
|
+
...entry,
|
|
559
|
+
handler: { [slotName]: unwrappedSlots[slotName]! },
|
|
560
|
+
middleware: [...entry.middleware],
|
|
561
|
+
revalidate: [...entry.revalidate],
|
|
562
|
+
errorBoundary: [...entry.errorBoundary],
|
|
563
|
+
notFoundBoundary: [...entry.notFoundBoundary],
|
|
564
|
+
layout: [...entry.layout],
|
|
565
|
+
parallel: { ...entry.parallel },
|
|
566
|
+
intercept: [...entry.intercept],
|
|
567
|
+
loader: [...entry.loader],
|
|
568
|
+
...(entry.staticHandlerIds?.[slotName]
|
|
569
|
+
? {
|
|
570
|
+
isStaticPrerender: true as const,
|
|
571
|
+
staticHandlerIds: { [slotName]: entry.staticHandlerIds[slotName]! },
|
|
572
|
+
}
|
|
573
|
+
: {
|
|
574
|
+
isStaticPrerender: undefined,
|
|
575
|
+
staticHandlerIds: undefined,
|
|
576
|
+
}),
|
|
577
|
+
} satisfies EntryData;
|
|
578
|
+
|
|
579
|
+
// Per-slot merge order (narrowest-scope-wins for single-assignment items
|
|
580
|
+
// like loading()):
|
|
581
|
+
// 1. handler.use — defaults baked into the handler
|
|
582
|
+
// 2. shared `use` — broadcast at the parallel() call site
|
|
583
|
+
// 3. slot-local `use` — per-slot override via `{ handler, use }` descriptor
|
|
584
|
+
// Items that accumulate (loader, middleware, revalidate, …) compose
|
|
585
|
+
// across all three layers regardless of order.
|
|
586
|
+
const rawSlot = (slots as Record<string, any>)[slotName];
|
|
587
|
+
const slotHandlerForUse = isSlotDescriptor(rawSlot)
|
|
588
|
+
? rawSlot.handler
|
|
589
|
+
: rawSlot;
|
|
590
|
+
const slotHandlerUse = resolveHandlerUse(slotHandlerForUse);
|
|
591
|
+
const slotLocalUse = slotLocalUses[slotName];
|
|
592
|
+
const explicitUse = combineExplicitUses(use, slotLocalUse);
|
|
593
|
+
const slotMergedUse = mergeHandlerUse(
|
|
594
|
+
slotHandlerUse,
|
|
595
|
+
explicitUse,
|
|
596
|
+
"parallel",
|
|
454
597
|
);
|
|
455
|
-
|
|
598
|
+
if (slotMergedUse) {
|
|
599
|
+
const result = store.run(namespace, slotEntry, slotMergedUse)?.flat(3);
|
|
600
|
+
invariant(
|
|
601
|
+
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
602
|
+
`parallel() use() callback must return an array of use items [${namespace}]`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
456
605
|
|
|
457
|
-
|
|
606
|
+
ctx.parent.parallel[slotName] = slotEntry;
|
|
607
|
+
}
|
|
458
608
|
return { name: namespace, type: "parallel" } as ParallelItem;
|
|
459
609
|
};
|
|
460
610
|
|
|
611
|
+
function isSlotDescriptor(
|
|
612
|
+
value: unknown,
|
|
613
|
+
): value is { handler: unknown; use?: () => any[] } {
|
|
614
|
+
return (
|
|
615
|
+
typeof value === "object" &&
|
|
616
|
+
value !== null &&
|
|
617
|
+
!("__brand" in value) &&
|
|
618
|
+
"handler" in value &&
|
|
619
|
+
typeof (value as any).handler !== "undefined"
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function combineExplicitUses(
|
|
624
|
+
sharedUse: (() => any[]) | undefined,
|
|
625
|
+
slotLocalUse: (() => any[]) | undefined,
|
|
626
|
+
): (() => any[]) | undefined {
|
|
627
|
+
if (!sharedUse && !slotLocalUse) return undefined;
|
|
628
|
+
if (!slotLocalUse) return sharedUse;
|
|
629
|
+
if (!sharedUse) return slotLocalUse;
|
|
630
|
+
return () => [...sharedUse(), ...slotLocalUse()];
|
|
631
|
+
}
|
|
632
|
+
|
|
461
633
|
/**
|
|
462
634
|
* Intercept helper - defines an intercepting route for soft navigation
|
|
463
635
|
*/
|
|
@@ -502,8 +674,12 @@ const intercept = (
|
|
|
502
674
|
when: [], // Selector conditions for conditional interception
|
|
503
675
|
};
|
|
504
676
|
|
|
505
|
-
//
|
|
506
|
-
|
|
677
|
+
// Merge handler.use defaults with explicit use
|
|
678
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
679
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "intercept");
|
|
680
|
+
|
|
681
|
+
// Run merged use callback to collect loaders, revalidate, middleware, etc.
|
|
682
|
+
if (mergedUse) {
|
|
507
683
|
// Create a temporary parent context for the use() callback
|
|
508
684
|
// so that middleware, loader, revalidate attach to the intercept entry
|
|
509
685
|
const originalParent = ctx.parent;
|
|
@@ -530,7 +706,7 @@ const intercept = (
|
|
|
530
706
|
};
|
|
531
707
|
ctx.parent = tempParent as EntryData;
|
|
532
708
|
|
|
533
|
-
const result =
|
|
709
|
+
const result = mergedUse()?.flat(3);
|
|
534
710
|
|
|
535
711
|
// Restore original parent
|
|
536
712
|
ctx.parent = originalParent;
|
|
@@ -627,11 +803,15 @@ const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
|
627
803
|
invariant(false, "No parent entry available for loading()");
|
|
628
804
|
}
|
|
629
805
|
|
|
806
|
+
// Unwrap function form: loading(() => <Skeleton />) → loading(<Skeleton />)
|
|
807
|
+
const resolved =
|
|
808
|
+
typeof component === "function" ? (component as () => any)() : component;
|
|
809
|
+
|
|
630
810
|
// If ssr: false and we're in SSR, set loading to false
|
|
631
811
|
if (options?.ssr === false && ctx.isSSR) {
|
|
632
812
|
parent.loading = false;
|
|
633
813
|
} else {
|
|
634
|
-
parent.loading =
|
|
814
|
+
parent.loading = resolved;
|
|
635
815
|
}
|
|
636
816
|
|
|
637
817
|
const name = `$${store.getNextIndex("loading")}`;
|
|
@@ -687,7 +867,7 @@ const transitionFn = (
|
|
|
687
867
|
errorBoundary: [],
|
|
688
868
|
notFoundBoundary: [],
|
|
689
869
|
layout: [],
|
|
690
|
-
parallel:
|
|
870
|
+
parallel: {},
|
|
691
871
|
intercept: [],
|
|
692
872
|
loader: [],
|
|
693
873
|
} as EntryData;
|
|
@@ -727,14 +907,14 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
727
907
|
shortCode: store.getShortCode("route"),
|
|
728
908
|
type: "route",
|
|
729
909
|
parent: ctx.parent,
|
|
730
|
-
handler,
|
|
910
|
+
handler: handler as unknown as Handler<any, any, any>,
|
|
731
911
|
loading: undefined, // Allow loading() to attach loading state
|
|
732
912
|
middleware: [],
|
|
733
913
|
revalidate: [],
|
|
734
914
|
errorBoundary: [],
|
|
735
915
|
notFoundBoundary: [],
|
|
736
916
|
layout: [],
|
|
737
|
-
parallel:
|
|
917
|
+
parallel: {},
|
|
738
918
|
intercept: [],
|
|
739
919
|
loader: [],
|
|
740
920
|
} satisfies EntryData;
|
|
@@ -746,9 +926,12 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
746
926
|
);
|
|
747
927
|
/* Register route entry */
|
|
748
928
|
ctx.manifest.set(name, entry);
|
|
929
|
+
/* Merge handler.use defaults with explicit use */
|
|
930
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
931
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
|
|
749
932
|
/* Run use and attach handlers */
|
|
750
|
-
if (
|
|
751
|
-
const result = store.run(namespace, entry,
|
|
933
|
+
if (mergedUse) {
|
|
934
|
+
const result = store.run(namespace, entry, mergedUse)?.flat(3);
|
|
752
935
|
invariant(
|
|
753
936
|
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
754
937
|
`route() use() callback must return an array of use items [${namespace}]`,
|
|
@@ -791,7 +974,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
791
974
|
revalidate: [],
|
|
792
975
|
errorBoundary: [],
|
|
793
976
|
notFoundBoundary: [],
|
|
794
|
-
parallel:
|
|
977
|
+
parallel: {},
|
|
795
978
|
intercept: [],
|
|
796
979
|
layout: [],
|
|
797
980
|
loader: [],
|
|
@@ -809,10 +992,14 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
809
992
|
(handler as any).$$routePrefix = ctx.namePrefix;
|
|
810
993
|
}
|
|
811
994
|
|
|
812
|
-
//
|
|
995
|
+
// Merge handler.use defaults with explicit use
|
|
996
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
997
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "layout");
|
|
998
|
+
|
|
999
|
+
// Run merged use callback if present
|
|
813
1000
|
let result: AllUseItems[] | undefined;
|
|
814
|
-
if (
|
|
815
|
-
result = store.run(namespace, entry,
|
|
1001
|
+
if (mergedUse) {
|
|
1002
|
+
result = store.run(namespace, entry, mergedUse)?.flat(3);
|
|
816
1003
|
|
|
817
1004
|
invariant(
|
|
818
1005
|
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
@@ -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
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* **Wrapping mode** — scopes middleware to the children only:
|
|
221
|
+
* ```typescript
|
|
222
|
+
* middleware(authMiddleware, () => [
|
|
223
|
+
* path("/dashboard", DashboardPage),
|
|
224
|
+
* path("/settings", SettingsPage),
|
|
225
|
+
* ])
|
|
193
226
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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
|
|
@@ -228,11 +272,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
228
272
|
* revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
229
273
|
* ])
|
|
230
274
|
*
|
|
231
|
-
* //
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* }
|
|
275
|
+
* // Consume in client components with useLoader()
|
|
276
|
+
* // (preferred — cache-safe, always fresh)
|
|
277
|
+
* function ProductDetails() {
|
|
278
|
+
* const { data } = useLoader(ProductLoader);
|
|
279
|
+
* return <div>{data.name}</div>;
|
|
280
|
+
* }
|
|
236
281
|
* ```
|
|
237
282
|
* @param loaderDef - Loader created with createLoader()
|
|
238
283
|
* @param use - Optional callback for loader-specific revalidation rules
|
|
@@ -254,7 +299,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
254
299
|
* @param options - Configuration options
|
|
255
300
|
* @param options.ssr - If false, skip showing loading on document requests (SSR)
|
|
256
301
|
*/
|
|
257
|
-
loading: (
|
|
302
|
+
loading: (
|
|
303
|
+
component: ReactNode | (() => ReactNode),
|
|
304
|
+
options?: { ssr?: boolean },
|
|
305
|
+
) => LoadingItem;
|
|
258
306
|
/**
|
|
259
307
|
* Attach an error boundary to catch errors in this segment and children
|
|
260
308
|
* ```typescript
|
|
@@ -2,6 +2,7 @@ import type { LocationStateEntry } from "../browser/react/location-state-shared.
|
|
|
2
2
|
import {
|
|
3
3
|
requireRequestContext,
|
|
4
4
|
getRequestContext,
|
|
5
|
+
_getRequestContext,
|
|
5
6
|
} from "../server/request-context.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -71,9 +72,9 @@ export function redirect(
|
|
|
71
72
|
// actions both deliver state through Flight payloads, so suppress for those.
|
|
72
73
|
if (
|
|
73
74
|
reqCtx &&
|
|
74
|
-
!reqCtx.
|
|
75
|
+
!reqCtx.originalUrl.searchParams.has("_rsc_partial") &&
|
|
75
76
|
!reqCtx.request.headers.has("rsc-action") &&
|
|
76
|
-
!reqCtx.
|
|
77
|
+
!reqCtx.originalUrl.searchParams.has("_rsc_action")
|
|
77
78
|
) {
|
|
78
79
|
console.warn(
|
|
79
80
|
`[Router] redirect() with state during a full-page (SSR) request to "${url}". ` +
|
|
@@ -83,10 +84,17 @@ export function redirect(
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// Auto-prefix root-relative URLs with basename for app-local redirects.
|
|
88
|
+
const bn = _getRequestContext()?._basename;
|
|
89
|
+
let resolvedUrl = url;
|
|
90
|
+
if (bn && url.startsWith("/") && !url.startsWith(bn + "/") && url !== bn) {
|
|
91
|
+
resolvedUrl = url === "/" ? bn : bn + url;
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
return new Response(null, {
|
|
87
95
|
status,
|
|
88
96
|
headers: {
|
|
89
|
-
Location:
|
|
97
|
+
Location: resolvedUrl,
|
|
90
98
|
"X-RSC-Redirect": "soft",
|
|
91
99
|
},
|
|
92
100
|
});
|