@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1

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 (214) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +914 -485
  5. package/package.json +55 -11
  6. package/skills/bundle-analysis/SKILL.md +159 -0
  7. package/skills/cache-guide/SKILL.md +220 -30
  8. package/skills/caching/SKILL.md +116 -8
  9. package/skills/composability/SKILL.md +27 -2
  10. package/skills/document-cache/SKILL.md +78 -55
  11. package/skills/handler-use/SKILL.md +3 -1
  12. package/skills/hooks/SKILL.md +214 -18
  13. package/skills/host-router/SKILL.md +45 -20
  14. package/skills/intercept/SKILL.md +26 -4
  15. package/skills/layout/SKILL.md +6 -7
  16. package/skills/links/SKILL.md +173 -17
  17. package/skills/loader/SKILL.md +149 -6
  18. package/skills/middleware/SKILL.md +13 -9
  19. package/skills/migrate-nextjs/SKILL.md +1 -1
  20. package/skills/mime-routes/SKILL.md +27 -0
  21. package/skills/observability/SKILL.md +137 -0
  22. package/skills/parallel/SKILL.md +5 -6
  23. package/skills/prerender/SKILL.md +14 -33
  24. package/skills/rango/SKILL.md +242 -26
  25. package/skills/react-compiler/SKILL.md +168 -0
  26. package/skills/response-routes/SKILL.md +58 -9
  27. package/skills/route/SKILL.md +13 -4
  28. package/skills/router-setup/SKILL.md +3 -3
  29. package/skills/server-actions/SKILL.md +53 -41
  30. package/skills/testing/SKILL.md +599 -0
  31. package/skills/typesafety/SKILL.md +310 -26
  32. package/skills/use-cache/SKILL.md +34 -5
  33. package/skills/view-transitions/SKILL.md +294 -0
  34. package/src/__augment-tests__/augment.ts +81 -0
  35. package/src/__augment-tests__/augmented.check.ts +117 -0
  36. package/src/browser/action-coordinator.ts +53 -36
  37. package/src/browser/event-controller.ts +42 -66
  38. package/src/browser/history-state.ts +21 -0
  39. package/src/browser/index.ts +3 -3
  40. package/src/browser/navigation-bridge.ts +6 -6
  41. package/src/browser/navigation-client.ts +12 -15
  42. package/src/browser/navigation-store.ts +7 -8
  43. package/src/browser/navigation-transaction.ts +10 -28
  44. package/src/browser/partial-update.ts +9 -19
  45. package/src/browser/react/NavigationProvider.tsx +29 -40
  46. package/src/browser/react/index.ts +3 -0
  47. package/src/browser/react/location-state-shared.ts +175 -4
  48. package/src/browser/react/location-state.ts +39 -13
  49. package/src/browser/react/use-handle.ts +17 -9
  50. package/src/browser/react/use-params.ts +3 -4
  51. package/src/browser/react/use-reverse.ts +106 -0
  52. package/src/browser/react/use-router.ts +14 -1
  53. package/src/browser/response-adapter.ts +25 -0
  54. package/src/browser/rsc-router.tsx +30 -16
  55. package/src/browser/scroll-restoration.ts +22 -14
  56. package/src/browser/segment-structure-assert.ts +2 -2
  57. package/src/browser/server-action-bridge.ts +23 -30
  58. package/src/browser/types.ts +2 -0
  59. package/src/build/collect-fallback-refs.ts +107 -0
  60. package/src/build/generate-manifest.ts +60 -35
  61. package/src/build/generate-route-types.ts +2 -0
  62. package/src/build/index.ts +2 -0
  63. package/src/build/route-types/codegen.ts +4 -4
  64. package/src/build/route-types/include-resolution.ts +1 -1
  65. package/src/build/route-types/per-module-writer.ts +7 -4
  66. package/src/build/route-types/router-processing.ts +55 -14
  67. package/src/build/route-types/scan-filter.ts +1 -1
  68. package/src/build/route-types/source-scan.ts +118 -0
  69. package/src/build/runtime-discovery.ts +9 -20
  70. package/src/cache/cache-scope.ts +28 -42
  71. package/src/cache/cf/cf-cache-store.ts +49 -6
  72. package/src/client.rsc.tsx +3 -0
  73. package/src/client.tsx +10 -8
  74. package/src/context-var.ts +5 -5
  75. package/src/decode-loader-results.ts +36 -0
  76. package/src/errors.ts +30 -1
  77. package/src/handle.ts +26 -13
  78. package/src/host/index.ts +2 -2
  79. package/src/host/router.ts +129 -57
  80. package/src/host/types.ts +31 -2
  81. package/src/host/utils.ts +1 -1
  82. package/src/href-client.ts +140 -20
  83. package/src/index.rsc.ts +6 -4
  84. package/src/index.ts +13 -6
  85. package/src/loader-store.ts +500 -0
  86. package/src/loader.rsc.ts +2 -5
  87. package/src/loader.ts +3 -10
  88. package/src/missing-id-error.ts +68 -0
  89. package/src/prerender.ts +4 -4
  90. package/src/response-utils.ts +9 -0
  91. package/src/reverse.ts +65 -41
  92. package/src/route-content-wrapper.tsx +6 -28
  93. package/src/route-definition/dsl-helpers.ts +238 -263
  94. package/src/route-definition/helper-factories.ts +29 -139
  95. package/src/route-definition/helpers-types.ts +37 -14
  96. package/src/route-definition/use-item-types.ts +32 -0
  97. package/src/route-types.ts +19 -41
  98. package/src/router/basename.ts +14 -0
  99. package/src/router/content-negotiation.ts +15 -2
  100. package/src/router/error-handling.ts +1 -1
  101. package/src/router/handler-context.ts +4 -42
  102. package/src/router/intercept-resolution.ts +4 -18
  103. package/src/router/lazy-includes.ts +2 -2
  104. package/src/router/loader-resolution.ts +16 -2
  105. package/src/router/match-handlers.ts +62 -20
  106. package/src/router/match-middleware/cache-lookup.ts +44 -91
  107. package/src/router/match-middleware/cache-store.ts +3 -2
  108. package/src/router/match-result.ts +32 -30
  109. package/src/router/metrics.ts +1 -1
  110. package/src/router/middleware-types.ts +1 -1
  111. package/src/router/middleware.ts +46 -78
  112. package/src/router/prerender-match.ts +1 -1
  113. package/src/router/preview-match.ts +3 -1
  114. package/src/router/request-classification.ts +4 -28
  115. package/src/router/revalidation.ts +43 -1
  116. package/src/router/router-interfaces.ts +45 -28
  117. package/src/router/router-options.ts +40 -1
  118. package/src/router/router-registry.ts +2 -5
  119. package/src/router/segment-resolution/fresh.ts +19 -6
  120. package/src/router/segment-resolution/revalidation.ts +19 -6
  121. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  122. package/src/router/substitute-pattern-params.ts +56 -0
  123. package/src/router/telemetry.ts +99 -0
  124. package/src/router/types.ts +8 -0
  125. package/src/router.ts +37 -21
  126. package/src/rsc/handler-context.ts +2 -2
  127. package/src/rsc/handler.ts +20 -65
  128. package/src/rsc/helpers.ts +22 -2
  129. package/src/rsc/index.ts +1 -1
  130. package/src/rsc/origin-guard.ts +28 -10
  131. package/src/rsc/response-route-handler.ts +32 -52
  132. package/src/rsc/rsc-rendering.ts +27 -53
  133. package/src/rsc/runtime-warnings.ts +9 -10
  134. package/src/rsc/server-action.ts +13 -37
  135. package/src/rsc/ssr-setup.ts +16 -0
  136. package/src/rsc/types.ts +2 -2
  137. package/src/search-params.ts +4 -4
  138. package/src/segment-system.tsx +121 -65
  139. package/src/serialize.ts +243 -0
  140. package/src/server/context.ts +118 -51
  141. package/src/server/cookie-store.ts +28 -4
  142. package/src/server/request-context.ts +10 -0
  143. package/src/static-handler.ts +1 -1
  144. package/src/testing/cache-status.ts +166 -0
  145. package/src/testing/collect-handle.ts +63 -0
  146. package/src/testing/dispatch.ts +440 -0
  147. package/src/testing/dom.entry.ts +22 -0
  148. package/src/testing/e2e/fixture.ts +154 -0
  149. package/src/testing/e2e/index.ts +149 -0
  150. package/src/testing/e2e/matchers.ts +51 -0
  151. package/src/testing/e2e/page-helpers.ts +272 -0
  152. package/src/testing/e2e/parity.ts +306 -0
  153. package/src/testing/e2e/server.ts +183 -0
  154. package/src/testing/flight-matchers.ts +104 -0
  155. package/src/testing/flight-runtime.d.ts +21 -0
  156. package/src/testing/flight.entry.ts +22 -0
  157. package/src/testing/flight.ts +182 -0
  158. package/src/testing/generated-routes.ts +223 -0
  159. package/src/testing/index.ts +105 -0
  160. package/src/testing/internal/context.ts +193 -0
  161. package/src/testing/render-route.tsx +536 -0
  162. package/src/testing/run-loader.ts +296 -0
  163. package/src/testing/run-middleware.ts +170 -0
  164. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  165. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  166. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  167. package/src/testing/vitest-stubs/version.ts +5 -0
  168. package/src/testing/vitest.ts +183 -0
  169. package/src/types/global-namespace.ts +39 -26
  170. package/src/types/handler-context.ts +56 -11
  171. package/src/types/index.ts +1 -0
  172. package/src/types/segments.ts +18 -1
  173. package/src/urls/include-helper.ts +10 -53
  174. package/src/urls/index.ts +0 -3
  175. package/src/urls/path-helper-types.ts +11 -3
  176. package/src/urls/path-helper.ts +17 -52
  177. package/src/urls/pattern-types.ts +36 -19
  178. package/src/urls/response-types.ts +20 -19
  179. package/src/urls/type-extraction.ts +26 -116
  180. package/src/urls/urls-function.ts +1 -5
  181. package/src/use-loader.tsx +413 -42
  182. package/src/vite/debug.ts +1 -0
  183. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  184. package/src/vite/discovery/discover-routers.ts +70 -48
  185. package/src/vite/discovery/discovery-errors.ts +194 -0
  186. package/src/vite/discovery/prerender-collection.ts +19 -25
  187. package/src/vite/discovery/route-types-writer.ts +40 -84
  188. package/src/vite/discovery/state.ts +33 -0
  189. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  190. package/src/vite/index.ts +2 -0
  191. package/src/vite/plugin-types.ts +67 -0
  192. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  193. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  194. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  195. package/src/vite/plugins/expose-action-id.ts +2 -2
  196. package/src/vite/plugins/expose-id-utils.ts +12 -8
  197. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  198. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  199. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  200. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  201. package/src/vite/plugins/performance-tracks.ts +12 -16
  202. package/src/vite/plugins/use-cache-transform.ts +13 -11
  203. package/src/vite/plugins/version-injector.ts +2 -12
  204. package/src/vite/plugins/version-plugin.ts +59 -2
  205. package/src/vite/plugins/virtual-entries.ts +2 -2
  206. package/src/vite/rango.ts +67 -15
  207. package/src/vite/router-discovery.ts +208 -63
  208. package/src/vite/utils/ast-handler-extract.ts +15 -15
  209. package/src/vite/utils/bundle-analysis.ts +4 -2
  210. package/src/vite/utils/client-chunks.ts +190 -0
  211. package/src/vite/utils/forward-user-plugins.ts +193 -0
  212. package/src/vite/utils/manifest-utils.ts +21 -5
  213. package/src/vite/utils/shared-utils.ts +107 -26
  214. package/src/browser/action-response-classifier.ts +0 -99
