@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc

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 (285) hide show
  1. package/README.md +196 -43
  2. package/dist/bin/rango.js +277 -99
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2779 -1064
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +57 -11
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +155 -6
  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 +249 -17
  21. package/skills/loader/SKILL.md +273 -53
  22. package/skills/middleware/SKILL.md +49 -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 +197 -6
  28. package/skills/prerender/SKILL.md +123 -100
  29. package/skills/rango/SKILL.md +242 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +88 -4
  33. package/skills/router-setup/SKILL.md +90 -5
  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 +716 -0
  37. package/skills/typesafety/SKILL.md +329 -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 +117 -0
  42. package/src/__internal.ts +1 -1
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +91 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/navigation-bridge.ts +102 -16
  50. package/src/browser/navigation-client.ts +164 -59
  51. package/src/browser/navigation-store.ts +75 -17
  52. package/src/browser/navigation-transaction.ts +21 -37
  53. package/src/browser/partial-update.ts +139 -38
  54. package/src/browser/prefetch/cache.ts +175 -15
  55. package/src/browser/prefetch/fetch.ts +180 -33
  56. package/src/browser/prefetch/queue.ts +123 -20
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +53 -13
  59. package/src/browser/react/Link.tsx +81 -9
  60. package/src/browser/react/NavigationProvider.tsx +110 -33
  61. package/src/browser/react/context.ts +7 -2
  62. package/src/browser/react/filter-segment-order.ts +51 -7
  63. package/src/browser/react/index.ts +3 -0
  64. package/src/browser/react/location-state-shared.ts +175 -4
  65. package/src/browser/react/location-state.ts +39 -13
  66. package/src/browser/react/use-handle.ts +23 -64
  67. package/src/browser/react/use-navigation.ts +22 -2
  68. package/src/browser/react/use-params.ts +20 -8
  69. package/src/browser/react/use-reverse.ts +106 -0
  70. package/src/browser/react/use-router.ts +43 -10
  71. package/src/browser/react/use-segments.ts +11 -8
  72. package/src/browser/response-adapter.ts +25 -0
  73. package/src/browser/rsc-router.tsx +191 -74
  74. package/src/browser/scroll-restoration.ts +41 -14
  75. package/src/browser/segment-reconciler.ts +36 -9
  76. package/src/browser/segment-structure-assert.ts +2 -2
  77. package/src/browser/server-action-bridge.ts +31 -36
  78. package/src/browser/types.ts +57 -5
  79. package/src/build/collect-fallback-refs.ts +107 -0
  80. package/src/build/generate-manifest.ts +65 -40
  81. package/src/build/generate-route-types.ts +5 -0
  82. package/src/build/index.ts +2 -0
  83. package/src/build/route-trie.ts +52 -25
  84. package/src/build/route-types/codegen.ts +4 -4
  85. package/src/build/route-types/include-resolution.ts +9 -2
  86. package/src/build/route-types/per-module-writer.ts +7 -4
  87. package/src/build/route-types/router-processing.ts +278 -88
  88. package/src/build/route-types/scan-filter.ts +9 -2
  89. package/src/build/route-types/source-scan.ts +118 -0
  90. package/src/build/runtime-discovery.ts +9 -20
  91. package/src/cache/cache-runtime.ts +15 -11
  92. package/src/cache/cache-scope.ts +76 -49
  93. package/src/cache/cf/cf-cache-store.ts +501 -18
  94. package/src/cache/cf/index.ts +5 -1
  95. package/src/cache/document-cache.ts +17 -7
  96. package/src/cache/index.ts +1 -0
  97. package/src/cache/taint.ts +55 -0
  98. package/src/client.rsc.tsx +3 -0
  99. package/src/client.tsx +94 -238
  100. package/src/context-var.ts +72 -2
  101. package/src/debug.ts +2 -2
  102. package/src/decode-loader-results.ts +36 -0
  103. package/src/errors.ts +30 -1
  104. package/src/handle.ts +65 -12
  105. package/src/host/index.ts +2 -2
  106. package/src/host/router.ts +129 -57
  107. package/src/host/types.ts +31 -2
  108. package/src/host/utils.ts +1 -1
  109. package/src/href-client.ts +140 -20
  110. package/src/index.rsc.ts +12 -5
  111. package/src/index.ts +61 -11
  112. package/src/loader-store.ts +500 -0
  113. package/src/loader.rsc.ts +2 -5
  114. package/src/loader.ts +3 -10
  115. package/src/missing-id-error.ts +68 -0
  116. package/src/outlet-context.ts +1 -1
  117. package/src/prerender/store.ts +5 -4
  118. package/src/prerender.ts +141 -80
  119. package/src/response-utils.ts +37 -0
  120. package/src/reverse.ts +65 -15
  121. package/src/route-content-wrapper.tsx +6 -28
  122. package/src/route-definition/dsl-helpers.ts +435 -260
  123. package/src/route-definition/helper-factories.ts +29 -139
  124. package/src/route-definition/helpers-types.ts +110 -34
  125. package/src/route-definition/index.ts +3 -0
  126. package/src/route-definition/redirect.ts +11 -3
  127. package/src/route-definition/resolve-handler-use.ts +155 -0
  128. package/src/route-definition/use-item-types.ts +32 -0
  129. package/src/route-map-builder.ts +7 -1
  130. package/src/route-types.ts +37 -41
  131. package/src/router/basename.ts +14 -0
  132. package/src/router/content-negotiation.ts +113 -1
  133. package/src/router/error-handling.ts +1 -1
  134. package/src/router/find-match.ts +4 -2
  135. package/src/router/handler-context.ts +77 -38
  136. package/src/router/intercept-resolution.ts +15 -22
  137. package/src/router/lazy-includes.ts +12 -9
  138. package/src/router/loader-resolution.ts +174 -22
  139. package/src/router/logging.ts +5 -2
  140. package/src/router/manifest.ts +31 -16
  141. package/src/router/match-api.ts +128 -192
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/background-revalidation.ts +30 -2
  144. package/src/router/match-middleware/cache-lookup.ts +136 -106
  145. package/src/router/match-middleware/cache-store.ts +54 -10
  146. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  147. package/src/router/match-middleware/segment-resolution.ts +61 -5
  148. package/src/router/match-result.ts +125 -10
  149. package/src/router/metrics.ts +7 -2
  150. package/src/router/middleware-types.ts +21 -34
  151. package/src/router/middleware.ts +103 -90
  152. package/src/router/navigation-snapshot.ts +182 -0
  153. package/src/router/pattern-matching.ts +101 -17
  154. package/src/router/prerender-match.ts +110 -10
  155. package/src/router/preview-match.ts +32 -102
  156. package/src/router/request-classification.ts +286 -0
  157. package/src/router/revalidation.ts +58 -2
  158. package/src/router/route-snapshot.ts +245 -0
  159. package/src/router/router-context.ts +6 -1
  160. package/src/router/router-interfaces.ts +77 -28
  161. package/src/router/router-options.ts +76 -11
  162. package/src/router/router-registry.ts +2 -5
  163. package/src/router/segment-resolution/fresh.ts +223 -24
  164. package/src/router/segment-resolution/helpers.ts +29 -24
  165. package/src/router/segment-resolution/loader-cache.ts +1 -0
  166. package/src/router/segment-resolution/revalidation.ts +466 -285
  167. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  168. package/src/router/segment-wrappers.ts +2 -0
  169. package/src/router/substitute-pattern-params.ts +56 -0
  170. package/src/router/telemetry.ts +99 -0
  171. package/src/router/trie-matching.ts +18 -13
  172. package/src/router/types.ts +9 -0
  173. package/src/router/url-params.ts +49 -0
  174. package/src/router.ts +91 -23
  175. package/src/rsc/handler-context.ts +2 -2
  176. package/src/rsc/handler.ts +440 -381
  177. package/src/rsc/helpers.ts +91 -43
  178. package/src/rsc/index.ts +1 -1
  179. package/src/rsc/loader-fetch.ts +23 -3
  180. package/src/rsc/manifest-init.ts +5 -1
  181. package/src/rsc/origin-guard.ts +28 -10
  182. package/src/rsc/progressive-enhancement.ts +18 -2
  183. package/src/rsc/response-route-handler.ts +46 -53
  184. package/src/rsc/rsc-rendering.ts +41 -48
  185. package/src/rsc/runtime-warnings.ts +9 -10
  186. package/src/rsc/server-action.ts +25 -37
  187. package/src/rsc/ssr-setup.ts +18 -2
  188. package/src/rsc/types.ts +17 -3
  189. package/src/search-params.ts +4 -4
  190. package/src/segment-content-promise.ts +67 -0
  191. package/src/segment-loader-promise.ts +122 -0
  192. package/src/segment-system.tsx +219 -67
  193. package/src/serialize.ts +243 -0
  194. package/src/server/context.ts +277 -61
  195. package/src/server/cookie-store.ts +28 -4
  196. package/src/server/handle-store.ts +19 -0
  197. package/src/server/loader-registry.ts +9 -8
  198. package/src/server/request-context.ts +204 -60
  199. package/src/ssr/index.tsx +9 -1
  200. package/src/static-handler.ts +19 -7
  201. package/src/testing/cache-status.ts +166 -0
  202. package/src/testing/collect-handle.ts +63 -0
  203. package/src/testing/dispatch.ts +440 -0
  204. package/src/testing/dom.entry.ts +22 -0
  205. package/src/testing/e2e/fixture.ts +154 -0
  206. package/src/testing/e2e/index.ts +149 -0
  207. package/src/testing/e2e/matchers.ts +51 -0
  208. package/src/testing/e2e/page-helpers.ts +272 -0
  209. package/src/testing/e2e/parity.ts +306 -0
  210. package/src/testing/e2e/server.ts +183 -0
  211. package/src/testing/flight-matchers.ts +104 -0
  212. package/src/testing/flight-runtime.d.ts +21 -0
  213. package/src/testing/flight.entry.ts +22 -0
  214. package/src/testing/flight.ts +182 -0
  215. package/src/testing/generated-routes.ts +223 -0
  216. package/src/testing/index.ts +106 -0
  217. package/src/testing/internal/context.ts +255 -0
  218. package/src/testing/render-route.tsx +565 -0
  219. package/src/testing/run-loader.ts +296 -0
  220. package/src/testing/run-middleware.ts +179 -0
  221. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  222. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  223. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  224. package/src/testing/vitest-stubs/version.ts +5 -0
  225. package/src/testing/vitest.ts +183 -0
  226. package/src/types/cache-types.ts +4 -4
  227. package/src/types/global-namespace.ts +39 -26
  228. package/src/types/handler-context.ts +194 -72
  229. package/src/types/index.ts +1 -0
  230. package/src/types/loader-types.ts +41 -15
  231. package/src/types/request-scope.ts +126 -0
  232. package/src/types/route-entry.ts +19 -1
  233. package/src/types/segments.ts +37 -1
  234. package/src/urls/include-helper.ts +34 -67
  235. package/src/urls/index.ts +0 -3
  236. package/src/urls/path-helper-types.ts +50 -9
  237. package/src/urls/path-helper.ts +63 -63
  238. package/src/urls/pattern-types.ts +48 -19
  239. package/src/urls/response-types.ts +25 -22
  240. package/src/urls/type-extraction.ts +26 -116
  241. package/src/urls/urls-function.ts +1 -5
  242. package/src/use-loader.tsx +487 -44
  243. package/src/vite/debug.ts +185 -0
  244. package/src/vite/discovery/bundle-postprocess.ts +34 -37
  245. package/src/vite/discovery/discover-routers.ts +105 -51
  246. package/src/vite/discovery/discovery-errors.ts +194 -0
  247. package/src/vite/discovery/gate-state.ts +171 -0
  248. package/src/vite/discovery/prerender-collection.ts +188 -93
  249. package/src/vite/discovery/route-types-writer.ts +40 -84
  250. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  251. package/src/vite/discovery/state.ts +46 -6
  252. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  253. package/src/vite/index.ts +6 -0
  254. package/src/vite/plugin-types.ts +111 -72
  255. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  256. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  257. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  258. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  259. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  260. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  261. package/src/vite/plugins/expose-action-id.ts +55 -33
  262. package/src/vite/plugins/expose-id-utils.ts +24 -8
  263. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  264. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  265. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  266. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  267. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  268. package/src/vite/plugins/performance-tracks.ts +92 -0
  269. package/src/vite/plugins/refresh-cmd.ts +88 -26
  270. package/src/vite/plugins/use-cache-transform.ts +65 -50
  271. package/src/vite/plugins/version-injector.ts +39 -23
  272. package/src/vite/plugins/version-plugin.ts +72 -3
  273. package/src/vite/plugins/virtual-entries.ts +2 -2
  274. package/src/vite/rango.ts +265 -226
  275. package/src/vite/router-discovery.ts +920 -137
  276. package/src/vite/utils/ast-handler-extract.ts +15 -15
  277. package/src/vite/utils/banner.ts +4 -4
  278. package/src/vite/utils/bundle-analysis.ts +4 -2
  279. package/src/vite/utils/client-chunks.ts +190 -0
  280. package/src/vite/utils/forward-user-plugins.ts +193 -0
  281. package/src/vite/utils/manifest-utils.ts +21 -5
  282. package/src/vite/utils/package-resolution.ts +41 -1
  283. package/src/vite/utils/prerender-utils.ts +38 -5
  284. package/src/vite/utils/shared-utils.ts +109 -27
  285. package/src/browser/action-response-classifier.ts +0 -99
