@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c

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 (189) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +172 -50
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1160 -508
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +17 -16
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/cache-guide/SKILL.md +32 -0
  10. package/skills/caching/SKILL.md +49 -8
  11. package/skills/document-cache/SKILL.md +2 -2
  12. package/skills/handler-use/SKILL.md +362 -0
  13. package/skills/hooks/SKILL.md +61 -51
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +20 -0
  16. package/skills/layout/SKILL.md +22 -0
  17. package/skills/links/SKILL.md +91 -17
  18. package/skills/loader/SKILL.md +107 -24
  19. package/skills/middleware/SKILL.md +34 -3
  20. package/skills/migrate-nextjs/SKILL.md +560 -0
  21. package/skills/migrate-react-router/SKILL.md +765 -0
  22. package/skills/parallel/SKILL.md +185 -0
  23. package/skills/prerender/SKILL.md +112 -70
  24. package/skills/rango/SKILL.md +24 -23
  25. package/skills/response-routes/SKILL.md +8 -0
  26. package/skills/route/SKILL.md +58 -4
  27. package/skills/router-setup/SKILL.md +95 -5
  28. package/skills/streams-and-websockets/SKILL.md +283 -0
  29. package/skills/typesafety/SKILL.md +38 -24
  30. package/src/__internal.ts +92 -0
  31. package/src/browser/app-shell.ts +52 -0
  32. package/src/browser/app-version.ts +14 -0
  33. package/src/browser/event-controller.ts +5 -0
  34. package/src/browser/link-interceptor.ts +4 -0
  35. package/src/browser/navigation-bridge.ts +175 -17
  36. package/src/browser/navigation-client.ts +177 -44
  37. package/src/browser/navigation-store.ts +68 -9
  38. package/src/browser/navigation-transaction.ts +11 -9
  39. package/src/browser/partial-update.ts +113 -17
  40. package/src/browser/prefetch/cache.ts +275 -28
  41. package/src/browser/prefetch/fetch.ts +191 -46
  42. package/src/browser/prefetch/policy.ts +6 -0
  43. package/src/browser/prefetch/queue.ts +123 -20
  44. package/src/browser/prefetch/resource-ready.ts +77 -0
  45. package/src/browser/rango-state.ts +53 -13
  46. package/src/browser/react/Link.tsx +98 -14
  47. package/src/browser/react/NavigationProvider.tsx +89 -14
  48. package/src/browser/react/context.ts +7 -2
  49. package/src/browser/react/use-handle.ts +9 -58
  50. package/src/browser/react/use-navigation.ts +22 -2
  51. package/src/browser/react/use-params.ts +11 -1
  52. package/src/browser/react/use-router.ts +29 -9
  53. package/src/browser/rsc-router.tsx +177 -66
  54. package/src/browser/scroll-restoration.ts +41 -42
  55. package/src/browser/segment-reconciler.ts +36 -9
  56. package/src/browser/server-action-bridge.ts +8 -6
  57. package/src/browser/types.ts +73 -5
  58. package/src/build/generate-manifest.ts +6 -6
  59. package/src/build/generate-route-types.ts +3 -0
  60. package/src/build/route-trie.ts +67 -25
  61. package/src/build/route-types/include-resolution.ts +8 -1
  62. package/src/build/route-types/router-processing.ts +223 -74
  63. package/src/build/route-types/scan-filter.ts +8 -1
  64. package/src/cache/cache-runtime.ts +15 -11
  65. package/src/cache/cache-scope.ts +48 -7
  66. package/src/cache/cf/cf-cache-store.ts +455 -15
  67. package/src/cache/cf/index.ts +5 -1
  68. package/src/cache/document-cache.ts +17 -7
  69. package/src/cache/index.ts +1 -0
  70. package/src/cache/taint.ts +55 -0
  71. package/src/client.rsc.tsx +2 -1
  72. package/src/client.tsx +85 -276
  73. package/src/context-var.ts +72 -2
  74. package/src/debug.ts +2 -2
  75. package/src/handle.ts +40 -0
  76. package/src/handles/breadcrumbs.ts +66 -0
  77. package/src/handles/index.ts +1 -0
  78. package/src/host/index.ts +0 -3
  79. package/src/index.rsc.ts +9 -36
  80. package/src/index.ts +79 -70
  81. package/src/outlet-context.ts +1 -1
  82. package/src/prerender/store.ts +57 -15
  83. package/src/prerender.ts +138 -77
  84. package/src/response-utils.ts +28 -0
  85. package/src/reverse.ts +27 -2
  86. package/src/route-definition/dsl-helpers.ts +240 -40
  87. package/src/route-definition/helpers-types.ts +67 -19
  88. package/src/route-definition/index.ts +3 -3
  89. package/src/route-definition/redirect.ts +11 -3
  90. package/src/route-definition/resolve-handler-use.ts +155 -0
  91. package/src/route-map-builder.ts +7 -1
  92. package/src/route-types.ts +18 -0
  93. package/src/router/content-negotiation.ts +100 -1
  94. package/src/router/find-match.ts +4 -2
  95. package/src/router/handler-context.ts +129 -26
  96. package/src/router/intercept-resolution.ts +11 -4
  97. package/src/router/lazy-includes.ts +10 -7
  98. package/src/router/loader-resolution.ts +160 -22
  99. package/src/router/logging.ts +5 -2
  100. package/src/router/manifest.ts +31 -16
  101. package/src/router/match-api.ts +128 -193
  102. package/src/router/match-middleware/background-revalidation.ts +30 -2
  103. package/src/router/match-middleware/cache-lookup.ts +94 -17
  104. package/src/router/match-middleware/cache-store.ts +53 -10
  105. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  106. package/src/router/match-middleware/segment-resolution.ts +61 -5
  107. package/src/router/match-result.ts +103 -18
  108. package/src/router/metrics.ts +238 -13
  109. package/src/router/middleware-types.ts +48 -27
  110. package/src/router/middleware.ts +201 -86
  111. package/src/router/navigation-snapshot.ts +182 -0
  112. package/src/router/pattern-matching.ts +77 -11
  113. package/src/router/prerender-match.ts +114 -10
  114. package/src/router/preview-match.ts +30 -102
  115. package/src/router/request-classification.ts +310 -0
  116. package/src/router/revalidation.ts +27 -7
  117. package/src/router/route-snapshot.ts +245 -0
  118. package/src/router/router-context.ts +6 -1
  119. package/src/router/router-interfaces.ts +50 -5
  120. package/src/router/router-options.ts +50 -19
  121. package/src/router/segment-resolution/fresh.ts +215 -19
  122. package/src/router/segment-resolution/helpers.ts +30 -25
  123. package/src/router/segment-resolution/loader-cache.ts +1 -0
  124. package/src/router/segment-resolution/revalidation.ts +454 -301
  125. package/src/router/segment-wrappers.ts +2 -0
  126. package/src/router/trie-matching.ts +30 -6
  127. package/src/router/types.ts +1 -0
  128. package/src/router/url-params.ts +49 -0
  129. package/src/router.ts +89 -17
  130. package/src/rsc/handler.ts +563 -364
  131. package/src/rsc/helpers.ts +69 -41
  132. package/src/rsc/index.ts +0 -20
  133. package/src/rsc/loader-fetch.ts +23 -3
  134. package/src/rsc/manifest-init.ts +5 -1
  135. package/src/rsc/progressive-enhancement.ts +37 -10
  136. package/src/rsc/response-route-handler.ts +14 -1
  137. package/src/rsc/rsc-rendering.ts +47 -44
  138. package/src/rsc/server-action.ts +24 -10
  139. package/src/rsc/ssr-setup.ts +128 -0
  140. package/src/rsc/types.ts +11 -1
  141. package/src/search-params.ts +16 -13
  142. package/src/segment-content-promise.ts +67 -0
  143. package/src/segment-loader-promise.ts +122 -0
  144. package/src/segment-system.tsx +109 -23
  145. package/src/server/context.ts +174 -19
  146. package/src/server/handle-store.ts +19 -0
  147. package/src/server/loader-registry.ts +9 -8
  148. package/src/server/request-context.ts +218 -65
  149. package/src/server.ts +6 -0
  150. package/src/ssr/index.tsx +4 -0
  151. package/src/static-handler.ts +18 -6
  152. package/src/theme/index.ts +4 -13
  153. package/src/types/cache-types.ts +4 -4
  154. package/src/types/handler-context.ts +140 -72
  155. package/src/types/loader-types.ts +41 -15
  156. package/src/types/request-scope.ts +126 -0
  157. package/src/types/route-config.ts +17 -8
  158. package/src/types/route-entry.ts +19 -1
  159. package/src/types/segments.ts +2 -5
  160. package/src/urls/include-helper.ts +24 -14
  161. package/src/urls/path-helper-types.ts +39 -6
  162. package/src/urls/path-helper.ts +48 -13
  163. package/src/urls/pattern-types.ts +12 -0
  164. package/src/urls/response-types.ts +18 -16
  165. package/src/use-loader.tsx +77 -5
  166. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  167. package/src/vite/discovery/discover-routers.ts +7 -4
  168. package/src/vite/discovery/prerender-collection.ts +162 -88
  169. package/src/vite/discovery/state.ts +17 -13
  170. package/src/vite/index.ts +8 -3
  171. package/src/vite/plugin-types.ts +51 -79
  172. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  173. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  174. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  175. package/src/vite/plugins/expose-action-id.ts +1 -3
  176. package/src/vite/plugins/expose-id-utils.ts +12 -0
  177. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  178. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  179. package/src/vite/plugins/performance-tracks.ts +88 -0
  180. package/src/vite/plugins/refresh-cmd.ts +127 -0
  181. package/src/vite/plugins/version-plugin.ts +13 -1
  182. package/src/vite/rango.ts +190 -217
  183. package/src/vite/router-discovery.ts +241 -45
  184. package/src/vite/utils/banner.ts +4 -4
  185. package/src/vite/utils/package-resolution.ts +34 -1
  186. package/src/vite/utils/prerender-utils.ts +97 -5
  187. package/src/vite/utils/shared-utils.ts +3 -2
  188. package/skills/testing/SKILL.md +0 -226
  189. package/src/route-definition/route-function.ts +0 -119
