@rangojs/router 0.20.0 → 0.21.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 (178) hide show
  1. package/dist/bin/rango.js +26 -14
  2. package/dist/types/browser/event-controller.d.ts +32 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  5. package/dist/types/browser/rango-state.d.ts +3 -1
  6. package/dist/types/browser/react/context.d.ts +11 -0
  7. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  8. package/dist/types/browser/react/location-state.d.ts +7 -2
  9. package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
  10. package/dist/types/cache/cache-scope.d.ts +4 -5
  11. package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
  12. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  13. package/dist/types/cache/handle-snapshot.d.ts +36 -50
  14. package/dist/types/cache/index.d.ts +2 -0
  15. package/dist/types/cache/shell-snapshot.d.ts +19 -3
  16. package/dist/types/cache/types.d.ts +21 -10
  17. package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
  18. package/dist/types/client-urls/navigation.d.ts +7 -1
  19. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  20. package/dist/types/handles/deferred-resolution.d.ts +11 -1
  21. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  22. package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
  23. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  24. package/dist/types/router/router-interfaces.d.ts +2 -1
  25. package/dist/types/router/router-options.d.ts +17 -7
  26. package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
  27. package/dist/types/router-versions.d.ts +39 -0
  28. package/dist/types/rsc/handler-context.d.ts +5 -0
  29. package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
  30. package/dist/types/rsc/types.d.ts +4 -6
  31. package/dist/types/server/build-version-table.d.ts +44 -0
  32. package/dist/types/server/handle-store.d.ts +87 -46
  33. package/dist/types/server/request-context.d.ts +22 -8
  34. package/dist/types/testing/build-versions.d.ts +32 -0
  35. package/dist/types/testing/dom.entry.d.ts +1 -1
  36. package/dist/types/testing/flight.entry.d.ts +1 -1
  37. package/dist/types/testing/index.d.ts +2 -0
  38. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  39. package/dist/types/testing/location-state-key.d.ts +5 -1
  40. package/dist/types/testing/render-route.d.ts +77 -6
  41. package/dist/types/testing/serve-shell-request.d.ts +21 -0
  42. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  43. package/dist/types/types/handler-context.d.ts +8 -5
  44. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  45. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  46. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  47. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  48. package/dist/types/vite/discovery/state.d.ts +14 -0
  49. package/dist/types/vite/encryption-key.d.ts +18 -0
  50. package/dist/types/vite/plugin-types.d.ts +27 -0
  51. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  52. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  53. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  54. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  55. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  56. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  57. package/dist/types/vite/router-discovery.d.ts +6 -0
  58. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  59. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  60. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  61. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  62. package/dist/vite/index.js +1537 -444
  63. package/package.json +1 -2
  64. package/skills/cache-guide/SKILL.md +84 -20
  65. package/skills/caching/SKILL.md +16 -5
  66. package/skills/client-urls/SKILL.md +16 -0
  67. package/skills/cloudflare/SKILL.md +2 -1
  68. package/skills/comparison/references/framework-comparison.md +16 -12
  69. package/skills/hooks/SKILL.md +3 -0
  70. package/skills/hooks/handle-and-actions.md +6 -0
  71. package/skills/hooks/navigation.md +28 -0
  72. package/skills/hooks/state.md +209 -8
  73. package/skills/hooks/urls.md +40 -1
  74. package/skills/host-router/SKILL.md +13 -0
  75. package/skills/intercept/SKILL.md +6 -0
  76. package/skills/loader/SKILL.md +19 -1
  77. package/skills/middleware/SKILL.md +2 -0
  78. package/skills/ppr/SKILL.md +119 -40
  79. package/skills/prerender/SKILL.md +7 -3
  80. package/skills/rango/SKILL.md +8 -7
  81. package/skills/router-setup/SKILL.md +15 -1
  82. package/skills/testing/SKILL.md +30 -26
  83. package/skills/testing/client-components.md +193 -8
  84. package/skills/testing/handles.md +2 -0
  85. package/skills/testing/loader.md +1 -1
  86. package/skills/testing/middleware.md +1 -1
  87. package/skills/testing/render-handler.md +18 -18
  88. package/skills/testing/serve-shell-request.md +63 -14
  89. package/skills/testing/server-actions.md +17 -17
  90. package/skills/testing/server-tree.md +1 -1
  91. package/skills/typesafety/generated-files-and-cli.md +1 -1
  92. package/skills/use-cache/SKILL.md +16 -5
  93. package/skills/vercel/SKILL.md +3 -3
  94. package/src/bin/rango.ts +1 -1
  95. package/src/browser/event-controller.ts +75 -2
  96. package/src/browser/history-state.ts +39 -17
  97. package/src/browser/navigation-bridge.ts +41 -18
  98. package/src/browser/navigation-transaction.ts +15 -10
  99. package/src/browser/partial-update.ts +10 -2
  100. package/src/browser/rango-state.ts +3 -1
  101. package/src/browser/react/Link.tsx +2 -3
  102. package/src/browser/react/NavigationProvider.tsx +25 -1
  103. package/src/browser/react/context.ts +13 -0
  104. package/src/browser/react/location-state-shared.ts +200 -34
  105. package/src/browser/react/location-state.ts +40 -75
  106. package/src/browser/react/subscribe-to-nothing.ts +7 -0
  107. package/src/browser/react/use-handle.ts +20 -1
  108. package/src/browser/rsc-router.tsx +33 -11
  109. package/src/browser/server-action-bridge.ts +17 -1
  110. package/src/cache/cache-scope.ts +18 -8
  111. package/src/cache/cf/cf-cache-store.ts +75 -55
  112. package/src/cache/cf/cf-cache-types.ts +12 -4
  113. package/src/cache/handle-snapshot.ts +69 -87
  114. package/src/cache/index.ts +4 -0
  115. package/src/cache/shell-snapshot.ts +35 -5
  116. package/src/cache/types.ts +21 -10
  117. package/src/cache/vercel/vercel-cache-store.ts +48 -20
  118. package/src/client-urls/client-root.tsx +1 -0
  119. package/src/client-urls/navigation.ts +10 -0
  120. package/src/client-urls/optimistic-location.ts +10 -4
  121. package/src/handles/deferred-resolution.ts +31 -0
  122. package/src/prerender/build-shell-capture.ts +11 -9
  123. package/src/router/manifest.ts +2 -0
  124. package/src/router/match-middleware/cache-lookup.ts +11 -18
  125. package/src/router/navigation-snapshot.ts +25 -6
  126. package/src/router/request-classification.ts +15 -12
  127. package/src/router/router-interfaces.ts +2 -1
  128. package/src/router/router-options.ts +17 -7
  129. package/src/router/segment-resolution/loader-cache.ts +95 -60
  130. package/src/router-versions.ts +60 -0
  131. package/src/rsc/full-payload.ts +6 -2
  132. package/src/rsc/handler-context.ts +5 -0
  133. package/src/rsc/handler.ts +14 -4
  134. package/src/rsc/rsc-rendering.ts +12 -1
  135. package/src/rsc/shell-build-manifest.ts +10 -7
  136. package/src/rsc/shell-capture.ts +36 -26
  137. package/src/rsc/types.ts +4 -6
  138. package/src/server/build-version-table.ts +115 -0
  139. package/src/server/handle-store.ts +344 -127
  140. package/src/server/request-context.ts +62 -11
  141. package/src/ssr/ssr-root.tsx +16 -4
  142. package/src/testing/build-versions.ts +64 -0
  143. package/src/testing/dispatch.ts +6 -0
  144. package/src/testing/dom.entry.ts +1 -0
  145. package/src/testing/flight.entry.ts +1 -0
  146. package/src/testing/index.ts +3 -0
  147. package/src/testing/internal/context.ts +2 -1
  148. package/src/testing/internal/seed-vars.ts +14 -2
  149. package/src/testing/location-state-key.ts +18 -8
  150. package/src/testing/render-handler.ts +2 -1
  151. package/src/testing/render-route.tsx +277 -43
  152. package/src/testing/serve-shell-request.ts +80 -11
  153. package/src/testing/vitest-stubs/version.ts +4 -1
  154. package/src/types/handler-context.ts +8 -5
  155. package/src/vite/discovery/build-versions.ts +1281 -0
  156. package/src/vite/discovery/installed-packages.ts +159 -0
  157. package/src/vite/discovery/prerender-collection.ts +38 -1
  158. package/src/vite/discovery/router-versions-phase.ts +336 -0
  159. package/src/vite/discovery/shell-prerender-phase.ts +28 -38
  160. package/src/vite/discovery/state.ts +19 -0
  161. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  162. package/src/vite/encryption-key.ts +101 -9
  163. package/src/vite/plugin-types.ts +28 -0
  164. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  165. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  166. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  167. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  168. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  169. package/src/vite/plugins/version-plugin.ts +19 -24
  170. package/src/vite/plugins/version.d.ts +17 -2
  171. package/src/vite/plugins/virtual-entries.ts +31 -5
  172. package/src/vite/rango.ts +28 -3
  173. package/src/vite/router-discovery.ts +82 -13
  174. package/src/vite/utils/client-chunks.ts +5 -1
  175. package/src/vite/utils/compare-strings.ts +9 -0
  176. package/src/vite/utils/package-resolution.ts +9 -0
  177. package/src/vite/utils/prerender-utils.ts +4 -1
  178. package/src/vite/utils/shared-utils.ts +1 -1
