@rangojs/router 0.0.0-experimental.8a4d0430 → 0.0.0-experimental.8bcfea43

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 (174) 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 +1171 -461
  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 +184 -16
  37. package/src/browser/prefetch/fetch.ts +180 -33
  38. package/src/browser/prefetch/policy.ts +6 -0
  39. package/src/browser/prefetch/queue.ts +123 -20
  40. package/src/browser/prefetch/resource-ready.ts +77 -0
  41. package/src/browser/rango-state.ts +53 -13
  42. package/src/browser/react/Link.tsx +81 -9
  43. package/src/browser/react/NavigationProvider.tsx +89 -14
  44. package/src/browser/react/context.ts +7 -2
  45. package/src/browser/react/use-handle.ts +9 -58
  46. package/src/browser/react/use-navigation.ts +22 -2
  47. package/src/browser/react/use-params.ts +11 -1
  48. package/src/browser/react/use-router.ts +29 -9
  49. package/src/browser/rsc-router.tsx +168 -65
  50. package/src/browser/scroll-restoration.ts +41 -42
  51. package/src/browser/segment-reconciler.ts +36 -9
  52. package/src/browser/server-action-bridge.ts +8 -6
  53. package/src/browser/types.ts +49 -5
  54. package/src/build/generate-manifest.ts +6 -6
  55. package/src/build/generate-route-types.ts +3 -0
  56. package/src/build/route-trie.ts +50 -24
  57. package/src/build/route-types/include-resolution.ts +8 -1
  58. package/src/build/route-types/router-processing.ts +223 -74
  59. package/src/build/route-types/scan-filter.ts +8 -1
  60. package/src/cache/cache-runtime.ts +15 -11
  61. package/src/cache/cache-scope.ts +48 -7
  62. package/src/cache/cf/cf-cache-store.ts +455 -15
  63. package/src/cache/cf/index.ts +5 -1
  64. package/src/cache/document-cache.ts +17 -7
  65. package/src/cache/index.ts +1 -0
  66. package/src/cache/taint.ts +55 -0
  67. package/src/client.tsx +84 -230
  68. package/src/context-var.ts +72 -2
  69. package/src/debug.ts +2 -2
  70. package/src/handle.ts +40 -0
  71. package/src/index.rsc.ts +6 -1
  72. package/src/index.ts +49 -6
  73. package/src/outlet-context.ts +1 -1
  74. package/src/prerender/store.ts +5 -4
  75. package/src/prerender.ts +138 -77
  76. package/src/response-utils.ts +28 -0
  77. package/src/reverse.ts +27 -2
  78. package/src/route-definition/dsl-helpers.ts +240 -40
  79. package/src/route-definition/helpers-types.ts +67 -19
  80. package/src/route-definition/index.ts +3 -0
  81. package/src/route-definition/redirect.ts +11 -3
  82. package/src/route-definition/resolve-handler-use.ts +155 -0
  83. package/src/route-map-builder.ts +7 -1
  84. package/src/route-types.ts +18 -0
  85. package/src/router/content-negotiation.ts +100 -1
  86. package/src/router/find-match.ts +4 -2
  87. package/src/router/handler-context.ts +101 -25
  88. package/src/router/intercept-resolution.ts +11 -4
  89. package/src/router/lazy-includes.ts +10 -7
  90. package/src/router/loader-resolution.ts +159 -21
  91. package/src/router/logging.ts +5 -2
  92. package/src/router/manifest.ts +31 -16
  93. package/src/router/match-api.ts +127 -192
  94. package/src/router/match-middleware/background-revalidation.ts +30 -2
  95. package/src/router/match-middleware/cache-lookup.ts +94 -17
  96. package/src/router/match-middleware/cache-store.ts +53 -10
  97. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  98. package/src/router/match-middleware/segment-resolution.ts +61 -5
  99. package/src/router/match-result.ts +104 -10
  100. package/src/router/metrics.ts +6 -1
  101. package/src/router/middleware-types.ts +8 -30
  102. package/src/router/middleware.ts +36 -10
  103. package/src/router/navigation-snapshot.ts +182 -0
  104. package/src/router/pattern-matching.ts +60 -9
  105. package/src/router/prerender-match.ts +110 -10
  106. package/src/router/preview-match.ts +30 -102
  107. package/src/router/request-classification.ts +310 -0
  108. package/src/router/route-snapshot.ts +245 -0
  109. package/src/router/router-context.ts +6 -1
  110. package/src/router/router-interfaces.ts +36 -4
  111. package/src/router/router-options.ts +37 -11
  112. package/src/router/segment-resolution/fresh.ts +198 -20
  113. package/src/router/segment-resolution/helpers.ts +29 -24
  114. package/src/router/segment-resolution/loader-cache.ts +1 -0
  115. package/src/router/segment-resolution/revalidation.ts +438 -300
  116. package/src/router/segment-wrappers.ts +2 -0
  117. package/src/router/trie-matching.ts +10 -4
  118. package/src/router/types.ts +1 -0
  119. package/src/router/url-params.ts +49 -0
  120. package/src/router.ts +60 -8
  121. package/src/rsc/handler.ts +478 -374
  122. package/src/rsc/helpers.ts +69 -41
  123. package/src/rsc/loader-fetch.ts +23 -3
  124. package/src/rsc/manifest-init.ts +5 -1
  125. package/src/rsc/progressive-enhancement.ts +16 -2
  126. package/src/rsc/response-route-handler.ts +14 -1
  127. package/src/rsc/rsc-rendering.ts +19 -1
  128. package/src/rsc/server-action.ts +10 -0
  129. package/src/rsc/ssr-setup.ts +2 -2
  130. package/src/rsc/types.ts +9 -1
  131. package/src/segment-content-promise.ts +67 -0
  132. package/src/segment-loader-promise.ts +122 -0
  133. package/src/segment-system.tsx +109 -23
  134. package/src/server/context.ts +166 -17
  135. package/src/server/handle-store.ts +19 -0
  136. package/src/server/loader-registry.ts +9 -8
  137. package/src/server/request-context.ts +194 -60
  138. package/src/ssr/index.tsx +4 -0
  139. package/src/static-handler.ts +18 -6
  140. package/src/types/cache-types.ts +4 -4
  141. package/src/types/handler-context.ts +137 -65
  142. package/src/types/loader-types.ts +41 -15
  143. package/src/types/request-scope.ts +126 -0
  144. package/src/types/route-entry.ts +19 -1
  145. package/src/types/segments.ts +2 -0
  146. package/src/urls/include-helper.ts +24 -14
  147. package/src/urls/path-helper-types.ts +39 -6
  148. package/src/urls/path-helper.ts +48 -13
  149. package/src/urls/pattern-types.ts +12 -0
  150. package/src/urls/response-types.ts +18 -16
  151. package/src/use-loader.tsx +77 -5
  152. package/src/vite/debug.ts +55 -0
  153. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  154. package/src/vite/discovery/discover-routers.ts +5 -1
  155. package/src/vite/discovery/prerender-collection.ts +128 -74
  156. package/src/vite/discovery/state.ts +13 -6
  157. package/src/vite/index.ts +4 -0
  158. package/src/vite/plugin-types.ts +51 -79
  159. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  160. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  161. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  162. package/src/vite/plugins/expose-action-id.ts +1 -3
  163. package/src/vite/plugins/expose-id-utils.ts +12 -0
  164. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  165. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  166. package/src/vite/plugins/performance-tracks.ts +86 -0
  167. package/src/vite/plugins/refresh-cmd.ts +88 -26
  168. package/src/vite/plugins/version-plugin.ts +13 -1
  169. package/src/vite/rango.ts +204 -217
  170. package/src/vite/router-discovery.ts +335 -64
  171. package/src/vite/utils/banner.ts +4 -4
  172. package/src/vite/utils/package-resolution.ts +41 -1
  173. package/src/vite/utils/prerender-utils.ts +37 -5
  174. 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,56 @@ 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 } from "./debug.js";
