@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
@@ -187,6 +187,32 @@ export class CacheScope {
187
187
  return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
188
188
  }
189
189
 
190
+ /**
191
+ * Evaluate the cache `condition` predicate. Returns false (skip the cache
192
+ * operation) when the predicate returns false or throws; returns true when
193
+ * there is no condition or no request context to evaluate it against.
194
+ */
195
+ private conditionAllows(op: "read" | "write"): boolean {
196
+ if (this.config === false || !this.config.condition) return true;
197
+ const requestCtx = getRequestContext();
198
+ if (!requestCtx) return true;
199
+ try {
200
+ if (!this.config.condition(requestCtx)) {
201
+ debugCacheLog(
202
+ `[CacheScope] condition returned false, skipping cache ${op}`,
203
+ );
204
+ return false;
205
+ }
206
+ return true;
207
+ } catch (error) {
208
+ console.error(
209
+ `[CacheScope] condition function threw, skipping cache ${op}:`,
210
+ error,
211
+ );
212
+ return false;
213
+ }
214
+ }
215
+
190
216
  /**
191
217
  * Lookup cached segments for a route (single cache entry per request).
192
218
  * Returns { segments, shouldRevalidate } or null if cache miss.
@@ -204,27 +230,7 @@ export class CacheScope {
204
230
  shouldRevalidate: boolean;
205
231
  } | null> {
206
232
  if (!this.enabled) return null;
207
-
208
- // Evaluate condition — skip cache read when condition returns false
209
- if (this.config !== false && this.config.condition) {
210
- const requestCtx = getRequestContext();
211
- if (requestCtx) {
212
- try {
213
- if (!this.config.condition(requestCtx)) {
214
- debugCacheLog(
215
- `[CacheScope] condition returned false, skipping cache read`,
216
- );
217
- return null;
218
- }
219
- } catch (error) {
220
- console.error(
221
- `[CacheScope] condition function threw, skipping cache read:`,
222
- error,
223
- );
224
- return null;
225
- }
226
- }
227
- }
233
+ if (!this.conditionAllows("read")) return null;
228
234
 
229
235
  const store = this.getStore();
230
236
  if (!store) return null;
@@ -284,27 +290,7 @@ export class CacheScope {
284
290
  isIntercept?: boolean,
285
291
  ): Promise<void> {
286
292
  if (!this.enabled || segments.length === 0) return;
287
-
288
- // Evaluate condition — skip cache write when condition returns false
289
- if (this.config !== false && this.config.condition) {
290
- const conditionCtx = getRequestContext();
291
- if (conditionCtx) {
292
- try {
293
- if (!this.config.condition(conditionCtx)) {
294
- debugCacheLog(
295
- `[CacheScope] condition returned false, skipping cache write`,
296
- );
297
- return;
298
- }
299
- } catch (error) {
300
- console.error(
301
- `[CacheScope] condition function threw, skipping cache write:`,
302
- error,
303
- );
304
- return;
305
- }
306
- }
307
- }
293
+ if (!this.conditionAllows("write")) return;
308
294
 
309
295
  const store = this.getStore();
310
296
  if (!store) return;
@@ -56,6 +56,15 @@ export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
56
56
  /** Header storing cache status: HIT | REVALIDATING */
57
57
  export const CACHE_STATUS_HEADER = "x-edge-cache-status";
58
58
 
59
+ /**
60
+ * Header stashing the route author's original Cache-Control on L1 document
61
+ * entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
62
+ * `max-age` so the CF Cache API retains the entry across the whole SWR window;
63
+ * getResponse restores this original value before serving so the client and any
64
+ * upstream CDN see the author's intended directive, not the internal edge TTL.
65
+ */
66
+ const CACHE_ORIG_CC_HEADER = "x-edge-cache-orig-cc";
67
+
59
68
  /**
60
69
  * Maximum age in seconds for REVALIDATING status before allowing new revalidation.
61
70
  * After this period, a stale entry in REVALIDATING status will trigger revalidation again.
@@ -67,13 +76,11 @@ export const MAX_REVALIDATION_INTERVAL = 30;
67
76
  // Types
68
77
  // ============================================================================
69
78
 
70
- /**
71
- * Cloudflare Workers ExecutionContext (subset we need)
72
- */
73
- export interface ExecutionContext {
74
- waitUntil(promise: Promise<any>): void;
75
- passThroughOnException(): void;
76
- }
79
+ // Re-exported from the canonical home so cf-cache-store consumers keep
80
+ // importing `ExecutionContext` from this module without a second interface
81
+ // drifting over time.
82
+ export type { ExecutionContext } from "../../types/request-scope.js";
83
+ import type { ExecutionContext } from "../../types/request-scope.js";
77
84
 