@@ -5,8 +5,23 @@
5
5
 
6
6
  declare module "@rangojs/router:version" {
7
7
  /**
8
- * Auto-generated version string for cache invalidation.
9
- * Changes on server restart (dev) or build (prod).
8
+ * The build's version string.
9
+ *
10
+ * - Dev: a stamp that changes on server start and on every RSC module edit.
11
+ * - Production: the whole-build document version, a hash of the server
12
+ * output and the client asset names. The same source builds to the same
13
+ * value; any change to any router changes it.
14
+ *
15
+ * A router serves with its own versions (`ROUTER_VERSIONS`), not this one.
10
16
  */
11
17
  export const VERSION: string;
18
+
19
+ /**
20
+ * Per-router cache versions computed at build time: router id to
21
+ * `[data, document]`, with `"*"` holding the whole-build pair.
22
+ * `undefined` in dev, where `VERSION` is used for both.
23
+ */
24
+ export const ROUTER_VERSIONS:
25
+ | Readonly<Record<string, readonly [data: string, document: string]>>
26
+ | undefined;
12
27
  }
@@ -1,4 +1,5 @@
1
1
  import type { HeadScriptsOption } from "../plugin-types.js";
2
+ import { DEFAULT_ROUTER_VERSIONS_KEY } from "../../router-versions.js";
2
3
 
