@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7d061845

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 (252) 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 +2138 -841
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +68 -21
  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 +3 -1
  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 +26 -4
  18. package/skills/layout/SKILL.md +6 -7
  19. package/skills/links/SKILL.md +247 -17
  20. package/skills/loader/SKILL.md +219 -9
  21. package/skills/middleware/SKILL.md +15 -9
  22. package/skills/migrate-nextjs/SKILL.md +4 -2
  23. package/skills/migrate-react-router/SKILL.md +5 -0
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +12 -6
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +242 -24
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +66 -9
  31. package/skills/route/SKILL.md +33 -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 +816 -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 +65 -9
  47. package/src/browser/navigation-client.ts +45 -25
  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 +52 -26
  51. package/src/browser/prefetch/cache.ts +124 -26
  52. package/src/browser/prefetch/fetch.ts +114 -38
  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 +18 -13
  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-structure-assert.ts +2 -2
  71. package/src/browser/server-action-bridge.ts +23 -30
  72. package/src/browser/types.ts +21 -0
  73. package/src/build/collect-fallback-refs.ts +107 -0
  74. package/src/build/generate-manifest.ts +60 -35
  75. package/src/build/generate-route-types.ts +2 -0
  76. package/src/build/index.ts +2 -0
  77. package/src/build/route-trie.ts +2 -1
  78. package/src/build/route-types/codegen.ts +4 -4
  79. package/src/build/route-types/include-resolution.ts +1 -1
  80. package/src/build/route-types/per-module-writer.ts +7 -4
  81. package/src/build/route-types/router-processing.ts +55 -14
  82. package/src/build/route-types/scan-filter.ts +1 -1
  83. package/src/build/route-types/source-scan.ts +118 -0
  84. package/src/build/runtime-discovery.ts +9 -20
  85. package/src/cache/cache-scope.ts +28 -42
  86. package/src/cache/cf/cf-cache-store.ts +54 -13
  87. package/src/client.rsc.tsx +3 -0
  88. package/src/client.tsx +10 -8
  89. package/src/context-var.ts +5 -5
  90. package/src/decode-loader-results.ts +36 -0
  91. package/src/errors.ts +30 -1
  92. package/src/handle.ts +26 -13
  93. package/src/host/index.ts +2 -2
  94. package/src/host/router.ts +129 -57
  95. package/src/host/types.ts +31 -2
  96. package/src/host/utils.ts +1 -1
  97. package/src/href-client.ts +140 -20
  98. package/src/index.rsc.ts +9 -4
  99. package/src/index.ts +16 -6
  100. package/src/loader-store.ts +500 -0
  101. package/src/loader.rsc.ts +21 -6
  102. package/src/loader.ts +3 -10
  103. package/src/missing-id-error.ts +68 -0
  104. package/src/outlet-context.ts +1 -1
  105. package/src/prerender.ts +4 -4
  106. package/src/response-utils.ts +37 -0
  107. package/src/reverse.ts +65 -39
  108. package/src/route-content-wrapper.tsx +6 -28
  109. package/src/route-definition/dsl-helpers.ts +253 -265
  110. package/src/route-definition/helper-factories.ts +29 -139
  111. package/src/route-definition/helpers-types.ts +43 -15
  112. package/src/route-definition/resolve-handler-use.ts +6 -0
  113. package/src/route-definition/use-item-types.ts +32 -0
  114. package/src/route-types.ts +19 -41
  115. package/src/router/basename.ts +14 -0
  116. package/src/router/content-negotiation.ts +15 -2
  117. package/src/router/error-handling.ts +1 -1
  118. package/src/router/handler-context.ts +21 -41
  119. package/src/router/intercept-resolution.ts +4 -18
  120. package/src/router/lazy-includes.ts +3 -3
  121. package/src/router/loader-resolution.ts +19 -2
  122. package/src/router/match-api.ts +4 -3
  123. package/src/router/match-handlers.ts +63 -20
  124. package/src/router/match-middleware/cache-lookup.ts +44 -91
  125. package/src/router/match-middleware/cache-store.ts +3 -2
  126. package/src/router/match-result.ts +53 -32
  127. package/src/router/metrics.ts +1 -1
  128. package/src/router/middleware-types.ts +15 -26
  129. package/src/router/middleware.ts +99 -84
  130. package/src/router/pattern-matching.ts +101 -17
  131. package/src/router/prerender-match.ts +1 -1
  132. package/src/router/preview-match.ts +3 -1
  133. package/src/router/request-classification.ts +4 -28
  134. package/src/router/revalidation.ts +58 -2
  135. package/src/router/router-interfaces.ts +45 -28
  136. package/src/router/router-options.ts +40 -1
  137. package/src/router/router-registry.ts +2 -5
  138. package/src/router/segment-resolution/fresh.ts +27 -6
  139. package/src/router/segment-resolution/revalidation.ts +147 -106
  140. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  141. package/src/router/substitute-pattern-params.ts +56 -0
  142. package/src/router/telemetry.ts +99 -0
  143. package/src/router/trie-matching.ts +18 -13
  144. package/src/router/types.ts +8 -0
  145. package/src/router/url-params.ts +49 -0
  146. package/src/router.ts +38 -23
  147. package/src/rsc/handler-context.ts +2 -2
  148. package/src/rsc/handler.ts +28 -69
  149. package/src/rsc/helpers.ts +91 -43
  150. package/src/rsc/index.ts +1 -1
  151. package/src/rsc/origin-guard.ts +28 -10
  152. package/src/rsc/progressive-enhancement.ts +4 -0
  153. package/src/rsc/response-route-handler.ts +46 -53
  154. package/src/rsc/rsc-rendering.ts +35 -51
  155. package/src/rsc/runtime-warnings.ts +9 -10
  156. package/src/rsc/server-action.ts +17 -37
  157. package/src/rsc/ssr-setup.ts +16 -0
  158. package/src/rsc/types.ts +8 -2
  159. package/src/search-params.ts +4 -4
  160. package/src/segment-system.tsx +122 -56
  161. package/src/serialize.ts +243 -0
  162. package/src/server/context.ts +118 -51
  163. package/src/server/cookie-store.ts +28 -4
  164. package/src/server/request-context.ts +20 -42
  165. package/src/ssr/index.tsx +5 -1
  166. package/src/static-handler.ts +1 -1
  167. package/src/testing/cache-status.ts +166 -0
  168. package/src/testing/collect-handle.ts +63 -0
  169. package/src/testing/dispatch.ts +440 -0
  170. package/src/testing/dom.entry.ts +22 -0
  171. package/src/testing/e2e/fixture.ts +154 -0
  172. package/src/testing/e2e/index.ts +149 -0
  173. package/src/testing/e2e/matchers.ts +51 -0
  174. package/src/testing/e2e/page-helpers.ts +272 -0
  175. package/src/testing/e2e/parity.ts +306 -0
  176. package/src/testing/e2e/server.ts +183 -0
  177. package/src/testing/flight-matchers.ts +104 -0
  178. package/src/testing/flight-runtime.d.ts +57 -0
  179. package/src/testing/flight-tree.ts +332 -0
  180. package/src/testing/flight.entry.ts +46 -0
  181. package/src/testing/flight.ts +224 -0
  182. package/src/testing/generated-routes.ts +223 -0
  183. package/src/testing/index.ts +106 -0
  184. package/src/testing/internal/context.ts +304 -0
  185. package/src/testing/internal/flight-client-globals.ts +30 -0
  186. package/src/testing/internal/seed-vars.ts +42 -0
  187. package/src/testing/render-handler.ts +267 -0
  188. package/src/testing/render-route.tsx +565 -0
  189. package/src/testing/run-loader.ts +341 -0
  190. package/src/testing/run-middleware.ts +188 -0
  191. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  192. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  193. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  194. package/src/testing/vitest-stubs/version.ts +5 -0
  195. package/src/testing/vitest.ts +270 -0
  196. package/src/types/global-namespace.ts +39 -26
  197. package/src/types/handler-context.ts +68 -50
  198. package/src/types/index.ts +1 -0
  199. package/src/types/loader-types.ts +5 -6
  200. package/src/types/request-scope.ts +126 -0
  201. package/src/types/segments.ts +35 -1
  202. package/src/urls/include-helper.ts +10 -53
  203. package/src/urls/index.ts +0 -3
  204. package/src/urls/path-helper-types.ts +11 -3
  205. package/src/urls/path-helper.ts +17 -52
  206. package/src/urls/pattern-types.ts +36 -19
  207. package/src/urls/response-types.ts +22 -29
  208. package/src/urls/type-extraction.ts +26 -116
  209. package/src/urls/urls-function.ts +1 -5
  210. package/src/use-loader.tsx +413 -42
  211. package/src/vite/debug.ts +185 -0
  212. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  213. package/src/vite/discovery/discover-routers.ts +101 -51
  214. package/src/vite/discovery/discovery-errors.ts +194 -0
  215. package/src/vite/discovery/gate-state.ts +171 -0
  216. package/src/vite/discovery/prerender-collection.ts +67 -26
  217. package/src/vite/discovery/route-types-writer.ts +40 -84
  218. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  219. package/src/vite/discovery/state.ts +33 -0
  220. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  221. package/src/vite/index.ts +2 -0
  222. package/src/vite/plugin-types.ts +67 -0
  223. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  224. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  225. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  226. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  227. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  228. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  229. package/src/vite/plugins/expose-action-id.ts +54 -30
  230. package/src/vite/plugins/expose-id-utils.ts +12 -8
  231. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  232. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  233. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  234. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  235. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  236. package/src/vite/plugins/performance-tracks.ts +29 -25
  237. package/src/vite/plugins/use-cache-transform.ts +65 -50
  238. package/src/vite/plugins/version-injector.ts +39 -23
  239. package/src/vite/plugins/version-plugin.ts +59 -2
  240. package/src/vite/plugins/virtual-entries.ts +2 -2
  241. package/src/vite/rango.ts +116 -29
  242. package/src/vite/router-discovery.ts +750 -100
  243. package/src/vite/utils/ast-handler-extract.ts +15 -15
  244. package/src/vite/utils/banner.ts +1 -1
  245. package/src/vite/utils/bundle-analysis.ts +4 -2
  246. package/src/vite/utils/client-chunks.ts +190 -0
  247. package/src/vite/utils/forward-user-plugins.ts +193 -0
  248. package/src/vite/utils/manifest-utils.ts +21 -5
  249. package/src/vite/utils/package-resolution.ts +41 -1
  250. package/src/vite/utils/prerender-utils.ts +5 -4
  251. package/src/vite/utils/shared-utils.ts +107 -26
  252. package/src/browser/action-response-classifier.ts +0 -99
