@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
@@ -8,6 +8,7 @@ import type {
8
8
  import type { NonceProvider } from "../rsc/types.js";
9
9
  import type { ExecutionContext } from "../server/request-context.js";
10
10
  import type { UrlPatterns } from "../urls.js";
11
+ import type { UrlBuilder } from "../urls/pattern-types.js";
11
12
  import type { NamedRouteEntry } from "./content-negotiation.js";
12
13
  import type { TelemetrySink } from "./telemetry.js";
13
14
  import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
@@ -72,7 +73,7 @@ export interface RootLayoutProps {
72
73
  /**
73
74
  * Router configuration options
74
75
  */
75
- export interface RSCRouterOptions<TEnv = any> {
76
+ export interface RangoOptions<TEnv = any> {
76
77
  /**
77
78
  * Unique identifier for this router instance.
78
79
  * Used to namespace static output files and route maps.
@@ -95,6 +96,28 @@ export interface RSCRouterOptions<TEnv = any> {
95
96
  */
96
97
  $$sourceFile?: string;
97
98
 
99
+ /**
100
+ * URL prefix applied to all routes registered with this router.
101
+ *
102
+ * Useful when the app is served under a sub-path (e.g. `/admin` or `/v2`).
103
+ * All `path()` patterns are automatically prefixed and `reverse()` returns
104
+ * full paths including the basename. Route names are NOT prefixed.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const router = createRouter({
109
+ * basename: "/admin",
110
+ * }).routes(({ path }) => [
111
+ * path("/", Dashboard, { name: "home" }), // matches /admin
112
+ * path("/users", Users, { name: "users" }), // matches /admin/users
113
+ * ]);
114
+ *
115
+ * router.reverse("home"); // "/admin"
116
+ * router.reverse("users"); // "/admin/users"
117
+ * ```
118
+ */
119
+ basename?: string;
120
+
98
121
  /**
99
122
  * Enable performance metrics collection
100
123
  * When enabled, metrics are output to console and available via Server-Timing header
@@ -109,6 +132,21 @@ export interface RSCRouterOptions<TEnv = any> {
109
132
  */
110
133
  allowDebugManifest?: boolean;
111
134
 
135
+ /**
136
+ * DEVELOPMENT/TEST ONLY. Emit an `X-Rango-Cache` response header describing
137
+ * the cache status of the matched route, for use by testing primitives such
138
+ * as `assertCacheStatus`.
139
+ *
140
+ * Defaults to `false`. When neither this option nor the
141
+ * `RANGO_TEST_SIGNALS=1` environment flag is set, NO header is emitted and
142
+ * router output is byte-identical to the default.
143
+ *
144
+ * The header encodes per-segment (v1: coarse route-level) status keyed by the
145
+ * route NAME, e.g. `X-Rango-Cache: product.detail=hit`. Do NOT enable in
146
+ * production — it exposes internal cache decisions.
147
+ */
148
+ debugCacheSignal?: boolean;
149
+
112
150
  /**
113
151
  * Document component that wraps the entire application.
114
152
  *
@@ -335,27 +373,54 @@ export interface RSCRouterOptions<TEnv = any> {
335
373
  theme?: import("../theme/types.js").ThemeConfig | true;
336
374
 
337
375
  /**
338
- * URL patterns to register with the router.
376
+ * Default for whether the router wraps `transition()` segments in its own
377
+ * React `<ViewTransition>` boundary (experimental React only).
339
378
  *
340
- * Alternative to calling `.routes()` method - allows passing patterns
341
- * directly in the config for a more concise setup.
379
+ * - "auto" (default): every route/layout that opts in via `transition()`
380
+ * gets a router-owned cross-fade.
381
+ * - false: the router never places its own boundary. Routes that use
382
+ * `transition()` still drive navigation through startTransition (so loaders
383
+ * hold instead of flashing a skeleton) and still let consumer-placed
384
+ * `<ViewTransition>` elements animate — the router just contributes no
385
+ * cross-fade of its own. This is the "router triggers, you place the
386
+ * transitions" model.
387
+ *
388
+ * A per-segment `transition({ viewTransition })` overrides this default.
342
389
  *
343
390
  * @example
344
391
  * ```typescript
345
- * import { urls } from "@rangojs/router/server";
392
+ * // App-wide: drive + hold, but never auto-wrap. Place <ViewTransition>
393
+ * // yourself in components where you want a morph.
394
+ * const router = createRouter<AppEnv>({ viewTransition: false });
395
+ * ```
396
+ */
397
+ viewTransition?: "auto" | false;
398
+
399
+ /**
400
+ * URL patterns to register with the router.
346
401
  *
347
- * const urlpatterns = urls(({ path, layout }) => [
348
- * path("/", HomePage, { name: "home" }),
349
- * path("/about", AboutPage, { name: "about" }),
350
- * ]);
402
+ * Accepts either a `UrlPatterns` object from `urls()` or a builder function
403
+ * directly (urls() is called implicitly).
351
404
  *
352
- * const router = createRouter<AppEnv>({
405
+ * @example
406
+ * ```typescript
407
+ * // With urls()
408
+ * createRouter<AppEnv>({
353
409
  * document: Document,
354
410
  * urls: urlpatterns,
355
411
  * });
412
+ *
413
+ * // With builder function
414
+ * createRouter<AppEnv>({
415
+ * document: Document,
416
+ * urls: ({ path }) => [
417
+ * path("/", HomePage, { name: "home" }),
418
+ * path("/about", AboutPage, { name: "about" }),
419
+ * ],
420
+ * });
356
421
  * ```
357
422
  */
358
- urls?: UrlPatterns<TEnv, any>;
423
+ urls?: UrlPatterns<TEnv, any> | UrlBuilder<TEnv>;
359
424
 
360
425
  /**
361
426
  * Injected by the Vite transform at compile time.
@@ -1,4 +1,4 @@
1
- import type { RSCRouterInternal } from "./router-interfaces.js";
1
+ import type { RangoInternal } from "./router-interfaces.js";
2
2
 
3
3
  /**
4
4
  * Brand marker for identifying router instances at build time.
@@ -12,10 +12,7 @@ export const RSC_ROUTER_BRAND = "__rsc_router__" as const;
12
12
  * Used by the Vite plugin at build time to discover routers and extract
13
13
  * manifests, prefix trees, and pre-render candidates.
14
14
  */
15
- export const RouterRegistry: Map<
16
- string,
17
- RSCRouterInternal<any, any>
18
- > = new Map();
15
+ export const RouterRegistry: Map<string, RangoInternal<any, any>> = new Map();
19
16
 
20
17
  export let routerAutoId = 0;
21
18
 
@@ -7,7 +7,11 @@
7
7
 
8
8
  import type { ReactNode } from "react";
9
9
  import { invariant } from "../../errors";
10
- import type { EntryData } from "../../server/context";
10
+ import {
11
+ getParallelEntries,
12
+ getParallelSlotEntries,
13
+ type EntryData,
14
+ } from "../../server/context";
11
15
  import type {
12
16
  HandlerContext,
13
17
  InternalHandlerContext,
@@ -15,6 +19,8 @@ import type {
15
19
  } from "../../types";
16
20
  import type { SegmentResolutionDeps } from "../types.js";
17
21
  import { resolveLoaderData } from "./loader-cache.js";
22
+ import { _getRequestContext } from "../../server/request-context.js";
23
+ import { appendMetric } from "../metrics.js";
18
24
  import {
19
25
  handleHandlerResult,
20
26
  tryStaticHandler,
@@ -22,9 +28,14 @@ import {
22
28
  resolveLayoutComponent,
23
29
  resolveWithErrorBoundary,
24
30
  } from "./helpers.js";
31
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
25
32
  import { getRouterContext } from "../router-context.js";
26
33
  import { resolveSink, safeEmit } from "../telemetry.js";
27
- import { track } from "../../server/context.js";
34
+ import {
35
+ track,
36
+ RangoContext,
37
+ runInsideLoaderScope,
38
+ } from "../../server/context.js";
28
39
 
29
40
  // ---------------------------------------------------------------------------
30
41
  // Streamed handler telemetry
@@ -90,9 +101,11 @@ export async function resolveLoaders<TEnv>(
90
101
  const shortCode = shortCodeOverride ?? entry.shortCode;
91
102
  const hasLoading = "loading" in entry && entry.loading !== undefined;
92
103
  const loadingDisabled = hasLoading && entry.loading === false;
104
+ const ms = _getRequestContext()?._metricsStore;
93
105
 
94
106
  if (!loadingDisabled) {
95
- return loaderEntries.map((loaderEntry, i) => {
107
+ // Streaming loaders: promises kick off now, settle during RSC serialization.
108
+ const segments = loaderEntries.map((loaderEntry, i) => {
96
109
  const { loader } = loaderEntry;
97
110
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
98
111
  return {
@@ -104,7 +117,9 @@ export async function resolveLoaders<TEnv>(
104
117
  params: ctx.params,
105
118
  loaderId: loader.$$id,
106
119
  loaderData: deps.wrapLoaderPromise(
107
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
120
+ runInsideLoaderScope(() =>
121
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
122
+ ),
108
123
  entry,
109
124
  segmentId,
110
125
  ctx.pathname,
@@ -112,18 +127,38 @@ export async function resolveLoaders<TEnv>(
112
127
  belongsToRoute,
113
128
  };
114
129
  });
130
+
131
+ return segments;
115
132
  }
116
133
 
117
134
  // Loading disabled: still start all loaders in parallel, but only emit
118
135
  // settled promises so handlers don't stream loading placeholders.
119
- const pendingLoaderData = loaderEntries.map((loaderEntry) =>
120
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
121
- );
122
- await Promise.all(pendingLoaderData);
136
+ const pendingLoaderData = loaderEntries.map((loaderEntry) => {
137
+ const start = performance.now();
138
+ const promise = runInsideLoaderScope(() =>
139
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
140
+ );
141
+ return { promise, start, loaderId: loaderEntry.loader.$$id };
142
+ });
143
+ await Promise.all(pendingLoaderData.map((p) => p.promise));
123
144
 
124
145
  return loaderEntries.map((loaderEntry, i) => {
125
146
  const { loader } = loaderEntry;
126
147
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
148
+ const pending = pendingLoaderData[i]!;
149
+ if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
150
+ // All loaders ran in parallel via Promise.all — each span covers
151
+ // from its own kickoff to the batch settlement, giving a ceiling
152
+ // on that loader's contribution to the overall wait.
153
+ const batchEnd = performance.now();
154
+ appendMetric(
155
+ ms,
156
+ `loader:${loader.$$id}`,
157
+ pending.start,
158
+ batchEnd - pending.start,
159
+ 2,
160
+ );
161
+ }
127
162
  return {
128
163
  id: segmentId,
129
164
  namespace: entry.id,
@@ -133,7 +168,7 @@ export async function resolveLoaders<TEnv>(
133
168
  params: ctx.params,
134
169
  loaderId: loader.$$id,
135
170
  loaderData: deps.wrapLoaderPromise(
136
- pendingLoaderData[i]!,
171
+ pending.promise,
137
172
  entry,
138
173
  segmentId,
139
174
  ctx.pathname,
@@ -190,14 +225,20 @@ export async function resolveSegment<TEnv>(
190
225
  index: 0,
191
226
  component,
192
227
  loading: entry.loading === false ? null : entry.loading,
193
- transition: entry.transition,
228
+ transition: applyViewTransitionDefault(
229
+ entry.transition,
230
+ deps.viewTransitionDefault,
231
+ ),
194
232
  params,
195
233
  belongsToRoute: false,
196
234
  layoutName: entry.id,
197
235
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
198
236
  });
199
237
 
200
- for (const parallelEntry of entry.parallel) {
238
+ const resolvedParallelEntries = new Set<string>();
239
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
240
+ entry.parallel,
241
+ )) {
201
242
  const parallelSegments = await resolveParallelEntry(
202
243
  parallelEntry,
203
244
  params,
@@ -207,8 +248,11 @@ export async function resolveSegment<TEnv>(
207
248
  deps,
208
249
  options,
209
250
  routeKey,
251
+ [slot],
252
+ !resolvedParallelEntries.has(parallelEntry.id),
210
253
  );
211
254
  segments.push(...parallelSegments);
255
+ resolvedParallelEntries.add(parallelEntry.id);
212
256
  }
213
257
 
214
258
  for (const orphan of entry.layout) {
@@ -244,9 +288,14 @@ export async function resolveSegment<TEnv>(
244
288
  entry.shortCode,
245
289
  );
246
290
  if (component === undefined) {
291
+ // For Passthrough routes at runtime, use the live handler instead of
292
+ // the build handler. At build time (context.build === true), always
293
+ // use the build handler from entry.handler.
294
+ const handler =
295
+ !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
247
296
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
248
297
  if (entry.loading) {
249
- const result = handleHandlerResult(entry.handler(context));
298
+ const result = handleHandlerResult(handler(context));
250
299
  if (result instanceof Promise) {
251
300
  result.finally(doneRouteHandler).catch(() => {});
252
301
  const tracked = deps.trackHandler(result, {
@@ -267,7 +316,7 @@ export async function resolveSegment<TEnv>(
267
316
  component = result;
268
317
  }
269
318
  } else {
270
- component = handleHandlerResult(await entry.handler(context));
319
+ component = handleHandlerResult(await handler(context));
271
320
  doneRouteHandler();
272
321
  }
273
322
  }
@@ -282,11 +331,15 @@ export async function resolveSegment<TEnv>(
282
331
  deps,
283
332
  options,
284
333
  routeKey,
334
+ entry,
285
335
  );
286
336
  segments.push(...orphanSegments);
287
337
  }
288
338
 
289
- for (const parallelEntry of entry.parallel) {
339
+ const resolvedParallelEntries = new Set<string>();
340
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
341
+ entry.parallel,
342
+ )) {
290
343
  const parallelSegments = await resolveParallelEntry(
291
344
  parallelEntry,
292
345
  params,
@@ -296,8 +349,11 @@ export async function resolveSegment<TEnv>(
296
349
  deps,
297
350
  options,
298
351
  routeKey,
352
+ [slot],
353
+ !resolvedParallelEntries.has(parallelEntry.id),
299
354
  );
300
355
  segments.push(...parallelSegments);
356
+ resolvedParallelEntries.add(parallelEntry.id);
301
357
  }
302
358
 
303
359
  segments.push({
@@ -305,9 +361,12 @@ export async function resolveSegment<TEnv>(
305
361
  namespace: entry.id,
306
362
  type: "route",
307
363
  index: 0,
308
- component,
364
+ component: component ?? null,
309
365
  loading: entry.loading === false ? null : entry.loading,
310
- transition: entry.transition,
366
+ transition: applyViewTransitionDefault(
367
+ entry.transition,
368
+ deps.viewTransitionDefault,
369
+ ),
311
370
  params,
312
371
  belongsToRoute: true,
313
372
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
@@ -331,6 +390,9 @@ export async function resolveOrphanLayout<TEnv>(
331
390
  deps: SegmentResolutionDeps<TEnv>,
332
391
  options?: ResolveSegmentOptions,
333
392
  routeKey?: string,
393
+ /** Parent route entry — its loaders are inherited by the layout so
394
+ * parallel slots inside this layout can access them via useLoader(). */
395
+ parentRouteEntry?: EntryData,
334
396
  ): Promise<ResolvedSegment[]> {
335
397
  invariant(
336
398
  orphan.type === "layout" || orphan.type === "cache",
@@ -346,6 +408,30 @@ export async function resolveOrphanLayout<TEnv>(
346
408
  deps,
347
409
  );
348
410
  segments.push(...loaderSegments);
411
+
412
+ // Inherit parent route's loaders so parallel slots inside this layout
413
+ // can access them via useLoader(). Without this, the route's loaders
414
+ // are only in the route's OutletProvider (rendered as <Outlet /> content),
415
+ // which is a child — not a parent — of the layout's context.
416
+ if (
417
+ parentRouteEntry &&
418
+ parentRouteEntry.loader &&
419
+ parentRouteEntry.loader.length > 0 &&
420
+ Object.keys(orphan.parallel).length > 0
421
+ ) {
422
+ const inheritedLoaders = await resolveLoaders(
423
+ parentRouteEntry,
424
+ context,
425
+ belongsToRoute,
426
+ deps,
427
+ orphan.shortCode,
428
+ );
429
+ // Tag as inherited so buildMatchResult can deduplicate when safe
430
+ for (const s of inheritedLoaders) {
431
+ s._inherited = true;
432
+ }
433
+ segments.push(...inheritedLoaders);
434
+ }
349
435
  }
350
436
 
351
437
  // Handler-first: orphan layout handler executes before its parallels
@@ -364,11 +450,17 @@ export async function resolveOrphanLayout<TEnv>(
364
450
  belongsToRoute,
365
451
  layoutName: orphan.id,
366
452
  loading: orphan.loading === false ? null : orphan.loading,
367
- transition: orphan.transition,
453
+ transition: applyViewTransitionDefault(
454
+ orphan.transition,
455
+ deps.viewTransitionDefault,
456
+ ),
368
457
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
369
458
  });
370
459
 
371
- for (const parallelEntry of orphan.parallel) {
460
+ const resolvedParallelEntries = new Set<string>();
461
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
462
+ orphan.parallel,
463
+ )) {
372
464
  const parallelSegments = await resolveParallelEntry(
373
465
  parallelEntry,
374
466
  params,
@@ -378,8 +470,11 @@ export async function resolveOrphanLayout<TEnv>(
378
470
  deps,
379
471
  options,
380
472
  routeKey,
473
+ [slot],
474
+ !resolvedParallelEntries.has(parallelEntry.id),
381
475
  );
382
476
  segments.push(...parallelSegments);
477
+ resolvedParallelEntries.add(parallelEntry.id);
383
478
  }
384
479
 
385
480
  return segments;
@@ -397,6 +492,8 @@ export async function resolveParallelEntry<TEnv>(
397
492
  deps: SegmentResolutionDeps<TEnv>,
398
493
  options?: ResolveSegmentOptions,
399
494
  routeKey?: string,
495
+ slotNames?: `@${string}`[],
496
+ includeLoaders: boolean = true,
400
497
  ): Promise<ResolvedSegment[]> {
401
498
  invariant(
402
499
  parallelEntry.type === "parallel",
@@ -411,7 +508,12 @@ export async function resolveParallelEntry<TEnv>(
411
508
  | ReactNode
412
509
  >;
413
510
 
414
- for (const [slot, handler] of Object.entries(slots)) {
511
+ const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
512
+
513
+ for (const slot of slotsToResolve) {
514
+ // Try static lookup first — in production, handler bodies are evicted
515
+ // and replaced with stubs that have no .handler property (undefined).
516
+ // The static store holds the pre-rendered component for these slots.
415
517
  let component: ReactNode | undefined = await tryStaticSlot(
416
518
  parallelEntry,
417
519
  slot,
@@ -419,6 +521,18 @@ export async function resolveParallelEntry<TEnv>(
419
521
  );
420
522
 
421
523
  if (component === undefined) {
524
+ const handler = slots[slot];
525
+ if (handler === undefined) {
526
+ continue;
527
+ }
528
+ // Pin `_currentSegmentId` to the slot's own id so handle pushes from
529
+ // inside the slot handler get their own bucket in the HandleStore.
530
+ // Parent-keying would collapse them into the parent layout's bucket;
531
+ // the partial-update merge then replaces the parent's bucket on a
532
+ // slot-only revalidation and drops layout-pushed Meta/Breadcrumbs.
533
+ // filterSegmentOrder() retains slot ids so the client preserves them.
534
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
535
+ `${parentShortCode}.${slot}`;
422
536
  const doneParallelHandler = track(
423
537
  `handler:${parallelEntry.id}.${slot}`,
424
538
  2,
@@ -461,7 +575,10 @@ export async function resolveParallelEntry<TEnv>(
461
575
  index: 0,
462
576
  component,
463
577
  loading: parallelEntry.loading === false ? null : parallelEntry.loading,
464
- transition: parallelEntry.transition,
578
+ transition: applyViewTransitionDefault(
579
+ parallelEntry.transition,
580
+ deps.viewTransitionDefault,
581
+ ),
465
582
  params,
466
583
  slot,
467
584
  belongsToRoute,
@@ -472,7 +589,7 @@ export async function resolveParallelEntry<TEnv>(
472
589
  });
473
590
  }
474
591
 
475
- if (!parallelEntry.loading && !options?.skipLoaders) {
592
+ if (!options?.skipLoaders && includeLoaders) {
476
593
  const loaderSegments = await resolveLoaders(
477
594
  parallelEntry,
478
595
  context,
@@ -480,6 +597,15 @@ export async function resolveParallelEntry<TEnv>(
480
597
  deps,
481
598
  parentShortCode,
482
599
  );
600
+ // Tag parallel-owned loaders so renderSegments can stream them
601
+ // using the parallel's loading() instead of awaiting on the layout
602
+ const parallelLoading =
603
+ parallelEntry.loading === false ? undefined : parallelEntry.loading;
604
+ if (parallelLoading) {
605
+ for (const seg of loaderSegments) {
606
+ seg.parallelLoading = parallelLoading;
607
+ }
608
+ }
483
609
  segments.push(...loaderSegments);
484
610
  }
485
611
 
@@ -515,6 +641,13 @@ export async function resolveAllSegments<TEnv>(
515
641
  } catch {}
516
642
 
517
643
  for (const entry of entries) {
644
+ // Set ALS flag when entering a cache() boundary so that ctx.get()
645
+ // can guard non-cacheable variable reads. Also guards response-level
646
+ // side effects (headers.set). Persists for all descendant entries.
647
+ if (entry.type === "cache") {
648
+ const store = RangoContext.getStore();
649
+ if (store) store.insideCacheScope = true;
650
+ }
518
651
  const doneEntry = track(`segment:${entry.id}`, 1);
519
652
  const resolvedSegments = await resolveWithErrorBoundary(
520
653
  entry,
@@ -559,11 +692,77 @@ export async function resolveLoadersOnly<TEnv>(
559
692
  deps: SegmentResolutionDeps<TEnv>,
560
693
  ): Promise<ResolvedSegment[]> {
561
694
  const loaderSegments: ResolvedSegment[] = [];
695
+ const seenIds = new Set<string>();
696
+
697
+ async function collectEntryLoaders(
698
+ entry: EntryData,
699
+ belongsToRoute: boolean,
700
+ shortCodeOverride?: string,
701
+ ): Promise<void> {
702
+ // Skip if all loaders from this entry have already been resolved
703
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
704
+ const entryLoaders = entry.loader ?? [];
705
+ const sc = shortCodeOverride ?? entry.shortCode;
706
+ const allAlreadySeen =
707
+ entryLoaders.length > 0 &&
708
+ entryLoaders.every((le, i) =>
709
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
710
+ );
711
+ if (!allAlreadySeen) {
712
+ const segments = await resolveLoaders(
713
+ entry,
714
+ context,
715
+ belongsToRoute,
716
+ deps,
717
+ shortCodeOverride,
718
+ );
719
+ for (const seg of segments) {
720
+ if (!seenIds.has(seg.id)) {
721
+ seenIds.add(seg.id);
722
+ loaderSegments.push(seg);
723
+ }
724
+ }
725
+ }
726
+
727
+ const seenParallelEntryIds = new Set<string>();
728
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
729
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
730
+ seenParallelEntryIds.add(parallelEntry.id);
731
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
732
+ }
733
+
734
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
735
+ for (const layoutEntry of entry.layout) {
736
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
737
+ // Inherit route loaders for orphan layouts with parallels.
738
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
739
+ // route entry, as that would re-iterate route.layout and loop.
740
+ if (
741
+ entry.type === "route" &&
742
+ entry.loader &&
743
+ entry.loader.length > 0 &&
744
+ Object.keys(layoutEntry.parallel).length > 0
745
+ ) {
746
+ const inherited = await resolveLoaders(
747
+ entry,
748
+ context,
749
+ childBelongsToRoute,
750
+ deps,
751
+ layoutEntry.shortCode,
752
+ );
753
+ for (const seg of inherited) {
754
+ if (!seenIds.has(seg.id)) {
755
+ seenIds.add(seg.id);
756
+ seg._inherited = true;
757
+ loaderSegments.push(seg);
758
+ }
759
+ }
760
+ }
761
+ }
762
+ }
562
763
 
563
764
  for (const entry of entries) {
564
- const belongsToRoute = entry.type === "route";
565
- const segments = await resolveLoaders(entry, context, belongsToRoute, deps);
566
- loaderSegments.push(...segments);
765
+ await collectEntryLoaders(entry, entry.type === "route");
567
766
  }
568
767
 
569
768
  return loaderSegments;
@@ -8,7 +8,7 @@
8
8
  * - Error boundary segment creation
9
9
  */
10
10
 
11
- import type { ReactNode } from "react";
11
+ import { createElement, type ReactNode } from "react";
12
12
  import { DataNotFoundError } from "../../errors";
13
13
  import {
14
14
  createErrorInfo,
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
180
180
 
181
181
  if (error instanceof DataNotFoundError) {
182
182
  const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
183
+ // Fall back to router's notFound component, then a plain default
184
+ const notFoundOption = deps.notFoundComponent;
185
+ const defaultFallback =
186
+ typeof notFoundOption === "function"
187
+ ? notFoundOption({ pathname: pathname ?? "" })
188
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
189
+ const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
183
190
 
184
- if (notFoundFallback) {
185
- const notFoundInfo = createNotFoundInfo(
186
- error,
187
- entry.shortCode,
188
- entry.type,
189
- pathname,
190
- );
191
+ const notFoundInfo = createNotFoundInfo(
192
+ error,
193
+ entry.shortCode,
194
+ entry.type,
195
+ pathname,
196
+ );
191
197
 
192
- reportError(true, {
193
- notFound: true,
194
- message: notFoundInfo.message,
195
- });
198
+ reportError(true, {
199
+ notFound: true,
200
+ message: notFoundInfo.message,
201
+ });
196
202
 
197
- debugLog("segment", "notFound boundary handled error", {
198
- segmentId: entry.shortCode,
199
- message: notFoundInfo.message,
200
- });
203
+ debugLog("segment", "notFound boundary handled error", {
204
+ segmentId: entry.shortCode,
205
+ message: notFoundInfo.message,
206
+ });
201
207
 
202
- setResponseStatus(404);
208
+ setResponseStatus(404);
203
209
 
204
- return createNotFoundSegment(
205
- notFoundInfo,
206
- notFoundFallback,
207
- entry,
208
- params,
209
- );
210
- }
210
+ return createNotFoundSegment(
211
+ notFoundInfo,
212
+ effectiveNotFoundFallback,
213
+ entry,
214
+ params,
215
+ );
211
216
  }
212
217
 
213
218
  const fallback = deps.findNearestErrorBoundary(entry);
@@ -147,6 +147,7 @@ export function resolveLoaderData<TEnv>(
147
147
  }
148
148
 
149
149
  const loaderId = loaderEntry.loader.$$id;
150
+
150
151
  const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
151
152
  const swrWindow = resolveSwrWindow(options.swr, store.defaults);
152
153
  const swr = swrWindow || undefined;