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
4 changed files with 337 additions and 22 deletions

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 {