@@ -11,122 +11,40 @@ import {
11
11
  when,
12
12
  errorBoundary,
13
13
  notFoundBoundary,
14
- loaderFn,
15
- loadingFn,
16
- transitionFn,
17
- routeFn,
14
+ route,
15
+ loader,
16
+ loading,
17
+ transition,
18
18
  } from "./dsl-helpers.js";
19
19
  import RootLayout from "../server/root-layout";
20
20
  import { invariant } from "../errors";
21
21
 
22
- /*
23
- * Create revalidate helper
24
- */
25
- const createRevalidateHelper = <TEnv>(): RouteHelpers<
26
- any,
27
- TEnv
28
- >["revalidate"] => {
29
- return revalidate as RouteHelpers<any, TEnv>["revalidate"];
30
- };
31
-
32
- /**
33
- * Create errorBoundary helper
34
- */
35
- const createErrorBoundaryHelper = <TEnv>(): RouteHelpers<
36
- any,
37
- TEnv
38
- >["errorBoundary"] => {
39
- return errorBoundary as RouteHelpers<any, TEnv>["errorBoundary"];
40
- };
41
-
42
- /**
43
- * Create notFoundBoundary helper
44
- */
45
- const createNotFoundBoundaryHelper = <TEnv>(): RouteHelpers<
46
- any,
47
- TEnv
48
- >["notFoundBoundary"] => {
49
- return notFoundBoundary as RouteHelpers<any, TEnv>["notFoundBoundary"];
50
- };
51
-
52
22
  /**
53
- * Create middleware helper
23
+ * Assemble the RouteHelpers object. The helpers are the DSL functions
24
+ * themselves; the single cast erases the phantom generics (and the extra
25
+ * `route` key) that the per-router RouteHelpers<T, TEnv> type carries but the
26
+ * runtime functions do not.
54
27
  */
