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

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 (255) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2151 -846
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/document-cache/SKILL.md +78 -55
  13. package/skills/handler-use/SKILL.md +364 -0
  14. package/skills/hooks/SKILL.md +229 -20
  15. package/skills/host-router/SKILL.md +45 -20
  16. package/skills/i18n/SKILL.md +276 -0
  17. package/skills/intercept/SKILL.md +46 -4
  18. package/skills/layout/SKILL.md +28 -7
  19. package/skills/links/SKILL.md +247 -17
  20. package/skills/loader/SKILL.md +219 -9
  21. package/skills/middleware/SKILL.md +47 -12
  22. package/skills/migrate-nextjs/SKILL.md +562 -0
  23. package/skills/migrate-react-router/SKILL.md +769 -0
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +71 -6
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +242 -22
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +66 -9
  31. package/skills/route/SKILL.md +57 -4
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +751 -0
  34. package/skills/streams-and-websockets/SKILL.md +283 -0
  35. package/skills/testing/SKILL.md +778 -0
  36. package/skills/typesafety/SKILL.md +319 -27
  37. package/skills/use-cache/SKILL.md +34 -5
  38. package/skills/view-transitions/SKILL.md +294 -0
  39. package/src/__augment-tests__/augment.ts +81 -0
  40. package/src/__augment-tests__/augmented.check.ts +117 -0
  41. package/src/browser/action-coordinator.ts +53 -36
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/event-controller.ts +86 -70
  44. package/src/browser/history-state.ts +21 -0
  45. package/src/browser/index.ts +3 -3
  46. package/src/browser/navigation-bridge.ts +84 -11
  47. package/src/browser/navigation-client.ts +76 -28
  48. package/src/browser/navigation-store.ts +32 -9
  49. package/src/browser/navigation-transaction.ts +10 -28
  50. package/src/browser/partial-update.ts +64 -26
  51. package/src/browser/prefetch/cache.ts +129 -21
  52. package/src/browser/prefetch/fetch.ts +148 -16
  53. package/src/browser/prefetch/queue.ts +36 -5
  54. package/src/browser/rango-state.ts +53 -13
  55. package/src/browser/react/Link.tsx +30 -2
  56. package/src/browser/react/NavigationProvider.tsx +72 -31
  57. package/src/browser/react/filter-segment-order.ts +51 -7
  58. package/src/browser/react/index.ts +3 -0
  59. package/src/browser/react/location-state-shared.ts +175 -4
  60. package/src/browser/react/location-state.ts +39 -13
  61. package/src/browser/react/use-handle.ts +17 -9
  62. package/src/browser/react/use-navigation.ts +22 -2
  63. package/src/browser/react/use-params.ts +20 -8
  64. package/src/browser/react/use-reverse.ts +106 -0
  65. package/src/browser/react/use-router.ts +22 -2
  66. package/src/browser/react/use-segments.ts +11 -8
  67. package/src/browser/response-adapter.ts +25 -0
  68. package/src/browser/rsc-router.tsx +64 -22
  69. package/src/browser/scroll-restoration.ts +22 -14
  70. package/src/browser/segment-reconciler.ts +36 -14
  71. package/src/browser/segment-structure-assert.ts +2 -2
  72. package/src/browser/server-action-bridge.ts +23 -30
  73. package/src/browser/types.ts +21 -0
  74. package/src/build/collect-fallback-refs.ts +107 -0
  75. package/src/build/generate-manifest.ts +60 -35
  76. package/src/build/generate-route-types.ts +2 -0
  77. package/src/build/index.ts +2 -0
  78. package/src/build/route-trie.ts +52 -25
  79. package/src/build/route-types/codegen.ts +4 -4
  80. package/src/build/route-types/include-resolution.ts +1 -1
  81. package/src/build/route-types/per-module-writer.ts +7 -4
  82. package/src/build/route-types/router-processing.ts +55 -14
  83. package/src/build/route-types/scan-filter.ts +1 -1
  84. package/src/build/route-types/source-scan.ts +118 -0
  85. package/src/build/runtime-discovery.ts +9 -20
  86. package/src/cache/cache-scope.ts +28 -42
  87. package/src/cache/cf/cf-cache-store.ts +54 -13
  88. package/src/client.rsc.tsx +3 -0
  89. package/src/client.tsx +92 -182
  90. package/src/context-var.ts +5 -5
  91. package/src/decode-loader-results.ts +36 -0
  92. package/src/errors.ts +30 -1
  93. package/src/handle.ts +26 -13
  94. package/src/host/index.ts +2 -2
  95. package/src/host/router.ts +129 -57
  96. package/src/host/types.ts +31 -2
  97. package/src/host/utils.ts +1 -1
  98. package/src/href-client.ts +140 -20
  99. package/src/index.rsc.ts +9 -4
  100. package/src/index.ts +53 -15
  101. package/src/loader-store.ts +500 -0
  102. package/src/loader.rsc.ts +21 -6
  103. package/src/loader.ts +3 -10
  104. package/src/missing-id-error.ts +68 -0
  105. package/src/outlet-context.ts +1 -1
  106. package/src/prerender.ts +4 -4
  107. package/src/response-utils.ts +37 -0
  108. package/src/reverse.ts +65 -36
  109. package/src/route-content-wrapper.tsx +6 -28
  110. package/src/route-definition/dsl-helpers.ts +384 -257
  111. package/src/route-definition/helper-factories.ts +29 -139
  112. package/src/route-definition/helpers-types.ts +100 -28
  113. package/src/route-definition/resolve-handler-use.ts +6 -0
  114. package/src/route-definition/use-item-types.ts +32 -0
  115. package/src/route-types.ts +26 -41
  116. package/src/router/basename.ts +14 -0
  117. package/src/router/content-negotiation.ts +15 -2
  118. package/src/router/error-handling.ts +1 -1
  119. package/src/router/handler-context.ts +21 -38
  120. package/src/router/intercept-resolution.ts +4 -18
  121. package/src/router/lazy-includes.ts +8 -8
  122. package/src/router/loader-resolution.ts +19 -2
  123. package/src/router/manifest.ts +22 -13
  124. package/src/router/match-api.ts +4 -3
  125. package/src/router/match-handlers.ts +63 -20
  126. package/src/router/match-middleware/cache-lookup.ts +44 -91
  127. package/src/router/match-middleware/cache-store.ts +3 -2
  128. package/src/router/match-result.ts +53 -32
  129. package/src/router/metrics.ts +1 -1
  130. package/src/router/middleware-types.ts +15 -26
  131. package/src/router/middleware.ts +99 -84
  132. package/src/router/pattern-matching.ts +101 -17
  133. package/src/router/prerender-match.ts +1 -1
  134. package/src/router/preview-match.ts +3 -1
  135. package/src/router/request-classification.ts +4 -28
  136. package/src/router/revalidation.ts +58 -2
  137. package/src/router/router-interfaces.ts +45 -28
  138. package/src/router/router-options.ts +40 -1
  139. package/src/router/router-registry.ts +2 -5
  140. package/src/router/segment-resolution/fresh.ts +27 -6
  141. package/src/router/segment-resolution/revalidation.ts +147 -106
  142. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  143. package/src/router/substitute-pattern-params.ts +56 -0
  144. package/src/router/telemetry.ts +99 -0
  145. package/src/router/trie-matching.ts +18 -13
  146. package/src/router/types.ts +8 -0
  147. package/src/router/url-params.ts +49 -0
  148. package/src/router.ts +38 -23
  149. package/src/rsc/handler-context.ts +2 -2
  150. package/src/rsc/handler.ts +28 -69
  151. package/src/rsc/helpers.ts +91 -43
  152. package/src/rsc/index.ts +1 -1
  153. package/src/rsc/origin-guard.ts +28 -10
  154. package/src/rsc/progressive-enhancement.ts +4 -0
  155. package/src/rsc/response-route-handler.ts +46 -53
  156. package/src/rsc/rsc-rendering.ts +35 -51
  157. package/src/rsc/runtime-warnings.ts +9 -10
  158. package/src/rsc/server-action.ts +17 -37
  159. package/src/rsc/ssr-setup.ts +16 -0
  160. package/src/rsc/types.ts +8 -2
  161. package/src/search-params.ts +4 -4
  162. package/src/segment-content-promise.ts +67 -0
  163. package/src/segment-loader-promise.ts +122 -0
  164. package/src/segment-system.tsx +132 -116
  165. package/src/serialize.ts +243 -0
  166. package/src/server/context.ts +143 -53
  167. package/src/server/cookie-store.ts +28 -4
  168. package/src/server/request-context.ts +20 -42
  169. package/src/ssr/index.tsx +5 -1
  170. package/src/static-handler.ts +1 -1
  171. package/src/testing/cache-status.ts +166 -0
  172. package/src/testing/collect-handle.ts +63 -0
  173. package/src/testing/dispatch.ts +440 -0
  174. package/src/testing/dom.entry.ts +22 -0
  175. package/src/testing/e2e/fixture.ts +154 -0
  176. package/src/testing/e2e/index.ts +149 -0
  177. package/src/testing/e2e/matchers.ts +51 -0
  178. package/src/testing/e2e/page-helpers.ts +272 -0
  179. package/src/testing/e2e/parity.ts +306 -0
  180. package/src/testing/e2e/server.ts +183 -0
  181. package/src/testing/flight-matchers.ts +104 -0
  182. package/src/testing/flight-runtime.d.ts +57 -0
  183. package/src/testing/flight-tree.ts +320 -0
  184. package/src/testing/flight.entry.ts +39 -0
  185. package/src/testing/flight.ts +197 -0
  186. package/src/testing/generated-routes.ts +223 -0
  187. package/src/testing/index.ts +106 -0
  188. package/src/testing/internal/context.ts +331 -0
  189. package/src/testing/internal/flight-client-globals.ts +30 -0
  190. package/src/testing/render-route.tsx +565 -0
  191. package/src/testing/run-loader.ts +341 -0
  192. package/src/testing/run-middleware.ts +188 -0
  193. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  194. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  195. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  196. package/src/testing/vitest-stubs/version.ts +5 -0
  197. package/src/testing/vitest.ts +270 -0
  198. package/src/types/global-namespace.ts +39 -26
  199. package/src/types/handler-context.ts +68 -50
  200. package/src/types/index.ts +1 -0
  201. package/src/types/loader-types.ts +5 -6
  202. package/src/types/request-scope.ts +126 -0
  203. package/src/types/route-entry.ts +11 -0
  204. package/src/types/segments.ts +35 -2
  205. package/src/urls/include-helper.ts +34 -67
  206. package/src/urls/index.ts +0 -3
  207. package/src/urls/path-helper-types.ts +41 -7
  208. package/src/urls/path-helper.ts +17 -52
  209. package/src/urls/pattern-types.ts +36 -19
  210. package/src/urls/response-types.ts +22 -29
  211. package/src/urls/type-extraction.ts +26 -116
  212. package/src/urls/urls-function.ts +1 -5
  213. package/src/use-loader.tsx +413 -42
  214. package/src/vite/debug.ts +185 -0
  215. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  216. package/src/vite/discovery/discover-routers.ts +101 -51
  217. package/src/vite/discovery/discovery-errors.ts +194 -0
  218. package/src/vite/discovery/gate-state.ts +171 -0
  219. package/src/vite/discovery/prerender-collection.ts +67 -26
  220. package/src/vite/discovery/route-types-writer.ts +40 -84
  221. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  222. package/src/vite/discovery/state.ts +33 -0
  223. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  224. package/src/vite/index.ts +2 -0
  225. package/src/vite/plugin-types.ts +67 -0
  226. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  227. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  228. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  229. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  230. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  231. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  232. package/src/vite/plugins/expose-action-id.ts +54 -30
  233. package/src/vite/plugins/expose-id-utils.ts +12 -8
  234. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  235. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  236. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  237. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  238. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  239. package/src/vite/plugins/performance-tracks.ts +29 -25
  240. package/src/vite/plugins/use-cache-transform.ts +65 -50
  241. package/src/vite/plugins/version-injector.ts +39 -23
  242. package/src/vite/plugins/version-plugin.ts +59 -2
  243. package/src/vite/plugins/virtual-entries.ts +2 -2
  244. package/src/vite/rango.ts +116 -29
  245. package/src/vite/router-discovery.ts +750 -100
  246. package/src/vite/utils/ast-handler-extract.ts +15 -15
  247. package/src/vite/utils/banner.ts +1 -1
  248. package/src/vite/utils/bundle-analysis.ts +4 -2
  249. package/src/vite/utils/client-chunks.ts +190 -0
  250. package/src/vite/utils/forward-user-plugins.ts +193 -0
  251. package/src/vite/utils/manifest-utils.ts +21 -5
  252. package/src/vite/utils/package-resolution.ts +41 -1
  253. package/src/vite/utils/prerender-utils.ts +21 -6
  254. package/src/vite/utils/shared-utils.ts +107 -26
  255. package/src/browser/action-response-classifier.ts +0 -99
