]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: compileScriptSetup
authorEvan You <yyx990803@gmail.com>
Mon, 6 Jul 2020 19:56:24 +0000 (15:56 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 9 Jul 2020 16:17:28 +0000 (12:17 -0400)
packages/compiler-core/package.json
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/package.json
packages/compiler-sfc/src/compileScript.ts [new file with mode: 0644]
packages/compiler-sfc/src/index.ts
packages/compiler-sfc/src/parse.ts
packages/shared/src/index.ts
yarn.lock

index ee12f9228878dc2859472e275b051dfdeee169db..91a86806bb79c844d97e9b7acc7c16023101d403 100644 (file)
@@ -31,8 +31,8 @@
   "homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-core#readme",
   "dependencies": {
     "@vue/shared": "3.0.0-beta.20",
-    "@babel/parser": "^7.8.6",
-    "@babel/types": "^7.8.6",
+    "@babel/parser": "^7.10.4",
+    "@babel/types": "^7.10.4",
     "estree-walker": "^0.8.1",
     "source-map": "^0.6.1"
   }
index b1da05b4589aa323cb439e32759de4a2b7e813bd..97e8fece2365798388a667ed3ca4da8924e0acb0 100644 (file)
@@ -22,10 +22,15 @@ import {
   parseJS,
   walkJS
 } from '../utils'
-import { isGloballyWhitelisted, makeMap } from '@vue/shared'
+import {
+  isGloballyWhitelisted,
+  makeMap,
+  babelParserDefautPlugins
+} from '@vue/shared'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
 import { validateBrowserExpression } from '../validateExpression'
+import { ParserPlugin } from '@babel/parser'
 
 const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
 
@@ -127,12 +132,7 @@ export function processExpression(
     ast = parseJS(source, {
       plugins: [
         ...context.expressionPlugins,
-        // by default we enable proposals slated for ES2020.
-        // full list at https://babeljs.io/docs/en/next/babel-parser#plugins
-        // this will need to be updated as the spec moves forward.
-        'bigInt',
-        'optionalChaining',
-        'nullishCoalescingOperator'
+        ...(babelParserDefautPlugins as ParserPlugin[])
       ]
     }).program
   } catch (e) {
index e57651e972b1938523cce43b501cbf9010b01d28..fa9d8e27c20a66ff0da13ec9c0337600d252b893 100644 (file)
@@ -34,6 +34,7 @@
     "vue": "3.0.0-beta.20"
   },
   "dependencies": {
+    "@babel/parser": "^7.10.4",
     "@vue/compiler-core": "3.0.0-beta.20",
     "@vue/compiler-dom": "3.0.0-beta.20",
     "@vue/compiler-ssr": "3.0.0-beta.20",
@@ -41,6 +42,7 @@
     "consolidate": "^0.15.1",
     "hash-sum": "^2.0.0",
     "lru-cache": "^5.1.1",
+    "magic-string": "^0.25.7",
     "merge-source-map": "^1.1.0",
     "postcss": "^7.0.27",
     "postcss-modules": "^3.1.0",
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
new file mode 100644 (file)
index 0000000..4cfe536
--- /dev/null
@@ -0,0 +1,214 @@
+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'
+
+export interface BindingMetadata {
+  [key: string]: 'data' | 'props' | 'setup' | 'ctx'
+}
+
+export interface SFCScriptCompileOptions {
+  parserPlugins?: ParserPlugin[]
+}
+
+/**
+ * Compile `<script setup>`
+ * It requires the whole SFC descriptor because we need to handle and merge
+ * normal `<script>` + `<script setup>` if both are present.
+ */
+export function compileScriptSetup(
+  sfc: SFCDescriptor,
+  options: SFCScriptCompileOptions = {}
+) {
+  const { script, scriptSetup, source, filename } = sfc
+  if (!scriptSetup) {
+    throw new Error('SFC has no <script setup>.')
+  }
+
+  if (script && script.lang !== scriptSetup.lang) {
+    throw new Error(
+      `<script> and <script setup> must have the same language type.`
+    )
+  }
+
+  const bindings: BindingMetadata = {}
+  const setupExports: string[] = []
+  let exportAllIndex = 0
+
+  const s = new MagicString(source)
+  const startOffset = scriptSetup.loc.start.offset
+  const endOffset = scriptSetup.loc.end.offset
+
+  // parse and transform <script setup>
+  const plugins: ParserPlugin[] = [
+    ...(options.parserPlugins || []),
+    ...(babelParserDefautPlugins as ParserPlugin[])
+  ]
+  if (scriptSetup.lang === 'ts') {
+    plugins.push('typescript')
+  }
+
+  const ast = parse(scriptSetup.content, {
+    plugins,
+    sourceType: 'module'
+  }).program.body
+
+  for (const node of ast) {
+    const start = node.start! + startOffset
+    let end = node.end! + startOffset
+    // import or type declarations: move to top
+    // locate the end of whitespace between this statement and the next
+    while (end <= source.length) {
+      if (!/\s/.test(source.charAt(end))) {
+        break
+      }
+      end++
+    }
+    if (node.type === 'ImportDeclaration') {
+      s.move(start, end, 0)
+    }
+    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)
+        }
+      }
+      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)
+          }
+        }
+        if (node.source) {
+          // export { x } from './x'
+          // change it to import and move to top
+          s.overwrite(start, start + 6, 'import')
+          s.move(start, end, 0)
+        } else {
+          // export { x }
+          s.remove(start, end)
+        }
+      }
+    } else if (node.type === 'ExportAllDeclaration') {
+      // export * from './x'
+      s.overwrite(
+        start,
+        node.source.start! + startOffset,
+        `import * as __import_all_${exportAllIndex++}__ from `
+      )
+      s.move(start, end, 0)
+    }
+  }
+
+  // remove non-script content
+  if (script) {
+    const s2 = script.loc.start.offset
+    const e2 = script.loc.end.offset
+    if (startOffset < s2) {
+      // <script setup> before <script>
+      s.remove(endOffset, s2)
+      s.remove(e2, source.length)
+    } else {
+      // <script> before <script setup>
+      s.remove(0, s2)
+      s.remove(e2, startOffset)
+      s.remove(endOffset, source.length)
+    }
+  } else {
+    // only <script setup>
+    s.remove(0, startOffset)
+    s.remove(endOffset, source.length)
+  }
+
+  // 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(', ')} }`
+
+  // handle `export * from`. We need to call `toRefs` on the imported module
+  // object before merging.
+  if (exportAllIndex > 0) {
+    s.prepend(`import { toRefs as __toRefs__ } from 'vue'\n`)
+    for (let i = 0; i < exportAllIndex; i++) {
+      returned += `,\n  __toRefs__(__export_all_${i}__)`
+    }
+    returned = `Object.assign(\n  ${returned}\n)`
+  }
+
+  s.appendRight(
+    endOffset,
+    `\nreturn ${returned}\n}\n\nexport default { setup }\n`
+  )
+
+  s.trim()
+
+  setupExports.forEach(key => {
+    bindings[key] = 'setup'
+  })
+
+  return {
+    bindings,
+    code: s.toString(),
+    map: s.generateMap({
+      source: filename,
+      includeContent: true
+    }) as SourceMap
+  }
+}
+
+/**
+ * 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 {}
+}
+
+function walkObjectPattern(_node: ObjectPattern, _setupExports: string[]) {
+  // TODO
+}
+
+function walkArrayPattern(_node: ArrayPattern, _setupExports: string[]) {
+  // TODO
+}
index d5858758eefe30eb498bc9e89b3bfdec17a82548..40c2ce7d86cafac5889ce67d2b2b99713fdfbe51 100644 (file)
@@ -2,6 +2,7 @@
 export { parse } from './parse'
 export { compileTemplate } from './compileTemplate'
 export { compileStyle, compileStyleAsync } from './compileStyle'
+export { compileScriptSetup, analyzeScriptBindings } from './compileScript'
 
 // Types
 export {
index 6e92acaacb3714d3a074d6e4973528f75673dcf0..f0e2001c3ca7af118e12ce7985e88dfa9ec009a4 100644 (file)
@@ -35,7 +35,7 @@ export interface SFCTemplateBlock extends SFCBlock {
 
 export interface SFCScriptBlock extends SFCBlock {
   type: 'script'
-  setup?: boolean
+  setup?: boolean | string
 }
 
 export interface SFCStyleBlock extends SFCBlock {
@@ -46,6 +46,7 @@ export interface SFCStyleBlock extends SFCBlock {
 
 export interface SFCDescriptor {
   filename: string
+  source: string
   template: SFCTemplateBlock | null
   script: SFCScriptBlock | null
   scriptSetup: SFCScriptBlock | null
@@ -86,6 +87,7 @@ export function parse(
 
   const descriptor: SFCDescriptor = {
     filename,
+    source,
     template: null,
     script: null,
     scriptSetup: null,
@@ -152,7 +154,7 @@ export function parse(
           descriptor.script = block
           break
         }
-        warnDuplicateBlock(source, filename, node, block.setup)
+        warnDuplicateBlock(source, filename, node, !!block.setup)
         break
       case 'style':
         descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
@@ -251,7 +253,7 @@ function createBlock(
       } else if (type === 'template' && p.name === 'functional') {
         ;(block as SFCTemplateBlock).functional = true
       } else if (type === 'script' && p.name === 'setup') {
-        ;(block as SFCScriptBlock).setup = true
+        ;(block as SFCScriptBlock).setup = attrs.setup || true
       }
     }
   })
index 9aa9ad9a34832b60ad5e61cf70581f149217626c..96b3f9157748bc49d64a9d9857122b08ba448ea9 100644 (file)
@@ -13,6 +13,18 @@ export * from './escapeHtml'
 export * from './looseEqual'
 export * from './toDisplayString'
 
+/**
+ * List of @babel/parser plugins that are used for template expression
+ * transforms and SFC script transforms. By default we enable proposals slated
+ * for ES2020. This will need to be updated as the spec moves forward.
+ * Full list at https://babeljs.io/docs/en/next/babel-parser#plugins
+ */
+export const babelParserDefautPlugins = [
+  'bigInt',
+  'optionalChaining',
+  'nullishCoalescingOperator'
+]
+
 export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
   ? Object.freeze({})
   : {}
index bcfe75d4b4691df75496b069f7d498e74f404f9d..000ae8f2a6cc03fdde2449cd91353944100bf4f4 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.1.0", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0":
+"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.4.tgz#9eedf27e1998d87739fb5028a5120557c06a1a64"
   integrity sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==
     globals "^11.1.0"
     lodash "^4.17.13"
 
-"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0":
+"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.4.tgz#369517188352e18219981efd156bfdb199fff1ee"
   integrity sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==
@@ -4648,6 +4648,13 @@ magic-string@^0.25.2, magic-string@^0.25.5:
   dependencies:
     sourcemap-codec "^1.4.4"
 
+magic-string@^0.25.7:
+  version "0.25.7"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
+  integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
+  dependencies:
+    sourcemap-codec "^1.4.4"
+
 make-dir@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801"