@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.
@@ -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.a769fbe7",
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.14",
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 globalThis[GLOBAL_KEY] ??= {
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
- apply: "serve",
4881
- enforce: "pre",
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
- if (!existingId && !isHtml) {
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: created channel for",
5046
+ "[perf-tracks] middleware: channel for",
4986
5047
  debugId,
4987
- "from",
4988
- existingId ? "client header" : "SSR inject"
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.a769fbe7",
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.14",
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: create debug channel for React Performance Tracks
113
- const debugId = (import.meta as any).hot
114
- ? crypto.randomUUID()
115
- : undefined;
116
- const debugChannel = debugId
117
- ? createClientDebugChannel(debugId)
118
- : undefined;
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
- await deps.createFromReadableStream<RscPayload>(rscStream);
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
@@ -350,7 +350,16 @@ export interface RscBrowserDependencies {
350
350
  ) => string | null;
351
351
  },
352
352
  ) => Promise<T>;
353
- createFromReadableStream: <T>(stream: ReadableStream) => Promise<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 },
@@ -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
- const debugId = request.headers.get(DEBUG_ID_HEADER);
432
- console.log("[perf-tracks] handler: debugId header =", debugId);
433
- if (debugId) {
434
- const channel = createServerDebugChannel(debugId);
435
- console.log(
436
- "[perf-tracks] handler: channel =",
437
- channel ? "created" : "NOT FOUND",
438
- );
439
- if (channel) {
440
- requestContext._debugChannel = channel;
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
@@ -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
- const bootstrapScriptContent = await loadBootstrapScriptContent();
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 ((globalThis as any)[GLOBAL_KEY] ??= {
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
- apply: "serve",
94
- enforce: "pre",
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
- // Register middleware directly (not as post-hook) so it runs
201
- // BEFORE the RSC handler — the channel must exist before rendering.
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: created channel for",
301
+ "[perf-tracks] middleware: channel for",
223
302
  debugId,
224
- "from",
225
- existingId ? "client header" : "SSR inject",
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
  }