@@ -7,6 +7,7 @@
7
7
  import type { RouteEntry, TrailingSlashMode } from "../types";
8
8
  import type { EntryData } from "../server/context";
9
9
  import { debugLog, isRouterDebugEnabled } from "./logging.js";
10
+ import { safeDecodeURIComponent } from "./url-params.js";
10
11
 
11
12
  /**
12
13
  * Parsed segment info
@@ -82,6 +83,13 @@ export interface CompiledPattern {
82
83
  paramNames: string[];
83
84
  optionalParams: Set<string>;
84
85
  hasTrailingSlash: boolean;
86
+ /**
87
+ * Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
88
+ * Validated against the **decoded** param value after regex extraction so
89
+ * a URL like `/en%20GB` still matches `:lang(en GB)` — matching the trie
90
+ * path's behavior (trie-matching.ts:validateAndBuild).
91
+ */
92
+ constraints?: Record<string, string[]>;
85
93
  }
86
94
 
87
95
  // Module-level cache for compiled patterns. Route patterns are a finite set
@@ -142,6 +150,7 @@ export function compilePattern(pattern: string): CompiledPattern {
142
150
  const segments = parsePattern(normalizedPattern);
143
151
  const paramNames: string[] = [];
144
152
  const optionalParams = new Set<string>();
153
+ let constraints: Record<string, string[]> | undefined;
145
154
 
146
155
  let regexPattern = "";
147
156
 
@@ -152,11 +161,14 @@ export function compilePattern(pattern: string): CompiledPattern {
152
161
  } else if (segment.type === "param") {
153
162
  paramNames.push(segment.value);
154
163
  const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
155
- const valuePattern = segment.constraint
156
- ? `(${segment.constraint.map(escapeRegex).join("|")})`
157
- : segment.suffix
158
- ? "([^/]+?)"
159
- : "([^/]+)";
164
+ // Constrained params capture anything here; the allowed values are
165
+ // checked post-decode in findMatch so URL-encoded constraint values
166
+ // (e.g. `:lang(en GB)` via `/en%20GB`) still match.
167
+ const valuePattern = segment.suffix ? "([^/]+?)" : "([^/]+)";
168
+
169
+ if (segment.constraint) {
170
+ (constraints ??= {})[segment.value] = segment.constraint;
171
+ }
160
172
 
161
173
  if (segment.optional) {
162
174
  optionalParams.add(segment.value);
@@ -176,6 +188,20 @@ export function compilePattern(pattern: string): CompiledPattern {
176
188
  regexPattern = "/";
177
189
  }
178
190
 
191
+ // Patterns of only optional segments (e.g. `/:locale?`, `/:a?/:b?`) need
192
+ // an explicit `/` alternative so a bare `/` matches the absent form. The
193
+ // optional template `(?:/X)?` matches `/X` or empty string, but pathnames
194
+ // are never empty. Arises from `include("/:locale?", routes)` + inner
195
+ // `path("/")`. Skip when an explicit trailing slash already anchors the
196
+ // match.
197
+ const hasOnlyOptionalSegments =
198
+ !hasTrailingSlash &&
199
+ segments.length > 0 &&
200
+ segments.every((segment) => segment.type === "param" && segment.optional);
201
+ if (hasOnlyOptionalSegments) {
202
+ regexPattern = `(?:/|${regexPattern})`;
203
+ }
204
+
179
205
  // Add trailing slash to regex if pattern has one
180
206
  if (hasTrailingSlash) {
181
207
  regexPattern += "/";
@@ -186,9 +212,35 @@ export function compilePattern(pattern: string): CompiledPattern {
186
212
  paramNames,
187
213
  optionalParams,
188
214
  hasTrailingSlash,
215
+ ...(constraints ? { constraints } : {}),
189
216
  };
190
217
  }
191
218
 
219
+ /**
220
+ * Validate decoded params against a compiled pattern's constraints.
221
+ * Returns false if any constrained param has a non-empty value not in the
222
+ * allowed list. Absent optionals (key missing or `undefined`) are allowed;
223
+ * `""` is also tolerated as "absent" so user-provided params or fixtures
224
+ * that pass empty strings explicitly behave the same way.
225
+ */
226
+ function satisfiesConstraints(
227
+ params: Record<string, string>,
228
+ constraints: Record<string, string[]> | undefined,
229
+ ): boolean {
230
+ if (!constraints) return true;
231
+ for (const name in constraints) {
232
+ const value = params[name];
233
+ if (
234
+ value !== undefined &&
235
+ value !== "" &&
236
+ !constraints[name].includes(value)
237
+ ) {
238
+ return false;
239
+ }
240
+ }
241
+ return true;
242
+ }
243
+
192
244
  /**
193
245
  * Escape special regex characters in a string
194
246
  */
@@ -196,6 +248,27 @@ function escapeRegex(str: string): string {
196
248
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
197
249
  }
198
250
 
251
+ /**
252
+ * Build the named-params record from a regex match. Optional segments that
253
+ * didn't capture leave the corresponding group `undefined`; we skip those
254
+ * keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
255
+ * keeps the runtime aligned with the `ExtractParams` type and matches the
256
+ * trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
257
+ */
258
+ function buildParamsFromMatch(
259
+ match: RegExpExecArray,
260
+ paramNames: string[],
261
+ ): Record<string, string> {
262
+ const params: Record<string, string> = {};
263
+ paramNames.forEach((name, index) => {
264
+ const captured = match[index + 1];
265
+ if (captured !== undefined) {
266
+ params[name] = safeDecodeURIComponent(captured);
267
+ }
268
+ });
269
+ return params;
270
+ }
271
+
199
272
  /**
200
273
  * Extract the static prefix from a route pattern.
201
274
  * Returns everything before the first param/wildcard.
@@ -247,8 +320,10 @@ export function extractStaticPrefix(pattern: string): string {
247
320
  /**
248
321
  * Match a pathname against registered routes
249
322
  *
250
- * Note: Optional params that are absent in the path will have empty string value.
251
- * Use the pattern definition to determine if a param is optional.
323
+ * Note: Optional params that are absent in the path are omitted from the
324
+ * returned `params` (read as `undefined`), matching the trie matcher and
325
+ * the `ExtractParams<"/:locale?/...">` type. Use the pattern definition or
326
+ * `optionalParams` to determine which keys are optional.
252
327
  *
253
328
  * Trailing slash handling (priority order):
254
329
  * 1. Per-route `trailingSlash` config from route()
@@ -392,8 +467,13 @@ export function findMatch<TEnv>(
392
467
  fullPattern = entry.prefix + pattern;
393
468
  }
394
469
 
395
- const { regex, paramNames, optionalParams, hasTrailingSlash } =
396
- getCompiledPattern(fullPattern);
470
+ const {
471
+ regex,
472
+ paramNames,
473
+ optionalParams,
474
+ hasTrailingSlash,
475
+ constraints,
476
+ } = getCompiledPattern(fullPattern);
397
477
 
398
478
  // Get trailing slash mode for this route (per-route config or pattern-based)
399
479
  const trailingSlashMode: TrailingSlashMode | undefined =
@@ -410,10 +490,13 @@ export function findMatch<TEnv>(
410
490
  // Try exact match first
411
491
  const match = regex.exec(pathname);
412
492
  if (match) {
413
- const params: Record<string, string> = {};
414
- paramNames.forEach((name, index) => {
415
- params[name] = match[index + 1] ?? "";
416
- });
493
+ const params = buildParamsFromMatch(match, paramNames);
494
+
495
+ // Validate constraints against decoded values; a failure falls
496
+ // through to the next route so other patterns can still match.
497
+ if (!satisfiesConstraints(params, constraints)) {
498
+ continue;
499
+ }
417
500
 
418
501
  if (effectiveDebug) {
419
502
  debugLog("findMatch", "matched route", {
@@ -465,10 +548,11 @@ export function findMatch<TEnv>(
465
548
  // Try alternate pathname (opposite trailing slash)
466
549
  const altMatch = regex.exec(alternatePathname);
467
550
  if (altMatch) {
468
- const params: Record<string, string> = {};
469
- paramNames.forEach((name, index) => {
470
- params[name] = altMatch[index + 1] ?? "";
471
- });
551
+ const params = buildParamsFromMatch(altMatch, paramNames);
552
+
553
+ if (!satisfiesConstraints(params, constraints)) {
554
+ continue;
555
+ }
472
556
 
473
557
  // Determine redirect behavior based on mode
474
558
  if (trailingSlashMode === "ignore") {
@@ -126,7 +126,7 @@ export async function matchForPrerender<TEnv = any>(
126
126
  get env() {
127
127
  if (buildEnv !== undefined) return buildEnv;
128
128
  throw new Error(
129
- "[rsc-router] ctx.env is not available during dev-mode getParams(). " +
129
+ "[rango] ctx.env is not available during dev-mode getParams(). " +
130
130
  "Configure buildEnv in your rango() plugin options to enable build-time env access.",
131
131
  );
132
132
  },
@@ -67,9 +67,11 @@ export async function previewMatch<TEnv = any>(
67
67
  responseType: negotiation.responseType,
68
68
  handler: negotiation.handler,
69
69
  params: matched.params,
70
- negotiated: true,
71
70
  manifestEntry: negotiation.manifestEntry,
72
71
  routeKey: matched.routeKey,
72
+ // omitted unless a variant negotiated, preserving the prior public
73
+ // shape (absent for plain response routes, not negotiated:false)
74
+ ...(negotiation.negotiated ? { negotiated: true } : {}),
73
75
  };
74
76
  }
75
77
 
@@ -278,33 +278,9 @@ async function classifyResponseRoute<TEnv>(
278
278
  pathname: string,
279
279
  snapshot: RouteSnapshot<TEnv>,
280
280
  ): Promise<ResponseRoutePlan<TEnv> | null> {
281
- const { manifestEntry, responseType } = snapshot;
282
-
281
+ // negotiateRoute returns the response plan (variant or plain) or null for RSC.
283
282
  const negotiation = await negotiateRoute(request, pathname, snapshot);
284
- if (negotiation) {
285
- return {
286
- mode: "response",
287
- route: snapshot,
288
- ...negotiation,
289
- };
290
- }
291
-
292
- // Non-negotiated response route (no variants, or RSC won negotiation)
293
- if (responseType) {
294
- const handler =
295
- manifestEntry.type === "route" ? manifestEntry.handler : undefined;
296
- if (handler) {
297
- return {
298
- mode: "response",
299
- route: snapshot,
300
- handler,
301
- responseType,
302
- negotiated: false,
303
- manifestEntry,
304
- routeMiddleware: snapshot.routeMiddleware,
305
- };
306
- }
307
- }
308
-
309
- return null;
283
+ return negotiation
284
+ ? { mode: "response", route: snapshot, ...negotiation }
285
+ : null;
310
286
  }
@@ -4,7 +4,7 @@
4
4
  * Evaluates whether segments should revalidate based on params, actions, and custom functions.
5
5
  */
6
6
 
7
- import type { ResolvedSegment, HandlerContext } from "../types";
7
+ import type { ResolvedSegment, HandlerContext, ActionRef } from "../types";
8
8
  import type { ActionContext } from "./types";
9
9
  import {
10
10
  debugLog,
@@ -15,6 +15,47 @@ import type { RevalidationTraceEntry } from "./logging.js";
15
15
  import { _getRequestContext } from "../server/request-context.js";
16
16
  import { isAutoGeneratedRouteName } from "../route-name.js";
17
17
 
18
+ /**
19
+ * Resolve a server-action reference's stable id, mirroring how the action
20
+ * boundary derives `actionContext.actionId` in `rsc/server-action.ts`
21
+ * (`$id ?? $$id`): the file-path `$id` set by the expose-action-id plugin in a
22
+ * production RSC build when present, otherwise React's `$$id`. Resolving both
23
+ * the incoming `actionId` and the reference with the same precedence makes
24
+ * `isAction()` form-agnostic across dev and production.
25
+ */
26
+ function resolveActionRefId(ref: unknown): string | undefined {
27
+ if (ref == null) return undefined;
28
+ const r = ref as { $id?: unknown; $$id?: unknown };
29
+ if (typeof r.$id === "string") return r.$id;
30
+ if (typeof r.$$id === "string") return r.$$id;
31
+ return undefined;
32
+ }
33
+
34
+ /**
35
+ * Build the `isAction()` helper bound to the current action's id. Matches a
36
+ * single imported action reference, several (variadic), or any export of a
37
+ * namespace import (`import * as Mod`). Returns `false` when there is no action
38
+ * (plain navigation) or nothing matches.
39
+ */
40
+ function makeIsAction(
41
+ currentActionId: string | undefined,
42
+ ): (...actions: ActionRef[]) => boolean {
43
+ return (...actions: ActionRef[]): boolean => {
44
+ if (!currentActionId) return false;
45
+ for (const action of actions) {
46
+ if (typeof action === "function") {
47
+ if (resolveActionRefId(action) === currentActionId) return true;
48
+ } else if (action && typeof action === "object") {
49
+ // Namespace import: match any export of the module.
50
+ for (const value of Object.values(action)) {
51
+ if (resolveActionRefId(value) === currentActionId) return true;
52
+ }
53
+ }
54
+ }
55
+ return false;
56
+ };
57
+ }
58
+
18
59
  function paramsEqual(
19
60
  a: Record<string, string>,
20
61
  b: Record<string, string>,
@@ -59,6 +100,14 @@ interface EvaluateRevalidationOptions<TEnv> {
59
100
  stale?: boolean;
60
101
  /** Trace source hint for the revalidation trace */
61
102
  traceSource?: RevalidationTraceEntry["source"];
103
+ /**
104
+ * Override the segment-type-derived default. When set, the value is used as
105
+ * the seed `defaultShouldRevalidate` passed to user revalidate fns and the
106
+ * reason flows into the trace. Callers use this when client-knowledge
107
+ * (e.g. parallel slot not in clientSegmentIds) should dictate the seed
108
+ * instead of the params/method-based heuristic.
109
+ */
110
+ defaultOverride?: { value: boolean; reason: string };
62
111
  }
63
112
 
64
113
  /**
@@ -81,6 +130,7 @@ export async function evaluateRevalidation<TEnv>(
81
130
  actionContext,
82
131
  stale,
83
132
  traceSource,
133
+ defaultOverride,
84
134
  } = options;
85
135
  const nextParams = segment.params || {};
86
136
  const paramsChanged = !paramsEqual(nextParams, prevParams);
@@ -110,7 +160,12 @@ export async function evaluateRevalidation<TEnv>(
110
160
  let defaultShouldRevalidate: boolean;
111
161
  let defaultReason: string;
112
162
 
113
- if (request.method === "POST") {
163
+ if (defaultOverride) {
164
+ // Caller injected the seed (e.g. parallel slot not in clientSegmentIds).
165
+ // Skip the type-derived heuristic — caller knows better in this context.
166
+ defaultShouldRevalidate = defaultOverride.value;
167
+ defaultReason = defaultOverride.reason;
168
+ } else if (request.method === "POST") {
114
169
  // Actions: revalidate segments that belong to the route, skip parent chain
115
170
  if (segment.type === "route") {
116
171
  // Route segment always revalidates on actions
@@ -226,6 +281,7 @@ export async function evaluateRevalidation<TEnv>(
226
281
  slotName: segment.slot,
227
282
  // Action context (only populated when triggered by server action)
228
283
  actionId: actionContext?.actionId,
284
+ isAction: makeIsAction(actionContext?.actionId),
229
285
  actionUrl: actionContext?.actionUrl,
230
286
  actionResult: actionContext?.actionResult,
231
287
  formData: actionContext?.formData,
@@ -2,7 +2,7 @@ import type { ComponentType, ReactNode } from "react";
2
2
  import type { SerializedManifest } from "../debug.js";
3
3
  import type { ReverseFunction } from "../reverse.js";
4
4
  import type { UrlPatterns } from "../urls.js";
5
- import type { UrlBuilder } from "../urls/pattern-types.js";
5
+ import type { UrlBuilder, EnvCompatible } from "../urls/pattern-types.js";
6
6
  import type { EntryData } from "../server/context";
7
7
  import type { ErrorInfo, MatchResult } from "../types";
8
8
  import type { NonceProvider } from "../rsc/types.js";
@@ -13,7 +13,7 @@ import type {
13
13
  } from "../cache/types.js";
14
14
  import type { MiddlewareEntry, MiddlewareFn } from "./middleware.js";
15
15
  import { RSC_ROUTER_BRAND } from "./router-registry.js";
16
- import type { RSCRouterOptions, RootLayoutProps } from "./router-options.js";
16
+ import type { RangoOptions, RootLayoutProps } from "./router-options.js";
17
17
  import type { DefaultVars } from "../types/global-namespace.js";
18
18
  import type { ResolvedTimeouts, OnTimeoutCallback } from "./timeout.js";
19
19
 
@@ -49,16 +49,16 @@ type MergeRoutesWithResponses<
49
49
  };
50
50
 
51
51
  /**
52
- * Public RSC Router interface — the user-facing API surface.
52
+ * Public Rango router interface — the user-facing API surface.
53
53
  *
54
54
  * Users interact with this type when building and using routers.
55
- * Internal framework code uses RSCRouterInternal (via toInternal()) to access
55
+ * Internal framework code uses RangoInternal (via toInternal()) to access
56
56
  * matching, build-time, and configuration members that are not part of the
57
57
  * public contract.
58
58
  *
59
59
  * TRoutes accumulates all registered route types through the builder chain.
60
60
  */
61
- export interface RSCRouter<
61
+ export interface Rango<
62
62
  TEnv = any,
63
63
  TRoutes extends Record<string, unknown> = Record<string, string>,
64
64
  > {
@@ -89,16 +89,16 @@ export interface RSCRouter<
89
89
  * ])
90
90
  * ```
91
91
  */
92
- routes<T extends UrlPatterns<TEnv, any>>(
93
- patterns: T,
94
- ): RSCRouter<
92
+ routes<T extends UrlPatterns<any, any, any>>(
93
+ patterns: T & EnvCompatible<T, TEnv>,
94
+ ): Rango<
95
95
  TEnv,
96
96
  TRoutes &
97
97
  (NonNullable<T["_routes"]> extends Record<string, unknown>
98
98
  ? MergeRoutesWithResponses<NonNullable<T["_routes"]>, T["_responses"]>
99
99
  : Record<string, string>)
100
100
  >;
101
- routes(builder: UrlBuilder<TEnv>): RSCRouter<TEnv, TRoutes>;
101
+ routes(builder: UrlBuilder<TEnv>): Rango<TEnv, TRoutes>;
102
102
 
103
103
  /**
104
104
  * Add global middleware that runs on all routes
@@ -114,7 +114,7 @@ export interface RSCRouter<
114
114
  use(
115
115
  patternOrMiddleware: string | MiddlewareFn<TEnv>,
116
116
  middleware?: MiddlewareFn<TEnv>,
117
- ): RSCRouter<TEnv, TRoutes>;
117
+ ): Rango<TEnv, TRoutes>;
118
118
 
119
119
  /**
120
120
  * Type-safe URL builder for registered routes
@@ -141,7 +141,7 @@ export interface RSCRouter<
141
141
  * type AppRoutes = typeof _router.routeMap;
142
142
  *
143
143
  * declare global {
144
- * namespace RSCRouter {
144
+ * namespace Rango {
145
145
  * interface RegisteredRoutes extends AppRoutes {}
146
146
  * }
147
147
  * }
@@ -177,16 +177,16 @@ export interface RSCRouter<
177
177
  }
178
178
 
179
179
  /**
180
- * Internal RSC Router interface — the full framework-facing API.
180
+ * Internal Rango router interface — the full framework-facing API.
181
181
  *
182
182
  * This type includes all members used by the Vite plugin, RSC handler,
183
183
  * pre-rendering pipeline, and other framework internals. It is NOT exported
184
184
  * from the public package API.
185
185
  *
186
- * Use toInternal(router) to assert a public RSCRouter into this type
186
+ * Use toInternal(router) to assert a public Rango into this type
187
187
  * at the boundary where framework code receives a user-provided router.
188
188
  */
189
- export interface RSCRouterInternal<
189
+ export interface RangoInternal<
190
190
  TEnv = any,
191
191
  TRoutes extends Record<string, unknown> = Record<string, string>,
192
192
  > {
@@ -206,18 +206,24 @@ export interface RSCRouterInternal<
206
206
  readonly basename: string | undefined;
207
207
 
208
208
  /**
209
- * Register routes using URL patterns from urls() or a builder function
210
- */
211
- routes<T extends UrlPatterns<TEnv, any>>(
212
- patterns: T,
213
- ): RSCRouter<
209
+ * Register routes using URL patterns from urls() or a builder function.
210
+ *
211
+ * Env compatibility is checked by EnvCompatible: an env-agnostic urls() block
212
+ * (its env is `unknown` — e.g. a shared module, or an app that does not augment
213
+ * `Rango.Env`) attaches to any router, while a urls<TEnv>() block carrying a
214
+ * concrete env is accepted only when this router's `TEnv` satisfies it. So a
215
+ * `urls<{ DB }>()` cannot be mounted on a `createRouter<{}>()`.
216
+ */
217
+ routes<T extends UrlPatterns<any, any, any>>(
218
+ patterns: T & EnvCompatible<T, TEnv>,
219
+ ): Rango<
214
220
  TEnv,
215
221
  TRoutes &
216
222
  (NonNullable<T["_routes"]> extends Record<string, unknown>
217
223
  ? MergeRoutesWithResponses<NonNullable<T["_routes"]>, T["_responses"]>
218
224
  : Record<string, string>)
219
225
  >;
220
- routes(builder: UrlBuilder<TEnv>): RSCRouter<TEnv, TRoutes>;
226
+ routes(builder: UrlBuilder<TEnv>): Rango<TEnv, TRoutes>;
221
227
 
222
228
  /**
223
229
  * Add global middleware that runs on all routes
@@ -225,7 +231,7 @@ export interface RSCRouterInternal<
225
231
  use(
226
232
  patternOrMiddleware: string | MiddlewareFn<TEnv>,
227
233
  middleware?: MiddlewareFn<TEnv>,
228
- ): RSCRouter<TEnv, TRoutes>;
234
+ ): Rango<TEnv, TRoutes>;
229
235
 
230
236
  /**
231
237
  * Type-safe URL builder for registered routes
@@ -247,17 +253,17 @@ export interface RSCRouterInternal<
247
253
  * Error callback for monitoring/alerting
248
254
  * Called when errors occur in loaders, actions, or routes
249
255
  */
250
- readonly onError?: RSCRouterOptions<TEnv>["onError"];
256
+ readonly onError?: RangoOptions<TEnv>["onError"];
251
257
 
252
258
  /**
253
259
  * Cache configuration
254
260
  */
255
- readonly cache?: RSCRouterOptions<TEnv>["cache"];
261
+ readonly cache?: RangoOptions<TEnv>["cache"];
256
262
 
257
263
  /**
258
264
  * Not found component to render when no route matches
259
265
  */
260
- readonly notFound?: RSCRouterOptions<TEnv>["notFound"];
266
+ readonly notFound?: RangoOptions<TEnv>["notFound"];
261
267
 
262
268
  /**
263
269
  * Resolved theme configuration (null if theme not enabled)
@@ -359,6 +365,17 @@ export interface RSCRouterInternal<
359
365
  /** @internal basename for runtime manifest generation */
360
366
  readonly __basename?: string;
361
367
 
368
+ /**
369
+ * @internal Router-level error/notFound fallbacks (`createRouter` options),
370
+ * exposed for the build-time clientChunks discovery so a `"use client"`
371
+ * default boundary is routed into the dedicated `app-fallback` chunk. Unlike
372
+ * the route-tree `errorBoundary()`/`notFoundBoundary()` helpers these never
373
+ * land in `EntryData`, so they are read directly off the router instance.
374
+ */
375
+ readonly __defaultErrorBoundary?: RangoOptions<TEnv>["defaultErrorBoundary"];
376
+ readonly __defaultNotFoundBoundary?: RangoOptions<TEnv>["defaultNotFoundBoundary"];
377
+ readonly __notFound?: RangoOptions<TEnv>["notFound"];
378
+
362
379
  match(
363
380
  request: Request,
364
381
  input?: RouterRequestInput<TEnv>,
@@ -469,16 +486,16 @@ export interface RSCRouterInternal<
469
486
  }
470
487
 
471
488
  /**
472
- * Assert a public RSCRouter into the internal type.
489
+ * Assert a public Rango into the internal type.
473
490
  *
474
491
  * Use this at the boundary where framework code receives a user-provided
475
492
  * router and needs access to internal members (match, config, build-time).
476
493
  * The cast is safe because createRouter() always produces an object that
477
- * satisfies RSCRouterInternal; the public type is just a narrower view.
494
+ * satisfies RangoInternal; the public type is just a narrower view.
478
495
  */
479
496
  export function toInternal<
480
497
  TEnv = any,
481
498
  TRoutes extends Record<string, unknown> = Record<string, string>,
482
- >(router: RSCRouter<TEnv, TRoutes>): RSCRouterInternal<TEnv, TRoutes> {
483
- return router as RSCRouterInternal<TEnv, TRoutes>;
499
+ >(router: Rango<TEnv, TRoutes>): RangoInternal<TEnv, TRoutes> {
500
+ return router as RangoInternal<TEnv, TRoutes>;
484
501
  }
@@ -73,7 +73,7 @@ export interface RootLayoutProps {
73
73
  /**
74
74
  * Router configuration options
75
75
  */
76
- export interface RSCRouterOptions<TEnv = any> {
76
+ export interface RangoOptions<TEnv = any> {
77
77
  /**
78
78
  * Unique identifier for this router instance.
79
79
  * Used to namespace static output files and route maps.
@@ -132,6 +132,21 @@ export interface RSCRouterOptions<TEnv = any> {
132
132
  */
133
133
  allowDebugManifest?: boolean;
134
134
 
135
+ /**
136
+ * DEVELOPMENT/TEST ONLY. Emit an `X-Rango-Cache` response header describing
137
+ * the cache status of the matched route, for use by testing primitives such
138
+ * as `assertCacheStatus`.
139
+ *
140
+ * Defaults to `false`. When neither this option nor the
141
+ * `RANGO_TEST_SIGNALS=1` environment flag is set, NO header is emitted and
142
+ * router output is byte-identical to the default.
143
+ *
144
+ * The header encodes per-segment (v1: coarse route-level) status keyed by the
145
+ * route NAME, e.g. `X-Rango-Cache: product.detail=hit`. Do NOT enable in
146
+ * production — it exposes internal cache decisions.
147
+ */
148
+ debugCacheSignal?: boolean;
149
+
135
150
  /**
136
151
  * Document component that wraps the entire application.
137
152
  *
@@ -357,6 +372,30 @@ export interface RSCRouterOptions<TEnv = any> {
357
372
  */
358
373
  theme?: import("../theme/types.js").ThemeConfig | true;
359
374
 
375
+ /**
376
+ * Default for whether the router wraps `transition()` segments in its own
377
+ * React `<ViewTransition>` boundary (experimental React only).
378
+ *
379
+ * - "auto" (default): every route/layout that opts in via `transition()`
380
+ * gets a router-owned cross-fade.
381
+ * - false: the router never places its own boundary. Routes that use
382
+ * `transition()` still drive navigation through startTransition (so loaders
383
+ * hold instead of flashing a skeleton) and still let consumer-placed
384
+ * `<ViewTransition>` elements animate — the router just contributes no
385
+ * cross-fade of its own. This is the "router triggers, you place the
386
+ * transitions" model.
387
+ *
388
+ * A per-segment `transition({ viewTransition })` overrides this default.
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * // App-wide: drive + hold, but never auto-wrap. Place <ViewTransition>
393
+ * // yourself in components where you want a morph.
394
+ * const router = createRouter<AppEnv>({ viewTransition: false });
395
+ * ```
396
+ */
397
+ viewTransition?: "auto" | false;
398
+
360
399
  /**
361
400
  * URL patterns to register with the router.
362
401
  *
@@ -1,4 +1,4 @@
1
- import type { RSCRouterInternal } from "./router-interfaces.js";
1
+ import type { RangoInternal } from "./router-interfaces.js";
2
2
 
3
3
  /**
4
4
  * Brand marker for identifying router instances at build time.
@@ -12,10 +12,7 @@ export const RSC_ROUTER_BRAND = "__rsc_router__" as const;
12
12
  * Used by the Vite plugin at build time to discover routers and extract
13
13
  * manifests, prefix trees, and pre-render candidates.
14
14
  */
15
- export const RouterRegistry: Map<
16
- string,
17
- RSCRouterInternal<any, any>
18
- > = new Map();
15
+ export const RouterRegistry: Map<string, RangoInternal<any, any>> = new Map();
19
16
 
20
17
  export let routerAutoId = 0;
21
18