@rangojs/router 0.0.0-experimental.ae6e7825 → 0.0.0-experimental.b02a2fec
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/README.md +76 -18
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +688 -433
- package/dist/vite/index.js.bak +5448 -0
- package/package.json +1 -1
- package/skills/links/SKILL.md +3 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/router-setup/SKILL.md +35 -0
- package/src/__internal.ts +1 -1
- package/src/browser/app-version.ts +14 -0
- package/src/browser/navigation-bridge.ts +19 -4
- package/src/browser/navigation-client.ts +64 -63
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +27 -5
- package/src/browser/prefetch/fetch.ts +8 -2
- package/src/browser/react/Link.tsx +44 -8
- package/src/browser/react/NavigationProvider.tsx +8 -1
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +28 -22
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/server-action-bridge.ts +8 -18
- package/src/browser/types.ts +21 -15
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/client.tsx +2 -56
- package/src/deps/browser.ts +0 -1
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +12 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +42 -19
- package/src/route-definition/helpers-types.ts +4 -1
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +48 -15
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/loader-resolution.ts +150 -21
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/cache-lookup.ts +28 -8
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +82 -4
- package/src/router/middleware-types.ts +0 -6
- package/src/router/middleware.ts +0 -3
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +70 -5
- package/src/router/segment-resolution/revalidation.ts +87 -9
- package/src/router.ts +53 -5
- package/src/rsc/handler.ts +472 -398
- package/src/rsc/loader-fetch.ts +18 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +12 -3
- package/src/rsc/rsc-rendering.ts +8 -3
- package/src/rsc/server-action.ts +8 -2
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +6 -7
- package/src/server/context.ts +39 -2
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +131 -16
- package/src/ssr/index.tsx +4 -13
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +17 -11
- package/src/types/loader-types.ts +32 -5
- package/src/types/route-entry.ts +1 -1
- package/src/types/segments.ts +1 -0
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +128 -74
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +57 -280
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/rango.ts +17 -11
- package/src/vite/router-discovery.ts +178 -37
- package/src/vite/utils/prerender-utils.ts +18 -0
- package/src/vite/utils/shared-utils.ts +3 -2
- package/src/browser/debug-channel.ts +0 -112
|
@@ -1,104 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* React Performance Tracks —
|
|
2
|
+
* React Performance Tracks — RSDW client patch
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
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.
|
|
18
13
|
*/
|
|
19
14
|
|
|
20
15
|
import type { Plugin } from "vite";
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
21
17
|
|
|
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
|
-
// Patch for RSDW client: React's flushComponentPerformance uses splice(0) to
|
|
95
|
-
// empty chunk._debugInfo after resolution, then tries to recover it from the
|
|
96
|
-
// resolved value. The fallback only works for arrays, async iterables, React
|
|
97
|
-
// elements, and lazy types — not plain objects. Since our RscPayload is a
|
|
98
|
-
// plain object, _debugInfo is lost and the Server Components track stays empty.
|
|
99
|
-
// This patch relaxes the check so _debugInfo is recovered from any object.
|
|
100
|
-
//
|
|
101
|
-
// Uses regex to be resilient to Vite's dep optimizer reformatting.
|
|
102
18
|
const RSDW_PATCH_RE =
|
|
103
19
|
/((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
|
|
104
20
|
|
|
@@ -112,200 +28,61 @@ if (${debugInfoVar} && 0 === ${debugInfoVar}.length && "fulfilled" === root.stat
|
|
|
112
28
|
}`;
|
|
113
29
|
}
|
|
114
30
|
|
|
115
|
-
export function
|
|
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
|
+
|
|
116
40
|
return {
|
|
117
|
-
|
|
118
|
-
|
|
41
|
+
code: code.replace(match[1]!, buildPatchReplacement(match[1]!, match[2]!)),
|
|
42
|
+
debugInfoVar: match[2]!,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
119
45
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
},
|
|
133
66
|
);
|
|
134
|
-
return patched;
|
|
135
67
|
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
136
70
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const registry = getRegistry();
|
|
141
|
-
const sessions = registry.sessions;
|
|
142
|
-
|
|
143
|
-
const sendChunk = (debugId: string, chunk: Uint8Array) => {
|
|
144
|
-
hot.send(DEBUG_S2C_EVENT, {
|
|
145
|
-
i: debugId,
|
|
146
|
-
b: bytesToBase64(chunk),
|
|
147
|
-
} satisfies DebugPayload);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const cleanupIfEnded = (debugId: string, session: DebugSession) => {
|
|
151
|
-
if (session.pendingChunks || !session.ended) return;
|
|
152
|
-
sessions.delete(debugId);
|
|
153
|
-
hot.send(DEBUG_S2C_EVENT, {
|
|
154
|
-
i: debugId,
|
|
155
|
-
d: true,
|
|
156
|
-
} satisfies DebugPayload);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const registerDebugChannel = (debugId: string) => {
|
|
160
|
-
let session = sessions.get(debugId);
|
|
161
|
-
if (!session) {
|
|
162
|
-
session = { pendingChunks: [], ended: false };
|
|
163
|
-
sessions.set(debugId, session);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Readable: receives client-to-server commands via WS
|
|
167
|
-
const readable = new ReadableStream<Uint8Array>({
|
|
168
|
-
start(controller) {
|
|
169
|
-
session!.cmdController = controller;
|
|
170
|
-
},
|
|
171
|
-
cancel() {
|
|
172
|
-
delete session!.cmdController;
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
let chunkCount = 0;
|
|
177
|
-
// Writable: React writes debug data here, we forward to client via WS
|
|
178
|
-
const writable = new WritableStream<Uint8Array>({
|
|
179
|
-
write(chunk) {
|
|
180
|
-
chunkCount++;
|
|
181
|
-
if (chunkCount <= 3) {
|
|
182
|
-
console.log(
|
|
183
|
-
"[perf-tracks] writable: chunk #" + chunkCount,
|
|
184
|
-
"size:",
|
|
185
|
-
chunk.byteLength,
|
|
186
|
-
"for",
|
|
187
|
-
debugId.slice(0, 8),
|
|
188
|
-
session!.pendingChunks ? "(buffered)" : "(sent)",
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
if (session!.pendingChunks) {
|
|
192
|
-
session!.pendingChunks.push(chunk);
|
|
193
|
-
} else {
|
|
194
|
-
sendChunk(debugId, chunk);
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
close() {
|
|
198
|
-
console.log(
|
|
199
|
-
"[perf-tracks] writable: closed after",
|
|
200
|
-
chunkCount,
|
|
201
|
-
"chunks for",
|
|
202
|
-
debugId.slice(0, 8),
|
|
203
|
-
);
|
|
204
|
-
session!.ended = true;
|
|
205
|
-
cleanupIfEnded(debugId, session!);
|
|
206
|
-
},
|
|
207
|
-
abort() {
|
|
208
|
-
session!.ended = true;
|
|
209
|
-
cleanupIfEnded(debugId, session!);
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Store on globalThis so the RSC handler can retrieve it
|
|
214
|
-
registry.channels.set(debugId, { readable, writable });
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
// Listen for client-to-server debug messages
|
|
218
|
-
// Payload shapes: { i, d: true } (done), { i, b } (chunk), { i } (ready)
|
|
219
|
-
hot.on(DEBUG_C2S_EVENT, (raw: unknown) => {
|
|
220
|
-
const payload = raw as { i: string; b?: string; d?: true };
|
|
221
|
-
const session = sessions.get(payload.i);
|
|
222
|
-
|
|
223
|
-
if (payload.d) {
|
|
224
|
-
if (session?.cmdController) {
|
|
225
|
-
try {
|
|
226
|
-
session.cmdController.close();
|
|
227
|
-
} catch {
|
|
228
|
-
// ignore
|
|
229
|
-
}
|
|
230
|
-
delete session.cmdController;
|
|
231
|
-
}
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (payload.b) {
|
|
236
|
-
if (session?.cmdController) {
|
|
237
|
-
try {
|
|
238
|
-
session.cmdController.enqueue(base64ToBytes(payload.b));
|
|
239
|
-
} catch {
|
|
240
|
-
delete session!.cmdController;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Ready signal — flush pending chunks
|
|
247
|
-
if (session) {
|
|
248
|
-
const pending = session.pendingChunks?.length ?? 0;
|
|
249
|
-
const ended = session.ended;
|
|
250
|
-
console.log(
|
|
251
|
-
"[perf-tracks] ready signal for",
|
|
252
|
-
payload.i.slice(0, 8),
|
|
253
|
-
"pending:",
|
|
254
|
-
pending,
|
|
255
|
-
"ended:",
|
|
256
|
-
ended,
|
|
257
|
-
);
|
|
258
|
-
if (session.pendingChunks) {
|
|
259
|
-
for (const chunk of session.pendingChunks) {
|
|
260
|
-
sendChunk(payload.i, chunk);
|
|
261
|
-
}
|
|
262
|
-
delete session.pendingChunks;
|
|
263
|
-
}
|
|
264
|
-
cleanupIfEnded(payload.i, session);
|
|
265
|
-
} else {
|
|
266
|
-
console.log(
|
|
267
|
-
"[perf-tracks] ready signal for",
|
|
268
|
-
payload.i.slice(0, 8),
|
|
269
|
-
"(no session yet)",
|
|
270
|
-
);
|
|
271
|
-
sessions.set(payload.i, { ended: false });
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Create debug channels only for RSC-handled requests:
|
|
276
|
-
// - HTML page requests (SSR)
|
|
277
|
-
// - RSC partial requests (client navigation)
|
|
278
|
-
// - Requests with existing debug header (client-initiated)
|
|
279
|
-
server.middlewares.use((req: any, _res: any, next: any) => {
|
|
280
|
-
const url: string = req.url || "";
|
|
281
|
-
const existingId = req.headers[DEBUG_ID_HEADER.toLowerCase()] as string;
|
|
282
|
-
const isHtml = req.headers.accept?.includes("text/html");
|
|
283
|
-
const isRsc = url.includes("_rsc_partial");
|
|
284
|
-
|
|
285
|
-
if (!existingId && !isHtml && !isRsc) {
|
|
286
|
-
next();
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const debugId = existingId || crypto.randomUUID();
|
|
291
|
-
if (!existingId) {
|
|
292
|
-
const lowerName = DEBUG_ID_HEADER.toLowerCase();
|
|
293
|
-
req.headers[lowerName] = debugId;
|
|
294
|
-
if (req.rawHeaders) {
|
|
295
|
-
req.rawHeaders.push(DEBUG_ID_HEADER, debugId);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
71
|
+
export function performanceTracksPlugin(): Plugin {
|
|
72
|
+
return {
|
|
73
|
+
name: "@rangojs/router:performance-tracks",
|
|
298
74
|
|
|
299
|
-
|
|
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)
|
|
300
80
|
console.log(
|
|
301
|
-
"[perf-tracks]
|
|
302
|
-
|
|
303
|
-
"
|
|
304
|
-
url.slice(0, 80),
|
|
305
|
-
existingId ? "(client)" : isHtml ? "(SSR)" : "(RSC partial)",
|
|
81
|
+
"[perf-tracks] patched RSDW client (var:",
|
|
82
|
+
patched.debugInfoVar,
|
|
83
|
+
")",
|
|
306
84
|
);
|
|
307
|
-
|
|
308
|
-
});
|
|
85
|
+
return patched.code;
|
|
309
86
|
},
|
|
310
87
|
};
|
|
311
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
|
|
5
|
-
*
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
if (data
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
@@ -55,14 +55,22 @@ import { performanceTracksPlugin } from "./plugins/performance-tracks.js";
|
|
|
55
55
|
export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
56
56
|
const resolvedOptions: RangoOptions = options ?? { preset: "node" };
|
|
57
57
|
const preset = resolvedOptions.preset ?? "node";
|
|
58
|
-
console.log("[perf-tracks] rango() called, preset:", preset);
|
|
59
58
|
const showBanner = resolvedOptions.banner ?? true;
|
|
60
59
|
|
|
61
60
|
const plugins: PluginOption[] = [];
|
|
62
61
|
|
|
63
62
|
// Get package resolution info (workspace vs npm install)
|
|
64
63
|
const rangoAliases = getPackageAliases();
|
|
65
|
-
const excludeDeps =
|
|
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
|
+
];
|
|
66
74
|
|
|
67
75
|
// Mutable ref for router path (node preset only).
|
|
68
76
|
// Set immediately when user-specified, or populated by the auto-discover
|
|
@@ -184,6 +192,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
184
192
|
|
|
185
193
|
plugins.push(createVirtualEntriesPlugin(finalEntries));
|
|
186
194
|
|
|
195
|
+
// Dev-only: RSDW client patch for React Performance Tracks
|
|
196
|
+
plugins.push(performanceTracksPlugin());
|
|
197
|
+
|
|
187
198
|
// Add RSC plugin with cloudflare-specific options
|
|
188
199
|
// Note: loadModuleDevProxy should NOT be used with childEnvironments
|
|
189
200
|
// since SSR runs in workerd alongside RSC
|
|
@@ -336,14 +347,8 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
336
347
|
// Add virtual entries plugin (RSC entry generated lazily from routerRef)
|
|
337
348
|
plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
|
|
338
349
|
|
|
339
|
-
// Dev-only:
|
|
340
|
-
|
|
341
|
-
const perfPlugin = performanceTracksPlugin();
|
|
342
|
-
console.log(
|
|
343
|
-
"[perf-tracks] rango: plugin created, has configureServer:",
|
|
344
|
-
!!perfPlugin.configureServer,
|
|
345
|
-
);
|
|
346
|
-
plugins.push(perfPlugin);
|
|
350
|
+
// Dev-only: RSDW client patch for React Performance Tracks
|
|
351
|
+
plugins.push(performanceTracksPlugin());
|
|
347
352
|
|
|
348
353
|
plugins.push(
|
|
349
354
|
rsc({
|
|
@@ -448,7 +453,8 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
448
453
|
createRouterDiscoveryPlugin(discoveryEntryPath, {
|
|
449
454
|
routerPathRef: discoveryRouterRef,
|
|
450
455
|
enableBuildPrerender: prerenderEnabled,
|
|
451
|
-
|
|
456
|
+
buildEnv: options?.buildEnv,
|
|
457
|
+
preset,
|
|
452
458
|
}),
|
|
453
459
|
);
|
|
454
460
|
|