@rangojs/router 0.26.6 → 0.28.0

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 (234) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/action-coordinator.d.ts +0 -4
  4. package/dist/types/browser/action-fence.d.ts +11 -3
  5. package/dist/types/browser/event-controller.d.ts +20 -1
  6. package/dist/types/browser/history-state.d.ts +7 -9
  7. package/dist/types/browser/intercept-utils.d.ts +23 -0
  8. package/dist/types/browser/navigation-store.d.ts +8 -6
  9. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  10. package/dist/types/browser/rsc-router.d.ts +1 -1
  11. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  12. package/dist/types/browser/types.d.ts +2 -1
  13. package/dist/types/browser/wire-protocol.d.ts +25 -0
  14. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  15. package/dist/types/cache/cache-scope.d.ts +34 -22
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  18. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  19. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  20. package/dist/types/cache/segment-codec.d.ts +14 -0
  21. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  22. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  23. package/dist/types/cache/taint.d.ts +5 -3
  24. package/dist/types/cache/types.d.ts +7 -3
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  26. package/dist/types/prefetch-markers.d.ts +4 -6
  27. package/dist/types/prerender/store.d.ts +31 -0
  28. package/dist/types/prerender/warm-request.d.ts +3 -3
  29. package/dist/types/prerender.d.ts +2 -2
  30. package/dist/types/route-name.d.ts +5 -0
  31. package/dist/types/router/error-handling.d.ts +4 -4
  32. package/dist/types/router/instrument.d.ts +4 -3
  33. package/dist/types/router/intercept-resolution.d.ts +25 -9
  34. package/dist/types/router/match-context.d.ts +12 -11
  35. package/dist/types/router/match-handlers.d.ts +2 -6
  36. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  37. package/dist/types/router/match-middleware/cache-lookup.d.ts +35 -87
  38. package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
  39. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  40. package/dist/types/router/prerender-match.d.ts +21 -6
  41. package/dist/types/router/preview-match.d.ts +2 -7
  42. package/dist/types/router/request-classification.d.ts +5 -0
  43. package/dist/types/router/revalidation.d.ts +6 -1
  44. package/dist/types/router/router-context.d.ts +9 -63
  45. package/dist/types/router/router-interfaces.d.ts +8 -14
  46. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  47. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  48. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  49. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  50. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  51. package/dist/types/router/segment-resolution.d.ts +1 -2
  52. package/dist/types/router/segment-wrappers.d.ts +11 -48
  53. package/dist/types/router/telemetry.d.ts +2 -0
  54. package/dist/types/router/types.d.ts +10 -0
  55. package/dist/types/rsc/full-payload.d.ts +2 -2
  56. package/dist/types/rsc/handler-context.d.ts +6 -6
  57. package/dist/types/rsc/json-route-result.d.ts +5 -9
  58. package/dist/types/rsc/request-stages.d.ts +203 -0
  59. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  60. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  61. package/dist/types/rsc/routine-plan.d.ts +1 -1
  62. package/dist/types/rsc/server-action.d.ts +2 -2
  63. package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
  64. package/dist/types/rsc/shell-capture.d.ts +132 -15
  65. package/dist/types/rsc/shell-serve.d.ts +37 -12
  66. package/dist/types/server/context.d.ts +9 -8
  67. package/dist/types/server/handle-store.d.ts +16 -0
  68. package/dist/types/server/render-pass.d.ts +72 -0
  69. package/dist/types/server/request-context.d.ts +24 -104
  70. package/dist/types/server/rewrite-trail.d.ts +2 -2
  71. package/dist/types/testing/dispatch.d.ts +35 -18
  72. package/dist/types/testing/serve-shell-request.d.ts +7 -0
  73. package/dist/types/types/cache-types.d.ts +6 -3
  74. package/dist/types/types/error-types.d.ts +9 -1
  75. package/dist/types/types/handler-context.d.ts +18 -7
  76. package/dist/types/types/segments.d.ts +1 -1
  77. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  78. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  79. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  80. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  81. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  82. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  83. package/dist/types/vite/discovery/state.d.ts +12 -0
  84. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  85. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  86. package/dist/types/vite/router-discovery.d.ts +8 -47
  87. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  88. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  89. package/dist/types/wire-params.d.ts +34 -0
  90. package/dist/vite/index.js +3809 -3710
  91. package/package.json +1 -1
  92. package/skills/cache-guide/SKILL.md +8 -5
  93. package/skills/caching/SKILL.md +37 -18
  94. package/skills/client-urls/SKILL.md +2 -1
  95. package/skills/document-cache/SKILL.md +18 -1
  96. package/skills/handler-use/SKILL.md +1 -1
  97. package/skills/intercept/SKILL.md +53 -13
  98. package/skills/layout/SKILL.md +11 -7
  99. package/skills/loader/SKILL.md +30 -13
  100. package/skills/middleware/SKILL.md +19 -1
  101. package/skills/migrate-nextjs/SKILL.md +5 -4
  102. package/skills/parallel/SKILL.md +5 -4
  103. package/skills/ppr/SKILL.md +19 -3
  104. package/skills/prerender/SKILL.md +39 -15
  105. package/skills/rango/SKILL.md +23 -11
  106. package/skills/server-actions/SKILL.md +51 -6
  107. package/skills/testing/predicates.md +1 -1
  108. package/skills/testing/response-routes.md +6 -4
  109. package/skills/testing/serve-shell-request.md +7 -7
  110. package/skills/use-cache/SKILL.md +46 -8
  111. package/skills/ux-architecture/SKILL.md +3 -2
  112. package/src/__internal.ts +7 -1
  113. package/src/browser/action-coordinator.ts +2 -13
  114. package/src/browser/action-fence.ts +11 -3
  115. package/src/browser/connection-warmup.ts +3 -1
  116. package/src/browser/event-controller.ts +42 -3
  117. package/src/browser/history-state.ts +15 -13
  118. package/src/browser/intercept-utils.ts +51 -0
  119. package/src/browser/navigation-bridge.ts +34 -30
  120. package/src/browser/navigation-client.ts +21 -33
  121. package/src/browser/navigation-store.ts +25 -27
  122. package/src/browser/navigation-transaction.ts +7 -11
  123. package/src/browser/partial-update.ts +8 -0
  124. package/src/browser/prefetch/cache.ts +2 -1
  125. package/src/browser/prefetch/fetch.ts +8 -16
  126. package/src/browser/prefetch/queue.ts +4 -4
  127. package/src/browser/react/NavigationProvider.tsx +2 -1
  128. package/src/browser/react/filter-segment-order.ts +2 -2
  129. package/src/browser/rsc-router.tsx +17 -2
  130. package/src/browser/segment-reconciler.ts +66 -53
  131. package/src/browser/server-action-bridge.ts +265 -127
  132. package/src/browser/types.ts +2 -1
  133. package/src/browser/wire-protocol.ts +41 -0
  134. package/src/cache/cache-key-utils.ts +11 -5
  135. package/src/cache/cache-runtime.ts +110 -50
  136. package/src/cache/cache-scope.ts +110 -64
  137. package/src/cache/cf/cf-cache-constants.ts +2 -2
  138. package/src/cache/cf/cf-cache-store.ts +486 -874
  139. package/src/cache/cf/cf-kv-utils.ts +1 -1
  140. package/src/cache/document-cache.ts +42 -15
  141. package/src/cache/handle-snapshot.ts +19 -7
  142. package/src/cache/segment-codec.ts +55 -0
  143. package/src/cache/shell-snapshot.ts +2 -2
  144. package/src/cache/tag-invalidation.ts +21 -2
  145. package/src/cache/taint.ts +5 -3
  146. package/src/cache/types.ts +7 -3
  147. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  148. package/src/handles/pushed-by.ts +1 -1
  149. package/src/prefetch-markers.ts +4 -7
  150. package/src/prerender/store.ts +76 -0
  151. package/src/prerender/warm-request.ts +8 -4
  152. package/src/prerender.ts +2 -2
  153. package/src/route-name.ts +8 -0
  154. package/src/router/error-handling.ts +4 -4
  155. package/src/router/instrument.ts +4 -3
  156. package/src/router/intercept-resolution.ts +77 -75
  157. package/src/router/loader-resolution.ts +5 -4
  158. package/src/router/match-api.ts +4 -5
  159. package/src/router/match-context.ts +33 -12
  160. package/src/router/match-handlers.ts +8 -20
  161. package/src/router/match-middleware/background-revalidation.ts +41 -53
  162. package/src/router/match-middleware/cache-lookup.ts +680 -720
  163. package/src/router/match-middleware/cache-store.ts +14 -12
  164. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  165. package/src/router/match-middleware/segment-resolution.ts +34 -61
  166. package/src/router/match-result.ts +1 -20
  167. package/src/router/middleware.ts +69 -11
  168. package/src/router/navigation-snapshot.ts +4 -3
  169. package/src/router/prerender-match.ts +100 -44
  170. package/src/router/preview-match.ts +6 -41
  171. package/src/router/request-classification.ts +13 -7
  172. package/src/router/revalidation.ts +33 -7
  173. package/src/router/route-snapshot.ts +2 -5
  174. package/src/router/router-context.ts +14 -117
  175. package/src/router/router-interfaces.ts +8 -14
  176. package/src/router/segment-resolution/loader-cache.ts +14 -9
  177. package/src/router/segment-resolution/loader-mask.ts +5 -5
  178. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  179. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  180. package/src/router/segment-resolution/revalidation.ts +897 -776
  181. package/src/router/segment-resolution.ts +4 -13
  182. package/src/router/segment-wrappers.ts +51 -269
  183. package/src/router/telemetry.ts +5 -0
  184. package/src/router/types.ts +10 -0
  185. package/src/router.ts +12 -13
  186. package/src/rsc/full-payload.ts +6 -5
  187. package/src/rsc/handler-context.ts +8 -14
  188. package/src/rsc/handler.ts +126 -507
  189. package/src/rsc/json-route-result.ts +5 -9
  190. package/src/rsc/loader-fetch.ts +6 -5
  191. package/src/rsc/progressive-enhancement.ts +5 -0
  192. package/src/rsc/request-stages.ts +757 -0
  193. package/src/rsc/response-cache-serve.ts +10 -14
  194. package/src/rsc/response-route-handler.ts +13 -5
  195. package/src/rsc/rewrite-passes.ts +2 -1
  196. package/src/rsc/routine-plan.ts +1 -1
  197. package/src/rsc/rsc-rendering.ts +172 -349
  198. package/src/rsc/server-action.ts +87 -113
  199. package/src/rsc/shell-capture-constants.ts +3 -1
  200. package/src/rsc/shell-capture.ts +212 -169
  201. package/src/rsc/shell-serve.ts +121 -18
  202. package/src/rsc/ssr-setup.ts +5 -4
  203. package/src/segment-system.tsx +3 -3
  204. package/src/server/context.ts +22 -25
  205. package/src/server/handle-store.ts +55 -0
  206. package/src/server/render-pass.ts +291 -0
  207. package/src/server/request-context.ts +26 -115
  208. package/src/server/rewrite-trail.ts +6 -4
  209. package/src/ssr/index.tsx +1 -1
  210. package/src/ssr/ssr-root.tsx +1 -0
  211. package/src/testing/dispatch.ts +223 -710
  212. package/src/testing/e2e/page-helpers.ts +2 -2
  213. package/src/testing/serve-shell-request.ts +37 -32
  214. package/src/types/cache-types.ts +6 -3
  215. package/src/types/error-types.ts +9 -1
  216. package/src/types/handler-context.ts +21 -7
  217. package/src/types/segments.ts +1 -1
  218. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  219. package/src/vite/discovery/build-env.ts +148 -0
  220. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  221. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  222. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  223. package/src/vite/discovery/discover-routers.ts +1 -1
  224. package/src/vite/discovery/discovery-errors.ts +1 -1
  225. package/src/vite/discovery/prerender-collection.ts +24 -39
  226. package/src/vite/discovery/state.ts +15 -0
  227. package/src/vite/plugins/version-plugin.ts +1 -1
  228. package/src/vite/plugins/virtual-entries.ts +1 -1
  229. package/src/vite/router-discovery.ts +29 -2428
  230. package/src/vite/temp-rsc-server.ts +223 -0
  231. package/src/vite/utils/prerender-utils.ts +47 -0
  232. package/src/wire-params.ts +35 -0
  233. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  234. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { ConsoleMessage, Expect, Locator, Page } from "@playwright/test";