55
- const createMiddlewareHelper = <TEnv>(): RouteHelpers<
56
- any,
28
+ function buildRouteHelpers<T extends RouteDefinition, TEnv>(): RouteHelpers<
29
+ T,
57
30
  TEnv
58
- >["middleware"] => {
59
- return middleware as RouteHelpers<any, TEnv>["middleware"];
60
- };
61
-
62
- /**
63
- * Create parallel helper
64
- */
65
- const createParallelHelper = <TEnv>(): RouteHelpers<any, TEnv>["parallel"] => {
66
- return parallel as RouteHelpers<any, TEnv>["parallel"];
67
- };
68
-
69
- /**
70
- * Create intercept helper
71
- */
72
- const createInterceptHelper = <
73
- const T extends RouteDefinition,
74
- TEnv,
75
- >(): RouteHelpers<T, TEnv>["intercept"] => {
76
- return intercept as RouteHelpers<T, TEnv>["intercept"];
77
- };
78
-
79
- /**
80
- * Create loader helper
81
- */
82
- const createLoaderHelper = <TEnv>(): RouteHelpers<any, TEnv>["loader"] => {
83
- return loaderFn as RouteHelpers<any, TEnv>["loader"];
84
- };
85
-
86
- /**
87
- * Create loading helper
88
- */
89
- const createLoadingHelper = (): RouteHelpers<any, any>["loading"] => {
90
- return loadingFn;
91
- };
92
-
93
- /**
94
- * Create route helper
95
- */
96
- const createRouteHelper = <
97
- const T extends RouteDefinition,
98
- TEnv,
99
- >(): RouteHelpers<T, TEnv>["route"] => {
100
- return routeFn as unknown as RouteHelpers<T, TEnv>["route"];
101
- };
102
-
103
- /**
104
- * Create layout helper
105
- */
106
- const createLayoutHelper = <TEnv>(): RouteHelpers<any, TEnv>["layout"] => {
107
- return layout as RouteHelpers<any, TEnv>["layout"];
108
- };
109
-
110
- /**
111
- * Create when helper for intercept conditions
112
- */
113
- const createWhenHelper = (): RouteHelpers<any, any>["when"] => {
114
- return when;
115
- };
116
-
117
- /**
118
- * Create cache helper for cache configuration
119
- */
120
- const createCacheHelper = (): RouteHelpers<any, any>["cache"] => {
121
- return cache;
122
- };
123
-
124
- /**
125
- * Create transition helper
126
- */
127
- const createTransitionHelper = (): RouteHelpers<any, any>["transition"] => {
128
- return transitionFn as RouteHelpers<any, any>["transition"];
129
- };
31
+ > {
32
+ return {
33
+ route,
34
+ layout,
35
+ parallel,
36
+ intercept,
37
+ middleware,
38
+ revalidate,
39
+ loader,
40
+ loading,
41
+ errorBoundary,
42
+ notFoundBoundary,
43
+ when,
44
+ cache,
45
+ transition,
46
+ } as unknown as RouteHelpers<T, TEnv>;
47
+ }
130
48
 
