@rangojs/router 0.0.0-experimental.8a4d0430 → 0.0.0-experimental.8bcfea43
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 +126 -38
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1171 -461
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +19 -16
- package/skills/breadcrumbs/SKILL.md +3 -1
- 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 +28 -20
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +88 -45
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +24 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +55 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +13 -1
- package/src/__internal.ts +1 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +90 -16
- package/src/browser/navigation-client.ts +167 -59
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +184 -16
- package/src/browser/prefetch/fetch.ts +180 -33
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +81 -9
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +168 -65
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +49 -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 +455 -15
- 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.tsx +84 -230
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +6 -1
- package/src/index.ts +49 -6
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- 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 +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +101 -25
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +159 -21
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +127 -192
- 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 +8 -30
- package/src/router/middleware.ts +36 -10
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +60 -9
- package/src/router/prerender-match.ts +110 -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 +29 -24
- 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/trie-matching.ts +10 -4
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +60 -8
- package/src/rsc/handler.ts +478 -374
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +16 -2
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +19 -1
- package/src/rsc/server-action.ts +10 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +9 -1
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +166 -17
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +194 -60
- 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 +137 -65
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -0
- package/src/urls/include-helper.ts +24 -14
- 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 +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/debug.ts +55 -0
- 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/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 +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 +86 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +204 -217
- package/src/vite/router-discovery.ts +335 -64
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +41 -1
- 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 } : {}),
|
|
@@ -300,6 +304,15 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
300
304
|
return { name: namespace, type: "cache" } as CacheItem;
|
|
301
305
|
}
|
|
302
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
|
+
|
|
303
316
|
// With children: create a cache entry (like layout with caching semantics)
|
|
304
317
|
const namespace = `${ctx.namespace}.${cacheIndex}`;
|
|
305
318
|
const cacheShortCode = store.getShortCode("cache");
|
|
@@ -320,7 +333,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
320
333
|
errorBoundary: [],
|
|
321
334
|
notFoundBoundary: [],
|
|
322
335
|
layout: [],
|
|
323
|
-
parallel:
|
|
336
|
+
parallel: {},
|
|
324
337
|
intercept: [],
|
|
325
338
|
loader: [],
|
|
326
339
|
...(cacheUrlPrefix2 ? { mountPath: cacheUrlPrefix2 } : {}),
|
|
@@ -352,10 +365,37 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
352
365
|
return { name: namespace, type: "cache", uses: result } as CacheItem;
|
|
353
366
|
};
|
|
354
367
|
|
|
355
|
-
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
|
+
|
|
356
397
|
// Prevent "use cache" functions from being used as middleware.
|
|
357
|
-
|
|
358
|
-
for (const f of fn) {
|
|
398
|
+
for (const f of fns) {
|
|
359
399
|
if (isCachedFunction(f)) {
|
|
360
400
|
throw new Error(
|
|
361
401
|
`A "use cache" function cannot be used as middleware. ` +
|
|
@@ -366,17 +406,80 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...fn) => {
|
|
|
366
406
|
}
|
|
367
407
|
}
|
|
368
408
|
|
|
369
|
-
const
|
|
409
|
+
const store = getContext();
|
|
410
|
+
const ctx = store.getStore();
|
|
370
411
|
if (!ctx) throw new Error("middleware() must be called inside map()");
|
|
371
412
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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;
|
|
376
422
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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;
|
|
380
483
|
};
|
|
381
484
|
|
|
382
485
|
const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
@@ -393,15 +496,29 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
393
496
|
"parallel() cannot be nested inside another parallel()",
|
|
394
497
|
);
|
|
395
498
|
|
|
499
|
+
const slotNames = Object.keys(slots as Record<string, any>) as `@${string}`[];
|
|
500
|
+
|
|
396
501
|
const namespace = `${ctx.namespace}.$${store.getNextIndex("parallel")}`;
|
|
397
502
|
|
|
398
|
-
// 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.
|
|
399
510
|
const unwrappedSlots: Record<string, any> = {};
|
|
511
|
+
const slotLocalUses: Record<string, (() => any[]) | undefined> = {};
|
|
400
512
|
let hasStaticSlot = false;
|
|
401
513
|
const staticSlotIds: Record<string, string> = {};
|
|
402
|
-
for (const [slotName,
|
|
514
|
+
for (const [slotName, rawSlot] of Object.entries(
|
|
403
515
|
slots as Record<string, any>,
|
|
404
516
|
)) {
|
|
517
|
+
let slotHandler: any = rawSlot;
|
|
518
|
+
if (isSlotDescriptor(rawSlot)) {
|
|
519
|
+
slotHandler = rawSlot.handler;
|
|
520
|
+
slotLocalUses[slotName] = rawSlot.use;
|
|
521
|
+
}
|
|
405
522
|
if (isStaticHandler(slotHandler)) {
|
|
406
523
|
hasStaticSlot = true;
|
|
407
524
|
unwrappedSlots[slotName] = slotHandler.handler;
|
|
@@ -431,7 +548,7 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
431
548
|
errorBoundary: [],
|
|
432
549
|
notFoundBoundary: [],
|
|
433
550
|
layout: [],
|
|
434
|
-
parallel:
|
|
551
|
+
parallel: {},
|
|
435
552
|
intercept: [],
|
|
436
553
|
loader: [],
|
|
437
554
|
...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
|
|
@@ -445,19 +562,83 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
445
562
|
: {}),
|
|
446
563
|
} satisfies EntryData;
|
|
447
564
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
565
|
+
for (const slotName of slotNames) {
|
|
566
|
+
const slotEntry = {
|
|
567
|
+
...entry,
|
|
568
|
+
handler: { [slotName]: unwrappedSlots[slotName]! },
|
|
569
|
+
middleware: [...entry.middleware],
|
|
570
|
+
revalidate: [...entry.revalidate],
|
|
571
|
+
errorBoundary: [...entry.errorBoundary],
|
|
572
|
+
notFoundBoundary: [...entry.notFoundBoundary],
|
|
573
|
+
layout: [...entry.layout],
|
|
574
|
+
parallel: { ...entry.parallel },
|
|
575
|
+
intercept: [...entry.intercept],
|
|
576
|
+
loader: [...entry.loader],
|
|
577
|
+
...(entry.staticHandlerIds?.[slotName]
|
|
578
|
+
? {
|
|
579
|
+
isStaticPrerender: true as const,
|
|
580
|
+
staticHandlerIds: { [slotName]: entry.staticHandlerIds[slotName]! },
|
|
581
|
+
}
|
|
582
|
+
: {
|
|
583
|
+
isStaticPrerender: undefined,
|
|
584
|
+
staticHandlerIds: undefined,
|
|
585
|
+
}),
|
|
586
|
+
} satisfies EntryData;
|
|
587
|
+
|
|
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",
|
|
454
606
|
);
|
|
455
|
-
|
|
607
|
+
if (slotMergedUse) {
|
|
608
|
+
const result = store.run(namespace, slotEntry, slotMergedUse)?.flat(3);
|
|
609
|
+
invariant(
|
|
610
|
+
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
611
|
+
`parallel() use() callback must return an array of use items [${namespace}]`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
456
614
|
|
|
457
|
-
|
|
615
|
+
ctx.parent.parallel[slotName] = slotEntry;
|
|
616
|
+
}
|
|
458
617
|
return { name: namespace, type: "parallel" } as ParallelItem;
|
|
459
618
|
};
|
|
460
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
|
+
|
|
461
642
|
/**
|
|
462
643
|
* Intercept helper - defines an intercepting route for soft navigation
|
|
463
644
|
*/
|
|
@@ -502,8 +683,12 @@ const intercept = (
|
|
|
502
683
|
when: [], // Selector conditions for conditional interception
|
|
503
684
|
};
|
|
504
685
|
|
|
505
|
-
//
|
|
506
|
-
|
|
686
|
+
// Merge handler.use defaults with explicit use
|
|
687
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
688
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "intercept");
|
|
689
|
+
|
|
690
|
+
// Run merged use callback to collect loaders, revalidate, middleware, etc.
|
|
691
|
+
if (mergedUse) {
|
|
507
692
|
// Create a temporary parent context for the use() callback
|
|
508
693
|
// so that middleware, loader, revalidate attach to the intercept entry
|
|
509
694
|
const originalParent = ctx.parent;
|
|
@@ -530,7 +715,7 @@ const intercept = (
|
|
|
530
715
|
};
|
|
531
716
|
ctx.parent = tempParent as EntryData;
|
|
532
717
|
|
|
533
|
-
const result =
|
|
718
|
+
const result = mergedUse()?.flat(3);
|
|
534
719
|
|
|
535
720
|
// Restore original parent
|
|
536
721
|
ctx.parent = originalParent;
|
|
@@ -574,8 +759,12 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
574
759
|
revalidate: [] as ShouldRevalidateFn<any, any>[],
|
|
575
760
|
};
|
|
576
761
|
|
|
577
|
-
//
|
|
578
|
-
|
|
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) {
|
|
579
768
|
// Temporarily set context for revalidate()/cache() calls to target this loader
|
|
580
769
|
const originalParent = ctx.parent;
|
|
581
770
|
// Create a temporary "parent" with type "loader" so cache() can detect it.
|
|
@@ -588,7 +777,7 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
588
777
|
};
|
|
589
778
|
ctx.parent = tempParent as EntryData;
|
|
590
779
|
|
|
591
|
-
const result =
|
|
780
|
+
const result = mergedUse()?.flat(3);
|
|
592
781
|
|
|
593
782
|
// Copy cache config only if cache() was called during the use() callback.
|
|
594
783
|
// The spread from originalParent may carry an inherited .cache from
|
|
@@ -627,11 +816,15 @@ const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
|
627
816
|
invariant(false, "No parent entry available for loading()");
|
|
628
817
|
}
|
|
629
818
|
|
|
819
|
+
// Unwrap function form: loading(() => <Skeleton />) → loading(<Skeleton />)
|
|
820
|
+
const resolved =
|
|
821
|
+
typeof component === "function" ? (component as () => any)() : component;
|
|
822
|
+
|
|
630
823
|
// If ssr: false and we're in SSR, set loading to false
|
|
631
824
|
if (options?.ssr === false && ctx.isSSR) {
|
|
632
825
|
parent.loading = false;
|
|
633
826
|
} else {
|
|
634
|
-
parent.loading =
|
|
827
|
+
parent.loading = resolved;
|
|
635
828
|
}
|
|
636
829
|
|
|
637
830
|
const name = `$${store.getNextIndex("loading")}`;
|
|
@@ -687,7 +880,7 @@ const transitionFn = (
|
|
|
687
880
|
errorBoundary: [],
|
|
688
881
|
notFoundBoundary: [],
|
|
689
882
|
layout: [],
|
|
690
|
-
parallel:
|
|
883
|
+
parallel: {},
|
|
691
884
|
intercept: [],
|
|
692
885
|
loader: [],
|
|
693
886
|
} as EntryData;
|
|
@@ -727,14 +920,14 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
727
920
|
shortCode: store.getShortCode("route"),
|
|
728
921
|
type: "route",
|
|
729
922
|
parent: ctx.parent,
|
|
730
|
-
handler,
|
|
923
|
+
handler: handler as unknown as Handler<any, any, any>,
|
|
731
924
|
loading: undefined, // Allow loading() to attach loading state
|
|
732
925
|
middleware: [],
|
|
733
926
|
revalidate: [],
|
|
734
927
|
errorBoundary: [],
|
|
735
928
|
notFoundBoundary: [],
|
|
736
929
|
layout: [],
|
|
737
|
-
parallel:
|
|
930
|
+
parallel: {},
|
|
738
931
|
intercept: [],
|
|
739
932
|
loader: [],
|
|
740
933
|
} satisfies EntryData;
|
|
@@ -746,9 +939,12 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
746
939
|
);
|
|
747
940
|
/* Register route entry */
|
|
748
941
|
ctx.manifest.set(name, entry);
|
|
942
|
+
/* Merge handler.use defaults with explicit use */
|
|
943
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
944
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
|
|
749
945
|
/* Run use and attach handlers */
|
|
750
|
-
if (
|
|
751
|
-
const result = store.run(namespace, entry,
|
|
946
|
+
if (mergedUse) {
|
|
947
|
+
const result = store.run(namespace, entry, mergedUse)?.flat(3);
|
|
752
948
|
invariant(
|
|
753
949
|
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
754
950
|
`route() use() callback must return an array of use items [${namespace}]`,
|
|
@@ -791,7 +987,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
791
987
|
revalidate: [],
|
|
792
988
|
errorBoundary: [],
|
|
793
989
|
notFoundBoundary: [],
|
|
794
|
-
parallel:
|
|
990
|
+
parallel: {},
|
|
795
991
|
intercept: [],
|
|
796
992
|
layout: [],
|
|
797
993
|
loader: [],
|
|
@@ -809,10 +1005,14 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
809
1005
|
(handler as any).$$routePrefix = ctx.namePrefix;
|
|
810
1006
|
}
|
|
811
1007
|
|
|
812
|
-
//
|
|
1008
|
+
// Merge handler.use defaults with explicit use
|
|
1009
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
1010
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, "layout");
|
|
1011
|
+
|
|
1012
|
+
// Run merged use callback if present
|
|
813
1013
|
let result: AllUseItems[] | undefined;
|
|
814
|
-
if (
|
|
815
|
-
result = store.run(namespace, entry,
|
|
1014
|
+
if (mergedUse) {
|
|
1015
|
+
result = store.run(namespace, entry, mergedUse)?.flat(3);
|
|
816
1016
|
|
|
817
1017
|
invariant(
|
|
818
1018
|
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
|
});
|