@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d

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 (278) 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 +2154 -861
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +220 -30
  11. package/skills/caching/SKILL.md +116 -8
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -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 +71 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +243 -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 +57 -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/testing/SKILL.md +128 -0
  37. package/skills/testing/bindings.md +89 -0
  38. package/skills/testing/cache-prerender.md +98 -0
  39. package/skills/testing/client-components.md +121 -0
  40. package/skills/testing/e2e-parity.md +124 -0
  41. package/skills/testing/flight.md +89 -0
  42. package/skills/testing/handles.md +127 -0
  43. package/skills/testing/loader.md +108 -0
  44. package/skills/testing/middleware.md +97 -0
  45. package/skills/testing/render-handler.md +102 -0
  46. package/skills/testing/response-routes.md +94 -0
  47. package/skills/testing/reverse-and-types.md +83 -0
  48. package/skills/testing/server-actions.md +89 -0
  49. package/skills/testing/server-tree.md +128 -0
  50. package/skills/testing/setup.md +120 -0
  51. package/skills/typesafety/SKILL.md +319 -27
  52. package/skills/use-cache/SKILL.md +34 -5
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/app-shell.ts +52 -0
  58. package/src/browser/event-controller.ts +86 -70
  59. package/src/browser/history-state.ts +21 -0
  60. package/src/browser/index.ts +3 -3
  61. package/src/browser/navigation-bridge.ts +84 -11
  62. package/src/browser/navigation-client.ts +104 -68
  63. package/src/browser/navigation-store.ts +32 -9
  64. package/src/browser/navigation-transaction.ts +10 -28
  65. package/src/browser/partial-update.ts +64 -26
  66. package/src/browser/prefetch/cache.ts +183 -44
  67. package/src/browser/prefetch/fetch.ts +228 -37
  68. package/src/browser/prefetch/queue.ts +36 -5
  69. package/src/browser/rango-state.ts +53 -13
  70. package/src/browser/react/Link.tsx +30 -2
  71. package/src/browser/react/NavigationProvider.tsx +72 -31
  72. package/src/browser/react/filter-segment-order.ts +51 -7
  73. package/src/browser/react/index.ts +3 -0
  74. package/src/browser/react/location-state-shared.ts +175 -4
  75. package/src/browser/react/location-state.ts +39 -13
  76. package/src/browser/react/use-handle.ts +17 -9
  77. package/src/browser/react/use-navigation.ts +22 -2
  78. package/src/browser/react/use-params.ts +20 -8
  79. package/src/browser/react/use-reverse.ts +106 -0
  80. package/src/browser/react/use-router.ts +22 -2
  81. package/src/browser/react/use-segments.ts +11 -8
  82. package/src/browser/response-adapter.ts +32 -1
  83. package/src/browser/rsc-router.tsx +69 -22
  84. package/src/browser/scroll-restoration.ts +22 -14
  85. package/src/browser/segment-reconciler.ts +36 -14
  86. package/src/browser/segment-structure-assert.ts +2 -2
  87. package/src/browser/server-action-bridge.ts +23 -30
  88. package/src/browser/types.ts +21 -0
  89. package/src/build/collect-fallback-refs.ts +107 -0
  90. package/src/build/generate-manifest.ts +60 -35
  91. package/src/build/generate-route-types.ts +2 -0
  92. package/src/build/index.ts +8 -1
  93. package/src/build/prefix-tree-utils.ts +123 -0
  94. package/src/build/route-trie.ts +95 -25
  95. package/src/build/route-types/codegen.ts +4 -4
  96. package/src/build/route-types/include-resolution.ts +1 -1
  97. package/src/build/route-types/per-module-writer.ts +7 -4
  98. package/src/build/route-types/router-processing.ts +55 -14
  99. package/src/build/route-types/scan-filter.ts +1 -1
  100. package/src/build/route-types/source-scan.ts +118 -0
  101. package/src/build/runtime-discovery.ts +9 -20
  102. package/src/cache/cache-scope.ts +28 -42
  103. package/src/cache/cf/cf-cache-store.ts +54 -13
  104. package/src/client.rsc.tsx +3 -0
  105. package/src/client.tsx +96 -205
  106. package/src/context-var.ts +5 -5
  107. package/src/decode-loader-results.ts +36 -0
  108. package/src/errors.ts +30 -4
  109. package/src/handle.ts +32 -14
  110. package/src/host/index.ts +2 -2
  111. package/src/host/router.ts +129 -57
  112. package/src/host/types.ts +31 -2
  113. package/src/host/utils.ts +1 -1
  114. package/src/href-client.ts +140 -21
  115. package/src/index.rsc.ts +10 -6
  116. package/src/index.ts +54 -17
  117. package/src/loader-store.ts +500 -0
  118. package/src/loader.rsc.ts +25 -7
  119. package/src/loader.ts +16 -9
  120. package/src/missing-id-error.ts +68 -0
  121. package/src/outlet-context.ts +1 -1
  122. package/src/prerender.ts +27 -6
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -36
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +384 -257
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +100 -28
  129. package/src/route-definition/resolve-handler-use.ts +6 -0
  130. package/src/route-definition/use-item-types.ts +32 -0
  131. package/src/route-types.ts +26 -41
  132. package/src/router/basename.ts +14 -0
  133. package/src/router/content-negotiation.ts +15 -2
  134. package/src/router/error-handling.ts +1 -1
  135. package/src/router/find-match.ts +54 -6
  136. package/src/router/handler-context.ts +21 -38
  137. package/src/router/intercept-resolution.ts +4 -18
  138. package/src/router/lazy-includes.ts +41 -22
  139. package/src/router/loader-resolution.ts +82 -36
  140. package/src/router/manifest.ts +41 -19
  141. package/src/router/match-api.ts +4 -3
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/cache-lookup.ts +44 -91
  144. package/src/router/match-middleware/cache-store.ts +3 -2
  145. package/src/router/match-result.ts +53 -32
  146. package/src/router/metrics.ts +1 -1
  147. package/src/router/middleware-types.ts +15 -26
  148. package/src/router/middleware.ts +99 -84
  149. package/src/router/pattern-matching.ts +116 -19
  150. package/src/router/prerender-match.ts +1 -1
  151. package/src/router/preview-match.ts +3 -1
  152. package/src/router/request-classification.ts +4 -28
  153. package/src/router/revalidation.ts +58 -2
  154. package/src/router/router-interfaces.ts +45 -28
  155. package/src/router/router-options.ts +40 -1
  156. package/src/router/router-registry.ts +2 -5
  157. package/src/router/segment-resolution/fresh.ts +27 -6
  158. package/src/router/segment-resolution/revalidation.ts +147 -106
  159. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  160. package/src/router/substitute-pattern-params.ts +56 -0
  161. package/src/router/telemetry.ts +99 -0
  162. package/src/router/trie-matching.ts +40 -16
  163. package/src/router/types.ts +8 -0
  164. package/src/router/url-params.ts +49 -0
  165. package/src/router.ts +52 -30
  166. package/src/rsc/handler-context.ts +2 -2
  167. package/src/rsc/handler.ts +28 -69
  168. package/src/rsc/helpers.ts +91 -43
  169. package/src/rsc/index.ts +1 -1
  170. package/src/rsc/manifest-init.ts +28 -41
  171. package/src/rsc/origin-guard.ts +28 -10
  172. package/src/rsc/progressive-enhancement.ts +4 -0
  173. package/src/rsc/response-error.ts +79 -12
  174. package/src/rsc/response-route-handler.ts +57 -61
  175. package/src/rsc/rsc-rendering.ts +35 -51
  176. package/src/rsc/runtime-warnings.ts +9 -10
  177. package/src/rsc/server-action.ts +17 -37
  178. package/src/rsc/ssr-setup.ts +16 -0
  179. package/src/rsc/types.ts +8 -2
  180. package/src/runtime-env.ts +18 -0
  181. package/src/search-params.ts +4 -4
  182. package/src/segment-content-promise.ts +67 -0
  183. package/src/segment-loader-promise.ts +122 -0
  184. package/src/segment-system.tsx +132 -116
  185. package/src/serialize.ts +243 -0
  186. package/src/server/context.ts +175 -53
  187. package/src/server/cookie-store.ts +28 -4
  188. package/src/server/request-context.ts +67 -51
  189. package/src/ssr/index.tsx +5 -1
  190. package/src/static-handler.ts +25 -3
  191. package/src/testing/cache-status.ts +166 -0
  192. package/src/testing/collect-handle.ts +63 -0
  193. package/src/testing/dispatch.ts +581 -0
  194. package/src/testing/dom.entry.ts +22 -0
  195. package/src/testing/e2e/fixture.ts +188 -0
  196. package/src/testing/e2e/index.ts +149 -0
  197. package/src/testing/e2e/matchers.ts +51 -0
  198. package/src/testing/e2e/page-helpers.ts +272 -0
  199. package/src/testing/e2e/parity.ts +326 -0
  200. package/src/testing/e2e/server.ts +195 -0
  201. package/src/testing/flight-matchers.ts +110 -0
  202. package/src/testing/flight-normalize.ts +38 -0
  203. package/src/testing/flight-runtime.d.ts +57 -0
  204. package/src/testing/flight-tree.ts +682 -0
  205. package/src/testing/flight.entry.ts +51 -0
  206. package/src/testing/flight.ts +234 -0
  207. package/src/testing/generated-routes.ts +223 -0
  208. package/src/testing/index.ts +106 -0
  209. package/src/testing/internal/context.ts +304 -0
  210. package/src/testing/internal/flight-client-globals.ts +30 -0
  211. package/src/testing/internal/seed-vars.ts +42 -0
  212. package/src/testing/render-handler.ts +323 -0
  213. package/src/testing/render-route.tsx +590 -0
  214. package/src/testing/run-loader.ts +363 -0
  215. package/src/testing/run-middleware.ts +205 -0
  216. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  217. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  218. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  219. package/src/testing/vitest-stubs/version.ts +5 -0
  220. package/src/testing/vitest.ts +285 -0
  221. package/src/types/global-namespace.ts +39 -26
  222. package/src/types/handler-context.ts +68 -50
  223. package/src/types/index.ts +1 -0
  224. package/src/types/loader-types.ts +11 -9
  225. package/src/types/request-scope.ts +126 -0
  226. package/src/types/route-entry.ts +11 -0
  227. package/src/types/segments.ts +35 -2
  228. package/src/urls/include-helper.ts +34 -67
  229. package/src/urls/index.ts +1 -5
  230. package/src/urls/path-helper-types.ts +41 -7
  231. package/src/urls/path-helper.ts +17 -52
  232. package/src/urls/pattern-types.ts +36 -19
  233. package/src/urls/response-types.ts +22 -29
  234. package/src/urls/type-extraction.ts +58 -139
  235. package/src/urls/urls-function.ts +1 -5
  236. package/src/use-loader.tsx +413 -42
  237. package/src/vite/debug.ts +185 -0
  238. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  239. package/src/vite/discovery/discover-routers.ts +106 -75
  240. package/src/vite/discovery/discovery-errors.ts +194 -0
  241. package/src/vite/discovery/gate-state.ts +171 -0
  242. package/src/vite/discovery/prerender-collection.ts +67 -26
  243. package/src/vite/discovery/route-types-writer.ts +40 -84
  244. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  245. package/src/vite/discovery/state.ts +33 -0
  246. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  247. package/src/vite/index.ts +2 -0
  248. package/src/vite/plugin-types.ts +67 -0
  249. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  250. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  251. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  252. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  253. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  254. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  255. package/src/vite/plugins/expose-action-id.ts +54 -30
  256. package/src/vite/plugins/expose-id-utils.ts +12 -8
  257. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  258. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  259. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  260. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  261. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  262. package/src/vite/plugins/performance-tracks.ts +29 -25
  263. package/src/vite/plugins/use-cache-transform.ts +65 -50
  264. package/src/vite/plugins/version-injector.ts +39 -23
  265. package/src/vite/plugins/version-plugin.ts +59 -2
  266. package/src/vite/plugins/virtual-entries.ts +2 -2
  267. package/src/vite/rango.ts +116 -29
  268. package/src/vite/router-discovery.ts +750 -100
  269. package/src/vite/utils/ast-handler-extract.ts +15 -15
  270. package/src/vite/utils/banner.ts +1 -1
  271. package/src/vite/utils/bundle-analysis.ts +4 -2
  272. package/src/vite/utils/client-chunks.ts +190 -0
  273. package/src/vite/utils/forward-user-plugins.ts +193 -0
  274. package/src/vite/utils/manifest-utils.ts +8 -59
  275. package/src/vite/utils/package-resolution.ts +41 -1
  276. package/src/vite/utils/prerender-utils.ts +21 -6
  277. package/src/vite/utils/shared-utils.ts +107 -26
  278. 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";