131
49
  /**
132
50
  * Branded type for route handlers that carries the route type info.
@@ -152,21 +70,7 @@ export function map<const T extends RouteDefinition, TEnv = DefaultEnv>(
152
70
  "map() expects a builder function as its argument",
153
71
  );
154
72
  // Create helpers
155
- const helpers: RouteHelpers<T, TEnv> = {
156
- route: createRouteHelper<T, TEnv>(),
157
- layout: createLayoutHelper<TEnv>(),
158
- parallel: createParallelHelper<TEnv>(),
159
- intercept: createInterceptHelper<T, TEnv>(),
160
- middleware: createMiddlewareHelper<TEnv>(),
161
- revalidate: createRevalidateHelper<TEnv>(),
162
- loader: createLoaderHelper<TEnv>(),
163
- loading: createLoadingHelper(),
164
- errorBoundary: createErrorBoundaryHelper<TEnv>(),
165
- notFoundBoundary: createNotFoundBoundaryHelper<TEnv>(),
166
- when: createWhenHelper(),
167
- cache: createCacheHelper(),
168
- transition: createTransitionHelper(),
169
- };
73
+ const helpers = buildRouteHelpers<T, TEnv>();
170
74
 
171
75
  return [layout(RootLayout, () => builder(helpers))].flat(3);
172
76
  };
@@ -182,19 +86,5 @@ export function createRouteHelpers<
182
86
  T extends RouteDefinition,
183
87
  TEnv,
184
88
  >(): RouteHelpers<T, TEnv> {
185
- return {
186
- route: createRouteHelper<T, TEnv>(),
187
- layout: createLayoutHelper<TEnv>(),
188
- parallel: createParallelHelper<TEnv>(),
189
- intercept: createInterceptHelper<T, TEnv>(),
190
- middleware: createMiddlewareHelper<TEnv>(),
191
- revalidate: createRevalidateHelper<TEnv>(),
192
- loader: createLoaderHelper<TEnv>(),
193
- loading: createLoadingHelper(),
194
- errorBoundary: createErrorBoundaryHelper<TEnv>(),
195
- notFoundBoundary: createNotFoundBoundaryHelper<TEnv>(),
196
- when: createWhenHelper(),
197
- cache: createCacheHelper(),
198
- transition: createTransitionHelper(),
199
- };
89
+ return buildRouteHelpers<T, TEnv>();
200
90
  }
@@ -198,10 +198,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
198
198
  use?: () => UseItems<InterceptUseItem>,
199
199
  ): InterceptItem;
200
200
  // Global: unprefixed, params inferred from global route map
201
- <K extends keyof RSCRouter.GeneratedRouteMap & string>(
201
+ <K extends keyof Rango.GeneratedRouteMap & string>(
202
202
  slotName: `@${string}`,
203
203
  routeName: K,
204
- handler: ReactNode | Handler<K, RSCRouter.GeneratedRouteMap, TEnv>,
204
+ handler: ReactNode | Handler<K, Rango.GeneratedRouteMap, TEnv>,
205
205
  use?: () => UseItems<InterceptUseItem>,
206
206
  ): InterceptItem;
207
207
  };
@@ -250,8 +250,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
250
250
  * )
251
251
  *
252
252
  * // Revalidate after specific actions (actionId format: "path/to/file.ts#exportName")
253
+ * // Use `|| undefined` (defer), not `?? false` (hard short-circuit), so the
254
+ * // chain and the segment default still apply when there is no match.
253
255
  * revalidate(({ actionId }) =>
254
- * actionId?.includes("Cart") ?? false
256
+ * actionId?.includes("Cart") || undefined
255
257
  * )
256
258
  *
257
259
  * // Soft decision (suggest but allow override)
@@ -259,7 +261,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
259
261
  * ({ defaultShouldRevalidate: true })
260
262
  * )
261
263
  * ```
262
- * @param fn - Function that returns boolean (hard) or { defaultShouldRevalidate } (soft)
264
+ * @param fn - Function returning either:
265
+ * - `boolean` (hard decision — short-circuits the chain),
266
+ * - `{ defaultShouldRevalidate: boolean }` (soft — updates the suggestion
267
+ * for downstream revalidators),
268
+ * - or nothing / `null` / `undefined` (defer — leaves the suggestion
269
+ * unchanged and continues to the next revalidator).
263
270
  */
