@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc

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 (285) hide show
  1. package/README.md +196 -43
  2. package/dist/bin/rango.js +277 -99
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2779 -1064
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +57 -11
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +155 -6
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +249 -17
  21. package/skills/loader/SKILL.md +273 -53
  22. package/skills/middleware/SKILL.md +49 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +197 -6
  28. package/skills/prerender/SKILL.md +123 -100
  29. package/skills/rango/SKILL.md +242 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +88 -4
  33. package/skills/router-setup/SKILL.md +90 -5
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +716 -0
  37. package/skills/typesafety/SKILL.md +329 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +117 -0
  42. package/src/__internal.ts +1 -1
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +91 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/navigation-bridge.ts +102 -16
  50. package/src/browser/navigation-client.ts +164 -59
  51. package/src/browser/navigation-store.ts +75 -17
  52. package/src/browser/navigation-transaction.ts +21 -37
  53. package/src/browser/partial-update.ts +139 -38
  54. package/src/browser/prefetch/cache.ts +175 -15
  55. package/src/browser/prefetch/fetch.ts +180 -33
  56. package/src/browser/prefetch/queue.ts +123 -20
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +53 -13
  59. package/src/browser/react/Link.tsx +81 -9
  60. package/src/browser/react/NavigationProvider.tsx +110 -33
  61. package/src/browser/react/context.ts +7 -2
  62. package/src/browser/react/filter-segment-order.ts +51 -7
  63. package/src/browser/react/index.ts +3 -0
  64. package/src/browser/react/location-state-shared.ts +175 -4
  65. package/src/browser/react/location-state.ts +39 -13
  66. package/src/browser/react/use-handle.ts +23 -64
  67. package/src/browser/react/use-navigation.ts +22 -2
  68. package/src/browser/react/use-params.ts +20 -8
  69. package/src/browser/react/use-reverse.ts +106 -0
  70. package/src/browser/react/use-router.ts +43 -10
  71. package/src/browser/react/use-segments.ts +11 -8
  72. package/src/browser/response-adapter.ts +25 -0
  73. package/src/browser/rsc-router.tsx +191 -74
  74. package/src/browser/scroll-restoration.ts +41 -14
  75. package/src/browser/segment-reconciler.ts +36 -9
  76. package/src/browser/segment-structure-assert.ts +2 -2
  77. package/src/browser/server-action-bridge.ts +31 -36
  78. package/src/browser/types.ts +57 -5
  79. package/src/build/collect-fallback-refs.ts +107 -0
  80. package/src/build/generate-manifest.ts +65 -40
  81. package/src/build/generate-route-types.ts +5 -0
  82. package/src/build/index.ts +2 -0
  83. package/src/build/route-trie.ts +52 -25
  84. package/src/build/route-types/codegen.ts +4 -4
  85. package/src/build/route-types/include-resolution.ts +9 -2
  86. package/src/build/route-types/per-module-writer.ts +7 -4
  87. package/src/build/route-types/router-processing.ts +278 -88
  88. package/src/build/route-types/scan-filter.ts +9 -2
  89. package/src/build/route-types/source-scan.ts +118 -0
  90. package/src/build/runtime-discovery.ts +9 -20
  91. package/src/cache/cache-runtime.ts +15 -11
  92. package/src/cache/cache-scope.ts +76 -49
  93. package/src/cache/cf/cf-cache-store.ts +501 -18
  94. package/src/cache/cf/index.ts +5 -1
  95. package/src/cache/document-cache.ts +17 -7
  96. package/src/cache/index.ts +1 -0
  97. package/src/cache/taint.ts +55 -0
  98. package/src/client.rsc.tsx +3 -0
  99. package/src/client.tsx +94 -238
  100. package/src/context-var.ts +72 -2
  101. package/src/debug.ts +2 -2
  102. package/src/decode-loader-results.ts +36 -0
  103. package/src/errors.ts +30 -1
  104. package/src/handle.ts +65 -12
  105. package/src/host/index.ts +2 -2
  106. package/src/host/router.ts +129 -57
  107. package/src/host/types.ts +31 -2
  108. package/src/host/utils.ts +1 -1
  109. package/src/href-client.ts +140 -20
  110. package/src/index.rsc.ts +12 -5
  111. package/src/index.ts +61 -11
  112. package/src/loader-store.ts +500 -0
  113. package/src/loader.rsc.ts +2 -5
  114. package/src/loader.ts +3 -10
  115. package/src/missing-id-error.ts +68 -0
  116. package/src/outlet-context.ts +1 -1
  117. package/src/prerender/store.ts +5 -4
  118. package/src/prerender.ts +141 -80
  119. package/src/response-utils.ts +37 -0
  120. package/src/reverse.ts +65 -15
  121. package/src/route-content-wrapper.tsx +6 -28
  122. package/src/route-definition/dsl-helpers.ts +435 -260
  123. package/src/route-definition/helper-factories.ts +29 -139
  124. package/src/route-definition/helpers-types.ts +110 -34
  125. package/src/route-definition/index.ts +3 -0
  126. package/src/route-definition/redirect.ts +11 -3
  127. package/src/route-definition/resolve-handler-use.ts +155 -0
  128. package/src/route-definition/use-item-types.ts +32 -0
  129. package/src/route-map-builder.ts +7 -1
  130. package/src/route-types.ts +37 -41
  131. package/src/router/basename.ts +14 -0
  132. package/src/router/content-negotiation.ts +113 -1
  133. package/src/router/error-handling.ts +1 -1
  134. package/src/router/find-match.ts +4 -2
  135. package/src/router/handler-context.ts +77 -38
  136. package/src/router/intercept-resolution.ts +15 -22
  137. package/src/router/lazy-includes.ts +12 -9
  138. package/src/router/loader-resolution.ts +174 -22
  139. package/src/router/logging.ts +5 -2
  140. package/src/router/manifest.ts +31 -16
  141. package/src/router/match-api.ts +128 -192
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/background-revalidation.ts +30 -2
  144. package/src/router/match-middleware/cache-lookup.ts +136 -106
  145. package/src/router/match-middleware/cache-store.ts +54 -10
  146. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  147. package/src/router/match-middleware/segment-resolution.ts +61 -5
  148. package/src/router/match-result.ts +125 -10
  149. package/src/router/metrics.ts +7 -2
  150. package/src/router/middleware-types.ts +21 -34
  151. package/src/router/middleware.ts +103 -90
  152. package/src/router/navigation-snapshot.ts +182 -0
  153. package/src/router/pattern-matching.ts +101 -17
  154. package/src/router/prerender-match.ts +110 -10
  155. package/src/router/preview-match.ts +32 -102
  156. package/src/router/request-classification.ts +286 -0
  157. package/src/router/revalidation.ts +58 -2
  158. package/src/router/route-snapshot.ts +245 -0
  159. package/src/router/router-context.ts +6 -1
  160. package/src/router/router-interfaces.ts +77 -28
  161. package/src/router/router-options.ts +76 -11
  162. package/src/router/router-registry.ts +2 -5
  163. package/src/router/segment-resolution/fresh.ts +223 -24
  164. package/src/router/segment-resolution/helpers.ts +29 -24
  165. package/src/router/segment-resolution/loader-cache.ts +1 -0
  166. package/src/router/segment-resolution/revalidation.ts +466 -285
  167. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  168. package/src/router/segment-wrappers.ts +2 -0
  169. package/src/router/substitute-pattern-params.ts +56 -0
  170. package/src/router/telemetry.ts +99 -0
  171. package/src/router/trie-matching.ts +18 -13
  172. package/src/router/types.ts +9 -0
  173. package/src/router/url-params.ts +49 -0
  174. package/src/router.ts +91 -23
  175. package/src/rsc/handler-context.ts +2 -2
  176. package/src/rsc/handler.ts +440 -381
  177. package/src/rsc/helpers.ts +91 -43
  178. package/src/rsc/index.ts +1 -1
  179. package/src/rsc/loader-fetch.ts +23 -3
  180. package/src/rsc/manifest-init.ts +5 -1
  181. package/src/rsc/origin-guard.ts +28 -10
  182. package/src/rsc/progressive-enhancement.ts +18 -2
  183. package/src/rsc/response-route-handler.ts +46 -53
  184. package/src/rsc/rsc-rendering.ts +41 -48
  185. package/src/rsc/runtime-warnings.ts +9 -10
  186. package/src/rsc/server-action.ts +25 -37
  187. package/src/rsc/ssr-setup.ts +18 -2
  188. package/src/rsc/types.ts +17 -3
  189. package/src/search-params.ts +4 -4
  190. package/src/segment-content-promise.ts +67 -0
  191. package/src/segment-loader-promise.ts +122 -0
  192. package/src/segment-system.tsx +219 -67
  193. package/src/serialize.ts +243 -0
  194. package/src/server/context.ts +277 -61
  195. package/src/server/cookie-store.ts +28 -4
  196. package/src/server/handle-store.ts +19 -0
  197. package/src/server/loader-registry.ts +9 -8
  198. package/src/server/request-context.ts +204 -60
  199. package/src/ssr/index.tsx +9 -1
  200. package/src/static-handler.ts +19 -7
  201. package/src/testing/cache-status.ts +166 -0
  202. package/src/testing/collect-handle.ts +63 -0
  203. package/src/testing/dispatch.ts +440 -0
  204. package/src/testing/dom.entry.ts +22 -0
  205. package/src/testing/e2e/fixture.ts +154 -0
  206. package/src/testing/e2e/index.ts +149 -0
  207. package/src/testing/e2e/matchers.ts +51 -0
  208. package/src/testing/e2e/page-helpers.ts +272 -0
  209. package/src/testing/e2e/parity.ts +306 -0
  210. package/src/testing/e2e/server.ts +183 -0
  211. package/src/testing/flight-matchers.ts +104 -0
  212. package/src/testing/flight-runtime.d.ts +21 -0
  213. package/src/testing/flight.entry.ts +22 -0
  214. package/src/testing/flight.ts +182 -0
  215. package/src/testing/generated-routes.ts +223 -0
  216. package/src/testing/index.ts +106 -0
  217. package/src/testing/internal/context.ts +255 -0
  218. package/src/testing/render-route.tsx +565 -0
  219. package/src/testing/run-loader.ts +296 -0
  220. package/src/testing/run-middleware.ts +179 -0
  221. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  222. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  223. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  224. package/src/testing/vitest-stubs/version.ts +5 -0
  225. package/src/testing/vitest.ts +183 -0
  226. package/src/types/cache-types.ts +4 -4
  227. package/src/types/global-namespace.ts +39 -26
  228. package/src/types/handler-context.ts +194 -72
  229. package/src/types/index.ts +1 -0
  230. package/src/types/loader-types.ts +41 -15
  231. package/src/types/request-scope.ts +126 -0
  232. package/src/types/route-entry.ts +19 -1
  233. package/src/types/segments.ts +37 -1
  234. package/src/urls/include-helper.ts +34 -67
  235. package/src/urls/index.ts +0 -3
  236. package/src/urls/path-helper-types.ts +50 -9
  237. package/src/urls/path-helper.ts +63 -63
  238. package/src/urls/pattern-types.ts +48 -19
  239. package/src/urls/response-types.ts +25 -22
  240. package/src/urls/type-extraction.ts +26 -116
  241. package/src/urls/urls-function.ts +1 -5
  242. package/src/use-loader.tsx +487 -44
  243. package/src/vite/debug.ts +185 -0
  244. package/src/vite/discovery/bundle-postprocess.ts +34 -37
  245. package/src/vite/discovery/discover-routers.ts +105 -51
  246. package/src/vite/discovery/discovery-errors.ts +194 -0
  247. package/src/vite/discovery/gate-state.ts +171 -0
  248. package/src/vite/discovery/prerender-collection.ts +188 -93
  249. package/src/vite/discovery/route-types-writer.ts +40 -84
  250. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  251. package/src/vite/discovery/state.ts +46 -6
  252. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  253. package/src/vite/index.ts +6 -0
  254. package/src/vite/plugin-types.ts +111 -72
  255. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  256. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  257. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  258. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  259. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  260. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  261. package/src/vite/plugins/expose-action-id.ts +55 -33
  262. package/src/vite/plugins/expose-id-utils.ts +24 -8
  263. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  264. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  265. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  266. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  267. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  268. package/src/vite/plugins/performance-tracks.ts +92 -0
  269. package/src/vite/plugins/refresh-cmd.ts +88 -26
  270. package/src/vite/plugins/use-cache-transform.ts +65 -50
  271. package/src/vite/plugins/version-injector.ts +39 -23
  272. package/src/vite/plugins/version-plugin.ts +72 -3
  273. package/src/vite/plugins/virtual-entries.ts +2 -2
  274. package/src/vite/rango.ts +265 -226
  275. package/src/vite/router-discovery.ts +920 -137
  276. package/src/vite/utils/ast-handler-extract.ts +15 -15
  277. package/src/vite/utils/banner.ts +4 -4
  278. package/src/vite/utils/bundle-analysis.ts +4 -2
  279. package/src/vite/utils/client-chunks.ts +190 -0
  280. package/src/vite/utils/forward-user-plugins.ts +193 -0
  281. package/src/vite/utils/manifest-utils.ts +21 -5
  282. package/src/vite/utils/package-resolution.ts +41 -1
  283. package/src/vite/utils/prerender-utils.ts +38 -5
  284. package/src/vite/utils/shared-utils.ts +109 -27
  285. package/src/browser/action-response-classifier.ts +0 -99
