@rangojs/router 0.0.0-experimental.98 → 0.0.0-experimental.98914650

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 (355) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +60 -11
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/intercept/SKILL.md +29 -5
  18. package/skills/layout/SKILL.md +13 -9
  19. package/skills/links/SKILL.md +173 -17
  20. package/skills/loader/SKILL.md +170 -23
  21. package/skills/middleware/SKILL.md +16 -10
  22. package/skills/migrate-nextjs/SKILL.md +38 -16
  23. package/skills/mime-routes/SKILL.md +27 -0
  24. package/skills/observability/SKILL.md +137 -0
  25. package/skills/parallel/SKILL.md +11 -7
  26. package/skills/prerender/SKILL.md +14 -33
  27. package/skills/rango/SKILL.md +250 -26
  28. package/skills/react-compiler/SKILL.md +168 -0
  29. package/skills/response-routes/SKILL.md +114 -47
  30. package/skills/route/SKILL.md +22 -5
  31. package/skills/router-setup/SKILL.md +3 -3
  32. package/skills/server-actions/SKILL.md +78 -42
  33. package/skills/tailwind/SKILL.md +27 -3
  34. package/skills/testing/SKILL.md +129 -0
  35. package/skills/testing/bindings.md +89 -0
  36. package/skills/testing/cache-prerender.md +124 -0
  37. package/skills/testing/client-components.md +122 -0
  38. package/skills/testing/e2e-parity.md +125 -0
  39. package/skills/testing/flight.md +92 -0
  40. package/skills/testing/handles.md +129 -0
  41. package/skills/testing/loader.md +128 -0
  42. package/skills/testing/middleware.md +99 -0
  43. package/skills/testing/render-handler.md +121 -0
  44. package/skills/testing/response-routes.md +95 -0
  45. package/skills/testing/reverse-and-types.md +84 -0
  46. package/skills/testing/server-actions.md +107 -0
  47. package/skills/testing/server-tree.md +128 -0
  48. package/skills/testing/setup.md +120 -0
  49. package/skills/typesafety/SKILL.md +310 -26
  50. package/skills/use-cache/SKILL.md +36 -5
  51. package/skills/vercel/SKILL.md +107 -0
  52. package/skills/view-transitions/SKILL.md +294 -0
  53. package/src/__augment-tests__/augment.ts +81 -0
  54. package/src/__augment-tests__/augmented.check.ts +116 -0
  55. package/src/__internal.ts +0 -65
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/action-fence.ts +47 -0
  58. package/src/browser/app-shell.ts +14 -27
  59. package/src/browser/cookie-name.ts +140 -0
  60. package/src/browser/event-controller.ts +37 -143
  61. package/src/browser/history-state.ts +21 -0
  62. package/src/browser/index.ts +3 -3
  63. package/src/browser/invalidate-client-cache.ts +52 -0
  64. package/src/browser/navigation-bridge.ts +30 -59
  65. package/src/browser/navigation-client.ts +96 -84
  66. package/src/browser/navigation-store-handle.ts +38 -0
  67. package/src/browser/navigation-store.ts +32 -82
  68. package/src/browser/navigation-transaction.ts +9 -59
  69. package/src/browser/partial-update.ts +60 -127
  70. package/src/browser/prefetch/cache.ts +82 -72
  71. package/src/browser/prefetch/fetch.ts +108 -33
  72. package/src/browser/prefetch/queue.ts +6 -3
  73. package/src/browser/rango-state.ts +157 -115
  74. package/src/browser/react/Link.tsx +0 -2
  75. package/src/browser/react/NavigationProvider.tsx +41 -48
  76. package/src/browser/react/ScrollRestoration.tsx +10 -6
  77. package/src/browser/react/filter-segment-order.ts +0 -2
  78. package/src/browser/react/index.ts +0 -48
  79. package/src/browser/react/location-state-shared.ts +166 -8
  80. package/src/browser/react/location-state.ts +39 -14
  81. package/src/browser/react/use-action.ts +6 -15
  82. package/src/browser/react/use-handle.ts +17 -14
  83. package/src/browser/react/use-link-status.ts +0 -4
  84. package/src/browser/react/use-navigation.ts +0 -3
  85. package/src/browser/react/use-params.ts +3 -6
  86. package/src/browser/react/use-reverse.ts +106 -0
  87. package/src/browser/react/use-router.ts +20 -5
  88. package/src/browser/react/use-search-params.ts +0 -5
  89. package/src/browser/react/use-segments.ts +0 -13
  90. package/src/browser/response-adapter.ts +52 -1
  91. package/src/browser/rsc-router.tsx +70 -34
  92. package/src/browser/scroll-restoration.ts +22 -14
  93. package/src/browser/segment-structure-assert.ts +2 -2
  94. package/src/browser/server-action-bridge.ts +168 -44
  95. package/src/browser/types.ts +36 -21
  96. package/src/browser/validate-redirect-origin.ts +43 -16
  97. package/src/build/collect-fallback-refs.ts +107 -0
  98. package/src/build/generate-manifest.ts +60 -35
  99. package/src/build/generate-route-types.ts +3 -0
  100. package/src/build/index.ts +8 -2
  101. package/src/build/prefix-tree-utils.ts +123 -0
  102. package/src/build/route-trie.ts +89 -11
  103. package/src/build/route-types/codegen.ts +4 -4
  104. package/src/build/route-types/include-resolution.ts +1 -1
  105. package/src/build/route-types/param-extraction.ts +6 -3
  106. package/src/build/route-types/per-module-writer.ts +7 -4
  107. package/src/build/route-types/router-processing.ts +122 -22
  108. package/src/build/route-types/scan-filter.ts +1 -1
  109. package/src/build/route-types/source-scan.ts +118 -0
  110. package/src/build/runtime-discovery.ts +9 -20
  111. package/src/cache/cache-error.ts +104 -0
  112. package/src/cache/cache-policy.ts +68 -28
  113. package/src/cache/cache-runtime.ts +134 -32
  114. package/src/cache/cache-scope.ts +100 -74
  115. package/src/cache/cache-tag.ts +98 -0
  116. package/src/cache/cf/cf-cache-store.ts +2255 -238
  117. package/src/cache/cf/index.ts +6 -16
  118. package/src/cache/document-cache.ts +61 -20
  119. package/src/cache/handle-snapshot.ts +63 -0
  120. package/src/cache/index.ts +22 -20
  121. package/src/cache/memory-segment-store.ts +136 -37
  122. package/src/cache/profile-registry.ts +6 -30
  123. package/src/cache/read-through-swr.ts +41 -11
  124. package/src/cache/segment-codec.ts +0 -16
  125. package/src/cache/tag-invalidation.ts +230 -0
  126. package/src/cache/types.ts +33 -100
  127. package/src/cache/vercel/index.ts +11 -0
  128. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  129. package/src/client.rsc.tsx +6 -21
  130. package/src/client.tsx +25 -61
  131. package/src/component-utils.ts +19 -0
  132. package/src/context-var.ts +17 -5
  133. package/src/decode-loader-results.ts +36 -0
  134. package/src/defer.ts +196 -0
  135. package/src/deps/ssr.ts +0 -1
  136. package/src/errors.ts +30 -4
  137. package/src/handle.ts +31 -23
  138. package/src/handles/MetaTags.tsx +0 -14
  139. package/src/handles/breadcrumbs.ts +16 -5
  140. package/src/handles/meta.ts +0 -39
  141. package/src/host/cookie-handler.ts +0 -36
  142. package/src/host/errors.ts +0 -24
  143. package/src/host/index.ts +8 -2
  144. package/src/host/pattern-matcher.ts +7 -50
  145. package/src/host/router.ts +107 -99
  146. package/src/host/testing.ts +40 -27
  147. package/src/host/types.ts +37 -4
  148. package/src/host/utils.ts +1 -1
  149. package/src/href-client.ts +137 -22
  150. package/src/index.rsc.ts +63 -9
  151. package/src/index.ts +64 -9
  152. package/src/internal-debug.ts +2 -4
  153. package/src/loader-store.ts +500 -0
  154. package/src/loader.rsc.ts +20 -13
  155. package/src/loader.ts +12 -11
  156. package/src/missing-id-error.ts +68 -0
  157. package/src/network-error-thrower.tsx +1 -6
  158. package/src/outlet-provider.tsx +1 -5
  159. package/src/prerender/param-hash.ts +10 -11
  160. package/src/prerender/store.ts +32 -37
  161. package/src/prerender.ts +61 -6
  162. package/src/redirect-origin.ts +100 -0
  163. package/src/response-utils.ts +9 -0
  164. package/src/reverse.ts +65 -41
  165. package/src/root-error-boundary.tsx +1 -19
  166. package/src/route-content-wrapper.tsx +7 -72
  167. package/src/route-definition/dsl-helpers.ts +244 -281
  168. package/src/route-definition/helper-factories.ts +29 -139
  169. package/src/route-definition/helpers-types.ts +40 -17
  170. package/src/route-definition/redirect.ts +43 -9
  171. package/src/route-definition/resolve-handler-use.ts +6 -0
  172. package/src/route-definition/use-item-types.ts +32 -0
  173. package/src/route-map-builder.ts +0 -16
  174. package/src/route-types.ts +19 -41
  175. package/src/router/basename.ts +14 -0
  176. package/src/router/content-negotiation.ts +15 -15
  177. package/src/router/error-handling.ts +13 -17
  178. package/src/router/find-match.ts +44 -23
  179. package/src/router/handler-context.ts +4 -42
  180. package/src/router/intercept-resolution.ts +14 -19
  181. package/src/router/lazy-includes.ts +9 -46
  182. package/src/router/loader-resolution.ts +91 -46
  183. package/src/router/logging.ts +0 -6
  184. package/src/router/manifest.ts +18 -29
  185. package/src/router/match-api.ts +0 -20
  186. package/src/router/match-context.ts +0 -22
  187. package/src/router/match-handlers.ts +57 -58
  188. package/src/router/match-middleware/background-revalidation.ts +0 -7
  189. package/src/router/match-middleware/cache-lookup.ts +150 -271
  190. package/src/router/match-middleware/cache-store.ts +3 -33
  191. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  192. package/src/router/match-middleware/segment-resolution.ts +0 -22
  193. package/src/router/match-pipelines.ts +1 -42
  194. package/src/router/match-result.ts +31 -80
  195. package/src/router/metrics.ts +0 -34
  196. package/src/router/middleware-types.ts +0 -116
  197. package/src/router/middleware.ts +118 -133
  198. package/src/router/navigation-snapshot.ts +0 -51
  199. package/src/router/params-util.ts +23 -0
  200. package/src/router/pattern-matching.ts +20 -58
  201. package/src/router/prerender-match.ts +99 -63
  202. package/src/router/preview-match.ts +3 -1
  203. package/src/router/request-classification.ts +28 -62
  204. package/src/router/revalidation.ts +50 -56
  205. package/src/router/route-snapshot.ts +0 -1
  206. package/src/router/router-context.ts +0 -27
  207. package/src/router/router-interfaces.ts +68 -35
  208. package/src/router/router-options.ts +55 -1
  209. package/src/router/router-registry.ts +2 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -63
  211. package/src/router/segment-resolution/helpers.ts +34 -0
  212. package/src/router/segment-resolution/loader-cache.ts +40 -37
  213. package/src/router/segment-resolution/revalidation.ts +203 -285
  214. package/src/router/segment-resolution/static-store.ts +19 -5
  215. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  216. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  217. package/src/router/segment-resolution.ts +4 -1
  218. package/src/router/segment-wrappers.ts +0 -3
  219. package/src/router/state-cookie-name.ts +33 -0
  220. package/src/router/substitute-pattern-params.ts +56 -0
  221. package/src/router/telemetry-otel.ts +0 -20
  222. package/src/router/telemetry.ts +96 -19
  223. package/src/router/timeout.ts +0 -20
  224. package/src/router/trie-matching.ts +87 -47
  225. package/src/router/types.ts +9 -63
  226. package/src/router/url-params.ts +0 -5
  227. package/src/router.ts +80 -41
  228. package/src/rsc/handler-context.ts +3 -2
  229. package/src/rsc/handler.ts +83 -78
  230. package/src/rsc/helpers.ts +93 -5
  231. package/src/rsc/index.ts +1 -1
  232. package/src/rsc/json-route-result.ts +38 -0
  233. package/src/rsc/manifest-init.ts +28 -41
  234. package/src/rsc/origin-guard.ts +39 -25
  235. package/src/rsc/progressive-enhancement.ts +12 -1
  236. package/src/rsc/redirect-guard.ts +99 -0
  237. package/src/rsc/response-error.ts +79 -12
  238. package/src/rsc/response-route-handler.ts +76 -62
  239. package/src/rsc/rsc-rendering.ts +41 -60
  240. package/src/rsc/runtime-warnings.ts +23 -10
  241. package/src/rsc/server-action.ts +62 -67
  242. package/src/rsc/ssr-setup.ts +16 -0
  243. package/src/rsc/types.ts +10 -5
  244. package/src/runtime-env.ts +18 -0
  245. package/src/search-params.ts +4 -20
  246. package/src/segment-loader-promise.ts +14 -2
  247. package/src/segment-system.tsx +199 -142
  248. package/src/serialize.ts +243 -0
  249. package/src/server/context.ts +150 -51
  250. package/src/server/cookie-store.ts +80 -5
  251. package/src/server/handle-store.ts +7 -24
  252. package/src/server/loader-registry.ts +5 -24
  253. package/src/server/request-context.ts +165 -87
  254. package/src/ssr/index.tsx +14 -14
  255. package/src/static-handler.ts +10 -13
  256. package/src/testing/cache-status.ts +162 -0
  257. package/src/testing/collect-handle.ts +40 -0
  258. package/src/testing/dispatch.ts +618 -0
  259. package/src/testing/dom.entry.ts +22 -0
  260. package/src/testing/e2e/fixture.ts +188 -0
  261. package/src/testing/e2e/index.ts +128 -0
  262. package/src/testing/e2e/matchers.ts +35 -0
  263. package/src/testing/e2e/page-helpers.ts +272 -0
  264. package/src/testing/e2e/parity.ts +387 -0
  265. package/src/testing/e2e/server.ts +195 -0
  266. package/src/testing/flight-matchers.ts +97 -0
  267. package/src/testing/flight-normalize.ts +11 -0
  268. package/src/testing/flight-runtime.d.ts +57 -0
  269. package/src/testing/flight-tree.ts +682 -0
  270. package/src/testing/flight.entry.ts +52 -0
  271. package/src/testing/flight.ts +232 -0
  272. package/src/testing/generated-routes.ts +183 -0
  273. package/src/testing/index.ts +99 -0
  274. package/src/testing/internal/context.ts +348 -0
  275. package/src/testing/internal/flight-client-globals.ts +30 -0
  276. package/src/testing/internal/seed-vars.ts +54 -0
  277. package/src/testing/render-handler.ts +330 -0
  278. package/src/testing/render-route.tsx +566 -0
  279. package/src/testing/run-loader.ts +378 -0
  280. package/src/testing/run-middleware.ts +205 -0
  281. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  282. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  283. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  284. package/src/testing/vitest-stubs/version.ts +5 -0
  285. package/src/testing/vitest.ts +305 -0
  286. package/src/theme/ThemeProvider.tsx +0 -52
  287. package/src/theme/ThemeScript.tsx +0 -6
  288. package/src/theme/constants.ts +0 -12
  289. package/src/theme/index.ts +0 -7
  290. package/src/theme/theme-context.ts +1 -5
  291. package/src/theme/theme-script.ts +0 -14
  292. package/src/theme/use-theme.ts +0 -3
  293. package/src/types/boundaries.ts +0 -35
  294. package/src/types/cache-types.ts +13 -4
  295. package/src/types/error-types.ts +30 -90
  296. package/src/types/global-namespace.ts +54 -41
  297. package/src/types/handler-context.ts +97 -22
  298. package/src/types/index.ts +1 -10
  299. package/src/types/loader-types.ts +6 -3
  300. package/src/types/request-scope.ts +0 -19
  301. package/src/types/route-config.ts +6 -50
  302. package/src/types/route-entry.ts +0 -6
  303. package/src/types/segments.ts +18 -14
  304. package/src/urls/include-helper.ts +9 -56
  305. package/src/urls/index.ts +1 -11
  306. package/src/urls/path-helper-types.ts +19 -5
  307. package/src/urls/path-helper.ts +17 -106
  308. package/src/urls/pattern-types.ts +36 -19
  309. package/src/urls/response-types.ts +20 -19
  310. package/src/urls/type-extraction.ts +58 -139
  311. package/src/urls/urls-function.ts +1 -18
  312. package/src/use-loader.tsx +292 -107
  313. package/src/vite/debug.ts +1 -0
  314. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  315. package/src/vite/discovery/discover-routers.ts +95 -82
  316. package/src/vite/discovery/discovery-errors.ts +194 -0
  317. package/src/vite/discovery/prerender-collection.ts +26 -34
  318. package/src/vite/discovery/route-types-writer.ts +40 -84
  319. package/src/vite/discovery/state.ts +39 -1
  320. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  321. package/src/vite/index.ts +4 -0
  322. package/src/vite/plugin-types.ts +185 -10
  323. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  324. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  325. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  326. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  327. package/src/vite/plugins/expose-action-id.ts +4 -75
  328. package/src/vite/plugins/expose-id-utils.ts +3 -54
  329. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  330. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  331. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  332. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  333. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  334. package/src/vite/plugins/performance-tracks.ts +9 -16
  335. package/src/vite/plugins/refresh-cmd.ts +1 -1
  336. package/src/vite/plugins/use-cache-transform.ts +26 -49
  337. package/src/vite/plugins/vercel-output.ts +258 -0
  338. package/src/vite/plugins/version-injector.ts +2 -32
  339. package/src/vite/plugins/version-plugin.ts +32 -23
  340. package/src/vite/plugins/virtual-entries.ts +35 -17
  341. package/src/vite/rango.ts +148 -115
  342. package/src/vite/router-discovery.ts +220 -68
  343. package/src/vite/utils/ast-handler-extract.ts +15 -31
  344. package/src/vite/utils/bundle-analysis.ts +10 -15
  345. package/src/vite/utils/client-chunks.ts +184 -0
  346. package/src/vite/utils/forward-user-plugins.ts +171 -0
  347. package/src/vite/utils/manifest-utils.ts +4 -59
  348. package/src/vite/utils/package-resolution.ts +1 -73
  349. package/src/vite/utils/prerender-utils.ts +0 -35
  350. package/src/vite/utils/shared-utils.ts +95 -43
  351. package/src/browser/action-response-classifier.ts +0 -99
  352. package/src/browser/react/use-client-cache.ts +0 -58
  353. package/src/browser/shallow.ts +0 -40
  354. package/src/handles/index.ts +0 -7
  355. package/src/router/middleware-cookies.ts +0 -55
