Skip to content

Commit 08986dd

Browse files
authored
fix(rsc): relax async function requirement for "use server" module directive (#754)
1 parent 7542e6f commit 08986dd

File tree

2 files changed

+77
-31
lines changed

2 files changed

+77
-31
lines changed

packages/plugin-rsc/src/transforms/wrap-export.test.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { parseAstAsync } from 'vite'
22
import { describe, expect, test } from 'vitest'
33
import { debugSourceMap } from './test-utils'
44
import {
5-
type TransformWrapExportFilter,
5+
type TransformWrapExportOptions,
66
transformWrapExport,
77
} from './wrap-export'
88

99
async function testTransform(
1010
input: string,
11-
options?: { filter?: TransformWrapExportFilter },
11+
options?: Omit<TransformWrapExportOptions, 'runtime'>,
1212
) {
1313
const ast = await parseAstAsync(input)
1414
const { output } = transformWrapExport(input, ast, {
@@ -304,4 +304,55 @@ export default Page;
304304
"
305305
`)
306306
})
307+
308+
test('reject non async function', async () => {
309+
// next.js's validataion isn't entirely consisten.
310+
// for now we aim to make it at least as forgiving as next.js.
311+
312+
const accepted = [
313+
`export async function f() {}`,
314+
`export default async function f() {}`,
315+
`export const fn = async function fn() {}`,
316+
`export const fn = async () => {}`,
317+
`export const fn = async () => {}, fn2 = x`,
318+
`export const fn = x`,
319+
`export const fn = x({ x: y })`,
320+
`export const fn = x(async () => {})`,
321+
`export default x`,
322+
`const y = x; export { y }`,
323+
`export const fn = x(() => {})`, // rejected by next.js
324+
]
325+
326+
const rejected = [
327+
`export function f() {}`,
328+
`export default function f() {}`,
329+
`export const fn = function fn() {}`,
330+
`export const fn = () => {}`,
331+
`export const fn = x, fn2 = () => {}`,
332+
`export class Cls {}`,
333+
]
334+
335+
async function toActual(input: string) {
336+
try {
337+
await testTransform(input, {
338+
rejectNonAsyncFunction: true,
339+
})
340+
return [input, true]
341+
} catch (e) {
342+
return [input, e instanceof Error ? e.message : e]
343+
}
344+
}
345+
346+
const actual = [
347+
...(await Promise.all(accepted.map((e) => toActual(e)))),
348+
...(await Promise.all(rejected.map((e) => toActual(e)))),
349+
]
350+
351+
const expected = [
352+
...accepted.map((e) => [e, true]),
353+
...rejected.map((e) => [e, 'unsupported non async function']),
354+
]
355+
356+
expect(actual).toEqual(expected)
357+
})
307358
})

packages/plugin-rsc/src/transforms/wrap-export.ts

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ export type TransformWrapExportFilter = (
1414
meta: ExportMeta,
1515
) => boolean
1616

17+
export type TransformWrapExportOptions = {
18+
runtime: (value: string, name: string, meta: ExportMeta) => string
19+
ignoreExportAllDeclaration?: boolean
20+
rejectNonAsyncFunction?: boolean
21+
filter?: TransformWrapExportFilter
22+
}
23+
1724
export function transformWrapExport(
1825
input: string,
1926
ast: Program,
20-
options: {
21-
runtime: (value: string, name: string, meta: ExportMeta) => string
22-
ignoreExportAllDeclaration?: boolean
23-
rejectNonAsyncFunction?: boolean
24-
filter?: TransformWrapExportFilter
25-
},
27+
options: TransformWrapExportOptions,
2628
): {
2729
exportNames: string[]
2830
output: MagicString
@@ -81,8 +83,15 @@ export function transformWrapExport(
8183
)
8284
}
8385

84-
function validateNonAsyncFunction(node: Node, ok?: boolean) {
85-
if (options.rejectNonAsyncFunction && !ok) {
86+
function validateNonAsyncFunction(node: Node) {
87+
if (!options.rejectNonAsyncFunction) return
88+
if (
89+
node.type === 'ClassDeclaration' ||
90+
((node.type === 'FunctionDeclaration' ||
91+
node.type === 'FunctionExpression' ||
92+
node.type === 'ArrowFunctionExpression') &&
93+
!node.async)
94+
) {
8695
throw Object.assign(new Error(`unsupported non async function`), {
8796
pos: node.start,
8897
})
@@ -100,11 +109,7 @@ export function transformWrapExport(
100109
/**
101110
* export function foo() {}
102111
*/
103-
validateNonAsyncFunction(
104-
node,
105-
node.declaration.type === 'FunctionDeclaration' &&
106-
node.declaration.async,
107-
)
112+
validateNonAsyncFunction(node.declaration)
108113
const name = node.declaration.id.name
109114
wrapSimple(node.start, node.declaration.start, [
110115
{ name, meta: { isFunction: true, declName: name } },
@@ -113,14 +118,11 @@ export function transformWrapExport(
113118
/**
114119
* export const foo = 1, bar = 2
115120
*/
116-
validateNonAsyncFunction(
117-
node,
118-
node.declaration.declarations.every(
119-
(decl) =>
120-
decl.init?.type === 'ArrowFunctionExpression' &&
121-
decl.init.async,
122-
),
123-
)
121+
for (const decl of node.declaration.declarations) {
122+
if (decl.init) {
123+
validateNonAsyncFunction(decl.init)
124+
}
125+
}
124126
if (node.declaration.kind === 'const') {
125127
output.update(
126128
node.declaration.start,
@@ -201,14 +203,7 @@ export function transformWrapExport(
201203
* export default () => {}
202204
*/
203205
if (node.type === 'ExportDefaultDeclaration') {
204-
validateNonAsyncFunction(
205-
node,
206-
// TODO: somehow identifier is allowed in next.js?
207-
// (see packages/react-server/examples/next/app/actions/server/actions.ts)
208-
node.declaration.type === 'Identifier' ||
209-
(node.declaration.type === 'FunctionDeclaration' &&
210-
node.declaration.async),
211-
)
206+
validateNonAsyncFunction(node.declaration as Node)
212207
let localName: string
213208
let isFunction = false
214209
let declName: string | undefined

0 commit comments

Comments
 (0)