@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  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 +111 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -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 +469 -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 +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -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 +10 -15
  114. package/src/client.tsx +114 -135
  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 +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -92,12 +92,178 @@
92
92
  import type { ResolvedSegment } from "../../types.js";
93
93
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
94
94
  import { getRouterContext } from "../router-context.js";
95
+ import { resolveSink, safeEmit } from "../telemetry.js";
96
+ import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
97
+ import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
98
+ import type { HandleStore } from "../../server/handle-store.js";
99
+ import {
100
+ getRequestContext,
101
+ _getRequestContext,
102
+ } from "../../server/request-context.js";
103
+
104
+ // Lazily initialized prerender store singleton and dynamically imported deps.
105
+ // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
106
+ // top-level, which breaks vitest (only URLs with file:, data:, node: schemes).
107
+ let prerenderStoreInstance: PrerenderStore | null | undefined;
108
+ let _deserializeSegments:
109
+ | typeof import("../../cache/segment-codec.js").deserializeSegments
110
+ | undefined;
111
+ let _restoreHandles:
112
+ | typeof import("../../cache/handle-snapshot.js").restoreHandles
113
+ | undefined;
114
+ let _hashParams:
115
+ | typeof import("../../prerender/param-hash.js").hashParams
116
+ | undefined;
117
+ let _lazyGetRequestContext:
118
+ | typeof import("../../server/request-context.js").getRequestContext
119
+ | undefined;
120
+
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
+ async function ensurePrerenderDeps() {
138
+ if (!_deserializeSegments) {
139
+ const [codec, snapshot, paramHash, reqCtx, store] = await Promise.all([
140
+ import("../../cache/segment-codec.js"),
141
+ import("../../cache/handle-snapshot.js"),
142
+ import("../../prerender/param-hash.js"),
143
+ import("../../server/request-context.js"),
144
+ import("../../prerender/store.js"),
145
+ ]);
146
+ _deserializeSegments = codec.deserializeSegments;
147
+ _restoreHandles = snapshot.restoreHandles;
148
+ _hashParams = paramHash.hashParams;
149
+ _lazyGetRequestContext = reqCtx.getRequestContext;
150
+ if (prerenderStoreInstance === undefined) {
151
+ prerenderStoreInstance = store.createPrerenderStore();
152
+ }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Shared yield logic for prerender and static handler store entries.
158
+ * Deserializes segments, replays handle data, yields segments with partial
159
+ * navigation nullification, and resolves fresh loaders.
160
+ */
161
+ async function* yieldFromStore<TEnv>(
162
+ entry: PrerenderEntry,
163
+ ctx: MatchContext<TEnv>,
164
+ state: MatchPipelineState,
165
+ pipelineStart: number,
166
+ handleStoreRef?: HandleStore,
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
+ }
211
+
212
+ // Resolve loaders fresh (loaders are never pre-rendered/cached)
213
+ if (ctx.isFullMatch) {
214
+ if (resolveLoadersOnly) {
215
+ const loaderSegments = await ctx.Store.run(() =>
216
+ resolveLoadersOnly(ctx.entries, ctx.handlerContext),
217
+ );
218
+ state.matchedIds = state.cachedMatchedIds!;
219
+ for (const segment of loaderSegments) {
220
+ yield segment;
221
+ }
222
+ } else {
223
+ state.matchedIds = state.cachedMatchedIds!;
224
+ }
225
+ } else {
226
+ if (resolveLoadersOnlyWithRevalidation) {
227
+ const loaderResult = await ctx.Store.run(() =>
228
+ resolveLoadersOnlyWithRevalidation(
229
+ ctx.entries,
230
+ ctx.handlerContext,
231
+ ctx.clientSegmentSet,
232
+ ctx.prevParams,
233
+ ctx.request,
234
+ ctx.prevUrl,
235
+ ctx.url,
236
+ ctx.routeKey,
237
+ ctx.actionContext,
238
+ ),
239
+ );
240
+ state.matchedIds = [
241
+ ...state.cachedMatchedIds!,
242
+ ...loaderResult.matchedIds,
243
+ ];
244
+ for (const segment of loaderResult.segments) {
245
+ yield segment;
246
+ }
247
+ } else {
248
+ state.matchedIds = state.cachedMatchedIds!;
249
+ }
250
+ }
251
+
252
+ const ms = ctx.metricsStore;
253
+ if (ms) {
254
+ ms.metrics.push({
255
+ label: "pipeline:cache-lookup",
256
+ duration: performance.now() - pipelineStart,
257
+ startTime: pipelineStart - ms.requestStart,
258
+ });
259
+ }
260
+ }
95
261
 
96
262
  /**
97
263
  * Async generator middleware type
98
264
  */
99
265
  export type GeneratorMiddleware<T> = (
100
- source: AsyncGenerator<T>
266
+ source: AsyncGenerator<T>,
101
267
  ) => AsyncGenerator<T>;
102
268
 
103
269
  /**
@@ -115,11 +281,21 @@ export type GeneratorMiddleware<T> = (
115
281
  */
116
282
  export function withCacheLookup<TEnv>(
117
283
  ctx: MatchContext<TEnv>,
118
- state: MatchPipelineState
284
+ state: MatchPipelineState,
119
285
  ): GeneratorMiddleware<ResolvedSegment> {
120
286
  return async function* (
121
- source: AsyncGenerator<ResolvedSegment>
287
+ source: AsyncGenerator<ResolvedSegment>,
122
288
  ): AsyncGenerator<ResolvedSegment> {
289
+ const pipelineStart = performance.now();
290
+ const ms = ctx.metricsStore;
291
+
292
+ // Eagerly capture the HandleStore before any async operations.
293
+ // In workerd/Cloudflare, dynamic imports and fetch() inside the pipeline
294
+ // can disrupt AsyncLocalStorage, causing getRequestContext() to return
295
+ // undefined afterward. Capturing the reference early ensures handle replay
296
+ // and handler handle-push work regardless of ALS state.
297
+ const handleStoreRef = _getRequestContext()?._handleStore;
298
+
123
299
  const {
124
300
  evaluateRevalidation,
125
301
  buildEntryRevalidateMap,
@@ -127,10 +303,145 @@ export function withCacheLookup<TEnv>(
127
303
  resolveLoadersOnly,
128
304
  } = getRouterContext<TEnv>();
129
305
 
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) {
309
+ await ensurePrerenderDeps();
310
+ 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,
316
+ );
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
+ }
361
+ }
362
+ }
363
+
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
+ if (!ctx.isAction && !ctx.matched.pr && globalThis.__PRERENDER_DEV_URL) {
373
+ const hasStatic = ctx.entries.some(
374
+ (e) =>
375
+ (e.type === "layout" ||
376
+ e.type === "route" ||
377
+ e.type === "parallel") &&
378
+ e.isStaticPrerender,
379
+ );
380
+ if (hasStatic) {
381
+ await ensurePrerenderDeps();
382
+ 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,
388
+ );
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
+ }
430
+ }
431
+ }
432
+ }
433
+
130
434
  // Skip cache during actions
