@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
@@ -10,7 +10,7 @@ import type {
10
10
  ShouldRevalidateFn,
11
11
  TransitionConfig,
12
12
  } from "../types";
13
- import { invariant } from "../errors";
13
+ import { invariant, DslContextError } from "../errors";
14
14
  import type { DefaultRouteName } from "../types/global-namespace.js";
15
15
 
16
16
  // ============================================================================
@@ -40,7 +40,7 @@ export interface MetricsStore {
40
40
  metrics: PerformanceMetric[];
41
41
  }
42
42
  // ============================================================================
43
- // RSC Router Context
43
+ // Rango Context
44
44
  // ============================================================================
45
45
 
46
46
  /**
@@ -71,6 +71,10 @@ export type EntryPropCommon = {
71
71
  };
72
72
 
73
73
  /**
74
+ * Attachments resolved by walking the parent chain, not owned by the entry:
75
+ * middleware composes downward; revalidate and the error/notFound boundaries are
76
+ * resolved by nearest-ancestor lookup. Inherited, not a single execution chain.
77
+ *
74
78
  * @internal This type is an implementation detail and may change without notice.
75
79
  */
76
80
  export type EntryPropDatas = {
@@ -80,6 +84,16 @@ export type EntryPropDatas = {
80
84
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
81
85
  };
82
86
 
87
+ /**
88
+ * Render-time presentation fields shared by every entry variant.
89
+ *
90
+ * @internal This type is an implementation detail and may change without notice.
91
+ */
92
+ export type EntryPropRender = {
93
+ loading?: ReactNode | false;
94
+ transition?: TransitionConfig;
95
+ };
96
+
83
97
  /**
84
98
  * Loader entry stored in EntryData
85
99
  * Contains the loader definition and its revalidation rules
@@ -157,10 +171,29 @@ export type InterceptEntry = {
157
171
  when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
158
172
  };
159
173
 
174
+ export interface ParallelEntryData
175
+ extends EntryPropCommon, EntryPropDatas, EntryPropSegments, EntryPropRender {
176
+ type: "parallel";
177
+ handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
178
+ /** Set when any parallel slot is a Static definition */
179
+ isStaticPrerender?: true;
180
+ /** Per-slot static handler $$ids for build-time store lookup */
181
+ staticHandlerIds?: Record<string, string>;
182
+ }
183
+
184
+ export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
185
+
186
+ /**
187
+ * This entry's own structural children plus its owned loaders. `loader` lives
188
+ * here (not in EntryPropDatas) because loaders are owned by the entry, not
189
+ * inherited from ancestors.
190
+ *
191
+ * @internal This type is an implementation detail and may change without notice.
192
+ */
160
193
  export type EntryPropSegments = {
161
194
  loader: LoaderEntry[];
162
195
  layout: EntryData[];
163
- parallel: EntryData[]; // type: "parallel" entries with their own loaders/revalidate/loading
196
+ parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
164
197
  intercept: InterceptEntry[]; // intercept definitions for soft navigation
165
198
  };
166
199
 
