@rangojs/router 0.0.0-experimental.48 → 0.0.0-experimental.4ffa0f9b

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 (108) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +130 -47
  3. package/dist/vite/index.js +421 -121
  4. package/package.json +2 -2
  5. package/skills/cache-guide/SKILL.md +32 -0
  6. package/skills/caching/SKILL.md +8 -0
  7. package/skills/links/SKILL.md +3 -1
  8. package/skills/loader/SKILL.md +53 -43
  9. package/skills/middleware/SKILL.md +2 -0
  10. package/skills/parallel/SKILL.md +67 -0
  11. package/skills/prerender/SKILL.md +110 -68
  12. package/skills/route/SKILL.md +31 -0
  13. package/skills/router-setup/SKILL.md +87 -2
  14. package/skills/typesafety/SKILL.md +10 -0
  15. package/src/__internal.ts +1 -1
  16. package/src/browser/app-version.ts +14 -0
  17. package/src/browser/navigation-bridge.ts +16 -3
  18. package/src/browser/navigation-client.ts +64 -40
  19. package/src/browser/navigation-store.ts +43 -8
  20. package/src/browser/partial-update.ts +27 -5
  21. package/src/browser/prefetch/fetch.ts +8 -2
  22. package/src/browser/prefetch/queue.ts +61 -29
  23. package/src/browser/prefetch/resource-ready.ts +77 -0
  24. package/src/browser/react/Link.tsx +44 -8
  25. package/src/browser/react/NavigationProvider.tsx +13 -4
  26. package/src/browser/react/context.ts +7 -2
  27. package/src/browser/react/use-handle.ts +9 -58
  28. package/src/browser/react/use-router.ts +21 -8
  29. package/src/browser/rsc-router.tsx +26 -3
  30. package/src/browser/scroll-restoration.ts +10 -8
  31. package/src/browser/server-action-bridge.ts +8 -6
  32. package/src/browser/types.ts +27 -5
  33. package/src/build/generate-manifest.ts +6 -6
  34. package/src/build/generate-route-types.ts +3 -0
  35. package/src/build/route-types/include-resolution.ts +8 -1
  36. package/src/build/route-types/router-processing.ts +211 -72
  37. package/src/build/route-types/scan-filter.ts +8 -1
  38. package/src/cache/cache-scope.ts +46 -5
  39. package/src/cache/taint.ts +55 -0
  40. package/src/client.tsx +2 -56
  41. package/src/context-var.ts +72 -2
  42. package/src/handle.ts +40 -0
  43. package/src/index.rsc.ts +3 -1
  44. package/src/index.ts +8 -0
  45. package/src/prerender/store.ts +5 -4
  46. package/src/prerender.ts +138 -77
  47. package/src/reverse.ts +22 -1
  48. package/src/route-definition/dsl-helpers.ts +42 -19
  49. package/src/route-definition/helpers-types.ts +10 -6
  50. package/src/route-definition/index.ts +3 -0
  51. package/src/route-definition/redirect.ts +9 -1
  52. package/src/route-definition/resolve-handler-use.ts +149 -0
  53. package/src/route-types.ts +11 -0
  54. package/src/router/content-negotiation.ts +100 -1
  55. package/src/router/handler-context.ts +79 -23
  56. package/src/router/intercept-resolution.ts +9 -4
  57. package/src/router/loader-resolution.ts +121 -10
  58. package/src/router/match-api.ts +124 -189
  59. package/src/router/match-middleware/cache-lookup.ts +22 -7
  60. package/src/router/match-middleware/cache-store.ts +5 -0
  61. package/src/router/match-middleware/segment-resolution.ts +53 -0
  62. package/src/router/middleware-types.ts +6 -8
  63. package/src/router/middleware.ts +2 -5
  64. package/src/router/navigation-snapshot.ts +182 -0
  65. package/src/router/prerender-match.ts +110 -10
  66. package/src/router/preview-match.ts +30 -102
  67. package/src/router/request-classification.ts +310 -0
  68. package/src/router/route-snapshot.ts +245 -0
  69. package/src/router/router-interfaces.ts +36 -4
  70. package/src/router/router-options.ts +37 -11
  71. package/src/router/segment-resolution/fresh.ts +65 -9
  72. package/src/router/segment-resolution/helpers.ts +29 -24
  73. package/src/router/segment-resolution/revalidation.ts +65 -7
  74. package/src/router/types.ts +1 -0
  75. package/src/router.ts +54 -5
  76. package/src/rsc/handler.ts +460 -368
  77. package/src/rsc/manifest-init.ts +5 -1
  78. package/src/rsc/progressive-enhancement.ts +4 -0
  79. package/src/rsc/rsc-rendering.ts +5 -0
  80. package/src/rsc/server-action.ts +2 -0
  81. package/src/rsc/ssr-setup.ts +2 -2
  82. package/src/rsc/types.ts +8 -1
  83. package/src/server/context.ts +50 -1
  84. package/src/server/loader-registry.ts +9 -8
  85. package/src/server/request-context.ts +134 -14
  86. package/src/ssr/index.tsx +3 -0
  87. package/src/static-handler.ts +18 -6
  88. package/src/types/cache-types.ts +4 -4
  89. package/src/types/handler-context.ts +37 -19
  90. package/src/types/loader-types.ts +36 -9
  91. package/src/types/route-entry.ts +1 -1
  92. package/src/urls/path-helper-types.ts +9 -2
  93. package/src/urls/path-helper.ts +47 -12
  94. package/src/urls/pattern-types.ts +12 -0
  95. package/src/urls/response-types.ts +16 -6
  96. package/src/use-loader.tsx +73 -4
  97. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  98. package/src/vite/discovery/discover-routers.ts +5 -1
  99. package/src/vite/discovery/prerender-collection.ts +14 -1
  100. package/src/vite/discovery/state.ts +13 -4
  101. package/src/vite/index.ts +4 -0
  102. package/src/vite/plugin-types.ts +60 -5
  103. package/src/vite/plugins/performance-tracks.ts +88 -0
  104. package/src/vite/plugins/refresh-cmd.ts +88 -26
  105. package/src/vite/rango.ts +19 -2
  106. package/src/vite/router-discovery.ts +153 -34
  107. package/src/vite/utils/prerender-utils.ts +18 -0
  108. package/src/vite/utils/shared-utils.ts +3 -2
