@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { createElement } from "react";
11
- import { RouteNotFoundError } from "../errors.js";
11
+ import { isRouteNotFoundError } from "../errors.js";
12
12
  import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
13
13
  import {
14
14
  runWithRequestContext,
@@ -31,6 +31,7 @@ import {
31
31
  interceptRedirectForPartial,
32
32
  buildRouteMiddlewareEntries,
33
33
  } from "./helpers.js";
34
+ import { guardOutgoingRedirect } from "./redirect-guard.js";
34
35
  import { isWebSocketUpgradeResponse } from "../response-utils.js";
35
36
  import {
36
37
  handleResponseRoute,
@@ -57,6 +58,7 @@ import {
57
58
  getRouterTrie,
58
59
  } from "../route-map-builder.js";
59
60
  import type { HandlerContext } from "./handler-context.js";
61
+ import type { CacheErrorCategory } from "../cache/cache-error.js";
60
62
  import type { SegmentCacheStore } from "../cache/types.js";
61
63
  import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
62
64
  import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
@@ -66,7 +68,10 @@ import {
66
68
  type ActionContinuation,
67
69
  } from "./server-action.js";
68
70
  import { handleLoaderFetch } from "./loader-fetch.js";
69
- import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
71
+ import {
72
+ checkRequestOrigin,
73
+ ORIGIN_CHECK_PHASE_BY_MODE,
74
+ } from "./origin-guard.js";
70
75
  import { handleRscRendering } from "./rsc-rendering.js";
71
76
  import {
72
77
  withTimeout,
@@ -83,6 +88,7 @@ import {
83
88
  startSSRSetup,
84
89
  getSSRSetup,
85
90
  mayNeedSSR,
91
+ isRscRequest,
86
92
  SSR_SETUP_VAR,
87
93
  } from "./ssr-setup.js";
88
94
  import {
@@ -124,12 +130,35 @@ import {
124
130
  * });
125
131
  * ```
126
132
  */
133
+
134
+ /**
135
+ * Response that tells the client to do a full document navigation. Shared by
136
+ * the terminal reload plans (version-mismatch and app-switch): an empty 200
137
+ * carrying X-RSC-Reload, which the client turns into window.location.href.
138
+ */
139
+ function createReloadResponse(reloadUrl: string) {
140
+ return createResponseWithMergedHeaders(null, {
141
+ status: 200,
142
+ headers: {
143
+ "X-RSC-Reload": reloadUrl,
144
+ "content-type": "text/x-component;charset=utf-8",
145
+ },
146
+ });
147
+ }
148
+
127
149
  export function createRSCHandler<
128
150
  TEnv = unknown,
129
151
  TRoutes extends Record<string, string> = Record<string, string>,
130
152
  >(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
131
153
  const { router, version = VERSION, nonce: nonceProvider } = options;
132
154
 
155
+ // Handler-owned registry of explicit per-scope stores from cache({ store }).
156
+ // Lives in the closure so it is scoped per handler (multi-router deployments
157
+ // get separate registries) and accumulates every explicit store this handler
158
+ // resolves across requests. updateTag()/revalidateTag() iterate it to reach
159
+ // stores not covered by the app-level ctx._cacheStore.
160
+ const explicitTaggedStores = new Set<SegmentCacheStore>();
161
+
133
162
  // Use provided deps or default to @vitejs/plugin-rsc/rsc exports
134
163
  const deps = options.deps ?? rscDeps;
135
164
  const {
@@ -264,12 +293,13 @@ export function createRSCHandler<
264
293
  function createRedirectFlightResponse(
265
294
  redirectUrl: string,
266
295
  locationState?: Record<string, unknown>,
296
+ external?: boolean,
267
297
  ): Response {
268
298
  const redirectPayload: RscPayload = {
269
299
  metadata: {
270
300
  pathname: redirectUrl,
271
301
  segments: [],
272
- redirect: { url: redirectUrl },
302
+ redirect: { url: redirectUrl, ...(external && { external: true }) },
273
303
  ...(locationState && { locationState }),
274
304
  },
275
305
  };
@@ -421,9 +451,12 @@ export function createRSCHandler<
421
451
  url,
422
452
  variables,
423
453
  cacheStore,
454
+ explicitTaggedStores,
424
455
  cacheProfiles: router.cacheProfiles,
425
456
  executionContext: executionCtx,
426
457
  themeConfig: router.themeConfig,
458
+ stateCookieName: router.resolvedStateCookieName,
459
+ version,
427
460
  });
428
461
  if (earlyMetricsStore) {
429
462
  requestContext._debugPerformance = true;
@@ -433,7 +466,7 @@ export function createRSCHandler<
433
466
  // can surface non-fatal errors through the router's onError callback.
434
467
  requestContext._reportBackgroundError = (
435
468
  error: unknown,
436
- category: string,
469
+ category: CacheErrorCategory,
437
470
  ) => {
438
471
  callOnError(error, "cache", {
439
472
  request,
@@ -539,7 +572,12 @@ export function createRSCHandler<
539
572
  response.headers.set("Server-Timing", fullTiming);
540
573
  }
541
574
 
542
- return response;
575
+ // Single open-redirect chokepoint: every response (PE, full-page,
576
+ // middleware short-circuit, response-route) funnels through here, so
577
+ // guarding browser-followed (3xx) redirects once covers them all and any
578
+ // future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
579
+ // through untouched (validated client-side instead).
580
+ return guardOutgoingRedirect(response, url.origin, router.basename);
543
581
  });
544
582
  };
545
583
 
@@ -597,10 +635,7 @@ export function createRSCHandler<
597
635
  routerId: router.id,
598
636
  });
599
637
  } catch (error) {
600
- if (
601
- error instanceof RouteNotFoundError ||
602
- (error instanceof Error && error.name === "RouteNotFoundError")
603
- ) {
638
+ if (isRouteNotFoundError(error)) {
604
639
  // Let the render path handle 404 — match()/matchPartial() will
605
640
  // re-throw RouteNotFoundError and the catch block in
606
641
  // executeRenderWithMiddleware renders the not-found page.
@@ -641,24 +676,18 @@ export function createRSCHandler<
641
676
  console.log(
642
677
  `[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
643
678
  );
644
- return createResponseWithMergedHeaders(null, {
645
- status: 200,
646
- headers: {
647
- "X-RSC-Reload": plan.reloadUrl,
648
- "content-type": "text/x-component;charset=utf-8",
649
- },
650
- });
679
+ return createReloadResponse(plan.reloadUrl);
680
+ }
681
+
682
+ if (plan.mode === "app-switch") {
683
+ // Cross-app SPA navigation crossed a host-router app boundary. Force a
684
+ // real document navigation so the target app's document is re-established
685
+ // (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
686
+ return createReloadResponse(plan.reloadUrl);
651
687
  }
652
688
 
653
689
  // ---- 3. Origin guard (gate for action/loader/PE modes) ----
654
- const originPhase: OriginCheckPhase | null =
655
- plan.mode === "action"
656
- ? "action"
657
- : plan.mode === "loader"
658
- ? "loader"
659
- : plan.mode === "pe-render"
660
- ? "pe-form"
661
- : null;
690
+ const originPhase = ORIGIN_CHECK_PHASE_BY_MODE[plan.mode];
662
691
  if (originPhase) {
663
692
  const originResult = await checkRequestOrigin(
664
693
  request,
@@ -925,47 +954,17 @@ export function createRSCHandler<
925
954
  );
926
955
  }
927
956
 
928
- // ---- Full render / Partial render (or PE that fell through) ----
929
- if (plan.mode === "full-render" || plan.mode === "partial-render") {
930
- const isPartial = plan.mode === "partial-render";
931
- return executeRenderWithMiddleware(
932
- plan.route.routeMiddleware,
933
- plan.negotiated,
934
- plan.route.routeKey,
935
- routeReverse,
936
- request,
937
- env,
938
- url,
939
- variables,
940
- nonce,
941
- handleStore,
942
- isPartial,
943
- );
944
- }
945
-
946
- // PE that fell through (handleProgressiveEnhancement returned null)
947
- // falls back to full render
948
- if (plan.mode === "pe-render") {
949
- return executeRenderWithMiddleware(
950
- plan.route.routeMiddleware,
951
- false,
952
- plan.route.routeKey,
953
- routeReverse,
954
- request,
955
- env,
956
- url,
957
- variables,
958
- nonce,
959
- handleStore,
960
- false,
961
- );
962
- }
963
-
964
- // Redirect plan that wasn't handled above (full-page redirect — let
965
- // the pipeline handle it via match() which returns { redirect: url })
957
+ // Full render, partial render, fallen-through PE, and full-page redirect all
958
+ // render through the same middleware-wrapped path. Only full/partial-render
959
+ // carry negotiation + the partial flag; pe/redirect render plainly.
960
+ const isPartial = plan.mode === "partial-render";
961
+ const negotiated =
962
+ plan.mode === "full-render" || plan.mode === "partial-render"
963
+ ? plan.negotiated
964
+ : false;
966
965
  return executeRenderWithMiddleware(
967
966
  plan.route.routeMiddleware,
968
- false,
967
+ negotiated,
969
968
  plan.route.routeKey,
970
969
  routeReverse,
971
970
  request,
@@ -974,7 +973,7 @@ export function createRSCHandler<
974
973
  variables,
975
974
  nonce,
976
975
  handleStore,
977
- false,
976
+ isPartial,
978
977
  );
979
978
  }
980
979
 
@@ -1025,10 +1024,19 @@ export function createRSCHandler<
1025
1024
  } catch (error) {
1026
1025
  // Check if middleware/handler returned Response
1027
1026
  if (error instanceof Response) {
1027
+ // An action revalidation render is delivered to the client over the
1028
+ // same Flight-parsing path as a partial navigation, so a Response
1029
+ // thrown during it must be converted exactly like a partial one
1030
+ // (raw 200 -> hard-nav hint, 3xx -> Flight redirect). Without this,
1031
+ // the no-middleware path returns the raw Response (the with-middleware
1032
+ // path is already covered by the isPartial || actionContinuation
1033
+ // guard below).
1034
+ const treatAsPartial = isPartial || actionContinuation != null;
1035
+
1028
1036
  // During partial (client-side navigation), a 200 Response from a handler
1029
1037
  // means the route serves raw content (JSON, text, etc.), not JSX.
1030
1038
  // Signal the browser to hard-navigate so it renders the raw response.
1031
- if (isPartial && error.status === 200) {
1039
+ if (treatAsPartial && error.status === 200) {
1032
1040
  console.warn(
1033
1041
  `[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
1034
1042
  `Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
@@ -1042,7 +1050,7 @@ export function createRSCHandler<
1042
1050
  });
1043
1051
  }
1044
1052
 
1045
- if (isPartial) {
1053
+ if (treatAsPartial) {
1046
1054
  const intercepted = interceptRedirectForPartial(
1047
1055
  error,
1048
1056
  createRedirectFlightResponse,
@@ -1054,10 +1062,7 @@ export function createRSCHandler<
1054
1062
  }
1055
1063
 
1056
1064
  // Render 404 page for unmatched routes
1057
- const isRouteNotFound =
1058
- error instanceof RouteNotFoundError ||
1059
- (error instanceof Error && error.name === "RouteNotFoundError");
1060
- if (isRouteNotFound) {
1065
+ if (isRouteNotFoundError(error)) {
1061
1066
  callOnError(error, "routing", {
1062
1067
  request,
1063
1068
  url,
@@ -1092,6 +1097,7 @@ export function createRSCHandler<
1092
1097
  rootLayout: router.rootLayout,
1093
1098
  handles: handleStore.stream(),
1094
1099
  version,
1100
+ stateCookieName: router.resolvedStateCookieName,
1095
1101
  themeConfig: router.themeConfig,
1096
1102
  warmupEnabled: router.warmupEnabled,
1097
1103
  initialTheme: requireRequestContext().theme,
@@ -1104,16 +1110,15 @@ export function createRSCHandler<
1104
1110
  },
1105
1111
  });
1106
1112
 
1107
- const isRscRequest =
1108
- isPartial ||
1109
- (!request.headers.get("accept")?.includes("text/html") &&
1110
- !url.searchParams.has("__html")) ||
1111
- url.searchParams.has("__rsc");
1112
-
1113
- if (isRscRequest) {
1113
+ if (isRscRequest(request, url, isPartial)) {
1114
1114
  return createResponseWithMergedHeaders(rscStream, {
1115
1115
  status: 404,
1116
- headers: { "content-type": "text/x-component;charset=utf-8" },
1116
+ headers: {
1117
+ "content-type": "text/x-component;charset=utf-8",
1118
+ // Router identity for the client's pre-decode integrity check; a
1119
+ // same-app 404 matches and applies in place. See response-adapter.
1120
+ "X-RSC-Router-Id": router.id,
1121
+ },
1117
1122
  });
1118
1123
  }
1119
1124
 
@@ -10,7 +10,32 @@ import {
10
10
  } from "../server/request-context.js";
11
11
  import type { RequestContext } from "../server/request-context.js";
12
12
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
13
+ import { isRedirectResponse } from "../response-utils.js";
14
+ import {
15
+ EXTERNAL_REDIRECT_MARKER,
16
+ isExternalRedirect,
17
+ markExternalRedirect,
18
+ } from "../redirect-origin.js";
13
19
  import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
20
+ import { formatCacheSignalHeader } from "../router/telemetry.js";
21
+ import type { RscPayload } from "./types.js";
22
+
23
+ /**
24
+ * DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
25
+ * match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
26
+ * header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
27
+ * attached — output is byte-identical to the default. Header mutation failures
28
+ * are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
29
+ */
30
+ function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
31
+ const signal = ctx._cacheSignal;
32
+ if (!signal || signal.length === 0) return;
33
+ try {
34
+ target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
35
+ } catch {
36
+ // Headers immutable — skip.
37
+ }
38
+ }
14
39
 
15
40
  /**
16
41
  * Copy stub headers from the request context onto a target Headers instance:
@@ -21,6 +46,10 @@ import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
21
46
  function applyStubHeaders(target: Headers, stub: Headers): void {
22
47
  stub.forEach((value, name) => {
23
48
  try {
49
+ // The reserved external-redirect marker is internal and never a trust
50
+ // signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
51
+ // browser-facing response. The opt-in is the out-of-band brand.
52
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
24
53
  if (name.toLowerCase() === "set-cookie") {
25
54
  target.append(name, value);
26
55
  } else if (!target.has(name)) {
@@ -44,10 +73,19 @@ function drainOnResponseCallbacks(
44
73
  const callbacks = ctx._onResponseCallbacks;
45
74
  if (callbacks.length === 0) return response;
46
75
  ctx._onResponseCallbacks = [];
76
+ // An onResponse callback may return a NEW Response (e.g. to add a header),
77
+ // which drops the out-of-band external-redirect brand (brand is keyed on
78
+ // Response object identity). Preserve a redirect(url, { external: true })
79
+ // opt-in across that rebuild so a callback can't silently neutralize the
80
+ // off-host redirect at the guard chokepoint.
81
+ const wasExternal = isExternalRedirect(response);
47
82
  let result = response;
48
83
  for (const callback of callbacks) {
49
84
  result = callback(result) ?? result;
50
85
  }
86
+ if (wasExternal && !isExternalRedirect(result)) {
87
+ markExternalRedirect(result);
88
+ }
51
89
  return result;
52
90
  }
53
91
 
@@ -84,6 +122,7 @@ export function createResponseWithMergedHeaders(
84
122
  const mergedHeaders = new Headers(init.headers);
85
123
  applyStubHeaders(mergedHeaders, ctx.res.headers);
86
124
  ctx.res.headers.delete("set-cookie");
125
+ applyCacheSignalHeader(mergedHeaders, ctx);
87
126
 
88
127
  // ctx.res.status overrides init.status when explicitly set (e.g. 404 for
89
128
  // notFound, 500 for error). Default ctx.res.status is 200.
@@ -114,8 +153,20 @@ export function createSimpleRedirectResponse(redirectUrl: string): Response {
114
153
 
115
154
  /**
116
155
  * Carry over headers from a source redirect Response to a wrapper Response.
117
- * Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper)
118
- * and appends Set-Cookie to avoid clobbering multiple cookie headers.
156
+ * Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper) and
157
+ * appends Set-Cookie to avoid clobbering multiple cookie headers.
158
+ *
159
+ * This is a GENERIC copier used by every redirect-rebuild path (PE
160
+ * extractRedirectResponse, the SPA intercept below, the guard's neutralize
161
+ * rebuild), so it has two redirect-specific jobs:
162
+ *
163
+ * 1. NEVER copy the reserved external-redirect header: it is no longer a trust
164
+ * signal (the opt-in is the out-of-band brand), and a forged value from a
165
+ * proxied upstream must not ride a rebuilt response to the browser.
166
+ * 2. Transfer the out-of-band external brand: a rebuilt document-native redirect
167
+ * has to carry the opt-in to the guard chokepoint, which reads and clears it.
168
+ * Without this transfer, redirect(url, { external: true }) would be silently
169
+ * neutralized on any rebuild path (fail-closed, but a feature regression).
119
170
  */
120
171
  export function carryOverRedirectHeaders(
121
172
  source: Response,
@@ -124,12 +175,16 @@ export function carryOverRedirectHeaders(
124
175
  source.headers.forEach((value, name) => {
125
176
  const lower = name.toLowerCase();
126
177
  if (lower === "location" || lower === "x-rsc-redirect") return;
178
+ if (lower === EXTERNAL_REDIRECT_MARKER) return;
127
179
  if (lower === "set-cookie") {
128
180
  target.headers.append(name, value);
129
181
  } else if (!target.headers.has(name)) {
130
182
  target.headers.set(name, value);
131
183
  }
132
184
  });
185
+ if (isExternalRedirect(source)) {
186
+ markExternalRedirect(target);
187
+ }
133
188
  }
134
189
 
135
190
  /**
@@ -143,28 +198,62 @@ export function interceptRedirectForPartial(
143
198
  createRedirectFlightResponse: (
144
199
  redirectUrl: string,
145
200
  locationState?: Record<string, unknown>,
201
+ external?: boolean,
146
202
  ) => Response,
147
203
  ): Response | null {
148
- const redirectUrl = response.headers.get("Location");
149
- if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
204
+ if (!isRedirectResponse(response)) {
150
205
  return null;
151
206
  }
207
+ const redirectUrl = response.headers.get("Location")!;
208
+ // redirect(url, { external: true }) marks an explicit off-host redirect via
209
+ // the out-of-band brand (not a wire header). On the SPA/action channel the
210
+ // intent must travel as a Flight payload (metadata.redirect.external) so the
211
+ // client does a scheme-validated hard navigation (location.assign) rather than
212
+ // a partial fetch. The client re-validates the scheme; see partial-update.ts.
213
+ const external = isExternalRedirect(response);
152
214
  const locationState = getLocationState();
153
215
  let intercepted: Response;
154
216
  if (locationState) {
155
217
  intercepted = createRedirectFlightResponse(
156
218
  redirectUrl,
157
219
  resolveLocationStateEntries(locationState),
220
+ external,
158
221
  );
222
+ } else if (external) {
223
+ intercepted = createRedirectFlightResponse(redirectUrl, undefined, true);
159
224
  } else {
160
225
  intercepted = createSimpleRedirectResponse(redirectUrl);
161
226
  }
162
227
 
163
228
  carryOverRedirectHeaders(response, intercepted);
229
+ // Defense-in-depth at the SPA browser-facing exit: carryOverRedirectHeaders
230
+ // already refuses to copy the reserved marker, but strip any value that might
231
+ // exist on `intercepted` so a forged header can never ride the 200/204 to the
232
+ // browser. The external intent travels in metadata.redirect.external (Flight),
233
+ // where the client re-validates the scheme.
234
+ try {
235
+ intercepted.headers.delete(EXTERNAL_REDIRECT_MARKER);
236
+ } catch {
237
+ // Immutable headers: the marker was never copied here, so this is inert.
238
+ }
164
239
 
165
240
  return intercepted;
166
241
  }
167
242
 
243
+ /**
244
+ * Attach location state set during a request to a payload's metadata.
245
+ * No-op if no location state was set. Callers must ensure payload.metadata
246
+ * is populated (the non-null assertion holds for the partial/action payloads
247
+ * that reach this helper).
248
+ */
249
+ export function attachLocationStateIfPresent(payload: RscPayload): void {
250
+ const locationState = getLocationState();
251
+ if (locationState) {
252
+ payload.metadata!.locationState =
253
+ resolveLocationStateEntries(locationState);
254
+ }
255
+ }
256
+
168
257
  /**
169
258
  * Only cache successful responses. Non-200 statuses (errors, redirects) are
170
259
  * not cached -- notFound() produces 500 in response routes, and explicit
@@ -191,7 +280,6 @@ export function buildRouteMiddlewareEntries<TEnv>(
191
280
  regex: null,
192
281
  paramNames: [],
193
282
  handler: mw.handler,
194
- mountPrefix: null,
195
283
  } as MiddlewareEntry<TEnv>,
196
284
  params: mw.params,
197
285
  }));
package/src/rsc/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * RSC Router - RSC Entry Point
2
+ * Rango - RSC Entry Point
3
3
  *
4
4
  * This module provides RSC utilities for server-side rendering,
5
5
  * server actions, loader fetching, and progressive enhancement.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared serialization for `json()` response-route results.
3
+ *
4
+ * Kept in its own lightweight module (depends only on `errors.js`) so the
5
+ * `dispatch()` testing primitive can import it WITHOUT dragging in
6
+ * `response-route-handler.ts`'s heavy runtime graph, which transitively reaches
7
+ * a Vite virtual module and breaks a plain (non-Vite) vitest import.
8
+ */
9
+
10
+ import { RouterError } from "../errors.js";
11
+
12
+ /**
13
+ * Serialize a `json()` response-route result, rejecting a nested unresolved
14
+ * Promise (the forgotten-await footgun: `() => ({ data: fetchSomething() })`).
15
+ * `JSON.stringify` would silently emit `{}` for a Promise, shipping empty data;
16
+ * the RSC pipeline awaits nested promises but this path does not. Throwing
17
+ * `RESPONSE_NOT_SERIALIZABLE` makes the failure loud.
18
+ *
19
+ * Shared by the production response-route handler and the `dispatch()` testing
20
+ * primitive so a `dispatch` json test of a forgotten await fails exactly where
21
+ * production 500s, instead of going green.
22
+ */
23
+ export function stringifyJsonRouteResult(result: unknown): string {
24
+ return JSON.stringify(result, (_key, value) => {
25
+ if (
26
+ value != null &&
27
+ typeof (value as { then?: unknown }).then === "function"
28
+ ) {
29
+ throw new RouterError(
30
+ "RESPONSE_NOT_SERIALIZABLE",
31
+ "A json() response route returned a Promise (likely a forgotten " +
32
+ "await). Await async values before returning so they serialize, " +
33
+ "instead of emitting an empty {}.",
34
+ );
35
+ }
36
+ return value;
37
+ });
38
+ }
@@ -13,6 +13,7 @@ import {
13
13
  setRouteTrie,
14
14
  setRouterManifest,
15
15
  setRouterTrie,
16
+ setRouterPrecomputedEntries,
16
17
  } from "../route-map-builder.js";
17
18
 
18
19
  /**
@@ -36,47 +37,13 @@ export async function buildRouterTrieFromUrlpatterns(
36
37
  undefined,
37
38
  router.basename ? { urlPrefix: router.basename } : undefined,
38
39
  );
39
- if (
40
- generated._routeAncestry &&
41
- Object.keys(generated._routeAncestry).length > 0
42
- ) {
43
- const { buildRouteTrie } = await import("../build/route-trie.js");
44
- // Map each route to its include() staticPrefix so the trie
45
- // returns the correct sp for lazy entry lookup in findMatch.
46
- const routeToStaticPrefix: Record<string, string> = {};
47
- for (const name of Object.keys(generated.routeManifest)) {
48
- routeToStaticPrefix[name] = "";
49
- }
50
- // Override with prefix from include() entries so the trie
51
- // returns the correct sp for lazy entry lookup in findMatch.
52
- // Walk recursively to include routes in nested includes.
53
- if (generated.prefixTree) {
54
- const visitPrefixNode = (node: any): void => {
55
- const sp = node.staticPrefix || "";
56
- for (const route of node.routes || []) {
57
- routeToStaticPrefix[route] = sp;
58
- }
59
- for (const child of Object.values(node.children || {})) {
60
- visitPrefixNode(child);
61
- }
62
- };
63
- for (const node of Object.values(generated.prefixTree)) {
64
- visitPrefixNode(node);
65
- }
66
- }
67
- const trie = buildRouteTrie(
68
- generated.routeManifest,
69
- generated._routeAncestry,
70
- routeToStaticPrefix,
71
- generated.routeTrailingSlash,
72
- generated.prerenderRoutes
73
- ? new Set(generated.prerenderRoutes)
74
- : undefined,
75
- generated.passthroughRoutes
76
- ? new Set(generated.passthroughRoutes)
77
- : undefined,
78
- generated.responseTypeRoutes,
79
- );
40
+ // Build the trie through the SAME shared helper the production discovery uses
41
+ // (discover-routers.ts), so the dev runtime-rebuilt trie and the prod
42
+ // serialized trie cannot drift. buildPerRouterTrie returns null when there
43
+ // are no routes.
44
+ const { buildPerRouterTrie } = await import("../build/route-trie.js");
45
+ const trie = buildPerRouterTrie(generated);
46
+ if (trie) {
80
47
  setRouterTrie(router.id, trie);
81
48
  // Set global trie only if not already set by another router
82
49
  if (!getRouteTrie()) {
@@ -84,6 +51,26 @@ export async function buildRouterTrieFromUrlpatterns(
84
51
  }
85
52
  }
86
53
  setRouterManifest(router.id, generated.routeManifest);
54
+
55
+ // Match the production discovery path: precompute leaf-include entries so the
56
+ // match-time shortcut in evaluateLazyEntry applies in dev/Cloudflare too.
57
+ // Without this, dev re-runs each matched leaf include's handler at match time
58
+ // (evaluateLazyEntry) AND again at render time (loadManifest); with it, the
59
+ // match-time run is skipped and the handler runs once per first request.
60
+ // Identical route ownership to the handler path (the shortcut is guarded by
61
+ // the same prefixIsShared and #506 checks production uses).
62
+ const { flattenLeafEntries } = await import("../build/prefix-tree-utils.js");
63
+ const precomputed: Array<{
64
+ staticPrefix: string;
65
+ routes: Record<string, string>;
66
+ }> = [];
67
+ flattenLeafEntries(
68
+ generated.prefixTree,
69
+ generated.routeManifest,
70
+ precomputed,
71
+ );
72
+ setRouterPrecomputedEntries(router.id, precomputed);
73
+
87
74
  // Merge into global manifest (needed for reverse/href across routers)
88
75
  const existing = hasCachedManifest() ? getGlobalRouteMap() : {};
89
76
  setCachedManifest({ ...existing, ...generated.routeManifest });