@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d

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 (278) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2154 -861
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +220 -30
  11. package/skills/caching/SKILL.md +116 -8
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +71 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +243 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +57 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +128 -0
  37. package/skills/testing/bindings.md +89 -0
  38. package/skills/testing/cache-prerender.md +98 -0
  39. package/skills/testing/client-components.md +121 -0
  40. package/skills/testing/e2e-parity.md +124 -0
  41. package/skills/testing/flight.md +89 -0
  42. package/skills/testing/handles.md +127 -0
  43. package/skills/testing/loader.md +108 -0
  44. package/skills/testing/middleware.md +97 -0
  45. package/skills/testing/render-handler.md +102 -0
  46. package/skills/testing/response-routes.md +94 -0
  47. package/skills/testing/reverse-and-types.md +83 -0
  48. package/skills/testing/server-actions.md +89 -0
  49. package/skills/testing/server-tree.md +128 -0
  50. package/skills/testing/setup.md +120 -0
  51. package/skills/typesafety/SKILL.md +319 -27
  52. package/skills/use-cache/SKILL.md +34 -5
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/app-shell.ts +52 -0
  58. package/src/browser/event-controller.ts +86 -70
  59. package/src/browser/history-state.ts +21 -0
  60. package/src/browser/index.ts +3 -3
  61. package/src/browser/navigation-bridge.ts +84 -11
  62. package/src/browser/navigation-client.ts +104 -68
  63. package/src/browser/navigation-store.ts +32 -9
  64. package/src/browser/navigation-transaction.ts +10 -28
  65. package/src/browser/partial-update.ts +64 -26
  66. package/src/browser/prefetch/cache.ts +183 -44
  67. package/src/browser/prefetch/fetch.ts +228 -37
  68. package/src/browser/prefetch/queue.ts +36 -5
  69. package/src/browser/rango-state.ts +53 -13
  70. package/src/browser/react/Link.tsx +30 -2
  71. package/src/browser/react/NavigationProvider.tsx +72 -31
  72. package/src/browser/react/filter-segment-order.ts +51 -7
  73. package/src/browser/react/index.ts +3 -0
  74. package/src/browser/react/location-state-shared.ts +175 -4
  75. package/src/browser/react/location-state.ts +39 -13
  76. package/src/browser/react/use-handle.ts +17 -9
  77. package/src/browser/react/use-navigation.ts +22 -2
  78. package/src/browser/react/use-params.ts +20 -8
  79. package/src/browser/react/use-reverse.ts +106 -0
  80. package/src/browser/react/use-router.ts +22 -2
  81. package/src/browser/react/use-segments.ts +11 -8
  82. package/src/browser/response-adapter.ts +32 -1
  83. package/src/browser/rsc-router.tsx +69 -22
  84. package/src/browser/scroll-restoration.ts +22 -14
  85. package/src/browser/segment-reconciler.ts +36 -14
  86. package/src/browser/segment-structure-assert.ts +2 -2
  87. package/src/browser/server-action-bridge.ts +23 -30
  88. package/src/browser/types.ts +21 -0
  89. package/src/build/collect-fallback-refs.ts +107 -0
  90. package/src/build/generate-manifest.ts +60 -35
  91. package/src/build/generate-route-types.ts +2 -0
  92. package/src/build/index.ts +8 -1
  93. package/src/build/prefix-tree-utils.ts +123 -0
  94. package/src/build/route-trie.ts +95 -25
  95. package/src/build/route-types/codegen.ts +4 -4
  96. package/src/build/route-types/include-resolution.ts +1 -1
  97. package/src/build/route-types/per-module-writer.ts +7 -4
  98. package/src/build/route-types/router-processing.ts +55 -14
  99. package/src/build/route-types/scan-filter.ts +1 -1
  100. package/src/build/route-types/source-scan.ts +118 -0
  101. package/src/build/runtime-discovery.ts +9 -20
  102. package/src/cache/cache-scope.ts +28 -42
  103. package/src/cache/cf/cf-cache-store.ts +54 -13
  104. package/src/client.rsc.tsx +3 -0
  105. package/src/client.tsx +96 -205
  106. package/src/context-var.ts +5 -5
  107. package/src/decode-loader-results.ts +36 -0
  108. package/src/errors.ts +30 -4
  109. package/src/handle.ts +32 -14
  110. package/src/host/index.ts +2 -2
  111. package/src/host/router.ts +129 -57
  112. package/src/host/types.ts +31 -2
  113. package/src/host/utils.ts +1 -1
  114. package/src/href-client.ts +140 -21
  115. package/src/index.rsc.ts +10 -6
  116. package/src/index.ts +54 -17
  117. package/src/loader-store.ts +500 -0
  118. package/src/loader.rsc.ts +25 -7
  119. package/src/loader.ts +16 -9
  120. package/src/missing-id-error.ts +68 -0
  121. package/src/outlet-context.ts +1 -1
  122. package/src/prerender.ts +27 -6
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -36
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +384 -257
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +100 -28
  129. package/src/route-definition/resolve-handler-use.ts +6 -0
  130. package/src/route-definition/use-item-types.ts +32 -0
  131. package/src/route-types.ts +26 -41
  132. package/src/router/basename.ts +14 -0
  133. package/src/router/content-negotiation.ts +15 -2
  134. package/src/router/error-handling.ts +1 -1
  135. package/src/router/find-match.ts +54 -6
  136. package/src/router/handler-context.ts +21 -38
  137. package/src/router/intercept-resolution.ts +4 -18
  138. package/src/router/lazy-includes.ts +41 -22
  139. package/src/router/loader-resolution.ts +82 -36
  140. package/src/router/manifest.ts +41 -19
  141. package/src/router/match-api.ts +4 -3
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/cache-lookup.ts +44 -91
  144. package/src/router/match-middleware/cache-store.ts +3 -2
  145. package/src/router/match-result.ts +53 -32
  146. package/src/router/metrics.ts +1 -1
  147. package/src/router/middleware-types.ts +15 -26
  148. package/src/router/middleware.ts +99 -84
  149. package/src/router/pattern-matching.ts +116 -19
  150. package/src/router/prerender-match.ts +1 -1
  151. package/src/router/preview-match.ts +3 -1
  152. package/src/router/request-classification.ts +4 -28
  153. package/src/router/revalidation.ts +58 -2
  154. package/src/router/router-interfaces.ts +45 -28
  155. package/src/router/router-options.ts +40 -1
  156. package/src/router/router-registry.ts +2 -5
  157. package/src/router/segment-resolution/fresh.ts +27 -6
  158. package/src/router/segment-resolution/revalidation.ts +147 -106
  159. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  160. package/src/router/substitute-pattern-params.ts +56 -0
  161. package/src/router/telemetry.ts +99 -0
  162. package/src/router/trie-matching.ts +40 -16
  163. package/src/router/types.ts +8 -0
  164. package/src/router/url-params.ts +49 -0
  165. package/src/router.ts +52 -30
  166. package/src/rsc/handler-context.ts +2 -2
  167. package/src/rsc/handler.ts +28 -69
  168. package/src/rsc/helpers.ts +91 -43
  169. package/src/rsc/index.ts +1 -1
  170. package/src/rsc/manifest-init.ts +28 -41
  171. package/src/rsc/origin-guard.ts +28 -10
  172. package/src/rsc/progressive-enhancement.ts +4 -0
  173. package/src/rsc/response-error.ts +79 -12
  174. package/src/rsc/response-route-handler.ts +57 -61
  175. package/src/rsc/rsc-rendering.ts +35 -51
  176. package/src/rsc/runtime-warnings.ts +9 -10
  177. package/src/rsc/server-action.ts +17 -37
  178. package/src/rsc/ssr-setup.ts +16 -0
  179. package/src/rsc/types.ts +8 -2
  180. package/src/runtime-env.ts +18 -0
  181. package/src/search-params.ts +4 -4
  182. package/src/segment-content-promise.ts +67 -0
  183. package/src/segment-loader-promise.ts +122 -0
  184. package/src/segment-system.tsx +132 -116
  185. package/src/serialize.ts +243 -0
  186. package/src/server/context.ts +175 -53
  187. package/src/server/cookie-store.ts +28 -4
  188. package/src/server/request-context.ts +67 -51
  189. package/src/ssr/index.tsx +5 -1
  190. package/src/static-handler.ts +25 -3
  191. package/src/testing/cache-status.ts +166 -0
  192. package/src/testing/collect-handle.ts +63 -0
  193. package/src/testing/dispatch.ts +581 -0
  194. package/src/testing/dom.entry.ts +22 -0
  195. package/src/testing/e2e/fixture.ts +188 -0
  196. package/src/testing/e2e/index.ts +149 -0
  197. package/src/testing/e2e/matchers.ts +51 -0
  198. package/src/testing/e2e/page-helpers.ts +272 -0
  199. package/src/testing/e2e/parity.ts +326 -0
  200. package/src/testing/e2e/server.ts +195 -0
  201. package/src/testing/flight-matchers.ts +110 -0
  202. package/src/testing/flight-normalize.ts +38 -0
  203. package/src/testing/flight-runtime.d.ts +57 -0
  204. package/src/testing/flight-tree.ts +682 -0
  205. package/src/testing/flight.entry.ts +51 -0
  206. package/src/testing/flight.ts +234 -0
  207. package/src/testing/generated-routes.ts +223 -0
  208. package/src/testing/index.ts +106 -0
  209. package/src/testing/internal/context.ts +304 -0
  210. package/src/testing/internal/flight-client-globals.ts +30 -0
  211. package/src/testing/internal/seed-vars.ts +42 -0
  212. package/src/testing/render-handler.ts +323 -0
  213. package/src/testing/render-route.tsx +590 -0
  214. package/src/testing/run-loader.ts +363 -0
  215. package/src/testing/run-middleware.ts +205 -0
  216. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  217. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  218. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  219. package/src/testing/vitest-stubs/version.ts +5 -0
  220. package/src/testing/vitest.ts +285 -0
  221. package/src/types/global-namespace.ts +39 -26
  222. package/src/types/handler-context.ts +68 -50
  223. package/src/types/index.ts +1 -0
  224. package/src/types/loader-types.ts +11 -9
  225. package/src/types/request-scope.ts +126 -0
  226. package/src/types/route-entry.ts +11 -0
  227. package/src/types/segments.ts +35 -2
  228. package/src/urls/include-helper.ts +34 -67
  229. package/src/urls/index.ts +1 -5
  230. package/src/urls/path-helper-types.ts +41 -7
  231. package/src/urls/path-helper.ts +17 -52
  232. package/src/urls/pattern-types.ts +36 -19
  233. package/src/urls/response-types.ts +22 -29
  234. package/src/urls/type-extraction.ts +58 -139
  235. package/src/urls/urls-function.ts +1 -5
  236. package/src/use-loader.tsx +413 -42
  237. package/src/vite/debug.ts +185 -0
  238. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  239. package/src/vite/discovery/discover-routers.ts +106 -75
  240. package/src/vite/discovery/discovery-errors.ts +194 -0
  241. package/src/vite/discovery/gate-state.ts +171 -0
  242. package/src/vite/discovery/prerender-collection.ts +67 -26
  243. package/src/vite/discovery/route-types-writer.ts +40 -84
  244. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  245. package/src/vite/discovery/state.ts +33 -0
  246. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  247. package/src/vite/index.ts +2 -0
  248. package/src/vite/plugin-types.ts +67 -0
  249. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  250. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  251. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  252. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  253. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  254. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  255. package/src/vite/plugins/expose-action-id.ts +54 -30
  256. package/src/vite/plugins/expose-id-utils.ts +12 -8
  257. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  258. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  259. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  260. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  261. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  262. package/src/vite/plugins/performance-tracks.ts +29 -25
  263. package/src/vite/plugins/use-cache-transform.ts +65 -50
  264. package/src/vite/plugins/version-injector.ts +39 -23
  265. package/src/vite/plugins/version-plugin.ts +59 -2
  266. package/src/vite/plugins/virtual-entries.ts +2 -2
  267. package/src/vite/rango.ts +116 -29
  268. package/src/vite/router-discovery.ts +750 -100
  269. package/src/vite/utils/ast-handler-extract.ts +15 -15
  270. package/src/vite/utils/banner.ts +1 -1
  271. package/src/vite/utils/bundle-analysis.ts +4 -2
  272. package/src/vite/utils/client-chunks.ts +190 -0
  273. package/src/vite/utils/forward-user-plugins.ts +193 -0
  274. package/src/vite/utils/manifest-utils.ts +8 -59
  275. package/src/vite/utils/package-resolution.ts +41 -1
  276. package/src/vite/utils/prerender-utils.ts +21 -6
  277. package/src/vite/utils/shared-utils.ts +107 -26
  278. package/src/browser/action-response-classifier.ts +0 -99