@@ -28,6 +28,7 @@ import {
28
28
  getImportedFnNames,
29
29
  collectCreateExportBindings,
30
30
  buildUnsupportedShapeWarning,
31
+ findUnsupportedCreateCallSites,
31
32
  isExportOnlyFile,
32
33
  } from "./expose-ids/export-analysis.js";
33
34
  import {
@@ -39,7 +40,6 @@ import {
39
40
  transformHandles,
40
41
  transformLocationState,
41
42
  generateWholeFileStubs,
42
- generateExprStubs,
43
43
  stubHandlerExprs,
44
44
  transformHandlerIds,
45
45
  } from "./expose-ids/handler-transform.js";
@@ -184,6 +184,16 @@ ${lazyImports.join(",\n")}
184
184
  async buildStart() {
185
185
  if (!isBuild) return;
186
186
 
187
+ // The loader pre-scan walks and reads the entire project, but the
188
+ // loaderRegistry it populates is consumed only by the RSC loader-manifest
189
+ // virtual module (and the transform hook already gates its registry writes
190
+ // with isRscEnv). plugin-rsc runs ~5 build passes (rsc-scan, ssr-scan, rsc,
191
+ // client, ssr) over this single shared plugin instance; without this gate
192
+ // the full-tree I/O ran on every pass with no consumer on the non-RSC
193
+ // ones. Skip only when the environment is known and not RSC, so an
194
+ // unavailable environment still falls through (no empty registry).
195
+ if (this.environment && this.environment.name !== "rsc") return;
196
+
187
197
  const fs = await import("node:fs/promises");
188
198
 
189
199
  const SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage"]);
@@ -335,7 +345,7 @@ ${lazyImports.join(",\n")}
335
345
  }
336
346
  if (_cachedAst !== undefined || _astParseFailed) return _cachedAst;
337
347
  try {
338
- _cachedAst = parseAst(code, { jsx: true });
348
+ _cachedAst = parseAst(code, { lang: "tsx" });
339
349
  } catch {
340
350
  _astParseFailed = true;
341
351
  }
@@ -371,14 +381,21 @@ ${lazyImports.join(",\n")}
371
381
  if (!hasCode) continue;
372
382
 
373
383
  const fnNames = getFnNames(cfg.fnName);
374
- const totalCalls = countCreateCallsForNames(code, fnNames);
375
- const supportedBindings = getBindings(code, fnNames).length;
376
- if (totalCalls <= supportedBindings) continue;
384
+ // Locate the real (comment/string-free) create* calls not covered by
385
+ // a supported, id-injectable export shape. Empty means every call is
386
+ // fine in particular, a create*() token in a comment or string no
387
+ // longer trips a spurious warning.
388
+ const sites = findUnsupportedCreateCallSites(
389
+ code,
390
+ fnNames,
391
+ getBindings(code, fnNames),
392
+ );
393
+ if (sites.length === 0) continue;
377
394
 
378
395
  const warnKey = `${id}::${cfg.fnName}`;
379
396
  if (unsupportedShapeWarnings.has(warnKey)) continue;
380
397
  unsupportedShapeWarnings.add(warnKey);
381
- this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName));
398
+ this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName, sites));
382
399
  }