3
4
  export const VIRTUAL_ENTRY_BROWSER: string = `
4
5
  import {
@@ -164,7 +165,7 @@ export const resumeShellHTML = createShellResumeHandler({
164
165
  * registered for the _rsc_loader endpoint and fail in production.
165
166
  *
166
167
  * Single source of truth: both the generated virtual RSC entry below and the
167
- * custom-entry injector (version-injector) consume this list, so a new
168
+ * custom-entry injector (entry-bootstrap-injector) consume this list, so a new
168
169
  * bootstrap manifest cannot be added to one path and forgotten on the other.
169
170
  * That exact drift (loader-manifest present here but missing from the injector)
170
171
  * is what left fetchable loaders unresolved on custom worker entries.
@@ -197,12 +198,11 @@ import {
197
198
  // consults them at router-module evaluation. Router-first would silently push
198
199
  // every clientUrls() app onto the deferred-subscription path and change mount
199
200
  // registration order with no error. The custom-entry injector keeps the same
200
- // guarantee by PREPENDING this list at file top (version-injector.ts).
201
+ // guarantee by PREPENDING this list at file top (entry-bootstrap-injector.ts).
201
202
  ${bootstrapImports}
202
203
 
203
204
  import { router } from "${routerPath}";
204
205
  import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
205
- import { VERSION } from "@rangojs/router:version";
206
206
 
207
207
  // Lazily create the handler on first request so that ESM live bindings
208
208
  // have resolved by the time we read \`router\`. During HMR the module may
@@ -212,7 +212,6 @@ export default function handler(request, env) {
212
212
  if (!_handler) {
213
213
  _handler = createRSCHandler({
214
214
  router,
215
- version: VERSION,
216
215
  // Forward the router's CSP nonce provider. createRSCHandler reads the
217
216
  // provider only from options.nonce; without this, createRouter({ nonce })
218
217
  // is silently dropped on the Node preset (the Cloudflare path wires it via
@@ -314,6 +313,33 @@ export const VIRTUAL_IDS = {
314
313
  version: "@rangojs/router:version",
315
314
  } as const;
316
315
 
316
+ /** Dev: one stamp for both versions of every router, bumped on RSC edits. */
317
317
  export function getVirtualVersionContent(version: string): string {
318
- return `export const VERSION = ${JSON.stringify(version)};`;
318
+ return [
319
+ `export const VERSION = ${JSON.stringify(version)};`,
320
+ `export const ROUTER_VERSIONS = undefined;`,
321
+ ].join("\n");
322
+ }
323
+
324
+ /**
325
+ * Free identifier the production version module ships in place of the table.
326
+ * The versions are hashes of the built server code, so they cannot be part of
327
+ * it: the buildApp post hook (router-discovery.ts) replaces this token in the
328
+ * written chunk once the hashes are final (runRouterVersionsPhase,
329
+ * discovery/router-versions-phase.ts). An unreplaced token throws a ReferenceError
330
+ * when the module evaluates, so a build that skipped the step cannot serve
331
+ * with a constant version.
332
+ */
333
+ export const ROUTER_VERSIONS_PLACEHOLDER = "__RANGO_ROUTER_VERSIONS__";
334
+
335
+ /**
336
+ * Production: byte-identical in every build, so it never moves the hashes it
337
+ * is later filled with. VERSION is the whole-build document version (the
338
+ * table's "*" entry).
339
+ */
340
+ export function getVirtualBuildVersionContent(): string {
341
+ return [
342
+ `export const ROUTER_VERSIONS = ${ROUTER_VERSIONS_PLACEHOLDER};`,
343
+ `export const VERSION = ROUTER_VERSIONS[${JSON.stringify(DEFAULT_ROUTER_VERSIONS_KEY)}][1];`,
344
+ ].join("\n");
319
345
  }
package/src/vite/rango.ts CHANGED
@@ -2,7 +2,10 @@ import { type PluginOption } from "vite";
2
2
  import { readFileSync, existsSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { exposeActionId } from "./plugins/expose-action-id.js";
5
- import { defineEncryptionKeyExpr } from "./encryption-key.js";
5
+ import {
6
+ configureEncryptionKey,
7
+ defineEncryptionKeyExpr,
8
+ } from "./encryption-key.js";
6
9
  import {
7
10
  createLoaderDirectiveGuardPlugin,
8
11
  createLoaderScanStubPlugin,
@@ -12,6 +15,7 @@ import {
12
15
  import { useCacheTransform } from "./plugins/use-cache-transform.js";
13
16
  import { transitionWhenHoistPlugin } from "./plugins/transition-when-hoist.js";
14
17
  import { clientRefDedup } from "./plugins/client-ref-dedup.js";
18
+ import { stableRscOutput } from "./plugins/stable-rsc-output.js";
15
19
  import { VIRTUAL_IDS } from "./plugins/virtual-entries.js";
16
20
  import {
17
21
  getExcludeDeps,
@@ -43,7 +47,7 @@ import type {
43
47
  } from "./plugin-types.js";
44
48
  import { createVercelOutputPlugin } from "./plugins/vercel-output.js";
45
49
  import { printBanner, rangoVersion } from "./utils/banner.js";
46
- import { createVersionInjectorPlugin } from "./plugins/version-injector.js";
50
+ import { createEntryBootstrapInjectorPlugin } from "./plugins/entry-bootstrap-injector.js";
47
51
  import { createCjsToEsmPlugin } from "./plugins/cjs-to-esm.js";
48
52
  import { createRouterDiscoveryPlugin } from "./router-discovery.js";
49
53
  import { performanceTracksPlugin } from "./plugins/performance-tracks.js";
@@ -76,6 +80,20 @@ const debugConfig = createRangoDebugger(NS.config);
76
80
  */
77
81
  const SERVER_BUILD_TARGET = "esnext";
78
82
 
83
+ /**
84
+ * Entry-signature mode for the node/vercel RSC build, the one
85
+ * `@cloudflare/vite-plugin` forces on its worker entry.
86
+ *
87
+ * Vite's default for a server build, "allow-extension", lets the bundler park
88
+ * modules shared with lazy chunks in the entry chunk. Under a host router that
89
+ * put the router runtime (80 modules, measured on e2e/test-app/.host-fixture
90
+ * and examples/vercel-multi-router) and every sub-app's route names into
91
+ * `index.js`, which every sub-app chunk imported back and which names each
92
+ * sub-app chunk by its content hash: no router's cache version was independent
93
+ * of the others (discovery/build-versions.ts hashes a router's chunks).
94
+ */
95
+ const RSC_ENTRY_SIGNATURES = "strict";
96
+
79
97
  // The leading-directive 'use client' sniff is shared with version-plugin's
80
98
  // getClientModuleSignature so the two cannot drift. Imported for local use by the
81
99
  // HMR transform below and re-exported because the E8 sniff test imports it from
@@ -111,6 +129,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
111
129
  const rangoStart = performance.now();
112
130
  const resolvedOptions: RangoOptions = options ?? { preset: "node" };
113
131
  const preset = resolvedOptions.preset ?? "node";
132
+ // Before any plugin-rsc instance (this build's, the discovery temp server's)
133
+ // reads the key, and so an invalid key fails the config load.
134
+ configureEncryptionKey(resolvedOptions.encryptionKey);
114
135
  const showBanner = resolvedOptions.banner ?? true;
115
136
  const clientChunksOption = resolvedOptions.clientChunks ?? true;
116
137
  const useBuiltInClientChunks = clientChunksOption === true;
@@ -467,6 +488,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
467
488
  ...(vercelServerEnv ?? {}),
468
489
  build: {
469
490
  target: SERVER_BUILD_TARGET,
491
+ rollupOptions: {
492
+ preserveEntrySignatures: RSC_ENTRY_SIGNATURES,
493
+ },
470
494
  },
471
495
  optimizeDeps: {
472
496
  entries: [VIRTUAL_IDS.rsc],
@@ -539,6 +563,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
539
563
  plugins.push(clientRefDedup());
540
564
  }
541
565
  plugins.push(clientChunksReport());
566
+ plugins.push(stableRscOutput());
542
567
 
543
568
  plugins.push({
544
569
  name: "@rangojs/router:client-component-hmr",
@@ -583,7 +608,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
583
608
  const discoveryRouterRef = preset !== "cloudflare" ? routerRef : undefined;
584
609
 
585
610
  if (preset === "cloudflare") {
586
- plugins.push(createVersionInjectorPlugin(undefined));
611
+ plugins.push(createEntryBootstrapInjectorPlugin(undefined));
587
612
  }
588
613
 
589
614
  plugins.push(createCjsToEsmPlugin());
@@ -8,8 +8,8 @@
8
8
 
9
9
  import type { Plugin, ViteDevServer } from "vite";
10
10
  import { createServer as createViteServer } from "vite";
11
- import { resolve } from "node:path";
12
- import { readFileSync } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { existsSync, readFileSync } from "node:fs";
13
13
  import { createRequire, register } from "node:module";
14
14
  import { pathToFileURL } from "node:url";
15
15
  import {
@@ -34,6 +34,7 @@ import {
34
34
  internalDebugNoCacheMiddleware,
35
35
  } from "./inject-client-debug.js";
36
36
  import { createVersionPlugin } from "./plugins/version-plugin.js";
37
+ import { uniformVersions } from "../router-versions.js";
37
38
  import { getVirtualEntrySSR, VIRTUAL_IDS } from "./plugins/virtual-entries.js";
38
39
  import { createVirtualStubPlugin } from "./plugins/virtual-stub-plugin.js";
39
40
  import {
@@ -64,6 +65,13 @@ import {
64
65
  refreshRecordedClientUrlProjections,
65
66
  } from "./discovery/client-urls-projection.js";
66
67
  import { runShellPrerenderPhase } from "./discovery/shell-prerender-phase.js";
68
+ import { runRouterVersionsPhase } from "./discovery/router-versions-phase.js";
69
+ import {
70
+ recordBundleFiles,
71
+ recordClientGraph,
72
+ recordServerGraph,
73
+ recordVersionModuleFiles,
74
+ } from "./discovery/build-versions.js";
67
75
  import { describeDiscoveryFailure } from "./discovery/discovery-errors.js";
68
76
  import { findTransitionWhenError } from "../transition-when-ref.js";
69
77
  import { transitionWhenHoistPlugin } from "./plugins/transition-when-hoist.js";
@@ -320,11 +328,44 @@ import type {
320
328
  BuildEnvResult,
321
329
  } from "./plugin-types.js";
322
330
 
331
+ const WRANGLER_CONFIG_NAMES: readonly string[] = [
332
+ "wrangler.json",
333
+ "wrangler.jsonc",
334
+ "wrangler.toml",
335
+ ];
336
+
337
+ /**
338
+ * getPlatformProxy() looks for the wrangler config from process.cwd() and
339
+ * resolves its default persist path against cwd, so building from another
340
+ * directory dropped the app's bindings (#1037). Search upward from the Vite
341
+ * root with wrangler's own file names and order. Persisted state goes under
342
+ * the Vite root, where @cloudflare/vite-plugin reads it in dev and preview
343
+ * (its getPersistenceRoot), also when the config sits above the root. No
344
+ * config found: no options, wrangler's own lookup.
345
+ */
346
+ function wranglerProxyOptions(
347
+ root: string,
348
+ ): Record<string, unknown> | undefined {
349
+ const viteRoot = resolve(root);
350
+ for (let dir = viteRoot; ; dir = dirname(dir)) {
351
+ for (const name of WRANGLER_CONFIG_NAMES) {
352
+ const configPath = join(dir, name);
353
+ if (existsSync(configPath)) {
354
+ return {
355
+ configPath,
356
+ persist: { path: join(viteRoot, ".wrangler", "state", "v3") },
357
+ };
358
+ }
359
+ }
360
+ if (dirname(dir) === dir) return undefined;
361
+ }
362
+ }
363
+
323
364
  /**
324
365
  * Resolve the buildEnv option into a concrete { env, dispose? } result.
325
366
  * Handles all four input shapes: false, "auto", factory, plain object.
326
367
  */
327
- async function resolveBuildEnv(
368
+ export async function resolveBuildEnv(
328
369
  option: BuildEnvOption | undefined,
329
370
  factoryCtx: BuildEnvFactoryContext,
330
371
  ): Promise<BuildEnvResult | null> {
@@ -348,7 +389,9 @@ async function resolveBuildEnv(
348
389
  )) as {
349
390
  getPlatformProxy: (opts?: any) => Promise<any>;
350
391
  };
351
- const proxy = await getPlatformProxy();
392
+ const proxy = await getPlatformProxy(
393
+ wranglerProxyOptions(factoryCtx.root),
394
+ );
352
395
  return {
353
396
  env: proxy.env as Record<string, unknown>,
354
397
  dispose: proxy.dispose,
@@ -410,6 +453,7 @@ function resetPrerenderCollection(s: DiscoveryState): void {
410
453
  s.staticManifestEntries = null;
411
454
  s.shellCandidates = null;
412
455
  s.prerenderPayloadValues = null;
456
+ s.buildData = [];
413
457
  }
414
458
 
415
459
  /**
@@ -1533,7 +1577,7 @@ export function createRouterDiscoveryPlugin(
1533
1577
  maxSnapshotBytes,
1534
1578
  captureTimeout,
1535
1579
  buildEnv: s.resolvedBuildEnv,
1536
- buildVersion: version,
1580
+ versions: uniformVersions(version),
1537
1581
  captureShellHTML: ssrModule.captureShellHTML,
1538
1582
  debug: !!debugDiscovery,
1539
1583
  });
@@ -2261,18 +2305,27 @@ export function createRouterDiscoveryPlugin(
2261
2305
  }
2262
2306
  },
2263
2307
 
2264
- // Post-build PPR shell capture (producer B, #699): runs after EVERY
2265
- // environment bundle is written — the shell prelude embeds built client
2266
- // asset URLs (bootstrap entry), which do not exist at buildStart. The
2267
- // kept temp server and the buildEnv were deferred AS A PAIR in
2268
- // buildStart's finally; this finally is the pair's success-path owner
2269
- // (buildEnd below owns the aborted-build path) — the phase itself is a
2270
- // pure producer and tears down only the globals it installs.
2308
+ // Post-build phases, after EVERY environment bundle is written.
2309
+ //
2310
+ // 1. Cache versions (discovery/router-versions-phase.ts): hashes the
2311
+ // server output as postprocessBundle left it and writes the per-router
2312
+ // table into the built version module. First, because phase 2 stamps
2313
+ // its entries with these versions and reads the same RSC out dir.
2314
+ // 2. PPR shell capture (producer B, #699): the shell prelude embeds built
2315
+ // client asset URLs (bootstrap entry), which do not exist at
2316
+ // buildStart. The kept temp server and the buildEnv were deferred AS A
2317
+ // PAIR in buildStart's finally; this finally is the pair's success-path
2318
+ // owner (buildEnd below owns the aborted-build path) — the phase itself
2319
+ // is a pure producer and tears down only the globals it installs.
2271
2320
  buildApp: {
2272
2321
  order: "post",
2273
2322
  async handler(builder) {
2274
2323
  try {
2275
- await runShellPrerenderPhase(s, builder as any);
2324
+ await runShellPrerenderPhase(
2325
+ s,
2326
+ builder as any,
2327
+ runRouterVersionsPhase(s, builder as any),
2328
+ );
2276
2329
  } finally {
2277
2330
  if (s.isBuildMode) {
2278
2331
  const tempServer = s.shellPhaseTempServer;
@@ -2387,6 +2440,22 @@ export function createRouterDiscoveryPlugin(
2387
2440
  // Record handler chunk metadata and RSC entry filename during RSC build.
2388
2441
  // Used by closeBundle for handler code eviction and prerender data injection.
2389
2442
  generateBundle(_options: any, bundle: any) {
2443
+ // Inputs of the per-router cache versions (discovery/build-versions.ts).
2444
+ // Each pass overwrites its slot: plugin-rsc runs the RSC and SSR builds
2445
+ // twice, and the real pass comes last.
2446
+ if (s.isBuildMode) {
2447
+ const envName = this.environment?.name;
2448
+ if (envName) {
2449
+ s.versionModuleFiles.set(envName, recordVersionModuleFiles(bundle));
2450
+ }
2451
+ if (envName === "rsc") {
2452
+ s.serverBuildGraph = recordServerGraph(this, bundle);
2453
+ } else if (envName === "client") {
2454
+ s.clientBuildGraph = recordClientGraph(this, bundle);
2455
+ } else if (envName === "ssr") {
2456
+ s.ssrBundle = recordBundleFiles(bundle);
2457
+ }
2458
+ }
2390
2459
  if (this.environment?.name !== "rsc") return;
2391
2460
  const genStart = debugBuild ? performance.now() : 0;
2392
2461
 
@@ -206,8 +206,12 @@ export function resolveClientChunks(
206
206
  return option;
207
207
  }
208
208
 
209
+ /** @vitejs/plugin-rsc's client-reference map; its group modules sit under it. */
210
+ export const CLIENT_REFERENCES_MODULE_ID =
211
+ "\0virtual:vite-rsc/client-references";
212
+
209
213
  /** Module-id prefix of the per-group virtual module @vitejs/plugin-rsc emits. */
210
- const CLIENT_GROUP_PREFIX = "\0virtual:vite-rsc/client-references/group/";
214
+ const CLIENT_GROUP_PREFIX = `${CLIENT_REFERENCES_MODULE_ID}/group/`;
211
215
 
212
216
  /**
213
217
  * Build-end group report under `DEBUG=rango:chunks`: one line per client
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Code-unit order, the same on every machine. Generated modules and the cache
3
+ * version inputs are sorted with it: `localeCompare` depends on the build
4
+ * machine's ICU data, and build output has to be byte-identical across machines
5
+ * (discovery/build-versions.ts hashes it).
6
+ */
7
+ export function compareStrings(a: string, b: string): number {
8
+ return a < b ? -1 : a > b ? 1 : 0;
9
+ }
@@ -72,11 +72,20 @@ export function getPackageAliases(): Record<string, string> {
72
72
  return aliases;
73
73
  }
74
74
 
75
+ /**
76
+ * @vitejs/plugin-rsc puts these vendor specifiers in `optimizeDeps.include`,
77
+ * and Vite resolves them from the app root, where pnpm's strict layout does
78
+ * not link the plugin unless the app lists it (#1050). Aliased to the copy
79
+ * the router resolves, which therefore also wins over an app's own
80
+ * @vitejs/plugin-rsc. Must hold every vendor entry the plugin includes:
81
+ * optimize-deps-vendor-resolution.test.ts checks the resolved config.
82
+ */
75
83
  export function getVendorAliases(): Record<string, string> {
76
84
  const specs = [
77
85
  "@vitejs/plugin-rsc/vendor/react-server-dom/client.browser",
78
86
  "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
79
87
  "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
88
+ "@vitejs/plugin-rsc/vendor/react-server-dom/static.edge",
80
89
  ];
81
90
  const aliases: Record<string, string> = {};
82
91
  for (const spec of specs) {
@@ -199,8 +199,11 @@ export function resolvePrerenderError(
199
199
  throw error;
200
200
  }
201
201
 
202
+ /** Build scratch space, relative to the project root. Never shipped. */
203
+ export const RANGO_BUILD_DIR = "node_modules/.rangojs-router-build";
204
+
202
205
  function getStagedAssetDir(projectRoot: string): string {
203
- return resolve(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
206
+ return resolve(projectRoot, RANGO_BUILD_DIR, "rsc-assets");
204
207
  }
205
208
 
206
209
  export function resetStagedBuildAssets(projectRoot: string): void {
@@ -15,7 +15,7 @@ import type { HeadScriptsOption } from "../plugin-types.js";
15
15
 
16
16
  // Cloudflare preset: @cloudflare/vite-plugin sets optimizeDeps.entries (string
17
17
  // or array) on the rsc environment. Single source for both the discovery plugin
18
- // and the version injector so they target the same entry.
18
+ // and the entry bootstrap injector so they target the same entry.
19
19
  export function resolveRscEntryFromConfig(
20
20
  config: ResolvedConfig,
21
21
  ): string | undefined {