264
271
  revalidate: (fn: ShouldRevalidateFn<any, TEnv>) => RevalidateItem;
265
272
  /**
@@ -269,7 +276,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
269
276
  *
270
277
  * // With loader-specific revalidation (match by file or export name)
271
278
  * loader(CartLoader, () => [
272
- * revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
279
+ * revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
273
280
  * ])
274
281
  *
275
282
  * // Consume in client components with useLoader()
@@ -442,10 +449,30 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
442
449
  ): CacheItem;
443
450
  };
444
451
  /**
445
- * Attach a ViewTransition boundary to the current segment or a group of routes
446
- *
447
- * Wraps segment content with React's `<ViewTransition>` component.
448
- * Only takes effect when React experimental is used (no-op on stable React).
452
+ * Opt a route (or group of routes) into transition-driven navigation.
453
+ *
454
+ * `transition()` does two independent things, and you choose how far to go:
455
+ * 1. startTransition (ALL React versions): the navigation commit is driven
456
+ * through React's startTransition, so a same-route nav (same route,
457
+ * different params, e.g. /product/1 -> /product/2) holds the previous
458
+ * content while the new loader resolves instead of flashing the route's
459
+ * loading() skeleton (see segment-system.tsx inTransitionScope). This is
460
+ * also the precondition for any view-transition animation.
461
+ * 2. <ViewTransition> (experimental React only): the segment content is also
462
+ * wrapped in React's <ViewTransition>, so the held swap cross-fades/morphs.
463
+ * Layered on by default; pass { viewTransition: false } to keep #1 without
464
+ * the router boundary (and place your own <ViewTransition> instead).
465
+ *
466
+ * A view transition cannot fire without a startTransition, so the meaningful
467
+ * choices are (see skills/view-transitions for the full matrix):
468
+ * - no transition() -> neither (remount + skeleton)
469
+ * - transition({ viewTransition: false }) -> startTransition only (hold)
470
+ * - transition({}) / transition({ enter… }) -> startTransition + ViewTransition
471
+ *
472
+ * Precedence: a bare transition({}) inherits createRouter({ viewTransition })
473
+ * (default "auto"); an explicit per-route `viewTransition` always wins. So
474
+ * transition({}) is startTransition + ViewTransition under the default and
475
+ * startTransition only when the router sets viewTransition: false.
449
476
  *
450
477
  * ```typescript
451
478
  * // Attach to a single route
@@ -459,13 +486,14 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
459
486
  * path("/about", AboutPage),
460
487
  * ])
461
488
  *
462
- * // Direction-aware transitions
463
- * transition({
464
- * enter: { "navigation": "slide-right", "navigation-back": "slide-left" },
465
- * exit: { "navigation": "slide-left", "navigation-back": "slide-right" },
466
- * })
489
+ * // Hold content + drive view transitions, but place no router boundary:
490
+ * path("/product/:id", ProductPage, { name: "product" }, () => [
491
+ * transition({ viewTransition: false }),
492
+ * ])
467
493
  * ```
468
- * @param config - ViewTransition configuration (enter, exit, update, share, default, name)
494
+ * @param config - ViewTransition configuration (enter, exit, update, share,
495
+ * default, name) plus `viewTransition: "auto" | false` to toggle the router
496
+ * boundary (createRouter({ viewTransition }) sets the app-wide default)
469
497
  * @param children - Optional callback returning child routes to wrap
470
498
  */
471
499
  transition: {
@@ -21,6 +21,10 @@ export function resolveHandlerUse(handler: unknown): (() => any[]) | undefined {
21
21
  if (isStaticHandler(handler)) {
22
22
  return (handler as any).use;
23
23
  }
24
+ // Loader definitions from createLoader() — branded objects with optional .use
25
+ if (typeof handler === "object" && (handler as any).__brand === "loader") {
26
+ return (handler as any).use;
27
+ }
24
28
  // Plain handler function
25
29
  if (typeof handler === "function") {
26
30
  return (handler as any).use;
@@ -99,6 +103,8 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
99
103
  "when",
100
104
  "transition",
101
105
  ]),
106
+ // LoaderUseItem — only revalidate + cache can attach to a loader entry
107
+ loader: new Set(["revalidate", "cache"]),
102
108
  };
