feat(compiler-sfc): <script setup> support (experimental)

This is the last commit for the feature which adds async/await detection.
This commit is contained in:
Evan You 2020-07-10 18:00:13 -04:00
parent 73bfce3706
commit 4c43d4e5b9
3 changed files with 80 additions and 14 deletions

View File

@ -263,8 +263,9 @@ export function processExpression(
return ret return ret
} }
const isFunction = (node: Node): node is Function => const isFunction = (node: Node): node is Function => {
/Function(Expression|Declaration)$/.test(node.type) return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
}
const isStaticProperty = (node: Node): node is ObjectProperty => const isStaticProperty = (node: Node): node is ObjectProperty =>
node && node &&

View File

@ -49,10 +49,6 @@ describe('SFC compile <script setup>', () => {
) )
}) })
test('async/await detection', () => {
// TODO
})
describe('exports', () => { describe('exports', () => {
test('export const x = ...', () => { test('export const x = ...', () => {
const { content, bindings } = compile( const { content, bindings } = compile(
@ -333,6 +329,45 @@ describe('SFC compile <script setup>', () => {
}) })
}) })
describe('async/await detection', () => {
function assertAwaitDetection(code: string, shouldAsync = true) {
const { content } = compile(`<script setup>${code}</script>`)
expect(content).toMatch(
`export ${shouldAsync ? `async ` : ``}function setup`
)
}
test('expression statement', () => {
assertAwaitDetection(`await foo`)
})
test('variable', () => {
assertAwaitDetection(`const a = 1 + (await foo)`)
})
test('export', () => {
assertAwaitDetection(`export const a = 1 + (await foo)`)
})
test('nested statements', () => {
assertAwaitDetection(`if (ok) { await foo } else { await bar }`)
})
test('should ignore await inside functions', () => {
// function declaration
assertAwaitDetection(`export async function foo() { await bar }`, false)
// function expression
assertAwaitDetection(`const foo = async () => { await bar }`, false)
// object method
assertAwaitDetection(`const obj = { async method() { await bar }}`, false)
// class method
assertAwaitDetection(
`const cls = class Foo { async method() { await bar }}`,
false
)
})
})
describe('errors', () => { describe('errors', () => {
test('<script> and <script setup> must have same lang', () => { test('<script> and <script setup> must have same lang', () => {
expect( expect(

View File

@ -11,6 +11,7 @@ import {
ExpressionStatement, ExpressionStatement,
ArrowFunctionExpression, ArrowFunctionExpression,
ExportSpecifier, ExportSpecifier,
Function as FunctionNode,
TSType, TSType,
TSTypeLiteral, TSTypeLiteral,
TSFunctionType, TSFunctionType,
@ -87,7 +88,8 @@ export function compileScript(
const setupExports: Record<string, boolean> = {} const setupExports: Record<string, boolean> = {}
let exportAllIndex = 0 let exportAllIndex = 0
let defaultExport: Node | undefined let defaultExport: Node | undefined
let needDefaultExportRefCheck: boolean = false let needDefaultExportRefCheck = false
let hasAwait = false
const checkDuplicateDefaultExport = (node: Node) => { const checkDuplicateDefaultExport = (node: Node) => {
if (defaultExport) { if (defaultExport) {
@ -230,7 +232,11 @@ export function compileScript(
// 3. parse <script setup> and walk over top level statements // 3. parse <script setup> and walk over top level statements
for (const node of parse(scriptSetup.content, { for (const node of parse(scriptSetup.content, {
plugins, plugins: [
...plugins,
// allow top level await but only inside <script setup>
'topLevelAwait'
],
sourceType: 'module' sourceType: 'module'
}).program.body) { }).program.body) {
const start = node.start! + startOffset const start = node.start! + startOffset
@ -439,6 +445,27 @@ export function compileScript(
recordType(node, declaredTypes) recordType(node, declaredTypes)
s.move(start, end, 0) s.move(start, end, 0)
} }
// walk statements & named exports / variable declarations for top level
// await
if (
node.type === 'VariableDeclaration' ||
(node.type === 'ExportNamedDeclaration' &&
node.declaration &&
node.declaration.type === 'VariableDeclaration') ||
node.type.endsWith('Statement')
) {
;(walk as any)(node, {
enter(node: Node) {
if (isFunction(node)) {
this.skip()
}
if (node.type === 'AwaitExpression') {
hasAwait = true
}
}
})
}
} }
// 4. check default export to make sure it doesn't reference setup scope // 4. check default export to make sure it doesn't reference setup scope
@ -503,7 +530,10 @@ export function compileScript(
// 6. wrap setup code with function. // 6. wrap setup code with function.
// export the content of <script setup> as a named export, `setup`. // export the content of <script setup> as a named export, `setup`.
// this allows `import { setup } from '*.vue'` for testing purposes. // this allows `import { setup } from '*.vue'` for testing purposes.
s.prependLeft(startOffset, `\nexport function setup(${args}) {\n`) s.prependLeft(
startOffset,
`\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n`
)
// generate return statement // generate return statement
let returned = `{ ${Object.keys(setupExports).join(', ')} }` let returned = `{ ${Object.keys(setupExports).join(', ')} }`
@ -867,11 +897,7 @@ function checkDefaultExport(
) )
) )
} }
} else if ( } else if (isFunction(node)) {
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
// walk function expressions and add its arguments to known identifiers // walk function expressions and add its arguments to known identifiers
// so that we don't prefix them // so that we don't prefix them
node.params.forEach(p => node.params.forEach(p =>
@ -927,6 +953,10 @@ function isStaticPropertyKey(node: Node, parent: Node): boolean {
) )
} }
function isFunction(node: Node): node is FunctionNode {
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
}
/** /**
* Analyze bindings in normal `<script>` * Analyze bindings in normal `<script>`
* Note that `compileScriptSetup` already analyzes bindings as part of its * Note that `compileScriptSetup` already analyzes bindings as part of its