@rangojs/router 0.0.0-experimental.dfdb0387 → 0.0.0-experimental.e16b7c00

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 (237) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/vite/index.js +2106 -842
  4. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  5. package/package.json +13 -8
  6. package/skills/breadcrumbs/SKILL.md +3 -1
  7. package/skills/bundle-analysis/SKILL.md +159 -0
  8. package/skills/cache-guide/SKILL.md +222 -30
  9. package/skills/caching/SKILL.md +188 -8
  10. package/skills/composability/SKILL.md +27 -2
  11. package/skills/document-cache/SKILL.md +78 -55
  12. package/skills/handler-use/SKILL.md +364 -0
  13. package/skills/hooks/SKILL.md +229 -20
  14. package/skills/host-router/SKILL.md +45 -20
  15. package/skills/i18n/SKILL.md +276 -0
  16. package/skills/intercept/SKILL.md +46 -4
  17. package/skills/layout/SKILL.md +28 -7
  18. package/skills/links/SKILL.md +247 -17
  19. package/skills/loader/SKILL.md +219 -9
  20. package/skills/middleware/SKILL.md +47 -12
  21. package/skills/migrate-nextjs/SKILL.md +582 -0
  22. package/skills/migrate-react-router/SKILL.md +769 -0
  23. package/skills/mime-routes/SKILL.md +27 -0
  24. package/skills/observability/SKILL.md +137 -0
  25. package/skills/parallel/SKILL.md +71 -6
  26. package/skills/prerender/SKILL.md +14 -33
  27. package/skills/rango/SKILL.md +236 -22
  28. package/skills/react-compiler/SKILL.md +168 -0
  29. package/skills/response-routes/SKILL.md +66 -9
  30. package/skills/route/SKILL.md +57 -4
  31. package/skills/router-setup/SKILL.md +3 -3
  32. package/skills/server-actions/SKILL.md +751 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/typesafety/SKILL.md +319 -27
  35. package/skills/use-cache/SKILL.md +36 -5
  36. package/skills/view-transitions/SKILL.md +294 -0
  37. package/src/__augment-tests__/augment.ts +81 -0
  38. package/src/__augment-tests__/augmented.check.ts +117 -0
  39. package/src/browser/action-coordinator.ts +53 -36
  40. package/src/browser/app-shell.ts +52 -0
  41. package/src/browser/event-controller.ts +86 -70
  42. package/src/browser/history-state.ts +21 -0
  43. package/src/browser/index.ts +3 -3
  44. package/src/browser/navigation-bridge.ts +86 -11
  45. package/src/browser/navigation-client.ts +45 -25
  46. package/src/browser/navigation-store.ts +32 -9
  47. package/src/browser/navigation-transaction.ts +10 -28
  48. package/src/browser/partial-update.ts +61 -28
  49. package/src/browser/prefetch/cache.ts +124 -26
  50. package/src/browser/prefetch/fetch.ts +129 -37
  51. package/src/browser/prefetch/queue.ts +36 -5
  52. package/src/browser/rango-state.ts +53 -13
  53. package/src/browser/react/Link.tsx +18 -13
  54. package/src/browser/react/NavigationProvider.tsx +72 -31
  55. package/src/browser/react/filter-segment-order.ts +51 -7
  56. package/src/browser/react/index.ts +3 -0
  57. package/src/browser/react/location-state-shared.ts +175 -4
  58. package/src/browser/react/location-state.ts +39 -13
  59. package/src/browser/react/use-handle.ts +17 -9
  60. package/src/browser/react/use-navigation.ts +22 -2
  61. package/src/browser/react/use-params.ts +20 -8
  62. package/src/browser/react/use-reverse.ts +106 -0
  63. package/src/browser/react/use-router.ts +22 -2
  64. package/src/browser/react/use-segments.ts +11 -8
  65. package/src/browser/response-adapter.ts +25 -0
  66. package/src/browser/rsc-router.tsx +64 -22
  67. package/src/browser/scroll-restoration.ts +22 -14
  68. package/src/browser/segment-reconciler.ts +10 -14
  69. package/src/browser/segment-structure-assert.ts +2 -2
  70. package/src/browser/server-action-bridge.ts +23 -30
  71. package/src/browser/types.ts +21 -0
  72. package/src/build/collect-fallback-refs.ts +107 -0
  73. package/src/build/generate-manifest.ts +60 -35
  74. package/src/build/generate-route-types.ts +2 -0
  75. package/src/build/index.ts +2 -0
  76. package/src/build/route-trie.ts +52 -25
  77. package/src/build/route-types/codegen.ts +4 -4
  78. package/src/build/route-types/include-resolution.ts +1 -1
  79. package/src/build/route-types/per-module-writer.ts +7 -4
  80. package/src/build/route-types/router-processing.ts +55 -14
  81. package/src/build/route-types/scan-filter.ts +1 -1
  82. package/src/build/route-types/source-scan.ts +118 -0
  83. package/src/build/runtime-discovery.ts +9 -20
  84. package/src/cache/cache-error.ts +104 -0
  85. package/src/cache/cache-policy.ts +95 -1
  86. package/src/cache/cache-runtime.ts +79 -13
  87. package/src/cache/cache-scope.ts +77 -46
  88. package/src/cache/cache-tag.ts +135 -0
  89. package/src/cache/cf/cf-cache-store.ts +1067 -176
  90. package/src/cache/cf/index.ts +4 -1
  91. package/src/cache/document-cache.ts +59 -7
  92. package/src/cache/index.ts +6 -0
  93. package/src/cache/memory-segment-store.ts +158 -14
  94. package/src/cache/tag-invalidation.ts +206 -0
  95. package/src/cache/types.ts +27 -0
  96. package/src/client.rsc.tsx +3 -0
  97. package/src/client.tsx +92 -182
  98. package/src/context-var.ts +5 -5
  99. package/src/decode-loader-results.ts +36 -0
  100. package/src/errors.ts +30 -1
  101. package/src/handle.ts +4 -6
  102. package/src/host/index.ts +2 -2
  103. package/src/host/router.ts +129 -57
  104. package/src/host/types.ts +31 -2
  105. package/src/host/utils.ts +1 -1
  106. package/src/href-client.ts +140 -20
  107. package/src/index.rsc.ts +16 -4
  108. package/src/index.ts +65 -15
  109. package/src/loader-store.ts +500 -0
  110. package/src/loader.rsc.ts +2 -5
  111. package/src/loader.ts +3 -10
  112. package/src/missing-id-error.ts +68 -0
  113. package/src/outlet-context.ts +1 -1
  114. package/src/prerender.ts +4 -4
  115. package/src/response-utils.ts +37 -0
  116. package/src/reverse.ts +65 -36
  117. package/src/route-content-wrapper.tsx +6 -28
  118. package/src/route-definition/dsl-helpers.ts +384 -257
  119. package/src/route-definition/helper-factories.ts +29 -139
  120. package/src/route-definition/helpers-types.ts +100 -28
  121. package/src/route-definition/resolve-handler-use.ts +6 -0
  122. package/src/route-definition/use-item-types.ts +32 -0
  123. package/src/route-types.ts +26 -41
  124. package/src/router/content-negotiation.ts +15 -2
  125. package/src/router/error-handling.ts +1 -1
  126. package/src/router/handler-context.ts +21 -38
  127. package/src/router/intercept-resolution.ts +4 -18
  128. package/src/router/lazy-includes.ts +8 -8
  129. package/src/router/loader-resolution.ts +19 -2
  130. package/src/router/manifest.ts +22 -13
  131. package/src/router/match-api.ts +4 -3
  132. package/src/router/match-handlers.ts +1 -0
  133. package/src/router/match-middleware/cache-lookup.ts +46 -92
  134. package/src/router/match-middleware/cache-store.ts +3 -2
  135. package/src/router/match-result.ts +53 -32
  136. package/src/router/metrics.ts +1 -1
  137. package/src/router/middleware-types.ts +15 -26
  138. package/src/router/middleware.ts +99 -84
  139. package/src/router/pattern-matching.ts +101 -17
  140. package/src/router/prerender-match.ts +3 -1
  141. package/src/router/preview-match.ts +3 -1
  142. package/src/router/request-classification.ts +4 -28
  143. package/src/router/revalidation.ts +58 -2
  144. package/src/router/router-interfaces.ts +45 -28
  145. package/src/router/router-options.ts +25 -1
  146. package/src/router/router-registry.ts +2 -5
  147. package/src/router/segment-resolution/fresh.ts +27 -6
  148. package/src/router/segment-resolution/loader-cache.ts +8 -17
  149. package/src/router/segment-resolution/revalidation.ts +147 -106
  150. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  151. package/src/router/substitute-pattern-params.ts +56 -0
  152. package/src/router/trie-matching.ts +18 -13
  153. package/src/router/types.ts +8 -0
  154. package/src/router/url-params.ts +49 -0
  155. package/src/router.ts +23 -18
  156. package/src/rsc/handler-context.ts +2 -2
  157. package/src/rsc/handler.ts +38 -70
  158. package/src/rsc/helpers.ts +72 -43
  159. package/src/rsc/index.ts +1 -1
  160. package/src/rsc/origin-guard.ts +28 -10
  161. package/src/rsc/progressive-enhancement.ts +4 -0
  162. package/src/rsc/response-route-handler.ts +54 -54
  163. package/src/rsc/rsc-rendering.ts +35 -51
  164. package/src/rsc/runtime-warnings.ts +9 -10
  165. package/src/rsc/server-action.ts +17 -37
  166. package/src/rsc/ssr-setup.ts +16 -0
  167. package/src/rsc/types.ts +8 -2
  168. package/src/search-params.ts +4 -4
  169. package/src/segment-content-promise.ts +67 -0
  170. package/src/segment-loader-promise.ts +122 -0
  171. package/src/segment-system.tsx +132 -116
  172. package/src/serialize.ts +243 -0
  173. package/src/server/context.ts +143 -53
  174. package/src/server/cookie-store.ts +28 -4
  175. package/src/server/request-context.ts +46 -44
  176. package/src/ssr/index.tsx +5 -1
  177. package/src/static-handler.ts +1 -1
  178. package/src/types/cache-types.ts +13 -4
  179. package/src/types/error-types.ts +5 -1
  180. package/src/types/global-namespace.ts +39 -26
  181. package/src/types/handler-context.ts +68 -50
  182. package/src/types/index.ts +1 -0
  183. package/src/types/loader-types.ts +5 -6
  184. package/src/types/request-scope.ts +126 -0
  185. package/src/types/route-entry.ts +11 -0
  186. package/src/types/segments.ts +35 -2
  187. package/src/urls/include-helper.ts +34 -67
  188. package/src/urls/index.ts +0 -3
  189. package/src/urls/path-helper-types.ts +41 -7
  190. package/src/urls/path-helper.ts +17 -52
  191. package/src/urls/pattern-types.ts +36 -19
  192. package/src/urls/response-types.ts +22 -29
  193. package/src/urls/type-extraction.ts +26 -116
  194. package/src/urls/urls-function.ts +1 -5
  195. package/src/use-loader.tsx +413 -42
  196. package/src/vite/debug.ts +185 -0
  197. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  198. package/src/vite/discovery/discover-routers.ts +101 -51
  199. package/src/vite/discovery/discovery-errors.ts +194 -0
  200. package/src/vite/discovery/gate-state.ts +171 -0
  201. package/src/vite/discovery/prerender-collection.ts +67 -26
  202. package/src/vite/discovery/route-types-writer.ts +40 -84
  203. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  204. package/src/vite/discovery/state.ts +33 -0
  205. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  206. package/src/vite/index.ts +2 -0
  207. package/src/vite/plugin-types.ts +67 -0
  208. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  209. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  210. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  211. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  212. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  213. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  214. package/src/vite/plugins/expose-action-id.ts +54 -30
  215. package/src/vite/plugins/expose-id-utils.ts +12 -8
  216. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  217. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  218. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  219. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  220. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  221. package/src/vite/plugins/performance-tracks.ts +29 -25
  222. package/src/vite/plugins/use-cache-transform.ts +65 -50
  223. package/src/vite/plugins/version-injector.ts +39 -23
  224. package/src/vite/plugins/version-plugin.ts +59 -2
  225. package/src/vite/plugins/virtual-entries.ts +2 -2
  226. package/src/vite/rango.ts +116 -29
  227. package/src/vite/router-discovery.ts +750 -100
  228. package/src/vite/utils/ast-handler-extract.ts +15 -15
  229. package/src/vite/utils/banner.ts +1 -1
  230. package/src/vite/utils/bundle-analysis.ts +4 -2
  231. package/src/vite/utils/client-chunks.ts +190 -0
  232. package/src/vite/utils/forward-user-plugins.ts +193 -0
  233. package/src/vite/utils/manifest-utils.ts +21 -5
  234. package/src/vite/utils/package-resolution.ts +41 -1
  235. package/src/vite/utils/prerender-utils.ts +21 -6
  236. package/src/vite/utils/shared-utils.ts +107 -26
  237. package/src/browser/action-response-classifier.ts +0 -99
