@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
@@ -6,9 +6,9 @@
6
6
  */
7
7
 
8
8
  import { resolve } from "node:path";
9
- import { createHash } from "node:crypto";
10
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
9
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
11
10
  import { evictHandlerCode } from "../utils/bundle-analysis.js";
11
+ import { copyStagedBuildAssets } from "../utils/prerender-utils.js";
12
12
  import type { DiscoveryState } from "./state.js";
13
13
 
14
14
  /**
@@ -17,11 +17,11 @@ import type { DiscoveryState } from "./state.js";
17
17
  */
18
18
  export function postprocessBundle(state: DiscoveryState): void {
19
19
  const hasPrerenderData =
20
- state.prerenderCollectedData &&
21
- Object.keys(state.prerenderCollectedData).length > 0;
20
+ state.prerenderManifestEntries &&
21
+ Object.keys(state.prerenderManifestEntries).length > 0;
22
22
  const hasStaticData =
23
- state.staticCollectedData &&
24
- Object.keys(state.staticCollectedData).length > 0;
23
+ state.staticManifestEntries &&
24
+ Object.keys(state.staticManifestEntries).length > 0;
25
25
  if (!hasPrerenderData && !hasStaticData) return;
26
26
 
27
27
  // Find RSC entry (recorded in generateBundle, fallback to dist/rsc/index.js)
@@ -31,25 +31,25 @@ export function postprocessBundle(state: DiscoveryState): void {
31
31
  state.rscEntryFileName ?? "index.js",
32
32
  );
33
33
 
34
- // 1. Evict handler code from __prerender-handlers and __static-handlers chunks.
35
- // handlerChunkInfo/staticHandlerChunkInfo are populated by generateBundle
34
+ // 1. Evict handler code from whichever chunks contain handler exports.
35
+ // handlerChunkInfoMap/staticHandlerChunkInfoMap are populated by generateBundle
36
36
  // after the production RSC build. In Vite 6 multi-environment builds, the
37
- // RSC build runs twice (analysis + production). Chunk info is only available
38
- // after the production pass, so we run eviction whenever it becomes available.
37
+ // RSC build runs twice (analysis + production). The maps are cleared at the
38
+ // start of each generateBundle pass so only production data is used here.
39
39
  const evictionTargets: Array<{
40
- info: typeof state.handlerChunkInfo;
40
+ infos: Iterable<import("./state.js").ChunkInfo>;
41
41
  fnName: string;
42
42
  brand: string;
43
43
  label: string;
44
44
  }> = [
45
45
  {
46
- info: state.handlerChunkInfo,
46
+ infos: state.handlerChunkInfoMap.values(),
47
47
  fnName: "Prerender",
48
48
  brand: "prerenderHandler",
49
49
  label: "handler code from RSC bundle",
50
50
  },
51
51
  {
52
- info: state.staticHandlerChunkInfo,
52
+ infos: state.staticHandlerChunkInfoMap.values(),
53
53
  fnName: "Static",
54
54
  brand: "staticHandler",
55
55
  label: "static handler code",
@@ -57,70 +57,58 @@ export function postprocessBundle(state: DiscoveryState): void {
57
57
  ];
58
58
 
59
59
  for (const target of evictionTargets) {
60
- if (!target.info) continue;
61
- const chunkPath = resolve(
62
- state.projectRoot,
63
- "dist/rsc",
64
- target.info.fileName,
65
- );
66
- try {
67
- const code = readFileSync(chunkPath, "utf-8");
68
- const result = evictHandlerCode(
69
- code,
70
- target.info.exports,
71
- target.fnName,
72
- target.brand,
73
- );
74
- if (result) {
75
- writeFileSync(chunkPath, result.code);
76
- const savedKB = (result.savedBytes / 1024).toFixed(1);
77
- console.log(
78
- `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${target.info.fileName}`,
60
+ for (const info of target.infos) {
61
+ const chunkPath = resolve(state.projectRoot, "dist/rsc", info.fileName);
62
+ try {
63
+ const code = readFileSync(chunkPath, "utf-8");
64
+ const result = evictHandlerCode(
65
+ code,
66
+ info.exports,
67
+ target.fnName,
68
+ target.brand,
69
+ );
70
+ if (result) {
71
+ writeFileSync(chunkPath, result.code);
72
+ const savedKB = (result.savedBytes / 1024).toFixed(1);
73
+ console.log(
74
+ `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${info.fileName}`,
75
+ );
76
+ }
77
+ } catch (replaceErr: any) {
78
+ console.warn(
79
+ `[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`,
79
80
  );
80
81
  }
81
- } catch (replaceErr: any) {
82
- console.warn(
83
- `[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`,
84
- );
85
82
  }
86
83
  }
87
- state.handlerChunkInfo = null;
88
- state.staticHandlerChunkInfo = null;
84
+ state.handlerChunkInfoMap.clear();
85
+ state.staticHandlerChunkInfoMap.clear();
89
86
 
90
87
  // 2. Write prerender data as separate importable asset modules
91
- // and inject a manifest import into the RSC entry.
88
+ // and inject a lazy manifest loader into the RSC entry.
92
89
  if (hasPrerenderData && existsSync(rscEntryPath)) {
93
90
  const rscCode = readFileSync(rscEntryPath, "utf-8");
94
- // Check for the specific injection marker, not just the variable name.
95
- // The runtime code (prerender store) also references __PRERENDER_MANIFEST,
96
- // so a broad string check would false-positive and skip injection.
91
+ // Check for the specific injection marker to avoid double-injection.
97
92
  if (!rscCode.includes("__prerender-manifest.js")) {
98
93
  try {
99
- const assetsDir = resolve(state.projectRoot, "dist/rsc/assets");
100
- mkdirSync(assetsDir, { recursive: true });
101
-
102
- const manifestEntries: string[] = [];
103
- let totalBytes = 0;
94
+ let totalBytes = copyStagedBuildAssets(
95
+ state.projectRoot,
96
+ Object.values(state.prerenderManifestEntries!),
97
+ );
104
98
 
105
- for (const [key, entry] of Object.entries(
106
- state.prerenderCollectedData!,
99
+ const manifestMap: Record<string, string> = {};
100
+ for (const [key, assetFileName] of Object.entries(
101
+ state.prerenderManifestEntries!,
107
102
  )) {
108
- const entryJson = JSON.stringify(entry);
109
- const contentHash = createHash("sha256")
110
- .update(entryJson)
111
- .digest("hex")
112
- .slice(0, 8);
113
- const assetFileName = `__pr-${contentHash}.js`;
114
- const assetPath = resolve(assetsDir, assetFileName);
115
- const assetCode = `export default ${entryJson};\n`;
116
- writeFileSync(assetPath, assetCode);
117
- totalBytes += Buffer.byteLength(assetCode);
118
- manifestEntries.push(
119
- `${JSON.stringify(key)}:()=>import("./assets/${assetFileName}")`,
120
- );
103
+ manifestMap[key] = `./assets/${assetFileName}`;
121
104
  }
122
105
 
123
- const manifestCode = `const m={${manifestEntries.join(",")}};export default m;\n`;
106
+ const manifestCode = [
107
+ `const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
108
+ `export function loadPrerenderAsset(s){return import(s)}`,
109
+ `export default m;`,
110
+ "",
111
+ ].join("\n");
124
112
  const manifestPath = resolve(
125
113
  state.projectRoot,
126
114
  "dist/rsc/__prerender-manifest.js",
@@ -128,12 +116,12 @@ export function postprocessBundle(state: DiscoveryState): void {
128
116
  writeFileSync(manifestPath, manifestCode);
129
117
  totalBytes += Buffer.byteLength(manifestCode);
130
118
 
131
- const injection = `import __pm from "./__prerender-manifest.js";\nglobalThis.__PRERENDER_MANIFEST = __pm;\n`;
119
+ const injection = `globalThis.__loadPrerenderManifestModule = () => import("./__prerender-manifest.js");\n`;
132
120
  writeFileSync(rscEntryPath, injection + rscCode);
133
121
 
134
122
  const totalKB = (totalBytes / 1024).toFixed(1);
135
123
  console.log(
136
- `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderCollectedData!).length} entries)`,
124
+ `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries!).length} entries)`,
137
125
  );
138
126
  } catch (err: any) {
139
127
  throw new Error(
@@ -147,33 +135,17 @@ export function postprocessBundle(state: DiscoveryState): void {
147
135
  // and inject a __STATIC_MANIFEST import into the RSC entry.
148
136
  if (hasStaticData && existsSync(rscEntryPath)) {
149
137
  const rscCode = readFileSync(rscEntryPath, "utf-8");
150
- if (!rscCode.includes("__STATIC_MANIFEST")) {
138
+ if (!rscCode.includes("__static-manifest.js")) {
151
139
  try {
152
- const assetsDir = resolve(state.projectRoot, "dist/rsc/assets");
153
- mkdirSync(assetsDir, { recursive: true });
154
-
155
140
  const manifestEntries: string[] = [];
156
- let totalBytes = 0;
141
+ let totalBytes = copyStagedBuildAssets(
142
+ state.projectRoot,
143
+ Object.values(state.staticManifestEntries!),
144
+ );
157
145
 
158
- for (const [handlerId, { encoded, handles }] of Object.entries(
159
- state.staticCollectedData!,
146
+ for (const [handlerId, assetFileName] of Object.entries(
147
+ state.staticManifestEntries!,
160
148
  )) {
161
- // Store both the Flight payload and handle data
162
- const hasHandles = Object.keys(handles).length > 0;
163
- const exportValue = hasHandles
164
- ? JSON.stringify({ encoded, handles })
165
- : JSON.stringify(encoded);
166
- // Hash the full payload that is written so distinct handle
167
- // snapshots produce distinct asset filenames.
168
- const contentHash = createHash("sha256")
169
- .update(exportValue)
170
- .digest("hex")
171
- .slice(0, 8);
172
- const assetFileName = `__st-${contentHash}.js`;
173
- const assetPath = resolve(assetsDir, assetFileName);
174
- const assetCode = `export default ${exportValue};\n`;
175
- writeFileSync(assetPath, assetCode);
176
- totalBytes += Buffer.byteLength(assetCode);
177
149
  manifestEntries.push(
178
150
  `${JSON.stringify(handlerId)}:()=>import("./assets/${assetFileName}")`,
179
151
  );
@@ -197,7 +169,7 @@ export function postprocessBundle(state: DiscoveryState): void {
197
169
 
198
170
  const totalKB = (totalBytes / 1024).toFixed(1);
199
171
  console.log(
200
- `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticCollectedData!).length} entries)`,
172
+ `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries!).length} entries)`,
201
173
  );
202
174
  } catch (err: any) {
203
175
  throw new Error(
@@ -48,9 +48,8 @@ export async function discoverRouters(
48
48
  // No RSC routers found directly. Check for host routers with lazy handlers
49
49
  // that need to be resolved to trigger sub-app createRouter() calls.
50
50
  try {
51
- const hostMod = await rscEnv.runner.import("@rangojs/router/host");
52
51
  const hostRegistry: Map<string, any> | undefined =
53
- hostMod.HostRouterRegistry;
52
+ serverMod.HostRouterRegistry;
54
53
 
55
54
  if (hostRegistry && hostRegistry.size > 0) {
56
55
  console.log(
@@ -89,7 +88,7 @@ export async function discoverRouters(
89
88
  }
90
89
  }
91
90
  } catch {
92
- // @rangojs/router/host not available or import failed, skip
91
+ // Host-router discovery is best-effort; skip if unavailable
93
92
  }
94
93
 
95
94
  // If still no routers after host router resolution, fail
@@ -136,7 +135,11 @@ export async function discoverRouters(
136
135
  continue;
137
136
  }
138
137
 
139
- const manifest = generateManifestFull(router.urlpatterns, routerMountIndex);
138
+ const manifest = generateManifestFull(
139
+ router.urlpatterns,
140
+ routerMountIndex,
141
+ router.__basename ? { urlPrefix: router.__basename } : undefined,
142
+ );
140
143
  routerMountIndex++;
141
144
  allManifests.push({ id, manifest });
142
145
  const routeCount = Object.keys(manifest.routeManifest).length;
@@ -13,12 +13,14 @@ import {
13
13
  runWithConcurrency,
14
14
  groupByConcurrency,
15
15
  notifyOnError,
16
+ stageBuildAssetModule,
16
17
  } from "../utils/prerender-utils.js";
17
18
  import type { DiscoveryState } from "./state.js";
18
19
 
19
20
  /**
20
21
  * Expand prerender routes into concrete URLs and render them via the
21
- * RSC runner. Stores collected data in state.prerenderCollectedData.
22
+ * RSC runner. Stages asset modules and stores key-to-file entries in
23
+ * state.prerenderManifestEntries.
22
24
  */
23
25
  export async function expandPrerenderRoutes(
24
26
  state: DiscoveryState,
@@ -49,93 +51,144 @@ export async function expandPrerenderRoutes(
49
51
  return substituteRouteParams(pattern, params);
50
52
  };
51
53
 
54
+ let resolvedRoutes = 0;
55
+ let totalDynamic = 0;
56
+
57
+ // Count dynamic routes upfront for progress reporting
52
58
  for (const { manifest } of allManifests) {
53
59
  if (!manifest.prerenderRoutes) continue;
54
- const defs = manifest._prerenderDefs || {};
55
60
  for (const routeName of manifest.prerenderRoutes) {
56
61
  const pattern = manifest.routeManifest[routeName];
57
- if (!pattern) continue;
58
- const def = defs[routeName];
59
- const isPassthroughRoute = !!def?.options?.passthrough;
60
- const hasDynamic = pattern.includes(":") || pattern.includes("*");
61
- if (!hasDynamic) {
62
- // Static route: use pattern directly (strip trailing slash for URL)
63
- entries.push({
64
- urlPath: pattern.replace(/\/$/, "") || "/",
65
- routeName,
66
- concurrency: 1,
67
- isPassthroughRoute,
68
- });
69
- } else {
70
- // Dynamic route: call getParams() to enumerate param combinations
71
- if (def?.getParams) {
72
- try {
73
- const buildVars: Record<string, any> = {};
74
- const getParamsCtx = {
75
- build: true as const,
76
- set: ((keyOrVar: any, value: any) => {
77
- contextSet(buildVars, keyOrVar, value);
78
- }) as any,
79
- reverse: getParamsReverse,
80
- };
81
- const paramsList = await def.getParams(getParamsCtx);
82
- const concurrency = def.options?.concurrency ?? 1;
83
- const hasBuildVars =
84
- Object.keys(buildVars).length > 0 ||
85
- Object.getOwnPropertySymbols(buildVars).length > 0;
86
- for (const params of paramsList) {
87
- let url = substituteRouteParams(
88
- pattern,
89
- params as Record<string, string>,
90
- encodePathParam,
91
- );
92
- // Anonymous wildcard fallback: use conventional keys if provided
93
- if (url.includes("*")) {
94
- const wildcardValue =
95
- (params as Record<string, string>)["*"] ??
96
- (params as Record<string, string>).splat;
97
- if (wildcardValue !== undefined) {
98
- url = url.replace(/\*[^/]*$/, encodePathParam(wildcardValue));
62
+ if (pattern && (pattern.includes(":") || pattern.includes("*"))) {
63
+ totalDynamic++;
64
+ }
65
+ }
66
+ }
67
+
68
+ // Periodic progress log so long getParams() calls don't look stalled
69
+ const paramsStart = performance.now();
70
+ const progressInterval =
71
+ totalDynamic > 0
72
+ ? setInterval(() => {
73
+ const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
74
+ console.log(
75
+ `[rsc-router] Resolving prerender params... ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
76
+ );
77
+ }, 5000)
78
+ : undefined;
79
+
80
+ try {
81
+ for (const { manifest } of allManifests) {
82
+ if (!manifest.prerenderRoutes) continue;
83
+ const defs = manifest._prerenderDefs || {};
84
+ const passthroughSet = new Set(manifest.passthroughRoutes || []);
85
+ for (const routeName of manifest.prerenderRoutes) {
86
+ const pattern = manifest.routeManifest[routeName];
87
+ if (!pattern) continue;
88
+ const def = defs[routeName];
89
+ const isPassthroughRoute = passthroughSet.has(routeName);
90
+ const hasDynamic = pattern.includes(":") || pattern.includes("*");
91
+ if (!hasDynamic) {
92
+ // Static route: use pattern directly (strip trailing slash for URL)
93
+ entries.push({
94
+ urlPath: pattern.replace(/\/$/, "") || "/",
95
+ routeName,
96
+ concurrency: 1,
97
+ isPassthroughRoute,
98
+ });
99
+ } else {
100
+ // Dynamic route: call getParams() to enumerate param combinations
101
+ if (def?.getParams) {
102
+ try {
103
+ const buildVars: Record<string, any> = {};
104
+ const buildEnv = state.resolvedBuildEnv;
105
+ const getParamsCtx = {
106
+ build: true as const,
107
+ dev: !state.isBuildMode,
108
+ set: ((keyOrVar: any, value: any) => {
109
+ contextSet(buildVars, keyOrVar, value);
110
+ }) as any,
111
+ reverse: getParamsReverse,
112
+ get env() {
113
+ if (buildEnv !== undefined) return buildEnv;
114
+ throw new Error(
115
+ "[rsc-router] ctx.env is not available during build-time getParams(). " +
116
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
117
+ );
118
+ },
119
+ };
120
+ const paramsList = await def.getParams(getParamsCtx);
121
+ const concurrency = def.options?.concurrency ?? 1;
122
+ const hasBuildVars =
123
+ Object.keys(buildVars).length > 0 ||
124
+ Object.getOwnPropertySymbols(buildVars).length > 0;
125
+ for (const params of paramsList) {
126
+ let url = substituteRouteParams(
127
+ pattern,
128
+ params as Record<string, string>,
129
+ encodePathParam,
130
+ );
131
+ // Anonymous wildcard fallback: use conventional keys if provided
132
+ if (url.includes("*")) {
133
+ const wildcardValue =
134
+ (params as Record<string, string>)["*"] ??
135
+ (params as Record<string, string>).splat;
136
+ if (wildcardValue !== undefined) {
137
+ url = url.replace(
138
+ /\*[^/]*$/,
139
+ encodePathParam(wildcardValue),
140
+ );
141
+ }
99
142
  }
143
+ entries.push({
144
+ urlPath: url.replace(/\/$/, "") || "/",
145
+ routeName,
146
+ concurrency,
147
+ ...(hasBuildVars ? { buildVars } : {}),
148
+ isPassthroughRoute,
149
+ });
100
150
  }
101
- entries.push({
102
- urlPath: url.replace(/\/$/, "") || "/",
103
- routeName,
104
- concurrency,
105
- ...(hasBuildVars ? { buildVars } : {}),
106
- isPassthroughRoute,
107
- });
108
- }
109
- } catch (err: any) {
110
- // Skip in getParams() skips the entire route
111
- if (err.name === "Skip") {
112
- console.log(
113
- `[rsc-router] SKIP route "${routeName}" - ${err.message}`,
114
- );
115
- notifyOnError(
116
- registry,
117
- err,
118
- "prerender",
119
- routeName,
120
- undefined,
121
- true,
151
+ resolvedRoutes++;
152
+ } catch (err: any) {
153
+ resolvedRoutes++;
154
+ // Skip in getParams() skips the entire route
155
+ if (err.name === "Skip") {
156
+ console.log(
157
+ `[rsc-router] SKIP route "${routeName}" - ${err.message}`,
158
+ );
159
+ notifyOnError(
160
+ registry,
161
+ err,
162
+ "prerender",
163
+ routeName,
164
+ undefined,
165
+ true,
166
+ );
167
+ continue;
168
+ }
169
+ // Regular error: fail the build
170
+ console.error(
171
+ `[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`,
122
172
  );
123
- continue;
173
+ notifyOnError(registry, err, "prerender", routeName);
174
+ throw err;
124
175
  }
125
- // Regular error: fail the build
126
- console.error(
127
- `[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`,
176
+ } else {
177
+ console.warn(
178
+ `[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
128
179
  );
129
- notifyOnError(registry, err, "prerender", routeName);
130
- throw err;
131
180
  }
132
- } else {
133
- console.warn(
134
- `[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
135
- );
136
181
  }
137
182
  }
138
183
  }
184
+ } finally {
185
+ if (progressInterval) {
186
+ clearInterval(progressInterval);
187
+ const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
188
+ console.log(
189
+ `[rsc-router] Resolved prerender params: ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
190
+ );
191
+ }
139
192
  }
140
193
 
141
194
  if (entries.length === 0) return;
@@ -150,7 +203,7 @@ export async function expandPrerenderRoutes(
150
203
 
151
204
  const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
152
205
 
153
- const collectedData: Record<string, any> = {};
206
+ const manifestEntries: Record<string, string> = {};
154
207
  let doneCount = 0;
155
208
  let skipCount = 0;
156
209
  const startTotal = performance.now();
@@ -173,6 +226,7 @@ export async function expandPrerenderRoutes(
173
226
  {},
174
227
  entry.buildVars,
175
228
  entry.isPassthroughRoute,
229
+ state.resolvedBuildEnv,
176
230
  );
177
231
  if (!result) continue;
178
232
 
@@ -187,18 +241,30 @@ export async function expandPrerenderRoutes(
187
241
  }
188
242
 
189
243
  const paramHash = hashParams(result.params || {});
190
- collectedData[`${result.routeName}/${paramHash}`] = {
244
+ const mainKey = `${result.routeName}/${paramHash}`;
245
+ const mainValue = JSON.stringify({
191
246
  segments: result.segments,
192
247
  handles: result.handles,
193
- };
248
+ });
249
+ manifestEntries[mainKey] = stageBuildAssetModule(
250
+ state.projectRoot,
251
+ "__pr",
252
+ mainValue,
253
+ );
194
254
  if (result.interceptSegments?.length) {
195
- collectedData[`${result.routeName}/${paramHash}/i`] = {
255
+ const interceptKey = `${result.routeName}/${paramHash}/i`;
256
+ const interceptValue = JSON.stringify({
196
257
  segments: [...result.segments, ...result.interceptSegments],
197
258
  handles: {
198
259
  ...result.handles,
199
260
  ...(result.interceptHandles || {}),
200
261
  },
201
- };
262
+ });
263
+ manifestEntries[interceptKey] = stageBuildAssetModule(
264
+ state.projectRoot,
265
+ "__pr",
266
+ interceptValue,
267
+ );
202
268
  }
203
269
  const elapsed = (performance.now() - startUrl).toFixed(0);
204
270
  console.log(
@@ -244,7 +310,7 @@ export async function expandPrerenderRoutes(
244
310
 
245
311
  const totalElapsed = (performance.now() - startTotal).toFixed(0);
246
312
  if (doneCount > 0) {
247
- state.prerenderCollectedData = collectedData;
313
+ state.prerenderManifestEntries = manifestEntries;
248
314
  }
249
315
  const parts = [`${doneCount} done`];
250
316
  if (skipCount > 0) parts.push(`${skipCount} skipped`);
@@ -256,7 +322,8 @@ export async function expandPrerenderRoutes(
256
322
  /**
257
323
  * Render Static handlers at build time. Each Static handler is called
258
324
  * with a synthetic BuildContext and its output is RSC-serialized.
259
- * Stores collected data in state.staticCollectedData.
325
+ * Stages asset modules and stores handlerId-to-file entries in
326
+ * state.staticManifestEntries.
260
327
  */
261
328
  export async function renderStaticHandlers(
262
329
  state: DiscoveryState,
@@ -270,10 +337,7 @@ export async function renderStaticHandlers(
270
337
  )
271
338
  return;
272
339
 
273
- const collected: Record<
274
- string,
275
- { encoded: string; handles: Record<string, unknown[]> }
276
- > = {};
340
+ const manifestEntries: Record<string, string> = {};
277
341
  let staticDone = 0;
278
342
  let staticSkip = 0;
279
343
  let totalStaticCount = 0;
@@ -314,9 +378,19 @@ export async function renderStaticHandlers(
314
378
  def.handler,
315
379
  def.$$id,
316
380
  (def as any).$$routePrefix,
381
+ state.resolvedBuildEnv,
382
+ !state.isBuildMode,
317
383
  );
318
384
  if (result) {
319
- collected[def.$$id] = result;
385
+ const hasHandles = Object.keys(result.handles).length > 0;
386
+ const exportValue = hasHandles
387
+ ? JSON.stringify(result)
388
+ : JSON.stringify(result.encoded);
389
+ manifestEntries[def.$$id] = stageBuildAssetModule(
390
+ state.projectRoot,
391
+ "__st",
392
+ exportValue,
393
+ );
320
394
  const elapsed = (performance.now() - startHandler).toFixed(0);
321
395
  console.log(
322
396
  `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`,
@@ -355,7 +429,7 @@ export async function renderStaticHandlers(
355
429
 
356
430
  const totalStaticElapsed = (performance.now() - startStatic).toFixed(0);
357
431
  if (staticDone > 0) {
358
- state.staticCollectedData = collected;
432
+ state.staticManifestEntries = manifestEntries;
359
433
  }
360
434
  const staticParts = [`${staticDone} done`];
361
435
  if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);