@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2

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 (160) hide show
  1. package/{CLAUDE.md → AGENTS.md} +4 -0
  2. package/README.md +122 -30
  3. package/dist/bin/rango.js +245 -63
  4. package/dist/vite/index.js +859 -418
  5. package/package.json +3 -3
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +49 -8
  9. package/skills/document-cache/SKILL.md +2 -2
  10. package/skills/hooks/SKILL.md +33 -31
  11. package/skills/host-router/SKILL.md +218 -0
  12. package/skills/links/SKILL.md +3 -1
  13. package/skills/loader/SKILL.md +72 -22
  14. package/skills/middleware/SKILL.md +2 -0
  15. package/skills/parallel/SKILL.md +126 -0
  16. package/skills/prerender/SKILL.md +112 -70
  17. package/skills/rango/SKILL.md +0 -1
  18. package/skills/route/SKILL.md +34 -4
  19. package/skills/router-setup/SKILL.md +95 -5
  20. package/skills/typesafety/SKILL.md +35 -23
  21. package/src/__internal.ts +92 -0
  22. package/src/bin/rango.ts +18 -0
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/link-interceptor.ts +4 -0
  26. package/src/browser/navigation-bridge.ts +114 -18
  27. package/src/browser/navigation-client.ts +126 -44
  28. package/src/browser/navigation-store.ts +43 -8
  29. package/src/browser/navigation-transaction.ts +11 -9
  30. package/src/browser/partial-update.ts +80 -15
  31. package/src/browser/prefetch/cache.ts +166 -27
  32. package/src/browser/prefetch/fetch.ts +52 -39
  33. package/src/browser/prefetch/policy.ts +6 -0
  34. package/src/browser/prefetch/queue.ts +92 -20
  35. package/src/browser/prefetch/resource-ready.ts +77 -0
  36. package/src/browser/react/Link.tsx +70 -14
  37. package/src/browser/react/NavigationProvider.tsx +40 -4
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +143 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +6 -1
  44. package/src/browser/server-action-bridge.ts +454 -436
  45. package/src/browser/types.ts +60 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +5 -0
  48. package/src/build/route-trie.ts +19 -3
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +346 -87
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.rsc.tsx +2 -1
  60. package/src/client.tsx +3 -102
  61. package/src/context-var.ts +72 -2
  62. package/src/debug.ts +2 -2
  63. package/src/handle.ts +40 -0
  64. package/src/handles/breadcrumbs.ts +66 -0
  65. package/src/handles/index.ts +1 -0
  66. package/src/host/index.ts +0 -3
  67. package/src/index.rsc.ts +8 -37
  68. package/src/index.ts +40 -66
  69. package/src/prerender/store.ts +57 -15
  70. package/src/prerender.ts +138 -77
  71. package/src/reverse.ts +22 -1
  72. package/src/route-definition/dsl-helpers.ts +73 -25
  73. package/src/route-definition/helpers-types.ts +10 -6
  74. package/src/route-definition/index.ts +3 -3
  75. package/src/route-definition/redirect.ts +11 -3
  76. package/src/route-definition/resolve-handler-use.ts +149 -0
  77. package/src/route-map-builder.ts +7 -1
  78. package/src/route-types.ts +11 -0
  79. package/src/router/content-negotiation.ts +100 -1
  80. package/src/router/find-match.ts +4 -2
  81. package/src/router/handler-context.ts +108 -25
  82. package/src/router/intercept-resolution.ts +11 -4
  83. package/src/router/lazy-includes.ts +4 -1
  84. package/src/router/loader-resolution.ts +123 -11
  85. package/src/router/logging.ts +5 -2
  86. package/src/router/manifest.ts +9 -3
  87. package/src/router/match-api.ts +125 -190
  88. package/src/router/match-middleware/background-revalidation.ts +30 -2
  89. package/src/router/match-middleware/cache-lookup.ts +88 -16
  90. package/src/router/match-middleware/cache-store.ts +53 -10
  91. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  92. package/src/router/match-middleware/segment-resolution.ts +61 -5
  93. package/src/router/match-result.ts +22 -15
  94. package/src/router/metrics.ts +238 -13
  95. package/src/router/middleware-types.ts +53 -12
  96. package/src/router/middleware.ts +172 -85
  97. package/src/router/navigation-snapshot.ts +182 -0
  98. package/src/router/pattern-matching.ts +20 -5
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/revalidation.ts +27 -7
  103. package/src/router/route-snapshot.ts +245 -0
  104. package/src/router/router-context.ts +6 -1
  105. package/src/router/router-interfaces.ts +50 -5
  106. package/src/router/router-options.ts +50 -19
  107. package/src/router/segment-resolution/fresh.ts +200 -19
  108. package/src/router/segment-resolution/helpers.ts +30 -25
  109. package/src/router/segment-resolution/loader-cache.ts +1 -0
  110. package/src/router/segment-resolution/revalidation.ts +429 -301
  111. package/src/router/segment-wrappers.ts +2 -0
  112. package/src/router/trie-matching.ts +20 -2
  113. package/src/router/types.ts +1 -0
  114. package/src/router.ts +88 -15
  115. package/src/rsc/handler.ts +546 -359
  116. package/src/rsc/index.ts +0 -20
  117. package/src/rsc/manifest-init.ts +5 -1
  118. package/src/rsc/progressive-enhancement.ts +25 -8
  119. package/src/rsc/rsc-rendering.ts +35 -43
  120. package/src/rsc/server-action.ts +16 -10
  121. package/src/rsc/ssr-setup.ts +128 -0
  122. package/src/rsc/types.ts +10 -1
  123. package/src/search-params.ts +16 -13
  124. package/src/segment-system.tsx +140 -4
  125. package/src/server/context.ts +148 -16
  126. package/src/server/loader-registry.ts +9 -8
  127. package/src/server/request-context.ts +182 -34
  128. package/src/server.ts +6 -0
  129. package/src/ssr/index.tsx +4 -0
  130. package/src/static-handler.ts +18 -6
  131. package/src/theme/index.ts +4 -13
  132. package/src/types/cache-types.ts +4 -4
  133. package/src/types/handler-context.ts +149 -49
  134. package/src/types/loader-types.ts +36 -9
  135. package/src/types/route-config.ts +17 -8
  136. package/src/types/route-entry.ts +8 -1
  137. package/src/types/segments.ts +2 -5
  138. package/src/urls/path-helper-types.ts +9 -2
  139. package/src/urls/path-helper.ts +48 -13
  140. package/src/urls/pattern-types.ts +12 -0
  141. package/src/urls/response-types.ts +16 -6
  142. package/src/use-loader.tsx +73 -4
  143. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  144. package/src/vite/discovery/discover-routers.ts +23 -5
  145. package/src/vite/discovery/prerender-collection.ts +48 -15
  146. package/src/vite/discovery/state.ts +17 -13
  147. package/src/vite/index.ts +8 -3
  148. package/src/vite/plugin-types.ts +51 -79
  149. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  150. package/src/vite/plugins/expose-action-id.ts +1 -3
  151. package/src/vite/plugins/performance-tracks.ts +88 -0
  152. package/src/vite/plugins/refresh-cmd.ts +127 -0
  153. package/src/vite/plugins/version-plugin.ts +13 -1
  154. package/src/vite/rango.ts +174 -211
  155. package/src/vite/router-discovery.ts +169 -42
  156. package/src/vite/utils/banner.ts +3 -3
  157. package/src/vite/utils/prerender-utils.ts +78 -0
  158. package/src/vite/utils/shared-utils.ts +3 -2
  159. package/skills/testing/SKILL.md +0 -226
  160. package/src/route-definition/route-function.ts +0 -119
