@rangojs/router 0.0.0-experimental.39 → 0.0.0-experimental.3b1deca8

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 (89) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/vite/index.js +292 -204
  3. package/package.json +1 -1
  4. package/skills/cache-guide/SKILL.md +32 -0
  5. package/skills/caching/SKILL.md +45 -4
  6. package/skills/loader/SKILL.md +53 -43
  7. package/skills/parallel/SKILL.md +126 -0
  8. package/skills/route/SKILL.md +31 -0
  9. package/skills/router-setup/SKILL.md +52 -2
  10. package/skills/typesafety/SKILL.md +10 -0
  11. package/src/browser/debug-channel.ts +93 -0
  12. package/src/browser/event-controller.ts +5 -0
  13. package/src/browser/navigation-bridge.ts +1 -5
  14. package/src/browser/navigation-client.ts +84 -27
  15. package/src/browser/navigation-transaction.ts +11 -9
  16. package/src/browser/partial-update.ts +50 -9
  17. package/src/browser/prefetch/cache.ts +57 -5
  18. package/src/browser/prefetch/fetch.ts +30 -21
  19. package/src/browser/prefetch/queue.ts +92 -20
  20. package/src/browser/prefetch/resource-ready.ts +77 -0
  21. package/src/browser/react/Link.tsx +9 -1
  22. package/src/browser/react/NavigationProvider.tsx +32 -3
  23. package/src/browser/rsc-router.tsx +109 -57
  24. package/src/browser/scroll-restoration.ts +31 -34
  25. package/src/browser/segment-reconciler.ts +6 -1
  26. package/src/browser/server-action-bridge.ts +12 -0
  27. package/src/browser/types.ts +17 -1
  28. package/src/build/route-types/router-processing.ts +12 -2
  29. package/src/cache/cache-runtime.ts +15 -11
  30. package/src/cache/cache-scope.ts +48 -7
  31. package/src/cache/cf/cf-cache-store.ts +453 -11
  32. package/src/cache/cf/index.ts +5 -1
  33. package/src/cache/document-cache.ts +17 -7
  34. package/src/cache/index.ts +1 -0
  35. package/src/cache/taint.ts +55 -0
  36. package/src/context-var.ts +72 -2
  37. package/src/debug.ts +2 -2
  38. package/src/deps/browser.ts +1 -0
  39. package/src/route-definition/dsl-helpers.ts +32 -7
  40. package/src/route-definition/helpers-types.ts +6 -5
  41. package/src/route-definition/redirect.ts +2 -2
  42. package/src/route-map-builder.ts +7 -1
  43. package/src/router/find-match.ts +4 -2
  44. package/src/router/handler-context.ts +31 -8
  45. package/src/router/intercept-resolution.ts +2 -0
  46. package/src/router/lazy-includes.ts +4 -1
  47. package/src/router/loader-resolution.ts +7 -1
  48. package/src/router/logging.ts +5 -2
  49. package/src/router/manifest.ts +9 -3
  50. package/src/router/match-middleware/background-revalidation.ts +30 -2
  51. package/src/router/match-middleware/cache-lookup.ts +66 -9
  52. package/src/router/match-middleware/cache-store.ts +53 -10
  53. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  54. package/src/router/match-middleware/segment-resolution.ts +8 -5
  55. package/src/router/match-result.ts +22 -6
  56. package/src/router/metrics.ts +6 -1
  57. package/src/router/middleware-types.ts +6 -2
  58. package/src/router/middleware.ts +4 -3
  59. package/src/router/router-context.ts +6 -1
  60. package/src/router/segment-resolution/fresh.ts +130 -17
  61. package/src/router/segment-resolution/helpers.ts +29 -24
  62. package/src/router/segment-resolution/loader-cache.ts +1 -0
  63. package/src/router/segment-resolution/revalidation.ts +352 -290
  64. package/src/router/segment-wrappers.ts +2 -0
  65. package/src/router/types.ts +1 -0
  66. package/src/router.ts +6 -1
  67. package/src/rsc/handler.ts +28 -2
  68. package/src/rsc/loader-fetch.ts +7 -2
  69. package/src/rsc/progressive-enhancement.ts +4 -1
  70. package/src/rsc/rsc-rendering.ts +4 -1
  71. package/src/rsc/server-action.ts +2 -0
  72. package/src/rsc/types.ts +7 -1
  73. package/src/segment-system.tsx +140 -4
  74. package/src/server/context.ts +102 -13
  75. package/src/server/request-context.ts +59 -12
  76. package/src/ssr/index.tsx +1 -0
  77. package/src/types/handler-context.ts +120 -22
  78. package/src/types/loader-types.ts +4 -4
  79. package/src/types/route-entry.ts +7 -0
  80. package/src/types/segments.ts +2 -0
  81. package/src/urls/path-helper.ts +1 -1
  82. package/src/vite/discovery/state.ts +0 -2
  83. package/src/vite/plugin-types.ts +0 -83
  84. package/src/vite/plugins/expose-action-id.ts +1 -3
  85. package/src/vite/plugins/performance-tracks.ts +235 -0
  86. package/src/vite/plugins/version-plugin.ts +13 -1
  87. package/src/vite/rango.ts +148 -209
  88. package/src/vite/router-discovery.ts +0 -8
  89. package/src/vite/utils/banner.ts +3 -3
