@rangojs/router 0.0.0-experimental.8874d8d2 → 0.0.0-experimental.89

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 (178) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +126 -38
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1705 -701
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +19 -16
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/cache-guide/SKILL.md +32 -0
  9. package/skills/caching/SKILL.md +45 -4
  10. package/skills/handler-use/SKILL.md +362 -0
  11. package/skills/hooks/SKILL.md +28 -20
  12. package/skills/intercept/SKILL.md +20 -0
  13. package/skills/layout/SKILL.md +22 -0
  14. package/skills/links/SKILL.md +91 -17
  15. package/skills/loader/SKILL.md +88 -45
  16. package/skills/middleware/SKILL.md +34 -3
  17. package/skills/migrate-nextjs/SKILL.md +560 -0
  18. package/skills/migrate-react-router/SKILL.md +765 -0
  19. package/skills/parallel/SKILL.md +185 -0
  20. package/skills/prerender/SKILL.md +110 -68
  21. package/skills/rango/SKILL.md +24 -22
  22. package/skills/response-routes/SKILL.md +8 -0
  23. package/skills/route/SKILL.md +55 -0
  24. package/skills/router-setup/SKILL.md +87 -2
  25. package/skills/streams-and-websockets/SKILL.md +283 -0
  26. package/skills/typesafety/SKILL.md +13 -1
  27. package/src/__internal.ts +1 -1
  28. package/src/browser/app-shell.ts +52 -0
  29. package/src/browser/app-version.ts +14 -0
  30. package/src/browser/event-controller.ts +5 -0
  31. package/src/browser/navigation-bridge.ts +90 -16
  32. package/src/browser/navigation-client.ts +167 -59
  33. package/src/browser/navigation-store.ts +68 -9
  34. package/src/browser/navigation-transaction.ts +11 -9
  35. package/src/browser/partial-update.ts +113 -17
  36. package/src/browser/prefetch/cache.ts +175 -15
  37. package/src/browser/prefetch/fetch.ts +180 -33
  38. package/src/browser/prefetch/queue.ts +123 -20
  39. package/src/browser/prefetch/resource-ready.ts +77 -0
  40. package/src/browser/rango-state.ts +53 -13
  41. package/src/browser/react/Link.tsx +81 -9
  42. package/src/browser/react/NavigationProvider.tsx +89 -14
  43. package/src/browser/react/context.ts +7 -2
  44. package/src/browser/react/use-handle.ts +9 -58
  45. package/src/browser/react/use-navigation.ts +22 -2
  46. package/src/browser/react/use-params.ts +11 -1
  47. package/src/browser/react/use-router.ts +29 -9
  48. package/src/browser/rsc-router.tsx +168 -65
  49. package/src/browser/scroll-restoration.ts +41 -42
  50. package/src/browser/segment-reconciler.ts +36 -9
  51. package/src/browser/server-action-bridge.ts +8 -6
  52. package/src/browser/types.ts +49 -5
  53. package/src/build/generate-manifest.ts +6 -6
  54. package/src/build/generate-route-types.ts +3 -0
  55. package/src/build/route-trie.ts +50 -24
  56. package/src/build/route-types/include-resolution.ts +8 -1
  57. package/src/build/route-types/router-processing.ts +223 -74
  58. package/src/build/route-types/scan-filter.ts +8 -1
  59. package/src/cache/cache-runtime.ts +15 -11
  60. package/src/cache/cache-scope.ts +48 -7
  61. package/src/cache/cf/cf-cache-store.ts +455 -15
  62. package/src/cache/cf/index.ts +5 -1
  63. package/src/cache/document-cache.ts +17 -7
  64. package/src/cache/index.ts +1 -0
  65. package/src/cache/taint.ts +55 -0
  66. package/src/client.tsx +84 -230
  67. package/src/context-var.ts +72 -2
  68. package/src/debug.ts +2 -2
  69. package/src/handle.ts +40 -0
  70. package/src/index.rsc.ts +6 -1
  71. package/src/index.ts +49 -6
  72. package/src/outlet-context.ts +1 -1
  73. package/src/prerender/store.ts +5 -4
  74. package/src/prerender.ts +138 -77
  75. package/src/response-utils.ts +28 -0
  76. package/src/reverse.ts +27 -2
  77. package/src/route-definition/dsl-helpers.ts +240 -40
  78. package/src/route-definition/helpers-types.ts +67 -19
  79. package/src/route-definition/index.ts +3 -0
  80. package/src/route-definition/redirect.ts +11 -3
  81. package/src/route-definition/resolve-handler-use.ts +155 -0
  82. package/src/route-map-builder.ts +7 -1
  83. package/src/route-types.ts +18 -0
  84. package/src/router/content-negotiation.ts +100 -1
  85. package/src/router/find-match.ts +4 -2
  86. package/src/router/handler-context.ts +101 -25
  87. package/src/router/intercept-resolution.ts +11 -4
  88. package/src/router/lazy-includes.ts +10 -7
  89. package/src/router/loader-resolution.ts +159 -21
  90. package/src/router/logging.ts +5 -2
  91. package/src/router/manifest.ts +31 -16
  92. package/src/router/match-api.ts +127 -192
  93. package/src/router/match-middleware/background-revalidation.ts +30 -2
  94. package/src/router/match-middleware/cache-lookup.ts +94 -17
  95. package/src/router/match-middleware/cache-store.ts +53 -10
  96. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  97. package/src/router/match-middleware/segment-resolution.ts +61 -5
  98. package/src/router/match-result.ts +104 -10
  99. package/src/router/metrics.ts +6 -1
  100. package/src/router/middleware-types.ts +8 -30
  101. package/src/router/middleware.ts +36 -10
  102. package/src/router/navigation-snapshot.ts +182 -0
  103. package/src/router/pattern-matching.ts +60 -9
  104. package/src/router/prerender-match.ts +110 -10
  105. package/src/router/preview-match.ts +30 -102
  106. package/src/router/request-classification.ts +310 -0
  107. package/src/router/route-snapshot.ts +245 -0
  108. package/src/router/router-context.ts +6 -1
  109. package/src/router/router-interfaces.ts +36 -4
  110. package/src/router/router-options.ts +37 -11
  111. package/src/router/segment-resolution/fresh.ts +198 -20
  112. package/src/router/segment-resolution/helpers.ts +29 -24
  113. package/src/router/segment-resolution/loader-cache.ts +1 -0
  114. package/src/router/segment-resolution/revalidation.ts +437 -297
  115. package/src/router/segment-wrappers.ts +2 -0
  116. package/src/router/trie-matching.ts +10 -4
  117. package/src/router/types.ts +1 -0
  118. package/src/router/url-params.ts +49 -0
  119. package/src/router.ts +60 -8
  120. package/src/rsc/handler.ts +478 -374
  121. package/src/rsc/helpers.ts +69 -41
  122. package/src/rsc/loader-fetch.ts +23 -3
  123. package/src/rsc/manifest-init.ts +5 -1
  124. package/src/rsc/progressive-enhancement.ts +16 -2
  125. package/src/rsc/response-route-handler.ts +14 -1
  126. package/src/rsc/rsc-rendering.ts +17 -1
  127. package/src/rsc/server-action.ts +10 -0
  128. package/src/rsc/ssr-setup.ts +2 -2
  129. package/src/rsc/types.ts +9 -1
  130. package/src/segment-content-promise.ts +67 -0
  131. package/src/segment-loader-promise.ts +122 -0
  132. package/src/segment-system.tsx +109 -23
  133. package/src/server/context.ts +166 -17
  134. package/src/server/handle-store.ts +19 -0
  135. package/src/server/loader-registry.ts +9 -8
  136. package/src/server/request-context.ts +194 -60
  137. package/src/ssr/index.tsx +4 -0
  138. package/src/static-handler.ts +18 -6
  139. package/src/types/cache-types.ts +4 -4
  140. package/src/types/handler-context.ts +137 -65
  141. package/src/types/loader-types.ts +41 -15
  142. package/src/types/request-scope.ts +126 -0
  143. package/src/types/route-entry.ts +19 -1
  144. package/src/types/segments.ts +2 -0
  145. package/src/urls/include-helper.ts +24 -14
  146. package/src/urls/path-helper-types.ts +39 -6
  147. package/src/urls/path-helper.ts +48 -13
  148. package/src/urls/pattern-types.ts +12 -0
  149. package/src/urls/response-types.ts +18 -16
  150. package/src/use-loader.tsx +77 -5
  151. package/src/vite/debug.ts +184 -0
  152. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  153. package/src/vite/discovery/discover-routers.ts +36 -4
  154. package/src/vite/discovery/prerender-collection.ts +175 -74
  155. package/src/vite/discovery/state.ts +13 -6
  156. package/src/vite/index.ts +4 -0
  157. package/src/vite/plugin-types.ts +51 -79
  158. package/src/vite/plugins/cjs-to-esm.ts +5 -0
  159. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  160. package/src/vite/plugins/client-ref-hashing.ts +16 -4
  161. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  162. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  163. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  164. package/src/vite/plugins/expose-action-id.ts +53 -31
  165. package/src/vite/plugins/expose-id-utils.ts +12 -0
  166. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  167. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  168. package/src/vite/plugins/expose-internal-ids.ts +563 -316
  169. package/src/vite/plugins/performance-tracks.ts +96 -0
  170. package/src/vite/plugins/refresh-cmd.ts +88 -26
  171. package/src/vite/plugins/use-cache-transform.ts +56 -43
  172. package/src/vite/plugins/version-plugin.ts +13 -1
  173. package/src/vite/rango.ts +204 -217
  174. package/src/vite/router-discovery.ts +393 -67
  175. package/src/vite/utils/banner.ts +4 -4
  176. package/src/vite/utils/package-resolution.ts +41 -1
  177. package/src/vite/utils/prerender-utils.ts +37 -5
  178. package/src/vite/utils/shared-utils.ts +3 -2
