@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7d061845

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 (252) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2138 -841
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +68 -21
  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/document-cache/SKILL.md +78 -55
  13. package/skills/handler-use/SKILL.md +3 -1
  14. package/skills/hooks/SKILL.md +229 -20
  15. package/skills/host-router/SKILL.md +45 -20
  16. package/skills/i18n/SKILL.md +276 -0
  17. package/skills/intercept/SKILL.md +26 -4
  18. package/skills/layout/SKILL.md +6 -7
  19. package/skills/links/SKILL.md +247 -17
  20. package/skills/loader/SKILL.md +219 -9
  21. package/skills/middleware/SKILL.md +15 -9
  22. package/skills/migrate-nextjs/SKILL.md +4 -2
  23. package/skills/migrate-react-router/SKILL.md +5 -0
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +12 -6
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +242 -24
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +66 -9
  31. package/skills/route/SKILL.md +33 -4
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +751 -0
  34. package/skills/streams-and-websockets/SKILL.md +283 -0
  35. package/skills/testing/SKILL.md +816 -0
  36. package/skills/typesafety/SKILL.md +319 -27
  37. package/skills/use-cache/SKILL.md +34 -5
  38. package/skills/view-transitions/SKILL.md +294 -0
  39. package/src/__augment-tests__/augment.ts +81 -0
  40. package/src/__augment-tests__/augmented.check.ts +117 -0
  41. package/src/browser/action-coordinator.ts +53 -36
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/event-controller.ts +86 -70
  44. package/src/browser/history-state.ts +21 -0
  45. package/src/browser/index.ts +3 -3
  46. package/src/browser/navigation-bridge.ts +65 -9
  47. package/src/browser/navigation-client.ts +45 -25
  48. package/src/browser/navigation-store.ts +32 -9
  49. package/src/browser/navigation-transaction.ts +10 -28
  50. package/src/browser/partial-update.ts +52 -26
  51. package/src/browser/prefetch/cache.ts +124 -26
  52. package/src/browser/prefetch/fetch.ts +114 -38
  53. package/src/browser/prefetch/queue.ts +36 -5
  54. package/src/browser/rango-state.ts +53 -13
  55. package/src/browser/react/Link.tsx +18 -13
  56. package/src/browser/react/NavigationProvider.tsx +72 -31
  57. package/src/browser/react/filter-segment-order.ts +51 -7
  58. package/src/browser/react/index.ts +3 -0
  59. package/src/browser/react/location-state-shared.ts +175 -4
  60. package/src/browser/react/location-state.ts +39 -13
  61. package/src/browser/react/use-handle.ts +17 -9
  62. package/src/browser/react/use-navigation.ts +22 -2
  63. package/src/browser/react/use-params.ts +20 -8
  64. package/src/browser/react/use-reverse.ts +106 -0
  65. package/src/browser/react/use-router.ts +22 -2
  66. package/src/browser/react/use-segments.ts +11 -8
  67. package/src/browser/response-adapter.ts +25 -0
  68. package/src/browser/rsc-router.tsx +64 -22
  69. package/src/browser/scroll-restoration.ts +22 -14
  70. package/src/browser/segment-structure-assert.ts +2 -2
  71. package/src/browser/server-action-bridge.ts +23 -30
  72. package/src/browser/types.ts +21 -0
  73. package/src/build/collect-fallback-refs.ts +107 -0
  74. package/src/build/generate-manifest.ts +60 -35
  75. package/src/build/generate-route-types.ts +2 -0
  76. package/src/build/index.ts +2 -0
  77. package/src/build/route-trie.ts +2 -1
  78. package/src/build/route-types/codegen.ts +4 -4
  79. package/src/build/route-types/include-resolution.ts +1 -1
  80. package/src/build/route-types/per-module-writer.ts +7 -4
  81. package/src/build/route-types/router-processing.ts +55 -14
  82. package/src/build/route-types/scan-filter.ts +1 -1
  83. package/src/build/route-types/source-scan.ts +118 -0
  84. package/src/build/runtime-discovery.ts +9 -20
  85. package/src/cache/cache-scope.ts +28 -42
  86. package/src/cache/cf/cf-cache-store.ts +54 -13
  87. package/src/client.rsc.tsx +3 -0
  88. package/src/client.tsx +10 -8
  89. package/src/context-var.ts +5 -5
  90. package/src/decode-loader-results.ts +36 -0
  91. package/src/errors.ts +30 -1
  92. package/src/handle.ts +26 -13
  93. package/src/host/index.ts +2 -2
  94. package/src/host/router.ts +129 -57
  95. package/src/host/types.ts +31 -2
  96. package/src/host/utils.ts +1 -1
  97. package/src/href-client.ts +140 -20
  98. package/src/index.rsc.ts +9 -4
  99. package/src/index.ts +16 -6
  100. package/src/loader-store.ts +500 -0
  101. package/src/loader.rsc.ts +21 -6
  102. package/src/loader.ts +3 -10
  103. package/src/missing-id-error.ts +68 -0
  104. package/src/outlet-context.ts +1 -1
  105. package/src/prerender.ts +4 -4
  106. package/src/response-utils.ts +37 -0
  107. package/src/reverse.ts +65 -39
  108. package/src/route-content-wrapper.tsx +6 -28
  109. package/src/route-definition/dsl-helpers.ts +253 -265
  110. package/src/route-definition/helper-factories.ts +29 -139
  111. package/src/route-definition/helpers-types.ts +43 -15
  112. package/src/route-definition/resolve-handler-use.ts +6 -0
  113. package/src/route-definition/use-item-types.ts +32 -0
  114. package/src/route-types.ts +19 -41
  115. package/src/router/basename.ts +14 -0
  116. package/src/router/content-negotiation.ts +15 -2
  117. package/src/router/error-handling.ts +1 -1
  118. package/src/router/handler-context.ts +21 -41
  119. package/src/router/intercept-resolution.ts +4 -18
  120. package/src/router/lazy-includes.ts +3 -3
  121. package/src/router/loader-resolution.ts +19 -2
  122. package/src/router/match-api.ts +4 -3
  123. package/src/router/match-handlers.ts +63 -20
  124. package/src/router/match-middleware/cache-lookup.ts +44 -91
  125. package/src/router/match-middleware/cache-store.ts +3 -2
  126. package/src/router/match-result.ts +53 -32
  127. package/src/router/metrics.ts +1 -1
  128. package/src/router/middleware-types.ts +15 -26
  129. package/src/router/middleware.ts +99 -84
  130. package/src/router/pattern-matching.ts +101 -17
  131. package/src/router/prerender-match.ts +1 -1
  132. package/src/router/preview-match.ts +3 -1
  133. package/src/router/request-classification.ts +4 -28
  134. package/src/router/revalidation.ts +58 -2
  135. package/src/router/router-interfaces.ts +45 -28
  136. package/src/router/router-options.ts +40 -1
  137. package/src/router/router-registry.ts +2 -5
  138. package/src/router/segment-resolution/fresh.ts +27 -6
  139. package/src/router/segment-resolution/revalidation.ts +147 -106
  140. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  141. package/src/router/substitute-pattern-params.ts +56 -0
  142. package/src/router/telemetry.ts +99 -0
  143. package/src/router/trie-matching.ts +18 -13
  144. package/src/router/types.ts +8 -0
  145. package/src/router/url-params.ts +49 -0
  146. package/src/router.ts +38 -23
  147. package/src/rsc/handler-context.ts +2 -2
  148. package/src/rsc/handler.ts +28 -69
  149. package/src/rsc/helpers.ts +91 -43
  150. package/src/rsc/index.ts +1 -1
  151. package/src/rsc/origin-guard.ts +28 -10
  152. package/src/rsc/progressive-enhancement.ts +4 -0
  153. package/src/rsc/response-route-handler.ts +46 -53
  154. package/src/rsc/rsc-rendering.ts +35 -51
  155. package/src/rsc/runtime-warnings.ts +9 -10
  156. package/src/rsc/server-action.ts +17 -37
  157. package/src/rsc/ssr-setup.ts +16 -0
  158. package/src/rsc/types.ts +8 -2
  159. package/src/search-params.ts +4 -4
  160. package/src/segment-system.tsx +122 -56
  161. package/src/serialize.ts +243 -0
  162. package/src/server/context.ts +118 -51
  163. package/src/server/cookie-store.ts +28 -4
  164. package/src/server/request-context.ts +20 -42
  165. package/src/ssr/index.tsx +5 -1
  166. package/src/static-handler.ts +1 -1
  167. package/src/testing/cache-status.ts +166 -0
  168. package/src/testing/collect-handle.ts +63 -0
  169. package/src/testing/dispatch.ts +440 -0
  170. package/src/testing/dom.entry.ts +22 -0
  171. package/src/testing/e2e/fixture.ts +154 -0
  172. package/src/testing/e2e/index.ts +149 -0
  173. package/src/testing/e2e/matchers.ts +51 -0
  174. package/src/testing/e2e/page-helpers.ts +272 -0
  175. package/src/testing/e2e/parity.ts +306 -0
  176. package/src/testing/e2e/server.ts +183 -0
  177. package/src/testing/flight-matchers.ts +104 -0
  178. package/src/testing/flight-runtime.d.ts +57 -0
  179. package/src/testing/flight-tree.ts +332 -0
  180. package/src/testing/flight.entry.ts +46 -0
  181. package/src/testing/flight.ts +224 -0
  182. package/src/testing/generated-routes.ts +223 -0
  183. package/src/testing/index.ts +106 -0
  184. package/src/testing/internal/context.ts +304 -0
  185. package/src/testing/internal/flight-client-globals.ts +30 -0
  186. package/src/testing/internal/seed-vars.ts +42 -0
  187. package/src/testing/render-handler.ts +267 -0
  188. package/src/testing/render-route.tsx +565 -0
  189. package/src/testing/run-loader.ts +341 -0
  190. package/src/testing/run-middleware.ts +188 -0
  191. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  192. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  193. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  194. package/src/testing/vitest-stubs/version.ts +5 -0
  195. package/src/testing/vitest.ts +270 -0
  196. package/src/types/global-namespace.ts +39 -26
  197. package/src/types/handler-context.ts +68 -50
  198. package/src/types/index.ts +1 -0
  199. package/src/types/loader-types.ts +5 -6
  200. package/src/types/request-scope.ts +126 -0
  201. package/src/types/segments.ts +35 -1
  202. package/src/urls/include-helper.ts +10 -53
  203. package/src/urls/index.ts +0 -3
  204. package/src/urls/path-helper-types.ts +11 -3
  205. package/src/urls/path-helper.ts +17 -52
  206. package/src/urls/pattern-types.ts +36 -19
  207. package/src/urls/response-types.ts +22 -29
  208. package/src/urls/type-extraction.ts +26 -116
  209. package/src/urls/urls-function.ts +1 -5
  210. package/src/use-loader.tsx +413 -42
  211. package/src/vite/debug.ts +185 -0
  212. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  213. package/src/vite/discovery/discover-routers.ts +101 -51
  214. package/src/vite/discovery/discovery-errors.ts +194 -0
  215. package/src/vite/discovery/gate-state.ts +171 -0
  216. package/src/vite/discovery/prerender-collection.ts +67 -26
  217. package/src/vite/discovery/route-types-writer.ts +40 -84
  218. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  219. package/src/vite/discovery/state.ts +33 -0
  220. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  221. package/src/vite/index.ts +2 -0
  222. package/src/vite/plugin-types.ts +67 -0
  223. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  224. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  225. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  226. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  227. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  228. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  229. package/src/vite/plugins/expose-action-id.ts +54 -30
  230. package/src/vite/plugins/expose-id-utils.ts +12 -8
  231. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  232. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  233. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  234. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  235. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  236. package/src/vite/plugins/performance-tracks.ts +29 -25
  237. package/src/vite/plugins/use-cache-transform.ts +65 -50
  238. package/src/vite/plugins/version-injector.ts +39 -23
  239. package/src/vite/plugins/version-plugin.ts +59 -2
  240. package/src/vite/plugins/virtual-entries.ts +2 -2
  241. package/src/vite/rango.ts +116 -29
  242. package/src/vite/router-discovery.ts +750 -100
  243. package/src/vite/utils/ast-handler-extract.ts +15 -15
  244. package/src/vite/utils/banner.ts +1 -1
  245. package/src/vite/utils/bundle-analysis.ts +4 -2
  246. package/src/vite/utils/client-chunks.ts +190 -0
  247. package/src/vite/utils/forward-user-plugins.ts +193 -0
  248. package/src/vite/utils/manifest-utils.ts +21 -5
  249. package/src/vite/utils/package-resolution.ts +41 -1
  250. package/src/vite/utils/prerender-utils.ts +5 -4
  251. package/src/vite/utils/shared-utils.ts +107 -26
  252. 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,7 +15,9 @@ 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
