diff --git a/packages/plugin-rsc/e2e/error.test.ts b/packages/plugin-rsc/e2e/error.test.ts
new file mode 100644
index 00000000..ea2b63b1
--- /dev/null
+++ b/packages/plugin-rsc/e2e/error.test.ts
@@ -0,0 +1,57 @@
+import { test, expect } from '@playwright/test'
+import { setupInlineFixture } from './fixture'
+import { x } from 'tinyexec'
+
+test.describe('invalid directives', () => {
+ test.describe('"use server" in "use client"', () => {
+ const root = 'examples/e2e/temp/use-server-in-use-client'
+ test.beforeAll(async () => {
+ await setupInlineFixture({
+ src: 'examples/starter',
+ dest: root,
+ files: {
+ 'src/client.tsx': /* tsx */ `
+ "use client";
+
+ export function TestClient() {
+ return
[test-client]
+ }
+
+ function testFn() {
+ "use server";
+ console.log("testFn");
+ }
+ `,
+ 'src/root.tsx': /* tsx */ `
+ import { TestClient } from './client.tsx'
+
+ export function Root() {
+ return (
+
+
+
+
+
+ [test-server]
+
+
+
+ )
+ }
+ `,
+ },
+ })
+ })
+
+ test('build', async () => {
+ const result = await x('pnpm', ['build'], {
+ throwOnError: false,
+ nodeOptions: { cwd: root },
+ })
+ expect(result.stderr).toContain(
+ `'use server' directive is not allowed inside 'use client'`,
+ )
+ expect(result.exitCode).not.toBe(0)
+ })
+ })
+})
diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts
index 08565c09..df214f2b 100644
--- a/packages/plugin-rsc/src/plugin.ts
+++ b/packages/plugin-rsc/src/plugin.ts
@@ -29,6 +29,7 @@ import {
transformDirectiveProxyExport,
transformServerActionServer,
transformWrapExport,
+ findDirectives,
} from './transforms'
import { generateEncryptionKey, toBase64 } from './utils/encryption-utils'
import { createRpcServer } from './utils/rpc'
@@ -1135,6 +1136,16 @@ function vitePluginUseClient(
return
}
+ if (code.includes('use server')) {
+ const directives = findDirectives(ast, 'use server')
+ if (directives.length > 0) {
+ this.error(
+ `'use server' directive is not allowed inside 'use client'`,
+ directives[0]?.start,
+ )
+ }
+ }
+
let importId: string
let referenceKey: string
const packageSource = packageSources.get(id)
diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts
index a471f2c5..21c760ca 100644
--- a/packages/plugin-rsc/src/transforms/hoist.ts
+++ b/packages/plugin-rsc/src/transforms/hoist.ts
@@ -1,5 +1,5 @@
import { tinyassert } from '@hiogawa/utils'
-import type { Program } from 'estree'
+import type { Program, Literal } from 'estree'
import { walk } from 'estree-walker'
import MagicString from 'magic-string'
import { analyze } from 'periscopic'
@@ -56,7 +56,7 @@ export function transformHoistInlineDirective(
node.type === 'ArrowFunctionExpression') &&
node.body.type === 'BlockStatement'
) {
- const match = matchDirective(node.body.body, directive)
+ const match = matchDirective(node.body.body, directive)?.match
if (!match) return
if (!node.async && rejectNonAsyncFunction) {
throw Object.assign(
@@ -156,7 +156,7 @@ const exactRegex = (s: string): RegExp =>
function matchDirective(
body: Program['body'],
directive: RegExp,
-): RegExpMatchArray | undefined {
+): { match: RegExpMatchArray; node: Literal } | undefined {
for (const stable of body) {
if (
stable.type === 'ExpressionStatement' &&
@@ -165,8 +165,24 @@ function matchDirective(
) {
const match = stable.expression.value.match(directive)
if (match) {
- return match
+ return { match, node: stable.expression }
}
}
}
}
+
+export function findDirectives(ast: Program, directive: string): Literal[] {
+ const directiveRE = exactRegex(directive)
+ const nodes: Literal[] = []
+ walk(ast, {
+ enter(node) {
+ if (node.type === 'Program' || node.type === 'BlockStatement') {
+ const match = matchDirective(node.body, directiveRE)
+ if (match) {
+ nodes.push(match.node)
+ }
+ }
+ },
+ })
+ return nodes
+}