@rangojs/router 0.0.0-experimental.77 → 0.0.0-experimental.77ed8945

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 (239) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/vite/index.js +2103 -861
  4. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  5. package/package.json +13 -8
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +66 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +26 -4
  19. package/skills/layout/SKILL.md +6 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +12 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +238 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +33 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/tailwind/SKILL.md +27 -3
  37. package/skills/typesafety/SKILL.md +319 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +116 -0
  42. package/src/browser/action-coordinator.ts +53 -36
  43. package/src/browser/app-shell.ts +39 -0
  44. package/src/browser/event-controller.ts +86 -70
  45. package/src/browser/history-state.ts +21 -0
  46. package/src/browser/index.ts +3 -3
  47. package/src/browser/navigation-bridge.ts +29 -9
  48. package/src/browser/navigation-client.ts +99 -77
  49. package/src/browser/navigation-store.ts +7 -8
  50. package/src/browser/navigation-transaction.ts +10 -28
  51. package/src/browser/partial-update.ts +60 -40
  52. package/src/browser/prefetch/cache.ts +196 -49
  53. package/src/browser/prefetch/fetch.ts +203 -59
  54. package/src/browser/prefetch/queue.ts +36 -5
  55. package/src/browser/rango-state.ts +37 -13
  56. package/src/browser/react/Link.tsx +18 -13
  57. package/src/browser/react/NavigationProvider.tsx +75 -31
  58. package/src/browser/react/filter-segment-order.ts +51 -7
  59. package/src/browser/react/index.ts +3 -0
  60. package/src/browser/react/location-state-shared.ts +175 -4
  61. package/src/browser/react/location-state.ts +39 -13
  62. package/src/browser/react/use-handle.ts +17 -9
  63. package/src/browser/react/use-navigation.ts +22 -2
  64. package/src/browser/react/use-params.ts +20 -8
  65. package/src/browser/react/use-reverse.ts +106 -0
  66. package/src/browser/react/use-router.ts +23 -2
  67. package/src/browser/react/use-segments.ts +11 -8
  68. package/src/browser/response-adapter.ts +52 -1
  69. package/src/browser/rsc-router.tsx +71 -22
  70. package/src/browser/scroll-restoration.ts +22 -14
  71. package/src/browser/segment-reconciler.ts +10 -14
  72. package/src/browser/segment-structure-assert.ts +2 -2
  73. package/src/browser/server-action-bridge.ts +44 -30
  74. package/src/browser/types.ts +12 -2
  75. package/src/build/collect-fallback-refs.ts +107 -0
  76. package/src/build/generate-manifest.ts +60 -35
  77. package/src/build/generate-route-types.ts +2 -0
  78. package/src/build/index.ts +8 -1
  79. package/src/build/prefix-tree-utils.ts +123 -0
  80. package/src/build/route-trie.ts +45 -1
  81. package/src/build/route-types/codegen.ts +4 -4
  82. package/src/build/route-types/include-resolution.ts +1 -1
  83. package/src/build/route-types/per-module-writer.ts +7 -4
  84. package/src/build/route-types/router-processing.ts +55 -14
  85. package/src/build/route-types/scan-filter.ts +1 -1
  86. package/src/build/route-types/source-scan.ts +118 -0
  87. package/src/build/runtime-discovery.ts +9 -20
  88. package/src/cache/cache-runtime.ts +17 -5
  89. package/src/cache/cache-scope.ts +51 -49
  90. package/src/cache/cf/cf-cache-store.ts +502 -32
  91. package/src/cache/cf/index.ts +3 -0
  92. package/src/cache/handle-snapshot.ts +103 -0
  93. package/src/cache/index.ts +3 -0
  94. package/src/cache/memory-segment-store.ts +3 -2
  95. package/src/cache/types.ts +10 -6
  96. package/src/client.rsc.tsx +3 -0
  97. package/src/client.tsx +96 -205
  98. package/src/context-var.ts +5 -5
  99. package/src/decode-loader-results.ts +36 -0
  100. package/src/errors.ts +30 -4
  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 -21
  107. package/src/index.rsc.ts +10 -6
  108. package/src/index.ts +17 -8
  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/store.ts +9 -7
  115. package/src/prerender.ts +4 -4
  116. package/src/response-utils.ts +37 -0
  117. package/src/reverse.ts +65 -39
  118. package/src/route-content-wrapper.tsx +6 -28
  119. package/src/route-definition/dsl-helpers.ts +253 -265
  120. package/src/route-definition/helper-factories.ts +29 -139
  121. package/src/route-definition/helpers-types.ts +43 -15
  122. package/src/route-definition/resolve-handler-use.ts +6 -0
  123. package/src/route-definition/use-item-types.ts +32 -0
  124. package/src/route-types.ts +26 -41
  125. package/src/router/content-negotiation.ts +15 -2
  126. package/src/router/error-handling.ts +1 -1
  127. package/src/router/find-match.ts +54 -6
  128. package/src/router/handler-context.ts +21 -41
  129. package/src/router/intercept-resolution.ts +4 -18
  130. package/src/router/lazy-includes.ts +41 -22
  131. package/src/router/loader-resolution.ts +82 -36
  132. package/src/router/manifest.ts +41 -19
  133. package/src/router/match-api.ts +4 -3
  134. package/src/router/match-handlers.ts +1 -0
  135. package/src/router/match-middleware/cache-lookup.ts +57 -95
  136. package/src/router/match-middleware/cache-store.ts +3 -2
  137. package/src/router/match-result.ts +53 -32
  138. package/src/router/metrics.ts +1 -1
  139. package/src/router/middleware-types.ts +15 -26
  140. package/src/router/middleware.ts +99 -84
  141. package/src/router/pattern-matching.ts +116 -19
  142. package/src/router/prerender-match.ts +40 -15
  143. package/src/router/preview-match.ts +3 -1
  144. package/src/router/request-classification.ts +40 -37
  145. package/src/router/revalidation.ts +58 -2
  146. package/src/router/router-interfaces.ts +51 -35
  147. package/src/router/router-options.ts +25 -1
  148. package/src/router/router-registry.ts +2 -5
  149. package/src/router/segment-resolution/fresh.ts +27 -6
  150. package/src/router/segment-resolution/revalidation.ts +147 -106
  151. package/src/router/segment-resolution/static-store.ts +19 -5
  152. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  153. package/src/router/substitute-pattern-params.ts +56 -0
  154. package/src/router/trie-matching.ts +40 -16
  155. package/src/router/types.ts +8 -0
  156. package/src/router/url-params.ts +49 -0
  157. package/src/router.ts +37 -25
  158. package/src/rsc/handler-context.ts +2 -2
  159. package/src/rsc/handler.ts +58 -77
  160. package/src/rsc/helpers.ts +72 -43
  161. package/src/rsc/index.ts +1 -1
  162. package/src/rsc/manifest-init.ts +28 -41
  163. package/src/rsc/origin-guard.ts +30 -10
  164. package/src/rsc/progressive-enhancement.ts +4 -0
  165. package/src/rsc/response-error.ts +79 -12
  166. package/src/rsc/response-route-handler.ts +76 -61
  167. package/src/rsc/rsc-rendering.ts +45 -51
  168. package/src/rsc/runtime-warnings.ts +9 -10
  169. package/src/rsc/server-action.ts +33 -39
  170. package/src/rsc/ssr-setup.ts +16 -0
  171. package/src/rsc/types.ts +8 -2
  172. package/src/search-params.ts +4 -4
  173. package/src/segment-content-promise.ts +67 -0
  174. package/src/segment-loader-promise.ts +122 -0
  175. package/src/segment-system.tsx +132 -116
  176. package/src/serialize.ts +243 -0
  177. package/src/server/context.ts +175 -53
  178. package/src/server/cookie-store.ts +28 -4
  179. package/src/server/request-context.ts +57 -51
  180. package/src/ssr/index.tsx +5 -1
  181. package/src/static-handler.ts +1 -1
  182. package/src/types/global-namespace.ts +39 -26
  183. package/src/types/handler-context.ts +68 -50
  184. package/src/types/index.ts +1 -0
  185. package/src/types/loader-types.ts +11 -9
  186. package/src/types/request-scope.ts +126 -0
  187. package/src/types/route-entry.ts +11 -0
  188. package/src/types/segments.ts +35 -2
  189. package/src/urls/include-helper.ts +34 -67
  190. package/src/urls/index.ts +1 -5
  191. package/src/urls/path-helper-types.ts +17 -3
  192. package/src/urls/path-helper.ts +17 -52
  193. package/src/urls/pattern-types.ts +36 -19
  194. package/src/urls/response-types.ts +22 -29
  195. package/src/urls/type-extraction.ts +58 -139
  196. package/src/urls/urls-function.ts +1 -5
  197. package/src/use-loader.tsx +413 -42
  198. package/src/vite/debug.ts +185 -0
  199. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  200. package/src/vite/discovery/discover-routers.ts +106 -75
  201. package/src/vite/discovery/discovery-errors.ts +194 -0
  202. package/src/vite/discovery/gate-state.ts +171 -0
  203. package/src/vite/discovery/prerender-collection.ts +72 -31
  204. package/src/vite/discovery/route-types-writer.ts +40 -84
  205. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  206. package/src/vite/discovery/state.ts +33 -0
  207. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  208. package/src/vite/index.ts +2 -0
  209. package/src/vite/plugin-types.ts +67 -0
  210. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  211. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  212. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  213. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  214. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  215. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  216. package/src/vite/plugins/expose-action-id.ts +54 -30
  217. package/src/vite/plugins/expose-id-utils.ts +12 -8
  218. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  219. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  220. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  221. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  222. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  223. package/src/vite/plugins/performance-tracks.ts +29 -25
  224. package/src/vite/plugins/use-cache-transform.ts +65 -50
  225. package/src/vite/plugins/version-injector.ts +39 -23
  226. package/src/vite/plugins/version-plugin.ts +59 -2
  227. package/src/vite/plugins/virtual-entries.ts +2 -2
  228. package/src/vite/rango.ts +116 -29
  229. package/src/vite/router-discovery.ts +753 -104
  230. package/src/vite/utils/ast-handler-extract.ts +15 -15
  231. package/src/vite/utils/banner.ts +1 -1
  232. package/src/vite/utils/bundle-analysis.ts +4 -2
  233. package/src/vite/utils/client-chunks.ts +190 -0
  234. package/src/vite/utils/forward-user-plugins.ts +193 -0
  235. package/src/vite/utils/manifest-utils.ts +8 -59
  236. package/src/vite/utils/package-resolution.ts +41 -1
  237. package/src/vite/utils/prerender-utils.ts +5 -4
  238. package/src/vite/utils/shared-utils.ts +107 -26
  239. package/src/browser/action-response-classifier.ts +0 -99
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { TrieNode, TrieLeaf } from "../build/route-trie.js";
9
+ import { safeDecodeURIComponent } from "./url-params.js";
9
10
 
