Skip to content

Commit 9ef6089

Browse files
Merge remote-tracking branch 'upstream/main' into chore-rsc-nightly
2 parents 2114fc5 + d0c93af commit 9ef6089

File tree

5 files changed

+105
-10
lines changed

5 files changed

+105
-10
lines changed

packages/plugin-rsc/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## <small>[0.4.32](https://github.com/vitejs/vite-plugin-react/compare/[email protected]@0.4.32) (2025-09-26)</small>
2+
### Bug Fixes
3+
4+
* **deps:** update all non-major dependencies ([#851](https://github.com/vitejs/vite-plugin-react/issues/851)) ([3c2ebf8](https://github.com/vitejs/vite-plugin-react/commit/3c2ebf89de7f5e40ed0ef932993f7d0b7695719b))
5+
* **rsc:** reject inline "use server" inside "use client" module ([#884](https://github.com/vitejs/vite-plugin-react/issues/884)) ([5bc3f79](https://github.com/vitejs/vite-plugin-react/commit/5bc3f79fb4356ebf574b6ba28e4c7a315f4336de))
6+
7+
### Miscellaneous Chores
8+
9+
* **deps:** update all non-major dependencies ([#879](https://github.com/vitejs/vite-plugin-react/issues/879)) ([608f266](https://github.com/vitejs/vite-plugin-react/commit/608f266c8d53f41a6b1541de35b218fe2640ec05))
10+
* **rsc:** fix typo ([#885](https://github.com/vitejs/vite-plugin-react/issues/885)) ([b81470c](https://github.com/vitejs/vite-plugin-react/commit/b81470c3076e079be517b7bf92325760ba89fd3d))
11+
112
## <small>[0.4.31](https://github.com/vitejs/vite-plugin-react/compare/[email protected]@0.4.31) (2025-09-17)</small>
213
### Bug Fixes
314

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { test, expect } from '@playwright/test'
2+
import { setupInlineFixture } from './fixture'
3+
import { x } from 'tinyexec'
4+
5+
test.describe('invalid directives', () => {
6+
test.describe('"use server" in "use client"', () => {
7+
const root = 'examples/e2e/temp/use-server-in-use-client'
8+
test.beforeAll(async () => {
9+
await setupInlineFixture({
10+
src: 'examples/starter',
11+
dest: root,
12+
files: {
13+
'src/client.tsx': /* tsx */ `
14+
"use client";
15+
16+
export function TestClient() {
17+
return <div>[test-client]</div>
18+
}
19+
20+
function testFn() {
21+
"use server";
22+
console.log("testFn");
23+
}
24+
`,
25+
'src/root.tsx': /* tsx */ `
26+
import { TestClient } from './client.tsx'
27+
28+
export function Root() {
29+
return (
30+
<html lang="en">
31+
<head>
32+
<meta charSet="UTF-8" />
33+
</head>
34+
<body>
35+
<div>[test-server]</div>
36+
<TestClient />
37+
</body>
38+
</html>
39+
)
40+
}
41+
`,
42+
},
43+
})
44+
})
45+
46+
test('build', async () => {
47+
const result = await x('pnpm', ['build'], {
48+
throwOnError: false,
49+
nodeOptions: { cwd: root },
50+
})
51+
expect(result.stderr).toContain(
52+
`'use server' directive is not allowed inside 'use client'`,
53+
)
54+
expect(result.exitCode).not.toBe(0)
55+
})
56+
})
57+
})

packages/plugin-rsc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vitejs/plugin-rsc",
3-
"version": "0.4.31",
3+
"version": "0.4.32",
44
"description": "React Server Components (RSC) support for Vite.",
55
"keywords": [
66
"vite",

packages/plugin-rsc/src/plugin.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
transformDirectiveProxyExport,
3030
transformServerActionServer,
3131
transformWrapExport,
32+
findDirectives,
3233
} from './transforms'
3334
import { generateEncryptionKey, toBase64 } from './utils/encryption-utils'
3435
import { createRpcServer } from './utils/rpc'
@@ -1135,6 +1136,16 @@ function vitePluginUseClient(
11351136
return
11361137
}
11371138

1139+
if (code.includes('use server')) {
1140+
const directives = findDirectives(ast, 'use server')
1141+
if (directives.length > 0) {
1142+
this.error(
1143+
`'use server' directive is not allowed inside 'use client'`,
1144+
directives[0]?.start,
1145+
)
1146+
}
1147+
}
1148+
11381149
let importId: string
11391150
let referenceKey: string
11401151
const packageSource = packageSources.get(id)

packages/plugin-rsc/src/transforms/hoist.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { tinyassert } from '@hiogawa/utils'
2-
import type { Program } from 'estree'
2+
import type { Program, Literal } from 'estree'
33
import { walk } from 'estree-walker'
44
import MagicString from 'magic-string'
55
import { analyze } from 'periscopic'
@@ -56,7 +56,7 @@ export function transformHoistInlineDirective(
5656
node.type === 'ArrowFunctionExpression') &&
5757
node.body.type === 'BlockStatement'
5858
) {
59-
const match = matchDirective(node.body.body, directive)
59+
const match = matchDirective(node.body.body, directive)?.match
6060
if (!match) return
6161
if (!node.async && rejectNonAsyncFunction) {
6262
throw Object.assign(
@@ -156,17 +156,33 @@ const exactRegex = (s: string): RegExp =>
156156
function matchDirective(
157157
body: Program['body'],
158158
directive: RegExp,
159-
): RegExpMatchArray | undefined {
160-
for (const stable of body) {
159+
): { match: RegExpMatchArray; node: Literal } | undefined {
160+
for (const stmt of body) {
161161
if (
162-
stable.type === 'ExpressionStatement' &&
163-
stable.expression.type === 'Literal' &&
164-
typeof stable.expression.value === 'string'
162+
stmt.type === 'ExpressionStatement' &&
163+
stmt.expression.type === 'Literal' &&
164+
typeof stmt.expression.value === 'string'
165165
) {
166-
const match = stable.expression.value.match(directive)
166+
const match = stmt.expression.value.match(directive)
167167
if (match) {
168-
return match
168+
return { match, node: stmt.expression }
169169
}
170170
}
171171
}
172172
}
173+
174+
export function findDirectives(ast: Program, directive: string): Literal[] {
175+
const directiveRE = exactRegex(directive)
176+
const nodes: Literal[] = []
177+
walk(ast, {
178+
enter(node) {
179+
if (node.type === 'Program' || node.type === 'BlockStatement') {
180+
const match = matchDirective(node.body, directiveRE)
181+
if (match) {
182+
nodes.push(match.node)
183+
}
184+
}
185+
},
186+
})
187+
return nodes
188+
}

0 commit comments

Comments
 (0)