78
85
  /**
79
86
  * Minimal Cloudflare KV Namespace interface.
@@ -184,7 +191,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
184
191
  * Cache version string override. When this changes, all cached entries are
185
192
  * effectively invalidated (new keys won't match old entries).
186
193
  *
187
- * Defaults to the auto-generated VERSION from `rsc-router:version` virtual module.
194
+ * Defaults to the auto-generated VERSION from the `@rangojs/router:version` virtual module.
188
195
  * Only set this if you need a custom versioning strategy.
189
196
  */
190
197
  version?: string;
@@ -421,7 +428,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
421
428
  }
422
429
 
423
430
  // L2: persist to KV
424
- this.kvSetSegment(key, data, staleAt, totalTtl);
431
+ this.kvSetSegment(key, data, staleAt, totalTtl, swrWindow);
425
432
  } catch (error) {
426
433
  console.error("[CFCacheStore] set failed:", error);
427
434
  }
@@ -480,7 +487,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
480
487
  const isStale = staleAt > 0 && Date.now() > staleAt;
481
488
 
482
489
  return {
483
- response,
490
+ response: this.toClientResponse(response),
484
491
  shouldRevalidate: isStale,
485
492
  };
486
493
  } catch (error) {
@@ -489,6 +496,30 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
489
496
  }
490
497
  }
491
498
 