@@ -48,7 +48,7 @@ function findImportInsertionPos(
48
48
  ): number {
49
49
  let program: ProgramNode;
50
50
  try {
51
- program = parseAst(code, { jsx: true });
51
+ program = parseAst(code, { lang: "tsx" });
52
52
  } catch {
53
53
  return 0;
54
54
  }
@@ -127,7 +127,7 @@ export function findHandlerCalls(
127
127
  ): HandlerCallSite[] {
128
128
  let program: ProgramNode;
129
129
  try {
130
- program = parseAst(code, { jsx: true });
130
+ program = parseAst(code, { lang: "tsx" });
131
131
  } catch {
132
132
  return [];
133
133
  }
@@ -239,7 +239,7 @@ export function getImportedLocalNames(
239
239
  parseAst: (code: string, options?: any) => ProgramNode,
240
240
  ): Set<string> {
241
241
  try {
242
- const program = parseAst(code, { jsx: true });
242
+ const program = parseAst(code, { lang: "tsx" });
243
243
  return getImportedLocalNamesFromProgram(program, importedName);
244
244
  } catch {
245
245
  return new Set<string>();
@@ -256,7 +256,7 @@ export function extractImportDeclarations(
256
256
  ): string[] {
257
257
  let program: ProgramNode;
258
258
  try {
259
- program = parseAst(code, { jsx: true });
259
+ program = parseAst(code, { lang: "tsx" });
260
260
  } catch {
261
261
  return [];
262
262
  }
@@ -380,7 +380,7 @@ export function extractModuleLevelDeclarations(
380
380
  ): string[] {
381
381
  let program: ProgramNode;
382
382
  try {
383
- program = parseAst(code, { jsx: true });
383
+ program = parseAst(code, { lang: "tsx" });
384
384
  } catch {
385
385
  return [];
386
386
  }
@@ -468,19 +468,19 @@ export function transformInlineHandlers(
468
468
  handlerNames,
469
469
  );
470
470
 
471
- // Track line occurrences for same-line collision handling
472
- const lineCounts = new Map<number, number>();
473
-
474
471
  // Collect all import statements to prepend
475
472
  const importStatements: string[] = [];
476
473
 
477
- for (const site of inlineSites) {
478
- const lineCount = lineCounts.get(site.lineNumber) ?? 0;
479
- lineCounts.set(site.lineNumber, lineCount + 1);
480
-
481
- const hash = hashInlineId(filePath, site.lineNumber, lineCount);
474
+ for (const [siteIndex, site] of inlineSites.entries()) {
475
+ // Key the extracted handler on its source-order index (per fnName), NOT its
476
+ // line number. The id flows into BOTH the export name and the virtual module
477
+ // path (which hashId hashes for the runtime $$id), and line numbers shift
478
+ // between the prerender and production build contexts. The index is invariant
479
+ // to those shifts, keeping the prerender manifest key == the runtime id.
480
+ const hash = hashInlineId(filePath, fnName, siteIndex);
482
481
  const exportName = `__sh_${hash}`;
483
- const virtualId = `\0${virtualPrefix}${filePath}:${site.lineNumber}${lineCount > 0 ? `:${lineCount}` : ""}`;
482
+ const idSuffix = `${filePath}:${fnName}:${siteIndex}`;
483
+ const virtualId = `\0${virtualPrefix}${idSuffix}`;
484
484
 
485
485
  // Extract the full handler call expression text
486
486
  const handlerCode = code.slice(site.callStart, site.callEnd);
@@ -498,7 +498,7 @@ export function transformInlineHandlers(
498
498
  s.overwrite(site.callStart, site.callEnd, exportName);
499
499
 
500
500
  // Build the import specifier for this virtual module
501
- const importId = `${virtualPrefix}${filePath}:${site.lineNumber}${lineCount > 0 ? `:${lineCount}` : ""}`;
501
+ const importId = `${virtualPrefix}${idSuffix}`;
502
502
  importStatements.push(`import { ${exportName} } from "${importId}";`);
503
503
  }
504
504
 
@@ -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
 
@@ -59,7 +59,7 @@ export function extractHandlerExportsFromChunk(
59
59
  if (detectPassthrough) {
60
60
  const eFnName = escapeRegExp(fnName);
61
61
  const callStartRe = new RegExp(
62
- `const\\s+${eName}\\s*=\\s*${eFnName}\\s*(?:<[^>]*>)?\\s*\\(`,
62
+ `(?:const|let|var)\\s+${eName}\\s*=\\s*${eFnName}\\s*(?:<[^>]*>)?\\s*\\(`,
63
63
  );
64
64
  const callStart = callStartRe.exec(chunkCode);
65
65
  if (callStart) {
@@ -98,8 +98,10 @@ export function evictHandlerCode(
98
98
  if (passthrough) continue;
99
99
 
100
100
  const eName = escapeRegExp(name);
101
+ // Match const/let/var: Rolldown (Vite 8) emits top-level bindings in the
102
+ // non-minified RSC bundle as `var`, whereas Rollup used `const`.
101
103
  const callStartRe = new RegExp(
102
- `const\\s+${eName}\\s*=\\s*${eFnName}\\s*(?:<[^>]*>)?\\s*\\(`,
104
+ `(?:const|let|var)\\s+${eName}\\s*=\\s*${eFnName}\\s*(?:<[^>]*>)?\\s*\\(`,
103
105
  );
104
106
  const startMatch = callStartRe.exec(modified);
105
107
  if (!startMatch) continue;
@@ -0,0 +1,190 @@
1
+ // Resolution of the public `clientChunks` option into the callback shape that
2
+ // @vitejs/plugin-rsc expects. See plugin-types.ts (ClientChunks) and
3
+ // docs/client-chunking.md for the contract. The mechanism: a distinct returned
4
+ // name yields a distinct, dynamically-imported client chunk, independent of how
5
+ // the RSC/server build chunked the importing modules.
6
+
7
+ import type { ClientChunkMeta, ClientChunks } from "../plugin-types.js";
8
+ import { createRangoDebugger, NS } from "../debug.js";
9
+ import { hashRefKey } from "../plugins/client-ref-hashing.js";
10
+
11
+ /** The callback shape @vitejs/plugin-rsc's `clientChunks` option accepts. */
12
+ export type RscClientChunksFn = (meta: ClientChunkMeta) => string | undefined;
13
+
14
+ /**
15
+ * Build-time context the discovery pass populates and the built-in strategy
16
+ * reads. It refines how the catch-all (no route-root marker) modules are grouped
17
+ * without touching marker splits or the shared runtime:
18
+ *
19
+ * - `fallbackRefs`: production hashes of the `"use client"` modules a consumer
20
+ * registered as `errorBoundary`/`notFoundBoundary` fallbacks. Pulled into a
21
+ * dedicated `app-fallback` chunk so the error UI is not co-bundled with the
22
+ * very route code it exists to catch failures for (resilience), and so the
23
+ * chunk it would otherwise sit in gets named after a real module rather than
24
+ * the boundary. Populated by reading each fallback element's client-reference
25
+ * `$$id` during discovery (see discover-routers).
26
+ */
27
+ export interface ClientChunkContext {
28
+ fallbackRefs: Set<string>;
29
+ }
30
+
31
+ /**
32
+ * Opt-in observability for the built-in strategy. The route-root marker list is
33
+ * intentionally finite (see {@link ROUTE_ROOT_DIRS}); a consumer whose layout
34
+ * has no recognized marker (e.g. `src/parts/<feature>/…`) silently inherits the
35
+ * default grouping (no per-route split). That silence is the only real downside
36
+ * of a convention-based default, so we make the decision observable: run a build
37
+ * with `DEBUG=rango:chunks` to see, per client module, which route group it was
38
+ * assigned to or why it fell back to the shared grouping. Zero cost when off
39
+ * (the debugger is `undefined` unless the namespace is enabled). For full control
40
+ * over any layout, pass a `clientChunks` function instead of relying on the
41
+ * convention — that is the supported configurability path, not widening the list.
42
+ */
43
+ const debugChunks = createRangoDebugger(NS.chunks);
44
+
45
+ /**
46
+ * Modules that must stay on the default (shared) grouping regardless of strategy:
47
+ * React, the router client runtime, and anything in node_modules. Splitting these
48
+ * out per route would fragment the shared baseline and regress cache reuse — they
49
+ * are loaded on every route, so they belong in shared chunks.
50
+ *
51
+ * The Rango runtime is matched by package root only: `@rangojs/router` (the
52
+ * installed/aliased name) and the workspace `packages/(rangojs-router|rsc-router)/(src|dist)/`.
53
+ * The `(src|dist)` anchor matches the package's own source/build output but NOT
54
+ * consumer apps that merely live under a `packages/rangojs-router/` ancestor (the
55
+ * in-repo e2e apps), so their app components remain splittable. We deliberately do
56
+ * NOT match a bare `/src/browser/`: that is a consumer-owned path (a consumer's own
57
+ * `src/browser/Foo.tsx` must still split).
58
+ *
59
+ * We test BOTH `meta.id` (absolute) and `meta.normalizedId`. `normalizedId` is the
60
+ * project-root-relative form plugin-rsc derives (e.g. `../../src/browser/react/Link.tsx`
61
+ * for the in-repo runtime), which the package-root patterns miss; the absolute `id`
62
+ * always contains the package's real location, so it reliably catches the runtime.
63
+ */
64
+ function isSharedRuntime(meta: ClientChunkMeta): boolean {
65
+ return [meta.id, meta.normalizedId].some(
66
+ (path) =>
67
+ path.includes("/node_modules/") ||
68
+ /\/@rangojs\/router\//.test(path) ||
69
+ /\/packages\/(rangojs-router|rsc-router)\/(src|dist)\//.test(path),
70
+ );
71
+ }
72
+
73
+ /** Sanitize a raw group name into a filesystem/Rollup-safe chunk name fragment. */
74
+ function sanitizeGroup(name: string): string {
75
+ return name.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "") || "app";
76
+ }
77
+
78
+ /**
79
+ * Directory names that conventionally hold one sub-directory per route/feature.
80
+ * When a `"use client"` module lives under one of these, the built-in strategy
81
+ * keys the chunk on the segment IMMEDIATELY AFTER the marker (the route id),
82
+ * rather than the module's immediate parent directory. This is what keeps
83
+ * `routes/foo/components/Button.tsx` and `routes/bar/components/Button.tsx` in
84
+ * `app-foo` / `app-bar` instead of colliding in a single `app-components`.
85
+ *
86
+ * Route identity lives in the path PREFIX; the immediate parent (a suffix) is
87
+ * only a reliable proxy for the un-nested `routes/<route>/Widget.tsx` layout.
88
+ */
89
+ const ROUTE_ROOT_DIRS = new Set([
90
+ "routes",
91
+ "route",
92
+ "pages",
93
+ "page",
94
+ "app",
95
+ "features",
96
+ "feature",
97
+ "views",
98
+ "view",
99
+ "handlers",
100
+ "urls",
101
+ "modules",
102
+ "screens",
103
+ "sections",
104
+ ]);
105
+
106
+ /**
107
+ * Built-in strategy used when `clientChunks: true` (also the default). Splits app
108
+ * client components by route/feature identity ONLY where it can recognize a route
109
+ * structure; everywhere else it inherits the default grouping (returns undefined).
110
+ * This conservatism is what makes it safe as a default:
111
+ *
112
+ * - A recognized route structure (`routes/<id>/…`, `app/<id>/…`, `handlers/<id>/…`
113
+ * etc.) splits into a per-route chunk `app-<id>`, at any nesting depth.
114
+ * - A flat `src/components/Button.tsx`, or host sub-apps already split by a dynamic
115
+ * `import()` boundary (each app's `serverChunk` differs), get `undefined` and so
116
+ * keep `@vitejs/plugin-rsc`'s default `serverChunk` grouping — i.e. NO change
117
+ * versus not enabling the option. Returning a parent-dir name here would instead
118
+ * merge unrelated modules (e.g. every host app's `components/Layout.tsx` into one
119
+ * `app-components`), re-introducing cross-app leakage.
120
+ *
121
+ * Resolution order:
122
+ * 1. Shared runtime (React / router / node_modules) -> `undefined` (never split).
123
+ * 2. A registered error/notFound fallback (`ctx.fallbackRefs`) -> `app-fallback`,
124
+ * regardless of location, so the error UI is decoupled from the happy path.
125
+ * 3. A {@link ROUTE_ROOT_DIRS} marker with a directory after it -> key on that
126
+ * next segment (the route id), robust to any nesting depth.
127
+ * 4. Otherwise `undefined` (inherit the default `serverChunk` grouping).
128
+ */
129
+ export function directoryClientChunks(
130
+ meta: ClientChunkMeta,
131
+ ctx?: ClientChunkContext,
132
+ ): string | undefined {
133
+ if (isSharedRuntime(meta)) {
134
+ // React / router runtime / node_modules: always shared, expected, uninteresting.
135
+ return undefined;
136
+ }
137
+ // Registered error/notFound fallbacks -> a dedicated chunk. The error UI must
138
+ // not co-bundle with the code it catches failures for, and removing it lets the
139
+ // chunk it would otherwise anchor be named after a real module, not the boundary.
140
+ if (
141
+ ctx?.fallbackRefs.size &&
142
+ ctx.fallbackRefs.has(hashRefKey(meta.normalizedId))
143
+ ) {
144
+ debugChunks?.("fallback %s -> app-fallback", meta.normalizedId);
145
+ return "app-fallback";
146
+ }
147
+ const segments = meta.normalizedId.split("/").filter(Boolean);
148
+ const dirCount = segments.length - 1; // exclude the filename
149
+ if (dirCount >= 1) {
150
+ // Route-root marker -> the segment after it is the route id. First marker
151
+ // wins, so a top-level route owns its whole subtree. The `< dirCount - 1`
152
+ // bound guarantees the segment after the marker is a directory, not the file.
153
+ for (let i = 0; i < dirCount - 1; i++) {
154
+ if (ROUTE_ROOT_DIRS.has(segments[i].toLowerCase())) {
155
+ const group = `app-${sanitizeGroup(segments[i + 1])}`;
156
+ debugChunks?.("split %s -> %s", meta.normalizedId, group);
157
+ return group;
158
+ }
159
+ }
160
+ }
161
+ // No recognized route structure -> inherit the default serverChunk grouping.
162
+ // This is the actionable "silent" case: app code that did NOT split by route.
163
+ // Surface it (under DEBUG=rango:chunks) so a consumer can see their layout
164
+ // missed the convention and either colocate under a marker dir or pass a fn.
165
+ debugChunks?.(
166
+ "shared %s (no route-root marker; inherits default grouping)",
167
+ meta.normalizedId,
168
+ );
169
+ return undefined;
170
+ }
171
+
172
+ /**
173
+ * Resolve a Rango `clientChunks` option into a @vitejs/plugin-rsc `clientChunks`
174
+ * callback, or `undefined` to leave plugin-rsc on its default (serverChunk)
175
+ * grouping.
176
+ *
177
+ * - `false` / `undefined` -> `undefined` (no override).
178
+ * - `true` -> the built-in {@link directoryClientChunks} strategy,
179
+ * bound to the discovery-populated {@link ClientChunkContext} (fallback chunk).
180
+ * - function -> the user's function, used verbatim (full control; the
181
+ * fallback refinement does not apply — the consumer owns the grouping).
182
+ */
183
+ export function resolveClientChunks(
184
+ option: ClientChunks | undefined,
185
+ ctx?: ClientChunkContext,
186
+ ): RscClientChunksFn | undefined {
187
+ if (!option) return undefined;
188
+ if (option === true) return (meta) => directoryClientChunks(meta, ctx);
189
+ return option;
190
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Discovery Runner Config Parity
3
+ *
4
+ * The discovery temp server (createTempRscServer) runs the user's handler
5
+ * graph through a throwaway Node Vite server built with `configFile: false`.
6
+ * Without help, that server only sees a fixed Rango-owned plugin set, so any
7
+ * user resolution is absent during discovery, prerender, and static handler
8
+ * rendering — even though it applies at request time. Two flavors of user
9
+ * resolution must be carried across:
10
+ *
11
+ * - Third-party resolveId plugins (e.g. vite-tsconfig-paths) — forwarded as
12
+ * plugin instances, see selectForwardableResolvePlugins.
13
+ * - Native config-driven resolution, including Vite 8's built-in
14
+ * `resolve.tsconfigPaths` (which supersedes vite-tsconfig-paths) — forwarded
15
+ * as the data slice, see pickForwardedRunnerConfig.
16
+ *
17
+ * These helpers extract the resolution-relevant slice of the user's resolved
18
+ * config (resolve.*, define, oxc) and forward the user's resolution plugins
19
+ * into the temp server so discovery resolves modules the same way the real
20
+ * environment does.
21
+ */
22
+
23
+ import type { Plugin, ResolvedConfig, UserConfig } from "vite";
24
+
25
+ /**
26
+ * Whether a user plugin must NOT be forwarded into the discovery temp server.
27
+ *
28
+ * Framework-owned plugins are matched precisely -- by exact name or a
29
+ * namespaced prefix -- rather than by a loose substring/word prefix, so an
30
+ * unrelated user resolver named e.g. `rsc-paths` or `cloudflare-kv-alias` is
31
+ * still forwarded (it would otherwise reproduce issue #500).
32
+ *
33
+ * - `vite:*` Vite core + official plugins (incl.
34
+ * @vitejs/plugin-react's `vite:react-*`). The temp
35
+ * server already provides its own core; forwarding
36
+ * these would duplicate or conflict with it.
37
+ * - `rsc` / `rsc:*` @vitejs/plugin-rsc. createTempRscServer instantiates
38
+ * its own rsc() plugin; forwarding would duplicate it.
39
+ * Matched exactly (`rsc`) or by the `rsc:` namespace --
40
+ * NOT every `rsc`-prefixed name.
41
+ * - `@rangojs/router*` Our own plugins. The discovery plugin spawns the
42
+ * temp server, so forwarding would recurse infinitely.
43
+ * - `@cloudflare/vite-plugin*` / @cloudflare/vite-plugin (emits the scoped
44
+ * `vite-plugin-cloudflare*` `@cloudflare/vite-plugin` and unscoped
45
+ * `vite-plugin-cloudflare` / `vite-plugin-cloudflare:*`).
46
+ * Forwarding re-inits workerd, defeating the Node temp
47
+ * server. Matched specifically so a scoped user resolver
48
+ * like `@cloudflare/kv-alias` is still forwarded.
49
+ */
50
+ function isDenied(name: string): boolean {
51
+ return (
52
+ name.startsWith("vite:") ||
53
+ name === "rsc" ||
54
+ name.startsWith("rsc:") ||
55
+ name.startsWith("@rangojs/router") ||
56
+ name.startsWith("@cloudflare/vite-plugin") ||
57
+ name.startsWith("vite-plugin-cloudflare")
58
+ );
59
+ }
60
+
61
+ /**
62
+ * A plugin participates in resolution if it exposes `resolveId` or `load`.
63
+ * Plugins that only transform, configure the server, or hook into the build
64
+ * lifecycle do not affect how bare specifiers resolve, so we skip them to keep
65
+ * the forwarded surface minimal.
66
+ */
67
+ function hasResolutionHooks(p: Plugin): boolean {
68
+ return Boolean((p as any).resolveId || (p as any).load);
69
+ }
70
+
71
+ /**
72
+ * Strip a resolved plugin instance down to its resolution hooks plus the
73
+ * gating fields that decide whether/where it runs.
74
+ *
75
+ * We reuse the SAME instance objects captured from `config.plugins`. By the
76
+ * time `configResolved` fires on the discovery plugin, every plugin's own
77
+ * `config`/`configResolved` has already run on the main server, so any state
78
+ * a `resolveId` hook depends on (e.g. vite-tsconfig-paths' compiled path
79
+ * matcher, held in closure) is already populated. Forwarding only the
80
+ * resolution hooks therefore preserves correct resolution while avoiding a
81
+ * second `buildStart`/`configureServer`/`config` lifecycle in the temp server.
82
+ *
83
+ * `enforce` and `applyToEnvironment` are preserved so ordering and per-environment
84
+ * gating match the real pipeline.
85
+ *
86
+ * `apply` is intentionally dropped. Vite filters plugins by `apply` against the
87
+ * command during config resolution, so the `config.plugins` we read here is
88
+ * already command-filtered by the main server (build: `apply: "build"` +
89
+ * unconditional; dev: `apply: "serve"` + unconditional). The discovery temp
90
+ * server is always created with `createServer` (`command === "serve"`), so a
91
+ * forwarded `apply: "build"` plugin would be filtered straight back out -- even
92
+ * during a production build, where build-only resolvers are exactly what
93
+ * static/prerender rendering needs. Since the source list is already correct for
94
+ * the current command, the forwarded copy must carry no `apply` gate.
95
+ */
96
+ function stripToResolutionHooks(p: Plugin): Plugin {
97
+ const stripped: Plugin = { name: p.name };
98
+ if ((p as any).enforce) (stripped as any).enforce = (p as any).enforce;
99
+ if ((p as any).applyToEnvironment)
100
+ (stripped as any).applyToEnvironment = (p as any).applyToEnvironment;
101
+ if ((p as any).resolveId) (stripped as any).resolveId = (p as any).resolveId;
102
+ if ((p as any).load) (stripped as any).load = (p as any).load;
103
+ return stripped;
104
+ }
105
+
106
+ /**
107
+ * Pick the user's resolution plugins from the resolved plugin list, denylist
108
+ * framework-owned plugins, keep only those with resolution hooks, and strip
109
+ * each to its resolution surface. Returns plugin objects safe to drop into the
110
+ * discovery temp server's `plugins` array.
111
+ */
112
+ export function selectForwardableResolvePlugins(
113
+ plugins: readonly Plugin[] | undefined,
114
+ ): Plugin[] {
115
+ if (!plugins) return [];
116
+ const forwarded: Plugin[] = [];
117
+ for (const p of plugins) {
118
+ const name = p?.name;
119
+ if (!name || isDenied(name)) continue;
120
+ if (!hasResolutionHooks(p)) continue;
121
+ forwarded.push(stripToResolutionHooks(p));
122
+ }
123
+ return forwarded;
124
+ }
125
+
126
+ /**
127
+ * The resolution-relevant slice of the user's resolved config that is plain
128
+ * data (no plugin re-execution): everything under `resolve` that influences
129
+ * how specifiers map to files, plus `define` and `oxc` so transforms and
130
+ * compile-time constants match request time.
131
+ */
132
+ export interface ForwardedRunnerConfig {
133
+ resolve: UserConfig["resolve"];
134
+ define: UserConfig["define"];
135
+ oxc: UserConfig["oxc"];
136
+ }
137
+
138
+ /**
139
+ * Extract the data-only config slice to mirror into the discovery temp server.
140
+ * `alias` is included here so callers no longer need to thread it separately.
141
+ *
142
+ * `tsconfigPaths` is forwarded so Vite 8's native tsconfig `paths` resolution
143
+ * (a top-level `resolve` flag, off by default) reaches the temp server. The
144
+ * server is created with `configFile: false` and an explicit, allowlisted
145
+ * resolve slice, so a flag that is not copied here is simply absent during
146
+ * discovery — which would make path-aliased imports fail at prerender/static
147
+ * time the same way unforwarded resolveId plugins did (issue #500).
148
+ *
149
+ * `oxc` keeps the user's options but always pins the RSC-required JSX runtime
150
+ * (automatic, react), since the temp server compiles the handler graph as
151
+ * React server components regardless of the user's app-level JSX config. Vite 8
152
+ * replaced the deprecated `esbuild` transform option with `oxc`, so we read and
153
+ * forward `oxc` exclusively — no `esbuild` field is touched.
154
+ */
155
+ export function pickForwardedRunnerConfig(
156
+ config: ResolvedConfig,
157
+ ): ForwardedRunnerConfig {
158
+ const r = config.resolve ?? ({} as ResolvedConfig["resolve"]);
159
+ const resolve: NonNullable<UserConfig["resolve"]> = {};
160
+ if (r.alias !== undefined) resolve.alias = r.alias as any;
161
+ if (r.dedupe !== undefined) resolve.dedupe = r.dedupe;
162
+ if (r.conditions !== undefined) resolve.conditions = r.conditions;
163
+ if (r.mainFields !== undefined) resolve.mainFields = r.mainFields;
164
+ if (r.extensions !== undefined) resolve.extensions = r.extensions;
165
+ if (r.preserveSymlinks !== undefined)
166
+ resolve.preserveSymlinks = r.preserveSymlinks;
167
+ if (r.tsconfigPaths !== undefined) resolve.tsconfigPaths = r.tsconfigPaths;
168
+
169
+ // Pin the RSC JSX runtime on top of the user's oxc options. The user's
170
+ // jsx sub-options (e.g. `development`) are preserved when present; only
171
+ // `runtime`/`importSource` are forced to the values the RSC compile needs.
172
+ const userOxc = config.oxc;
173
+ const userJsx =
174
+ userOxc &&
175
+ typeof userOxc === "object" &&
176
+ typeof userOxc.jsx === "object" &&
177
+ userOxc.jsx !== null
178
+ ? userOxc.jsx
179
+ : {};
180
+ const oxc: UserConfig["oxc"] =
181
+ userOxc && typeof userOxc === "object"
182
+ ? {
183
+ ...userOxc,
184
+ jsx: { ...userJsx, runtime: "automatic", importSource: "react" },
185
+ }
186
+ : { jsx: { runtime: "automatic", importSource: "react" } };
187
+
188
+ return {
189
+ resolve,
190
+ define: config.define,
191
+ oxc,
192
+ };
193
+ }
@@ -1,62 +1,11 @@
1
- /**
2
- * Flatten prefix tree leaf nodes into precomputed route entries.
3
- * Leaf nodes have no children (no nested includes), so their routes can be
4
- * used directly by evaluateLazyEntry() without running the handler.
5
- * Non-leaf nodes are skipped because they have nested lazy includes that
6
- * require the handler to run for discovery.
7
- */
8
- export function flattenLeafEntries(
9
- prefixTree: Record<string, any>,
10
- routeManifest: Record<string, string>,
11
- result: Array<{ staticPrefix: string; routes: Record<string, string> }>,
12
- ): void {
13
- function visit(node: any): void {
14
- const children = node.children || {};
15
- if (
16
- Object.keys(children).length === 0 &&
17
- node.routes &&
18
- node.routes.length > 0
19
- ) {
20
- // Leaf node: collect its routes from the manifest
21
- const routes: Record<string, string> = {};
22
- for (const name of node.routes) {
23
- if (name in routeManifest) {
24
- routes[name] = routeManifest[name];
25
- }
26
- }
27
- result.push({ staticPrefix: node.staticPrefix, routes });
28
- } else {
29
- // Non-leaf: recurse into children
30
- for (const child of Object.values(children)) {
31
- visit(child);
32
- }
33
- }
34
- }
35
- for (const node of Object.values(prefixTree)) {
36
- visit(node);
37
- }
38
- }
39
-
40
- /**
41
- * Walk prefix tree to map each route name to its scope's staticPrefix.
42
- */
43
- export function buildRouteToStaticPrefix(
44
- prefixTree: Record<string, any>,
45
- result: Record<string, string>,
46
- ): void {
47
- function visit(node: any): void {
48
- const sp = node.staticPrefix || "";
49
- for (const name of node.routes || []) {
50
- result[name] = sp;
51
- }
52
- for (const child of Object.values(node.children || {})) {
53
- visit(child);
54
- }
55
- }
56
- for (const node of Object.values(prefixTree)) {
57
- visit(node);
58
- }
59
- }
1
+ // Pure prefix-tree walks live in the build layer so runtime code can consume
2
+ // them without importing from vite/. Re-exported here for the vite-side
3
+ // callers (discover-routers, virtual-module-codegen) that already import them
4
+ // from this module.
5
+ export {
6
+ flattenLeafEntries,
7
+ buildRouteToStaticPrefix,
8
+ } from "../../build/prefix-tree-utils.js";
60
9
 
61
10
  /**
62
11
  * Wrap a value as `JSON.parse('...')` instead of a JS object literal.
@@ -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,40 @@ 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
+ // client.browser is intentionally NOT aliased. plugin-rsc injects it into
141
+ // the client env's optimizeDeps.include; Vite's manual-include path resolves
142
+ // and pre-bundles regardless of optimizeDeps.exclude, so aliasing would
143
+ // trigger esbuild pre-bundling of the CJS vendor file and bypass the
144
+ // cjs-to-esm transform that patches `require('react'|'react-dom')` into
145
+ // real ESM imports. The consumer may still see a single "Failed to resolve"
146
+ // warning for client.browser; runtime resolution from plugin-rsc's own
147
+ // importer works because Vite resolves relative to the importer (not root).
148
+ const specs = [
149
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
150
+ "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
151
+ ];
152
+ const aliases: Record<string, string> = {};
153
+ for (const spec of specs) {
154
+ try {
155
+ aliases[spec] = require.resolve(spec);
156
+ } catch {
157
+ // Spec unresolvable (unexpected but non-fatal — Vite will warn as before).
158
+ }
159
+ }
160
+ return aliases;
161
+ }
@@ -41,14 +41,29 @@ export function substituteRouteParams(
41
41
  let result = pattern;
42
42
  let hadOmittedOptional = false;
43
43
 
44
- // First pass: substitute provided params
44
+ // First pass: substitute provided params.
45
+ // Empty string on an optional placeholder is treated as omitted —
46
+ // caller-supplied params or `getParams()` shapes may pass `""` for an
47
+ // absent optional, so letting the second pass strip them keeps slash
48
+ // cleanup consistent. Empty string on required `:key` or wildcard
49
+ // `*key` still substitutes, matching prior behaviour.
45
50
  for (const [key, value] of Object.entries(params)) {
46
51
  const escaped = escapeRegExp(key);
47
- result = result.replace(
48
- new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
49
- encode(value),
50
- );
51
- result = result.replace(`*${key}`, encode(value));
52
+ if (value === "") {
53
+ // Only replace required placeholders (negative lookahead for `?`);
54
+ // leave `:key?` for the second pass.
55
+ result = result.replace(
56
+ new RegExp(`:${escaped}(\\([^)]*\\))?(?!\\?)`),
57
+ "",
58
+ );
59
+ result = result.replace(`*${key}`, "");
60
+ } else {
61
+ result = result.replace(
62
+ new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
63
+ encode(value),
64
+ );
65
+ result = result.replace(`*${key}`, encode(value));
66
+ }
52
67
  }
53
68
 
54
69
  // Second pass: strip remaining optional param placeholders not in params