Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/plugin-rsc/e2e/error.test.ts
Original file line number Diff line number Diff line change
@@ -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 <div>[test-client]</div>
}

function testFn() {
"use server";
console.log("testFn");
}
`,
'src/root.tsx': /* tsx */ `
import { TestClient } from './client.tsx'

export function Root() {
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
</head>
<body>
<div>[test-server]</div>
<TestClient />
</body>
</html>
)
}
`,
},
})
})

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)
})
})
})
11 changes: 11 additions & 0 deletions packages/plugin-rsc/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
transformDirectiveProxyExport,
transformServerActionServer,
transformWrapExport,
findDirectives,
} from './transforms'
import { generateEncryptionKey, toBase64 } from './utils/encryption-utils'
import { createRpcServer } from './utils/rpc'
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 20 additions & 4 deletions packages/plugin-rsc/src/transforms/hoist.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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' &&
Expand All @@ -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
}
Loading