10
11
  export interface TrieMatchResult {
11
12
  /** Route name */
@@ -14,10 +15,10 @@ export interface TrieMatchResult {
14
15
  sp: string;
15
16
  /** Matched route params */
16
17
  params: Record<string, string>;
17
- /** Optional param names (absent params have empty string value) */
18
+ /** Optional param names declared on the route. Absent params are omitted
19
+ * from `params` (read as `undefined`), matching the
20
+ * `ExtractParams<"/:locale?/...">` type. */
18
21
  optionalParams?: string[];
19
- /** Ancestry shortCodes for layout pruning */
20
- ancestry: string[];
21
22
  /** Redirect target if trailing slash requires it */
22
23
  redirectTo?: string;
23
24
  /** Route has pre-rendered data available */
@@ -60,6 +61,19 @@ export function tryTrieMatch(
60
61
  pathnameHasTrailingSlash,
61
62
  );
62
63
  }
64
+ // A root-level wildcard ("/*") matches "/" with an empty remainder, the
65
+ // same value the regex matcher produces for the bare prefix. Without this
66
+ // the trie misses, the regex fallback runs, and its no-config branch emits
67
+ // a corrupt slice-off redirect. The static terminal still wins above.
68
+ if (trie.w) {
69
+ return validateAndBuild(
70
+ trie.w,
71
+ [],
72
+ "",
73
+ pathname,
74
+ pathnameHasTrailingSlash,
75
+ );
76
+ }
63
77
  return null;
64
78
  }
