@rsc-kit/core 0.21.4 → 0.22.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.
package/dist/vite.d.ts CHANGED
@@ -388,6 +388,16 @@ export declare function toAlias(name: string): string;
388
388
  * was controlling so they load uncontrolled. Nothing else runs in development.
389
389
  */
390
390
  export declare const DEV_WORKER = "self.addEventListener('install', () => self.skipWaiting())\nself.addEventListener('activate', (event) => {\n event.waitUntil((async () => {\n const keys = await caches.keys()\n await Promise.all(keys.filter((k) => k.startsWith('rsc-kit-')).map((k) => caches.delete(k)))\n await self.clients.claim()\n const pages = await self.clients.matchAll({ type: 'window' })\n await self.registration.unregister()\n for (const page of pages) page.navigate(page.url).catch(() => {})\n })())\n})\n";
391
+ /**
392
+ * A read of import.meta.env.PUBLIC_*, refused by name.
393
+ *
394
+ * PUBLIC_ was a client prefix beside VITE_ until 0.22. Dropped silently, a
395
+ * read of one would compile to undefined - in the browser, for a pixel id or
396
+ * an api url, with nothing said anywhere - so the build names each file that
397
+ * still reads one and the variable to rename, and stops. Server code reading
398
+ * process.env.PUBLIC_* is untouched: that is an ordinary server variable.
399
+ */
400
+ export declare function publicPrefixReads(code: string): string[];
391
401
  export declare function serverRendererInRsc(): Plugin;
392
402
  /**
393
403
  * The project's typecheck, as part of the build.
@@ -419,4 +429,19 @@ export type TypecheckOutcome = {
419
429
  * is the message, because that is what the developer would have read.
420
430
  */
421
431
  export declare function typecheckProject(root: string): TypecheckOutcome;
432
+ /**
433
+ * The PUBLIC_ variables an app still sets, and where each one is set.
434
+ *
435
+ * PUBLIC_ was a client prefix beside VITE_ until 0.22. An app that sets one
436
+ * expects it in the browser, and most read it through a validated env object
437
+ * rather than import.meta.env, so no source scan sees the read: in the
438
+ * browser the value was simply gone, and a required one threw from the
439
+ * validator on the first page that imported it. The variables themselves are
440
+ * the signal. Same files, same order and same directories as nodeEnvSetIn;
441
+ * a name only in the process environment is reported as such.
442
+ */
443
+ export declare function publicVariablesSet(mode: string, envDir?: string | false, environment?: Record<string, string | undefined>): {
444
+ name: string;
445
+ where: string;
446
+ }[];
422
447
  export declare function rscKit(options?: RscKitOptions): PluginOption[];
package/dist/vite.js CHANGED
@@ -5510,6 +5510,35 @@ self.addEventListener('activate', (event) => {
5510
5510
  })())
5511
5511
  })