499
+ /**
500
+ * Strip internal edge headers and restore the author's Cache-Control before a
501
+ * cached document Response is served to a client. L1 entries carry the
502
+ * internal staleness/status headers and a rewritten Cache-Control; none of
503
+ * those should reach the browser or an upstream CDN.
504
+ */
505
+ private toClientResponse(response: Response): Response {
506
+ const headers = new Headers(response.headers);
507
+ const originalCacheControl = headers.get(CACHE_ORIG_CC_HEADER);
508
+ if (originalCacheControl !== null) {
509
+ headers.set("Cache-Control", originalCacheControl);
510
+ } else {
511
+ headers.delete("Cache-Control");
512
+ }
513
+ headers.delete(CACHE_ORIG_CC_HEADER);
514
+ headers.delete(CACHE_STALE_AT_HEADER);
515
+ headers.delete(CACHE_STATUS_HEADER);
516
+ return new Response(response.body, {
517
+ status: response.status,
518
+ statusText: response.statusText,
519
+ headers,
520
+ });
521
+ }
522
+
492
523
  /**
493
524
  * Store a Response with TTL and optional SWR window (for document-level caching).
494
525
  * When KV is configured, also persists to L2.
@@ -515,8 +546,14 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
515
546
  : [null, null]
516
547
  : [response.body, null];
517
548
 
518
- // Clone and add cache headers
549
+ // Clone and add cache headers. The author's Cache-Control is stashed and
550
+ // replaced with a long max-age so the CF Cache API holds the entry across
551
+ // the SWR window; getResponse restores the original before serving.
519
552
  const headers = new Headers(response.headers);
553
+ const originalCacheControl = response.headers.get("Cache-Control");
554
+ if (originalCacheControl !== null) {
555
+ headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
556
+ }
520
557
  headers.set("Cache-Control", `public, max-age=${totalTtl}`);
521
558
  headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
522
559
 
@@ -766,13 +803,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
766
803
  data: CachedEntryData,
767
804
  staleAt: number,
768
805
  totalTtl: number,
806
+ swrWindow: number,
769
807
  ): void {
770
808
  // KV requires expirationTtl >= 60s. Skip write for short-lived entries.
771
809
  if (!this.kv || !this.waitUntil || totalTtl < 60) return;
772
810
 
773
811
  const kvKey = this.toKVKey(key);
774
- const swrWindow = totalTtl * 1000 - (staleAt - Date.now());
775
- const expiresAt = staleAt + swrWindow;
812
+ const expiresAt = staleAt + swrWindow * 1000;
776
813
 
777
814
  this.waitUntil(async () => {
778
815
  try {
@@ -939,6 +976,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
939
976
  const request = this.keyToRequest(`doc:${key}`);
940
977
 
941
978
  const headers = new Headers(envelope.hd);
979
+ const originalCacheControl = headers.get("Cache-Control");
980
+ if (originalCacheControl !== null) {
981
+ headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
982
+ }
942
983
  headers.set("Cache-Control", `public, max-age=${remainingTtl}`);
943
984
  headers.set(CACHE_STALE_AT_HEADER, String(envelope.s));
944
985
 
@@ -78,6 +78,9 @@ export {
78
78
  // Re-export useHref - it's a "use client" hook
79
79
  export { useHref } from "./browser/react/use-href.js";
80
80
 
81
+ // Re-export useReverse - it's a "use client" hook
82
+ export { useReverse } from "./browser/react/use-reverse.js";
83
+
81
84
  // Re-export useHandle - it's a "use client" hook
82
85
  export { useHandle } from "./browser/react/use-handle.js";
83
86
 
package/src/client.tsx CHANGED
@@ -214,6 +214,7 @@ export function useOutlet(): ReactNode {
214
214
  export {
215
215
  useLoader,
216
216
  useFetchLoader,
217
+ useRefreshLoaders,
217
218
  type LoadFunction,
218
219
  type UseLoaderResult,
219
220
  type UseFetchLoaderResult,
@@ -409,13 +410,10 @@ export {
409
410
  type LocationStateOptions,
410
411
  } from "./browser/react/location-state.js";
411
412
 
412
- // Type-safe href for client-side path validation
413
- export {
414
- href,
415
- type ValidPaths,
416
- type PatternToPath,
417
- type PathResponse,
418
- } from "./href-client.js";
413
+ // Type-safe href for client-side path validation. The path and response types
414
+ // are ambient as `Rango.Path` / `Rango.PathResponse` (declared in
415
+ // href-client.ts) — no import needed.
416
+ export { href, type PatternToPath } from "./href-client.js";
419
417
 
420
418
  // Response envelope types for consuming JSON response routes
421
419
  export type { ResponseEnvelope, ResponseError } from "./urls.js";
@@ -448,8 +446,12 @@ export { MountContext } from "./browser/react/mount-context.js";
448
446
  // Mount-aware href hook - auto-prefixes paths with include() mount
449
447
  export { useHref } from "./browser/react/use-href.js";
450
448
 
449
+ // Mount-aware reverse hook - resolves dot-prefixed names against an imported
450
+ // generated routes map (from a urls() module's .gen.ts).
451
+ export { useReverse } from "./browser/react/use-reverse.js";
452
+
451
453
  // Type-safe scoped reverse function for scopedReverse<typeof patterns>()
452
- export type { ScopedReverseFunction } from "./reverse.js";
454
+ export type { ScopedReverseFunction, LocalReverseFunction } from "./reverse.js";
453
455
 
454
456
  // Loader definition type - for typing loader props in client components
455
457
  export type { LoaderDefinition } from "./types.js";
@@ -12,7 +12,7 @@
12
12
  * interface PaginationData { current: number; total: number }
13
13
  * export const Pagination = createVar<PaginationData>();
14
14
  *
15
- * // Non-cacheable var — throws if set/get inside cache() or "use cache"
15
+ * // Non-cacheable var — ctx.get(User) throws inside a cache() boundary
16
16
  * export const User = createVar<UserData>({ cache: false });
17
17
  *
18
18
  * // handler
@@ -26,7 +26,7 @@
26
26
  export interface ContextVar<T> {
27
27
  readonly __brand: "context-var";
28
28
  readonly key: symbol;
29
- /** When false, the var is non-cacheable — throws inside cache() / "use cache" */
29
+ /** When false, ctx.get(var) throws inside a cache() boundary. */
30
30
  readonly cache: boolean;