383
400
 
384
401
  // --- Loader: track for manifest (RSC env only) ---
@@ -424,17 +441,6 @@ ${lazyImports.join(",\n")}
424
441
  if (wholeFile) return wholeFile;
425
442
  }
426
443
 
427
- // --- PrerenderHandler: RSC build module tracking ---
428
- if (hasPrerenderHandlerCode && isRscEnv && isBuild) {
429
- const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
430
- const exportNames = getBindings(code, fnNames).map(
431
- (b) => b.exportNames[0],
432
- );
433
- if (exportNames.length > 0) {
434
- prerenderHandlerModules.set(id, exportNames);
435
- }
436
- }
437
-
438
444
  // --- Inline handler extraction to virtual modules ---
439
445
  // Runs before stubs/tracking so inline calls become imports, then
440
446
  // the existing regex fast path handles both the original file's
@@ -710,14 +716,27 @@ ${lazyImports.join(",\n")}
710
716
  }
711
717
  }
712
718
 
713
- // --- StaticHandler: RSC build module tracking ---
714
- if (hasStaticHandlerCode && isRscEnv && isBuild) {
715
- const fnNames = getFnNames(STATIC_CONFIG.fnName);
716
- const exportNames = getBindings(code, fnNames).map(
717
- (b) => b.exportNames[0],
718
- );
719
- if (exportNames.length > 0) {
720
- staticHandlerModules.set(id, exportNames);
719
+ // RSC build module tracking (prerender + static), consumed via the
720
+ // plugin API for prerender freezing. Export-binding sets are invariant
721
+ // across the inline-extraction loop, so tracking both here is equivalent
722
+ // to the pre-extraction prerender tracking this replaces.
723
+ if (isRscEnv && isBuild) {
724
+ const trackTypes: Array<
725
+ [boolean, HandlerTransformConfig, Map<string, string[]>]
726
+ > = [
727
+ [
728
+ hasPrerenderHandlerCode,
729
+ PRERENDER_CONFIG,
730
+ prerenderHandlerModules,
731
+ ],
732
+ [hasStaticHandlerCode, STATIC_CONFIG, staticHandlerModules],
733
+ ];
734
+ for (const [has, cfg, trackMap] of trackTypes) {
735
+ if (!has) continue;
736
+ const exportNames = getBindings(code, getFnNames(cfg.fnName)).map(
737
+ (b) => b.exportNames[0],
738
+ );
739
+ if (exportNames.length > 0) trackMap.set(id, exportNames);
721
740
  }
722
741
  }
723
742
 
@@ -758,48 +777,19 @@ ${lazyImports.join(",\n")}
758
777
  isBuild,
759
778
  ) || changed;
760
779
  }
