@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ae6e7825
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/vite/index.js +78 -10
- package/package.json +2 -2
- package/src/browser/debug-channel.ts +19 -0
- package/src/browser/navigation-client.ts +17 -18
- package/src/browser/rsc-router.tsx +19 -2
- package/src/browser/types.ts +10 -1
- package/src/rsc/handler.ts +11 -11
- package/src/rsc/rsc-rendering.ts +1 -0
- package/src/rsc/types.ts +3 -0
- package/src/server/request-context.ts +4 -0
- package/src/ssr/index.tsx +13 -1
- package/src/vite/plugins/performance-tracks.ts +89 -9
- package/src/vite/rango.ts +10 -3
package/dist/vite/index.js
CHANGED
|
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
|
|
|
1745
1745
|
// package.json
|
|
1746
1746
|
var package_default = {
|
|
1747
1747
|
name: "@rangojs/router",
|
|
1748
|
-
version: "0.0.0-experimental.
|
|
1748
|
+
version: "0.0.0-experimental.ae6e7825",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
|
@@ -1887,7 +1887,7 @@ var package_default = {
|
|
|
1887
1887
|
"test:unit:watch": "vitest"
|
|
1888
1888
|
},
|
|
1889
1889
|
dependencies: {
|
|
1890
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
1890
|
+
"@vitejs/plugin-rsc": "^0.5.19",
|
|
1891
1891
|
"magic-string": "^0.30.17",
|
|
1892
1892
|
picomatch: "^4.0.3",
|
|
1893
1893
|
"rsc-html-stream": "^0.0.7"
|
|
@@ -4862,24 +4862,50 @@ ${details}`
|
|
|
4862
4862
|
}
|
|
4863
4863
|
|
|
4864
4864
|
// src/vite/plugins/performance-tracks.ts
|
|
4865
|
+
import { Module } from "node:module";
|
|
4865
4866
|
var DEBUG_ID_HEADER = "X-RSC-Debug-Id";
|
|
4866
4867
|
var DEBUG_S2C_EVENT = "rango:perf-s2c";
|
|
4867
4868
|
var DEBUG_C2S_EVENT = "rango:perf-c2s";
|
|
4868
4869
|
var GLOBAL_KEY = "__RANGO_DEBUG_CHANNELS__";
|
|
4869
4870
|
function getRegistry() {
|
|
4870
|
-
return
|
|
4871
|
+
return Module[GLOBAL_KEY] ??= {
|
|
4871
4872
|
channels: /* @__PURE__ */ new Map(),
|
|
4872
4873
|
sessions: /* @__PURE__ */ new Map()
|
|
4873
4874
|
};
|
|
4874
4875
|
}
|
|
4875
4876
|
var bytesToBase64 = (bytes) => Buffer.from(bytes).toString("base64");
|
|
4876
4877
|
var base64ToBytes = (base64) => new Uint8Array(Buffer.from(base64, "base64"));
|
|
4878
|
+
var RSDW_PATCH_RE = /((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
|
|
4879
|
+
function buildPatchReplacement(match, debugInfoVar) {
|
|
4880
|
+
return `${match}
|
|
4881
|
+
if (${debugInfoVar} && 0 === ${debugInfoVar}.length && "fulfilled" === root.status) {
|
|
4882
|
+
var _resolved = "function" === typeof resolveLazy ? resolveLazy(root.value) : root.value;
|
|
4883
|
+
if ("object" === typeof _resolved && null !== _resolved && isArrayImpl(_resolved._debugInfo)) {
|
|
4884
|
+
${debugInfoVar} = _resolved._debugInfo;
|
|
4885
|
+
}
|
|
4886
|
+
}`;
|
|
4887
|
+
}
|
|
4877
4888
|
function performanceTracksPlugin() {
|
|
4878
4889
|
return {
|
|
4879
4890
|
name: "@rangojs/router:performance-tracks",
|
|
4880
|
-
|
|
4881
|
-
|
|
4891
|
+
// configureServer + transform — naturally dev-only
|
|
4892
|
+
transform(code, id) {
|
|
4893
|
+
if (!id.includes("react-server-dom") || !id.includes("client")) return;
|
|
4894
|
+
const match = code.match(RSDW_PATCH_RE);
|
|
4895
|
+
if (!match) return;
|
|
4896
|
+
const patched = code.replace(
|
|
4897
|
+
match[1],
|
|
4898
|
+
buildPatchReplacement(match[1], match[2])
|
|
4899
|
+
);
|
|
4900
|
+
console.log(
|
|
4901
|
+
"[perf-tracks] patched RSDW client for plain-object _debugInfo recovery (var:",
|
|
4902
|
+
match[2],
|
|
4903
|
+
")"
|
|
4904
|
+
);
|
|
4905
|
+
return patched;
|
|
4906
|
+
},
|
|
4882
4907
|
configureServer(server) {
|
|
4908
|
+
console.log("[perf-tracks] plugin loaded, configureServer called");
|
|
4883
4909
|
const hot = server.environments.client.hot;
|
|
4884
4910
|
const registry = getRegistry();
|
|
4885
4911
|
const sessions = registry.sessions;
|
|
@@ -4911,8 +4937,20 @@ function performanceTracksPlugin() {
|
|
|
4911
4937
|
delete session.cmdController;
|
|
4912
4938
|
}
|
|
4913
4939
|
});
|
|
4940
|
+
let chunkCount = 0;
|
|
4914
4941
|
const writable = new WritableStream({
|
|
4915
4942
|
write(chunk) {
|
|
4943
|
+
chunkCount++;
|
|
4944
|
+
if (chunkCount <= 3) {
|
|
4945
|
+
console.log(
|
|
4946
|
+
"[perf-tracks] writable: chunk #" + chunkCount,
|
|
4947
|
+
"size:",
|
|
4948
|
+
chunk.byteLength,
|
|
4949
|
+
"for",
|
|
4950
|
+
debugId.slice(0, 8),
|
|
4951
|
+
session.pendingChunks ? "(buffered)" : "(sent)"
|
|
4952
|
+
);
|
|
4953
|
+
}
|
|
4916
4954
|
if (session.pendingChunks) {
|
|
4917
4955
|
session.pendingChunks.push(chunk);
|
|
4918
4956
|
} else {
|
|
@@ -4920,6 +4958,12 @@ function performanceTracksPlugin() {
|
|
|
4920
4958
|
}
|
|
4921
4959
|
},
|
|
4922
4960
|
close() {
|
|
4961
|
+
console.log(
|
|
4962
|
+
"[perf-tracks] writable: closed after",
|
|
4963
|
+
chunkCount,
|
|
4964
|
+
"chunks for",
|
|
4965
|
+
debugId.slice(0, 8)
|
|
4966
|
+
);
|
|
4923
4967
|
session.ended = true;
|
|
4924
4968
|
cleanupIfEnded(debugId, session);
|
|
4925
4969
|
},
|
|
@@ -4954,6 +4998,16 @@ function performanceTracksPlugin() {
|
|
|
4954
4998
|
return;
|
|
4955
4999
|
}
|
|
4956
5000
|
if (session) {
|
|
5001
|
+
const pending = session.pendingChunks?.length ?? 0;
|
|
5002
|
+
const ended = session.ended;
|
|
5003
|
+
console.log(
|
|
5004
|
+
"[perf-tracks] ready signal for",
|
|
5005
|
+
payload.i.slice(0, 8),
|
|
5006
|
+
"pending:",
|
|
5007
|
+
pending,
|
|
5008
|
+
"ended:",
|
|
5009
|
+
ended
|
|
5010
|
+
);
|
|
4957
5011
|
if (session.pendingChunks) {
|
|
4958
5012
|
for (const chunk of session.pendingChunks) {
|
|
4959
5013
|
sendChunk(payload.i, chunk);
|
|
@@ -4962,13 +5016,20 @@ function performanceTracksPlugin() {
|
|
|
4962
5016
|
}
|
|
4963
5017
|
cleanupIfEnded(payload.i, session);
|
|
4964
5018
|
} else {
|
|
5019
|
+
console.log(
|
|
5020
|
+
"[perf-tracks] ready signal for",
|
|
5021
|
+
payload.i.slice(0, 8),
|
|
5022
|
+
"(no session yet)"
|
|
5023
|
+
);
|
|
4965
5024
|
sessions.set(payload.i, { ended: false });
|
|
4966
5025
|
}
|
|
4967
5026
|
});
|
|
4968
5027
|
server.middlewares.use((req, _res, next) => {
|
|
5028
|
+
const url = req.url || "";
|
|
4969
5029
|
const existingId = req.headers[DEBUG_ID_HEADER.toLowerCase()];
|
|
4970
5030
|
const isHtml = req.headers.accept?.includes("text/html");
|
|
4971
|
-
|
|
5031
|
+
const isRsc = url.includes("_rsc_partial");
|
|
5032
|
+
if (!existingId && !isHtml && !isRsc) {
|
|
4972
5033
|
next();
|
|
4973
5034
|
return;
|
|
4974
5035
|
}
|
|
@@ -4982,10 +5043,11 @@ function performanceTracksPlugin() {
|
|
|
4982
5043
|
}
|
|
4983
5044
|
registerDebugChannel(debugId);
|
|
4984
5045
|
console.log(
|
|
4985
|
-
"[perf-tracks] middleware:
|
|
5046
|
+
"[perf-tracks] middleware: channel for",
|
|
4986
5047
|
debugId,
|
|
4987
|
-
"
|
|
4988
|
-
|
|
5048
|
+
"url:",
|
|
5049
|
+
url.slice(0, 80),
|
|
5050
|
+
existingId ? "(client)" : isHtml ? "(SSR)" : "(RSC partial)"
|
|
4989
5051
|
);
|
|
4990
5052
|
next();
|
|
4991
5053
|
});
|
|
@@ -4997,6 +5059,7 @@ function performanceTracksPlugin() {
|
|
|
4997
5059
|
async function rango(options) {
|
|
4998
5060
|
const resolvedOptions = options ?? { preset: "node" };
|
|
4999
5061
|
const preset = resolvedOptions.preset ?? "node";
|
|
5062
|
+
console.log("[perf-tracks] rango() called, preset:", preset);
|
|
5000
5063
|
const showBanner = resolvedOptions.banner ?? true;
|
|
5001
5064
|
const plugins = [];
|
|
5002
5065
|
const rangoAliases = getPackageAliases();
|
|
@@ -5214,6 +5277,12 @@ ${list}`);
|
|
|
5214
5277
|
}
|
|
5215
5278
|
});
|
|
5216
5279
|
plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
|
|
5280
|
+
const perfPlugin = performanceTracksPlugin();
|
|
5281
|
+
console.log(
|
|
5282
|
+
"[perf-tracks] rango: plugin created, has configureServer:",
|
|
5283
|
+
!!perfPlugin.configureServer
|
|
5284
|
+
);
|
|
5285
|
+
plugins.push(perfPlugin);
|
|
5217
5286
|
plugins.push(
|
|
5218
5287
|
rsc({
|
|
5219
5288
|
entries: finalEntries
|
|
@@ -5257,7 +5326,6 @@ ${list}`);
|
|
|
5257
5326
|
staticRouteTypesGeneration: resolvedOptions.staticRouteTypesGeneration
|
|
5258
5327
|
})
|
|
5259
5328
|
);
|
|
5260
|
-
plugins.push(performanceTracksPlugin());
|
|
5261
5329
|
return plugins;
|
|
5262
5330
|
}
|
|
5263
5331
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.ae6e7825",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"test:unit:watch": "vitest"
|
|
143
143
|
},
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
145
|
+
"@vitejs/plugin-rsc": "^0.5.19",
|
|
146
146
|
"magic-string": "^0.30.17",
|
|
147
147
|
"picomatch": "^4.0.3",
|
|
148
148
|
"rsc-html-stream": "^0.0.7"
|
|
@@ -55,17 +55,36 @@ export function createClientDebugChannel(debugId: string): {
|
|
|
55
55
|
// Readable: receives server-to-client debug data via HMR WS
|
|
56
56
|
const readable = new ReadableStream<Uint8Array>({
|
|
57
57
|
start(controller) {
|
|
58
|
+
let chunks = 0;
|
|
58
59
|
onServerData = (payload: DebugPayload) => {
|
|
59
60
|
if (closed || payload.i !== debugId) return;
|
|
60
61
|
if ("b" in payload) {
|
|
62
|
+
chunks++;
|
|
63
|
+
if (chunks <= 3)
|
|
64
|
+
console.log(
|
|
65
|
+
"[perf-tracks] client readable: chunk #" + chunks,
|
|
66
|
+
"size:",
|
|
67
|
+
base64ToBytes(payload.b).byteLength,
|
|
68
|
+
);
|
|
61
69
|
controller.enqueue(base64ToBytes(payload.b));
|
|
62
70
|
}
|
|
63
71
|
if ("d" in payload) {
|
|
72
|
+
console.log(
|
|
73
|
+
"[perf-tracks] client readable: done after",
|
|
74
|
+
chunks,
|
|
75
|
+
"chunks",
|
|
76
|
+
);
|
|
64
77
|
cleanup();
|
|
65
78
|
controller.close();
|
|
66
79
|
}
|
|
67
80
|
};
|
|
68
81
|
hot.on(DEBUG_S2C_EVENT, onServerData);
|
|
82
|
+
// Send "ready" signal so the server flushes buffered debug data
|
|
83
|
+
console.log(
|
|
84
|
+
"[perf-tracks] client: sending ready signal for",
|
|
85
|
+
debugId.slice(0, 8),
|
|
86
|
+
);
|
|
87
|
+
hot.send(DEBUG_C2S_EVENT, { i: debugId });
|
|
69
88
|
},
|
|
70
89
|
cancel() {
|
|
71
90
|
cleanup(true);
|
|
@@ -109,24 +109,25 @@ export function createNavigationClient(
|
|
|
109
109
|
resolveStreamComplete = resolve;
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
// Dev-only:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (debugId) {
|
|
120
|
-
console.log(
|
|
121
|
-
"[perf-tracks] client: debugId =",
|
|
122
|
-
debugId,
|
|
123
|
-
"channel =",
|
|
124
|
-
debugChannel ? "created" : "null (no HMR)",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
112
|
+
// Dev-only: debug channel is created lazily only for fresh fetches.
|
|
113
|
+
// Cached/inflight responses don't have a server-side channel, so passing
|
|
114
|
+
// debugChannel to createFromFetch would hang waiting for data that never comes.
|
|
115
|
+
let debugChannel: {
|
|
116
|
+
readable: ReadableStream<Uint8Array>;
|
|
117
|
+
writable: WritableStream<Uint8Array>;
|
|
118
|
+
} | null = null;
|
|
127
119
|
|
|
128
120
|
/** Start a fresh navigation fetch (no cache / inflight hit). */
|
|
129
121
|
const doFreshFetch = (): Promise<Response> => {
|
|
122
|
+
// Dev-only: create debug channel for React Performance Tracks
|
|
123
|
+
const debugId = (import.meta as any).hot
|
|
124
|
+
? crypto.randomUUID()
|
|
125
|
+
: undefined;
|
|
126
|
+
if (debugId) {
|
|
127
|
+
debugChannel = createClientDebugChannel(debugId);
|
|
128
|
+
console.log("[perf-tracks] client: fresh fetch, debugId =", debugId);
|
|
129
|
+
}
|
|
130
|
+
|
|
130
131
|
if (tx) {
|
|
131
132
|
browserDebugLog(tx, "fetching", {
|
|
132
133
|
path: `${fetchUrl.pathname}${fetchUrl.search}`,
|
|
@@ -241,9 +242,7 @@ export function createNavigationClient(
|
|
|
241
242
|
// Deserialize RSC payload
|
|
242
243
|
const payload = await deps.createFromFetch<RscPayload>(
|
|
243
244
|
responsePromise,
|
|
244
|
-
{
|
|
245
|
-
...(debugChannel && { debugChannel, findSourceMapURL }),
|
|
246
|
-
},
|
|
245
|
+
debugChannel ? { debugChannel, findSourceMapURL } : undefined,
|
|
247
246
|
);
|
|
248
247
|
if (tx) {
|
|
249
248
|
browserDebugLog(tx, "response received", {
|
|
@@ -11,6 +11,8 @@ import { createEventController } from "./event-controller.js";
|
|
|
11
11
|
import { createNavigationClient } from "./navigation-client.js";
|
|
12
12
|
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
13
|
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
14
|
+
import { createClientDebugChannel } from "./debug-channel.js";
|
|
15
|
+
import { findSourceMapURL } from "../deps/browser.js";
|
|
14
16
|
import { NavigationProvider } from "./react/index.js";
|
|
15
17
|
import type {
|
|
16
18
|
RscPayload,
|
|
@@ -139,9 +141,24 @@ export async function initBrowserApp(
|
|
|
139
141
|
initialTheme,
|
|
140
142
|
} = options;
|
|
141
143
|
|
|
144
|
+
// Dev-only: create debug channel for React Performance Tracks.
|
|
145
|
+
// The debugId was injected into the HTML bootstrap by the SSR handler.
|
|
146
|
+
// The client sends a "ready" signal so the server flushes buffered data.
|
|
147
|
+
const ssrDebugId = (globalThis as any).__RANGO_DEBUG_ID__ as
|
|
148
|
+
| string
|
|
149
|
+
| undefined;
|
|
150
|
+
const ssrDebugChannel =
|
|
151
|
+
ssrDebugId && (import.meta as any).hot
|
|
152
|
+
? createClientDebugChannel(ssrDebugId)
|
|
153
|
+
: undefined;
|
|
154
|
+
|
|
142
155
|
// Load initial payload from SSR-injected __FLIGHT_DATA__
|
|
143
|
-
const initialPayload =
|
|
144
|
-
|
|
156
|
+
const initialPayload = await deps.createFromReadableStream<RscPayload>(
|
|
157
|
+
rscStream,
|
|
158
|
+
ssrDebugChannel
|
|
159
|
+
? { debugChannel: ssrDebugChannel, findSourceMapURL }
|
|
160
|
+
: undefined,
|
|
161
|
+
);
|
|
145
162
|
|
|
146
163
|
// Extract themeConfig and initialTheme from payload if not explicitly provided
|
|
147
164
|
// This allows virtual entries to work without importing the router
|
package/src/browser/types.ts
CHANGED
|
@@ -350,7 +350,16 @@ export interface RscBrowserDependencies {
|
|
|
350
350
|
) => string | null;
|
|
351
351
|
},
|
|
352
352
|
) => Promise<T>;
|
|
353
|
-
createFromReadableStream: <T>(
|
|
353
|
+
createFromReadableStream: <T>(
|
|
354
|
+
stream: ReadableStream,
|
|
355
|
+
options?: {
|
|
356
|
+
debugChannel?: { readable?: ReadableStream; writable?: WritableStream };
|
|
357
|
+
findSourceMapURL?: (
|
|
358
|
+
filename: string,
|
|
359
|
+
environmentName: string,
|
|
360
|
+
) => string | null;
|
|
361
|
+
},
|
|
362
|
+
) => Promise<T>;
|
|
354
363
|
encodeReply: (
|
|
355
364
|
args: any[],
|
|
356
365
|
options?: { temporaryReferences?: any },
|
package/src/rsc/handler.ts
CHANGED
|
@@ -428,17 +428,17 @@ export function createRSCHandler<
|
|
|
428
428
|
}
|
|
429
429
|
// Dev-only: wire debug channel for React Performance Tracks
|
|
430
430
|
if (process.env.NODE_ENV !== "production") {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
431
|
+
// Client navigations send the debugId as a header.
|
|
432
|
+
// SSR requests have no client — generate one and create the channel directly.
|
|
433
|
+
const clientDebugId = request.headers.get(DEBUG_ID_HEADER);
|
|
434
|
+
const debugId = clientDebugId || crypto.randomUUID();
|
|
435
|
+
const channel = clientDebugId
|
|
436
|
+
? createServerDebugChannel(debugId)
|
|
437
|
+
: createServerDebugChannel(debugId);
|
|
438
|
+
if (channel) {
|
|
439
|
+
requestContext._debugChannel = channel;
|
|
440
|
+
requestContext._debugId = debugId;
|
|
441
|
+
console.log("[perf-tracks] debug channel attached for", debugId);
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
// Wire background error reporting so "use cache" and other subsystems
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -227,6 +227,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
227
227
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
228
228
|
nonce,
|
|
229
229
|
streamMode,
|
|
230
|
+
debugId: reqCtx._debugId,
|
|
230
231
|
});
|
|
231
232
|
const ssrRenderDur = performance.now() - ssrRenderStart;
|
|
232
233
|
appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
|
package/src/rsc/types.ts
CHANGED
|
@@ -130,6 +130,9 @@ export interface SSRRenderOptions {
|
|
|
130
130
|
* - `"allReady"` — await `stream.allReady` before returning.
|
|
131
131
|
*/
|
|
132
132
|
streamMode?: import("../router/router-options.js").SSRStreamMode;
|
|
133
|
+
|
|
134
|
+
/** @internal Dev-only: debug channel ID for React Performance Tracks */
|
|
135
|
+
debugId?: string;
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
/**
|
|
@@ -293,6 +293,9 @@ export interface RequestContext<
|
|
|
293
293
|
readable: ReadableStream;
|
|
294
294
|
writable: WritableStream;
|
|
295
295
|
};
|
|
296
|
+
|
|
297
|
+
/** @internal Dev-only: debug channel ID for React Performance Tracks */
|
|
298
|
+
_debugId?: string;
|
|
296
299
|
}
|
|
297
300
|
|
|
298
301
|
/**
|
|
@@ -323,6 +326,7 @@ export type PublicRequestContext<
|
|
|
323
326
|
| "_debugPerformance"
|
|
324
327
|
| "_metricsStore"
|
|
325
328
|
| "_debugChannel"
|
|
329
|
+
| "_debugId"
|
|
326
330
|
| "_setStatus"
|
|
327
331
|
| "res"
|
|
328
332
|
>;
|
package/src/ssr/index.tsx
CHANGED
|
@@ -64,6 +64,9 @@ export interface SSRRenderOptions {
|
|
|
64
64
|
* - `"allReady"` — await `stream.allReady` before returning.
|
|
65
65
|
*/
|
|
66
66
|
streamMode?: import("../router/router-options.js").SSRStreamMode;
|
|
67
|
+
|
|
68
|
+
/** @internal Dev-only: debug channel ID for React Performance Tracks */
|
|
69
|
+
debugId?: string;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
/**
|
|
@@ -329,7 +332,16 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
// Get bootstrap script content
|
|
332
|
-
|
|
335
|
+
let bootstrapScriptContent = await loadBootstrapScriptContent();
|
|
336
|
+
|
|
337
|
+
// Dev-only: inject debugId for React Performance Tracks.
|
|
338
|
+
// The client reads this during hydration to create the matching WS channel.
|
|
339
|
+
const debugId = options?.debugId;
|
|
340
|
+
if (debugId) {
|
|
341
|
+
bootstrapScriptContent =
|
|
342
|
+
`globalThis.__RANGO_DEBUG_ID__=${JSON.stringify(debugId)};` +
|
|
343
|
+
bootstrapScriptContent;
|
|
344
|
+
}
|
|
333
345
|
|
|
334
346
|
// Render React tree to HTML stream
|
|
335
347
|
// Pass formState for useActionState progressive enhancement if provided
|
|
@@ -46,8 +46,12 @@ type DebugChannelRegistry = {
|
|
|
46
46
|
|
|
47
47
|
const GLOBAL_KEY = "__RANGO_DEBUG_CHANNELS__";
|
|
48
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";
|
|
49
53
|
function getRegistry(): DebugChannelRegistry {
|
|
50
|
-
return ((
|
|
54
|
+
return ((Module as any)[GLOBAL_KEY] ??= {
|
|
51
55
|
channels: new Map(),
|
|
52
56
|
sessions: new Map(),
|
|
53
57
|
});
|
|
@@ -87,13 +91,51 @@ const bytesToBase64 = (bytes: Uint8Array) =>
|
|
|
87
91
|
const base64ToBytes = (base64: string) =>
|
|
88
92
|
new Uint8Array(Buffer.from(base64, "base64"));
|
|
89
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
|
+
const RSDW_PATCH_RE =
|
|
103
|
+
/((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
|
|
104
|
+
|
|
105
|
+
function buildPatchReplacement(match: string, debugInfoVar: string): string {
|
|
106
|
+
return `${match}
|
|
107
|
+
if (${debugInfoVar} && 0 === ${debugInfoVar}.length && "fulfilled" === root.status) {
|
|
108
|
+
var _resolved = "function" === typeof resolveLazy ? resolveLazy(root.value) : root.value;
|
|
109
|
+
if ("object" === typeof _resolved && null !== _resolved && isArrayImpl(_resolved._debugInfo)) {
|
|
110
|
+
${debugInfoVar} = _resolved._debugInfo;
|
|
111
|
+
}
|
|
112
|
+
}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
90
115
|
export function performanceTracksPlugin(): Plugin {
|
|
91
116
|
return {
|
|
92
117
|
name: "@rangojs/router:performance-tracks",
|
|
93
|
-
|
|
94
|
-
|
|
118
|
+
// configureServer + transform — naturally dev-only
|
|
119
|
+
|
|
120
|
+
transform(code, id) {
|
|
121
|
+
// Only patch RSDW client browser bundle
|
|
122
|
+
if (!id.includes("react-server-dom") || !id.includes("client")) return;
|
|
123
|
+
const match = code.match(RSDW_PATCH_RE);
|
|
124
|
+
if (!match) return;
|
|
125
|
+
const patched = code.replace(
|
|
126
|
+
match[1]!,
|
|
127
|
+
buildPatchReplacement(match[1]!, match[2]!),
|
|
128
|
+
);
|
|
129
|
+
console.log(
|
|
130
|
+
"[perf-tracks] patched RSDW client for plain-object _debugInfo recovery (var:",
|
|
131
|
+
match[2],
|
|
132
|
+
")",
|
|
133
|
+
);
|
|
134
|
+
return patched;
|
|
135
|
+
},
|
|
95
136
|
|
|
96
137
|
configureServer(server) {
|
|
138
|
+
console.log("[perf-tracks] plugin loaded, configureServer called");
|
|
97
139
|
const hot = server.environments.client.hot;
|
|
98
140
|
const registry = getRegistry();
|
|
99
141
|
const sessions = registry.sessions;
|
|
@@ -131,9 +173,21 @@ export function performanceTracksPlugin(): Plugin {
|
|
|
131
173
|
},
|
|
132
174
|
});
|
|
133
175
|
|
|
176
|
+
let chunkCount = 0;
|
|
134
177
|
// Writable: React writes debug data here, we forward to client via WS
|
|
135
178
|
const writable = new WritableStream<Uint8Array>({
|
|
136
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
|
+
}
|
|
137
191
|
if (session!.pendingChunks) {
|
|
138
192
|
session!.pendingChunks.push(chunk);
|
|
139
193
|
} else {
|
|
@@ -141,6 +195,12 @@ export function performanceTracksPlugin(): Plugin {
|
|
|
141
195
|
}
|
|
142
196
|
},
|
|
143
197
|
close() {
|
|
198
|
+
console.log(
|
|
199
|
+
"[perf-tracks] writable: closed after",
|
|
200
|
+
chunkCount,
|
|
201
|
+
"chunks for",
|
|
202
|
+
debugId.slice(0, 8),
|
|
203
|
+
);
|
|
144
204
|
session!.ended = true;
|
|
145
205
|
cleanupIfEnded(debugId, session!);
|
|
146
206
|
},
|
|
@@ -185,6 +245,16 @@ export function performanceTracksPlugin(): Plugin {
|
|
|
185
245
|
|
|
186
246
|
// Ready signal — flush pending chunks
|
|
187
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
|
+
);
|
|
188
258
|
if (session.pendingChunks) {
|
|
189
259
|
for (const chunk of session.pendingChunks) {
|
|
190
260
|
sendChunk(payload.i, chunk);
|
|
@@ -193,17 +263,26 @@ export function performanceTracksPlugin(): Plugin {
|
|
|
193
263
|
}
|
|
194
264
|
cleanupIfEnded(payload.i, session);
|
|
195
265
|
} else {
|
|
266
|
+
console.log(
|
|
267
|
+
"[perf-tracks] ready signal for",
|
|
268
|
+
payload.i.slice(0, 8),
|
|
269
|
+
"(no session yet)",
|
|
270
|
+
);
|
|
196
271
|
sessions.set(payload.i, { ended: false });
|
|
197
272
|
}
|
|
198
273
|
});
|
|
199
274
|
|
|
200
|
-
//
|
|
201
|
-
//
|
|
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)
|
|
202
279
|
server.middlewares.use((req: any, _res: any, next: any) => {
|
|
280
|
+
const url: string = req.url || "";
|
|
203
281
|
const existingId = req.headers[DEBUG_ID_HEADER.toLowerCase()] as string;
|
|
204
282
|
const isHtml = req.headers.accept?.includes("text/html");
|
|
283
|
+
const isRsc = url.includes("_rsc_partial");
|
|
205
284
|
|
|
206
|
-
if (!existingId && !isHtml) {
|
|
285
|
+
if (!existingId && !isHtml && !isRsc) {
|
|
207
286
|
next();
|
|
208
287
|
return;
|
|
209
288
|
}
|
|
@@ -219,10 +298,11 @@ export function performanceTracksPlugin(): Plugin {
|
|
|
219
298
|
|
|
220
299
|
registerDebugChannel(debugId);
|
|
221
300
|
console.log(
|
|
222
|
-
"[perf-tracks] middleware:
|
|
301
|
+
"[perf-tracks] middleware: channel for",
|
|
223
302
|
debugId,
|
|
224
|
-
"
|
|
225
|
-
|
|
303
|
+
"url:",
|
|
304
|
+
url.slice(0, 80),
|
|
305
|
+
existingId ? "(client)" : isHtml ? "(SSR)" : "(RSC partial)",
|
|
226
306
|
);
|
|
227
307
|
next();
|
|
228
308
|
});
|
package/src/vite/rango.ts
CHANGED
|
@@ -55,6 +55,7 @@ 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);
|
|
58
59
|
const showBanner = resolvedOptions.banner ?? true;
|
|
59
60
|
|
|
60
61
|
const plugins: PluginOption[] = [];
|
|
@@ -335,6 +336,15 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
335
336
|
// Add virtual entries plugin (RSC entry generated lazily from routerRef)
|
|
336
337
|
plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
|
|
337
338
|
|
|
339
|
+
// Dev-only: React Performance Tracks (debugChannel transport via HMR WS)
|
|
340
|
+
// Must be before rsc() so middleware runs before RSC handler.
|
|
341
|
+
const perfPlugin = performanceTracksPlugin();
|
|
342
|
+
console.log(
|
|
343
|
+
"[perf-tracks] rango: plugin created, has configureServer:",
|
|
344
|
+
!!perfPlugin.configureServer,
|
|
345
|
+
);
|
|
346
|
+
plugins.push(perfPlugin);
|
|
347
|
+
|
|
338
348
|
plugins.push(
|
|
339
349
|
rsc({
|
|
340
350
|
entries: finalEntries,
|
|
@@ -442,8 +452,5 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
442
452
|
}),
|
|
443
453
|
);
|
|
444
454
|
|
|
445
|
-
// Dev-only: React Performance Tracks (debugChannel transport via HMR WS)
|
|
446
|
-
plugins.push(performanceTracksPlugin());
|
|
447
|
-
|
|
448
455
|
return plugins;
|
|
449
456
|
}
|