]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: compileScriptSetup full js support
authorEvan You <yyx990803@gmail.com>
Tue, 7 Jul 2020 21:54:01 +0000 (17:54 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 9 Jul 2020 16:17:28 +0000 (12:17 -0400)
packages/compiler-core/package.json
packages/compiler-sfc/package.json
packages/compiler-sfc/src/compileScript.ts
yarn.lock

index 91a86806bb79c844d97e9b7acc7c16023101d403..7f74436742935a52decb0fc3cd5b04968e638556 100644 (file)
@@ -33,7 +33,7 @@
     "@vue/shared": "3.0.0-beta.20",
     "@babel/parser": "^7.10.4",
     "@babel/types": "^7.10.4",
-    "estree-walker": "^0.8.1",
+    "estree-walker": "^2.0.1",
     "source-map": "^0.6.1"
   }
 }
index fa9d8e27c20a66ff0da13ec9c0337600d252b893..980ba41ff7fd8b2c86bb723e89223debf242b665 100644 (file)
@@ -40,6 +40,7 @@
     "@vue/compiler-ssr": "3.0.0-beta.20",
     "@vue/shared": "3.0.0-beta.20",
     "consolidate": "^0.15.1",
+    "estree-walker": "^2.0.1",
     "hash-sum": "^2.0.0",
     "lru-cache": "^5.1.1",
     "magic-string": "^0.25.7",
index 4cfe5361a4fe13d5e64003d30410f21db40673fa..5305709f50b087823518e4f1d5636e96118cf3d9 100644 (file)
@@ -1,8 +1,20 @@
 import MagicString, { SourceMap } from 'magic-string'
 import { SFCDescriptor, SFCScriptBlock } from './parse'
 import { parse, ParserPlugin } from '@babel/parser'