@@ -19,6 +19,8 @@ import type {
19
19
  ResolvedRouteMap,
20
20
  } from "./route-config.js";
21
21
  import type { LoaderDefinition } from "./loader-types.js";
22
+ import type { UseItems, HandlerUseItem } from "../route-types.js";
23
+ import type { RequestScope } from "./request-scope.js";
22
24
 
23
25
  // Re-export MiddlewareFn for internal/advanced use
24
26
  export type { MiddlewareFn } from "../router/middleware.js";
@@ -41,7 +43,7 @@ export type { MiddlewareFn } from "../router/middleware.js";
41
43
  */
42
44
  export type ScopedRouteMap<
43
45
  TPrefix extends string,
44
- TMap = RSCRouter.GeneratedRouteMap,
46
+ TMap = Rango.GeneratedRouteMap,
45
47
  > = {
46
48
  [K in keyof TMap as K extends `${TPrefix}.${infer Rest}`
47
49
  ? Rest
@@ -135,7 +137,7 @@ export type Handler<
135
137
  | Record<string, any> = {},
136
138
  TRouteMap extends {} = DefaultHandlerRouteMap,
137
139
  TEnv = DefaultEnv,
138
- > = (
140
+ > = ((
139
141
  ctx: HandlerContext<
140
142
  T extends `.${infer Local}`
141
143
  ? Local extends keyof TRouteMap
@@ -160,7 +162,10 @@ export type Handler<
160
162
  : ExtractSearchFromEntry<DefaultHandlerRouteMap, T>,
161
163
  TRouteMap extends DefaultHandlerRouteMap ? never : TRouteMap
162
164
  >,
163
- ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>;
165
+ ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>) & {
166
+ /** Composable default DSL items merged when the handler is mounted. */
167
+ use?: () => UseItems<HandlerUseItem>;
168
+ };
164
169
 
165
170
  /**
166
171
  * Context passed to handlers (Hono-inspired type-safe context)
@@ -170,7 +175,7 @@ export type Handler<
170
175
  * - Cleaned route URL (`url`, `searchParams`, `pathname` — no `_rsc*` params)
171
176
  * - Original request (`request` — raw transport URL, headers, method, body)
172
177
  * - Platform bindings (env.DB, env.KV, env.SECRETS)
173
- * - Middleware variables (var.user, var.permissions)
178
+ * - Middleware variables (`get("user")`, `get("permissions")`)
174
179
  * - Getter/setter for variables (get('user'), set('user', ...))
175
180
  *
176
181
  * @example
@@ -178,8 +183,7 @@ export type Handler<
178
183
  * const handler = (ctx: HandlerContext<{ slug: string }, AppEnv>) => {
179
184
  * ctx.params.slug // Route param (string)
180
185
  * ctx.env.DB // Binding (D1Database)
181
- * ctx.var.user // Variable (User | undefined)
182
- * ctx.get('user') // Alternative getter
186
+ * ctx.get('user') // Variable (User | undefined)
183
187
  * ctx.set('user', {...}) // Setter
184
188
  * ctx.url // Clean URL (no _rsc* params)
185
189
  * ctx.searchParams // Clean params (no _rsc* params)
@@ -192,7 +196,7 @@ export type HandlerContext<
192
196
  TEnv = DefaultEnv,
193
197
  TSearch extends SearchSchema = {},
194
198
  TRouteMap = never,
195
- > = {
199
+ > = RequestScope<TEnv> & {
196
200
  /**
197
201
  * Route parameters extracted from the URL pattern.
198
202
  * Type-safe when using Handler<"/path/:param"> or Handler<{ param: string }>.
@@ -207,51 +211,19 @@ export type HandlerContext<
207
211
  */
208
212
  build: boolean;
209
213
  /**
210
- * The original incoming Request object (transport URL intact).
211
- * Use `ctx.url` / `ctx.searchParams` for application logic those have
212
- * internal `_rsc*` params stripped. `ctx.request` preserves the raw URL
213
- * for cases where you need original headers, method, or body.
214
- */
215
- request: Request;
216
- /**
217
- * Query parameters from the URL (system params like `_rsc*` are filtered).
218
- * Always a standard URLSearchParams instance.
214
+ * True when running in Vite dev mode, false during production build or
215
+ * live request rendering. Use this to branch on runtime mode without
216
+ * changing build semantics (e.g., skip expensive operations in dev).
219
217
  */
220
- searchParams: URLSearchParams;
218
+ dev: boolean;
221
219
  /**
222
220
  * Typed search parameters parsed from URL query string via the route's
223
221
  * search schema. Empty object when no schema is defined.
224
222
  */
225
223
  search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
226
- /**
227
- * The pathname portion of the request URL.
228
- */
229
- pathname: string;
230
- /**
231
- * The full URL object (with internal `_rsc*` params stripped).
232
- * Use this for application logic — routing, link generation, display.
233
- */
234
- url: URL;
235
- /**
236
- * The original request URL with all parameters intact, including
237
- * internal `_rsc*` transport params. Use `ctx.url` for application
238
- * logic — this is only needed for advanced cases like debugging
239
- * or custom cache keying.
240
- */
241
- originalUrl: URL;
242
- /**
243
- * Platform bindings (DB, KV, secrets, etc.).
244
- * Access resources like `ctx.env.DB`, `ctx.env.KV`.
245
- */
246
- env: TEnv;
247
- /**
248
- * Middleware-injected variables.
249
- * Access values like `ctx.var.user`, `ctx.var.permissions`.
250
- */
251
- var: DefaultVars;
252
224
  /**
253
225
  * Type-safe getter for middleware variables.
254
- * Alternative to `ctx.var.key` with better autocomplete.
226
+ * Preferred way to read middleware-injected variables.
255
227
  *
256
228
  * @example
257
229
  * ```typescript
@@ -272,8 +244,16 @@ export type HandlerContext<
272
244
  * ```
273
245
  */
274
246
  set: {
275
- <T>(contextVar: ContextVar<T>, value: T): void;
276
- } & (<K extends keyof DefaultVars>(key: K, value: DefaultVars[K]) => void);
247
+ <T>(
248
+ contextVar: ContextVar<T>,
249
+ value: T,
250
+ options?: { cache?: boolean },
251
+ ): void;
252
+ } & (<K extends keyof DefaultVars>(
253
+ key: K,
254
+ value: DefaultVars[K],
255
+ options?: { cache?: boolean },
256
+ ) => void);
277
257
  /**
278
258
  * Response headers. Headers set here are merged into the final response.
279
259
  *
@@ -289,8 +269,15 @@ export type HandlerContext<
289
269
  /**
290
270
  * Access loader data or push handle data.
291
271
  *
272
+ * Available in route handlers, layout handlers, middleware, server actions,
273
+ * and server components rendered within the request context.
274
+ *
292
275
  * For loaders: Returns a promise that resolves to the loader data.
293
276
  * Loaders are executed in parallel and memoized per request.
277
+ * Prefer DSL `loader()` + client `useLoader()` over `ctx.use(Loader)` —
278
+ * DSL loaders are always fresh and cache-safe. Use `ctx.use(Loader)` only
279
+ * when you need loader data in the handler itself (e.g., to set context
280
+ * variables or make routing decisions).
294
281
  *
295
282
  * For handles: Returns a push function to add data for this segment.
296
283
  * Handle data accumulates across all matched route segments.
@@ -298,10 +285,11 @@ export type HandlerContext<
298
285
  *
299
286
  * @example
300
287
  * ```typescript
301
- * // Loader usage
302
- * route("cart", async (ctx) => {
303
- * const cart = await ctx.use(CartLoader);
304
- * return <CartPage cart={cart} />;
288
+ * // Loader escape hatch — use when handler needs the data directly
289
+ * route("product", async (ctx) => {
290
+ * const { product } = await ctx.use(ProductLoader);
291
+ * ctx.set(Product, product); // make available to children
292
+ * return <ProductPage />;
305
293
  * });
306
294
  *
307
295
  * // Handle usage - direct value
@@ -432,6 +420,8 @@ export type InternalHandlerContext<
432
420
  > = HandlerContext<TParams, TEnv, TSearch> & {
433
421
  /** @internal Stub response for collecting headers/cookies. */
434
422
  res: Response;
423
+ /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
424
+ _variables: Record<string, any>;
435
425
  /** Prerender-only control flow helper, attached when the runtime context supports it. */
436
426
  passthrough?: () => unknown;
437
427
  /** Current segment ID for handle data attribution. */
@@ -481,13 +471,16 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
481
471
  * **Return Types:**
482
472
  * - `boolean` - Hard decision: immediately returns this value (short-circuits)
483
473
  * - `{ defaultShouldRevalidate: boolean }` - Soft decision: updates suggestion for next revalidator
474
+ * - `void` / `null` / `undefined` - Defer to the current suggestion (no opinion); the
475
+ * loop continues to the next revalidator without changing the running default
484
476
  *
485
477
  * **Execution Flow:**
486
478
  * 1. Start with built-in `defaultShouldRevalidate` (true if params changed)
487
479
  * 2. Execute global revalidators first, then route-specific
488
480
  * 3. Hard decision (boolean): stop immediately and use that value
489
481
  * 4. Soft decision (object): update suggestion and continue to next revalidator
490
- * 5. If all return soft decisions: use the final suggestion
482
+ * 5. Defer (`void` / `null` / `undefined`): leave suggestion unchanged and continue
483
+ * 6. If no hard decision was returned: use the final running suggestion
491
484
  *
492
485
  * @param args.currentParams - Previous route params (generic by default, can be narrowed)
493
486
  * @param args.currentUrl - Previous URL
@@ -499,7 +492,8 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
499
492
  * @param args.formData - Form data from action (future support)
500
493
  * @param args.formMethod - HTTP method from action (future support)
501
494
  *
502
- * @returns Hard decision (boolean) or soft suggestion (object)
495
+ * @returns Hard decision (boolean), soft suggestion (object), or defer
496
+ * (`void` / `null` / `undefined`) to keep the running suggestion as-is.
503
497
  *
504
498
  * @example
505
499
  * ```typescript
@@ -519,31 +513,159 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
519
513
  * })
520
514
  * ```
521
515
  */
516
+ /**
517
+ * A reference to a server action, used by `isAction()` in a revalidate predicate.
518
+ *
519
+ * Either a directly imported action (`import { addToCart }`) or a namespace
520
+ * import of an action module (`import * as CartActions`). Matching resolves the
521
+ * action's build-injected id (`path#export`) — the same identity the router uses
522
+ * for `actionId` — so a renamed or moved action breaks at compile time instead
523
+ * of silently failing to match.
524
+ */
525
+ export type ActionRef =
526
+ | ((...args: never[]) => unknown)
527
+ | Record<string, unknown>;
528
+
529
+ /**
530
+ * Revalidation function called during client-side navigation to decide whether
531
+ * a segment (layout, route, parallel slot, or loader) should be re-rendered.
532
+ *
533
+ * Return `true` to re-render, `false` to skip (keep client's current version),
534
+ * `{ defaultShouldRevalidate: boolean }` to update the running suggestion for
535
+ * downstream revalidators, or nothing (`void` / `null` / `undefined`) to defer
536
+ * to the current suggestion without changing it.
537
+ *
538
+ * @example
539
+ * ```ts
540
+ * // Re-render when a cart action happened or the browser signals staleness;
541
+ * // defer otherwise (|| undefined) so the segment default still applies
542
+ * revalidate(({ actionId, stale }) =>
543
+ * actionId?.includes("cart") || stale || undefined
544
+ * )
545
+ *
546
+ * // Always re-render when params change (default behavior made explicit)
547
+ * revalidate(({ defaultShouldRevalidate }) => defaultShouldRevalidate)
548
+ * ```
549
+ */
522
550
  export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
551
+ /** Route params from the page being navigated away from. */
523
552
  currentParams: TParams;
553
+ /** Full URL of the page being navigated away from. */
524
554
  currentUrl: URL;
555
+ /** Route params for the navigation target. */
525
556
  nextParams: TParams;
557
+ /** Full URL of the navigation target. */
526
558
  nextUrl: URL;
559
+ /**
560
+ * The router's default revalidation decision for this segment.
561
+ * `true` when params changed or the segment is new to the client.
562
+ * Return this when you want default behavior plus your own conditions.
563
+ */
527
564
  defaultShouldRevalidate: boolean;
565
+ /** Full handler context — access to `ctx.use()`, `ctx.env`, `ctx.params`, etc. */
528
566
  context: HandlerContext<TParams, TEnv>;
529
- // Segment metadata (which segment is being evaluated):
530
- segmentType: "layout" | "route" | "parallel";
531
- layoutName?: string; // Layout name (e.g., "root", "shop", "auth") - only for layouts
532
- slotName?: string; // Slot name (e.g., "@sidebar", "@modal") - only for parallels
533
- // Action context (populated when revalidation triggered by server action):
534
- actionId?: string; // Action identifier (e.g., "src/actions.ts#addToCart")
535
- actionUrl?: URL; // URL where action was executed
536
- actionResult?: any; // Return value from action execution
537
- formData?: FormData; // FormData from action request
538
- method?: string; // Request method: 'GET' for navigation, 'POST' for actions
539
- routeName?: DefaultRouteName; // Route name of the navigation target (alias for toRouteName)
540
- // Named-route identity for both ends of a navigation transition.
541
- // Undefined for unnamed internal routes (those without a `name` option).
542
- fromRouteName?: DefaultRouteName; // Route name being navigated away from
543
- toRouteName?: DefaultRouteName; // Route name being navigated to
544
- // Stale cache revalidation (SWR pattern):
545
- stale?: boolean; // True if this is a stale cache revalidation request
546
- }) => boolean | { defaultShouldRevalidate: boolean };
567
+
568
+ // ── Segment metadata (which segment is being evaluated) ──────────────
569
+
570
+ /**
571
+ * The type of segment being revalidated. `"loader"` is passed to revalidate
572
+ * functions attached to a `loader(Fn, () => [revalidate(...)])` registration.
573
+ */
574
+ segmentType: "layout" | "route" | "parallel" | "loader";
575
+ /** Layout name (e.g., `"root"`, `"shop"`, `"auth"`). Only set for layout segments. */
576
+ layoutName?: string;
577
+ /** Slot name (e.g., `"@sidebar"`, `"@modal"`). Only set for parallel segments. */
578
+ slotName?: string;
579
+
580
+ // ── Action context (populated when revalidation is triggered by a server action) ──
581
+
582
+ /**
583
+ * Identifier of the server action that triggered revalidation.
584
+ * `undefined` during normal navigation (no action involved).
585
+ *
586
+ * Format: `"src/<path>#<exportName>"` — the file path is the source path
587
+ * relative to the project root, followed by `#` and the exported function name.
588
+ *
589
+ * This is stable and can be used for path-based matching to revalidate
590
+ * when any action in a module or directory fires. Prefer `|| undefined`
591
+ * (defer to the segment default / downstream revalidators) over `?? false`
592
+ * (hard short-circuit that suppresses the default and ends the chain):
593
+ *
594
+ * @example
595
+ * ```ts
596
+ * // Match a specific action
597
+ * revalidate(({ actionId }) => actionId === "src/actions/cart.ts#addToCart" || undefined)
598
+ *
599
+ * // Match any action in the cart module
600
+ * revalidate(({ actionId }) => actionId?.includes("cart") || undefined)
601
+ *
602
+ * // Match any action under src/apps/store/actions/
603
+ * revalidate(({ actionId }) => actionId?.startsWith("src/apps/store/actions/") || undefined)
604
+ * ```
605
+ */
606
+ actionId?: string;
607
+ /**
608
+ * Typed, rename-safe action matching. Returns `true` when the action that
609
+ * triggered this revalidation is one of the given references — or, for a
610
+ * namespace import (`import * as CartActions`), any export of that module —
611
+ * and `false` otherwise (including plain navigation with no action).
612
+ *
613
+ * Prefer this over hand-written `actionId` substring matches: it resolves the
614
+ * action's stable `path#export` id from the imported reference, so a rename is
615
+ * a type error in one place instead of silent drift across consumers. It
616
+ * resolves the reference the same way the action boundary derives `actionId`
617
+ * (`$id ?? $$id`), so it matches in both dev and production.
618
+ *
619
+ * Returns a raw boolean, so for the common "revalidate on match, else defer"
620
+ * intent combine with `|| undefined`:
621
+ *
622
+ * @example
623
+ * ```ts
624
+ * import { addToCart, removeFromCart } from "./actions/cart";
625
+ * import * as CartActions from "./actions/cart";
626
+ *
627
+ * revalidate((ctx) => ctx.isAction(addToCart) || undefined); // one action
628
+ * revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
629
+ * revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any in the module
630
+ * ```
631
+ */
632
+ isAction: (...actions: ActionRef[]) => boolean;
633
+ /** URL where the action was executed (the page the user was on when they triggered the action). */
634
+ actionUrl?: URL;
635
+ /** Return value from the action execution. Can be used to conditionally revalidate based on the action's outcome. */
636
+ actionResult?: any;
637
+ /** FormData from the action request body. Only set for form-based actions (not inline `"use server"` actions). */
638
+ formData?: FormData;
639
+ /** HTTP method: `"GET"` for navigation, `"POST"` for server actions. */
640
+ method?: string;
641
+
642
+ // ── Route identity ───────────────────────────────────────────────────
643
+
644
+ /** Route name of the navigation target. Alias for `toRouteName`. */
645
+ routeName?: DefaultRouteName;
646
+ /**
647
+ * Route name being navigated away from.
648
+ * `undefined` for unnamed internal routes (those without a `name` option).
649
+ */
650
+ fromRouteName?: DefaultRouteName;
651
+ /**
652
+ * Route name being navigated to.
653
+ * `undefined` for unnamed internal routes (those without a `name` option).
654
+ */
655
+ toRouteName?: DefaultRouteName;
656
+
657
+ // ── Staleness signal ─────────────────────────────────────────────────
658
+
659
+ /**
660
+ * `true` when the browser signals that data may be stale — typically because
661
+ * a server action was executed in this or another tab (`_rsc_stale` header).
662
+ *
663
+ * This is NOT segment cache staleness (loaders are never segment-cached).
664
+ * Use this to decide whether loader data should be re-fetched after an
665
+ * action that may have mutated backend state.
666
+ */
667
+ stale?: boolean;
668
+ }) => boolean | { defaultShouldRevalidate: boolean } | null | void;
547
669
 
