@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126

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 (235) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/rango.js +3 -4
  3. package/dist/vite/index.js +315 -68
  4. package/package.json +19 -18
  5. package/skills/breadcrumbs/SKILL.md +60 -0
  6. package/skills/hooks/SKILL.md +2 -2
  7. package/skills/route/SKILL.md +6 -0
  8. package/skills/server-actions/SKILL.md +25 -1
  9. package/skills/testing/SKILL.md +17 -17
  10. package/skills/testing/cache-prerender.md +29 -3
  11. package/skills/testing/flight.md +13 -10
  12. package/skills/testing/render-handler.md +3 -0
  13. package/skills/testing/server-tree.md +1 -1
  14. package/skills/testing/setup.md +1 -1
  15. package/src/__internal.ts +0 -65
  16. package/src/browser/action-coordinator.ts +1 -1
  17. package/src/browser/action-fence.ts +10 -0
  18. package/src/browser/event-controller.ts +1 -83
  19. package/src/browser/navigation-store-handle.ts +3 -4
  20. package/src/browser/navigation-store.ts +0 -39
  21. package/src/browser/navigation-transaction.ts +0 -32
  22. package/src/browser/partial-update.ts +23 -84
  23. package/src/browser/prefetch/cache.ts +6 -45
  24. package/src/browser/prefetch/queue.ts +6 -3
  25. package/src/browser/rango-state.ts +2 -23
  26. package/src/browser/react/Link.tsx +0 -2
  27. package/src/browser/react/NavigationProvider.tsx +2 -1
  28. package/src/browser/react/ScrollRestoration.tsx +10 -6
  29. package/src/browser/react/filter-segment-order.ts +0 -2
  30. package/src/browser/react/index.ts +0 -45
  31. package/src/browser/react/location-state-shared.ts +0 -13
  32. package/src/browser/react/location-state.ts +0 -1
  33. package/src/browser/react/use-action.ts +6 -15
  34. package/src/browser/react/use-handle.ts +0 -5
  35. package/src/browser/react/use-link-status.ts +0 -4
  36. package/src/browser/react/use-navigation.ts +0 -3
  37. package/src/browser/react/use-params.ts +0 -2
  38. package/src/browser/react/use-router.ts +2 -1
  39. package/src/browser/react/use-search-params.ts +0 -5
  40. package/src/browser/react/use-segments.ts +0 -13
  41. package/src/browser/rsc-router.tsx +10 -3
  42. package/src/browser/server-action-bridge.ts +51 -3
  43. package/src/browser/types.ts +23 -5
  44. package/src/browser/validate-redirect-origin.ts +43 -16
  45. package/src/build/index.ts +8 -9
  46. package/src/build/route-trie.ts +46 -11
  47. package/src/build/route-types/param-extraction.ts +6 -3
  48. package/src/build/route-types/router-processing.ts +0 -8
  49. package/src/cache/cache-policy.ts +0 -54
  50. package/src/cache/cache-runtime.ts +48 -24
  51. package/src/cache/cache-scope.ts +0 -27
  52. package/src/cache/cache-tag.ts +0 -37
  53. package/src/cache/cf/cf-cache-store.ts +72 -45
  54. package/src/cache/cf/index.ts +0 -24
  55. package/src/cache/document-cache.ts +10 -36
  56. package/src/cache/handle-snapshot.ts +0 -40
  57. package/src/cache/index.ts +0 -27
  58. package/src/cache/memory-segment-store.ts +0 -52
  59. package/src/cache/profile-registry.ts +6 -30
  60. package/src/cache/read-through-swr.ts +41 -11
  61. package/src/cache/segment-codec.ts +0 -16
  62. package/src/cache/types.ts +0 -98
  63. package/src/client.rsc.tsx +4 -22
  64. package/src/client.tsx +19 -32
  65. package/src/context-var.ts +12 -0
  66. package/src/defer.ts +196 -0
  67. package/src/deps/ssr.ts +0 -1
  68. package/src/handle.ts +2 -12
  69. package/src/handles/MetaTags.tsx +0 -14
  70. package/src/handles/breadcrumbs.ts +16 -5
  71. package/src/handles/meta.ts +0 -39
  72. package/src/host/cookie-handler.ts +0 -36
  73. package/src/host/errors.ts +0 -24
  74. package/src/host/index.ts +6 -0
  75. package/src/host/pattern-matcher.ts +7 -50
  76. package/src/host/router.ts +1 -65
  77. package/src/host/testing.ts +0 -16
  78. package/src/host/types.ts +6 -2
  79. package/src/href-client.ts +0 -4
  80. package/src/index.rsc.ts +27 -2
  81. package/src/index.ts +7 -0
  82. package/src/internal-debug.ts +2 -4
  83. package/src/loader.rsc.ts +4 -15
  84. package/src/loader.ts +3 -9
  85. package/src/network-error-thrower.tsx +1 -6
  86. package/src/outlet-provider.tsx +1 -5
  87. package/src/prerender/param-hash.ts +10 -11
  88. package/src/prerender/store.ts +23 -30
  89. package/src/prerender.ts +34 -0
  90. package/src/redirect-origin.ts +100 -0
  91. package/src/root-error-boundary.tsx +1 -19
  92. package/src/route-content-wrapper.tsx +1 -44
  93. package/src/route-definition/dsl-helpers.ts +7 -19
  94. package/src/route-definition/helpers-types.ts +3 -3
  95. package/src/route-definition/redirect.ts +43 -9
  96. package/src/route-definition/resolve-handler-use.ts +6 -0
  97. package/src/route-map-builder.ts +0 -16
  98. package/src/router/content-negotiation.ts +0 -13
  99. package/src/router/error-handling.ts +12 -16
  100. package/src/router/find-match.ts +4 -31
  101. package/src/router/intercept-resolution.ts +10 -1
  102. package/src/router/lazy-includes.ts +1 -57
  103. package/src/router/loader-resolution.ts +25 -23
  104. package/src/router/logging.ts +0 -6
  105. package/src/router/manifest.ts +1 -25
  106. package/src/router/match-api.ts +0 -20
  107. package/src/router/match-context.ts +0 -22
  108. package/src/router/match-handlers.ts +0 -43
  109. package/src/router/match-middleware/background-revalidation.ts +0 -7
  110. package/src/router/match-middleware/cache-lookup.ts +96 -179
  111. package/src/router/match-middleware/cache-store.ts +0 -31
  112. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  113. package/src/router/match-middleware/segment-resolution.ts +0 -22
  114. package/src/router/match-pipelines.ts +1 -42
  115. package/src/router/match-result.ts +1 -52
  116. package/src/router/metrics.ts +0 -34
  117. package/src/router/middleware-types.ts +0 -116
  118. package/src/router/middleware.ts +77 -60
  119. package/src/router/navigation-snapshot.ts +0 -51
  120. package/src/router/params-util.ts +23 -0
  121. package/src/router/pattern-matching.ts +5 -56
  122. package/src/router/prerender-match.ts +56 -51
  123. package/src/router/request-classification.ts +1 -38
  124. package/src/router/revalidation.ts +14 -62
  125. package/src/router/route-snapshot.ts +0 -1
  126. package/src/router/router-context.ts +0 -27
  127. package/src/router/router-interfaces.ts +10 -0
  128. package/src/router/segment-resolution/fresh.ts +25 -57
  129. package/src/router/segment-resolution/helpers.ts +34 -0
  130. package/src/router/segment-resolution/loader-cache.ts +35 -23
  131. package/src/router/segment-resolution/revalidation.ts +188 -283
  132. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  133. package/src/router/segment-resolution.ts +4 -1
  134. package/src/router/segment-wrappers.ts +0 -3
  135. package/src/router/telemetry-otel.ts +0 -20
  136. package/src/router/telemetry.ts +0 -22
  137. package/src/router/timeout.ts +0 -20
  138. package/src/router/trie-matching.ts +66 -45
  139. package/src/router/types.ts +1 -63
  140. package/src/router/url-params.ts +0 -5
  141. package/src/router.ts +8 -11
  142. package/src/rsc/handler-context.ts +1 -0
  143. package/src/rsc/handler.ts +20 -4
  144. package/src/rsc/helpers.ts +71 -3
  145. package/src/rsc/json-route-result.ts +38 -0
  146. package/src/rsc/origin-guard.ts +9 -15
  147. package/src/rsc/progressive-enhancement.ts +10 -1
  148. package/src/rsc/redirect-guard.ts +99 -0
  149. package/src/rsc/response-route-handler.ts +23 -18
  150. package/src/rsc/rsc-rendering.ts +2 -7
  151. package/src/rsc/runtime-warnings.ts +14 -0
  152. package/src/rsc/server-action.ts +34 -29
  153. package/src/rsc/types.ts +6 -3
  154. package/src/search-params.ts +0 -16
  155. package/src/segment-loader-promise.ts +14 -2
  156. package/src/segment-system.tsx +79 -88
  157. package/src/server/handle-store.ts +7 -24
  158. package/src/server/loader-registry.ts +5 -24
  159. package/src/server/request-context.ts +29 -92
  160. package/src/ssr/index.tsx +14 -14
  161. package/src/static-handler.ts +2 -27
  162. package/src/testing/cache-status.ts +44 -48
  163. package/src/testing/collect-handle.ts +1 -24
  164. package/src/testing/dispatch.ts +43 -6
  165. package/src/testing/e2e/index.ts +1 -22
  166. package/src/testing/e2e/matchers.ts +0 -16
  167. package/src/testing/flight-matchers.ts +0 -13
  168. package/src/testing/flight-normalize.ts +3 -30
  169. package/src/testing/flight.ts +46 -48
  170. package/src/testing/generated-routes.ts +1 -41
  171. package/src/testing/index.ts +1 -21
  172. package/src/testing/internal/context.ts +3 -45
  173. package/src/testing/internal/seed-vars.ts +0 -26
  174. package/src/testing/render-handler.ts +31 -61
  175. package/src/testing/render-route.tsx +75 -103
  176. package/src/testing/run-loader.ts +0 -96
  177. package/src/testing/run-middleware.ts +0 -26
  178. package/src/theme/ThemeProvider.tsx +0 -52
  179. package/src/theme/ThemeScript.tsx +0 -6
  180. package/src/theme/constants.ts +0 -12
  181. package/src/theme/index.ts +0 -7
  182. package/src/theme/theme-context.ts +1 -5
  183. package/src/theme/theme-script.ts +0 -14
  184. package/src/theme/use-theme.ts +0 -3
  185. package/src/types/boundaries.ts +0 -35
  186. package/src/types/error-types.ts +25 -89
  187. package/src/types/global-namespace.ts +4 -14
  188. package/src/types/handler-context.ts +28 -9
  189. package/src/types/index.ts +0 -10
  190. package/src/types/request-scope.ts +0 -19
  191. package/src/types/route-config.ts +6 -50
  192. package/src/types/route-entry.ts +0 -6
  193. package/src/types/segments.ts +0 -13
  194. package/src/urls/include-helper.ts +0 -4
  195. package/src/urls/index.ts +0 -6
  196. package/src/urls/path-helper-types.ts +2 -2
  197. package/src/urls/path-helper.ts +0 -54
  198. package/src/urls/urls-function.ts +0 -13
  199. package/src/use-loader.tsx +0 -186
  200. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  201. package/src/vite/discovery/discover-routers.ts +28 -18
  202. package/src/vite/discovery/prerender-collection.ts +2 -4
  203. package/src/vite/discovery/state.ts +5 -0
  204. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  205. package/src/vite/plugin-types.ts +35 -9
  206. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  207. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  208. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  209. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  210. package/src/vite/plugins/expose-action-id.ts +2 -73
  211. package/src/vite/plugins/expose-id-utils.ts +0 -55
  212. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  213. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  214. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  215. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  216. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  217. package/src/vite/plugins/performance-tracks.ts +0 -3
  218. package/src/vite/plugins/refresh-cmd.ts +1 -1
  219. package/src/vite/plugins/use-cache-transform.ts +21 -46
  220. package/src/vite/plugins/version-injector.ts +0 -20
  221. package/src/vite/plugins/version-plugin.ts +1 -49
  222. package/src/vite/plugins/virtual-entries.ts +0 -15
  223. package/src/vite/rango.ts +2 -108
  224. package/src/vite/router-discovery.ts +9 -1
  225. package/src/vite/utils/ast-handler-extract.ts +0 -16
  226. package/src/vite/utils/bundle-analysis.ts +6 -13
  227. package/src/vite/utils/client-chunks.ts +0 -6
  228. package/src/vite/utils/forward-user-plugins.ts +0 -22
  229. package/src/vite/utils/manifest-utils.ts +0 -4
  230. package/src/vite/utils/package-resolution.ts +1 -73
  231. package/src/vite/utils/prerender-utils.ts +0 -35
  232. package/src/vite/utils/shared-utils.ts +3 -35
  233. package/src/browser/shallow.ts +0 -40
  234. package/src/handles/index.ts +0 -7
  235. package/src/router/middleware-cookies.ts +0 -55
