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 +}