@rsc-kit/core 0.18.1 → 0.19.0

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 (60) hide show
  1. package/dist/action.d.ts +16 -2
  2. package/dist/action.js +21 -12
  3. package/dist/action.js.map +1 -1
  4. package/dist/apiPrerender.js +24 -6
  5. package/dist/apiPrerender.js.map +1 -1
  6. package/dist/clientPackages.d.ts +30 -0
  7. package/dist/clientPackages.js +234 -0
  8. package/dist/clientPackages.js.map +1 -0
  9. package/dist/compress.d.ts +12 -0
  10. package/dist/compress.js +134 -0
  11. package/dist/compress.js.map +1 -0
  12. package/dist/compressRuntime.d.ts +8 -0
  13. package/dist/compressRuntime.js +62 -0
  14. package/dist/compressRuntime.js.map +1 -0
  15. package/dist/files.d.ts +20 -0
  16. package/dist/files.js +38 -0
  17. package/dist/files.js.map +1 -1
  18. package/dist/formSubmit.d.ts +1 -0
  19. package/dist/formSubmit.js +14 -0
  20. package/dist/formSubmit.js.map +1 -0
  21. package/dist/host.d.ts +23 -0
  22. package/dist/host.js +175 -31
  23. package/dist/host.js.map +1 -1
  24. package/dist/hostCalls.d.ts +15 -0
  25. package/dist/hostCalls.js +75 -8
  26. package/dist/hostCalls.js.map +1 -1
  27. package/dist/js/Form.d.ts +19 -2
  28. package/dist/js/Form.js +110 -83
  29. package/dist/js/Form.js.map +1 -1
  30. package/dist/js/createViteRscApp.d.ts +1 -0
  31. package/dist/js/createViteRscApp.js +34 -5
  32. package/dist/js/createViteRscApp.js.map +1 -1
  33. package/dist/js/errors.d.ts +3 -1
  34. package/dist/js/errors.js +26 -3
  35. package/dist/js/errors.js.map +1 -1
  36. package/dist/js/formEncoding.d.ts +72 -3
  37. package/dist/js/formEncoding.js +284 -20
  38. package/dist/js/formEncoding.js.map +1 -1
  39. package/dist/js/navigate.d.ts +3 -0
  40. package/dist/js/navigate.js +56 -6
  41. package/dist/js/navigate.js.map +1 -1
  42. package/dist/js/updateStore.js +8 -2
  43. package/dist/js/updateStore.js.map +1 -1
  44. package/dist/openapi.d.ts +74 -0
  45. package/dist/openapi.js +172 -0
  46. package/dist/openapi.js.map +1 -0
  47. package/dist/redirect.d.ts +2 -2
  48. package/dist/redirect.js.map +1 -1
  49. package/dist/request.d.ts +56 -2
  50. package/dist/request.js +68 -4
  51. package/dist/request.js.map +1 -1
  52. package/dist/routes.d.ts +15 -1
  53. package/dist/routes.js.map +1 -1
  54. package/dist/testing.d.ts +14 -0
  55. package/dist/testing.js +56 -2
  56. package/dist/testing.js.map +1 -1
  57. package/dist/vite.d.ts +69 -1
  58. package/dist/vite.js +451 -45
  59. package/dist/vite.js.map +1 -1
  60. package/package.json +5 -1
package/dist/vite.js CHANGED
@@ -32,6 +32,7 @@ import { ownHosts } from "./hostRouting.js";
32
32
  import { unrollBarrelImports } from "./barrelImports.js";
33
33
  import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, } from "./useSsr.js";
34
34
  import { httpHostCalls } from "./hostCalls.js";
35
+ import { clientPackages, importsServerRenderer, packageDir as installedPackageDir, packageEntryInGraph } from "./clientPackages.js";
35
36
  // Resolved once per rscKit() call. One build runs in one process, so these are
36
37
  // module state rather than threaded through every helper.
37
38
  let projectRoot;
@@ -52,6 +53,12 @@ const RUNTIME_BUILTINS = ["bun", /^bun:/];
52
53
  * Next keeps the same list under serverExternalPackages, for the same reason.
53
54
  * `rscKit({ serverExternalPackages })` adds to it.
54
55
  */