131
435
  if (ctx.isAction || !ctx.cacheScope?.enabled) {
132
436
  // Cache miss - pass through to segment resolution
133
437
  yield* source;
438
+ if (ms) {
439
+ ms.metrics.push({
440
+ label: "pipeline:cache-lookup",
441
+ duration: performance.now() - pipelineStart,
442
+ startTime: pipelineStart - ms.requestStart,
443
+ });
444
+ }
134
445
  return;
135
446
  }
136
447
 
@@ -138,27 +449,54 @@ export function withCacheLookup<TEnv>(
138
449
  const cacheResult = await ctx.cacheScope.lookupRoute(
139
450
  ctx.pathname,
140
451
  ctx.matched.params,
141
- ctx.isIntercept
452
+ ctx.isIntercept,
142
453
  );
143
454
 
144
455
  if (!cacheResult) {
145
456
  // Cache miss - pass through to segment resolution
146
457
  yield* source;
458
+ if (ms) {
459
+ ms.metrics.push({
460
+ label: "pipeline:cache-lookup",
461
+ duration: performance.now() - pipelineStart,
462
+ startTime: pipelineStart - ms.requestStart,
463
+ });
464
+ }
147
465
  return;
148
466
  }
149
467
 
150
468
  // Cache HIT
151
469
  state.cacheHit = true;
470
+ state.cacheSource = "runtime";
152
471
  state.shouldRevalidate = cacheResult.shouldRevalidate;
153
472
  state.cachedSegments = cacheResult.segments;
154
473
  state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
155
474
 
156
- // Apply revalidation to cached segments
157
- const entryRevalidateMap = buildEntryRevalidateMap?.(ctx.entries);
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
+ const canCheckSegmentRevalidation =
479
+ !ctx.isFullMatch &&
480
+ ctx.clientSegmentSet.size > 0 &&
481
+ !!buildEntryRevalidateMap;
482
+ const entryRevalidateMap = canCheckSegmentRevalidation
483
+ ? buildEntryRevalidateMap(ctx.entries)
484
+ : undefined;
158
485
 
159
486
  for (const segment of cacheResult.segments) {
160
487
  // Skip segments client doesn't have - they need their component
161
488
  if (!ctx.clientSegmentSet.has(segment.id)) {
489
+ if (isTraceActive()) {
490
+ pushRevalidationTraceEntry({
491
+ segmentId: segment.id,
492
+ segmentType: segment.type,
493
+ belongsToRoute: segment.belongsToRoute ?? false,
494
+ source: "cache-hit",
495
+ defaultShouldRevalidate: true,
496
+ finalShouldRevalidate: true,
497
+ reason: "new-segment",
498
+ });
499
+ }
162
500
  yield segment;
163
501
  continue;
164
502
  }
@@ -173,6 +511,17 @@ export function withCacheLookup<TEnv>(
173
511
  const entryInfo = entryRevalidateMap?.get(segment.id);
174
512
  if (!entryInfo || entryInfo.revalidate.length === 0) {
175
513
  // No revalidation rules, use default behavior (skip if client has)
514
+ if (isTraceActive()) {
515
+ pushRevalidationTraceEntry({
516
+ segmentId: segment.id,
517
+ segmentType: segment.type,
518
+ belongsToRoute: segment.belongsToRoute ?? false,
519
+ source: "cache-hit",
520
+ defaultShouldRevalidate: false,
521
+ finalShouldRevalidate: false,
522
+ reason: "cached-no-rules",
523
+ });
524
+ }
176
525
  segment.component = null;
177
526
  segment.loading = undefined;
178
527
  yield segment;
@@ -194,8 +543,24 @@ export function withCacheLookup<TEnv>(
194
543
  routeKey: ctx.routeKey,
195
544
  context: ctx.handlerContext,
196
545
  actionContext: ctx.actionContext,
546
+ stale: cacheResult.shouldRevalidate || undefined,
547
+ traceSource: "cache-hit",
197
548
  });
198
549
 
550
+ const routerCtx = getRouterContext<TEnv>();
551
+ if (routerCtx.telemetry) {
552
+ const tSink = resolveSink(routerCtx.telemetry);
553
+ safeEmit(tSink, {
554
+ type: "revalidation.decision",
555
+ timestamp: performance.now(),
556
+ requestId: routerCtx.requestId,
557
+ segmentId: segment.id,
558
+ pathname: ctx.pathname,
559
+ routeKey: ctx.routeKey,
560
+ shouldRevalidate,
561
+ });
562
+ }
563
+
199
564
  if (!shouldRevalidate) {
200
565
  // Client has it, no revalidation needed
201
566
  segment.component = null;
@@ -213,7 +578,7 @@ export function withCacheLookup<TEnv>(
213
578
  // Full match (document request) - simple loader resolution without revalidation
214
579
  if (resolveLoadersOnly) {
215
580
  const loaderSegments = await Store.run(() =>
216
- resolveLoadersOnly(ctx.entries, ctx.handlerContext)
581
+ resolveLoadersOnly(ctx.entries, ctx.handlerContext),
217
582
  );
218
583
 
219
584
  // Update state - full match doesn't track matchedIds separately
@@ -239,8 +604,9 @@ export function withCacheLookup<TEnv>(
239
604
  ctx.prevUrl,
240
605
  ctx.url,
241
606
  ctx.routeKey,
242
- ctx.actionContext
243
- )
607
+ ctx.actionContext,
608
+ cacheResult.shouldRevalidate || undefined,
609
+ ),
244
610
  );
245
611
 
246
612
  // Update state with fresh loader matchedIds
@@ -257,5 +623,12 @@ export function withCacheLookup<TEnv>(
257
623
  state.matchedIds = state.cachedMatchedIds!;
258
624
  }
259
625
  }
626
+ if (ms) {
627
+ ms.metrics.push({
628
+ label: "pipeline:cache-lookup",
629
+ duration: performance.now() - pipelineStart,
630
+ startTime: pipelineStart - ms.requestStart,
631
+ });
632
+ }
260
633
  };
261
634
  }
@@ -104,6 +104,7 @@ import type { ResolvedSegment } from "../../types.js";
104
104
  import { getRequestContext } from "../../server/request-context.js";
105
105
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
106
106
  import { getRouterContext } from "../router-context.js";
107
+ import { debugLog, debugWarn } from "../logging.js";
107
108
  import type { GeneratorMiddleware } from "./cache-lookup.js";
108
109
 
109
110
  /**
@@ -119,6 +120,9 @@ export function withCacheStore<TEnv>(
119
120
  return async function* (
120
121
  source: AsyncGenerator<ResolvedSegment>,
121
122
  ): AsyncGenerator<ResolvedSegment> {
123
+ const pipelineStart = performance.now();
124
+ const ms = ctx.metricsStore;
125
+
122
126
  // Collect all segments while passing them through
123
127
  const allSegments: ResolvedSegment[] = [];
124
128
  for await (const segment of source) {
@@ -137,14 +141,22 @@ export function withCacheStore<TEnv>(
137
141
  state.cacheHit ||
138
142
  ctx.request.method !== "GET"
139
143
  ) {
144
+ if (ms) {
145
+ ms.metrics.push({
146
+ label: "pipeline:cache-store",
147
+ duration: performance.now() - pipelineStart,
148
+ startTime: pipelineStart - ms.requestStart,
149
+ });
150
+ }
140
151
  return;
141
152
  }
142
153
 
143
154
  const {
144
155
  createHandlerContext,
145
- setupLoaderAccessSilent,
156
+ setupLoaderAccess,
146
157
  resolveAllSegments,
147
158
  resolveInterceptEntry,
159
+ createHandleStore,
148
160
  } = getRouterContext<TEnv>();
149
161
 
150
162
  // Combine main segments with intercept segments
@@ -166,9 +178,10 @@ export function withCacheStore<TEnv>(
166
178
  requestCtx.onResponse((response) => {
167
179
  // Only cache successful responses
168
180
  if (response.status !== 200) {
169
- console.log(
170
- `[CacheStore] Skipping cache: non-200 status ${response.status} for ${ctx.pathname}`,
171
- );
181
+ debugLog("cacheStore", "skipping cache for non-200 response", {
182
+ status: response.status,
183
+ pathname: ctx.pathname,
184
+ });
172
185
  return response;
173
186
  }
174
187
 
@@ -176,29 +189,34 @@ export function withCacheStore<TEnv>(
176
189
  // Proactive caching: render all segments fresh in background
177
190
  // This ensures cache has complete components for future requests
178
191
  requestCtx.waitUntil(async () => {
179
- console.log(
180
- `[Router.matchPartial] Proactive caching: ${ctx.pathname} (rendering null-component segments)`,
181
- );
192
+ debugLog("cacheStore", "proactive caching started", {
193
+ pathname: ctx.pathname,
194
+ });
195
+ // Swap to a fresh HandleStore so handle.push() calls from
196
+ // proactive resolution are captured (not silenced). The original
197
+ // store's stream is already sent by waitUntil time.
198
+ // cacheRoute reads from requestCtx._handleStore, so this ensures
199
+ // complete handle data (e.g. breadcrumbs) is cached.
200
+ const originalHandleStore = requestCtx._handleStore;
201
+ requestCtx._handleStore = createHandleStore();
182
202
  try {
183
203
  // Create fresh context for proactive caching
184
- // This prevents handle data from polluting the response stream
185
204
  const proactiveHandlerContext = createHandlerContext(
186
205
  ctx.matched.params,
187
206
  ctx.request,
188
207
  ctx.url.searchParams,
189
208
  ctx.pathname,
190
209
  ctx.url,
191
- ctx.bindings,
210
+ ctx.env,
192
211
  ctx.routeMap,
193
- ctx.matched.routeKey
212
+ ctx.matched.routeKey,
213
+ ctx.matched.responseType,
214
+ ctx.matched.pt === true,
194
215
  );
195
216
  const proactiveLoaderPromises = new Map<string, Promise<any>>();
196
217
 
197
- // Set up loader access that ignores handle pushes
198
- setupLoaderAccessSilent(
199
- proactiveHandlerContext,
200
- proactiveLoaderPromises,
201
- );
218
+ // Use normal loader access so handle data is captured
219
+ setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
202
220
 
203
221
  // Re-resolve ALL segments without revalidation
204
222
  const Store = ctx.Store;
@@ -231,20 +249,23 @@ export function withCacheStore<TEnv>(
231
249
  ...freshSegments,
232
250
  ...freshInterceptSegments,
233
251
  ];
252
+ requestCtx._handleStore.seal();
234
253
  await cacheScope.cacheRoute(
235
254
  ctx.pathname,
236
255
  ctx.matched.params,
237
256
  completeSegments,
238
257
  ctx.isIntercept,
239
258
  );
240
- console.log(
241
- `[Router.matchPartial] Proactive caching complete: ${ctx.pathname}`,
242
- );
259
+ debugLog("cacheStore", "proactive caching complete", {
260
+ pathname: ctx.pathname,
261
+ });
243
262
  } catch (error) {
244
- console.error(
245
- `[Router.matchPartial] Proactive caching failed:`,
246
- error,
247
- );
263
+ debugWarn("cacheStore", "proactive caching failed", {
264
+ pathname: ctx.pathname,
265
+ error: String(error),
266
+ });
267
+ } finally {
268
+ requestCtx._handleStore = originalHandleStore;
248
269
  }
249
270
  });
250
271
  } else {
@@ -262,5 +283,13 @@ export function withCacheStore<TEnv>(
262
283
 
263
284
  return response;
264
285
  });
286
+
287
+ if (ms) {
288
+ ms.metrics.push({
289
+ label: "pipeline:cache-store",
290
+ duration: performance.now() - pipelineStart,
291
+ startTime: pipelineStart - ms.requestStart,
292
+ });
293
+ }
265
294
  };
266
295
  }