761
- if (hasPrerenderHandlerCode) {
762
- const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
763
- const bindings = getBindings(code, fnNames);
764
- if (isRscEnv) {
765
- changed =
766
- transformHandlerIds(
767
- PRERENDER_CONFIG,
768
- bindings,
769
- s,
770
- filePath,
771
- isBuild,
772
- ) || changed;
773
- } else {
774
- // Non-RSC mixed-export file: replace Prerender() calls with stubs
775
- // on the shared MagicString so sourcemaps stay accurate.
776
- changed =
777
- stubHandlerExprs(
778
- PRERENDER_CONFIG,
779
- bindings,
780
- s,
781
- filePath,
782
- isBuild,
783
- ) || changed;
784
- }
785
- }
786
- if (hasStaticHandlerCode) {
787
- const fnNames = getFnNames(STATIC_CONFIG.fnName);
788
- const bindings = getBindings(code, fnNames);
789
- if (isRscEnv) {
790
- changed =
791
- transformHandlerIds(
792
- STATIC_CONFIG,
793
- bindings,
794
- s,
795
- filePath,
796
- isBuild,
797
- ) || changed;
798
- } else {
799
- changed =
800
- stubHandlerExprs(STATIC_CONFIG, bindings, s, filePath, isBuild) ||
801
- changed;
802
- }
780
+ // Prerender + Static share the RSC inject-id vs non-RSC stub dispatch.
781
+ // Call sites are disjoint (distinct fnNames), so loop order is irrelevant.
782
+ const finalHandlerConfigs = [
783
+ hasPrerenderHandlerCode && PRERENDER_CONFIG,
784
+ hasStaticHandlerCode && STATIC_CONFIG,
785
+ ].filter((c): c is HandlerTransformConfig => !!c);
786
+ for (const cfg of finalHandlerConfigs) {
787
+ const bindings = getBindings(code, getFnNames(cfg.fnName));
788
+ changed =
789
+ (isRscEnv
790
+ ? transformHandlerIds(cfg, bindings, s, filePath, isBuild)
791
+ : stubHandlerExprs(cfg, bindings, s, filePath, isBuild)) ||
792
+ changed;
803
793
  }
804
794
 
805
795
  if (!changed) return;
@@ -48,25 +48,18 @@ export function patchRsdwClientDebugInfoRecovery(code: string): {
48
48
 
49
49
  export function performanceTracksOptimizeDepsPlugin(): {
50
50
  name: string;
51
- setup(build: any): void;
51
+ load(id: string): Promise<{ code: string } | null>;
52
52
  } {
53
+ const RSDW_CLIENT_RE =
54
+ /react-server-dom-webpack-client\.browser\.(development|production)\.js$/;
53
55
  return {
54
56
  name: "@rangojs/router:performance-tracks-optimize-deps",
55
- setup(build: any): void {
56
- build.onLoad(
57
- {
58
- filter:
59
- /react-server-dom-webpack-client\.browser\.(development|production)\.js$/,
60
- },
61
- async (args: { path: string }) => {
62
- const code = await readFile(args.path, "utf8");
63
- const patched = patchRsdwClientDebugInfoRecovery(code);
64
- return {
65
- contents: patched.code,
66
- loader: "js",
67
- };
68
- },
69
- );
57
+ async load(id: string): Promise<{ code: string } | null> {
58
+ const cleanId = id.split("?")[0] ?? id;
59
+ if (!RSDW_CLIENT_RE.test(cleanId)) return null;
60
+ const code = await readFile(cleanId, "utf8");
61
+ const patched = patchRsdwClientDebugInfoRecovery(code);
62
+ return { code: patched.code };
70
63
  },
71
64
  };
72
65
  }
@@ -20,7 +20,7 @@ import type { Plugin } from "vite";
20
20
  */
21
21
  export function poke(): Plugin {
22
22
  return {
23
- name: "vite-plugin-poke",
23
+ name: "@rangojs/router:poke",
24
24
  apply: "serve",
25
25
 
26
26
  configureServer(server) {
@@ -30,6 +30,8 @@ const CACHE_RUNTIME_IMPORT = "@rangojs/router/cache-runtime";
30
30
  // and should not be wrapped (children can't be cache-keyed).
31
31
  const LAYOUT_TEMPLATE_PATTERN = /\/(layout|template)\.(tsx?|jsx?)$/;
32
32
 
33
+ export const USE_CACHE_DIRECTIVE_RE: RegExp = /^use cache(:\s*[\w-]+)?$/;
34
+
33
35
  export function useCacheTransform(): Plugin {
34
36
  let projectRoot = "";
35
37
  let isBuild = false;
@@ -65,7 +67,6 @@ export function useCacheTransform(): Plugin {
65
67
 
66
68
  const start = counter ? performance.now() : 0;
67
69
  try {
68
- // Lazy-load transform helpers
69
70
  if (!rscTransforms) {
70
71
  try {
71
72
  rscTransforms = await import("@vitejs/plugin-rsc/transforms");
@@ -80,11 +81,10 @@ export function useCacheTransform(): Plugin {
80
81
  transformHoistInlineDirective,
81
82
  } = rscTransforms;
82
83
 
83
- // Parse AST
84
84
  let ast: any;
85
85
  try {
86
86
  const { parseAst } = await import("vite");
87
- ast = parseAst(code);
87
+ ast = parseAst(code, { lang: "tsx" });
88
88
  } catch {
89
89
  return;
90
90
  }
@@ -92,7 +92,6 @@ export function useCacheTransform(): Plugin {
92
92
  const filePath = normalizePath(path.relative(projectRoot, id));
93
93
  const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
94
94
 
95
- // Check for file-level "use cache"
96
95
  if (hasDirective(ast.body, "use cache")) {
97
96
  return transformFileLevelUseCache(
98
97
  code,
@@ -105,8 +104,6 @@ export function useCacheTransform(): Plugin {
105
104
  );
106
105
  }
107
106
 
108
- // Check for function-level "use cache" / "use cache: profileName"
109
- // (only if there's no file-level directive but code still contains the string)
110
107
  const functionResult = transformFunctionLevelUseCache(
111
108
  code,
112
109
  ast,
@@ -116,9 +113,6 @@ export function useCacheTransform(): Plugin {
116
113
  transformHoistInlineDirective,
117
114
  );
118
115
 
119
- // Always check for near-miss directives, even when valid directives
120
- // exist. A file may contain both valid and invalid "use cache" directives
121
- // in different functions — the invalid ones should still warn.
122
116
  warnOnNearMissDirectives(ast, id, this.warn.bind(this));
123
117
 
124
118
  if (functionResult) return functionResult;
@@ -138,8 +132,7 @@ function transformFileLevelUseCache(
138
132
  isLayoutOrTemplate: boolean,
139
133
  transformWrapExport: (typeof import("@vitejs/plugin-rsc/transforms"))["transformWrapExport"],
140
134
  ) {
141
- // Collect non-function exports to report after wrapping
142
- const nonFunctionExports: string[] = [];
135
+ const unconfirmedExports: string[] = [];
143
136
 
144
137
  const { exportNames, output } = transformWrapExport(code, ast, {
145
138
  runtime: (value: string, name: string) => {
@@ -148,30 +141,38 @@ function transformFileLevelUseCache(
148
141
  },
149
142
  rejectNonAsyncFunction: false,
150
143
  filter: (name: string, meta: { isFunction?: boolean }) => {
151
- // Skip default export of layout/template files (they receive children)
152
144
  if (name === "default" && isLayoutOrTemplate) return false;
153
- // Non-function exports cannot be wrapped with registerCachedFunction
154
- if (meta.isFunction === false) {
155
- nonFunctionExports.push(name);
145
+ // isFunction is boolean | undefined: true = confirmed function, false =
146
+ // confirmed non-function, undefined = cannot tell statically (e.g. a
147
+ // factory/HOF initializer `const x = makeCached(fn)`). Deliberate policy:
148
+ // require a confirmed function and reject everything else, including
149
+ // indeterminate initializers that may be functions at runtime -- rewrite
150
+ // those as direct async functions. (Pre-#1246 plugin-rsc reported false,
151
+ // not undefined, here, so === false would wrongly wrap them post-bump.)
152
+ if (meta.isFunction !== true) {
153
+ unconfirmedExports.push(name);
156
154
  return false;
157
155
  }
158
156
  return true;
159
157
  },
160
158
  });
161
159
 
162
- if (nonFunctionExports.length > 0) {
160
+ if (unconfirmedExports.length > 0) {
161
+ const plural = unconfirmedExports.length > 1;
163
162
  throw new Error(
164
- `[rango:use-cache] File-level "use cache" in ${sourceId} cannot wrap ` +
165
- `non-function export${nonFunctionExports.length > 1 ? "s" : ""}: ` +
166
- `${nonFunctionExports.map((n) => `"${n}"`).join(", ")}. ` +
167
- `Only function exports can be cached. Either remove "use cache" from ` +
168
- `the file level and add it inside individual functions, or move the ` +
169
- `non-function exports to a separate module.`,
163
+ `[rango:use-cache] File-level "use cache" in ${sourceId} only wraps ` +
164
+ `exports that are statically-confirmed functions. ` +
165
+ `${plural ? "These exports are" : "This export is"} not: ` +
166
+ `${unconfirmedExports.map((n) => `"${n}"`).join(", ")}. ` +
167
+ `Declare them directly (export async function foo() {} or ` +
168
+ `export const foo = async () => {}). A factory or otherwise ` +
169
+ `statically-indeterminate initializer (export const foo = makeCached(fn)) ` +
170
+ `is rejected even if it returns a function at runtime -- rewrite it as a ` +
171
+ `direct async function, or move non-function exports to a separate module.`,
170
172
  );
171
173
  }
172
174
 
173
175
  if (exportNames.length === 0) {
174
- // Even if no exports were wrapped, strip the directive
175
176
  const s = new MagicString(code);
176
177
  const directive = findFileLevelDirective(ast);
177
178
  if (directive) {
@@ -188,12 +189,10 @@ function transformFileLevelUseCache(
188
189
  return;
189
190
  }
190
191
 
191
- // Prepend the import
192
192
  output.prepend(
193
193
  `import { registerCachedFunction as __rango_registerCachedFunction } from ${JSON.stringify(CACHE_RUNTIME_IMPORT)};\n`,
194
194
  );
195
195
 
196
- // Replace the directive with a comment
197
196
  const directive = findFileLevelDirective(ast);
198
197
  if (directive) {
199
198
  output.overwrite(
@@ -219,7 +218,7 @@ function transformFunctionLevelUseCache(
219
218
  ) {
220
219
  try {
221
220
  const { output, names } = transformHoistInlineDirective(code, ast, {
222
- directive: /^use cache(:\s*[\w-]+)?$/,
221
+ directive: USE_CACHE_DIRECTIVE_RE,
223
222
  runtime: (
224
223
  value: string,
225
224
  name: string,
@@ -237,9 +236,6 @@ function transformFunctionLevelUseCache(
237
236
 
238
237
  if (names.length === 0) return;
239
238
 
240
- // Use a top-level import instead of await import() — the hoisted wrapper
241
- // may be placed in a non-async context (e.g., inside a synchronous
242
- // urls() callback) where await is not allowed.
243
239
  output.prepend(
244
240
  `import { registerCachedFunction as __rango_registerCachedFunction } from ${JSON.stringify(CACHE_RUNTIME_IMPORT)};\n`,
245
241
  );
@@ -254,9 +250,6 @@ function transformFunctionLevelUseCache(
254
250
  }
255
251
  }
256
252
 
257
- /**
258
- * Find the file-level "use cache" directive AST node for removal.
259
- */
260
253
  function findFileLevelDirective(
261
254
  ast: any,
262
255
  ): { start: number; end: number } | null {
@@ -273,23 +266,8 @@ function findFileLevelDirective(
273
266
  return null;
274
267
  }
275
268
 
276
- /**
277
- * The valid directive regex (must stay in sync with transformFunctionLevelUseCache).
278
- */
279
- const VALID_DIRECTIVE_RE = /^use cache(:\s*[\w-]+)?$/;
280
-
281
- /**
282
- * Regex for near-miss: starts with "use cache:" but has invalid tokens.
283
- */
284
269
  const NEAR_MISS_RE = /^use cache:\s*.+$/;
285
270
 
286
- /**
287
- * Walk the AST looking for string literals that look like malformed
288
- * "use cache" directives and emit a Vite warning for each.
289
- *
290
- * This catches cases like `"use cache: bad.name"` or `"use cache: "`
291
- * that the transform regex silently ignores.
292
- */
293
271
  function warnOnNearMissDirectives(
294
272
  ast: any,
295
273
  fileId: string,
@@ -307,7 +285,7 @@ function warnOnNearMissDirectives(
307
285
  if (
308
286
  value.startsWith("use cache") &&
309
287
  NEAR_MISS_RE.test(value) &&
310
- !VALID_DIRECTIVE_RE.test(value)
288
+ !USE_CACHE_DIRECTIVE_RE.test(value)
311
289
  ) {
312
290
  const profilePart = value.slice("use cache:".length).trim();
313
291
  warn(
@@ -317,7 +295,6 @@ function warnOnNearMissDirectives(
317
295
  }
318
296
  }
319
297
 
320
- // Walk into function bodies where directives appear
321
298
  for (const key of Object.keys(node)) {
322
299
  const child = node[key];
323
300
  if (Array.isArray(child)) {
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Vercel Build Output (Build Output API v3) emitter for `preset: "vercel"`.
3
+ *
4
+ * After the full app build, restructures dist/ into .vercel/output:
5
+ *
6
+ * .vercel/output/
7
+ * config.json routing: static first, else the function
8
+ * static/ dist/client (browser assets, served at /)
9
+ * functions/<name>.func/
10
+ * .vc-config.json Node serverless, response streaming
11
+ * index.mjs bundled launcher (srvx + @vercel/functions)
12
+ * rsc/ dist/rsc (self-contained RSC server bundle)
13
+ * ssr/ dist/ssr (rsc imports ../ssr/index.js)
14
+ *
15
+ * A prebuilt .vercel/output gets no `npm install`, so everything the function
16
+ * imports must physically live inside the .func directory. This relies on two
17
+ * things the vercel preset arranges (each is a failure only a real deploy — or
18
+ * the isolated smoke test — catches, since a local in-place run is masked by the
19
+ * app's own package.json + node_modules up the tree):
20
+ *
21
+ * 1. The rsc/ssr builds are fully bundled (`resolve.noExternal`, set in
22
+ * rango.ts for this preset). The node default externalizes node_modules
23
+ * deps, which works under `vite preview` but leaves bare imports
24
+ * (@vercel/functions, react-dom/server.edge, ...) that have no node_modules
25
+ * to resolve against on Vercel.
26
+ * 2. A `package.json` with `"type": "module"` is written into the .func dir.
27
+ * The rsc/ssr bundles are ESM but use a `.js` extension; without a
28
+ * type:module in scope the deployed (isolated) function loads them as
29
+ * CommonJS and fails on the first `import`.
30
+ *
31
+ * The launcher is bundled with srvx (the Web->Node streaming bridge, a
32
+ * @rangojs/router dependency) and @vercel/functions (resolved from the app)
33
+ * inlined, keeping the RSC bundle a runtime-relative external.
34
+ *
35
+ * Timing: this runs in the `buildApp` hook (order "post"), which fires once
36
+ * after every environment has built, so dist/{client,rsc,ssr} all exist.
37
+ * closeBundle is unusable here — it fires per environment, and twice for ssr
38
+ * (the server-reference scan and the real build), so it would run before
39
+ * dist/client exists. rango's own prerender/static emitters hardcode dist/rsc,
40
+ * so we build to dist/ and restructure here rather than retargeting outDir.
41
+ */
42
+
43
+ import type { Plugin } from "vite";
44
+ import { rm, mkdir, cp, writeFile } from "node:fs/promises";
45
+ import { existsSync } from "node:fs";
46
+ import { resolve, join } from "node:path";
47
+ import { createRequire } from "node:module";
48
+ import { pathToFileURL } from "node:url";
49
+ import type { RangoVercelOptions } from "../plugin-types.js";
50
+
51
+ // Minimal structural types for the esbuild API we use, resolved dynamically from
52
+ // the app so @rangojs/router does not depend on esbuild's type package.
53
+ interface EsbuildPluginBuild {
54
+ onResolve(
55
+ options: { filter: RegExp },
56
+ callback: () => { path: string; external: boolean },
57
+ ): void;
58
+ }
59
+ type EsbuildBuild = (options: Record<string, unknown>) => Promise<unknown>;
60
+ interface EsbuildModule {
61
+ build?: EsbuildBuild;
62
+ default?: { build?: EsbuildBuild };
63
+ }
64
+
65
+ const LAUNCHER_SOURCE = `import { toNodeHandler } from "srvx/node";
66
+ import { waitUntil } from "@vercel/functions";
67
+ import rscHandler from "./rsc/index.js";
68
+
69
+ // The Vercel Node launcher invokes a Node (req, res) handler, not a Web fetch
70
+ // handler. srvx's toNodeHandler bridges the Rango Web fetch handler and pipes
71
+ // the streamed Response to the Node response (set supportsResponseStreaming).
72
+ const onVercel = Boolean(process.env.VERCEL);
73
+
74
+ const fetchHandler = (request) =>
75
+ rscHandler(request, {
76
+ env: process.env,
77
+ // Forward Vercel's waitUntil so cache writes / revalidation run off the
78
+ // response path. Omitted off-platform so those writes settle inline.
79
+ ctx: onVercel ? { waitUntil } : undefined,
80
+ });
81
+
82
+ export default toNodeHandler(fetchHandler);
83
+ `;
84
+
85
+ async function assemble(
86
+ root: string,
87
+ options: RangoVercelOptions,
88
+ ): Promise<void> {
89
+ const dist = join(root, "dist");
90
+ for (const dir of ["client", "rsc", "ssr"]) {
91
+ if (!existsSync(join(dist, dir))) {
92
+ throw new Error(
93
+ `[rango] preset "vercel": missing dist/${dir}. Run the production build first.`,
94
+ );
95
+ }
96
+ }
97
+
98
+ const vercel = options.vercel ?? {};
99
+ const functionName = vercel.functionName ?? "index";
100
+ const out = join(root, ".vercel", "output");
101
+ const funcDir = join(out, "functions", `${functionName}.func`);
102
+
103
+ await rm(out, { recursive: true, force: true });
104
+ await mkdir(funcDir, { recursive: true });
105
+
106
+ // 1. Static client assets -> served from the CDN at the root.
107
+ await cp(join(dist, "client"), join(out, "static"), { recursive: true });
108
+
109
+ // 2. Server bundle into the function. Preserve the rsc -> ../ssr/index.js
110
+ // relative import by mirroring the dist/{rsc,ssr} layout inside the func.
111
+ await cp(join(dist, "rsc"), join(funcDir, "rsc"), { recursive: true });
112
+ await cp(join(dist, "ssr"), join(funcDir, "ssr"), { recursive: true });
113
+
114
+ // Prerender/static payload manifests (present only when routes are prerendered).
115
+ if (existsSync(join(dist, "static"))) {
116
+ await cp(join(dist, "static"), join(funcDir, "static"), {
117
+ recursive: true,
118
+ });
119
+ }
120
+
121
+ // 3. Bundle the Node launcher. srvx (a @rangojs/router dependency) is aliased
122
+ // to its resolved path; @vercel/functions resolves from the app; the RSC
123
+ // server bundle stays a runtime-relative external.
124
+ const rangoRequire = createRequire(import.meta.url);
125
+ let srvxNodePath: string;
126
+ try {
127
+ srvxNodePath = rangoRequire.resolve("srvx/node");
128
+ } catch {
129
+ throw new Error(
130
+ '[rango] preset "vercel" requires "srvx" (a dependency of @rangojs/router). Reinstall dependencies.',
131
+ );
132
+ }
133
+
134
+ // esbuild ships with Vite; resolve it from the app so we never add it as a
135
+ // @rangojs/router dependency. Minimal structural types avoid coupling to
136
+ // esbuild's type package at compile time.
137
+ const appRequire = createRequire(join(root, "package.json"));
138
+ let esbuildModule: EsbuildModule;
139
+ try {
140
+ esbuildModule = (await import(
141
+ pathToFileURL(appRequire.resolve("esbuild")).href
142
+ )) as EsbuildModule;
143
+ } catch {
144
+ throw new Error(
145
+ '[rango] preset "vercel" requires "esbuild" to bundle the function launcher. It ships with Vite; reinstall dependencies.',
146
+ );
147
+ }
148
+ const esbuildBuild = esbuildModule.build ?? esbuildModule.default?.build;
149
+ if (typeof esbuildBuild !== "function") {
150
+ throw new Error('[rango] preset "vercel": could not load esbuild.build.');
151
+ }
152
+
153
+ try {
154
+ await esbuildBuild({
155
+ stdin: {
156
+ contents: LAUNCHER_SOURCE,
157
+ resolveDir: root,
158
+ sourcefile: "func-entry.mjs",
159
+ loader: "js",
160
+ },
161
+ outfile: join(funcDir, "index.mjs"),
162
+ bundle: true,
163
+ format: "esm",
164
+ platform: "node",
165
+ target: "node18",
166
+ alias: { "srvx/node": srvxNodePath },
167
+ plugins: [
168
+ {
169
+ name: "external-rsc-entry",
170
+ setup(b: EsbuildPluginBuild) {
171
+ b.onResolve({ filter: /^\.\/rsc\/index\.js$/ }, () => ({
172
+ path: "./rsc/index.js",
173
+ external: true,
174
+ }));
175
+ },
176
+ },
177
+ ],
178
+ });
179
+ } catch (error) {
180
+ const message = error instanceof Error ? error.message : String(error);
181
+ if (/@vercel\/functions/.test(message)) {
182
+ throw new Error(
183
+ '[rango] preset "vercel": could not resolve "@vercel/functions". Add it to your app dependencies (it also backs VercelCacheStore).\n' +
184
+ message,
185
+ );
186
+ }
187
+ throw error;
188
+ }
189
+
190
+ // 3b. Mark the function as ESM. The rsc/ssr bundles are .js ESM files with no
191
+ // package.json in scope on the deployed function (it is isolated at
192
+ // /var/task), so Node would load them as CommonJS and fail on `import`.
193
+ // Locally this is masked because the func inherits the app's
194
+ // "type": "module" up the tree; the deployed func has nothing above it.
195
+ await writeFile(
196
+ join(funcDir, "package.json"),
197
+ JSON.stringify({ type: "module" }, null, 2) + "\n",
198
+ );
199
+
200
+ // 4. Function config: Node serverless with response streaming.
201
+ const vcConfig: Record<string, unknown> = {
202
+ runtime: vercel.runtime ?? "nodejs22.x",
203
+ handler: "index.mjs",
204
+ launcherType: "Nodejs",
205
+ shouldAddHelpers: false,
206
+ supportsResponseStreaming: true,
207
+ maxDuration: vercel.maxDuration ?? 30,
208
+ };
209
+ if (vercel.memory != null) vcConfig.memory = vercel.memory;
210
+ if (vercel.regions != null) vcConfig.regions = vercel.regions;
211
+ await writeFile(
212
+ join(funcDir, ".vc-config.json"),
213
+ JSON.stringify(vcConfig, null, 2) + "\n",
214
+ );
215
+
216
+ // 5. Routing: filesystem (static/) first, then everything to the function.
217
+ await writeFile(
218
+ join(out, "config.json"),
219
+ JSON.stringify(
220
+ {
221
+ version: 3,
222
+ routes: [
223
+ { handle: "filesystem" },
224
+ { src: "/(.*)", dest: `/${functionName}` },
225
+ ],
226
+ },
227
+ null,
228
+ 2,
229
+ ) + "\n",
230
+ );
231
+
232
+ console.log(
233
+ `[rango] assembled .vercel/output (function: ${functionName}.func)`,
234
+ );
235
+ }
236
+
237
+ export function createVercelOutputPlugin(options: RangoVercelOptions): Plugin {
238
+ let root = process.cwd();
239
+ let isBuild = false;
240
+ return {
241
+ name: "@rangojs/router:vercel-output",
242
+ configResolved(config) {
243
+ root = resolve(config.root);
244
+ isBuild = config.command === "build";
245
+ },
246
+ // buildApp runs once after the whole multi-environment build (rsc, client,
247
+ // ssr), so dist/ is complete here. closeBundle is unusable for this: it
248
+ // fires per environment, and twice for ssr (the server-reference scan and
249
+ // the real build), so it would run before dist/client exists.
250
+ buildApp: {
251
+ order: "post",
252
+ async handler() {
253
+ if (!isBuild) return;
254
+ await assemble(root, options);
255
+ },
256
+ },
257
+ };
258
+ }