103
109
 
104
110
  /**
@@ -0,0 +1,32 @@
1
+ import type { AllUseItems, WhenItem } from "../route-types.js";
2
+
3
+ /**
4
+ * The set of valid use-item `type` discriminants — the single runtime source of
5
+ * truth for "is this a well-formed use item?" shape validation.
6
+ *
7
+ * Declared via a `Record<...>` so that adding a member to the union without
8
+ * updating this map is a compile error. `when` is included because when() items
9
+ * are valid inside intercept() even though WhenItem is not part of AllUseItems
10
+ * (it lives only in InterceptUseItem). This is shape validation only; per-mount-
11
+ * site rules remain the narrower hand-written tables in resolve-handler-use.ts.
12
+ */
13
+ const USE_ITEM_TYPES: Record<AllUseItems["type"] | WhenItem["type"], true> = {
14
+ layout: true,
15
+ route: true,
16
+ middleware: true,
17
+ revalidate: true,
18
+ parallel: true,
19
+ intercept: true,
20
+ loader: true,
21
+ loading: true,
22
+ errorBoundary: true,
23
+ notFoundBoundary: true,
24
+ when: true,
25
+ cache: true,
26
+ transition: true,
27
+ include: true,
28
+ };
29
+
30
+ export const ALL_USE_ITEM_TYPES: ReadonlySet<string> = new Set(
31
+ Object.keys(USE_ITEM_TYPES),
32
+ );
@@ -5,47 +5,43 @@
5
5
  */
6
6
 
7
7
  /**
8
- * Branded return types for route helpers
8
+ * Brand for UrlPatterns nominal typing (see pattern-types.ts). The route-item
9
+ * types below are discriminated by their `type` literal, so they carry no brand.
9
10
  */
10
- export declare const LayoutBrand: unique symbol;
11
- export declare const RouteBrand: unique symbol;
12
- export declare const ParallelBrand: unique symbol;
13
- export declare const InterceptBrand: unique symbol;
14
- export declare const MiddlewareBrand: unique symbol;
15
- export declare const RevalidateBrand: unique symbol;
16
- export declare const LoaderBrand: unique symbol;
17
- export declare const LoadingBrand: unique symbol;
18
- export declare const ErrorBoundaryBrand: unique symbol;
19
- export declare const NotFoundBoundaryBrand: unique symbol;
20
- export declare const WhenBrand: unique symbol;
21
- export declare const CacheBrand: unique symbol;
22
- export declare const TransitionBrand: unique symbol;
23
- export declare const IncludeBrand: unique symbol;
24
11
  export declare const UrlPatternsBrand: unique symbol;
25
12
 
26
13
  export type LayoutItem = {
27
14
  name: string;
28
15
  type: "layout";
29
16
  uses?: AllUseItems[];
30
- [LayoutBrand]: void;
31
17
  };
32
18
 
33
19
  /**
34
- * Typed layout item that carries child routes as phantom type
35
- * Used for type inference in urls() API
20
+ * Phantom inference fields attached to wrapper items (layout/cache/transition)
21
+ * so the urls() type extractor can read their child routes/responses. The fields
22
+ * never exist at runtime.
36
23
  */
37
- export type TypedLayoutItem<
24
+ type WithChildren<
25
+ TBase,
38
26
  TChildRoutes extends Record<string, any> = Record<string, string>,
39
27
  TChildResponses extends Record<string, unknown> = Record<string, unknown>,