22
  /** Ancestry shortCodes for layout pruning */
20
23
  ancestry: string[];
@@ -173,20 +176,25 @@ function validateAndBuild(
173
176
  originalPathname: string,
174
177
  pathnameHasTrailingSlash: boolean,
175
178
  ): TrieMatchResult | null {
176
- // Build named params by zipping leaf.pa with positional paramValues
179
+ // Build named params by zipping leaf.pa with positional paramValues.
180
+ // Params are URL-decoded at this boundary so ctx.params holds the values
181
+ // apps expect (matching Express/React Router) and round-trip cleanly
182
+ // through ctx.reverse.
177
183
  const params: Record<string, string> = {};
178
184
  if (leaf.pa) {
179
185
  for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
180
- params[leaf.pa[i]] = paramValues[i];
186
+ params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
181
187
  }
182
188
  }
183
189
 
184
190
  // Add wildcard param (wildcard leaves have pn from TrieNode.w type)
185
191
  if (wildcardValue !== undefined && "pn" in leaf) {
186
- params[(leaf as TrieLeaf & { pn: string }).pn] = wildcardValue;
192
+ params[(leaf as TrieLeaf & { pn: string }).pn] =
193
+ safeDecodeURIComponent(wildcardValue);
187
194
  }
188
195
 
