@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125

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 (260) hide show
  1. package/dist/bin/rango.js +10 -6
  2. package/dist/testing/vitest.js +82 -0
  3. package/dist/vite/index.js +55 -48
  4. package/package.json +61 -21
  5. package/skills/caching/SKILL.md +2 -1
  6. package/skills/hooks/SKILL.md +40 -29
  7. package/skills/host-router/SKILL.md +16 -2
  8. package/skills/intercept/SKILL.md +4 -2
  9. package/skills/layout/SKILL.md +11 -6
  10. package/skills/loader/SKILL.md +6 -2
  11. package/skills/middleware/SKILL.md +4 -2
  12. package/skills/migrate-nextjs/SKILL.md +3 -1
  13. package/skills/parallel/SKILL.md +9 -4
  14. package/skills/rango/SKILL.md +12 -0
  15. package/skills/route/SKILL.md +10 -2
  16. package/skills/testing/SKILL.md +129 -0
  17. package/skills/testing/bindings.md +89 -0
  18. package/skills/testing/cache-prerender.md +98 -0
  19. package/skills/testing/client-components.md +122 -0
  20. package/skills/testing/e2e-parity.md +125 -0
  21. package/skills/testing/flight.md +89 -0
  22. package/skills/testing/handles.md +129 -0
  23. package/skills/testing/loader.md +128 -0
  24. package/skills/testing/middleware.md +99 -0
  25. package/skills/testing/render-handler.md +118 -0
  26. package/skills/testing/response-routes.md +95 -0
  27. package/skills/testing/reverse-and-types.md +84 -0
  28. package/skills/testing/server-actions.md +107 -0
  29. package/skills/testing/server-tree.md +128 -0
  30. package/skills/testing/setup.md +120 -0
  31. package/src/__internal.ts +0 -65
  32. package/src/browser/action-coordinator.ts +1 -1
  33. package/src/browser/action-fence.ts +47 -0
  34. package/src/browser/cookie-name.ts +140 -0
  35. package/src/browser/event-controller.ts +1 -83
  36. package/src/browser/invalidate-client-cache.ts +52 -0
  37. package/src/browser/navigation-bridge.ts +14 -1
  38. package/src/browser/navigation-client.ts +14 -1
  39. package/src/browser/navigation-store-handle.ts +38 -0
  40. package/src/browser/navigation-store.ts +26 -51
  41. package/src/browser/navigation-transaction.ts +0 -32
  42. package/src/browser/partial-update.ts +1 -83
  43. package/src/browser/prefetch/cache.ts +6 -45
  44. package/src/browser/prefetch/fetch.ts +7 -0
  45. package/src/browser/prefetch/queue.ts +6 -3
  46. package/src/browser/rango-state.ts +157 -99
  47. package/src/browser/react/Link.tsx +0 -2
  48. package/src/browser/react/NavigationProvider.tsx +2 -1
  49. package/src/browser/react/ScrollRestoration.tsx +10 -6
  50. package/src/browser/react/filter-segment-order.ts +0 -2
  51. package/src/browser/react/index.ts +0 -51
  52. package/src/browser/react/location-state-shared.ts +0 -13
  53. package/src/browser/react/location-state.ts +0 -1
  54. package/src/browser/react/use-action.ts +6 -15
  55. package/src/browser/react/use-handle.ts +0 -5
  56. package/src/browser/react/use-link-status.ts +0 -4
  57. package/src/browser/react/use-navigation.ts +0 -3
  58. package/src/browser/react/use-params.ts +0 -2
  59. package/src/browser/react/use-search-params.ts +0 -5
  60. package/src/browser/react/use-segments.ts +0 -13
  61. package/src/browser/rsc-router.tsx +12 -4
  62. package/src/browser/server-action-bridge.ts +77 -15
  63. package/src/browser/types.ts +7 -2
  64. package/src/browser/validate-redirect-origin.ts +4 -5
  65. package/src/build/route-trie.ts +3 -0
  66. package/src/build/route-types/param-extraction.ts +6 -3
  67. package/src/build/route-types/router-processing.ts +0 -8
  68. package/src/cache/cache-policy.ts +0 -54
  69. package/src/cache/cache-runtime.ts +27 -24
  70. package/src/cache/cache-scope.ts +0 -27
  71. package/src/cache/cache-tag.ts +0 -37
  72. package/src/cache/cf/cf-cache-store.ts +94 -46
  73. package/src/cache/cf/index.ts +0 -24
  74. package/src/cache/document-cache.ts +11 -36
  75. package/src/cache/handle-snapshot.ts +0 -40
  76. package/src/cache/index.ts +0 -27
  77. package/src/cache/memory-segment-store.ts +2 -48
  78. package/src/cache/profile-registry.ts +7 -3
  79. package/src/cache/read-through-swr.ts +41 -11
  80. package/src/cache/segment-codec.ts +0 -16
  81. package/src/cache/types.ts +0 -98
  82. package/src/client.rsc.tsx +1 -22
  83. package/src/client.tsx +14 -38
  84. package/src/component-utils.ts +19 -0
  85. package/src/deps/ssr.ts +0 -1
  86. package/src/handle.ts +28 -18
  87. package/src/handles/MetaTags.tsx +0 -14
  88. package/src/handles/meta.ts +0 -39
  89. package/src/host/cookie-handler.ts +0 -36
  90. package/src/host/errors.ts +0 -24
  91. package/src/host/index.ts +6 -0
  92. package/src/host/pattern-matcher.ts +7 -50
  93. package/src/host/router.ts +1 -65
  94. package/src/host/testing.ts +40 -27
  95. package/src/host/types.ts +6 -2
  96. package/src/href-client.ts +0 -4
  97. package/src/index.rsc.ts +42 -3
  98. package/src/index.ts +31 -1
  99. package/src/internal-debug.ts +2 -4
  100. package/src/loader.rsc.ts +19 -9
  101. package/src/loader.ts +12 -4
  102. package/src/network-error-thrower.tsx +1 -6
  103. package/src/outlet-provider.tsx +1 -5
  104. package/src/prerender/param-hash.ts +10 -11
  105. package/src/prerender/store.ts +23 -30
  106. package/src/prerender.ts +58 -3
  107. package/src/root-error-boundary.tsx +1 -19
  108. package/src/route-content-wrapper.tsx +1 -44
  109. package/src/route-definition/dsl-helpers.ts +7 -19
  110. package/src/route-definition/helpers-types.ts +3 -3
  111. package/src/route-definition/redirect.ts +11 -1
  112. package/src/route-map-builder.ts +0 -16
  113. package/src/router/basename.ts +14 -0
  114. package/src/router/content-negotiation.ts +0 -13
  115. package/src/router/error-handling.ts +12 -16
  116. package/src/router/find-match.ts +4 -30
  117. package/src/router/intercept-resolution.ts +10 -1
  118. package/src/router/lazy-includes.ts +1 -57
  119. package/src/router/loader-resolution.ts +3 -2
  120. package/src/router/logging.ts +0 -6
  121. package/src/router/manifest.ts +1 -25
  122. package/src/router/match-api.ts +0 -20
  123. package/src/router/match-context.ts +0 -22
  124. package/src/router/match-handlers.ts +57 -58
  125. package/src/router/match-middleware/background-revalidation.ts +0 -7
  126. package/src/router/match-middleware/cache-lookup.ts +1 -54
  127. package/src/router/match-middleware/cache-store.ts +0 -31
  128. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  129. package/src/router/match-middleware/segment-resolution.ts +0 -21
  130. package/src/router/match-pipelines.ts +1 -42
  131. package/src/router/match-result.ts +1 -52
  132. package/src/router/metrics.ts +0 -34
  133. package/src/router/middleware-cookies.ts +0 -13
  134. package/src/router/middleware-types.ts +0 -115
  135. package/src/router/middleware.ts +7 -30
  136. package/src/router/navigation-snapshot.ts +0 -51
  137. package/src/router/params-util.ts +23 -0
  138. package/src/router/pattern-matching.ts +1 -33
  139. package/src/router/prerender-match.ts +33 -45
  140. package/src/router/request-classification.ts +1 -38
  141. package/src/router/revalidation.ts +5 -58
  142. package/src/router/router-context.ts +0 -26
  143. package/src/router/router-interfaces.ts +7 -0
  144. package/src/router/router-options.ts +30 -0
  145. package/src/router/segment-resolution/fresh.ts +25 -57
  146. package/src/router/segment-resolution/helpers.ts +34 -0
  147. package/src/router/segment-resolution/loader-cache.ts +10 -13
  148. package/src/router/segment-resolution/revalidation.ts +5 -42
  149. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  150. package/src/router/segment-resolution.ts +4 -1
  151. package/src/router/state-cookie-name.ts +33 -0
  152. package/src/router/telemetry-otel.ts +0 -20
  153. package/src/router/telemetry.ts +96 -19
  154. package/src/router/timeout.ts +0 -20
  155. package/src/router/trie-matching.ts +63 -40
  156. package/src/router/types.ts +1 -63
  157. package/src/router/url-params.ts +0 -5
  158. package/src/router.ts +40 -9
  159. package/src/rsc/handler.ts +14 -2
  160. package/src/rsc/helpers.ts +34 -0
  161. package/src/rsc/origin-guard.ts +0 -12
  162. package/src/rsc/progressive-enhancement.ts +4 -1
  163. package/src/rsc/rsc-rendering.ts +4 -7
  164. package/src/rsc/runtime-warnings.ts +14 -0
  165. package/src/rsc/server-action.ts +30 -28
  166. package/src/rsc/types.ts +2 -1
  167. package/src/runtime-env.ts +18 -0
  168. package/src/search-params.ts +0 -16
  169. package/src/segment-loader-promise.ts +14 -2
  170. package/src/segment-system.tsx +79 -88
  171. package/src/server/cookie-store.ts +52 -1
  172. package/src/server/handle-store.ts +7 -24
  173. package/src/server/loader-registry.ts +5 -24
  174. package/src/server/request-context.ts +74 -77
  175. package/src/ssr/index.tsx +14 -14
  176. package/src/static-handler.ts +10 -13
  177. package/src/testing/cache-status.ts +119 -0
  178. package/src/testing/collect-handle.ts +40 -0
  179. package/src/testing/dispatch.ts +581 -0
  180. package/src/testing/dom.entry.ts +22 -0
  181. package/src/testing/e2e/fixture.ts +188 -0
  182. package/src/testing/e2e/index.ts +127 -0
  183. package/src/testing/e2e/matchers.ts +35 -0
  184. package/src/testing/e2e/page-helpers.ts +272 -0
  185. package/src/testing/e2e/parity.ts +387 -0
  186. package/src/testing/e2e/server.ts +195 -0
  187. package/src/testing/flight-matchers.ts +97 -0
  188. package/src/testing/flight-normalize.ts +11 -0
  189. package/src/testing/flight-runtime.d.ts +57 -0
  190. package/src/testing/flight-tree.ts +682 -0
  191. package/src/testing/flight.entry.ts +52 -0
  192. package/src/testing/flight.ts +186 -0
  193. package/src/testing/generated-routes.ts +183 -0
  194. package/src/testing/index.ts +98 -0
  195. package/src/testing/internal/context.ts +348 -0
  196. package/src/testing/internal/flight-client-globals.ts +30 -0
  197. package/src/testing/internal/seed-vars.ts +54 -0
  198. package/src/testing/render-handler.ts +311 -0
  199. package/src/testing/render-route.tsx +504 -0
  200. package/src/testing/run-loader.ts +378 -0
  201. package/src/testing/run-middleware.ts +205 -0
  202. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  203. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  204. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  205. package/src/testing/vitest-stubs/version.ts +5 -0
  206. package/src/testing/vitest.ts +305 -0
  207. package/src/theme/ThemeProvider.tsx +0 -52
  208. package/src/theme/ThemeScript.tsx +0 -6
  209. package/src/theme/constants.ts +0 -12
  210. package/src/theme/index.ts +0 -7
  211. package/src/theme/theme-context.ts +1 -5
  212. package/src/theme/theme-script.ts +0 -14
  213. package/src/theme/use-theme.ts +0 -3
  214. package/src/types/boundaries.ts +0 -35
  215. package/src/types/error-types.ts +25 -89
  216. package/src/types/global-namespace.ts +15 -15
  217. package/src/types/handler-context.ts +16 -13
  218. package/src/types/index.ts +0 -10
  219. package/src/types/request-scope.ts +0 -19
  220. package/src/types/route-config.ts +6 -50
  221. package/src/types/route-entry.ts +0 -6
  222. package/src/types/segments.ts +0 -13
  223. package/src/urls/include-helper.ts +0 -4
  224. package/src/urls/index.ts +0 -6
  225. package/src/urls/path-helper-types.ts +2 -2
  226. package/src/urls/path-helper.ts +0 -54
  227. package/src/urls/urls-function.ts +0 -13
  228. package/src/use-loader.tsx +0 -186
  229. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  230. package/src/vite/discovery/discover-routers.ts +6 -7
  231. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  232. package/src/vite/plugin-types.ts +3 -1
  233. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  234. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  235. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  236. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  237. package/src/vite/plugins/expose-action-id.ts +2 -73
  238. package/src/vite/plugins/expose-id-utils.ts +0 -55
  239. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  240. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  241. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  242. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  243. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  244. package/src/vite/plugins/performance-tracks.ts +0 -3
  245. package/src/vite/plugins/use-cache-transform.ts +0 -36
  246. package/src/vite/plugins/version-injector.ts +0 -20
  247. package/src/vite/plugins/version-plugin.ts +1 -49
  248. package/src/vite/plugins/virtual-entries.ts +0 -15
  249. package/src/vite/rango.ts +1 -108
  250. package/src/vite/router-discovery.ts +2 -1
  251. package/src/vite/utils/ast-handler-extract.ts +0 -16
  252. package/src/vite/utils/bundle-analysis.ts +6 -13
  253. package/src/vite/utils/client-chunks.ts +0 -6
  254. package/src/vite/utils/forward-user-plugins.ts +0 -22
  255. package/src/vite/utils/manifest-utils.ts +0 -4
  256. package/src/vite/utils/package-resolution.ts +1 -73
  257. package/src/vite/utils/prerender-utils.ts +0 -35
  258. package/src/vite/utils/shared-utils.ts +3 -35
  259. package/src/browser/react/use-client-cache.ts +0 -58
  260. package/src/browser/shallow.ts +0 -40