40
- > = LayoutItem & {
28
+ > = TBase & {
41
29
  readonly __childRoutes?: TChildRoutes;
42
30
  readonly __childResponses?: TChildResponses;
43
31
  };
32
+
33
+ /**
34
+ * Typed layout item that carries child routes as phantom type
35
+ * Used for type inference in urls() API
36
+ */
37
+ export type TypedLayoutItem<
38
+ TChildRoutes extends Record<string, any> = Record<string, string>,
39
+ TChildResponses extends Record<string, unknown> = Record<string, unknown>,
40
+ > = WithChildren<LayoutItem, TChildRoutes, TChildResponses>;
44
41
  export type RouteItem = {
45
42
  name: string;
46
43
  type: "route";
47
44
  uses?: AllUseItems[];
48
- [RouteBrand]: void;
49
45
  };
50
46
 
51
47
  /**
@@ -67,64 +63,53 @@ export type ParallelItem = {
67
63
  name: string;
68
64
  type: "parallel";
69
65
  uses?: ParallelUseItem[];
70
- [ParallelBrand]: void;
71
66
  };
72
67
  export type InterceptItem = {
73
68
  name: string;
74
69
  type: "intercept";
75
70
  uses?: InterceptUseItem[];
76
- [InterceptBrand]: void;
77
71
  };
78
72
  export type LoaderItem = {
79
73
  name: string;
80
74
  type: "loader";
81
75
  uses?: LoaderUseItem[];
82
- [LoaderBrand]: void;
83
76
  };
84
77
  export type MiddlewareItem = {
85
78
  name: string;
86
79
  type: "middleware";
87
80
  uses?: AllUseItems[];
88
- [MiddlewareBrand]: void;
89
81
  };
90
82
  export type RevalidateItem = {
91
83
  name: string;
92
84
  type: "revalidate";
93
85
  uses?: AllUseItems[];
94
- [RevalidateBrand]: void;
95
86
  };
96
87
  export type LoadingItem = {
97
88
  name: string;
98
89
  type: "loading";
99
- [LoadingBrand]: void;
100
90
  };
101
91
  export type ErrorBoundaryItem = {
102
92
  name: string;
103
93
  type: "errorBoundary";
104
94
  uses?: AllUseItems[];
105
- [ErrorBoundaryBrand]: void;
106
95
  };
107
96
  export type NotFoundBoundaryItem = {
108
97
  name: string;
109
98
  type: "notFoundBoundary";
110
99
  uses?: AllUseItems[];
111
- [NotFoundBoundaryBrand]: void;
112
100
  };
113
101
  export type WhenItem = {
114
102
  name: string;
115
103
  type: "when";
116
- [WhenBrand]: void;
117
104
  };
118
105
  export type CacheItem = {
119
106
  name: string;
120
107
  type: "cache";
121
108
  uses?: AllUseItems[];
122
- [CacheBrand]: void;
123
109
  };
124
110
  export type TransitionItem = {
125
111
  name: string;
126
112
  type: "transition";
127
- [TransitionBrand]: void;
128
113
  };
129
114
 
130
115
  /**
@@ -134,10 +119,7 @@ export type TransitionItem = {
134
119
  export type TypedTransitionItem<
135
120
  TChildRoutes extends Record<string, any> = Record<string, string>,
136
121
  TChildResponses extends Record<string, unknown> = Record<string, unknown>,
137
- > = TransitionItem & {
138
- readonly __childRoutes?: TChildRoutes;
139
- readonly __childResponses?: TChildResponses;
140
- };
122
+ > = WithChildren<TransitionItem, TChildRoutes, TChildResponses>;
141
123
 
142
124
  /**
143
125
  * Typed cache item that carries child routes as phantom type
@@ -146,10 +128,7 @@ export type TypedTransitionItem<
146
128
  export type TypedCacheItem<
147
129
  TChildRoutes extends Record<string, any> = Record<string, string>,
148
130
  TChildResponses extends Record<string, unknown> = Record<string, unknown>,
149
- > = CacheItem & {
150
- readonly __childRoutes?: TChildRoutes;
151
- readonly __childResponses?: TChildResponses;
152
- };
131
+ > = WithChildren<CacheItem, TChildRoutes, TChildResponses>;
153
132
 
154
133
  /**
155
134
  * Include item for URL pattern composition (used by urls() API)
@@ -184,7 +163,6 @@ export type IncludeItem = {
184
163
  */
185
164
  includeScope?: string;
186
165
  };
187
- [IncludeBrand]: void;
188
166
  };
189
167
 
190
168
  /**
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalize a router basename to its canonical form: a single leading slash,
3
+ * no trailing slash, and `undefined` for an empty or bare-"/" value.
4
+ *
5
+ * This is the single source of truth used by both createRouter() (so the RSC
6
+ * handler stores a canonical basename on the request context) and the testing
7
+ * primitives (so a consumer can pass the same un-normalized string their
8
+ * createRouter() accepts and observe the same redirect() prefixing).
9
+ */
10
+ export function normalizeBasename(basename?: string): string | undefined {
11
+ if (!basename) return undefined;
12
+ const trimmed = basename.replace(/^\/+|\/+$/g, "");
13
+ return trimmed ? "/" + trimmed : undefined;
14
+ }
@@ -135,8 +135,8 @@ export interface NegotiationResult {
135
135
  manifestEntry: EntryData;
136
136
  /** Route middleware for the winning variant */
137
137
  routeMiddleware: CollectedMiddleware[];
138
- /** Always true negotiation occurred */
139
- negotiated: true;
138
+ /** True when negotiation selected a variant; false for a plain response route. */
139
+ negotiated: boolean;
140
140
  }
141
141
 