65
79
 
@@ -102,6 +116,15 @@ function walkTrie(
102
116
  if (node.r) {
103
117
  return { leaf: node.r, paramValues: [...paramValues] };
104
118
  }
119
+ // A wildcard at this node matches the bare prefix with an empty remainder
120
+ // (e.g. "/files" against "/files/*"), mirroring the regex matcher's `*=""`.
121
+ // walkTrie otherwise only reaches node.w in the index<length branch below,
122
+ // so without this a request to the wildcard's own prefix misses the trie
123
+ // and the regex fallback emits a corrupt redirect. A static terminal
124
+ // (node.r) still wins.
125
+ if (node.w) {
126
+ return { leaf: node.w, paramValues: [...paramValues], wildcardValue: "" };
127
+ }
105
128
  return null;
106
129
  }
107
130
 
@@ -173,20 +196,25 @@ function validateAndBuild(
173
196
  originalPathname: string,
174
197
  pathnameHasTrailingSlash: boolean,
175
198
  ): TrieMatchResult | null {
176
- // Build named params by zipping leaf.pa with positional paramValues
199
+ // Build named params by zipping leaf.pa with positional paramValues.
200
+ // Params are URL-decoded at this boundary so ctx.params holds the values
201
+ // apps expect (matching Express/React Router) and round-trip cleanly
202
+ // through ctx.reverse.
177
203
  const params: Record<string, string> = {};
178
204
  if (leaf.pa) {
179
205
  for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
180
- params[leaf.pa[i]] = paramValues[i];
206
+ params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
181
207
  }
182
208
  }