@@ -0,0 +1,36 @@
1
+ /**
2
+ * View-transition boundary default resolution.
3
+ *
4
+ * Kept in its own module (rather than helpers.ts) because several resolution
5
+ * tests mock helpers.ts with an explicit export list; a shared util here is
6
+ * never mocked, so the fresh and revalidation paths always get the real
7
+ * implementation.
8
+ */
9
+
10
+ import type { EntryData } from "../../server/context";
11
+
12
+ /**
13
+ * Resolve the effective `viewTransition` for a segment's transition config.
14
+ *
15
+ * The per-segment value (set via the transition() DSL) always wins. When it is
16
+ * unset, the router-level createRouter({ viewTransition }) default is stamped
17
+ * in so the render gate reads the boundary decision off the segment — server
18
+ * and client, via the serialized segment — without the router option being
19
+ * threaded to the client. Only `false` is ever stamped; an unset (or "auto")
20
+ * value is left untouched because it already means "wrap" at the gate, which
21
+ * also avoids needless object allocation and payload growth. Used by both the
22
+ * fresh and revalidation resolution paths.
23
+ */
24
+ export function applyViewTransitionDefault(
25
+ transition: EntryData["transition"],
26
+ viewTransitionDefault: "auto" | false | undefined,
27
+ ): EntryData["transition"] {
28
+ if (!transition) return transition;
29
+ if (
30
+ transition.viewTransition === undefined &&
31
+ viewTransitionDefault === false
32
+ ) {
33
+ return { ...transition, viewTransition: false };
34
+ }
35
+ return transition;
36
+ }
@@ -204,6 +204,7 @@ export function createSegmentWrappers<TEnv = any>(
204
204
  interceptResult: { intercept: InterceptEntry; entry: EntryData } | null,
205
205
  localRouteName: string,
206
206
  pathname: string,
207
+ stale?: boolean,
207
208
  ): ReturnType<typeof _resolveAllSegmentsWithRevalidation> {
208
209
  return _resolveAllSegmentsWithRevalidation(
209
210
  entries,
@@ -221,6 +222,7 @@ export function createSegmentWrappers<TEnv = any>(
221
222
  localRouteName,
222
223
  pathname,
223
224
  segmentDeps,
225
+ stale,
224
226
  );
225
227
  }
226
228
 
@@ -0,0 +1,56 @@
1
+ import { encodePathSegment } from "./url-params.js";
2
+
3
+ /**
4
+ * Substitute `:param` placeholders in a route pattern with values from
5
+ * `params`. Two-pass: optional params (`:name?`) first so absent values
6
+ * collapse cleanly, then required params (throws on missing). Constraint
7
+ * syntax (`:name(en|gb)`) is stripped from the result. Trailing-slash
8
+ * patterns like `/blog/` are preserved unless an optional segment was
9
+ * actually omitted.
10
+ *
11
+ * Shared by `ctx.reverse()` (server), `createReverse()` (typed runtime
12
+ * helper), and `useReverse()` (client hook). The behavior must stay
13
+ * identical across all three call sites.
14
+ */
15
+ export function substitutePatternParams(
16
+ pattern: string,
17
+ params: Record<string, string | undefined>,
18
+ routeName: string,
19
+ ): string {
20
+ let result = pattern;
21
+ let hadOmittedOptional = false;
22
+
23
+ result = result.replace(
24
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
25
+ (_match, key) => {
26
+ const value = params[key as string];
27
+ // The matcher omits absent optional params (so `value` is `undefined`
28
+ // here), but caller-supplied params or `getParams()` shapes may still
29
+ // pass `""` explicitly. Treat both as the absent form.
30
+ if (value === undefined || value === "") {
31
+ hadOmittedOptional = true;
32
+ return "";
33
+ }
34
+ return encodePathSegment(value);
35
+ },
36
+ );
37
+
38
+ result = result.replace(
39
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
40
+ (_match, key) => {
41
+ const value = params[key as string];
42
+ if (value === undefined) {
43
+ throw new Error(`Missing param "${key}" for route "${routeName}"`);
44
+ }
45
+ return encodePathSegment(value);
46
+ },
47
+ );
48
+
49
+ if (hadOmittedOptional) {
50
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
51
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
52
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
53
+ }
54
+
55
+ return result;
56
+ }
@@ -90,6 +90,34 @@ export interface HandlerErrorEvent extends BaseEvent {
90
90
  params?: Record<string, string>;
91
91
  }
92
92
 
93
+ /**
94
+ * Per-segment (or coarse route-level) cache status carried on the
95
+ * cache.decision telemetry event and the X-Rango-Cache debug header.
96
+ *
97
+ * v1 is COARSE: the router's pipeline tracks cache decisions at the
98
+ * route/entry level (cacheHit/cacheSource/shouldRevalidate), not per
99
+ * individual segment. The `segments` array therefore contains a single
100
+ * route-level entry keyed by the route key. The shape is forward-compatible
101
+ * with genuine per-segment status if the pipeline later exposes it.
102
+ */
103
+ export type CacheSegmentStatus =
104
+ | "hit"
105
+ | "miss"
106
+ | "stale"
107
+ | "prerendered"
108
+ | "passthrough";
109
+
110
+ export interface CacheSegmentSignal {
111
+ /** Segment id (v1: the route key, since status is route-level). */
112
+ id: string;
113
+ /** Segment type (v1: "route" for the coarse route-level entry). */
114
+ type: string;
115
+ /** Resolved cache status for this segment. */
116
+ cacheStatus: CacheSegmentStatus;
117
+ /** Whether stale-while-revalidate was triggered for this segment. */
118
+ shouldRevalidate?: boolean;
119
+ }
120
+
93
121
  export interface CacheDecisionEvent extends BaseEvent {
94
122
  type: "cache.decision";
95
123
  pathname: string;
@@ -98,6 +126,12 @@ export interface CacheDecisionEvent extends BaseEvent {
98
126
  /** Whether stale-while-revalidate was triggered */
99
127
  shouldRevalidate: boolean;
100
128
  source?: "runtime" | "prerender";
129
+ /**
130
+ * Optional per-segment (v1: coarse route-level) cache status. Present only
131
+ * when telemetry or the debug cache signal is enabled. Optional so existing
132
+ * sinks are unaffected.
133
+ */
134
+ segments?: CacheSegmentSignal[];
101
135
  }
102
136
 
103
137
  export interface RevalidationDecisionEvent extends BaseEvent {
@@ -140,6 +174,71 @@ export type TelemetryEvent =
140
174
  | RequestTimeoutEvent
141
175
  | OriginCheckRejectedEvent;
142
176
 
177
+ // ---------------------------------------------------------------------------
178
+ // Cache signal derivation (coarse, route-level)
179
+ // ---------------------------------------------------------------------------
180
+
181
+ /**
182
+ * Derive the coarse, route-level cache status from pipeline cache state.
183
+ *
184
+ * v1 mapping (route-level — see CacheSegmentSignal):
185
+ * - prerender hit -> "prerendered"
186
+ * - runtime hit + shouldRevalidate (SWR) -> "stale"
187
+ * - runtime hit -> "hit"
188
+ * - no hit -> "miss"
189
+ *
190
+ * Note: "passthrough" is a build-time prerender concept (a route opts out of
191
+ * being prerendered for some params). At runtime a passthrough route renders
192
+ * fresh and is indistinguishable from a normal miss in the pipeline state, so
193
+ * v1 reports it as "miss". The "passthrough" status remains in the type union
194
+ * for forward compatibility.
195
+ */
196
+ export function deriveCacheStatus(state: {
197
+ cacheHit: boolean;
198
+ cacheSource?: "runtime" | "prerender";
199
+ shouldRevalidate?: boolean;
200
+ }): CacheSegmentStatus {
201
+ if (state.cacheHit) {
202
+ if (state.cacheSource === "prerender") return "prerendered";
203
+ if (state.shouldRevalidate) return "stale";
204
+ return "hit";
205
+ }
206
+ return "miss";
207
+ }
208
+
209
+ /**
210
+ * Build the coarse route-level cache signal array (a single entry keyed by
211
+ * the route key). Used for both the cache.decision telemetry event and the
212
+ * X-Rango-Cache debug header.
213
+ */
214
+ export function buildCacheSignalSegments(
215
+ routeKey: string,
216
+ state: {
217
+ cacheHit: boolean;
218
+ cacheSource?: "runtime" | "prerender";
219
+ shouldRevalidate?: boolean;
220
+ },
221
+ ): CacheSegmentSignal[] {
222
+ return [
223
+ {
224
+ id: routeKey,
225
+ type: "route",
226
+ cacheStatus: deriveCacheStatus(state),
227
+ shouldRevalidate: !!state.shouldRevalidate,
228
+ },
229
+ ];
230
+ }
231
+
232
+ /**
233
+ * Serialize cache signal segments into the X-Rango-Cache header value:
234
+ * `<segId>=<status>, <segId2>=<status2>`.
235
+ */
236
+ export function formatCacheSignalHeader(
237
+ segments: CacheSegmentSignal[],
238
+ ): string {
239
+ return segments.map((s) => `${s.id}=${s.cacheStatus}`).join(", ");
240
+ }
241
+
143
242
  // ---------------------------------------------------------------------------
144
243
  // Sink interface
145
244
  // ---------------------------------------------------------------------------
@@ -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;
@@ -96,7 +96,16 @@ export interface SegmentResolutionDeps<TEnv = any> {
96
96
  findNearestNotFoundBoundary: (
97
97
  entry: EntryData | null,
98
98
  ) => ReactNode | NotFoundBoundaryHandler | null;
99
+ notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
99
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;
100
109
  }
101
110
 
102
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
@@ -19,11 +19,12 @@ import {
19
19
  import MapRootLayout from "./server/root-layout.js";
20
20
  import type { AllUseItems } from "./route-types.js";
21
21
  import type { UrlPatterns } from "./urls.js";
22
+ import type { UrlBuilder } from "./urls/pattern-types.js";
23
+ import { urls } from "./urls.js";
22
24
  import {
23
- EntryData,
24
- InterceptSelectorContext,
25
+ type EntryData,
25
26
  getContext,
26
- RSCRouterContext,
27
+ RangoContext,
27
28
  type MetricsStore,
28
29
  } from "./server/context";
29
30
  import { createHandleStore, type HandleStore } from "./server/handle-store.js";
@@ -55,6 +56,7 @@ import { buildDebugManifest } from "./router/debug-manifest.js";
55
56
 
56
57
  import type { SegmentResolutionDeps, MatchApiDeps } from "./router/types.js";
57
58
  import { createHandlerContext } from "./router/handler-context.js";
59
+ import { normalizeBasename } from "./router/basename.js";
58
60
  import {
59
61
  setupLoaderAccess,
60
62
  setupLoaderAccessSilent,
@@ -89,13 +91,10 @@ import {
89
91
  RouterRegistry,
90
92
  nextRouterAutoId,
91
93
  } from "./router/router-registry.js";
94
+ import type { RangoOptions, RootLayoutProps } from "./router/router-options.js";
92
95
  import type {
93
- RSCRouterOptions,
94
- RootLayoutProps,
95
- } from "./router/router-options.js";
96
- import type {
97
- RSCRouter,
98
- RSCRouterInternal,
96
+ Rango,
97
+ RangoInternal,
99
98
  RouterRequestInput,
100
99
  } from "./router/router-interfaces.js";
101
100
 
@@ -114,25 +113,26 @@ import {
114
113
  // Re-export public types and values from extracted modules
115
114
  export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
116
115
  export type {
117
- RSCRouterOptions,
116
+ RangoOptions,
118
117
  RootLayoutProps,
119
118
  SSRStreamMode,
120
119
  SSROptions,
121
120
  ResolveStreamingContext,
122
121
  } from "./router/router-options.js";
123
122
  export type {
124
- RSCRouter,
125
- RSCRouterInternal,
123
+ Rango,
124
+ RangoInternal,
126
125
  RouterRequestInput,
127
126
  } from "./router/router-interfaces.js";
128
127
  export { toInternal } from "./router/router-interfaces.js";
129
128
 
130
129
  export function createRouter<TEnv = any>(
131
- options: RSCRouterOptions<TEnv> = {},
132
- ): RSCRouter<TEnv, {}> {
130
+ options: RangoOptions<TEnv> = {},
131
+ ): Rango<TEnv, {}> {
133
132
  const {
134
133
  id: userProvidedId,
135
134
  $$id: injectedId,
135
+ basename: basenameOption,
136
136
  debugPerformance = false,
137
137
  document: documentOption,
138
138
  defaultErrorBoundary,
@@ -156,8 +156,24 @@ export function createRouter<TEnv = any>(
156
156
  timeouts: timeoutsOption,
157
157
  onTimeout,
158
158
  originCheck: originCheckOption,
159
+ viewTransition: viewTransitionOption = "auto",
160
+ debugCacheSignal: debugCacheSignalOption = false,
159
161
  } = options;
160
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
+
172
+ // Normalize basename: ensure leading slash, strip trailing slash.
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);
176
+
161
177
  // Resolve telemetry sink (no-op when not configured)
162
178
  const telemetry = resolveSink(telemetrySink);
163
179
 
@@ -526,7 +542,9 @@ export function createRouter<TEnv = any>(
526
542
  trackHandler,
527
543
  findNearestErrorBoundary,
528
544
  findNearestNotFoundBoundary,
545
+ notFoundComponent: notFound,
529
546
  callOnError,
547
+ viewTransitionDefault: viewTransitionOption,
530
548
  };
531
549
 
532
550
  // Match API dependencies
@@ -560,6 +578,7 @@ export function createRouter<TEnv = any>(
560
578
  mergedRouteMap,
561
579
  nextMountIndex: () => mountIndex++,
562
580
  getPrecomputedByPrefix,
581
+ routerId,
563
582
  };
564
583
 
565
584
  function evaluateLazyEntry(entry: RouteEntry<TEnv>): void {
@@ -613,6 +632,8 @@ export function createRouter<TEnv = any>(
613
632
  params: Record<string, string>,
614
633
  buildVars?: Record<string, any>,
615
634
  isPassthroughRoute?: boolean,
635
+ buildEnv?: TEnv,
636
+ devMode?: boolean,
616
637
  ) {
617
638
  return _matchForPrerender(
618
639
  pathname,
@@ -620,6 +641,8 @@ export function createRouter<TEnv = any>(
620
641
  prerenderDeps,
621
642
  buildVars,
622
643
  isPassthroughRoute,
644
+ buildEnv,
645
+ devMode,
623
646
  );
624
647
  }
625
648
 
@@ -627,12 +650,16 @@ export function createRouter<TEnv = any>(
627
650
  handler: Function,
628
651
  handlerId: string,
629
652
  routeName?: string,
653
+ buildEnv?: TEnv,
654
+ devMode?: boolean,
630
655
  ) {
631
656
  return _renderStaticSegment<TEnv>(
632
657
  handler,
633
658
  handlerId,
634
659
  mergedRouteMap,
635
660
  routeName,
661
+ buildEnv,
662
+ devMode,
636
663
  );
637
664
  }
638
665
 
@@ -645,6 +672,7 @@ export function createRouter<TEnv = any>(
645
672
  findMatch,
646
673
  findInterceptForRoute,
647
674
  telemetry: telemetrySink,
675
+ cacheSignalEnabled,
648
676
  });
649
677
 
650
678
  const { match, matchPartial, matchError, previewMatch } = matchHandlers;
@@ -654,11 +682,18 @@ export function createRouter<TEnv = any>(
654
682
  * The type system tracks accumulated routes through the builder chain
655
683
  * Initial TRoutes is {} (empty) to avoid poisoning accumulated types with Record<string, string>
656
684
  */
657
- const router: RSCRouterInternal<TEnv, {}> = {
685
+ const router: RangoInternal<TEnv, {}> = {
658
686
  __brand: RSC_ROUTER_BRAND,
659
687
  id: routerId,
688
+ basename,
689
+
690
+ routes(patternsOrBuilder: UrlPatterns<TEnv> | UrlBuilder<TEnv>): any {
691
+ // Wrap builder functions in urls() automatically
692
+ const urlPatterns: UrlPatterns<TEnv> =
693
+ typeof patternsOrBuilder === "function"
694
+ ? (urls(patternsOrBuilder) as UrlPatterns<TEnv>)
695
+ : patternsOrBuilder;
660
696
 
661
- routes(urlPatterns: UrlPatterns<TEnv>): any {
662
697
  // Store reference for runtime manifest generation
663
698
  storedUrlPatterns = urlPatterns;
664
699
  const currentMountIndex = mountIndex++;
@@ -689,13 +724,13 @@ export function createRouter<TEnv = any>(
689
724
  errorBoundary: [],
690
725
  notFoundBoundary: [],
691
726
  layout: [],
692
- parallel: [],
727
+ parallel: {},
693
728
  intercept: [],
694
729
  loader: [],
695
730
  };
696
731
 
697
732
  let handlerResult: AllUseItems[] = [];
698
- RSCRouterContext.run(
733
+ RangoContext.run(
699
734
  {
700
735
  manifest,
701
736
  patterns: routePatterns,
@@ -706,6 +741,10 @@ export function createRouter<TEnv = any>(
706
741
  counters: {},
707
742
  mountIndex: currentMountIndex,
708
743
  cacheProfiles: resolvedCacheProfiles,
744
+ // basename sets the initial URL prefix so all path() patterns
745
+ // are registered with the prefix (e.g. "/admin" + "/users" = "/admin/users").
746
+ // No namePrefix — route names stay unprefixed.
747
+ ...(basename ? { urlPrefix: basename } : {}),
709
748
  },
710
749
  () => {
711
750
  handlerResult = urlPatterns.handler() as AllUseItems[];
@@ -725,7 +764,7 @@ export function createRouter<TEnv = any>(
725
764
  if (entry.type === "route" && entry.isPrerender) {
726
765
  if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
727
766
  prerenderRouteKeys.add(name);
728
- if (entry.prerenderDef?.options?.passthrough === true) {
767
+ if (entry.isPassthrough === true) {
729
768
  if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
730
769
  passthroughRouteKeys.add(name);
731
770
  }
@@ -751,6 +790,7 @@ export function createRouter<TEnv = any>(
751
790
  trailingSlash: trailingSlashConfig,
752
791
  handler: urlPatterns.handler,
753
792
  mountIndex: currentMountIndex,
793
+ routerId,
754
794
  cacheProfiles: resolvedCacheProfiles,
755
795
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
756
796
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
@@ -770,6 +810,7 @@ export function createRouter<TEnv = any>(
770
810
  trailingSlash: trailingSlashConfig,
771
811
  handler: urlPatterns.handler,
772
812
  mountIndex: currentMountIndex,
813
+ routerId,
773
814
  cacheProfiles: resolvedCacheProfiles,
774
815
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
775
816
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
@@ -813,6 +854,7 @@ export function createRouter<TEnv = any>(
813
854
  trailingSlash: trailingSlashConfig,
814
855
  handler: urlPatterns.handler,
815
856
  mountIndex: mountIndex++,
857
+ routerId,
816
858
  // Lazy evaluation fields
817
859
  lazy: true,
818
860
  lazyPatterns: lazyInclude.patterns,
@@ -851,8 +893,18 @@ export function createRouter<TEnv = any>(
851
893
  patternOrMiddleware: string | MiddlewareFn<TEnv>,
852
894
  middleware?: MiddlewareFn<TEnv>,
853
895
  ): any {
854
- // Global middleware - no mount prefix
855
- addMiddleware(patternOrMiddleware, middleware, null);
896
+ // Auto-prefix pattern with basename so router-level middleware
897
+ // patterns are router-relative (e.g. "/users/*" matches "/app/users/*").
898
+ if (basename && typeof patternOrMiddleware === "string") {
899
+ const pattern = patternOrMiddleware;
900
+ const prefixed =
901
+ pattern === "/*" || pattern === "*"
902
+ ? `${basename}/*`
903
+ : `${basename}${pattern}`;
904
+ addMiddleware(prefixed, middleware, null);
905
+ } else {
906
+ addMiddleware(patternOrMiddleware, middleware, null);
907
+ }
856
908
  return router;
857
909
  },
858
910
 
@@ -953,6 +1005,16 @@ export function createRouter<TEnv = any>(
953
1005
  // Expose source file for per-router type generation
954
1006
  __sourceFile,
955
1007
 
1008
+ // Expose basename for runtime manifest generation
1009
+ __basename: basename,
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
+
956
1018
  // RSC request handler (lazily created on first call)
957
1019
  fetch: (() => {
958
1020
  // Handler is created on first call and reused
@@ -986,6 +1048,10 @@ export function createRouter<TEnv = any>(
986
1048
  };
987
1049
  })(),
988
1050
 
1051
+ // Low-level route matching for request classification
1052
+ findMatch: (pathname: string, metricsStore?: any) =>
1053
+ findMatch(pathname, metricsStore),
1054
+
989
1055
  // Debug utility for manifest inspection
990
1056
  debugManifest: () => buildDebugManifest<TEnv>(routesEntries),
991
1057
  };
@@ -994,8 +1060,10 @@ export function createRouter<TEnv = any>(
994
1060
  RouterRegistry.set(routerId, router);
995
1061
 
996
1062
  // If urls option was provided, auto-register them
997
- if (urlsOption) {
998
- return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1063
+ if (typeof urlsOption === "function") {
1064
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
1065
+ } else if (urlsOption) {
1066
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
999
1067
  }
1000
1068
 
1001
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"];