142
142
  /**
@@ -155,6 +155,19 @@ export async function negotiateRoute(
155
155
  ): Promise<NegotiationResult | null> {
156
156
  const { matched, manifestEntry, routeMiddleware, responseType } = snapshot;
157
157
  if (!matched.negotiateVariants || matched.negotiateVariants.length === 0) {
158
+ // No variants: a plain response route still yields a result (negotiated:false)
159
+ // so callers don't re-derive it; RSC routes (no responseType/handler) -> null.
160
+ const handler =
161
+ manifestEntry.type === "route" ? manifestEntry.handler : undefined;
162
+ if (responseType && handler) {
163
+ return {
164
+ responseType,
165
+ handler: handler as Function,
166
+ manifestEntry,
167
+ routeMiddleware,
168
+ negotiated: false,
169
+ };
170
+ }
158
171
  return null;
159
172
  }
160
173
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Router Error Handling Utilities
3
3
  *
4
- * Error boundary and not-found boundary handling for RSC Router.
4
+ * Error boundary and not-found boundary handling for Rango.
5
5
  * Also includes the shared invokeOnError utility for error callback invocation.
6
6
  */
7
7
 
@@ -18,6 +18,8 @@ import { isInsideCacheScope } from "../server/context.js";
18
18
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
19
19
  import { isAutoGeneratedRouteName } from "../route-name.js";
20
20
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
21
+ import { substitutePatternParams } from "./substitute-pattern-params.js";
22
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
21
23
 
22
24
  /**
23
25
  * Strip internal _rsc* query params from a URL.
@@ -158,51 +160,14 @@ export function createReverseFunction(
158
160
  );
159
161
  }
160
162
 
161
- let result = pattern;
162
-
163
163
  // Merge current request params as defaults, explicit params override
164
164
  const effectiveParams = currentParams
165
165
  ? { ...currentParams, ...hrefParams }
166
166
  : hrefParams;
167
167
 
168
- // Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
169
- // Optional params (:param?) are omitted when not provided
170
- if (effectiveParams) {
171
- let hadOmittedOptional = false;
172
- // First pass: optional params (trailing ?)
173
- result = result.replace(
174
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
175
- (_, key) => {
176
- const value = effectiveParams[key];
177
- // Empty string is treated as omitted — the trie matcher fills
178
- // unmatched optional params with "" (not undefined), so reverse
179
- // must collapse those segments instead of leaving empty slots.
180
- if (value === undefined || value === "") {
181
- hadOmittedOptional = true;
182
- return "";
183
- }
184
- return encodeURIComponent(value);
185
- },
186
- );
187
- // Second pass: required params (no trailing ?)
188
- result = result.replace(
189
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
190
- (_, key) => {
191
- const value = effectiveParams[key];
192
- if (value === undefined) {
193
- throw new Error(`Missing param "${key}" for route "${name}"`);
194
- }
195
- return encodeURIComponent(value);
196
- },
197
- );
198
- // Clean up slashes only when an optional param was actually omitted,
199
- // so intentional trailing-slash patterns like "/blog/" are preserved.
200
- if (hadOmittedOptional) {
201
- const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
202
- result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
203
- if (hadTrailingSlash && !result.endsWith("/")) result += "/";
204
- }
205
- }
168
+ let result = effectiveParams
169
+ ? substitutePatternParams(pattern, effectiveParams, name)
170
+ : pattern;
206
171
 
207
172
  // Append search params as query string
208
173
  if (search) {
@@ -281,8 +246,12 @@ export function createHandlerContext<TEnv>(
281
246
  search: searchSchema ? resolvedSearchParams : {},
282
247
  pathname,
283
248
  url,
284
- originalUrl: new URL(request.url),
249
+ originalUrl: requestContext?.originalUrl ?? new URL(request.url),
285
250
  env: bindings,
251
+ waitUntil: requestContext
252
+ ? requestContext.waitUntil.bind(requestContext)
253
+ : fireAndForgetWaitUntil,
254
+ executionContext: requestContext?.executionContext,
286
255
  _variables: variables,
287
256
  get: ((keyOrVar: any) => {
288
257
  // Read-time guard: non-cacheable var inside cache() → throw.
@@ -387,6 +356,12 @@ export function createPrerenderContext<TEnv>(
387
356
  "Configure buildEnv in your rango() plugin options to enable build-time env access.",
388
357
  );
389
358
  },
359
+ // Build-time prerender has no live request. waitUntil is a true no-op
360
+ // (running fn() here would fire side effects during build, which is
361
+ // incorrect — these are meant to outlive the live response).
362
+ // executionContext is absent for the same reason.
363
+ waitUntil: () => {},
364
+ executionContext: undefined,
390
365
  _variables: variables,
391
366
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
392
367
  set: ((keyOrVar: any, value: any) => {
@@ -476,6 +451,11 @@ export function createStaticContext<TEnv>(
476
451
  "Configure buildEnv in your rango() plugin options to enable build-time env access.",
477
452
  );
478
453
  },
454
+ // Static() handlers have no live request. waitUntil is a true no-op
455
+ // (running fn() here would fire side effects during build, which is
456
+ // incorrect). executionContext is absent for the same reason.
457
+ waitUntil: () => {},
458
+ executionContext: undefined,
479
459
  _variables: variables,
480
460
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
481
461
  set: ((keyOrVar: any, value: any) => {