@rsc-kit/core 0.16.1 → 0.16.2

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.
package/dist/vite.js CHANGED
@@ -12,23 +12,58 @@
12
12
  // carry the route composition and the worker's render contract, and supplies
13
13
  // the structural config (entries, output dirs, base). @vitejs/plugin-rsc is
14
14
  // included here so it always runs before any react() layer the app adds.
15
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
16
- import { gzipSync } from 'node:zlib';
17
- import { createHash } from 'node:crypto';
18
- import { createRequire } from 'node:module';
19
- import { dirname, join, relative, resolve } from 'node:path';
20
- import { fileURLToPath, pathToFileURL } from 'node:url';
21
- import rsc, { getPluginApi } from '@vitejs/plugin-rsc';
22
- import { loadEnv } from 'vite';
23
- import { REPORT_FILE, buildReport } from './buildReport.js';
24
- import { MANIFEST_PATH, manifestWarning, webManifest } from './webManifest.js';
25
- import { ASSET_BASE, appAssets, headTags } from './appAssets.js';
26
- import { httpHostCalls } from './hostCalls.js';
15
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
16
+ import { gzipSync } from "node:zlib";
17
+ import { createHash } from "node:crypto";
18
+ import { createRequire } from "node:module";
19
+ import { dirname, join, relative, resolve } from "node:path";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
+ import rsc, { getPluginApi } from "@vitejs/plugin-rsc";
22
+ import { loadEnv } from "vite";
23
+ import { REPORT_FILE, buildReport } from "./buildReport.js";
24
+ import { MANIFEST_PATH, manifestWarning, webManifest } from "./webManifest.js";
25
+ import { ASSET_BASE, appAssets, headTags } from "./appAssets.js";
26
+ import { httpHostCalls } from "./hostCalls.js";
27
27
  // Resolved once per rscKit() call. One build runs in one process, so these are
28
28
  // module state rather than threaded through every helper.
29
29
  let projectRoot;
30
30
  let sourceDir;
31
- let inlineStylesheets = 'auto';
31
+ let inlineStylesheets = "auto";
32
+ let resolvedConfig = null;
33
+ /** Modules a runtime provides and no bundle should try to carry. */
34
+ const RUNTIME_BUILTINS = ["bun", /^bun:/];
35
+ /**
36
+ * What a client chunk is called on disk.
37
+ *
38
+ * A "use server" module reaches the browser as a proxy - one stub per export
39
+ * that calls the action by id - and the proxy keeps the file's identity, so
40
+ * the bundler can name a chunk after it. When shared client code is merged
41
+ * into that chunk the output has a file called `auth-actions-…js` in
42
+ * public/assets: fifty kilobytes of UI components with the name of the
43
+ * server file, which reads as a leak to anyone who looks. Nothing leaked -
44
+ * the proxy holds ids, never bodies - but a name that says otherwise is a
45
+ * bug. Such a chunk is called what it is.
46
+ */
47
+ function clientChunkFileName(chunk) {
48
+ const table = resolvedConfig
49
+ ? getPluginApi(resolvedConfig)?.manager?.serverReferences?.metaMap
50
+ : undefined;
51
+ if (table && table.size > 0) {
52
+ const bare = (id) => {
53
+ const file = id.split("?")[0].split("/").pop() ?? "";
54
+ return file.replace(/\.[^.]+$/, "");
55
+ };
56
+ const serverModules = new Set([...table.keys()].map(bare));
57
+ const namedAfterServerModule = (chunk.facadeModuleId !== null &&
58
+ serverModules.has(bare(chunk.facadeModuleId))) ||
59
+ (serverModules.has(chunk.name) &&
60
+ chunk.moduleIds.some((id) => bare(id) === chunk.name));
61
+ if (namedAfterServerModule)
62
+ return "assets/client-[hash].js";
63
+ }
64
+ return "assets/[name]-[hash].js";
65
+ }
66
+ let maxActionBody;
32
67
  let outDir;
33
68
  let appDir;
34
69
  let genDir;
@@ -63,7 +98,13 @@ let viewTransitions = false;
63
98
  /** Whether a service worker is generated and registered — see options. */
64
99
  let offline = false;
65
100
  let webManifestOptions = null;
66
- let foundAssets = { favicon: null, icons: [], appleIcon: null, openGraph: null, twitter: null };
101
+ let foundAssets = {
102
+ favicon: null,
103
+ icons: [],
104
+ appleIcon: null,
105
+ openGraph: null,
106
+ twitter: null,
107
+ };
67
108
  /** Host functions to generate stubs for — see RscKitOptions.hostActions. */
68
109
  let hostActions;
69
110
  /**
@@ -78,11 +119,11 @@ let hostActions;
78
119
  /** This package's name, for excluding it from dep optimization. */
79
120
  const PACKAGE_NAME = (() => {
80
121
  try {
81
- const manifest = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8'));
82
- return manifest.name ?? '@rsc-kit/core';
122
+ const manifest = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
123
+ return manifest.name ?? "@rsc-kit/core";
83
124
  }
84
125
  catch {
85
- return '@rsc-kit/core';
126
+ return "@rsc-kit/core";
86
127
  }
87
128
  })();
