@rangojs/router 0.0.0-experimental.a769fbe7 → 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.
Files changed (104) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +130 -47
  3. package/dist/vite/index.js +689 -366
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/package.json +2 -2
  6. package/skills/links/SKILL.md +3 -1
  7. package/skills/middleware/SKILL.md +2 -0
  8. package/skills/prerender/SKILL.md +110 -68
  9. package/skills/router-setup/SKILL.md +35 -0
  10. package/src/__internal.ts +1 -1
  11. package/src/browser/app-version.ts +14 -0
  12. package/src/browser/navigation-bridge.ts +19 -4
  13. package/src/browser/navigation-client.ts +64 -64
  14. package/src/browser/navigation-store.ts +43 -8
  15. package/src/browser/partial-update.ts +27 -5
  16. package/src/browser/prefetch/fetch.ts +8 -2
  17. package/src/browser/react/Link.tsx +44 -8
  18. package/src/browser/react/NavigationProvider.tsx +8 -1
  19. package/src/browser/react/context.ts +7 -2
  20. package/src/browser/react/use-handle.ts +9 -58
  21. package/src/browser/react/use-router.ts +21 -8
  22. package/src/browser/rsc-router.tsx +26 -3
  23. package/src/browser/scroll-restoration.ts +10 -8
  24. package/src/browser/server-action-bridge.ts +8 -18
  25. package/src/browser/types.ts +20 -5
  26. package/src/build/generate-manifest.ts +6 -6
  27. package/src/build/generate-route-types.ts +3 -0
  28. package/src/build/route-types/include-resolution.ts +8 -1
  29. package/src/build/route-types/router-processing.ts +211 -72
  30. package/src/build/route-types/scan-filter.ts +8 -1
  31. package/src/client.tsx +2 -56
  32. package/src/deps/browser.ts +0 -1
  33. package/src/handle.ts +40 -0
  34. package/src/index.rsc.ts +3 -1
  35. package/src/index.ts +12 -0
  36. package/src/prerender/store.ts +5 -4
  37. package/src/prerender.ts +138 -77
  38. package/src/reverse.ts +22 -1
  39. package/src/route-definition/dsl-helpers.ts +42 -19
  40. package/src/route-definition/helpers-types.ts +4 -1
  41. package/src/route-definition/index.ts +3 -0
  42. package/src/route-definition/redirect.ts +9 -1
  43. package/src/route-definition/resolve-handler-use.ts +149 -0
  44. package/src/route-types.ts +11 -0
  45. package/src/router/content-negotiation.ts +100 -1
  46. package/src/router/handler-context.ts +48 -15
  47. package/src/router/intercept-resolution.ts +9 -4
  48. package/src/router/loader-resolution.ts +150 -21
  49. package/src/router/match-api.ts +124 -189
  50. package/src/router/match-middleware/cache-lookup.ts +28 -8
  51. package/src/router/match-middleware/segment-resolution.ts +53 -0
  52. package/src/router/match-result.ts +82 -4
  53. package/src/router/middleware-types.ts +0 -6
  54. package/src/router/middleware.ts +0 -3
  55. package/src/router/navigation-snapshot.ts +182 -0
  56. package/src/router/prerender-match.ts +110 -10
  57. package/src/router/preview-match.ts +30 -102
  58. package/src/router/request-classification.ts +310 -0
  59. package/src/router/route-snapshot.ts +245 -0
  60. package/src/router/router-interfaces.ts +36 -4
  61. package/src/router/router-options.ts +37 -11
  62. package/src/router/segment-resolution/fresh.ts +70 -5
  63. package/src/router/segment-resolution/revalidation.ts +87 -9
  64. package/src/router.ts +53 -5
  65. package/src/rsc/handler.ts +472 -398
  66. package/src/rsc/loader-fetch.ts +18 -3
  67. package/src/rsc/manifest-init.ts +5 -1
  68. package/src/rsc/progressive-enhancement.ts +12 -3
  69. package/src/rsc/rsc-rendering.ts +8 -2
  70. package/src/rsc/server-action.ts +8 -2
  71. package/src/rsc/ssr-setup.ts +2 -2
  72. package/src/rsc/types.ts +6 -4
  73. package/src/server/context.ts +39 -2
  74. package/src/server/handle-store.ts +19 -0
  75. package/src/server/loader-registry.ts +9 -8
  76. package/src/server/request-context.ts +132 -13
  77. package/src/ssr/index.tsx +3 -0
  78. package/src/static-handler.ts +18 -6
  79. package/src/types/cache-types.ts +4 -4
  80. package/src/types/handler-context.ts +17 -11
  81. package/src/types/loader-types.ts +32 -5
  82. package/src/types/route-entry.ts +1 -1
  83. package/src/types/segments.ts +1 -0
  84. package/src/urls/path-helper-types.ts +9 -2
  85. package/src/urls/path-helper.ts +47 -12
  86. package/src/urls/pattern-types.ts +12 -0
  87. package/src/urls/response-types.ts +16 -6
  88. package/src/use-loader.tsx +77 -5
  89. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  90. package/src/vite/discovery/discover-routers.ts +5 -1
  91. package/src/vite/discovery/prerender-collection.ts +128 -74
  92. package/src/vite/discovery/state.ts +13 -4
  93. package/src/vite/index.ts +4 -0
  94. package/src/vite/plugin-types.ts +60 -5
  95. package/src/vite/plugins/expose-id-utils.ts +12 -0
  96. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  97. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  98. package/src/vite/plugins/performance-tracks.ts +64 -207
  99. package/src/vite/plugins/refresh-cmd.ts +88 -26
  100. package/src/vite/rango.ts +18 -5
  101. package/src/vite/router-discovery.ts +178 -37
  102. package/src/vite/utils/prerender-utils.ts +18 -0
  103. package/src/vite/utils/shared-utils.ts +3 -2
  104. package/src/browser/debug-channel.ts +0 -93