56
+ /**
57
+ * Side-effect polyfills a dependency checks for at module evaluation, which
58
+ * the generated entry imports before anything else when the project has
59
+ * them. Bundler chunking does not keep an external import's place.
60
+ */
61
+ const FIRST_POLYFILLS = ["reflect-metadata"];
55
62
  const DEFAULT_SERVER_EXTERNALS = [
56
63
  "sharp",
57
64
  "canvas",
@@ -78,6 +85,13 @@ const DEFAULT_SERVER_EXTERNALS = [
78
85
  "oslo",
79
86
  "@resvg/resvg-js",
80
87
  "@napi-rs/canvas",
88
+ // Not native: a polyfill that must run before anything reads Reflect.getMetadata.
89
+ // tsyringe, typeorm and inversify check for it at module evaluation, and a
90
+ // bundler that splits them into their own chunk hoists that chunk's evaluation
91
+ // above the polyfill's - so the check fails in a bundle that has the polyfill
92
+ // in it. External, the import statement stays where the source put it, and
93
+ // Node runs it first.
94
+ "reflect-metadata",
81
95
  ];
82
96
  /** A package name as a rollup external: the package and every subpath of it. */
83
97
  function externalPackage(name) {
@@ -156,6 +170,8 @@ let siteHosts = [];
156
170
  let hostsOption = [];
157
171
  let barrelImports = true;
158
172
  let identify = true;
173
+ /** Resolved from RscKitOptions.openapi; null when off. */
174
+ let openapi = null;
159
175
  let foundAssets = {
160
176
  favicon: null,
161
177
  icons: [],
@@ -218,6 +234,12 @@ function fileHostActions(root) {
218
234
  return {};
219
235
  try {
220
236
  const parsed = JSON.parse(readFileSync(path, "utf-8"));
237
+ // An empty list is an empty map. PHP's json_encode writes an empty
238
+ // array as [] whatever it was declared as, so a backend with no
239
+ // actions yet wrote exactly that - and the first `dev` of a fresh
240
+ // install refused to start over a file that said, correctly, "none".
241
+ if (Array.isArray(parsed) && parsed.length === 0)
242
+ return {};
221
243
  if (parsed === null ||
222
244
  typeof parsed !== "object" ||
223
245
  Array.isArray(parsed)) {
@@ -243,16 +265,23 @@ function fileHostActions(root) {
243
265
  * two cannot drift.
244
266
  */
245
267
  function aliasEntries() {
268
+ const entries = [
269
+ // A library written for Next reads the request through next/headers -
270
+ // Vercel's flags SDK does, through `flags/next` - and headers() and
271
+ // cookies() here have the same names and shapes, with one object per
272
+ // request, which is what its per-request dedupe keys on. Answered by
273
+ // this package, so such a library runs unchanged and without Next.
274
+ { find: /^next\/headers$/, replacement: join(packageDir, "request") },
275
+ ];
246
276
  if (!packageAlias)
247
- return [];
277
+ return entries;
248
278
  if (existsSync(join(projectRoot, "node_modules", packageAlias)))
249
- return [];
250
- return [
251
- {
252
- find: new RegExp("^" + packageAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/(.*)$"),
253
- replacement: join(packageDir, "js") + "/$1",
254
- },
255
- ];
279
+ return entries;
280
+ entries.push({
281
+ find: new RegExp("^" + packageAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/(.*)$"),
282
+ replacement: join(packageDir, "js") + "/$1",
283
+ });
284
+ return entries;
256
285
  }
257
286
  function resolvePaths(options) {
258
287
  projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
@@ -321,6 +350,12 @@ function resolvePaths(options) {
321
350
  hostsOption = options.hosts ?? [];
322
351
  barrelImports = options.barrelImports !== false;
323
352
  identify = options.identify !== false;
353
+ openapi = options.openapi
354
+ ? (() => {
355
+ const { path, ...document } = typeof options.openapi === "object" ? options.openapi : {};
356
+ return { path: path || "/openapi.json", document };
357
+ })()
358
+ : null;
324
359
  inlineStylesheets = options.inlineStylesheets ?? "auto";
325
360
  maxActionBody = options.maxActionBody;
326
361
  // One place, and it is the file. A plugin option as well would be the same
@@ -765,7 +800,32 @@ self.addEventListener('install', (event) => {
765
800
  // it is served by the host out of the prerendered directory, which this
766
801
  // worker cannot see. Added here so it is in the cache before it is
767
802
  // needed, which is the only moment it cannot be fetched.
768
- .then((cache) => cache.addAll(OFFLINE_URL ? [...PRECACHE, OFFLINE_URL] : PRECACHE))
803
+ .then((cache) => cache.addAll(OFFLINE_URL ? [...PRECACHE, OFFLINE_URL] : PRECACHE).then(() => cache))
804
+ // And the payload each precached page boots from. A document alone is
805
+ // markup that never hydrates: the client fetches its payload on boot,
806
+ // and a precached / rendered offline and stayed inert because that
807
+ // request had nothing cached to answer it.
808
+ .then((cache) =>
809
+ Promise.all(
810
+ PRECACHE.filter((url) => !/\\.[a-z0-9]+$/i.test(url))
811
+ .concat(OFFLINE_URL ? [OFFLINE_URL] : [])
812
+ .map((url) => {
813
+ const warm = new Request(url, { headers: { 'X-RSC': '1' } })
814
+
815
+ return fetch(warm)
816
+ .then((payload) => {
817
+ if (mayStore(payload)) return cache.put(keyFor(warm), payload)
818
+
819
+ // Said, not swallowed: a page whose payload cannot be kept
820
+ // is one that will render offline and never hydrate, and
821
+ // the reason - a no-store from a guard above it, usually -
822
+ // is only visible here.
823
+ console.warn('[rsc-kit] offline: the payload for ' + url + ' was not stored (' + (payload.headers.get('Cache-Control') || payload.status) + '), so it will not hydrate offline')
824
+ })
825
+ .catch(() => {})
826
+ }),
827
+ ),
828
+ )
769
829
  .then(() => self.skipWaiting()),
770
830
  )
771
831
  })
@@ -774,9 +834,16 @@ self.addEventListener('activate', (event) => {
774
834
  event.waitUntil(
775
835
  caches
776
836
  .keys()
777
- .then((keys) => Promise.all(keys.filter((k) => k.startsWith('rsc-kit-') && k !== CACHE).map((k) => caches.delete(k))))
778
- .then(() => self.clients.claim())
779
- .then(() => tellTheOpenPages()),
837
+ .then((keys) => {
838
+ const older = keys.filter((k) => k.startsWith('rsc-kit-') && k !== CACHE)
839
+
840
+ return Promise.all(older.map((k) => caches.delete(k))).then(() => older.length > 0)
841
+ })
842
+ .then((swept) => self.clients.claim().then(() => swept))
843
+ // Only when something older was swept. The first worker a visitor ever
844
+ // gets activates too, and told them "a new version is ready" on their
845
+ // second page - there was no previous version to be new against.
846
+ .then((swept) => (swept ? tellTheOpenPages() : undefined)),
780
847
  )
781
848
  })
782
849
 
@@ -821,6 +888,37 @@ const keyFor = (request) => {
821
888
  return new Request(url, { headers: request.headers })
822
889
  }
823
890
 
891
+ // Nothing cached for a request and no network: the offline page for a
892
+ // navigation, and for the boot of a page the offline page stood in for -
893
+ // the runtime asks for the payload of the url in the address bar, which is
894
+ // the one thing nothing has - the offline page's own payload, which is what
895
+ // the document on screen is. A boot only, no segments header: a navigation
896
+ // while offline still fails as itself, and the page already open keeps its
897
+ // banner. Null when there is no offline page, or nothing of it cached.
898
+ const standIn = async (request) => {
899
+ if (!OFFLINE_URL) return null
900
+
901
+ if (request.mode === 'navigate') return (await caches.match(OFFLINE_URL)) ?? null
902
+
903
+ if (request.headers.get('X-RSC') && !request.headers.get('X-RSC-Segments')) {
904
+ return (
905
+ (await caches.match(
906
+ keyFor(new Request(new URL(OFFLINE_URL, self.location.origin), { headers: { 'X-RSC': '1' } })),
907
+ MATCH,
908
+ )) ?? null
909
+ )
910
+ }
911
+
912
+ return null
913
+ }
914
+
915
+ // The Cache API honours Vary, and a stored payload varies on X-RSC - so a
916
+ // payload warmed with one spelling of that header was a miss for a boot
917
+ // that sent another, and a precached page rendered offline with its payload
918
+ // sitting in the cache, unmatched. What Vary is for is already in the key:
919
+ // the ?__rsc=<segments> above is the one header the answer differs on.
920
+ const MATCH = { ignoreVary: true }
921
+
824
922
  self.addEventListener('fetch', (event) => {
825
923
  const request = event.request
826
924
  const url = new URL(request.url)
@@ -855,7 +953,7 @@ self.addEventListener('fetch', (event) => {
855
953
  // differently for every value of it.
856
954
  if (FROZEN.has(url.pathname.replace(/\\/+$/, '') || '/') && !url.search) {
857
955
  event.respondWith(
858
- caches.match(keyFor(request)).then((hit) => {
956
+ caches.match(keyFor(request), MATCH).then((hit) => {
859
957
  const fresh = fetch(request)
860
958
  .then((response) => {
861
959
  if (mayStore(response)) {
@@ -865,10 +963,17 @@ self.addEventListener('fetch', (event) => {
865
963
 
866
964
  return response
867
965
  })
868
- .catch((error) => {
966
+ .catch(async (error) => {
869
967
  if (hit) return hit
870
968
 
871
- throw error
969
+ // Stored by the build but never visited from this browser, so
970
+ // nothing is cached for it, and no network: the offline page is
971
+ // the honest answer, the same as for any other navigation - and
972
+ // its payload for the boot that follows, so the page it stood
973
+ // in for hydrates. Without this a frozen page failed with
974
+ // ERR_FAILED where an unstored one showed /offline, and then
975
+ // stood inert where an unstored one hydrated.
976
+ return (await standIn(request)) ?? Promise.reject(error)
872
977
  })
873
978
 
874
979
  // Not awaited when there is a hit: the point is that the visitor does
@@ -893,11 +998,11 @@ self.addEventListener('fetch', (event) => {
893
998
  // cached, and a reload with no network has the markup and nothing to
894
999
  // hydrate it with.
895
1000
  //
896
- // \`X-RSC: true\` and no segments header is exactly what a fresh boot
1001
+ // \`X-RSC: 1\` and no segments header is exactly what a fresh boot
897
1002
  // sends, which is what makes this entry the one it finds: the server
898
1003
  // varies on those, and the Cache API matches on the same.
899
1004
  if (request.mode === 'navigate') {
900
- const warm = new Request(request.url, { headers: { 'X-RSC': 'true' } })
1005
+ const warm = new Request(request.url, { headers: { 'X-RSC': '1' } })
901
1006
 
902
1007
  fetch(warm)
903
1008
  .then((payload) => {
@@ -932,7 +1037,7 @@ self.addEventListener('fetch', (event) => {
932
1037
  return response
933
1038
  })
934
1039
  .catch(async () => {
935
- const hit = await caches.match(keyFor(request))
1040
+ const hit = await caches.match(keyFor(request), MATCH)
936
1041
 
937
1042
  if (hit) return hit
938
1043
 
@@ -947,11 +1052,9 @@ self.addEventListener('fetch', (event) => {
947
1052
  // stand in for another: it is about being offline, not about the url it
948
1053
  // appears under. Navigations only — a payload request answered with a
949
1054
  // document would be decoded as one and throw.
950
- if (OFFLINE_URL && request.mode === 'navigate') {
951
- const page = await caches.match(OFFLINE_URL)
1055
+ const stood = await standIn(request)
952
1056
 
953
- if (page) return page
954
- }
1057
+ if (stood) return stood
955
1058
 
956
1059
  // Letting it fail says what is true, and a page already open is
957
1060
  // unaffected.
@@ -1066,17 +1169,24 @@ function writeWebManifest(clientDir, options) {
1066
1169
  * precached and found wanting at the one moment it matters, and the build says
1067
1170
  * which read did it.
1068
1171
  */
1069
- function offlineFallback(frozen, results) {
1172
+ export function offlineFallback(frozen, results) {
1070
1173
  const found = results.find((r) => r.url === "/offline");
1071
1174
  if (!found)
1072
1175
  return null;
1073
1176
  if (!frozen.includes("/offline")) {
1074
1177
  // The reason already reads "dynamic — called cookies()", and this sentence
1075
1178
  // has said "not stored" by the time it gets there, so the prefix would say
1076
- // it twice with a dash in the middle of both.
1077
- const why = found.reason?.replace(/^dynamic — /, "") ?? null;
1179
+ // it twice with a dash in the middle of both. A shell's reason is a
1180
+ // sentence of its own - "connection() awaited by AuthLinks streams per
1181
+ // request; the rest is stored" - and takes no "it" in front, and the
1182
+ // part about the rest being stored is not the point here.
1183
+ const why = found.type === "shell"
1184
+ ? (found.reason?.replace(/; the rest is stored$/, "") ?? null)
1185
+ : found.reason
1186
+ ? "it " + found.reason.replace(/^dynamic — /, "")
1187
+ : null;
1078
1188
  log("offline: /offline cannot be the fallback" +
1079
- (why ? `, because it ${why}` : "") +
1189
+ (why ? `, because ${why}` : "") +
1080
1190
  ". A fallback has to be servable with no network at all.");
1081
1191
  return null;
1082
1192
  }
@@ -1102,6 +1212,24 @@ function copyServiceWorkerExtra(clientDir) {
1102
1212
  copyFileSync(source, join(clientDir, "sw-app.js"));
1103
1213
  return "/sw-app.js";
1104
1214
  }
1215
+ /**
1216
+ * Whether a public file is worth fetching before it is asked for.
1217
+ *
1218
+ * The precache is what lets the app boot with no network: the scripts, the
1219
+ * stylesheets, the fonts, the manifest and its icons. Everything else in
1220
+ * public/ - an image, a wasm module, the share card - is cached the first
1221
+ * time it is used, which is what the fetch handler does for any hashed
1222
+ * asset. Precaching all of it made an install cost a megabyte before the
1223
+ * visitor had seen a page: 765 kB of it a webp encoder, the rest a png no
1224
+ * browser ever renders.
1225
+ */
1226
+ export function bootsTheApp(file) {
1227
+ if (/\.(?:m?js|css|woff2?|webmanifest|json)$/i.test(file))
1228
+ return !/\.wasm\.js$/i.test(file);
1229
+ if (/(?:^|\/)(?:icon|apple-icon|favicon)[^/]*\.(?:png|ico|svg)$/i.test(file))
1230
+ return true;
1231
+ return false;
1232
+ }
1105
1233
  function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
1106
1234
  if (!existsSync(clientDir))
1107
1235
  return;
@@ -1120,7 +1248,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
1120
1248
  const path = join(dir, entry.name);
1121
1249
  if (entry.isDirectory())
1122
1250
  walk(path, `${prefix}${entry.name}/`);
1123
- else
1251
+ else if (bootsTheApp(`${prefix}${entry.name}`))
1124
1252
  files.push(`${prefix}${entry.name}`);
1125
1253
  }
1126
1254
  };
@@ -1723,8 +1851,20 @@ function componentName(absPath) {
1723
1851
  const rel = relative(sourceDir, absPath).replace(/\\/g, "/");
1724
1852
  return rel.replace(/\.(tsx|jsx|ts|js)$/, "");
1725
1853
  }
1726
- function toAlias(name) {
1727
- return "_c_" + name.replace(/[^a-zA-Z0-9]/g, "_");
1854
+ /**
1855
+ * The identifier a route file is imported as in the generated entries.
1856
+ *
1857
+ * One-to-one with the path, which the obvious "replace everything odd with an
1858
+ * underscore" is not: app/agent-account/page and app/agent/account/page
1859
+ * both came out as _c_app_agent_account_page, and the entry declared the
1860
+ * same import twice. A slash is an underscore, because that is what every
1861
+ * alias reads like; anything else that is not a letter or digit - a hyphen,
1862
+ * a dot, the brackets of a param, the parens of a group, an underscore of
1863
+ * its own - is its character code, so no two paths share an alias.
1864
+ */
1865
+ export function toAlias(name) {
1866
+ return ("_c_" +
1867
+ name.replace(/[^a-zA-Z0-9]/g, (char) => (char === "/" ? "_" : "$" + char.charCodeAt(0).toString(16))));
1728
1868
  }
1729
1869
  const components = new Map();
1730
1870
  /**
@@ -1736,6 +1876,48 @@ const components = new Map();
1736
1876
  * a default component.
1737
1877
  */
1738
1878
  const apiRoutes = new Map();
1879
+ const OPENAPI_ROUTE_ID = "virtual:rsc-kit/openapi";
1880
+ /**
1881
+ * /openapi.json, when asked for, registered as an api route at the path the
1882
+ * option names. Synthesised like a sitemap: stored by the build, without
1883
+ * middleware - a document exists to be read.
1884
+ */
1885
+ function registerOpenApiRoutes() {
1886
+ if (!openapi)
1887
+ return;
1888
+ const name = `app${openapi.path.replace(/\/$/, "")}/route`;
1889
+ if (apiRoutes.has(name)) {
1890
+ throw new Error(`[rsc-kit] ${openapi.path} is answered by ${apiRoutes.get(name).absPath} and by rscKit({ openapi }). Move one.`);
1891
+ }
1892
+ apiRoutes.set(name, { name, absPath: OPENAPI_ROUTE_ID, methods: ["GET"], generated: { kind: "openapi", file: "" } });
1893
+ }
1894
+ /** The module behind the synthesised /openapi.json route. */
1895
+ function openApiPlugin() {
1896
+ return {
1897
+ name: "rsc-kit:openapi",
1898
+ resolveId(id) {
1899
+ if (id === OPENAPI_ROUTE_ID)
1900
+ return "\0" + id;
1901
+ },
1902
+ load(id) {
1903
+ if (id !== "\0" + OPENAPI_ROUTE_ID || !openapi)
1904
+ return;
1905
+ // Every route.ts the app wrote, imported so its schemas can describe
1906
+ // themselves at request time; the synthesised ones are not an API.
1907
+ const routes = [...apiRoutes.values()].filter((r) => !r.generated);
1908
+ const guarded = new Set((routeManifest().apis ?? []).filter((api) => api.middleware.length > 0).map((api) => api.name));
1909
+ const imports = routes.map((r, i) => `import * as __r${i} from ${JSON.stringify(r.absPath)}`);
1910
+ const entries = routes.map((r, i) => ` { pattern: ${JSON.stringify(patternOf(urlSegments(r.name)))}, methods: ${JSON.stringify(r.methods)}, guarded: ${guarded.has(r.name)}, module: __r${i} }`);
1911
+ return [
1912
+ ...imports,
1913
+ `import { openApiResponse } from ${JSON.stringify(join(packageDir, "openapi"))}`,
1914
+ `const routes = [\n${entries.join(",\n")}\n]`,
1915
+ `export const GET = () => openApiResponse(routes, ${JSON.stringify(openapi.document)})`,
1916
+ "",
1917
+ ].join("\n");
1918
+ },
1919
+ };
1920
+ }
1739
1921
  /** Files beside the root layout served at the root as they are: robots.txt, a hand-written sitemap.xml, humans.txt. */
1740
1922
  let rootFiles = [];
1741
1923
  const METADATA_ROUTE_ID = "virtual:rsc-kit/metadata-route/";
@@ -2071,6 +2253,9 @@ const NITRO_HANDLER_OPTIONS = ` props: (match, request) => ({
2071
2253
  ...Object.fromEntries(new URL(request.url).searchParams),
2072
2254
  }),
2073
2255
  version: process.env.RSC_BUILD_VERSION,
2256
+ // A built server gzips what it answers, where the runtime can; the dev
2257
+ // server answers raw, which is what a person reading a response wants.
2258
+ compress: import.meta.env.PROD,
2074
2259
  `;
2075
2260
  const NITRO_HOST_CALLS = `
2076
2261
  let hostInstalled = false
@@ -2199,12 +2384,12 @@ import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultR
2199
2384
  import { searchParams as requestSearchParams } from ${JSON.stringify(join(packageDir, "request"))}
2200
2385
  import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
2201
2386
  import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
2202
- import { noteRequestRead } from ${JSON.stringify(join(packageDir, "request"))}
2387
+ import { noteRequestRead, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
2203
2388
  import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
2204
2389
  import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
2205
2390
  import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
2206
2391
  import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
2207
- import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
2392
+ import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
2208
2393
  import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
2209
2394
  import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
2210
2395
  import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
@@ -2364,7 +2549,10 @@ export async function handleApiRoute(
2364
2549
  // reaches for the value.
2365
2550
  noteRequestRead('searchParams')
2366
2551
 
2367
- return parseSearchParams(mod.searchParams, new URL(request.url).searchParams)
2552
+ // Through urlOf, so the build's probe books this read to searchParams
2553
+ // rather than to the url - a route reads request.url itself when it
2554
+ // wants the query the Next way, and that read is the one that matters.
2555
+ return parseSearchParams(mod.searchParams, new URL(urlOf(request)).searchParams)
2368
2556
  }),
2369
2557
  body: hasBody(method) ? lazily(() => parseBody(mod.body, request)) : undefined,
2370
2558
  }
@@ -3315,6 +3503,54 @@ export async function handleRscHtmlStream(
3315
3503
  return { htmlStream, rscPayloadPromise, clientChunks: {} }
3316
3504
  }
3317
3505
 
3506
+ /**
3507
+ * A form posted to the page before the page had a runtime.
3508
+ *
3509
+ * React writes the action's id into the form it emits for a server action,
3510
+ * so a browser with no javascript yet - or none at all - posts the fields
3511
+ * to the page's own url. The action runs from those fields, exactly as it
3512
+ * would have been called, and the page renders afterwards with what it
3513
+ * returned as React's form state: a useActionState form shows its result,
3514
+ * a redirect() thrown by the action leaves through the scope the way a
3515
+ * page's would, and a cookie it set is on the answer.
3516
+ */
3517
+ export async function handleRscFormPost(
3518
+ component: string,
3519
+ props: Record<string, unknown> = {},
3520
+ layouts: LayoutEntry[] = [],
3521
+ loadings: string[] = [],
3522
+ parallelSlots: Record<string, string> = {},
3523
+ slotOverrides: Record<string, SlotOverride> = {},
3524
+ nonce?: string,
3525
+ pageKey = '',
3526
+ bootstrap = true,
3527
+ formData: FormData = new FormData(),
3528
+ ): Promise<{ htmlStream: ReadableStream }> {
3529
+ await instrumented()
3530
+ applyHost()
3531
+ await runMiddleware(component, props)
3532
+
3533
+ // The function the form named, bound to what it posted. Nothing named -
3534
+ // a plain form posted here by mistake - and the page simply renders.
3535
+ const action = await decodeAction(formData)
3536
+ let formState: unknown
3537
+
3538
+ if (action) {
3539
+ const result = await action()
3540
+
3541
+ formState = await decodeFormState(result, formData)
3542
+ }
3543
+
3544
+ const flight = renderToReadableStream(
3545
+ await renderTree(component, props, layouts, loadings, parallelSlots, slotOverrides, 0, pageKey, bootstrap),
3546
+ { onError: flightOnError },
3547
+ )
3548
+ const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
3549
+ const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, formState)
3550
+
3551
+ return { htmlStream }
3552
+ }
3553
+
3318
3554
  /**
3319
3555
  * Finish a shell that was frozen at build time.
3320
3556
  *
@@ -3941,7 +4177,7 @@ export async function handleRscPprShell(
3941
4177
  // say so on the route's line; the digest goes into the document for the
3942
4178
  // browser to recognise.
3943
4179
  if ((e as { digest?: string } | null)?.digest === 'rsc-kit:search-params-fallback') {
3944
- const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
4180
+ const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
3945
4181
  noteCaughtRead('useSearchParams()' + (where ? ' in ' + where : ''))
3946
4182
  return 'rsc-kit:search-params-fallback'
3947
4183
  }
@@ -4091,6 +4327,7 @@ async function serve(request: Request): Promise<Response> {
4091
4327
  handleRsc,
4092
4328
  handleRscStream,
4093
4329
  handleRscHtmlStream,
4330
+ handleRscFormPost,
4094
4331
  handleRscRevalidate,
4095
4332
  handleRscPayload,
4096
4333
  handleRscPprShell,
@@ -4181,6 +4418,8 @@ function generateEntrySsr() {
4181
4418
  const devUrls = join(packageDir, "devUrls");
4182
4419
  const request = join(packageDir, "request");
4183
4420
  const fallbackReport = join(packageDir, "js/fallbackReport");
4421
+ const redirectDigestModule = join(packageDir, "redirectDigest");
4422
+ const notFoundModule = join(packageDir, "notFound");
4184
4423
  return `// GENERATED by rscKit() — do not edit.
4185
4424
  import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
4186
4425
  import { renderToReadableStream, resume } from 'react-dom/server.edge'
@@ -4188,6 +4427,8 @@ import { prerender } from 'react-dom/static.edge'
4188
4427
  import { rewriteViteDevUrlStream } from ${JSON.stringify(devUrls)}
4189
4428
  import { noteFallback } from ${JSON.stringify(request)}
4190
4429
  import { cancelledByConsumer, caughtByLoading } from ${JSON.stringify(fallbackReport)}
4430
+ import { parseRedirectDigest } from ${JSON.stringify(redirectDigestModule)}
4431
+ import { isNotFoundDigest } from ${JSON.stringify(notFoundModule)}
4191
4432
 
4192
4433
  // Set only by the dev server. @vitejs/plugin-rsc emits its bootstrap and CSS
4193
4434
  // links root-relative in dev, which would send the browser to the host for
@@ -4199,6 +4440,7 @@ export async function handleSsr(
4199
4440
  nonce?: string,
4200
4441
  onError?: (error: unknown) => void,
4201
4442
  bootstrap = true,
4443
+ formState?: unknown,
4202
4444
  ): Promise<ReadableStream> {
4203
4445
  const root = await createFromReadableStream(rscStream)
4204
4446
 
@@ -4216,6 +4458,9 @@ export async function handleSsr(
4216
4458
  bootstrapScriptContent,
4217
4459
  nonce,
4218
4460
  onError: onError ?? reportRenderError('ssr'),
4461
+ // What a posted form's action returned, for the useActionState that
4462
+ // asked: React seats it in the form it belongs to.
4463
+ ...(formState !== undefined ? { formState: formState as any } : {}),
4219
4464
  })
4220
4465
 
4221
4466
  return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
@@ -4255,7 +4500,7 @@ export async function handleSsrPrerender(
4255
4500
  // caught at a boundary is: the build attaches it to the route.
4256
4501
  onError: (error: unknown, info?: { componentStack?: string }) => {
4257
4502
  if ((error as { digest?: string } | null)?.digest !== 'rsc-kit:search-params-fallback') return
4258
- const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
4503
+ const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
4259
4504
  noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
4260
4505
  return 'rsc-kit:search-params-fallback'
4261
4506
  },
@@ -4283,11 +4528,18 @@ function reportRenderError(phase: 'ssr' | 'resume') {
4283
4528
 
4284
4529
  const digest = (error as { digest?: string } | null)?.digest
4285
4530
 
4531
+ // A redirect or a notFound() the render asked for arrives here as the
4532
+ // Flight row's error, once per boundary it was thrown in. The rsc side
4533
+ // already turned it into a digest on purpose; it is the page's answer,
4534
+ // and the host reads it off the digest. Not an error to print, four
4535
+ // times or once.
4536
+ if (parseRedirectDigest(digest) || isNotFoundDigest(digest)) return digest
4537
+
4286
4538
  if (digest === 'rsc-kit:search-params-fallback') {
4287
4539
  // The component is the first frame of React's stack. Noted on the
4288
4540
  // request so the build attaches it to the route; printed as one line,
4289
4541
  // not a stack, so the server says which boundary it wants.
4290
- const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
4542
+ const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
4291
4543
 
4292
4544
  noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
4293
4545
 
@@ -4381,6 +4633,9 @@ import { refresh } from ${JSON.stringify(refreshModule)}
4381
4633
  createViteRscApp(document, ${JSON.stringify(interceptManifest())}, ${JSON.stringify({
4382
4634
  staticPayloads: staticPayloads || null,
4383
4635
  routes: routesForClient,
4636
+ // Every route.ts, as a url pattern, so a link to one is treated as an
4637
+ // anchor rather than prefetched and fetched as a page.
4638
+ apiRoutes: (routeManifest().apis ?? []).map((api) => patternOf(api.segments)),
4384
4639
  })})
4385
4640
 
4386
4641
  // A server component is not a module the browser has, so Vite cannot replace
@@ -4411,12 +4666,19 @@ ${offline
4411
4666
  // worker answering from a cache in front of it turns every edit into a
4412
4667
  // question about which copy you are looking at.
4413
4668
  if ('serviceWorker' in navigator && import.meta.env.PROD) {
4414
- window.addEventListener('load', () => {
4669
+ const register = () => {
4415
4670
  void navigator.serviceWorker.register('/sw.js').catch(() => {
4416
4671
  // A worker that will not register is not a reason for the page to fail.
4417
4672
  // The app works; it just will not survive being reloaded offline.
4418
4673
  })
4419
- })
4674
+ }
4675
+
4676
+ // This runs after the boot payload has been fetched and decoded, and on a
4677
+ // fast load the page has finished loading by then - a listener added now
4678
+ // waits for an event that has already fired, and nothing registered until
4679
+ // the next navigation.
4680
+ if (document.readyState === 'complete') register()
4681
+ else window.addEventListener('load', register)
4420
4682
  }
4421
4683
  `
4422
4684
  : ""}`;
@@ -4687,7 +4949,21 @@ function useSsrModules() {
4687
4949
  * directive that moves it. The build says the same once, as a warning.
4688
4950
  */
4689
4951
  const RENDERER_STUB = "\0rsc-kit:react-dom-server?from=";
4690
- function serverRendererInRsc() {
4952
+ /** What react-dom/server exports, across its node, edge and browser entries. */
4953
+ const SERVER_RENDERER_EXPORTS = [
4954
+ "renderToString",
4955
+ "renderToStaticMarkup",
4956
+ "renderToReadableStream",
4957
+ "renderToPipeableStream",
4958
+ "renderToStaticNodeStream",
4959
+ "resume",
4960
+ "resumeToPipeableStream",
4961
+ "resumeAndPrerender",
4962
+ "resumeAndPrerenderToNodeStream",
4963
+ "prerender",
4964
+ "prerenderToNodeStream",
4965
+ ];
4966
+ export function serverRendererInRsc() {
4691
4967
  const warned = new Set();
4692
4968
  const appImporter = (ctx, importer) => {
4693
4969
  let current = importer;
@@ -4706,24 +4982,93 @@ function serverRendererInRsc() {
4706
4982
  }
4707
4983
  return importer ? relative(process.cwd(), importer.split("?")[0]) : null;
4708
4984
  };
4985
+ // Per package, so a dependency imported from twenty files is read once.
4986
+ const externalRenderers = new Map();
4987
+ // Packages already named by the warning above, so the stub's own warning
4988
+ // - "imported by node_modules/<package>/dist/index.mjs" - stays quiet for
4989
+ // them: one problem, one line.
4990
+ const warnedPackages = new Set();
4709
4991
  return {
4710
4992
  name: "rsc-kit:server-renderer",
4993
+ // Before Vite's own resolver, which otherwise answers react-dom/server
4994
+ // with React's react-server entry - a real module missing the export,
4995
+ // and a build that fails on MISSING_EXPORT with no mention of the fix.
4996
+ enforce: "pre",
4711
4997
  applyToEnvironment: (environment) => environment.name === "rsc",
4712
- resolveId(source, importer) {
4713
- if (!SERVER_RENDERER.test(source))
4998
+ async resolveId(source, importer) {
4999
+ // Only an import from a module: a resolve with no importer is Vite
5000
+ // asking whether the specifier exists - deciding what to externalise -
5001
+ // and answering that with the stub would answer the wrong question.
5002
+ if (SERVER_RENDERER.test(source)) {
5003
+ return importer ? RENDERER_STUB + encodeURIComponent(importer) : undefined;
5004
+ }
5005
+ // A bare import from app code of a package the build leaves external:
5006
+ // its own import of react-dom/server is never resolved here, so the
5007
+ // stub above never sees it. @react-email/render, imported by an
5008
+ // action, built clean and threw React's refusal at the first visitor.
5009
+ // The package is read once for the import, and the build warns the
5010
+ // same way it does for a direct one - naming the app file and the
5011
+ // package - without stubbing a module that may also be used well.
5012
+ if (!importer || importer.includes("/node_modules/") || importer.startsWith("\0"))
4714
5013
  return;
4715
- return RENDERER_STUB + encodeURIComponent(importer ?? "");
5014
+ if (!/^(?:@[\w.-]+\/)?[\w.-]+/.test(source) || source.startsWith("."))
5015
+ return;
5016
+ // The generated entries import plugin-rsc, whose ssr half imports the
5017
+ // renderer for its own reasons; that is the build's, not the app's.
5018
+ if (outDir && importer.startsWith(outDir))
5019
+ return;
5020
+ const name = source.startsWith("@") ? source.split("/").slice(0, 2).join("/") : source.split("/")[0];
5021
+ if (name === "react-dom" || name === "react" || name.startsWith("@rsc-kit/") || name.startsWith("@vitejs/"))
5022
+ return;
5023
+ let imports = externalRenderers.get(name);
5024
+ if (imports === undefined) {
5025
+ // Vite answers a dependency either as external, by its bare name,
5026
+ // or by the file it resolved to under node_modules - which of the
5027
+ // two depends on how the environment was set up, and both are a
5028
+ // dependency whose imports the build will not look inside.
5029
+ const resolved = await this.resolve(source, importer, { skipSelf: true });
5030
+ const id = resolved?.id ?? "";
5031
+ const marker = `/node_modules/${name}/`;
5032
+ const at = id.lastIndexOf(marker);
5033
+ const dir = resolved?.external
5034
+ ? installedPackageDir(name, dirname(importer.split("?")[0]))
5035
+ : at !== -1
5036
+ ? id.slice(0, at + marker.length - 1)
5037
+ : null;
5038
+ imports = dir !== null && importsServerRenderer(dir);
5039
+ externalRenderers.set(name, imports);
5040
+ }
5041
+ if (!imports)
5042
+ return;
5043
+ const message = `${relative(process.cwd(), importer.split("?")[0])} imports ${name}, which imports react-dom/server, and ` +
5044
+ serverRendererMessage(null).replace(/^react-dom\/server cannot run/, "that cannot run");
5045
+ if (!warned.has(message)) {
5046
+ warned.add(message);
5047
+ warnedPackages.add(name);
5048
+ this.warn(message);
5049
+ }
4716
5050
  },
4717
5051
  load(id) {
4718
5052
  if (!id.startsWith(RENDERER_STUB))
4719
5053
  return;
4720
5054
  const importer = decodeURIComponent(id.slice(RENDERER_STUB.length)) || null;
4721
5055
  const message = serverRendererMessage(appImporter(this, importer));
4722
- if (this.environment.mode === "build" && !warned.has(message)) {
5056
+ // The last node_modules segment: Bun's store nests the real package
5057
+ // under node_modules/.bun/<pkg>@<v>/node_modules/<pkg>.
5058
+ const viaPackage = [...(importer ?? "").matchAll(/\/node_modules\/((?:@[^/]+\/)?[^/]+)\//g)].at(-1)?.[1];
5059
+ if (this.environment.mode === "build" &&
5060
+ !warned.has(message) &&
5061
+ !(viaPackage && warnedPackages.has(viaPackage))) {
4723
5062
  warned.add(message);
4724
5063
  this.warn(message);
4725
5064
  }
4726
- return `throw new Error(${JSON.stringify(message)});\n`;
5065
+ // Every name the renderer exports, each throwing the fix when called.
5066
+ // A module that threw when loaded took the whole server down at boot
5067
+ // for one action nobody had called yet, and a stub with no exports
5068
+ // failed the build on MISSING_EXPORT with the message scrolled past.
5069
+ const refuse = `() => { throw new Error(${JSON.stringify(message)}) }`;
5070
+ return (SERVER_RENDERER_EXPORTS.map((name) => `export const ${name} = ${refuse};`).join("\n") +
5071
+ `\nexport const version = "0.0.0";\nexport default { ${SERVER_RENDERER_EXPORTS.join(", ")}, version };\n`);
4727
5072
  },
4728
5073
  };
4729
5074
  }
@@ -4911,6 +5256,14 @@ export function rscKit(options = {}) {
4911
5256
  ...RUNTIME_BUILTINS,
4912
5257
  ...[...DEFAULT_SERVER_EXTERNALS, ...(options.serverExternalPackages ?? [])].map(externalPackage),
4913
5258
  ];
5259
+ // Dependencies with a "use client" file that plugin-rsc would leave
5260
+ // external, because they never declared react as a peer. Bundled into the
5261
+ // server graphs so the directive is read, the same as a package that did.
5262
+ const bundledClientPackages = clientPackages(projectRoot);
5263
+ for (const name of bundledClientPackages) {
5264
+ console.warn(`[rsc-kit] bundling ${name}: it has "use client" files but does not declare react ` +
5265
+ "as a peer dependency, so its components would otherwise run on the server.");
5266
+ }
4914
5267
  const routesPlugin = {
4915
5268
  name: "rsc-kit",
4916
5269
  // A Nitro module, which Nitro's Vite plugin collects from any plugin
@@ -4920,9 +5273,47 @@ export function rscKit(options = {}) {
4920
5273
  nitro: {
4921
5274
  name: "rsc-kit",
4922
5275
  setup(nitro) {
4923
- if (nitro.options.dev || !instrumentationFile())
5276
+ if (nitro.options.dev)
4924
5277
  return;
5278
+ // External at Nitro's layer too. The Vite build leaves these as
5279
+ // imports, and Nitro would then bundle them into its own chunks -
5280
+ // which for a native package fails at load, and for a polyfill like
5281
+ // reflect-metadata runs it after the chunk that checks for it.
5282
+ // Traced, each is copied into .output/server/node_modules and
5283
+ // imported by the built server the way its author expected.
5284
+ nitro.options.traceDeps = [
5285
+ ...(nitro.options.traceDeps ?? []),
5286
+ ...DEFAULT_SERVER_EXTERNALS,
5287
+ ...(options.serverExternalPackages ?? []),
5288
+ ];
5289
+ // The assets, precompressed at build and served with their encoding
5290
+ // by Nitro's own static handler; the host gzips the rest as it
5291
+ // answers. Between them a bun or node server answering the internet
5292
+ // by itself sends nothing raw. A config that set this keeps it.
5293
+ // Nitro's default is a literal false, so ?? would not do; an object
5294
+ // is a configuration and is kept.
5295
+ if (typeof nitro.options.compressPublicAssets !== "object")
5296
+ nitro.options.compressPublicAssets = true;
4925
5297
  nitro.options.virtual ??= {};
5298
+ // A polyfill a dependency checks for at module evaluation, loaded
5299
+ // before any service. The bundler places an external import after
5300
+ // the chunk imports of the module that had it, whatever the source
5301
+ // said, so reflect-metadata came up after tsyringe (under
5302
+ // @peculiar/x509, under @simplewebauthn/server) - by luck survivable
5303
+ // run as a directory, fatal compiled into a binary. A Nitro plugin
5304
+ // is evaluated by the entry itself, and the services that carry the
5305
+ // app are loaded lazily after it; imported by file, the polyfill is
5306
+ // inlined there and runs first wherever the server runs. Only for a
5307
+ // project whose graph has it.
5308
+ const polyfills = FIRST_POLYFILLS.map((name) => packageEntryInGraph(projectRoot, name)).filter((entry) => entry !== null);
5309
+ if (polyfills.length) {
5310
+ nitro.options.virtual["#rsc-kit/polyfills"] =
5311
+ polyfills.map((entry) => `import ${JSON.stringify(entry)};`).join("\n") +
5312
+ "\nexport default () => {};\n";
5313
+ nitro.options.plugins = ["#rsc-kit/polyfills", ...(nitro.options.plugins ?? [])];
5314
+ }
5315
+ if (!instrumentationFile())
5316
+ return;
4926
5317
  nitro.options.virtual["#rsc-kit/startup"] = STARTUP_PLUGIN;
4927
5318
  nitro.options.plugins = [...(nitro.options.plugins ?? []), "#rsc-kit/startup"];
4928
5319
  },
@@ -4938,6 +5329,7 @@ export function rscKit(options = {}) {
4938
5329
  apiRoutes.clear();
4939
5330
  discover(appDir);
4940
5331
  registerMetadataRoutes(appDir);
5332
+ registerOpenApiRoutes();
4941
5333
  siteHosts = ownHosts(rootMetadataBase(appDir), hostsOption);
4942
5334
  // Silent when it worked. The names were printed on every dev start and
4943
5335
  // every build — thirty of them for a middling app, above the output that
@@ -5117,6 +5509,11 @@ export function rscKit(options = {}) {
5117
5509
  alias: aliasEntries(),
5118
5510
  },
5119
5511
  build: { emptyOutDir: true },
5512
+ // What reaches the browser. Vite's own VITE_ prefix, and PUBLIC_ - the
5513
+ // scaffold's spelling, and Next's minus its brand - so a variable
5514
+ // named for a port reads through import.meta.env without a config
5515
+ // line. A config that set its own prefixes keeps them; Vite merges.
5516
+ envPrefix: ["VITE_", "PUBLIC_"],
5120
5517
  environments: {
5121
5518
  // Server bundles — stay under the (non-public) out dir. `bun` and
5122
5519
  // `bun:*` are the runtime's own modules, like `node:*`: nothing to
@@ -5129,6 +5526,8 @@ export function rscKit(options = {}) {
5129
5526
  external: serverExternals,
5130
5527
  },
5131
5528
  },
5529
+ resolve: { noExternal: bundledClientPackages },
5530
+ optimizeDeps: { exclude: bundledClientPackages },
5132
5531
  },
5133
5532
  ssr: {
5134
5533
  build: {
@@ -5137,6 +5536,8 @@ export function rscKit(options = {}) {
5137
5536
  external: serverExternals,
5138
5537
  },
5139
5538
  },
5539
+ resolve: { noExternal: bundledClientPackages },
5540
+ optimizeDeps: { exclude: bundledClientPackages },
5140
5541
  },
5141
5542
  // Client bundle — emitted into public/ for the web server to serve.
5142
5543
  client: {
@@ -5358,8 +5759,12 @@ export function rscKit(options = {}) {
5358
5759
  // when the directory is not there. Only under Nitro, whose presets are
5359
5760
  // the ones without a disk; on its own the plugin serves from outDir.
5360
5761
  if (clientOut && existsSync(staticDir)) {
5361
- const { inlineModuleName, inlineModuleSource } = await import("./files.js");
5762
+ const { inlineModuleName, inlineModuleSource, compileEntrySource } = await import("./files.js");
5362
5763
  writeFileSync(join(dirname(staticDir), inlineModuleName(NITRO_STATIC_DIR)), await inlineModuleSource(staticDir));
5764
+ // And the entry a binary is compiled from, so the pages above end up
5765
+ // inside it rather than rendered live: bun build --compile
5766
+ // .output/server/compile.mjs. The scaffold's script names it.
5767
+ writeFileSync(join(dirname(staticDir), "compile.mjs"), compileEntrySource(NITRO_STATIC_DIR));
5363
5768
  }
5364
5769
  // Manifest first. The service worker precaches whatever it finds in this
5365
5770
  // directory, so writing it afterwards leaves it out of the list — and an
@@ -5444,6 +5849,7 @@ export function rscKit(options = {}) {
5444
5849
  useSsrModules(),
5445
5850
  serverRendererInRsc(),
5446
5851
  metadataRoutesPlugin(),
5852
+ openApiPlugin(),
5447
5853
  extendableClientReferences(),
5448
5854
  typecheckPlugin(),
5449
5855
  clientImportsAudit(),