183
209
 
184
210
  // Add wildcard param (wildcard leaves have pn from TrieNode.w type)
185
211
  if (wildcardValue !== undefined && "pn" in leaf) {
186
- params[(leaf as TrieLeaf & { pn: string }).pn] = wildcardValue;
212
+ params[(leaf as TrieLeaf & { pn: string }).pn] =
213
+ safeDecodeURIComponent(wildcardValue);
187
214
  }
188
215
 
189
- // Validate constraints
216
+ // Validate constraints against decoded values so constraint lists can be
217
+ // written in decoded form (e.g. ["en-GB", "en US"]).
190
218
  if (leaf.cv) {
191
219
  for (const paramName in leaf.cv) {
192
220
  const allowed = leaf.cv[paramName]!;
@@ -197,14 +225,11 @@ function validateAndBuild(
197
225
  }
198
226
  }
199
227
 
200
- // Fill in empty strings for optional params that weren't matched
201
- if (leaf.op) {
202
- for (const name of leaf.op) {
203
- if (!(name in params)) {
204
- params[name] = "";
205
- }
206
- }
207
- }
228
+ // Optional params that weren't matched are left absent from `params` so
229
+ // `ctx.params.locale` reads as `undefined`, matching the
230
+ // `ExtractParams<"/:locale?/...">` type (`{ locale?: string }`). Both
231
+ // internal consumers — the constraint check above and `reverse()`
232
+ // already treat missing/undefined as the absent form.
208
233
 
209
234
  // Trailing slash handling
210
235
  const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
@@ -224,7 +249,6 @@ function validateAndBuild(
224
249
  routeKey: leaf.n,
225
250
  sp: leaf.sp,
226
251
  params,
227
- ancestry: leaf.a,
228
252
  };
229
253
 
230
254
  if (leaf.op) result.optionalParams = leaf.op;
@@ -98,6 +98,14 @@ export interface SegmentResolutionDeps<TEnv = any> {
98
98
  ) => ReactNode | NotFoundBoundaryHandler | null;
99
99
  notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
100
100
  callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
101
+ /**
102
+ * Router-level default for the per-segment `transition({ viewTransition })`
103
+ * flag, from createRouter({ viewTransition }). Resolved into each segment's
104
+ * transition config during resolution (only `false` is stamped) so the render
105
+ * gate reads the boundary decision off the segment on both server and client.
106
+ * Undefined is treated as "auto" (wrap).
107
+ */
108
+ viewTransitionDefault?: "auto" | false;
101
109
  }
102
110
 
103
111
  /**
@@ -0,0 +1,49 @@
1
+ /**
2
+ * URL param encode/decode at the route boundary.
3
+ *
4
+ * Extraction (decode): regex/trie matchers keep param values URL-encoded;
5
+ * `safeDecodeURIComponent` turns them back into raw strings so `ctx.params`
6
+ * matches the contract apps expect (Express/React Router/Fastify/Koa) and
7
+ * round-trips through reverse stay stable. Malformed %-encoding is
8
+ * preserved as-is so a broken URL doesn't crash matching.
9
+ *
10
+ * Reversal (encode): `encodePathSegment` escapes only what RFC 3986
11
+ * requires for a path segment — `/`, `?`, `#`, space, control chars,
12
+ * non-ASCII — and leaves pchar sub-delims (`@ : $ & + , ; =` and friends)
13
+ * readable. `encodeURIComponent` over-encodes for path segments, which
14
+ * makes generated URLs harder for humans to read in the address bar
15
+ * (e.g. mailbox IDs like `ivo@example.com` would become
16
+ * `ivo%40example.com` even though `@` is path-legal).
17
+ */
18
+
19
+ export function safeDecodeURIComponent(raw: string): string {
20
+ if (raw === "" || raw.indexOf("%") === -1) return raw;
21
+ try {
22
+ return decodeURIComponent(raw);
23
+ } catch {
24
+ return raw;
25
+ }
26
+ }
27
+
28
+ // encodeURIComponent over-encodes for path segments. After running it,
29
+ // un-encode the pchar sub-delims + (`:` / `@`) so the resulting URL
30
+ // keeps human-readable characters that are legal in a path segment.
31
+ // Everything dangerous — `/ ? # %` and space/control/non-ASCII — stays
32
+ // encoded.
33
+ const PATH_SAFE_ESCAPES: Record<string, string> = {
34
+ "%3A": ":",
35
+ "%40": "@",
36
+ "%24": "$",
37
+ "%26": "&",
38
+ "%2B": "+",
39
+ "%2C": ",",
40
+ "%3B": ";",
41
+ "%3D": "=",
42
+ };
43
+
44
+ export function encodePathSegment(value: string): string {
45
+ return encodeURIComponent(value).replace(
46
+ /%(?:3A|40|24|26|2B|2C|3B|3D)/gi,
47
+ (match) => PATH_SAFE_ESCAPES[match.toUpperCase()] ?? match,
48
+ );
49
+ }
package/src/router.ts CHANGED
@@ -21,11 +21,11 @@ import type { AllUseItems } from "./route-types.js";
21
21
  import type { UrlPatterns } from "./urls.js";
