Skip to content

Commit 72f70b9

Browse files
committed
fix: align export async function checks
1 parent c1651ac commit 72f70b9

4 files changed

Lines changed: 83 additions & 47 deletions

File tree

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { transformProxyExport } from './proxy-export'
44
import { debugSourceMap } from './test-utils'
55
import { transformWrapExport } from './wrap-export'
66

7-
async function testTransform(input: string, options?: { keep?: boolean }) {
7+
async function testTransform(
8+
input: string,
9+
options?: { keep?: boolean; rejectNonAsyncFunction?: boolean },
10+
) {
811
const ast = await parseAstAsync(input)
912
const result = transformProxyExport(ast, {
1013
code: input,
@@ -239,4 +242,46 @@ export const MyClientComp = () => { throw new Error('...') }
239242
}
240243
`)
241244
})
245+
246+
test('reject non async function', async () => {
247+
const accepted = [
248+
'export async function f() {}',
249+
'export default async function f() {}',
250+
'export const fn = async function fn() {}',
251+
'export const fn = async () => {}',
252+
'export const fn = async () => {}, fn2 = x',
253+
'export const fn = x',
254+
'export const fn = x({ x: y })',
255+
'export const fn = x(async () => {})',
256+
'export default x',
257+
'const y = x; export { y }',
258+
'export const fn = x(() => {})',
259+
'export const testAction = actionClient.action(async () => { return { message: "Hello, world!" }; });',
260+
]
261+
262+
const rejected = [
263+
'export function f() {}',
264+
'export default function f() {}',
265+
'export const fn = function fn() {}',
266+
'export const fn = () => {}',
267+
'export const fn = x, fn2 = () => {}',
268+
'export class Cls {}',
269+
'export const Cls = class {}',
270+
'export const Cls = class Foo {}',
271+
]
272+
273+
for (const code of accepted) {
274+
await expect(
275+
testTransform(code, { rejectNonAsyncFunction: true }),
276+
).resolves.not.toThrow()
277+
}
278+
279+
for (const code of rejected) {
280+
await expect(
281+
testTransform(code, { rejectNonAsyncFunction: true }),
282+
).rejects.toThrow(/unsupported non async function/)
283+
}
284+
285+
expect.assertions(rejected.length + accepted.length)
286+
})
242287
})

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

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Node, Program } from 'estree'
33
import MagicString from 'magic-string'
44
import { extract_names } from 'periscopic'
55
import { hasDirective } from './utils'
6+
import { validateNonAsyncFunction } from './wrap-export'
67

78
export type TransformProxyExportOptions = {
89
/** Required for source map and `keep` options */
@@ -59,14 +60,6 @@ export function transformProxyExport(
5960
output.update(node.start, node.end, newCode)
6061
}
6162

62-
function validateNonAsyncFunction(node: Node, ok?: boolean) {
63-
if (options.rejectNonAsyncFunction && !ok) {
64-
throw Object.assign(new Error(`unsupported non async function`), {
65-
pos: node.start,
66-
})
67-
}
68-
}
69-
7063
for (const node of ast.body) {
7164
if (node.type === 'ExportNamedDeclaration') {
7265
if (node.declaration) {
@@ -77,24 +70,15 @@ export function transformProxyExport(
7770
/**
7871
* export function foo() {}
7972
*/
80-
validateNonAsyncFunction(
81-
node,
82-
node.declaration.type === 'FunctionDeclaration' &&
83-
node.declaration.async,
84-
)
73+
validateNonAsyncFunction(options, node.declaration)
8574
createExport(node, [node.declaration.id.name])
8675
} else if (node.declaration.type === 'VariableDeclaration') {
8776
/**
8877
* export const foo = 1, bar = 2
8978
*/
90-
validateNonAsyncFunction(
91-
node,
92-
node.declaration.declarations.every(
93-
(decl) =>
94-
decl.init?.type === 'ArrowFunctionExpression' &&
95-
decl.init.async,
96-
),
97-
)
79+
for (const decl of node.declaration.declarations) {
80+
if (decl.init) validateNonAsyncFunction(options, decl.init)
81+
}
9882
if (options.keep && options.code) {
9983
if (node.declaration.declarations.length === 1) {
10084
const decl = node.declaration.declarations[0]!
@@ -149,12 +133,7 @@ export function transformProxyExport(
149133
* export default () => {}
150134
*/
151135
if (node.type === 'ExportDefaultDeclaration') {
152-
validateNonAsyncFunction(
153-
node,
154-
node.declaration.type === 'Identifier' ||
155-
(node.declaration.type === 'FunctionDeclaration' &&
156-
node.declaration.async),
157-
)
136+
validateNonAsyncFunction(options, node.declaration)
158137
createExport(node, ['default'])
159138
continue
160139
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ export default Page;
321321
`export default x`,
322322
`const y = x; export { y }`,
323323
`export const fn = x(() => {})`, // rejected by next.js
324+
`export const testAction = actionClient.action(async () => { return { message: "Hello, world!" }; });`,
324325
]
325326

326327
const rejected = [
@@ -330,6 +331,8 @@ export default Page;
330331
`export const fn = () => {}`,
331332
`export const fn = x, fn2 = () => {}`,
332333
`export class Cls {}`,
334+
`export const Cls = class {}`,
335+
`export const Cls = class Foo {}`,
333336
]
334337

335338
async function toActual(input: string) {

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

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { tinyassert } from '@hiogawa/utils'
2-
import type { Node, Program } from 'estree'
2+
import type {
3+
MaybeNamedClassDeclaration,
4+
MaybeNamedFunctionDeclaration,
5+
Node,
6+
Program,
7+
} from 'estree'
38
import MagicString from 'magic-string'
49
import { extract_names } from 'periscopic'
510

@@ -21,6 +26,25 @@ export type TransformWrapExportOptions = {
2126
filter?: TransformWrapExportFilter
2227
}
2328

29+
export function validateNonAsyncFunction(
30+
opts: { rejectNonAsyncFunction?: boolean },
31+
node: Node | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration,
32+
): void {
33+
if (!opts.rejectNonAsyncFunction) return
34+
if (
35+
node.type === 'ClassDeclaration' ||
36+
node.type === 'ClassExpression' ||
37+
((node.type === 'FunctionDeclaration' ||
38+
node.type === 'FunctionExpression' ||
39+
node.type === 'ArrowFunctionExpression') &&
40+
!node.async)
41+
) {
42+
throw Object.assign(new Error(`unsupported non async function`), {
43+
pos: node.start,
44+
})
45+
}
46+
}
47+
2448
export function transformWrapExport(
2549
input: string,
2650
ast: Program,
@@ -83,21 +107,6 @@ export function transformWrapExport(
83107
)
84108
}
85109

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-
) {
95-
throw Object.assign(new Error(`unsupported non async function`), {
96-
pos: node.start,
97-
})
98-
}
99-
}
100-
101110
for (const node of ast.body) {
102111
// named exports
103112
if (node.type === 'ExportNamedDeclaration') {
@@ -109,7 +118,7 @@ export function transformWrapExport(
109118
/**
110119
* export function foo() {}
111120
*/
112-
validateNonAsyncFunction(node.declaration)
121+
validateNonAsyncFunction(options, node.declaration)
113122
const name = node.declaration.id.name
114123
wrapSimple(node.start, node.declaration.start, [
115124
{ name, meta: { isFunction: true, declName: name } },
@@ -120,7 +129,7 @@ export function transformWrapExport(
120129
*/
121130
for (const decl of node.declaration.declarations) {
122131
if (decl.init) {
123-
validateNonAsyncFunction(decl.init)
132+
validateNonAsyncFunction(options, decl.init)
124133
}
125134
}
126135
if (node.declaration.kind === 'const') {
@@ -203,7 +212,7 @@ export function transformWrapExport(
203212
* export default () => {}
204213
*/
205214
if (node.type === 'ExportDefaultDeclaration') {
206-
validateNonAsyncFunction(node.declaration as Node)
215+
validateNonAsyncFunction(options, node.declaration)
207216
let localName: string
208217
let isFunction = false
209218
let declName: string | undefined

0 commit comments

Comments
 (0)