@@ -168,8 +201,6 @@ export type EntryData =
168
201
  | ({
169
202
  type: "route";
170
203
  handler: Handler<any, any, any>;
171
- loading?: ReactNode | false;
172
- transition?: TransitionConfig;
173
204
  /** URL pattern for this route (used by path() in urls()) */
174
205
  pattern?: string;
175
206
  /** Set when handler is a Prerender definition */
@@ -177,8 +208,12 @@ export type EntryData =
177
208
  /** Original PrerenderHandlerDefinition (for build-time getParams access) */
178
209
  prerenderDef?: {
179
210
  getParams?: (ctx: any) => Promise<any[]> | any[];
180
- options?: { passthrough?: boolean };
211
+ options?: { concurrency?: number };
181
212
  };
213
+ /** Set when route is wrapped with Passthrough() — has a separate live handler */
214
+ isPassthrough?: true;
215
+ /** Live handler for runtime fallback (only set on Passthrough routes) */
216
+ liveHandler?: Handler<any, any, any>;
182
217
  /** Set when handler is a Static definition (build-time only) */
183
218
  isStaticPrerender?: true;
184
219
  /** Static handler $$id for build-time store lookup */
@@ -187,40 +222,28 @@ export type EntryData =
187
222
  responseType?: string;
188
223
  } & EntryPropCommon &
189
224
  EntryPropDatas &
190
- EntryPropSegments)
225
+ EntryPropSegments &
226
+ EntryPropRender)
191
227
  | ({
192
228
  type: "layout";
193
229
  handler: ReactNode | Handler<any, any, any>;
194
- loading?: ReactNode | false;
195
- transition?: TransitionConfig;
196
230
  /** Set when handler is a Static definition (build-time only) */
197
231
  isStaticPrerender?: true;
198
232
  /** Static handler $$id for build-time store lookup */
199
233
  staticHandlerId?: string;
200
234
  } & EntryPropCommon &
201
235
  EntryPropDatas &
202
- EntryPropSegments)
203
- | ({
204
- type: "parallel";
205
- handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
206
- loading?: ReactNode | false;
207
- transition?: TransitionConfig;
208
- /** Set when any parallel slot is a Static definition */
209
- isStaticPrerender?: true;
210
- /** Per-slot static handler $$ids for build-time store lookup */
211
- staticHandlerIds?: Record<string, string>;
212
- } & EntryPropCommon &
213
- EntryPropDatas &
214
- EntryPropSegments)
236
+ EntryPropSegments &
237
+ EntryPropRender)
238
+ | ParallelEntryData
215
239
  | ({
216
240
  type: "cache";
217
241
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
218
242
  handler: ReactNode | Handler<any, any, any>;
219
- loading?: ReactNode | false;
220
- transition?: TransitionConfig;
221
243
  } & EntryPropCommon &
222
244
  EntryPropDatas &
223
- EntryPropSegments);
245
+ EntryPropSegments &
246
+ EntryPropRender);
224
247
 
225
248
  /**
226
249
  * Tracked include info for build-time manifest generation
@@ -270,6 +293,25 @@ interface HelperContext {
270
293
  string,
271
294
  import("../cache/profile-registry.js").CacheProfile
272
295
  >;
296
+ /** True when resolving handlers inside a cache() DSL boundary.
297
+ * Read by ctx.get() to guard non-cacheable variable reads. */
298
+ insideCacheScope?: boolean;
299
+ /**
300
+ * Include scope string applied to direct-descendant shortCodes.
301
+ *
302
+ * Each `include(...)` call allocates a sibling-positional token like `I0`,
303
+ * `I1` from its parent's include counter and stores the composed scope
304
+ * (`${parentScope}I${idx}`) in its lazyContext. When the include's handler
305
+ * evaluates lazily, the store's `includeScope` is set from that context so
306
+ * every direct-descendant shortCode is generated as
307
+ * `${parent.shortCode}${includeScope}${prefix}${index}` — preventing
308
+ * collisions with siblings declared outside the include.
309
+ *
310
+ * The scope is NOT propagated through `store.run(...)`, so layouts /
311
+ * parallels / caches inside the include absorb the scope into their own
312
+ * shortCodes and their children start fresh.
313
+ */
314
+ includeScope?: string;
273
315
  }
274
316
  // Use a global symbol key so the AsyncLocalStorage instance survives HMR
275
317
  // module re-evaluation. Without this, Vite's RSC module runner may create
