@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.81

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 (316) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +5091 -941
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +61 -52
  7. package/skills/breadcrumbs/SKILL.md +250 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +167 -0
  14. package/skills/handler-use/SKILL.md +362 -0
  15. package/skills/hooks/SKILL.md +340 -72
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/intercept/SKILL.md +151 -8
  18. package/skills/layout/SKILL.md +122 -3
  19. package/skills/links/SKILL.md +92 -31
  20. package/skills/loader/SKILL.md +404 -44
  21. package/skills/middleware/SKILL.md +205 -37
  22. package/skills/migrate-nextjs/SKILL.md +560 -0
  23. package/skills/migrate-react-router/SKILL.md +765 -0
  24. package/skills/mime-routes/SKILL.md +128 -0
  25. package/skills/parallel/SKILL.md +263 -1
  26. package/skills/prerender/SKILL.md +685 -0
  27. package/skills/rango/SKILL.md +87 -16
  28. package/skills/response-routes/SKILL.md +411 -0
  29. package/skills/route/SKILL.md +281 -14
  30. package/skills/router-setup/SKILL.md +210 -32
  31. package/skills/tailwind/SKILL.md +129 -0
  32. package/skills/theme/SKILL.md +9 -8
  33. package/skills/typesafety/SKILL.md +328 -89
  34. package/skills/use-cache/SKILL.md +324 -0
  35. package/src/__internal.ts +102 -4
  36. package/src/bin/rango.ts +321 -0
  37. package/src/browser/action-coordinator.ts +97 -0
  38. package/src/browser/action-response-classifier.ts +99 -0
  39. package/src/browser/app-version.ts +14 -0
  40. package/src/browser/event-controller.ts +92 -64
  41. package/src/browser/history-state.ts +80 -0
  42. package/src/browser/intercept-utils.ts +52 -0
  43. package/src/browser/link-interceptor.ts +24 -4
  44. package/src/browser/logging.ts +55 -0
  45. package/src/browser/merge-segment-loaders.ts +20 -12
  46. package/src/browser/navigation-bridge.ts +317 -560
  47. package/src/browser/navigation-client.ts +206 -68
  48. package/src/browser/navigation-store.ts +73 -55
  49. package/src/browser/navigation-transaction.ts +297 -0
  50. package/src/browser/network-error-handler.ts +61 -0
  51. package/src/browser/partial-update.ts +343 -316
  52. package/src/browser/prefetch/cache.ts +216 -0
  53. package/src/browser/prefetch/fetch.ts +206 -0
  54. package/src/browser/prefetch/observer.ts +65 -0
  55. package/src/browser/prefetch/policy.ts +48 -0
  56. package/src/browser/prefetch/queue.ts +160 -0
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +112 -0
  59. package/src/browser/react/Link.tsx +253 -74
  60. package/src/browser/react/NavigationProvider.tsx +91 -11
  61. package/src/browser/react/context.ts +11 -0
  62. package/src/browser/react/filter-segment-order.ts +11 -0
  63. package/src/browser/react/index.ts +12 -12
  64. package/src/browser/react/location-state-shared.ts +95 -53
  65. package/src/browser/react/location-state.ts +60 -15
  66. package/src/browser/react/mount-context.ts +6 -1
  67. package/src/browser/react/nonce-context.ts +23 -0
  68. package/src/browser/react/shallow-equal.ts +27 -0
  69. package/src/browser/react/use-action.ts +29 -51
  70. package/src/browser/react/use-client-cache.ts +5 -3
  71. package/src/browser/react/use-handle.ts +30 -126
  72. package/src/browser/react/use-href.tsx +2 -2
  73. package/src/browser/react/use-link-status.ts +6 -5
  74. package/src/browser/react/use-navigation.ts +44 -65
  75. package/src/browser/react/use-params.ts +75 -0
  76. package/src/browser/react/use-pathname.ts +47 -0
  77. package/src/browser/react/use-router.ts +76 -0
  78. package/src/browser/react/use-search-params.ts +56 -0
  79. package/src/browser/react/use-segments.ts +80 -97
  80. package/src/browser/response-adapter.ts +73 -0
  81. package/src/browser/rsc-router.tsx +214 -58
  82. package/src/browser/scroll-restoration.ts +127 -52
  83. package/src/browser/segment-reconciler.ts +243 -0
  84. package/src/browser/segment-structure-assert.ts +16 -0
  85. package/src/browser/server-action-bridge.ts +510 -603
  86. package/src/browser/shallow.ts +6 -1
  87. package/src/browser/types.ts +141 -48
  88. package/src/browser/validate-redirect-origin.ts +29 -0
  89. package/src/build/generate-manifest.ts +235 -24
  90. package/src/build/generate-route-types.ts +39 -0
  91. package/src/build/index.ts +13 -0
  92. package/src/build/route-trie.ts +291 -0
  93. package/src/build/route-types/ast-helpers.ts +25 -0
  94. package/src/build/route-types/ast-route-extraction.ts +98 -0
  95. package/src/build/route-types/codegen.ts +102 -0
  96. package/src/build/route-types/include-resolution.ts +418 -0
  97. package/src/build/route-types/param-extraction.ts +48 -0
  98. package/src/build/route-types/per-module-writer.ts +128 -0
  99. package/src/build/route-types/router-processing.ts +618 -0
  100. package/src/build/route-types/scan-filter.ts +85 -0
  101. package/src/build/runtime-discovery.ts +231 -0
  102. package/src/cache/background-task.ts +34 -0
  103. package/src/cache/cache-key-utils.ts +44 -0
  104. package/src/cache/cache-policy.ts +125 -0
  105. package/src/cache/cache-runtime.ts +342 -0
  106. package/src/cache/cache-scope.ts +167 -309
  107. package/src/cache/cf/cf-cache-store.ts +571 -17
  108. package/src/cache/cf/index.ts +13 -3
  109. package/src/cache/document-cache.ts +116 -77
  110. package/src/cache/handle-capture.ts +81 -0
  111. package/src/cache/handle-snapshot.ts +41 -0
  112. package/src/cache/index.ts +1 -15
  113. package/src/cache/memory-segment-store.ts +191 -13
  114. package/src/cache/profile-registry.ts +73 -0
  115. package/src/cache/read-through-swr.ts +134 -0
  116. package/src/cache/segment-codec.ts +256 -0
  117. package/src/cache/taint.ts +153 -0
  118. package/src/cache/types.ts +72 -122
  119. package/src/client.rsc.tsx +3 -1
  120. package/src/client.tsx +135 -301
  121. package/src/component-utils.ts +4 -4
  122. package/src/components/DefaultDocument.tsx +5 -1
  123. package/src/context-var.ts +156 -0
  124. package/src/debug.ts +19 -9
  125. package/src/errors.ts +108 -2
  126. package/src/handle.ts +55 -29
  127. package/src/handles/MetaTags.tsx +73 -20
  128. package/src/handles/breadcrumbs.ts +66 -0
  129. package/src/handles/index.ts +1 -0
  130. package/src/handles/meta.ts +30 -13
  131. package/src/host/cookie-handler.ts +21 -15
  132. package/src/host/errors.ts +8 -8
  133. package/src/host/index.ts +4 -7
  134. package/src/host/pattern-matcher.ts +27 -27
  135. package/src/host/router.ts +61 -39
  136. package/src/host/testing.ts +8 -8
  137. package/src/host/types.ts +15 -7
  138. package/src/host/utils.ts +1 -1
  139. package/src/href-client.ts +119 -29
  140. package/src/index.rsc.ts +155 -19
  141. package/src/index.ts +251 -30
  142. package/src/internal-debug.ts +11 -0
  143. package/src/loader.rsc.ts +26 -157
  144. package/src/loader.ts +27 -10
  145. package/src/network-error-thrower.tsx +3 -1
  146. package/src/outlet-provider.tsx +45 -0
  147. package/src/prerender/param-hash.ts +37 -0
  148. package/src/prerender/store.ts +186 -0
  149. package/src/prerender.ts +524 -0
  150. package/src/reverse.ts +354 -0
  151. package/src/root-error-boundary.tsx +41 -29
  152. package/src/route-content-wrapper.tsx +7 -4
  153. package/src/route-definition/dsl-helpers.ts +1121 -0
  154. package/src/route-definition/helper-factories.ts +200 -0
  155. package/src/route-definition/helpers-types.ts +478 -0
  156. package/src/route-definition/index.ts +55 -0
  157. package/src/route-definition/redirect.ts +101 -0
  158. package/src/route-definition/resolve-handler-use.ts +149 -0
  159. package/src/route-definition.ts +1 -1428
  160. package/src/route-map-builder.ts +217 -123
  161. package/src/route-name.ts +53 -0
  162. package/src/route-types.ts +77 -8
  163. package/src/router/content-negotiation.ts +215 -0
  164. package/src/router/debug-manifest.ts +72 -0
  165. package/src/router/error-handling.ts +9 -9
  166. package/src/router/find-match.ts +160 -0
  167. package/src/router/handler-context.ts +438 -86
  168. package/src/router/intercept-resolution.ts +402 -0
  169. package/src/router/lazy-includes.ts +237 -0
  170. package/src/router/loader-resolution.ts +356 -128
  171. package/src/router/logging.ts +251 -0
  172. package/src/router/manifest.ts +163 -35
  173. package/src/router/match-api.ts +555 -0
  174. package/src/router/match-context.ts +5 -3
  175. package/src/router/match-handlers.ts +440 -0
  176. package/src/router/match-middleware/background-revalidation.ts +108 -93
  177. package/src/router/match-middleware/cache-lookup.ts +460 -10
  178. package/src/router/match-middleware/cache-store.ts +98 -26
  179. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  180. package/src/router/match-middleware/segment-resolution.ts +80 -6
  181. package/src/router/match-pipelines.ts +10 -45
  182. package/src/router/match-result.ts +135 -35
  183. package/src/router/metrics.ts +240 -15
  184. package/src/router/middleware-cookies.ts +55 -0
  185. package/src/router/middleware-types.ts +220 -0
  186. package/src/router/middleware.ts +324 -369
  187. package/src/router/navigation-snapshot.ts +182 -0
  188. package/src/router/pattern-matching.ts +211 -43
  189. package/src/router/prerender-match.ts +502 -0
  190. package/src/router/preview-match.ts +98 -0
  191. package/src/router/request-classification.ts +310 -0
  192. package/src/router/revalidation.ts +137 -38
  193. package/src/router/route-snapshot.ts +245 -0
  194. package/src/router/router-context.ts +41 -21
  195. package/src/router/router-interfaces.ts +484 -0
  196. package/src/router/router-options.ts +618 -0
  197. package/src/router/router-registry.ts +24 -0
  198. package/src/router/segment-resolution/fresh.ts +748 -0
  199. package/src/router/segment-resolution/helpers.ts +268 -0
  200. package/src/router/segment-resolution/loader-cache.ts +199 -0
  201. package/src/router/segment-resolution/revalidation.ts +1379 -0
  202. package/src/router/segment-resolution/static-store.ts +67 -0
  203. package/src/router/segment-resolution.ts +21 -0
  204. package/src/router/segment-wrappers.ts +291 -0
  205. package/src/router/telemetry-otel.ts +299 -0
  206. package/src/router/telemetry.ts +300 -0
  207. package/src/router/timeout.ts +148 -0
  208. package/src/router/trie-matching.ts +239 -0
  209. package/src/router/types.ts +78 -3
  210. package/src/router.ts +740 -4252
  211. package/src/rsc/handler-context.ts +45 -0
  212. package/src/rsc/handler.ts +907 -797
  213. package/src/rsc/helpers.ts +140 -6
  214. package/src/rsc/index.ts +0 -20
  215. package/src/rsc/loader-fetch.ts +229 -0
  216. package/src/rsc/manifest-init.ts +90 -0
  217. package/src/rsc/nonce.ts +14 -0
  218. package/src/rsc/origin-guard.ts +141 -0
  219. package/src/rsc/progressive-enhancement.ts +393 -0
  220. package/src/rsc/response-error.ts +37 -0
  221. package/src/rsc/response-route-handler.ts +347 -0
  222. package/src/rsc/rsc-rendering.ts +246 -0
  223. package/src/rsc/runtime-warnings.ts +42 -0
  224. package/src/rsc/server-action.ts +358 -0
  225. package/src/rsc/ssr-setup.ts +128 -0
  226. package/src/rsc/types.ts +46 -11
  227. package/src/search-params.ts +230 -0
  228. package/src/segment-content-promise.ts +67 -0
  229. package/src/segment-loader-promise.ts +122 -0
  230. package/src/segment-system.tsx +134 -36
  231. package/src/server/context.ts +341 -61
  232. package/src/server/cookie-store.ts +190 -0
  233. package/src/server/fetchable-loader-store.ts +37 -0
  234. package/src/server/handle-store.ts +113 -15
  235. package/src/server/loader-registry.ts +24 -64
  236. package/src/server/request-context.ts +607 -81
  237. package/src/server.ts +35 -130
  238. package/src/ssr/index.tsx +103 -30
  239. package/src/static-handler.ts +126 -0
  240. package/src/theme/ThemeProvider.tsx +21 -15
  241. package/src/theme/ThemeScript.tsx +5 -5
  242. package/src/theme/constants.ts +5 -2
  243. package/src/theme/index.ts +4 -14
  244. package/src/theme/theme-context.ts +4 -30
  245. package/src/theme/theme-script.ts +21 -18
  246. package/src/types/boundaries.ts +158 -0
  247. package/src/types/cache-types.ts +198 -0
  248. package/src/types/error-types.ts +192 -0
  249. package/src/types/global-namespace.ts +100 -0
  250. package/src/types/handler-context.ts +791 -0
  251. package/src/types/index.ts +88 -0
  252. package/src/types/loader-types.ts +210 -0
  253. package/src/types/route-config.ts +170 -0
  254. package/src/types/route-entry.ts +120 -0
  255. package/src/types/segments.ts +150 -0
  256. package/src/types.ts +1 -1623
  257. package/src/urls/include-helper.ts +207 -0
  258. package/src/urls/index.ts +53 -0
  259. package/src/urls/path-helper-types.ts +372 -0
  260. package/src/urls/path-helper.ts +364 -0
  261. package/src/urls/pattern-types.ts +107 -0
  262. package/src/urls/response-types.ts +116 -0
  263. package/src/urls/type-extraction.ts +372 -0
  264. package/src/urls/urls-function.ts +98 -0
  265. package/src/urls.ts +1 -802
  266. package/src/use-loader.tsx +161 -81
  267. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  268. package/src/vite/discovery/discover-routers.ts +348 -0
  269. package/src/vite/discovery/prerender-collection.ts +439 -0
  270. package/src/vite/discovery/route-types-writer.ts +258 -0
  271. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  272. package/src/vite/discovery/state.ts +117 -0
  273. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  274. package/src/vite/index.ts +15 -1133
  275. package/src/vite/plugin-types.ts +103 -0
  276. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  277. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  278. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  279. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  280. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  281. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  282. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  283. package/src/vite/plugins/expose-id-utils.ts +299 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  290. package/src/vite/plugins/performance-tracks.ts +88 -0
  291. package/src/vite/plugins/refresh-cmd.ts +127 -0
  292. package/src/vite/plugins/use-cache-transform.ts +323 -0
  293. package/src/vite/plugins/version-injector.ts +83 -0
  294. package/src/vite/plugins/version-plugin.ts +266 -0
  295. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +462 -0
  298. package/src/vite/router-discovery.ts +977 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  304. package/src/vite/utils/prerender-utils.ts +221 -0
  305. package/src/vite/utils/shared-utils.ts +170 -0
  306. package/CLAUDE.md +0 -43
  307. package/src/browser/lru-cache.ts +0 -69
  308. package/src/browser/request-controller.ts +0 -164
  309. package/src/cache/memory-store.ts +0 -253
  310. package/src/href-context.ts +0 -33
  311. package/src/href.ts +0 -255
  312. package/src/server/route-manifest-cache.ts +0 -173
  313. package/src/vite/expose-handle-id.ts +0 -209
  314. package/src/vite/expose-loader-id.ts +0 -426
  315. package/src/vite/expose-location-state-id.ts +0 -177
  316. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -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
