@vitejs/plugin-rsc 0.4.30 → 0.4.31

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/README.md CHANGED
@@ -353,6 +353,17 @@ export default defineConfig({
353
353
  // this behavior can be customized by `serverHandler` option.
354
354
  serverHandler: false,
355
355
 
356
+ // the plugin provides build-time validation of 'server-only' and 'client-only' imports.
357
+ // this is enabled by default. See the "server-only and client-only import" section below for details.
358
+ validateImports: true,
359
+
360
+ // by default, the plugin uses a build-time generated encryption key for
361
+ // "use server" closure argument binding.
362
+ // This can be overwritten by configuring `defineEncryptionKey` option,
363
+ // for example, to obtain a key through environment variable during runtime.
364
+ // cf. https://nextjs.org/docs/app/guides/data-security#overwriting-encryption-keys-advanced
365
+ defineEncryptionKey: 'process.env.MY_ENCRYPTION_KEY',
366
+
356
367
  // when `loadModuleDevProxy: true`, `import.meta.viteRsc.loadModule` is implemented
357
368
  // through `fetch` based RPC, which allows, for example, rsc environment inside
358
369
  // cloudflare workers to communicate with node ssr environment on main Vite process.
@@ -362,13 +373,6 @@ export default defineConfig({
362
373
  // if it breaks, it can be opt-out or selectively applied based on files.
363
374
  rscCssTransform: { filter: (id) => id.includes('/my-app/') },
364
375
 
365
- // by default, the plugin uses a build-time generated encryption key for
366
- // "use server" closure argument binding.
367
- // This can be overwritten by configuring `defineEncryptionKey` option,
368
- // for example, to obtain a key through environment variable during runtime.
369
- // cf. https://nextjs.org/docs/app/guides/data-security#overwriting-encryption-keys-advanced
370
- defineEncryptionKey: 'process.env.MY_ENCRYPTION_KEY',
371
-
372
376
  // see `RscPluginOptions` for full options ...
373
377
  }),
374
378
  ],
@@ -405,7 +409,9 @@ This module re-exports RSC runtime API provided by `react-server-dom/client.brow
405
409
  - `createFromFetch`: a robust way of `createFromReadableStream((await fetch("...")).body)`
406
410
  - `encodeReply/setServerCallback`: server function related...
407
411
 
408
- ## CSS Support
412
+ ## Tips
413
+
414
+ ### CSS Support
409
415
 
410
416
  The plugin automatically handles CSS code-splitting and injection for server components. This eliminates the need to manually call [`import.meta.viteRsc.loadCss()`](#importmetaviterscloadcss) in most cases.
411
417
 
@@ -439,11 +445,11 @@ export function Page() {
439
445
  }
440
446
  ```
441
447
 
442
- ## Canary and Experimental channel releases
448
+ ### Canary and Experimental channel releases
443
449
 
444
450
  See https://github.com/vitejs/vite-plugin-react/pull/524 for how to install the package for React [canary](https://react.dev/community/versioning-policy#canary-channel) and [experimental](https://react.dev/community/versioning-policy#all-release-channels) usages.
445
451
 
446
- ## Using `@vitejs/plugin-rsc` as a framework package's `dependencies`
452
+ ### Using `@vitejs/plugin-rsc` as a framework package's `dependencies`
447
453
 
448
454
  By default, `@vitejs/plugin-rsc` is expected to be used as `peerDependencies` similar to `react` and `react-dom`. When `@vitejs/plugin-rsc` is not available at the project root (e.g., in `node_modules/@vitejs/plugin-rsc`), you will see warnings like:
449
455
 
@@ -474,7 +480,7 @@ export default function myRscFrameworkPlugin() {
474
480
  }
475
481
  ```
476
482
 
477
- ## Typescript
483
+ ### Typescript
478
484
 
479
485
  Types for global API are defined in `@vitejs/plugin-rsc/types`. For example, you can add it to `tsconfig.json` to have types for `import.meta.viteRsc` APIs:
480
486
 
@@ -494,6 +500,67 @@ import.meta.viteRsc.loadModule
494
500
 
495
501
  See also [Vite documentation](https://vite.dev/guide/api-hmr.html#intellisense-for-typescript) for `vite/client` types.
496
502
 
503
+ ### `server-only` and `client-only` import
504
+
505
+ <!-- references? -->
506
+ <!-- https://nextjs.org/docs/app/getting-started/server-and-client-components#preventing-environment-poisoning -->
507
+ <!-- https://overreacted.io/how-imports-work-in-rsc/ -->
508
+
509
+ You can use the `server-only` import to prevent accidentally importing server-only code into client bundles, which can expose sensitive server code in public static assets.
510
+ For example, the plugin will show an error `'server-only' cannot be imported in client build` for the following code:
511
+
512
+ - server-utils.js
513
+
514
+ ```tsx
515
+ import 'server-only'
516
+
517
+ export async function getData() {
518
+ const res = await fetch('https://internal-service.com/data', {
519
+ headers: {
520
+ authorization: process.env.API_KEY,
521
+ },
522
+ })
523
+ return res.json()
524
+ }
525
+ ```
526
+
527
+ - client.js
528
+
529
+ ```tsx
530
+ 'use client'
531
+ import { getData } from './server-utils.js' // ❌ 'server-only' cannot be imported in client build
532
+ ...
533
+ ```
534
+
535
+ Similarly, the `client-only` import ensures browser-specific code isn't accidentally imported into server environments.
536
+ For example, the plugin will show an error `'client-only' cannot be imported in server build` for the following code:
537
+
538
+ - client-utils.js
539
+
540
+ ```tsx
541
+ import 'client-only'
542
+
543
+ export function getStorage(key) {
544
+ // This uses browser-only APIs
545
+ return window.localStorage.getItem(key)
546
+ }
547
+ ```
548
+
549
+ - server.js
550
+
551
+ ```tsx
552
+ import { getStorage } from './client-utils.js' // ❌ 'client-only' cannot be imported in server build
553
+
554
+ export function ServerComponent() {
555
+ const data = getStorage("settings")
556
+ ...
557
+ }
558
+ ```
559
+
560
+ Note that while there are official npm packages [`server-only`](https://www.npmjs.com/package/server-only) and [`client-only`](https://www.npmjs.com/package/client-only) created by React team, they don't need to be installed. The plugin internally overrides these imports and surfaces their runtime errors as build-time errors.
561
+
562
+ This build-time validation is enabled by default and can be disabled by setting `validateImports: false` in the plugin options.
563
+
497
564
  ## Credits
498
565
 
499
566
  This project builds on fundamental techniques and insights from pioneering Vite RSC implementations.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import "./plugin-CZbI4rhS.js";
3
3
  import "./transforms-D1-2JfCh.js";
4
4
  import "./encryption-utils-BDwwcMVT.js";
5
5
  import "./rpc-CUvSZurk.js";
6
- import { getPluginApi, vitePluginRsc } from "./plugin-Bzocj-4a.js";
6
+ import { getPluginApi, vitePluginRsc } from "./plugin-BI8kK-GE.js";
7
7
  import "./cjs-WQBk0zA_.js";
8
8
  import "./shared-AvKUASD5.js";
9
9
 
@@ -183,29 +183,82 @@ function validateImportPlugin() {
183
183
  name: "rsc:validate-imports",
184
184
  resolveId: {
185
185
  order: "pre",
186
- async handler(source, importer, options) {
186
+ async handler(source, _importer, options) {
187
187
  if ("scan" in options && options.scan) return;
188
- if (source === "client-only") {
189
- if (this.environment.name === "rsc") throw new Error(`'client-only' cannot be imported in server build (importer: '${importer ?? "unknown"}', environment: ${this.environment.name})`);
190
- return {
191
- id: `\0virtual:vite-rsc/empty`,
192
- moduleSideEffects: false
188
+ if (source === "client-only" || source === "server-only") {
189
+ if (source === "client-only" && this.environment.name === "rsc" || source === "server-only" && this.environment.name !== "rsc") return {
190
+ id: `\0virtual:vite-rsc/validate-imports/invalid/${source}`,
191
+ moduleSideEffects: true
193
192
  };
194
- }
195
- if (source === "server-only") {
196
- if (this.environment.name !== "rsc") throw new Error(`'server-only' cannot be imported in client build (importer: '${importer ?? "unknown"}', environment: ${this.environment.name})`);
197
193
  return {
198
- id: `\0virtual:vite-rsc/empty`,
194
+ id: `\0virtual:vite-rsc/validate-imports/valid/${source}`,
199
195
  moduleSideEffects: false
200
196
  };
201
197
  }
202
198
  }
203
199
  },
204
200
  load(id) {
205
- if (id.startsWith("\0virtual:vite-rsc/empty")) return `export {}`;
201
+ if (id.startsWith("\0virtual:vite-rsc/validate-imports/invalid/")) return `throw new Error("invalid import of '${id.slice(id.lastIndexOf("/") + 1)}'")`;
202
+ if (id.startsWith("\0virtual:vite-rsc/validate-imports/")) return `export {}`;
203
+ },
204
+ transform: {
205
+ order: "post",
206
+ async handler(_code, id) {
207
+ if (this.environment.mode === "dev") {
208
+ if (id.startsWith(`\0virtual:vite-rsc/validate-imports/invalid/`)) {
209
+ const chain = getImportChainDev(this.environment, id);
210
+ validateImportChain(chain, this.environment.name, this.environment.config.root);
211
+ }
212
+ }
213
+ }
214
+ },
215
+ buildEnd() {
216
+ if (this.environment.mode === "build") {
217
+ const serverOnly = getImportChainBuild(this, "\0virtual:vite-rsc/validate-imports/invalid/server-only");
218
+ validateImportChain(serverOnly, this.environment.name, this.environment.config.root);
219
+ const clientOnly = getImportChainBuild(this, "\0virtual:vite-rsc/validate-imports/invalid/client-only");
220
+ validateImportChain(clientOnly, this.environment.name, this.environment.config.root);
221
+ }
206
222
  }
207
223
  };
208
224
  }
225
+ function getImportChainDev(environment, id) {
226
+ const chain = [];
227
+ const recurse = (id$1) => {
228
+ if (chain.includes(id$1)) return;
229
+ const info = environment.moduleGraph.getModuleById(id$1);
230
+ if (!info) return;
231
+ chain.push(id$1);
232
+ const next = [...info.importers][0];
233
+ if (next && next.id) recurse(next.id);
234
+ };
235
+ recurse(id);
236
+ return chain;
237
+ }
238
+ function getImportChainBuild(ctx, id) {
239
+ const chain = [];
240
+ const recurse = (id$1) => {
241
+ if (chain.includes(id$1)) return;
242
+ const info = ctx.getModuleInfo(id$1);
243
+ if (!info) return;
244
+ chain.push(id$1);
245
+ const next = info.importers[0];
246
+ if (next) recurse(next);
247
+ };
248
+ recurse(id);
249
+ return chain;
250
+ }
251
+ function validateImportChain(chain, environmentName, root) {
252
+ if (chain.length === 0) return;
253
+ const id = chain[0];
254
+ const source = id.slice(id.lastIndexOf("/") + 1);
255
+ let result = `'${source}' cannot be imported in ${source === "server-only" ? "client" : "server"} build ('${environmentName}' environment):\n`;
256
+ result += chain.slice(1, 6).map((id$1, i) => " ".repeat(i + 1) + `imported by ${path.relative(root, id$1).replaceAll("\0", "")}\n`).join("");
257
+ if (chain.length > 6) result += " ".repeat(7) + "...\n";
258
+ const error = new Error(result);
259
+ if (chain[1]) Object.assign(error, { id: chain[1] });
260
+ throw error;
261
+ }
209
262
 
210
263
  //#endregion
211
264
  //#region src/plugins/find-source-map-url.ts
@@ -813,6 +866,11 @@ import.meta.hot.on("rsc:update", () => {
813
866
  document.querySelectorAll("vite-error-overlay").forEach((n) => n.close())
814
867
  });
815
868
  `;
869
+ code += `import.meta.hot.on("rsc:prune", ${(e) => {
870
+ document.querySelectorAll("link[rel='stylesheet']").forEach((node) => {
871
+ if (e.paths.includes(node.dataset.rscCssHref)) node.remove();
872
+ });
873
+ }});`;
816
874
  return code;
817
875
  }),
