@rangojs/router 0.5.2 → 0.7.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.
Files changed (178) hide show
  1. package/dist/bin/rango.js +343 -125
  2. package/dist/types/browser/react/use-router.d.ts +10 -3
  3. package/dist/types/browser/react/use-search-params.d.ts +57 -10
  4. package/dist/types/browser/types.d.ts +22 -0
  5. package/dist/types/build/merge-full-manifests.d.ts +3 -0
  6. package/dist/types/build/route-trie.d.ts +4 -73
  7. package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
  8. package/dist/types/build/route-types/router-processing.d.ts +2 -3
  9. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  10. package/dist/types/cache/taint.d.ts +12 -6
  11. package/dist/types/client-urls/client-root.d.ts +38 -0
  12. package/dist/types/client-urls/client-urls.d.ts +5 -0
  13. package/dist/types/client-urls/navigation.d.ts +38 -0
  14. package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
  15. package/dist/types/client-urls/server-projection.d.ts +71 -0
  16. package/dist/types/client-urls/types.d.ts +147 -0
  17. package/dist/types/client.d.ts +12 -4
  18. package/dist/types/client.rsc.d.ts +4 -1
  19. package/dist/types/decode-loader-results.d.ts +37 -0
  20. package/dist/types/errors.d.ts +1 -0
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/loader-redirect.d.ts +27 -0
  23. package/dist/types/outlet-context.d.ts +12 -0
  24. package/dist/types/outlet-provider.d.ts +3 -1
  25. package/dist/types/redirect-origin.d.ts +4 -0
  26. package/dist/types/route-content-wrapper.d.ts +42 -1
  27. package/dist/types/route-definition/helpers-types.d.ts +13 -2
  28. package/dist/types/router/error-handling.d.ts +35 -1
  29. package/dist/types/router/intercept-resolution.d.ts +12 -0
  30. package/dist/types/router/loader-resolution.d.ts +24 -2
  31. package/dist/types/router/revalidation.d.ts +7 -0
  32. package/dist/types/router/route-trie-builder.d.ts +77 -0
  33. package/dist/types/router/router-interfaces.d.ts +20 -0
  34. package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
  35. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
  36. package/dist/types/router/trie-matching.d.ts +1 -1
  37. package/dist/types/rsc/manifest-init.d.ts +5 -5
  38. package/dist/types/rsc/shell-capture.d.ts +9 -0
  39. package/dist/types/rsc/shell-serve.d.ts +11 -0
  40. package/dist/types/rsc/types.d.ts +30 -0
  41. package/dist/types/segment-system.d.ts +2 -0
  42. package/dist/types/server/context.d.ts +10 -0
  43. package/dist/types/server/handle-store.d.ts +34 -3
  44. package/dist/types/server/request-context.d.ts +11 -1
  45. package/dist/types/server.d.ts +1 -0
  46. package/dist/types/ssr/index.d.ts +22 -0
  47. package/dist/types/ssr/ssr-root.d.ts +10 -0
  48. package/dist/types/testing/dom.entry.d.ts +1 -1
  49. package/dist/types/testing/render-route.d.ts +16 -6
  50. package/dist/types/testing/run-loader.d.ts +9 -0
  51. package/dist/types/types/boundaries.d.ts +22 -0
  52. package/dist/types/types/index.d.ts +1 -1
  53. package/dist/types/types/loader-types.d.ts +57 -5
  54. package/dist/types/types/segments.d.ts +7 -0
  55. package/dist/types/urls/path-helper-types.d.ts +13 -4
  56. package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
  57. package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
  58. package/dist/types/vite/discovery/state.d.ts +8 -1
  59. package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
  60. package/dist/vite/index.js +6159 -2959
  61. package/package.json +6 -5
  62. package/skills/breadcrumbs/SKILL.md +39 -9
  63. package/skills/catalog.json +7 -1
  64. package/skills/client-urls/SKILL.md +338 -0
  65. package/skills/comparison/references/framework-comparison.md +23 -9
  66. package/skills/hooks/SKILL.md +2 -2
  67. package/skills/hooks/data.md +11 -2
  68. package/skills/hooks/handle-and-actions.md +7 -0
  69. package/skills/hooks/outlets.md +26 -5
  70. package/skills/hooks/urls.md +40 -3
  71. package/skills/loader/SKILL.md +132 -20
  72. package/skills/migrate-nextjs/SKILL.md +70 -10
  73. package/skills/migrate-react-router/SKILL.md +49 -13
  74. package/skills/migrate-react-router/component-migration.md +18 -13
  75. package/skills/migrate-react-router/data-and-actions.md +14 -3
  76. package/skills/migrate-react-router/route-mapping.md +15 -2
  77. package/skills/parallel/SKILL.md +32 -1
  78. package/skills/ppr/SKILL.md +16 -6
  79. package/skills/prerender/SKILL.md +8 -4
  80. package/skills/rango/SKILL.md +21 -17
  81. package/skills/react-compiler/SKILL.md +3 -3
  82. package/skills/route/SKILL.md +5 -2
  83. package/skills/router-setup/SKILL.md +16 -2
  84. package/skills/scripts/SKILL.md +16 -6
  85. package/skills/shell-manifest/SKILL.md +16 -7
  86. package/skills/testing/SKILL.md +2 -2
  87. package/skills/testing/client-components.md +6 -0
  88. package/skills/testing/handles.md +30 -8
  89. package/skills/testing/loader.md +51 -49
  90. package/skills/testing/middleware.md +1 -1
  91. package/skills/theme/SKILL.md +8 -5
  92. package/src/bin/rango.ts +7 -3
  93. package/src/browser/navigation-bridge.ts +6 -0
  94. package/src/browser/navigation-client.ts +5 -0
  95. package/src/browser/partial-update.ts +65 -13
  96. package/src/browser/react/use-router.ts +40 -11
  97. package/src/browser/react/use-search-params.ts +140 -17
  98. package/src/browser/rsc-router.tsx +59 -0
  99. package/src/browser/server-action-bridge.ts +26 -0
  100. package/src/browser/types.ts +22 -0
  101. package/src/build/merge-full-manifests.ts +161 -0
  102. package/src/build/route-trie.ts +9 -332
  103. package/src/build/route-types/include-resolution.ts +66 -11
  104. package/src/build/route-types/per-module-writer.ts +11 -6
  105. package/src/build/route-types/router-processing.ts +184 -153
  106. package/src/build/runtime-discovery.ts +23 -12
  107. package/src/cache/cache-exec-scope.ts +47 -0
  108. package/src/cache/cache-runtime.ts +24 -25
  109. package/src/cache/taint.ts +28 -9
  110. package/src/client-urls/client-root.tsx +168 -0
  111. package/src/client-urls/client-urls.ts +776 -0
  112. package/src/client-urls/navigation.ts +237 -0
  113. package/src/client-urls/revalidation-protocol.ts +56 -0
  114. package/src/client-urls/server-projection.ts +670 -0
  115. package/src/client-urls/types.ts +201 -0
  116. package/src/client.rsc.tsx +12 -0
  117. package/src/client.tsx +49 -6
  118. package/src/decode-loader-results.ts +113 -0
  119. package/src/errors.ts +14 -0
  120. package/src/handles/deferred-resolution.ts +14 -7
  121. package/src/index.ts +1 -0
  122. package/src/loader-redirect.tsx +64 -0
  123. package/src/outlet-context.ts +12 -0
  124. package/src/outlet-provider.tsx +15 -1
  125. package/src/redirect-origin.ts +29 -0
  126. package/src/route-content-wrapper.tsx +96 -3
  127. package/src/route-definition/dsl-helpers.ts +28 -3
  128. package/src/route-definition/helpers-types.ts +13 -0
  129. package/src/route-definition/redirect.ts +17 -18
  130. package/src/router/error-handling.ts +65 -11
  131. package/src/router/intercept-resolution.ts +29 -0
  132. package/src/router/loader-resolution.ts +261 -28
  133. package/src/router/match-result.ts +7 -0
  134. package/src/router/revalidation.ts +24 -11
  135. package/src/router/route-trie-builder.ts +334 -0
  136. package/src/router/router-interfaces.ts +38 -0
  137. package/src/router/segment-resolution/fresh.ts +55 -2
  138. package/src/router/segment-resolution/helpers.ts +9 -11
  139. package/src/router/segment-resolution/loader-cache.ts +34 -23
  140. package/src/router/segment-resolution/loader-mask.ts +9 -9
  141. package/src/router/segment-resolution/revalidation.ts +23 -2
  142. package/src/router/trie-matching.ts +3 -3
  143. package/src/router.ts +46 -1
  144. package/src/rsc/full-payload.ts +6 -0
  145. package/src/rsc/handler.ts +10 -7
  146. package/src/rsc/loader-fetch.ts +2 -2
  147. package/src/rsc/manifest-init.ts +28 -9
  148. package/src/rsc/rsc-rendering.ts +15 -1
  149. package/src/rsc/shell-capture.ts +12 -0
  150. package/src/rsc/shell-serve.ts +15 -2
  151. package/src/rsc/ssr-setup.ts +10 -1
  152. package/src/rsc/types.ts +31 -2
  153. package/src/segment-system.tsx +83 -26
  154. package/src/server/context.ts +10 -0
  155. package/src/server/cookie-store.ts +19 -19
  156. package/src/server/handle-store.ts +185 -48
  157. package/src/server/request-context.ts +30 -6
  158. package/src/server.ts +7 -0
  159. package/src/ssr/index.tsx +37 -2
  160. package/src/ssr/ssr-root.tsx +29 -2
  161. package/src/testing/dom.entry.ts +1 -1
  162. package/src/testing/render-route.tsx +22 -8
  163. package/src/testing/run-loader.ts +51 -13
  164. package/src/types/boundaries.ts +19 -0
  165. package/src/types/index.ts +1 -0
  166. package/src/types/loader-types.ts +60 -5
  167. package/src/types/segments.ts +7 -0
  168. package/src/urls/include-helper.ts +22 -4
  169. package/src/urls/path-helper-types.ts +17 -1
  170. package/src/use-loader.tsx +67 -6
  171. package/src/vite/discovery/client-urls-projection.ts +322 -0
  172. package/src/vite/discovery/discover-routers.ts +43 -17
  173. package/src/vite/discovery/state.ts +11 -1
  174. package/src/vite/discovery/virtual-module-codegen.ts +20 -0
  175. package/src/vite/plugins/client-ref-dedup.ts +281 -19
  176. package/src/vite/plugins/expose-action-id.ts +45 -23
  177. package/src/vite/plugins/virtual-entries.ts +12 -3
  178. 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 INSIDE_CACHE_EXEC guard (cookies()/headers()/ctx.set() rejection)
