Skip to content

Commit 4c43d4e

Browse files
committed
feat(compiler-sfc): <script setup> support (experimental)
This is the last commit for the feature which adds async/await detection.
1 parent 73bfce3 commit 4c43d4e

File tree

3 files changed

+80
-14
lines changed

3 files changed

+80
-14
lines changed

packages/compiler-core/src/transforms/transformExpression.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,9 @@ export function processExpression(
263263
return ret
264264
}
265265

266-
const isFunction = (node: Node): node is Function =>
267-
/Function(Expression|Declaration)$/.test(node.type)
266+
const isFunction = (node: Node): node is Function => {
267+
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
268+
}
268269

269270
const isStaticProperty = (node: Node): node is ObjectProperty =>
270271
node &&

packages/compiler-sfc/__tests__/compileScript.spec.ts

+39-4
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ describe('SFC compile <script setup>', () => {
4949
)
5050
})
5151

52-
test('async/await detection', () => {
53-
// TODO
54-
})
55-
5652
describe('exports', () => {
5753
test('export const x = ...', () => {
5854
const { content, bindings } = compile(
@@ -333,6 +329,45 @@ describe('SFC compile <script setup>', () => {
333329
})
334330
})
335331

332+
describe('async/await detection', () => {
333+
function assertAwaitDetection(code: string, shouldAsync = true) {
334+
const { content } = compile(`<script setup>${code}</script>`)
335+
expect(content).toMatch(
336+
`export ${shouldAsync ? `async ` : ``}function setup`
337+
)
338+
}
339+
340+
test('expression statement', () => {
341+
assertAwaitDetection(`await foo`)
342+
})
343+
344+
test('variable', () => {
345+
assertAwaitDetection(`const a = 1 + (await foo)`)
346+
})
347+
348+
test('export', () => {
349+
assertAwaitDetection(`export const a = 1 + (await foo)`)
350+
})
351+
352+
test('nested statements', () => {
353+
assertAwaitDetection(`if (ok) { await foo } else { await bar }`)
354+
})
355+
356+
test('should ignore await inside functions', () => {
357+
// function declaration
358+
assertAwaitDetection(`export async function foo() { await bar }`, false)
359+
// function expression
360+
assertAwaitDetection(`const foo = async () => { await bar }`, false)
361+
// object method
362+
assertAwaitDetection(`const obj = { async method() { await bar }}`, false)
363+
// class method
364+
assertAwaitDetection(
365+
`const cls = class Foo { async method() { await bar }}`,
366+
false
367+
)
368+
})
369+
})
370+
336371
describe('errors', () => {
337372
test('<script> and <script setup> must have same lang', () => {
338373
expect(

packages/compiler-sfc/src/compileScript.ts

+38-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ExpressionStatement,
1212
ArrowFunctionExpression,
1313
ExportSpecifier,
14+
Function as FunctionNode,
1415
TSType,
1516
TSTypeLiteral,
1617
TSFunctionType,
@@ -87,7 +88,8 @@ export function compileScript(
8788
const setupExports: Record<string, boolean> = {}
8889
let exportAllIndex = 0
8990
let defaultExport: Node | undefined
90-
let needDefaultExportRefCheck: boolean = false
91+
let needDefaultExportRefCheck = false
92+
let hasAwait = false
9193

9294
const checkDuplicateDefaultExport = (node: Node) => {
9395
if (defaultExport) {
@@ -230,7 +232,11 @@ export function compileScript(
230232

231233
// 3. parse <script setup> and walk over top level statements
232234
for (const node of parse(scriptSetup.content, {
233-
plugins,
235+
plugins: [
236+
...plugins,
237+
// allow top level await but only inside <script setup>
238+
'topLevelAwait'
239+
],
234240
sourceType: 'module'
235241
}).program.body) {
236242
const start = node.start! + startOffset
@@ -439,6 +445,27 @@ export function compileScript(
439445
recordType(node, declaredTypes)
440446
s.move(start, end, 0)
441447
}
448+
449+
// walk statements & named exports / variable declarations for top level
450+
// await
451+
if (
452+
node.type === 'VariableDeclaration' ||
453+
(node.type === 'ExportNamedDeclaration' &&
454+
node.declaration &&
455+
node.declaration.type === 'VariableDeclaration') ||
456+
node.type.endsWith('Statement')
457+
) {
458+
;(walk as any)(node, {
459+
enter(node: Node) {
460+
if (isFunction(node)) {
461+
this.skip()
462+
}
463+
if (node.type === 'AwaitExpression') {
464+
hasAwait = true
465+
}
466+
}
467+
})
468+
}
442469
}
443470

444471
// 4. check default export to make sure it doesn't reference setup scope
@@ -503,7 +530,10 @@ export function compileScript(
503530
// 6. wrap setup code with function.
504531
// export the content of <script setup> as a named export, `setup`.
505532
// this allows `import { setup } from '*.vue'` for testing purposes.
506-
s.prependLeft(startOffset, `\nexport function setup(${args}) {\n`)
533+
s.prependLeft(
534+
startOffset,
535+
`\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n`
536+
)
507537

508538
// generate return statement
509539
let returned = `{ ${Object.keys(setupExports).join(', ')} }`
@@ -867,11 +897,7 @@ function checkDefaultExport(
867897
)
868898
)
869899
}
870-
} else if (
871-
node.type === 'FunctionDeclaration' ||
872-
node.type === 'FunctionExpression' ||
873-
node.type === 'ArrowFunctionExpression'
874-
) {
900+
} else if (isFunction(node)) {
875901
// walk function expressions and add its arguments to known identifiers
876902
// so that we don't prefix them
877903
node.params.forEach(p =>
@@ -927,6 +953,10 @@ function isStaticPropertyKey(node: Node, parent: Node): boolean {
927953
)
928954
}
929955

956+
function isFunction(node: Node): node is FunctionNode {
957+
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
958+
}
959+
930960
/**
931961
* Analyze bindings in normal `<script>`
932962
* Note that `compileScriptSetup` already analyzes bindings as part of its

0 commit comments

Comments
 (0)