818
876
  ...vitePluginRscMinimal(rscPluginOptions, manager),
@@ -997,7 +1055,7 @@ function vitePluginUseClient(useClientPluginOptions, manager) {
997
1055
  resolveId: {
998
1056
  order: "pre",
999
1057
  async handler(source, importer, options) {
1000
- if (this.environment.name === serverEnvironmentName && bareImportRE.test(source)) {
1058
+ if (this.environment.name === serverEnvironmentName && bareImportRE.test(source) && !(source === "client-only" || source === "server-only")) {
1001
1059
  const resolved = await this.resolve(source, importer, options);
1002
1060
  if (resolved && resolved.id.includes("/node_modules/")) {
1003
1061
  packageSources.set(resolved.id, source);
@@ -1408,6 +1466,19 @@ function vitePluginRscCss(rscCssOptions = {}, manager) {
1408
1466
  },
1409
1467
  {
1410
1468
  name: "rsc:importer-resources",
1469
+ configureServer(server) {
1470
+ const hot = server.environments.rsc.hot;
1471
+ const original = hot.send;
1472
+ hot.send = function(...args) {
1473
+ const e = args[0];
1474
+ if (e && typeof e === "object" && e.type === "prune") server.environments.client.hot.send({
1475
+ type: "custom",
1476
+ event: "rsc:prune",
1477
+ data: e
1478
+ });
1479
+ return original.apply(this, args);
1480
+ };
1481
+ },
1411
1482
  async transform(code, id) {
1412
1483
  if (!code.includes("import.meta.viteRsc.loadCss")) return;
1413
1484
  assert(this.environment.name === "rsc");
@@ -1507,7 +1578,8 @@ function generateResourcesCode(depsCode, manager) {
1507
1578
  key: "css:" + href,
1508
1579
  rel: "stylesheet",
1509
1580
  precedence: "vite-rsc/importer-resources",
1510
- href
1581
+ href,
1582
+ "data-rsc-css-href": href
1511
1583
  })), RemoveDuplicateServerCss && React.createElement(RemoveDuplicateServerCss, { key: "remove-duplicate-css" })]);
1512
1584
  };
1513
1585
  };
package/dist/plugin.js CHANGED
@@ -3,7 +3,7 @@ import "./plugin-CZbI4rhS.js";
3
3
  import "./transforms-D1-2JfCh.js";
4
4
  import "./encryption-utils-BDwwcMVT.js";
5
5
  import "./rpc-CUvSZurk.js";
6
- import { getPluginApi, transformRscCssExport, vitePluginRsc, vitePluginRscMinimal } from "./plugin-Bzocj-4a.js";
6
+ import { getPluginApi, transformRscCssExport, vitePluginRsc, vitePluginRscMinimal } from "./plugin-BI8kK-GE.js";
7
7
  import "./cjs-WQBk0zA_.js";
8
8
  import "./shared-AvKUASD5.js";
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitejs/plugin-rsc",
3
- "version": "0.4.30",
3
+ "version": "0.4.31",
4
4
  "description": "React Server Components (RSC) support for Vite.",
5
5
  "keywords": [
6
6
  "vite",