52
+
53
+ const debugDiscovery = createRangoDebugger("rango:discovery");
54
+ const debugRoutes = createRangoDebugger("rango:routes");
46
55
 
47
56
  export { VIRTUAL_ROUTES_MANIFEST_ID };
48
57
 
58
+ // ============================================================================
59
+ // Node ESM Loader Hook Registration
60
+ // ============================================================================
61
+
62
+ /**
63
+ * Registers a Node ESM loader hook that resolves `cloudflare:*` specifiers
64
+ * to a data: URL stub. Defense-in-depth alongside the Vite transform in
65
+ * `cloudflare-protocol-stub.ts`:
66
+ *
67
+ * - The Vite transform catches `cloudflare:*` imports in modules that flow
68
+ * through Vite's plugin pipeline. That's the vast majority of cases.
69
+ * - The Node loader catches imports in modules that Vite/Rollup externalize
70
+ * (e.g. the `partyserver` package, which has a top-level
71
+ * `import { DurableObject, env } from "cloudflare:workers"` and ships
72
+ * shapes plugin-rsc marks as external). Externalized modules are loaded
73
+ * via Node's native ESM loader, which rejects URL schemes.
74
+ *
75
+ * Registration is process-global and one-shot. The hook only intercepts
76
+ * `cloudflare:*` specifiers; everything else passes through via
77
+ * `nextResolve()`. It runs in a separate worker thread (Node ESM loader
78
+ * architecture), so it can't read the `globalThis[BUILD_ENV_GLOBAL_KEY]`
79
+ * bridge that the Vite transform uses — the stubs served here always
80
+ * return `env = {}`. That's fine because externalized libraries don't
81
+ * typically access `env` at module top level; user source (where real
82
+ * `env` matters at build time) flows through the Vite transform.
83
+ */
84
+ let loaderHookRegistered = false;
85
+ function ensureCloudflareProtocolLoaderRegistered(): void {
86
+ if (loaderHookRegistered) return;
87
+ loaderHookRegistered = true;
88
+ try {
89
+ register(
90
+ new URL("./plugins/cloudflare-protocol-loader-hook.mjs", import.meta.url),
91
+ );
92
+ } catch (err: any) {
93
+ // register() requires Node 18.19+ / 20.6+. Older Node still has the
94
+ // Vite transform as primary defense.
95
+ console.warn(
96
+ `[rsc-router] Could not register Node ESM loader hook for cloudflare:* imports (${err?.message ?? err}). Falling back to Vite transform only.`,
97
+ );
98
+ }
99
+ }
100
+
49
101
  // ============================================================================