189
- // Validate constraints
196
+ // Validate constraints against decoded values so constraint lists can be
197
+ // written in decoded form (e.g. ["en-GB", "en US"]).
190
198
  if (leaf.cv) {
191
199
  for (const paramName in leaf.cv) {
192
200
  const allowed = leaf.cv[paramName]!;
@@ -197,14 +205,11 @@ function validateAndBuild(
197
205
  }
198
206
  }
199
207
 
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
- }
208
+ // Optional params that weren't matched are left absent from `params` so
209
+ // `ctx.params.locale` reads as `undefined`, matching the
210
+ // `ExtractParams<"/:locale?/...">` type (`{ locale?: string }`). Both
211
+ // internal consumers — the constraint check above and `reverse()`
212
+ // already treat missing/undefined as the absent form.
208
213
 
209
214
  // Trailing slash handling
210
215
  const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
@@ -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
@@ -22,10 +22,9 @@ import type { UrlPatterns } from "./urls.js";
22
22
  import type { UrlBuilder } from "./urls/pattern-types.js";
23
23
  import { urls } from "./urls.js";
24
24
  import {
25
- EntryData,
26
- InterceptSelectorContext,
25
+ type EntryData,
27
26
  getContext,
28
- RSCRouterContext,
27
+ RangoContext,
29
28
  type MetricsStore,
30
29
  } from "./server/context";
