]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: defineContext
authorEvan You <yyx990803@gmail.com>
Thu, 12 Nov 2020 00:40:27 +0000 (19:40 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 12 Nov 2020 00:40:27 +0000 (19:40 -0500)
packages/compiler-sfc/src/compileScript.ts
packages/runtime-core/src/helpers/useSetupContext.ts [new file with mode: 0644]
packages/runtime-core/src/index.ts

index 8978196119882d7e86e75397185bc355ba01c56d..ebb60a29c058ed9532a4ab15c9392a62e25e0f19 100644 (file)
@@ -10,8 +10,6 @@ import {
   ObjectExpression,
   ArrayPattern,
   Identifier,
-  ExpressionStatement,
-  ArrowFunctionExpression,
   ExportSpecifier,
   Function as FunctionNode,
   TSType,
@@ -29,6 +27,8 @@ import { RawSourceMap } from 'source-map'
 import { genCssVarsCode, injectCssVarsCalls } from './genCssVars'
 import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
 
+const CTX_FN_NAME = 'defineContext'
+
 export interface SFCScriptCompileOptions {
   /**
    * https://babeljs.io/docs/en/babel-parser#plugins
@@ -127,13 +127,21 @@ export function compileScript(
   const defaultTempVar = `__default__`
   const bindingMetadata: BindingMetadata = {}
   const helperImports: Set<string> = new Set()
-  const userImports: Record<string, string> = Object.create(null)
+  const userImports: Record<
+    string,
+    {
+      imported: string | null
+      source: string
+    }
+  > = Object.create(null)
   const setupBindings: Record<string, boolean> = Object.create(null)
   const refBindings: Record<string, boolean> = Object.create(null)
   const refIdentifiers: Set<Identifier> = new Set()
   const enableRefSugar = options.refSugar !== false
   let defaultExport: Node | undefined
-  let needDefaultExportRefCheck = false
+  let setupContextExp: string | undefined
+  let setupContextArg: Node | undefined
+  let setupContextType: TSTypeLiteral | undefined
   let hasAwait = false
 
   const s = new MagicString(source)
@@ -314,10 +322,16 @@ export function compileScript(
     for (const node of scriptAst) {
       if (node.type === 'ImportDeclaration') {
         // record imports for dedupe
-        for (const {
-          local: { name }
-        } of node.specifiers) {
-          userImports[name] = node.source.value
+        for (const specifier of node.specifiers) {
+          const name = specifier.local.name
+          const imported =
+            specifier.type === 'ImportSpecifier' &&
+            specifier.imported.type === 'Identifier' &&
+            specifier.imported.name
+          userImports[name] = {
+            imported: imported || null,
+            source: node.source.value
+          }
         }
       } else if (node.type === 'ExportDefaultDeclaration') {
         // export default
@@ -367,75 +381,17 @@ export function compileScript(
     }
   }
 
-  // 2. check <script setup="xxx"> function signature
-  const setupValue = scriptSetup.setup
-  const hasExplicitSignature = typeof setupValue === 'string'
-
-  let propsIdentifier: string | undefined
-  let emitIdentifier: string | undefined
-  let slotsIdentifier: string | undefined
-  let attrsIdentifier: string | undefined
-
   let propsType = `{}`
   let emitType = `(e: string, ...args: any[]) => void`
   let slotsType = `Slots`
   let attrsType = `Record<string, any>`
 
-  let propsASTNode
-  let setupCtxASTNode
-
   // props/emits declared via types
   const typeDeclaredProps: Record<string, PropTypeData> = {}
   const typeDeclaredEmits: Set<string> = new Set()
   // record declared types for runtime props type generation
   const declaredTypes: Record<string, string[]> = {}
 
-  // <script setup="xxx">
-  if (hasExplicitSignature) {
-    let signatureAST
-    try {
-      signatureAST = _parse(`(${setupValue})=>{}`, { plugins }).program.body[0]
-    } catch (e) {
-      throw new Error(
-        `[@vue/compiler-sfc] Invalid <script setup> signature: ${setupValue}\n\n${generateCodeFrame(
-          source,
-          startOffset - 1,
-          startOffset
-        )}`
-      )
-    }
-
-    if (isTS) {
-      // <script setup="xxx" lang="ts">
-      // parse the signature to extract the identifiers users are assigning to
-      // the arguments. They are needed for matching type delcarations.
-      const params = ((signatureAST as ExpressionStatement)
-        .expression as ArrowFunctionExpression).params
-      if (params[0] && params[0].type === 'Identifier') {
-        propsASTNode = params[0]
-        propsIdentifier = propsASTNode.name
-      }
-      if (params[1] && params[1].type === 'ObjectPattern') {
-        setupCtxASTNode = params[1]
-        for (const p of params[1].properties) {
-          if (
-            p.type === 'ObjectProperty' &&
-            p.key.type === 'Identifier' &&
-            p.value.type === 'Identifier'
-          ) {
-            if (p.key.name === 'emit') {
-              emitIdentifier = p.value.name
-            } else if (p.key.name === 'slots') {
-              slotsIdentifier = p.value.name
-            } else if (p.key.name === 'attrs') {
-              attrsIdentifier = p.value.name
-            }
-          }
-        }
-      }
-    }
-  }
-
   // 3. parse <script setup> and  walk over top level statements
   const scriptSetupAst = parse(
     scriptSetup.content,
@@ -503,15 +459,35 @@ export function compileScript(
       let prev
       let removed = 0
       for (const specifier of node.specifiers) {
-        if (userImports[specifier.local.name]) {
-          // already imported in <script setup>, dedupe
+        const local = specifier.local.name
+        const imported =
+          specifier.type === 'ImportSpecifier' &&
+          specifier.imported.type === 'Identifier' &&
+          specifier.imported.name
+        const source = node.source.value
+        const existing = userImports[local]
+        if (source === 'vue' && imported === CTX_FN_NAME) {
           removed++
           s.remove(
             prev ? prev.end! + startOffset : specifier.start! + startOffset,
             specifier.end! + startOffset
           )
+        } else if (existing) {
+          if (existing.source === source && existing.imported === imported) {
+            // already imported in <script setup>, dedupe
+            removed++
+            s.remove(
+              prev ? prev.end! + startOffset : specifier.start! + startOffset,
+              specifier.end! + startOffset
+            )
+          } else {
+            error(`different imports aliased to same local name.`, specifier)
+          }
         } else {
-          userImports[specifier.local.name] = node.source.value
+          userImports[local] = {
+            imported: imported || null,
+            source: node.source.value
+          }
         }
         prev = specifier
       }
@@ -520,37 +496,42 @@ export function compileScript(
       }
     }
 
-    if (
-      (node.type === 'ExportNamedDeclaration' && node.exportKind !== 'type') ||
-      node.type === 'ExportAllDeclaration'
-    ) {
-      error(
-        `<script setup> cannot contain non-type named or * exports. ` +
-          `If you are using a previous version of <script setup>, please ` +
-          `consult the updated RFC at https://github.com/vuejs/rfcs/pull/227.`,
-        node
-      )
-    }
+    if (node.type === 'VariableDeclaration' && !node.declare) {
+      for (const decl of node.declarations) {
+        if (
+          decl.init &&
+          decl.init.type === 'CallExpression' &&
+          decl.init.callee.type === 'Identifier' &&
+          decl.init.callee.name === CTX_FN_NAME
+        ) {
+          if (node.declarations.length === 1) {
+            s.remove(node.start! + startOffset, node.end! + startOffset)
+          } else {
+            s.remove(decl.start! + startOffset, decl.end! + startOffset)
+          }
+          setupContextExp = scriptSetup.content.slice(
+            decl.id.start!,
+            decl.id.end!
+          )
+          setupContextArg = decl.init.arguments[0]
 
-    if (node.type === 'ExportDefaultDeclaration') {
-      if (defaultExport) {
-        // <script> already has export default
-        error(
-          `Default export is already declared in normal <script>.`,
-          node,
-          node.start! + startOffset + `export default`.length
-        )
+          // useSetupContext() has type parameters - infer runtime types from it
+          if (decl.init.typeParameters) {
+            const typeArg = decl.init.typeParameters.params[0]
+            if (typeArg.type === 'TSTypeLiteral') {
+              setupContextType = typeArg
+            } else {
+              error(
+                `type argument passed to ${CTX_FN_NAME}() must be a literal type.`,
+                typeArg
+              )
+            }
+          }
+        }
       }
-      // 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
-      needDefaultExportRefCheck = true
     }
 
+    // walk decalrations to record declared bindings
     if (
       (node.type === 'VariableDeclaration' ||
         node.type === 'FunctionDeclaration' ||
@@ -563,47 +544,6 @@ export function compileScript(
     // Type declarations
     if (node.type === 'VariableDeclaration' && node.declare) {
       s.remove(start, end)
-      for (const { id } of node.declarations) {
-        if (id.type === 'Identifier') {
-          if (
-            id.typeAnnotation &&
-            id.typeAnnotation.type === 'TSTypeAnnotation'
-          ) {
-            const typeNode = id.typeAnnotation.typeAnnotation
-            const typeString = source.slice(
-              typeNode.start! + startOffset,
-              typeNode.end! + startOffset
-            )
-            if (typeNode.type === 'TSTypeLiteral') {
-              if (id.name === propsIdentifier) {
-                propsType = typeString
-                extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
-              } else if (id.name === slotsIdentifier) {
-                slotsType = typeString
-              } else if (id.name === attrsIdentifier) {
-                attrsType = typeString
-              }
-            } else if (
-              id.name === emitIdentifier &&
-              typeNode.type === 'TSFunctionType'
-            ) {
-              emitType = typeString
-              extractRuntimeEmits(typeNode, typeDeclaredEmits)
-            }
-          }
-        }
-      }
-    }
-
-    if (
-      node.type === 'TSDeclareFunction' &&
-      node.id &&
-      node.id.name === emitIdentifier
-    ) {
-      const index = node.id.start! + startOffset
-      s.overwrite(index, index + emitIdentifier.length, '__emit__')
-      emitType = `typeof __emit__`
-      extractRuntimeEmits(node, typeDeclaredEmits)
     }
 
     // move all type declarations to outer scope
@@ -618,7 +558,7 @@ export function compileScript(
     // walk statements & named exports / variable declarations for top level
     // await
     if (
-      node.type === 'VariableDeclaration' ||
+      (node.type === 'VariableDeclaration' && !node.declare) ||
       node.type.endsWith('Statement')
     ) {
       ;(walk as any)(node, {
@@ -632,6 +572,19 @@ export function compileScript(
         }
       })
     }
+
+    if (
+      (node.type === 'ExportNamedDeclaration' && node.exportKind !== 'type') ||
+      node.type === 'ExportAllDeclaration' ||
+      node.type === 'ExportDefaultDeclaration'
+    ) {
+      error(
+        `<script setup> cannot contain ES module exports. ` +
+          `If you are using a previous version of <script setup>, please ` +
+          `consult the updated RFC at https://github.com/vuejs/rfcs/pull/227.`,
+        node
+      )
+    }
   }
 
   // 4. Do a full walk to rewrite identifiers referencing let exports with ref
@@ -660,13 +613,47 @@ export function compileScript(
     }
   }
 
-  // 5. check default export to make sure it doesn't reference setup scope
+  // 5. extract runtime props/emits code from setup context type
+  if (setupContextType) {
+    for (const m of setupContextType.members) {
+      if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
+        const typeNode = m.typeAnnotation!.typeAnnotation
+        const typeString = scriptSetup.content.slice(
+          typeNode.start!,
+          typeNode.end!
+        )
+        if (m.key.name === 'props') {
+          propsType = typeString
+          if (typeNode.type === 'TSTypeLiteral') {
+            extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
+          } else {
+            // TODO be able to trace references
+            error(`props type must be an object literal type`, typeNode)
+          }
+        } else if (m.key.name === 'emit') {
+          emitType = typeString
+          if (typeNode.type === 'TSFunctionType') {
+            extractRuntimeEmits(typeNode, typeDeclaredEmits)
+          } else {
+            // TODO be able to trace references
+            error(`emit type must be a function type`, typeNode)
+          }
+        } else if (m.key.name === 'attrs') {
+          attrsType = typeString
+        } else if (m.key.name === 'slots') {
+          slotsType = typeString
+        }
+      }
+    }
+  }
+
+  // 5. check useSetupContext args to make sure it doesn't reference setup scope
   // variables
-  if (needDefaultExportRefCheck) {
-    walkIdentifiers(defaultExport!, id => {
+  if (setupContextArg) {
+    walkIdentifiers(setupContextArg, id => {
       if (setupBindings[id.name]) {
         error(
-          `\`export default\` in <script setup> cannot reference locally ` +
+          `\`${CTX_FN_NAME}()\` 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 ` +
@@ -697,31 +684,30 @@ export function compileScript(
   }
 
   // 7. finalize setup argument signature.
-  let args = ``
+  let args = setupContextExp ? `__props, ${setupContextExp}` : ``
   if (isTS) {
     if (slotsType === 'Slots') {
       helperImports.add('Slots')
     }
-    const ctxType = `{
+    args += `: {
+  props: ${propsType},
   emit: ${emitType},
   slots: ${slotsType},
   attrs: ${attrsType}
 }`
-    if (hasExplicitSignature) {
-      // inject types to user signature
-      args = setupValue as string
-      const ss = new MagicString(args)
-      if (propsASTNode) {
-        // compensate for () wraper offset
-        ss.appendRight(propsASTNode.end! - 1, `: ${propsType}`)
-      }
-      if (setupCtxASTNode) {
-        ss.appendRight(setupCtxASTNode.end! - 1!, `: ${ctxType}`)
-      }
-      args = ss.toString()
-    }
-  } else if (hasExplicitSignature) {
-    args = setupValue as string
+    // if (hasExplicitSignature) {
+    //   // inject types to user signature
+    //   args = setupValue as string
+    //   const ss = new MagicString(args)
+    //   if (propsASTNode) {
+    //     // compensate for () wraper offset
+    //     ss.appendRight(propsASTNode.end! - 1, `: ${propsType}`)
+    //   }
+    //   if (setupCtxASTNode) {
+    //     ss.appendRight(setupCtxASTNode.end! - 1!, `: ${ctxType}`)
+    //   }
+    //   args = ss.toString()
+    // }
   }
 
   // 8. wrap setup code with function.
@@ -732,7 +718,10 @@ export function compileScript(
     `\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n`
   )
 
-  const allBindings = { ...userImports, ...setupBindings }
+  const allBindings = { ...setupBindings }
+  for (const key in userImports) {
+    allBindings[key] = true
+  }
 
   // 9. inject `useCssVars` calls
   if (hasCssVars) {
@@ -753,8 +742,8 @@ export function compileScript(
     Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
   }
   if (options.inlineTemplate) {
-    for (const [key, value] of Object.entries(userImports)) {
-      bindingMetadata[key] = value.endsWith('.vue')
+    for (const [key, { source }] of Object.entries(userImports)) {
+      bindingMetadata[key] = source.endsWith('.vue')
         ? 'component-import'
         : 'setup'
     }
@@ -769,7 +758,6 @@ export function compileScript(
   for (const key in typeDeclaredProps) {
     bindingMetadata[key] = 'props'
   }
-  Object.assign(bindingMetadata, analyzeScriptBindings(scriptSetupAst))
 
   // 11. generate return statement
   let returned
@@ -833,7 +821,9 @@ export function compileScript(
   }
 
   // 13. finalize Vue helper imports
-  const helpers = [...helperImports].filter(i => userImports[i] !== 'vue')
+  // TODO account for cases where user imports a helper with the same name
+  // from a non-vue source
+  const helpers = [...helperImports].filter(i => !userImports[i])
   if (helpers.length) {
     s.prepend(`import { ${helpers.join(', ')} } from 'vue'\n`)
   }
diff --git a/packages/runtime-core/src/helpers/useSetupContext.ts b/packages/runtime-core/src/helpers/useSetupContext.ts
new file mode 100644 (file)
index 0000000..82fea43
--- /dev/null
@@ -0,0 +1,15 @@
+import { EMPTY_OBJ } from '@vue/shared'
+import { Slots } from '../componentSlots'
+
+interface DefaultContext {
+  props: Record<string, unknown>
+  attrs: Record<string, unknown>
+  emit: (...args: any[]) => void
+  slots: Slots
+}
+
+export function useSetupContext<T extends Partial<DefaultContext> = {}>(
+  opts?: any // TODO infer
+): { [K in keyof DefaultContext]: T[K] extends {} ? T[K] : DefaultContext[K] } {
+  return EMPTY_OBJ as any
+}
index b711f8895e8161ab21121f45d7ec1c73baed3d6f..236a1d6db6cdf9f08a9a918ecc1c4fd73b81521b 100644 (file)
@@ -261,6 +261,8 @@ import {
   setCurrentRenderingInstance
 } from './componentRenderUtils'
 import { isVNode, normalizeVNode } from './vnode'
+import { Slots } from './componentSlots'
+import { EMPTY_OBJ } from '@vue/shared/src'
 
 const _ssrUtils = {
   createComponentInstance,