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)
|
assertCode(content)
|
||||||
expect(bindings).toStrictEqual({
|
expect(bindings).toStrictEqual({
|
||||||
|
foo: 'props',
|
||||||
y: 'setup'
|
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,
|
Node,
|
||||||
Declaration,
|
Declaration,
|
||||||
ObjectPattern,
|
ObjectPattern,
|
||||||
|
ObjectExpression,
|
||||||
ArrayPattern,
|
ArrayPattern,
|
||||||
Identifier,
|
Identifier,
|
||||||
ExpressionStatement,
|
ExpressionStatement,
|
||||||
@ -16,7 +17,10 @@ import {
|
|||||||
TSType,
|
TSType,
|
||||||
TSTypeLiteral,
|
TSTypeLiteral,
|
||||||
TSFunctionType,
|
TSFunctionType,
|
||||||
TSDeclareFunction
|
TSDeclareFunction,
|
||||||
|
ObjectProperty,
|
||||||
|
ArrayExpression,
|
||||||
|
Statement
|
||||||
} from '@babel/types'
|
} from '@babel/types'
|
||||||
import { walk } from 'estree-walker'
|
import { walk } from 'estree-walker'
|
||||||
import { RawSourceMap } from 'source-map'
|
import { RawSourceMap } from 'source-map'
|
||||||
@ -56,11 +60,9 @@ export function compileScript(
|
|||||||
const scriptLang = script && script.lang
|
const scriptLang = script && script.lang
|
||||||
const scriptSetupLang = scriptSetup && scriptSetup.lang
|
const scriptSetupLang = scriptSetup && scriptSetup.lang
|
||||||
const isTS = scriptLang === 'ts' || scriptSetupLang === 'ts'
|
const isTS = scriptLang === 'ts' || scriptSetupLang === 'ts'
|
||||||
const plugins: ParserPlugin[] = [
|
const plugins: ParserPlugin[] = [...babelParserDefaultPlugins]
|
||||||
...(options.babelParserPlugins || []),
|
if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
|
||||||
...babelParserDefaultPlugins,
|
if (isTS) plugins.push('typescript')
|
||||||
...(isTS ? (['typescript'] as const) : [])
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!scriptSetup) {
|
if (!scriptSetup) {
|
||||||
if (!script) {
|
if (!script) {
|
||||||
@ -70,10 +72,15 @@ export function compileScript(
|
|||||||
// do not process non js/ts script blocks
|
// do not process non js/ts script blocks
|
||||||
return script
|
return script
|
||||||
}
|
}
|
||||||
|
const scriptAst = parse(script.content, {
|
||||||
|
plugins,
|
||||||
|
sourceType: 'module'
|
||||||
|
}).program.body
|
||||||
return {
|
return {
|
||||||
...script,
|
...script,
|
||||||
content: hasCssVars ? injectCssVarsCalls(sfc, plugins) : script.content,
|
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 scriptStartOffset = script && script.loc.start.offset
|
||||||
const scriptEndOffset = script && script.loc.end.offset
|
const scriptEndOffset = script && script.loc.end.offset
|
||||||
|
|
||||||
|
let scriptAst
|
||||||
|
|
||||||
// 1. process normal <script> first if it exists
|
// 1. process normal <script> first if it exists
|
||||||
if (script) {
|
if (script) {
|
||||||
// import dedupe between <script> and <script setup>
|
// import dedupe between <script> and <script setup>
|
||||||
const scriptAST = parse(script.content, {
|
scriptAst = parse(script.content, {
|
||||||
plugins,
|
plugins,
|
||||||
sourceType: 'module'
|
sourceType: 'module'
|
||||||
}).program.body
|
}).program.body
|
||||||
|
|
||||||
for (const node of scriptAST) {
|
for (const node of scriptAst) {
|
||||||
if (node.type === 'ImportDeclaration') {
|
if (node.type === 'ImportDeclaration') {
|
||||||
// record imports for dedupe
|
// record imports for dedupe
|
||||||
for (const {
|
for (const {
|
||||||
@ -238,14 +247,16 @@ 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, {
|
const scriptSetupAst = parse(scriptSetup.content, {
|
||||||
plugins: [
|
plugins: [
|
||||||
...plugins,
|
...plugins,
|
||||||
// allow top level await but only inside <script setup>
|
// allow top level await but only inside <script setup>
|
||||||
'topLevelAwait'
|
'topLevelAwait'
|
||||||
],
|
],
|
||||||
sourceType: 'module'
|
sourceType: 'module'
|
||||||
}).program.body) {
|
}).program.body
|
||||||
|
|
||||||
|
for (const node of scriptSetupAst) {
|
||||||
const start = node.start! + startOffset
|
const start = node.start! + startOffset
|
||||||
let end = node.end! + startOffset
|
let end = node.end! + startOffset
|
||||||
// import or type declarations: move to top
|
// import or type declarations: move to top
|
||||||
@ -595,8 +606,8 @@ export function compileScript(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 8. expose bindings for template compiler optimization
|
// 8. expose bindings for template compiler optimization
|
||||||
if (script) {
|
if (scriptAst) {
|
||||||
Object.assign(bindings, analyzeScriptBindings(script))
|
Object.assign(bindings, analyzeScriptBindings(scriptAst))
|
||||||
}
|
}
|
||||||
Object.keys(setupExports).forEach(key => {
|
Object.keys(setupExports).forEach(key => {
|
||||||
bindings[key] = 'setup'
|
bindings[key] = 'setup'
|
||||||
@ -604,8 +615,7 @@ export function compileScript(
|
|||||||
Object.keys(typeDeclaredProps).forEach(key => {
|
Object.keys(typeDeclaredProps).forEach(key => {
|
||||||
bindings[key] = 'props'
|
bindings[key] = 'props'
|
||||||
})
|
})
|
||||||
// TODO analyze props if user declared props via `export default {}` inside
|
Object.assign(bindings, analyzeScriptBindings(scriptSetupAst))
|
||||||
// <script setup>
|
|
||||||
|
|
||||||
s.trim()
|
s.trim()
|
||||||
return {
|
return {
|
||||||
@ -616,7 +626,9 @@ export function compileScript(
|
|||||||
source: filename,
|
source: filename,
|
||||||
hires: true,
|
hires: true,
|
||||||
includeContent: 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)
|
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>`
|
* 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
|
||||||
* compilation process so this should only be used on single `<script>` SFCs.
|
* compilation process so this should only be used on single `<script>` SFCs.
|
||||||
*/
|
*/
|
||||||
export function analyzeScriptBindings(
|
function analyzeScriptBindings(ast: Statement[]): BindingMetadata {
|
||||||
_script: SFCScriptBlock
|
const bindings: BindingMetadata = {}
|
||||||
): BindingMetadata {
|
|
||||||
return {
|
for (const node of ast) {
|
||||||
// TODO
|
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 { parse } from './parse'
|
||||||
export { compileTemplate } from './compileTemplate'
|
export { compileTemplate } from './compileTemplate'
|
||||||
export { compileStyle, compileStyleAsync } from './compileStyle'
|
export { compileStyle, compileStyleAsync } from './compileStyle'
|
||||||
export { compileScript, analyzeScriptBindings } from './compileScript'
|
export { compileScript } from './compileScript'
|
||||||
export { rewriteDefault } from './rewriteDefault'
|
export { rewriteDefault } from './rewriteDefault'
|
||||||
export { generateCodeFrame } from '@vue/compiler-core'
|
export { generateCodeFrame } from '@vue/compiler-core'
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
import * as CompilerDOM from '@vue/compiler-dom'
|
import * as CompilerDOM from '@vue/compiler-dom'
|
||||||
import { RawSourceMap, SourceMapGenerator } from 'source-map'
|
import { RawSourceMap, SourceMapGenerator } from 'source-map'
|
||||||
import { TemplateCompiler } from './compileTemplate'
|
import { TemplateCompiler } from './compileTemplate'
|
||||||
|
import { Statement } from '@babel/types'
|
||||||
|
|
||||||
export interface SFCParseOptions {
|
export interface SFCParseOptions {
|
||||||
filename?: string
|
filename?: string
|
||||||
@ -37,6 +38,8 @@ export interface SFCScriptBlock extends SFCBlock {
|
|||||||
type: 'script'
|
type: 'script'
|
||||||
setup?: string | boolean
|
setup?: string | boolean
|
||||||
bindings?: BindingMetadata
|
bindings?: BindingMetadata
|
||||||
|
scriptAst?: Statement[]
|
||||||
|
scriptSetupAst?: Statement[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SFCStyleBlock extends SFCBlock {
|
export interface SFCStyleBlock extends SFCBlock {
|
||||||
|
Loading…
Reference in New Issue
Block a user