@@ -1,8 +1,13 @@
1
1
  import type { Plugin } from "vite";
2
2
 
3
3
  /**
4
- * Vite plugin that triggers a full browser reload when Ctrl+R is pressed
5
- * in the terminal running the dev server.
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
- stdin.setRawMode(true);
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 (data.length !== 1) return;
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
- // Ctrl+C (0x03) defensive fallback. This plugin enables raw mode
38
- // before Vite's internal stdin handler is registered (user plugins
39
- // run first), so there is a brief window where Ctrl+C would be
40
- // swallowed. Re-emit SIGINT so the process exits as expected.
41
- if (data[0] === 0x03) {
42
- process.emit("SIGINT", "SIGINT");
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
- // Ctrl+R = 0x12 in raw mode
47
- if (data[0] === 0x12) {
48
- server.hot.send({ type: "full-reload", path: "*" });
49
- server.config.logger.info(" browser reload (ctrl+r)", {
50
- timestamp: true,
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
@@ -61,7 +61,16 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
61
61
 
62
62
  // Get package resolution info (workspace vs npm install)
63
63
  const rangoAliases = getPackageAliases();
64
- const excludeDeps = getExcludeDeps();
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
+ ];
65
74
 
66
75
  // Mutable ref for router path (node preset only).
67
76
  // Set immediately when user-specified, or populated by the auto-discover
@@ -183,6 +192,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
183
192
 
184
193
  plugins.push(createVirtualEntriesPlugin(finalEntries));
185
194
 
195
+ // Dev-only: RSDW client patch for React Performance Tracks
196
+ plugins.push(performanceTracksPlugin());
197
+
186
198
  // Add RSC plugin with cloudflare-specific options
187
199
  // Note: loadModuleDevProxy should NOT be used with childEnvironments
188
200
  // since SSR runs in workerd alongside RSC
@@ -335,6 +347,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
335
347
  // Add virtual entries plugin (RSC entry generated lazily from routerRef)
336
348
  plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
337
349
 
350
+ // Dev-only: RSDW client patch for React Performance Tracks
351
+ plugins.push(performanceTracksPlugin());
352
+
338
353
  plugins.push(
339
354
  rsc({
340
355
  entries: finalEntries,
@@ -438,12 +453,10 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
438
453
  createRouterDiscoveryPlugin(discoveryEntryPath, {
439
454
  routerPathRef: discoveryRouterRef,
440
455
  enableBuildPrerender: prerenderEnabled,
441
- staticRouteTypesGeneration: resolvedOptions.staticRouteTypesGeneration,
456
+ buildEnv: options?.buildEnv,
457
+ preset,
442
458
  }),
443
459
  );
444
460
 
445
- // Dev-only: React Performance Tracks (debugChannel transport via HMR WS)
446
- plugins.push(performanceTracksPlugin());
447
-
448
461
  return plugins;
449
462
  }
@@ -10,6 +10,8 @@ import type { Plugin } from "vite";
10
10
  import { createServer as createViteServer } from "vite";
11
11
  import { resolve } from "node:path";
12
12
  import { readFileSync } from "node:fs";
13
+ import { createRequire } from "node:module";
14
+ import { pathToFileURL } from "node:url";
13
15
  import {
14
16
  formatNestedRouterConflictError,
15
17
  findNestedRouterConflict,
@@ -94,6 +96,105 @@ async function createTempRscServer(
94
96
  });
95
97
  }
96
98
 
99
+ // ============================================================================
100
+ // Build-Time Env Resolution
101
+ // ============================================================================
102
+
103
+ import type {
104
+ BuildEnvOption,
105
+ BuildEnvFactoryContext,
106
+ BuildEnvResult,
107
+ } from "./plugin-types.js";
108
+
109
+ /**
110
+ * Resolve the buildEnv option into a concrete { env, dispose? } result.
111
+ * Handles all four input shapes: false, "auto", factory, plain object.
112
+ */
113
+ async function resolveBuildEnv(
114
+ option: BuildEnvOption | undefined,
115
+ factoryCtx: BuildEnvFactoryContext,
116
+ ): Promise<BuildEnvResult | null> {
117
+ if (!option) return null;
118
+
119
+ if (option === "auto") {
120
+ if (factoryCtx.preset !== "cloudflare") {
121
+ throw new Error(
122
+ '[rsc-router] buildEnv: "auto" is only supported with preset: "cloudflare". ' +
123
+ "Use a factory function or plain object for other presets.",
124
+ );
125
+ }
126
+ try {
127
+ // Resolve wrangler from the user's project root (not the router package)
128
+ const userRequire = createRequire(
129
+ resolve(factoryCtx.root, "package.json"),
130
+ );
131
+ const wranglerPath = userRequire.resolve("wrangler");
132
+ const { getPlatformProxy } = (await import(
133
+ pathToFileURL(wranglerPath).href
134
+ )) as {
135
+ getPlatformProxy: (opts?: any) => Promise<any>;
136
+ };
137
+ const proxy = await getPlatformProxy();
138
+ return {
139
+ env: proxy.env as Record<string, unknown>,
140
+ dispose: proxy.dispose,
141
+ };
142
+ } catch (err: any) {
143
+ throw new Error(
144
+ '[rsc-router] buildEnv: "auto" requires wrangler to be installed.\n' +
145
+ `Install it with: pnpm add -D wrangler\n${err.message}`,
146
+ );
147
+ }
148
+ }
149
+
150
+ if (typeof option === "function") {
151
+ return await option(factoryCtx);
152
+ }
153
+
154
+ // Plain object
155
+ return { env: option };
156
+ }
157
+
158
+ /**
159
+ * Acquire build-time env bindings and store on discovery state.
160
+ * Returns true if env was acquired, false if buildEnv is disabled.
161
+ */
162
+ async function acquireBuildEnv(
163
+ s: DiscoveryState,
164
+ command: "serve" | "build",
165
+ mode: string,
166
+ ): Promise<boolean> {
167
+ const option = s.opts?.buildEnv;
168
+ if (!option) return false;
169
+
170
+ const result = await resolveBuildEnv(option, {
171
+ root: s.projectRoot,
172
+ mode,
173
+ command,
174
+ preset: s.opts?.preset ?? "node",
175
+ });
176
+ if (!result) return false;
177
+
178
+ s.resolvedBuildEnv = result.env;
179
+ s.buildEnvDispose = result.dispose ?? null;
180
+ return true;
181
+ }
182
+
183
+ /**
184
+ * Release build-time env resources and clear state.
185
+ */
186
+ async function releaseBuildEnv(s: DiscoveryState): Promise<void> {
187
+ if (s.buildEnvDispose) {
188
+ try {
189
+ await s.buildEnvDispose();
190
+ } catch (err: any) {
191
+ console.warn(`[rsc-router] buildEnv dispose failed: ${err.message}`);
192
+ }
193
+ s.buildEnvDispose = null;
194
+ }
195
+ s.resolvedBuildEnv = undefined;
196
+ }
197
+
97
198
  /**
98
199
  * Plugin that discovers router instances at dev/build time via the RSC environment.
99
200
  *
@@ -111,6 +212,8 @@ export function createRouterDiscoveryPlugin(
111
212
  opts?: PluginOptions,
112
213
  ): Plugin {
113
214
  const s = createDiscoveryState(entryPath, opts);
215
+ let viteCommand: "serve" | "build" = "build";
216
+ let viteMode = "production";
114
217
 
115
218
  return {
116
219
  name: "@rangojs/router:discovery",
@@ -121,32 +224,20 @@ export function createRouterDiscoveryPlugin(
121
224
  __RANGO_DEBUG__: JSON.stringify(!!process.env.INTERNAL_RANGO_DEBUG),
122
225
  },
123
226
  };
124
- if (opts?.enableBuildPrerender) {
125
- config.environments = {
126
- rsc: {
127
- build: {
128
- rollupOptions: {
129
- output: {
130
- manualChunks(id: string) {
131
- if (s.resolvedPrerenderModules?.has(id)) {
132
- return "__prerender-handlers";
133
- }
134
- if (s.resolvedStaticModules?.has(id)) {
135
- return "__static-handlers";
136
- }
137
- },
138
- },
139
- },
140
- },
141
- },
142
- };
143
- }
227
+ // Prerender/static handler modules are bundled naturally with the
228
+ // rest of the RSC entry. A previous design forced them into dedicated
229
+ // __prerender-handlers / __static-handlers chunks via manualChunks,
230
+ // but Rollup hoisted all shared dependencies into those chunks,
231
+ // inflating them to ~1 MB with active runtime code. Handler code is
232
+ // evicted in closeBundle regardless of which chunk it lands in.
144
233
  return config;
145
234
  },
146
235
 
147
236
  configResolved(config) {
148
237
  s.projectRoot = config.root;
149
238
  s.isBuildMode = config.command === "build";
239
+ viteCommand = config.command as "serve" | "build";
240
+ viteMode = config.mode;
150
241
  // Capture user's resolve aliases for the temp server
151
242
  s.userResolveAlias = config.resolve.alias;
152
243
  // Node preset: pick up auto-discovered router path from the config() hook.
@@ -217,12 +308,13 @@ export function createRouterDiscoveryPlugin(
217
308
  let prerenderTempServer: any = null;
218
309
  let prerenderNodeRegistry: Map<string, any> | null = null;
219
310
 
220
- // Clean up the temporary server when the dev server shuts down
311
+ // Clean up the temporary server and build env when the dev server shuts down
221
312
  server.httpServer?.on("close", () => {
222
313
  if (prerenderTempServer) {
223
314
  prerenderTempServer.close().catch(() => {});
224
315
  prerenderTempServer = null;
225
316
  }
317
+ releaseBuildEnv(s).catch(() => {});
226
318
  });
227
319
 
228
320
  async function getOrCreateTempServer(): Promise<any | null> {
@@ -262,6 +354,9 @@ export function createRouterDiscoveryPlugin(
262
354
  // Create a temp Node.js server to run runtime discovery and generate
263
355
  // named route types (static parser can't resolve factory calls).
264
356
  try {
357
+ // Acquire build-time env bindings for dev prerender
358
+ await acquireBuildEnv(s, viteCommand, viteMode);
359
+
265
360
  const tempRscEnv = await getOrCreateTempServer();
266
361
  if (tempRscEnv) {
267
362
  await discoverRouters(s, tempRscEnv);
@@ -278,6 +373,9 @@ export function createRouterDiscoveryPlugin(
278
373
  }
279
374
 
280
375
  try {
376
+ // Acquire build-time env bindings for dev prerender (Node.js path)
377
+ await acquireBuildEnv(s, viteCommand, viteMode);
378
+
281
379
  // Set the readiness gate BEFORE discovery so early requests
282
380
  // block until manifest is populated
283
381
  const serverMod = await rscEnv.runner.import(
@@ -382,9 +480,31 @@ export function createRouterDiscoveryPlugin(
382
480
  return;
383
481
  }
384
482
 
385
- // Prefer the main server's registry (Node.js preset: module runner available).
386
- // Fall back to a temp server for Cloudflare where the main RSC env uses workerd.
387
- let registry = mainRegistry;
483
+ // Import the user's entry module to force re-evaluation of any
484
+ // HMR-invalidated modules in the chain (entry router urls handlers).
485
+ // This ensures createRouter() re-runs with updated handler code before
486
+ // we read RouterRegistry. Without this, edits to prerender handler files
487
+ // produce stale content because the old router instance remains registered.
488
+ const rscEnv = (server.environments as any)?.rsc;
489
+ let registry: Map<string, any> | null = null;
490
+ if (rscEnv?.runner && s.resolvedEntryPath) {
491
+ try {
492
+ await rscEnv.runner.import(s.resolvedEntryPath);
493
+ const serverMod = await rscEnv.runner.import(
494
+ "@rangojs/router/server",
495
+ );
496
+ registry = serverMod.RouterRegistry ?? null;
497
+ } catch (err: any) {
498
+ console.warn(
499
+ `[rsc-router] Dev prerender module refresh failed: ${err.message}`,
500
+ );
501
+ res.statusCode = 500;
502
+ res.end(`Prerender handler error: ${err.message}`);
503
+ return;
504
+ }
505
+ } else {
506
+ registry = mainRegistry;
507
+ }
388
508
 
389
509
  if (!registry) {
390
510
  // No main registry: the RSC env has no module runner (Cloudflare dev).
@@ -413,6 +533,8 @@ export function createRouterDiscoveryPlugin(
413
533
  {},
414
534
  undefined,
415
535
  wantPassthrough,
536
+ s.resolvedBuildEnv,
537
+ true, // devMode: check getParams for passthrough routes
416
538
  );
417
539
  if (!result) continue;
418
540
  if (result.passthrough) continue;
@@ -601,6 +723,9 @@ export function createRouterDiscoveryPlugin(
601
723
  s.prerenderManifestEntries = null;
602
724
  s.staticManifestEntries = null;
603
725
 
726
+ // Acquire build-time env bindings if configured
727
+ await acquireBuildEnv(s, viteCommand, viteMode);
728
+
604
729
  let tempServer: any = null;
605
730
  // Signal to user-space code (e.g. reverse.ts) that build-time discovery
606
731
  // is active. Uses globalThis because the temp server's module runner
@@ -659,6 +784,7 @@ export function createRouterDiscoveryPlugin(
659
784
  if (tempServer) {
660
785
  await tempServer.close();
661
786
  }
787
+ await releaseBuildEnv(s);
662
788
  }
663
789
  },
664
790
 
@@ -719,33 +845,40 @@ export function createRouterDiscoveryPlugin(
719
845
  if (!s.resolvedPrerenderModules?.size && !s.resolvedStaticModules?.size)
720
846
  return;
721
847
 
848
+ // Clear maps at the start of each RSC generateBundle pass.
849
+ // Vite 6 multi-environment builds run RSC twice (analysis + production);
850
+ // clearing prevents stale/duplicate records from the analysis pass.
851
+ s.handlerChunkInfoMap.clear();
852
+ s.staticHandlerChunkInfoMap.clear();
853
+
722
854
  for (const [fileName, chunk] of Object.entries(bundle) as [
723
855
  string,
724
856
  any,
725
857
  ][]) {
726
858
  if (chunk.type !== "chunk") continue;
727
859
 
728
- // Prerender handlers chunk
729
- if (
730
- fileName.includes("__prerender-handlers") &&
731
- s.resolvedPrerenderModules?.size
732
- ) {
860
+ // Scan all chunks for handler exports (handlers may land in any chunk)
861
+ if (s.resolvedPrerenderModules?.size) {
733
862
  const handlers = extractHandlerExportsFromChunk(
734
863
  chunk.code,
735
864
  s.resolvedPrerenderModules,
736
865
  "Prerender",
737
- true,
866
+ false,
738
867
  );
739
868
  if (handlers.length > 0) {
740
- s.handlerChunkInfo = { fileName, exports: handlers };
869
+ const existing = s.handlerChunkInfoMap.get(fileName);
870
+ if (existing) {
871
+ existing.exports.push(...handlers);
872
+ } else {
873
+ s.handlerChunkInfoMap.set(fileName, {
874
+ fileName,
875
+ exports: handlers,
876
+ });
877
+ }
741
878
  }
742
879
  }
743
880
 
744
- // Static handlers chunk
745
- if (
746
- fileName.includes("__static-handlers") &&
747
- s.resolvedStaticModules?.size
748
- ) {
881
+ if (s.resolvedStaticModules?.size) {
749
882
  const handlers = extractHandlerExportsFromChunk(
750
883
  chunk.code,
751
884
  s.resolvedStaticModules,
@@ -753,7 +886,15 @@ export function createRouterDiscoveryPlugin(
753
886
  false,
754
887
  );
755
888
  if (handlers.length > 0) {
756
- s.staticHandlerChunkInfo = { fileName, exports: handlers };
889
+ const existing = s.staticHandlerChunkInfoMap.get(fileName);
890
+ if (existing) {
891
+ existing.exports.push(...handlers);
892
+ } else {
893
+ s.staticHandlerChunkInfoMap.set(fileName, {
894
+ fileName,
895
+ exports: handlers,
896
+ });
897
+ }
757
898
  }
758
899
  }
759
900
  }
@@ -31,6 +31,7 @@ export function encodePathParam(value: unknown): string {
31
31
  /**
32
32
  * Substitute route params into a pattern, stripping constraint and optional
33
33
  * syntax (:param(a|b)? -> value). Also handles wildcard params (*key).
34
+ * Optional params not present in `params` are removed from the output.
34
35
  */
