]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: support returning render fn from setup() + improve createComponent type inference
authorEvan You <yyx990803@gmail.com>
Wed, 12 Jun 2019 07:43:19 +0000 (15:43 +0800)
committerEvan You <yyx990803@gmail.com>
Wed, 12 Jun 2019 07:43:19 +0000 (15:43 +0800)
jest.config.js
packages/runtime-core/__tests__/createComponent.spec.tsx [moved from packages/runtime-core/__tests__/createComponent.spec.ts with 63% similarity]
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProps.ts
tsconfig.json

index d92d388646399514a39004a62881307706523f6f..08d09f94fed7c82c7c7dc82f0598b1d650cf3165 100644 (file)
@@ -8,10 +8,10 @@ module.exports = {
   coverageDirectory: 'coverage',
   coverageReporters: ['html', 'lcov', 'text'],
   collectCoverageFrom: ['packages/*/src/**/*.ts'],
-  moduleFileExtensions: ['ts', 'js', 'json'],
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
   moduleNameMapper: {
     '^@vue/(.*?)$': '<rootDir>/packages/$1/src'
   },
   rootDir: __dirname,
-  testMatch: ['<rootDir>/packages/**/__tests__/**/*spec.(t|j)s']
+  testMatch: ['<rootDir>/packages/**/__tests__/**/*spec.[jt]s?(x)']
 }
similarity index 63%
rename from packages/runtime-core/__tests__/createComponent.spec.ts
rename to packages/runtime-core/__tests__/createComponent.spec.tsx
index 41789ff7d1aec28c42b8dd3e2fe8bfe312918c1c..885b58dec5a7ffb803ef92404c3a974ab0e13562 100644 (file)
@@ -2,8 +2,13 @@ import { createComponent } from '../src/component'
 import { value } from '@vue/reactivity'
 import { PropType } from '../src/componentProps'
 