6
6
  import { PREFETCH_HEADER } from "../../prefetch-markers.js";
7
+ import { WIRE } from "../../wire-params.js";
7
8
 
8
9
  // ============================================================================
9
10
  // Standalone helpers (need only a page/locator)
@@ -177,8 +178,7 @@ export function isPrefetchRequest(req: {
177
178
 
178
179
  // Module-level matcher + handler so blockPrefetch/unblockPrefetch share
179
180
  // stable references — Playwright's unroute matches by identity.
180
- const rscPartialUrl = (url: URL): boolean =>
181
- url.searchParams.has("_rsc_partial");
181
+ const rscPartialUrl = (url: URL): boolean => url.searchParams.has(WIRE.partial);
182
182
  async function abortPrefetchRoute(
183
183
  route: import("@playwright/test").Route,
184
184
  ): Promise<void> {
@@ -53,12 +53,16 @@ import type { ExecutionContext } from "../types/request-scope.js";
53
53
  import type { HandlerCacheConfig, SSRModule } from "../rsc/types.js";
54
54
  import type { createRSCHandler } from "../rsc/handler.js";
55
55
  import { setDefaultSSRModuleLoaderForTests } from "../rsc/ssr-module-loader.js";
56
+ import {
57
+ createPrerenderStore,
58
+ prerenderManifestEntries,
59
+ } from "../prerender/store.js";
56
60
  import {
57
61
  POST_QUIESCE_TASK_HOPS,
58
62
  SHELL_CAPTURE_MAX_WAIT_MS,
59
63
  } from "../rsc/shell-capture-constants.js";
60
64
  import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../segment-fragments.js";
61
- import { FILL_PARAM, PREFETCH_HEADER } from "../prefetch-markers.js";
65
+ import { PREFETCH_HEADER } from "../prefetch-markers.js";
62
66
  import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
63
67
  import { withoutPushedBy } from "../handles/pushed-by.js";
64
68
  import type { HandleData } from "../server/handle-store.js";
@@ -72,6 +76,8 @@ import {
72
76
  type PprReplayStatus,
73
77
  type ShellStatus,
74
78
  } from "./shell-status.js";
79
+ import { WIRE } from "../wire-params.js";
80
+ import { writePartialParams } from "../browser/wire-protocol.js";
75
81
 
76
82
  /** Options for {@link serveShellRequest}. */
77
83
  export interface ServeShellRequestOptions<TEnv = any> {
@@ -105,6 +111,12 @@ export interface ServeShellRequestOptions<TEnv = any> {
105
111
  * that source. `rewriteTarget` sends the `metadata.rewriteTarget` the page at
106
112
  * `from` carried when middleware rewrote it (`_rsc_rt`), as the browser
107
113
  * does: the server then diffs against the route that rendered that page.
114
+ * `stale: true` sends a stale request (`_rsc_stale`), as the browser sends
115
+ * it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`)
116
+ * and segments take an action's revalidation defaults; it renders live, as
117
+ * an action does, so neither the route cache nor the document cache answers
118
+ * or stores it, and a `ppr` route's `replayStatus` is
119
+ * `{ outcome: "BYPASS", reason: "stale" }`.
108
120
  */
109
121
  partial?:
110
122
  | true
@@ -115,6 +127,7 @@ export interface ServeShellRequestOptions<TEnv = any> {
115
127
  prefetch?: boolean;
116
128
  fill?: boolean;
117
129
  interceptSource?: string;
130
+ stale?: boolean;
118
131
  };
119
132
  /**
120
133
  * POST this form as a submit before hydration: the no-JS request the
@@ -474,29 +487,14 @@ async function servePrerenderArtifacts(
474
487
  const { hashParams } = await import("../prerender/param-hash.js");
475
488
  baked.add(pathname);
476
489
  if (result && !result.passthrough) {
477
- const key = `${result.routeName}/${hashParams(result.params)}`;
478
- payloads.set(
479
- key,
480
- JSON.stringify({ segments: result.segments, handles: result.handles }),
481
- );
482
- // The intercept variant, keyed and shaped as the build writes it
483
- // (vite/discovery/prerender-collection.ts).
484
- if (result.interceptSegments?.length) {
485
- payloads.set(
486
- `${key}/i`,
487
- JSON.stringify({
488
- segments: [...result.segments, ...result.interceptSegments],
489
- handles: result.interceptHandles ?? "",
490
- }),
491
- );
490
+ const paramHash = hashParams(result.params);
491
+ for (const [key, value] of prerenderManifestEntries(result, paramHash)) {
492
+ payloads.set(key, value);
492
493
  }
493
494
  }
494
495
  }
495
- const [{ createPrerenderStore }, { setPrerenderStoreForTests }] =
496
- await Promise.all([
497
- import("../prerender/store.js"),
498
- import("../router/match-middleware/cache-lookup.js"),
499
- ]);
496
+ const { setPrerenderStoreForTests } =
497
+ await import("../router/match-middleware/cache-lookup.js");
500
498
  const previousLoader = globalThis.__loadPrerenderManifestModule;
501
499
  globalThis.__loadPrerenderManifestModule = async () => ({
502
500
  default: Object.fromEntries([...payloads.keys()].map((key) => [key, key])),
@@ -574,12 +572,12 @@ async function buildActionRequest(
574
572
  const { encodeReply } =
575
573
  await import("@vitejs/plugin-rsc/vendor/react-server-dom/client.edge");
576
574
  registerTestAction(action.ref, action.id, "serveShellRequest");
577
- const target = new URL(url);
578
- target.searchParams.set("_rsc_action", action.id);
579
- target.searchParams.set("_rsc_segments", (action.segments ?? []).join(","));
580
- if (action.rewriteTarget) {
581
- target.searchParams.set("_rsc_rt", action.rewriteTarget);
582
- }
575
+ const target = writePartialParams(new URL(url), {
576
+ action: action.id,
577
+ segmentIds: action.segments ?? [],
578
+ emptySegments: "send",
579
+ rewriteTarget: action.rewriteTarget,
580
+ });
583
581
  headers.set("rsc-action", action.id);
584
582
  if (!headers.has("X-RSC-Router-Client-Path")) {
585
583
  headers.set("X-RSC-Router-Client-Path", url.href);
@@ -628,14 +626,21 @@ async function buildRequest(
628
626
  prefetch = false,
629
627
  fill = false,
630
628
  interceptSource,
629
+ stale = false,
631
630
  } = partial === true ? {} : partial;
631
+ if (stale && prefetch) {
632
+ throw new Error(
633
+ "serveShellRequest: partial.stale cannot be combined with partial.prefetch",
634
+ );
635
+ }
632
636
  const target = new URL(url);
633
- target.searchParams.set("_rsc_partial", "true");
634
- target.searchParams.set("_rsc_segments", segments.join(","));
635
- if (rewriteTarget) target.searchParams.set("_rsc_rt", rewriteTarget);
636
- // As the browser sends them: browser/navigation-client.ts (fill) and
637
+ target.searchParams.set(WIRE.partial, "true");
638
+ target.searchParams.set(WIRE.segments, segments.join(","));
639
+ if (rewriteTarget) target.searchParams.set(WIRE.rewriteTarget, rewriteTarget);
640
+ // As the browser sends them: browser/navigation-client.ts (fill, stale) and
637
641
  // browser/prefetch/fetch.ts (prefetch).
638
- if (fill) target.searchParams.set(FILL_PARAM, "1");
642
+ if (fill) target.searchParams.set(WIRE.fill, "1");
643
+ if (stale) target.searchParams.set(WIRE.stale, "true");
639
644
  if (prefetch && !headers.has(PREFETCH_HEADER)) {
640
645
  headers.set(PREFETCH_HEADER, "1");
641
646
  }
@@ -137,9 +137,12 @@ export interface CacheOptions<TEnv = unknown> {
137
137
  ) => boolean;
138
138
 
139
139
  /**
140
- * Custom cache key function - FULL OVERRIDE.
141
- * Bypasses default key generation AND store's keyGenerator. The result is
142
- * stored namespaced (`key:` plus its URI encoding), so no value it returns
140
+ * Custom cache key function. Replaces the URL part of the default key
141
+ * (host, path, params, search) AND bypasses the store's keyGenerator. The
142
+ * request kind stays: a document request, a navigation and each intercept
143
+ * navigation keep their own record under one result, so the result decides
144
+ * which requests of one kind share a record. It is stored namespaced
145
+ * (`key:` plus its URI encoding, then the kind), so no value it returns
143
146
  * names another route's record; a nested cache() keys its records within
144
147
  * this partition.
145
148
  *
@@ -83,7 +83,15 @@ export interface OnErrorContext<TEnv = any> {
83
83
  env?: TEnv;
84
84
  /** Duration from request start to error (milliseconds) */
85
85
  duration?: number;
86
- /** Whether this is a partial/navigation request */
86
+ /**
87
+ * Whether this is a partial/navigation request, on an error the segment
88
+ * walk catches (a handler that throws, a DSL loader that rejects): `true`
89
+ * for a navigation, unset for a document. An error in the background
90
+ * re-render of a `cache()` record (a stale HIT's refresh, the write of a
91
+ * navigation that kept a layout mounted) reports the request that started
92
+ * it. A streamed (`loading()`) handler's rejection does not carry it, in
93
+ * either render; the `routing` phase reports `false`.
94
+ */
87
95
  isPartial?: boolean;
88
96
  /** Whether an error boundary caught the error */
89
97
  handledByBoundary?: boolean;
@@ -462,7 +462,7 @@ export type InternalHandlerContext<
462
462
  _reverseScope?: import("../server/context.js").ReverseScope;
463
463
  /**
464
464
  * @internal The on-demand overlay holds a "removed" marker for this request
465
- * (tryPrerenderLookup, cache-lookup.ts); gateOnDemandProducer reads it. On
465
+ * (readPrerenderSource, cache-lookup.ts); gateOnDemandProducer reads it. On
466
466
  * this context, not the request's: withCacheLookup documents that the
467
467
  * request ALS can be lost mid-pipeline on workerd.
468
468
  */
@@ -510,6 +510,17 @@ export type InternalHandlerContext<
510
510
  * request is a fill. See router/segment-resolution/prefetch-deferral.ts.
511
511
  */
512
512
  _prefetchDeferral?: import("../router/segment-resolution/prefetch-deferral.js").PrefetchDeferral;
513
+ /**
514
+ * @internal DSL loaders registered and not started, by `$$id`: a prefetch
515
+ * deferred them, or a pass that skips loaders (`skipLoaders`) did not run
516
+ * them. A reader's ctx.use() starts one through its registration
517
+ * (registerDeferredLoader, startDeferredLoader in
518
+ * router/segment-resolution/prefetch-deferral.ts).
519
+ */
520
+ _deferredLoaders?: Map<
521
+ string,
522
+ import("../router/segment-resolution/prefetch-deferral.js").DeferredLoader
523
+ >;
513
524
  /**
514
525
  * @internal Whether a loader was started in this request: a run of it (a
515
526
  * handler's or another loader's ctx.use()) or its cache() binding. A
@@ -637,10 +648,10 @@ export type IsActionFn = (...actions: ActionRef[]) => boolean;
637
648
  * ```ts
638
649
  * import * as CartActions from "./actions/cart";
639
650
  *
640
- * // Idiom A — "mine, else defer": re-render on my actions, otherwise return
641
- * // undefined so the segment's default decision still applies (and downstream
642
- * // revalidators get a say).
643
- * revalidate((ctx) => ctx.isAction(CartActions) || undefined)
651
+ * // Idiom A — "mine, else defer": re-render on my actions and on Back after
652
+ * // an action, otherwise return undefined so the segment's default decision
653
+ * // still applies (and downstream revalidators get a say).
654
+ * revalidate((ctx) => ctx.isAction(CartActions) || ctx.stale || undefined)
644
655
  *
645
656
  * // Idiom B — "mine only": re-render on my actions and suppress everything
646
657
  * // else (isAction returns a raw boolean, so an unrelated action yields false).
@@ -768,8 +779,11 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
768
779
  // ── Staleness signal ─────────────────────────────────────────────────
769
780
 
770
781
  /**
771
- * `true` when the browser signals that data may be stale — typically because
772
- * a server action was executed in this or another tab (`_rsc_stale` header).
782
+ * `true` on a revalidation of a page the browser marked stale (the
783
+ * `_rsc_stale` query param): Back/Forward onto a history entry after a
784
+ * server action in this or another tab, or while one is in flight. A request
785
+ * that holds no segments (the follow-up after an action, `refresh()`) is
786
+ * marked stale too, but there every segment renders and no predicate runs.
773
787
  *
774
788
  * This is NOT segment cache staleness (loaders are never segment-cached).
775
789
  * Use this to decide whether loader data should be re-fetched after an
@@ -165,7 +165,7 @@ export interface ResolvedSegment {
165
165
  loaderData?: any; // For loaders: the resolved data from loader execution
166
166
  /**
167
167
  * True when this loader was awaited before first flush
168
- * (loader(Def, { ssr: false })). Stamped by resolveLoaders (fresh.ts) on
168
+ * (loader(Def, { ssr: false })). Stamped by resolveOwnerLoaders on
169
169
  * document AND shell-capture renders — capture bakes flagged loaders and
170
170
  * awaits the same lane. Feeds segment-system's settled-value delivery and
171
171
  * the dev SSR suspension warning (ssr-suspension-warning.ts).
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Build discovery phase: the discovery plugin's buildStart, buildApp (post)
3
+ * and buildEnd hooks.
4
+ *
5
+ * buildStart discovers routers through a temp RSC server and writes the
6
+ * generated route types. When PPR shell candidates exist, it defers closing
7
+ * that server, and releasing the buildEnv, as a PAIR: the buildApp post hook
8
+ * releases them after the shell capture (success), buildEnd after an
9
+ * environment build failure (abort).
10
+ */
11
+
12
+ import type { Rolldown, ViteBuilder } from "vite";
13
+ import type { DiscoveryState } from "./state.js";
14
+ import { createTempRscServer } from "../temp-rsc-server.js";
15
+ import { acquireBuildEnv, releaseBuildEnv } from "./build-env.js";
16
+ import { discoverRouters } from "./discover-routers.js";
17
+ import { writeRouteTypesFiles } from "./route-types-writer.js";
18
+ import { runShellPrerenderPhase } from "./shell-prerender-phase.js";
19
+ import { runRouterVersionsPhase } from "./router-versions-phase.js";
20
+ import { resetStagedBuildAssets } from "../utils/prerender-utils.js";
21
+ import { createRangoDebugger, timed, timedSync, NS } from "../debug.js";
22
+
23
+ const debugDiscovery = createRangoDebugger(NS.discovery);
24
+
25
+ /**
26
+ * Reset the per-build prerender collection state. A helper (not inline
27
+ * assignments in buildStart) so TS's property narrowing does not pin
28
+ * `s.shellCandidates` to `null` across the discovery call that repopulates
29
+ * it — the finally block re-reads it to decide the temp-server keep-alive.
30
+ */
31
+ function resetPrerenderCollection(s: DiscoveryState): void {
32
+ s.prerenderManifestEntries = null;
33
+ s.staticManifestEntries = null;
34
+ s.shellCandidates = null;
35
+ s.prerenderPayloadValues = null;
36
+ s.buildData = [];
37
+ }
38
+
39
+ /**
40
+ * buildStart. Build mode: create a temporary Vite dev server to access the
41
+ * RSC environment's module runner, then discover routers and generate
42
+ * manifests. The manifest data is stored for the virtual module's load hook.
43
+ */
44
+ export async function buildDiscoveryPhase(
45
+ s: DiscoveryState,
46
+ context: Rolldown.PluginContext,
47
+ ): Promise<void> {
48
+ if (!s.isBuildMode) return;
49
+ // Only run once across environment builds
50
+ if (s.mergedRouteManifest !== null) {
51
+ debugDiscovery?.(
52
+ "build: skip (already discovered, env=%s)",
53
+ context.environment?.name ?? "?",
54
+ );
55
+ return;
56
+ }
57
+ const buildStartTime = performance.now();
58
+ debugDiscovery?.("build: start (env=%s)", context.environment?.name ?? "?");
59
+ resetStagedBuildAssets(s.projectRoot);
60
+ resetPrerenderCollection(s);
61
+
62
+ // Acquire build-time env bindings if configured
63
+ await timed(debugDiscovery, "build acquireBuildEnv", () =>
64
+ acquireBuildEnv(s, s.viteCommand, s.viteMode),
65
+ );
66
+
67
+ let tempServer: any = null;
68
+ // Signal to user-space code (e.g. reverse.ts) that build-time discovery
69
+ // is active. Uses globalThis because the temp server's module runner
70
+ // creates a separate module context — there is no shared import path
71
+ // between the vite plugin and user code loaded via runner.import().
72
+ (globalThis as any).__rscRouterDiscoveryActive = true;
73
+ try {
74
+ tempServer = await timed(debugDiscovery, "build createTempRscServer", () =>
75
+ createTempRscServer(s, { forceBuild: true }),
76
+ );
77
+
78
+ const rscEnv = (tempServer.environments as any)?.rsc;
79
+ if (!rscEnv?.runner) {
80
+ console.warn(
81
+ "[rango] RSC environment runner not available during build, skipping manifest generation",
82
+ );
83
+ return;
84
+ }
85
+
86
+ // Point resolvedStaticModules at the temp server's expose-internal-ids
87
+ // plugin so that discoverRouters() can access the static handler module
88
+ // map after the temp server's transforms populate it.
89
+ const tempIdsPlugin = (tempServer as any).config?.plugins?.find(
90
+ (p: any) => p.name === "@rangojs/router:expose-internal-ids",
91
+ );
92
+ if (tempIdsPlugin?.api?.staticHandlerModules) {
93
+ s.resolvedStaticModules = tempIdsPlugin.api.staticHandlerModules;
94
+ }
95
+
96
+ await timed(debugDiscovery, "build discoverRouters", () =>
97
+ discoverRouters(s, rscEnv, (tempServer.environments as any)?.ssr),
98
+ );
99
+ // Update named-routes.gen.ts from runtime discovery.
100
+ // The runtime manifest includes dynamically generated routes
101
+ // that the static parser cannot extract from source code.
102
+ timedSync(debugDiscovery, "build writeRouteTypesFiles", () =>
103
+ writeRouteTypesFiles(s),
104
+ );
105
+ } catch (err: any) {
106
+ // Extract the user source file from the stack trace (skip internal frames)
107
+ const sourceFile = err.stack
108
+ ?.split("\n")
109
+ .find(
110
+ (line: string) =>
111
+ line.includes(s.projectRoot) && !line.includes("node_modules"),
112
+ )
113
+ ?.match(/\(([^)]+)\)/)?.[1];
114
+ // Extract the route name from "Unknown route: <name>" errors
115
+ const routeName = err.message?.match(/Unknown route: (.+)/)?.[1];
116
+ const details = [
117
+ routeName ? ` Route name: ${routeName}` : null,
118
+ sourceFile ? ` File: ${sourceFile}` : null,
119
+ err.stack ? ` Stack:\n${err.stack}` : null,
120
+ ]
121
+ .filter(Boolean)
122
+ .join("\n");
123
+ throw new Error(`[rango] Build-time router discovery failed:\n${details}`, {
124
+ cause: err,
125
+ });
126
+ } finally {
127
+ delete (globalThis as any).__rscRouterDiscoveryActive;
128
+ if (tempServer && s.shellCandidates?.length) {
129
+ // Prerender+ppr candidates exist: keep the temp server (and its
130
+ // realm — tries installed, registry populated) alive for the
131
+ // post-build shell capture phase (buildApp post, producer B #699).
132
+ // The prelude embeds built client asset URLs, so the capture can
133
+ // only run after the client build; that phase closes the server.
134
+ // buildEnv release is deferred with it — a bake-lane loader
135
+ // executing during the capture may read ctx.env.
136
+ s.shellPhaseTempServer = tempServer;
137
+ } else {
138
+ if (tempServer) {
139
+ await timed(debugDiscovery, "build tempServer.close", () =>
140
+ tempServer.close(),
141
+ );
142
+ }
143
+ await releaseBuildEnv(s);
144
+ }
145
+ debugDiscovery?.(
146
+ "build discovery done (%sms)",
147
+ (performance.now() - buildStartTime).toFixed(1),
148
+ );
149
+ }
150
+ }
151
+
152
+ /**
153
+ * buildApp (post). Post-build phases, after EVERY environment bundle is
154
+ * written.
155
+ *
156
+ * 1. Cache versions (router-versions-phase.ts): hashes the server output as
157
+ * postprocessBundle left it and writes the per-router table into the
158
+ * built version module. First, because phase 2 stamps its entries with
159
+ * these versions and reads the same RSC out dir.
160
+ * 2. PPR shell capture (producer B, #699): the shell prelude embeds built
161
+ * client asset URLs (bootstrap entry), which do not exist at buildStart.
162
+ * The kept temp server and the buildEnv were deferred AS A PAIR in
163
+ * buildStart's finally; this finally is the pair's success-path owner
164
+ * (buildEndPhase owns the aborted-build path) — the phase itself is a pure
165
+ * producer and tears down only the globals it installs.
166
+ */
167
+ export async function postBuildPhase(
168
+ s: DiscoveryState,
169
+ builder: ViteBuilder,
170
+ ): Promise<void> {
171
+ try {
172
+ await runShellPrerenderPhase(
173
+ s,
174
+ builder as any,
175
+ runRouterVersionsPhase(s, builder as any),
176
+ );
177
+ } finally {
178
+ if (s.isBuildMode) {
179
+ const tempServer = s.shellPhaseTempServer;
180
+ s.shellPhaseTempServer = null;
181
+ if (tempServer) await tempServer.close();
182
+ await releaseBuildEnv(s);
183
+ }
184
+ }
185
+ }
186
+
187
+ /**
188
+ * buildEnd. An environment build failure aborts the builder before the
189
+ * buildApp post hook — never leak the kept temp server (open handles hang
190
+ * the CLI) or the deferred buildEnv (a live miniflare proxy).
191
+ */
192
+ export async function buildEndPhase(
193
+ s: DiscoveryState,
194
+ error: Error | undefined,
195
+ ): Promise<void> {
196
+ if (!error || !s.shellPhaseTempServer) return;
197
+ const tempServer = s.shellPhaseTempServer;
198
+ s.shellPhaseTempServer = null;
199
+ try {
200
+ await tempServer.close();
201
+ } finally {
202
+ await releaseBuildEnv(s);
203
+ }
204
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Build-time env (rango({ buildEnv })): resolution of the option and the
3
+ * lifetime of the resolved bindings on DiscoveryState. Acquired by dev
4
+ * discovery (dev-server-phase.ts) and build discovery
5
+ * (build-discovery-phase.ts).
6
+ */
7
+
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { existsSync } from "node:fs";
10
+ import { createRequire } from "node:module";
11
+ import { pathToFileURL } from "node:url";
12
+ import { BUILD_ENV_GLOBAL_KEY } from "../plugins/cloudflare-protocol-stub.js";
13
+ import type {
14
+ BuildEnvOption,
15
+ BuildEnvFactoryContext,
16
+ BuildEnvResult,
17
+ } from "../plugin-types.js";
18
+ import type { DiscoveryState } from "./state.js";
19
+
20
+ const WRANGLER_CONFIG_NAMES: readonly string[] = [
21
+ "wrangler.json",
22
+ "wrangler.jsonc",
23
+ "wrangler.toml",
24
+ ];
25
+
26
+ /**
27
+ * getPlatformProxy() looks for the wrangler config from process.cwd() and
28
+ * resolves its default persist path against cwd, so building from another
29
+ * directory dropped the app's bindings (#1037). Search upward from the Vite
30
+ * root with wrangler's own file names and order. Persisted state goes under
31
+ * the Vite root, where @cloudflare/vite-plugin reads it in dev and preview
32
+ * (its getPersistenceRoot), also when the config sits above the root. No
33
+ * config found: no options, wrangler's own lookup.
34
+ */
35
+ function wranglerProxyOptions(
36
+ root: string,
37
+ ): Record<string, unknown> | undefined {
38
+ const viteRoot = resolve(root);
39
+ for (let dir = viteRoot; ; dir = dirname(dir)) {
40
+ for (const name of WRANGLER_CONFIG_NAMES) {
41
+ const configPath = join(dir, name);
42
+ if (existsSync(configPath)) {
43
+ return {
44
+ configPath,
45
+ persist: { path: join(viteRoot, ".wrangler", "state", "v3") },
46
+ };
47
+ }
48
+ }
49
+ if (dirname(dir) === dir) return undefined;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Resolve the buildEnv option into a concrete { env, dispose? } result.
55
+ * Handles all four input shapes: false, "auto", factory, plain object.
56
+ */
57
+ export async function resolveBuildEnv(
58
+ option: BuildEnvOption | undefined,
59
+ factoryCtx: BuildEnvFactoryContext,
60
+ ): Promise<BuildEnvResult | null> {
61
+ if (!option) return null;
62
+
63
+ if (option === "auto") {
64
+ if (factoryCtx.preset !== "cloudflare") {
65
+ throw new Error(
66
+ '[rango] buildEnv: "auto" is only supported with preset: "cloudflare". ' +
67
+ "Use a factory function or plain object for other presets.",
68
+ );
69
+ }
70
+ try {
71
+ // Resolve wrangler from the user's project root (not the router package)
72
+ const userRequire = createRequire(
73
+ resolve(factoryCtx.root, "package.json"),
74
+ );
75
+ const wranglerPath = userRequire.resolve("wrangler");
76
+ const { getPlatformProxy } = (await import(
77
+ pathToFileURL(wranglerPath).href
78
+ )) as {
79
+ getPlatformProxy: (opts?: any) => Promise<any>;
80
+ };
81
+ const proxy = await getPlatformProxy(
82
+ wranglerProxyOptions(factoryCtx.root),
83
+ );
84
+ return {
85
+ env: proxy.env as Record<string, unknown>,
86
+ dispose: proxy.dispose,
87
+ };
88
+ } catch (err: any) {
89
+ throw new Error(
90
+ '[rango] buildEnv: "auto" requires wrangler to be installed.\n' +
91
+ `Install it with: pnpm add -D wrangler\n${err.message}`,
92
+ );
93
+ }
94
+ }
95
+
96
+ if (typeof option === "function") {
97
+ return await option(factoryCtx);
98
+ }
99
+
100
+ // Plain object
101
+ return { env: option };
102
+ }
103
+
104
+ /**
105
+ * Acquire build-time env bindings and store on discovery state.
106
+ * Returns true if env was acquired, false if buildEnv is disabled.
107
+ */
108
+ export async function acquireBuildEnv(
109
+ s: DiscoveryState,
110
+ command: "serve" | "build",
111
+ mode: string,
112
+ ): Promise<boolean> {
113
+ const option = s.opts?.buildEnv;
114
+ if (!option) return false;
115
+
116
+ const result = await resolveBuildEnv(option, {
117
+ root: s.projectRoot,
118
+ mode,
119
+ command,
120
+ preset: s.opts?.preset ?? "node",
121
+ });
122
+ if (!result) return false;
123
+
124
+ s.resolvedBuildEnv = result.env;
125
+ s.buildEnvDispose = result.dispose ?? null;
126
+ // Bridge the resolved env into `cloudflare:workers`'s stubbed `env`
127
+ // export so user code that does `import { env } from "cloudflare:workers"`
128
+ // sees the real bindings proxy during discovery + prerender instead of
129
+ // an empty object. The stub reads this global at module-evaluation time.
130
+ (globalThis as Record<string, unknown>)[BUILD_ENV_GLOBAL_KEY] = result.env;
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * Release build-time env resources and clear state.
136
+ */
137
+ export async function releaseBuildEnv(s: DiscoveryState): Promise<void> {
138
+ if (s.buildEnvDispose) {
139
+ try {
140
+ await s.buildEnvDispose();
141
+ } catch (err: any) {
142
+ console.warn(`[rango] buildEnv dispose failed: ${err.message}`);
143
+ }
144
+ s.buildEnvDispose = null;
145
+ }
146
+ s.resolvedBuildEnv = undefined;
147
+ delete (globalThis as Record<string, unknown>)[BUILD_ENV_GLOBAL_KEY];
148
+ }