feat(compiler-sfc): analyze script bindings (#1962)

Also expose `scriptAst` and `scriptSetupAst` on returned script block
This commit is contained in:
Stanislav Lashmanov 2020-08-28 23:21:03 +03:00 committed by GitHub
parent 94d94bafc5
commit 4421c00903
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 337 additions and 22 deletions

View File

@ -190,6 +190,7 @@ describe('SFC compile <script setup>', () => {
)
assertCode(content)
expect(bindings).toStrictEqual({
foo: 'props',
y: 'setup'
})
})
@ -517,3 +518,197 @@ describe('SFC compile <script setup>', () => {
})
})
})
describe('SFC analyze <script> bindings', () => {
it('recognizes props array declaration', () => {
const { bindings } = compile(`
<script>
export default {
props: ['foo', 'bar']
}
</script>
`)
expect(bindings).toStrictEqual({ foo: 'props', bar: 'props' })
})
it('recognizes props object declaration', () => {
const { bindings } = compile(`
<script>
export default {
props: {
foo: String,
bar: {
type: String,
},
baz: null,
qux: [String, Number]
}
}
</script>
`)
expect(bindings).toStrictEqual({
foo: 'props',
bar: 'props',
baz: 'props',
qux: 'props'
})
})
it('recognizes setup return', () => {
const { bindings } = compile(`
<script>
const bar = 2
export default {
setup() {
return {
foo: 1,
bar
}
}
}
</script>
`)
expect(bindings).toStrictEqual({ foo: 'setup', bar: 'setup' })
})
it('recognizes async setup return', () => {
const { bindings } = compile(`
<script>
const bar = 2
export default {
async setup() {
return {
foo: 1,
bar
}
}
}
</script>
`)
expect(bindings).toStrictEqual({ foo: 'setup', bar: 'setup' })
})
it('recognizes data return', () => {
const { bindings } = compile(`
<script>
const bar = 2
export default {
data() {
return {
foo: null,
bar
}
}
}
</script>
`)
expect(bindings).toStrictEqual({ foo: 'data', bar: 'data' })
})
it('recognizes methods', () => {
const { bindings } = compile(`
<script>
export default {
methods: {
foo() {}
}
}
</script>
`)
expect(bindings).toStrictEqual({ foo: 'options' })
})
it('recognizes computeds', () => {
const { bindings } = compile(`
<script>
export default {
computed: {
foo() {},
bar: {
get() {},
set() {},
}
}
}
</script>
`)
expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' })
})
it('recognizes injections array declaration', () => {
const { bindings } = compile(`
<script>
export default {
inject: ['foo', 'bar']
}
</script>
`)
expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' })
})
it('recognizes injections object declaration', () => {
const { bindings } = compile(`
<script>
export default {
inject: {
foo: {},
bar: {},
}
}
</script>
`)
expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' })
})
it('works for mixed bindings', () => {
const { bindings } = compile(`
<script>
export default {
inject: ['foo'],
props: {
bar: String,
},
setup() {
return {
baz: null,
}
},
data() {
return {
qux: null
}
},
methods: {
quux() {}
},
computed: {
quuz() {}
}
}
</script>
`)
expect(bindings).toStrictEqual({
foo: 'options',
bar: 'props',
baz: 'setup',
qux: 'data',
quux: 'options',
quuz: 'options'
})
})
it('works for script setup', () => {
const { bindings } = compile(`
<script setup>
export default {
props: {
foo: String,
},
}
</script>
`)
expect(bindings).toStrictEqual({
foo: 'props'
})
})
})

View File