@@ -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,
@@ -42,9 +47,53 @@ import {
42
47
  generatePerRouterModule,
43
48
  } from "./discovery/virtual-module-codegen.js";
44
49
  import { postprocessBundle } from "./discovery/bundle-postprocess.js";
50
+ import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
45
51
 
46
52
  export { VIRTUAL_ROUTES_MANIFEST_ID };
47
53
 
54
+ // ============================================================================
55
+ // Node ESM Loader Hook Registration
56
+ // ============================================================================
57
+
58
+ /**
59
+ * Registers a Node ESM loader hook that resolves `cloudflare:*` specifiers
60
+ * to a data: URL stub. Defense-in-depth alongside the Vite transform in
61
+ * `cloudflare-protocol-stub.ts`:
62
+ *
63
+ * - The Vite transform catches `cloudflare:*` imports in modules that flow
64
+ * through Vite's plugin pipeline. That's the vast majority of cases.
65
+ * - The Node loader catches imports in modules that Vite/Rollup externalize
66
+ * (e.g. the `partyserver` package, which has a top-level
67
+ * `import { DurableObject, env } from "cloudflare:workers"` and ships
68
+ * shapes plugin-rsc marks as external). Externalized modules are loaded
69
+ * via Node's native ESM loader, which rejects URL schemes.
70
+ *
71
+ * Registration is process-global and one-shot. The hook only intercepts
72
+ * `cloudflare:*` specifiers; everything else passes through via
73
+ * `nextResolve()`. It runs in a separate worker thread (Node ESM loader
74
+ * architecture), so it can't read the `globalThis[BUILD_ENV_GLOBAL_KEY]`
75
+ * bridge that the Vite transform uses — the stubs served here always
76
+ * return `env = {}`. That's fine because externalized libraries don't
77
+ * typically access `env` at module top level; user source (where real
78
+ * `env` matters at build time) flows through the Vite transform.
79
+ */
80
+ let loaderHookRegistered = false;
81
+ function ensureCloudflareProtocolLoaderRegistered(): void {
82
+ if (loaderHookRegistered) return;
83
+ loaderHookRegistered = true;
84
+ try {
85
+ register(
86
+ new URL("./plugins/cloudflare-protocol-loader-hook.mjs", import.meta.url),
87
+ );
88
+ } catch (err: any) {
89
+ // register() requires Node 18.19+ / 20.6+. Older Node still has the
90
+ // Vite transform as primary defense.
91
+ console.warn(
92
+ `[rsc-router] Could not register Node ESM loader hook for cloudflare:* imports (${err?.message ?? err}). Falling back to Vite transform only.`,
93
+ );
94
+ }
95
+ }
96
+
48
97
  // ============================================================================