88
129
  function thisDir() {
@@ -97,7 +138,7 @@ function envRouteConfig() {
97
138
  return { file, dynamicPattern: new RegExp(pattern) };
98
139
  }
99
140
  /** The file a backend writes its action names into. */
100
- const HOST_ACTIONS_FILE = 'rsc-host-actions.json';
141
+ const HOST_ACTIONS_FILE = "rsc-host-actions.json";
101
142
  /**
102
143
  * Host actions, read from a file the backend wrote.
103
144
  *
@@ -118,8 +159,10 @@ function fileHostActions(root) {
118
159
  if (!existsSync(path))
119
160
  return {};
120
161
  try {
121
- const parsed = JSON.parse(readFileSync(path, 'utf-8'));
122
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
162
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
163
+ if (parsed === null ||
164
+ typeof parsed !== "object" ||
165
+ Array.isArray(parsed)) {
123
166
  throw new Error('expected an object of { jsName: "Class.method" }');
124
167
  }
125
168
  return parsed;
@@ -144,23 +187,23 @@ function fileHostActions(root) {
144
187
  function aliasEntries() {
145
188
  if (!packageAlias)
146
189
  return [];
147
- if (existsSync(join(projectRoot, 'node_modules', packageAlias)))
190
+ if (existsSync(join(projectRoot, "node_modules", packageAlias)))
148
191
  return [];
149
192
  return [
150
193
  {
151
- find: new RegExp('^' + packageAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/(.*)$'),
152
- replacement: join(packageDir, 'js') + '/$1',
194
+ find: new RegExp("^" + packageAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/(.*)$"),
195
+ replacement: join(packageDir, "js") + "/$1",
153
196
  },
154
197
  ];
155
198
  }
156
199
  function resolvePaths(options) {
157
200
  projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
158
- sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot, 'src'));
159
- outDir = resolve(options.outDir || process.env.RSC_OUT_DIR || join(projectRoot, '.rsc'));
160
- appDir = join(sourceDir, 'app');
201
+ sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot, "src"));
202
+ outDir = resolve(options.outDir || process.env.RSC_OUT_DIR || join(projectRoot, ".rsc"));
203
+ appDir = join(sourceDir, "app");
161
204
  // Generated entries live under the (in-project) out dir so module resolution
162
205
  // can walk up to the project's node_modules (@vitejs/plugin-rsc, react, ...).
163
- genDir = join(outDir, '.gen');
206
+ genDir = join(outDir, ".gen");
164
207
  // Not a migration guard — those are not worth carrying in a pre-release.
165
208
  // This is the silent-failure guard the rest of this file is written to be.
166
209
  //
@@ -172,31 +215,35 @@ function resolvePaths(options) {
172
215
  //
173
216
  // There is deliberately no check for `nitro`. It is gone from the type, and
174
217
  // a config still passing `nitro: true` is asking for exactly what it gets.
175
- const removed = ['assetsDir', 'assetsUrl'].filter((key) => options[key] !== undefined);
218
+ const removed = ["assetsDir", "assetsUrl"].filter((key) => options[key] !== undefined);
176
219
  if (removed.length > 0) {
177
- throw new Error(`[rsc-kit] ${removed.join(' and ')} ${removed.length === 1 ? 'is' : 'are'} no longer an option.\n\n` +
178
- 'Nitro publishes the browser assets to .output/public and serves them from its own\n' +
179
- 'root, so there is no prefix to set. That directory is the deployment — point nginx\n' +
180
- 'or a CDN at it if something other than the app should serve them.');
220
+ throw new Error(`[rsc-kit] ${removed.join(" and ")} ${removed.length === 1 ? "is" : "are"} no longer an option.\n\n` +
221
+ "Nitro publishes the browser assets to .output/public and serves them from its own\n" +
222
+ "root, so there is no prefix to set. That directory is the deployment — point nginx\n" +
223
+ "or a CDN at it if something other than the app should serve them.");
181
224
  }
182
225
  // Vite's own default. Nitro overrides it with .output/public, which is why
183
226
  // there is nothing here to configure.
184
- publicAssetsDir = resolve(join(projectRoot, 'dist/client'));
185
- typesDir = join(projectRoot, '.rsc-kit');
186
- hotFile = options.hotFile || process.env.RSC_HOT_FILE || '';
227
+ publicAssetsDir = resolve(join(projectRoot, "dist/client"));
228
+ typesDir = join(projectRoot, ".rsc-kit");
229
+ hotFile = options.hotFile || process.env.RSC_HOT_FILE || "";
187
230
  hostCallOptions = options.hostCall;
188
231
  packageDir = resolve(options.packageDir || process.env.RSC_PACKAGE_DIR || thisDir());
189
- hostGlobal = options.hostGlobal || process.env.RSC_HOST_GLOBAL || 'rpc';
190
- interceptManifestFile = resolve(options.interceptManifestFile || process.env.RSC_INTERCEPT_MANIFEST || join(outDir, 'intercept-manifest.json'));
232
+ hostGlobal = options.hostGlobal || process.env.RSC_HOST_GLOBAL || "rpc";
233
+ interceptManifestFile = resolve(options.interceptManifestFile ||
234
+ process.env.RSC_INTERCEPT_MANIFEST ||
235
+ join(outDir, "intercept-manifest.json"));
191
236
  packageAlias = options.packageAlias || process.env.RSC_PACKAGE_ALIAS || null;
192
- devOrigin = options.devOrigin || process.env.RSC_DEV_ORIGIN || '';
193
- output = options.output || process.env.RSC_OUTPUT || 'server';
194
- exportPath = options.exportPath || process.env.RSC_EXPORT_PATH || 'dist';
237
+ devOrigin = options.devOrigin || process.env.RSC_DEV_ORIGIN || "";
238
+ output = options.output || process.env.RSC_OUTPUT || "server";
239
+ exportPath = options.exportPath || process.env.RSC_EXPORT_PATH || "dist";
195
240
  // An export decides this for itself: the client has to ask for payloads by
196
241
  // url because there is no server to read a header, and the name it asks for
197
242
  // is the one the export writes.
198
243
  staticPayloads =
199
- options.staticPayloads || process.env.RSC_STATIC_PAYLOADS || (output === 'export' ? 'index.rsc' : '');
244
+ options.staticPayloads ||
245
+ process.env.RSC_STATIC_PAYLOADS ||
246
+ (output === "export" ? "index.rsc" : "");
200
247
  // No default: which file marks a route dynamic is the host's convention, and
201
248
  // guessing one here would bake a particular backend into a generic plugin.
202
249
  // The env pair exists so a host driving the build out of process can pass it
@@ -204,19 +251,28 @@ function resolvePaths(options) {
204
251
  routeConfig = options.routeConfig ?? envRouteConfig();
205
252
  // A host driving the build out of process cannot pass an option, and may
206
253
  // prerender itself afterwards with paths only it knows.
207
- prerenderAfterBuild = options.prerender ?? process.env.RSC_PRERENDER !== '0';
254
+ // No public switch. A page that must not be frozen says so with
255
+ // `await connection()`, and the build names every page that could not be
256
+ // rendered - the same model Next has with cache components, where the
257
+ // only opt-out is per page. RSC_PRERENDER=0 remains, internal: watch mode
258
+ // sets it, and so does a host that drives the build out of process and
259
+ // prerenders itself afterwards with paths only it knows.
260
+ prerenderAfterBuild = process.env.RSC_PRERENDER !== "0";
208
261
  viewTransitions = options.viewTransitions === true;
209
262
  offline = options.offline === true;
210
- inlineStylesheets = options.inlineStylesheets ?? 'auto';
263
+ inlineStylesheets = options.inlineStylesheets ?? "auto";
264
+ maxActionBody = options.maxActionBody;
211
265
  // One place, and it is the file. A plugin option as well would be the same
212
266
  // thing sayable in two places, which is the problem the file was moved to
213
267
  // solve rather than a convenience to keep beside it.
214
- webManifestOptions = declaredManifest(join(sourceDir, 'app'));
215
- foundAssets = appAssets(join(sourceDir, 'app'));
268
+ webManifestOptions = declaredManifest(join(sourceDir, "app"));
269
+ foundAssets = appAssets(join(sourceDir, "app"));
216
270
  // An app that put icons where they could be found has already listed them.
217
271
  // Writing them again in the manifest is the same set in two places, and the
218
272
  // one that goes stale is the one nobody looks at.
219
- if (webManifestOptions && !webManifestOptions.icons?.length && foundAssets.icons.length) {
273
+ if (webManifestOptions &&
274
+ !webManifestOptions.icons?.length &&
275
+ foundAssets.icons.length) {
220
276
  webManifestOptions = {
221
277
  ...webManifestOptions,
222
278
  icons: foundAssets.icons.map((icon) => icon.href),
@@ -225,7 +281,7 @@ function resolvePaths(options) {
225
281
  hostActions = options.hostActions ?? fileHostActions(projectRoot);
226
282
  }
227
283
  function log(...args) {
228
- console.error('[rsc-kit]', ...args);
284
+ console.error("[rsc-kit]", ...args);
229
285
  }
230
286
  // ── The route manifest ───────────────────────────────────────────────────────
231
287
  /**
@@ -243,37 +299,37 @@ function log(...args) {
243
299
  */
244
300
  /** `[...path]` → catchAll, `[id]` → param, `(group)` → nothing at all. */
245
301
  function urlSegments(componentName) {
246
- const parts = componentName.split('/').slice(1, -1);
302
+ const parts = componentName.split("/").slice(1, -1);
247
303
  const segments = [];
248
304
  for (const part of parts) {
249
305
  // A route group organises files without appearing in the url.
250
- if (part.startsWith('(') && part.endsWith(')'))
306
+ if (part.startsWith("(") && part.endsWith(")"))
251
307
  continue;
252
308
  // A slot directory is not part of its page's url either.
253
- if (part.startsWith('@'))
309
+ if (part.startsWith("@"))
254
310
  continue;
255
- if (part.startsWith('[...') && part.endsWith(']')) {
256
- segments.push({ type: 'catchAll', value: part.slice(4, -1) });
311
+ if (part.startsWith("[...") && part.endsWith("]")) {
312
+ segments.push({ type: "catchAll", value: part.slice(4, -1) });
257
313
  continue;
258
314
  }
259
- if (part.startsWith('[') && part.endsWith(']')) {
260
- segments.push({ type: 'param', value: part.slice(1, -1) });
315
+ if (part.startsWith("[") && part.endsWith("]")) {
316
+ segments.push({ type: "param", value: part.slice(1, -1) });
261
317
  continue;
262
318
  }
263
319
  // An interception marker says which url this replaces, not what it is
264
320
  // called: (.)photo intercepts the sibling /photo. Left in place the
265
321
  // manifest would claim a route at /(.)photo, which nothing can navigate to.
266
- segments.push({ type: 'static', value: part.replace(/^\(\.{1,3}\)/, '') });
322
+ segments.push({ type: "static", value: part.replace(/^\(\.{1,3}\)/, "") });
267
323
  }
268
324
  return segments;
269
325
  }
270
326
  /** Whether a component sits under an interception marker: (.) (..) (...) */
271
327
  function isIntercept(componentName) {
272
- return componentName.split('/').some((part) => /^\(\.{1,3}\)/.test(part));
328
+ return componentName.split("/").some((part) => /^\(\.{1,3}\)/.test(part));
273
329
  }
274
330
  /** The slot directory a component lives under, if any. */
275
331
  function slotOf(componentName) {
276
- const part = componentName.split('/').find((p) => p.startsWith('@'));
332
+ const part = componentName.split("/").find((p) => p.startsWith("@"));
277
333
  return part ? part.slice(1) : null;
278
334
  }
279
335
  /**
@@ -284,15 +340,17 @@ function slotOf(componentName) {
284
340
  */
285
341
  function routeManifest() {
286
342
  const names = [...components.keys()];
287
- const dirOf = (name) => name.split('/').slice(0, -1).join('/');
343
+ const dirOf = (name) => name.split("/").slice(0, -1).join("/");
288
344
  // By path, not by string: 'app/slow3' begins with 'app/slow' as text and is
289
345
  // not inside it, which would hand /slow3 the loading state of /slow.
290
- const isUnder = (dir, ancestor) => dir === ancestor || dir.startsWith(ancestor + '/');
346
+ const isUnder = (dir, ancestor) => dir === ancestor || dir.startsWith(ancestor + "/");
291
347
  const ancestors = (name, base) => names
292
- .filter((n) => n.endsWith('/' + base) && !isIntercept(n) && isUnder(dirOf(name), dirOf(n)))
348
+ .filter((n) => n.endsWith("/" + base) &&
349
+ !isIntercept(n) &&
350
+ isUnder(dirOf(name), dirOf(n)))
293
351
  .sort((a, b) => a.length - b.length);
294
352
  /** Project-root-relative, posix — the same string on every machine. */
295
- const fromRoot = (abs) => relative(projectRoot, abs).replace(/\\/g, '/');
353
+ const fromRoot = (abs) => relative(projectRoot, abs).replace(/\\/g, "/");
296
354
  /** The host's config file in a directory, if the host named one and it exists. */
297
355
  const configIn = (absDir) => {
298
356
  if (!routeConfig)
@@ -303,9 +361,9 @@ function routeManifest() {
303
361
  /** Ancestor configs, outermost first, excluding the page's own directory. */
304
362
  const ancestorConfigs = (dir) => {
305
363
  const found = [];
306
- const parts = dir.split('/').slice(0, -1);
364
+ const parts = dir.split("/").slice(0, -1);
307
365
  while (parts.length > 0) {
308
- const path = configIn(join(sourceDir, parts.join('/')));
366
+ const path = configIn(join(sourceDir, parts.join("/")));
309
367
  if (path)
310
368
  found.unshift(path);
311
369
  parts.pop();
@@ -327,11 +385,11 @@ function routeManifest() {
327
385
  * to whoever knows what they mean.
328
386
  */
329
387
  const middlewareIn = (absDir) => {
330
- for (const file of ['route.ts', 'route.tsx']) {
388
+ for (const file of ["route.ts", "route.tsx"]) {
331
389
  const path = join(absDir, file);
332
390
  if (!existsSync(path))
333
391
  continue;
334
- const match = readFileSync(path, 'utf-8').match(/export\s+const\s+middleware\s*(?::[^=]+)?=\s*\[([^\]]*)\]/);
392
+ const match = readFileSync(path, "utf-8").match(/export\s+const\s+middleware\s*(?::[^=]+)?=\s*\[([^\]]*)\]/);
335
393
  if (!match)
336
394
  continue;
337
395
  // Each quoted literal, rather than splitting the list on commas: a
@@ -353,7 +411,7 @@ function routeManifest() {
353
411
  * outermost point it was asked for.
354
412
  */
355
413
  const hostMiddleware = (dir) => {
356
- const parts = dir.split('/').filter(Boolean);
414
+ const parts = dir.split("/").filter(Boolean);
357
415
  const found = [];
358
416
  for (let depth = 0; depth <= parts.length; depth++) {
359
417
  for (const name of middlewareIn(join(sourceDir, ...parts.slice(0, depth)))) {
@@ -366,35 +424,43 @@ function routeManifest() {
366
424
  const routes = [];
367
425
  const intercepts = [];
368
426
  for (const name of names) {
369
- if (name.endsWith('/page') && isIntercept(name)) {
427
+ if (name.endsWith("/page") && isIntercept(name)) {
370
428
  const slot = slotOf(name);
371
429
  if (slot) {
372
- const marker = name.split('/').find((p) => /^\(\.{1,3}\)/.test(p))?.match(/^\(\.{1,3}\)/)?.[0] ?? '(.)';
373
- intercepts.push({ component: name, slot, segments: urlSegments(name), marker });
430
+ const marker = name
431
+ .split("/")
432
+ .find((p) => /^\(\.{1,3}\)/.test(p))
433
+ ?.match(/^\(\.{1,3}\)/)?.[0] ?? "(.)";
434
+ intercepts.push({
435
+ component: name,
436
+ slot,
437
+ segments: urlSegments(name),
438
+ marker,
439
+ });
374
440
  }
375
441
  continue;
376
442
  }
377
- if (!name.endsWith('/page') || slotOf(name))
443
+ if (!name.endsWith("/page") || slotOf(name))
378
444
  continue;
379
445
  const slots = {};
380
446
  for (const candidate of names) {
381
447
  const slot = slotOf(candidate);
382
448
  // A slot belongs to the layout in the directory that declares it, so it
383
449
  // applies to a page only if that directory is on the page's path.
384
- if (!slot || isIntercept(candidate) || !candidate.endsWith('/default'))
450
+ if (!slot || isIntercept(candidate) || !candidate.endsWith("/default"))
385
451
  continue;
386
- if (isUnder(dirOf(name), candidate.split('/@')[0]))
452
+ if (isUnder(dirOf(name), candidate.split("/@")[0]))
387
453
  slots[slot] = candidate;
388
454
  }
389
455
  routes.push({
390
456
  component: name,
391
457
  segments: urlSegments(name),
392
- layouts: ancestors(name, 'layout').map((n) => n),
393
- loadings: ancestors(name, 'loading').map((n) => n),
394
- errors: ancestors(name, 'error'),
395
- middleware: ancestors(name, 'middleware').map((n) => n),
458
+ layouts: ancestors(name, "layout").map((n) => n),
459
+ loadings: ancestors(name, "loading").map((n) => n),
460
+ errors: ancestors(name, "error"),
461
+ middleware: ancestors(name, "middleware").map((n) => n),
396
462
  slots,
397
- sections: names.filter((n) => SECTION_FILE.test(n + '.tsx') && dirOf(n) === dirOf(name)),
463
+ sections: names.filter((n) => SECTION_FILE.test(n + ".tsx") && dirOf(n) === dirOf(name)),
398
464
  config: configIn(join(sourceDir, dirOf(name))),
399
465
  hostMiddleware: hostMiddleware(dirOf(name)),
400
466
  ancestorConfigs: ancestorConfigs(dirOf(name)),
@@ -412,7 +478,7 @@ function routeManifest() {
412
478
  name,
413
479
  segments: urlSegments(name),
414
480
  methods,
415
- middleware: ancestors(name, 'middleware'),
481
+ middleware: ancestors(name, "middleware"),
416
482
  })),
417
483
  };
418
484
  }
@@ -432,17 +498,17 @@ function routeManifest() {
432
498
  * a line of output; silent costs the types.
433
499
  */
434
500
  function warnIfTypesUnreachable() {
435
- const config = join(projectRoot, 'tsconfig.json');
501
+ const config = join(projectRoot, "tsconfig.json");
436
502
  if (!existsSync(config))
437
503
  return;
438
504
  try {
439
505
  // Comments are legal in a tsconfig and JSON.parse does not take them.
440
- const text = readFileSync(config, 'utf-8').replace(/\/\*[\s\S]*?\*\/|(^|\s)\/\/.*$/gm, '$1');
506
+ const text = readFileSync(config, "utf-8").replace(/\/\*[\s\S]*?\*\/|(^|\s)\/\/.*$/gm, "$1");
441
507
  const include = JSON.parse(text).include;
442
508
  if (!Array.isArray(include))
443
509
  return;
444
- const covers = (what) => include.some((entry) => typeof entry === 'string' && entry.includes(what));
445
- if (!covers('.rsc-kit')) {
510
+ const covers = (what) => include.some((entry) => typeof entry === "string" && entry.includes(what));
511
+ if (!covers(".rsc-kit")) {
446
512
  log(`tsconfig.json does not include .rsc-kit, where the generated types are written.\n` +
447
513
  ` Add ".rsc-kit/**/*" to "include", or typed routes and rpc() fall back to string.`);
448
514
  }
@@ -479,17 +545,17 @@ function writeHostBindings(manifest) {
479
545
  // The global is installed at runtime, so nothing in app source declares it
480
546
  // and a typecheck cannot see it. Written whether or not there are actions:
481
547
  // server components call it directly too.
482
- writeFileSync(join(typesDir, 'rsc-env.d.ts'), renderHostGlobalTypes());
548
+ writeFileSync(join(typesDir, "rsc-env.d.ts"), renderHostGlobalTypes());
483
549
  // The urls this build found, so a link to a page that does not exist fails
484
550
  // the typecheck instead of the browser.
485
- writeFileSync(join(typesDir, 'rsc-routes.d.ts'), renderRouteTypes(manifest));
551
+ writeFileSync(join(typesDir, "rsc-routes.d.ts"), renderRouteTypes(manifest));
486
552
  // The bundle the host imports is generated, so nothing declares it. Written
487
553
  // here rather than left to the app: every app needs the identical file, and
488
554
  // an app-authored one goes stale — the first version named only RscEngine,
489
555
  // which typechecks a server and fails a prerender script.
490
- writeFileSync(join(typesDir, 'rsc-engine.d.ts'), ENGINE_TYPES);
556
+ writeFileSync(join(typesDir, "rsc-engine.d.ts"), ENGINE_TYPES);
491
557
  warnIfTypesUnreachable();
492
- const target = join(sourceDir, 'server-actions.generated.ts');
558
+ const target = join(sourceDir, "server-actions.generated.ts");
493
559
  // A host with no functions of its own leaves no file behind: kept, its
494
560
  // stubs would go on naming targets the host has stopped answering for.
495
561
  if (Object.keys(hostActions).length === 0) {
@@ -538,8 +604,8 @@ function reportAllDynamic() {
538
604
  // The pattern rather than a url: nothing was rendered, so there are no
539
605
  // params and inventing one would name a page that may not exist.
540
606
  const path = route.segments
541
- .map((segment) => (segment.type === 'static' ? segment.value : `[${segment.value}]`))
542
- .join('/');
607
+ .map((segment) => segment.type === "static" ? segment.value : `[${segment.value}]`)
608
+ .join("/");
543
609
  console.log(` \u0192 /${path}`);
544
610
  }
545
611
  console.log(`
@@ -607,7 +673,7 @@ ${swExtra
607
673
  // before anything here can call respondWith on it.
608
674
  self.importScripts(${JSON.stringify(swExtra)})
609
675
  `
610
- : ''}
676
+ : ""}
611
677
 
612
678
  self.addEventListener('install', (event) => {
613
679
  // The new worker takes over rather than waiting for every tab to close.
@@ -848,11 +914,11 @@ self.addEventListener('fetch', (event) => {
848
914
  * it. The file is a literal object by contract, which is all that has to parse.
849
915
  */
850
916
  export function declaredManifest(appDir) {
851
- for (const extension of ['ts', 'tsx', 'js', 'mjs']) {
917
+ for (const extension of ["ts", "tsx", "js", "mjs"]) {
852
918
  const file = join(appDir, `manifest.${extension}`);
853
919
  if (!existsSync(file))
854
920
  continue;
855
- const source = readFileSync(file, 'utf-8');
921
+ const source = readFileSync(file, "utf-8");
856
922
  // The object literal after `export default`, with a `satisfies` or `as`
857
923
  // annotation tolerated after it — which is how anyone who wants the type
858
924
  // checked will actually write the file, and getting that wrong would refuse
@@ -864,8 +930,8 @@ export function declaredManifest(appDir) {
864
930
  const match = /export\s+default\s+(\{[\s\S]*\})(?:\s+(?:satisfies|as)\s+[\w.<>\[\]| ]+)?\s*;?\s*$/.exec(source.trim());
865
931
  if (!match) {
866
932
  throw new Error(`[rsc-kit] app/manifest.${extension} must default-export an object literal.\n` +
867
- 'It is read at build time, before there is a module graph to evaluate it in, so it ' +
868
- 'cannot be computed or imported from elsewhere.');
933
+ "It is read at build time, before there is a module graph to evaluate it in, so it " +
934
+ "cannot be computed or imported from elsewhere.");
869
935
  }
870
936
  try {
871
937
  // Function rather than JSON.parse: the file is TypeScript source with
@@ -902,7 +968,7 @@ function copyAppAssets(clientDir) {
902
968
  for (const asset of all) {
903
969
  const to = join(clientDir, asset.href.slice(1));
904
970
  mkdirSync(dirname(to), { recursive: true });
905
- copyFileSync(join(sourceDir, 'app', asset.file), to);
971
+ copyFileSync(join(sourceDir, "app", asset.file), to);
906
972
  }
907
973
  log(`icons: ${all.length} copied from app/`);
908
974
  }
@@ -926,20 +992,20 @@ function writeWebManifest(clientDir, options) {
926
992
  * which read did it.
927
993
  */
928
994
  function offlineFallback(frozen, results) {
929
- const found = results.find((r) => r.url === '/offline');
995
+ const found = results.find((r) => r.url === "/offline");
930
996
  if (!found)
931
997
  return null;
932
- if (!frozen.includes('/offline')) {
998
+ if (!frozen.includes("/offline")) {
933
999
  // The reason already reads "dynamic — called cookies()", and this sentence
934
1000
  // has said "not stored" by the time it gets there, so the prefix would say
935
1001
  // it twice with a dash in the middle of both.
936
- const why = found.reason?.replace(/^dynamic — /, '') ?? null;
937
- log('offline: /offline cannot be the fallback' +
938
- (why ? `, because it ${why}` : '') +
939
- '. A fallback has to be servable with no network at all.');
1002
+ const why = found.reason?.replace(/^dynamic — /, "") ?? null;
1003
+ log("offline: /offline cannot be the fallback" +
1004
+ (why ? `, because it ${why}` : "") +
1005
+ ". A fallback has to be servable with no network at all.");
940
1006
  return null;
941
1007
  }
942
- return '/offline';
1008
+ return "/offline";
943
1009
  }
944
1010
  /**
945
1011
  * The app's own service worker code, if it wrote any.
@@ -955,11 +1021,11 @@ function offlineFallback(frozen, results) {
955
1021
  * file is there or it is not.
956
1022
  */
957
1023
  function copyServiceWorkerExtra(clientDir) {
958
- const source = join(sourceDir, 'app', 'sw.js');
1024
+ const source = join(sourceDir, "app", "sw.js");
959
1025
  if (!existsSync(source))
960
1026
  return null;
961
- copyFileSync(source, join(clientDir, 'sw-app.js'));
962
- return '/sw-app.js';
1027
+ copyFileSync(source, join(clientDir, "sw-app.js"));
1028
+ return "/sw-app.js";
963
1029
  }
964
1030
  function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
965
1031
  if (!existsSync(clientDir))
@@ -974,7 +1040,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
974
1040
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
975
1041
  // Not itself, and not a map: a debugger asks for those, a visitor does
976
1042
  // not, and precaching them doubles what an install costs.
977
- if (entry.name === 'sw.js' || entry.name.endsWith('.map'))
1043
+ if (entry.name === "sw.js" || entry.name.endsWith(".map"))
978
1044
  continue;
979
1045
  const path = join(dir, entry.name);
980
1046
  if (entry.isDirectory())
@@ -983,13 +1049,16 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
983
1049
  files.push(`${prefix}${entry.name}`);
984
1050
  }
985
1051
  };
986
- walk(clientDir, '');
987
- const precache = ['/', ...files.map((file) => `/${file}`)].sort();
988
- const version = createHash('sha256').update(precache.join('\n')).digest('hex').slice(0, 12);
989
- writeFileSync(join(clientDir, 'sw.js'), SERVICE_WORKER(version, precache, frozen, offlineUrl, extra));
1052
+ walk(clientDir, "");
1053
+ const precache = ["/", ...files.map((file) => `/${file}`)].sort();
1054
+ const version = createHash("sha256")
1055
+ .update(precache.join("\n"))
1056
+ .digest("hex")
1057
+ .slice(0, 12);
1058
+ writeFileSync(join(clientDir, "sw.js"), SERVICE_WORKER(version, precache, frozen, offlineUrl, extra));
990
1059
  log(`offline: ${precache.length} files precached as rsc-kit-${version}` +
991
- (offlineUrl ? `, falling back to ${offlineUrl}` : '') +
992
- (extra ? ', with app/sw.js' : ''));
1060
+ (offlineUrl ? `, falling back to ${offlineUrl}` : "") +
1061
+ (extra ? ", with app/sw.js" : ""));
993
1062
  }
994
1063
  /**
995
1064
  * The rsc bundle the build just wrote, whatever it decided to call it.
@@ -1003,7 +1072,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
1003
1072
  * prints.
1004
1073
  */
1005
1074
  function resolveRscBundle(dir) {
1006
- for (const name of ['index.js', 'index.mjs']) {
1075
+ for (const name of ["index.js", "index.mjs"]) {
1007
1076
  const candidate = join(dir, name);
1008
1077
  if (existsSync(candidate))
1009
1078
  return candidate;
@@ -1023,9 +1092,9 @@ function knownActionsOf(config, root) {
1023
1092
  return [];
1024
1093
  const out = [];
1025
1094
  for (const meta of metaMap.values()) {
1026
- if (meta.importId.includes('/node_modules/'))
1095
+ if (meta.importId.includes("/node_modules/"))
1027
1096
  continue;
1028
- const file = relative(root, meta.importId.split('?')[0] ?? meta.importId);
1097
+ const file = relative(root, meta.importId.split("?")[0] ?? meta.importId);
1029
1098
  for (const name of meta.exportNames) {
1030
1099
  out.push({ id: `${meta.referenceKey}#${name}`, name, file });
1031
1100
  }
@@ -1033,12 +1102,15 @@ function knownActionsOf(config, root) {
1033
1102
  return out;
1034
1103
  }
1035
1104
  async function auditActions(engine, known) {
1036
- const audit = engine
1037
- .auditActions;
1105
+ const audit = engine.auditActions;
1038
1106
  if (!audit || known.length === 0)
1039
1107
  return [];
1040
1108
  const byId = new Map(known.map((k) => [k.id, k]));
1041
- return (await audit(known.map((k) => k.id))).map((a) => ({ ...byId.get(a.id), client: a.client, query: a.query }));
1109
+ return (await audit(known.map((k) => k.id))).map((a) => ({
1110
+ ...byId.get(a.id),
1111
+ client: a.client,
1112
+ query: a.query,
1113
+ }));
1042
1114
  }
1043
1115
  /**
1044
1116
  * Inlined into a page with no runtime when it is at most this big on the
@@ -1050,12 +1122,16 @@ async function auditActions(engine, known) {
1050
1122
  */
1051
1123
  const INLINE_STYLESHEET_LIMIT = 10 * 1024;
1052
1124
  export function smallStylesheetReader(assetsDir, setting) {
1053
- const limit = setting === true ? Infinity : setting === 'auto' ? INLINE_STYLESHEET_LIMIT : setting;
1125
+ const limit = setting === true
1126
+ ? Infinity
1127
+ : setting === "auto"
1128
+ ? INLINE_STYLESHEET_LIMIT
1129
+ : setting;
1054
1130
  return (href) => {
1055
- if (!href.startsWith('/') || href.includes('..'))
1131
+ if (!href.startsWith("/") || href.includes(".."))
1056
1132
  return null;
1057
1133
  try {
1058
- const css = readFileSync(join(assetsDir, href), 'utf-8');
1134
+ const css = readFileSync(join(assetsDir, href), "utf-8");
1059
1135
  return gzipSync(css).length > limit ? null : css;
1060
1136
  }
1061
1137
  catch {
@@ -1072,21 +1148,25 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
1072
1148
  // visitor. The caller resolves the path from the environment now, so this is
1073
1149
  // back to being the impossible case it reads as.
1074
1150
  if (!bundle) {
1075
- throw new Error('[rsc-kit] The build produced no rsc bundle to prerender from.\n' +
1076
- 'Prerendering renders the app, so it needs the bundle the build just wrote. ' +
1077
- 'Build with prerender: false to render every page on demand instead.');
1151
+ throw new Error("[rsc-kit] The build produced no rsc bundle to prerender from.\n" +
1152
+ "Prerendering renders the app, so it needs the bundle the build just wrote.");
1078
1153
  }
1079
- const [{ prerender, NotPrerenderable, summary, legend, notes, clientJsSize, pathKey: pathKeyOf }, { writeTo }, { prerenderApiRoutes },] = await Promise.all([
1080
- import('./prerender.js'),
1081
- import('./files.js'),
1082
- import('./apiPrerender.js'),
1154
+ const [{ prerender, NotPrerenderable, summary, legend, notes, clientJsSize, pathKey: pathKeyOf, }, { writeTo }, { prerenderApiRoutes },] = await Promise.all([
1155
+ import("./prerender.js"),
1156
+ import("./files.js"),
1157
+ import("./apiPrerender.js"),
1083
1158
  ]);
1084
1159
  // Cleared first: a route that changes classification between builds
1085
1160
  // otherwise leaves its old shell on disk and the host goes on serving it.
1086
1161
  // Nothing warns — the page loads, with content from the previous build.
1087
1162
  rmSync(staticDir, { recursive: true, force: true });
1088
1163
  const engine = (await import(pathToFileURL(bundle).href));
1089
- const mark = { frozen: '○', shell: '◐', blocked: '✗', error: '✗' };
1164
+ const mark = {
1165
+ frozen: "○",
1166
+ shell: "◐",
1167
+ blocked: "✗",
1168
+ error: "✗",
1169
+ };
1090
1170
  let failed = 0;
1091
1171
  // Weighed from the page the prerenderer just wrote, so the column is what
1092
1172
  // that page actually loads rather than a total every route is charged for.
@@ -1109,20 +1189,22 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
1109
1189
  engine,
1110
1190
  write: writeTo(staticDir),
1111
1191
  serviceWorker: offline,
1112
- stylesheet: inlineStylesheets === false ? undefined : smallStylesheetReader(assetsDir, inlineStylesheets),
1192
+ stylesheet: inlineStylesheets === false
1193
+ ? undefined
1194
+ : smallStylesheetReader(assetsDir, inlineStylesheets),
1113
1195
  onResult: (r) => {
1114
1196
  collected.push(r);
1115
- if (r.type === 'error' || r.type === 'blocked')
1197
+ if (r.type === "error" || r.type === "blocked")
1116
1198
  failed++;
1117
1199
  const key = pathKeyOf(r.url);
1118
1200
  const file = [`${key}.html`, `${key}.ppr.html`]
1119
1201
  .map((name) => join(staticDir, name))
1120
1202
  .find((path) => existsSync(path));
1121
- const bytes = file ? weigh(readFileSync(file, 'utf-8')) : null;
1203
+ const bytes = file ? weigh(readFileSync(file, "utf-8")) : null;
1122
1204
  if (bytes !== null)
1123
1205
  sized.set(r.url, bytes);
1124
1206
  pending.push({
1125
- line: ` ${mark[r.type] ?? ' '} ${r.url}`,
1207
+ line: ` ${mark[r.type] ?? " "} ${r.url}`,
1126
1208
  bytes,
1127
1209
  extra: [
1128
1210
  ...(r.reason ? [` ${r.reason}`] : []),
@@ -1147,16 +1229,19 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
1147
1229
  const apis = await prerenderApiRoutes(engine, manifest, writeTo(staticDir));
1148
1230
  for (const api of apis) {
1149
1231
  pending.push({
1150
- line: ` ${api.type === 'frozen' ? '○' : 'ƒ'} ${api.url}`,
1232
+ line: ` ${api.type === "frozen" ? "○" : "ƒ"} ${api.url}`,
1151
1233
  bytes: null,
1152
- extra: api.reason ? [` ${api.reason}`] : [],
1234
+ extra: [
1235
+ ...(api.reason ? [` ${api.reason}`] : []),
1236
+ ...(api.warning ? [` ⚠ ${api.warning}`] : []),
1237
+ ],
1153
1238
  });
1154
1239
  }
1155
1240
  // Printed together rather than as each route lands, because a column has to
1156
1241
  // line up and the widest url is not known until the last one is in.
1157
1242
  const column = Math.max(...pending.map((p) => p.line.length)) + 2;
1158
1243
  for (const row of pending) {
1159
- const size = row.bytes === null ? '' : clientJsSize(row.bytes);
1244
+ const size = row.bytes === null ? "" : clientJsSize(row.bytes);
1160
1245
  console.log(size ? row.line.padEnd(column) + size : row.line);
1161
1246
  for (const line of row.extra)
1162
1247
  console.log(line);
@@ -1172,10 +1257,12 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
1172
1257
  const byFile = new Map();
1173
1258
  for (const a of bare)
1174
1259
  byFile.set(a.file, [...(byFile.get(a.file) ?? []), a.name]);
1175
- console.log(`\n \u26a0 ${bare.length} ${bare.length === 1 ? 'action runs' : 'actions run'} no middleware: ` +
1176
- [...byFile].map(([file, names]) => `${names.join(', ')} (${file})`).join('; '));
1177
- console.log(' Nothing checks who calls them. Fine for a public one; otherwise build it\n' +
1178
- ' from an action client, so the check cannot be forgotten.');
1260
+ console.log(`\n \u26a0 ${bare.length} ${bare.length === 1 ? "action runs" : "actions run"} no middleware: ` +
1261
+ [...byFile]
1262
+ .map(([file, names]) => `${names.join(", ")} (${file})`)
1263
+ .join("; "));
1264
+ console.log(" Nothing checks who calls them. Fine for a public one; otherwise build it\n" +
1265
+ " from an action client, so the check cannot be forgotten.");
1179
1266
  }
1180
1267
  // Written from the rows that were just printed rather than recomputed: the
1181
1268
  // report and the terminal must not be able to disagree about what happened.
@@ -1187,22 +1274,29 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
1187
1274
  warning: r.warning ?? null,
1188
1275
  note: r.note ?? null,
1189
1276
  clientJs: sized.get(r.url) ?? null,
1190
- })), apis.map((a) => ({ url: a.url, name: a.name, type: a.type, reason: a.reason })), audited));
1277
+ })), apis.map((a) => ({
1278
+ url: a.url,
1279
+ name: a.name,
1280
+ type: a.type,
1281
+ reason: a.reason,
1282
+ warning: a.warning ?? null,
1283
+ })), audited));
1191
1284
  const note = notes(results);
1192
1285
  const counted = [...results, ...apis];
1193
1286
  console.log(`
1194
1287
  ${legend(counted)}
1195
1288
 
1196
- ${summary(counted)}${note ? `\n\n${note}` : ''}`);
1289
+ ${summary(counted)}${note ? `\n\n${note}` : ""}`);
1197
1290
  // The table and the report are on disk. Now the refusal, in its own words.
1198
1291
  if (refusal)
1199
1292
  throw new Error(refusal.message);
1200
1293
  if (failed > 0) {
1201
- throw new Error(`[rsc-kit] ${failed} route${failed === 1 ? '' : 's'} failed to render.\n` +
1202
- 'Prerendering runs your app: whatever those pages need at render time has to be\n' +
1203
- 'reachable from the build. Fix them, or build with prerender: false and render on demand.');
1294
+ throw new Error(`[rsc-kit] ${failed} route${failed === 1 ? "" : "s"} failed to render.\n` +
1295
+ "Prerendering runs your app: whatever those pages need at render time has to be\n" +
1296
+ "reachable from the build. Give it that, or mark the read with `await connection()`\n" +
1297
+ "so the page renders per request and the rest of it is still stored.");
1204
1298
  }
1205
- if (output === 'export')
1299
+ if (output === "export")
1206
1300
  await exportAfterPrerender(results, staticDir, assetsDir);
1207
1301
  // The urls whose answer cannot change until the next build. The service
1208
1302
  // worker serves these from its cache first rather than asking the network
@@ -1221,8 +1315,8 @@ ${legend(counted)}
1221
1315
  const guarded = new Set(manifest.routes.filter((r) => r.middleware?.length).map((r) => r.component));
1222
1316
  return {
1223
1317
  frozen: results
1224
- .filter((r) => r.type === 'frozen' && !guarded.has(r.component))
1225
- .filter((r) => existsSync(join(staticDir, pathKeyOf(r.url) + '.html')))
1318
+ .filter((r) => r.type === "frozen" && !guarded.has(r.component))
1319
+ .filter((r) => existsSync(join(staticDir, pathKeyOf(r.url) + ".html")))
1226
1320
  .map((r) => r.url),
1227
1321
  results,
1228
1322
  };
@@ -1249,7 +1343,9 @@ function weighClientJs(assetsDir) {
1249
1343
  if (cached !== undefined)
1250
1344
  return cached;
1251
1345
  const file = join(assetsDir, asset);
1252
- const bytes = existsSync(file) ? gzipSync(readFileSync(file)).byteLength : 0;
1346
+ const bytes = existsSync(file)
1347
+ ? gzipSync(readFileSync(file)).byteLength
1348
+ : 0;
1253
1349
  weighed.set(asset, bytes);
1254
1350
  return bytes;
1255
1351
  };
@@ -1259,10 +1355,10 @@ function weighClientJs(assetsDir) {
1259
1355
  const named = new Set();
1260
1356
  // Split rather than matched: /assets/name.js is the only shape written, and
1261
1357
  // a regex over a whole document is the slower half of this function.
1262
- for (const piece of html.split('/assets/').slice(1)) {
1358
+ for (const piece of html.split("/assets/").slice(1)) {
1263
1359
  const name = piece.split(/["'\s)]/)[0];
1264
- if (name.endsWith('.js'))
1265
- named.add('assets/' + name);
1360
+ if (name.endsWith(".js"))
1361
+ named.add("assets/" + name);
1266
1362
  }
1267
1363
  let total = 0;
1268
1364
  for (const asset of named)
@@ -1286,22 +1382,19 @@ function weighClientJs(assetsDir) {
1286
1382
  * page that loads and then stays empty forever.
1287
1383
  */
1288
1384
  async function exportAfterPrerender(results, staticDir, assetsDir) {
1289
- const [{ exportSite, NotExportable }, { writeTo, prerenderedFrom, copyAssets }] = await Promise.all([
1290
- import('./export.js'),
1291
- import('./files.js'),
1292
- ]);
1385
+ const [{ exportSite, NotExportable }, { writeTo, prerenderedFrom, copyAssets },] = await Promise.all([import("./export.js"), import("./files.js")]);
1293
1386
  try {
1294
1387
  const { pages, refused } = await exportSite({
1295
1388
  results,
1296
1389
  read: prerenderedFrom(staticDir),
1297
1390
  write: writeTo(exportPath),
1298
1391
  manifest: routeManifest(),
1299
- assets: copyAssets(join(assetsDir, 'assets'), exportPath, '/assets/'),
1300
- force: process.env.RSC_EXPORT_FORCE === '1',
1392
+ assets: copyAssets(join(assetsDir, "assets"), exportPath, "/assets/"),
1393
+ force: process.env.RSC_EXPORT_FORCE === "1",
1301
1394
  });
1302
- console.log(`\n Exported ${pages} page${pages === 1 ? '' : 's'} to ${relative(projectRoot, exportPath)}`);
1395
+ console.log(`\n Exported ${pages} page${pages === 1 ? "" : "s"} to ${relative(projectRoot, exportPath)}`);
1303
1396
  if (refused.length > 0) {
1304
- console.log(` Left out ${refused.length}: ${refused.map((r) => r.url).join(', ')}`);
1397
+ console.log(` Left out ${refused.length}: ${refused.map((r) => r.url).join(", ")}`);
1305
1398
  }
1306
1399
  }
1307
1400
  catch (error) {
@@ -1313,15 +1406,15 @@ async function exportAfterPrerender(results, staticDir, assetsDir) {
1313
1406
  /** `/posts/[slug]` — the pattern, in the shape the app writes its links in. */
1314
1407
  function patternOf(segments) {
1315
1408
  if (segments.length === 0)
1316
- return '/';
1317
- return ('/' +
1409
+ return "/";
1410
+ return ("/" +
1318
1411
  segments
1319
- .map((segment) => segment.type === 'static'
1412
+ .map((segment) => segment.type === "static"
1320
1413
  ? segment.value
1321
- : segment.type === 'catchAll'
1414
+ : segment.type === "catchAll"
1322
1415
  ? `[...${segment.value}]`
1323
1416
  : `[${segment.value}]`)
1324
- .join('/'));
1417
+ .join("/"));
1325
1418
  }
1326
1419
  /**
1327
1420
  * The app's routes as a union, for `@rsc-kit/core/routes` to derive from.
@@ -1335,8 +1428,12 @@ function patternOf(segments) {
1335
1428
  * the union twice and imply you could link to a modal.
1336
1429
  */
1337
1430
  function renderRouteTypes(manifest) {
1338
- const patterns = [...new Set(manifest.routes.map((route) => patternOf(route.segments)))].sort();
1339
- const apis = [...new Set((manifest.apis ?? []).map((route) => patternOf(route.segments)))].sort();
1431
+ const patterns = [
1432
+ ...new Set(manifest.routes.map((route) => patternOf(route.segments))),
1433
+ ].sort();
1434
+ const apis = [
1435
+ ...new Set((manifest.apis ?? []).map((route) => patternOf(route.segments))),
1436
+ ].sort();
1340
1437
  // Each pattern to the page module that answers it, as a type-only import
1341
1438
  // from the generated file's own directory. SearchExportOf reads the page's
1342
1439
  // `searchParams` export, or undefined when there is none, so nothing here
@@ -1347,82 +1444,114 @@ function renderRouteTypes(manifest) {
1347
1444
  const pattern = patternOf(route.segments);
1348
1445
  if (search.has(pattern))
1349
1446
  continue;
1350
- const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g, '/');
1351
- search.set(pattern, target.startsWith('.') ? target : './' + target);
1447
+ const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g, "/");
1448
+ search.set(pattern, target.startsWith(".") ? target : "./" + target);
1352
1449
  }
1353
1450
  return [
1354
- '// @generated — do not edit. Written by the RSC build from the route tree.',
1355
- '//',
1356
- '// Turns Link, navigate() and route() into typed apis: an href that no route',
1357
- '// answers stops compiling. Delete this file and they fall back to `string`,',
1358
- '// which is what a project that has not built yet gets.',
1359
- '',
1360
- '// `export {}` is load-bearing: in a file with no import or export,',
1361
- '// `declare module` *replaces* the real module rather than augmenting it,',
1362
- '// and Href and route() vanish from it with no error to explain why.',
1363
- 'export {}',
1364
- '',
1451
+ "// @generated — do not edit. Written by the RSC build from the route tree.",
1452
+ "//",
1453
+ "// Turns Link, navigate() and route() into typed apis: an href that no route",
1454
+ "// answers stops compiling. Delete this file and they fall back to `string`,",
1455
+ "// which is what a project that has not built yet gets.",
1456
+ "",
1457
+ "// `export {}` is load-bearing: in a file with no import or export,",
1458
+ "// `declare module` *replaces* the real module rather than augmenting it,",
1459
+ "// and Href and route() vanish from it with no error to explain why.",
1460
+ "export {}",
1461
+ "",
1365
1462
  "declare module '@rsc-kit/core/routes' {",
1366
- ' interface Register {',
1463
+ " interface Register {",
1367
1464
  patterns.length > 0
1368
- ? ' routes:\n' + patterns.map((p) => ' | ' + JSON.stringify(p)).join('\n')
1369
- : ' // No routes found under the source directory.\n routes: never',
1465
+ ? " routes:\n" +
1466
+ patterns.map((p) => " | " + JSON.stringify(p)).join("\n")
1467
+ : " // No routes found under the source directory.\n routes: never",
1370
1468
  // The searchParams schema each page exports, read off the module's type.
1371
1469
  // This is what types Link's `search` prop and href() per route.
1372
- ' search: {',
1373
- ...[...search].sort().map(([pattern, target]) => ' ' + JSON.stringify(pattern) + ': SearchExportOf<typeof import(' + JSON.stringify(target) + ')>'),
1374
- ' }',
1375
- ' }',
1470
+ " search: {",
1471
+ ...[...search]
1472
+ .sort()
1473
+ .map(([pattern, target]) => " " +
1474
+ JSON.stringify(pattern) +
1475
+ ": SearchExportOf<typeof import(" +
1476
+ JSON.stringify(target) +
1477
+ ")>"),
1478
+ " }",
1479
+ " }",
1376
1480
  // Api routes are a separate union, so Link refuses an api url and apiUrl()
1377
1481
  // refuses a page. Linking to an api route navigates the browser away to a
1378
1482
  // json document, which is the mistake worth catching.
1379
- ' interface RegisterApi {',
1483
+ " interface RegisterApi {",
1380
1484
  apis.length > 0
1381
- ? ' apis:\n' + apis.map((p) => ' | ' + JSON.stringify(p)).join('\n')
1382
- : ' // No route.ts files found under the source directory.\n apis: never',
1383
- ' }',
1384
- '}',
1385
- '',
1386
- ].join('\n');
1485
+ ? " apis:\n" +
1486
+ apis.map((p) => " | " + JSON.stringify(p)).join("\n")
1487
+ : " // No route.ts files found under the source directory.\n apis: never",
1488
+ " }",
1489
+ "}",
1490
+ "",
1491
+ ].join("\n");
1387
1492
  }
1388
1493
  /** The "use server" module exposing each host function as a plain async call. */
1389
1494
  function renderHostActions() {
1390
1495
  const lines = [
1391
1496
  '"use server";',
1392
- '// @generated — do not edit. Written by the RSC build from the host action map.',
1393
- '',
1497
+ "// @generated — do not edit. Written by the RSC build from the host action map.",
1498
+ "",
1394
1499
  ];
1395
1500
  for (const [name, target] of Object.entries(hostActions)) {
1396
- lines.push('export async function ' + name + '(...args: unknown[]) {');
1397
- lines.push(' return await (globalThis as any).' + hostGlobal + '(' + JSON.stringify(target) + ', ...args);');
1398
- lines.push('}');
1399
- lines.push('');
1501
+ lines.push("export async function " + name + "(...args: unknown[]) {");
1502
+ lines.push(" return await (globalThis as any)." +
1503
+ hostGlobal +
1504
+ "(" +
1505
+ JSON.stringify(target) +
1506
+ ", ...args);");
1507
+ lines.push("}");
1508
+ lines.push("");
1400
1509
  }
1401
- return lines.join('\n');
1510
+ return lines.join("\n");
1402
1511
  }
1403
1512
  /** Ambient declaration for the host global, written beside the app's source. */
1404
1513
  function renderHostGlobalTypes() {
1405
1514
  return [
1406
- '// @generated — do not edit.',
1407
- '//',
1408
- '// ' + hostGlobal + '() is installed on globalThis by the RSC worker, so it has no',
1409
- '// import to resolve. This declares it for the typechecker; run',
1410
- '// `tsc --noEmit` to catch calls to a host global that no longer exists.',
1411
- '//',
1412
- '// Deliberately not a module — no import/export — so the declaration is',
1413
- '// global to the project without every file having to reference it.',
1414
- '',
1415
- 'declare function ' + hostGlobal + '<T = unknown>(name: string, ...args: unknown[]): Promise<T>;',
1416
- '',
1417
- ].join('\n');
1515
+ "// @generated — do not edit.",
1516
+ "//",
1517
+ "// " +
1518
+ hostGlobal +
1519
+ "() is installed on globalThis by the RSC worker, so it has no",
1520
+ "// import to resolve. This declares it for the typechecker; run",
1521
+ "// `tsc --noEmit` to catch calls to a host global that no longer exists.",
1522
+ "//",
1523
+ "// Deliberately not a module — no import/export — so the declaration is",
1524
+ "// global to the project without every file having to reference it.",
1525
+ "",
1526
+ "declare function " +
1527
+ hostGlobal +
1528
+ "<T = unknown>(name: string, ...args: unknown[]): Promise<T>;",
1529
+ "",
1530
+ ].join("\n");
1418
1531
  }
1419
1532
  // ── Discovery ────────────────────────────────────────────────────────────────
1420
- const ROUTE_FILES = ['page', 'layout', 'loading', 'error', 'not-found', 'default', 'middleware'];
1533
+ const ROUTE_FILES = [
1534
+ "page",
1535
+ "layout",
1536
+ "loading",
1537
+ "error",
1538
+ "not-found",
1539
+ "default",
1540
+ "middleware",
1541
+ ];
1421
1542
  /** The methods a route.ts may export. HEAD and OPTIONS are answered for you. */
1422
- const API_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
1543
+ const API_METHODS = [
1544
+ "GET",
1545
+ "POST",
1546
+ "PUT",
1547
+ "PATCH",
1548
+ "DELETE",
1549
+ "HEAD",
1550
+ "OPTIONS",
1551
+ ];
1423
1552
  /** `orders.section.tsx` — a region of a page that can be refreshed by name. */
1424
1553
  const SECTION_FILE = /\.section\.(tsx|jsx|ts|js)$/;
1425
- const EXTS = ['tsx', 'jsx', 'ts', 'js'];
1554
+ const EXTS = ["tsx", "jsx", "ts", "js"];
1426
1555
  function findRouteFile(dir, base) {
1427
1556
  for (const ext of EXTS) {
1428
1557
  const p = join(dir, `${base}.${ext}`);
@@ -1432,11 +1561,11 @@ function findRouteFile(dir, base) {
1432
1561
  return null;
1433
1562
  }
1434
1563
  function componentName(absPath) {
1435
- const rel = relative(sourceDir, absPath).replace(/\\/g, '/');
1436
- return rel.replace(/\.(tsx|jsx|ts|js)$/, '');
1564
+ const rel = relative(sourceDir, absPath).replace(/\\/g, "/");
1565
+ return rel.replace(/\.(tsx|jsx|ts|js)$/, "");
1437
1566
  }
1438
1567
  function toAlias(name) {
1439
- return '_c_' + name.replace(/[^a-zA-Z0-9]/g, '_');
1568
+ return "_c_" + name.replace(/[^a-zA-Z0-9]/g, "_");
1440
1569
  }
1441
1570
  const components = new Map();
1442
1571
  /**
@@ -1467,8 +1596,8 @@ function register(absPath) {
1467
1596
  */
1468
1597
  function registerApiRoute(absPath) {
1469
1598
  const name = componentName(absPath);
1470
- const source = readFileSync(absPath, 'utf-8');
1471
- const methods = API_METHODS.filter((method) => new RegExp(`^\\s*export\\s+(?:async\\s+function|function|const|let|var)\\s+${method}\\b`, 'm').test(source));
1599
+ const source = readFileSync(absPath, "utf-8");
1600
+ const methods = API_METHODS.filter((method) => new RegExp(`^\\s*export\\s+(?:async\\s+function|function|const|let|var)\\s+${method}\\b`, "m").test(source));
1472
1601
  if (methods.length === 0) {
1473
1602
  // `route.ts` is also where a host declares the guards for everything below
1474
1603
  // it — the file predates api routes and is still read that way. One that
@@ -1478,7 +1607,7 @@ function registerApiRoute(absPath) {
1478
1607
  return;
1479
1608
  throw new Error(`[rsc-kit] ${relative(projectRoot, absPath)} exports no request methods.\n` +
1480
1609
  ` Export one named for the method it answers — export function GET(request: Request) — ` +
1481
- `or delete the file. One of: ${API_METHODS.join(', ')}.`);
1610
+ `or delete the file. One of: ${API_METHODS.join(", ")}.`);
1482
1611
  }
1483
1612
  apiRoutes.set(name, { name, absPath, methods });
1484
1613
  }
@@ -1491,7 +1620,7 @@ function discover(dir) {
1491
1620
  // route.ts — an api endpoint, colocated with the pages it sits among. Read
1492
1621
  // for its method exports here rather than at request time, so a route that
1493
1622
  // exports nothing callable is a build error instead of a 404 nobody explains.
1494
- const api = findRouteFile(dir, 'route');
1623
+ const api = findRouteFile(dir, "route");
1495
1624
  if (api)
1496
1625
  registerApiRoute(api);
1497
1626
  // Named regions. Registered like any other component so the generated entry
@@ -1517,7 +1646,7 @@ function discover(dir) {
1517
1646
  * for almost every route in an app.
1518
1647
  */
1519
1648
  function metadataExports(absPath) {
1520
- const src = readFileSync(absPath, 'utf-8');
1649
+ const src = readFileSync(absPath, "utf-8");
1521
1650
  return {
1522
1651
  static: /export\s+const\s+metadata\b/.test(src),
1523
1652
  generate: /export\s+(async\s+)?function\s+generateMetadata\b/.test(src),
@@ -1533,7 +1662,7 @@ function metadataExports(absPath) {
1533
1662
  * inferred because there is nothing to infer it from.
1534
1663
  */
1535
1664
  function hasStaticParams(absPath) {
1536
- const src = readFileSync(absPath, 'utf-8');
1665
+ const src = readFileSync(absPath, "utf-8");
1537
1666
  return /export\s+((async\s+)?function\s+generateStaticParams|const\s+generateStaticParams)/.test(src);
1538
1667
  }
1539
1668
  /**
@@ -1549,7 +1678,7 @@ function hasStaticParams(absPath) {
1549
1678
  * import for something that can never validate.
1550
1679
  */
1551
1680
  function urlSchemaExports(absPath) {
1552
- const src = readFileSync(absPath, 'utf-8');
1681
+ const src = readFileSync(absPath, "utf-8");
1553
1682
  return {
1554
1683
  params: /export\s+const\s+params\s*[=:]/.test(src),
1555
1684
  searchParams: /export\s+const\s+searchParams\s*[=:]/.test(src),
@@ -1671,7 +1800,7 @@ const FALLBACK_BODY = ` const answer = await devHandler(request)
1671
1800
  * the failure is silent: the server finds no directory, decides nothing was
1672
1801
  * frozen, and renders every page live exactly as it did before.
1673
1802
  */
1674
- const NITRO_STATIC_DIR = 'rsc-static';
1803
+ const NITRO_STATIC_DIR = "rsc-static";
1675
1804
  /**
1676
1805
  * Serve what the build froze.
1677
1806
  *
@@ -1710,15 +1839,15 @@ function installHostCallsOnce(): void {
1710
1839
  }
1711
1840
 
1712
1841
  `;
1713
- function generateEntryRsc(fallbackOrigin = '') {
1842
+ function generateEntryRsc(fallbackOrigin = "") {
1714
1843
  // The 404 page, if the app has one, and the layouts it renders inside.
1715
1844
  // Computed here rather than looked up at runtime: not-found is not a route,
1716
1845
  // so the manifest has no entry to read its chain from.
1717
- const notFoundComponent = [...components.keys()].find((name) => name.endsWith('/not-found'));
1846
+ const notFoundComponent = [...components.keys()].find((name) => name.endsWith("/not-found"));
1718
1847
  const notFoundLayouts = notFoundComponent
1719
1848
  ? [...components.keys()]
1720
- .filter((name) => name.endsWith('/layout') &&
1721
- notFoundComponent.startsWith(name.slice(0, -'layout'.length)))
1849
+ .filter((name) => name.endsWith("/layout") &&
1850
+ notFoundComponent.startsWith(name.slice(0, -"layout".length)))
1722
1851
  .sort((a, b) => a.length - b.length)
1723
1852
  : [];
1724
1853
  const imports = [];
@@ -1743,7 +1872,7 @@ function generateEntryRsc(fallbackOrigin = '') {
1743
1872
  meta.static ? `static: ${c.alias}_meta.metadata` : null,
1744
1873
  meta.generate ? `generate: ${c.alias}_meta.generateMetadata` : null,
1745
1874
  ].filter(Boolean);
1746
- metaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(', ')} },`);
1875
+ metaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(", ")} },`);
1747
1876
  }
1748
1877
  if (hasStaticParams(c.absPath)) {
1749
1878
  // The namespace import may already be in place for metadata; a second
@@ -1756,9 +1885,11 @@ function generateEntryRsc(fallbackOrigin = '') {
1756
1885
  imports.push(`import * as ${c.alias}_schema from ${JSON.stringify(c.absPath)}`);
1757
1886
  const fields = [
1758
1887
  urlSchemas.params ? `params: ${c.alias}_schema.params` : null,
1759
- urlSchemas.searchParams ? `searchParams: ${c.alias}_schema.searchParams` : null,
1888
+ urlSchemas.searchParams
1889
+ ? `searchParams: ${c.alias}_schema.searchParams`
1890
+ : null,
1760
1891
  ].filter(Boolean);
1761
- schemaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(', ')} },`);
1892
+ schemaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(", ")} },`);
1762
1893
  }
1763
1894
  }
1764
1895
  // The engine's own modules are named without an extension: this plugin runs
@@ -1777,21 +1908,21 @@ import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDi
1777
1908
  import { noteRequestRead } from ${JSON.stringify(join(packageDir, "request"))}
1778
1909
  import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
1779
1910
  import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
1780
- import { httpHostCalls } from ${JSON.stringify(join(packageDir, 'hostCalls'))}
1781
- import { prerenderedBeside } from ${JSON.stringify(join(packageDir, 'files'))}
1911
+ import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
1912
+ import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
1782
1913
  import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
1783
- import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, 'query'))}
1784
- import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, 'action'))}
1914
+ import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
1915
+ import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
1785
1916
  import { Suspense, createElement, Fragment } from 'react'
1786
1917
  import { AsyncLocalStorage } from 'node:async_hooks'
1787
- ${imports.join('\n')}
1918
+ ${imports.join("\n")}
1788
1919
 
1789
1920
  type HostFn = (name: string, ...args: unknown[]) => Promise<unknown>
1790
1921
  type LayoutEntry = { component: string; props?: Record<string, unknown> }
1791
1922
  type SlotOverride = { component: string; props?: Record<string, unknown> }
1792
1923
 
1793
1924
  const components: Record<string, any> = {
1794
- ${mapEntries.join('\n')}
1925
+ ${mapEntries.join("\n")}
1795
1926
  }
1796
1927
 
1797
1928
  /**
@@ -1811,16 +1942,21 @@ ${mapEntries.join('\n')}
1811
1942
  const APP_HEAD: { tag: any; props: Record<string, string> }[] = ${JSON.stringify(headTags(foundAssets))}
1812
1943
 
1813
1944
  const WEB_MANIFEST: { href: string; themeColor?: string } | null = ${JSON.stringify(webManifestOptions
1814
- ? { href: MANIFEST_PATH, ...(webManifestOptions.themeColor ? { themeColor: webManifestOptions.themeColor } : {}) }
1945
+ ? {
1946
+ href: MANIFEST_PATH,
1947
+ ...(webManifestOptions.themeColor
1948
+ ? { themeColor: webManifestOptions.themeColor }
1949
+ : {}),
1950
+ }
1815
1951
  : null)}
1816
1952
 
1817
1953
  const urlSchemas: Record<string, { params?: any; searchParams?: any }> = {
1818
- ${schemaEntries.join('\n')}
1954
+ ${schemaEntries.join("\n")}
1819
1955
  }
1820
1956
 
1821
1957
  /** route.ts modules, by the name the manifest matched. */
1822
1958
  const apiRoutes: Record<string, any> = {
1823
- ${apiEntries.join('\n')}
1959
+ ${apiEntries.join("\n")}
1824
1960
  }
1825
1961
 
1826
1962
  /**
@@ -1956,11 +2092,11 @@ export async function handleApiRoute(
1956
2092
  }
1957
2093
 
1958
2094
  const metadataMap: Record<string, { static?: any; generate?: (p: any) => any }> = {
1959
- ${metaEntries.join('\n')}
2095
+ ${metaEntries.join("\n")}
1960
2096
  }
1961
2097
 
1962
2098
  const staticParamsMap: Record<string, () => any> = {
1963
- ${paramEntries.join('\n')}
2099
+ ${paramEntries.join("\n")}
1964
2100
  }
1965
2101
 
1966
2102
  /**
@@ -3490,7 +3626,7 @@ export async function handleRscPprShell(
3490
3626
  * server rendering live pages it had already frozen. The gate is the mode, not
3491
3627
  * the file.
3492
3628
  */
3493
- ${fallbackOrigin ? FALLBACK_CONSTS.replace('__ORIGIN__', JSON.stringify(fallbackOrigin)) : ''}${NITRO_HOST_CALLS}
3629
+ ${fallbackOrigin ? FALLBACK_CONSTS.replace("__ORIGIN__", JSON.stringify(fallbackOrigin)) : ""}${NITRO_HOST_CALLS}
3494
3630
  let devHandler: ((request: Request) => Promise<Response | null>) | null = null
3495
3631
 
3496
3632
  export default async function handler(request: Request): Promise<Response> {
@@ -3514,7 +3650,8 @@ export default async function handler(request: Request): Promise<Response> {
3514
3650
  resolveMetadata,
3515
3651
  runRouteMiddleware,
3516
3652
  } as never,
3517
- ${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED} })
3653
+ ${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED} maxActionBody: ${maxActionBody === undefined ? "undefined" : String(maxActionBody)},
3654
+ })
3518
3655
 
3519
3656
  ${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (await notFound())\n"}}
3520
3657
 
@@ -3528,7 +3665,8 @@ ${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (awa
3528
3665
  * Without a not-found.tsx this is the string it always was.
3529
3666
  */
3530
3667
  async function notFound(): Promise<Response> {
3531
- ${notFoundComponent ? `
3668
+ ${notFoundComponent
3669
+ ? `
3532
3670
  try {
3533
3671
  const { htmlStream } = await handleRscHtmlStream(
3534
3672
  ${JSON.stringify(notFoundComponent)},
@@ -3549,13 +3687,14 @@ async function notFound(): Promise<Response> {
3549
3687
  // A 404 page that throws is still a 404. Falling back rather than
3550
3688
  // answering 500 keeps the status honest about what happened.
3551
3689
  }
3552
- ` : ''}
3690
+ `
3691
+ : ""}
3553
3692
  return new Response('Not found', { status: 404 })
3554
3693
  }
3555
3694
  `;
3556
3695
  }
3557
3696
  function generateEntrySsr() {
3558
- const devUrls = join(packageDir, 'devUrls');
3697
+ const devUrls = join(packageDir, "devUrls");
3559
3698
  return `// GENERATED by rscKit() — do not edit.
3560
3699
  import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
3561
3700
  import { renderToReadableStream, resume } from 'react-dom/server.edge'
@@ -3682,7 +3821,7 @@ export default {
3682
3821
  }
3683
3822
  `;
3684
3823
  function generateEntryBrowser() {
3685
- const clientBootstrap = join(packageDir, 'js/createViteRscApp');
3824
+ const clientBootstrap = join(packageDir, "js/createViteRscApp");
3686
3825
  // Only for an exported build, and only what the client needs to work out how
3687
3826
  // much of a page to ask for: a file server sends no headers, so without this
3688
3827
  // every navigation takes the whole document and replaces the root. Inlined
@@ -3690,9 +3829,12 @@ function generateEntryBrowser() {
3690
3829
  // a server answers this, and shipping a route table to every browser for
3691
3830
  // nothing is a page-weight cost with no benefit.
3692
3831
  const routesForClient = staticPayloads
3693
- ? routeManifest().routes.map((route) => ({ segments: route.segments, layouts: route.layouts }))
3832
+ ? routeManifest().routes.map((route) => ({
3833
+ segments: route.segments,
3834
+ layouts: route.layouts,
3835
+ }))
3694
3836
  : null;
3695
- const refreshModule = join(packageDir, 'js/navigate');
3837
+ const refreshModule = join(packageDir, "js/navigate");
3696
3838
  return `// GENERATED by rscKit() — do not edit.
3697
3839
  import { createViteRscApp } from ${JSON.stringify(clientBootstrap)}
3698
3840
  import { refresh } from ${JSON.stringify(refreshModule)}
@@ -3738,7 +3880,7 @@ if ('serviceWorker' in navigator && import.meta.env.PROD) {
3738
3880
  })
3739
3881
  }
3740
3882
  `
3741
- : ''}`;
3883
+ : ""}`;
3742
3884
  }
3743
3885
  /**
3744
3886
  * Intercepted URL patterns, published by the host before the build.
@@ -3760,10 +3902,10 @@ if ('serviceWorker' in navigator && import.meta.env.PROD) {
3760
3902
  function interceptManifest() {
3761
3903
  return routeManifest().intercepts.map((entry) => ({
3762
3904
  // The client writes [id] where a Laravel route writes {id}.
3763
- urlPattern: '/' +
3905
+ urlPattern: "/" +
3764
3906
  entry.segments
3765
- .map((seg) => (seg.type === 'static' ? seg.value : '[' + seg.value + ']'))
3766
- .join('/'),
3907
+ .map((seg) => seg.type === "static" ? seg.value : "[" + seg.value + "]")
3908
+ .join("/"),
3767
3909
  slot: entry.slot,
3768
3910
  }));
3769
3911
  }
@@ -3784,9 +3926,9 @@ function defaultExportBody(source) {
3784
3926
  const start = match.index + match[0].length - 1;
3785
3927
  for (let i = start; i < source.length; i++) {
3786
3928
  const ch = source[i];
3787
- if (ch === '{')
3929
+ if (ch === "{")
3788
3930
  depth++;
3789
- else if (ch === '}' && --depth === 0)
3931
+ else if (ch === "}" && --depth === 0)
3790
3932
  return source.slice(start, i + 1);
3791
3933
  }
3792
3934
  return null;
@@ -3809,7 +3951,7 @@ function pageBlocksOnHostCall(source) {
3809
3951
  const body = defaultExportBody(source);
3810
3952
  // Matches `await rpc(`, and the explicitly-qualified forms a typed codebase
3811
3953
  // may use: `await globalThis.rpc(` / `await (globalThis as any).rpc(`.
3812
- const qualifier = '(?:\\(\\s*globalThis[^)]*\\)\\s*\\.\\s*|globalThis\\s*\\.\\s*)?';
3954
+ const qualifier = "(?:\\(\\s*globalThis[^)]*\\)\\s*\\.\\s*|globalThis\\s*\\.\\s*)?";
3813
3955
  const awaited = new RegExp(`\\bawait\\s+${qualifier}${hostGlobal}\\s*[<(]`);
3814
3956
  return body !== null && awaited.test(body);
3815
3957
  }
@@ -3817,7 +3959,7 @@ function pageBlocksOnHostCall(source) {
3817
3959
  function hasLoadingInChain(pageDir) {
3818
3960
  let dir = pageDir;
3819
3961
  while (dir.startsWith(appDir)) {
3820
- if (findRouteFile(dir, 'loading'))
3962
+ if (findRouteFile(dir, "loading"))
3821
3963
  return true;
3822
3964
  if (dir === appDir)
3823
3965
  break;
@@ -3844,9 +3986,9 @@ function hasLoadingInChain(pageDir) {
3844
3986
  function validateErrorBoundaries() {
3845
3987
  const wrong = [];
3846
3988
  for (const c of components.values()) {
3847
- if (!c.name.endsWith('/error'))
3989
+ if (!c.name.endsWith("/error"))
3848
3990
  continue;
3849
- const source = readFileSync(c.absPath, 'utf-8');
3991
+ const source = readFileSync(c.absPath, "utf-8");
3850
3992
  if (!/^\s*['"]use client['"]/m.test(source)) {
3851
3993
  wrong.push(` ${relative(projectRoot, c.absPath)}`);
3852
3994
  }
@@ -3856,17 +3998,20 @@ function validateErrorBoundaries() {
3856
3998
  function validateLoadingBoundaries() {
3857
3999
  const errors = [];
3858
4000
  for (const c of components.values()) {
3859
- if (!c.name.endsWith('/page') && c.name !== 'app/page')
4001
+ if (!c.name.endsWith("/page") && c.name !== "app/page")
3860
4002
  continue;
3861
4003
  const pageDir = dirname(c.absPath);
3862
- const source = readFileSync(c.absPath, 'utf-8');
4004
+ const source = readFileSync(c.absPath, "utf-8");
3863
4005
  let reason = null;
3864
4006
  if (pageBlocksOnHostCall(source)) {
3865
4007
  reason = `its default export awaits ${hostGlobal}()`;
3866
4008
  }
3867
4009
  else {
3868
4010
  const configPath = routeConfig ? join(pageDir, routeConfig.file) : null;
3869
- if (routeConfig && configPath && existsSync(configPath) && routeConfig.dynamicPattern.test(readFileSync(configPath, 'utf-8'))) {
4011
+ if (routeConfig &&
4012
+ configPath &&
4013
+ existsSync(configPath) &&
4014
+ routeConfig.dynamicPattern.test(readFileSync(configPath, "utf-8"))) {
3870
4015
  reason = `${routeConfig.file} resolves props dynamically`;
3871
4016
  }
3872
4017
  }
@@ -3879,10 +4024,42 @@ function validateLoadingBoundaries() {
3879
4024
  // ── Plugin ───────────────────────────────────────────────────────────────────
3880
4025
  /** Names of plugins that transform JSX and must run after rsc() has split it. */
3881
4026
  const JSX_PLUGIN_PATTERN = /react|babel|oxc/i;
4027
+ /**
4028
+ * The server-side stand-in for a client export, made extendable.
4029
+ *
4030
+ * In the rsc environment plugin-rsc replaces every export of a "use client"
4031
+ * module with a stub that throws when called. It emits the stub as an arrow
4032
+ * function, and an arrow function has no prototype, so a server module that
4033
+ * does `class NullStore extends ReactStore` - base-ui does, with only the
4034
+ * base class marked "use client" - dies at the class declaration with
4035
+ * "The superclass is not a constructor", on a line nobody wrote, whenever a
4036
+ * server component imports the library directly. Next.js allows that import;
4037
+ * React's own client references are proxies over a real function.
4038
+ *
4039
+ * A `function` stub has a prototype, so the declaration succeeds, and the
4040
+ * stub still throws - now from `super()`, with its own message - if the
4041
+ * class is ever constructed on the server.
4042
+ */
4043
+ const CLIENT_STUB = /registerClientReference\(\s*\(\) => \{ throw new Error\("Unexpectedly client reference export '"/g;
4044
+ function extendableClientReferences() {
4045
+ return {
4046
+ name: "rsc-kit:extendable-client-references",
4047
+ enforce: "post",
4048
+ applyToEnvironment: (environment) => environment.name === "rsc",
4049
+ transform(code) {
4050
+ if (!code.includes("Unexpectedly client reference export"))
4051
+ return;
4052
+ return {
4053
+ code: code.replace(CLIENT_STUB, 'registerClientReference(function () { throw new Error("Unexpectedly client reference export \'"'),
4054
+ map: null,
4055
+ };
4056
+ },
4057
+ };
4058
+ }
3882
4059
  export function rscKit(options = {}) {
3883
4060
  resolvePaths(options);
3884
4061
  const routesPlugin = {
3885
- name: 'rsc-kit',
4062
+ name: "rsc-kit",
3886
4063
  config(_config, env) {
3887
4064
  if (!existsSync(appDir)) {
3888
4065
  throw new Error(`[rsc-kit] No app directory at ${appDir} — nothing to build.`);
@@ -3901,24 +4078,24 @@ export function rscKit(options = {}) {
3901
4078
  // editing is a state to pass through, not to be thrown out of.
3902
4079
  if (components.size === 0) {
3903
4080
  const message = `No routes under ${appDir}. A directory with a page.tsx in it is a route; ` +
3904
- 'without one there is nothing to serve.';
3905
- if (env.command === 'build')
4081
+ "without one there is nothing to serve.";
4082
+ if (env.command === "build")
3906
4083
  throw new Error(`[rsc-kit] ${message}`);
3907
4084
  log(message);
3908
4085
  }
3909
4086
  const notClient = validateErrorBoundaries();
3910
4087
  if (notClient.length) {
3911
- throw new Error('[rsc-kit] An error.tsx must be a client component.\n\n' +
3912
- notClient.join('\n') +
4088
+ throw new Error("[rsc-kit] An error.tsx must be a client component.\n\n" +
4089
+ notClient.join("\n") +
3913
4090
  "\n\nAdd 'use client' at the top. It is rendered inside an error boundary and is\n" +
3914
- 'handed a reset() callback to call, neither of which a server component can do.');
4091
+ "handed a reset() callback to call, neither of which a server component can do.");
3915
4092
  }
3916
4093
  const loadingErrors = validateLoadingBoundaries();
3917
4094
  if (loadingErrors.length) {
3918
- throw new Error('[rsc-kit] A page that blocks before it can paint needs a loading.tsx boundary.\n\n' +
3919
- loadingErrors.join('\n') +
3920
- '\n\nAdd loading.tsx in the page directory (or a parent), or move the slow work\n' +
3921
- 'into a child component wrapped in its own <Suspense> so the page can paint.');
4095
+ throw new Error("[rsc-kit] A page that blocks before it can paint needs a loading.tsx boundary.\n\n" +
4096
+ loadingErrors.join("\n") +
4097
+ "\n\nAdd loading.tsx in the page directory (or a parent), or move the slow work\n" +
4098
+ "into a child component wrapped in its own <Suspense> so the page can paint.");
3922
4099
  }
3923
4100
  // Before the entries, because the app's own source imports these and the
3924
4101
  // module graph is walked as soon as this hook returns.
@@ -3931,7 +4108,7 @@ export function rscKit(options = {}) {
3931
4108
  // host calls resolve their endpoint, from the app's own .env, so the two
3932
4109
  // cannot end up pointing at different backends. Development only: a
3933
4110
  // build's server.ts decides this for itself.
3934
- const backendEnv = loadEnv(env.mode, projectRoot, '');
4111
+ const backendEnv = loadEnv(env.mode, projectRoot, "");
3935
4112
  // Only a backend this server could actually call. The shared secret is
3936
4113
  // what makes one a backend rather than a url that happens to be in the
3937
4114
  // environment — APP_URL is not a Laravel-only name, and a JavaScript
@@ -3943,16 +4120,19 @@ export function rscKit(options = {}) {
3943
4120
  // the address down means it.
3944
4121
  const backendSecret = hostCallOptions?.secret ?? backendEnv.RSC_HOST_CALL_SECRET;
3945
4122
  const detected = backendSecret
3946
- ? (hostCallOptions?.endpoint ?? backendEnv.RSC_BACKEND ?? backendEnv.APP_URL ?? '')
3947
- : '';
3948
- const fallbackOrigin = options.devFallback === false ? '' : (options.devFallback ?? detected);
3949
- writeFileSync(join(genDir, 'entry.rsc.tsx'), generateEntryRsc(fallbackOrigin));
3950
- writeFileSync(join(genDir, 'entry.ssr.tsx'), generateEntrySsr());
3951
- writeFileSync(join(genDir, 'entry.browser.tsx'), generateEntryBrowser());
4123
+ ? (hostCallOptions?.endpoint ??
4124
+ backendEnv.RSC_BACKEND ??
4125
+ backendEnv.APP_URL ??
4126
+ "")
4127
+ : "";
4128
+ const fallbackOrigin = options.devFallback === false ? "" : (options.devFallback ?? detected);
4129
+ writeFileSync(join(genDir, "entry.rsc.tsx"), generateEntryRsc(fallbackOrigin));
4130
+ writeFileSync(join(genDir, "entry.ssr.tsx"), generateEntrySsr());
4131
+ writeFileSync(join(genDir, "entry.browser.tsx"), generateEntryBrowser());
3952
4132
  // Written beside the entries, for a host to read instead of walking the
3953
4133
  // route tree itself. Laravel scans it a second time today; a JS host
3954
4134
  // would otherwise have to write a third walk of the same directories.
3955
- writeFileSync(join(outDir, 'routes.json'), JSON.stringify(manifest, null, 2));
4135
+ writeFileSync(join(outDir, "routes.json"), JSON.stringify(manifest, null, 2));
3956
4136
  return {
3957
4137
  // Off, not merely unused: Vite warns when publicDir sits inside outDir,
3958
4138
  // and assetsDir is normally a directory under the build output. An app
@@ -3973,7 +4153,7 @@ export function rscKit(options = {}) {
3973
4153
  * answer travels with the bundle instead of with whoever starts it.
3974
4154
  */
3975
4155
  define: {
3976
- 'process.env.NODE_ENV': JSON.stringify(env.mode === 'development' ? 'development' : 'production'),
4156
+ "process.env.NODE_ENV": JSON.stringify(env.mode === "development" ? "development" : "production"),
3977
4157
  // A constant, so the boundary and its import fall out of the bundle
3978
4158
  // entirely when this is off rather than shipping a branch nobody takes.
3979
4159
  __RSC_VIEW_TRANSITIONS__: JSON.stringify(viewTransitions),
@@ -4010,7 +4190,7 @@ export function rscKit(options = {}) {
4010
4190
  // "The server is configured with a public base URL", which reads as a
4011
4191
  // routing bug rather than as this line. In dev the pages are the root;
4012
4192
  // the assets come from the same origin either way.
4013
- base: '/',
4193
+ base: "/",
4014
4194
  // Where the generated entries live, so Vite resolves them as its own
4015
4195
  // source. A build concern only.
4016
4196
  //
@@ -4030,7 +4210,7 @@ export function rscKit(options = {}) {
4030
4210
  // built bundles reference it zero times — deduping it was a no-op left
4031
4211
  // over from the hand-rolled engine.
4032
4212
  resolve: {
4033
- dedupe: ['react', 'react-dom', '@vitejs/plugin-rsc'],
4213
+ dedupe: ["react", "react-dom", "@vitejs/plugin-rsc"],
4034
4214
  // `import Link from '<packageAlias>/Link'` resolves to the client
4035
4215
  // runtime shipped here, for hosts that vendor this package outside
4036
4216
  // node_modules. Installed from npm the name resolves on its own.
@@ -4038,15 +4218,35 @@ export function rscKit(options = {}) {
4038
4218
  },
4039
4219
  build: { emptyOutDir: true },
4040
4220
  environments: {
4041
- // Server bundles — stay under the (non-public) out dir.
4042
- rsc: { build: { rollupOptions: { input: { index: join(genDir, 'entry.rsc.tsx') } } } },
4043
- ssr: { build: { rollupOptions: { input: { index: join(genDir, 'entry.ssr.tsx') } } } },
4221
+ // Server bundles — stay under the (non-public) out dir. `bun` and
4222
+ // `bun:*` are the runtime's own modules, like `node:*`: nothing to
4223
+ // bundle, and a build under Node has nothing to resolve them to.
4224
+ // Left as imports for the runtime that has them.
4225
+ rsc: {
4226
+ build: {
4227
+ rollupOptions: {
4228
+ input: { index: join(genDir, "entry.rsc.tsx") },
4229
+ external: RUNTIME_BUILTINS,
4230
+ },
4231
+ },
4232
+ },
4233
+ ssr: {
4234
+ build: {
4235
+ rollupOptions: {
4236
+ input: { index: join(genDir, "entry.ssr.tsx") },
4237
+ external: RUNTIME_BUILTINS,
4238
+ },
4239
+ },
4240
+ },
4044
4241
  // Client bundle — emitted into public/ for the web server to serve.
4045
4242
  client: {
4046
4243
  build: {
4047
4244
  outDir: publicAssetsDir,
4048
4245
  emptyOutDir: true,
4049
- rollupOptions: { input: { index: join(genDir, 'entry.browser.tsx') } },
4246
+ rollupOptions: {
4247
+ input: { index: join(genDir, "entry.browser.tsx") },
4248
+ output: { chunkFileNames: clientChunkFileName },
4249
+ },
4050
4250
  },
4051
4251
  },
4052
4252
  },
@@ -4073,22 +4273,24 @@ export function rscKit(options = {}) {
4073
4273
  // no host installed every rpc() is refused — so a page whose data comes
4074
4274
  // from the backend renders its shell and then blanks, which reads as a
4075
4275
  // hydration bug rather than a missing wire.
4076
- server.httpServer?.once('listening', async () => {
4276
+ server.httpServer?.once("listening", async () => {
4077
4277
  const env = server.environments?.rsc;
4078
4278
  if (!env?.runner)
4079
4279
  return;
4080
4280
  // The app's own .env, unprefixed. A Laravel app already has both of
4081
4281
  // these, which is what makes this need no configuring: APP_URL is the
4082
4282
  // backend and RSC_HOST_CALL_SECRET is the secret it checks.
4083
- const fromEnv = loadEnv(server.config.mode, projectRoot, '');
4283
+ const fromEnv = loadEnv(server.config.mode, projectRoot, "");
4084
4284
  const secret = hostCallOptions?.secret ?? fromEnv.RSC_HOST_CALL_SECRET;
4085
4285
  const origin = hostCallOptions?.endpoint ?? fromEnv.RSC_BACKEND ?? fromEnv.APP_URL;
4086
4286
  if (!secret || !origin)
4087
4287
  return;
4088
- const path = hostCallOptions?.path ?? fromEnv.RSC_HOST_CALL_PATH ?? '/__rsc/host-call';
4089
- const endpoint = origin.replace(/\/$/, '') + path;
4288
+ const path = hostCallOptions?.path ??
4289
+ fromEnv.RSC_HOST_CALL_PATH ??
4290
+ "/__rsc/host-call";
4291
+ const endpoint = origin.replace(/\/$/, "") + path;
4090
4292
  try {
4091
- const entry = await env.runner.import(join(genDir, 'entry.rsc.tsx'));
4293
+ const entry = await env.runner.import(join(genDir, "entry.rsc.tsx"));
4092
4294
  const install = entry.installHostFn;
4093
4295
  install?.(httpHostCalls({ endpoint, secret }));
4094
4296
  }
@@ -4111,7 +4313,7 @@ export function rscKit(options = {}) {
4111
4313
  }
4112
4314
  catch { }
4113
4315
  };
4114
- server.httpServer?.once('listening', () => {
4316
+ server.httpServer?.once("listening", () => {
4115
4317
  // The url Vite resolved, not one built from the port. A dev server
4116
4318
  // whose port is already taken on IPv4 binds IPv6 only and keeps the
4117
4319
  // number — so http://127.0.0.1:<port> is a reachable-looking address
@@ -4120,25 +4322,25 @@ export function rscKit(options = {}) {
4120
4322
  const resolved = server.resolvedUrls?.local?.[0];
4121
4323
  const address = server.httpServer?.address();
4122
4324
  const url = resolved ??
4123
- (typeof address === 'object' && address
4124
- ? `http://${address.family === 'IPv6' ? `[${address.address}]` : address.address}:${address.port}`
4325
+ (typeof address === "object" && address
4326
+ ? `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`
4125
4327
  : null);
4126
4328
  if (!url)
4127
4329
  return;
4128
4330
  mkdirSync(dirname(hotFile), { recursive: true });
4129
- writeFileSync(hotFile, url.replace(/\/$/, ''));
4331
+ writeFileSync(hotFile, url.replace(/\/$/, ""));
4130
4332
  });
4131
- for (const signal of ['SIGINT', 'SIGTERM', 'exit']) {
4333
+ for (const signal of ["SIGINT", "SIGTERM", "exit"]) {
4132
4334
  process.once(signal, remove);
4133
4335
  }
4134
- server.httpServer?.once('close', remove);
4336
+ server.httpServer?.once("close", remove);
4135
4337
  }
4136
4338
  const shapes = new Set(ROUTE_FILES.map((name) => name));
4137
4339
  const affectsRouting = (file) => {
4138
4340
  if (!file.startsWith(sourceDir))
4139
4341
  return false;
4140
- const base = file.split('/').pop() ?? '';
4141
- const stem = base.replace(/\.(tsx|jsx|ts|js)$/, '');
4342
+ const base = file.split("/").pop() ?? "";
4343
+ const stem = base.replace(/\.(tsx|jsx|ts|js)$/, "");
4142
4344
  // The host's route-config file, whatever it named it. Hardcoding one
4143
4345
  // here would put a backend's convention back into a plugin that is
4144
4346
  // published without any — generic-host.test.ts fails if it reappears.
@@ -4155,8 +4357,8 @@ export function rscKit(options = {}) {
4155
4357
  // Watched explicitly: the Vite root is the *out* directory, so the app's
4156
4358
  // source tree is outside it and nothing would report a file appearing.
4157
4359
  server.watcher.add(sourceDir);
4158
- server.watcher.on('add', restart);
4159
- server.watcher.on('unlink', restart);
4360
+ server.watcher.on("add", restart);
4361
+ server.watcher.on("unlink", restart);
4160
4362
  },
4161
4363
  /**
4162
4364
  * Freeze what can be frozen, once every bundle exists.
@@ -4192,7 +4394,8 @@ export function rscKit(options = {}) {
4192
4394
  // differ — <outDir>/dist/rsc on its own, node_modules/.nitro/… under
4193
4395
  // Nitro — and hard-coding the first is what silently skipped the second.
4194
4396
  // Both environments emit `index.js`, so this is one path, not a branch.
4195
- const rscOut = builder?.environments?.rsc?.config?.build?.outDir ?? join(outDir, 'dist/rsc');
4397
+ const rscOut = builder?.environments?.rsc?.config?.build?.outDir ??
4398
+ join(outDir, "dist/rsc");
4196
4399
  const bundle = resolveRscBundle(rscOut);
4197
4400
  // Where the frozen pages go, which is not the same question.
4198
4401
  //
@@ -4204,7 +4407,7 @@ export function rscKit(options = {}) {
4204
4407
  // they are in place by the time it does.
4205
4408
  const clientOut = builder?.environments?.client?.config?.build?.outDir;
4206
4409
  const staticDir = clientOut
4207
- ? join(dirname(clientOut), 'server', NITRO_STATIC_DIR)
4410
+ ? join(dirname(clientOut), "server", NITRO_STATIC_DIR)
4208
4411
  : join(outDir, NITRO_STATIC_DIR);
4209
4412
  const { frozen, results } = await prerenderAfterBundles(bundle, staticDir, clientOut ?? publicAssetsDir, builder ? knownActionsOf(builder.config, projectRoot) : []);
4210
4413
  // The same pages once more, as a module. A Worker has no filesystem, and
@@ -4214,7 +4417,7 @@ export function rscKit(options = {}) {
4214
4417
  // when the directory is not there. Only under Nitro, whose presets are
4215
4418
  // the ones without a disk; on its own the plugin serves from outDir.
4216
4419
  if (clientOut && existsSync(staticDir)) {
4217
- const { inlineModuleName, inlineModuleSource } = await import('./files.js');
4420
+ const { inlineModuleName, inlineModuleSource } = await import("./files.js");
4218
4421
  writeFileSync(join(dirname(staticDir), inlineModuleName(NITRO_STATIC_DIR)), await inlineModuleSource(staticDir));
4219
4422
  }
4220
4423
  // Manifest first. The service worker precaches whatever it finds in this
@@ -4230,11 +4433,12 @@ export function rscKit(options = {}) {
4230
4433
  },
4231
4434
  configResolved(config) {
4232
4435
  isWatch = config.build?.watch != null;
4233
- const nitroMain = config.plugins.find((p) => p.name === 'nitro:main');
4436
+ resolvedConfig = config;
4437
+ const nitroMain = config.plugins.find((p) => p.name === "nitro:main");
4234
4438
  if (nitroMain?.hotUpdate) {
4235
4439
  const original = nitroMain.hotUpdate;
4236
4440
  nitroMain.hotUpdate = function (ctx) {
4237
- if (this.environment?.name === 'rsc')
4441
+ if (this.environment?.name === "rsc")
4238
4442
  return;
4239
4443
  return original.call(this, ctx);
4240
4444
  };
@@ -4243,12 +4447,12 @@ export function rscKit(options = {}) {
4243
4447
  // placed ahead of it sees the wrong graph and fails in ways that are hard
4244
4448
  // to trace back here. Cheaper to refuse than to let it through.
4245
4449
  const names = config.plugins.map((p) => p.name);
4246
- const rscAt = names.findIndex((n) => n === 'rsc' || n.startsWith('rsc:'));
4450
+ const rscAt = names.findIndex((n) => n === "rsc" || n.startsWith("rsc:"));
4247
4451
  const jsxAt = names.findIndex((n) => JSX_PLUGIN_PATTERN.test(n));
4248
4452
  if (rscAt !== -1 && jsxAt !== -1 && jsxAt < rscAt) {
4249
4453
  throw new Error(`[rsc-kit] Plugin "${names[jsxAt]}" is resolved ahead of rsc(), so it would ` +
4250
- 'transform JSX before the client/server split.\n' +
4251
- 'Put rscKit() first in your plugins array. If it already is, that plugin ' +
4454
+ "transform JSX before the client/server split.\n" +
4455
+ "Put rscKit() first in your plugins array. If it already is, that plugin " +
4252
4456
  "sets enforce: 'pre' and needs to be moved after rsc() explicitly.");
4253
4457
  }
4254
4458
  },
@@ -4277,6 +4481,7 @@ export function rscKit(options = {}) {
4277
4481
  clientChunks: (meta) => meta.normalizedId,
4278
4482
  ...actionEncryptionKey(),
4279
4483
  }),
4484
+ extendableClientReferences(),
4280
4485
  routesPlugin,
4281
4486
  ];
4282
4487
  }
@@ -4325,14 +4530,14 @@ function actionEncryptionKey() {
4325
4530
  // the key is read, so what ships is the lookup rather than the secret. A
4326
4531
  // literal here would put the key in the bundle, which is the thing being
4327
4532
  // avoided.
4328
- return { defineEncryptionKey: 'process.env.RSC_ACTION_ENCRYPTION_KEY' };
4533
+ return { defineEncryptionKey: "process.env.RSC_ACTION_ENCRYPTION_KEY" };
4329
4534
  }
4330
4535
  async function appPluginRsc(options = {}) {
4331
4536
  try {
4332
4537
  // Resolved against a file *in* the root, since a directory specifier
4333
4538
  // resolves relative to its parent.
4334
- const fromApp = createRequire(join(projectRoot, 'package.json'));
4335
- const entry = fromApp.resolve('@vitejs/plugin-rsc');
4539
+ const fromApp = createRequire(join(projectRoot, "package.json"));
4540
+ const entry = fromApp.resolve("@vitejs/plugin-rsc");
4336
4541
  const mod = (await import(pathToFileURL(entry).href));
4337
4542
  return (mod.default ?? rsc)(options);
4338
4543
  }