5512
5512
  `;
5513
+ /**
5514
+ * A read of import.meta.env.PUBLIC_*, refused by name.
5515
+ *
5516
+ * PUBLIC_ was a client prefix beside VITE_ until 0.22. Dropped silently, a
5517
+ * read of one would compile to undefined - in the browser, for a pixel id or
5518
+ * an api url, with nothing said anywhere - so the build names each file that
5519
+ * still reads one and the variable to rename, and stops. Server code reading
5520
+ * process.env.PUBLIC_* is untouched: that is an ordinary server variable.
5521
+ */
5522
+ export function publicPrefixReads(code) {
5523
+ return [...new Set([...code.matchAll(/\bimport\.meta\.env\.(PUBLIC_[A-Za-z0-9_]+)/g)].map((m) => m[1]))];
5524
+ }
5525
+ function publicPrefixRefused() {
5526
+ return {
5527
+ name: "rsc-kit:public-prefix",
5528
+ enforce: "pre",
5529
+ transform(code, id) {
5530
+ if (id.includes("/node_modules/") || !code.includes("import.meta.env.PUBLIC_"))
5531
+ return;
5532
+ const names = publicPrefixReads(code);
5533
+ if (names.length === 0)
5534
+ return;
5535
+ this.error(`${id.split("?")[0]} reads ${names.map((n) => `import.meta.env.${n}`).join(", ")}. ` +
5536
+ "Only VITE_ variables reach the browser now; rename " +
5537
+ names.map((n) => `${n} to VITE_${n.slice("PUBLIC_".length)}`).join(", ") +
5538
+ " in the code and in your .env files.");
5539
+ },
5540
+ };
5541
+ }
5513
5542
  /**
5514
5543
  * Named imports from a barrel package, unrolled on the server environments.
5515
5544
  *
@@ -5881,6 +5910,45 @@ function nodeEnvSetIn(mode, envDir) {
5881
5910
  }
5882
5911
  return culprit;
5883
5912
  }
5913
+ /**
5914
+ * The PUBLIC_ variables an app still sets, and where each one is set.
5915
+ *
5916
+ * PUBLIC_ was a client prefix beside VITE_ until 0.22. An app that sets one
5917
+ * expects it in the browser, and most read it through a validated env object
5918
+ * rather than import.meta.env, so no source scan sees the read: in the
5919
+ * browser the value was simply gone, and a required one threw from the
5920
+ * validator on the first page that imported it. The variables themselves are
5921
+ * the signal. Same files, same order and same directories as nodeEnvSetIn;
5922
+ * a name only in the process environment is reported as such.
5923
+ */
5924
+ export function publicVariablesSet(mode, envDir, environment = process.env) {
5925
+ const files = [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`];
5926
+ const found = new Map();
5927
+ const dirs = [...new Set([projectRoot, envDir || projectRoot])].filter((dir) => !!dir);
5928
+ for (const dir of dirs)
5929
+ for (const file of files) {
5930
+ const path = join(dir, file);
5931
+ if (!existsSync(path))
5932
+ continue;
5933
+ readFileSync(path, "utf-8").split("\n").forEach((line, at) => {
5934
+ const name = /^\s*(?:export\s+)?(PUBLIC_[A-Za-z0-9_]+)\s*=/.exec(line)?.[1];
5935
+ if (name)
5936
+ found.set(name, `${path}:${at + 1}`);
5937
+ });
5938
+ }
5939
+ for (const name of Object.keys(environment)) {
5940
+ if (name.startsWith("PUBLIC_") && !found.has(name))
5941
+ found.set(name, "the environment");
5942
+ }
5943
+ return [...found].map(([name, where]) => ({ name, where }));
5944
+ }
5945
+ function refusePublicVariables(set) {
5946
+ return ("[rsc-kit] Only VITE_ variables reach the browser; PUBLIC_ stopped being a client prefix in 0.22. " +
5947
+ "These are still set, and would be undefined in the browser:\n\n" +
5948
+ set.map(({ name, where }) => ` ${name} (${where}) -> VITE_${name.slice("PUBLIC_".length)}`).join("\n") +
5949
+ "\n\nRename each where it is set and where it is read - an env schema's clientPrefix and client " +
5950
+ "keys included.");
5951
+ }
5884
5952
  function refuseDevelopmentBuild(config) {
5885
5953
  const culprit = nodeEnvSetIn(config.mode, config.envDir);
5886
5954
  return ("[rsc-kit] vite build is running as a development build, and the output cannot work: " +
@@ -6225,11 +6293,11 @@ export function rscKit(options = {}) {
6225
6293
  // saying so at the end of each docker build is not a finding.
6226
6294
  rollupOptions: { checks: { pluginTimings: false } },
6227
6295
  },
6228
- // What reaches the browser. Vite's own VITE_ prefix, and PUBLIC_ - the
6229
- // scaffold's spelling, and Next's minus its brand - so a variable
6230
- // named for a port reads through import.meta.env without a config
6231
- // line. A config that set its own prefixes keeps them; Vite merges.
6232
- envPrefix: ["VITE_", "PUBLIC_"],
6296
+ // What reaches the browser: Vite's own VITE_ prefix, and only that.
6297
+ // PUBLIC_ was accepted beside it until 0.22; a read of one is now
6298
+ // refused by name (publicPrefixRefused) rather than left undefined.
6299
+ // A config that set its own prefixes keeps them; Vite merges.
6300
+ envPrefix: ["VITE_"],
6233
6301
  environments: {
6234
6302
  // Server bundles — stay under the (non-public) out dir. `bun` and
6235
6303
  // `bun:*` are the runtime's own modules, like `node:*`: nothing to
@@ -6327,159 +6395,167 @@ export function rscKit(options = {}) {
6327
6395
  * and restarting on every keystroke would throw away the module graph for
6328
6396
  * nothing.
6329
6397
  */
6330
- configureServer(server) {
6331
- // The icons and share images live in app/, which nothing serves; the
6332
- // build copies them beside the client output. There is no output while
6333
- // developing, so the same hrefs the head tags carry are answered from
6334
- // app/ here - and the web manifest with them - or the tab has no icon
6335
- // and the console a 404 for a file that is right there.
6336
- server.middlewares.use((req, res, next) => {
6337
- const url = (req.url ?? "").split("?")[0];
6338
- // A worker registered by a production run on this origin - `bun run
6339
- // start` on the port the dev server uses next - outlives that run and
6340
- // answers the dev server's documents from its cache: a stored page is
6341
- // cache-first, so a refresh brings back yesterday's document with
6342
- // yesterday's module hashes, and a 504 for each. The browser fetches
6343
- // /sw.js again on every navigation to look for an update; in
6344
- // development that fetch gets a worker whose only job is to remove
6345
- // itself, its caches, and reload the pages it controlled.
6346
- // robots.txt, a hand-written sitemap.xml, humans.txt: beside the root
6347
- // layout, served at the root. The build copies them beside the client
6348
- // output; here they are read from app/.
6349
- if (url.startsWith("/") && rootFiles.includes(url.slice(1))) {
6350
- res.setHeader("Content-Type", rootFileType(url.slice(1)));
6351
- res.end(readFileSync(join(sourceDir, "app", url.slice(1))));
6352
- return;
6353
- }
6354
- if (url === "/sw.js") {
6355
- res.setHeader("Content-Type", "text/javascript");
6356
- res.setHeader("Cache-Control", "no-store");
6357
- res.end(DEV_WORKER);
6358
- return;
6359
- }
6360
- if (webManifestOptions && url === MANIFEST_PATH) {
6361
- res.setHeader("Content-Type", "application/manifest+json");
6362
- res.end(webManifest(webManifestOptions));
6363
- return;
6364
- }
6365
- const asset = allAppAssets(foundAssets).find((a) => a.href === url);
6366
- const file = asset ? join(sourceDir, "app", asset.file) : null;
6367
- if (!asset || !file || !existsSync(file))
6368
- return next();
6369
- res.setHeader("Content-Type", typeOf(asset.file));
6370
- createReadStream(file).pipe(res);
6371
- });
6372
- // rpc() has to reach the backend while the dev server is serving.
6373
- //
6374
- // A built deployment installs this itself: the server running
6375
- // createRscHandler passes `hostCalls`. Nothing does that here, and with
6376
- // no host installed every rpc() is refused — so a page whose data comes
6377
- // from the backend renders its shell and then blanks, which reads as a
6378
- // hydration bug rather than a missing wire.
6379
- server.httpServer?.once("listening", async () => {
6380
- const env = server.environments?.rsc;
6381
- if (!env?.runner)
6382
- return;
6383
- // The app's own .env, unprefixed. A Laravel app already has both of
6384
- // these, which is what makes this need no configuring: APP_URL is the
6385
- // backend and RSC_HOST_CALL_SECRET is the secret it checks.
6386
- const fromEnv = loadEnv(server.config.mode, projectRoot, "");
6387
- const secret = hostCallOptions?.secret ?? fromEnv.RSC_HOST_CALL_SECRET;
6388
- const origin = hostCallOptions?.endpoint ?? fromEnv.RSC_BACKEND ?? fromEnv.APP_URL;
6389
- if (!secret || !origin)
6390
- return;
6391
- const path = hostCallOptions?.path ??
6392
- fromEnv.RSC_HOST_CALL_PATH ??
6393
- "/__rsc/host-call";
6394
- const endpoint = origin.replace(/\/$/, "") + path;
6395
- try {
6396
- const entry = await env.runner.import(join(genDir, "entry.rsc.tsx"));
6397
- const install = entry.installHostFn;
6398
- install?.(httpHostCalls({ endpoint, secret }));
6399
- }
6400
- catch (error) {
6401
- // Reported rather than thrown: the dev server is still useful for
6402
- // every page that needs no data, and a failure here would otherwise
6403
- // look like the server refusing to start.
6404
- server.config.logger.warn(`[rsc-kit] could not wire host calls to ${endpoint}: ` +
6405
- (error instanceof Error ? error.message : String(error)));
6406
- }
6407
- });
6408
- // Written once the server is listening, because only then is the port
6409
- // known. Removed on shutdown so a backend can tell a dev server that is
6410
- // gone from one that is merely slow to answer.
6411
- if (hotFile) {
6412
- const remove = () => {
6413
- try {
6414
- if (existsSync(hotFile))
6415
- rmSync(hotFile);
6398
+ // Ahead of every other plugin's configureServer, so this middleware is
6399
+ // first in the stack. Nitro's dev handler is registered by its own plugin,
6400
+ // which an app lists before this one, and it answers every request that
6401
+ // does not look like an asset itself - /manifest.webmanifest and
6402
+ // /robots.txt went to the app's router and came back 404, or 500 from a
6403
+ // page that could not render for that url, and this never saw them.
6404
+ // Swapping the plugins in the app's config is not a fix: Nitro then loads
6405
+ // .env too late for the app's first request.
6406
+ configureServer: { order: "pre", handler(server) {
6407
+ // The icons and share images live in app/, which nothing serves; the
6408
+ // build copies them beside the client output. There is no output while
6409
+ // developing, so the same hrefs the head tags carry are answered from
6410
+ // app/ here - and the web manifest with them - or the tab has no icon
6411
+ // and the console a 404 for a file that is right there.
6412
+ server.middlewares.use((req, res, next) => {
6413
+ const url = (req.url ?? "").split("?")[0];
6414
+ // A worker registered by a production run on this origin - `bun run
6415
+ // start` on the port the dev server uses next - outlives that run and
6416
+ // answers the dev server's documents from its cache: a stored page is
6417
+ // cache-first, so a refresh brings back yesterday's document with
6418
+ // yesterday's module hashes, and a 504 for each. The browser fetches
6419
+ // /sw.js again on every navigation to look for an update; in
6420
+ // development that fetch gets a worker whose only job is to remove
6421
+ // itself, its caches, and reload the pages it controlled.
6422
+ // robots.txt, a hand-written sitemap.xml, humans.txt: beside the root
6423
+ // layout, served at the root. The build copies them beside the client
6424
+ // output; here they are read from app/.
6425
+ if (url.startsWith("/") && rootFiles.includes(url.slice(1))) {
6426
+ res.setHeader("Content-Type", rootFileType(url.slice(1)));
6427
+ res.end(readFileSync(join(sourceDir, "app", url.slice(1))));
6428
+ return;
6416
6429
  }
6417
- catch { }
6418
- };
6419
- server.httpServer?.once("listening", () => {
6420
- // The url Vite resolved, not one built from the port. A dev server
6421
- // whose port is already taken on IPv4 binds IPv6 only and keeps the
6422
- // number — so http://127.0.0.1:<port> is a reachable-looking address
6423
- // that nothing answers, and the backend reports the renderer as down
6424
- // while it is plainly running.
6425
- const resolved = server.resolvedUrls?.local?.[0];
6426
- const address = server.httpServer?.address();
6427
- const url = resolved ??
6428
- (typeof address === "object" && address
6429
- ? `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`
6430
- : null);
6431
- if (!url)
6430
+ if (url === "/sw.js") {
6431
+ res.setHeader("Content-Type", "text/javascript");
6432
+ res.setHeader("Cache-Control", "no-store");
6433
+ res.end(DEV_WORKER);
6434
+ return;
6435
+ }
6436
+ if (webManifestOptions && url === MANIFEST_PATH) {
6437
+ res.setHeader("Content-Type", "application/manifest+json");
6438
+ res.end(webManifest(webManifestOptions));
6432
6439
  return;
6433
- mkdirSync(dirname(hotFile), { recursive: true });
6434
- writeFileSync(hotFile, url.replace(/\/$/, ""));
6440
+ }
6441
+ const asset = allAppAssets(foundAssets).find((a) => a.href === url);
6442
+ const file = asset ? join(sourceDir, "app", asset.file) : null;
6443
+ if (!asset || !file || !existsSync(file))
6444
+ return next();
6445
+ res.setHeader("Content-Type", typeOf(asset.file));
6446
+ createReadStream(file).pipe(res);
6435
6447
  });
6436
- for (const signal of ["SIGINT", "SIGTERM", "exit"]) {
6437
- process.once(signal, remove);
6448
+ // rpc() has to reach the backend while the dev server is serving.
6449
+ //
6450
+ // A built deployment installs this itself: the server running
6451
+ // createRscHandler passes `hostCalls`. Nothing does that here, and with
6452
+ // no host installed every rpc() is refused — so a page whose data comes
6453
+ // from the backend renders its shell and then blanks, which reads as a
6454
+ // hydration bug rather than a missing wire.
6455
+ server.httpServer?.once("listening", async () => {
6456
+ const env = server.environments?.rsc;
6457
+ if (!env?.runner)
6458
+ return;
6459
+ // The app's own .env, unprefixed. A Laravel app already has both of
6460
+ // these, which is what makes this need no configuring: APP_URL is the
6461
+ // backend and RSC_HOST_CALL_SECRET is the secret it checks.
6462
+ const fromEnv = loadEnv(server.config.mode, projectRoot, "");
6463
+ const secret = hostCallOptions?.secret ?? fromEnv.RSC_HOST_CALL_SECRET;
6464
+ const origin = hostCallOptions?.endpoint ?? fromEnv.RSC_BACKEND ?? fromEnv.APP_URL;
6465
+ if (!secret || !origin)
6466
+ return;
6467
+ const path = hostCallOptions?.path ??
6468
+ fromEnv.RSC_HOST_CALL_PATH ??
6469
+ "/__rsc/host-call";
6470
+ const endpoint = origin.replace(/\/$/, "") + path;
6471
+ try {
6472
+ const entry = await env.runner.import(join(genDir, "entry.rsc.tsx"));
6473
+ const install = entry.installHostFn;
6474
+ install?.(httpHostCalls({ endpoint, secret }));
6475
+ }
6476
+ catch (error) {
6477
+ // Reported rather than thrown: the dev server is still useful for
6478
+ // every page that needs no data, and a failure here would otherwise
6479
+ // look like the server refusing to start.
6480
+ server.config.logger.warn(`[rsc-kit] could not wire host calls to ${endpoint}: ` +
6481
+ (error instanceof Error ? error.message : String(error)));
6482
+ }
6483
+ });
6484
+ // Written once the server is listening, because only then is the port
6485
+ // known. Removed on shutdown so a backend can tell a dev server that is
6486
+ // gone from one that is merely slow to answer.
6487
+ if (hotFile) {
6488
+ const remove = () => {
6489
+ try {
6490
+ if (existsSync(hotFile))
6491
+ rmSync(hotFile);
6492
+ }
6493
+ catch { }
6494
+ };
6495
+ server.httpServer?.once("listening", () => {
6496
+ // The url Vite resolved, not one built from the port. A dev server
6497
+ // whose port is already taken on IPv4 binds IPv6 only and keeps the
6498
+ // number — so http://127.0.0.1:<port> is a reachable-looking address
6499
+ // that nothing answers, and the backend reports the renderer as down
6500
+ // while it is plainly running.
6501
+ const resolved = server.resolvedUrls?.local?.[0];
6502
+ const address = server.httpServer?.address();
6503
+ const url = resolved ??
6504
+ (typeof address === "object" && address
6505
+ ? `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`
6506
+ : null);
6507
+ if (!url)
6508
+ return;
6509
+ mkdirSync(dirname(hotFile), { recursive: true });
6510
+ writeFileSync(hotFile, url.replace(/\/$/, ""));
6511
+ });
6512
+ for (const signal of ["SIGINT", "SIGTERM", "exit"]) {
6513
+ process.once(signal, remove);
6514
+ }
6515
+ server.httpServer?.once("close", remove);
6438
6516
  }
6439
- server.httpServer?.once("close", remove);
6440
- }
6441
- const shapes = new Set(ROUTE_FILES.map((name) => name));
6442
- const affectsRouting = (file) => {
6443
- if (!file.startsWith(sourceDir))
6444
- return false;
6445
- const base = file.split("/").pop() ?? "";
6446
- const stem = base.replace(/\.(tsx|jsx|ts|js)$/, "");
6447
- // The host's route-config file, whatever it named it. Hardcoding one
6448
- // here would put a backend's convention back into a plugin that is
6449
- // published without any — generic-host.test.ts fails if it reappears.
6450
- return ((base !== stem && shapes.has(stem)) ||
6451
- SECTION_FILE.test(base) ||
6452
- (routeConfig !== null && base === routeConfig.file));
6453
- };
6454
- const restart = (file) => {
6455
- if (!affectsRouting(file))
6456
- return;
6457
- server.config.logger.info(`[rsc-kit] route tree changed (${file.slice(sourceDir.length + 1)}) — restarting`);
6458
- void server.restart();
6459
- };
6460
- // Watched explicitly: the Vite root is the *out* directory, so the app's
6461
- // source tree is outside it and nothing would report a file appearing.
6462
- server.watcher.add(sourceDir);
6463
- server.watcher.on("add", restart);
6464
- server.watcher.on("unlink", restart);
6465
- // The host actions are read once, in config(), from the file the
6466
- // backend writes; the "use server" stubs are generated from it. A
6467
- // backend that writes a new action - `make:rsc-action` under a running
6468
- // dev server - rewrites the file, and until the server started again
6469
- // the stub was not there to import. Any event: the file appears on a
6470
- // first action, changes on the next, and goes when the last is removed.
6471
- const hostActionsPath = join(projectRoot, HOST_ACTIONS_FILE);
6472
- const hostActionsChanged = (file) => {
6473
- if (file !== hostActionsPath)
6474
- return;
6475
- server.config.logger.info(`[rsc-kit] ${HOST_ACTIONS_FILE} changed — restarting`);
6476
- void server.restart();
6477
- };
6478
- server.watcher.add(hostActionsPath);
6479
- server.watcher.on("add", hostActionsChanged);
6480
- server.watcher.on("change", hostActionsChanged);
6481
- server.watcher.on("unlink", hostActionsChanged);
6482
- },
6517
+ const shapes = new Set(ROUTE_FILES.map((name) => name));
6518
+ const affectsRouting = (file) => {
6519
+ if (!file.startsWith(sourceDir))
6520
+ return false;
6521
+ const base = file.split("/").pop() ?? "";
6522
+ const stem = base.replace(/\.(tsx|jsx|ts|js)$/, "");
6523
+ // The host's route-config file, whatever it named it. Hardcoding one
6524
+ // here would put a backend's convention back into a plugin that is
6525
+ // published without any — generic-host.test.ts fails if it reappears.
6526
+ return ((base !== stem && shapes.has(stem)) ||
6527
+ SECTION_FILE.test(base) ||
6528
+ (routeConfig !== null && base === routeConfig.file));
6529
+ };
6530
+ const restart = (file) => {
6531
+ if (!affectsRouting(file))
6532
+ return;
6533
+ server.config.logger.info(`[rsc-kit] route tree changed (${file.slice(sourceDir.length + 1)}) — restarting`);
6534
+ void server.restart();
6535
+ };
6536
+ // Watched explicitly: the Vite root is the *out* directory, so the app's
6537
+ // source tree is outside it and nothing would report a file appearing.
6538
+ server.watcher.add(sourceDir);
6539
+ server.watcher.on("add", restart);
6540
+ server.watcher.on("unlink", restart);
6541
+ // The host actions are read once, in config(), from the file the
6542
+ // backend writes; the "use server" stubs are generated from it. A
6543
+ // backend that writes a new action - `make:rsc-action` under a running
6544
+ // dev server - rewrites the file, and until the server started again
6545
+ // the stub was not there to import. Any event: the file appears on a
6546
+ // first action, changes on the next, and goes when the last is removed.
6547
+ const hostActionsPath = join(projectRoot, HOST_ACTIONS_FILE);
6548
+ const hostActionsChanged = (file) => {
6549
+ if (file !== hostActionsPath)
6550
+ return;
6551
+ server.config.logger.info(`[rsc-kit] ${HOST_ACTIONS_FILE} changed — restarting`);
6552
+ void server.restart();
6553
+ };
6554
+ server.watcher.add(hostActionsPath);
6555
+ server.watcher.on("add", hostActionsChanged);
6556
+ server.watcher.on("change", hostActionsChanged);
6557
+ server.watcher.on("unlink", hostActionsChanged);
6558
+ } },
6483
6559
  /**
6484
6560
  * Freeze what can be frozen, once every bundle exists.
6485
6561
  *
@@ -6578,6 +6654,12 @@ export function rscKit(options = {}) {
6578
6654
  if (config.command === "build" && !config.isProduction) {
6579
6655
  throw new Error(refuseDevelopmentBuild(config));
6580
6656
  }
6657
+ // Named before anything is bundled, in development and in a build: a
6658
+ // PUBLIC_ variable is one the app expects in the browser, and it would
6659
+ // arrive there as undefined. See publicVariablesSet.
6660
+ const publicSet = publicVariablesSet(config.mode, config.envDir);
6661
+ if (publicSet.length > 0)
6662
+ throw new Error(refusePublicVariables(publicSet));
6581
6663
  isWatch = config.build?.watch != null;
6582
6664
  resolvedConfig = config;
6583
6665
  const nitroMain = config.plugins.find((p) => p.name === "nitro:main");
@@ -6627,6 +6709,7 @@ export function rscKit(options = {}) {
6627
6709
  clientChunks: (meta) => meta.normalizedId,
6628
6710
  ...actionEncryptionKey(),
6629
6711
  }),
6712
+ publicPrefixRefused(),
6630
6713
  barrelImportsPlugin(),
6631
6714
  useSsrModules(),
6632
6715
  serverRendererInRsc(),