49
98
  // Temp Server Factory
50
99
  // ============================================================================
@@ -64,6 +113,11 @@ async function createTempRscServer(
64
113
  state: DiscoveryState,
65
114
  options: { forceBuild?: boolean; cacheDir?: string } = {},
66
115
  ) {
116
+ // Install the Node ESM loader hook before any module evaluation so
117
+ // `cloudflare:*` specifiers in externalized/loader-delegated modules
118
+ // (e.g. packages plugin-rsc marks as external) resolve to stubs
119
+ // instead of crashing Node's native loader.
120
+ ensureCloudflareProtocolLoaderRegistered();
67
121
  const { default: rsc } = await import("@vitejs/plugin-rsc");
68
122
  return createViteServer({
69
123
  root: state.projectRoot,
@@ -86,6 +140,7 @@ async function createTempRscServer(
86
140
  ...(options.forceBuild ? [hashClientRefs(state.projectRoot)] : []),
87
141
  createVersionPlugin(),
88
142
  createVirtualStubPlugin(),
143
+ createCloudflareProtocolStubPlugin(),
89
144
  // Dev prerender must use dev-mode IDs (path-based) to match the workerd
90
145
  // runtime. forceBuild produces hashed IDs for production bundle consistency.
91
146
  exposeInternalIds(options.forceBuild ? { forceBuild: true } : undefined),
@@ -94,6 +149,111 @@ async function createTempRscServer(
94
149
  });
95
150
  }
96
151
 
152
+ // ============================================================================
153
+ // Build-Time Env Resolution
154
+ // ============================================================================
155
+
156
+ import type {
157
+ BuildEnvOption,
158
+ BuildEnvFactoryContext,
159
+ BuildEnvResult,
160
+ } from "./plugin-types.js";
161
+
162
+ /**
163
+ * Resolve the buildEnv option into a concrete { env, dispose? } result.
164
+ * Handles all four input shapes: false, "auto", factory, plain object.
165
+ */
166
+ async function resolveBuildEnv(
167
+ option: BuildEnvOption | undefined,
168
+ factoryCtx: BuildEnvFactoryContext,
169
+ ): Promise<BuildEnvResult | null> {
170
+ if (!option) return null;
171
+
172
+ if (option === "auto") {
173
+ if (factoryCtx.preset !== "cloudflare") {
174
+ throw new Error(
175
+ '[rsc-router] buildEnv: "auto" is only supported with preset: "cloudflare". ' +
176
+ "Use a factory function or plain object for other presets.",
177
+ );
178
+ }
179
+ try {
180
+ // Resolve wrangler from the user's project root (not the router package)
181
+ const userRequire = createRequire(
182
+ resolve(factoryCtx.root, "package.json"),
183
+ );
184
+ const wranglerPath = userRequire.resolve("wrangler");
185
+ const { getPlatformProxy } = (await import(
186
+ pathToFileURL(wranglerPath).href
187
+ )) as {
188
+ getPlatformProxy: (opts?: any) => Promise<any>;
189
+ };
190
+ const proxy = await getPlatformProxy();
191
+ return {
192
+ env: proxy.env as Record<string, unknown>,
193
+ dispose: proxy.dispose,
194
+ };
195
+ } catch (err: any) {
196
+ throw new Error(
197
+ '[rsc-router] buildEnv: "auto" requires wrangler to be installed.\n' +
198
+ `Install it with: pnpm add -D wrangler\n${err.message}`,
199
+ );
200
+ }
201
+ }
202
+
203
+ if (typeof option === "function") {
204
+ return await option(factoryCtx);
205
+ }
206
+
207
+ // Plain object
208
+ return { env: option };
209
+ }
210
+
211
+ /**
212
+ * Acquire build-time env bindings and store on discovery state.
213
+ * Returns true if env was acquired, false if buildEnv is disabled.
214
+ */
215
+ async function acquireBuildEnv(
216
+ s: DiscoveryState,
217
+ command: "serve" | "build",
218
+ mode: string,
219
+ ): Promise<boolean> {
220
+ const option = s.opts?.buildEnv;
221
+ if (!option) return false;
222
+
223
+ const result = await resolveBuildEnv(option, {
224
+ root: s.projectRoot,
225
+ mode,
226
+ command,
227
+ preset: s.opts?.preset ?? "node",
228
+ });
229
+ if (!result) return false;
230
+
231
+ s.resolvedBuildEnv = result.env;
232
+ s.buildEnvDispose = result.dispose ?? null;
233
+ // Bridge the resolved env into `cloudflare:workers`'s stubbed `env`
234
+ // export so user code that does `import { env } from "cloudflare:workers"`
235
+ // sees the real bindings proxy during discovery + prerender instead of
236
+ // an empty object. The stub reads this global at module-evaluation time.
237
+ (globalThis as Record<string, unknown>)[BUILD_ENV_GLOBAL_KEY] = result.env;
238
+ return true;
239
+ }
240
+
241
+ /**
242
+ * Release build-time env resources and clear state.
243
+ */
244
+ async function releaseBuildEnv(s: DiscoveryState): Promise<void> {
245
+ if (s.buildEnvDispose) {
246
+ try {
247
+ await s.buildEnvDispose();
248
+ } catch (err: any) {
249
+ console.warn(`[rsc-router] buildEnv dispose failed: ${err.message}`);
250
+ }
251
+ s.buildEnvDispose = null;
252
+ }
253
+ s.resolvedBuildEnv = undefined;
254
+ delete (globalThis as Record<string, unknown>)[BUILD_ENV_GLOBAL_KEY];
255
+ }
256
+
97
257
  /**
98
258
  * Plugin that discovers router instances at dev/build time via the RSC environment.
99
259
  *
@@ -111,6 +271,8 @@ export function createRouterDiscoveryPlugin(
111
271
  opts?: PluginOptions,
112
272
  ): Plugin {
113
273
  const s = createDiscoveryState(entryPath, opts);
274
+ let viteCommand: "serve" | "build" = "build";
275
+ let viteMode = "production";
114
276
 
115
277
  return {
116
278
  name: "@rangojs/router:discovery",
@@ -121,32 +283,20 @@ export function createRouterDiscoveryPlugin(
121
283
  __RANGO_DEBUG__: JSON.stringify(!!process.env.INTERNAL_RANGO_DEBUG),
122
284
  },
123
285
  };
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
- }
286
+ // Prerender/static handler modules are bundled naturally with the
287
+ // rest of the RSC entry. A previous design forced them into dedicated
288
+ // __prerender-handlers / __static-handlers chunks via manualChunks,
289
+ // but Rollup hoisted all shared dependencies into those chunks,
290
+ // inflating them to ~1 MB with active runtime code. Handler code is
291
+ // evicted in closeBundle regardless of which chunk it lands in.
144
292
  return config;
145
293
  },
146
294
 
147
295
  configResolved(config) {
148
296
  s.projectRoot = config.root;
149
297
  s.isBuildMode = config.command === "build";
298
+ viteCommand = config.command as "serve" | "build";
299
+ viteMode = config.mode;
150
300
  // Capture user's resolve aliases for the temp server
151
301
  s.userResolveAlias = config.resolve.alias;
152
302
  // Node preset: pick up auto-discovered router path from the config() hook.
@@ -167,13 +317,6 @@ export function createRouterDiscoveryPlugin(
167
317
  s.resolvedEntryPath = entries[0];
168
318
  }
169
319
  }
170
- // Compile include/exclude patterns into a scan filter
171
- if (opts?.include || opts?.exclude) {
172
- s.scanFilter = createScanFilter(s.projectRoot, {
173
- include: opts.include,
174
- exclude: opts.exclude,
175
- });
176
- }
177
320
  // Generate combined named-routes.gen.ts from static source parsing.
178
321
  // Runs before the dev server starts so the gen file exists immediately for IDE.
179
322
  // In build mode, the runtime discovery in buildStart produces the definitive
@@ -224,12 +367,13 @@ export function createRouterDiscoveryPlugin(
224
367
  let prerenderTempServer: any = null;
225
368
  let prerenderNodeRegistry: Map<string, any> | null = null;
226
369
 
227
- // Clean up the temporary server when the dev server shuts down
370
+ // Clean up the temporary server and build env when the dev server shuts down
228
371
  server.httpServer?.on("close", () => {
229
372
  if (prerenderTempServer) {
230
373
  prerenderTempServer.close().catch(() => {});
231
374
  prerenderTempServer = null;
232
375
  }
376
+ releaseBuildEnv(s).catch(() => {});
233
377
  });
234
378
 
235
379
  async function getOrCreateTempServer(): Promise<any | null> {
@@ -269,6 +413,9 @@ export function createRouterDiscoveryPlugin(
269
413
  // Create a temp Node.js server to run runtime discovery and generate
270
414
  // named route types (static parser can't resolve factory calls).
271
415
  try {
416
+ // Acquire build-time env bindings for dev prerender
417
+ await acquireBuildEnv(s, viteCommand, viteMode);
418
+
272
419
  const tempRscEnv = await getOrCreateTempServer();
273
420
  if (tempRscEnv) {
274
421
  await discoverRouters(s, tempRscEnv);
@@ -285,6 +432,9 @@ export function createRouterDiscoveryPlugin(
285
432
  }
286
433
 
287
434
  try {
435
+ // Acquire build-time env bindings for dev prerender (Node.js path)
436
+ await acquireBuildEnv(s, viteCommand, viteMode);
437
+
288
438
  // Set the readiness gate BEFORE discovery so early requests
289
439
  // block until manifest is populated
290
440
  const serverMod = await rscEnv.runner.import(
@@ -389,9 +539,31 @@ export function createRouterDiscoveryPlugin(
389
539
  return;
390
540
  }
391
541
 
392
- // Prefer the main server's registry (Node.js preset: module runner available).
393
- // Fall back to a temp server for Cloudflare where the main RSC env uses workerd.
394
- let registry = mainRegistry;
542
+ // Import the user's entry module to force re-evaluation of any
543
+ // HMR-invalidated modules in the chain (entry router urls handlers).
544
+ // This ensures createRouter() re-runs with updated handler code before
545
+ // we read RouterRegistry. Without this, edits to prerender handler files
546
+ // produce stale content because the old router instance remains registered.
547
+ const rscEnv = (server.environments as any)?.rsc;
548
+ let registry: Map<string, any> | null = null;
549
+ if (rscEnv?.runner && s.resolvedEntryPath) {
550
+ try {
551
+ await rscEnv.runner.import(s.resolvedEntryPath);
552
+ const serverMod = await rscEnv.runner.import(
553
+ "@rangojs/router/server",
554
+ );
555
+ registry = serverMod.RouterRegistry ?? null;
556
+ } catch (err: any) {
557
+ console.warn(
558
+ `[rsc-router] Dev prerender module refresh failed: ${err.message}`,
559
+ );
560
+ res.statusCode = 500;
561
+ res.end(`Prerender handler error: ${err.message}`);
562
+ return;
563
+ }
564
+ } else {
565
+ registry = mainRegistry;
566
+ }
395
567
 
396
568
  if (!registry) {
397
569
  // No main registry: the RSC env has no module runner (Cloudflare dev).
@@ -420,6 +592,8 @@ export function createRouterDiscoveryPlugin(
420
592
  {},
421
593
  undefined,
422
594
  wantPassthrough,
595
+ s.resolvedBuildEnv,
596
+ true, // devMode: check getParams for passthrough routes
423
597
  );
424
598
  if (!result) continue;
425
599
  if (result.passthrough) continue;
@@ -604,6 +778,12 @@ export function createRouterDiscoveryPlugin(
604
778
  if (!s.isBuildMode) return;
605
779
  // Only run once across environment builds
606
780
  if (s.mergedRouteManifest !== null) return;
781
+ resetStagedBuildAssets(s.projectRoot);
782
+ s.prerenderManifestEntries = null;
783
+ s.staticManifestEntries = null;
784
+
785
+ // Acquire build-time env bindings if configured
786
+ await acquireBuildEnv(s, viteCommand, viteMode);
607
787
 
608
788
  let tempServer: any = null;
609
789
  // Signal to user-space code (e.g. reverse.ts) that build-time discovery
@@ -663,6 +843,7 @@ export function createRouterDiscoveryPlugin(
663
843
  if (tempServer) {
664
844
  await tempServer.close();
665
845
  }
846
+ await releaseBuildEnv(s);
666
847
  }
667
848
  },
668
849
 
@@ -723,33 +904,40 @@ export function createRouterDiscoveryPlugin(
723
904
  if (!s.resolvedPrerenderModules?.size && !s.resolvedStaticModules?.size)
724
905
  return;
725
906
 
907
+ // Clear maps at the start of each RSC generateBundle pass.
908
+ // Vite 6 multi-environment builds run RSC twice (analysis + production);
909
+ // clearing prevents stale/duplicate records from the analysis pass.
910
+ s.handlerChunkInfoMap.clear();
911
+ s.staticHandlerChunkInfoMap.clear();
912
+
726
913
  for (const [fileName, chunk] of Object.entries(bundle) as [
727
914
  string,
728
915
  any,
729
916
  ][]) {
730
917
  if (chunk.type !== "chunk") continue;
731
918
 
732
- // Prerender handlers chunk
733
- if (
734
- fileName.includes("__prerender-handlers") &&
735
- s.resolvedPrerenderModules?.size
736
- ) {
919
+ // Scan all chunks for handler exports (handlers may land in any chunk)
920
+ if (s.resolvedPrerenderModules?.size) {
737
921
  const handlers = extractHandlerExportsFromChunk(
738
922
  chunk.code,
739
923
  s.resolvedPrerenderModules,
740
924
  "Prerender",
741
- true,
925
+ false,
742
926
  );
743
927
  if (handlers.length > 0) {
744
- s.handlerChunkInfo = { fileName, exports: handlers };
928
+ const existing = s.handlerChunkInfoMap.get(fileName);
929
+ if (existing) {
930
+ existing.exports.push(...handlers);
931
+ } else {
932
+ s.handlerChunkInfoMap.set(fileName, {
933
+ fileName,
934
+ exports: handlers,
935
+ });
936
+ }
745
937
  }
746
938
  }
747
939
 
748
- // Static handlers chunk
749
- if (
750
- fileName.includes("__static-handlers") &&
751
- s.resolvedStaticModules?.size
752
- ) {
940
+ if (s.resolvedStaticModules?.size) {
753
941
  const handlers = extractHandlerExportsFromChunk(
754
942
  chunk.code,
755
943
  s.resolvedStaticModules,
@@ -757,7 +945,15 @@ export function createRouterDiscoveryPlugin(
757
945
  false,
758
946
  );
759
947
  if (handlers.length > 0) {
760
- s.staticHandlerChunkInfo = { fileName, exports: handlers };
948
+ const existing = s.staticHandlerChunkInfoMap.get(fileName);
949
+ if (existing) {
950
+ existing.exports.push(...handlers);
951
+ } else {
952
+ s.staticHandlerChunkInfoMap.set(fileName, {
953
+ fileName,
954
+ exports: handlers,
955
+ });
956
+ }
761
957
  }
762
958
  }
763
959
  }
@@ -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
  `;
@@ -6,8 +6,11 @@
6
6
  */
7
7
 
8
8
  import { existsSync } from "node:fs";
9
+ import { createRequire } from "node:module";
9
10
  import { resolve } from "node:path";
10
- import packageJson from "../../../package.json" with { type: "json" };
11
+ import packageJson from "../../../package.json";
12
+
13
+ const require = createRequire(import.meta.url);
11
14
 
12
15
  /**
13
16
  * The canonical name used in virtual entries (without scope)
@@ -119,3 +122,33 @@ export function getPackageAliases(): Record<string, string> {
119
122
 
120
123
  return aliases;
121
124
  }
125
+
126
+ /**
127
+ * Plugin-rsc pushes bare specs like
128
+ * `@vitejs/plugin-rsc/vendor/react-server-dom/client.edge` into
129
+ * `optimizeDeps.include` for the ssr and rsc environments. In strict pnpm
130
+ * consumer apps, `@vitejs/plugin-rsc` is only reachable from @rangojs/router's
131
+ * node_modules, so Vite's optimizer — which resolves from the project root —
132
+ * can't find them and emits "Failed to resolve dependency" warnings.
133
+ *
134
+ * We resolve those specs from this plugin's location (where plugin-rsc is
135
+ * guaranteed to be installed as our dep) and expose them as `resolve.alias`
136
+ * entries. The optimizer's resolver honors aliases, so the bare specs map to
137
+ * absolute paths and resolve cleanly.
138
+ */
139
+ export function getVendorAliases(): Record<string, string> {
140
+ const specs = [
141
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
142
+ "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
143
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.browser",
144
+ ];
145
+ const aliases: Record<string, string> = {};
146
+ for (const spec of specs) {
147
+ try {
148
+ aliases[spec] = require.resolve(spec);
149
+ } catch {
150
+ // Spec unresolvable (unexpected but non-fatal — Vite will warn as before).
151
+ }
152
+ }
153
+ return aliases;
154
+ }
@@ -1,3 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ rmSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { resolve } from "node:path";
11
+
1
12
  /**
2
13
  * Escape special RegExp characters in a string for safe interpolation
3
14
  * into new RegExp() patterns.
@@ -20,6 +31,7 @@ export function encodePathParam(value: unknown): string {
20
31
  /**
21
32
  * Substitute route params into a pattern, stripping constraint and optional
22
33
  * syntax (:param(a|b)? -> value). Also handles wildcard params (*key).
34
+ * Optional params not present in `params` are removed from the output.
23
35
  */
24
36
  export function substituteRouteParams(
25
37
  pattern: string,
@@ -27,14 +39,45 @@ export function substituteRouteParams(
27
39
  encode: (value: string) => string = encodeURIComponent,
28
40
  ): string {
29
41
  let result = pattern;
42
+ let hadOmittedOptional = false;
43
+
44
+ // First pass: substitute provided params.
45
+ // Empty string on an optional placeholder is treated as omitted (the trie
46
+ // matcher fills unmatched optionals with "" — letting the second pass
47
+ // strip them keeps slash cleanup consistent). Empty string on required
48
+ // `:key` or wildcard `*key` still substitutes, matching prior behaviour.
30
49
  for (const [key, value] of Object.entries(params)) {
31
50
  const escaped = escapeRegExp(key);
32
- result = result.replace(
33
- new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
34
- encode(value),
35
- );
36
- result = result.replace(`*${key}`, encode(value));
51
+ if (value === "") {
52
+ // Only replace required placeholders (negative lookahead for `?`);
53
+ // leave `:key?` for the second pass.
54
+ result = result.replace(
55
+ new RegExp(`:${escaped}(\\([^)]*\\))?(?!\\?)`),
56
+ "",
57
+ );
58
+ result = result.replace(`*${key}`, "");
59
+ } else {
60
+ result = result.replace(
61
+ new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
62
+ encode(value),
63
+ );
64
+ result = result.replace(`*${key}`, encode(value));
65
+ }
66
+ }
67
+
68
+ // Second pass: strip remaining optional param placeholders not in params
69
+ result = result.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\?/g, () => {
70
+ hadOmittedOptional = true;
71
+ return "";
72
+ });
73
+
74
+ // Clean up slashes from omitted optional segments
75
+ if (hadOmittedOptional) {
76
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
77
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
78
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
37
79
  }
80
+
38
81
  return result;
39
82
  }
40
83
 
@@ -127,3 +170,52 @@ export function notifyOnError(
127
170
  break; // Only notify the first router with onError
128
171
  }
129
172
  }
173
+
174
+ function getStagedAssetDir(projectRoot: string): string {
175
+ return resolve(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
176
+ }
177
+
178
+ export function resetStagedBuildAssets(projectRoot: string): void {
179
+ rmSync(getStagedAssetDir(projectRoot), { recursive: true, force: true });
180
+ }
181
+
182
+ export function stageBuildAssetModule(
183
+ projectRoot: string,
184
+ prefix: "__pr" | "__st",
185
+ exportValue: string,
186
+ ): string {
187
+ const stagedDir = getStagedAssetDir(projectRoot);
188
+ mkdirSync(stagedDir, { recursive: true });
189
+
190
+ const contentHash = createHash("sha256")
191
+ .update(exportValue)
192
+ .digest("hex")
193
+ .slice(0, 8);
194
+ const fileName = `${prefix}-${contentHash}.js`;
195
+ const filePath = resolve(stagedDir, fileName);
196
+
197
+ if (!existsSync(filePath)) {
198
+ writeFileSync(filePath, `export default ${exportValue};\n`);
199
+ }
200
+
201
+ return fileName;
202
+ }
203
+
204
+ export function copyStagedBuildAssets(
205
+ projectRoot: string,
206
+ fileNames: Iterable<string>,
207
+ ): number {
208
+ const stagedDir = getStagedAssetDir(projectRoot);
209
+ const distAssetsDir = resolve(projectRoot, "dist/rsc/assets");
210
+ mkdirSync(distAssetsDir, { recursive: true });
211
+
212
+ let totalBytes = 0;
213
+ for (const fileName of new Set(fileNames)) {
214
+ const stagedPath = resolve(stagedDir, fileName);
215
+ const distPath = resolve(distAssetsDir, fileName);
216
+ copyFileSync(stagedPath, distPath);
217
+ totalBytes += statSync(stagedPath).size;
218
+ }
219
+
220
+ return totalBytes;
221
+ }
@@ -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
  /**