)
assertCode(content)
expect(bindings).toStrictEqual({
+ foo: 'props',
y: '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'
+ })
+ })
+})
Node,
Declaration,
ObjectPattern,
+ ObjectExpression,
ArrayPattern,
Identifier,
ExpressionStatement,
TSType,
TSTypeLiteral,
TSFunctionType,
- TSDeclareFunction
+ TSDeclareFunction,
+ ObjectProperty,
+ ArrayExpression,
+ Statement
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
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) {
// 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
}
}
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 {
}
// 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
}
// 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'
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 {
source: filename,
hires: true,
includeContent: true
- }) as unknown) as RawSourceMap
+ }) as unknown) as RawSourceMap,
+ scriptAst,
+ scriptSetupAst
}
}
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
}