]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: defineContext()
authorEvan You <yyx990803@gmail.com>
Thu, 12 Nov 2020 19:10:39 +0000 (14:10 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 12 Nov 2020 19:10:39 +0000 (14:10 -0500)
packages/compiler-core/src/options.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/src/compileScript.ts
packages/runtime-core/src/apiDefineContext.ts [new file with mode: 0644]
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/helpers/useSetupContext.ts [deleted file]
packages/runtime-core/src/index.ts

index 37641a458b9abe680f4947c7ab72e3cc1625d35d..05c3d2cd08207584172e91a29cf979d3b0749334 100644 (file)
@@ -62,7 +62,7 @@ export type HoistTransform = (
 ) => void
 
 export interface BindingMetadata {
-  [key: string]: 'data' | 'props' | 'setup' | 'options' | 'component-import'
+  [key: string]: 'data' | 'props' | 'setup' | 'options' | 'setup-raw'
 }
 
 interface SharedTransformCodegenOptions {
index 1025e602f49d2a73e7c4a68dcd2cbcb18a6c4454..937e3a71da8642fabd0b3d82ff2378843afe7572 100644 (file)
@@ -270,9 +270,9 @@ export function resolveComponentType(
           `${context.helperString(UNREF)}(${tagFromSetup})`
         : `$setup[${JSON.stringify(tagFromSetup)}]`
     }
-    const tagFromImport = checkType('component-import')
+    const tagFromImport = checkType('setup-raw')
     if (tagFromImport) {
-      // imports can be used as-is
+      // raw setup bindings (e.g. imports) can be used as-is
       return tagFromImport
     }
   }
index 924d217a893a548481894c2e5933ef816e40649b..87c6c65b806d70a727ec975e086c4f45fb510eed 100644 (file)
@@ -105,7 +105,11 @@ export function processExpression(
       // setup inline mode
       if (type === 'setup') {
         return `${context.helperString(UNREF)}(${raw})`
-      } else if (type === 'component-import') {
+      } else if (type === 'props') {
+        // use __props which is generated by compileScript so in ts mode
+        // it gets correct type
+        return `__props.${raw}`
+      } else if (type === 'setup-raw') {
         return raw
       }
     }
index ebb60a29c058ed9532a4ab15c9392a62e25e0f19..4306fa37b65de1fc52f43d0d859bb7076471cc2b 100644 (file)
@@ -134,13 +134,13 @@ export function compileScript(
       source: string
     }
   > = Object.create(null)
-  const setupBindings: Record<string, boolean> = Object.create(null)
-  const refBindings: Record<string, boolean> = Object.create(null)
+  const setupBindings: Record<string, 'var' | 'const'> = Object.create(null)
+  const refBindings: Record<string, 'var'> = Object.create(null)
   const refIdentifiers: Set<Identifier> = new Set()
   const enableRefSugar = options.refSugar !== false
   let defaultExport: Node | undefined
   let setupContextExp: string | undefined
-  let setupContextArg: Node | undefined
+  let setupContextArg: ObjectExpression | undefined
   let setupContextType: TSTypeLiteral | undefined
   let hasAwait = false
 
@@ -222,7 +222,7 @@ export function compileScript(
     if (id.name[0] === '$') {
       error(`ref variable identifiers cannot start with $.`, id)
     }
-    refBindings[id.name] = setupBindings[id.name] = true
+    refBindings[id.name] = setupBindings[id.name] = 'var'
     refIdentifiers.add(id)
   }
 
@@ -513,10 +513,25 @@ export function compileScript(
             decl.id.start!,
             decl.id.end!
           )
-          setupContextArg = decl.init.arguments[0]
+          const optsArg = decl.init.arguments[0]
+          if (optsArg.type === 'ObjectExpression') {
+            setupContextArg = optsArg
+          } else {
+            error(
+              `${CTX_FN_NAME}() argument must be an object literal.`,
+              optsArg
+            )
+          }
 
           // useSetupContext() has type parameters - infer runtime types from it
           if (decl.init.typeParameters) {
+            if (setupContextArg) {
+              error(
+                `${CTX_FN_NAME}() cannot accept both type and non-type arguments ` +
+                  `at the same time. Use one or the other.`,
+                decl.init
+              )
+            }
             const typeArg = decl.init.typeParameters.params[0]
             if (typeArg.type === 'TSTypeLiteral') {
               setupContextType = typeArg
@@ -685,7 +700,7 @@ export function compileScript(
 
   // 7. finalize setup argument signature.
   let args = setupContextExp ? `__props, ${setupContextExp}` : ``
-  if (isTS) {
+  if (setupContextExp && setupContextType) {
     if (slotsType === 'Slots') {
       helperImports.add('Slots')
     }
@@ -695,32 +710,11 @@ export function compileScript(
   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()
-    // }
   }
 
-  // 8. wrap setup code with function.
-  // export the content of <script setup> as a named export, `setup`.
-  // this allows `import { setup } from '*.vue'` for testing purposes.
-  s.prependLeft(
-    startOffset,
-    `\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n`
-  )
-
   const allBindings = { ...setupBindings }
   for (const key in userImports) {
-    allBindings[key] = true
+    allBindings[key] = 'var'
   }
 
   // 9. inject `useCssVars` calls
@@ -741,23 +735,27 @@ export function compileScript(
   if (scriptAst) {
     Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
   }
+  if (setupContextType) {
+    for (const key in typeDeclaredProps) {
+      bindingMetadata[key] = 'props'
+    }
+  }
+  if (setupContextArg) {
+    Object.assign(bindingMetadata, analyzeBindingsFromOptions(setupContextArg))
+  }
   if (options.inlineTemplate) {
     for (const [key, { source }] of Object.entries(userImports)) {
-      bindingMetadata[key] = source.endsWith('.vue')
-        ? 'component-import'
-        : 'setup'
+      bindingMetadata[key] = source.endsWith('.vue') ? 'setup-raw' : 'setup'
     }
     for (const key in setupBindings) {
-      bindingMetadata[key] = 'setup'
+      bindingMetadata[key] =
+        setupBindings[key] === 'var' ? 'setup' : 'setup-raw'
     }
   } else {
     for (const key in allBindings) {
       bindingMetadata[key] = 'setup'
     }
   }
-  for (const key in typeDeclaredProps) {
-    bindingMetadata[key] = 'props'
-  }
 
   // 11. generate return statement
   let returned
@@ -798,6 +796,16 @@ export function compileScript(
   s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
 
   // 12. finalize default export
+  let runtimeOptions = ``
+  if (setupContextArg) {
+    runtimeOptions = `\n  ${scriptSetup.content
+      .slice(setupContextArg.start! + 1, setupContextArg.end! - 1)
+      .trim()},`
+  } else if (setupContextType) {
+    runtimeOptions =
+      genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits)
+  }
+
   if (isTS) {
     // for TS, make sure the exported type is still valid type with
     // correct props information
@@ -805,18 +813,34 @@ export function compileScript(
     // we have to use object spread for types to be merged properly
     // user's TS setting should compile it down to proper targets
     const def = defaultExport ? `\n  ...${defaultTempVar},` : ``
-    const runtimeProps = genRuntimeProps(typeDeclaredProps)
-    const runtimeEmits = genRuntimeEmits(typeDeclaredEmits)
-    s.append(
-      `export default defineComponent({${def}${runtimeProps}${runtimeEmits}\n  setup\n})`
+    // wrap setup code with function.
+    // export the content of <script setup> as a named export, `setup`.
+    // this allows `import { setup } from '*.vue'` for testing purposes.
+    s.prependLeft(
+      startOffset,
+      `\nexport default defineComponent({${def}${runtimeOptions}\n  ${
+        hasAwait ? `async ` : ``
+      }setup(${args}) {\n`
     )
+    s.append(`})`)
   } else {
     if (defaultExport) {
+      // can't rely on spread operator in non ts mode
+      s.prependLeft(
+        startOffset,
+        `\n${hasAwait ? `async ` : ``}function setup(${args}) {\n`
+      )
       s.append(
-        `${defaultTempVar}.setup = setup\nexport default ${defaultTempVar}`
+        `/*#__PURE__*/ Object.assign(${defaultTempVar}, {${runtimeOptions}\n  setup\n})\n` +
+          `export default ${defaultTempVar}`
       )
     } else {
-      s.append(`export default { setup }`)
+      s.prependLeft(
+        startOffset,
+        `\nexport default {${runtimeOptions}\n  ` +
+          `${hasAwait ? `async ` : ``}setup(${args}) {\n`
+      )
+      s.append(`}`)
     }
   }
 
@@ -843,16 +867,25 @@ export function compileScript(
   }
 }
 
-function walkDeclaration(node: Declaration, bindings: Record<string, boolean>) {
+function walkDeclaration(node: Declaration, bindings: Record<string, string>) {
   if (node.type === 'VariableDeclaration') {
+    const isConst = node.kind === 'const'
     // export const foo = ...
-    for (const { id } of node.declarations) {
+    for (const { id, init } of node.declarations) {
       if (id.type === 'Identifier') {
-        bindings[id.name] = true
+        bindings[id.name] =
+          // if a declaration is a const literal, we can mark it so that
+          // the generated render fn code doesn't need to unref() it
+          isConst &&
+          init!.type !== 'Identifier' && // const a = b
+          init!.type !== 'CallExpression' && // const a = ref()
+          init!.type !== 'MemberExpression' // const a = b.c
+            ? 'const'
+            : 'var'
       } else if (id.type === 'ObjectPattern') {
-        walkObjectPattern(id, bindings)
+        walkObjectPattern(id, bindings, isConst)
       } else if (id.type === 'ArrayPattern') {
-        walkArrayPattern(id, bindings)
+        walkArrayPattern(id, bindings, isConst)
       }
     }
   } else if (
@@ -861,13 +894,14 @@ function walkDeclaration(node: Declaration, bindings: Record<string, boolean>) {
   ) {
     // export function foo() {} / export class Foo {}
     // export declarations must be named.
-    bindings[node.id!.name] = true
+    bindings[node.id!.name] = 'const'
   }
 }
 
 function walkObjectPattern(
   node: ObjectPattern,
-  bindings: Record<string, boolean>
+  bindings: Record<string, string>,
+  isConst: boolean
 ) {
   for (const p of node.properties) {
     if (p.type === 'ObjectProperty') {
@@ -875,43 +909,48 @@ function walkObjectPattern(
       if (p.key.type === 'Identifier') {
         if (p.key === p.value) {
           // const { x } = ...
-          bindings[p.key.name] = true
+          bindings[p.key.name] = 'var'
         } else {
-          walkPattern(p.value, bindings)
+          walkPattern(p.value, bindings, isConst)
         }
       }
     } else {
       // ...rest
       // argument can only be identifer when destructuring
-      bindings[(p.argument as Identifier).name] = true
+      bindings[(p.argument as Identifier).name] = isConst ? 'const' : 'var'
     }
   }
 }
 
 function walkArrayPattern(
   node: ArrayPattern,
-  bindings: Record<string, boolean>
+  bindings: Record<string, string>,
+  isConst: boolean
 ) {
   for (const e of node.elements) {
-    e && walkPattern(e, bindings)
+    e && walkPattern(e, bindings, isConst)
   }
 }
 
-function walkPattern(node: Node, bindings: Record<string, boolean>) {
+function walkPattern(
+  node: Node,
+  bindings: Record<string, string>,
+  isConst: boolean
+) {
   if (node.type === 'Identifier') {
-    bindings[node.name] = true
+    bindings[node.name] = 'var'
   } else if (node.type === 'RestElement') {
     // argument can only be identifer when destructuring
-    bindings[(node.argument as Identifier).name] = true
+    bindings[(node.argument as Identifier).name] = isConst ? 'const' : 'var'
   } else if (node.type === 'ObjectPattern') {
-    walkObjectPattern(node, bindings)
+    walkObjectPattern(node, bindings, isConst)
   } else if (node.type === 'ArrayPattern') {
-    walkArrayPattern(node, bindings)
+    walkArrayPattern(node, bindings, isConst)
   } else if (node.type === 'AssignmentPattern') {
     if (node.left.type === 'Identifier') {
-      bindings[node.left.name] = true
+      bindings[node.left.name] = 'var'
     } else {
-      walkPattern(node.left, bindings)
+      walkPattern(node.left, bindings, isConst)
     }
   }
 }
@@ -1273,72 +1312,75 @@ function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] {
  * compilation process so this should only be used on single `<script>` SFCs.
  */
 function analyzeScriptBindings(ast: Statement[]): BindingMetadata {
-  const bindings: BindingMetadata = {}
-
   for (const node of ast) {
     if (
       node.type === 'ExportDefaultDeclaration' &&
       node.declaration.type === 'ObjectExpression'
     ) {
-      for (const property of node.declaration.properties) {
-        if (
-          property.type === 'ObjectProperty' &&
-          !property.computed &&
-          property.key.type === 'Identifier'
-        ) {
-          // props
-          if (property.key.name === 'props') {
-            // props: ['foo']
-            // props: { foo: ... }
-            for (const key of getObjectOrArrayExpressionKeys(property)) {
-              bindings[key] = 'props'
-            }
-          }
+      return analyzeBindingsFromOptions(node.declaration)
+    }
+  }
+  return {}
+}
 
-          // inject
-          else if (property.key.name === 'inject') {
-            // inject: ['foo']
-            // inject: { foo: {} }
-            for (const key of getObjectOrArrayExpressionKeys(property)) {
-              bindings[key] = 'options'
-            }
-          }
+function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
+  const bindings: BindingMetadata = {}
+  for (const property of node.properties) {
+    if (
+      property.type === 'ObjectProperty' &&
+      !property.computed &&
+      property.key.type === 'Identifier'
+    ) {
+      // props
+      if (property.key.name === 'props') {
+        // props: ['foo']
+        // props: { foo: ... }
+        for (const key of getObjectOrArrayExpressionKeys(property)) {
+          bindings[key] = 'props'
+        }
+      }
 
-          // computed & methods
-          else if (
-            property.value.type === 'ObjectExpression' &&
-            (property.key.name === 'computed' ||
-              property.key.name === 'methods')
-          ) {
-            // methods: { foo() {} }
-            // computed: { foo() {} }
-            for (const key of getObjectExpressionKeys(property.value)) {
-              bindings[key] = 'options'
-            }
-          }
+      // inject
+      else if (property.key.name === 'inject') {
+        // inject: ['foo']
+        // inject: { foo: {} }
+        for (const key of getObjectOrArrayExpressionKeys(property)) {
+          bindings[key] = 'options'
+        }
+      }
+
+      // computed & methods
+      else if (
+        property.value.type === 'ObjectExpression' &&
+        (property.key.name === 'computed' || property.key.name === 'methods')
+      ) {
+        // methods: { foo() {} }
+        // computed: { foo() {} }
+        for (const key of getObjectExpressionKeys(property.value)) {
+          bindings[key] = 'options'
         }
+      }
+    }
 
-        // setup & data
-        else if (
-          property.type === 'ObjectMethod' &&
-          property.key.type === 'Identifier' &&
-          (property.key.name === 'setup' || property.key.name === 'data')
+    // setup & data
+    else if (
+      property.type === 'ObjectMethod' &&
+      property.key.type === 'Identifier' &&
+      (property.key.name === 'setup' || property.key.name === 'data')
+    ) {
+      for (const bodyItem of property.body.body) {
+        // setup() {
+        //   return {
+        //     foo: null
+        //   }
+        // }
+        if (
+          bodyItem.type === 'ReturnStatement' &&
+          bodyItem.argument &&
+          bodyItem.argument.type === 'ObjectExpression'
         ) {
-          for (const bodyItem of property.body.body) {
-            // setup() {
-            //   return {
-            //     foo: null
-            //   }
-            // }
-            if (
-              bodyItem.type === 'ReturnStatement' &&
-              bodyItem.argument &&
-              bodyItem.argument.type === 'ObjectExpression'
-            ) {
-              for (const key of getObjectExpressionKeys(bodyItem.argument)) {
-                bindings[key] = property.key.name
-              }
-            }
+          for (const key of getObjectExpressionKeys(bodyItem.argument)) {
+            bindings[key] = property.key.name
           }
         }
       }
diff --git a/packages/runtime-core/src/apiDefineContext.ts b/packages/runtime-core/src/apiDefineContext.ts
new file mode 100644 (file)
index 0000000..25d4bd9
--- /dev/null
@@ -0,0 +1,22 @@
+import { Slots } from './componentSlots'
+import { warn } from './warning'
+
+interface DefaultContext {
+  props: Record<string, unknown>
+  attrs: Record<string, unknown>
+  emit: (...args: any[]) => void
+  slots: Slots
+}
+
+export function defineContext<T extends Partial<DefaultContext> = {}>(
+  opts?: any // TODO infer
+): { [K in keyof DefaultContext]: T[K] extends {} ? T[K] : DefaultContext[K] } {
+  if (__DEV__) {
+    warn(
+      `defineContext() is a compiler-hint helper that is only usable inside ` +
+        `<script setup> of a single file component. It will be compiled away ` +
+        `and should not be used in final distributed code.`
+    )
+  }
+  return null as any
+}
index 8f089eb4ee37a16610295e47375e7835e69a2e9c..8a7bd69fe4a0cb0903bd53b0a493c4500c06c248 100644 (file)
@@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
 export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
   extends ComponentInternalOptions {
   // use of any here is intentional so it can be a valid JSX Element constructor
-  (props: P, ctx: SetupContext<E>): any
+  (props: P, ctx: SetupContext<E, P>): any
   props?: ComponentPropsOptions<P>
   emits?: E | (keyof E)[]
   inheritAttrs?: boolean
@@ -167,7 +167,8 @@ export const enum LifecycleHooks {
   ERROR_CAPTURED = 'ec'
 }
 
-export interface SetupContext<E = EmitsOptions> {
+export interface SetupContext<E = EmitsOptions, P = Data> {
+  props: P
   attrs: Data
   slots: Slots
   emit: EmitFn<E>
@@ -735,6 +736,9 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
     // We use getters in dev in case libs like test-utils overwrite instance
     // properties (overwrites should not be done in prod)
     return Object.freeze({
+      get props() {
+        return instance.props
+      },
       get attrs() {
         return new Proxy(instance.attrs, attrHandlers)
       },
@@ -747,6 +751,7 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
     })
   } else {
     return {
+      props: instance.props,
       attrs: instance.attrs,
       slots: instance.slots,
       emit: instance.emit
index ee4062ba8b96a7cd8ccb5abd288e215deb5552cd..a5541579045d0112d48bf0a3107da7d6b3fa0cf9 100644 (file)
@@ -96,7 +96,7 @@ export interface ComponentOptionsBase<
   setup?: (
     this: void,
     props: Props,
-    ctx: SetupContext<E>
+    ctx: SetupContext<E, Props>
   ) => Promise<RawBindings> | RawBindings | RenderFunction | void
   name?: string
   template?: string | object // can be a direct DOM node
index 9c5ecaf5b68dbf583f16388e3639c9ef66e0069a..bd8521415ff3bee4ffedbbf09047acbe94450099 100644 (file)
@@ -95,6 +95,7 @@ export function renderComponentRoot(
               props,
               __DEV__
                 ? {
+                    props,
                     get attrs() {
                       markAttrsAccessed()
                       return attrs
@@ -102,7 +103,7 @@ export function renderComponentRoot(
                     slots,
                     emit
                   }
-                : { attrs, slots, emit }
+                : { props, attrs, slots, emit }
             )
           : render(props, null as any /* we know it doesn't need it */)
       )
diff --git a/packages/runtime-core/src/helpers/useSetupContext.ts b/packages/runtime-core/src/helpers/useSetupContext.ts
deleted file mode 100644 (file)
index 82fea43..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-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 236a1d6db6cdf9f08a9a918ecc1c4fd73b81521b..0c4879e1fc1b9c7c63e538e60d6491ddb1c64a68 100644 (file)
@@ -43,6 +43,7 @@ export { provide, inject } from './apiInject'
 export { nextTick } from './scheduler'
 export { defineComponent } from './apiDefineComponent'
 export { defineAsyncComponent } from './apiAsyncComponent'
+export { defineContext } from './apiDefineContext'
 
 // Advanced API ----------------------------------------------------------------
 
@@ -261,8 +262,6 @@ import {
   setCurrentRenderingInstance
 } from './componentRenderUtils'
 import { isVNode, normalizeVNode } from './vnode'
-import { Slots } from './componentSlots'
-import { EMPTY_OBJ } from '@vue/shared/src'
 
 const _ssrUtils = {
   createComponentInstance,