feat(compiler-sfc): analyze script bindings (#1962)
Also expose `scriptAst` and `scriptSetupAst` on returned script block
This commit is contained in:
parent
94d94bafc5
commit
4421c00903
@ -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'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user