@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +120 -204
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +190 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +63 -24
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +338 -126
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -70,9 +70,11 @@
70
70
  * - No segments yielded from this middleware
71
71
  *
72
72
  * Loaders:
73
- * - NEVER cached by design
73
+ * - NEVER cached in the segment cache
74
74
  * - Always resolved fresh on every request
75
75
  * - Ensures data freshness even with cached UI components
76
+ * - Segment cache staleness does NOT propagate to loader revalidation;
77
+ * loaders use their own revalidation rules (actionId, user-defined)
76
78
  *
77
79
  *
78
80
  * REVALIDATION RULES
@@ -91,15 +93,17 @@
91
93
  */
92
94
  import type { ResolvedSegment } from "../../types.js";
93
95
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
94
- import { getRouterContext } from "../router-context.js";
96
+ import { getRouterContext, type RouterContext } from "../router-context.js";
95
97
  import { resolveSink, safeEmit } from "../telemetry.js";
96
98
  import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
99
+ import { treeHasStreaming } from "./segment-resolution.js";
97
100
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
98
101
  import type { HandleStore } from "../../server/handle-store.js";
99
102
  import {
100
103
  getRequestContext,
101
104
  _getRequestContext,
102
105
  } from "../../server/request-context.js";
106
+ import { paramsEqual } from "../params-util.js";
103
107
 
104
108
  // Lazily initialized prerender store singleton and dynamically imported deps.
105
109
  // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
@@ -111,6 +115,9 @@ let _deserializeSegments:
111
115
  let _restoreHandles:
112
116
  | typeof import("../../cache/handle-snapshot.js").restoreHandles
113
117
  | undefined;
118
+ let _decodeHandles:
119
+ | typeof import("../../cache/handle-snapshot.js").decodeHandles
120
+ | undefined;
114
121
  let _hashParams:
115
122
  | typeof import("../../prerender/param-hash.js").hashParams
116
123
  | undefined;
@@ -118,22 +125,6 @@ let _lazyGetRequestContext:
118
125
  | typeof import("../../server/request-context.js").getRequestContext
119
126
  | undefined;
120
127
 
