@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
@@ -168,8 +168,6 @@ export function withBackgroundRevalidation<TEnv>(
168
168
  requestCtx._handleStore = createHandleStore();
169
169
 
170
170
  try {
171
- // Create fresh handler context and loader promises to avoid
172
- // reusing memoized results from the foreground pass
173
171
  const freshHandlerContext = createHandlerContext(
174
172
  ctx.matched.params,
175
173
  ctx.request,
@@ -185,10 +183,6 @@ export function withBackgroundRevalidation<TEnv>(
185
183
  const freshLoaderPromises = new Map<string, Promise<any>>();
186
184
  setupLoaderAccess(freshHandlerContext, freshLoaderPromises);
187
185
 
188
- // Resolve all segments fresh (without revalidation logic)
189
- // to ensure complete components for caching.
190
- // Skip DSL loaders — they are never cached (cacheRoute filters them)
191
- // and are always resolved fresh on each request.
192
186
  const freshSegments = await ctx.Store.run(() =>
193
187
  resolveAllSegments(
194
188
  ctx.entries,
@@ -200,7 +194,6 @@ export function withBackgroundRevalidation<TEnv>(
200
194
  ),
201
195
  );
202
196
 
203
- // Also resolve intercept segments fresh if applicable
204
197
  let freshInterceptSegments: ResolvedSegment[] = [];
205
198
  if (ctx.interceptResult) {
206
199
  freshInterceptSegments = await ctx.Store.run(() =>
@@ -93,7 +93,7 @@
93
93
  */
94
94
  import type { ResolvedSegment } from "../../types.js";
95
95
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
96
- import { getRouterContext } from "../router-context.js";
96
+ import { getRouterContext, type RouterContext } from "../router-context.js";
97
97
  import { resolveSink, safeEmit } from "../telemetry.js";
98
98
  import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
99
99
  import { treeHasStreaming } from "./segment-resolution.js";
@@ -103,6 +103,7 @@ import {
103
103
  getRequestContext,
104
104
  _getRequestContext,
105
105
  } from "../../server/request-context.js";
106
+ import { paramsEqual } from "../params-util.js";
106
107
 
107
108
  // Lazily initialized prerender store singleton and dynamically imported deps.
108
109
  // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
@@ -114,6 +115,9 @@ let _deserializeSegments:
114
115
  let _restoreHandles:
115
116
  | typeof import("../../cache/handle-snapshot.js").restoreHandles
116
117
  | undefined;
118
+ let _decodeHandles:
119
+ | typeof import("../../cache/handle-snapshot.js").decodeHandles
120
+ | undefined;
117
121
  let _hashParams:
118
122
  | typeof import("../../prerender/param-hash.js").hashParams
119
123
  | undefined;
@@ -121,22 +125,6 @@ let _lazyGetRequestContext:
121
125
  | typeof import("../../server/request-context.js").getRequestContext
122
126
  | undefined;
123
127
 
124
- function paramsEqual(
125
- a: Record<string, string>,
126
- b: Record<string, string>,
127
- ): boolean {
128
- if (a === b) return true;
129
-
130
- const keysA = Object.keys(a);
131
- if (keysA.length !== Object.keys(b).length) return false;
132
-
133
- for (const key of keysA) {
134
- if (a[key] !== b[key]) return false;
135
- }
136
-
137
- return true;
138
- }
139
-
140
128
  async function ensurePrerenderDeps() {
141
129
  if (!_deserializeSegments) {
142
130
  const [codec, snapshot, paramHash, reqCtx, store] = await Promise.all([
@@ -148,6 +136,7 @@ async function ensurePrerenderDeps() {
148
136
  ]);
149
137
  _deserializeSegments = codec.deserializeSegments;
150
138
  _restoreHandles = snapshot.restoreHandles;
139
+ _decodeHandles = snapshot.decodeHandles;
151
140
  _hashParams = paramHash.hashParams;
152
141
  _lazyGetRequestContext = reqCtx.getRequestContext;
153
142
  if (prerenderStoreInstance === undefined) {
@@ -161,69 +150,21 @@ async function ensurePrerenderDeps() {
161
150
  * Deserializes segments, replays handle data, yields segments with partial
162
151
  * navigation nullification, and resolves fresh loaders.
163
152
  */
164
- async function* yieldFromStore<TEnv>(
165
- entry: PrerenderEntry,
153
+ // Resolve loaders fresh on a cache hit (loaders are NEVER cached) and yield
154
+ // the loader segments, updating state.matchedIds and pushing the loader-resolve
155
+ // + cache-hit metrics. Shared verbatim by the prerender-store path
156
+ // (yieldFromStore) and the runtime cache-hit path (withCacheLookup). The router
157
+ // resolve fns are passed in (captured early by the callers) rather than re-read
158
+ // from getRouterContext() here, because that is ALS-backed and the callers run
159
+ // awaits before reaching this point (workerd can disrupt ALS mid-pipeline).
160
+ async function* resolveFreshLoadersAndYield<TEnv>(
166
161
  ctx: MatchContext<TEnv>,
167
162
  state: MatchPipelineState,
168
163
  pipelineStart: number,
169
- handleStoreRef?: HandleStore,
164
+ ms: MatchContext<TEnv>["metricsStore"],
165
+ resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
166
+ resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
170
167
  ): AsyncGenerator<ResolvedSegment> {
171
- const { resolveLoadersOnlyWithRevalidation, resolveLoadersOnly } =
172
- getRouterContext<TEnv>();
173
-
174
- if (
175
- !_deserializeSegments ||
176
- !_restoreHandles ||
177
- !_hashParams ||
178
- !_lazyGetRequestContext
179
- ) {
180
- throw new Error("yieldFromStore called before ensurePrerenderDeps");
181
- }
182
-
183
- const segments = await _deserializeSegments(entry.segments);
184
-
185
- // Replay handle data (same as runtime cache hit path).
186
- // Prefer the eagerly-captured handleStoreRef to avoid ALS disruption in workerd.
187
- const handleStore = handleStoreRef ?? _lazyGetRequestContext()?._handleStore;
188
- if (handleStore) {
189
- _restoreHandles(entry.handles, handleStore);
190
- }
191
-
192
- state.cacheHit = true;
193
- state.cacheSource = "prerender";
194
- state.cachedSegments = segments;
195
- state.cachedMatchedIds = segments.map((s) => s.id);
196
-
197
- // Set streaming flag (once) and resolve render barrier.
198
- const reqCtx = handleStoreRef ? undefined : _lazyGetRequestContext?.();
199
- const barrierReqCtx = reqCtx ?? _getRequestContext();
200
- if (barrierReqCtx) {
201
- if (barrierReqCtx._treeHasStreaming === undefined) {
202
- barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
203
- }
204
- barrierReqCtx._resolveRenderBarrier(segments);
205
- }
206
-
207
- // For partial navigation, nullify components the client already has
208
- // so parent layouts stay live (client keeps its existing versions).
209
- // When params changed (e.g., different guide slug), the segments have
210
- // different content, so we must NOT nullify.
211
- const paramsChanged =
212
- !ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
213
- for (const segment of segments) {
214
- if (
215
- !ctx.isFullMatch &&
216
- !paramsChanged &&
217
- ctx.clientSegmentSet.has(segment.id)
218
- ) {
219
- segment.component = null;
220
- segment.loading = undefined;
221
- }
222
- yield segment;
223
- }
224
-
225
- // Resolve loaders fresh (loaders are never pre-rendered/cached)
226
- const ms = ctx.metricsStore;
227
168
  const loaderStart = performance.now();
228
169
 
229
170
  if (ctx.isFullMatch) {
@@ -251,6 +192,8 @@ async function* yieldFromStore<TEnv>(
251
192
  ctx.url,
252
193
  ctx.routeKey,
253
194
  ctx.actionContext,
195
+ // Loaders are never cached in the segment cache, so segment staleness
196
+ // must not propagate; browser-sent staleness (ctx.stale) still must.
254
197
  ctx.stale || undefined,
255
198
  ),
256
199
  );
@@ -282,6 +225,115 @@ async function* yieldFromStore<TEnv>(
282
225
  }
283
226
  }
284
227
 
228
+ async function* yieldFromStore<TEnv>(
229
+ entry: PrerenderEntry,
230
+ ctx: MatchContext<TEnv>,
231
+ state: MatchPipelineState,
232
+ pipelineStart: number,
233
+ handleStoreRef?: HandleStore,
234
+ ): AsyncGenerator<ResolvedSegment> {
235
+ const { resolveLoadersOnlyWithRevalidation, resolveLoadersOnly } =
236
+ getRouterContext<TEnv>();
237
+
238
+ if (
239
+ !_deserializeSegments ||
240
+ !_restoreHandles ||
241
+ !_decodeHandles ||
242
+ !_hashParams ||
243
+ !_lazyGetRequestContext
244
+ ) {
245
+ throw new Error("yieldFromStore called before ensurePrerenderDeps");
246
+ }
247
+
248
+ const segments = await _deserializeSegments(entry.segments);
249
+
250
+ // Replay handle data (same as runtime cache hit path). entry.handles is a
251
+ // Flight-encoded string ("" when none) — decode before restore so
252
+ // Promise/ReactNode handle values are revived, not the corrupted JSON form.
253
+ const handleStore = handleStoreRef ?? _lazyGetRequestContext()?._handleStore;
254
+ if (handleStore && entry.handles) {
255
+ const handlesRecord = await _decodeHandles(entry.handles);
256
+ if (handlesRecord) {
257
+ _restoreHandles(handlesRecord, handleStore);
258
+ }
259
+ }
260
+
261
+ state.cacheHit = true;
262
+ state.cacheSource = "prerender";
263
+ state.cachedSegments = segments;
264
+ state.cachedMatchedIds = segments.map((s) => s.id);
265
+
266
+ // Set streaming flag (once) and resolve render barrier.
267
+ const reqCtx = handleStoreRef ? undefined : _lazyGetRequestContext?.();
268
+ const barrierReqCtx = reqCtx ?? _getRequestContext();
269
+ if (barrierReqCtx) {
270
+ if (barrierReqCtx._treeHasStreaming === undefined) {
271
+ barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
272
+ }
273
+ barrierReqCtx._resolveRenderBarrier(segments);
274
+ }
275
+
276
+ // For partial navigation, nullify components the client already has
277
+ // so parent layouts stay live (client keeps its existing versions).
278
+ // When params changed (e.g., different guide slug), the segments have
279
+ // different content, so we must NOT nullify.
280
+ const paramsChanged =
281
+ !ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
282
+ for (const segment of segments) {
283
+ if (
284
+ !ctx.isFullMatch &&
285
+ !paramsChanged &&
286
+ ctx.clientSegmentSet.has(segment.id)
287
+ ) {
288
+ segment.component = null;
289
+ segment.loading = undefined;
290
+ }
291
+ yield segment;
292
+ }
293
+
294
+ // Resolve loaders fresh (loaders are never pre-rendered/cached).
295
+ yield* resolveFreshLoadersAndYield(
296
+ ctx,
297
+ state,
298
+ pipelineStart,
299
+ ctx.metricsStore,
300
+ resolveLoadersOnly,
301
+ resolveLoadersOnlyWithRevalidation,
302
+ );
303
+ }
304
+
305
+ /**
306
+ * Look up a prerendered (build-time cached) entry for the current route and, on
307
+ * a hit, yield its segments. Returns true when an entry was served (the caller
308
+ * should stop the pipeline) and false on a miss. Intercept navigations consult
309
+ * only the intercept-specific entry (`paramHash + "/i"`); a miss there falls
310
+ * through to the normal pipeline so intercept-resolution can run. Callers must
311
+ * guard on `prerenderStoreInstance` after `ensurePrerenderDeps()`.
312
+ */
313
+ async function* tryPrerenderLookup<TEnv>(
314
+ ctx: MatchContext<TEnv>,
315
+ state: MatchPipelineState,
316
+ pipelineStart: number,
317
+ handleStoreRef?: HandleStore,
318
+ ): AsyncGenerator<ResolvedSegment, boolean> {
319
+ const paramHash = _hashParams!(ctx.matched.params);
320
+ const isPassthroughPrerenderRoute = ctx.entries.some(
321
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
322
+ );
323
+ const lookupHash = ctx.isIntercept ? paramHash + "/i" : paramHash;
324
+ const entry = await prerenderStoreInstance!.get(
325
+ ctx.matched.routeKey,
326
+ lookupHash,
327
+ {
328
+ pathname: ctx.pathname,
329
+ isPassthroughRoute: isPassthroughPrerenderRoute,
330
+ },
331
+ );
332
+ if (!entry) return false;
333
+ yield* yieldFromStore(entry, ctx, state, pipelineStart, handleStoreRef);
334
+ return true;
335
+ }
336
+
285
337
  /**
286
338
  * Async generator middleware type
287
339
  */
@@ -326,73 +378,20 @@ export function withCacheLookup<TEnv>(
326
378
  resolveLoadersOnly,
327
379
  } = getRouterContext<TEnv>();
328
380
 
329
- // Prerender lookup: check build-time cached data before runtime cache.
330
- // Prerender data is available regardless of runtime cache configuration.
331
- // Skip for HMR requests — the dev prerender endpoint reads from a stale
332
- // RouterRegistry snapshot; rendering fresh ensures edits are visible.
333
381
  const isHmr = !!ctx.request.headers.get("X-RSC-HMR");
334
382
  if (!ctx.isAction && !isHmr && ctx.matched.pr) {
335
383
  await ensurePrerenderDeps();
336
384
  if (prerenderStoreInstance) {
337
- const paramHash = _hashParams!(ctx.matched.params);
338
- const isPassthroughPrerenderRoute = ctx.entries.some(
339
- (entry) => entry.type === "route" && entry.isPassthrough === true,
385
+ const served = yield* tryPrerenderLookup(
386
+ ctx,
387
+ state,
388
+ pipelineStart,
389
+ handleStoreRef,
340
390
  );
341
-
342
- if (ctx.isIntercept) {
343
- // Intercept navigation: try intercept-specific prerender entry
344
- const entry = await prerenderStoreInstance.get(
345
- ctx.matched.routeKey,
346
- paramHash + "/i",
347
- {
348
- pathname: ctx.pathname,
349
- isPassthroughRoute: isPassthroughPrerenderRoute,
350
- },
351
- );
352
- if (entry) {
353
- yield* yieldFromStore(
354
- entry,
355
- ctx,
356
- state,
357
- pipelineStart,
358
- handleStoreRef,
359
- );
360
- return;
361
- }
362
- // No intercept prerender -- fall through to normal pipeline
363
- // (skip non-intercept prerender to let intercept-resolution run)
364
- } else {
365
- // Normal navigation: existing behavior
366
- const entry = await prerenderStoreInstance.get(
367
- ctx.matched.routeKey,
368
- paramHash,
369
- {
370
- pathname: ctx.pathname,
371
- isPassthroughRoute: isPassthroughPrerenderRoute,
372
- },
373
- );
374
- if (entry) {
375
- yield* yieldFromStore(
376
- entry,
377
- ctx,
378
- state,
379
- pipelineStart,
380
- handleStoreRef,
381
- );
382
- return;
383
- }
384
- }
391
+ if (served) return;
385
392
  }
386
393
  }
387
394
 
388
- // Dev-mode static handler interception for non-Node.js runtimes.
389
- // __PRERENDER_DEV_URL is set by the Vite plugin when the RSC environment
390
- // lacks a Node.js module runner (e.g. workerd, Deno workers). In those
391
- // runtimes, handlers that depend on Node APIs like node:fs can't run
392
- // in-process. We redirect them to the /__rsc_prerender endpoint which
393
- // resolves segments in a Node.js temp server, same as prerender routes.
394
- // In Node.js dev mode this variable is undefined -- handlers run
395
- // in-process where Node APIs work, so no interception is needed.
396
395
  if (!ctx.isAction && !ctx.matched.pr && globalThis.__PRERENDER_DEV_URL) {
397
396
  const hasStatic = ctx.entries.some(
398
397
  (e) =>
@@ -404,58 +403,18 @@ export function withCacheLookup<TEnv>(
404
403
  if (hasStatic) {
405
404
  await ensurePrerenderDeps();
406
405
  if (prerenderStoreInstance) {
407
- const paramHash = _hashParams!(ctx.matched.params);
408
- const isPassthroughPrerenderRoute = ctx.entries.some(
409
- (entry) => entry.type === "route" && entry.isPassthrough === true,
406
+ const served = yield* tryPrerenderLookup(
407
+ ctx,
408
+ state,
409
+ pipelineStart,
410
+ handleStoreRef,
410
411
  );
411
-
412
- if (ctx.isIntercept) {
413
- const entry = await prerenderStoreInstance.get(
414
- ctx.matched.routeKey,
415
- paramHash + "/i",
416
- {
417
- pathname: ctx.pathname,
418
- isPassthroughRoute: isPassthroughPrerenderRoute,
419
- },
420
- );
421
- if (entry) {
422
- yield* yieldFromStore(
423
- entry,
424
- ctx,
425
- state,
426
- pipelineStart,
427
- handleStoreRef,
428
- );
429
- return;
430
- }
431
- // No intercept prerender -- fall through to normal pipeline
432
- } else {
433
- const entry = await prerenderStoreInstance.get(
434
- ctx.matched.routeKey,
435
- paramHash,
436
- {
437
- pathname: ctx.pathname,
438
- isPassthroughRoute: isPassthroughPrerenderRoute,
439
- },
440
- );
441
- if (entry) {
442
- yield* yieldFromStore(
443
- entry,
444
- ctx,
445
- state,
446
- pipelineStart,
447
- handleStoreRef,
448
- );
449
- return;
450
- }
451
- }
412
+ if (served) return;
452
413
  }
453
414
  }
454
415
  }
455
416
 
456
- // Skip cache during actions
457
417
  if (ctx.isAction || !ctx.cacheScope?.enabled) {
458
- // Cache miss - pass through to segment resolution
459
418
  yield* source;
460
419
  if (ms) {
461
420
  ms.metrics.push({
@@ -467,7 +426,6 @@ export function withCacheLookup<TEnv>(
467
426
  return;
468
427
  }
469
428
 
470
- // Lookup cache
471
429
  const cacheResult = await ctx.cacheScope.lookupRoute(
472
430
  ctx.pathname,
473
431
  ctx.matched.params,
@@ -475,7 +433,6 @@ export function withCacheLookup<TEnv>(
475
433
  );
476
434
 
477
435
  if (!cacheResult) {
478
- // Cache miss - pass through to segment resolution
479
436
  yield* source;
480
437
  if (ms) {
481
438
  ms.metrics.push({
@@ -487,16 +444,12 @@ export function withCacheLookup<TEnv>(
487
444
  return;
488
445
  }
489
446
 
490
- // Cache HIT
491
447
  state.cacheHit = true;
492
448
  state.cacheSource = "runtime";
493
449
  state.shouldRevalidate = cacheResult.shouldRevalidate;
494
450
  state.cachedSegments = cacheResult.segments;
495
451
  state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
496
452
 
497
- // Apply revalidation to cached segments.
498
- // For full matches or empty client segment sets, this map is unnecessary:
499
- // we never run segment-level revalidation and can stream segments directly.
500
453
  const canCheckSegmentRevalidation =
501
454
  !ctx.isFullMatch &&
502
455
  ctx.clientSegmentSet.size > 0 &&
@@ -506,7 +459,6 @@ export function withCacheLookup<TEnv>(
506
459
  : undefined;
507
460
 
508
461
  for (const segment of cacheResult.segments) {
509
- // Skip segments client doesn't have - they need their component
510
462
  if (!ctx.clientSegmentSet.has(segment.id)) {
511
463
  if (isTraceActive()) {
512
464
  pushRevalidationTraceEntry({
@@ -523,19 +475,13 @@ export function withCacheLookup<TEnv>(
523
475
  continue;
524
476
  }
525
477
 
526
- // Skip intercept segments - they're handled separately
527
478
  if (segment.namespace?.startsWith("intercept:")) {
528
479
  yield segment;
529
480
  continue;
530
481
  }
531
482
 
532
- // Look up revalidation rules for this segment
533
483
  const entryInfo = entryRevalidateMap?.get(segment.id);
534
484
 
535
- // Even without explicit revalidation rules, route segments and their
536
- // children must re-render when params or search params change — the
537
- // handler reads ctx.params/ctx.searchParams so different values produce
538
- // different content. Matches evaluateRevalidation's default logic.
539
485
  const searchChanged = ctx.prevUrl.search !== ctx.url.search;
540
486
  const routeParamsChanged = !paramsEqual(
541
487
  ctx.matched.params,
@@ -549,7 +495,6 @@ export function withCacheLookup<TEnv>(
549
495
 
550
496
  if (!entryInfo || entryInfo.revalidate.length === 0) {
551
497
  if (shouldDefaultRevalidate) {
552
- // Params or search params changed — must re-render even without custom rules
553
498
  if (isTraceActive()) {
554
499
  pushRevalidationTraceEntry({
555
500
  segmentId: segment.id,
@@ -566,7 +511,6 @@ export function withCacheLookup<TEnv>(
566
511
  yield segment;
567
512
  continue;
568
513
  }
569
- // No revalidation rules, use default behavior (skip if client has)
570
514
  if (isTraceActive()) {
571
515
  pushRevalidationTraceEntry({
572
516
  segmentId: segment.id,
@@ -584,7 +528,6 @@ export function withCacheLookup<TEnv>(
584
528
  continue;
585
529
  }
586
530
 
587
- // Evaluate revalidation rules
588
531
  const shouldRevalidate = await evaluateRevalidation({
589
532
  segment,
590
533
  prevParams: ctx.prevParams,
@@ -618,7 +561,6 @@ export function withCacheLookup<TEnv>(
618
561
  }
619
562
 
620
563
  if (!shouldRevalidate) {
621
- // Client has it, no revalidation needed
622
564
  segment.component = null;
623
565
  segment.loading = undefined;
624
566
  }
@@ -626,7 +568,6 @@ export function withCacheLookup<TEnv>(
626
568
  yield segment;
627
569
  }
628
570
 
629
- // Set streaming flag (once) and resolve render barrier.
630
571
  const barrierReqCtx = _getRequestContext();
631
572
  if (barrierReqCtx) {
632
573
  if (barrierReqCtx._treeHasStreaming === undefined) {
@@ -635,77 +576,15 @@ export function withCacheLookup<TEnv>(
635
576
  barrierReqCtx._resolveRenderBarrier(cacheResult.segments);
636
577
  }
637
578
 
638
- // Resolve loaders fresh (loaders are NOT cached by default)
639
- // This ensures fresh data even on cache hit
640
- const Store = ctx.Store;
641
- const loaderStart = performance.now();
642
-
643
- if (ctx.isFullMatch) {
644
- // Full match (document request) - simple loader resolution without revalidation
645
- if (resolveLoadersOnly) {
646
- const loaderSegments = await Store.run(() =>
647
- resolveLoadersOnly(ctx.entries, ctx.handlerContext),
648
- );
649
-
650
- // Update state - full match doesn't track matchedIds separately
651
- state.matchedIds = state.cachedMatchedIds!;
652
-
653
- // Yield fresh loader segments
654
- for (const segment of loaderSegments) {
655
- yield segment;
656
- }
657
- } else {
658
- state.matchedIds = state.cachedMatchedIds!;
659
- }
660
- } else {
661
- // Partial match (navigation) - loader resolution with revalidation
662
- if (resolveLoadersOnlyWithRevalidation) {
663
- const loaderResult = await Store.run(() =>
664
- resolveLoadersOnlyWithRevalidation(
665
- ctx.entries,
666
- ctx.handlerContext,
667
- ctx.clientSegmentSet,
668
- ctx.prevParams,
669
- ctx.request,
670
- ctx.prevUrl,
671
- ctx.url,
672
- ctx.routeKey,
673
- ctx.actionContext,
674
- // Loaders are never cached in the segment cache, so segment
675
- // staleness (cacheResult.shouldRevalidate) must not propagate.
676
- // But browser-sent staleness (ctx.stale) — indicating an action
677
- // happened in this or another tab — must still reach loaders.
678
- ctx.stale || undefined,
679
- ),
680
- );
681
-
682
- // Update state with fresh loader matchedIds
683
- state.matchedIds = [
684
- ...state.cachedMatchedIds!,
685
- ...loaderResult.matchedIds,
686
- ];
687
-
688
- // Yield fresh loader segments
689
- for (const segment of loaderResult.segments) {
690
- yield segment;
691
- }
692
- } else {
693
- state.matchedIds = state.cachedMatchedIds!;
694
- }
695
- }
696
- if (ms) {
697
- const loaderEnd = performance.now();
698
- ms.metrics.push({
699
- label: "pipeline:loader-resolve",
700
- duration: loaderEnd - loaderStart,
701
- startTime: loaderStart - ms.requestStart,
702
- depth: 1,
703
- });
704
- ms.metrics.push({
705
- label: "pipeline:cache-hit",
706
- duration: loaderEnd - pipelineStart,
707
- startTime: pipelineStart - ms.requestStart,
708
- });
709
- }
579
+ // Resolve loaders fresh (loaders are never cached). Shared with the
580
+ // prerender-store path via resolveFreshLoadersAndYield.
581
+ yield* resolveFreshLoadersAndYield(
582
+ ctx,
583
+ state,
584
+ pipelineStart,
585
+ ms,
586
+ resolveLoadersOnly,
587
+ resolveLoadersOnlyWithRevalidation,
588
+ );
710
589
  };
711
590
  }