22
22
  import type { UrlBuilder } from "./urls/pattern-types.js";
23
23
  import { urls } from "./urls.js";
24
+ import { buildPrecomputedByPrefix } from "./build/prefix-tree-utils.js";
24
25
  import {
25
- EntryData,
26
- InterceptSelectorContext,
26
+ type EntryData,
27
27
  getContext,
28
- RSCRouterContext,
28
+ RangoContext,
29
29
  type MetricsStore,
30
30
  } from "./server/context";
31
31
  import { createHandleStore, type HandleStore } from "./server/handle-store.js";
@@ -71,6 +71,7 @@ import {
71
71
  } from "./router/middleware.js";
72
72
  import {
73
73
  extractStaticPrefix,
74
+ joinPrefix,
74
75
  traverseBack,
75
76
  } from "./router/pattern-matching.js";
76
77
  import { resolveSink, safeEmit, getRequestId } from "./router/telemetry.js";
@@ -91,13 +92,10 @@ import {
91
92
  RouterRegistry,
92
93
  nextRouterAutoId,
93
94
  } from "./router/router-registry.js";
95
+ import type { RangoOptions, RootLayoutProps } from "./router/router-options.js";
94
96
  import type {
95
- RSCRouterOptions,
96
- RootLayoutProps,
97
- } from "./router/router-options.js";
98
- import type {
99
- RSCRouter,
100
- RSCRouterInternal,
97
+ Rango,
98
+ RangoInternal,
101
99
  RouterRequestInput,
102
100
  } from "./router/router-interfaces.js";
103
101
 