31
30
  import { createHandleStore, type HandleStore } from "./server/handle-store.js";
@@ -57,6 +56,7 @@ import { buildDebugManifest } from "./router/debug-manifest.js";
57
56
 
58
57
  import type { SegmentResolutionDeps, MatchApiDeps } from "./router/types.js";
59
58
  import { createHandlerContext } from "./router/handler-context.js";
59
+ import { normalizeBasename } from "./router/basename.js";
60
60
  import {
61
61
  setupLoaderAccess,
62
62
  setupLoaderAccessSilent,
@@ -91,13 +91,10 @@ import {
91
91
  RouterRegistry,
92
92
  nextRouterAutoId,
93
93
  } from "./router/router-registry.js";
94
+ import type { RangoOptions, RootLayoutProps } from "./router/router-options.js";
94
95
  import type {
95
- RSCRouterOptions,
96
- RootLayoutProps,
97
- } from "./router/router-options.js";
98
- import type {
99
- RSCRouter,
100
- RSCRouterInternal,
96
+ Rango,
97
+ RangoInternal,
101
98
  RouterRequestInput,
102
99
  } from "./router/router-interfaces.js";
103
100
 
@@ -116,22 +113,22 @@ import {
116
113
  // Re-export public types and values from extracted modules
117
114
  export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
118
115
  export type {
119
- RSCRouterOptions,
116
+ RangoOptions,
120
117
  RootLayoutProps,
121
118
  SSRStreamMode,
122
119
  SSROptions,
123
120
  ResolveStreamingContext,
124
121
  } from "./router/router-options.js";
125
122
  export type {
126
- RSCRouter,
127
- RSCRouterInternal,
123
+ Rango,
124
+ RangoInternal,
128
125
  RouterRequestInput,
129
126
  } from "./router/router-interfaces.js";
130
127
  export { toInternal } from "./router/router-interfaces.js";
131
128
 
132
129
  export function createRouter<TEnv = any>(
133
- options: RSCRouterOptions<TEnv> = {},
134
- ): RSCRouter<TEnv, {}> {
130
+ options: RangoOptions<TEnv> = {},
131
+ ): Rango<TEnv, {}> {
135
132
  const {
136
133
  id: userProvidedId,
137
134
  $$id: injectedId,
@@ -159,14 +156,23 @@ export function createRouter<TEnv = any>(
159
156
  timeouts: timeoutsOption,
160
157
  onTimeout,
161
158
  originCheck: originCheckOption,
159
+ viewTransition: viewTransitionOption = "auto",
160
+ debugCacheSignal: debugCacheSignalOption = false,
162
161
  } = options;
163
162
 
163
+ // Debug cache signal gate (DEVELOPMENT/TEST ONLY). Enabled by the
164
+ // debugCacheSignal option OR the RANGO_TEST_SIGNALS=1 env flag. When off,
165
+ // no X-Rango-Cache header is emitted and output is byte-identical.
166
+ const cacheSignalEnabled =
167
+ debugCacheSignalOption ||
168
+ (typeof process !== "undefined" &&
169
+ (process as { env?: Record<string, string | undefined> }).env
170
+ ?.RANGO_TEST_SIGNALS === "1");
171
+
164
172
  // Normalize basename: ensure leading slash, strip trailing slash.
165
- // A bare "/" is equivalent to no basename.
166
- const basename =
167
- basenameOption && basenameOption.replace(/^\/+|\/+$/g, "")
168
- ? "/" + basenameOption.replace(/^\/+|\/+$/g, "")
169
- : undefined;
173
+ // A bare "/" is equivalent to no basename. Shared with the testing
174
+ // primitives via normalizeBasename so they can never drift.
175
+ const basename = normalizeBasename(basenameOption);
170
176
 
171
177
  // Resolve telemetry sink (no-op when not configured)
172
178
  const telemetry = resolveSink(telemetrySink);
@@ -538,6 +544,7 @@ export function createRouter<TEnv = any>(
538
544
  findNearestNotFoundBoundary,
539
545
  notFoundComponent: notFound,
540
546
  callOnError,
547
+ viewTransitionDefault: viewTransitionOption,
541
548
  };
542
549
 
543
550
  // Match API dependencies
@@ -665,6 +672,7 @@ export function createRouter<TEnv = any>(
665
672
  findMatch,
666
673
  findInterceptForRoute,
667
674
  telemetry: telemetrySink,
675
+ cacheSignalEnabled,
668
676
  });
669
677
 
670
678
  const { match, matchPartial, matchError, previewMatch } = matchHandlers;
@@ -674,7 +682,7 @@ export function createRouter<TEnv = any>(
674
682
  * The type system tracks accumulated routes through the builder chain
675
683
  * Initial TRoutes is {} (empty) to avoid poisoning accumulated types with Record<string, string>
676
684
  */
677
- const router: RSCRouterInternal<TEnv, {}> = {
685
+ const router: RangoInternal<TEnv, {}> = {
678
686
  __brand: RSC_ROUTER_BRAND,
679
687
  id: routerId,
680
688
  basename,
@@ -722,7 +730,7 @@ export function createRouter<TEnv = any>(
722
730
  };
723
731
 
724
732
  let handlerResult: AllUseItems[] = [];
725
- RSCRouterContext.run(
733
+ RangoContext.run(
726
734
  {
727
735
  manifest,
728
736
  patterns: routePatterns,
@@ -1000,6 +1008,13 @@ export function createRouter<TEnv = any>(
1000
1008
  // Expose basename for runtime manifest generation
1001
1009
  __basename: basename,
1002
1010
 
1011
+ // Expose router-level boundary defaults for build-time clientChunks
1012
+ // discovery (so a "use client" default boundary lands in app-fallback).
1013
+ // These are createRouter options, never pushed onto EntryData.
1014
+ __defaultErrorBoundary: defaultErrorBoundary,
1015
+ __defaultNotFoundBoundary: defaultNotFoundBoundary,
1016
+ __notFound: notFound,
1017
+
1003
1018
  // RSC request handler (lazily created on first call)
1004
1019
  fetch: (() => {
1005
1020
  // Handler is created on first call and reused
@@ -1046,9 +1061,9 @@ export function createRouter<TEnv = any>(
1046
1061
 
1047
1062
  // If urls option was provided, auto-register them
1048
1063
  if (typeof urlsOption === "function") {
1049
- return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1064
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
1050
1065
  } else if (urlsOption) {
1051
- return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1066
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
1052
1067
  }
1053
1068
 
1054
1069
  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 {
@@ -352,7 +358,7 @@ export function createRSCHandler<
352
358
  // Resolve cache store configuration
353
359
  // Priority: options.cache (handler override) > router.cache (router default)
354
360
  // Store is enabled only if: config provided, enabled, and no ?__no_cache query param
355
- let cacheStore = undefined;
361
+ let cacheStore: SegmentCacheStore | undefined;
356
362
  const cacheOption = options.cache ?? router.cache;
357
363
  if (cacheOption && !url.searchParams.has("__no_cache")) {
358
364
  const cacheConfig =
@@ -533,7 +539,9 @@ export function createRSCHandler<
533
539
  }
534
540
 
535
541
  const fullTiming = timingParts.join(", ");
536
- if (fullTiming) response.headers.set("Server-Timing", fullTiming);
542
+ if (fullTiming && !isWebSocketUpgradeResponse(response)) {
543
+ response.headers.set("Server-Timing", fullTiming);
544
+ }
537
545
 
538
546
  return response;
539
547
  });
@@ -593,10 +601,7 @@ export function createRSCHandler<
593
601
  routerId: router.id,
594
602
  });
595
603
  } catch (error) {
596
- if (
597
- error instanceof RouteNotFoundError ||
598
- (error instanceof Error && error.name === "RouteNotFoundError")
599
- ) {
604
+ if (isRouteNotFoundError(error)) {
600
605
  // Let the render path handle 404 — match()/matchPartial() will
601
606
  // re-throw RouteNotFoundError and the catch block in
602
607
  // executeRenderWithMiddleware renders the not-found page.
@@ -647,14 +652,7 @@ export function createRSCHandler<
647
652
  }
648
653
 
649
654
  // ---- 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;
655
+ const originPhase = ORIGIN_CHECK_PHASE_BY_MODE[plan.mode];
658
656
  if (originPhase) {
659
657
  const originResult = await checkRequestOrigin(
660
658
  request,
@@ -804,7 +802,7 @@ export function createRSCHandler<
804
802
  );
805
803
  }
806
804
  const response = responseOutcome.result;
807
- if (plan.negotiated) {
805
+ if (plan.negotiated && !isWebSocketUpgradeResponse(response)) {
808
806
  response.headers.append("Vary", "Accept");
809
807
  }
810
808
  return response;
@@ -921,47 +919,17 @@ export function createRSCHandler<
921
919
  );
922
920
  }
923
921
 
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 })
922
+ // Full render, partial render, fallen-through PE, and full-page redirect all
923
+ // render through the same middleware-wrapped path. Only full/partial-render
924
+ // carry negotiation + the partial flag; pe/redirect render plainly.
925
+ const isPartial = plan.mode === "partial-render";
926
+ const negotiated =
927
+ plan.mode === "full-render" || plan.mode === "partial-render"
928
+ ? plan.negotiated
929
+ : false;
962
930
  return executeRenderWithMiddleware(
963
931
  plan.route.routeMiddleware,
964
- false,
932
+ negotiated,
965
933
  plan.route.routeKey,
966
934
  routeReverse,
967
935
  request,
@@ -970,7 +938,7 @@ export function createRSCHandler<
970
938
  variables,
971
939
  nonce,
972
940
  handleStore,
973
- false,
941
+ isPartial,
974
942
  );
975
943
  }
976
944
 
@@ -1014,7 +982,7 @@ export function createRSCHandler<
1014
982
  nonce,
1015
983
  );
1016
984
  }
1017
- if (negotiated) {
985
+ if (negotiated && !isWebSocketUpgradeResponse(response)) {
1018
986
  response.headers.append("Vary", "Accept");
1019
987
  }
1020
988
  return response;
@@ -1050,10 +1018,7 @@ export function createRSCHandler<
1050
1018
  }
1051
1019
 
1052
1020
  // Render 404 page for unmatched routes
1053
- const isRouteNotFound =
1054
- error instanceof RouteNotFoundError ||
1055
- (error instanceof Error && error.name === "RouteNotFoundError");
1056
- if (isRouteNotFound) {
1021
+ if (isRouteNotFoundError(error)) {
1057
1022
  callOnError(error, "routing", {
1058
1023
  request,
1059
1024
  url,
@@ -1100,13 +1065,7 @@ export function createRSCHandler<
1100
1065
  },
1101
1066
  });
1102
1067
 
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) {
1068
+ if (isRscRequest(request, url, isPartial)) {
1110
1069
  return createResponseWithMergedHeaders(rscStream, {
1111
1070
  status: 404,
1112
1071
  headers: { "content-type": "text/x-component;charset=utf-8" },
@@ -8,8 +8,67 @@ import {
8
8
  _getRequestContext,
9
9
  getLocationState,
10
10
  } from "../server/request-context.js";
11
+ import type { RequestContext } from "../server/request-context.js";
11
12
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
13
+ import { isRedirectResponse } from "../response-utils.js";
12
14
  import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
15
+ import { formatCacheSignalHeader } from "../router/telemetry.js";
16
+
17
+ /**
18
+ * DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
19
+ * match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
20
+ * header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
21
+ * attached — output is byte-identical to the default. Header mutation failures
22
+ * are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
23
+ */
24
+ function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
25
+ const signal = ctx._cacheSignal;
26
+ if (!signal || signal.length === 0) return;
27
+ try {
28
+ target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
29
+ } catch {
30
+ // Headers immutable — skip.
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Copy stub headers from the request context onto a target Headers instance:
36
+ * append Set-Cookie entries, set everything else only if absent. Header
37
+ * mutation failures are swallowed so the same logic works against Response
38
+ * headers that may be immutable (e.g. Cloudflare protocol-switch responses).
39
+ */
40
+ function applyStubHeaders(target: Headers, stub: Headers): void {
41
+ stub.forEach((value, name) => {
42
+ try {
43
+ if (name.toLowerCase() === "set-cookie") {
44
+ target.append(name, value);
45
+ } else if (!target.has(name)) {
46
+ target.set(name, value);
47
+ }
48
+ } catch {
49
+ // Headers immutable — skip.
50
+ }
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Drain ctx._onResponseCallbacks onto a response. Swapping the array before
56
+ * iteration prevents re-entrant registrations from double-firing and matches
57
+ * the contract that each callback runs at most once per request.
58
+ */
59
+ function drainOnResponseCallbacks(
60
+ ctx: RequestContext,
61
+ response: Response,
62
+ ): Response {
63
+ const callbacks = ctx._onResponseCallbacks;
64
+ if (callbacks.length === 0) return response;
65
+ ctx._onResponseCallbacks = [];
66
+ let result = response;
67
+ for (const callback of callbacks) {
68
+ result = callback(result) ?? result;
69
+ }
70
+ return result;
71
+ }
13
72
 
14
73
  /**
15
74
  * Check if a request body has content to decode
@@ -39,40 +98,24 @@ export function createResponseWithMergedHeaders(
39
98
  return new Response(body, init);
40
99
  }
41
100
 
42
- // Merge headers from stub response into the new response.
43
- // Delete Set-Cookie from the stub after consuming so that downstream
44
- // merge points (e.g. executeMiddleware) do not duplicate them.
101
+ // Delete Set-Cookie from the stub after consuming so downstream merge
102
+ // points (e.g. executeMiddleware) don't duplicate them.
45
103
  const mergedHeaders = new Headers(init.headers);
46
- ctx.res.headers.forEach((value, name) => {
47
- if (name.toLowerCase() === "set-cookie") {
48
- mergedHeaders.append(name, value);
49
- } else if (!mergedHeaders.has(name)) {
50
- // Only set if not already present in init.headers
51
- mergedHeaders.set(name, value);
52
- }
53
- });
104
+ applyStubHeaders(mergedHeaders, ctx.res.headers);
54
105
  ctx.res.headers.delete("set-cookie");
106
+ applyCacheSignalHeader(mergedHeaders, ctx);
55
107
 
56
- // Use ctx.res.status if it was set (e.g., 404 for notFound, 500 for error)
57
- // Otherwise use the status from init
108
+ // ctx.res.status overrides init.status when explicitly set (e.g. 404 for
109
+ // notFound, 500 for error). Default ctx.res.status is 200.
58
110
  const status = ctx.res.status !== 200 ? ctx.res.status : init.status;
59
111
 
60
- let response = new Response(body, {
112
+ const response = new Response(body, {
61
113
  ...init,
62
114
  status,
63
115
  headers: mergedHeaders,
64
116
  });
65
117
 
66
- // Run onResponse callbacks - each can inspect/modify the response.
67
- // Drain the array so that downstream callers (e.g. finalizeResponse)
68
- // do not re-execute the same callbacks on this response.
69
- const callbacks = ctx._onResponseCallbacks;
70
- ctx._onResponseCallbacks = [];
71
- for (const callback of callbacks) {
72
- response = callback(response) ?? response;
73
- }
74
-
75
- return response;
118
+ return drainOnResponseCallbacks(ctx, response);
76
119
  }
77
120
 
78
121
  /**
@@ -122,10 +165,10 @@ export function interceptRedirectForPartial(
122
165
  locationState?: Record<string, unknown>,
123
166
  ) => Response,
124
167
  ): Response | null {
125
- const redirectUrl = response.headers.get("Location");
126
- if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
168
+ if (!isRedirectResponse(response)) {
127
169
  return null;
128
170
  }
171
+ const redirectUrl = response.headers.get("Location")!;
129
172
  const locationState = getLocationState();
130
173
  let intercepted: Response;
131
174
  if (locationState) {
@@ -175,24 +218,29 @@ export function buildRouteMiddlewareEntries<TEnv>(
175
218
  }
176
219
 
177
220
  /**
178
- * Run onResponse callbacks on an existing Response.
179
- *
180
- * Used for code paths that bypass createResponseWithMergedHeaders(), such as
181
- * middleware short-circuits where the Response is already constructed but
182
- * ctx.onResponse() callbacks still need to fire.
221
+ * Merge stub headers from the request context onto an existing Response in
222
+ * place, then drain onResponse callbacks. Used when a Response cannot flow
223
+ * through `new Response()` status 101 is outside the constructor's
224
+ * 200-599 range, and the Cloudflare-specific `webSocket` property would be
225
+ * lost on reconstruction.
183
226
  */
184
- export function finalizeResponse(response: Response): Response {
227
+ export function mergeStubHeadersAndFinalize(response: Response): Response {
185
228
  const ctx = _getRequestContext();
186
- if (!ctx || ctx._onResponseCallbacks.length === 0) {
187
- return response;
188
- }
229
+ if (!ctx) return response;
189
230
 
190
- // Drain the array so callbacks run at most once per request.
191
- const callbacks = ctx._onResponseCallbacks;
192
- ctx._onResponseCallbacks = [];
193
- let result = response;
194
- for (const callback of callbacks) {
195
- result = callback(result) ?? result;
196
- }
197
- return result;
231
+ applyStubHeaders(response.headers, ctx.res.headers);
232
+ ctx.res.headers.delete("set-cookie");
233
+
234
+ return drainOnResponseCallbacks(ctx, response);
235
+ }
236
+
237
+ /**
238
+ * Run onResponse callbacks on an existing Response. Used by code paths that
239
+ * bypass createResponseWithMergedHeaders (e.g. middleware short-circuits)
240
+ * but still need ctx.onResponse() callbacks to fire.
241
+ */
242
+ export function finalizeResponse(response: Response): Response {
243
+ const ctx = _getRequestContext();
244
+ if (!ctx) return response;
245
+ return drainOnResponseCallbacks(ctx, response);
198
246
  }