@@ -25,6 +25,7 @@ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
25
25
  import {
26
26
  extractRscHeaderUrl,
27
27
  emptyResponse,
28
+ handleReloadHeader,
28
29
  teeWithCompletion,
29
30
  } from "./response-adapter.js";
30
31
  import { mergeLocationState } from "./history-state.js";
@@ -77,6 +78,20 @@ export function createServerActionBridge(
77
78
  onNavigate,
78
79
  } = config;
79
80
 
81
+ // SPA-navigate when onNavigate is set, else hard-reload. state is omitted (not
82
+ // passed as undefined) to match the header path's prior call shape.
83
+ async function dispatchRedirect(url: string, state?: unknown): Promise<void> {
84
+ if (onNavigate) {
85
+ await onNavigate(url, {
86
+ ...(state !== undefined ? { state } : {}),
87
+ replace: true,
88
+ _skipCache: true,
89
+ });
90
+ } else {
91
+ window.location.href = url;
92
+ }
93
+ }
94
+
80
95
  let isRegistered = false;
81
96
 
82
97
  const fetchPartialUpdate = createPartialUpdater({
@@ -222,18 +237,12 @@ export function createServerActionBridge(
222
237
  handle.signal.removeEventListener("abort", onHandleAbort);
223
238
 
224
239
  // Check for version mismatch - server wants us to reload
225
- const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
226
- if (reload === "blocked") {
227
- resolveStreamComplete();
228
- return emptyResponse();
229
- }
230
- if (reload) {
231
- log("version mismatch on action, reloading", {
232
- reloadUrl: reload.url,
233
- });
234
- window.location.href = reload.url;
235
- return new Promise<Response>(() => {});
236
- }
240
+ const reloadResult = handleReloadHeader(response, {
241
+ onBlocked: resolveStreamComplete,
242
+ onReload: (url) =>
243
+ log("version mismatch on action, reloading", { reloadUrl: url }),
244
+ });
245
+ if (reloadResult) return reloadResult;
237
246
 
238
247
  // Simple redirect from action (no state, no RSC payload).
239
248
  // Short-circuits before createFromFetch — no Flight deserialization needed.
@@ -243,14 +252,7 @@ export function createServerActionBridge(
243
252
  if (redirect && redirect !== "blocked" && !handle.signal.aborted) {
244
253
  log("action simple redirect", { url: redirect.url });
245
254
  handle.complete(undefined);
246
- if (onNavigate) {
247
- await onNavigate(redirect.url, {
248
- replace: true,
249
- _skipCache: true,
250
- });
251
- } else {
252
- window.location.href = redirect.url;
253
- }
255
+ await dispatchRedirect(redirect.url);
254
256
  return new Promise<Response>(() => {});
255
257
  }
256
258
  if (redirect === "blocked") {
@@ -339,18 +341,9 @@ export function createServerActionBridge(
339
341
  handle.complete(returnValue?.data);
340
342
  return returnValue?.data;
341
343
  }
342
- const redirectState = metadata.locationState;
343
344
  log("action redirect", { url: redirectUrl });
344
345
  handle.complete(returnValue?.data);
345
- if (onNavigate) {
346
- await onNavigate(redirectUrl, {
347
- state: redirectState,
348
- replace: true,
349
- _skipCache: true,
350
- });
351
- } else {
352
- window.location.href = redirectUrl;
353
- }
346
+ await dispatchRedirect(redirectUrl, metadata.locationState);
354
347
  return returnValue?.data;
355
348
  }
356
349
 
@@ -39,6 +39,12 @@ export interface RscMetadata {
39
39
  isError?: boolean;
40
40
  matched?: string[];
41
41
  diff?: string[];
42
+ /**
43
+ * All segment ids re-resolved on the server, including null-component
44
+ * ones excluded from `segments`/`diff`. Drives client-side handle-bucket
45
+ * cleanup. Superset of `diff`. See MatchResult.resolvedIds.
46
+ */
47
+ resolvedIds?: string[];
42
48
  /** Merged route params from the matched route */
43
49
  params?: Record<string, string>;
44
50
  /**
@@ -427,6 +433,12 @@ export interface NavigationStore {
427
433
  markCacheAsStale(): void;
428
434
  markCacheAsStaleAndBroadcast(): void;
429
435
  clearHistoryCache(): void;
436
+ /**
437
+ * Clear this tab's nav + prefetch caches without broadcasting or rotating
438
+ * shared state. Intended for app-switch transitions that affect only this
439
+ * tab's session.
440
+ */
441
+ clearHistoryCacheLocal(): void;
430
442
  broadcastCacheInvalidation(): void;
431
443
 
432
444
  // Cross-tab refresh callback (set by navigation bridge)
@@ -540,8 +552,17 @@ export interface NavigationBridge {
540
552
  refresh(): Promise<void>;
541
553
  handlePopstate(): Promise<void>;
542
554
  registerLinkInterception(): () => void;
555
+ /** Current RSC version (live, reflects the latest updateVersion). */
556
+ getVersion(): string | undefined;
543
557
  /** Update the RSC version (e.g. after HMR). Clears prefetch cache. */
544
558
  updateVersion(newVersion: string): void;
559
+ /**
560
+ * Replace the active app-shell snapshot (rootLayout, basename, version)
561
+ * atomically. Used on cross-app navigations when the response's routerId
562
+ * indicates the user entered a different app. Theme, warmup, and prefetch
563
+ * TTL are document-lifetime and not part of the shell.
564
+ */
565
+ updateAppShell(next: import("./app-shell.js").AppShell): void;
545
566
  }
546
567
 
547
568
  /**
@@ -0,0 +1,107 @@
1
+ // Collect the `"use client"` client-reference keys reachable from an error /
2
+ // notFound boundary registration, for routing them into the dedicated
3
+ // `app-fallback` chunk (see vite/utils/client-chunks.ts).
4
+ //
5
+ // A boundary registration is not always a bare client element. The common,
6
+ // load-bearing pattern wraps the client boundary in providers a thrown handler
7
+ // needs (the layout that would normally supply them did not mount):
8
+ //
9
+ // defaultErrorBoundary: ({ error }) => (
10
+ // <FallbackIntl locales={...}>
11
+ // <ThemedError error={error} /> // <- the real "use client" boundary
12
+ // </FallbackIntl>
13
+ // )
14
+ //
15
+ // So the value may be (a) a handler FUNCTION returning a tree, or (b) an element
16
+ // tree with the client boundary nested below server wrappers. We:
17
+ // 1. If it's a function, CALL it with synthetic props to get the returned tree.
18
+ // This only constructs JSX — the inner components are element `type`s, never
19
+ // invoked — so no hooks run. Guarded: a boundary that needs a real render
20
+ // context (request globals, etc.) throws and is skipped (graceful: it simply
21
+ // stays on the default grouping, as before).
22
+ // 2. Walk the resulting tree and report every element whose `.type` is a
23
+ // plugin-rsc client reference.
24
+ //
25
+ // Limit: a boundary that *conditionally* renders different client components based
26
+ // on the runtime error cannot be resolved statically — only the branch taken with
27
+ // the synthetic error is seen. Such cases fall back to the default chunk; the
28
+ // custom `clientChunks` function is the escape hatch.
29
+
30
+ const CLIENT_REF = Symbol.for("react.client.reference");
31
+ const MAX_DEPTH = 40;
32
+
33
+ // Synthetic props covering the error-boundary (`{ error, reset }`) and notFound
34
+ // (`{ pathname }`) handler shapes. The handler destructures what it needs.
35
+ const SYNTHETIC_PROPS = {
36
+ error: new Error("rango: build-time fallback-chunk discovery"),
37
+ reset: () => {},
38
+ pathname: "/",
39
+ info: { componentStack: "" },
40
+ };
41
+
42
+ interface MaybeElement {
43
+ type?: { $$typeof?: symbol; $$id?: string };
44
+ props?: Record<string, unknown>;
45
+ }
46
+
47
+ function isReactNodeLike(v: unknown): boolean {
48
+ return (
49
+ Array.isArray(v) ||
50
+ (typeof v === "object" && v !== null && "$$typeof" in (v as object))
51
+ );
52
+ }
53
+
54
+ function walkElementTree(
55
+ node: unknown,
56
+ report: (refKey: string) => void,
57
+ depth: number,
58
+ ): void {
59
+ if (node == null || depth > MAX_DEPTH) return;
60
+ if (Array.isArray(node)) {
61
+ for (const child of node) walkElementTree(child, report, depth + 1);
62
+ return;
63
+ }
64
+ if (typeof node !== "object") return;
65
+
66
+ const el = node as MaybeElement;
67
+ const type = el.type;
68
+ if (type?.$$typeof === CLIENT_REF && typeof type.$$id === "string") {
69
+ // $$id is `<referenceKey>#<exportName>` in build mode — keep the referenceKey.
70
+ report(type.$$id.split("#")[0]);
71
+ }
72
+
73
+ const props = el.props;
74
+ if (props && typeof props === "object") {
75
+ // Children are always nodes; other props are followed only when they look
76
+ // like React nodes (slots/icons), never arbitrary data objects.
77
+ walkElementTree(props.children, report, depth + 1);
78
+ for (const key in props) {
79
+ if (key === "children") continue;
80
+ const value = props[key];
81
+ if (isReactNodeLike(value)) walkElementTree(value, report, depth + 1);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Report every `"use client"` client-reference key reachable from a single
88
+ * error/notFound boundary registration (handler function or element tree).
89
+ */
90
+ export function collectFallbackClientRefs(
91
+ boundary: unknown,
92
+ report: (refKey: string) => void,
93
+ ): void {
94
+ try {
95
+ let node = boundary;
96
+ if (typeof node === "function") {
97
+ node = (node as (props: unknown) => unknown)(SYNTHETIC_PROPS);
98
+ }
99
+ walkElementTree(node, report, 0);
100
+ } catch {
101
+ // The boundary needs a real render context (request globals, hooks at the
102
+ // top level) or its tree has hostile getters. Its client refs can't be
103
+ // resolved statically — skip. It stays on the default grouping (no
104
+ // regression vs. not collecting), and the custom clientChunks fn is the
105
+ // escape hatch for such cases.
106
+ }
107
+ }
@@ -11,11 +11,12 @@
11
11
  import type { UrlPatterns } from "../urls.js";
12
12
  import type { AllUseItems } from "../route-types.js";
13
13
  import { extractStaticPrefix } from "../router/pattern-matching.js";
14
- import { RSCRouterContext, runWithPrefixes } from "../server/context.js";
14
+ import { RangoContext, runWithPrefixes } from "../server/context.js";
15
15
  import type { EntryData, TrackedInclude } from "../server/context.js";
16
16
  import type { TrailingSlashMode } from "../types.js";
17
17
  import { createRouteHelpers } from "../route-definition.js";
18
18
  import MapRootLayout from "../server/root-layout.js";
19
+ import { collectFallbackClientRefs } from "./collect-fallback-refs.js";
19
20
 
20
21
  /**
21
22
  * Node in the prefix tree
@@ -57,6 +58,26 @@ export interface GeneratedManifest {
57
58
  * Build prefix tree node by running the patterns with proper context.
58
59
  * Uses a visited set to detect circular includes and prevent infinite recursion.
59
60
  */
61
+ // Merge tracked nested includes into `target`. Multiple includes can share a
62
+ // fullPrefix (e.g. include("/", a), include("/", b)) — concat their routes and
63
+ // Object.assign children rather than overwrite.
64
+ function mergeIncludeNodes(
65
+ target: Record<string, PrefixTreeNode>,
66
+ includes: TrackedInclude[],
67
+ buildChild: (include: TrackedInclude) => PrefixTreeNode,
68
+ ): void {
69
+ for (const include of includes) {
70
+ const node = buildChild(include);
71
+ const existing = target[include.fullPrefix];
72
+ if (existing) {
73
+ existing.routes.push(...node.routes);
74
+ Object.assign(existing.children, node.children);
75
+ } else {
76
+ target[include.fullPrefix] = node;
77
+ }
78
+ }
79
+ }
80
+
60
81
  function buildPrefixTreeNode(
61
82
  urlPrefix: string,
62
83
  namePrefix: string | undefined,
@@ -93,7 +114,7 @@ function buildPrefixTreeNode(
93
114
  const searchSchemasMap = new Map<string, Record<string, string>>();
94
115
  const trackedIncludes: TrackedInclude[] = [];
95
116
 
96
- RSCRouterContext.run(
117
+ RangoContext.run(
97
118
  {
98
119
  manifest,
99
120
  patterns: patternsMap,
@@ -166,13 +187,9 @@ function buildPrefixTreeNode(
166
187
  }
167
188
  }
168
189
 
169
- // Build children from tracked nested includes.
170
- // Multiple includes can share the same fullPrefix (e.g., include("/", patternsA),
171
- // include("/", patternsB)). Merge their routes instead of overwriting.
172
190
  const children: Record<string, PrefixTreeNode> = {};
173
-
174
- for (const include of trackedIncludes) {
175
- const childNode = buildPrefixTreeNode(
191
+ mergeIncludeNodes(children, trackedIncludes, (include) =>
192
+ buildPrefixTreeNode(
176
193
  include.fullPrefix,
177
194
  include.namePrefix,
178
195
  include.patterns as UrlPatterns<any>,
@@ -186,16 +203,8 @@ function buildPrefixTreeNode(
186
203
  passthroughRoutes,
187
204
  responseTypeRoutes,
188
205
  routeSearchSchemas,
189
- );
190
-
191
- const existing = children[include.fullPrefix];
192
- if (existing) {
193
- existing.routes.push(...childNode.routes);
194
- Object.assign(existing.children, childNode.children);
195
- } else {
196
- children[include.fullPrefix] = childNode;
197
- }
198
- }
206
+ ),
207
+ );
199
208
 
200
209
  // Remove from visited so sibling branches can reuse the same patterns
201
210
  // without false circular-include detection. Only ancestors in the current
@@ -282,7 +291,17 @@ export function generateManifest<TEnv>(
282
291
  export function generateManifestFull<TEnv>(
283
292
  urlpatterns: UrlPatterns<TEnv, any>,
284
293
  mountIndex: number = 0,
285
- options?: { urlPrefix?: string },
294
+ options?: {
295
+ urlPrefix?: string;
296
+ /**
297
+ * Called once per `"use client"` component registered as an
298
+ * errorBoundary/notFoundBoundary fallback, with its client-reference key
299
+ * (`$$id`). Lets the build collect fallback module ids for dedicated
300
+ * chunking without exposing the otherwise-discarded EntryData tree. The
301
+ * EntryData map built below is local; this is the only seam that surfaces it.
302
+ */
303
+ collectClientFallbackRef?: (refKey: string) => void;
304
+ },
286
305
  ): FullManifest {
287
306
  const routeManifest: Record<string, string> = {};
288
307
  const routeAncestry: Record<string, string[]> = {};
@@ -296,7 +315,7 @@ export function generateManifestFull<TEnv>(
296
315
  const searchSchemasMap = new Map<string, Record<string, string>>();
297
316
  const trackedIncludes: TrackedInclude[] = [];
298
317
 
299
- RSCRouterContext.run(
318
+ RangoContext.run(
300
319
  {
301
320
  manifest,
302
321
  patterns: patternsMap,
@@ -320,6 +339,22 @@ export function generateManifestFull<TEnv>(
320
339
  },
321
340
  );
322
341
 
342
+ // Surface the "use client" components registered as error/notFound fallbacks
343
+ // (route-tree errorBoundary()/notFoundBoundary() helpers, stored on EntryData).
344
+ // The boundary may be a handler function and/or wrap the client boundary in
345
+ // server providers, so walk the whole tree (see collectFallbackClientRefs).
346
+ if (options?.collectClientFallbackRef) {
347
+ const report = options.collectClientFallbackRef;
348
+ const collect = (boundary: unknown[] | undefined) => {
349
+ for (const item of boundary ?? [])
350
+ collectFallbackClientRefs(item, report);
351
+ };
352
+ for (const entry of manifest.values()) {
353
+ collect(entry.errorBoundary);
354
+ collect(entry.notFoundBoundary);
355
+ }
356
+ }
357
+
323
358
  // Collect root-level routes and trailing slash config
324
359
  const routeTrailingSlash: Record<string, string> = {};
325
360
  for (const [name, pattern] of patternsMap.entries()) {
@@ -356,12 +391,10 @@ export function generateManifestFull<TEnv>(
356
391
  }
357
392
  }
358
393
 
359
- // Build prefix tree from tracked includes (shared visited set for cycle detection).
360
- // Multiple includes can share the same fullPrefix (e.g., include("/", patternsA),
361
- // include("/", patternsB)). Merge their routes instead of overwriting.
394
+ // Shared visited set for cycle detection across all root-level includes.
362
395
  const visited = new Set<unknown>();
363
- for (const include of trackedIncludes) {
364
- const node = buildPrefixTreeNode(
396
+ mergeIncludeNodes(prefixTree, trackedIncludes, (include) =>
397
+ buildPrefixTreeNode(
365
398
  include.fullPrefix,
366
399
  include.namePrefix,
367
400
  include.patterns as UrlPatterns<any>,
@@ -375,16 +408,8 @@ export function generateManifestFull<TEnv>(
375
408
  passthroughRoutes,
376
409
  responseTypeRoutes,
377
410
  routeSearchSchemas,
378
- );
379
-
380
- const existing = prefixTree[include.fullPrefix];
381
- if (existing) {
382
- existing.routes.push(...node.routes);
383
- Object.assign(existing.children, node.children);
384
- } else {
385
- prefixTree[include.fullPrefix] = node;
386
- }
387
- }
411
+ ),
412
+ );
388
413
 
389
414
  return {
390
415
  prefixTree,
@@ -35,5 +35,7 @@ export {
35
35
  formatNestedRouterConflictError,
36
36
  findRouterFiles,
37
37
  writeCombinedRouteTypes,
38
+ genFileTsPath,
39
+ resolveSearchSchemas,
38
40
  } from "./route-types/router-processing.js";
39
41
  export { findUrlsVariableNames } from "./route-types/per-module-writer.js";
@@ -24,6 +24,8 @@ export {
24
24
 
25
25
  export { buildRouteTrie, type TrieNode, type TrieLeaf } from "./route-trie.js";
26
26
 
27
+ export { collectFallbackClientRefs } from "./collect-fallback-refs.js";
28
+
27
29
  export {
28
30
  writePerModuleRouteTypes,
29
31
  extractRoutesFromSource,
@@ -20,7 +20,8 @@ export interface TrieLeaf {
20
20
  sp: string;
21
21
  /** Ancestry shortCodes from root to route [M0L0, M0L0L0, M0L0L0R499] */
22
22
  a: string[];
23
- /** Optional param names (absent params get empty string value) */
23
+ /** Optional param names declared on the route. Absent params are
24
+ * omitted from the matched params record (read as `undefined`). */
24
25
  op?: string[];
25
26
  /** Constraint validation: paramName -> allowed values */
26
27
  cv?: Record<string, string[]>;
@@ -98,8 +99,14 @@ export function buildRouteTrie(
98
99
  }
99
100
 
100
101
  /**
101
- * Insert a route into the trie, handling optional params by forking
102
- * the insertion path (one terminal without the param, one with).
102
+ * Insert a route into the trie. Optional params expand into two branches at
103
+ * registration time (skip-first, then present), so each terminal lives at the
104
+ * correct depth for its number of bound params and carries a branch-local
105
+ * `pa` listing only those names. The trie's single-slot `node.p` is reused
106
+ * across branches because matching ignores `node.p.n` — the leaf's `pa` is
107
+ * the source of truth for naming. Skip-first ordering lets `mergeLeaf`'s
108
+ * last-wins rule produce greedy-leftmost semantics for free at any shared
109
+ * terminal depth.
103
110
  */
104
111
  function insertRoute(
105
112
  node: TrieNode,
@@ -107,14 +114,13 @@ function insertRoute(
107
114
  index: number,
108
115
  leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
109
116
  ): void {
110
- // Collect param names, optional param names, and constraints across all segments
111
- const paramNames: string[] = [];
117
+ // op (full optional list) and cv (full constraint map) are route-level and
118
+ // identical on every terminal, so compute them once on the shared base.
112
119
  const optionalParams: string[] = [];
113
120
  const constraints: Record<string, string[]> = {};
114
121
 
115
122
  for (const seg of segments) {
116
123
  if (seg.type === "param") {
117
- paramNames.push(seg.value);
118
124
  if (seg.optional) {
119
125
  optionalParams.push(seg.value);
120
126
  }
@@ -124,21 +130,15 @@ function insertRoute(
124
130
  }
125
131
  }
126
132
 
127
- const fullLeaf: TrieLeaf = {
133
+ const leafBase: Omit<TrieLeaf, "pa"> = {
128
134
  ...leaf,
129
- ...(paramNames.length > 0 ? { pa: paramNames } : {}),
130
135
  ...(optionalParams.length > 0 ? { op: optionalParams } : {}),
131
136
  ...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
132
137
  };
133
138
 
134
- insertSegments(node, segments, index, fullLeaf);
139
+ insertSegments(node, segments, index, leafBase, []);
135
140
  }
136
141
 
137
- /**
138
- * Recursively insert segments into the trie.
139
- * For optional params, we add a terminal at the current node (param absent)
140
- * AND continue inserting into the param child (param present).
141
- */
142
142
  /**
143
143
  * Extract ancestry map from a built trie by visiting all leaf nodes.
144
144
  * Returns { routeName: ancestryShortCodes[] } for every route in the trie.
@@ -218,15 +218,25 @@ function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
218
218
  node.r = mergeLeaves(node.r, leaf);
219
219
  }
220
220
 
221
+ function buildLeaf(
222
+ leafBase: Omit<TrieLeaf, "pa">,
223
+ paramNames: string[],
224
+ ): TrieLeaf {
225
+ return paramNames.length > 0
226
+ ? { ...leafBase, pa: [...paramNames] }
227
+ : { ...leafBase };
228
+ }
229
+
221
230
  function insertSegments(
222
231
  node: TrieNode,
223
232
  segments: ParsedSegment[],
224
233
  index: number,
225
- leaf: TrieLeaf,
234
+ leafBase: Omit<TrieLeaf, "pa">,
235
+ paramNames: string[],
226
236
  ): void {
227
- // Base case: all segments consumed, add terminal
237
+ // Base case: all segments consumed, add terminal with branch-local pa
228
238
  if (index >= segments.length) {
229
- mergeLeaf(node, leaf);
239
+ mergeLeaf(node, buildLeaf(leafBase, paramNames));
230
240
  return;
231
241
  }
232
242
 
@@ -235,12 +245,19 @@ function insertSegments(
235
245
  if (segment.type === "static") {
236
246
  if (!node.s) node.s = {};
237
247
  if (!node.s[segment.value]) node.s[segment.value] = {};
238
- insertSegments(node.s[segment.value], segments, index + 1, leaf);
248
+ insertSegments(
249
+ node.s[segment.value],
250
+ segments,
251
+ index + 1,
252
+ leafBase,
253
+ paramNames,
254
+ );
239
255
  } else if (segment.type === "param") {
240
256
  if (segment.optional) {
241
- // Optional param: add terminal at current node (param absent)
242
- mergeLeaf(node, leaf);
243
- // AND continue with param child (param present)
257
+ // SKIP first: continue at the same node without binding this name.
258
+ // Skip-first ordering means the present-branch's TAKE overwrites any
259
+ // shared terminal later, giving greedy-leftmost semantics.
260
+ insertSegments(node, segments, index + 1, leafBase, paramNames);
244
261
  }
245
262
  if (segment.suffix) {
246
263
  // Suffix param: keyed by suffix string (e.g., ".html")
@@ -248,16 +265,26 @@ function insertSegments(
248
265
  if (!node.xp[segment.suffix]) {
249
266
  node.xp[segment.suffix] = { n: segment.value, c: {} };
250
267
  }
251
- insertSegments(node.xp[segment.suffix].c, segments, index + 1, leaf);
268
+ insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
269
+ ...paramNames,
270
+ segment.value,
271
+ ]);
252
272
  } else {
253
273
  if (!node.p) {
254
274
  node.p = { n: segment.value, c: {} };
255
275
  }
256
- insertSegments(node.p.c, segments, index + 1, leaf);
276
+ insertSegments(node.p.c, segments, index + 1, leafBase, [
277
+ ...paramNames,
278
+ segment.value,
279
+ ]);
257
280
  }
258
281
  } else if (segment.type === "wildcard") {
259
- // Wildcard consumes all remaining segments
260
- const wildLeaf = { ...leaf, pn: "*" };
282
+ // Wildcard consumes all remaining segments. Carry any params bound before
283
+ // the wildcard in pa so they zip correctly against paramValues at match.
284
+ const wildLeaf: TrieLeaf & { pn: string } = {
285
+ ...buildLeaf(leafBase, paramNames),
286
+ pn: "*",
287
+ };
261
288
  const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
262
289
  const merged = mergeLeaves(existing, wildLeaf);
263
290
  node.w = merged as TrieLeaf & { pn: string };
@@ -23,7 +23,7 @@ export function generatePerModuleTypesSource(
23
23
  const valid = routes.filter(({ name }) => {
24
24
  if (!name || /["'\\`\n\r]/.test(name)) {
25
25
  console.warn(
26
- `[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`,
26
+ `[rango] Skipping route with invalid name: ${JSON.stringify(name)}`,
27
27
  );
28
28
  return false;
29
29
  }
@@ -42,7 +42,7 @@ export function generatePerModuleTypesSource(
42
42
  for (const { name, pattern, params, search } of valid) {
43
43
  if (deduped.has(name)) {
44
44
  console.warn(
45
- `[rsc-router] Duplicate route name "${name}" — keeping first definition`,
45
+ `[rango] Duplicate route name "${name}" — keeping first definition`,
46
46
  );
47
47
  continue;
48
48
  }
@@ -59,7 +59,7 @@ export function generatePerModuleTypesSource(
59
59
  }
60
60
 
61
61
  /**
62
- * Generates a .ts file that augments RSCRouter.GeneratedRouteMap
62
+ * Generates a .ts file that augments Rango.GeneratedRouteMap
63
63
  * with route name -> pattern mappings. This enables Handler<"routeName">
64
64
  * without circular references since the file has no imports from the app.
65
65
  */
@@ -94,7 +94,7 @@ ${objectBody}
94
94
  } as const;
95
95
 
96
96
  declare global {
97
- namespace RSCRouter {
97
+ namespace Rango {
98
98
  interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
99
99
  }
100
100
  }
@@ -376,7 +376,7 @@ export function buildCombinedRouteMapWithSearch(
376
376
  const realPath = resolve(filePath);
377
377
  const key = variableName ? `${realPath}:${variableName}` : realPath;
378
378
  if (visited.has(key)) {
379
- console.warn(`[rsc-router] Circular include detected, skipping: ${key}`);
379
+ console.warn(`[rango] Circular include detected, skipping: ${key}`);
380
380
  return { routes: {}, searchSchemas: {} };
381
381
  }
382
382
  visited.add(key);
@@ -97,7 +97,10 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
97
97
  routes = extractRoutesFromSource(source);
98
98
  }
99
99
 
100
- const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
100
+ // Match .ts/.tsx/.js/.jsx (same as router-processing.ts / router-transform.ts).
101
+ // Without the jsx? branch a .jsx/.js source produced genPath === filePath,
102
+ // overwriting the source file instead of writing a sibling .gen.ts.
103
+ const genPath = filePath.replace(/\.(tsx?|jsx?)$/, ".gen.ts");
101
104
 
102
105
  // When a urls() variable was found but static resolution yields zero
103
106
  // routes, write an empty placeholder so generated imports stay
@@ -106,7 +109,7 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
106
109
  if (varNames.length > 0 && !existsSync(genPath)) {
107
110
  writeFileSync(genPath, generatePerModuleTypesSource([]));
108
111
  console.log(
109
- `[rsc-router] Generated route types (placeholder) -> ${genPath}`,
112
+ `[rango] Generated route types (placeholder) -> ${genPath}`,
110
113
  );
111
114
  }
112
115
  return;
@@ -118,11 +121,11 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
118
121
  : null;
119
122
  if (existing !== genSource) {
120
123
  writeFileSync(genPath, genSource);
121
- console.log(`[rsc-router] Generated route types -> ${genPath}`);
124
+ console.log(`[rango] Generated route types -> ${genPath}`);
122
125
  }
123
126
  } catch (err) {
124
127
  console.warn(
125
- `[rsc-router] Failed to generate route types for ${filePath}: ${(err as Error).message}`,
128
+ `[rango] Failed to generate route types for ${filePath}: ${(err as Error).message}`,
126
129
  );
127
130
  }
128
131
  }