121
- function paramsEqual(
122
- a: Record<string, string>,
123
- b: Record<string, string>,
124
- ): boolean {
125
- if (a === b) return true;
126
-
127
- const keysA = Object.keys(a);
128
- if (keysA.length !== Object.keys(b).length) return false;
129
-
130
- for (const key of keysA) {
131
- if (a[key] !== b[key]) return false;
132
- }
133
-
134
- return true;
135
- }
136
-
137
128
  async function ensurePrerenderDeps() {
138
129
  if (!_deserializeSegments) {
139
130
  const [codec, snapshot, paramHash, reqCtx, store] = await Promise.all([
@@ -145,6 +136,7 @@ async function ensurePrerenderDeps() {
145
136
  ]);
146
137
  _deserializeSegments = codec.deserializeSegments;
147
138
  _restoreHandles = snapshot.restoreHandles;
139
+ _decodeHandles = snapshot.decodeHandles;
148
140
  _hashParams = paramHash.hashParams;
149
141
  _lazyGetRequestContext = reqCtx.getRequestContext;
150
142
  if (prerenderStoreInstance === undefined) {
@@ -158,58 +150,23 @@ async function ensurePrerenderDeps() {
158
150
  * Deserializes segments, replays handle data, yields segments with partial
159
151
  * navigation nullification, and resolves fresh loaders.
160
152
  */
161
- async function* yieldFromStore<TEnv>(
162
- 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>(
163
161
  ctx: MatchContext<TEnv>,
164
162
  state: MatchPipelineState,
165
163
  pipelineStart: number,
166
- handleStoreRef?: HandleStore,
164
+ ms: MatchContext<TEnv>["metricsStore"],
165
+ resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
166
+ resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
167
167
  ): AsyncGenerator<ResolvedSegment> {
168
- const { resolveLoadersOnlyWithRevalidation, resolveLoadersOnly } =
169
- getRouterContext<TEnv>();
170
-
171
- if (
172
- !_deserializeSegments ||
173
- !_restoreHandles ||
174
- !_hashParams ||
175
- !_lazyGetRequestContext
176
- ) {
177
- throw new Error("yieldFromStore called before ensurePrerenderDeps");
178
- }
179
-
180
- const segments = await _deserializeSegments(entry.segments);
181
-
182
- // Replay handle data (same as runtime cache hit path).
183
- // Prefer the eagerly-captured handleStoreRef to avoid ALS disruption in workerd.
184
- const handleStore = handleStoreRef ?? _lazyGetRequestContext()?._handleStore;
185
- if (handleStore) {
186
- _restoreHandles(entry.handles, handleStore);
187
- }
188
-
189
- state.cacheHit = true;
190
- state.cacheSource = "prerender";
191
- state.cachedSegments = segments;
192
- state.cachedMatchedIds = segments.map((s) => s.id);
193
-
194
- // For partial navigation, nullify components the client already has
195
- // so parent layouts stay live (client keeps its existing versions).
196
- // When params changed (e.g., different guide slug), the segments have
197
- // different content, so we must NOT nullify.
198
- const paramsChanged =
199
- !ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
200
- for (const segment of segments) {
201
- if (
202
- !ctx.isFullMatch &&
203
- !paramsChanged &&
204
- ctx.clientSegmentSet.has(segment.id)
205
- ) {
206
- segment.component = null;
207
- segment.loading = undefined;
208
- }
209
- yield segment;
210
- }
168
+ const loaderStart = performance.now();
211
169
 
212
- // Resolve loaders fresh (loaders are never pre-rendered/cached)
213
170
  if (ctx.isFullMatch) {
214
171
  if (resolveLoadersOnly) {
215
172
  const loaderSegments = await ctx.Store.run(() =>
@@ -235,6 +192,9 @@ async function* yieldFromStore<TEnv>(
235
192
  ctx.url,
236
193
  ctx.routeKey,
237
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.
197
+ ctx.stale || undefined,
238
198
  ),
239
199
  );
240
200
  state.matchedIds = [
@@ -249,16 +209,131 @@ async function* yieldFromStore<TEnv>(
249
209
  }
250
210
  }
251
211
 
252
- const ms = ctx.metricsStore;
253
212
  if (ms) {
213
+ const loaderEnd = performance.now();
214
+ ms.metrics.push({
215
+ label: "pipeline:loader-resolve",
216
+ duration: loaderEnd - loaderStart,
217
+ startTime: loaderStart - ms.requestStart,
218
+ depth: 1,
219
+ });
254
220
  ms.metrics.push({
255
- label: "pipeline:cache-lookup",
256
- duration: performance.now() - pipelineStart,
221
+ label: "pipeline:cache-hit",
222
+ duration: loaderEnd - pipelineStart,
257
223
  startTime: pipelineStart - ms.requestStart,
258
224
  });
259
225
  }
260
226
  }
261
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
+
262
337
  /**
263
338
  * Async generator middleware type
264
339
  */
@@ -303,72 +378,20 @@ export function withCacheLookup<TEnv>(
303
378
  resolveLoadersOnly,
304
379
  } = getRouterContext<TEnv>();
305
380
 
306
- // Prerender lookup: check build-time cached data before runtime cache.
307
- // Prerender data is available regardless of runtime cache configuration.
308
- if (!ctx.isAction && ctx.matched.pr) {
381
+ const isHmr = !!ctx.request.headers.get("X-RSC-HMR");
382
+ if (!ctx.isAction && !isHmr && ctx.matched.pr) {
309
383
  await ensurePrerenderDeps();
310
384
  if (prerenderStoreInstance) {
311
- const paramHash = _hashParams!(ctx.matched.params);
312
- const isPassthroughPrerenderRoute = ctx.entries.some(
313
- (entry) =>
314
- entry.type === "route" &&
315
- entry.prerenderDef?.options?.passthrough === true,
385
+ const served = yield* tryPrerenderLookup(
386
+ ctx,
387
+ state,
388
+ pipelineStart,
389
+ handleStoreRef,
316
390
  );
317
-
318
- if (ctx.isIntercept) {
319
- // Intercept navigation: try intercept-specific prerender entry
320
- const entry = await prerenderStoreInstance.get(
321
- ctx.matched.routeKey,
322
- paramHash + "/i",
323
- {
324
- pathname: ctx.pathname,
325
- isPassthroughRoute: isPassthroughPrerenderRoute,
326
- },
327
- );
328
- if (entry) {
329
- yield* yieldFromStore(
330
- entry,
331
- ctx,
332
- state,
333
- pipelineStart,
334
- handleStoreRef,
335
- );
336
- return;
337
- }
338
- // No intercept prerender -- fall through to normal pipeline
339
- // (skip non-intercept prerender to let intercept-resolution run)
340
- } else {
341
- // Normal navigation: existing behavior
342
- const entry = await prerenderStoreInstance.get(
343
- ctx.matched.routeKey,
344
- paramHash,
345
- {
346
- pathname: ctx.pathname,
347
- isPassthroughRoute: isPassthroughPrerenderRoute,
348
- },
349
- );
350
- if (entry) {
351
- yield* yieldFromStore(
352
- entry,
353
- ctx,
354
- state,
355
- pipelineStart,
356
- handleStoreRef,
357
- );
358
- return;
359
- }
360
- }
391
+ if (served) return;
361
392
  }
362
393
  }
363
394
 
364
- // Dev-mode static handler interception for non-Node.js runtimes.
365
- // __PRERENDER_DEV_URL is set by the Vite plugin when the RSC environment
366
- // lacks a Node.js module runner (e.g. workerd, Deno workers). In those
367
- // runtimes, handlers that depend on Node APIs like node:fs can't run
368
- // in-process. We redirect them to the /__rsc_prerender endpoint which
369
- // resolves segments in a Node.js temp server, same as prerender routes.
370
- // In Node.js dev mode this variable is undefined -- handlers run
371
- // in-process where Node APIs work, so no interception is needed.
372
395
  if (!ctx.isAction && !ctx.matched.pr && globalThis.__PRERENDER_DEV_URL) {
373
396
  const hasStatic = ctx.entries.some(
374
397
  (e) =>
@@ -380,64 +403,22 @@ export function withCacheLookup<TEnv>(
380
403
  if (hasStatic) {
381
404
  await ensurePrerenderDeps();
382
405
  if (prerenderStoreInstance) {
383
- const paramHash = _hashParams!(ctx.matched.params);
384
- const isPassthroughPrerenderRoute = ctx.entries.some(
385
- (entry) =>
386
- entry.type === "route" &&
387
- entry.prerenderDef?.options?.passthrough === true,
406
+ const served = yield* tryPrerenderLookup(
407
+ ctx,
408
+ state,
409
+ pipelineStart,
410
+ handleStoreRef,
388
411
  );
389
-
390
- if (ctx.isIntercept) {
391
- const entry = await prerenderStoreInstance.get(
392
- ctx.matched.routeKey,
393
- paramHash + "/i",
394
- {
395
- pathname: ctx.pathname,
396
- isPassthroughRoute: isPassthroughPrerenderRoute,
397
- },
398
- );
399
- if (entry) {
400
- yield* yieldFromStore(
401
- entry,
402
- ctx,
403
- state,
404
- pipelineStart,
405
- handleStoreRef,
406
- );
407
- return;
408
- }
409
- // No intercept prerender -- fall through to normal pipeline
410
- } else {
411
- const entry = await prerenderStoreInstance.get(
412
- ctx.matched.routeKey,
413
- paramHash,
414
- {
415
- pathname: ctx.pathname,
416
- isPassthroughRoute: isPassthroughPrerenderRoute,
417
- },
418
- );
419
- if (entry) {
420
- yield* yieldFromStore(
421
- entry,
422
- ctx,
423
- state,
424
- pipelineStart,
425
- handleStoreRef,
426
- );
427
- return;
428
- }
429
- }
412
+ if (served) return;
430
413
  }
431
414
  }
432
415
  }
433
416
 
434
- // Skip cache during actions
435
417
  if (ctx.isAction || !ctx.cacheScope?.enabled) {
436
- // Cache miss - pass through to segment resolution
437
418
  yield* source;
438
419
  if (ms) {
439
420
  ms.metrics.push({
440
- label: "pipeline:cache-lookup",
421
+ label: "pipeline:cache-miss",
441
422
  duration: performance.now() - pipelineStart,
442
423
  startTime: pipelineStart - ms.requestStart,
443
424
  });
@@ -445,7 +426,6 @@ export function withCacheLookup<TEnv>(
445
426
  return;
446
427
  }
447
428
 
448
- // Lookup cache
449
429
  const cacheResult = await ctx.cacheScope.lookupRoute(
450
430
  ctx.pathname,
451
431
  ctx.matched.params,
@@ -453,11 +433,10 @@ export function withCacheLookup<TEnv>(
453
433
  );
454
434
 
455
435
  if (!cacheResult) {
456
- // Cache miss - pass through to segment resolution
457
436
  yield* source;
458
437
  if (ms) {
459
438
  ms.metrics.push({
460
- label: "pipeline:cache-lookup",
439
+ label: "pipeline:cache-miss",
461
440
  duration: performance.now() - pipelineStart,
462
441
  startTime: pipelineStart - ms.requestStart,
463
442
  });
@@ -465,16 +444,12 @@ export function withCacheLookup<TEnv>(
465
444
  return;
466
445
  }
467
446
 
468
- // Cache HIT
469
447
  state.cacheHit = true;
470
448
  state.cacheSource = "runtime";
471
449
  state.shouldRevalidate = cacheResult.shouldRevalidate;
472
450
  state.cachedSegments = cacheResult.segments;
473
451
  state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
474
452
 
475
- // Apply revalidation to cached segments.
476
- // For full matches or empty client segment sets, this map is unnecessary:
477
- // we never run segment-level revalidation and can stream segments directly.
478
453
  const canCheckSegmentRevalidation =
479
454
  !ctx.isFullMatch &&
480
455
  ctx.clientSegmentSet.size > 0 &&
@@ -484,7 +459,6 @@ export function withCacheLookup<TEnv>(
484
459
  : undefined;
485
460
 
486
461
  for (const segment of cacheResult.segments) {
487
- // Skip segments client doesn't have - they need their component
488
462
  if (!ctx.clientSegmentSet.has(segment.id)) {
489
463
  if (isTraceActive()) {
490
464
  pushRevalidationTraceEntry({
@@ -501,16 +475,42 @@ export function withCacheLookup<TEnv>(
501
475
  continue;
502
476
  }
503
477
 
504
- // Skip intercept segments - they're handled separately
505
478
  if (segment.namespace?.startsWith("intercept:")) {
506
479
  yield segment;
507
480
  continue;
508
481
  }
509
482
 
510
- // Look up revalidation rules for this segment
511
483
  const entryInfo = entryRevalidateMap?.get(segment.id);
484
+
485
+ const searchChanged = ctx.prevUrl.search !== ctx.url.search;
486
+ const routeParamsChanged = !paramsEqual(
487
+ ctx.matched.params,
488
+ ctx.prevParams,
489
+ );
490
+ const shouldDefaultRevalidate =
491
+ (searchChanged || routeParamsChanged) &&
492
+ (segment.type === "route" ||
493
+ (segment.belongsToRoute &&
494
+ (segment.type === "layout" || segment.type === "parallel")));
495
+
512
496
  if (!entryInfo || entryInfo.revalidate.length === 0) {
513
- // No revalidation rules, use default behavior (skip if client has)
497
+ if (shouldDefaultRevalidate) {
498
+ if (isTraceActive()) {
499
+ pushRevalidationTraceEntry({
500
+ segmentId: segment.id,
501
+ segmentType: segment.type,
502
+ belongsToRoute: segment.belongsToRoute ?? false,
503
+ source: "cache-hit",
504
+ defaultShouldRevalidate: true,
505
+ finalShouldRevalidate: true,
506
+ reason: routeParamsChanged
507
+ ? "cached-params-changed"
508
+ : "cached-search-changed",
509
+ });
510
+ }
511
+ yield segment;
512
+ continue;
513
+ }
514
514
  if (isTraceActive()) {
515
515
  pushRevalidationTraceEntry({
516
516
  segmentId: segment.id,
@@ -528,7 +528,6 @@ export function withCacheLookup<TEnv>(
528
528
  continue;
529
529
  }
530
530
 
531
- // Evaluate revalidation rules
532
531
  const shouldRevalidate = await evaluateRevalidation({
533
532
  segment,
534
533
  prevParams: ctx.prevParams,
@@ -543,7 +542,7 @@ export function withCacheLookup<TEnv>(
543
542
  routeKey: ctx.routeKey,
544
543
  context: ctx.handlerContext,
545
544
  actionContext: ctx.actionContext,
546
- stale: cacheResult.shouldRevalidate || undefined,
545
+ stale: cacheResult.shouldRevalidate || ctx.stale || undefined,
547
546
  traceSource: "cache-hit",
548
547
  });
549
548
 
@@ -562,7 +561,6 @@ export function withCacheLookup<TEnv>(
562
561
  }
563
562
 
564
563
  if (!shouldRevalidate) {
565
- // Client has it, no revalidation needed
566
564
  segment.component = null;
567
565
  segment.loading = undefined;
568
566
  }
@@ -570,65 +568,23 @@ export function withCacheLookup<TEnv>(
570
568
  yield segment;
571
569
  }
572
570
 
573
- // Resolve loaders fresh (loaders are NOT cached by default)
574
- // This ensures fresh data even on cache hit
575
- const Store = ctx.Store;
576
-
577
- if (ctx.isFullMatch) {
578
- // Full match (document request) - simple loader resolution without revalidation
579
- if (resolveLoadersOnly) {
580
- const loaderSegments = await Store.run(() =>
581
- resolveLoadersOnly(ctx.entries, ctx.handlerContext),
582
- );
583
-
584
- // Update state - full match doesn't track matchedIds separately
585
- state.matchedIds = state.cachedMatchedIds!;
586
-
587
- // Yield fresh loader segments
588
- for (const segment of loaderSegments) {
589
- yield segment;
590
- }
591
- } else {
592
- state.matchedIds = state.cachedMatchedIds!;
593
- }
594
- } else {
595
- // Partial match (navigation) - loader resolution with revalidation
596
- if (resolveLoadersOnlyWithRevalidation) {
597
- const loaderResult = await Store.run(() =>
598
- resolveLoadersOnlyWithRevalidation(
599
- ctx.entries,
600
- ctx.handlerContext,
601
- ctx.clientSegmentSet,
602
- ctx.prevParams,
603
- ctx.request,
604
- ctx.prevUrl,
605
- ctx.url,
606
- ctx.routeKey,
607
- ctx.actionContext,
608
- cacheResult.shouldRevalidate || undefined,
609
- ),
610
- );
611
-
612
- // Update state with fresh loader matchedIds
613
- state.matchedIds = [
614
- ...state.cachedMatchedIds!,
615
- ...loaderResult.matchedIds,
616
- ];
617
-
618
- // Yield fresh loader segments
619
- for (const segment of loaderResult.segments) {
620
- yield segment;
621
- }
622
- } else {
623
- state.matchedIds = state.cachedMatchedIds!;
571
+ const barrierReqCtx = _getRequestContext();
572
+ if (barrierReqCtx) {
573
+ if (barrierReqCtx._treeHasStreaming === undefined) {
574
+ barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
624
575
  }
576
+ barrierReqCtx._resolveRenderBarrier(cacheResult.segments);
625
577
  }
626
- if (ms) {
627
- ms.metrics.push({
628
- label: "pipeline:cache-lookup",
629
- duration: performance.now() - pipelineStart,
630
- startTime: pipelineStart - ms.requestStart,
631
- });
632
- }
578
+
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
+ );
633
589
  };
634
590
  }