]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): analyze script bindings (#1962)
authorStanislav Lashmanov <stasvarenkin@gmail.com>
Fri, 28 Aug 2020 20:21:03 +0000 (23:21 +0300)
committerGitHub <noreply@github.com>
Fri, 28 Aug 2020 20:21:03 +0000 (16:21 -0400)
Also expose `scriptAst` and `scriptSetupAst` on returned script block

packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/index.ts
packages/compiler-sfc/src/parse.ts

index 01f0d32899b49dd921e89348c04772a93131be55..11f739752073c43ffdb61e9202f73d98162a33b3 100644 (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'
+    })
+  })
+})
index a8e378d0f71f7796b9e255a1b5983999199b63dc..1eec51604adc4ee045b026bbaea1506bcf68df02 100644 (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
 }
index 6bbe14b314e97e3c1dde6aa460ee6c4c89c59362..2015dcb6d9c21a2ff287de52d9c56121e0669048 100644 (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'
 
index 93e105ad769d8358665c0af2db755a3c2503a2eb..9780e350ec4781d4f824abbd67a819c41ae2e937 100644 (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 {