31
31
  /** Phantom field to carry the type parameter. Never set at runtime. */
32
32
  readonly __type?: T;
@@ -35,9 +35,9 @@ export interface ContextVar<T> {
35
35
  export interface ContextVarOptions {
36
36
  /**
37
37
  * When false, marks this variable as non-cacheable.
38
- * Setting or getting this var inside a cache() boundary or "use cache"
39
- * function will throw. Use for inherently request-specific data (user
40
- * sessions, auth tokens, etc.) that must never be baked into cached segments.
38
+ * Reading this var with ctx.get() inside a cache() boundary throws. Use for
39
+ * inherently request-specific data (user sessions, auth tokens, etc.) that
40
+ * must never be baked into cached segments.
41
41
  *
42
42
  * @default true
43
43
  */
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from "react";
2
+ import { isLoaderDataResult } from "./types.js";
3
+
4
+ // Shared by segment-system (server) and LoaderResolver (client) so the
5
+ // legacy/ok/error-fallback/throw decode of resolved loader values lives once.
6
+ // Last failing loader wins errorFallback; an error without a fallback throws.
7
+ export function decodeLoaderResults(
8
+ resolvedData: any[],
9
+ loaderIds: string[],
10
+ ): { loaderData: Record<string, any>; errorFallback: ReactNode } {
11
+ const loaderData: Record<string, any> = {};
12
+ let errorFallback: ReactNode = null;
13
+
14
+ for (let i = 0; i < loaderIds.length; i++) {
15
+ const id = loaderIds[i];
16
+ const result = resolvedData[i];
17
+
18
+ if (!isLoaderDataResult(result)) {
19
+ loaderData[id] = result;
20
+ continue;
21
+ }
22
+
23
+ if (result.ok) {
24
+ loaderData[id] = result.data;
25
+ continue;
26
+ }
27
+
28
+ if (result.fallback) {
29
+ errorFallback = result.fallback;
30
+ } else {
31
+ throw new Error(result.error.message);
32
+ }
33
+ }
34
+
35
+ return { loaderData, errorFallback };
36
+ }
package/src/errors.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Custom error classes for RSC Router
2
+ * Custom error classes for Rango
3
3
  *
4
4
  * All errors include:
5
5
  * - Descriptive names for easy identification
@@ -27,6 +27,17 @@ export class RouteNotFoundError extends Error {
27
27
  }
28
28
  }
29
29
 
30
+ // name fallback covers cross-realm errors (Vite dev dupes, RSC serialization)
31
+ // where instanceof fails.
32
+ export function isRouteNotFoundError(
33
+ error: unknown,
34
+ ): error is RouteNotFoundError {
35
+ return (
36
+ error instanceof RouteNotFoundError ||
37
+ (error instanceof Error && error.name === "RouteNotFoundError")
38
+ );
39
+ }
40
+
30
41
  /**
31
42
  * Thrown when data is not found (e.g., product with ID doesn't exist)
32
43
  * Use this in handlers/loaders to trigger the nearest notFoundBoundary
@@ -109,6 +120,24 @@ export class BuildError extends Error {
109
120
  }
110
121
  }
111
122
 
123
+ /**
124
+ * Thrown when a route-definition DSL helper (route/layout/loader/cache/…) is
125
+ * called outside an active urls()/map() builder, so there is no
126
+ * AsyncLocalStorage build context to attach to. The message names the specific
127
+ * helper and how to fix it; the `cause` records the mechanical reason so the
128
+ * failure mode is identifiable (not conflated with an unrelated throw).
129
+ */
130
+ export class DslContextError extends Error {
131
+ name = "DslContextError" as const;
132
+ cause?: unknown;
133
+
134
+ constructor(message: string, options?: ErrorOptions) {
135
+ super(message);
136
+ Object.setPrototypeOf(this, DslContextError.prototype);
137
+ this.cause = options?.cause;
138
+ }
139
+ }
140
+
112
141
  /**
113
142
  * Thrown when a network request fails (server unreachable, no internet, etc.)
114
143
  * This error triggers the root error boundary with retry capability.
package/src/handle.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { missingInjectedIdError } from "./missing-id-error.js";
2
+
1
3
  /**
2
4
  * Handle definition for accumulating data across route segments.
3
5
  *
@@ -43,6 +45,11 @@ function defaultCollect<T>(segments: T[][]): T[] {
43
45
  // Used by useHandle() to recover collect when handle is deserialized from RSC prop.
44
46
  const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
45
47
 
48
+ // Monotonic counter for runtime fallback ids (see createHandle). Module-scoped
49
+ // and deterministic, so each createHandle() call gets a stable, unique id within
50
+ // the process. Only used when no build id was injected (a bare unit test).
51
+ let runtimeHandleIdCounter = 0;
52
+
46
53
  /**
47
54
  * Look up a collect function from the registry by handle $$id.
48
55
  * Returns undefined if not registered (falls back to defaultCollect in useHandle).
@@ -93,14 +100,22 @@ export function createHandle<TData, TAccumulated = TData[]>(
93
100
  collect?: (segments: TData[][]) => TAccumulated,
94
101
  __injectedId?: string,
95
102
  ): Handle<TData, TAccumulated> {
96
- const handleId = __injectedId ?? "";
103
+ let handleId = __injectedId ?? "";
97
104
 
98
105
  if (!handleId && process.env.NODE_ENV === "development") {
99
- throw new Error(
100
- "[rsc-router] Handle is missing $$id. " +
101
- "Make sure the exposeInternalIds Vite plugin is enabled and " +
102
- "the handle is exported with: export const MyHandle = createHandle(...)",
103
- );
106
+ throw missingInjectedIdError("Handle", "createHandle");
107
+ }
108
+
109
+ // No build-injected id. This only happens in a bare unit test — every real
110
+ // build runs the rango Vite plugin, which always injects a stable id (and the
111
+ // line above throws for a genuinely non-exported handle in dev). Assign a
112
+ // process-stable runtime id so the collect registers below and the handle is
113
+ // fully exercisable in tests (useHandle, collectHandle, renderRoute's `handles`
114
+ // seeding run the REAL collect). Provably inert in production: the fallback
115
+ // never triggers when the plugin injects the id, so server/client id
116
+ // consistency (required for RSC recovery) is unaffected.
117
+ if (!handleId) {
118
+ handleId = `__rango_runtime_handle_${runtimeHandleIdCounter++}`;
104
119
  }
105
120
 
106
121
  const collectFn =
@@ -109,12 +124,10 @@ export function createHandle<TData, TAccumulated = TData[]>(
109
124
 
110
125
  // Register collect in module-level registry so useHandle() can recover it
111
126
  // when the handle is deserialized from RSC props (toJSON strips collect).
112
- if (handleId) {
113
- collectRegistry.set(
114
- handleId,
115
- collectFn as (segments: unknown[][]) => unknown,
116
- );
117
- }
127
+ collectRegistry.set(
128
+ handleId,
129
+ collectFn as (segments: unknown[][]) => unknown,
130
+ );
118
131
 
119
132
  return {
120
133
  __brand: "handle" as const,
@@ -151,7 +164,7 @@ export function collectHandleData<TData, TAccumulated>(
151
164
  const collectFn = getCollectFn(handle.$$id);
152
165
  if (!collectFn && process.env.NODE_ENV !== "production") {
153
166
  console.warn(
154
- `[rsc-router] Handle "${handle.$$id}" has no registered collect function. ` +
167
+ `[rango] Handle "${handle.$$id}" has no registered collect function. ` +
155
168
  `Falling back to flat array. Ensure the handle module is imported so ` +
156
169
  `createHandle() runs and registers the collect function.`,
157
170
  );
package/src/host/index.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * const router = createHostRouter();
13
13
  *
14
- * router.host(['.']).map(() => import('./apps/main'));
15
- * router.host(['admin.*']).map(() => import('./apps/admin'));
14
+ * router.host(['.']).lazy(() => import('./apps/main'));
15
+ * router.host(['admin.*']).lazy(() => import('./apps/admin'));
16
16
  *
17
17
  * export default {
18
18
  * fetch(request) {