@@ -54,11 +54,12 @@ export async function expandPrerenderRoutes(
54
54
  for (const { manifest } of allManifests) {
55
55
  if (!manifest.prerenderRoutes) continue;
56
56
  const defs = manifest._prerenderDefs || {};
57
+ const passthroughSet = new Set(manifest.passthroughRoutes || []);
57
58
  for (const routeName of manifest.prerenderRoutes) {
58
59
  const pattern = manifest.routeManifest[routeName];
59
60
  if (!pattern) continue;
60
61
  const def = defs[routeName];
61
- const isPassthroughRoute = !!def?.options?.passthrough;
62
+ const isPassthroughRoute = passthroughSet.has(routeName);
62
63
  const hasDynamic = pattern.includes(":") || pattern.includes("*");
63
64
  if (!hasDynamic) {
64
65
  // Static route: use pattern directly (strip trailing slash for URL)
@@ -73,12 +74,21 @@ export async function expandPrerenderRoutes(
73
74
  if (def?.getParams) {
74
75
  try {
75
76
  const buildVars: Record<string, any> = {};
77
+ const buildEnv = state.resolvedBuildEnv;
76
78
  const getParamsCtx = {
77
79
  build: true as const,
80
+ dev: !state.isBuildMode,
78
81
  set: ((keyOrVar: any, value: any) => {
79
82
  contextSet(buildVars, keyOrVar, value);
80
83
  }) as any,
81
84
  reverse: getParamsReverse,
85
+ get env() {
86
+ if (buildEnv !== undefined) return buildEnv;
87
+ throw new Error(
88
+ "[rsc-router] ctx.env is not available during build-time getParams(). " +
89
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
90
+ );
91
+ },
82
92
  };
83
93
  const paramsList = await def.getParams(getParamsCtx);
84
94
  const concurrency = def.options?.concurrency ?? 1;
@@ -175,6 +185,7 @@ export async function expandPrerenderRoutes(
175
185
  {},
176
186
  entry.buildVars,
177
187
  entry.isPassthroughRoute,
188
+ state.resolvedBuildEnv,
178
189
  );
179
190
  if (!result) continue;
180
191
 
@@ -326,6 +337,8 @@ export async function renderStaticHandlers(
326
337
  def.handler,
327
338
  def.$$id,
328
339
  (def as any).$$routePrefix,
340
+ state.resolvedBuildEnv,
341
+ !state.isBuildMode,
329
342
  );
330
343
  if (result) {
331
344
  const hasHandles = Object.keys(result.handles).length > 0;
@@ -16,6 +16,10 @@ export interface PluginOptions {
16
16
  // Mutable ref for deferred auto-discovery (node preset).
17
17
  // The auto-discover config() hook populates this before configResolved.
18
18
  routerPathRef?: { path?: string };
19
+ /** Build-time env option from rango() config. */
20
+ buildEnv?: import("../plugin-types.js").BuildEnvOption;
21
+ /** Deployment preset (needed for buildEnv "auto" resolution). */
22
+ preset?: "node" | "cloudflare";
19
23
  }
20
24
 
21
25
  export interface PrecomputedEntry {
@@ -56,8 +60,8 @@ export interface DiscoveryState {
56
60
 
57
61
  prerenderManifestEntries: Record<string, string> | null;
58
62
  staticManifestEntries: Record<string, string> | null;
59
- handlerChunkInfo: ChunkInfo | null;
60
- staticHandlerChunkInfo: ChunkInfo | null;
63
+ handlerChunkInfoMap: Map<string, ChunkInfo>;
64
+ staticHandlerChunkInfoMap: Map<string, ChunkInfo>;
61
65
  rscEntryFileName: string | null;
62
66
  resolvedPrerenderModules: Map<string, string[]> | undefined;
63
67
  resolvedStaticModules: Map<string, string[]> | undefined;
@@ -67,6 +71,11 @@ export interface DiscoveryState {
67
71
  devServer: any;
68
72
  selfWrittenGenFiles: Map<string, { at: number; hash: string }>;
69
73
  SELF_WRITE_WINDOW_MS: number;
74
+
75
+ /** Resolved build-time env bindings (set during buildStart/configureServer). */
76
+ resolvedBuildEnv?: Record<string, unknown>;
77
+ /** Cleanup function for build-time env resources (e.g., miniflare). */
78
+ buildEnvDispose?: (() => Promise<void> | void) | null;
70
79
  }
71
80
 
72
81
  export function createDiscoveryState(
@@ -93,8 +102,8 @@ export function createDiscoveryState(
93
102
 
94
103
  prerenderManifestEntries: null,
95
104
  staticManifestEntries: null,
96
- handlerChunkInfo: null,
97
- staticHandlerChunkInfo: null,
105
+ handlerChunkInfoMap: new Map(),
106
+ staticHandlerChunkInfoMap: new Map(),
98
107
  rscEntryFileName: null,
99
108
  resolvedPrerenderModules: undefined,
100
109
  resolvedStaticModules: undefined,
package/src/vite/index.ts CHANGED
@@ -13,4 +13,8 @@ export type {
13
13
  RangoNodeOptions,
14
14
  RangoCloudflareOptions,
15
15
  RangoOptions,
16
+ BuildEnvOption,
17
+ BuildEnvFactory,
18
+ BuildEnvFactoryContext,
19
+ BuildEnvResult,
16
20
  } from "./plugin-types.js";
@@ -1,3 +1,54 @@
1
+ // -- Build-time environment types -------------------------------------------
2
+
3
+ /**
4
+ * Context passed to a buildEnv factory function.
5
+ * Provides Vite config details for conditional env setup.
6
+ */
7
+ export interface BuildEnvFactoryContext {
8
+ /** Vite project root directory. */
9
+ root: string;
10
+ /** Vite mode (e.g. "development", "production"). */
11
+ mode: string;
12
+ /** Vite command ("serve" for dev, "build" for production). */
13
+ command: "serve" | "build";
14
+ /** Router deployment preset. */
15
+ preset: "node" | "cloudflare";
16
+ }
17
+
18
+ /**
19
+ * Factory function that creates build-time environment bindings.
20
+ * Called once at plugin startup. Return `dispose` to clean up resources.
21
+ */
22
+ export type BuildEnvFactory = (
23
+ ctx: BuildEnvFactoryContext,
24
+ ) => Promise<BuildEnvResult> | BuildEnvResult;
25
+
26
+ /**
27
+ * Result of resolving build-time environment bindings.
28
+ */
29
+ export interface BuildEnvResult {
30
+ /** Environment bindings available to Prerender/Static handlers via ctx.env. */
31
+ env: Record<string, unknown>;
32
+ /** Called after build completes to clean up resources (e.g., miniflare). */
33
+ dispose?: () => Promise<void> | void;
34
+ }
35
+
36
+ /**
37
+ * Build-time environment configuration for Prerender and Static handlers.
38
+ *
39
+ * - `false` (default): no build-time env, `ctx.env` throws.
40
+ * - `"auto"`: calls `wrangler.getPlatformProxy()` (cloudflare preset only).
41
+ * - Object: used directly as `ctx.env` during build.
42
+ * - Factory: called once at startup, must return `{ env, dispose? }`.
43
+ */
44
+ export type BuildEnvOption =
45
+ | false
46
+ | "auto"
47
+ | Record<string, unknown>
48
+ | BuildEnvFactory;
49
+
50
+ // -- Plugin options ---------------------------------------------------------
51
+
1
52
  /**
2
53
  * Base options shared by all presets
3
54
  */
@@ -9,12 +60,16 @@ interface RangoBaseOptions {
9
60
  banner?: boolean;
10
61
 
11
62
  /**
12
- * Generate named-routes.gen.ts by parsing url modules at startup.
13
- * Provides type-safe Handler<"name"> and href() without executing router code.
14
- * Set to `false` to disable (run `npx rango extract-names` manually instead).
15
- * @default true
63
+ * Environment bindings available to Prerender and Static handlers at build
64
+ * time via `ctx.env`. Applies to both production build and dev on-demand
65
+ * prerender (`/__rsc_prerender`).
66
+ *
67
+ * This is the build-time env supplied by the Vite plugin, not the live
68
+ * request env. It is shared across all prerender invocations for the build.
69
+ *
70
+ * @default false
16
71
  */
17
- staticRouteTypesGeneration?: boolean;
72
+ buildEnv?: BuildEnvOption;
18
73
  }
19
74
 
20
75
  /**
@@ -0,0 +1,88 @@
1
+ /**
2
+ * React Performance Tracks — RSDW client patch
3
+ *
4
+ * Patches the RSDW client so _debugInfo recovery works for plain-object
5
+ * payloads (our RscPayload shape). Without this, the Server Components
6
+ * track in Chrome DevTools stays empty.
7
+ *
8
+ * React's flushComponentPerformance uses splice(0) to empty _debugInfo
9
+ * after resolution, then recovers it from the resolved value — but only
10
+ * for arrays, async iterables, React elements, and lazy types. Since our
11
+ * RscPayload is a plain object, _debugInfo is lost. This patch relaxes
12
+ * the check so _debugInfo is recovered from any object.
13
+ */
14
+
15
+ import type { Plugin } from "vite";
16
+ import { readFile } from "node:fs/promises";
17
+
18
+ const RSDW_PATCH_RE =
19
+ /((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
20
+
21
+ function buildPatchReplacement(match: string, debugInfoVar: string): string {
22
+ return `${match}
23
+ if (${debugInfoVar} && 0 === ${debugInfoVar}.length && "fulfilled" === root.status) {
24
+ var _resolved = "function" === typeof resolveLazy ? resolveLazy(root.value) : root.value;
25
+ if ("object" === typeof _resolved && null !== _resolved && isArrayImpl(_resolved._debugInfo)) {
26
+ ${debugInfoVar} = _resolved._debugInfo;
27
+ }
28
+ }`;
29
+ }
30
+
31
+ export function patchRsdwClientDebugInfoRecovery(code: string): {
32
+ code: string;
33
+ debugInfoVar: string | null;
34
+ } {
35
+ const match = code.match(RSDW_PATCH_RE);
36
+ if (!match) {
37
+ return { code, debugInfoVar: null };
38
+ }
39
+
40
+ return {
41
+ code: code.replace(match[1]!, buildPatchReplacement(match[1]!, match[2]!)),
42
+ debugInfoVar: match[2]!,
43
+ };
44
+ }
45
+
46
+ export function performanceTracksOptimizeDepsPlugin(): {
47
+ name: string;
48
+ setup(build: any): void;
49
+ } {
50
+ return {
51
+ name: "@rangojs/router:performance-tracks-optimize-deps",
52
+ setup(build: any): void {
53
+ build.onLoad(
54
+ {
55
+ filter:
56
+ /react-server-dom-webpack-client\.browser\.(development|production)\.js$/,
57
+ },
58
+ async (args: { path: string }) => {
59
+ const code = await readFile(args.path, "utf8");
60
+ const patched = patchRsdwClientDebugInfoRecovery(code);
61
+ return {
62
+ contents: patched.code,
63
+ loader: "js",
64
+ };
65
+ },
66
+ );
67
+ },
68
+ };
69
+ }
70
+
71
+ export function performanceTracksPlugin(): Plugin {
72
+ return {
73
+ name: "@rangojs/router:performance-tracks",
74
+
75
+ transform(code, id) {
76
+ if (!id.includes("react-server-dom") || !id.includes("client")) return;
77
+ const patched = patchRsdwClientDebugInfoRecovery(code);
78
+ if (!patched.debugInfoVar) return;
79
+ if (process.env.INTERNAL_RANGO_DEBUG)
80
+ console.log(
81
+ "[perf-tracks] patched RSDW client (var:",
82
+ patched.debugInfoVar,
83
+ ")",
84
+ );
85
+ return patched.code;
86
+ },
87
+ };
88
+ }
@@ -1,8 +1,13 @@
1
1
  import type { Plugin } from "vite";
2
2
 
3
3
  /**
4
- * Vite plugin that triggers a full browser reload when Ctrl+R is pressed
5
- * in the terminal running the dev server.
4
+ * Vite plugin that triggers a full browser reload from terminal input.
5
+ *
6
+ * This plugin is intentionally passive:
7
+ * - it never enables raw mode on stdin
8
+ * - it never restores terminal state
9
+ * - it reacts to Ctrl+R when that raw byte reaches the process
10
+ * - it also supports safe line-based fallbacks like "e" + Enter
6
11
  *
7
12
  * Usage:
8
13
  * ```ts
@@ -20,35 +25,95 @@ export function poke(): Plugin {
20
25
 
21
26
  configureServer(server) {
22
27
  const stdin = process.stdin;
28
+ const debug = process.env.RANGO_POKE_DEBUG === "1";
29
+
30
+ const triggerReload = (source: string) => {
31
+ server.hot.send({ type: "full-reload", path: "*" });
32
+ server.config.logger.info(` browser reload (${source})`, {
33
+ timestamp: true,
34
+ });
35
+ };
36
+
37
+ const toBuffer = (chunk: string | Buffer): Buffer => {
38
+ return typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
39
+ };
40
+
41
+ const formatChunk = (chunk: string | Buffer): string => {
42
+ const data = toBuffer(chunk);
43
+ const hex = Array.from(data)
44
+ .map((byte) => `0x${byte.toString(16).padStart(2, "0")}`)
45
+ .join(" ");
46
+ const ascii = Array.from(data)
47
+ .map((byte) => {
48
+ if (byte >= 0x20 && byte <= 0x7e) return String.fromCharCode(byte);
49
+ if (byte === 0x0a) return "\\n";
50
+ if (byte === 0x0d) return "\\r";
51
+ if (byte === 0x09) return "\\t";
52
+ return ".";
53
+ })
54
+ .join("");
55
+ return `len=${data.length} hex=[${hex}] ascii="${ascii}"`;
56
+ };
57
+
58
+ const readCtrlR = (chunk: string | Buffer): boolean => {
59
+ const data =
60
+ typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
61
+ return data.length === 1 && data[0] === 0x12;
62
+ };
63
+
64
+ const readSubmittedCommands = (chunk: string | Buffer): string[] => {
65
+ const text = toBuffer(chunk)
66
+ .toString("utf8")
67
+ .replace(/\r\n/g, "\n")
68
+ .replace(/\r/g, "\n");
69
+
70
+ if (!text.includes("\n")) return [];
71
+
72
+ const lines = text.split("\n");
73
+ lines.pop();
74
+ return lines;
75
+ };
76
+
77
+ if (debug) {
78
+ server.config.logger.info(
79
+ ` poke debug enabled (isTTY=${stdin.isTTY ? "yes" : "no"}, isRaw=${stdin.isTTY ? (stdin.isRaw ? "yes" : "no") : "n/a"})`,
80
+ { timestamp: true },
81
+ );
82
+ }
23
83
 
24
- // Raw mode delivers individual keystrokes as immediate single-byte
25
- // events instead of waiting for Enter (cooked/line-buffered mode).
26
- // Without it, Ctrl+R (0x12) is never delivered as a discrete byte.
27
- // When stdin is a pipe (CI, spawned process) setRawMode is unavailable
28
- // but data already arrives unbuffered, so the isTTY guard suffices.
29
- const previousRawMode = stdin.isTTY ? stdin.isRaw : null;
30
84
  if (stdin.isTTY) {
31
- stdin.setRawMode(true);
85
+ server.config.logger.info(
86
+ " poke ready: press e + enter to reload browser (ctrl+r also works when available)",
87
+ { timestamp: true },
88
+ );
32
89
  }
33
90
 
34
- const onData = (data: Buffer) => {
35
- if (data.length !== 1) return;
91
+ const onData = (data: string | Buffer) => {
92
+ if (debug) {
93
+ server.config.logger.info(` poke stdin ${formatChunk(data)}`, {
94
+ timestamp: true,
95
+ });
96
+ }
36
97
 
37
- // Ctrl+C (0x03) defensive fallback. This plugin enables raw mode
38
- // before Vite's internal stdin handler is registered (user plugins
39
- // run first), so there is a brief window where Ctrl+C would be
40
- // swallowed. Re-emit SIGINT so the process exits as expected.
41
- if (data[0] === 0x03) {
42
- process.emit("SIGINT", "SIGINT");
98
+ // Only react to the exact Ctrl+R byte when some host terminal or
99
+ // wrapper already delivers it to this process. We intentionally do
100
+ // not enable raw mode here because that can steal Vite shortcuts
101
+ // like "r" / "q" and interfere with terminal-level controls.
102
+ if (readCtrlR(data)) {
103
+ triggerReload("ctrl+r");
43
104
  return;
44
105
  }
45
106
 
46
- // Ctrl+R = 0x12 in raw mode
47
- if (data[0] === 0x12) {
48
- server.hot.send({ type: "full-reload", path: "*" });
49
- server.config.logger.info(" browser reload (ctrl+r)", {
50
- timestamp: true,
51
- });
107
+ for (const command of readSubmittedCommands(data)) {
108
+ if (command === "e") {
109
+ triggerReload("e+enter");
110
+ return;
111
+ }
112
+
113
+ if (command === "\u001br") {
114
+ triggerReload("option+r+enter");
115
+ return;
116
+ }
52
117
  }
53
118
  };
54
119
 
@@ -56,9 +121,6 @@ export function poke(): Plugin {
56
121
 
57
122
  server.httpServer?.on("close", () => {
58
123
  stdin.off("data", onData);
59
- if (stdin.isTTY && previousRawMode !== null) {
60
- stdin.setRawMode(previousRawMode);
61
- }
62
124
  });
63
125
  },
64
126
  };
package/src/vite/rango.ts CHANGED
@@ -26,6 +26,7 @@ import { printBanner, rangoVersion } from "./utils/banner.js";
26
26
  import { createVersionInjectorPlugin } from "./plugins/version-injector.js";
27
27
  import { createCjsToEsmPlugin } from "./plugins/cjs-to-esm.js";
28
28
  import { createRouterDiscoveryPlugin } from "./router-discovery.js";
29
+ import { performanceTracksPlugin } from "./plugins/performance-tracks.js";
29
30
 
30
31
  /**
31
32
  * Vite plugin for @rangojs/router.
@@ -60,7 +61,16 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
60
61
 
61
62
  // Get package resolution info (workspace vs npm install)
62
63
  const rangoAliases = getPackageAliases();
63
- const excludeDeps = getExcludeDeps();
64
+ const excludeDeps = [
65
+ ...getExcludeDeps(),
66
+ // The public browser entry re-exports the RSDW browser client.
67
+ // Excluding both keeps Vite from freezing the unpatched bundle into
68
+ // .vite/deps before our source transforms run.
69
+ "@vitejs/plugin-rsc/browser",
70
+ // Keep the browser RSDW client out of Vite's dep optimizer so our
71
+ // cjs-to-esm transform can patch the real file.
72
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.browser",
73
+ ];
64
74
 
65
75
  // Mutable ref for router path (node preset only).
66
76
  // Set immediately when user-specified, or populated by the auto-discover
@@ -182,6 +192,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
182
192
 
183
193
  plugins.push(createVirtualEntriesPlugin(finalEntries));
184
194
 
195
+ // Dev-only: RSDW client patch for React Performance Tracks
196
+ plugins.push(performanceTracksPlugin());
197
+
185
198
  // Add RSC plugin with cloudflare-specific options
186
199
  // Note: loadModuleDevProxy should NOT be used with childEnvironments
187
200
  // since SSR runs in workerd alongside RSC
@@ -334,6 +347,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
334
347
  // Add virtual entries plugin (RSC entry generated lazily from routerRef)
335
348
  plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
336
349
 
350
+ // Dev-only: RSDW client patch for React Performance Tracks
351
+ plugins.push(performanceTracksPlugin());
352
+
337
353
  plugins.push(
338
354
  rsc({
339
355
  entries: finalEntries,
@@ -437,7 +453,8 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
437
453
  createRouterDiscoveryPlugin(discoveryEntryPath, {
438
454
  routerPathRef: discoveryRouterRef,
439
455
  enableBuildPrerender: prerenderEnabled,
440
- staticRouteTypesGeneration: resolvedOptions.staticRouteTypesGeneration,
456
+ buildEnv: options?.buildEnv,
457
+ preset,
441
458
  }),
442
459
  );
443
460