@@ -125,10 +125,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
125
125
  return {
126
126
  emit(event: TelemetryEvent): void {
127
127
  switch (event.type) {
128
- // -----------------------------------------------------------------
129
- // Request lifecycle
130
- // -----------------------------------------------------------------
131
-
132
128
  case "request.start": {
133
129
  const span = tracer.startSpan("rango.request", {
134
130
  attributes: {
@@ -169,10 +165,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
169
165
  break;
170
166
  }
171
167
 
172
- // -----------------------------------------------------------------
173
- // Loader lifecycle
174
- // -----------------------------------------------------------------
175
-
176
168
  case "loader.start": {
177
169
  const span = tracer.startSpan("rango.loader", {
178
170
  attributes: {
@@ -231,10 +223,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
231
223
  break;
232
224
  }
233
225
 
234
- // -----------------------------------------------------------------
235
- // Handler errors (instant span)
236
- // -----------------------------------------------------------------
237
-
238
226
  case "handler.error": {
239
227
  const attrs: Record<string, string | number | boolean> = {
240
228
  "rango.handled_by_boundary": event.handledByBoundary,
@@ -257,10 +245,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
257
245
  break;
258
246
  }
259
247
 
260
- // -----------------------------------------------------------------
261
- // Cache decision (instant span)
262
- // -----------------------------------------------------------------
263
-
264
248
  case "cache.decision": {
265
249
  const attrs: Record<string, string | number | boolean> = {
266
250
  "http.route": event.pathname,
@@ -277,10 +261,6 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
277
261
  break;
278
262
  }
279
263
 
280
- // -----------------------------------------------------------------
281
- // Revalidation decision (instant span)
282
- // -----------------------------------------------------------------
283
-
284
264
  case "revalidation.decision": {
285
265
  const span = tracer.startSpan("rango.revalidation.decision", {
286
266
  attributes: {
@@ -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;
@@ -239,10 +235,6 @@ export function formatCacheSignalHeader(
239
235
  return segments.map((s) => `${s.id}=${s.cacheStatus}`).join(", ");
240
236
  }
241
237
 
242
- // ---------------------------------------------------------------------------
243
- // Sink interface
244
- // ---------------------------------------------------------------------------
245
-
246
238
  /**
247
239
  * Telemetry sink receives structured lifecycle events from the router.
248
240
  * Implement this interface to integrate with any observability backend.
@@ -253,10 +245,6 @@ export interface TelemetrySink {
253
245
  emit(event: TelemetryEvent): void;
254
246
  }
255
247
 
256
- // ---------------------------------------------------------------------------
257
- // No-op singleton (zero-cost disabled state)
258
- // ---------------------------------------------------------------------------
259
-
260
248
  const noopSink: TelemetrySink = {
261
249
  emit() {},
262
250
  };
@@ -284,12 +272,6 @@ export function safeEmit(sink: TelemetrySink, event: TelemetryEvent): void {
284
272
  }
285
273
  }
286
274
 
287
- // ---------------------------------------------------------------------------
288
- // Request ID extraction (for span correlation)
289
- // ---------------------------------------------------------------------------
290
-
291
- // Per-request memoization so the same Request object always maps to the
292
- // same ID. WeakMap allows GC when the Request is no longer referenced.
293
275
  const requestIds = new WeakMap<Request, string>();
294
276
  let telemetryRequestCounter = 0;
295
277
 
@@ -323,10 +305,6 @@ export function getRequestId(request: Request): string {
323
305
  return id;
324
306
  }
325
307
 
326
- // ---------------------------------------------------------------------------
327
- // Console sink (built-in, replaces ad-hoc console.log debug traces)
328
- // ---------------------------------------------------------------------------
329
-
330
308
  /**
331
309
  * Built-in console sink that logs events in a structured format.
332
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.
@@ -15,10 +15,6 @@ export interface TrieMatchResult {
15
15
  sp: string;
16
16
  /** Matched route params */
17
17
  params: Record<string, string>;
18
- /** Optional param names declared on the route. Absent params are omitted
19
- * from `params` (read as `undefined`), matching the
20
- * `ExtractParams<"/:locale?/...">` type. */
21
- optionalParams?: string[];
22
18
  /** Redirect target if trailing slash requires it */
23
19
  redirectTo?: string;
24
20
  /** Route has pre-rendered data available */
@@ -43,14 +39,12 @@ export function tryTrieMatch(
43
39
  ): TrieMatchResult | null {
44
40
  if (!trie) return null;
45
41
 
46
- // Split pathname into segments, filtering empty strings from leading/trailing slashes
47
42
  const pathnameHasTrailingSlash =
48
43
  pathname.length > 1 && pathname.endsWith("/");
49
44
  const normalizedPath = pathnameHasTrailingSlash
50
45
  ? pathname.slice(0, -1)
51
46
  : pathname;
52
47
 
53
- // Handle root path
54
48
  if (normalizedPath === "" || normalizedPath === "/") {
55
49
  if (trie.r) {
56
50
  return validateAndBuild(
@@ -77,10 +71,8 @@ export function tryTrieMatch(
77
71
  return null;
78
72
  }
79
73
 
80
- // Remove leading slash and split
81
74
  const segments = normalizedPath.slice(1).split("/");
82
75
 
83
- // Try exact match with normalized path (no trailing slash)
84
76
  const result = walkTrie(trie, segments, 0, []);
85
77
  if (result) {
86
78
  return validateAndBuild(
@@ -102,8 +94,58 @@ interface WalkResult {
102
94
  }
103
95
 
104
96
  /**
105
- * Walk the trie by segments with priority: static > param > wildcard.
106
- * Uses backtracking to try all possible matches.
97
+ * Check a leaf's constraints (leaf.cv) against already-resolved named params.
98
+ * Empty/undefined values are exempt (optional params that were not bound).
99
+ */
100
+ function constraintsSatisfied(
101
+ leaf: TrieLeaf,
102
+ params: Record<string, string>,
103
+ ): boolean {
104
+ if (!leaf.cv) return true;
105
+ for (const paramName in leaf.cv) {
106
+ const allowed = leaf.cv[paramName]!;
107
+ const value = params[paramName];
108
+ if (value !== undefined && value !== "" && !allowed.includes(value)) {
109
+ return false;
110
+ }
111
+ }
112
+ return true;
113
+ }
114
+
115
+ /**
116
+ * Constraint check for a candidate terminal DURING the walk. Builds the named
117
+ * params from positional walk values (decoded the same way validateAndBuild
118
+ * does) and validates leaf.cv. Returning false lets walkTrie unwind to a
119
+ * lower-priority sibling instead of committing to a leaf that would only be
120
+ * rejected post-walk — that post-walk rejection is what forced the regex
121
+ * fallback (and its false "trie gap" R3 warning) for perfectly valid configs.
122
+ */
123
+ function leafConstraintsPass(
124
+ leaf: TrieLeaf,
125
+ paramValues: string[],
126
+ wildcardValue: string | undefined,
127
+ ): boolean {
128
+ if (!leaf.cv) return true;
129
+ const params: Record<string, string> = {};
130
+ if (leaf.pa) {
131
+ for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
132
+ params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
133
+ }
134
+ }
135
+ if (wildcardValue !== undefined && "pn" in leaf) {
136
+ params[(leaf as TrieLeaf & { pn: string }).pn] =
137
+ safeDecodeURIComponent(wildcardValue);
138
+ }
139
+ return constraintsSatisfied(leaf, params);
140
+ }
141
+
142
+ /**
143
+ * Walk the trie by segments with priority: static > suffix-param > param >
144
+ * wildcard (Priority 1-4 below; matches the canonical M4 ordering in
145
+ * docs/internal/matching-and-lazy-discovery.md).
146
+ * Uses backtracking to try all possible matches. Per-leaf constraints are
147
+ * enforced at each candidate terminal so a constraint miss backtracks to a
148
+ * lower-priority sibling rather than aborting the whole match.
107
149
  */
108
150
  function walkTrie(
109
151
  node: TrieNode,
@@ -111,9 +153,8 @@ function walkTrie(
111
153
  index: number,
112
154
  paramValues: string[],
113
155
  ): WalkResult | null {
114
- // All segments consumed: check for terminal
115
156
  if (index === segments.length) {
116
- if (node.r) {
157
+ if (node.r && leafConstraintsPass(node.r, paramValues, undefined)) {
117
158
  return { leaf: node.r, paramValues: [...paramValues] };
118
159
  }
119
160
  // A wildcard at this node matches the bare prefix with an empty remainder
@@ -122,7 +163,7 @@ function walkTrie(
122
163
  // so without this a request to the wildcard's own prefix misses the trie
123
164
  // and the regex fallback emits a corrupt redirect. A static terminal
124
165
  // (node.r) still wins.
125
- if (node.w) {
166
+ if (node.w && leafConstraintsPass(node.w, paramValues, "")) {
126
167
  return { leaf: node.w, paramValues: [...paramValues], wildcardValue: "" };
127
168
  }
128
169
  return null;
@@ -131,14 +172,15 @@ function walkTrie(
131
172
  const segment = segments[index];
132
173
  const staticChild = node.s?.[segment];
133
174
 
134
- // Priority 1: Static match
135
175
  if (staticChild) {
136
176
  const result = walkTrie(staticChild, segments, index + 1, paramValues);
137
177
  if (result) return result;
138
178
  }
139
179
 
140
- // Priority 2: Suffix-param match (e.g., :productId.html)
141
180
  if (node.xp) {
181
+ // node.xp keys are pre-sorted longest-suffix-first at build time
182
+ // (route-trie.ts sortSuffixParams), so the first match is the most specific
183
+ // suffix: `/app.min.js` matches `:file.min.js` before `:file.js`.
142
184
  for (const suffix in node.xp) {
143
185
  if (segment.endsWith(suffix) && segment.length > suffix.length) {
144
186
  const paramValue = segment.slice(0, -suffix.length);
@@ -155,7 +197,6 @@ function walkTrie(
155
197
  }
156
198
  }
157
199
 
158
- // Priority 3: Param match
159
200
  if (node.p) {
160
201
  paramValues.push(segment);
161
202
  const result = walkTrie(node.p.c, segments, index + 1, paramValues);
@@ -163,14 +204,15 @@ function walkTrie(
163
204
  if (result) return result;
164
205
  }
165
206
 
166
- // Priority 4: Wildcard match (consumes rest)
167
207
  if (node.w) {
168
208
  const rest = joinRemainingSegments(segments, index);
169
- return {
170
- leaf: node.w,
171
- paramValues: [...paramValues],
172
- wildcardValue: rest,
173
- };
209
+ if (leafConstraintsPass(node.w, paramValues, rest)) {
210
+ return {
211
+ leaf: node.w,
212
+ paramValues: [...paramValues],
213
+ wildcardValue: rest,
214
+ };
215
+ }
174
216
  }
175
217
 
176
218
  return null;
@@ -196,10 +238,6 @@ function validateAndBuild(
196
238
  originalPathname: string,
197
239
  pathnameHasTrailingSlash: boolean,
198
240
  ): 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
241
  const params: Record<string, string> = {};
204
242
  if (leaf.pa) {
205
243
  for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
@@ -207,31 +245,15 @@ function validateAndBuild(
207
245
  }
208
246
  }
209
247
 
210
- // Add wildcard param (wildcard leaves have pn from TrieNode.w type)
211
248
  if (wildcardValue !== undefined && "pn" in leaf) {
212
249
  params[(leaf as TrieLeaf & { pn: string }).pn] =
213
250
  safeDecodeURIComponent(wildcardValue);
214
251
  }
215
252
 
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
- }
253
+ if (!constraintsSatisfied(leaf, params)) {
254
+ return null;
226
255
  }
227
256
 
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
257
  const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
236
258
  let redirectTo: string | undefined;
237
259
 
@@ -251,7 +273,6 @@ function validateAndBuild(
251
273
  params,
252
274
  };
253
275
 
254
- if (leaf.op) result.optionalParams = leaf.op;
255
276
  if (redirectTo) result.redirectTo = redirectTo;
256
277
  if (leaf.pr) result.pr = true;
257
278
  if (leaf.pt) result.pt = true;
@@ -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
@@ -1,9 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { createCacheScope } from "./cache/cache-scope.js";
3
- import {
4
- setCacheProfiles,
5
- resolveCacheProfiles,
6
- } from "./cache/profile-registry.js";
3
+ import { resolveCacheProfiles } from "./cache/profile-registry.js";
7
4
  import { isCachedFunction } from "./cache/taint.js";
8
5
  import { assertClientComponent } from "./component-utils.js";
9
6
  import { DefaultDocument } from "./components/DefaultDocument.js";
@@ -181,11 +178,10 @@ export function createRouter<TEnv = any>(
181
178
  // Resolve telemetry sink (no-op when not configured)
182
179
  const telemetry = resolveSink(telemetrySink);
183
180
 
184
- // Resolve cache profiles: merge user config with guaranteed default profile.
185
- // This resolved map is both stored on the router (for per-request context)
186
- // and written to the global registry (for DSL-time cache("profileName")).
181
+ // Resolve cache profiles: merge user config with the guaranteed default
182
+ // profile. This resolved map is threaded onto each request context; the
183
+ // "use cache: <profile>" runtime path reads it request-scoped.
187
184
  const resolvedCacheProfiles = resolveCacheProfiles(cacheProfilesOption);
188
- setCacheProfiles(resolvedCacheProfiles);
189
185
 
190
186
  // Source file: prefer Vite-injected path (zero cost), fall back to
191
187
  // stack trace parsing for non-Vite environments (e.g. tests).
@@ -355,7 +351,6 @@ export function createRouter<TEnv = any>(
355
351
  regex,
356
352
  paramNames,
357
353
  handler,
358
- mountPrefix,
359
354
  });
360
355
  }
361
356
 
@@ -1058,8 +1053,10 @@ export function createRouter<TEnv = any>(
1058
1053
  if (!handler) {
1059
1054
  // Lazy import deferred to first request to avoid dev mode issues
1060
1055
  const { createRSCHandler } = await import("./rsc/handler.js");
1061
- // Cast: handler.ts still accepts (request, env) will be updated
1062
- // separately to accept RouterRequestInput.
1056
+ // Cast: createRSCHandler receives `router as any`, which erases TEnv
1057
+ // and infers its handler as RouterRequestInput<unknown>. Re-narrow the
1058
+ // returned handler to RouterRequestInput<TEnv> so the call below stays
1059
+ // typed. (The handler already accepts (request, RouterRequestInput).)
1063
1060
  handler = createRSCHandler({
1064
1061
  router: router as any,
1065
1062
  cache,
@@ -31,6 +31,7 @@ export interface HandlerContext<TEnv = unknown> {
31
31
  createRedirectFlightResponse: (
32
32
  redirectUrl: string,
33
33
  locationState?: Record<string, unknown>,
34
+ external?: boolean,
34
35
  ) => Response;
35
36
 
36
37
  /**
@@ -31,6 +31,7 @@ import {
31
31
  interceptRedirectForPartial,
32
32
  buildRouteMiddlewareEntries,
33
33
  } from "./helpers.js";
34
+ import { guardOutgoingRedirect } from "./redirect-guard.js";
34
35
  import { isWebSocketUpgradeResponse } from "../response-utils.js";
35
36
  import {
36
37
  handleResponseRoute,
@@ -292,12 +293,13 @@ export function createRSCHandler<
292
293
  function createRedirectFlightResponse(
293
294
  redirectUrl: string,
294
295
  locationState?: Record<string, unknown>,
296
+ external?: boolean,
295
297
  ): Response {
296
298
  const redirectPayload: RscPayload = {
297
299
  metadata: {
298
300
  pathname: redirectUrl,
299
301
  segments: [],
300
- redirect: { url: redirectUrl },
302
+ redirect: { url: redirectUrl, ...(external && { external: true }) },
301
303
  ...(locationState && { locationState }),
302
304
  },
303
305
  };
@@ -570,7 +572,12 @@ export function createRSCHandler<
570
572
  response.headers.set("Server-Timing", fullTiming);
571
573
  }
572
574
 
573
- return response;
575
+ // Single open-redirect chokepoint: every response (PE, full-page,
576
+ // middleware short-circuit, response-route) funnels through here, so
577
+ // guarding browser-followed (3xx) redirects once covers them all and any
578
+ // future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
579
+ // through untouched (validated client-side instead).
580
+ return guardOutgoingRedirect(response, url.origin, router.basename);
574
581
  });
575
582
  };
576
583
 
@@ -1017,10 +1024,19 @@ export function createRSCHandler<
1017
1024
  } catch (error) {
1018
1025
  // Check if middleware/handler returned Response
1019
1026
  if (error instanceof Response) {
1027
+ // An action revalidation render is delivered to the client over the
1028
+ // same Flight-parsing path as a partial navigation, so a Response
1029
+ // thrown during it must be converted exactly like a partial one
1030
+ // (raw 200 -> hard-nav hint, 3xx -> Flight redirect). Without this,
1031
+ // the no-middleware path returns the raw Response (the with-middleware
1032
+ // path is already covered by the isPartial || actionContinuation
1033
+ // guard below).
1034
+ const treatAsPartial = isPartial || actionContinuation != null;
1035
+
1020
1036
  // During partial (client-side navigation), a 200 Response from a handler
1021
1037
  // means the route serves raw content (JSON, text, etc.), not JSX.
1022
1038
  // Signal the browser to hard-navigate so it renders the raw response.
1023
- if (isPartial && error.status === 200) {
1039
+ if (treatAsPartial && error.status === 200) {
1024
1040
  console.warn(
1025
1041
  `[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
1026
1042
  `Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
@@ -1034,7 +1050,7 @@ export function createRSCHandler<
1034
1050
  });
1035
1051
  }
1036
1052
 
1037
- if (isPartial) {
1053
+ if (treatAsPartial) {
1038
1054
  const intercepted = interceptRedirectForPartial(
1039
1055
  error,
1040
1056
  createRedirectFlightResponse,