@@ -57,6 +57,7 @@ import { buildDebugManifest } from "./router/debug-manifest.js";
57
57
 
58
58
  import type { SegmentResolutionDeps, MatchApiDeps } from "./router/types.js";
59
59
  import { createHandlerContext } from "./router/handler-context.js";
60
+ import { normalizeBasename } from "./router/basename.js";
60
61
  import {
61
62
  setupLoaderAccess,
62
63
  setupLoaderAccessSilent,
@@ -71,6 +72,7 @@ import {
71
72
  } from "./router/middleware.js";
72
73
  import {
73
74
  extractStaticPrefix,
75
+ joinPrefix,
74
76
  traverseBack,
75
77
  } from "./router/pattern-matching.js";
76
78
  import { resolveSink, safeEmit, getRequestId } from "./router/telemetry.js";
@@ -91,13 +93,10 @@ import {
91
93
  RouterRegistry,
92
94
  nextRouterAutoId,
93
95
  } from "./router/router-registry.js";
96
+ import type { RangoOptions, RootLayoutProps } from "./router/router-options.js";
94
97
  import type {
95
- RSCRouterOptions,
96
- RootLayoutProps,
97
- } from "./router/router-options.js";
98
- import type {
99
- RSCRouter,
100
- RSCRouterInternal,
98
+ Rango,
99
+ RangoInternal,
101
100
  RouterRequestInput,
102
101
  } from "./router/router-interfaces.js";
103
102
 
@@ -116,22 +115,22 @@ import {
116
115
  // Re-export public types and values from extracted modules
117
116
  export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
118
117
  export type {
119
- RSCRouterOptions,
118
+ RangoOptions,
120
119
  RootLayoutProps,
121
120
  SSRStreamMode,
122
121
  SSROptions,
123
122
  ResolveStreamingContext,
124
123
  } from "./router/router-options.js";
125
124
  export type {
126
- RSCRouter,
127
- RSCRouterInternal,
125
+ Rango,
126
+ RangoInternal,
128
127
  RouterRequestInput,
129
128
  } from "./router/router-interfaces.js";
130
129
  export { toInternal } from "./router/router-interfaces.js";
131
130
 
132
131
  export function createRouter<TEnv = any>(
133
- options: RSCRouterOptions<TEnv> = {},
134
- ): RSCRouter<TEnv, {}> {
132
+ options: RangoOptions<TEnv> = {},
133
+ ): Rango<TEnv, {}> {
135
134
  const {
136
135
  id: userProvidedId,
137
136
  $$id: injectedId,
@@ -159,14 +158,23 @@ export function createRouter<TEnv = any>(
159
158
  timeouts: timeoutsOption,
160
159
  onTimeout,
161
160
  originCheck: originCheckOption,
161
+ viewTransition: viewTransitionOption = "auto",
162
+ debugCacheSignal: debugCacheSignalOption = false,
162
163
  } = options;
163
164
 
165
+ // Debug cache signal gate (DEVELOPMENT/TEST ONLY). Enabled by the
166
+ // debugCacheSignal option OR the RANGO_TEST_SIGNALS=1 env flag. When off,
167
+ // no X-Rango-Cache header is emitted and output is byte-identical.
168
+ const cacheSignalEnabled =
169
+ debugCacheSignalOption ||
170
+ (typeof process !== "undefined" &&
171
+ (process as { env?: Record<string, string | undefined> }).env
172
+ ?.RANGO_TEST_SIGNALS === "1");
173
+
164
174
  // 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;
175
+ // A bare "/" is equivalent to no basename. Shared with the testing
176
+ // primitives via normalizeBasename so they can never drift.
177
+ const basename = normalizeBasename(basenameOption);
170
178
 
171
179
  // Resolve telemetry sink (no-op when not configured)
172
180
  const telemetry = resolveSink(telemetrySink);
@@ -366,9 +374,11 @@ export function createRouter<TEnv = any>(
366
374
  getRouterPrecomputedEntries(routerId) ?? getPrecomputedEntries();
367
375
  if (current !== precomputedSource) {
368
376
  precomputedSource = current;
369
- precomputedByPrefix = current
370
- ? new Map(current.map((e) => [e.staticPrefix, e.routes]))
371
- : null;
377
+ // buildPrecomputedByPrefix drops any staticPrefix owned by more than one
378
+ // leaf include instead of collapsing it last-wins (which would mis-assign
379
+ // one include's routes to another's entry and 500 a valid sibling route).
380
+ // Such shared-prefix includes resolve via the handler path instead.
381
+ precomputedByPrefix = current ? buildPrecomputedByPrefix(current) : null;
372
382
  }
373
383
  return precomputedByPrefix;
374
384
  }
@@ -538,6 +548,7 @@ export function createRouter<TEnv = any>(
538
548
  findNearestNotFoundBoundary,
539
549
  notFoundComponent: notFound,
540
550
  callOnError,
551
+ viewTransitionDefault: viewTransitionOption,
541
552
  };
542
553
 
543
554
  // Match API dependencies
@@ -665,6 +676,7 @@ export function createRouter<TEnv = any>(
665
676
  findMatch,
666
677
  findInterceptForRoute,
667
678
  telemetry: telemetrySink,
679
+ cacheSignalEnabled,
668
680
  });
669
681
 
670
682
  const { match, matchPartial, matchError, previewMatch } = matchHandlers;
@@ -674,7 +686,7 @@ export function createRouter<TEnv = any>(
674
686
  * The type system tracks accumulated routes through the builder chain
675
687
  * Initial TRoutes is {} (empty) to avoid poisoning accumulated types with Record<string, string>
676
688
  */
677
- const router: RSCRouterInternal<TEnv, {}> = {
689
+ const router: RangoInternal<TEnv, {}> = {
678
690
  __brand: RSC_ROUTER_BRAND,
679
691
  id: routerId,
680
692
  basename,
@@ -722,7 +734,7 @@ export function createRouter<TEnv = any>(
722
734
  };
723
735
 
724
736
  let handlerResult: AllUseItems[] = [];
725
- RSCRouterContext.run(
737
+ RangoContext.run(
726
738
  {
727
739
  manifest,
728
740
  patterns: routePatterns,
@@ -834,10 +846,13 @@ export function createRouter<TEnv = any>(
834
846
 
835
847
  // Create placeholder RouteEntry for each lazy include
836
848
  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;
849
+ // Compute the full URL prefix (combining parent prefix if any). Use the
850
+ // slash-collapsing join so a trailing-slash parent prefix does not
851
+ // produce a double-slash staticPrefix the trie's sp can never match.
852
+ const fullPrefix = joinPrefix(
853
+ lazyInclude.context.urlPrefix,
854
+ lazyInclude.prefix,
855
+ );
841
856
 
842
857
  const lazyEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
843
858
  prefix: "",
@@ -1000,6 +1015,13 @@ export function createRouter<TEnv = any>(
1000
1015
  // Expose basename for runtime manifest generation
1001
1016
  __basename: basename,
1002
1017
 
1018
+ // Expose router-level boundary defaults for build-time clientChunks
1019
+ // discovery (so a "use client" default boundary lands in app-fallback).
1020
+ // These are createRouter options, never pushed onto EntryData.
1021
+ __defaultErrorBoundary: defaultErrorBoundary,
1022
+ __defaultNotFoundBoundary: defaultNotFoundBoundary,
1023
+ __notFound: notFound,
1024
+
1003
1025
  // RSC request handler (lazily created on first call)
1004
1026
  fetch: (() => {
1005
1027
  // Handler is created on first call and reused
@@ -1046,9 +1068,9 @@ export function createRouter<TEnv = any>(
1046
1068
 
1047
1069
  // If urls option was provided, auto-register them
1048
1070
  if (typeof urlsOption === "function") {
1049
- return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1071
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
1050
1072
  } else if (urlsOption) {
1051
- return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1073
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
1052
1074
  }
1053
1075
 
1054
1076
  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" },