@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.
- package/dist/bin/rango.js +8 -3
- package/dist/vite/index.js +292 -204
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/loader/SKILL.md +53 -43
- package/skills/parallel/SKILL.md +126 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +52 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/browser/debug-channel.ts +93 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -5
- package/src/browser/navigation-client.ts +84 -27
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +50 -9
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +30 -21
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +32 -3
- package/src/browser/rsc-router.tsx +109 -57
- package/src/browser/scroll-restoration.ts +31 -34
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +12 -0
- package/src/browser/types.ts +17 -1
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/deps/browser.ts +1 -0
- package/src/route-definition/dsl-helpers.ts +32 -7
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/route-definition/redirect.ts +2 -2
- package/src/route-map-builder.ts +7 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +31 -8
- package/src/router/intercept-resolution.ts +2 -0
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +7 -1
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +66 -9
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +8 -5
- package/src/router/match-result.ts +22 -6
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +6 -2
- package/src/router/middleware.ts +4 -3
- package/src/router/router-context.ts +6 -1
- package/src/router/segment-resolution/fresh.ts +130 -17
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +352 -290
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/types.ts +1 -0
- package/src/router.ts +6 -1
- package/src/rsc/handler.ts +28 -2
- package/src/rsc/loader-fetch.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -1
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/types.ts +7 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +102 -13
- package/src/server/request-context.ts +59 -12
- package/src/ssr/index.tsx +1 -0
- package/src/types/handler-context.ts +120 -22
- package/src/types/loader-types.ts +4 -4
- package/src/types/route-entry.ts +7 -0
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper.ts +1 -1
- package/src/vite/discovery/state.ts +0 -2
- package/src/vite/plugin-types.ts +0 -83
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/performance-tracks.ts +235 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +148 -209
- package/src/vite/router-discovery.ts +0 -8
- 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
|
-
|
|
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(
|
|
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
|
-
//
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
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
|
}
|