50
102
  // Temp Server Factory
51
103
  // ============================================================================
@@ -65,6 +117,11 @@ async function createTempRscServer(
65
117
  state: DiscoveryState,
66
118
  options: { forceBuild?: boolean; cacheDir?: string } = {},
67
119
  ) {
120
+ // Install the Node ESM loader hook before any module evaluation so
121
+ // `cloudflare:*` specifiers in externalized/loader-delegated modules
122
+ // (e.g. packages plugin-rsc marks as external) resolve to stubs
123
+ // instead of crashing Node's native loader.
124
+ ensureCloudflareProtocolLoaderRegistered();
68
125
  const { default: rsc } = await import("@vitejs/plugin-rsc");
69
126
  return createViteServer({
70
127
  root: state.projectRoot,
@@ -87,6 +144,7 @@ async function createTempRscServer(
87
144
  ...(options.forceBuild ? [hashClientRefs(state.projectRoot)] : []),
88
145
  createVersionPlugin(),
89
146
  createVirtualStubPlugin(),
147
+ createCloudflareProtocolStubPlugin(),
90
148
  // Dev prerender must use dev-mode IDs (path-based) to match the workerd
91
149
  // runtime. forceBuild produces hashed IDs for production bundle consistency.
92
150
  exposeInternalIds(options.forceBuild ? { forceBuild: true } : undefined),
@@ -95,6 +153,111 @@ async function createTempRscServer(
95
153
  });
96
154
  }
97
155
 
156
+ // ============================================================================
157
+ // Build-Time Env Resolution
158
+ // ============================================================================
159
+
160
+ import type {
161
+ BuildEnvOption,
162
+ BuildEnvFactoryContext,
163
+ BuildEnvResult,
164
+ } from "./plugin-types.js";
165
+
166
+ /**
167
+ * Resolve the buildEnv option into a concrete { env, dispose? } result.
168
+ * Handles all four input shapes: false, "auto", factory, plain object.
169
+ */
170
+ async function resolveBuildEnv(
171
+ option: BuildEnvOption | undefined,
172
+ factoryCtx: BuildEnvFactoryContext,
173
+ ): Promise<BuildEnvResult | null> {
174
+ if (!option) return null;
175
+
176
+ if (option === "auto") {
177
+ if (factoryCtx.preset !== "cloudflare") {
178
+ throw new Error(
179
+ '[rsc-router] buildEnv: "auto" is only supported with preset: "cloudflare". ' +
180
+ "Use a factory function or plain object for other presets.",
181
+ );
182
+ }
183
+ try {
184
+ // Resolve wrangler from the user's project root (not the router package)
185
+ const userRequire = createRequire(
186
+ resolve(factoryCtx.root, "package.json"),
187
+ );
188
+ const wranglerPath = userRequire.resolve("wrangler");
189
+ const { getPlatformProxy } = (await import(
190
+ pathToFileURL(wranglerPath).href
191
+ )) as {
192
+ getPlatformProxy: (opts?: any) => Promise<any>;
193
+ };
194
+ const proxy = await getPlatformProxy();
195
+ return {
196
+ env: proxy.env as Record<string, unknown>,
197
+ dispose: proxy.dispose,
198
+ };
199
+ } catch (err: any) {
200
+ throw new Error(
201
+ '[rsc-router] buildEnv: "auto" requires wrangler to be installed.\n' +
202
+ `Install it with: pnpm add -D wrangler\n${err.message}`,
203
+ );
204
+ }
205
+ }
206
+
207
+ if (typeof option === "function") {
208
+ return await option(factoryCtx);
209
+ }
210
+
211
+ // Plain object
212
+ return { env: option };
213
+ }
214
+
215
+ /**
216
+ * Acquire build-time env bindings and store on discovery state.
217
+ * Returns true if env was acquired, false if buildEnv is disabled.
218
+ */
219
+ async function acquireBuildEnv(
220
+ s: DiscoveryState,
221
+ command: "serve" | "build",
222
+ mode: string,
223
+ ): Promise<boolean> {
224
+ const option = s.opts?.buildEnv;
225
+ if (!option) return false;
226
+
227
+ const result = await resolveBuildEnv(option, {
228
+ root: s.projectRoot,
229
+ mode,
230
+ command,
231
+ preset: s.opts?.preset ?? "node",
232
+ });
233
+ if (!result) return false;
234
+
235
+ s.resolvedBuildEnv = result.env;
236
+ s.buildEnvDispose = result.dispose ?? null;
237
+ // Bridge the resolved env into `cloudflare:workers`'s stubbed `env`
238
+ // export so user code that does `import { env } from "cloudflare:workers"`
239
+ // sees the real bindings proxy during discovery + prerender instead of
240
+ // an empty object. The stub reads this global at module-evaluation time.
241
+ (globalThis as Record<string, unknown>)[BUILD_ENV_GLOBAL_KEY] = result.env;
242
+ return true;
243
+ }
244
+
245
+ /**
246
+ * Release build-time env resources and clear state.
247
+ */
248
+ async function releaseBuildEnv(s: DiscoveryState): Promise<void> {
249
+ if (s.buildEnvDispose) {
250
+ try {
251
+ await s.buildEnvDispose();
252
+ } catch (err: any) {
253
+ console.warn(`[rsc-router] buildEnv dispose failed: ${err.message}`);
254
+ }
255
+ s.buildEnvDispose = null;
256
+ }
257
+ s.resolvedBuildEnv = undefined;
258
+ delete (globalThis as Record<string, unknown>)[BUILD_ENV_GLOBAL_KEY];
259
+ }
260
+
98
261
  /**
99
262
  * Plugin that discovers router instances at dev/build time via the RSC environment.
100
263
  *
@@ -112,6 +275,8 @@ export function createRouterDiscoveryPlugin(
112
275
  opts?: PluginOptions,
113
276
  ): Plugin {
114
277
  const s = createDiscoveryState(entryPath, opts);
278
+ let viteCommand: "serve" | "build" = "build";
279
+ let viteMode = "production";
115
280
 
116
281
  return {
117
282
  name: "@rangojs/router:discovery",
@@ -122,32 +287,20 @@ export function createRouterDiscoveryPlugin(
122
287
  __RANGO_DEBUG__: JSON.stringify(!!process.env.INTERNAL_RANGO_DEBUG),
123
288
  },
124
289
  };
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
- }
290
+ // Prerender/static handler modules are bundled naturally with the
291
+ // rest of the RSC entry. A previous design forced them into dedicated
292
+ // __prerender-handlers / __static-handlers chunks via manualChunks,
293
+ // but Rollup hoisted all shared dependencies into those chunks,
294
+ // inflating them to ~1 MB with active runtime code. Handler code is
295
+ // evicted in closeBundle regardless of which chunk it lands in.
145
296
  return config;
146
297
  },
147
298
 
148
299
  configResolved(config) {
149
300
  s.projectRoot = config.root;
150
301
  s.isBuildMode = config.command === "build";
302
+ viteCommand = config.command as "serve" | "build";
303
+ viteMode = config.mode;
151
304
  // Capture user's resolve aliases for the temp server
152
305
  s.userResolveAlias = config.resolve.alias;
153
306
  // Node preset: pick up auto-discovered router path from the config() hook.
@@ -168,13 +321,6 @@ export function createRouterDiscoveryPlugin(
168
321
  s.resolvedEntryPath = entries[0];
169
322
  }
170
323
  }
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
324
  // Generate combined named-routes.gen.ts from static source parsing.
179
325
  // Runs before the dev server starts so the gen file exists immediately for IDE.
180
326
  // In build mode, the runtime discovery in buildStart produces the definitive
@@ -225,12 +371,13 @@ export function createRouterDiscoveryPlugin(
225
371
  let prerenderTempServer: any = null;
226
372
  let prerenderNodeRegistry: Map<string, any> | null = null;
227
373
 
228
- // Clean up the temporary server when the dev server shuts down
374
+ // Clean up the temporary server and build env when the dev server shuts down
229
375
  server.httpServer?.on("close", () => {
230
376
  if (prerenderTempServer) {
231
377
  prerenderTempServer.close().catch(() => {});
232
378
  prerenderTempServer = null;
233
379
  }
380
+ releaseBuildEnv(s).catch(() => {});
234
381
  });
235
382
 
236
383
  async function getOrCreateTempServer(): Promise<any | null> {
@@ -260,20 +407,35 @@ export function createRouterDiscoveryPlugin(
260
407
  }
261
408
 
262
409
  const discover = async () => {
410
+ const discoverStart = performance.now();
263
411
  const rscEnv = (server.environments as any)?.rsc;
264
412
  if (!rscEnv?.runner) {
265
413
  // Cloudflare dev: no module runner available (workerd-based RSC env).
266
414
  // Set devServerOrigin so the virtual module can inject __PRERENDER_DEV_URL
267
415
  // for on-demand prerender via the /__rsc_prerender endpoint.
416
+ debugDiscovery?.("dev: no rsc runner (cloudflare path)");
268
417
  s.devServerOrigin = getDevServerOrigin();
269
418
 
270
419
  // Create a temp Node.js server to run runtime discovery and generate
271
420
  // named route types (static parser can't resolve factory calls).
272
421
  try {
273
- const tempRscEnv = await getOrCreateTempServer();
422
+ // Acquire build-time env bindings for dev prerender
423
+ await timed(debugDiscovery, "acquireBuildEnv", () =>
424
+ acquireBuildEnv(s, viteCommand, viteMode),
425
+ );
426
+
427
+ const tempRscEnv = await timed(
428
+ debugDiscovery,
429
+ "getOrCreateTempServer",
430
+ () => getOrCreateTempServer(),
431
+ );
274
432
  if (tempRscEnv) {
275
- await discoverRouters(s, tempRscEnv);
276
- writeRouteTypesFiles(s);
433
+ await timed(debugDiscovery, "discoverRouters (cloudflare)", () =>
434
+ discoverRouters(s, tempRscEnv),
435
+ );
436
+ timed(debugDiscovery, "writeRouteTypesFiles", () =>
437
+ writeRouteTypesFiles(s),
438
+ );
277
439
  }
278
440
  } catch (err: any) {
279
441
  console.warn(
@@ -281,21 +443,35 @@ export function createRouterDiscoveryPlugin(
281
443
  );
282
444
  }
283
445
 
446
+ debugDiscovery?.(
447
+ "dev discovery done (%sms)",
448
+ (performance.now() - discoverStart).toFixed(1),
449
+ );
284
450
  resolveDiscovery!();
285
451
  return;
286
452
  }
287
453
 
288
454
  try {
455
+ // Acquire build-time env bindings for dev prerender (Node.js path)
456
+ debugDiscovery?.("dev: node path start");
457
+ await timed(debugDiscovery, "acquireBuildEnv", () =>
458
+ acquireBuildEnv(s, viteCommand, viteMode),
459
+ );
460
+
289
461
  // Set the readiness gate BEFORE discovery so early requests
290
462
  // block until manifest is populated
291
- const serverMod = await rscEnv.runner.import(
292
- "@rangojs/router/server",
463
+ const serverMod = await timed(
464
+ debugDiscovery,
465
+ "import @rangojs/router/server",
466
+ () => rscEnv.runner.import("@rangojs/router/server"),
293
467
  );
294
468
  if (serverMod?.setManifestReadyPromise) {
295
469
  serverMod.setManifestReadyPromise(discoveryPromise);
296
470
  }
297
471
 
298
- await discoverRouters(s, rscEnv);
472
+ await timed(debugDiscovery, "discoverRouters", () =>
473
+ discoverRouters(s, rscEnv),
474
+ );
299
475
 
300
476
  // Store server origin for dev prerender endpoint (virtual module injection)
301
477
  s.devServerOrigin = getDevServerOrigin();
@@ -305,15 +481,23 @@ export function createRouterDiscoveryPlugin(
305
481
  // routes (e.g. Array.from loops) that the static parser cannot see.
306
482
  // writeRouteTypesFiles() only writes when content changes, so this
307
483
  // won't cause unnecessary HMR triggers.
308
- writeRouteTypesFiles(s);
484
+ timed(debugDiscovery, "writeRouteTypesFiles", () =>
485
+ writeRouteTypesFiles(s),
486
+ );
309
487
 
310
488
  // Populate the route map and per-router data in the RSC env
311
- await propagateDiscoveryState(rscEnv);
489
+ await timed(debugDiscovery, "propagateDiscoveryState", () =>
490
+ propagateDiscoveryState(rscEnv),
491
+ );
312
492
  } catch (err: any) {
313
493
  console.warn(
314
494
  `[rsc-router] Router discovery failed: ${err.message}\n${err.stack}`,
315
495
  );
316
496
  } finally {
497
+ debugDiscovery?.(
498
+ "dev discovery done (%sms)",
499
+ (performance.now() - discoverStart).toFixed(1),
500
+ );
317
501
  resolveDiscovery!();
318
502
  }
319
503
  };
@@ -390,9 +574,31 @@ export function createRouterDiscoveryPlugin(
390
574
  return;
391
575
  }
392
576
 
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;
577
+ // Import the user's entry module to force re-evaluation of any
578
+ // HMR-invalidated modules in the chain (entry router urls handlers).
579
+ // This ensures createRouter() re-runs with updated handler code before
580
+ // we read RouterRegistry. Without this, edits to prerender handler files
581
+ // produce stale content because the old router instance remains registered.
582
+ const rscEnv = (server.environments as any)?.rsc;
583
+ let registry: Map<string, any> | null = null;
584
+ if (rscEnv?.runner && s.resolvedEntryPath) {
585
+ try {
586
+ await rscEnv.runner.import(s.resolvedEntryPath);
587
+ const serverMod = await rscEnv.runner.import(
588
+ "@rangojs/router/server",
589
+ );
590
+ registry = serverMod.RouterRegistry ?? null;
591
+ } catch (err: any) {
592
+ console.warn(
593
+ `[rsc-router] Dev prerender module refresh failed: ${err.message}`,
594
+ );
595
+ res.statusCode = 500;
596
+ res.end(`Prerender handler error: ${err.message}`);
597
+ return;
598
+ }
599
+ } else {
600
+ registry = mainRegistry;
601
+ }
396
602
 
397
603
  if (!registry) {
398
604
  // No main registry: the RSC env has no module runner (Cloudflare dev).
@@ -421,6 +627,8 @@ export function createRouterDiscoveryPlugin(
421
627
  {},
422
628
  undefined,
423
629
  wantPassthrough,
630
+ s.resolvedBuildEnv,
631
+ true, // devMode: check getParams for passthrough routes
424
632
  );
425
633
  if (!result) continue;
426
634
  if (result.passthrough) continue;
@@ -499,15 +707,26 @@ export function createRouterDiscoveryPlugin(
499
707
  const rscEnv = (server.environments as any)?.rsc;
500
708
  if (!rscEnv?.runner || runtimeRediscoveryInProgress) return;
501
709
  runtimeRediscoveryInProgress = true;
710
+ const hmrStart = performance.now();
502
711
  try {
503
- await discoverRouters(s, rscEnv);
504
- writeRouteTypesFiles(s);
505
- await propagateDiscoveryState(rscEnv);
712
+ await timed(debugDiscovery, "hmr discoverRouters", () =>
713
+ discoverRouters(s, rscEnv),
714
+ );
715
+ timed(debugDiscovery, "hmr writeRouteTypesFiles", () =>
716
+ writeRouteTypesFiles(s),
717
+ );
718
+ await timed(debugDiscovery, "hmr propagateDiscoveryState", () =>
719
+ propagateDiscoveryState(rscEnv),
720
+ );
506
721
  } catch (err: any) {
507
722
  console.warn(
508
723
  `[rsc-router] Runtime re-discovery failed: ${err.message}`,
509
724
  );
510
725
  } finally {
726
+ debugDiscovery?.(
727
+ "hmr re-discovery done (%sms)",
728
+ (performance.now() - hmrStart).toFixed(1),
729
+ );
511
730
  runtimeRediscoveryInProgress = false;
512
731
  }
513
732
  };
@@ -605,10 +824,17 @@ export function createRouterDiscoveryPlugin(
605
824
  if (!s.isBuildMode) return;
606
825
  // Only run once across environment builds
607
826
  if (s.mergedRouteManifest !== null) return;
827
+ const buildStartTime = performance.now();
828
+ debugDiscovery?.("build: start");
608
829
  resetStagedBuildAssets(s.projectRoot);
609
830
  s.prerenderManifestEntries = null;
610
831
  s.staticManifestEntries = null;
611
832
 
833
+ // Acquire build-time env bindings if configured
834
+ await timed(debugDiscovery, "build acquireBuildEnv", () =>
835
+ acquireBuildEnv(s, viteCommand, viteMode),
836
+ );
837
+
612
838
  let tempServer: any = null;
613
839
  // Signal to user-space code (e.g. reverse.ts) that build-time discovery
614
840
  // is active. Uses globalThis because the temp server's module runner
@@ -616,7 +842,11 @@ export function createRouterDiscoveryPlugin(
616
842
  // between the vite plugin and user code loaded via runner.import().
617
843
  (globalThis as any).__rscRouterDiscoveryActive = true;
618
844
  try {
619
- tempServer = await createTempRscServer(s, { forceBuild: true });
845
+ tempServer = await timed(
846
+ debugDiscovery,
847
+ "build createTempRscServer",
848
+ () => createTempRscServer(s, { forceBuild: true }),
849
+ );
620
850
 
621
851
  const rscEnv = (tempServer.environments as any)?.rsc;
622
852
  if (!rscEnv?.runner) {
@@ -636,11 +866,15 @@ export function createRouterDiscoveryPlugin(
636
866
  s.resolvedStaticModules = tempIdsPlugin.api.staticHandlerModules;
637
867
  }
638
868
 
639
- await discoverRouters(s, rscEnv);
869
+ await timed(debugDiscovery, "build discoverRouters", () =>
870
+ discoverRouters(s, rscEnv),
871
+ );
640
872
  // Update named-routes.gen.ts from runtime discovery.
641
873
  // The runtime manifest includes dynamically generated routes
642
874
  // that the static parser cannot extract from source code.
643
- writeRouteTypesFiles(s);
875
+ timed(debugDiscovery, "build writeRouteTypesFiles", () =>
876
+ writeRouteTypesFiles(s),
877
+ );
644
878
  } catch (err: any) {
645
879
  // Extract the user source file from the stack trace (skip internal frames)
646
880
  const sourceFile = err.stack
@@ -665,8 +899,15 @@ export function createRouterDiscoveryPlugin(
665
899
  } finally {
666
900
  delete (globalThis as any).__rscRouterDiscoveryActive;
667
901
  if (tempServer) {
668
- await tempServer.close();
902
+ await timed(debugDiscovery, "build tempServer.close", () =>
903
+ tempServer.close(),
904
+ );
669
905
  }
906
+ await releaseBuildEnv(s);
907
+ debugDiscovery?.(
908
+ "build discovery done (%sms)",
909
+ (performance.now() - buildStartTime).toFixed(1),
910
+ );
670
911
  }
671
912
  },
672
913
 
@@ -690,19 +931,34 @@ export function createRouterDiscoveryPlugin(
690
931
  // This is critical for Cloudflare dev where the worker runs in a separate
691
932
  // Miniflare process and can only receive manifest data via the virtual module.
692
933
  if (s.discoveryDone) {
693
- await s.discoveryDone;
934
+ await timed(debugRoutes, "await discoveryDone (manifest)", () =>
935
+ Promise.resolve(s.discoveryDone),
936
+ );
694
937
  }
695
- return generateRoutesManifestModule(s);
938
+ const code = await timed(
939
+ debugRoutes,
940
+ "generateRoutesManifestModule",
941
+ () => generateRoutesManifestModule(s),
942
+ );
943
+ debugRoutes?.("manifest module emitted (%d bytes)", code?.length ?? 0);
944
+ return code;
696
945
  }
697
946
  // Per-router virtual modules: pure data exports (no side effects).
698
947
  // ensureRouterManifest() imports the module and stores the data.
699
948
  const perRouterPrefix = "\0" + VIRTUAL_ROUTES_MANIFEST_ID + "/";
700
949
  if (id.startsWith(perRouterPrefix)) {
701
950
  if (s.discoveryDone) {
702
- await s.discoveryDone;
951
+ await timed(debugRoutes, "await discoveryDone (per-router)", () =>
952
+ Promise.resolve(s.discoveryDone),
953
+ );
703
954
  }
704
955
  const routerId = id.slice(perRouterPrefix.length);
705
- return generatePerRouterModule(s, routerId);
956
+ const code = await timed(
957
+ debugRoutes,
958
+ `generatePerRouterModule ${routerId}`,
959
+ () => generatePerRouterModule(s, routerId),
960
+ );
961
+ return code;
706
962
  }
707
963
  // virtual:rsc-router/prerender-paths load handler removed
708
964
  return null;
@@ -727,33 +983,40 @@ export function createRouterDiscoveryPlugin(
727
983
  if (!s.resolvedPrerenderModules?.size && !s.resolvedStaticModules?.size)
728
984
  return;
729
985
 
986
+ // Clear maps at the start of each RSC generateBundle pass.
987
+ // Vite 6 multi-environment builds run RSC twice (analysis + production);
988
+ // clearing prevents stale/duplicate records from the analysis pass.
989
+ s.handlerChunkInfoMap.clear();
990
+ s.staticHandlerChunkInfoMap.clear();
991
+
730
992
  for (const [fileName, chunk] of Object.entries(bundle) as [
731
993
  string,
732
994
  any,
733
995
  ][]) {
734
996
  if (chunk.type !== "chunk") continue;
735
997
 
736
- // Prerender handlers chunk
737
- if (
738
- fileName.includes("__prerender-handlers") &&
739
- s.resolvedPrerenderModules?.size
740
- ) {
998
+ // Scan all chunks for handler exports (handlers may land in any chunk)
999
+ if (s.resolvedPrerenderModules?.size) {
741
1000
  const handlers = extractHandlerExportsFromChunk(
742
1001
  chunk.code,
743
1002
  s.resolvedPrerenderModules,
744
1003
  "Prerender",
745
- true,
1004
+ false,
746
1005
  );
747
1006
  if (handlers.length > 0) {
748
- s.handlerChunkInfo = { fileName, exports: handlers };
1007
+ const existing = s.handlerChunkInfoMap.get(fileName);
1008
+ if (existing) {
1009
+ existing.exports.push(...handlers);
1010
+ } else {
1011
+ s.handlerChunkInfoMap.set(fileName, {
1012
+ fileName,
1013
+ exports: handlers,
1014
+ });
1015
+ }
749
1016
  }
750
1017
  }
751
1018
 
752
- // Static handlers chunk
753
- if (
754
- fileName.includes("__static-handlers") &&
755
- s.resolvedStaticModules?.size
756
- ) {
1019
+ if (s.resolvedStaticModules?.size) {
757
1020
  const handlers = extractHandlerExportsFromChunk(
758
1021
  chunk.code,
759
1022
  s.resolvedStaticModules,
@@ -761,7 +1024,15 @@ export function createRouterDiscoveryPlugin(
761
1024
  false,
762
1025
  );
763
1026
  if (handlers.length > 0) {
764
- s.staticHandlerChunkInfo = { fileName, exports: handlers };
1027
+ const existing = s.staticHandlerChunkInfoMap.get(fileName);
1028
+ if (existing) {
1029
+ existing.exports.push(...handlers);
1030
+ } else {
1031
+ s.staticHandlerChunkInfoMap.set(fileName, {
1032
+ fileName,
1033
+ exports: handlers,
1034
+ });
1035
+ }
765
1036
  }
766
1037
  }
767
1038
  }
@@ -1,4 +1,4 @@
1
- import packageJson from "../../../package.json" with { type: "json" };
1
+ import packageJson from "../../../package.json";
2
2
 
3
3
  export const rangoVersion: string = packageJson.version;
4
4
 
@@ -23,11 +23,11 @@ ${dim} ╱${reset} ${bold}╔═╗${reset}${dim} * ╱
23
23
  ${dim} ${reset}${bold}║ ║${reset} ${bold}╔═╗${reset}${dim} * ✧. ╱${reset}
24
24
  ${dim} ${reset}${bold}╔╗ ║ ║ ║ ║${reset}${dim} * ╱${reset}
25
25
  ${dim} ${reset}${bold}║║ ║ ║ ║ ║ ╦═╗╔═╗╔╗╔╔═╗╔═╗${reset}${dim} ✧ ✦${reset}
26
- ${dim} ${reset}${bold}═╣║ ║ ╠═╝ ║ ╠╦╝╠═╣║║║║ ╦║ ║${reset}${dim} * ✧${reset}
26
+ ${dim} ${reset}${bold}║║ ║ ╠═╝ ║ ╠╦╝╠═╣║║║║ ╦║ ║${reset}${dim} * ✧${reset}
27
27
  ${dim} ${reset}${bold}║╚═╝ ╔═══╝ ╩╚═╩ ╩╝╚╝╚═╝╚═╝${reset}${dim} ✦ . *${reset}
28
28
  ${dim} ${reset}${bold}╚══╗ ║${reset}${dim} * RSC Wrangler ✧ ✦${reset}
29
- ${dim} * ${reset}${bold}║ ╠═${reset}${dim} * ✧. ╱${reset}
30
- ${bold}══════╝ ╚═════════╩═══${reset}${dim} ✦ *${reset}
29
+ ${dim} * ${reset}${bold}║ ║${reset}${dim} * ✧. ╱${reset}
30
+ ${dim} ${reset}${bold}═══╝ ╚════${reset}${dim} ✦ *${reset}
31
31
 
32
32
  v${version} · ${preset} · ${mode}
33
33
  `;