@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
@@ -129,6 +129,14 @@ export async function resolveLoadersWithRevalidation<TEnv>(
129
129
 
130
130
  const shortCode = shortCodeOverride ?? entry.shortCode;
131
131
 
132
+ // Pin `_currentSegmentId` to the OWNING entry before the kickoffs below —
133
+ // createLoaderExecutor captures it synchronously for ctx.use(Handle) push
134
+ // attribution. Same pin as resolveLoaders (fresh.ts); this REVALIDATION
135
+ // funnel is the navigation/action lane's kickoff site, where nothing else
136
+ // assigns the id first (pushes silently vanished: an id outside
137
+ // matched/segmentOrder is dropped by collectHandleData).
138
+ (ctx as InternalHandlerContext<any, TEnv>)._currentSegmentId = shortCode;
139
+
132
140
  const loaderMeta = loaderEntries.map((loaderEntry, i) => ({
133
141
  loaderEntry,
134
142
  loader: loaderEntry.loader,
@@ -232,7 +240,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
232
240
  loaderEntry,
233
241
  ctx,
234
242
  ctx.pathname,
235
- bakeLane ? segmentId : null,
243
+ bakeLane || loaderEntry.awaitBeforeFlush === true
244
+ ? segmentId
245
+ : null,
236
246
  ),
237
247
  ),
238
248
  entry,
@@ -605,7 +615,17 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
605
615
  // For non-empty client sets, consult user revalidate fns. When the slot
606
616
  // is unknown to the client, override the type-derived default so the
607
617
  // soft chain seeds with the right "new segment" / "parent-chain" value.
608
- let defaultOverride: { value: boolean; reason: string } | undefined;
618
+ //
619
+ // A "new segment" seed is floored: the client has no cached copy of this
620
+ // slot, so a user `false` would render component:null and leave it blank
621
+ // rather than keep anything. Sibling resolvers (loaders, layout/route
622
+ // entries, orphan layouts) get the same guarantee by short-circuiting
623
+ // without consulting user fns at all; #482 made this path consult them so
624
+ // a "skip-parent-chain" seed could still be raised, so floor instead of
625
+ // short-circuiting — user fns run, and may only raise.
626
+ let defaultOverride:
627
+ | { value: boolean; reason: string; floor?: boolean }
628
+ | undefined;
609
629
  if (!clientSegmentIds.has(parallelId)) {
610
630
  const value =
611
631
  parentChainDefault === "force-render"
@@ -614,6 +634,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
614
634
  defaultOverride = {
615
635
  value,
616
636
  reason: value ? "new-segment" : "skip-parent-chain",
637
+ floor: value,
617
638
  };
618
639
  }
619
640
 
@@ -9,7 +9,7 @@ import type {
9
9
  TrieNode,
10
10
  TrieLeaf,
11
11
  NegotiateVariant,
12
- } from "../build/route-trie.js";
12
+ } from "./route-trie-builder.js";
13
13
  import { safeDecodeURIComponent } from "./url-params.js";
14
14
 
15
15
  export interface TrieMatchResult {
@@ -219,8 +219,8 @@ function walkTrie(
219
219
 
220
220
  if (node.xp) {
221
221
  // node.xp keys are pre-sorted longest-suffix-first at build time
222
- // (route-trie.ts sortSuffixParams), so the first match is the most specific
223
- // suffix: `/app.min.js` matches `:file.min.js` before `:file.js`.
222
+ // (route-trie-builder.ts sortSuffixParams), so the first match is the most
223
+ // specific suffix: `/app.min.js` matches `:file.min.js` before `:file.js`.
224
224
  for (const suffix in node.xp) {
225
225
  if (segment.endsWith(suffix) && segment.length > suffix.length) {
226
226
  const paramValue = segment.slice(0, -suffix.length);
package/src/router.ts CHANGED
@@ -3,6 +3,11 @@ import { createCacheScope } from "./cache/cache-scope.js";
3
3
  import { resolveCacheProfiles } from "./cache/profile-registry.js";
4
4
  import { isCachedFunction } from "./cache/taint.js";
5
5
  import { assertClientComponent } from "./component-utils.js";
6
+ import {
7
+ isClientUrlPatterns,
8
+ isClientUrlReference,
9
+ } from "./client-urls/server-projection.js";
10
+ import type { ClientUrlPatterns } from "./client-urls/types.js";
6
11
  import { DefaultDocument } from "./components/DefaultDocument.js";
7
12
  import type { SerializedManifest } from "./debug.js";
8
13
  import {
@@ -100,6 +105,7 @@ import type {
100
105
  Rango,
101
106
  RangoInternal,
102
107
  RouterRequestInput,
108
+ UrlPatternMount,
103
109
  } from "./router/router-interfaces.js";
104
110
 
105
111
  // Extracted closure functions
@@ -330,6 +336,7 @@ export function createRouter<TEnv = any>(
330
336
 
331
337
  // Store reference to urlpatterns for runtime manifest generation
332
338
  let storedUrlPatterns: UrlPatterns<TEnv, any> | null = null;
339
+ const urlpatternMounts: UrlPatternMount<TEnv>[] = [];
333
340
 
334
341
  // Global middleware storage
335
342
  const globalMiddleware: MiddlewareEntry<TEnv>[] = [];
@@ -572,6 +579,13 @@ export function createRouter<TEnv = any>(
572
579
  }
573
580
  }
574
581
  : undefined,
582
+ // notFound() resolution deps: the streamed envelope carries the
583
+ // server-rendered not-found UI, mirroring the consumption lane's
584
+ // boundary resolution (segment-resolution/helpers.ts).
585
+ {
586
+ findNearestNotFoundBoundary,
587
+ notFoundComponent: notFound,
588
+ },
575
589
  );
576
590
 
577
591
  // Emit loader.end after the promise settles (fire-and-forget)
@@ -756,7 +770,30 @@ export function createRouter<TEnv = any>(
756
770
  id: routerId,
757
771
  basename,
758
772
 
759
- routes(patternsOrBuilder: UrlPatterns<TEnv> | UrlBuilder<TEnv>): any {
773
+ routes(
774
+ patternsOrBuilder:
775
+ | UrlPatterns<TEnv>
776
+ | UrlBuilder<TEnv>
777
+ | ClientUrlPatterns,
778
+ ): any {
779
+ // Pure-client mounting shorthand: a clientUrls() definition passed
780
+ // directly NORMALIZES to a root include in the canonical urls() tree —
781
+ // exactly `include("/", definition, { name: "" })`, keeping local route
782
+ // names bare and local patterns app-absolute. This is sugar over the
783
+ // ONE composition model, not a second registration path: it rides the
784
+ // same lazy include materialization, so no ordering, one-definition, or
785
+ // deferral rules exist. Prefixing, wrapping RSC layouts, and middleware
786
+ // scope still come from mounting through include() in urls() yourself.
787
+ if (
788
+ isClientUrlPatterns(patternsOrBuilder) ||
789
+ isClientUrlReference(patternsOrBuilder)
790
+ ) {
791
+ const clientSource = patternsOrBuilder as ClientUrlPatterns;
792
+ patternsOrBuilder = urls(({ include }) => [
793
+ include("/", clientSource, { name: "" }),
794
+ ]) as UrlPatterns<TEnv>;
795
+ }
796
+
760
797
  // Wrap builder functions in urls() automatically
761
798
  const urlPatterns: UrlPatterns<TEnv> =
762
799
  typeof patternsOrBuilder === "function"
@@ -766,6 +803,10 @@ export function createRouter<TEnv = any>(
766
803
  // Store reference for runtime manifest generation
767
804
  storedUrlPatterns = urlPatterns;
768
805
  const currentMountIndex = mountIndex++;
806
+ urlpatternMounts.push({
807
+ patterns: urlPatterns,
808
+ mountIndex: currentMountIndex,
809
+ });
769
810
 
770
811
  // Create manifest and patterns maps for route registration
771
812
  const manifest = new Map<string, EntryData>();
@@ -1095,6 +1136,10 @@ export function createRouter<TEnv = any>(
1095
1136
  return storedUrlPatterns ?? undefined;
1096
1137
  },
1097
1138
 
1139
+ get __urlpatternMounts() {
1140
+ return urlpatternMounts;
1141
+ },
1142
+
1098
1143
  // Expose source file for per-router type generation
1099
1144
  __sourceFile,
1100
1145
 
@@ -44,11 +44,17 @@ export function buildFullPayload(
44
44
  resolvedIds: m.resolvedIds,
45
45
  params: m.params,
46
46
  isPartial: false,
47
+ interceptTargets: m.interceptTargets,
47
48
  rootLayout: ctx.router.rootLayout,
48
49
  // Full render: resolve deferred handle values server-side so SSR markup and
49
50
  // the first sync useHandle read see resolved values. Partial payloads
50
51
  // (rsc-rendering.ts) keep streaming (handleStore.stream()).
51
52
  handles: resolvedHandleStream(handleStore),
53
+ // Post-handler-barrier pushes (streaming loader ctx.handle() writes)
54
+ // stream here; the client applies them after hydration. Instantly
55
+ // complete when the loader lane is idle — including PPR shell capture,
56
+ // where masked loaders never run.
57
+ handlesLate: handleStore.streamLate(),
52
58
  version: ctx.version,
53
59
  prefetchCacheTTL: ctx.router.prefetchCacheTTL,
54
60
  prefetchCacheSize: ctx.router.prefetchCacheSize,
@@ -7,7 +7,6 @@
7
7
  * and progressive enhancement (no-JS form submissions).
8
8
  */
9
9
 
10
- import { createElement } from "react";
11
10
  import { isRouteNotFoundError } from "../errors.js";
12
11
  import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
13
12
  import {
@@ -50,7 +49,10 @@ import { generateNonce, nonce as nonceToken } from "./nonce.js";
50
49
  import { VERSION } from "@rangojs/router:version";
51
50
  import type { ErrorPhase } from "../types.js";
52
51
  import type { RouterRequestInput } from "../router/router-interfaces.js";
53
- import { invokeOnError } from "../router/error-handling.js";
52
+ import {
53
+ invokeOnError,
54
+ resolveDefaultNotFound,
55
+ } from "../router/error-handling.js";
54
56
  import {
55
57
  createReverseFunction,
56
58
  stripInternalParams,
@@ -1309,11 +1311,12 @@ export function createRSCHandler<
1309
1311
  handledByBoundary: true,
1310
1312
  });
1311
1313
 
1312
- const notFoundOption = router.notFound;
1313
- const notFoundComponent =
1314
- typeof notFoundOption === "function"
1315
- ? notFoundOption({ pathname: url.pathname })
1316
- : (notFoundOption ?? createElement("h1", null, "Not Found"));
1314
+ // No boundary to consult: an unmatched route has no entry chain, so
1315
+ // this always lands on the router option or the shared default.
1316
+ const notFoundComponent = resolveDefaultNotFound(
1317
+ router.notFound,
1318
+ url.pathname,
1319
+ );
1317
1320
 
1318
1321
  const notFoundSegment = {
1319
1322
  id: "notFound",
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { getLoaderLazy } from "../server/loader-registry.js";
15
- import { DataNotFoundError } from "../errors.js";
15
+ import { isDataNotFoundError } from "../errors.js";
16
16
  import { executeLoaderMiddleware } from "../router/middleware.js";
17
17
  import { getRequestContext } from "../server/request-context.js";
18
18
  import { observePhase, PHASES } from "../router/instrument.js";
@@ -289,7 +289,7 @@ export async function handleLoaderFetch<TEnv>(
289
289
  // so it does not match the branch above. Map it to a 404 before the generic
290
290
  // 500 coercion so a no-middleware fetchable loader's notFound() is honored
291
291
  // (the with-middleware path resolves it through the notFoundBoundary).
292
- if (error instanceof DataNotFoundError) {
292
+ if (isDataNotFoundError(error)) {
293
293
  return finalizeResponse(
294
294
  createResponseWithMergedHeaders(null, { status: 404 }),
295
295
  );
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Router Manifest Initialization
3
3
  *
4
- * Builds a fresh route trie from router.urlpatterns for dev/HMR scenarios
5
- * where the manifest exists but the trie needs rebuilding.
4
+ * Builds a fresh route trie from every registered URL-pattern mount for
5
+ * dev/HMR scenarios where the manifest exists but the trie needs rebuilding.
6
6
  */
7
7
 
8
8
  import {
@@ -13,11 +13,12 @@ import {
13
13
  setRouterTrie,
14
14
  setRouterPrecomputedEntries,
15
15
  } from "../route-map-builder.js";
16
+ import { mergeFullManifests } from "../build/merge-full-manifests.js";
16
17
 
17
18
  /**
18
- * Build a fresh route trie from router.urlpatterns and store it in the
19
- * per-router cache. Also sets the per-router manifest and merges into
20
- * the global manifest for reverse()/href().
19
+ * Build a fresh route trie from the router's URL-pattern mounts and store it in
20
+ * the per-router cache. Also sets the per-router manifest and merges into the
21
+ * global manifest for reverse()/href().
21
22
  *
22
23
  * Called when manifest data may exist but the per-router trie is missing,
23
24
  * which happens in dev mode after HMR: the virtual module sets the manifest
@@ -30,10 +31,28 @@ export async function buildRouterTrieFromUrlpatterns(
30
31
  ): Promise<void> {
31
32
  const { generateManifestFull } =
32
33
  await import("../build/generate-manifest.js");
33
- const generated = await generateManifestFull(router.urlpatterns, undefined, {
34
- routerId: router.id,
35
- ...(router.basename ? { urlPrefix: router.basename } : {}),
36
- });
34
+ const mounts =
35
+ router.__urlpatternMounts ??
36
+ (router.urlpatterns
37
+ ? [{ patterns: router.urlpatterns, mountIndex: undefined }]
38
+ : []);
39
+ // mount.mountIndex mirrors the live router's per-mount counter partitioning
40
+ // (context.ts getShortCode `M{i}` prefixes), so entries generated here carry
41
+ // the same shortCodes as runtime registration. Build discovery
42
+ // (discover-routers.ts / runtime-discovery.ts) numbers mounts GLOBALLY across
43
+ // routers instead — that divergence is invisible: shortCodes never leave
44
+ // their realm, and route names (the trie keys) carry no mount index (unnamed
45
+ // routes are `$path_*`, see path-helper.ts).
46
+ const mountManifests = [];
47
+ for (const mount of mounts) {
48
+ mountManifests.push(
49
+ await generateManifestFull(mount.patterns, mount.mountIndex, {
50
+ routerId: router.id,
51
+ ...(router.basename ? { urlPrefix: router.basename } : {}),
52
+ }),
53
+ );
54
+ }
55
+ const generated = mergeFullManifests(mountManifests);
37
56
  // Build the trie through the SAME shared helper the production discovery uses
38
57
  // (discover-routers.ts), so the dev runtime-rebuilt trie and the prod
39
58
  // serialized trie cannot drift. buildPerRouterTrie returns null when there
@@ -57,6 +57,7 @@ import {
57
57
  SHELL_STATUS_HEADER,
58
58
  resolvePprConfig,
59
59
  buildShellKey,
60
+ shellSearchSeed,
60
61
  isValidShellHit,
61
62
  hasIntactShellPayload,
62
63
  base64ToBytes,
@@ -153,12 +154,14 @@ function buildNavigationShellKey(
153
154
  function createShellCaptureDescriptor(
154
155
  ctx: HandlerContext<any>,
155
156
  key: string,
157
+ searchSeed: string,
156
158
  pprConfig: ResolvedPprConfig,
157
159
  store: SegmentCacheStore<any>,
158
160
  navigationOnly?: true,
159
161
  ): ShellCaptureDescriptor {
160
162
  return {
161
163
  key,
164
+ searchSeed,
162
165
  buildVersion: ctx.version,
163
166
  ttl: pprConfig.ttl,
164
167
  swr: pprConfig.swr,
@@ -512,7 +515,13 @@ function* shellServePlan<TEnv>(
512
515
  ) {
513
516
  return { kind: "pass" };
514
517
  }
515
- const descriptor = createShellCaptureDescriptor(ctx, key, pprConfig, store);
518
+ const descriptor = createShellCaptureDescriptor(
519
+ ctx,
520
+ key,
521
+ shellSearchSeed(url, reqCtx._searchParamsFilter),
522
+ pprConfig,
523
+ store,
524
+ );
516
525
 
517
526
  if (
518
527
  cached &&
@@ -699,6 +708,7 @@ function* preparePayloadPlan<TEnv>(
699
708
  params: result.params,
700
709
  isPartial: true,
701
710
  slots: result.slots,
711
+ interceptTargets: result.interceptTargets,
702
712
  handles: handleStore.stream(),
703
713
  version: ctx.version,
704
714
  prefetchCacheTTL: ctx.router.prefetchCacheTTL,
@@ -993,6 +1003,7 @@ function scheduleNavigationShellCapture<TEnv>(
993
1003
  ctx,
994
1004
  partialCaptureKey ??
995
1005
  buildNavigationShellKey(url, reqCtx._searchParamsFilter),
1006
+ shellSearchSeed(url, reqCtx._searchParamsFilter),
996
1007
  pprConfig,
997
1008
  store,
998
1009
  true,
@@ -1510,6 +1521,9 @@ function serveShellHit(
1510
1521
  ssrModule.resumeShellHTML!(rscStream, {
1511
1522
  postponed: entry.postponed,
1512
1523
  nonce: undefined,
1524
+ // The shell key's own search — identical to the capture seed for
1525
+ // this key, so the resume tree matches the captured tree.
1526
+ search: shellSearchSeed(url, reqCtx._searchParamsFilter),
1513
1527
  }),
1514
1528
  );
1515
1529
  };
@@ -907,6 +907,15 @@ export function gateFlightForCapture(
907
907
  */
908
908
  export interface ShellCaptureDescriptor {
909
909
  key: string;
910
+ /**
911
+ * The key's search portion (shellSearchSeed: `?`-prefixed sorted search,
912
+ * cache.searchParams filter applied, or ""). Seeds the capture render's SSR
913
+ * store so static-part `useSearchParams` reads bake markup consistent with
914
+ * the shell's own key; the resume pass derives the identical string from
915
+ * the HIT request. Computed next to the key so the two cannot drift.
916
+ * Omitted (build-time bare-pathname captures) = search-less, same as "".
917
+ */
918
+ searchSeed?: string;
910
919
  /**
911
920
  * The RSC handler's build version (HandlerContext.version), stamped into the
912
921
  * stored entry as ShellCacheEntry.buildVersion — the serve-side
@@ -1753,6 +1762,9 @@ async function captureAndStoreShell(
1753
1762
  captureShellHTML(gate.stream, {
1754
1763
  quiesce,
1755
1764
  maxWaitMs: capture.captureTimeout ?? SHELL_CAPTURE_MAX_WAIT_MS,
1765
+ // The shell key's own search seeds the capture render's store —
1766
+ // static-part search reads bake what the key names.
1767
+ search: capture.searchSeed,
1756
1768
  }),
1757
1769
  );
1758
1770
  } catch (error) {
@@ -129,9 +129,22 @@ export function resolvePprConfig(
129
129
  * and lookup makes every shell request a permanent miss.
130
130
  */
131
131
  export function buildShellKey(url: URL, filter?: SearchParamsFilter): string {
132
+ return `${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
133
+ }
134
+
135
+ /**
136
+ * The shell key's search portion (`?`-prefixed sorted search with the
137
+ * cache.searchParams filter applied, or "") — ALSO the string the capture and
138
+ * resume SSR renders seed their store location with (SSRRenderOptions.search
139
+ * / ShellCaptureOptions.search / ShellResumeOptions.search). Search is part
140
+ * of shell identity, so static-part `useSearchParams` reads render exactly
141
+ * what the key names; deriving seed and key from this one helper is what
142
+ * keeps capture, resume, and lookup byte-agreed (drift = replay mismatch or
143
+ * permanent MISS).
144
+ */
145
+ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
132
146
  const sorted = sortedSearchString(url.searchParams, filter);
133
- const searchSuffix = sorted ? `?${sorted}` : "";
134
- return `${url.host}${url.pathname}${searchSuffix}:shell`;
147
+ return sorted ? `?${sorted}` : "";
135
148
  }
136
149
 
137
150
  /**
@@ -111,7 +111,16 @@ export function createSsrHtmlStage<TEnv>(
111
111
  );
112
112
  return {
113
113
  render: () =>
114
- ssrModule.renderHTML(flight.stream, { ...options.render, streamMode }),
114
+ // search: the LIVE request's query string, threaded out-of-band into
115
+ // the SSR pass so useSearchParams sees real values during document
116
+ // renders. Deliberately not payload metadata: cached/prerendered
117
+ // payloads replay captured metadata, and search is not route
118
+ // identity — it must always come from the request being served.
119
+ ssrModule.renderHTML(flight.stream, {
120
+ ...options.render,
121
+ streamMode,
122
+ search: options.url.search,
123
+ }),
115
124
  ...(options.init && { init: options.init }),
116
125
  };
117
126
  };
package/src/rsc/types.ts CHANGED
@@ -35,10 +35,27 @@ export interface RscPayload {
35
35
  /** Merged route params from the matched route */
36
36
  params?: Record<string, string>;
37
37
  slots?: Record<string, SlotState>;
38
+ /**
39
+ * Intercept TARGET route names reachable from this location as a
40
+ * navigation origin (see MatchResult.interceptTargets). The browser-local
41
+ * clientUrls matcher declines optimistic presentation for these.
42
+ */
43
+ interceptTargets?: string[];
38
44
  /** Root layout component for browser-side re-renders (client component reference) */
39
45
  rootLayout?: React.ComponentType<{ children: React.ReactNode }>;
40
46
  /** Handle data accumulated across route segments (async generator that yields on each push) */
41
47
  handles?: AsyncGenerator<HandleData, void, unknown>;
48
+ /**
49
+ * Document-lane late handle channel: full-state updates for pushes landing
50
+ * AFTER the handler barrier (streaming loader bodies writing handles
51
+ * mid-body). `handles` above is drained to completion in blocking
52
+ * positions (SSR seed, pre-hydration), so late pushes need this separate
53
+ * generator, consumed non-blocking post-hydration (rsc-router.tsx).
54
+ * Empty/instantly-complete when no auxiliary-lane work is pending at the
55
+ * handler barrier. Full payloads only — partial payloads' `handles`
56
+ * generator streams to full settle already.
57
+ */
58
+ handlesLate?: AsyncGenerator<HandleData, void, unknown>;
42
59
  /** RSC version string for cache invalidation */
43
60
  version?: string;
44
61
  /** Cloudflare dev worker generation used for stale-document convergence. */
@@ -158,6 +175,18 @@ export interface SSRRenderOptions {
158
175
  * - `"allReady"` — await `stream.allReady` before returning.
159
176
  */
160
177
  streamMode?: import("../router/router-options.js").SSRStreamMode;
178
+
179
+ /**
180
+ * The live request's query string (`url.search`, `?`-prefixed or empty).
181
+ * Seeds the SSR navigation store so `useSearchParams` (and
182
+ * `useNavigation().location`) carry real values during document renders.
183
+ * Out-of-band by design — never payload metadata, which cached/prerendered
184
+ * payloads replay; search is not route identity. The build-time prerender
185
+ * pass passes none — build shells capture bare pathnames and serve
186
+ * search-less requests only (runtime ppr captures seed the shell key's
187
+ * search; see ShellCaptureOptions.search in the SSR entry).
188
+ */
189
+ search?: string;
161
190
  }
162
191
 
163
192
  /**
@@ -179,7 +208,7 @@ export interface SSRModule {
179
208
  */
180
209
  captureShellHTML?: (
181
210
  rscStream: ReadableStream<Uint8Array>,
182
- options: { quiesce: Promise<void>; maxWaitMs?: number },
211
+ options: { quiesce: Promise<void>; maxWaitMs?: number; search?: string },
183
212
  ) => Promise<{ prelude: Uint8Array; postponed: string | null } | null>;
184
213
 
185
214
  /**
@@ -192,7 +221,7 @@ export interface SSRModule {
192
221
  */
193
222
  resumeShellHTML?: (
194
223
  rscStream: ReadableStream<Uint8Array>,
195
- options: { postponed: string | null; nonce?: string },
224
+ options: { postponed: string | null; nonce?: string; search?: string },
196
225
  ) => Promise<ReadableStream<Uint8Array>>;
197
226
  }
198
227