-import { babelParserDefautPlugins } from '@vue/shared'
-import { ObjectPattern, ArrayPattern } from '@babel/types'
+import { babelParserDefautPlugins, generateCodeFrame } from '@vue/shared'
+import {
+  Node,
+  Declaration,
+  ObjectPattern,
+  ArrayPattern,
+  Identifier,
+  ExpressionStatement,
+  ArrowFunctionExpression,
+  TSTypeLiteral,
+  TSFunctionType,
+  TSDeclareFunction
+} from '@babel/types'
+import { walk } from 'estree-walker'
 
 export interface BindingMetadata {
   [key: string]: 'data' | 'props' | 'setup' | 'ctx'
@@ -33,12 +45,18 @@ export function compileScriptSetup(
   }
 
   const bindings: BindingMetadata = {}
-  const setupExports: string[] = []
+  const imports: Record<string, boolean> = {}
+  const setupScopeVars: Record<string, boolean> = {}
+  const setupExports: Record<string, boolean> = {}
   let exportAllIndex = 0
+  let defaultExport: Node | undefined
+  let needDefaultExportCheck: boolean = false
 
   const s = new MagicString(source)
   const startOffset = scriptSetup.loc.start.offset
   const endOffset = scriptSetup.loc.end.offset
+  const scriptStartOffset = script && script.loc.start.offset
+  const scriptEndOffset = script && script.loc.end.offset
 
   // parse and transform <script setup>
   const plugins: ParserPlugin[] = [
@@ -49,12 +67,85 @@ export function compileScriptSetup(
     plugins.push('typescript')
   }
 
-  const ast = parse(scriptSetup.content, {
+  // process normal <script> first if it exists
+  if (script) {
+    // import dedupe between <script> and <script setup>
+    const scriptAST = parse(script.content, {
+      plugins,
+      sourceType: 'module'
+    }).program.body
+
+    for (const node of scriptAST) {
+      if (node.type === 'ImportDeclaration') {
+        // record imports for dedupe
+        for (const {
+          local: { name }
+        } of node.specifiers) {
+          imports[name] = true
+        }
+      } else if (node.type === 'ExportDefaultDeclaration') {
+        // export default
+        defaultExport = node
+        const start = node.start! + scriptStartOffset!
+        s.overwrite(
+          start,
+          start + `export default`.length,
+          `const __default__ =`
+        )
+      } else if (
+        node.type === 'ExportNamedDeclaration' &&
+        node.specifiers &&
+        node.specifiers.some(s => s.exported.name === 'default')
+      ) {
+        defaultExport = node
+        if (node.source) {
+          // export { x as default } from './x'
+        } else {
+          // export { x as default }
+        }
+      }
+    }
+  }
+
+  // check <script setup="xxx"> function signature
+  let propsVar = `$props`
+  let emitVar = `$emit`
+  let args = `${propsVar}, { emit: ${emitVar}, attrs: $attrs, slots: $slots }`
+  if (typeof scriptSetup.setup === 'string') {
+    // <script setup="xxx" lang="ts">
+    // parse the signature to extract the props/emit variables the user wants
+    // we need them to find corresponding type declarations.
+    if (scriptSetup.lang === 'ts') {
+      const signatureAST = parse(`(${scriptSetup.setup})=>{}`, { plugins })
+        .program.body[0]
+      const params = ((signatureAST as ExpressionStatement)
+        .expression as ArrowFunctionExpression).params
+      if (params[0] && params[0].type === 'Identifier') {
+        propsVar = params[0].name
+      }
+      if (params[1] && params[1].type === 'ObjectPattern') {
+        for (const p of params[1].properties) {
+          if (
+            p.type === 'ObjectProperty' &&
+            p.key.type === 'Identifier' &&
+            p.key.name === 'emit' &&
+            p.value.type === 'Identifier'
+          ) {
+            emitVar = p.value.name
+          }
+        }
+      }
+    }
+    args = scriptSetup.setup
+  }
+
+  const scriptSetupAST = parse(scriptSetup.content, {
     plugins,
     sourceType: 'module'
   }).program.body
 
-  for (const node of ast) {
+  // walk over top level statements
+  for (const node of scriptSetupAST) {
     const start = node.start! + startOffset
     let end = node.end! + startOffset
     // import or type declarations: move to top
@@ -65,49 +156,41 @@ export function compileScriptSetup(
       }
       end++
     }
+
     if (node.type === 'ImportDeclaration') {
+      // import declarations are moved to top
       s.move(start, end, 0)
+      // dedupe imports
+      let prev
+      let removed = 0
+      for (const specifier of node.specifiers) {
+        if (imports[specifier.local.name]) {
+          // already imported in <script setup>, dedupe
+          removed++
+          s.remove(
+            prev ? prev.end! + startOffset : specifier.start! + startOffset,
+            specifier.end! + startOffset
+          )
+        } else {
+          imports[specifier.local.name] = true
+        }
+        prev = specifier
+      }
+      if (removed === node.specifiers.length) {
+        s.remove(node.start! + startOffset, node.end! + startOffset)
+      }
     }
+
     if (node.type === 'ExportNamedDeclaration') {
       // named exports
       if (node.declaration) {
         // variable/function/class declarations.
         // remove leading `export ` keyword
         s.remove(start, start + 7)
-        if (node.declaration.type === 'VariableDeclaration') {
-          // export const foo = ...
-          // export declarations can only have one declaration at a time
-          const id = node.declaration.declarations[0].id
-          if (id.type === 'Identifier') {
-            setupExports.push(id.name)
-          } else if (id.type === 'ObjectPattern') {
-            walkObjectPattern(id, setupExports)
-          } else if (id.type === 'ArrayPattern') {
-            walkArrayPattern(id, setupExports)
-          }
-        } else if (
-          node.declaration.type === 'FunctionDeclaration' ||
-          node.declaration.type === 'ClassDeclaration'
-        ) {
-          // export function foo() {} / export class Foo {}
-          // export declarations must be named.
-          setupExports.push(node.declaration.id!.name)
-        }
+        walkDeclaration(node.declaration, setupExports, propsVar, emitVar)
       }
       if (node.specifiers.length) {
-        for (const { exported } of node.specifiers) {
-          if (exported.name === 'default') {
-            // TODO
-            // check duplicated default export
-            // walk export default to make sure it does not reference exported
-            // variables
-            throw new Error(
-              'export default in <script setup> not supported yet'
-            )
-          } else {
-            setupExports.push(exported.name)
-          }
-        }
+        // named export with specifiers
         if (node.source) {
           // export { x } from './x'
           // change it to import and move to top
@@ -117,8 +200,46 @@ export function compileScriptSetup(
           // export { x }
           s.remove(start, end)
         }
+        for (const specifier of node.specifiers) {
+          if (specifier.type == 'ExportDefaultSpecifier') {
+            // export default from './x'
+            // rewrite to `import __default__ from './x'`
+            defaultExport = node
+            s.overwrite(
+              specifier.exported.start! + startOffset,
+              specifier.exported.start! + startOffset + 7,
+              '__default__'
+            )
+          } else if (specifier.type == 'ExportSpecifier') {
+            if (specifier.exported.name === 'default') {
+              defaultExport = node
+              if (!node.source) {
+                // export { x as default }
+                // rewrite to `const __default__ = x`
+                s.overwrite(
+                  start,
+                  end,
+                  `const __default__ = ${specifier.local.name}\n`
+                )
+                s.move(start, end, source.length)
+              } else {
+                // export { x as default } from './x'
+                // rewrite to `import { x as __default__ } from './x'`
+                s.overwrite(
+                  specifier.exported.start! + startOffset,
+                  specifier.exported.start! + startOffset + 7,
+                  '__default__'
+                )
+              }
+            } else {
+              setupExports[specifier.exported.name] = true
+            }
+          }
+        }
       }
-    } else if (node.type === 'ExportAllDeclaration') {
+    }
+
+    if (node.type === 'ExportAllDeclaration') {
       // export * from './x'
       s.overwrite(
         start,
@@ -127,20 +248,74 @@ export function compileScriptSetup(
       )
       s.move(start, end, 0)
     }
+
+    if (node.type === 'ExportDefaultDeclaration') {
+      if (defaultExport) {
+        // <script> already has export default
+        throw new Error(
+          `Default export is already declared in normal <script>.\n\n` +
+            generateCodeFrame(
+              source,
+              node.start! + startOffset,
+              node.start! + startOffset + `export default`.length
+            )
+        )
+      } else {
+        // export default {} inside <script setup>
+        // this should be kept in module scope - move it to the end
+        s.move(start, end, source.length)
+        s.overwrite(
+          start,
+          start + `export default`.length,
+          `const __default__ =`
+        )
+        // save it for analysis when all imports and variable declarations have
+        // been recorded
+        defaultExport = node
+        needDefaultExportCheck = true
+      }
+    }
+
+    if (
+      node.type === 'VariableDeclaration' ||
+      node.type === 'FunctionDeclaration' ||
+      node.type === 'ClassDeclaration'
+    ) {
+      walkDeclaration(node, setupScopeVars, propsVar, emitVar)
+    }
+
+    if (
+      node.type === 'TSDeclareFunction' &&
+      node.id &&
+      node.id.name === emitVar
+    ) {
+      genEmits(node)
+    }
+  }
+
+  // check default export to make sure it doesn't reference setup scope
+  // variables
+  if (needDefaultExportCheck) {
+    checkDefaultExport(
+      defaultExport!,
+      setupScopeVars,
+      imports,
+      setupExports,
+      source,
+      startOffset
+    )
   }
 
   // remove non-script content
   if (script) {
-    const s2 = script.loc.start.offset
-    const e2 = script.loc.end.offset
-    if (startOffset < s2) {
+    if (startOffset < scriptStartOffset!) {
       // <script setup> before <script>
-      s.remove(endOffset, s2)
-      s.remove(e2, source.length)
+      s.remove(endOffset, scriptStartOffset!)
+      s.remove(scriptEndOffset!, source.length)
     } else {
       // <script> before <script setup>
-      s.remove(0, s2)
-      s.remove(e2, startOffset)
+      s.remove(0, scriptStartOffset!)
+      s.remove(scriptEndOffset!, startOffset)
       s.remove(endOffset, source.length)
     }
   } else {
@@ -151,17 +326,12 @@ export function compileScriptSetup(
 
   // wrap setup code with function
   // determine the argument signature.
-  const args =
-    typeof scriptSetup.setup === 'string'
-      ? scriptSetup.setup
-      : // TODO should we force explicit args  signature?
-        `$props, { attrs: $attrs, slots: $slots, emit: $emit }`
   // export the content of <script setup> as a named export, `setup`.
   // this allows `import { setup } from '*.vue'` for testing purposes.
   s.appendLeft(startOffset, `\nexport function setup(${args}) {\n`)
 
   // generate return statement
-  let returned = `{ ${setupExports.join(', ')} }`
+  let returned = `{ ${Object.keys(setupExports).join(', ')} }`
 
   // handle `export * from`. We need to call `toRefs` on the imported module
   // object before merging.
@@ -173,14 +343,21 @@ export function compileScriptSetup(
     returned = `Object.assign(\n  ${returned}\n)`
   }
 
-  s.appendRight(
-    endOffset,
-    `\nreturn ${returned}\n}\n\nexport default { setup }\n`
-  )
+  s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
+
+  if (defaultExport) {
+    s.append(`__default__.setup = setup\nexport default __default__`)
+  } else {
+    s.append(`export default { setup }`)
+  }
 
   s.trim()
 
-  setupExports.forEach(key => {
+  // analyze bindings for template compiler optimization
+  if (script) {
+    Object.assign(bindings, analyzeScriptBindings(script))
+  }
+  Object.keys(setupExports).forEach(key => {
     bindings[key] = 'setup'
   })
 
@@ -189,11 +366,213 @@ export function compileScriptSetup(
     code: s.toString(),
     map: s.generateMap({
       source: filename,
+      hires: true,
       includeContent: true
     }) as SourceMap
   }
 }
 
+function walkDeclaration(
+  node: Declaration,
+  bindings: Record<string, boolean>,
+  propsKey: string,
+  emitsKey: string
+) {
+  if (node.type === 'VariableDeclaration') {
+    // export const foo = ...
+    for (const { id } of node.declarations) {
+      if (node.declare) {
+        // TODO `declare const $props...`
+        if (id.type === 'Identifier') {
+          if (
+            id.name === propsKey &&
+            id.typeAnnotation &&
+            id.typeAnnotation.type === 'TSTypeAnnotation' &&
+            id.typeAnnotation.typeAnnotation.type === 'TSTypeLiteral'
+          ) {
+            genProps(id.typeAnnotation.typeAnnotation)
+          } else if (
+            id.name === emitsKey &&
+            id.typeAnnotation &&
+            id.typeAnnotation.type === 'TSTypeAnnotation' &&
+            id.typeAnnotation.typeAnnotation.type === 'TSFunctionType'
+          ) {
+            genEmits(id.typeAnnotation.typeAnnotation)
+          }
+        }
+      } else if (id.type === 'Identifier') {
+        bindings[id.name] = true
+      } else if (id.type === 'ObjectPattern') {
+        walkObjectPattern(id, bindings)
+      } else if (id.type === 'ArrayPattern') {
+        walkArrayPattern(id, bindings)
+      }
+    }
+  } else if (
+    node.type === 'FunctionDeclaration' ||
+    node.type === 'ClassDeclaration'
+  ) {
+    // export function foo() {} / export class Foo {}
+    // export declarations must be named.
+    bindings[node.id!.name] = true
+  }
+}
+
+function walkObjectPattern(
+  node: ObjectPattern,
+  bindings: Record<string, boolean>
+) {
+  for (const p of node.properties) {
+    if (p.type === 'ObjectProperty') {
+      // key can only be Identifier in ObjectPattern
+      if (p.key.type === 'Identifier') {
+        if (p.key === p.value) {
+          // const { x } = ...
+          bindings[p.key.name] = true
+        } else {
+          walkPattern(p.value, bindings)
+        }
+      }
+    } else {
+      // ...rest
+      // argument can only be identifer when destructuring
+      bindings[(p.argument as Identifier).name] = true
+    }
+  }
+}
+
+function walkArrayPattern(
+  node: ArrayPattern,
+  bindings: Record<string, boolean>
+) {
+  for (const e of node.elements) {
+    e && walkPattern(e, bindings)
+  }
+}
+
+function walkPattern(node: Node, bindings: Record<string, boolean>) {
+  if (node.type === 'Identifier') {
+    bindings[node.name] = true
+  } else if (node.type === 'RestElement') {
+    // argument can only be identifer when destructuring
+    bindings[(node.argument as Identifier).name] = true
+  } else if (node.type === 'ObjectPattern') {
+    walkObjectPattern(node, bindings)
+  } else if (node.type === 'ArrayPattern') {
+    walkArrayPattern(node, bindings)
+  } else if (node.type === 'AssignmentPattern') {
+    if (node.left.type === 'Identifier') {
+      bindings[node.left.name] = true
+    } else {
+      walkPattern(node.left, bindings)
+    }
+  }
+}
+
+function genProps(node: TSTypeLiteral) {
+  // TODO
+  console.log('gen props', node)
+}
+
+function genEmits(node: TSFunctionType | TSDeclareFunction) {
+  // TODO
+  console.log('gen emits', node)
+}
+
+/**
+ * export default {} inside <script setup> cannot access variables declared
+ * inside since it's hoisted. Walk and check to make sure.
+ */
+function checkDefaultExport(
+  root: Node,
+  scopeVars: Record<string, boolean>,
+  imports: Record<string, boolean>,
+  exports: Record<string, boolean>,
+  source: string,
+  offset: number
+) {
+  const knownIds: Record<string, number> = Object.create(null)
+  ;(walk as any)(root, {
+    enter(node: Node & { scopeIds?: Set<string> }, parent: Node) {
+      if (node.type === 'Identifier') {
+        if (
+          !knownIds[node.name] &&
+          !isStaticPropertyKey(node, parent) &&
+          (scopeVars[node.name] || (!imports[node.name] && exports[node.name]))
+        ) {
+          throw new Error(
+            `\`export default\` in <script setup> cannot reference locally ` +
+              `declared variables because it will be hoisted outside of the ` +
+              `setup() function. If your component options requires initialization ` +
+              `in the module scope, use a separate normal <script> to export ` +
+              `the options instead.\n\n` +
+              generateCodeFrame(
+                source,
+                node.start! + offset,
+                node.end! + offset
+              )
+          )
+        }
+      } else if (
+        node.type === 'FunctionDeclaration' ||
+        node.type === 'FunctionExpression' ||
+        node.type === 'ArrowFunctionExpression'
+      ) {
+        // walk function expressions and add its arguments to known identifiers
+        // so that we don't prefix them
+        node.params.forEach(p =>
+          (walk as any)(p, {
+            enter(child: Node, parent: Node) {
+              if (
+                child.type === 'Identifier' &&
+                // do not record as scope variable if is a destructured key
+                !isStaticPropertyKey(child, parent) &&
+                // do not record if this is a default value
+                // assignment of a destructured variable
+                !(
+                  parent &&
+                  parent.type === 'AssignmentPattern' &&
+                  parent.right === child
+                )
+              ) {
+                const { name } = child
+                if (node.scopeIds && node.scopeIds.has(name)) {
+                  return
+                }
+                if (name in knownIds) {
+                  knownIds[name]++
+                } else {
+                  knownIds[name] = 1
+                }
+                ;(node.scopeIds || (node.scopeIds = new Set())).add(name)
+              }
+            }
+          })
+        )
+      }
+    },
+    leave(node: Node & { scopeIds?: Set<string> }) {
+      if (node.scopeIds) {
+        node.scopeIds.forEach((id: string) => {
+          knownIds[id]--
+          if (knownIds[id] === 0) {
+            delete knownIds[id]
+          }
+        })
+      }
+    }
+  })
+}
+
+function isStaticPropertyKey(node: Node, parent: Node): boolean {
+  return (
+    parent &&
+    (parent.type === 'ObjectProperty' || parent.type === 'ObjectMethod') &&
+    !parent.computed &&
+    parent.key === node
+  )
+}
+
 /**
  * Analyze bindings in normal `<script>`
  * Note that `compileScriptSetup` already analyzes bindings as part of its
@@ -202,13 +581,7 @@ export function compileScriptSetup(
 export function analyzeScriptBindings(
   _script: SFCScriptBlock
 ): BindingMetadata {
-  return {}
-}
-
-function walkObjectPattern(_node: ObjectPattern, _setupExports: string[]) {
-  // TODO
-}
-
-function walkArrayPattern(_node: ArrayPattern, _setupExports: string[]) {
-  // TODO
+  return {
+    // TODO
+  }
 }
index 000ae8f2a6cc03fdde2449cd91353944100bf4f4..93e2977818a75a788f4ab415cd980925b5eb03e2 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -2636,16 +2636,16 @@ estree-walker@^0.6.1:
   resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"
   integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==
 
-estree-walker@^0.8.1:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.8.1.tgz#6230ce2ec9a5cb03888afcaf295f97d90aa52b79"
-  integrity sha512-H6cJORkqvrNziu0KX2hqOMAlA2CiuAxHeGJXSIoKA/KLv229Dw806J3II6mKTm5xiDX1At1EXCfsOQPB+tMB+g==
-
 estree-walker@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
   integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
 
+estree-walker@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.1.tgz#f8e030fb21cefa183b44b7ad516b747434e7a3e0"
+  integrity sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg==
+
 esutils@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"