@@ -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>,
@@ -240,6 +281,7 @@ export async function evaluateRevalidation<TEnv>(
240
281
  slotName: segment.slot,
241
282
  // Action context (only populated when triggered by server action)
242
283
  actionId: actionContext?.actionId,
284
+ isAction: makeIsAction(actionContext?.actionId),
243
285
  actionUrl: actionContext?.actionUrl,
244
286
  actionResult: actionContext?.actionResult,
245
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
 
@@ -28,11 +28,12 @@ import {
28
28
  resolveLayoutComponent,
29
29
  resolveWithErrorBoundary,
30
30
  } from "./helpers.js";
31
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
31
32
  import { getRouterContext } from "../router-context.js";
32
33
  import { resolveSink, safeEmit } from "../telemetry.js";
33
34
  import {
34
35
  track,
35
- RSCRouterContext,
36
+ RangoContext,
36
37
  runInsideLoaderScope,
37
38
  } from "../../server/context.js";
38
39
 
@@ -224,7 +225,10 @@ export async function resolveSegment<TEnv>(
224
225
  index: 0,
225
226
  component,
226
227
  loading: entry.loading === false ? null : entry.loading,
227
- transition: entry.transition,
228
+ transition: applyViewTransitionDefault(
229
+ entry.transition,
230
+ deps.viewTransitionDefault,
231
+ ),
228
232
  params,
229
233
  belongsToRoute: false,
230
234
  layoutName: entry.id,
@@ -359,7 +363,10 @@ export async function resolveSegment<TEnv>(
359
363
  index: 0,
360
364
  component: component ?? null,
361
365
  loading: entry.loading === false ? null : entry.loading,
362
- transition: entry.transition,
366
+ transition: applyViewTransitionDefault(
367
+ entry.transition,
368
+ deps.viewTransitionDefault,
369
+ ),
363
370
  params,
364
371
  belongsToRoute: true,
365
372
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
@@ -443,7 +450,10 @@ export async function resolveOrphanLayout<TEnv>(
443
450
  belongsToRoute,
444
451
  layoutName: orphan.id,
445
452
  loading: orphan.loading === false ? null : orphan.loading,
446
- transition: orphan.transition,
453
+ transition: applyViewTransitionDefault(
454
+ orphan.transition,
455
+ deps.viewTransitionDefault,
456
+ ),
447
457
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
448
458
  });
449
459
 
@@ -565,7 +575,10 @@ export async function resolveParallelEntry<TEnv>(
565
575
  index: 0,
566
576
  component,
567
577
  loading: parallelEntry.loading === false ? null : parallelEntry.loading,
568
- transition: parallelEntry.transition,
578
+ transition: applyViewTransitionDefault(
579
+ parallelEntry.transition,
580
+ deps.viewTransitionDefault,
581
+ ),
569
582
  params,
570
583
  slot,
571
584
  belongsToRoute,
@@ -632,7 +645,7 @@ export async function resolveAllSegments<TEnv>(
632
645
  // can guard non-cacheable variable reads. Also guards response-level
633
646
  // side effects (headers.set). Persists for all descendant entries.
634
647
  if (entry.type === "cache") {
635
- const store = RSCRouterContext.getStore();
648
+ const store = RangoContext.getStore();
636
649
  if (store) store.insideCacheScope = true;
637
650
  }
638
651
  const doneEntry = track(`segment:${entry.id}`, 1);
@@ -39,11 +39,12 @@ import {
39
39
  resolveLayoutComponent,
40
40
  resolveWithErrorBoundary,
41
41
  } from "./helpers.js";
42
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
42
43
  import { getRouterContext } from "../router-context.js";
43
44
  import { resolveSink, safeEmit } from "../telemetry.js";
44
45
  import {
45
46
  track,
46
- RSCRouterContext,
47
+ RangoContext,
47
48
  runInsideLoaderScope,
48
49
  } from "../../server/context.js";
49
50
 
@@ -593,7 +594,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
593
594
  index: 0,
594
595
  component,
595
596
  loading: parallelEntry.loading === false ? null : parallelEntry.loading,
596
- transition: parallelEntry.transition,
597
+ transition: applyViewTransitionDefault(
598
+ parallelEntry.transition,
599
+ deps.viewTransitionDefault,
600
+ ),
597
601
  params,
598
602
  slot,
599
603
  _handlerRan: handlerRan,
@@ -803,7 +807,10 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
803
807
  index: 0,
804
808
  component: resolvedComponent,
805
809
  loading: entry.loading === false ? null : entry.loading,
806
- transition: entry.transition,
810
+ transition: applyViewTransitionDefault(
811
+ entry.transition,
812
+ deps.viewTransitionDefault,
813
+ ),
807
814
  params,
808
815
  belongsToRoute,
809
816
  ...(entry.type === "layout" || entry.type === "cache"
@@ -1137,7 +1144,10 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1137
1144
  belongsToRoute,
1138
1145
  layoutName: orphan.id,
1139
1146
  loading: orphan.loading === false ? null : orphan.loading,
1140
- transition: orphan.transition,
1147
+ transition: applyViewTransitionDefault(
1148
+ orphan.transition,
1149
+ deps.viewTransitionDefault,
1150
+ ),
1141
1151
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
1142
1152
  });
1143
1153
 
@@ -1294,7 +1304,10 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1294
1304
  index: 0,
1295
1305
  component,
1296
1306
  loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1297
- transition: parallelEntry.transition,
1307
+ transition: applyViewTransitionDefault(
1308
+ parallelEntry.transition,
1309
+ deps.viewTransitionDefault,
1310
+ ),
1298
1311
  params,
1299
1312
  slot,
1300
1313
  _handlerRan: handlerRan,
@@ -1356,7 +1369,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1356
1369
 
1357
1370
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1358
1371
  if (entry.type === "cache") {
1359
- const store = RSCRouterContext.getStore();
1372
+ const store = RangoContext.getStore();
1360
1373
  if (store) store.insideCacheScope = true;
1361
1374
  }
1362
1375
  const doneEntry = track(`segment:${entry.id}`, 1);
@@ -0,0 +1,36 @@
1
+ /**
2
+ * View-transition boundary default resolution.
3
+ *
4
+ * Kept in its own module (rather than helpers.ts) because several resolution
5
+ * tests mock helpers.ts with an explicit export list; a shared util here is
6
+ * never mocked, so the fresh and revalidation paths always get the real
7
+ * implementation.
8
+ */
9
+
10
+ import type { EntryData } from "../../server/context";
11
+
12
+ /**
13
+ * Resolve the effective `viewTransition` for a segment's transition config.
14
+ *
15
+ * The per-segment value (set via the transition() DSL) always wins. When it is
16
+ * unset, the router-level createRouter({ viewTransition }) default is stamped
17
+ * in so the render gate reads the boundary decision off the segment — server
18
+ * and client, via the serialized segment — without the router option being
19
+ * threaded to the client. Only `false` is ever stamped; an unset (or "auto")
20
+ * value is left untouched because it already means "wrap" at the gate, which
21
+ * also avoids needless object allocation and payload growth. Used by both the
22
+ * fresh and revalidation resolution paths.
23
+ */
24
+ export function applyViewTransitionDefault(
25
+ transition: EntryData["transition"],
26
+ viewTransitionDefault: "auto" | false | undefined,
27
+ ): EntryData["transition"] {
28
+ if (!transition) return transition;
29
+ if (
30
+ transition.viewTransition === undefined &&
31
+ viewTransitionDefault === false
32
+ ) {
33
+ return { ...transition, viewTransition: false };
34
+ }
35
+ return transition;
36
+ }
@@ -0,0 +1,56 @@
1
+ import { encodePathSegment } from "./url-params.js";
2
+
3
+ /**
4
+ * Substitute `:param` placeholders in a route pattern with values from
5
+ * `params`. Two-pass: optional params (`:name?`) first so absent values
6
+ * collapse cleanly, then required params (throws on missing). Constraint
7
+ * syntax (`:name(en|gb)`) is stripped from the result. Trailing-slash
8
+ * patterns like `/blog/` are preserved unless an optional segment was
9
+ * actually omitted.
10
+ *
11
+ * Shared by `ctx.reverse()` (server), `createReverse()` (typed runtime
12
+ * helper), and `useReverse()` (client hook). The behavior must stay
13
+ * identical across all three call sites.
14
+ */
15
+ export function substitutePatternParams(
16
+ pattern: string,
17
+ params: Record<string, string | undefined>,
18
+ routeName: string,
19
+ ): string {
20
+ let result = pattern;
21
+ let hadOmittedOptional = false;
22
+
23
+ result = result.replace(
24
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
25
+ (_match, key) => {
26
+ const value = params[key as string];
27
+ // The matcher omits absent optional params (so `value` is `undefined`
28
+ // here), but caller-supplied params or `getParams()` shapes may still
29
+ // pass `""` explicitly. Treat both as the absent form.
30
+ if (value === undefined || value === "") {
31
+ hadOmittedOptional = true;
32
+ return "";
33
+ }
34
+ return encodePathSegment(value);
35
+ },
36
+ );
37
+
38
+ result = result.replace(
39
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
40
+ (_match, key) => {
41
+ const value = params[key as string];
42
+ if (value === undefined) {
43
+ throw new Error(`Missing param "${key}" for route "${routeName}"`);
44
+ }
45
+ return encodePathSegment(value);
46
+ },
47
+ );
48
+
49
+ if (hadOmittedOptional) {
50
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
51
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
52
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
53
+ }
54
+
55
+ return result;
56
+ }