35
36
  export function substituteRouteParams(
36
37
  pattern: string,
@@ -38,6 +39,9 @@ export function substituteRouteParams(
38
39
  encode: (value: string) => string = encodeURIComponent,
39
40
  ): string {
40
41
  let result = pattern;
42
+ let hadOmittedOptional = false;
43
+
44
+ // First pass: substitute provided params
41
45
  for (const [key, value] of Object.entries(params)) {
42
46
  const escaped = escapeRegExp(key);
43
47
  result = result.replace(
@@ -46,6 +50,20 @@ export function substituteRouteParams(
46
50
  );
47
51
  result = result.replace(`*${key}`, encode(value));
48
52
  }
53
+
54
+ // Second pass: strip remaining optional param placeholders not in params
55
+ result = result.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\?/g, () => {
56
+ hadOmittedOptional = true;
57
+ return "";
58
+ });
59
+
60
+ // Clean up slashes from omitted optional segments
61
+ if (hadOmittedOptional) {
62
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
63
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
64
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
65
+ }
66
+
49
67
  return result;
50
68
  }
51
69
 
@@ -1,6 +1,7 @@
1
1
  import type { Plugin } from "vite";
2
2
  import * as Vite from "vite";
3
3
  import { getPublishedPackageName } from "./package-resolution.js";
4
+ import { performanceTracksOptimizeDepsPlugin } from "../plugins/performance-tracks.js";
4
5
  import {
5
6
  VIRTUAL_ENTRY_BROWSER,
6
7
  VIRTUAL_ENTRY_SSR,
@@ -35,9 +36,9 @@ const versionEsbuildPlugin = {
35
36
  * Includes the version stub plugin for all environments.
36
37
  */
37
38
  export const sharedEsbuildOptions: {
38
- plugins: (typeof versionEsbuildPlugin)[];
39
+ plugins: any[];
39
40
  } = {
40
- plugins: [versionEsbuildPlugin],
41
+ plugins: [versionEsbuildPlugin, performanceTracksOptimizeDepsPlugin()],
41
42
  };
42
43
 
43
44
  /**
@@ -1,93 +0,0 @@
1
- /**
2
- * Client-side debug channel for React Performance Tracks.
3
- *
4
- * Creates a bidirectional channel that communicates with the server-side
5
- * debug channel via Vite's HMR WebSocket. Used with createFromFetch()
6
- * so Chrome DevTools can display Server Components in the Performance tab.
7
- *
8
- * Dev-only — gated behind import.meta.hot.
9
- */
10
-
11
- export const DEBUG_ID_HEADER = "X-RSC-Debug-Id";
12
- const DEBUG_S2C_EVENT = "rango:perf-s2c";
13
- const DEBUG_C2S_EVENT = "rango:perf-c2s";
14
-
15
- type DebugPayload =
16
- | { i: string; b: string } // chunk (base64)
17
- | { i: string; d: true }; // done
18
-
19
- const bytesToBase64 = (bytes: Uint8Array) => {
20
- let binary = "";
21
- for (let i = 0; i < bytes.length; i++) {
22
- binary += String.fromCharCode(bytes[i]!);
23
- }
24
- return btoa(binary);
25
- };
26
-
27
- const base64ToBytes = (base64: string) =>
28
- Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
29
-
30
- /**
31
- * Create a client-side debug channel for the given debugId.
32
- * The channel communicates with the server via Vite's HMR WebSocket.
33
- */
34
- export function createClientDebugChannel(debugId: string): {
35
- readable: ReadableStream<Uint8Array>;
36
- writable: WritableStream<Uint8Array>;
37
- } | null {
38
- const hot = (import.meta as any).hot;
39
- if (!hot) return null;
40
-
41
- let closed = false;
42
- let onServerData: ((payload: DebugPayload) => void) | undefined;
43
-
44
- const cleanup = (notify?: boolean) => {
45
- if (closed) return;
46
- closed = true;
47
- if (onServerData) {
48
- hot.off(DEBUG_S2C_EVENT, onServerData);
49
- }
50
- if (notify) {
51
- hot.send(DEBUG_C2S_EVENT, { i: debugId, d: true } satisfies DebugPayload);
52
- }
53
- };
54
-
55
- // Readable: receives server-to-client debug data via HMR WS
56
- const readable = new ReadableStream<Uint8Array>({
57
- start(controller) {
58
- onServerData = (payload: DebugPayload) => {
59
- if (closed || payload.i !== debugId) return;
60
- if ("b" in payload) {
61
- controller.enqueue(base64ToBytes(payload.b));
62
- }
63
- if ("d" in payload) {
64
- cleanup();
65
- controller.close();
66
- }
67
- };
68
- hot.on(DEBUG_S2C_EVENT, onServerData);
69
- },
70
- cancel() {
71
- cleanup(true);
72
- },
73
- });
74
-
75
- // Writable: sends client-to-server commands via HMR WS
76
- const writable = new WritableStream<Uint8Array>({
77
- write(chunk) {
78
- if (closed) throw new TypeError("Channel is closed");
79
- hot.send(DEBUG_C2S_EVENT, {
80
- i: debugId,
81
- b: bytesToBase64(chunk),
82
- } satisfies DebugPayload);
83
- },
84
- close() {
85
- cleanup(true);
86
- },
87
- abort() {
88
- cleanup(true);
89
- },
90
- });
91
-
92
- return { readable, writable };
93
- }