@ -7,6 +7,7 @@ import {
Node,
Declaration,
ObjectPattern,
ObjectExpression,
ArrayPattern,
Identifier,
ExpressionStatement,
@ -16,7 +17,10 @@ import {
TSType,
TSTypeLiteral,
TSFunctionType,
TSDeclareFunction
TSDeclareFunction,
ObjectProperty,
ArrayExpression,
Statement
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
@ -56,11 +60,9 @@ export function compileScript(
const scriptLang = script && script.lang
const scriptSetupLang = scriptSetup && scriptSetup.lang
const isTS = scriptLang === 'ts' || scriptSetupLang === 'ts'
const plugins: ParserPlugin[] = [
...(options.babelParserPlugins || []),
...babelParserDefaultPlugins,
...(isTS ? (['typescript'] as const) : [])
]
const plugins: ParserPlugin[] = [...babelParserDefaultPlugins]
if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
if (isTS) plugins.push('typescript')
if (!scriptSetup) {
if (!script) {
@ -70,10 +72,15 @@ export function compileScript(
// do not process non js/ts script blocks
return script
}
const scriptAst = parse(script.content, {
plugins,
sourceType: 'module'
}).program.body
return {
...script,
content: hasCssVars ? injectCssVarsCalls(sfc, plugins) : script.content,
bindings: analyzeScriptBindings(script)
bindings: analyzeScriptBindings(scriptAst),
scriptAst
}
}
@ -118,15 +125,17 @@ export function compileScript(
const scriptStartOffset = script && script.loc.start.offset
const scriptEndOffset = script && script.loc.end.offset
let scriptAst
// 1. process normal <script> first if it exists
if (script) {
// import dedupe between <script> and <script setup>
const scriptAST = parse(script.content, {
scriptAst = parse(script.content, {
plugins,
sourceType: 'module'
}).program.body
for (const node of scriptAST) {
for (const node of scriptAst) {
if (node.type === 'ImportDeclaration') {
// record imports for dedupe
for (const {
@ -238,14 +247,16 @@ export function compileScript(
}
// 3. parse <script setup> and walk over top level statements
for (const node of parse(scriptSetup.content, {
const scriptSetupAst = parse(scriptSetup.content, {
plugins: [
...plugins,
// allow top level await but only inside <script setup>
'topLevelAwait'
],
sourceType: 'module'
}).program.body) {
}).program.body
for (const node of scriptSetupAst) {
const start = node.start! + startOffset
let end = node.end! + startOffset
// import or type declarations: move to top
@ -595,8 +606,8 @@ export function compileScript(
}
// 8. expose bindings for template compiler optimization
if (script) {
Object.assign(bindings, analyzeScriptBindings(script))
if (scriptAst) {
Object.assign(bindings, analyzeScriptBindings(scriptAst))
}
Object.keys(setupExports).forEach(key => {
bindings[key] = 'setup'
@ -604,8 +615,7 @@ export function compileScript(
Object.keys(typeDeclaredProps).forEach(key => {
bindings[key] = 'props'
})
// TODO analyze props if user declared props via `export default {}` inside
// <script setup>
Object.assign(bindings, analyzeScriptBindings(scriptSetupAst))
s.trim()
return {
@ -616,7 +626,9 @@ export function compileScript(
source: filename,
hires: true,
includeContent: true
}) as unknown) as RawSourceMap
}) as unknown) as RawSourceMap,
scriptAst,
scriptSetupAst
}
}
@ -969,15 +981,120 @@ function isFunction(node: Node): node is FunctionNode {
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
}
function getObjectExpressionKeys(node: ObjectExpression): string[] {
const keys = []
for (const prop of node.properties) {
if (
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
!prop.computed
) {
if (prop.key.type === 'Identifier') {
keys.push(prop.key.name)
} else if (prop.key.type === 'StringLiteral') {
keys.push(prop.key.value)
}
}
}
return keys
}
function getArrayExpressionKeys(node: ArrayExpression): string[] {
const keys = []
for (const element of node.elements) {
if (element && element.type === 'StringLiteral') {
keys.push(element.value)
}
}
return keys
}
function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] {
if (property.value.type === 'ArrayExpression') {
return getArrayExpressionKeys(property.value)
}
if (property.value.type === 'ObjectExpression') {
return getObjectExpressionKeys(property.value)
}
return []
}
/**
* Analyze bindings in normal `<script>`
* Note that `compileScriptSetup` already analyzes bindings as part of its
* compilation process so this should only be used on single `<script>` SFCs.
*/
export function analyzeScriptBindings(
_script: SFCScriptBlock
): BindingMetadata {
return {
// TODO
function analyzeScriptBindings(ast: Statement[]): BindingMetadata {
const bindings: BindingMetadata = {}
for (const node of ast) {
if (
node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression'
) {
for (const property of node.declaration.properties) {
if (
property.type === 'ObjectProperty' &&
!property.computed &&
property.key.type === 'Identifier'
) {
// props
if (property.key.name === 'props') {
// props: ['foo']
// props: { foo: ... }
for (const key of getObjectOrArrayExpressionKeys(property)) {
bindings[key] = 'props'
}
}
// inject
else if (property.key.name === 'inject') {
// inject: ['foo']
// inject: { foo: {} }
for (const key of getObjectOrArrayExpressionKeys(property)) {
bindings[key] = 'options'
}
}
// computed & methods
else if (
property.value.type === 'ObjectExpression' &&
(property.key.name === 'computed' ||
property.key.name === 'methods')
) {
// methods: { foo() {} }
// computed: { foo() {} }
for (const key of getObjectExpressionKeys(property.value)) {
bindings[key] = 'options'
}
}
}
// setup & data
else if (
property.type === 'ObjectMethod' &&
property.key.type === 'Identifier' &&
(property.key.name === 'setup' || property.key.name === 'data')
) {
for (const bodyItem of property.body.body) {
// setup() {
// return {
// foo: null
// }
// }
if (
bodyItem.type === 'ReturnStatement' &&
bodyItem.argument &&
bodyItem.argument.type === 'ObjectExpression'
) {
for (const key of getObjectExpressionKeys(bodyItem.argument)) {
bindings[key] = property.key.name
}
}
}
}
}
}
}
return bindings
}

View File

@ -2,7 +2,7 @@
export { parse } from './parse'
export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle'
export { compileScript, analyzeScriptBindings } from './compileScript'
export { compileScript } from './compileScript'
export { rewriteDefault } from './rewriteDefault'
export { generateCodeFrame } from '@vue/compiler-core'

View File

@ -9,6 +9,7 @@ import {
import * as CompilerDOM from '@vue/compiler-dom'
import { RawSourceMap, SourceMapGenerator } from 'source-map'
import { TemplateCompiler } from './compileTemplate'
import { Statement } from '@babel/types'
export interface SFCParseOptions {
filename?: string
@ -37,6 +38,8 @@ export interface SFCScriptBlock extends SFCBlock {
type: 'script'
setup?: string | boolean
bindings?: BindingMetadata
scriptAst?: Statement[]
scriptSetupAst?: Statement[]
}
export interface SFCStyleBlock extends SFCBlock {