+// mock React just for TSX testing purposes
+const React = {
+  createElement: () => {}
+}
+
 test('createComponent type inference', () => {
-  createComponent({
+  const MyComponent = createComponent({
     props: {
       a: Number,
       // required should make property non-void
@@ -36,9 +41,7 @@ test('createComponent type inference', () => {
         }
       }
     },
-    render({ state, props }) {
-      state.c * 2
-      state.d.e.slice()
+    render(props) {
       props.a && props.a * 2
       props.b.slice()
       props.bb.slice()
@@ -53,47 +56,53 @@ test('createComponent type inference', () => {
       this.dd.push('dd')
     }
   })
-  // rename this file to .tsx to test TSX props inference
-  // ;(<MyComponent a={1} b="foo"/>)
+  // test TSX props inference
+  ;(<MyComponent a={1} b="foo" dd={['foo']}/>)
 })
 
 test('type inference w/ optional props declaration', () => {
-  createComponent({
-    setup(props) {
-      props.anything
+  const Comp = createComponent({
+    setup(props: { msg: string }) {
+      props.msg
       return {
         a: 1
       }
     },
-    render({ props, state }) {
-      props.foobar
-      state.a * 2
+    render(props) {
+      props.msg
       this.a * 2
-
       // should not make state and this indexable
       // state.foobar
       // this.foobar
     }
   })
+  ;(<Comp msg="hello"/>)
 })
 
-// test('type inference w/ array props declaration', () => {
-//   createComponent({
-//     props: ['a', 'b'],
-//     setup(props) {
-//       props.a
-//       props.b
-//       return {
-//         c: 1
-//       }
-//     },
-//     render({ props, state }) {
-//       props.a
-//       props.b
-//       state.c
-//       this.a
-//       this.b
-//       this.c
-//     }
-//   })
-// })
+test('type inference w/ direct setup function', () => {
+  const Comp = createComponent((props: { msg: string }) => {
+    return () => <div>{props.msg}</div>
+  })
+  ;(<Comp msg="hello"/>)
+})
+
+test('type inference w/ array props declaration', () => {
+  const Comp = createComponent({
+    props: ['a', 'b'],
+    setup(props) {
+      props.a
+      props.b
+      return {
+        c: 1
+      }
+    },
+    render(props) {
+      props.a
+      props.b
+      this.a
+      this.b
+      this.c
+    }
+  })
+  ;(<Comp a={1} b={2}/>)
+})
index b68fee36f0a4154aaaf91324e5fa43125d7c317a..a3874ac67f141375bdc094368d9b669c6d094062 100644 (file)
@@ -5,7 +5,7 @@ import {
   state,
   immutableState
 } from '@vue/reactivity'
-import { EMPTY_OBJ } from '@vue/shared'
+import { EMPTY_OBJ, isFunction } from '@vue/shared'
 import { RenderProxyHandlers } from './componentProxy'
 import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
 import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags'
@@ -14,9 +14,9 @@ import { STATEFUL_COMPONENT } from './typeFlags'
 
 export type Data = { [key: string]: any }
 
-export type ComponentPublicProperties<P = {}, S = {}> = {
+export type ComponentRenderProxy<P = {}, S = {}, PublicProps = P> = {
   $state: S
-  $props: P
+  $props: PublicProps
   $attrs: Data
 
   // TODO
@@ -28,22 +28,61 @@ export type ComponentPublicProperties<P = {}, S = {}> = {
 } & P &
   S
 
-interface ComponentOptions<
-  RawProps = ComponentPropsOptions,
+type RenderFunction<P = Data> = (
+  props: P,
+  slots: Slots,
+  attrs: Data,
+  vnode: VNode
+) => any
+
+type RenderFunctionWithThis<Props, RawBindings> = <
+  Bindings extends UnwrapValue<RawBindings>
+>(
+  this: ComponentRenderProxy<Props, Bindings>,
+  props: Props,
+  slots: Slots,
+  attrs: Data,
+  vnode: VNode
+) => VNodeChild
+
+interface ComponentOptionsWithProps<
+  PropsOptions = ComponentPropsOptions,
   RawBindings = Data,
-  Props = ExtractPropTypes<RawProps>,
-  ExposedProps = RawProps extends object ? Props : {}
+  Props = ExtractPropTypes<PropsOptions>
 > {
-  props?: RawProps
-  setup?: (this: ComponentPublicProperties, props: Props) => RawBindings
-  render?: <State extends UnwrapValue<RawBindings>>(
-    this: ComponentPublicProperties<ExposedProps, State>,
-    ctx: ComponentInstance<Props, State>
-  ) => VNodeChild
+  props: PropsOptions
+  setup?: (
+    this: ComponentRenderProxy<Props>,
+    props: Props
+  ) => RawBindings | RenderFunction<Props>
+  render?: RenderFunctionWithThis<Props, RawBindings>
+}
+
+interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
+  props?: undefined
+  setup?: (
+    this: ComponentRenderProxy<Props>,
+    props: Props
+  ) => RawBindings | RenderFunction<Props>
+  render?: RenderFunctionWithThis<Props, RawBindings>
 }
 
-export interface FunctionalComponent<P = {}> {
-  (ctx: ComponentInstance<P>): any
+interface ComponentOptionsWithArrayProps<
+  PropNames extends string,
+  RawBindings = Data,
+  Props = { [key in PropNames]?: any }
+> {
+  props: PropNames[]
+  setup?: (
+    this: ComponentRenderProxy<Props>,
+    props: Props
+  ) => RawBindings | RenderFunction<Props>
+  render?: RenderFunctionWithThis<Props, RawBindings>
+}
+
+type ComponentOptions = ComponentOptionsWithProps | ComponentOptionsWithoutProps
+
+export interface FunctionalComponent<P = {}> extends RenderFunction<P> {
   props?: ComponentPropsOptions<P>
   displayName?: string
 }
@@ -73,8 +112,9 @@ export type ComponentInstance<P = Data, S = Data> = {
   subTree: VNode
   update: ReactiveEffect
   effects: ReactiveEffect[] | null
+  render: RenderFunction<P> | null
   // the rest are only for stateful components
-  renderProxy: ComponentPublicProperties | null
+  renderProxy: ComponentRenderProxy | null
   propsProxy: Data | null
   state: S
   props: P
@@ -84,13 +124,36 @@ export type ComponentInstance<P = Data, S = Data> = {
 } & LifecycleHooks
 
 // no-op, for type inference only
-export function createComponent<RawProps, RawBindings>(
-  options: ComponentOptions<RawProps, RawBindings>
+export function createComponent<Props>(
+  setup: (props: Props) => RenderFunction<Props>
+): (props: Props) => any
+export function createComponent<PropNames extends string, RawBindings>(
+  options: ComponentOptionsWithArrayProps<PropNames, RawBindings>
 ): {
-  // for TSX
-  new (): { $props: ExtractPropTypes<RawProps> }
-} {
-  return options as any
+  // for Vetur and TSX support
+  new (): ComponentRenderProxy<
+    { [key in PropNames]?: any },
+    UnwrapValue<RawBindings>
+  >
+}
+export function createComponent<Props, RawBindings>(
+  options: ComponentOptionsWithoutProps<Props, RawBindings>
+): {
+  // for Vetur and TSX support
+  new (): ComponentRenderProxy<Props, UnwrapValue<RawBindings>>
+}
+export function createComponent<PropsOptions, RawBindings>(
+  options: ComponentOptionsWithProps<PropsOptions, RawBindings>
+): {
+  // for Vetur and TSX support
+  new (): ComponentRenderProxy<
+    ExtractPropTypes<PropsOptions>,
+    UnwrapValue<RawBindings>,
+    ExtractPropTypes<PropsOptions, false>
+  >
+}
+export function createComponent(options: any) {
+  return isFunction(options) ? { setup: options } : (options as any)
 }
 
 export function createComponentInstance(
@@ -105,6 +168,7 @@ export function createComponentInstance(
     next: null,
     subTree: null as any,
     update: null as any,
+    render: null,
     renderProxy: null,
     propsProxy: null,
 
@@ -153,23 +217,39 @@ export function setupStatefulComponent(instance: ComponentInstance) {
     const propsProxy = (instance.propsProxy = setup.length
       ? immutableState(instance.props)
       : null)
-    instance.state = state(setup.call(proxy, propsProxy))
+    const setupResult = setup.call(proxy, propsProxy)
+    if (isFunction(setupResult)) {
+      // setup returned a render function
+      instance.render = setupResult
+    } else {
+      // setup returned bindings
+      instance.state = state(setupResult)
+      if (__DEV__ && !Component.render) {
+        // TODO warn missing render fn
+      }
+      instance.render = Component.render as RenderFunction
+    }
     currentInstance = null
   }
 }
 
 export function renderComponentRoot(instance: ComponentInstance): VNode {
-  const { type: Component, vnode } = instance
+  const { type: Component, renderProxy, props, slots, attrs, vnode } = instance
   if (vnode.shapeFlag & STATEFUL_COMPONENT) {
-    if (__DEV__ && !(Component as any).render) {
-      // TODO warn missing render
-    }
     return normalizeVNode(
-      (Component as any).render.call(instance.renderProxy, instance)
+      (instance.render as RenderFunction).call(
+        renderProxy,
+        props,
+        slots,
+        attrs,
+        vnode
+      )
     )
   } else {
     // functional
-    return normalizeVNode((Component as FunctionalComponent)(instance))
+    return normalizeVNode(
+      (Component as FunctionalComponent)(props, slots, attrs, vnode)
+    )
   }
 }
 
index 0dfe477cad3dc26a06c5ecf14fb56031d6a4e6ed..c7bf08043d871c500e3365b17d8f3b2e2a0fc3c3 100644 (file)
@@ -31,11 +31,18 @@ export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
 
 type PropConstructor<T> = { new (...args: any[]): T & object } | { (): T }
 
-type RequiredKeys<T> = {
-  [K in keyof T]: T[K] extends { required: true } | { default: any } ? K : never
+type RequiredKeys<T, MakeDefautRequired> = {
+  [K in keyof T]: T[K] extends
+    | { required: true }
+    | (MakeDefautRequired extends true ? { default: any } : never)
+    ? K
+    : never
 }[keyof T]
 
-type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
+type OptionalKeys<T, MakeDefautRequired> = Exclude<
+  keyof T,
+  RequiredKeys<T, MakeDefautRequired>
+>
 
 type InferPropType<T> = T extends null
   ? any // null & true would fail to infer
@@ -45,9 +52,18 @@ type InferPropType<T> = T extends null
       ? { [key: string]: any }
       : T extends Prop<infer V> ? V : T
 
-export type ExtractPropTypes<O> = O extends object
-  ? { readonly [K in RequiredKeys<O>]: InferPropType<O[K]> } &
-      { readonly [K in OptionalKeys<O>]?: InferPropType<O[K]> }
+export type ExtractPropTypes<
+  O,
+  MakeDefautRequired extends boolean = true
+> = O extends object
+  ? {
+      readonly [K in RequiredKeys<O, MakeDefautRequired>]: InferPropType<O[K]>
+    } &
+      {
+        readonly [K in OptionalKeys<O, MakeDefautRequired>]?: InferPropType<
+          O[K]
+        >
+      }
   : { [K in string]: any }
 
 const enum BooleanFlags {
index f7e7ec242a33f3872878f8861a8d7a24123fa76c..c1c7a2ef44900add389e019fa61143b6e258fc31 100644 (file)
@@ -12,7 +12,7 @@
     "noImplicitAny": true,
     "experimentalDecorators": true,
     "removeComments": false,
-    "jsx": "preserve",
+    "jsx": "react",
     "lib": [
       "esnext",
       "dom"