@@ -116,22 +114,22 @@ import {
116
114
  // Re-export public types and values from extracted modules
117
115
  export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
118
116
  export type {
119
- RSCRouterOptions,
117
+ RangoOptions,
120
118
  RootLayoutProps,
121
119
  SSRStreamMode,
122
120
  SSROptions,
123
121
  ResolveStreamingContext,
124
122
  } from "./router/router-options.js";
125
123
  export type {
126
- RSCRouter,
127
- RSCRouterInternal,
124
+ Rango,
125
+ RangoInternal,
128
126
  RouterRequestInput,
129
127
  } from "./router/router-interfaces.js";
130
128
  export { toInternal } from "./router/router-interfaces.js";
131
129
 
132
130
  export function createRouter<TEnv = any>(
133
- options: RSCRouterOptions<TEnv> = {},
134
- ): RSCRouter<TEnv, {}> {
131
+ options: RangoOptions<TEnv> = {},
132
+ ): Rango<TEnv, {}> {
135
133
  const {
136
134
  id: userProvidedId,
137
135
  $$id: injectedId,
@@ -159,6 +157,7 @@ export function createRouter<TEnv = any>(
159
157
  timeouts: timeoutsOption,
160
158
  onTimeout,
161
159
  originCheck: originCheckOption,
160
+ viewTransition: viewTransitionOption = "auto",
162
161
  } = options;
163
162
 
164
163
  // Normalize basename: ensure leading slash, strip trailing slash.
@@ -366,9 +365,11 @@ export function createRouter<TEnv = any>(
366
365
  getRouterPrecomputedEntries(routerId) ?? getPrecomputedEntries();
367
366
  if (current !== precomputedSource) {
368
367
  precomputedSource = current;
369
- precomputedByPrefix = current
370
- ? new Map(current.map((e) => [e.staticPrefix, e.routes]))
371
- : null;
368
+ // buildPrecomputedByPrefix drops any staticPrefix owned by more than one
369
+ // leaf include instead of collapsing it last-wins (which would mis-assign
370
+ // one include's routes to another's entry and 500 a valid sibling route).
371
+ // Such shared-prefix includes resolve via the handler path instead.
372
+ precomputedByPrefix = current ? buildPrecomputedByPrefix(current) : null;
372
373
  }
373
374
  return precomputedByPrefix;
374
375
  }
@@ -538,6 +539,7 @@ export function createRouter<TEnv = any>(
538
539
  findNearestNotFoundBoundary,
539
540
  notFoundComponent: notFound,
540
541
  callOnError,
542
+ viewTransitionDefault: viewTransitionOption,
541
543
  };
542
544
 
543
545
  // Match API dependencies
@@ -674,7 +676,7 @@ export function createRouter<TEnv = any>(
674
676
  * The type system tracks accumulated routes through the builder chain
675
677
  * Initial TRoutes is {} (empty) to avoid poisoning accumulated types with Record<string, string>
676
678
  */
677
- const router: RSCRouterInternal<TEnv, {}> = {
679
+ const router: RangoInternal<TEnv, {}> = {
678
680
  __brand: RSC_ROUTER_BRAND,
679
681
  id: routerId,
680
682
  basename,
@@ -722,7 +724,7 @@ export function createRouter<TEnv = any>(
722
724
  };
723
725
 
724
726
  let handlerResult: AllUseItems[] = [];
725
- RSCRouterContext.run(
727
+ RangoContext.run(
726
728
  {
727
729
  manifest,
728
730
  patterns: routePatterns,
@@ -834,10 +836,13 @@ export function createRouter<TEnv = any>(
834
836
 
835
837
  // Create placeholder RouteEntry for each lazy include
836
838
  for (const lazyInclude of lazyIncludes) {
837
- // Compute the full URL prefix (combining parent prefix if any)
838
- const fullPrefix = lazyInclude.context.urlPrefix
839
- ? lazyInclude.context.urlPrefix + lazyInclude.prefix
840
- : lazyInclude.prefix;
839
+ // Compute the full URL prefix (combining parent prefix if any). Use the
840
+ // slash-collapsing join so a trailing-slash parent prefix does not
841
+ // produce a double-slash staticPrefix the trie's sp can never match.
842
+ const fullPrefix = joinPrefix(
843
+ lazyInclude.context.urlPrefix,
844
+ lazyInclude.prefix,
845
+ );
841
846
 
842
847
  const lazyEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
843
848
  prefix: "",
@@ -1000,6 +1005,13 @@ export function createRouter<TEnv = any>(
1000
1005
  // Expose basename for runtime manifest generation
1001
1006
  __basename: basename,
1002
1007
 
1008
+ // Expose router-level boundary defaults for build-time clientChunks
1009
+ // discovery (so a "use client" default boundary lands in app-fallback).
1010
+ // These are createRouter options, never pushed onto EntryData.
1011
+ __defaultErrorBoundary: defaultErrorBoundary,
1012
+ __defaultNotFoundBoundary: defaultNotFoundBoundary,
1013
+ __notFound: notFound,
1014
+
1003
1015
  // RSC request handler (lazily created on first call)
1004
1016
  fetch: (() => {
1005
1017
  // Handler is created on first call and reused
@@ -1046,9 +1058,9 @@ export function createRouter<TEnv = any>(
1046
1058
 
1047
1059
  // If urls option was provided, auto-register them
1048
1060
  if (typeof urlsOption === "function") {
1049
- return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1061
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
1050
1062
  } else if (urlsOption) {
1051
- return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1063
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
1052
1064
  }
1053
1065
 
1054
1066
  return router;
@@ -6,14 +6,14 @@
6
6
  * RSC rendering) so they can be standalone modules without closure coupling.
7
7
  */
8
8
 
9
- import type { RSCRouterInternal } from "../router/router-interfaces.js";
9
+ import type { RangoInternal } from "../router/router-interfaces.js";
10
10
  import type { ErrorPhase } from "../types.js";
11
11
  import type { InvokeOnErrorContext } from "../router/error-handling.js";
12
12
  import type { RSCDependencies, LoadSSRModule } from "./types.js";
13
13
  import type { SSRStreamMode } from "../router/router-options.js";
14
14
 
15
15
  export interface HandlerContext<TEnv = unknown> {
16
- router: RSCRouterInternal<TEnv, any>;
16
+ router: RangoInternal<TEnv, any>;
17
17
  version: string;
18
18
  renderToReadableStream: RSCDependencies["renderToReadableStream"];
19
19
  decodeReply: RSCDependencies["decodeReply"];
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { createElement } from "react";
11
- import { RouteNotFoundError } from "../errors.js";
11
+ import { isRouteNotFoundError } from "../errors.js";
12
12
  import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
13
13
  import {
14
14
  runWithRequestContext,
@@ -31,6 +31,7 @@ import {
31
31
  interceptRedirectForPartial,
32
32
  buildRouteMiddlewareEntries,
33
33
  } from "./helpers.js";
34
+ import { isWebSocketUpgradeResponse } from "../response-utils.js";
34
35
  import {
35
36
  handleResponseRoute,
36
37
  type ResponseRouteMatch,
@@ -56,6 +57,7 @@ import {
56
57
  getRouterTrie,
57
58
  } from "../route-map-builder.js";
58
59
  import type { HandlerContext } from "./handler-context.js";
60
+ import type { SegmentCacheStore } from "../cache/types.js";
59
61
  import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
60
62
  import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
61
63
  import {
@@ -64,7 +66,10 @@ import {
64
66
  type ActionContinuation,
65
67
  } from "./server-action.js";
66
68
  import { handleLoaderFetch } from "./loader-fetch.js";
67
- import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
69
+ import {
70
+ checkRequestOrigin,
71
+ ORIGIN_CHECK_PHASE_BY_MODE,
72
+ } from "./origin-guard.js";
68
73
  import { handleRscRendering } from "./rsc-rendering.js";
69
74
  import {
70
75
  withTimeout,
@@ -81,6 +86,7 @@ import {
81
86
  startSSRSetup,
82
87
  getSSRSetup,
83
88
  mayNeedSSR,
89
+ isRscRequest,
84
90
  SSR_SETUP_VAR,
85
91
  } from "./ssr-setup.js";
86
92
  import {
@@ -122,6 +128,22 @@ import {
122
128
  * });
123
129
  * ```
124
130
  */
131
+
132
+ /**
133
+ * Response that tells the client to do a full document navigation. Shared by
134
+ * the terminal reload plans (version-mismatch and app-switch): an empty 200
135
+ * carrying X-RSC-Reload, which the client turns into window.location.href.
136
+ */
137
+ function createReloadResponse(reloadUrl: string) {
138
+ return createResponseWithMergedHeaders(null, {
139
+ status: 200,
140
+ headers: {
141
+ "X-RSC-Reload": reloadUrl,
142
+ "content-type": "text/x-component;charset=utf-8",
143
+ },
144
+ });
145
+ }
146
+
125
147
  export function createRSCHandler<
126
148
  TEnv = unknown,
127
149
  TRoutes extends Record<string, string> = Record<string, string>,
@@ -352,7 +374,7 @@ export function createRSCHandler<
352
374
  // Resolve cache store configuration
353
375
  // Priority: options.cache (handler override) > router.cache (router default)
354
376
  // Store is enabled only if: config provided, enabled, and no ?__no_cache query param
355
- let cacheStore = undefined;
377
+ let cacheStore: SegmentCacheStore | undefined;
356
378
  const cacheOption = options.cache ?? router.cache;
357
379
  if (cacheOption && !url.searchParams.has("__no_cache")) {
358
380
  const cacheConfig =
@@ -533,7 +555,9 @@ export function createRSCHandler<
533
555
  }
534
556
 
535
557
  const fullTiming = timingParts.join(", ");
536
- if (fullTiming) response.headers.set("Server-Timing", fullTiming);
558
+ if (fullTiming && !isWebSocketUpgradeResponse(response)) {
559
+ response.headers.set("Server-Timing", fullTiming);
560
+ }
537
561
 
538
562
  return response;
539
563
  });
@@ -593,10 +617,7 @@ export function createRSCHandler<
593
617
  routerId: router.id,
594
618
  });
595
619
  } catch (error) {
596
- if (
597
- error instanceof RouteNotFoundError ||
598
- (error instanceof Error && error.name === "RouteNotFoundError")
599
- ) {
620
+ if (isRouteNotFoundError(error)) {
600
621
  // Let the render path handle 404 — match()/matchPartial() will
601
622
  // re-throw RouteNotFoundError and the catch block in
602
623
  // executeRenderWithMiddleware renders the not-found page.
@@ -637,24 +658,18 @@ export function createRSCHandler<
637
658
  console.log(
638
659
  `[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
639
660
  );
640
- return createResponseWithMergedHeaders(null, {
641
- status: 200,
642
- headers: {
643
- "X-RSC-Reload": plan.reloadUrl,
644
- "content-type": "text/x-component;charset=utf-8",
645
- },
646
- });
661
+ return createReloadResponse(plan.reloadUrl);
662
+ }
663
+
664
+ if (plan.mode === "app-switch") {
665
+ // Cross-app SPA navigation crossed a host-router app boundary. Force a
666
+ // real document navigation so the target app's document is re-established
667
+ // (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
668
+ return createReloadResponse(plan.reloadUrl);
647
669
  }
648
670
 
649
671
  // ---- 3. Origin guard (gate for action/loader/PE modes) ----
650
- const originPhase: OriginCheckPhase | null =
651
- plan.mode === "action"
652
- ? "action"
653
- : plan.mode === "loader"
654
- ? "loader"
655
- : plan.mode === "pe-render"
656
- ? "pe-form"
657
- : null;
672
+ const originPhase = ORIGIN_CHECK_PHASE_BY_MODE[plan.mode];
658
673
  if (originPhase) {
659
674
  const originResult = await checkRequestOrigin(
660
675
  request,
@@ -804,7 +819,7 @@ export function createRSCHandler<
804
819
  );
805
820
  }
806
821
  const response = responseOutcome.result;
807
- if (plan.negotiated) {
822
+ if (plan.negotiated && !isWebSocketUpgradeResponse(response)) {
808
823
  response.headers.append("Vary", "Accept");
809
824
  }
810
825
  return response;
@@ -921,47 +936,17 @@ export function createRSCHandler<
921
936
  );
922
937
  }
923
938
 
924
- // ---- Full render / Partial render (or PE that fell through) ----
925
- if (plan.mode === "full-render" || plan.mode === "partial-render") {
926
- const isPartial = plan.mode === "partial-render";
927
- return executeRenderWithMiddleware(
928
- plan.route.routeMiddleware,
929
- plan.negotiated,
930
- plan.route.routeKey,
931
- routeReverse,
932
- request,
933
- env,
934
- url,
935
- variables,
936
- nonce,
937
- handleStore,
938
- isPartial,
939
- );
940
- }
941
-
942
- // PE that fell through (handleProgressiveEnhancement returned null)
943
- // falls back to full render
944
- if (plan.mode === "pe-render") {
945
- return executeRenderWithMiddleware(
946
- plan.route.routeMiddleware,
947
- false,
948
- plan.route.routeKey,
949
- routeReverse,
950
- request,
951
- env,
952
- url,
953
- variables,
954
- nonce,
955
- handleStore,
956
- false,
957
- );
958
- }
959
-
960
- // Redirect plan that wasn't handled above (full-page redirect — let
961
- // the pipeline handle it via match() which returns { redirect: url })
939
+ // Full render, partial render, fallen-through PE, and full-page redirect all
940
+ // render through the same middleware-wrapped path. Only full/partial-render
941
+ // carry negotiation + the partial flag; pe/redirect render plainly.
942
+ const isPartial = plan.mode === "partial-render";
943
+ const negotiated =
944
+ plan.mode === "full-render" || plan.mode === "partial-render"
945
+ ? plan.negotiated
946
+ : false;
962
947
  return executeRenderWithMiddleware(
963
948
  plan.route.routeMiddleware,
964
- false,
949
+ negotiated,
965
950
  plan.route.routeKey,
966
951
  routeReverse,
967
952
  request,
@@ -970,7 +955,7 @@ export function createRSCHandler<
970
955
  variables,
971
956
  nonce,
972
957
  handleStore,
973
- false,
958
+ isPartial,
974
959
  );
975
960
  }
976
961
 
@@ -1014,7 +999,7 @@ export function createRSCHandler<
1014
999
  nonce,
1015
1000
  );
1016
1001
  }
1017
- if (negotiated) {
1002
+ if (negotiated && !isWebSocketUpgradeResponse(response)) {
1018
1003
  response.headers.append("Vary", "Accept");
1019
1004
  }
1020
1005
  return response;
@@ -1050,10 +1035,7 @@ export function createRSCHandler<
1050
1035
  }
1051
1036
 
1052
1037
  // Render 404 page for unmatched routes
1053
- const isRouteNotFound =
1054
- error instanceof RouteNotFoundError ||
1055
- (error instanceof Error && error.name === "RouteNotFoundError");
1056
- if (isRouteNotFound) {
1038
+ if (isRouteNotFoundError(error)) {
1057
1039
  callOnError(error, "routing", {
1058
1040
  request,
1059
1041
  url,
@@ -1100,16 +1082,15 @@ export function createRSCHandler<
1100
1082
  },
1101
1083
  });
1102
1084
 
1103
- const isRscRequest =
1104
- isPartial ||
1105
- (!request.headers.get("accept")?.includes("text/html") &&
1106
- !url.searchParams.has("__html")) ||
1107
- url.searchParams.has("__rsc");
1108
-
1109
- if (isRscRequest) {
1085
+ if (isRscRequest(request, url, isPartial)) {
1110
1086
  return createResponseWithMergedHeaders(rscStream, {
1111
1087
  status: 404,
1112
- headers: { "content-type": "text/x-component;charset=utf-8" },
1088
+ headers: {
1089
+ "content-type": "text/x-component;charset=utf-8",
1090
+ // Router identity for the client's pre-decode integrity check; a
1091
+ // same-app 404 matches and applies in place. See response-adapter.
1092
+ "X-RSC-Router-Id": router.id,
1093
+ },
1113
1094
  });
1114
1095
  }
1115
1096