@@ -92,12 +94,199 @@
92
94
  import type { ResolvedSegment } from "../../types.js";
93
95
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
94
96
  import { getRouterContext } from "../router-context.js";
97
+ import { resolveSink, safeEmit } from "../telemetry.js";
98
+ import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
99
+ import { treeHasStreaming } from "./segment-resolution.js";
100
+ import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
101
+ import type { HandleStore } from "../../server/handle-store.js";
102
+ import {
103
+ getRequestContext,
104
+ _getRequestContext,
105
+ } from "../../server/request-context.js";
106
+
107
+ // Lazily initialized prerender store singleton and dynamically imported deps.
108
+ // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
109
+ // top-level, which breaks vitest (only URLs with file:, data:, node: schemes).
110
+ let prerenderStoreInstance: PrerenderStore | null | undefined;
111
+ let _deserializeSegments:
112
+ | typeof import("../../cache/segment-codec.js").deserializeSegments
113
+ | undefined;
114
+ let _restoreHandles:
115
+ | typeof import("../../cache/handle-snapshot.js").restoreHandles
116
+ | undefined;
117
+ let _hashParams:
118
+ | typeof import("../../prerender/param-hash.js").hashParams
119
+ | undefined;
120
+ let _lazyGetRequestContext:
121
+ | typeof import("../../server/request-context.js").getRequestContext
122
+ | undefined;
123
+
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
+ async function ensurePrerenderDeps() {
141
+ if (!_deserializeSegments) {
142
+ const [codec, snapshot, paramHash, reqCtx, store] = await Promise.all([
143
+ import("../../cache/segment-codec.js"),
144
+ import("../../cache/handle-snapshot.js"),
145
+ import("../../prerender/param-hash.js"),
146
+ import("../../server/request-context.js"),
147
+ import("../../prerender/store.js"),
148
+ ]);
149
+ _deserializeSegments = codec.deserializeSegments;
150
+ _restoreHandles = snapshot.restoreHandles;
151
+ _hashParams = paramHash.hashParams;
152
+ _lazyGetRequestContext = reqCtx.getRequestContext;
153
+ if (prerenderStoreInstance === undefined) {
154
+ prerenderStoreInstance = store.createPrerenderStore();
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Shared yield logic for prerender and static handler store entries.
161
+ * Deserializes segments, replays handle data, yields segments with partial
162
+ * navigation nullification, and resolves fresh loaders.
163
+ */
164
+ async function* yieldFromStore<TEnv>(
165
+ entry: PrerenderEntry,
166
+ ctx: MatchContext<TEnv>,
167
+ state: MatchPipelineState,
168
+ pipelineStart: number,
169
+ handleStoreRef?: HandleStore,
170
+ ): 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
+ const loaderStart = performance.now();
228
+
229
+ if (ctx.isFullMatch) {
230
+ if (resolveLoadersOnly) {
231
+ const loaderSegments = await ctx.Store.run(() =>
232
+ resolveLoadersOnly(ctx.entries, ctx.handlerContext),
233
+ );
234
+ state.matchedIds = state.cachedMatchedIds!;
235
+ for (const segment of loaderSegments) {
236
+ yield segment;
237
+ }
238
+ } else {
239
+ state.matchedIds = state.cachedMatchedIds!;
240
+ }
241
+ } else {
242
+ if (resolveLoadersOnlyWithRevalidation) {
243
+ const loaderResult = await ctx.Store.run(() =>
244
+ resolveLoadersOnlyWithRevalidation(
245
+ ctx.entries,
246
+ ctx.handlerContext,
247
+ ctx.clientSegmentSet,
248
+ ctx.prevParams,
249
+ ctx.request,
250
+ ctx.prevUrl,
251
+ ctx.url,
252
+ ctx.routeKey,
253
+ ctx.actionContext,
254
+ ctx.stale || undefined,
255
+ ),
256
+ );
257
+ state.matchedIds = [
258
+ ...state.cachedMatchedIds!,
259
+ ...loaderResult.matchedIds,
260
+ ];
261
+ for (const segment of loaderResult.segments) {
262
+ yield segment;
263
+ }
264
+ } else {
265
+ state.matchedIds = state.cachedMatchedIds!;
266
+ }
267
+ }
268
+
269
+ if (ms) {
270
+ const loaderEnd = performance.now();
271
+ ms.metrics.push({
272
+ label: "pipeline:loader-resolve",
273
+ duration: loaderEnd - loaderStart,
274
+ startTime: loaderStart - ms.requestStart,
275
+ depth: 1,
276
+ });
277
+ ms.metrics.push({
278
+ label: "pipeline:cache-hit",
279
+ duration: loaderEnd - pipelineStart,
280
+ startTime: pipelineStart - ms.requestStart,
281
+ });
282
+ }
283
+ }
95
284
 
96
285
  /**
97
286
  * Async generator middleware type
98
287
  */
99
288
  export type GeneratorMiddleware<T> = (
100
- source: AsyncGenerator<T>
289
+ source: AsyncGenerator<T>,
101
290
  ) => AsyncGenerator<T>;
102
291
 
103
292
  /**
@@ -115,11 +304,21 @@ export type GeneratorMiddleware<T> = (
115
304
  */
116
305
  export function withCacheLookup<TEnv>(
117
306
  ctx: MatchContext<TEnv>,
118
- state: MatchPipelineState
307
+ state: MatchPipelineState,
119
308
  ): GeneratorMiddleware<ResolvedSegment> {
120
309
  return async function* (
121
- source: AsyncGenerator<ResolvedSegment>
310
+ source: AsyncGenerator<ResolvedSegment>,
122
311
  ): AsyncGenerator<ResolvedSegment> {
312
+ const pipelineStart = performance.now();
313
+ const ms = ctx.metricsStore;
314
+
315
+ // Eagerly capture the HandleStore before any async operations.
316
+ // In workerd/Cloudflare, dynamic imports and fetch() inside the pipeline
317
+ // can disrupt AsyncLocalStorage, causing getRequestContext() to return
318
+ // undefined afterward. Capturing the reference early ensures handle replay
319
+ // and handler handle-push work regardless of ALS state.
320
+ const handleStoreRef = _getRequestContext()?._handleStore;
321
+
123
322
  const {
124
323
  evaluateRevalidation,
125
324
  buildEntryRevalidateMap,
@@ -127,10 +326,144 @@ export function withCacheLookup<TEnv>(
127
326
  resolveLoadersOnly,
128
327
  } = getRouterContext<TEnv>();
129
328
 
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
+ const isHmr = !!ctx.request.headers.get("X-RSC-HMR");
334
+ if (!ctx.isAction && !isHmr && ctx.matched.pr) {
335
+ await ensurePrerenderDeps();
336
+ if (prerenderStoreInstance) {
337
+ const paramHash = _hashParams!(ctx.matched.params);
338
+ const isPassthroughPrerenderRoute = ctx.entries.some(
339
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
340
+ );
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
+ }
385
+ }
386
+ }
387
+
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
+ if (!ctx.isAction && !ctx.matched.pr && globalThis.__PRERENDER_DEV_URL) {
397
+ const hasStatic = ctx.entries.some(
398
+ (e) =>
399
+ (e.type === "layout" ||
400
+ e.type === "route" ||
401
+ e.type === "parallel") &&
402
+ e.isStaticPrerender,
403
+ );
404
+ if (hasStatic) {
405
+ await ensurePrerenderDeps();
406
+ if (prerenderStoreInstance) {
407
+ const paramHash = _hashParams!(ctx.matched.params);
408
+ const isPassthroughPrerenderRoute = ctx.entries.some(
409
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
410
+ );
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
+ }
452
+ }
453
+ }
454
+ }
455
+
130
456
  // Skip cache during actions
131
457
  if (ctx.isAction || !ctx.cacheScope?.enabled) {
132
458
  // Cache miss - pass through to segment resolution
133
459
  yield* source;
460
+ if (ms) {
461
+ ms.metrics.push({
462
+ label: "pipeline:cache-miss",
463
+ duration: performance.now() - pipelineStart,
464
+ startTime: pipelineStart - ms.requestStart,
465
+ });
466
+ }
134
467
  return;
135
468
  }
136
469
 
@@ -138,27 +471,54 @@ export function withCacheLookup<TEnv>(
138
471
  const cacheResult = await ctx.cacheScope.lookupRoute(
139
472
  ctx.pathname,
140
473
  ctx.matched.params,
141
- ctx.isIntercept
474
+ ctx.isIntercept,
142
475
  );
143
476
 
144
477
  if (!cacheResult) {
145
478
  // Cache miss - pass through to segment resolution
146
479
  yield* source;
480
+ if (ms) {
481
+ ms.metrics.push({
482
+ label: "pipeline:cache-miss",
483
+ duration: performance.now() - pipelineStart,
484
+ startTime: pipelineStart - ms.requestStart,
485
+ });
486
+ }
147
487
  return;
148
488
  }
149
489
 
150
490
  // Cache HIT
151
491
  state.cacheHit = true;
492
+ state.cacheSource = "runtime";
152
493
  state.shouldRevalidate = cacheResult.shouldRevalidate;
153
494
  state.cachedSegments = cacheResult.segments;
154
495
  state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
155
496
 
156
- // Apply revalidation to cached segments
157
- const entryRevalidateMap = buildEntryRevalidateMap?.(ctx.entries);
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
+ const canCheckSegmentRevalidation =
501
+ !ctx.isFullMatch &&
502
+ ctx.clientSegmentSet.size > 0 &&
503
+ !!buildEntryRevalidateMap;
504
+ const entryRevalidateMap = canCheckSegmentRevalidation
505
+ ? buildEntryRevalidateMap(ctx.entries)
506
+ : undefined;
158
507
 
159
508
  for (const segment of cacheResult.segments) {
160
509
  // Skip segments client doesn't have - they need their component
161
510
  if (!ctx.clientSegmentSet.has(segment.id)) {
511
+ if (isTraceActive()) {
512
+ pushRevalidationTraceEntry({
513
+ segmentId: segment.id,
514
+ segmentType: segment.type,
515
+ belongsToRoute: segment.belongsToRoute ?? false,
516
+ source: "cache-hit",
517
+ defaultShouldRevalidate: true,
518
+ finalShouldRevalidate: true,
519
+ reason: "new-segment",
520
+ });
521
+ }
162
522
  yield segment;
163
523
  continue;
164
524
  }
@@ -171,8 +531,53 @@ export function withCacheLookup<TEnv>(
171
531
 
172
532
  // Look up revalidation rules for this segment
173
533
  const entryInfo = entryRevalidateMap?.get(segment.id);
534
+
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
+ const searchChanged = ctx.prevUrl.search !== ctx.url.search;
540
+ const routeParamsChanged = !paramsEqual(
541
+ ctx.matched.params,
542
+ ctx.prevParams,
543
+ );
544
+ const shouldDefaultRevalidate =
545
+ (searchChanged || routeParamsChanged) &&
546
+ (segment.type === "route" ||
547
+ (segment.belongsToRoute &&
548
+ (segment.type === "layout" || segment.type === "parallel")));
549
+
174
550
  if (!entryInfo || entryInfo.revalidate.length === 0) {
551
+ if (shouldDefaultRevalidate) {
552
+ // Params or search params changed — must re-render even without custom rules
553
+ if (isTraceActive()) {
554
+ pushRevalidationTraceEntry({
555
+ segmentId: segment.id,
556
+ segmentType: segment.type,
557
+ belongsToRoute: segment.belongsToRoute ?? false,
558
+ source: "cache-hit",
559
+ defaultShouldRevalidate: true,
560
+ finalShouldRevalidate: true,
561
+ reason: routeParamsChanged
562
+ ? "cached-params-changed"
563
+ : "cached-search-changed",
564
+ });
565
+ }
566
+ yield segment;
567
+ continue;
568
+ }
175
569
  // No revalidation rules, use default behavior (skip if client has)
570
+ if (isTraceActive()) {
571
+ pushRevalidationTraceEntry({
572
+ segmentId: segment.id,
573
+ segmentType: segment.type,
574
+ belongsToRoute: segment.belongsToRoute ?? false,
575
+ source: "cache-hit",
576
+ defaultShouldRevalidate: false,
577
+ finalShouldRevalidate: false,
578
+ reason: "cached-no-rules",
579
+ });
580
+ }
176
581
  segment.component = null;
177
582
  segment.loading = undefined;
178
583
  yield segment;
@@ -194,8 +599,24 @@ export function withCacheLookup<TEnv>(
194
599
  routeKey: ctx.routeKey,
195
600
  context: ctx.handlerContext,
196
601
  actionContext: ctx.actionContext,
602
+ stale: cacheResult.shouldRevalidate || ctx.stale || undefined,
603
+ traceSource: "cache-hit",
197
604
  });
198
605
 
606
+ const routerCtx = getRouterContext<TEnv>();
607
+ if (routerCtx.telemetry) {
608
+ const tSink = resolveSink(routerCtx.telemetry);
609
+ safeEmit(tSink, {
610
+ type: "revalidation.decision",
611
+ timestamp: performance.now(),
612
+ requestId: routerCtx.requestId,
613
+ segmentId: segment.id,
614
+ pathname: ctx.pathname,
615
+ routeKey: ctx.routeKey,
616
+ shouldRevalidate,
617
+ });
618
+ }
619
+
199
620
  if (!shouldRevalidate) {
200
621
  // Client has it, no revalidation needed
201
622
  segment.component = null;
@@ -205,15 +626,25 @@ export function withCacheLookup<TEnv>(
205
626
  yield segment;
206
627
  }
207
628
 
629
+ // Set streaming flag (once) and resolve render barrier.
630
+ const barrierReqCtx = _getRequestContext();
631
+ if (barrierReqCtx) {
632
+ if (barrierReqCtx._treeHasStreaming === undefined) {
633
+ barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
634
+ }
635
+ barrierReqCtx._resolveRenderBarrier(cacheResult.segments);
636
+ }
637
+
208
638
  // Resolve loaders fresh (loaders are NOT cached by default)
209
639
  // This ensures fresh data even on cache hit
210
640
  const Store = ctx.Store;
641
+ const loaderStart = performance.now();
211
642
 
212
643
  if (ctx.isFullMatch) {
213
644
  // Full match (document request) - simple loader resolution without revalidation
214
645
  if (resolveLoadersOnly) {
215
646
  const loaderSegments = await Store.run(() =>
216
- resolveLoadersOnly(ctx.entries, ctx.handlerContext)
647
+ resolveLoadersOnly(ctx.entries, ctx.handlerContext),
217
648
  );
218
649
 
219
650
  // Update state - full match doesn't track matchedIds separately
@@ -239,8 +670,13 @@ export function withCacheLookup<TEnv>(
239
670
  ctx.prevUrl,
240
671
  ctx.url,
241
672
  ctx.routeKey,
242
- ctx.actionContext
243
- )
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
+ ),
244
680
  );
245
681
 
246
682
  // Update state with fresh loader matchedIds
@@ -257,5 +693,19 @@ export function withCacheLookup<TEnv>(
257
693
  state.matchedIds = state.cachedMatchedIds!;
258
694
  }
259
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
+ }
260
710
  };
261
711
  }