@@ -14,10 +14,6 @@
14
14
  * - revalidation.decision (revalidation evaluation)
15
15
  */
16
16
 
17
- // ---------------------------------------------------------------------------
18
- // Event types
19
- // ---------------------------------------------------------------------------
20
-
21
17
  interface BaseEvent {
22
18
  /** Monotonic timestamp from performance.now() */
23
19
  timestamp: number;
@@ -90,6 +86,34 @@ export interface HandlerErrorEvent extends BaseEvent {
90
86
  params?: Record<string, string>;
91
87
  }
92
88
 
89
+ /**
90
+ * Per-segment (or coarse route-level) cache status carried on the
91
+ * cache.decision telemetry event and the X-Rango-Cache debug header.
92
+ *
93
+ * v1 is COARSE: the router's pipeline tracks cache decisions at the
94
+ * route/entry level (cacheHit/cacheSource/shouldRevalidate), not per
95
+ * individual segment. The `segments` array therefore contains a single
96
+ * route-level entry keyed by the route key. The shape is forward-compatible
97
+ * with genuine per-segment status if the pipeline later exposes it.
98
+ */
99
+ export type CacheSegmentStatus =
100
+ | "hit"
101
+ | "miss"
102
+ | "stale"
103
+ | "prerendered"
104
+ | "passthrough";
105
+
106
+ export interface CacheSegmentSignal {
107
+ /** Segment id (v1: the route key, since status is route-level). */
108
+ id: string;
109
+ /** Segment type (v1: "route" for the coarse route-level entry). */
110
+ type: string;
111
+ /** Resolved cache status for this segment. */
112
+ cacheStatus: CacheSegmentStatus;
113
+ /** Whether stale-while-revalidate was triggered for this segment. */
114
+ shouldRevalidate?: boolean;
115
+ }
116
+
93
117
  export interface CacheDecisionEvent extends BaseEvent {
94
118
  type: "cache.decision";
95
119
  pathname: string;
@@ -98,6 +122,12 @@ export interface CacheDecisionEvent extends BaseEvent {
98
122
  /** Whether stale-while-revalidate was triggered */
99
123
  shouldRevalidate: boolean;
100
124
  source?: "runtime" | "prerender";
125
+ /**
126
+ * Optional per-segment (v1: coarse route-level) cache status. Present only
127
+ * when telemetry or the debug cache signal is enabled. Optional so existing
128
+ * sinks are unaffected.
129
+ */
130
+ segments?: CacheSegmentSignal[];
101
131
  }
102
132
 
103
133
  export interface RevalidationDecisionEvent extends BaseEvent {
@@ -141,9 +171,70 @@ export type TelemetryEvent =
141
171
  | OriginCheckRejectedEvent;
142
172
 
143
173
  // ---------------------------------------------------------------------------
144
- // Sink interface
174
+ // Cache signal derivation (coarse, route-level)
145
175
  // ---------------------------------------------------------------------------
146
176
 
177
+ /**
178
+ * Derive the coarse, route-level cache status from pipeline cache state.
179
+ *
180
+ * v1 mapping (route-level — see CacheSegmentSignal):
181
+ * - prerender hit -> "prerendered"
182
+ * - runtime hit + shouldRevalidate (SWR) -> "stale"
183
+ * - runtime hit -> "hit"
184
+ * - no hit -> "miss"
185
+ *
186
+ * Note: "passthrough" is a build-time prerender concept (a route opts out of
187
+ * being prerendered for some params). At runtime a passthrough route renders
188
+ * fresh and is indistinguishable from a normal miss in the pipeline state, so
189
+ * v1 reports it as "miss". The "passthrough" status remains in the type union
190
+ * for forward compatibility.
191
+ */
192
+ export function deriveCacheStatus(state: {
193
+ cacheHit: boolean;
194
+ cacheSource?: "runtime" | "prerender";
195
+ shouldRevalidate?: boolean;
196
+ }): CacheSegmentStatus {
197
+ if (state.cacheHit) {
198
+ if (state.cacheSource === "prerender") return "prerendered";
199
+ if (state.shouldRevalidate) return "stale";
200
+ return "hit";
201
+ }
202
+ return "miss";
203
+ }
204
+
205
+ /**
206
+ * Build the coarse route-level cache signal array (a single entry keyed by
207
+ * the route key). Used for both the cache.decision telemetry event and the
208
+ * X-Rango-Cache debug header.
209
+ */
210
+ export function buildCacheSignalSegments(
211
+ routeKey: string,
212
+ state: {
213
+ cacheHit: boolean;
214
+ cacheSource?: "runtime" | "prerender";
215
+ shouldRevalidate?: boolean;
216
+ },
217
+ ): CacheSegmentSignal[] {
218
+ return [
219
+ {
220
+ id: routeKey,
221
+ type: "route",
222
+ cacheStatus: deriveCacheStatus(state),
223
+ shouldRevalidate: !!state.shouldRevalidate,
224
+ },
225
+ ];
226
+ }
227
+
228
+ /**
229
+ * Serialize cache signal segments into the X-Rango-Cache header value:
230
+ * `<segId>=<status>, <segId2>=<status2>`.
231
+ */
232
+ export function formatCacheSignalHeader(
233
+ segments: CacheSegmentSignal[],
234
+ ): string {
235
+ return segments.map((s) => `${s.id}=${s.cacheStatus}`).join(", ");
236
+ }
237
+
147
238
  /**
148
239
  * Telemetry sink receives structured lifecycle events from the router.
149
240
  * Implement this interface to integrate with any observability backend.
@@ -154,10 +245,6 @@ export interface TelemetrySink {
154
245
  emit(event: TelemetryEvent): void;
155
246
  }
156
247
 
157
- // ---------------------------------------------------------------------------
158
- // No-op singleton (zero-cost disabled state)
159
- // ---------------------------------------------------------------------------
160
-
161
248
  const noopSink: TelemetrySink = {
162
249
  emit() {},
163
250
  };
@@ -185,12 +272,6 @@ export function safeEmit(sink: TelemetrySink, event: TelemetryEvent): void {
185
272
  }
186
273
  }
187
274
 
188
- // ---------------------------------------------------------------------------
189
- // Request ID extraction (for span correlation)
190
- // ---------------------------------------------------------------------------
191
-
192
- // Per-request memoization so the same Request object always maps to the
193
- // same ID. WeakMap allows GC when the Request is no longer referenced.
194
275
  const requestIds = new WeakMap<Request, string>();
195
276
  let telemetryRequestCounter = 0;
196
277
 
@@ -224,10 +305,6 @@ export function getRequestId(request: Request): string {
224
305
  return id;
225
306
  }
226
307
 
227
- // ---------------------------------------------------------------------------
228
- // Console sink (built-in, replaces ad-hoc console.log debug traces)
229
- // ---------------------------------------------------------------------------
230
-
231
308
  /**
232
309
  * Built-in console sink that logs events in a structured format.
233
310
  * Designed as the default sink for development / debugging.
@@ -6,10 +6,6 @@
6
6
  * a Promise.race mechanism, returning 504 on expiry.
7
7
  */
8
8
 
9
- // ---------------------------------------------------------------------------
10
- // Public types
11
- // ---------------------------------------------------------------------------
12
-
13
9
  export interface RouterTimeouts {
14
10
  /** Timeout for server action execution (ms). */
15
11
  actionMs?: number;
@@ -35,10 +31,6 @@ export type OnTimeoutCallback<TEnv = any> = (
35
31
  ctx: TimeoutContext<TEnv>,
36
32
  ) => Response | Promise<Response>;
37
33
 
38
- // ---------------------------------------------------------------------------
39
- // Internal resolved form
40
- // ---------------------------------------------------------------------------
41
-
42
34
  export interface ResolvedTimeouts {
43
35
  actionMs: number | undefined;
44
36
  renderStartMs: number | undefined;
@@ -63,10 +55,6 @@ export function resolveTimeouts(
63
55
  };
64
56
  }
65
57
 
66
- // ---------------------------------------------------------------------------
67
- // Error class
68
- // ---------------------------------------------------------------------------
69
-
70
58
  export class RouterTimeoutError extends Error {
71
59
  override name = "RouterTimeoutError" as const;
72
60
  phase: TimeoutPhase;
@@ -81,10 +69,6 @@ export class RouterTimeoutError extends Error {
81
69
  }
82
70
  }
83
71
 
84
- // ---------------------------------------------------------------------------
85
- // Race helper
86
- // ---------------------------------------------------------------------------
87
-
88
72
  type TimeoutResult<T> =
89
73
  | { result: T; timedOut: false }
90
74
  | { timedOut: true; durationMs: number };
@@ -129,10 +113,6 @@ export async function withTimeout<T>(
129
113
  }
130
114
  }
131
115
 
132
- // ---------------------------------------------------------------------------
133
- // Default response
134
- // ---------------------------------------------------------------------------
135
-
136
116
  /**
137
117
  * Create the default 504 response for a timed-out request.
138
118
  * Includes `X-Rango-Timeout-Phase` header for observability.
@@ -43,14 +43,12 @@ export function tryTrieMatch(
43
43
  ): TrieMatchResult | null {
44
44
  if (!trie) return null;
45
45
 
46
- // Split pathname into segments, filtering empty strings from leading/trailing slashes
47
46
  const pathnameHasTrailingSlash =
48
47
  pathname.length > 1 && pathname.endsWith("/");
49
48
  const normalizedPath = pathnameHasTrailingSlash
50
49
  ? pathname.slice(0, -1)
51
50
  : pathname;
52
51
 
53
- // Handle root path
54
52
  if (normalizedPath === "" || normalizedPath === "/") {
55
53
  if (trie.r) {
56
54
  return validateAndBuild(
@@ -77,10 +75,8 @@ export function tryTrieMatch(
77
75
  return null;
78
76
  }
79
77
 
80
- // Remove leading slash and split
81
78
  const segments = normalizedPath.slice(1).split("/");
82
79
 
83
- // Try exact match with normalized path (no trailing slash)
84
80
  const result = walkTrie(trie, segments, 0, []);
85
81
  if (result) {
86
82
  return validateAndBuild(
@@ -102,8 +98,58 @@ interface WalkResult {
102
98
  }
103
99
 
104
100
  /**
105
- * Walk the trie by segments with priority: static > param > wildcard.
106
- * Uses backtracking to try all possible matches.
101
+ * Check a leaf's constraints (leaf.cv) against already-resolved named params.
102
+ * Empty/undefined values are exempt (optional params that were not bound).
103
+ */
104
+ function constraintsSatisfied(
105
+ leaf: TrieLeaf,
106
+ params: Record<string, string>,
107
+ ): boolean {
108
+ if (!leaf.cv) return true;
109
+ for (const paramName in leaf.cv) {
110
+ const allowed = leaf.cv[paramName]!;
111
+ const value = params[paramName];
112
+ if (value !== undefined && value !== "" && !allowed.includes(value)) {
113
+ return false;
114
+ }
115
+ }
116
+ return true;
117
+ }
118
+
119
+ /**
120
+ * Constraint check for a candidate terminal DURING the walk. Builds the named
121
+ * params from positional walk values (decoded the same way validateAndBuild
122
+ * does) and validates leaf.cv. Returning false lets walkTrie unwind to a
123
+ * lower-priority sibling instead of committing to a leaf that would only be
124
+ * rejected post-walk — that post-walk rejection is what forced the regex
125
+ * fallback (and its false "trie gap" R3 warning) for perfectly valid configs.
126
+ */
127
+ function leafConstraintsPass(
128
+ leaf: TrieLeaf,
129
+ paramValues: string[],
130
+ wildcardValue: string | undefined,
131
+ ): boolean {
132
+ if (!leaf.cv) return true;
133
+ const params: Record<string, string> = {};
134
+ if (leaf.pa) {
135
+ for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
136
+ params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
137
+ }
138
+ }
139
+ if (wildcardValue !== undefined && "pn" in leaf) {
140
+ params[(leaf as TrieLeaf & { pn: string }).pn] =
141
+ safeDecodeURIComponent(wildcardValue);
142
+ }
143
+ return constraintsSatisfied(leaf, params);
144
+ }
145
+
146
+ /**
147
+ * Walk the trie by segments with priority: static > suffix-param > param >
148
+ * wildcard (Priority 1-4 below; matches the canonical M4 ordering in
149
+ * docs/internal/matching-and-lazy-discovery.md).
150
+ * Uses backtracking to try all possible matches. Per-leaf constraints are
151
+ * enforced at each candidate terminal so a constraint miss backtracks to a
152
+ * lower-priority sibling rather than aborting the whole match.
107
153
  */
108
154
  function walkTrie(
109
155
  node: TrieNode,
@@ -111,9 +157,8 @@ function walkTrie(
111
157
  index: number,
112
158
  paramValues: string[],
113
159
  ): WalkResult | null {
114
- // All segments consumed: check for terminal
115
160
  if (index === segments.length) {
116
- if (node.r) {
161
+ if (node.r && leafConstraintsPass(node.r, paramValues, undefined)) {
117
162
  return { leaf: node.r, paramValues: [...paramValues] };
118
163
  }
119
164
  // A wildcard at this node matches the bare prefix with an empty remainder
@@ -122,7 +167,7 @@ function walkTrie(
122
167
  // so without this a request to the wildcard's own prefix misses the trie
123
168
  // and the regex fallback emits a corrupt redirect. A static terminal
124
169
  // (node.r) still wins.
125
- if (node.w) {
170
+ if (node.w && leafConstraintsPass(node.w, paramValues, "")) {
126
171
  return { leaf: node.w, paramValues: [...paramValues], wildcardValue: "" };
127
172
  }
128
173
  return null;
@@ -131,13 +176,11 @@ function walkTrie(
131
176
  const segment = segments[index];
132
177
  const staticChild = node.s?.[segment];
133
178
 
134
- // Priority 1: Static match
135
179
  if (staticChild) {
136
180
  const result = walkTrie(staticChild, segments, index + 1, paramValues);
137
181
  if (result) return result;
138
182
  }
139
183
 
140
- // Priority 2: Suffix-param match (e.g., :productId.html)
141
184
  if (node.xp) {
142
185
  for (const suffix in node.xp) {
143
186
  if (segment.endsWith(suffix) && segment.length > suffix.length) {
@@ -155,7 +198,6 @@ function walkTrie(
155
198
  }
156
199
  }
157
200
 
158
- // Priority 3: Param match
159
201
  if (node.p) {
160
202
  paramValues.push(segment);
161
203
  const result = walkTrie(node.p.c, segments, index + 1, paramValues);
@@ -163,14 +205,15 @@ function walkTrie(
163
205
  if (result) return result;
164
206
  }
165
207
 
166
- // Priority 4: Wildcard match (consumes rest)
167
208
  if (node.w) {
168
209
  const rest = joinRemainingSegments(segments, index);
169
- return {
170
- leaf: node.w,
171
- paramValues: [...paramValues],
172
- wildcardValue: rest,
173
- };
210
+ if (leafConstraintsPass(node.w, paramValues, rest)) {
211
+ return {
212
+ leaf: node.w,
213
+ paramValues: [...paramValues],
214
+ wildcardValue: rest,
215
+ };
216
+ }
174
217
  }
175
218
 
176
219
  return null;
@@ -196,10 +239,6 @@ function validateAndBuild(
196
239
  originalPathname: string,
197
240
  pathnameHasTrailingSlash: boolean,
198
241
  ): TrieMatchResult | null {
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.
203
242
  const params: Record<string, string> = {};
204
243
  if (leaf.pa) {
205
244
  for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
@@ -207,31 +246,15 @@ function validateAndBuild(
207
246
  }
208
247
  }
209
248
 
210
- // Add wildcard param (wildcard leaves have pn from TrieNode.w type)
211
249
  if (wildcardValue !== undefined && "pn" in leaf) {
212
250
  params[(leaf as TrieLeaf & { pn: string }).pn] =
213
251
  safeDecodeURIComponent(wildcardValue);
214
252
  }
215
253
 
216
- // Validate constraints against decoded values so constraint lists can be
217
- // written in decoded form (e.g. ["en-GB", "en US"]).
218
- if (leaf.cv) {
219
- for (const paramName in leaf.cv) {
220
- const allowed = leaf.cv[paramName]!;
221
- const value = params[paramName];
222
- if (value !== undefined && value !== "" && !allowed.includes(value)) {
223
- return null;
224
- }
225
- }
254
+ if (!constraintsSatisfied(leaf, params)) {
255
+ return null;
226
256
  }
227
257
 
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.
233
-
234
- // Trailing slash handling
235
258
  const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
236
259
  let redirectTo: string | undefined;
237
260
 
@@ -22,27 +22,11 @@ import type {
22
22
  ShouldRevalidateFn,
23
23
  } from "../types";
24
24
 
25
- /**
26
- * Result of resolving loaders with revalidation
27
- * Contains both segments to render and all matched segment IDs
28
- */
29
- export interface LoaderRevalidationResult {
30
- segments: ResolvedSegment[];
31
- matchedIds: string[];
32
- }
33
-
34
- /**
35
- * Result of resolving segments with revalidation
36
- * Contains both segments to render and all matched segment IDs
37
- */
38
25
  export interface SegmentRevalidationResult {
39
26
  segments: ResolvedSegment[];
40
27
  matchedIds: string[];
41
28
  }
42
29
 
43
- /**
44
- * Action context type for revalidation
45
- */
46
30
  export type ActionContext = {
47
31
  actionId?: string;
48
32
  actionUrl?: URL;
@@ -50,23 +34,6 @@ export type ActionContext = {
50
34
  formData?: FormData;
51
35
  };
52
36
 
53
- /**
54
- * Dependencies passed to segment resolution functions
55
- * These are created within createRouter and passed to extracted utilities
56
- */
57
- export interface RouterDependencies<TEnv> {
58
- findNearestErrorBoundary: (
59
- entry: EntryData | null,
60
- ) => ReactNode | ErrorBoundaryHandler | null;
61
- findNearestNotFoundBoundary: (
62
- entry: EntryData | null,
63
- ) => ReactNode | NotFoundBoundaryHandler | null;
64
- }
65
-
66
- /**
67
- * Dependencies injected from createRouter closure into extracted segment resolution functions.
68
- * These are the closure-bound helpers that cannot be imported directly.
69
- */
70
37
  export interface SegmentResolutionDeps<TEnv = any> {
71
38
  wrapLoaderPromise: <T>(
72
39
  promise: Promise<T>,
@@ -108,21 +75,6 @@ export interface SegmentResolutionDeps<TEnv = any> {
108
75
  viewTransitionDefault?: "auto" | false;
109
76
  }
110
77
 
111
- /**
112
- * Dependencies injected from createRouter closure into extracted intercept resolution functions.
113
- */
114
- export interface InterceptResolutionDeps<TEnv = any> {
115
- wrapLoaderPromise: SegmentResolutionDeps<TEnv>["wrapLoaderPromise"];
116
- evaluateInterceptWhen: (
117
- intercept: InterceptEntry,
118
- selectorContext: InterceptSelectorContext | null,
119
- isAction: boolean,
120
- ) => boolean;
121
- }
122
-
123
- /**
124
- * Dependencies injected from createRouter closure into extracted match API functions.
125
- */
126
78
  export interface MatchApiDeps<TEnv = any> {
127
79
  findMatch: (pathname: string, ms?: any) => any;
128
80
  getMetricsStore: () => any;
@@ -137,23 +89,13 @@ export interface MatchApiDeps<TEnv = any> {
137
89
  getRouteMap: () => Record<string, string>;
138
90
  }
139
91
 
140
- /**
141
- * Title descriptor types for template support
142
- */
143
92
  export type TitleDescriptor =
144
93
  | string
145
94
  | { template: string; default: string } // For layouts - template applied to child titles
146
- | { absolute: string }; // Bypass parent template
95
+ | { absolute: string };
147
96
 
148
- /**
149
- * Unset descriptor to remove inherited meta
150
- * Key format matches getMetaKey output: "title", "name:description", "property:og:image"
151
- */
152
97
  export type UnsetDescriptor = { unset: string };
153
98
 
154
- /**
155
- * Base meta descriptor types (sync values)
156
- */
157
99
  export type MetaDescriptorBase =
158
100
  | { charSet: "utf-8" }
159
101
  | { title: TitleDescriptor }
@@ -165,10 +107,6 @@ export type MetaDescriptorBase =
165
107
  | UnsetDescriptor
166
108
  | { [name: string]: unknown };
167
109
 
168
- /**
169
- * Meta descriptor that can be sync or async.
170
- * Use Promise<MetaDescriptorBase> for streaming meta that resolves after initial render.
171
- */
172
110
  export type MetaDescriptor = MetaDescriptorBase | Promise<MetaDescriptorBase>;
173
111
 
174
112
  type LdJsonObject = { [Key in string]: LdJsonValue } & {
@@ -25,11 +25,6 @@ export function safeDecodeURIComponent(raw: string): string {
25
25
  }
26
26
  }
27
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
28
  const PATH_SAFE_ESCAPES: Record<string, string> = {
34
29
  "%3A": ":",
35
30
  "%40": "@",
package/src/router.ts CHANGED
@@ -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,
@@ -110,6 +111,7 @@ import {
110
111
  matchForPrerender as _matchForPrerender,
111
112
  renderStaticSegment as _renderStaticSegment,
112
113
  } from "./router/prerender-match.js";
114
+ import { resolveStateCookieName } from "./router/state-cookie-name.js";
113
115
 
114
116
  // Re-export public types and values from extracted modules
115
117
  export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
@@ -149,6 +151,7 @@ export function createRouter<TEnv = any>(
149
151
  nonce,
150
152
  version,
151
153
  prefetchCacheTTL: prefetchCacheTTLOption,
154
+ stateCookiePrefix: stateCookiePrefixOption,
152
155
  warmup: warmupOption,
153
156
  allowDebugManifest: allowDebugManifestOption = false,
154
157
  telemetry: telemetrySink,
@@ -158,14 +161,22 @@ export function createRouter<TEnv = any>(
158
161
  onTimeout,
159
162
  originCheck: originCheckOption,
160
163
  viewTransition: viewTransitionOption = "auto",
164
+ debugCacheSignal: debugCacheSignalOption = false,
161
165
  } = options;
162
166
 
167
+ // Debug cache signal gate (DEVELOPMENT/TEST ONLY). Enabled by the
168
+ // debugCacheSignal option OR the RANGO_TEST_SIGNALS=1 env flag. When off,
169
+ // no X-Rango-Cache header is emitted and output is byte-identical.
170
+ const cacheSignalEnabled =
171
+ debugCacheSignalOption ||
172
+ (typeof process !== "undefined" &&
173
+ (process as { env?: Record<string, string | undefined> }).env
174
+ ?.RANGO_TEST_SIGNALS === "1");
175
+
163
176
  // Normalize basename: ensure leading slash, strip trailing slash.
164
- // A bare "/" is equivalent to no basename.
165
- const basename =
166
- basenameOption && basenameOption.replace(/^\/+|\/+$/g, "")
167
- ? "/" + basenameOption.replace(/^\/+|\/+$/g, "")
168
- : undefined;
177
+ // A bare "/" is equivalent to no basename. Shared with the testing
178
+ // primitives via normalizeBasename so they can never drift.
179
+ const basename = normalizeBasename(basenameOption);
169
180
 
170
181
  // Resolve telemetry sink (no-op when not configured)
171
182
  const telemetry = resolveSink(telemetrySink);
@@ -209,6 +220,14 @@ export function createRouter<TEnv = any>(
209
220
  const routerId =
210
221
  userProvidedId ?? injectedId ?? `router_${nextRouterAutoId()}`;
211
222
 
223
+ // Resolve the rango state cookie name once, here, so the two cookie writers
224
+ // (the client document.cookie writer and the server Set-Cookie writer)
225
+ // consume one pre-composed name and cannot drift.
226
+ const resolvedStateCookieName = resolveStateCookieName(
227
+ stateCookiePrefixOption,
228
+ routerId,
229
+ );
230
+
212
231
  // Resolve prefetch cache TTL (default: 300 seconds / 5 minutes)
213
232
  // Clamp to a non-negative integer for valid Cache-Control max-age.
214
233
  const rawTTL =
@@ -255,9 +274,14 @@ export function createRouter<TEnv = any>(
255
274
  invokeOnError(onError, error, phase, context, "Router");
256
275
  }
257
276
 
258
- // Validate document is a client component
277
+ // Validate document is a client component. Under a test runner the "use
278
+ // client" transform has not run, so a real exported document has no marker;
279
+ // allowServerInTest lets the router construct in a bare unit test (for
280
+ // dispatch / assertGeneratedRoutesMatch) while a real build still throws.
259
281
  if (documentOption !== undefined) {
260
- assertClientComponent(documentOption, "document");
282
+ assertClientComponent(documentOption, "document", {
283
+ allowServerInTest: true,
284
+ });
261
285
  }
262
286
 
263
287
  // Use default document if none provided (keeps internal name as rootLayout)
@@ -667,6 +691,7 @@ export function createRouter<TEnv = any>(
667
691
  findMatch,
668
692
  findInterceptForRoute,
669
693
  telemetry: telemetrySink,
694
+ cacheSignalEnabled,
670
695
  });
671
696
 
672
697
  const { match, matchPartial, matchError, previewMatch } = matchHandlers;
@@ -938,6 +963,10 @@ export function createRouter<TEnv = any>(
938
963
  prefetchCacheControl,
939
964
  prefetchCacheTTL,
940
965
 
966
+ // Expose the resolved rango state cookie name for the server-side writer
967
+ // (invalidateClientCache) and for shipping to the client in metadata.
968
+ resolvedStateCookieName,
969
+
941
970
  // Expose warmup enabled flag for handler and client
942
971
  warmupEnabled,
943
972
 
@@ -1029,8 +1058,10 @@ export function createRouter<TEnv = any>(
1029
1058
  if (!handler) {
1030
1059
  // Lazy import deferred to first request to avoid dev mode issues
1031
1060
  const { createRSCHandler } = await import("./rsc/handler.js");
1032
- // Cast: handler.ts still accepts (request, env) will be updated
1033
- // separately to accept RouterRequestInput.
1061
+ // Cast: createRSCHandler receives `router as any`, which erases TEnv
1062
+ // and infers its handler as RouterRequestInput<unknown>. Re-narrow the
1063
+ // returned handler to RouterRequestInput<TEnv> so the call below stays
1064
+ // typed. (The handler already accepts (request, RouterRequestInput).)
1034
1065
  handler = createRSCHandler({
1035
1066
  router: router as any,
1036
1067
  cache,