@@ -0,0 +1,235 @@
1
+ /**
2
+ * React Performance Tracks — Vite plugin
3
+ *
4
+ * Dev-only plugin that enables Chrome DevTools Performance tab integration
5
+ * for React Server Components. Creates a bidirectional debug channel per
6
+ * RSC request and transports data over Vite's HMR WebSocket.
7
+ *
8
+ * Architecture:
9
+ * - Server: renderToReadableStream writes timing data to debugChannel.writable
10
+ * - Transport: chunks are base64-encoded and sent via HMR custom events
11
+ * - Client: createFromFetch reads from debugChannel.readable
12
+ *
13
+ * Each request gets a unique debugId (UUID) to correlate the two sides.
14
+ *
15
+ * Uses globalThis to share state between the Vite plugin (main process)
16
+ * and the RSC handler (RSC module graph) — they run in the same Node.js
17
+ * process but different module evaluation contexts.
18
+ */
19
+
20
+ import type { Plugin } from "vite";
21
+
22
+ export const DEBUG_ID_HEADER = "X-RSC-Debug-Id";
23
+ const DEBUG_S2C_EVENT = "rango:perf-s2c";
24
+ const DEBUG_C2S_EVENT = "rango:perf-c2s";
25
+
26
+ type DebugPayload =
27
+ | { i: string; b: string } // chunk (base64)
28
+ | { i: string; d: true }; // done
29
+
30
+ interface DebugSession {
31
+ cmdController?: ReadableStreamDefaultController<Uint8Array>;
32
+ pendingChunks?: Uint8Array[];
33
+ ended: boolean;
34
+ }
35
+
36
+ type DebugChannelRegistry = {
37
+ channels: Map<
38
+ string,
39
+ {
40
+ readable: ReadableStream<Uint8Array>;
41
+ writable: WritableStream<Uint8Array>;
42
+ }
43
+ >;
44
+ sessions: Map<string, DebugSession>;
45
+ };
46
+
47
+ const GLOBAL_KEY = "__RANGO_DEBUG_CHANNELS__";
48
+
49
+ // Use Node.js `Module` built-in as carrier — Vite's RSC module runner
50
+ // uses a separate VM context where both `globalThis` and `process` are
51
+ // different objects, but built-in module singletons ARE shared.
52
+ import { Module } from "node:module";
53
+ function getRegistry(): DebugChannelRegistry {
54
+ return ((Module as any)[GLOBAL_KEY] ??= {
55
+ channels: new Map(),
56
+ sessions: new Map(),
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Create a debug channel for a given request.
62
+ * Called by the RSC handler for each request that has a debugId.
63
+ * Returns the { readable, writable } pair for renderToReadableStream.
64
+ *
65
+ * This works across module graphs because the channel is pre-created
66
+ * by the Vite plugin and stored on globalThis.
67
+ */
68
+ export function createServerDebugChannel(debugId: string): {
69
+ readable: ReadableStream<Uint8Array>;
70
+ writable: WritableStream<Uint8Array>;
71
+ } | null {
72
+ const registry = getRegistry();
73
+ const channel = registry.channels.get(debugId);
74
+ if (channel) {
75
+ registry.channels.delete(debugId);
76
+ console.log("[perf-tracks] debug channel attached for", debugId);
77
+ return channel;
78
+ }
79
+ console.log(
80
+ "[perf-tracks] no channel found for",
81
+ debugId,
82
+ "channels:",
83
+ registry.channels.size,
84
+ );
85
+ return null;
86
+ }
87
+
88
+ const bytesToBase64 = (bytes: Uint8Array) =>
89
+ Buffer.from(bytes).toString("base64");
90
+
91
+ const base64ToBytes = (base64: string) =>
92
+ new Uint8Array(Buffer.from(base64, "base64"));
93
+
94
+ export function performanceTracksPlugin(): Plugin {
95
+ return {
96
+ name: "@rangojs/router:performance-tracks",
97
+ apply: "serve",
98
+ enforce: "pre",
99
+
100
+ configureServer(server) {
101
+ const hot = server.environments.client.hot;
102
+ const registry = getRegistry();
103
+ const sessions = registry.sessions;
104
+
105
+ const sendChunk = (debugId: string, chunk: Uint8Array) => {
106
+ hot.send(DEBUG_S2C_EVENT, {
107
+ i: debugId,
108
+ b: bytesToBase64(chunk),
109
+ } satisfies DebugPayload);
110
+ };
111
+
112
+ const cleanupIfEnded = (debugId: string, session: DebugSession) => {
113
+ if (session.pendingChunks || !session.ended) return;
114
+ sessions.delete(debugId);
115
+ hot.send(DEBUG_S2C_EVENT, {
116
+ i: debugId,
117
+ d: true,
118
+ } satisfies DebugPayload);
119
+ };
120
+
121
+ const registerDebugChannel = (debugId: string) => {
122
+ let session = sessions.get(debugId);
123
+ if (!session) {
124
+ session = { pendingChunks: [], ended: false };
125
+ sessions.set(debugId, session);
126
+ }
127
+
128
+ // Readable: receives client-to-server commands via WS
129
+ const readable = new ReadableStream<Uint8Array>({
130
+ start(controller) {
131
+ session!.cmdController = controller;
132
+ },
133
+ cancel() {
134
+ delete session!.cmdController;
135
+ },
136
+ });
137
+
138
+ // Writable: React writes debug data here, we forward to client via WS
139
+ const writable = new WritableStream<Uint8Array>({
140
+ write(chunk) {
141
+ if (session!.pendingChunks) {
142
+ session!.pendingChunks.push(chunk);
143
+ } else {
144
+ sendChunk(debugId, chunk);
145
+ }
146
+ },
147
+ close() {
148
+ session!.ended = true;
149
+ cleanupIfEnded(debugId, session!);
150
+ },
151
+ abort() {
152
+ session!.ended = true;
153
+ cleanupIfEnded(debugId, session!);
154
+ },
155
+ });
156
+
157
+ // Store on globalThis so the RSC handler can retrieve it
158
+ registry.channels.set(debugId, { readable, writable });
159
+ };
160
+
161
+ // Listen for client-to-server debug messages
162
+ // Payload shapes: { i, d: true } (done), { i, b } (chunk), { i } (ready)
163
+ hot.on(DEBUG_C2S_EVENT, (raw: unknown) => {
164
+ const payload = raw as { i: string; b?: string; d?: true };
165
+ const session = sessions.get(payload.i);
166
+
167
+ if (payload.d) {
168
+ if (session?.cmdController) {
169
+ try {
170
+ session.cmdController.close();
171
+ } catch {
172
+ // ignore
173
+ }
174
+ delete session.cmdController;
175
+ }
176
+ return;
177
+ }
178
+
179
+ if (payload.b) {
180
+ if (session?.cmdController) {
181
+ try {
182
+ session.cmdController.enqueue(base64ToBytes(payload.b));
183
+ } catch {
184
+ delete session!.cmdController;
185
+ }
186
+ }
187
+ return;
188
+ }
189
+
190
+ // Ready signal — flush pending chunks
191
+ if (session) {
192
+ if (session.pendingChunks) {
193
+ for (const chunk of session.pendingChunks) {
194
+ sendChunk(payload.i, chunk);
195
+ }
196
+ delete session.pendingChunks;
197
+ }
198
+ cleanupIfEnded(payload.i, session);
199
+ } else {
200
+ sessions.set(payload.i, { ended: false });
201
+ }
202
+ });
203
+
204
+ // Register middleware directly (not as post-hook) so it runs
205
+ // BEFORE the RSC handler — the channel must exist before rendering.
206
+ server.middlewares.use((req: any, _res: any, next: any) => {
207
+ const existingId = req.headers[DEBUG_ID_HEADER.toLowerCase()] as string;
208
+ const isHtml = req.headers.accept?.includes("text/html");
209
+
210
+ if (!existingId && !isHtml) {
211
+ next();
212
+ return;
213
+ }
214
+
215
+ const debugId = existingId || crypto.randomUUID();
216
+ if (!existingId) {
217
+ const lowerName = DEBUG_ID_HEADER.toLowerCase();
218
+ req.headers[lowerName] = debugId;
219
+ if (req.rawHeaders) {
220
+ req.rawHeaders.push(DEBUG_ID_HEADER, debugId);
221
+ }
222
+ }
223
+
224
+ registerDebugChannel(debugId);
225
+ console.log(
226
+ "[perf-tracks] middleware: created channel for",
227
+ debugId,
228
+ "from",
229
+ existingId ? "client header" : "SSR inject",
230
+ );
231
+ next();
232
+ });
233
+ },
234
+ };
235
+ }
@@ -135,8 +135,11 @@ export function createVersionPlugin(): Plugin {
135
135
  let server: any = null;
136
136
  const clientModuleSignatures = new Map<string, ClientModuleSignature>();
137
137
 
138
+ let versionCounter = 0;
138
139
  const bumpVersion = (reason: string) => {
139
- currentVersion = Date.now().toString(16);
140
+ // Use timestamp + counter to guarantee uniqueness even when multiple
141
+ // bumps happen within the same millisecond (e.g. cascading HMR events).
142
+ currentVersion = Date.now().toString(16) + String(++versionCounter);
140
143
  console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
141
144
 
142
145
  const rscEnv = server?.environments?.rsc;
@@ -211,6 +214,15 @@ export function createVersionPlugin(): Plugin {
211
214
 
212
215
  if (!isRscModule) return;
213
216
 
217
+ // Skip re-bumping when the version virtual module itself is invalidated
218
+ // (our own bumpVersion() invalidates it, which re-triggers hotUpdate).
219
+ if (
220
+ ctx.modules.length === 1 &&
221
+ ctx.modules[0].id === "\0" + VIRTUAL_IDS.version
222
+ ) {
223
+ return;
224
+ }
225
+
214
226
  if (isCodeModule(ctx.file)) {
215
227
  const filePath = normalizeModuleId(ctx.file);
216
228
  const previousSignature = clientModuleSignatures.get(filePath);
package/src/vite/rango.ts CHANGED
@@ -13,10 +13,7 @@ import {
13
13
  getExcludeDeps,
14
14
  getPackageAliases,
15
15
  } from "./utils/package-resolution.js";
16
- import {
17
- createScanFilter,
18
- findRouterFiles,
19
- } from "../build/generate-route-types.js";
16
+ import { findRouterFiles } from "../build/generate-route-types.js";
20
17
  import { createVersionPlugin } from "./plugins/version-plugin.js";
21
18
  import {
22
19
  sharedEsbuildOptions,
@@ -24,15 +21,12 @@ import {
24
21
  onwarn,
25
22
  getManualChunks,
26
23
  } from "./utils/shared-utils.js";
27
- import type {
28
- RangoOptions,
29
- RangoNodeOptions,
30
- RscPluginOptions,
31
- } from "./plugin-types.js";
24
+ import type { RangoOptions } from "./plugin-types.js";
32
25
  import { printBanner, rangoVersion } from "./utils/banner.js";
33
26
  import { createVersionInjectorPlugin } from "./plugins/version-injector.js";
34
27
  import { createCjsToEsmPlugin } from "./plugins/cjs-to-esm.js";
35
28
  import { createRouterDiscoveryPlugin } from "./router-discovery.js";
29
+ import { performanceTracksPlugin } from "./plugins/performance-tracks.js";
36
30
 
37
31
  /**
38
32
  * Vite plugin for @rangojs/router.
@@ -43,7 +37,7 @@ import { createRouterDiscoveryPlugin } from "./router-discovery.js";
43
37
  * @example Node.js (default)
44
38
  * ```ts
45
39
  * export default defineConfig({
46
- * plugins: [react(), rango({ router: './src/router.tsx' })],
40
+ * plugins: [react(), rango()],
47
41
  * });
48
42
  * ```
49
43
  *
@@ -69,9 +63,6 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
69
63
  const rangoAliases = getPackageAliases();
70
64
  const excludeDeps = getExcludeDeps();
71
65
 
72
- // Track RSC entry path for version injection
73
- let rscEntryPath: string | null = null;
74
-
75
66
  // Mutable ref for router path (node preset only).
76
67
  // Set immediately when user-specified, or populated by the auto-discover
77
68
  // config() hook using Vite's resolved root.
@@ -207,198 +198,148 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
207
198
  // packages that are also imported directly by client components.
208
199
  plugins.push(clientRefDedup());
209
200
  } else {
210
- // Node preset: full RSC plugin integration
211
- const nodeOptions = resolvedOptions as RangoNodeOptions;
201
+ // Auto-discover router using Vite's resolved root (not process.cwd())
202
+ plugins.push({
203
+ name: "@rangojs/router:auto-discover",
204
+ config(userConfig) {
205
+ if (routerRef.path) return;
206
+ const root = userConfig.root
207
+ ? resolve(process.cwd(), userConfig.root)
208
+ : process.cwd();
209
+ const candidates = findRouterFiles(root);
210
+ if (candidates.length === 1) {
211
+ const abs = candidates[0];
212
+ routerRef.path = (
213
+ abs.startsWith(root) ? "./" + abs.slice(root.length + 1) : abs
214
+ ).replaceAll("\\", "/");
215
+ } else if (candidates.length > 1) {
216
+ const list = candidates
217
+ .map(
218
+ (f) =>
219
+ " - " + (f.startsWith(root) ? f.slice(root.length + 1) : f),
220
+ )
221
+ .join("\n");
222
+ throw new Error(`[rsc-router] Multiple routers found:\n${list}`);
223
+ }
224
+ // 0 found: routerRef.path stays undefined, warn at startup via discovery plugin
225
+ },
226
+ });
212
227
 
213
- routerRef.path = nodeOptions.router;
228
+ // Always use virtual entries for client, ssr, and rsc
229
+ const finalEntries = {
230
+ client: VIRTUAL_IDS.browser,
231
+ ssr: VIRTUAL_IDS.ssr,
232
+ rsc: VIRTUAL_IDS.rsc,
233
+ };
214
234
 
215
- // Auto-discover router using Vite's resolved root (not process.cwd())
216
- if (!routerRef.path) {
217
- plugins.push({
218
- name: "@rangojs/router:auto-discover",
219
- config(userConfig) {
220
- if (routerRef.path) return;
221
- const root = userConfig.root
222
- ? resolve(process.cwd(), userConfig.root)
223
- : process.cwd();
224
- const filter = createScanFilter(root, {
225
- include: resolvedOptions.include,
226
- exclude: resolvedOptions.exclude,
227
- });
228
- const candidates = findRouterFiles(root, filter);
229
- if (candidates.length === 1) {
230
- const abs = candidates[0];
231
- routerRef.path = (
232
- abs.startsWith(root) ? "./" + abs.slice(root.length + 1) : abs
233
- ).replaceAll("\\", "/");
234
- } else if (candidates.length > 1) {
235
- const list = candidates
236
- .map(
237
- (f) =>
238
- " - " + (f.startsWith(root) ? f.slice(root.length + 1) : f),
239
- )
240
- .join("\n");
241
- throw new Error(
242
- `[rsc-router] Multiple routers found. Specify \`router\` to choose one:\n${list}`,
243
- );
244
- }
245
- // 0 found: routerRef.path stays undefined, warn at startup via discovery plugin
246
- },
247
- });
248
- }
249
-
250
- const rscOption = nodeOptions.rsc ?? true;
251
-
252
- // Add RSC plugin by default (can be disabled with rsc: false)
253
- if (rscOption !== false) {
254
- // Dynamically import @vitejs/plugin-rsc
255
- const { default: rsc } = await import("@vitejs/plugin-rsc");
256
-
257
- // Resolve entry paths: use explicit config or virtual modules
258
- const userEntries =
259
- typeof rscOption === "boolean" ? {} : rscOption.entries || {};
260
- const finalEntries = {
261
- client: userEntries.client ?? VIRTUAL_IDS.browser,
262
- ssr: userEntries.ssr ?? VIRTUAL_IDS.ssr,
263
- rsc: userEntries.rsc ?? VIRTUAL_IDS.rsc,
264
- };
265
-
266
- // Track RSC entry for version injection (only if custom entry provided)
267
- rscEntryPath = userEntries.rsc ?? null;
268
-
269
- // Create wrapper plugin that checks for duplicates
270
- let hasWarnedDuplicate = false;
271
-
272
- plugins.push({
273
- name: "@rangojs/router:rsc-integration",
274
- enforce: "pre",
275
-
276
- config() {
277
- // Configure environments for RSC
278
- // When using virtual entries, we need to explicitly configure optimizeDeps
279
- // so Vite pre-bundles React before processing the virtual modules.
280
- // Without this, the dep optimizer may run multiple times with different hashes,
281
- // causing React instance mismatches.
282
- const useVirtualClient = finalEntries.client === VIRTUAL_IDS.browser;
283
- const useVirtualSSR = finalEntries.ssr === VIRTUAL_IDS.ssr;
284
- const useVirtualRSC = finalEntries.rsc === VIRTUAL_IDS.rsc;
285
-
286
- return {
287
- // Exclude rsc-router modules from optimization to prevent module duplication
288
- // This ensures the same Context instance is used by both browser entry and RSC proxy modules
289
- optimizeDeps: {
290
- exclude: excludeDeps,
291
- esbuildOptions: sharedEsbuildOptions,
292
- },
293
- build: {
294
- rollupOptions: { onwarn },
295
- },
296
- resolve: {
297
- alias: rangoAliases,
298
- },
299
- environments: {
300
- client: {
301
- build: {
302
- rollupOptions: {
303
- output: {
304
- manualChunks: getManualChunks,
305
- },
235
+ // Dynamically import @vitejs/plugin-rsc
236
+ const { default: rsc } = await import("@vitejs/plugin-rsc");
237
+
238
+ let hasWarnedDuplicate = false;
239
+
240
+ plugins.push({
241
+ name: "@rangojs/router:rsc-integration",
242
+ enforce: "pre",
243
+
244
+ config() {
245
+ return {
246
+ optimizeDeps: {
247
+ exclude: excludeDeps,
248
+ esbuildOptions: sharedEsbuildOptions,
249
+ },
250
+ build: {
251
+ rollupOptions: { onwarn },
252
+ },
253
+ resolve: {
254
+ alias: rangoAliases,
255
+ },
256
+ environments: {
257
+ client: {
258
+ build: {
259
+ rollupOptions: {
260
+ output: {
261
+ manualChunks: getManualChunks,
306
262
  },
307
263
  },
308
- // Always exclude rsc-router modules, conditionally add virtual entry
309
- optimizeDeps: {
310
- // Pre-bundle React and rsc-html-stream to prevent late discovery
311
- // triggering ERR_OUTDATED_OPTIMIZED_DEP on cold starts
312
- include: [
313
- "react",
314
- "react-dom",
315
- "react/jsx-runtime",
316
- "react/jsx-dev-runtime",
317
- "rsc-html-stream/client",
318
- ],
319
- exclude: excludeDeps,
320
- esbuildOptions: sharedEsbuildOptions,
321
- ...(useVirtualClient && {
322
- // Tell Vite to scan the virtual entry for dependencies
323
- entries: [VIRTUAL_IDS.browser],
324
- }),
325
- },
326
264
  },
327
- ...(useVirtualSSR && {
328
- ssr: {
329
- optimizeDeps: {
330
- entries: [VIRTUAL_IDS.ssr],
331
- // Pre-bundle all SSR deps to prevent late discovery triggering ERR_OUTDATED_OPTIMIZED_DEP
332
- include: [
333
- "react",
334
- "react-dom",
335
- "react-dom/server.edge",
336
- "react-dom/static.edge",
337
- "react/jsx-runtime",
338
- "react/jsx-dev-runtime",
339
- "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
340
- ],
341
- exclude: excludeDeps,
342
- esbuildOptions: sharedEsbuildOptions,
343
- },
344
- },
345
- }),
346
- ...(useVirtualRSC && {
347
- rsc: {
348
- optimizeDeps: {
349
- entries: [VIRTUAL_IDS.rsc],
350
- // Pre-bundle all RSC deps to prevent late discovery triggering ERR_OUTDATED_OPTIMIZED_DEP
351
- include: [
352
- "react",
353
- "react/jsx-runtime",
354
- "react/jsx-dev-runtime",
355
- "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
356
- ],
357
- esbuildOptions: sharedEsbuildOptions,
358
- },
359
- },
360
- }),
265
+ optimizeDeps: {
266
+ include: [
267
+ "react",
268
+ "react-dom",
269
+ "react/jsx-runtime",
270
+ "react/jsx-dev-runtime",
271
+ "rsc-html-stream/client",
272
+ ],
273
+ exclude: excludeDeps,
274
+ esbuildOptions: sharedEsbuildOptions,
275
+ entries: [VIRTUAL_IDS.browser],
276
+ },
361
277
  },
362
- };
363
- },
364
-
365
- configResolved(config) {
366
- if (showBanner) {
367
- const mode =
368
- config.command === "serve"
369
- ? process.argv.includes("preview")
370
- ? "preview"
371
- : "dev"
372
- : "build";
373
- printBanner(mode, "node", rangoVersion);
374
- }
375
-
376
- // Count how many RSC base plugins there are (rsc:minimal is the main one)
377
- const rscMinimalCount = config.plugins.filter(
378
- (p) => p.name === "rsc:minimal",
379
- ).length;
380
-
381
- if (rscMinimalCount > 1 && !hasWarnedDuplicate) {
382
- hasWarnedDuplicate = true;
383
- console.warn(
384
- "[rsc-router] Duplicate @vitejs/plugin-rsc detected. " +
385
- "Remove rsc() from your config or use rango({ rsc: false }) for manual configuration.",
386
- );
387
- }
388
- },
389
- });
390
-
391
- // Add virtual entries plugin (RSC entry generated lazily from routerRef)
392
- plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
393
-
394
- // Add the RSC plugin directly
395
- // Cast to PluginOption to handle type differences between bundled vite types
396
- plugins.push(
397
- rsc({
398
- entries: finalEntries,
399
- }) as PluginOption,
400
- );
401
- }
278
+ ssr: {
279
+ optimizeDeps: {
280
+ entries: [VIRTUAL_IDS.ssr],
281
+ include: [
282
+ "react",
283
+ "react-dom",
284
+ "react-dom/server.edge",
285
+ "react-dom/static.edge",
286
+ "react/jsx-runtime",
287
+ "react/jsx-dev-runtime",
288
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
289
+ ],
290
+ exclude: excludeDeps,
291
+ esbuildOptions: sharedEsbuildOptions,
292
+ },
293
+ },
294
+ rsc: {
295
+ optimizeDeps: {
296
+ entries: [VIRTUAL_IDS.rsc],
297
+ include: [
298
+ "react",
299
+ "react/jsx-runtime",
300
+ "react/jsx-dev-runtime",
301
+ "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
302
+ ],
303
+ esbuildOptions: sharedEsbuildOptions,
304
+ },
305
+ },
306
+ },
307
+ };
308
+ },
309
+
310
+ configResolved(config) {
311
+ if (showBanner) {
312
+ const mode =
313
+ config.command === "serve"
314
+ ? process.argv.includes("preview")
315
+ ? "preview"
316
+ : "dev"
317
+ : "build";
318
+ printBanner(mode, "node", rangoVersion);
319
+ }
320
+
321
+ const rscMinimalCount = config.plugins.filter(
322
+ (p) => p.name === "rsc:minimal",
323
+ ).length;
324
+
325
+ if (rscMinimalCount > 1 && !hasWarnedDuplicate) {
326
+ hasWarnedDuplicate = true;
327
+ console.warn(
328
+ "[rsc-router] Duplicate @vitejs/plugin-rsc detected. " +
329
+ "Remove rsc() from your vite config — rango() includes it automatically.",
330
+ );
331
+ }
332
+ },
333
+ });
334
+
335
+ // Add virtual entries plugin (RSC entry generated lazily from routerRef)
336
+ plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
337
+
338
+ plugins.push(
339
+ rsc({
340
+ entries: finalEntries,
341
+ }) as PluginOption,
342
+ );
402
343
 
403
344
  // Deduplicate client references from third-party packages in dev mode.
404
345
  // Prevents module duplication when server components import "use client"
@@ -479,14 +420,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
479
420
  // Ref for deferred auto-discovery (node preset only, undefined for cloudflare)
480
421
  const discoveryRouterRef = preset !== "cloudflare" ? routerRef : undefined;
481
422
 
482
- // Version injector: auto-injects VERSION and routes-manifest into custom entry.rsc files.
483
- // Only applies when there's an explicit rscEntryPath or for cloudflare preset (resolved
484
- // lazily in configResolved). For node preset without a custom entry, the router file
485
- // must NOT be transformed — injecting routes-manifest there creates a circular dependency.
486
- const injectorEntryPath =
487
- rscEntryPath ?? (preset === "cloudflare" ? undefined : null);
488
- if (injectorEntryPath !== null) {
489
- plugins.push(createVersionInjectorPlugin(injectorEntryPath));
423
+ // Version injector: auto-injects VERSION and routes-manifest into the RSC entry.
424
+ // For cloudflare preset, the entry is resolved lazily in configResolved.
425
+ // For node preset, the virtual entry already includes these imports.
426
+ if (preset === "cloudflare") {
427
+ plugins.push(createVersionInjectorPlugin(undefined));
490
428
  }
491
429
 
492
430
  // Transform CJS vendor files to ESM for browser compatibility
@@ -501,10 +439,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
501
439
  routerPathRef: discoveryRouterRef,
502
440
  enableBuildPrerender: prerenderEnabled,
503
441
  staticRouteTypesGeneration: resolvedOptions.staticRouteTypesGeneration,
504
- include: resolvedOptions.include,
505
- exclude: resolvedOptions.exclude,
506
442
  }),
507
443
  );
508
444
 
445
+ // Dev-only: React Performance Tracks (debugChannel transport via HMR WS)
446
+ plugins.push(performanceTracksPlugin());
447
+
509
448
  return plugins;
510
449
  }