548
670
  // MiddlewareFn is imported from "../router/middleware.js" and re-exported
549
671
 
@@ -648,7 +770,7 @@ export type Revalidate<
648
770
  * Middleware function with typed params and environment
649
771
  *
650
772
  * @template TParams - Params object (defaults to generic)
651
- * @template TEnv - Environment type (defaults to global RSCRouter.Env)
773
+ * @template TEnv - Environment type (defaults to global Rango.Env)
652
774
  *
653
775
  * Note: Middleware cannot directly use route names for params typing because
654
776
  * middleware is defined during router setup, before RegisteredRoutes is populated.
@@ -656,7 +778,7 @@ export type Revalidate<
656
778
  *
657
779
  * @example
658
780
  * ```typescript
659
- * // Basic middleware (uses global RSCRouter.Env via module augmentation)
781
+ * // Basic middleware (uses global Rango.Env via module augmentation)
660
782
  * const middleware: Middleware = async (ctx, next) => {
661
783
  * ctx.set("user", { id: "123" }); // Type-safe!
662
784
  * await next();
@@ -42,6 +42,7 @@ export type {
42
42
  GenericParams,
43
43
  RevalidateParams,
44
44
  ShouldRevalidateFn,
45
+ ActionRef,
45
46
  RouteKeys,
46
47
  ExtractRouteParams,
47
48
  HandlersForRouteMap,
@@ -1,12 +1,15 @@
1
1
  import type { ContextVar } from "../context-var.js";
2
+ import type { Handle } from "../handle.js";
2
3
  import type { MiddlewareFn } from "../router/middleware.js";
3
4
  import type { ScopedReverseFunction } from "../reverse.js";
4
5
  import type { SearchSchema, ResolveSearchSchema } from "../search-params.js";
6
+ import type { UseItems, LoaderUseItem } from "../route-types.js";
5
7
  import type {
6
8
  DefaultEnv,
7
9
  DefaultReverseRouteMap,
8
10
  DefaultVars,
9
11
  } from "./global-namespace.js";
12
+ import type { RequestScope } from "./request-scope.js";
10
13
 
11
14
  /**
12
15
  * Context passed to loader functions during execution
@@ -38,7 +41,7 @@ export type LoaderContext<
38
41
  TEnv = DefaultEnv,
39
42
  TBody = unknown,
40
43
  TSearch extends SearchSchema = {},
41
- > = {
44
+ > = RequestScope<TEnv> & {
42
45
  params: TParams;
43
46
  /**
44
47
  * Route params extracted from the URL pattern match (server-side only).
@@ -47,22 +50,43 @@ export type LoaderContext<
47
50
  * resource scoping.
48
51
  */
49
52
  routeParams: Record<string, string>;
50
- request: Request;
51
- searchParams: URLSearchParams;
52
53
  search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
53
- pathname: string;
54
- url: URL;
55
- env: TEnv;
56
- var: DefaultVars;
57
54
  get: {
58
55
  <T>(contextVar: ContextVar<T>): T | undefined;
59
56
  } & (<K extends keyof DefaultVars>(key: K) => DefaultVars[K]);
60
57
  /**
61
- * Access another loader's data (returns promise since loaders run in parallel)
58
+ * Access another loader's data, or read handle data after rendered().
59
+ *
60
+ * For loaders: returns a promise (loaders run in parallel).
61
+ * For handles: returns collected data (only after `await ctx.rendered()`).
62
+ */
63
+ use: {
64
+ <T, TLoaderParams = any>(
65
+ loader: LoaderDefinition<T, TLoaderParams>,
66
+ ): Promise<T>;
67
+ <TData, TAccumulated = TData[]>(
68
+ handle: Handle<TData, TAccumulated>,
69
+ ): TAccumulated;
70
+ };
71
+ /**
72
+ * **Experimental.** Wait for all non-loader segments to settle.
73
+ *
74
+ * After the returned promise resolves, handle data is available via
75
+ * `ctx.use(handle)`. Only supported in DSL loaders on non-streaming
76
+ * trees (no `loading()`). Throws if called from a handler-invoked
77
+ * loader or when the tree uses streaming.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const PricesLoader = createLoader(async (ctx) => {
82
+ * "use server";
83
+ * await ctx.rendered();
84
+ * const products = ctx.use(Products); // reads handle data
85
+ * return pricing.getLive(products.map(p => p.id));
86
+ * });
87
+ * ```
62
88
  */
