@rangojs/router 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +62 -0
- package/dist/types/client-urls/types.d.ts +144 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +10 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/vite/index.js +5313 -2365
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +698 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +579 -0
- package/src/client-urls/types.ts +195 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +47 -0
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +14 -24
- package/src/router/segment-resolution/revalidation.ts +20 -1
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +14 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
recordRequestTags,
|
|
52
52
|
runWithCacheTagScope,
|
|
53
53
|
} from "./cache-tag.js";
|
|
54
|
+
import { runWithCacheExecScope } from "./cache-exec-scope.js";
|
|
54
55
|
import { reportCacheError } from "./cache-error.js";
|
|
55
56
|
import type { CacheItemResult } from "./types.js";
|
|
56
57
|
|
|
@@ -289,11 +290,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
289
290
|
// cacheTag() call inside the function degrades to a no-op rather than
|
|
290
291
|
// throwing "must be called inside a use cache function" - adopting cacheTag()
|
|
291
292
|
// must not hard-fail in apps/tests without an item-capable cache configured.
|
|
292
|
-
// Note: the
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// non-serializable-args
|
|
293
|
+
// Note: the cache-exec guards (cookies()/headers()/ctx.set() rejection —
|
|
294
|
+
// runWithCacheExecScope + the INSIDE_CACHE_EXEC arg stamp) are intentionally
|
|
295
|
+
// NOT active here. They are cached-path-only checks; in the bypass the body
|
|
296
|
+
// actually executes, so the guarded side effects take effect and nothing is
|
|
297
|
+
// lost on a (non-existent) hit. Same applies to the non-serializable-args
|
|
298
|
+
// bypass below.
|
|
297
299
|
if (!store?.getItem) {
|
|
298
300
|
// Test-ergonomics guard: under a test runner, a "use cache" function that
|
|
299
301
|
// executes with no item-capable store seeded is exercising the UNCACHED
|
|
@@ -507,9 +509,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
507
509
|
// property stamped onto that SHARED object — so for the whole
|
|
508
510
|
// revalidation window a concurrent foreground ctx.set() /
|
|
509
511
|
// ctx.headers.*() would throw (issue #684, plan 010). requestCtx is
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
512
|
+
// never stamped anywhere for the same reason. In-body misuse IS
|
|
513
|
+
// still guarded here: runWithCacheExecScope below follows only this
|
|
514
|
+
// body's async chain, so a conditional cookies()/headers() read
|
|
515
|
+
// that fires only during revalidation throws instead of baking one
|
|
516
|
+
// user's value into the shared entry — with zero visibility to the
|
|
517
|
+
// concurrent foreground.
|
|
513
518
|
|
|
514
519
|
try {
|
|
515
520
|
// Re-establish the request-context ALS so a "use cache" body that
|
|
@@ -530,7 +535,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
530
535
|
PHASES.background("use-cache-revalidation"),
|
|
531
536
|
async () => {
|
|
532
537
|
const scoped = runWithCacheTagScope(() =>
|
|
533
|
-
fn.apply(this, args),
|
|
538
|
+
runWithCacheExecScope(() => fn.apply(this, args)),
|
|
534
539
|
);
|
|
535
540
|
const freshResult = await scoped.result;
|
|
536
541
|
bgStopCapture?.();
|
|
@@ -678,12 +683,13 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
678
683
|
// Uses ref-counted stamp/unstamp so overlapping executions
|
|
679
684
|
// sharing the same ctx don't clear each other's guards.
|
|
680
685
|
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
686
|
+
// The ambient guards (cookies()/headers() and getRequestContext()-reached
|
|
687
|
+
// ctx methods) ride runWithCacheExecScope below instead — an ALS scoped to
|
|
688
|
+
// the cached body's own async chain. The RequestContext itself is
|
|
689
|
+
// deliberately NOT stamped: a property on that SHARED object made every
|
|
690
|
+
// parallel read on the request throw for the cached body's whole
|
|
691
|
+
// execution window (a 2s cached fetch poisoned a sibling loader's
|
|
692
|
+
// cookies() — same hazard class as issue #684, plan 010).
|
|
687
693
|
const taintedArgs: unknown[] = [];
|
|
688
694
|
for (const arg of args) {
|
|
689
695
|
if (isTainted(arg)) {
|
|
@@ -691,17 +697,13 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
691
697
|
taintedArgs.push(arg);
|
|
692
698
|
}
|
|
693
699
|
}
|
|
694
|
-
// Always stamp the ALS RequestContext so cookies()/headers() guards fire
|
|
695
|
-
// even when the cached function receives no tainted args. The guard in
|
|
696
|
-
// cookie-store.ts checks RequestContext, not function args.
|
|
697
|
-
if (requestCtx) {
|
|
698
|
-
stampCacheExec(requestCtx as object);
|
|
699
|
-
}
|
|
700
700
|
|
|
701
701
|
let result: any;
|
|
702
702
|
let scoped: ReturnType<typeof runWithCacheTagScope>;
|
|
703
703
|
try {
|
|
704
|
-
scoped = runWithCacheTagScope(() =>
|
|
704
|
+
scoped = runWithCacheTagScope(() =>
|
|
705
|
+
runWithCacheExecScope(() => fn.apply(this, args)),
|
|
706
|
+
);
|
|
705
707
|
result = await scoped.result;
|
|
706
708
|
} catch (execError) {
|
|
707
709
|
// The function threw: drop the in-flight entry and reject any waiters so
|
|
@@ -714,9 +716,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
714
716
|
for (const arg of taintedArgs) {
|
|
715
717
|
unstampCacheExec(arg as object);
|
|
716
718
|
}
|
|
717
|
-
if (requestCtx) {
|
|
718
|
-
unstampCacheExec(requestCtx as object);
|
|
719
|
-
}
|
|
720
719
|
// Remove this capture token (order-independent, safe for concurrent use)
|
|
721
720
|
stopCapture?.();
|
|
722
721
|
}
|
package/src/cache/taint.ts
CHANGED
|
@@ -21,14 +21,19 @@ export function isTainted(value: unknown): boolean {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Symbol stamped on tainted ctx
|
|
25
|
-
*
|
|
26
|
-
* throw if present —
|
|
27
|
-
*
|
|
24
|
+
* Symbol stamped on tainted ctx objects PASSED AS ARGUMENTS to a "use cache"
|
|
25
|
+
* function during its execution. ctx.set(), ctx.header(), etc. check this
|
|
26
|
+
* flag and throw if present — those side effects are lost on cache hit.
|
|
27
|
+
*
|
|
28
|
+
* Argument objects only. Ambient access (cookies()/headers() and ctx methods
|
|
29
|
+
* reached via getRequestContext()) is guarded by the AsyncLocalStorage scope
|
|
30
|
+
* in cache-exec-scope.ts instead: stamping the SHARED RequestContext made
|
|
31
|
+
* every parallel read on the request throw for the cached body's whole
|
|
32
|
+
* execution window (a 2s cached fetch poisoned a sibling loader's cookies()).
|
|
28
33
|
*
|
|
29
34
|
* The value is a numeric reference count, not a boolean. Multiple concurrent
|
|
30
|
-
* cached functions sharing the same ctx
|
|
31
|
-
*
|
|
35
|
+
* cached functions sharing the same ctx each increment on entry and decrement
|
|
36
|
+
* on exit. Guards fire when count > 0.
|
|
32
37
|
*/
|
|
33
38
|
export const INSIDE_CACHE_EXEC: unique symbol = Symbol.for(
|
|
34
39
|
"rango:inside-cache-exec",
|
|
@@ -56,6 +61,20 @@ export function unstampCacheExec(obj: object): void {
|
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Probe for the AsyncLocalStorage-based "use cache" execution scope.
|
|
66
|
+
* Registered by cache-exec-scope.ts at module init; the default ("never
|
|
67
|
+
* inside") keeps this module free of node:async_hooks — taint.ts is reachable
|
|
68
|
+
* from browser-bundled DSL helpers. Covers AMBIENT ctx access inside a cached
|
|
69
|
+
* body (getRequestContext().set(...)) chain-scoped, where the object stamp
|
|
70
|
+
* below only covers ctx objects explicitly passed as arguments.
|
|
71
|
+
*/
|
|
72
|
+
let cacheExecScopeProbe: () => boolean = () => false;
|
|
73
|
+
|
|
74
|
+
export function _setCacheExecScopeProbe(probe: () => boolean): void {
|
|
75
|
+
cacheExecScopeProbe = probe;
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
/**
|
|
60
79
|
* Throw if ctx is inside a "use cache" execution.
|
|
61
80
|
* Call from side-effecting ctx methods (set, header, etc.) and cookie mutations.
|
|
@@ -64,12 +83,12 @@ export function assertNotInsideCacheExec(
|
|
|
64
83
|
ctx: unknown,
|
|
65
84
|
methodName: string,
|
|
66
85
|
): void {
|
|
67
|
-
|
|
86
|
+
const argStamped =
|
|
68
87
|
ctx !== null &&
|
|
69
88
|
ctx !== undefined &&
|
|
70
89
|
typeof ctx === "object" &&
|
|
71
|
-
(INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
|
|
72
|
-
) {
|
|
90
|
+
(INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>);
|
|
91
|
+
if (argStamped || cacheExecScopeProbe()) {
|
|
73
92
|
throw new Error(
|
|
74
93
|
`ctx.${methodName}() cannot be called inside a "use cache" function. ` +
|
|
75
94
|
`Side effects on the request context are lost on cache hit because ` +
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createElement,
|
|
5
|
+
Fragment,
|
|
6
|
+
useEffect,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { Outlet } from "../client.js";
|
|
11
|
+
import { OutletProvider } from "../outlet-provider.js";
|
|
12
|
+
import { useMount } from "../browser/react/use-mount.js";
|
|
13
|
+
import {
|
|
14
|
+
registerClientUrlGroup,
|
|
15
|
+
type ClientUrlNavigationIntent,
|
|
16
|
+
} from "./navigation.js";
|
|
17
|
+
import type {
|
|
18
|
+
ClientUrlInterceptRecord,
|
|
19
|
+
ClientUrlPatterns,
|
|
20
|
+
ClientUrlRouteRecord,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
function findRoute(
|
|
24
|
+
definition: ClientUrlPatterns,
|
|
25
|
+
routeId: string,
|
|
26
|
+
): ClientUrlRouteRecord {
|
|
27
|
+
const route = definition.routes.find((candidate) => candidate.id === routeId);
|
|
28
|
+
if (!route) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Client URL route mismatch: route id "${routeId}" was not found in the provided definition`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return route;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findIntercept(
|
|
37
|
+
definition: ClientUrlPatterns,
|
|
38
|
+
interceptIndex: number,
|
|
39
|
+
): ClientUrlInterceptRecord {
|
|
40
|
+
const record = definition.intercepts[interceptIndex];
|
|
41
|
+
if (!record) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Client URL intercept mismatch: intercept index ${interceptIndex} was not found in the provided definition`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return record;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Server-materialized wrapper layout for a clientUrls() group that declares
|
|
51
|
+
* intercepts. Attachment scar: intercepts emitted at the TOP LEVEL of a lazily
|
|
52
|
+
* included module attach to the isolated parent clone
|
|
53
|
+
* (getIsolatedLazyParent in server/context.ts) and are silently discarded —
|
|
54
|
+
* only intercepts attached to a layout entry created WITHIN the expansion
|
|
55
|
+
* survive in origin chains. Materialization therefore wraps the group's routes
|
|
56
|
+
* and intercept entries in this layout; it renders the child outlet plus one
|
|
57
|
+
* named outlet per declared slot, so the modal presents inside the group's own
|
|
58
|
+
* subtree (module-local declaration, module-local presentation).
|
|
59
|
+
*/
|
|
60
|
+
export function ClientUrlsGroupLayout({
|
|
61
|
+
slotNames,
|
|
62
|
+
}: {
|
|
63
|
+
slotNames: readonly `@${string}`[];
|
|
64
|
+
}): ReactNode {
|
|
65
|
+
return (
|
|
66
|
+
<Fragment>
|
|
67
|
+
<Outlet />
|
|
68
|
+
{slotNames.map((name) => (
|
|
69
|
+
<Outlet key={name} name={name} />
|
|
70
|
+
))}
|
|
71
|
+
</Fragment>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Slot content for a client-declared intercept: renders the definition's
|
|
76
|
+
* modal component; its useLoader() calls read the slot segment's loader data
|
|
77
|
+
* from the surrounding outlet context. */
|
|
78
|
+
export function ClientUrlsInterceptSlot({
|
|
79
|
+
definition,
|
|
80
|
+
interceptIndex,
|
|
81
|
+
}: {
|
|
82
|
+
definition: ClientUrlPatterns;
|
|
83
|
+
interceptIndex: number;
|
|
84
|
+
}): ReactNode {
|
|
85
|
+
const record = findIntercept(definition, interceptIndex);
|
|
86
|
+
return createElement(record.component, {
|
|
87
|
+
key: `intercept-${interceptIndex}`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function ClientUrlsInterceptLoading({
|
|
92
|
+
definition,
|
|
93
|
+
interceptIndex,
|
|
94
|
+
}: {
|
|
95
|
+
definition: ClientUrlPatterns;
|
|
96
|
+
interceptIndex: number;
|
|
97
|
+
}): ReactNode {
|
|
98
|
+
return findIntercept(definition, interceptIndex).loading ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ClientUrlsRoot({
|
|
102
|
+
definition,
|
|
103
|
+
routeId,
|
|
104
|
+
namePrefix = "",
|
|
105
|
+
}: {
|
|
106
|
+
definition: ClientUrlPatterns;
|
|
107
|
+
routeId: string;
|
|
108
|
+
/** include() route-name prefix, injected at materialization for canonical
|
|
109
|
+
* name composition (intercept-target coordination). */
|
|
110
|
+
namePrefix?: string;
|
|
111
|
+
}): ReactNode {
|
|
112
|
+
// The include() mount prefix this group renders under ("/" at root). The
|
|
113
|
+
// module trie is built from definition-LOCAL patterns; navigation strips
|
|
114
|
+
// this prefix before matching (see stripMountPrefix in navigation.ts), the
|
|
115
|
+
// same way client href()/useMount resolve include-relative URLs.
|
|
116
|
+
const mount = useMount();
|
|
117
|
+
const [intent, setIntent] = useState<ClientUrlNavigationIntent | null>(null);
|
|
118
|
+
useEffect(
|
|
119
|
+
() => registerClientUrlGroup(definition, mount, namePrefix, setIntent),
|
|
120
|
+
[definition, mount, namePrefix],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const pendingRoute =
|
|
124
|
+
intent && intent.routeId !== routeId
|
|
125
|
+
? findRoute(definition, intent.routeId)
|
|
126
|
+
: null;
|
|
127
|
+
// Presence must mirror the projection's hasLoading (`loading !== undefined`
|
|
128
|
+
// in server-projection.ts): a falsy-but-valid node like loading("") is still
|
|
129
|
+
// a configured destination loading state, not an absent one.
|
|
130
|
+
const hasPendingLoading =
|
|
131
|
+
pendingRoute !== null && pendingRoute.loading !== undefined;
|
|
132
|
+
const route = hasPendingLoading
|
|
133
|
+
? pendingRoute
|
|
134
|
+
: findRoute(definition, routeId);
|
|
135
|
+
// ANY in-flight group navigation is pending — including same-route navs
|
|
136
|
+
// (intent.routeId === routeId). For the search-only shape (filters, tabs)
|
|
137
|
+
// the canonical commit is HELD in a transition (isSameStructureNav in
|
|
138
|
+
// partial-update.ts) with no content swap to signal progress — this flag
|
|
139
|
+
// is the only affordance. The urgent setIntent at nav start flips it
|
|
140
|
+
// immediately; the transition-wrapped clear() entangles with the held
|
|
141
|
+
// commit, so pending drops exactly when the data lands.
|
|
142
|
+
const pending = intent !== null;
|
|
143
|
+
let content: ReactNode = hasPendingLoading
|
|
144
|
+
? pendingRoute.loading
|
|
145
|
+
: createElement(route.component, { key: route.id });
|
|
146
|
+
|
|
147
|
+
for (let index = route.layouts.length - 1; index >= 0; index--) {
|
|
148
|
+
const layoutKey = `${route.id}-layout-${index}`;
|
|
149
|
+
content = createElement(OutletProvider, {
|
|
150
|
+
key: layoutKey,
|
|
151
|
+
content,
|
|
152
|
+
pending,
|
|
153
|
+
children: createElement(route.layouts[index], { key: layoutKey }),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return content;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function ClientUrlsLoading({
|
|
161
|
+
definition,
|
|
162
|
+
routeId,
|
|
163
|
+
}: {
|
|
164
|
+
definition: ClientUrlPatterns;
|
|
165
|
+
routeId: string;
|
|
166
|
+
}): ReactNode {
|
|
167
|
+
return findRoute(definition, routeId).loading ?? null;
|
|
168
|
+
}
|