Skip to content

Commit 4be1231

Browse files
Merge remote-tracking branch 'upstream/main' into chore-rsc-nightly
2 parents 61b21a4 + a2a287a commit 4be1231

File tree

4 files changed

+293
-16
lines changed

4 files changed

+293
-16
lines changed

packages/plugin-rsc/README.md

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,17 @@ export default defineConfig({
353353
// this behavior can be customized by `serverHandler` option.
354354
serverHandler: false,
355355

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+
356367
// when `loadModuleDevProxy: true`, `import.meta.viteRsc.loadModule` is implemented
357368
// through `fetch` based RPC, which allows, for example, rsc environment inside
358369
// cloudflare workers to communicate with node ssr environment on main Vite process.
@@ -362,13 +373,6 @@ export default defineConfig({
362373
// if it breaks, it can be opt-out or selectively applied based on files.
363374
rscCssTransform: { filter: (id) => id.includes('/my-app/') },
364375

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-
372376
// see `RscPluginOptions` for full options ...
373377
}),
374378
],
@@ -405,7 +409,9 @@ This module re-exports RSC runtime API provided by `react-server-dom/client.brow
405409
- `createFromFetch`: a robust way of `createFromReadableStream((await fetch("...")).body)`
406410
- `encodeReply/setServerCallback`: server function related...
407411

408-
## CSS Support
412+
## Tips
413+
414+
### CSS Support
409415

410416
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.
411417

@@ -439,11 +445,11 @@ export function Page() {
439445
}
440446
```
441447

442-
## Canary and Experimental channel releases
448+
### Canary and Experimental channel releases
443449

444450
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.
445451

446-
## Using `@vitejs/plugin-rsc` as a framework package's `dependencies`
452+
### Using `@vitejs/plugin-rsc` as a framework package's `dependencies`
447453

448454
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:
449455

@@ -474,7 +480,7 @@ export default function myRscFrameworkPlugin() {
474480
}
475481
```
476482
477-
## Typescript
483+
### Typescript
478484
479485
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:
480486
@@ -494,6 +500,67 @@ import.meta.viteRsc.loadModule
494500
495501
See also [Vite documentation](https://vite.dev/guide/api-hmr.html#intellisense-for-typescript) for `vite/client` types.
496502
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+
497564
## Credits
498565
499566
This project builds on fundamental techniques and insights from pioneering Vite RSC implementations.

packages/plugin-rsc/e2e/validate-imports.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ test.describe('validate imports', () => {
102102
throwOnError: false,
103103
nodeOptions: { cwd: root },
104104
})
105+
// assertion is adjusted for rolldown-vite
106+
expect(result.stderr).toContain(`rsc:validate-imports`)
105107
expect(result.stderr).toContain(
106108
`'server-only' cannot be imported in client build`,
107109
)
@@ -151,6 +153,7 @@ test.describe('validate imports', () => {
151153
throwOnError: false,
152154
nodeOptions: { cwd: root },
153155
})
156+
expect(result.stderr).toContain(`rsc:validate-imports`)
154157
expect(result.stderr).toContain(
155158
`'client-only' cannot be imported in server build`,
156159
)

packages/plugin-rsc/src/plugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1311,7 +1311,8 @@ function vitePluginUseClient(
13111311
async handler(source, importer, options) {
13121312
if (
13131313
this.environment.name === serverEnvironmentName &&
1314-
bareImportRE.test(source)
1314+
bareImportRE.test(source) &&
1315+
!(source === 'client-only' || source === 'server-only')
13151316
) {
13161317
const resolved = await this.resolve(source, importer, options)
13171318
if (resolved && resolved.id.includes('/node_modules/')) {

0 commit comments

Comments
 (0)