63
- use: <T, TLoaderParams = any>(
64
- loader: LoaderDefinition<T, TLoaderParams>,
65
- ) => Promise<T>;
89
+ rendered: () => Promise<void>;
66
90
  /**
67
91
  * HTTP method (GET, POST, PUT, PATCH, DELETE)
68
92
  * Available when loader is called via load({ method: "POST", ... })
@@ -166,11 +190,11 @@ export type LoadOptions =
166
190
  * return await db.products.findBySlug(slug);
167
191
  * });
168
192
  *
169
- * // Server usage
170
- * const cart = ctx.use(CartLoader);
193
+ * // Client usage (preferred — cache-safe, always fresh)
194
+ * const { data } = useLoader(CartLoader);
171
195
  *
172
- * // Client usage (fn is stripped, only name remains)
173
- * const cart = useLoader(CartLoader);
196
+ * // Server escape hatch (handler needs data directly)
197
+ * const cart = await ctx.use(CartLoader);
174
198
  * ```
175
199
  */
176
200
  export type LoaderDefinition<
@@ -180,4 +204,6 @@ export type LoaderDefinition<
180
204
  __brand: "loader";
181
205
  $$id: string; // Injected by Vite plugin (exposeInternalIds) - unique identifier
182
206
  fn?: LoaderFn<T, TParams, any>; // Optional - server-side only, stored in registry for RSC
