Skip to content

Commit 14d8889

Browse files
fix(rsc): align export async function checks (#1169)
Co-authored-by: Hiroshi Ogawa <hi.ogawa.zz@gmail.com>
1 parent 2c4b500 commit 14d8889

File tree

5 files changed

+116
-100
lines changed

5 files changed

+116
-100
lines changed

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

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { tinyassert } from '@hiogawa/utils'
22
import type { Node, Program } from 'estree'
33
import MagicString from 'magic-string'
4-
import { extractNames, hasDirective } from './utils'
4+
import { extractNames, hasDirective, validateNonAsyncFunction } from './utils'
55

66
export type TransformProxyExportOptions = {
77
/** Required for source map and `keep` options */
@@ -58,14 +58,6 @@ export function transformProxyExport(
5858
output.update(node.start, node.end, newCode)
5959
}
6060

61-
function validateNonAsyncFunction(node: Node, ok?: boolean) {
62-
if (options.rejectNonAsyncFunction && !ok) {
63-
throw Object.assign(new Error(`unsupported non async function`), {
64-
pos: node.start,
65-
})
66-
}
67-
}
68-
6961
for (const node of ast.body) {
7062
if (node.type === 'ExportNamedDeclaration') {
7163
if (node.declaration) {
@@ -76,24 +68,15 @@ export function transformProxyExport(
7668
/**
7769
* export function foo() {}
7870
*/
79-
validateNonAsyncFunction(
80-
node,
81-
node.declaration.type === 'FunctionDeclaration' &&
82-
node.declaration.async,
83-
)
71+
validateNonAsyncFunction(options, node.declaration)
8472
createExport(node, [node.declaration.id.name])
8573
} else if (node.declaration.type === 'VariableDeclaration') {
8674
/**
8775
* export const foo = 1, bar = 2
8876
*/
89-
validateNonAsyncFunction(
90-
node,
91-
node.declaration.declarations.every(
92-
(decl) =>
93-
decl.init?.type === 'ArrowFunctionExpression' &&
94-
decl.init.async,
95-
),
96-
)
77+
for (const decl of node.declaration.declarations) {
78+
if (decl.init) validateNonAsyncFunction(options, decl.init)
79+
}
9780
if (options.keep && options.code) {
9881
if (node.declaration.declarations.length === 1) {
9982
const decl = node.declaration.declarations[0]!
@@ -148,12 +131,7 @@ export function transformProxyExport(
148131
* export default () => {}
149132
*/
150133
if (node.type === 'ExportDefaultDeclaration') {
151-
validateNonAsyncFunction(
152-
node,
153-
node.declaration.type === 'Identifier' ||
154-
(node.declaration.type === 'FunctionDeclaration' &&
155-
node.declaration.async),
156-
)
134+
validateNonAsyncFunction(options, node.declaration)
157135
createExport(node, ['default'])
158136
continue
159137
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { parseAstAsync } from 'vite'
2+
import { describe, expect, test } from 'vitest'
3+
import { transformProxyExport } from './proxy-export'
4+
import { validateNonAsyncFunction } from './utils'
5+
import { transformWrapExport } from './wrap-export'
6+
7+
describe(validateNonAsyncFunction, () => {
8+
// next.js's validation isn't entirely consistent.
9+
// for now we aim to make it at least as forgiving as next.js.
10+
11+
const accepted = [
12+
`export async function f() {}`,
13+
`export default async function f() {}`,
14+
`export const fn = async function fn() {}`,
15+
`export const fn = async () => {}`,
16+
`export const fn = async () => {}, fn2 = x`,
17+
`export const fn = x`,
18+
`export const fn = x({ x: y })`,
19+
`export const fn = x(async () => {})`,
20+
`export default x`,
21+
`const y = x; export { y }`,
22+
`export const fn = x(() => {})`, // rejected by next.js
23+
`export const testAction = actionClient.action(async () => { return { message: "Hello, world!" }; });`,
24+
]
25+
26+
const rejected = [
27+
`export function f() {}`,
28+
`export default function f() {}`,
29+
`export const fn = function fn() {}`,
30+
`export const fn = () => {}`,
31+
`export const fn = x, fn2 = () => {}`,
32+
`export class Cls {}`,
33+
`export const Cls = class {}`,
34+
`export const Cls = class Foo {}`,
35+
]
36+
37+
test(transformWrapExport, async () => {
38+
const testTransform = async (input: string) => {
39+
const ast = await parseAstAsync(input)
40+
const result = transformWrapExport(input, ast, {
41+
runtime: (value, name) =>
42+
`$$wrap(${value}, "<id>", ${JSON.stringify(name)})`,
43+
ignoreExportAllDeclaration: true,
44+
rejectNonAsyncFunction: true,
45+
})
46+
return result.output.hasChanged()
47+
}
48+
49+
for (const code of accepted) {
50+
await expect.soft(testTransform(code)).resolves.toBe(true)
51+
}
52+
for (const code of rejected) {
53+
await expect
54+
.soft(testTransform(code))
55+
.rejects.toMatchInlineSnapshot(
56+
`[Error: unsupported non async function]`,
57+
)
58+
}
59+
})
60+
61+
test(transformProxyExport, async () => {
62+
const testTransform = async (input: string) => {
63+
const ast = await parseAstAsync(input)
64+
const result = transformProxyExport(ast, {
65+
code: input,
66+
rejectNonAsyncFunction: true,
67+
runtime: (name) => `$$proxy("<id>", ${JSON.stringify(name)})`,
68+
})
69+
return result.output.hasChanged()
70+
}
71+
72+
for (const code of accepted) {
73+
await expect.soft(testTransform(code)).resolves.toBe(true)
74+
}
75+
for (const code of rejected) {
76+
await expect
77+
.soft(testTransform(code))
78+
.rejects.toMatchInlineSnapshot(
79+
`[Error: unsupported non async function]`,
80+
)
81+
}
82+
})
83+
})

packages/plugin-rsc/src/transforms/utils.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { tinyassert } from '@hiogawa/utils'
2-
import type { Identifier, Pattern, Program } from 'estree'
2+
import type { ExportDefaultDeclaration } from 'estree'
3+
import type { Identifier, Node, Pattern, Program } from 'estree'
34

45
export function hasDirective(
56
body: Program['body'],
@@ -136,3 +137,23 @@ export function extractIdentifiers(
136137
}
137138
return nodes
138139
}
140+
141+
export function validateNonAsyncFunction(
142+
opts: { rejectNonAsyncFunction?: boolean },
143+
// export default function/class can be unnamed
144+
node: Node | ExportDefaultDeclaration['declaration'],
145+
): void {
146+
if (!opts.rejectNonAsyncFunction) return
147+
if (
148+
node.type === 'ClassDeclaration' ||
149+
node.type === 'ClassExpression' ||
150+
((node.type === 'FunctionDeclaration' ||
151+
node.type === 'FunctionExpression' ||
152+
node.type === 'ArrowFunctionExpression') &&
153+
!node.async)
154+
) {
155+
throw Object.assign(new Error(`unsupported non async function`), {
156+
pos: node.start,
157+
})
158+
}
159+
}

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

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -304,55 +304,4 @@ 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-
})
358307
})

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

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { tinyassert } from '@hiogawa/utils'
2-
import type { Node, Program } from 'estree'
2+
import type { Program } from 'estree'
33
import MagicString from 'magic-string'
4-
import { extractNames } from './utils'
4+
import { extractNames, validateNonAsyncFunction } from './utils'
55

66
type ExportMeta = {
77
declName?: string
@@ -83,21 +83,6 @@ export function transformWrapExport(
8383
)
8484
}
8585

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-
10186
for (const node of ast.body) {
10287
// named exports
10388
if (node.type === 'ExportNamedDeclaration') {
@@ -109,7 +94,7 @@ export function transformWrapExport(
10994
/**
11095
* export function foo() {}
11196
*/
112-
validateNonAsyncFunction(node.declaration)
97+
validateNonAsyncFunction(options, node.declaration)
11398
const name = node.declaration.id.name
11499
wrapSimple(node.start, node.declaration.start, [
115100
{ name, meta: { isFunction: true, declName: name } },
@@ -120,7 +105,7 @@ export function transformWrapExport(
120105
*/
121106
for (const decl of node.declaration.declarations) {
122107
if (decl.init) {
123-
validateNonAsyncFunction(decl.init)
108+
validateNonAsyncFunction(options, decl.init)
124109
}
125110
}
126111
if (node.declaration.kind === 'const') {
@@ -203,7 +188,7 @@ export function transformWrapExport(
203188
* export default () => {}
204189
*/
205190
if (node.type === 'ExportDefaultDeclaration') {
206-
validateNonAsyncFunction(node.declaration as Node)
191+
validateNonAsyncFunction(options, node.declaration)
207192
let localName: string
208193
let isFunction = false
209194
let declName: string | undefined

0 commit comments

Comments
 (0)