@rangojs/router 0.0.0-experimental.5 → 0.0.0-experimental.50

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