207
+ /** Composable default DSL items merged when the loader is mounted. */
208
+ use?: () => UseItems<LoaderUseItem>;
183
209
  };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * RequestScope: the fields every user-facing context shares.
3
+ *
4
+ * A handler, middleware, loader, response handler, and the ALS-bound
5
+ * RequestContext are all different phases of the same request, and they
6
+ * all carry the same set of request-scoped capabilities: the raw Request,
7
+ * the parsed URL pair (`url` is cleaned of internal `_rsc*` params,
8
+ * `originalUrl` retains them), pathname/searchParams, platform bindings
9
+ * (`env`), and two escape hatches for work that outlives the response
10
+ * (`waitUntil`) or needs the raw Cloudflare runtime object
11
+ * (`executionContext`).
12
+ *
13
+ * Each public context type intersects `RequestScope<TEnv>` with its own
14
+ * phase-specific fields (e.g. `params`/`reverse` on HandlerContext,
15
+ * `headers`/`header()` on MiddlewareContext). That keeps platform surface
16
+ * in one place and lets the next runtime escape hatch we need land in
17
+ * one file instead of four.
18
+ */
19
+
20
+ import type { DefaultEnv } from "./global-namespace.js";
21
+
22
+ /**
23
+ * Minimal subset of Cloudflare Workers' ExecutionContext that the router
24
+ * uses. Defined locally so the package does not depend on
25
+ * `@cloudflare/workers-types`. Consumers that want the full type can cast.
26
+ *
27
+ * On non-Cloudflare runtimes (Node, dev server, tests), this is undefined
28
+ * — portable apps should prefer `ctx.waitUntil(...)`, which degrades
29
+ * gracefully. `ctx.executionContext` is the escape hatch for libraries
30
+ * (MCP, Durable Object routing, etc.) that type their arguments as the
31
+ * raw ExecutionContext.
32
+ */
33
+ export interface ExecutionContext {
34
+ waitUntil(promise: Promise<any>): void;
35
+ passThroughOnException(): void;
36
+ }
37
+
38
+ /**
39
+ * Fallback `waitUntil` body used when no Cloudflare `ExecutionContext`
40
+ * is available (Node, dev, tests). Runs the work fire-and-forget and
41
+ * logs errors so they don't silently swallow.
42
+ *
43
+ * Exported so every `waitUntil` call site degrades identically instead
44
+ * of inventing its own fallback policy.
45
+ */
46
+ export function fireAndForgetWaitUntil(fn: () => Promise<void>): void {
47
+ fn().catch((err) =>
48
+ console.error("[waitUntil] Background task failed:", err),
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Fields present on every user-facing request context.
54
+ *
55
+ * @template TEnv - Platform bindings type (Cloudflare env, etc.).
56
+ */
57
+ export interface RequestScope<TEnv = DefaultEnv> {
58
+ /**
59
+ * The original incoming Request object (transport URL intact).
60
+ * Use `url` / `searchParams` for application logic — those have
61
+ * internal `_rsc*` params stripped. `request` preserves the raw URL
62
+ * when you need original headers, method, or body.
63
+ */
64
+ request: Request;
65
+
66
+ /**
67
+ * The request URL with internal `_rsc*` transport params stripped.
68
+ * Use this for routing, link generation, and display.
69
+ */
70
+ url: URL;
71
+
72
+ /**
73
+ * The original request URL with all parameters intact, including
74
+ * internal `_rsc*` transport params. Use `url` for application logic
75
+ * — this is only needed for advanced cases like debugging or custom
76
+ * cache keying.
77
+ */
78
+ originalUrl: URL;
79
+
80
+ /** URL pathname (same as `url.pathname`). */
81
+ pathname: string;
82
+
83
+ /**
84
+ * Query parameters from the URL (system params like `_rsc*` are
85
+ * filtered). Always a standard `URLSearchParams` instance.
86
+ */
87
+ searchParams: URLSearchParams;
88
+
89
+ /**
90
+ * Platform bindings (DB, KV, secrets, etc.). On Cloudflare Workers
91
+ * these are the `env` object passed to the Worker's `fetch()` handler.
92
+ */
93
+ env: TEnv;
94
+
95
+ /**
96
+ * Schedule work to run after the response is sent.
97
+ * On Cloudflare Workers, delegates to `executionContext.waitUntil()`.
98
+ * On Node / dev / tests, runs as fire-and-forget with error logging.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * ctx.waitUntil(async () => {
103
+ * await cacheStore.set(key, data, ttl);
104
+ * });
105
+ * ```
106
+ */
107
+ waitUntil(fn: () => Promise<void>): void;
108
+
109
+ /**
110
+ * Raw Cloudflare Workers `ExecutionContext`, when running on a
111
+ * Cloudflare-compatible runtime. Undefined elsewhere.
112
+ *
113
+ * Escape hatch for libraries that type their arguments as
114
+ * `ExecutionContext` (MCP `fetch`, `routeAgentRequest`, etc.).
115
+ * For the common "do work after the response" case, prefer
116
+ * `ctx.waitUntil(...)` — it is platform-neutral.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * path.any("/mcp", (ctx) =>
121
+ * emailMcp.fetch(ctx.request, ctx.env, ctx.executionContext!),
122
+ * );
123
+ * ```
124
+ */
125
+ executionContext?: ExecutionContext;
126
+ }