@@ -277,10 +319,28 @@ interface HelperContext {
277
319
  // hold references to the old instance — causing getStore() to return
278
320
  // undefined even inside a run() callback.
279
321
  const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
280
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
322
+ export const RangoContext: AsyncLocalStorage<HelperContext> = ((
281
323
  globalThis as any
282
324
  )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
283
325
 
326
+ /** shortCode prefix letter per entry type (e.g. "L0", "R2", "M1C0"). */
327
+ const SHORT_CODE_PREFIX: Record<
328
+ "layout" | "parallel" | "route" | "loader" | "cache",
329
+ string
330
+ > = {
331
+ layout: "L",
332
+ parallel: "P",
333
+ route: "R",
334
+ loader: "D",
335
+ cache: "C",
336
+ };
337
+
338
+ /** Post-increment a named per-store counter, returning the prior value. */
339
+ function bumpCounter(store: HelperContext, key: string): number {
340
+ store.counters[key] ??= 0;
341
+ return store.counters[key]++;
342
+ }
343
+
284
344
  export const getContext = (): {
285
345
  context: AsyncLocalStorage<HelperContext>;
286
346
  getStore: () => HelperContext;
@@ -304,12 +364,12 @@ export const getContext = (): {
304
364
  callback: (...args: any[]) => T,
305
365
  ) => T;
306
366
  } => {
307
- const context = RSCRouterContext;
367
+ const context = RangoContext;
308
368
 
309
369
  return {
310
370
  context,
311
371
  getOrCreateStore: (forRoute?: string): HelperContext => {
312
- let store = RSCRouterContext.getStore();
372
+ let store = RangoContext.getStore();
313
373
  if (!store) {
314
374
  store = {
315
375
  manifest: new Map<string, EntryData>(),
@@ -329,7 +389,7 @@ export const getContext = (): {
329
389
  const store = context.getStore();
330
390
  if (!store) {
331
391
  throw new Error(
332
- "RSC Router context store is not available. Make sure to run within RSC Router context.",
392
+ "Rango context store is not available. Make sure to run within Rango context.",
333
393
  );
334
394
  }
335
395
  return store;
@@ -346,48 +406,36 @@ export const getContext = (): {
346
406
  type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
347
407
  ) => {
348
408
  const store = context.getStore();
349
- invariant(store, "No context RSCRouterContext available");
350
- store.counters[type] ??= 0;
351
- const index = store.counters[type];
352
- store.counters[type] = index + 1;
353
- return `$${type}.${index}`;
409
+ invariant(store, "No context RangoContext available");
410
+ return `$${type}.${bumpCounter(store, type)}`;
354
411
  },
355
412
  getShortCode: (
356
413
  type: "layout" | "parallel" | "route" | "loader" | "cache",
357
414
  ) => {
358
415
  const store = context.getStore();
359
- invariant(store, "No context RSCRouterContext available");
416
+ invariant(store, "No context RangoContext available");
360
417
 
361
418
  const parent = store.parent;
362
- const prefix =
363
- type === "layout"
364
- ? "L"
365
- : type === "parallel"
366
- ? "P"
367
- : type === "loader"
368
- ? "D"
369
- : type === "cache"
370
- ? "C"
371
- : "R";
419
+ const prefix = SHORT_CODE_PREFIX[type];
372
420
  const mountPrefix =
373
421
  store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
374
422
 
423
+ const includeScope = store.includeScope ?? "";
424
+
375
425
  if (!parent) {
376
426
  // Root entry: prefix with mount index and use mount-scoped counter
377
427
  const counterKey = mountPrefix
378
428
  ? `${mountPrefix}_root_${type}`
379
429
  : `root_${type}`;
380
- store.counters[counterKey] ??= 0;
381
- const index = store.counters[counterKey];
382
- store.counters[counterKey] = index + 1;
383
- return `${mountPrefix}${prefix}${index}`;
430
+ return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
384
431
  } else {
385
- // Child entry: use parent-scoped counter (parent already has M prefix)
386
- const counterKey = `${parent.shortCode}_${type}`;
387
- store.counters[counterKey] ??= 0;
388
- const index = store.counters[counterKey];
389
- store.counters[counterKey] = index + 1;
390
- return `${parent.shortCode}${prefix}${index}`;
432
+ // Child entry: use parent-scoped counter with includeScope appended.
433
+ // When we're evaluating a lazy include's direct children, includeScope
434
+ // is a per-include token like "I0" / "I1I0" that partitions the
435
+ // parent's counter namespace so routes inside one include cannot
436
+ // collide with siblings declared outside it.
437
+ const counterKey = `${parent.shortCode}${includeScope}_${type}`;
438
+ return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
391
439
  }
392
440
  },
393
441
  runWithStore: <T>(
@@ -414,6 +462,7 @@ export const getContext = (): {
414
462
  rootScoped: store.rootScoped,
415
463
  trackedIncludes: store.trackedIncludes,
416
464
  cacheProfiles: store.cacheProfiles,
465
+ includeScope: store.includeScope,
417
466
  },
418
467
  callback,
419
468
  );
@@ -460,6 +509,31 @@ export const getContext = (): {
460
509
  };
461
510
  };
462
511
 
512
+ /**
513
+ * Acquire the active DSL build context, throwing `message` if a helper was
514
+ * called outside a urls()/map() builder. Returns the store API and the live
515
+ * HelperContext so callers avoid a second getContext() lookup.
516
+ */
517
+ export function requireDslContext(message: string): {
518
+ store: ReturnType<typeof getContext>;
519
+ ctx: HelperContext;
520
+ } {
521
+ const store = getContext();
522
+ const ctx = store.context.getStore();
523
+ if (!ctx) {
524
+ // The only reason the store is absent here is that a route-definition helper
525
+ // ran with no active RangoContext — i.e. outside a urls()/map() builder.
526
+ // Record that as the cause so the throw is self-explanatory, not a bare
527
+ // "must be called inside urls()" with no indication of the mechanism.
528
+ throw new DslContextError(message, {
529
+ cause:
530
+ "RangoContext store is undefined: a route-definition helper was called " +
531
+ "outside an active urls()/map() builder.",
532
+ });
533
+ }
534
+ return { store, ctx };
535
+ }
536
+
463
537
  /**
464
538
  * Run a callback with specific URL and name prefixes
465
539
  * Used by include() to apply prefixes to nested patterns
@@ -469,7 +543,7 @@ export function runWithPrefixes<T>(
469
543
  namePrefix: string | undefined,
470
544
  callback: () => T,
471
545
  ): T {
472
- const store = RSCRouterContext.getStore();
546
+ const store = RangoContext.getStore();
473
547
  if (!store) {
474
548
  throw new Error("runWithPrefixes must be called within router context");
475
549
  }
@@ -514,7 +588,7 @@ export function runWithPrefixes<T>(
514
588
  ? (store.rootScoped ?? false)
515
589
  : store.rootScoped;
516
590
 
517
- return RSCRouterContext.run(
591
+ return RangoContext.run(
518
592
  {
519
593
  ...store,
520
594
  urlPrefix: combinedUrlPrefix,
@@ -529,7 +603,7 @@ export function runWithPrefixes<T>(
529
603
  * Get current URL prefix from context
530
604
  */
531
605
  export function getUrlPrefix(): string {
532
- const store = RSCRouterContext.getStore();
606
+ const store = RangoContext.getStore();
533
607
  return store?.urlPrefix || "";
534
608
  }
535
609
 
@@ -537,7 +611,7 @@ export function getUrlPrefix(): string {
537
611
  * Get current name prefix from context
538
612
  */
539
613
  export function getNamePrefix(): string | undefined {
540
- const store = RSCRouterContext.getStore();
614
+ const store = RangoContext.getStore();
541
615
  return store?.namePrefix;
542
616
  }
543
617
 
@@ -546,13 +620,87 @@ export function getNamePrefix(): string | undefined {
546
620
  * Returns true at root or inside { name: "" } includes, false inside named includes.
547
621
  */
548
622
  export function getRootScoped(): boolean {
549
- const store = RSCRouterContext.getStore();
623
+ const store = RangoContext.getStore();
550
624
  return store?.rootScoped ?? true;
551
625
  }
552
626
 
553
627
  // Export HelperContext type for use in other modules
554
628
  export type { HelperContext };
555
629
 
630
+ /**
631
+ * Return an isolated copy of a lazy include's captured parent entry.
632
+ *
633
+ * DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
634
+ * Multiple include() scopes capture the *same* syntheticMapRoot as their
635
+ * parent, so without isolation one include's loaders/middleware leak into
636
+ * every other route that shares that root.
637
+ *
638
+ * The clone is shallow: only the mutable arrays are copied so each
639
+ * include pushes to its own list. The rest of the entry (id, shortCode,
640
+ * parent pointer, handler) stays shared, which is correct and cheap.
641
+ */
642
+ export function getIsolatedLazyParent(
643
+ captured: EntryData | null | undefined,
644
+ ): EntryData | null {
645
+ if (!captured) return null;
646
+ return {
647
+ ...captured,
648
+ loader: [...captured.loader],
649
+ middleware: [...captured.middleware],
650
+ revalidate: [...captured.revalidate],
651
+ errorBoundary: [...captured.errorBoundary],
652
+ notFoundBoundary: [...captured.notFoundBoundary],
653
+ layout: [...captured.layout],
654
+ parallel: { ...captured.parallel },
655
+ intercept: [...captured.intercept],
656
+ };
657
+ }
658
+
659
+ export function getParallelEntries(
660
+ parallels: ParallelEntries | EntryData[] | undefined,
661
+ ): ParallelEntryData[] {
662
+ if (!parallels) return [];
663
+ if (Array.isArray(parallels)) {
664
+ return parallels.filter(
665
+ (entry): entry is ParallelEntryData => entry.type === "parallel",
666
+ );
667
+ }
668
+ return Object.values(parallels).filter(
669
+ (entry): entry is ParallelEntryData => !!entry,
670
+ );
671
+ }
672
+
673
+ export function getParallelSlotEntries(
674
+ parallels: ParallelEntries | EntryData[] | undefined,
675
+ ): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
676
+ if (!parallels) return [];
677
+
678
+ if (Array.isArray(parallels)) {
679
+ return getParallelEntries(parallels).flatMap((entry) =>
680
+ (Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
681
+ slot,
682
+ entry,
683
+ })),
684
+ );
685
+ }
686
+
687
+ return Object.entries(parallels)
688
+ .filter(([, entry]) => !!entry)
689
+ .map(([slot, entry]) => ({
690
+ slot: slot as `@${string}`,
691
+ entry: entry!,
692
+ }));
693
+ }
694
+
695
+ export function getParallelSlotCount(
696
+ parallels: ParallelEntries | EntryData[] | undefined,
697
+ ): number {
698
+ if (!parallels) return 0;
699
+ return Array.isArray(parallels)
700
+ ? parallels.filter((entry) => entry?.type === "parallel").length
701
+ : Object.keys(parallels).length;
702
+ }
703
+
556
704
  // ============================================================================
557
705
  // Performance Metrics Helpers
558
706
  // ============================================================================
@@ -569,7 +717,7 @@ export type { HelperContext };
569
717
  * ```
570
718
  */
571
719
  export function track(label: string, depth?: number): () => void {
572
- const store = RSCRouterContext.getStore();
720
+ const store = RangoContext.getStore();
573
721
 
574
722
  // No-op if context unavailable or metrics not enabled
575
723
  if (!store?.metrics?.enabled) {
@@ -589,3 +737,71 @@ export function track(label: string, depth?: number): () => void {
589
737
  });
590
738
  };
591
739
  }
740
+
741
+ /**
742
+ * Separate ALS for tracking loader execution scope.
743
+ * Uses a dedicated ALS (not RangoContext) to avoid issues with
744
+ * nested RangoContext.run() calls in Vite's module runner.
745
+ */
746
+ const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
747
+ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
748
+ globalThis as any
749
+ )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
750
+
751
+ // Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
752
+ // of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
753
+ // invoked via ctx.use). Consulted ONLY by isInsideCacheScope() to exempt
754
+ // request-scoped reads. It deliberately does NOT affect isInsideLoaderScope(),
755
+ // so rendered()/barrier/deadlock gating (which must distinguish DSL from
756
+ // handler-invoked loaders) is unchanged.
757
+ const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
758
+ const loaderBodyScopeALS: AsyncLocalStorage<{ active: true }> = ((
759
+ globalThis as any
760
+ )[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
761
+
762
+ /**
763
+ * Check if the current execution is inside a cache() DSL boundary.
764
+ * Returns false inside loader execution — loaders are always fresh
765
+ * (never cached), so non-cacheable reads are safe.
766
+ */
767
+ export function isInsideCacheScope(): boolean {
768
+ if (RangoContext.getStore()?.insideCacheScope !== true) return false;
769
+ // Loaders are always fresh — even inside a cache() boundary, the loader
770
+ // function re-executes on every request. Skip the guard when running
771
+ // inside a loader.
772
+ if (loaderScopeALS.getStore()?.active) return false;
773
+ // Also exempt handler-invoked loaders: their bodies run in a loader-body
774
+ // scope (not the DSL loader scope above), so request-scoped reads inside any
775
+ // loader — however invoked — are safe (loaders always re-run fresh).
776
+ if (loaderBodyScopeALS.getStore()?.active) return false;
777
+ return true;
778
+ }
779
+
780
+ /**
781
+ * Check if the current execution is inside a DSL loader scope
782
+ * (wrapped by runInsideLoaderScope). Used by rendered() barrier
783
+ * to distinguish DSL loaders from handler-invoked loaders.
784
+ */
785
+ export function isInsideLoaderScope(): boolean {
786
+ return loaderScopeALS.getStore()?.active === true;
787
+ }
788
+
789
+ /**
790
+ * Run `fn` inside a loader scope. While active, cache-scope guards
791
+ * are bypassed because loaders are always fresh (never cached) and
792
+ * their side effects (setCookie, header, etc.) are safe.
793
+ */
794
+ export function runInsideLoaderScope<T>(fn: () => T): T {
795
+ return loaderScopeALS.run({ active: true }, fn);
796
+ }
797
+
798
+ /**
799
+ * Run `fn` inside a loader BODY scope. Marks loader-function execution for the
800
+ * cache-purity guard only (isInsideCacheScope), WITHOUT affecting
801
+ * isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
802
+ * and handler-invoked via ctx.use) so request-scoped reads inside a loader
803
+ * never trip the cache-scope guards — loaders always run fresh.
804
+ */
805
+ export function runInsideLoaderBodyScope<T>(fn: () => T): T {
806
+ return loaderBodyScopeALS.run({ active: true }, fn);
807
+ }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import type { CookieOptions } from "../router/middleware-types.js";
11
11
  import { getRequestContext } from "./request-context.js";
12
+ import { isInsideCacheScope } from "./context.js";
12
13
  import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
13
14
 
14
15
  /**
@@ -84,10 +85,23 @@ export interface ReadonlyHeaders {
84
85
  type HeadersIterator<T> = IterableIterator<T>;
85
86
 
86
87
  /**
87
- * Throw if called inside a "use cache" function.
88
- * Reading request-scoped data (cookies, headers) inside a cached function
89
- * produces results that vary per request but the cache key does not include
90
- * those values, leading to one user's data being served to another.
88
+ * Throw if called inside a cache boundary — either a "use cache" function
89
+ * (`INSIDE_CACHE_EXEC` stamped on ctx by the cache runtime) or a `cache()`
90
+ * DSL boundary (`isInsideCacheScope()` the render-store flag set while
91
+ * resolving a `type: "cache"` route entry).
92
+ *
93
+ * Reading request-scoped data (cookies, headers) inside a cached scope
94
+ * produces per-request values that are NOT reflected in the cache key, so
95
+ * they would be frozen into the shared cache entry and served to the wrong
96
+ * users. This is the same hazard for both scopes: a `cache()` boundary caches
97
+ * everything except loaders (it is the document-level "PPR shell"), so a read
98
+ * here is baked into the shell exactly like a `"use cache"` return value is
99
+ * baked into its cache entry.
100
+ *
101
+ * `isInsideCacheScope()` returns false inside loaders (loaders always run
102
+ * fresh on every request, even on a cache hit), so reading cookies()/headers()
103
+ * from a loader is allowed — loaders are the dynamic "holes" of a cached
104
+ * document.
91
105
  */
92
106
  function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
93
107
  if (
@@ -106,6 +120,16 @@ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
106
120
  ` const data = await getCachedData(locale); // locale is now in the cache key`,
107
121
  );
108
122
  }
123
+ if (isInsideCacheScope()) {
124
+ throw new Error(
125
+ `${fnName}() cannot be called inside a cache() boundary. ` +
126
+ `A cache() scope caches everything except loaders, so request-scoped ` +
127
+ `data (cookies, headers) read here would be frozen into the shared ` +
128
+ `cached shell and served to other users. Read it inside a loader ` +
129
+ `instead — loaders always run fresh on every request, even on a cache hit:\n\n` +
130
+ ` loader("user", () => getUser(cookies().get("session")?.value));`,
131
+ );
132
+ }
109
133
  }
110
134
 
111
135
  const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
@@ -13,6 +13,25 @@
13
13
  */
14
14
  export type HandleData = Record<string, Record<string, unknown[]>>;
15
15
 
16
+ /**
17
+ * Build a HandleData snapshot from a HandleStore using segment ordering.
18
+ * Reads data directly from the store for each segment in order.
19
+ */
20
+ export function buildHandleSnapshot(
21
+ handleStore: HandleStore,
22
+ segmentOrder: string[],
23
+ ): HandleData {
24
+ const data: HandleData = {};
25
+ for (const segmentId of segmentOrder) {
26
+ const segData = handleStore.getDataForSegment(segmentId);
27
+ for (const handleName in segData) {
28
+ if (!data[handleName]) data[handleName] = {};
29
+ data[handleName][segmentId] = segData[handleName];
30
+ }
31
+ }
32
+ return data;
33
+ }
34
+
16
35
  function createLateHandlePushError(
17
36
  handleName: string,
18
37
  segmentId: string,
@@ -44,20 +44,21 @@ export function setLoaderImports(
44
44
  export async function getLoaderLazy(
45
45
  id: string,
46
46
  ): Promise<LoaderRegistryEntry | undefined> {
47
- // Check if already cached in main registry
48
- const existing = loaderRegistry.get(id);
49
- if (existing) {
50
- return existing;
51
- }
52
-
53
- // Check the fetchable loader registry (populated by createLoader)
47
+ // Always check fetchableLoaderRegistry first it's the source of truth.
48
+ // createLoader() updates it during module re-evaluation (HMR), so checking
49
+ // here ensures we pick up the fresh function after a loader file change.
54
50
  const fetchable = getFetchableLoader(id);
55
51
  if (fetchable) {
56
- // Cache in main registry for future requests
57
52
  loaderRegistry.set(id, fetchable);
58
53
  return fetchable;
59
54
  }
60
55
 
56
+ // Fall back to local cache (populated by previous lazy imports in production)
57
+ const existing = loaderRegistry.get(id);
58
+ if (existing) {
59
+ return existing;
60
+ }
61
+
61
62
  // Try to lazy load from the import map (production mode)
62
63
  if (lazyLoaderImports && lazyLoaderImports.size > 0) {
63
64
  const lazyImport = lazyLoaderImports.get(id);