From fce6a8fa51fe4fcbe334ea0f38c206682e09b814 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Jun 2019 15:43:19 +0800 Subject: [PATCH] wip: support returning render fn from setup() + improve createComponent type inference --- jest.config.js | 4 +- ...onent.spec.ts => createComponent.spec.tsx} | 75 +++++----- packages/runtime-core/src/component.ts | 138 ++++++++++++++---- packages/runtime-core/src/componentProps.ts | 28 +++- tsconfig.json | 2 +- 5 files changed, 176 insertions(+), 71 deletions(-) rename packages/runtime-core/__tests__/{createComponent.spec.ts => createComponent.spec.tsx} (63%) diff --git a/jest.config.js b/jest.config.js index d92d3886..08d09f94 100644 --- a/jest.config.js +++ b/jest.config.js @@ -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/(.*?)$': '/packages/$1/src' }, rootDir: __dirname, - testMatch: ['/packages/**/__tests__/**/*spec.(t|j)s'] + testMatch: ['/packages/**/__tests__/**/*spec.[jt]s?(x)'] } diff --git a/packages/runtime-core/__tests__/createComponent.spec.ts b/packages/runtime-core/__tests__/createComponent.spec.tsx similarity index 63% rename from packages/runtime-core/__tests__/createComponent.spec.ts rename to packages/runtime-core/__tests__/createComponent.spec.tsx index 41789ff7..885b58de 100644 --- a/packages/runtime-core/__tests__/createComponent.spec.ts +++ b/packages/runtime-core/__tests__/createComponent.spec.tsx @@ -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 - // ;() + // test TSX props inference + ;() }) 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 } }) + ;() }) -// 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 () =>
{props.msg}
+ }) + ;() +}) + +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 + } + }) + ;() +}) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index b68fee36..a3874ac6 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -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

= { +export type ComponentRenderProxy

= { $state: S - $props: P + $props: PublicProps $attrs: Data // TODO @@ -28,22 +28,61 @@ export type ComponentPublicProperties

= { } & P & S -interface ComponentOptions< - RawProps = ComponentPropsOptions, +type RenderFunction

= ( + props: P, + slots: Slots, + attrs: Data, + vnode: VNode +) => any + +type RenderFunctionWithThis = < + Bindings extends UnwrapValue +>( + this: ComponentRenderProxy, + props: Props, + slots: Slots, + attrs: Data, + vnode: VNode +) => VNodeChild + +interface ComponentOptionsWithProps< + PropsOptions = ComponentPropsOptions, RawBindings = Data, - Props = ExtractPropTypes, - ExposedProps = RawProps extends object ? Props : {} + Props = ExtractPropTypes > { - props?: RawProps - setup?: (this: ComponentPublicProperties, props: Props) => RawBindings - render?: >( - this: ComponentPublicProperties, - ctx: ComponentInstance - ) => VNodeChild + props: PropsOptions + setup?: ( + this: ComponentRenderProxy, + props: Props + ) => RawBindings | RenderFunction + render?: RenderFunctionWithThis } -export interface FunctionalComponent

{ - (ctx: ComponentInstance

): any +interface ComponentOptionsWithoutProps { + props?: undefined + setup?: ( + this: ComponentRenderProxy, + props: Props + ) => RawBindings | RenderFunction + render?: RenderFunctionWithThis +} + +interface ComponentOptionsWithArrayProps< + PropNames extends string, + RawBindings = Data, + Props = { [key in PropNames]?: any } +> { + props: PropNames[] + setup?: ( + this: ComponentRenderProxy, + props: Props + ) => RawBindings | RenderFunction + render?: RenderFunctionWithThis +} + +type ComponentOptions = ComponentOptionsWithProps | ComponentOptionsWithoutProps + +export interface FunctionalComponent

extends RenderFunction

{ props?: ComponentPropsOptions

displayName?: string } @@ -73,8 +112,9 @@ export type ComponentInstance

= { subTree: VNode update: ReactiveEffect effects: ReactiveEffect[] | null + render: RenderFunction

| 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

= { } & LifecycleHooks // no-op, for type inference only -export function createComponent( - options: ComponentOptions +export function createComponent( + setup: (props: Props) => RenderFunction +): (props: Props) => any +export function createComponent( + options: ComponentOptionsWithArrayProps ): { - // for TSX - new (): { $props: ExtractPropTypes } -} { - return options as any + // for Vetur and TSX support + new (): ComponentRenderProxy< + { [key in PropNames]?: any }, + UnwrapValue + > +} +export function createComponent( + options: ComponentOptionsWithoutProps +): { + // for Vetur and TSX support + new (): ComponentRenderProxy> +} +export function createComponent( + options: ComponentOptionsWithProps +): { + // for Vetur and TSX support + new (): ComponentRenderProxy< + ExtractPropTypes, + UnwrapValue, + ExtractPropTypes + > +} +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) + ) } } diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 0dfe477c..c7bf0804 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -31,11 +31,18 @@ export type PropType = PropConstructor | PropConstructor[] type PropConstructor = { new (...args: any[]): T & object } | { (): T } -type RequiredKeys = { - [K in keyof T]: T[K] extends { required: true } | { default: any } ? K : never +type RequiredKeys = { + [K in keyof T]: T[K] extends + | { required: true } + | (MakeDefautRequired extends true ? { default: any } : never) + ? K + : never }[keyof T] -type OptionalKeys = Exclude> +type OptionalKeys = Exclude< + keyof T, + RequiredKeys +> type InferPropType = T extends null ? any // null & true would fail to infer @@ -45,9 +52,18 @@ type InferPropType = T extends null ? { [key: string]: any } : T extends Prop ? V : T -export type ExtractPropTypes = O extends object - ? { readonly [K in RequiredKeys]: InferPropType } & - { readonly [K in OptionalKeys]?: InferPropType } +export type ExtractPropTypes< + O, + MakeDefautRequired extends boolean = true +> = O extends object + ? { + readonly [K in RequiredKeys]: InferPropType + } & + { + readonly [K in OptionalKeys]?: InferPropType< + O[K] + > + } : { [K in string]: any } const enum BooleanFlags { diff --git a/tsconfig.json b/tsconfig.json index f7e7ec24..c1c7a2ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "noImplicitAny": true, "experimentalDecorators": true, "removeComments": false, - "jsx": "preserve", + "jsx": "react", "lib": [ "esnext", "dom"