293
- // is intentionally NOT stamped here. It is a cached-path-only check; in the
294
- // bypass the body actually executes, so the guarded side effects take effect
295
- // and nothing is lost on a (non-existent) hit. Same applies to the
296
- // non-serializable-args bypass below.
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
- // not stamped for the same reason. In-fn misuse is already caught
511
- // by the miss path's stamps on the function's FIRST execution — the
512
- // background re-runs the same function with the same request.
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
- // LOAD-BEARING for the stale-revalidation path above: the background
682
- // re-execution deliberately does NOT re-stamp (the objects are live
683
- // foreground state mid-render), relying on THIS stamp having caught in-fn
684
- // misuse on the function's first execution an entry only becomes
685
- // stale-revalidatable because a stamped miss ran clean and stored it. Do
686
- // not create a "use cache" entry via any path that skips this stamp.
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(() => fn.apply(this, args));
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
  }
@@ -21,14 +21,19 @@ export function isTainted(value: unknown): boolean {
21
21
  }
22
22
 
23
23
  /**
24
- * Symbol stamped on tainted ctx during "use cache" function execution.
25
- * cookies(), headers(), ctx.set(), ctx.header(), etc. check this flag and
26
- * throw if present — reads would cache per-request data under a shared key,
27
- * and side effects would be lost on cache hit.
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/requestCtx each increment on entry
31
- * and decrement on exit. Guards fire when count > 0.
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
- if (
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
+ }