@@ -10,14 +10,19 @@ 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, register } from "node:module";
14
+ import { pathToFileURL } from "node:url";
13
15
  import {
14
16
  formatNestedRouterConflictError,
15
17
  findNestedRouterConflict,
16
18
  findRouterFiles,
17
- createScanFilter,
18
19
  } from "../build/generate-route-types.js";
19
20
  import { createVersionPlugin } from "./plugins/version-plugin.js";
20
21
  import { createVirtualStubPlugin } from "./plugins/virtual-stub-plugin.js";
22
+ import {
23
+ BUILD_ENV_GLOBAL_KEY,
24
+ createCloudflareProtocolStubPlugin,
25
+ } from "./plugins/cloudflare-protocol-stub.js";
21
26
  import {
22
27
  exposeInternalIds,
23
28
  exposeRouterId,
@@ -43,9 +48,58 @@ import {
43
48
  } from "./discovery/virtual-module-codegen.js";
44
49
  import { postprocessBundle } from "./discovery/bundle-postprocess.js";
45
50
  import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
51
+ import { createRangoDebugger, timed, timedSync, NS } from "./debug.js";
52
+
53
+ const debugDiscovery = createRangoDebugger(NS.discovery);
54
+ const debugRoutes = createRangoDebugger(NS.routes);
55
+ const debugBuild = createRangoDebugger(NS.build);
56
+ const debugDev = createRangoDebugger(NS.dev);
46
57
 
47
58
  export { VIRTUAL_ROUTES_MANIFEST_ID };
48
59
 
60
+ // ============================================================================
61
+ // Node ESM Loader Hook Registration
62
+ // ============================================================================
63
+
64
+ /**
65
+ * Registers a Node ESM loader hook that resolves `cloudflare:*` specifiers
66
+ * to a data: URL stub. Defense-in-depth alongside the Vite transform in
67
+ * `cloudflare-protocol-stub.ts`:
68
+ *
69
+ * - The Vite transform catches `cloudflare:*` imports in modules that flow
70
+ * through Vite's plugin pipeline. That's the vast majority of cases.
71
+ * - The Node loader catches imports in modules that Vite/Rollup externalize
72
+ * (e.g. the `partyserver` package, which has a top-level
73
+ * `import { DurableObject, env } from "cloudflare:workers"` and ships
74
+ * shapes plugin-rsc marks as external). Externalized modules are loaded
75
+ * via Node's native ESM loader, which rejects URL schemes.
76
+ *
77
+ * Registration is process-global and one-shot. The hook only intercepts
78
+ * `cloudflare:*` specifiers; everything else passes through via
79
+ * `nextResolve()`. It runs in a separate worker thread (Node ESM loader
80
+ * architecture), so it can't read the `globalThis[BUILD_ENV_GLOBAL_KEY]`
81
+ * bridge that the Vite transform uses — the stubs served here always
82
+ * return `env = {}`. That's fine because externalized libraries don't
83
+ * typically access `env` at module top level; user source (where real
84
+ * `env` matters at build time) flows through the Vite transform.
85
+ */
86
+ let loaderHookRegistered = false;
87
+ function ensureCloudflareProtocolLoaderRegistered(): void {
88
+ if (loaderHookRegistered) return;
89
+ loaderHookRegistered = true;
90
+ try {
91
+ register(
92
+ new URL("./plugins/cloudflare-protocol-loader-hook.mjs", import.meta.url),
93
+ );
94
+ } catch (err: any) {
95
+ // register() requires Node 18.19+ / 20.6+. Older Node still has the
96
+ // Vite transform as primary defense.
97
+ console.warn(
98
+ `[rsc-router] Could not register Node ESM loader hook for cloudflare:* imports (${err?.message ?? err}). Falling back to Vite transform only.`,
99
+ );
100
+ }
101
+ }
102
+
49
103
  // ============================================================================
50
104
  // Temp Server Factory
51
105
  // ============================================================================
@@ -65,6 +119,11 @@ async function createTempRscServer(
65
119
  state: DiscoveryState,
66
120
  options: { forceBuild?: boolean; cacheDir?: string } = {},
67
121
  ) {
122
+ // Install the Node ESM loader hook before any module evaluation so
123
+ // `cloudflare:*` specifiers in externalized/loader-delegated modules
124
+ // (e.g. packages plugin-rsc marks as external) resolve to stubs
125
+ // instead of crashing Node's native loader.
126
+ ensureCloudflareProtocolLoaderRegistered();
68
127
  const { default: rsc } = await import("@vitejs/plugin-rsc");
69
128
  return createViteServer({
70
129
  root: state.projectRoot,
@@ -87,6 +146,7 @@ async function createTempRscServer(
87
146
  ...(options.forceBuild ? [hashClientRefs(state.projectRoot)] : []),
88
147
  createVersionPlugin(),
89
148
  createVirtualStubPlugin(),
149
+ createCloudflareProtocolStubPlugin(),
90
150
  // Dev prerender must use dev-mode IDs (path-based) to match the workerd
91
151
  // runtime. forceBuild produces hashed IDs for production bundle consistency.
92
152
  exposeInternalIds(options.forceBuild ? { forceBuild: true } : undefined),
@@ -95,6 +155,111 @@ async function createTempRscServer(
95
155
  });
96
156
  }
97
157
 
158
+ // ============================================================================
159
+ // Build-Time Env Resolution
160
+ // ============================================================================
161
+
162
+ import type {
163
+ BuildEnvOption,
164
+ BuildEnvFactoryContext,
165
+ BuildEnvResult,
166
+ } from "./plugin-types.js";
167
+
168
+ /**
169
+ * Resolve the buildEnv option into a concrete { env, dispose? } result.
170
+ * Handles all four input shapes: false, "auto", factory, plain object.
171
+ */
172
+ async function resolveBuildEnv(
173
+ option: BuildEnvOption | undefined,
174
+ factoryCtx: BuildEnvFactoryContext,
175
+ ): Promise<BuildEnvResult | null> {
176
+ if (!option) return null;
177
+
178
+ if (option === "auto") {
179
+ if (factoryCtx.preset !== "cloudflare") {
180
+ throw new Error(
181
+ '[rsc-router] buildEnv: "auto" is only supported with preset: "cloudflare". ' +
182
+ "Use a factory function or plain object for other presets.",
183
+ );
184
+ }
185
+ try {
186
+ // Resolve wrangler from the user's project root (not the router package)
187
+ const userRequire = createRequire(
188
+ resolve(factoryCtx.root, "package.json"),
189
+ );
190
+ const wranglerPath = userRequire.resolve("wrangler");
191
+ const { getPlatformProxy } = (await import(
192
+ pathToFileURL(wranglerPath).href
193
+ )) as {
194
+ getPlatformProxy: (opts?: any) => Promise<any>;
195
+ };
196
+ const proxy = await getPlatformProxy();
197
+ return {
198
+ env: proxy.env as Record<string, unknown>,
199
+ dispose: proxy.dispose,
200
+ };
201
+ } catch (err: any) {
202
+ throw new Error(
203
+ '[rsc-router] buildEnv: "auto" requires wrangler to be installed.\n' +
204
+ `Install it with: pnpm add -D wrangler\n${err.message}`,
205
+ );
206
+ }
207
+ }
208
+
209
+ if (typeof option === "function") {
210
+ return await option(factoryCtx);
211
+ }
212
+
213
+ // Plain object
214
+ return { env: option };
215
+ }
216
+
217
+ /**
218
+ * Acquire build-time env bindings and store on discovery state.
219
+ * Returns true if env was acquired, false if buildEnv is disabled.
220
+ */
221
+ async function acquireBuildEnv(
222
+ s: DiscoveryState,
223
+ command: "serve" | "build",
224
+ mode: string,
225
+ ): Promise<boolean> {
226
+ const option = s.opts?.buildEnv;
227
+ if (!option) return false;
228
+
229
+ const result = await resolveBuildEnv(option, {
230
+ root: s.projectRoot,
231
+ mode,
232
+ command,
233
+ preset: s.opts?.preset ?? "node",
234
+ });
235
+ if (!result) return false;
236
+
237
+ s.resolvedBuildEnv = result.env;
238
+ s.buildEnvDispose = result.dispose ?? null;
239
+ // Bridge the resolved env into `cloudflare:workers`'s stubbed `env`
240
+ // export so user code that does `import { env } from "cloudflare:workers"`
241
+ // sees the real bindings proxy during discovery + prerender instead of
242
+ // an empty object. The stub reads this global at module-evaluation time.
243
+ (globalThis as Record<string, unknown>)[BUILD_ENV_GLOBAL_KEY] = result.env;
244
+ return true;
245
+ }
246
+
247
+ /**
248
+ * Release build-time env resources and clear state.
249
+ */
250
+ async function releaseBuildEnv(s: DiscoveryState): Promise<void> {
251
+ if (s.buildEnvDispose) {
252
+ try {
253
+ await s.buildEnvDispose();
254
+ } catch (err: any) {
255
+ console.warn(`[rsc-router] buildEnv dispose failed: ${err.message}`);
256
+ }
257
+ s.buildEnvDispose = null;
258
+ }
259
+ s.resolvedBuildEnv = undefined;
260
+ delete (globalThis as Record<string, unknown>)[BUILD_ENV_GLOBAL_KEY];
261
+ }
262
+
98
263
  /**
99
264
  * Plugin that discovers router instances at dev/build time via the RSC environment.
100
265
  *
@@ -112,6 +277,8 @@ export function createRouterDiscoveryPlugin(
112
277
  opts?: PluginOptions,
113
278
  ): Plugin {
114
279
  const s = createDiscoveryState(entryPath, opts);
280
+ let viteCommand: "serve" | "build" = "build";
281
+ let viteMode = "production";
115
282
 
116
283
  return {
117
284
  name: "@rangojs/router:discovery",
@@ -122,32 +289,20 @@ export function createRouterDiscoveryPlugin(
122
289
  __RANGO_DEBUG__: JSON.stringify(!!process.env.INTERNAL_RANGO_DEBUG),
123
290
  },
124
291
  };
125
- if (opts?.enableBuildPrerender) {
126
- config.environments = {
127
- rsc: {
128
- build: {
129
- rollupOptions: {
130
- output: {
131
- manualChunks(id: string) {
132
- if (s.resolvedPrerenderModules?.has(id)) {
133
- return "__prerender-handlers";
134
- }
135
- if (s.resolvedStaticModules?.has(id)) {
136
- return "__static-handlers";
137
- }
138
- },
139
- },
140
- },
141
- },
142
- },
143
- };
144
- }
292
+ // Prerender/static handler modules are bundled naturally with the
293
+ // rest of the RSC entry. A previous design forced them into dedicated
294
+ // __prerender-handlers / __static-handlers chunks via manualChunks,
295
+ // but Rollup hoisted all shared dependencies into those chunks,
296
+ // inflating them to ~1 MB with active runtime code. Handler code is
297
+ // evicted in closeBundle regardless of which chunk it lands in.
145
298
  return config;
146
299
  },
147
300
 
148
301
  configResolved(config) {
149
302
  s.projectRoot = config.root;
150
303
  s.isBuildMode = config.command === "build";
304
+ viteCommand = config.command as "serve" | "build";
305
+ viteMode = config.mode;
151
306
  // Capture user's resolve aliases for the temp server
152
307
  s.userResolveAlias = config.resolve.alias;
153
308
  // Node preset: pick up auto-discovered router path from the config() hook.
@@ -168,13 +323,6 @@ export function createRouterDiscoveryPlugin(
168
323
  s.resolvedEntryPath = entries[0];
169
324
  }
170
325
  }
171
- // Compile include/exclude patterns into a scan filter
172
- if (opts?.include || opts?.exclude) {
173
- s.scanFilter = createScanFilter(s.projectRoot, {
174
- include: opts.include,
175
- exclude: opts.exclude,
176
- });
177
- }
178
326
  // Generate combined named-routes.gen.ts from static source parsing.
179
327
  // Runs before the dev server starts so the gen file exists immediately for IDE.
180
328
  // In build mode, the runtime discovery in buildStart produces the definitive
@@ -225,12 +373,13 @@ export function createRouterDiscoveryPlugin(
225
373
  let prerenderTempServer: any = null;
226
374
  let prerenderNodeRegistry: Map<string, any> | null = null;
227
375
 
228
- // Clean up the temporary server when the dev server shuts down
376
+ // Clean up the temporary server and build env when the dev server shuts down
229
377
  server.httpServer?.on("close", () => {
230
378
  if (prerenderTempServer) {
231
379
  prerenderTempServer.close().catch(() => {});
232
380
  prerenderTempServer = null;
233
381
  }
382
+ releaseBuildEnv(s).catch(() => {});
234
383
  });
235
384
 
236
385
  async function getOrCreateTempServer(): Promise<any | null> {
@@ -260,20 +409,35 @@ export function createRouterDiscoveryPlugin(
260
409
  }
261
410
 
262
411
  const discover = async () => {
412
+ const discoverStart = performance.now();
263
413
  const rscEnv = (server.environments as any)?.rsc;
264
414
  if (!rscEnv?.runner) {
265
415
  // Cloudflare dev: no module runner available (workerd-based RSC env).
266
416
  // Set devServerOrigin so the virtual module can inject __PRERENDER_DEV_URL
267
417
  // for on-demand prerender via the /__rsc_prerender endpoint.
418
+ debugDiscovery?.("dev: no rsc runner (cloudflare path)");
268
419
  s.devServerOrigin = getDevServerOrigin();
269
420
 
270
421
  // Create a temp Node.js server to run runtime discovery and generate
271
422
  // named route types (static parser can't resolve factory calls).
272
423
  try {
273
- const tempRscEnv = await getOrCreateTempServer();
424
+ // Acquire build-time env bindings for dev prerender
425
+ await timed(debugDiscovery, "acquireBuildEnv", () =>
426
+ acquireBuildEnv(s, viteCommand, viteMode),
427
+ );
428
+
429
+ const tempRscEnv = await timed(
430
+ debugDiscovery,
431
+ "getOrCreateTempServer",
432
+ () => getOrCreateTempServer(),
433
+ );
274
434
  if (tempRscEnv) {
275
- await discoverRouters(s, tempRscEnv);
276
- writeRouteTypesFiles(s);
435
+ await timed(debugDiscovery, "discoverRouters (cloudflare)", () =>
436
+ discoverRouters(s, tempRscEnv),
437
+ );
438
+ timedSync(debugDiscovery, "writeRouteTypesFiles", () =>
439
+ writeRouteTypesFiles(s),
440
+ );
277
441
  }
278
442
  } catch (err: any) {
279
443
  console.warn(
@@ -281,21 +445,35 @@ export function createRouterDiscoveryPlugin(
281
445
  );
282
446
  }
283
447
 
448
+ debugDiscovery?.(
449
+ "dev discovery done (%sms)",
450
+ (performance.now() - discoverStart).toFixed(1),
451
+ );
284
452
  resolveDiscovery!();
285
453
  return;
286
454
  }
287
455
 
288
456
  try {
457
+ // Acquire build-time env bindings for dev prerender (Node.js path)
458
+ debugDiscovery?.("dev: node path start");
459
+ await timed(debugDiscovery, "acquireBuildEnv", () =>
460
+ acquireBuildEnv(s, viteCommand, viteMode),
461
+ );
462
+
289
463
  // Set the readiness gate BEFORE discovery so early requests
290
464
  // block until manifest is populated
291
- const serverMod = await rscEnv.runner.import(
292
- "@rangojs/router/server",
465
+ const serverMod = await timed(
466
+ debugDiscovery,
467
+ "import @rangojs/router/server",
468
+ () => rscEnv.runner.import("@rangojs/router/server"),
293
469
  );
294
470
  if (serverMod?.setManifestReadyPromise) {
295
471
  serverMod.setManifestReadyPromise(discoveryPromise);
296
472
  }
297
473
 
298
- await discoverRouters(s, rscEnv);
474
+ await timed(debugDiscovery, "discoverRouters", () =>
475
+ discoverRouters(s, rscEnv),
476
+ );
299
477
 
300
478
  // Store server origin for dev prerender endpoint (virtual module injection)
301
479
  s.devServerOrigin = getDevServerOrigin();
@@ -305,15 +483,23 @@ export function createRouterDiscoveryPlugin(
305
483
  // routes (e.g. Array.from loops) that the static parser cannot see.
306
484
  // writeRouteTypesFiles() only writes when content changes, so this
307
485
  // won't cause unnecessary HMR triggers.
308
- writeRouteTypesFiles(s);
486
+ timedSync(debugDiscovery, "writeRouteTypesFiles", () =>
487
+ writeRouteTypesFiles(s),
488
+ );
309
489
 
310
490
  // Populate the route map and per-router data in the RSC env
311
- await propagateDiscoveryState(rscEnv);
491
+ await timed(debugDiscovery, "propagateDiscoveryState", () =>
492
+ propagateDiscoveryState(rscEnv),
493
+ );
312
494
  } catch (err: any) {
313
495
  console.warn(
314
496
  `[rsc-router] Router discovery failed: ${err.message}\n${err.stack}`,
315
497
  );
316
498
  } finally {
499
+ debugDiscovery?.(
500
+ "dev discovery done (%sms)",
501
+ (performance.now() - discoverStart).toFixed(1),
502
+ );
317
503
  resolveDiscovery!();
318
504
  }
319
505
  };
@@ -380,6 +566,17 @@ export function createRouterDiscoveryPlugin(
380
566
  };
381
567
 
382
568
  server.middlewares.use("/__rsc_prerender", async (req: any, res: any) => {
569
+ const reqStart = debugDev ? performance.now() : 0;
570
+ const logResult = (status: number, note: string) => {
571
+ debugDev?.(
572
+ "/__rsc_prerender %s -> %d %s (%sms)",
573
+ req.url,
574
+ status,
575
+ note,
576
+ (performance.now() - reqStart).toFixed(1),
577
+ );
578
+ };
579
+
383
580
  if (s.discoveryDone) await s.discoveryDone;
384
581
 
385
582
  const url = new URL(req.url || "/", "http://localhost");
@@ -387,12 +584,36 @@ export function createRouterDiscoveryPlugin(
387
584
  if (!pathname) {
388
585
  res.statusCode = 400;
389
586
  res.end("Missing pathname");
587
+ logResult(400, "missing pathname");
390
588
  return;
391
589
  }
392
590
 
393
- // Prefer the main server's registry (Node.js preset: module runner available).
394
- // Fall back to a temp server for Cloudflare where the main RSC env uses workerd.
395
- let registry = mainRegistry;
591
+ // Import the user's entry module to force re-evaluation of any
592
+ // HMR-invalidated modules in the chain (entry router urls handlers).
593
+ // This ensures createRouter() re-runs with updated handler code before
594
+ // we read RouterRegistry. Without this, edits to prerender handler files
595
+ // produce stale content because the old router instance remains registered.
596
+ const rscEnv = (server.environments as any)?.rsc;
597
+ let registry: Map<string, any> | null = null;
598
+ if (rscEnv?.runner && s.resolvedEntryPath) {
599
+ try {
600
+ await rscEnv.runner.import(s.resolvedEntryPath);
601
+ const serverMod = await rscEnv.runner.import(
602
+ "@rangojs/router/server",
603
+ );
604
+ registry = serverMod.RouterRegistry ?? null;
605
+ } catch (err: any) {
606
+ console.warn(
607
+ `[rsc-router] Dev prerender module refresh failed: ${err.message}`,
608
+ );
609
+ res.statusCode = 500;
610
+ res.end(`Prerender handler error: ${err.message}`);
611
+ logResult(500, "module refresh failed");
612
+ return;
613
+ }
614
+ } else {
615
+ registry = mainRegistry;
616
+ }
396
617
 
397
618
  if (!registry) {
398
619
  // No main registry: the RSC env has no module runner (Cloudflare dev).
@@ -406,6 +627,7 @@ export function createRouterDiscoveryPlugin(
406
627
  if (!registry || registry.size === 0) {
407
628
  res.statusCode = 503;
408
629
  res.end("Prerender runner not available");
630
+ logResult(503, "no registry");
409
631
  return;
410
632
  }
411
633
 
@@ -421,6 +643,8 @@ export function createRouterDiscoveryPlugin(
421
643
  {},
422
644
  undefined,
423
645
  wantPassthrough,
646
+ s.resolvedBuildEnv,
647
+ true, // devMode: check getParams for passthrough routes
424
648
  );
425
649
  if (!result) continue;
426
650
  if (result.passthrough) continue;
@@ -442,6 +666,7 @@ export function createRouterDiscoveryPlugin(
442
666
  payload = { segments: result.segments, handles: result.handles };
443
667
  }
444
668
  res.end(JSON.stringify(payload));
669
+ logResult(200, `match ${result.routeName}`);
445
670
  return;
446
671
  } catch (err: any) {
447
672
  console.warn(
@@ -452,6 +677,7 @@ export function createRouterDiscoveryPlugin(
452
677
 
453
678
  res.statusCode = 404;
454
679
  res.end("No prerender match");
680
+ logResult(404, "no match");
455
681
  });
456
682
 
457
683
  // Watch url module and router files for changes and regenerate named-routes.gen.ts.
@@ -499,15 +725,26 @@ export function createRouterDiscoveryPlugin(
499
725
  const rscEnv = (server.environments as any)?.rsc;
500
726
  if (!rscEnv?.runner || runtimeRediscoveryInProgress) return;
501
727
  runtimeRediscoveryInProgress = true;
728
+ const hmrStart = performance.now();
502
729
  try {
503
- await discoverRouters(s, rscEnv);
504
- writeRouteTypesFiles(s);
505
- await propagateDiscoveryState(rscEnv);
730
+ await timed(debugDiscovery, "hmr discoverRouters", () =>
731
+ discoverRouters(s, rscEnv),
732
+ );
733
+ timedSync(debugDiscovery, "hmr writeRouteTypesFiles", () =>
734
+ writeRouteTypesFiles(s),
735
+ );
736
+ await timed(debugDiscovery, "hmr propagateDiscoveryState", () =>
737
+ propagateDiscoveryState(rscEnv),
738
+ );
506
739
  } catch (err: any) {
507
740
  console.warn(
508
741
  `[rsc-router] Runtime re-discovery failed: ${err.message}`,
509
742
  );
510
743
  } finally {
744
+ debugDiscovery?.(
745
+ "hmr re-discovery done (%sms)",
746
+ (performance.now() - hmrStart).toFixed(1),
747
+ );
511
748
  runtimeRediscoveryInProgress = false;
512
749
  }
513
750
  };
@@ -516,6 +753,7 @@ export function createRouterDiscoveryPlugin(
516
753
  clearTimeout(routeChangeTimer);
517
754
  routeChangeTimer = setTimeout(() => {
518
755
  routeChangeTimer = undefined;
756
+ const regenStart = debugDiscovery ? performance.now() : 0;
519
757
  try {
520
758
  writeCombinedRouteTypesWithTracking(s);
521
759
  if (s.perRouterManifests.length > 0) {
@@ -526,6 +764,10 @@ export function createRouterDiscoveryPlugin(
526
764
  `[rsc-router] Route regeneration error: ${err.message}`,
527
765
  );
528
766
  }
767
+ debugDiscovery?.(
768
+ "watcher: regenerated gen files (%sms)",
769
+ (performance.now() - regenStart).toFixed(1),
770
+ );
529
771
  // Async: re-run runtime discovery to refresh factory-generated
530
772
  // routes that the static parser cannot resolve.
531
773
  if (s.perRouterManifests.length > 0) {
@@ -560,6 +802,12 @@ export function createRouterDiscoveryPlugin(
560
802
  const hasUrls = source.includes("urls(");
561
803
  const hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
562
804
  if (!hasUrls && !hasCreateRouter) return;
805
+ debugDiscovery?.(
806
+ "watcher: %s matches (urls=%s, router=%s)",
807
+ filePath,
808
+ hasUrls,
809
+ hasCreateRouter,
810
+ );
563
811
  // Invalidate cache when a router file changes (new router added/removed)
564
812
  if (hasCreateRouter) {
565
813
  const nestedRouterConflict = findNestedRouterConflict([
@@ -604,11 +852,24 @@ export function createRouterDiscoveryPlugin(
604
852
  async buildStart() {
605
853
  if (!s.isBuildMode) return;
606
854
  // Only run once across environment builds
607
- if (s.mergedRouteManifest !== null) return;
855
+ if (s.mergedRouteManifest !== null) {
856
+ debugDiscovery?.(
857
+ "build: skip (already discovered, env=%s)",
858
+ this.environment?.name ?? "?",
859
+ );
860
+ return;
861
+ }
862
+ const buildStartTime = performance.now();
863
+ debugDiscovery?.("build: start (env=%s)", this.environment?.name ?? "?");
608
864
  resetStagedBuildAssets(s.projectRoot);
609
865
  s.prerenderManifestEntries = null;
610
866
  s.staticManifestEntries = null;
611
867
 
868
+ // Acquire build-time env bindings if configured
869
+ await timed(debugDiscovery, "build acquireBuildEnv", () =>
870
+ acquireBuildEnv(s, viteCommand, viteMode),
871
+ );
872
+
612
873
  let tempServer: any = null;
613
874
  // Signal to user-space code (e.g. reverse.ts) that build-time discovery
614
875
  // is active. Uses globalThis because the temp server's module runner
@@ -616,7 +877,11 @@ export function createRouterDiscoveryPlugin(
616
877
  // between the vite plugin and user code loaded via runner.import().
617
878
  (globalThis as any).__rscRouterDiscoveryActive = true;
618
879
  try {
619
- tempServer = await createTempRscServer(s, { forceBuild: true });
880
+ tempServer = await timed(
881
+ debugDiscovery,
882
+ "build createTempRscServer",
883
+ () => createTempRscServer(s, { forceBuild: true }),
884
+ );
620
885
 
621
886
  const rscEnv = (tempServer.environments as any)?.rsc;
622
887
  if (!rscEnv?.runner) {
@@ -636,11 +901,15 @@ export function createRouterDiscoveryPlugin(
636
901
  s.resolvedStaticModules = tempIdsPlugin.api.staticHandlerModules;
637
902
  }
638
903
 
639
- await discoverRouters(s, rscEnv);
904
+ await timed(debugDiscovery, "build discoverRouters", () =>
905
+ discoverRouters(s, rscEnv),
906
+ );
640
907
  // Update named-routes.gen.ts from runtime discovery.
641
908
  // The runtime manifest includes dynamically generated routes
642
909
  // that the static parser cannot extract from source code.
643
- writeRouteTypesFiles(s);
910
+ timedSync(debugDiscovery, "build writeRouteTypesFiles", () =>
911
+ writeRouteTypesFiles(s),
912
+ );
644
913
  } catch (err: any) {
645
914
  // Extract the user source file from the stack trace (skip internal frames)
646
915
  const sourceFile = err.stack
@@ -665,8 +934,15 @@ export function createRouterDiscoveryPlugin(
665
934
  } finally {
666
935
  delete (globalThis as any).__rscRouterDiscoveryActive;
667
936
  if (tempServer) {
668
- await tempServer.close();
937
+ await timed(debugDiscovery, "build tempServer.close", () =>
938
+ tempServer.close(),
939
+ );
669
940
  }
941
+ await releaseBuildEnv(s);
942
+ debugDiscovery?.(
943
+ "build discovery done (%sms)",
944
+ (performance.now() - buildStartTime).toFixed(1),
945
+ );
670
946
  }
671
947
  },
672
948
 
@@ -690,19 +966,38 @@ export function createRouterDiscoveryPlugin(
690
966
  // This is critical for Cloudflare dev where the worker runs in a separate
691
967
  // Miniflare process and can only receive manifest data via the virtual module.
692
968
  if (s.discoveryDone) {
693
- await s.discoveryDone;
969
+ await timed(
970
+ debugRoutes,
971
+ "await discoveryDone (manifest)",
972
+ () => s.discoveryDone,
973
+ );
694
974
  }
695
- return generateRoutesManifestModule(s);
975
+ const code = await timed(
976
+ debugRoutes,
977
+ "generateRoutesManifestModule",
978
+ () => generateRoutesManifestModule(s),
979
+ );
980
+ debugRoutes?.("manifest module emitted (%d bytes)", code?.length ?? 0);
981
+ return code;
696
982
  }
697
983
  // Per-router virtual modules: pure data exports (no side effects).
698
984
  // ensureRouterManifest() imports the module and stores the data.
699
985
  const perRouterPrefix = "\0" + VIRTUAL_ROUTES_MANIFEST_ID + "/";
700
986
  if (id.startsWith(perRouterPrefix)) {
701
987
  if (s.discoveryDone) {
702
- await s.discoveryDone;
988
+ await timed(
989
+ debugRoutes,
990
+ "await discoveryDone (per-router)",
991
+ () => s.discoveryDone,
992
+ );
703
993
  }
704
994
  const routerId = id.slice(perRouterPrefix.length);
705
- return generatePerRouterModule(s, routerId);
995
+ const code = await timed(
996
+ debugRoutes,
997
+ `generatePerRouterModule ${routerId}`,
998
+ () => generatePerRouterModule(s, routerId),
999
+ );
1000
+ return code;
706
1001
  }
707
1002
  // virtual:rsc-router/prerender-paths load handler removed
708
1003
  return null;
@@ -712,6 +1007,7 @@ export function createRouterDiscoveryPlugin(
712
1007
  // Used by closeBundle for handler code eviction and prerender data injection.
713
1008
  generateBundle(_options: any, bundle: any) {
714
1009
  if (this.environment?.name !== "rsc") return;
1010
+ const genStart = debugBuild ? performance.now() : 0;
715
1011
 
716
1012
  // Record RSC entry chunk filename for closeBundle injection
717
1013
  for (const [fileName, chunk] of Object.entries(bundle) as [
@@ -724,8 +1020,19 @@ export function createRouterDiscoveryPlugin(
724
1020
  }
725
1021
  }
726
1022
 
727
- if (!s.resolvedPrerenderModules?.size && !s.resolvedStaticModules?.size)
1023
+ if (!s.resolvedPrerenderModules?.size && !s.resolvedStaticModules?.size) {
1024
+ debugBuild?.(
1025
+ "generateBundle (rsc): no handlers to scan (%sms)",
1026
+ (performance.now() - genStart).toFixed(1),
1027
+ );
728
1028
  return;
1029
+ }
1030
+
1031
+ // Clear maps at the start of each RSC generateBundle pass.
1032
+ // Vite 6 multi-environment builds run RSC twice (analysis + production);
1033
+ // clearing prevents stale/duplicate records from the analysis pass.
1034
+ s.handlerChunkInfoMap.clear();
1035
+ s.staticHandlerChunkInfoMap.clear();
729
1036
 
730
1037
  for (const [fileName, chunk] of Object.entries(bundle) as [
731
1038
  string,
@@ -733,27 +1040,28 @@ export function createRouterDiscoveryPlugin(
733
1040
  ][]) {
734
1041
  if (chunk.type !== "chunk") continue;
735
1042
 
736
- // Prerender handlers chunk
737
- if (
738
- fileName.includes("__prerender-handlers") &&
739
- s.resolvedPrerenderModules?.size
740
- ) {
1043
+ // Scan all chunks for handler exports (handlers may land in any chunk)
1044
+ if (s.resolvedPrerenderModules?.size) {
741
1045
  const handlers = extractHandlerExportsFromChunk(
742
1046
  chunk.code,
743
1047
  s.resolvedPrerenderModules,
744
1048
  "Prerender",
745
- true,
1049
+ false,
746
1050
  );
747
1051
  if (handlers.length > 0) {
748
- s.handlerChunkInfo = { fileName, exports: handlers };
1052
+ const existing = s.handlerChunkInfoMap.get(fileName);
1053
+ if (existing) {
1054
+ existing.exports.push(...handlers);
1055
+ } else {
1056
+ s.handlerChunkInfoMap.set(fileName, {
1057
+ fileName,
1058
+ exports: handlers,
1059
+ });
1060
+ }
749
1061
  }
750
1062
  }
751
1063
 
752
- // Static handlers chunk
753
- if (
754
- fileName.includes("__static-handlers") &&
755
- s.resolvedStaticModules?.size
756
- ) {
1064
+ if (s.resolvedStaticModules?.size) {
757
1065
  const handlers = extractHandlerExportsFromChunk(
758
1066
  chunk.code,
759
1067
  s.resolvedStaticModules,
@@ -761,10 +1069,26 @@ export function createRouterDiscoveryPlugin(
761
1069
  false,
762
1070
  );
763
1071
  if (handlers.length > 0) {
764
- s.staticHandlerChunkInfo = { fileName, exports: handlers };
1072
+ const existing = s.staticHandlerChunkInfoMap.get(fileName);
1073
+ if (existing) {
1074
+ existing.exports.push(...handlers);
1075
+ } else {
1076
+ s.staticHandlerChunkInfoMap.set(fileName, {
1077
+ fileName,
1078
+ exports: handlers,
1079
+ });
1080
+ }
765
1081
  }
766
1082
  }
767
1083
  }
1084
+
1085
+ debugBuild?.(
1086
+ "generateBundle (rsc): scanned %d chunks, %d prerender chunk(s), %d static chunk(s) (%sms)",
1087
+ Object.keys(bundle).length,
1088
+ s.handlerChunkInfoMap.size,
1089
+ s.staticHandlerChunkInfoMap.size,
1090
+ (performance.now() - genStart).toFixed(1),
1091
+ );
768
1092
  },
769
1093
 
770
1094
  // Build-time pre-rendering: evict handler code and inject collected prerender data.
@@ -778,7 +1102,9 @@ export function createRouterDiscoveryPlugin(
778
1102
  // Only run for the RSC environment — other environments (client, ssr) have
779
1103
  // no prerender/static data to process and would just do redundant file I/O.
780
1104
  if (this.environment && this.environment.name !== "rsc") return;
781
- postprocessBundle(s);
1105
+ timedSync(debugBuild, "closeBundle postprocessBundle", () =>
1106
+ postprocessBundle(s),
1107
+ );
782
1108
  },
783
1109
  },
784
1110
  };