]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(compiler-sfc): (wip) split compileScript logic, use context, move defineProps
authorEvan You <yyx990803@gmail.com>
Tue, 11 Apr 2023 05:06:04 +0000 (13:06 +0800)
committerEvan You <yyx990803@gmail.com>
Tue, 11 Apr 2023 08:05:00 +0000 (16:05 +0800)
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/script/context.ts [new file with mode: 0644]
packages/compiler-sfc/src/script/defineProps.ts [new file with mode: 0644]
packages/compiler-sfc/src/script/definePropsDestructure.ts [moved from packages/compiler-sfc/src/script/propsDestructure.ts with 100% similarity]
packages/compiler-sfc/src/script/resolveType.ts [new file with mode: 0644]
packages/compiler-sfc/src/script/utils.ts [new file with mode: 0644]

index 575ce6caade17c5fb777c59d8c88b1e910f7f734..5dacc84d55209c6609c2bc0e54371db2616277bc 100644 (file)
@@ -15,12 +15,7 @@ import {
   isCallOf
 } from '@vue/compiler-dom'
 import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
-import {
-  parse as _parse,
-  parseExpression,
-  ParserOptions,
-  ParserPlugin
-} from '@babel/parser'
+import { parse as _parse, parseExpression, ParserPlugin } from '@babel/parser'
 import { camelize, capitalize, generateCodeFrame, makeMap } from '@vue/shared'
 import {
   Node,
@@ -41,7 +36,6 @@ import {
   TSInterfaceBody,
   TSTypeElement,
   AwaitExpression,
-  Program,
   ObjectMethod,
   LVal,
   Expression,
@@ -59,13 +53,19 @@ import { warnOnce } from './warn'
 import { rewriteDefaultAST } from './rewriteDefault'
 import { createCache } from './cache'
 import { shouldTransform, transformAST } from '@vue/reactivity-transform'
-import { transformDestructuredProps } from './script/propsDestructure'
+import { transformDestructuredProps } from './script/definePropsDestructure'
+import { resolveObjectKey, FromNormalScript } from './script/utils'
+import { ScriptCompileContext } from './script/context'
+import {
+  processDefineProps,
+  DEFINE_PROPS,
+  WITH_DEFAULTS,
+  PropsDeclType
+} from './script/defineProps'
 
 // Special compiler macros
-const DEFINE_PROPS = 'defineProps'
 const DEFINE_EMITS = 'defineEmits'
 const DEFINE_EXPOSE = 'defineExpose'
-const WITH_DEFAULTS = 'withDefaults'
 const DEFINE_OPTIONS = 'defineOptions'
 const DEFINE_SLOTS = 'defineSlots'
 const DEFINE_MODEL = 'defineModel'
@@ -149,8 +149,6 @@ export type PropsDestructureBindings = Record<
   }
 >
 
-type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
-type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
 type EmitsDeclType = FromNormalScript<
   TSFunctionType | TSTypeLiteral | TSInterfaceBody
 >
@@ -195,41 +193,15 @@ export function compileScript(
     ? `const ${options.genDefaultAs} =`
     : `export default`
   const normalScriptDefaultVar = `__default__`
-  const isJS =
-    scriptLang === 'js' ||
-    scriptLang === 'jsx' ||
-    scriptSetupLang === 'js' ||
-    scriptSetupLang === 'jsx'
-  const isTS =
-    scriptLang === 'ts' ||
-    scriptLang === 'tsx' ||
-    scriptSetupLang === 'ts' ||
-    scriptSetupLang === 'tsx'
-
-  // resolve parser plugins
-  const plugins: ParserPlugin[] = []
-  if (!isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
-    plugins.push('jsx')
-  } else {
-    // If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins
-    if (options.babelParserPlugins)
-      options.babelParserPlugins = options.babelParserPlugins.filter(
-        n => n !== 'jsx'
-      )
-  }
-  if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
-  if (isTS) {
-    plugins.push('typescript')
-    if (!plugins.includes('decorators')) {
-      plugins.push('decorators-legacy')
-    }
-  }
+
+  const ctx = new ScriptCompileContext(sfc, options)
+  const { isTS } = ctx
 
   if (!scriptSetup) {
     if (!script) {
       throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`)
     }
-    if (scriptLang && !isJS && !isTS) {
+    if (scriptLang && !ctx.isJS && !ctx.isTS) {
       // do not process non js/ts script blocks
       return script
     }
@@ -237,10 +209,7 @@ export function compileScript(
     try {
       let content = script.content
       let map = script.map
-      const scriptAst = _parse(content, {
-        plugins,
-        sourceType: 'module'
-      }).program
+      const scriptAst = ctx.scriptAst!
       const bindings = analyzeScriptBindings(scriptAst.body)
       if (enableReactivityTransform && shouldTransform(content)) {
         const s = new MagicString(source)
@@ -268,7 +237,7 @@ export function compileScript(
       if (cssVars.length || options.genDefaultAs) {
         const defaultVar = options.genDefaultAs || normalScriptDefaultVar
         const s = new MagicString(content)
-        rewriteDefaultAST(scriptAst.body, s, defaultVar)
+        rewriteDefaultAST(ctx.scriptAst!.body, s, defaultVar)
         content = s.toString()
         if (cssVars.length) {
           content += genNormalScriptCssVarsCode(
@@ -304,7 +273,7 @@ export function compileScript(
     )
   }
 
-  if (scriptSetupLang && !isJS && !isTS) {
+  if (scriptSetupLang && !ctx.isJS && !ctx.isTS) {
     // do not process non js/ts script blocks
     return scriptSetup
   }
@@ -317,21 +286,7 @@ export function compileScript(
   const setupBindings: Record<string, BindingTypes> = Object.create(null)
 
   let defaultExport: Node | undefined
-  let hasDefinePropsCall = false
-  let hasDefineEmitCall = false
-  let hasDefineExposeCall = false
-  let hasDefaultExportName = false
-  let hasDefaultExportRender = false
-  let hasDefineOptionsCall = false
-  let hasDefineSlotsCall = false
-  let hasDefineModelCall = false
-  let propsRuntimeDecl: Node | undefined
-  let propsRuntimeDefaults: Node | undefined
-  let propsDestructureDecl: Node | undefined
-  let propsDestructureRestId: string | undefined
-  let propsTypeDecl: PropsDeclType | undefined
-  let propsTypeDeclRaw: Node | undefined
-  let propsIdentifier: string | undefined
+  // let propsRuntimeDefaults: Node | undefined
   let emitsRuntimeDecl: Node | undefined
   let emitsTypeDecl: EmitsDeclType | undefined
   let emitIdentifier: string | undefined
@@ -344,9 +299,6 @@ export function compileScript(
   const typeDeclaredEmits: Set<string> = new Set()
   // record declared types for runtime props type generation
   const declaredTypes: Record<string, string[]> = {}
-  // props destructure data
-  const propsDestructuredBindings: PropsDestructureBindings =
-    Object.create(null)
 
   // magic-string state
   const s = new MagicString(source)
@@ -360,21 +312,6 @@ export function compileScript(
     return `_${key}`
   }
 
-  function parse(
-    input: string,
-    options: ParserOptions,
-    offset: number
-  ): Program {
-    try {
-      return _parse(input, options).program
-    } catch (e: any) {
-      e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
-        sfc.filename
-      }\n${generateCodeFrame(source, e.pos + offset, e.pos + offset + 1)}`
-      throw e
-    }
-  }
-
   function error(
     msg: string,
     node: Node,
@@ -439,136 +376,14 @@ export function compileScript(
     }
   }
 
-  function processDefineProps(node: Node, declId?: LVal): boolean {
-    if (!isCallOf(node, DEFINE_PROPS)) {
-      return false
-    }
-
-    if (hasDefinePropsCall) {
-      error(`duplicate ${DEFINE_PROPS}() call`, node)
-    }
-    hasDefinePropsCall = true
-
-    propsRuntimeDecl = node.arguments[0]
-
-    // call has type parameters - infer runtime types from it
-    if (node.typeParameters) {
-      if (propsRuntimeDecl) {
-        error(
-          `${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
-            `at the same time. Use one or the other.`,
-          node
-        )
-      }
-
-      propsTypeDeclRaw = node.typeParameters.params[0]
-      propsTypeDecl = resolveQualifiedType(
-        propsTypeDeclRaw,
-        node => node.type === 'TSTypeLiteral'
-      ) as PropsDeclType | undefined
-
-      if (!propsTypeDecl) {
-        error(
-          `type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
-            `or a reference to an interface or literal type.`,
-          propsTypeDeclRaw
-        )
-      }
-    }
-
-    if (declId) {
-      // handle props destructure
-      if (declId.type === 'ObjectPattern') {
-        propsDestructureDecl = declId
-        for (const prop of declId.properties) {
-          if (prop.type === 'ObjectProperty') {
-            const propKey = resolveObjectKey(prop.key, prop.computed)
-
-            if (!propKey) {
-              error(
-                `${DEFINE_PROPS}() destructure cannot use computed key.`,
-                prop.key
-              )
-            }
-
-            if (prop.value.type === 'AssignmentPattern') {
-              // default value { foo = 123 }
-              const { left, right } = prop.value
-              if (left.type !== 'Identifier') {
-                error(
-                  `${DEFINE_PROPS}() destructure does not support nested patterns.`,
-                  left
-                )
-              }
-              // store default value
-              propsDestructuredBindings[propKey] = {
-                local: left.name,
-                default: right
-              }
-            } else if (prop.value.type === 'Identifier') {
-              // simple destructure
-              propsDestructuredBindings[propKey] = {
-                local: prop.value.name
-              }
-            } else {
-              error(
-                `${DEFINE_PROPS}() destructure does not support nested patterns.`,
-                prop.value
-              )
-            }
-          } else {
-            // rest spread
-            propsDestructureRestId = (prop.argument as Identifier).name
-          }
-        }
-      } else {
-        propsIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!)
-      }
-    }
-
-    return true
-  }
-
-  function processWithDefaults(node: Node, declId?: LVal): boolean {
-    if (!isCallOf(node, WITH_DEFAULTS)) {
-      return false
-    }
-    if (processDefineProps(node.arguments[0], declId)) {
-      if (propsRuntimeDecl) {
-        error(
-          `${WITH_DEFAULTS} can only be used with type-based ` +
-            `${DEFINE_PROPS} declaration.`,
-          node
-        )
-      }
-      if (propsDestructureDecl) {
-        error(
-          `${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
-            `Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`,
-          node.callee
-        )
-      }
-      propsRuntimeDefaults = node.arguments[1]
-      if (!propsRuntimeDefaults) {
-        error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
-      }
-    } else {
-      error(
-        `${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
-        node.arguments[0] || node
-      )
-    }
-    return true
-  }
-
   function processDefineEmits(node: Node, declId?: LVal): boolean {
     if (!isCallOf(node, DEFINE_EMITS)) {
       return false
     }
-    if (hasDefineEmitCall) {
+    if (ctx.hasDefineEmitCall) {
       error(`duplicate ${DEFINE_EMITS}() call`, node)
     }
-    hasDefineEmitCall = true
+    ctx.hasDefineEmitCall = true
     emitsRuntimeDecl = node.arguments[0]
     if (node.typeParameters) {
       if (emitsRuntimeDecl) {
@@ -608,10 +423,10 @@ export function compileScript(
     if (!isCallOf(node, DEFINE_SLOTS)) {
       return false
     }
-    if (hasDefineSlotsCall) {
+    if (ctx.hasDefineSlotsCall) {
       error(`duplicate ${DEFINE_SLOTS}() call`, node)
     }
-    hasDefineSlotsCall = true
+    ctx.hasDefineSlotsCall = true
 
     if (node.arguments.length > 0) {
       error(`${DEFINE_SLOTS}() cannot accept arguments`, node)
@@ -632,7 +447,7 @@ export function compileScript(
     if (!enableDefineModel || !isCallOf(node, DEFINE_MODEL)) {
       return false
     }
-    hasDefineModelCall = true
+    ctx.hasDefineModelCall = true
 
     const type =
       (node.typeParameters && node.typeParameters.params[0]) || undefined
@@ -781,7 +596,7 @@ export function compileScript(
     if (!isCallOf(node, DEFINE_OPTIONS)) {
       return false
     }
-    if (hasDefineOptionsCall) {
+    if (ctx.hasDefineOptionsCall) {
       error(`duplicate ${DEFINE_OPTIONS}() call`, node)
     }
     if (node.typeParameters) {
@@ -789,7 +604,7 @@ export function compileScript(
     }
     if (!node.arguments[0]) return true
 
-    hasDefineOptionsCall = true
+    ctx.hasDefineOptionsCall = true
     optionsRuntimeDecl = unwrapTSNode(node.arguments[0])
 
     let propsOption = undefined
@@ -875,10 +690,10 @@ export function compileScript(
 
   function processDefineExpose(node: Node): boolean {
     if (isCallOf(node, DEFINE_EXPOSE)) {
-      if (hasDefineExposeCall) {
+      if (ctx.hasDefineExposeCall) {
         error(`duplicate ${DEFINE_EXPOSE}() call`, node)
       }
-      hasDefineExposeCall = true
+      ctx.hasDefineExposeCall = true
       return true
     }
     return false
@@ -958,9 +773,9 @@ export function compileScript(
    */
   function hasStaticWithDefaults() {
     return (
-      propsRuntimeDefaults &&
-      propsRuntimeDefaults.type === 'ObjectExpression' &&
-      propsRuntimeDefaults.properties.every(
+      ctx.propsRuntimeDefaults &&
+      ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
+      ctx.propsRuntimeDefaults.properties.every(
         node =>
           node.type !== 'SpreadElement' &&
           (!node.computed || node.key.type.endsWith('Literal'))
@@ -993,7 +808,7 @@ export function compileScript(
           }`
         } else if (hasStaticDefaults) {
           const prop = (
-            propsRuntimeDefaults as ObjectExpression
+            ctx.propsRuntimeDefaults as ObjectExpression
           ).properties.find(node => {
             if (node.type === 'SpreadElement') return false
             return resolveObjectKey(node.key, node.computed) === key
@@ -1045,10 +860,10 @@ export function compileScript(
       })
       .join(',\n    ')}\n  }`
 
-      if (propsRuntimeDefaults && !hasStaticDefaults) {
+      if (ctx.propsRuntimeDefaults && !hasStaticDefaults) {
         propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
-          propsRuntimeDefaults.start! + startOffset,
-          propsRuntimeDefaults.end! + startOffset
+          ctx.propsRuntimeDefaults.start! + startOffset,
+          ctx.propsRuntimeDefaults.end! + startOffset
         )})`
       }
 
@@ -1056,7 +871,7 @@ export function compileScript(
     }
 
     function genModels() {
-      if (!hasDefineModelCall) return
+      if (!ctx.hasDefineModelCall) return
 
       let modelPropsDecl = ''
       for (const [name, { type, options }] of Object.entries(modelDecls)) {
@@ -1088,7 +903,7 @@ export function compileScript(
 
         let decl: string
         if (runtimeType && options) {
-          decl = isTS
+          decl = ctx.isTS
             ? `{ ${codegenOptions}, ...${options} }`
             : `Object.assign({ ${codegenOptions} }, ${options})`
         } else {
@@ -1100,13 +915,13 @@ export function compileScript(
     }
 
     let propsDecls: undefined | string
-    if (propsRuntimeDecl) {
+    if (ctx.propsRuntimeDecl) {
       propsDecls = scriptSetup!.content
-        .slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
+        .slice(ctx.propsRuntimeDecl.start!, ctx.propsRuntimeDecl.end!)
         .trim()
-      if (propsDestructureDecl) {
+      if (ctx.propsDestructureDecl) {
         const defaults: string[] = []
-        for (const key in propsDestructuredBindings) {
+        for (const key in ctx.propsDestructuredBindings) {
           const d = genDestructuredDefaultValue(key)
           if (d)
             defaults.push(
@@ -1121,7 +936,7 @@ export function compileScript(
           )}(${propsDecls}, {\n  ${defaults.join(',\n  ')}\n})`
         }
       }
-    } else if (propsTypeDecl) {
+    } else if (ctx.propsTypeDecl) {
       propsDecls = genPropsFromTS()
     }
 
@@ -1143,7 +958,7 @@ export function compileScript(
         needSkipFactory: boolean
       }
     | undefined {
-    const destructured = propsDestructuredBindings[key]
+    const destructured = ctx.propsDestructuredBindings[key]
     const defaultVal = destructured && destructured.default
     if (defaultVal) {
       const value = scriptSetup!.content.slice(
@@ -1204,13 +1019,15 @@ export function compileScript(
           m.key.type === 'Identifier'
         ) {
           if (
-            (propsRuntimeDefaults as ObjectExpression).properties.some(p => {
-              if (p.type === 'SpreadElement') return false
-              return (
-                resolveObjectKey(p.key, p.computed) ===
-                (m.key as Identifier).name
-              )
-            })
+            (ctx.propsRuntimeDefaults as ObjectExpression).properties.some(
+              p => {
+                if (p.type === 'SpreadElement') return false
+                return (
+                  resolveObjectKey(p.key, p.computed) ===
+                  (m.key as Identifier).name
+                )
+              }
+            )
           ) {
             res +=
               m.key.name +
@@ -1248,7 +1065,7 @@ export function compileScript(
     } else if (emitsTypeDecl) {
       emitsDecl = genEmitsFromTS()
     }
-    if (hasDefineModelCall) {
+    if (ctx.hasDefineModelCall) {
       let modelEmitsDecl = `[${Object.keys(modelDecls)
         .map(n => JSON.stringify(`update:${n}`))
         .join(', ')}]`
@@ -1260,29 +1077,32 @@ export function compileScript(
   }
 
   // 0. parse both <script> and <script setup> blocks
-  const scriptAst =
-    script &&
-    parse(
-      script.content,
-      {
-        plugins,
-        sourceType: 'module'
-      },
-      scriptStartOffset!
-    )
-
-  const scriptSetupAst = parse(
-    scriptSetup.content,
-    {
-      plugins: [
-        ...plugins,
-        // allow top level await but only inside <script setup>
-        'topLevelAwait'
-      ],
-      sourceType: 'module'
-    },
-    startOffset
-  )
+  // const scriptAst =
+  //   script &&
+  //   parse(
+  //     script.content,
+  //     {
+  //       plugins,
+  //       sourceType: 'module'
+  //     },
+  //     scriptStartOffset!
+  //   )
+
+  // const scriptSetupAst = parse(
+  //   scriptSetup.content,
+  //   {
+  //     plugins: [
+  //       ...plugins,
+  //       // allow top level await but only inside <script setup>
+  //       'topLevelAwait'
+  //     ],
+  //     sourceType: 'module'
+  //   },
+  //   startOffset
+  // )
+
+  const scriptAst = ctx.scriptAst
+  const scriptSetupAst = ctx.scriptSetupAst!
 
   // 1.1 walk import delcarations of <script>
   if (scriptAst) {
@@ -1407,7 +1227,7 @@ export function compileScript(
               s.key.type === 'Identifier' &&
               s.key.name === 'name'
             ) {
-              hasDefaultExportName = true
+              ctx.hasDefaultExportName = true
             }
             if (
               (s.type === 'ObjectMethod' || s.type === 'ObjectProperty') &&
@@ -1415,7 +1235,7 @@ export function compileScript(
               s.key.name === 'render'
             ) {
               // TODO warn when we provide a better way to do it?
-              hasDefaultExportRender = true
+              ctx.hasDefaultExportRender = true
             }
           }
         }
@@ -1531,10 +1351,9 @@ export function compileScript(
       const expr = unwrapTSNode(node.expression)
       // process `defineProps` and `defineEmit(s)` calls
       if (
-        processDefineProps(expr) ||
+        processDefineProps(ctx, expr) ||
         processDefineEmits(expr) ||
         processDefineOptions(expr) ||
-        processWithDefaults(expr) ||
         processDefineSlots(expr)
       ) {
         s.remove(node.start! + startOffset, node.end! + startOffset)
@@ -1568,9 +1387,7 @@ export function compileScript(
           }
 
           // defineProps / defineEmits
-          const isDefineProps =
-            processDefineProps(init, decl.id) ||
-            processWithDefaults(init, decl.id)
+          const isDefineProps = processDefineProps(ctx, init, decl.id)
           const isDefineEmits =
             !isDefineProps && processDefineEmits(init, decl.id)
           !isDefineEmits &&
@@ -1697,12 +1514,12 @@ export function compileScript(
   }
 
   // 3.1 props destructure transform
-  if (propsDestructureDecl) {
+  if (ctx.propsDestructureDecl) {
     transformDestructuredProps(
       scriptSetupAst,
       s,
       startOffset,
-      propsDestructuredBindings,
+      ctx.propsDestructuredBindings,
       error,
       vueImportAliases
     )
@@ -1728,8 +1545,8 @@ export function compileScript(
   }
 
   // 4. extract runtime props/emits code from setup context type
-  if (propsTypeDecl) {
-    extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
+  if (ctx.propsTypeDecl) {
+    extractRuntimeProps(ctx.propsTypeDecl, typeDeclaredProps, declaredTypes)
   }
   if (emitsTypeDecl) {
     extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits, error)
@@ -1737,9 +1554,9 @@ export function compileScript(
 
   // 5. check macro args to make sure it doesn't reference setup scope
   // variables
-  checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
-  checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
-  checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
+  checkInvalidScopeReference(ctx.propsRuntimeDecl, DEFINE_PROPS)
+  checkInvalidScopeReference(ctx.propsRuntimeDefaults, DEFINE_PROPS)
+  checkInvalidScopeReference(ctx.propsDestructureDecl, DEFINE_PROPS)
   checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS)
   checkInvalidScopeReference(optionsRuntimeDecl, DEFINE_OPTIONS)
 
@@ -1766,8 +1583,8 @@ export function compileScript(
   if (scriptAst) {
     Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst.body))
   }
-  if (propsRuntimeDecl) {
-    for (const key of getObjectOrArrayExpressionKeys(propsRuntimeDecl)) {
+  if (ctx.propsRuntimeDecl) {
+    for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
       bindingMetadata[key] = BindingTypes.PROPS
     }
   }
@@ -1778,13 +1595,13 @@ export function compileScript(
     bindingMetadata[key] = BindingTypes.PROPS
   }
   // props aliases
-  if (propsDestructureDecl) {
-    if (propsDestructureRestId) {
-      bindingMetadata[propsDestructureRestId] =
+  if (ctx.propsDestructureDecl) {
+    if (ctx.propsDestructureRestId) {
+      bindingMetadata[ctx.propsDestructureRestId] =
         BindingTypes.SETUP_REACTIVE_CONST
     }
-    for (const key in propsDestructuredBindings) {
-      const { local } = propsDestructuredBindings[key]
+    for (const key in ctx.propsDestructuredBindings) {
+      const { local } = ctx.propsDestructuredBindings[key]
       if (local !== key) {
         bindingMetadata[local] = BindingTypes.PROPS_ALIASED
         ;(bindingMetadata.__propsAliases ||
@@ -1832,7 +1649,7 @@ export function compileScript(
 
   // 9. finalize setup() argument signature
   let args = `__props`
-  if (propsTypeDecl) {
+  if (ctx.propsTypeDecl) {
     // mark as any and only cast on assignment
     // since the user defined complex types may be incompatible with the
     // inferred type from generated runtime declarations
@@ -1841,20 +1658,22 @@ export function compileScript(
   // inject user assignment of props
   // we use a default __props so that template expressions referencing props
   // can use it directly
-  if (propsIdentifier) {
+  if (ctx.propsIdentifier) {
     s.prependLeft(
       startOffset,
-      `\nconst ${propsIdentifier} = __props${
-        propsTypeDecl ? ` as ${genSetupPropsType(propsTypeDecl)}` : ``
+      `\nconst ${ctx.propsIdentifier} = __props${
+        ctx.propsTypeDecl ? ` as ${genSetupPropsType(ctx.propsTypeDecl)}` : ``
       };\n`
     )
   }
-  if (propsDestructureRestId) {
+  if (ctx.propsDestructureRestId) {
     s.prependLeft(
       startOffset,
-      `\nconst ${propsDestructureRestId} = ${helper(
+      `\nconst ${ctx.propsDestructureRestId} = ${helper(
         `createPropsRestProxy`
-      )}(__props, ${JSON.stringify(Object.keys(propsDestructuredBindings))});\n`
+      )}(__props, ${JSON.stringify(
+        Object.keys(ctx.propsDestructuredBindings)
+      )});\n`
     )
   }
   // inject temp variables for async context preservation
@@ -1864,7 +1683,9 @@ export function compileScript(
   }
 
   const destructureElements =
-    hasDefineExposeCall || !options.inlineTemplate ? [`expose: __expose`] : []
+    ctx.hasDefineExposeCall || !options.inlineTemplate
+      ? [`expose: __expose`]
+      : []
   if (emitIdentifier) {
     destructureElements.push(
       emitIdentifier === `emit` ? `emit` : `emit: ${emitIdentifier}`
@@ -1876,7 +1697,10 @@ export function compileScript(
 
   // 10. generate return statement
   let returned
-  if (!options.inlineTemplate || (!sfc.template && hasDefaultExportRender)) {
+  if (
+    !options.inlineTemplate ||
+    (!sfc.template && ctx.hasDefaultExportRender)
+  ) {
     // non-inline mode, or has manual render in normal <script>
     // return bindings from script and script setup
     const allBindings: Record<string, any> = {
@@ -1986,7 +1810,7 @@ export function compileScript(
 
   // 11. finalize default export
   let runtimeOptions = ``
-  if (!hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
+  if (!ctx.hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
     const match = filename.match(/([^/\\]+)\.\w+$/)
     if (match) {
       runtimeOptions += `\n  __name: '${match[1]}',`
@@ -2012,7 +1836,7 @@ export function compileScript(
   // <script setup> components are closed by default. If the user did not
   // explicitly call `defineExpose`, call expose() with no args.
   const exposeCall =
-    hasDefineExposeCall || options.inlineTemplate ? `` : `  __expose();\n`
+    ctx.hasDefineExposeCall || options.inlineTemplate ? `` : `  __expose();\n`
   // wrap setup code with function.
   if (isTS) {
     // for TS, make sure the exported type is still valid type with
@@ -2893,14 +2717,3 @@ export function hmrShouldReload(
 
   return false
 }
-
-export function resolveObjectKey(node: Node, computed: boolean) {
-  switch (node.type) {
-    case 'StringLiteral':
-    case 'NumericLiteral':
-      return node.value
-    case 'Identifier':
-      if (!computed) return node.name
-  }
-  return undefined
-}
diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts
new file mode 100644 (file)
index 0000000..0afd50c
--- /dev/null
@@ -0,0 +1,153 @@
+import { Expression, Node, ObjectPattern, Program } from '@babel/types'
+import { SFCDescriptor } from '../parse'
+import { generateCodeFrame } from '@vue/shared'
+import { PropsDeclType } from './defineProps'
+import { parse as babelParse, ParserOptions, ParserPlugin } from '@babel/parser'
+import { SFCScriptCompileOptions } from '../compileScript'
+import MagicString from 'magic-string'
+
+export type PropsDestructureBindings = Record<
+  string, // public prop key
+  {
+    local: string // local identifier, may be different
+    default?: Expression
+  }
+>
+
+export class ScriptCompileContext {
+  isJS: boolean
+  isTS: boolean
+
+  scriptAst: Program | null
+  scriptSetupAst: Program | null
+
+  s = new MagicString(this.descriptor.source)
+
+  startOffset = this.descriptor.scriptSetup?.loc.start.offset
+  endOffset = this.descriptor.scriptSetup?.loc.end.offset
+
+  scriptStartOffset = this.descriptor.script?.loc.start.offset
+  scriptEndOffset = this.descriptor.script?.loc.end.offset
+
+  // macros presence check
+  hasDefinePropsCall = false
+  hasDefineEmitCall = false
+  hasDefineExposeCall = false
+  hasDefaultExportName = false
+  hasDefaultExportRender = false
+  hasDefineOptionsCall = false
+  hasDefineSlotsCall = false
+  hasDefineModelCall = false
+
+  // defineProps
+  propsIdentifier: string | undefined
+  propsRuntimeDecl: Node | undefined
+  propsTypeDecl: PropsDeclType | undefined
+  propsDestructureDecl: ObjectPattern | undefined
+  propsDestructuredBindings: PropsDestructureBindings = Object.create(null)
+  propsDestructureRestId: string | undefined
+  propsRuntimeDefaults: Node | undefined
+
+  constructor(
+    public descriptor: SFCDescriptor,
+    public options: SFCScriptCompileOptions
+  ) {
+    const { script, scriptSetup } = descriptor
+    const scriptLang = script && script.lang
+    const scriptSetupLang = scriptSetup && scriptSetup.lang
+
+    this.isJS =
+      scriptLang === 'js' ||
+      scriptLang === 'jsx' ||
+      scriptSetupLang === 'js' ||
+      scriptSetupLang === 'jsx'
+    this.isTS =
+      scriptLang === 'ts' ||
+      scriptLang === 'tsx' ||
+      scriptSetupLang === 'ts' ||
+      scriptSetupLang === 'tsx'
+
+    // resolve parser plugins
+    const plugins: ParserPlugin[] = []
+    if (!this.isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
+      plugins.push('jsx')
+    } else {
+      // If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins
+      if (options.babelParserPlugins)
+        options.babelParserPlugins = options.babelParserPlugins.filter(
+          n => n !== 'jsx'
+        )
+    }
+    if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
+    if (this.isTS) {
+      plugins.push('typescript')
+      if (!plugins.includes('decorators')) {
+        plugins.push('decorators-legacy')
+      }
+    }
+
+    function parse(
+      input: string,
+      options: ParserOptions,
+      offset: number
+    ): Program {
+      try {
+        return babelParse(input, options).program
+      } catch (e: any) {
+        e.message = `[@vue/compiler-sfc] ${e.message}\n\n${
+          descriptor.filename
+        }\n${generateCodeFrame(
+          descriptor.source,
+          e.pos + offset,
+          e.pos + offset + 1
+        )}`
+        throw e
+      }
+    }
+
+    this.scriptAst =
+      this.descriptor.script &&
+      parse(
+        this.descriptor.script.content,
+        {
+          plugins,
+          sourceType: 'module'
+        },
+        this.scriptStartOffset!
+      )
+
+    this.scriptSetupAst =
+      this.descriptor.scriptSetup &&
+      parse(
+        this.descriptor.scriptSetup!.content,
+        {
+          plugins: [...plugins, 'topLevelAwait'],
+          sourceType: 'module'
+        },
+        this.startOffset!
+      )
+  }
+
+  getString(node: Node, scriptSetup = true): string {
+    const block = scriptSetup
+      ? this.descriptor.scriptSetup!
+      : this.descriptor.script!
+    return block.content.slice(node.start!, node.end!)
+  }
+
+  error(
+    msg: string,
+    node: Node,
+    end: number = node.end! + this.startOffset!
+  ): never {
+    throw new Error(
+      `[@vue/compiler-sfc] ${msg}\n\n${
+        this.descriptor.filename
+      }\n${generateCodeFrame(
+        this.descriptor.source,
+        node.start! + this.startOffset!,
+        end
+      )}`
+    )
+  }
+}
diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts
new file mode 100644 (file)
index 0000000..731f820
--- /dev/null
@@ -0,0 +1,148 @@
+import {
+  Node,
+  LVal,
+  Identifier,
+  TSTypeLiteral,
+  TSInterfaceBody
+} from '@babel/types'
+import { isCallOf } from '@vue/compiler-dom'
+import { ScriptCompileContext } from './context'
+import { resolveObjectKey } from './utils'
+import { resolveQualifiedType } from './resolveType'
+
+export const DEFINE_PROPS = 'defineProps'
+export const WITH_DEFAULTS = 'withDefaults'
+
+export type PropsDeclType = (TSTypeLiteral | TSInterfaceBody) & {
+  __fromNormalScript?: boolean | null
+}
+
+export function processDefineProps(
+  ctx: ScriptCompileContext,
+  node: Node,
+  declId?: LVal
+) {
+  if (!isCallOf(node, DEFINE_PROPS)) {
+    return processWithDefaults(ctx, node, declId)
+  }
+
+  if (ctx.hasDefinePropsCall) {
+    ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
+  }
+  ctx.hasDefinePropsCall = true
+
+  ctx.propsRuntimeDecl = node.arguments[0]
+
+  // call has type parameters - infer runtime types from it
+  if (node.typeParameters) {
+    if (ctx.propsRuntimeDecl) {
+      ctx.error(
+        `${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
+          `at the same time. Use one or the other.`,
+        node
+      )
+    }
+
+    const rawDecl = node.typeParameters.params[0]
+    ctx.propsTypeDecl = resolveQualifiedType(
+      ctx,
+      rawDecl,
+      node => node.type === 'TSTypeLiteral'
+    ) as PropsDeclType | undefined
+    if (!ctx.propsTypeDecl) {
+      ctx.error(
+        `type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
+          `or a reference to an interface or literal type.`,
+        rawDecl
+      )
+    }
+  }
+
+  if (declId) {
+    // handle props destructure
+    if (declId.type === 'ObjectPattern') {
+      ctx.propsDestructureDecl = declId
+      for (const prop of declId.properties) {
+        if (prop.type === 'ObjectProperty') {
+          const propKey = resolveObjectKey(prop.key, prop.computed)
+
+          if (!propKey) {
+            ctx.error(
+              `${DEFINE_PROPS}() destructure cannot use computed key.`,
+              prop.key
+            )
+          }
+
+          if (prop.value.type === 'AssignmentPattern') {
+            // default value { foo = 123 }
+            const { left, right } = prop.value
+            if (left.type !== 'Identifier') {
+              ctx.error(
+                `${DEFINE_PROPS}() destructure does not support nested patterns.`,
+                left
+              )
+            }
+            // store default value
+            ctx.propsDestructuredBindings[propKey] = {
+              local: left.name,
+              default: right
+            }
+          } else if (prop.value.type === 'Identifier') {
+            // simple destructure
+            ctx.propsDestructuredBindings[propKey] = {
+              local: prop.value.name
+            }
+          } else {
+            ctx.error(
+              `${DEFINE_PROPS}() destructure does not support nested patterns.`,
+              prop.value
+            )
+          }
+        } else {
+          // rest spread
+          ctx.propsDestructureRestId = (prop.argument as Identifier).name
+        }
+      }
+    } else {
+      ctx.propsIdentifier = ctx.getString(declId)
+    }
+  }
+
+  return true
+}
+
+function processWithDefaults(
+  ctx: ScriptCompileContext,
+  node: Node,
+  declId?: LVal
+): boolean {
+  if (!isCallOf(node, WITH_DEFAULTS)) {
+    return false
+  }
+  if (processDefineProps(ctx, node.arguments[0], declId)) {
+    if (ctx.propsRuntimeDecl) {
+      ctx.error(
+        `${WITH_DEFAULTS} can only be used with type-based ` +
+          `${DEFINE_PROPS} declaration.`,
+        node
+      )
+    }
+    if (ctx.propsDestructureDecl) {
+      ctx.error(
+        `${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
+          `Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`,
+        node.callee
+      )
+    }
+    ctx.propsRuntimeDefaults = node.arguments[1]
+    if (!ctx.propsRuntimeDefaults) {
+      ctx.error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
+    }
+  } else {
+    ctx.error(
+      `${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
+      node.arguments[0] || node
+    )
+  }
+  return true
+}
diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts
new file mode 100644 (file)
index 0000000..f4e0c6b
--- /dev/null
@@ -0,0 +1,114 @@
+import { Node, Statement, TSInterfaceBody, TSTypeElement } from '@babel/types'
+import { FromNormalScript } from './utils'
+import { ScriptCompileContext } from './context'
+
+/**
+ * Resolve a type Node into
+ */
+export function resolveType() {}
+
+export function resolveQualifiedType(
+  ctx: ScriptCompileContext,
+  node: Node,
+  qualifier: (node: Node) => boolean
+): Node | undefined {
+  if (qualifier(node)) {
+    return node
+  }
+  if (node.type === 'TSTypeReference' && node.typeName.type === 'Identifier') {
+    const refName = node.typeName.name
+    const { scriptAst, scriptSetupAst } = ctx
+    const body = scriptAst
+      ? [...scriptSetupAst!.body, ...scriptAst.body]
+      : scriptSetupAst!.body
+    for (let i = 0; i < body.length; i++) {
+      const node = body[i]
+      let qualified = isQualifiedType(
+        node,
+        qualifier,
+        refName
+      ) as TSInterfaceBody
+      if (qualified) {
+        const extendsTypes = resolveExtendsType(body, node, qualifier)
+        if (extendsTypes.length) {
+          const bodies: TSTypeElement[] = [...qualified.body]
+          filterExtendsType(extendsTypes, bodies)
+          qualified.body = bodies
+        }
+        ;(qualified as FromNormalScript<Node>).__fromNormalScript =
+          scriptAst && i >= scriptSetupAst!.body.length
+        return qualified
+      }
+    }
+  }
+}
+
+function isQualifiedType(
+  node: Node,
+  qualifier: (node: Node) => boolean,
+  refName: String
+): Node | undefined {
+  if (node.type === 'TSInterfaceDeclaration' && node.id.name === refName) {
+    return node.body
+  } else if (
+    node.type === 'TSTypeAliasDeclaration' &&
+    node.id.name === refName &&
+    qualifier(node.typeAnnotation)
+  ) {
+    return node.typeAnnotation
+  } else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
+    return isQualifiedType(node.declaration, qualifier, refName)
+  }
+}
+
+function resolveExtendsType(
+  body: Statement[],
+  node: Node,
+  qualifier: (node: Node) => boolean,
+  cache: Array<Node> = []
+): Array<Node> {
+  if (node.type === 'TSInterfaceDeclaration' && node.extends) {
+    node.extends.forEach(extend => {
+      if (
+        extend.type === 'TSExpressionWithTypeArguments' &&
+        extend.expression.type === 'Identifier'
+      ) {
+        for (const node of body) {
+          const qualified = isQualifiedType(
+            node,
+            qualifier,
+            extend.expression.name
+          )
+          if (qualified) {
+            cache.push(qualified)
+            resolveExtendsType(body, node, qualifier, cache)
+            return cache
+          }
+        }
+      }
+    })
+  }
+  return cache
+}
+
+// filter all extends types to keep the override declaration
+function filterExtendsType(extendsTypes: Node[], bodies: TSTypeElement[]) {
+  extendsTypes.forEach(extend => {
+    const body = (extend as TSInterfaceBody).body
+    body.forEach(newBody => {
+      if (
+        newBody.type === 'TSPropertySignature' &&
+        newBody.key.type === 'Identifier'
+      ) {
+        const name = newBody.key.name
+        const hasOverride = bodies.some(
+          seenBody =>
+            seenBody.type === 'TSPropertySignature' &&
+            seenBody.key.type === 'Identifier' &&
+            seenBody.key.name === name
+        )
+        if (!hasOverride) bodies.push(newBody)
+      }
+    })
+  })
+}
diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts
new file mode 100644 (file)
index 0000000..1a07343
--- /dev/null
@@ -0,0 +1,14 @@
+import { Node } from '@babel/types'
+
+export type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
+
+export function resolveObjectKey(node: Node, computed: boolean) {
+  switch (node.type) {
+    case 'StringLiteral':
+    case 'NumericLiteral':
+      return node.value
+    case 'Identifier':
+      if (!computed) return node.name
+  }
+  return undefined
+}