@@ -10,9 +10,12 @@ import type { Plugin } from "vite";
10
10
  import { createServer as createViteServer } from "vite";
11
11
  import { resolve } from "node:path";
12
12
  import { readFileSync } from "node:fs";
13
+ import { createRequire } from "node:module";
14
+ import { pathToFileURL } from "node:url";
13
15
  import {
16
+ formatNestedRouterConflictError,
17
+ findNestedRouterConflict,
14
18
  findRouterFiles,
15
- createScanFilter,
16
19
  } from "../build/generate-route-types.js";
17
20
  import { createVersionPlugin } from "./plugins/version-plugin.js";
18
21
  import { createVirtualStubPlugin } from "./plugins/virtual-stub-plugin.js";
@@ -40,6 +43,7 @@ import {
40
43
  generatePerRouterModule,
41
44
  } from "./discovery/virtual-module-codegen.js";
42
45
  import { postprocessBundle } from "./discovery/bundle-postprocess.js";
46
+ import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
43
47
 
44
48
  export { VIRTUAL_ROUTES_MANIFEST_ID };
45
49
 
@@ -92,6 +96,105 @@ async function createTempRscServer(
92
96
  });
93
97
  }
94
98
 
99
+ // ============================================================================
100
+ // Build-Time Env Resolution
101
+ // ============================================================================
102
+
103
+ import type {
104
+ BuildEnvOption,
105
+ BuildEnvFactoryContext,
106
+ BuildEnvResult,
107
+ } from "./plugin-types.js";
108
+
109
+ /**
110
+ * Resolve the buildEnv option into a concrete { env, dispose? } result.
111
+ * Handles all four input shapes: false, "auto", factory, plain object.
112
+ */
113
+ async function resolveBuildEnv(
114
+ option: BuildEnvOption | undefined,
115
+ factoryCtx: BuildEnvFactoryContext,
116
+ ): Promise<BuildEnvResult | null> {
117
+ if (!option) return null;
118
+
119
+ if (option === "auto") {
120
+ if (factoryCtx.preset !== "cloudflare") {
121
+ throw new Error(
122
+ '[rsc-router] buildEnv: "auto" is only supported with preset: "cloudflare". ' +
123
+ "Use a factory function or plain object for other presets.",
124
+ );
125
+ }
126
+ try {
127
+ // Resolve wrangler from the user's project root (not the router package)
128
+ const userRequire = createRequire(
129
+ resolve(factoryCtx.root, "package.json"),
130
+ );
131
+ const wranglerPath = userRequire.resolve("wrangler");
132
+ const { getPlatformProxy } = (await import(
133
+ pathToFileURL(wranglerPath).href
134
+ )) as {
135
+ getPlatformProxy: (opts?: any) => Promise<any>;
136
+ };
137
+ const proxy = await getPlatformProxy();
138
+ return {
139
+ env: proxy.env as Record<string, unknown>,
140
+ dispose: proxy.dispose,
141
+ };
142
+ } catch (err: any) {
143
+ throw new Error(
144
+ '[rsc-router] buildEnv: "auto" requires wrangler to be installed.\n' +
145
+ `Install it with: pnpm add -D wrangler\n${err.message}`,
146
+ );
147
+ }
148
+ }
149
+
150
+ if (typeof option === "function") {
151
+ return await option(factoryCtx);
152
+ }
153
+
154
+ // Plain object
155
+ return { env: option };
156
+ }
157
+
158
+ /**
159
+ * Acquire build-time env bindings and store on discovery state.
160
+ * Returns true if env was acquired, false if buildEnv is disabled.
161
+ */
162
+ async function acquireBuildEnv(
163
+ s: DiscoveryState,
164
+ command: "serve" | "build",
165
+ mode: string,
166
+ ): Promise<boolean> {
167
+ const option = s.opts?.buildEnv;
168
+ if (!option) return false;
169
+
170
+ const result = await resolveBuildEnv(option, {
171
+ root: s.projectRoot,
172
+ mode,
173
+ command,
174
+ preset: s.opts?.preset ?? "node",
175
+ });
176
+ if (!result) return false;
177
+
178
+ s.resolvedBuildEnv = result.env;
179
+ s.buildEnvDispose = result.dispose ?? null;
180
+ return true;
181
+ }
182
+
183
+ /**
184
+ * Release build-time env resources and clear state.
185
+ */
186
+ async function releaseBuildEnv(s: DiscoveryState): Promise<void> {
187
+ if (s.buildEnvDispose) {
188
+ try {
189
+ await s.buildEnvDispose();
190
+ } catch (err: any) {
191
+ console.warn(`[rsc-router] buildEnv dispose failed: ${err.message}`);
192
+ }
193
+ s.buildEnvDispose = null;
194
+ }
195
+ s.resolvedBuildEnv = undefined;
196
+ }
197
+
95
198
  /**
96
199
  * Plugin that discovers router instances at dev/build time via the RSC environment.
97
200
  *
@@ -109,6 +212,8 @@ export function createRouterDiscoveryPlugin(
109
212
  opts?: PluginOptions,
110
213
  ): Plugin {
111
214
  const s = createDiscoveryState(entryPath, opts);
215
+ let viteCommand: "serve" | "build" = "build";
216
+ let viteMode = "production";
112
217
 
113
218
  return {
114
219
  name: "@rangojs/router:discovery",
@@ -119,32 +224,20 @@ export function createRouterDiscoveryPlugin(
119
224
  __RANGO_DEBUG__: JSON.stringify(!!process.env.INTERNAL_RANGO_DEBUG),
120
225
  },
121
226
  };
122
- if (opts?.enableBuildPrerender) {
123
- config.environments = {
124
- rsc: {
125
- build: {
126
- rollupOptions: {
127
- output: {
128
- manualChunks(id: string) {
129
- if (s.resolvedPrerenderModules?.has(id)) {
130
- return "__prerender-handlers";
131
- }
132
- if (s.resolvedStaticModules?.has(id)) {
133
- return "__static-handlers";
134
- }
135
- },
136
- },
137
- },
138
- },
139
- },
140
- };
141
- }
227
+ // Prerender/static handler modules are bundled naturally with the
228
+ // rest of the RSC entry. A previous design forced them into dedicated
229
+ // __prerender-handlers / __static-handlers chunks via manualChunks,
230
+ // but Rollup hoisted all shared dependencies into those chunks,
231
+ // inflating them to ~1 MB with active runtime code. Handler code is
232
+ // evicted in closeBundle regardless of which chunk it lands in.
142
233
  return config;
143
234
  },
144
235
 
145
236
  configResolved(config) {
146
237
  s.projectRoot = config.root;
147
238
  s.isBuildMode = config.command === "build";
239
+ viteCommand = config.command as "serve" | "build";
240
+ viteMode = config.mode;
148
241
  // Capture user's resolve aliases for the temp server
149
242
  s.userResolveAlias = config.resolve.alias;
150
243
  // Node preset: pick up auto-discovered router path from the config() hook.
@@ -165,13 +258,6 @@ export function createRouterDiscoveryPlugin(
165
258
  s.resolvedEntryPath = entries[0];
166
259
  }
167
260
  }
168
- // Compile include/exclude patterns into a scan filter
169
- if (opts?.include || opts?.exclude) {
170
- s.scanFilter = createScanFilter(s.projectRoot, {
171
- include: opts.include,
172
- exclude: opts.exclude,
173
- });
174
- }
175
261
  // Generate combined named-routes.gen.ts from static source parsing.
176
262
  // Runs before the dev server starts so the gen file exists immediately for IDE.
177
263
  // In build mode, the runtime discovery in buildStart produces the definitive
@@ -222,12 +308,13 @@ export function createRouterDiscoveryPlugin(
222
308
  let prerenderTempServer: any = null;
223
309
  let prerenderNodeRegistry: Map<string, any> | null = null;
224
310
 
225
- // Clean up the temporary server when the dev server shuts down
311
+ // Clean up the temporary server and build env when the dev server shuts down
226
312
  server.httpServer?.on("close", () => {
227
313
  if (prerenderTempServer) {
228
314
  prerenderTempServer.close().catch(() => {});
229
315
  prerenderTempServer = null;
230
316
  }
317
+ releaseBuildEnv(s).catch(() => {});
231
318
  });
232
319
 
233
320
  async function getOrCreateTempServer(): Promise<any | null> {
@@ -267,6 +354,9 @@ export function createRouterDiscoveryPlugin(
267
354
  // Create a temp Node.js server to run runtime discovery and generate
268
355
  // named route types (static parser can't resolve factory calls).
269
356
  try {
357
+ // Acquire build-time env bindings for dev prerender
358
+ await acquireBuildEnv(s, viteCommand, viteMode);
359
+
270
360
  const tempRscEnv = await getOrCreateTempServer();
271
361
  if (tempRscEnv) {
272
362
  await discoverRouters(s, tempRscEnv);
@@ -283,6 +373,9 @@ export function createRouterDiscoveryPlugin(
283
373
  }
284
374
 
285
375
  try {
376
+ // Acquire build-time env bindings for dev prerender (Node.js path)
377
+ await acquireBuildEnv(s, viteCommand, viteMode);
378
+
286
379
  // Set the readiness gate BEFORE discovery so early requests
287
380
  // block until manifest is populated
288
381
  const serverMod = await rscEnv.runner.import(
@@ -418,6 +511,8 @@ export function createRouterDiscoveryPlugin(
418
511
  {},
419
512
  undefined,
420
513
  wantPassthrough,
514
+ s.resolvedBuildEnv,
515
+ true, // devMode: check getParams for passthrough routes
421
516
  );
422
517
  if (!result) continue;
423
518
  if (result.passthrough) continue;
@@ -559,6 +654,16 @@ export function createRouterDiscoveryPlugin(
559
654
  if (!hasUrls && !hasCreateRouter) return;
560
655
  // Invalidate cache when a router file changes (new router added/removed)
561
656
  if (hasCreateRouter) {
657
+ const nestedRouterConflict = findNestedRouterConflict([
658
+ ...(s.cachedRouterFiles ?? []),
659
+ resolve(filePath),
660
+ ]);
661
+ if (nestedRouterConflict) {
662
+ server.config.logger.error(
663
+ formatNestedRouterConflictError(nestedRouterConflict),
664
+ );
665
+ return;
666
+ }
562
667
  s.cachedRouterFiles = undefined;
563
668
  }
564
669
  scheduleRouteRegeneration();
@@ -592,6 +697,12 @@ export function createRouterDiscoveryPlugin(
592
697
  if (!s.isBuildMode) return;
593
698
  // Only run once across environment builds
594
699
  if (s.mergedRouteManifest !== null) return;
700
+ resetStagedBuildAssets(s.projectRoot);
701
+ s.prerenderManifestEntries = null;
702
+ s.staticManifestEntries = null;
703
+
704
+ // Acquire build-time env bindings if configured
705
+ await acquireBuildEnv(s, viteCommand, viteMode);
595
706
 
596
707
  let tempServer: any = null;
597
708
  // Signal to user-space code (e.g. reverse.ts) that build-time discovery
@@ -651,6 +762,7 @@ export function createRouterDiscoveryPlugin(
651
762
  if (tempServer) {
652
763
  await tempServer.close();
653
764
  }
765
+ await releaseBuildEnv(s);
654
766
  }
655
767
  },
656
768
 
@@ -711,33 +823,40 @@ export function createRouterDiscoveryPlugin(
711
823
  if (!s.resolvedPrerenderModules?.size && !s.resolvedStaticModules?.size)
712
824
  return;
713
825
 
826
+ // Clear maps at the start of each RSC generateBundle pass.
827
+ // Vite 6 multi-environment builds run RSC twice (analysis + production);
828
+ // clearing prevents stale/duplicate records from the analysis pass.
829
+ s.handlerChunkInfoMap.clear();
830
+ s.staticHandlerChunkInfoMap.clear();
831
+
714
832
  for (const [fileName, chunk] of Object.entries(bundle) as [
715
833
  string,
716
834
  any,
717
835
  ][]) {
718
836
  if (chunk.type !== "chunk") continue;
719
837
 
720
- // Prerender handlers chunk
721
- if (
722
- fileName.includes("__prerender-handlers") &&
723
- s.resolvedPrerenderModules?.size
724
- ) {
838
+ // Scan all chunks for handler exports (handlers may land in any chunk)
839
+ if (s.resolvedPrerenderModules?.size) {
725
840
  const handlers = extractHandlerExportsFromChunk(
726
841
  chunk.code,
727
842
  s.resolvedPrerenderModules,
728
843
  "Prerender",
729
- true,
844
+ false,
730
845
  );
731
846
  if (handlers.length > 0) {
732
- s.handlerChunkInfo = { fileName, exports: handlers };
847
+ const existing = s.handlerChunkInfoMap.get(fileName);
848
+ if (existing) {
849
+ existing.exports.push(...handlers);
850
+ } else {
851
+ s.handlerChunkInfoMap.set(fileName, {
852
+ fileName,
853
+ exports: handlers,
854
+ });
855
+ }
733
856
  }
734
857
  }
735
858
 
736
- // Static handlers chunk
737
- if (
738
- fileName.includes("__static-handlers") &&
739
- s.resolvedStaticModules?.size
740
- ) {
859
+ if (s.resolvedStaticModules?.size) {
741
860
  const handlers = extractHandlerExportsFromChunk(
742
861
  chunk.code,
743
862
  s.resolvedStaticModules,
@@ -745,7 +864,15 @@ export function createRouterDiscoveryPlugin(
745
864
  false,
746
865
  );
747
866
  if (handlers.length > 0) {
748
- s.staticHandlerChunkInfo = { fileName, exports: handlers };
867
+ const existing = s.staticHandlerChunkInfoMap.get(fileName);
868
+ if (existing) {
869
+ existing.exports.push(...handlers);
870
+ } else {
871
+ s.staticHandlerChunkInfoMap.set(fileName, {
872
+ fileName,
873
+ exports: handlers,
874
+ });
875
+ }
749
876
  }
750
877
  }
751
878
  }
@@ -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
  `;
@@ -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,6 +39,9 @@ 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
30
45
  for (const [key, value] of Object.entries(params)) {
31
46
  const escaped = escapeRegExp(key);
32
47
  result = result.replace(
@@ -35,6 +50,20 @@ export function substituteRouteParams(
35
50
  );
36
51
  result = result.replace(`*${key}`, encode(value));
37
52
  }
53
+
54
+ // Second pass: strip remaining optional param placeholders not in params
55
+ result = result.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\?/g, () => {
56
+ hadOmittedOptional = true;
57
+ return "";
58
+ });
59
+
60
+ // Clean up slashes from omitted optional segments
61
+ if (hadOmittedOptional) {
62
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
63
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
64
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
65
+ }
66
+
38
67
  return result;
39
68
  }
40
69
 
@@ -127,3 +156,52 @@ export function notifyOnError(
127
156
  break; // Only notify the first router with onError
128
157
  }
129
158
  }
159
+
160
+ function getStagedAssetDir(projectRoot: string): string {
161
+ return resolve(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
162
+ }
163
+
164
+ export function resetStagedBuildAssets(projectRoot: string): void {
165
+ rmSync(getStagedAssetDir(projectRoot), { recursive: true, force: true });
166
+ }
167
+
168
+ export function stageBuildAssetModule(
169
+ projectRoot: string,
170
+ prefix: "__pr" | "__st",
171
+ exportValue: string,
172
+ ): string {
173
+ const stagedDir = getStagedAssetDir(projectRoot);
174
+ mkdirSync(stagedDir, { recursive: true });
175
+
176
+ const contentHash = createHash("sha256")
177
+ .update(exportValue)
178
+ .digest("hex")
179
+ .slice(0, 8);
180
+ const fileName = `${prefix}-${contentHash}.js`;
181
+ const filePath = resolve(stagedDir, fileName);
182
+
183
+ if (!existsSync(filePath)) {
184
+ writeFileSync(filePath, `export default ${exportValue};\n`);
185
+ }
186
+
187
+ return fileName;
188
+ }
189
+
190
+ export function copyStagedBuildAssets(
191
+ projectRoot: string,
192
+ fileNames: Iterable<string>,
193
+ ): number {
194
+ const stagedDir = getStagedAssetDir(projectRoot);
195
+ const distAssetsDir = resolve(projectRoot, "dist/rsc/assets");
196
+ mkdirSync(distAssetsDir, { recursive: true });
197
+
198
+ let totalBytes = 0;
199
+ for (const fileName of new Set(fileNames)) {
200
+ const stagedPath = resolve(stagedDir, fileName);
201
+ const distPath = resolve(distAssetsDir, fileName);
202
+ copyFileSync(stagedPath, distPath);
203
+ totalBytes += statSync(stagedPath).size;
204
+ }
205
+
206
+ return totalBytes;
207
+ }
@@ -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
  /**