wip: support returning render fn from setup() + improve createComponent type inference

This commit is contained in:
Evan You 2019-06-12 15:43:19 +08:00
parent bfe6987323
commit fce6a8fa51
5 changed files with 176 additions and 71 deletions

View File

@ -8,10 +8,10 @@ module.exports = {
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
coverageReporters: ['html', 'lcov', 'text'], coverageReporters: ['html', 'lcov', 'text'],
collectCoverageFrom: ['packages/*/src/**/*.ts'], collectCoverageFrom: ['packages/*/src/**/*.ts'],
moduleFileExtensions: ['ts', 'js', 'json'], moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
moduleNameMapper: { moduleNameMapper: {
'^@vue/(.*?)$': '<rootDir>/packages/$1/src' '^@vue/(.*?)$': '<rootDir>/packages/$1/src'
}, },
rootDir: __dirname, rootDir: __dirname,
testMatch: ['<rootDir>/packages/**/__tests__/**/*spec.(t|j)s'] testMatch: ['<rootDir>/packages/**/__tests__/**/*spec.[jt]s?(x)']
} }

View File

@ -2,8 +2,13 @@ import { createComponent } from '../src/component'
import { value } from '@vue/reactivity' import { value } from '@vue/reactivity'
import { PropType } from '../src/componentProps' import { PropType } from '../src/componentProps'
// mock React just for TSX testing purposes
const React = {
createElement: () => {}
}
test('createComponent type inference', () => { test('createComponent type inference', () => {
createComponent({ const MyComponent = createComponent({
props: { props: {
a: Number, a: Number,
// required should make property non-void // required should make property non-void
@ -36,9 +41,7 @@ test('createComponent type inference', () => {
} }
} }
}, },
render({ state, props }) { render(props) {
state.c * 2
state.d.e.slice()
props.a && props.a * 2 props.a && props.a * 2
props.b.slice() props.b.slice()
props.bb.slice() props.bb.slice()
@ -53,47 +56,53 @@ test('createComponent type inference', () => {
this.dd.push('dd') this.dd.push('dd')
} }
}) })
// rename this file to .tsx to test TSX props inference // test TSX props inference
// ;(<MyComponent a={1} b="foo"/>) ;(<MyComponent a={1} b="foo" dd={['foo']}/>)
}) })
test('type inference w/ optional props declaration', () => { test('type inference w/ optional props declaration', () => {
createComponent({ const Comp = createComponent({
setup(props) { setup(props: { msg: string }) {
props.anything props.msg
return { return {
a: 1 a: 1
} }
}, },
render({ props, state }) { render(props) {
props.foobar props.msg
state.a * 2
this.a * 2 this.a * 2
// should not make state and this indexable // should not make state and this indexable
// state.foobar // state.foobar
// this.foobar // this.foobar
} }
}) })
;(<Comp msg="hello"/>)
}) })
// test('type inference w/ array props declaration', () => { test('type inference w/ direct setup function', () => {
// createComponent({ const Comp = createComponent((props: { msg: string }) => {
// props: ['a', 'b'], return () => <div>{props.msg}</div>
// setup(props) { })
// props.a ;(<Comp msg="hello"/>)
// props.b })
// return {
// c: 1 test('type inference w/ array props declaration', () => {
// } const Comp = createComponent({
// }, props: ['a', 'b'],
// render({ props, state }) { setup(props) {
// props.a props.a
// props.b props.b
// state.c return {
// this.a c: 1
// this.b }
// this.c },
// } render(props) {
// }) props.a
// }) props.b
this.a
this.b
this.c
}
})
;(<Comp a={1} b={2}/>)
})

View File

@ -5,7 +5,7 @@ import {
state, state,
immutableState immutableState
} from '@vue/reactivity' } from '@vue/reactivity'
import { EMPTY_OBJ } from '@vue/shared' import { EMPTY_OBJ, isFunction } from '@vue/shared'
import { RenderProxyHandlers } from './componentProxy' import { RenderProxyHandlers } from './componentProxy'
import { ComponentPropsOptions, ExtractPropTypes } from './componentProps' import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags' 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 Data = { [key: string]: any }
export type ComponentPublicProperties<P = {}, S = {}> = { export type ComponentRenderProxy<P = {}, S = {}, PublicProps = P> = {
$state: S $state: S
$props: P $props: PublicProps
$attrs: Data $attrs: Data
// TODO // TODO
@ -28,22 +28,61 @@ export type ComponentPublicProperties<P = {}, S = {}> = {
} & P & } & P &
S S
interface ComponentOptions< type RenderFunction<P = Data> = (
RawProps = ComponentPropsOptions, 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, RawBindings = Data,
Props = ExtractPropTypes<RawProps>, Props = ExtractPropTypes<PropsOptions>
ExposedProps = RawProps extends object ? Props : {}
> { > {
props?: RawProps props: PropsOptions
setup?: (this: ComponentPublicProperties, props: Props) => RawBindings setup?: (
render?: <State extends UnwrapValue<RawBindings>>( this: ComponentRenderProxy<Props>,
this: ComponentPublicProperties<ExposedProps, State>, props: Props
ctx: ComponentInstance<Props, State> ) => RawBindings | RenderFunction<Props>
) => VNodeChild render?: RenderFunctionWithThis<Props, RawBindings>
} }
export interface FunctionalComponent<P = {}> { interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
(ctx: ComponentInstance<P>): any props?: undefined
setup?: (
this: ComponentRenderProxy<Props>,
props: Props
) => RawBindings | RenderFunction<Props>
render?: RenderFunctionWithThis<Props, RawBindings>
}
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> props?: ComponentPropsOptions<P>
displayName?: string displayName?: string
} }
@ -73,8 +112,9 @@ export type ComponentInstance<P = Data, S = Data> = {
subTree: VNode subTree: VNode
update: ReactiveEffect update: ReactiveEffect
effects: ReactiveEffect[] | null effects: ReactiveEffect[] | null
render: RenderFunction<P> | null
// the rest are only for stateful components // the rest are only for stateful components
renderProxy: ComponentPublicProperties | null renderProxy: ComponentRenderProxy | null
propsProxy: Data | null propsProxy: Data | null
state: S state: S
props: P props: P
@ -84,13 +124,36 @@ export type ComponentInstance<P = Data, S = Data> = {
} & LifecycleHooks } & LifecycleHooks
// no-op, for type inference only // no-op, for type inference only
export function createComponent<RawProps, RawBindings>( export function createComponent<Props>(
options: ComponentOptions<RawProps, RawBindings> setup: (props: Props) => RenderFunction<Props>
): (props: Props) => any
export function createComponent<PropNames extends string, RawBindings>(
options: ComponentOptionsWithArrayProps<PropNames, RawBindings>
): { ): {
// for TSX // for Vetur and TSX support
new (): { $props: ExtractPropTypes<RawProps> } new (): ComponentRenderProxy<
} { { [key in PropNames]?: any },
return options as 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( export function createComponentInstance(
@ -105,6 +168,7 @@ export function createComponentInstance(
next: null, next: null,
subTree: null as any, subTree: null as any,
update: null as any, update: null as any,
render: null,
renderProxy: null, renderProxy: null,
propsProxy: null, propsProxy: null,
@ -153,23 +217,39 @@ export function setupStatefulComponent(instance: ComponentInstance) {
const propsProxy = (instance.propsProxy = setup.length const propsProxy = (instance.propsProxy = setup.length
? immutableState(instance.props) ? immutableState(instance.props)
: null) : 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 currentInstance = null
} }
} }
export function renderComponentRoot(instance: ComponentInstance): VNode { 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 (vnode.shapeFlag & STATEFUL_COMPONENT) {
if (__DEV__ && !(Component as any).render) {
// TODO warn missing render
}
return normalizeVNode( return normalizeVNode(
(Component as any).render.call(instance.renderProxy, instance) (instance.render as RenderFunction).call(
renderProxy,
props,
slots,
attrs,
vnode
)
) )
} else { } else {
// functional // functional
return normalizeVNode((Component as FunctionalComponent)(instance)) return normalizeVNode(
(Component as FunctionalComponent)(props, slots, attrs, vnode)
)
} }
} }

View File

@ -31,11 +31,18 @@ export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
type PropConstructor<T> = { new (...args: any[]): T & object } | { (): T } type PropConstructor<T> = { new (...args: any[]): T & object } | { (): T }
type RequiredKeys<T> = { type RequiredKeys<T, MakeDefautRequired> = {
[K in keyof T]: T[K] extends { required: true } | { default: any } ? K : never [K in keyof T]: T[K] extends
| { required: true }
| (MakeDefautRequired extends true ? { default: any } : never)
? K
: never
}[keyof T] }[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 type InferPropType<T> = T extends null
? any // null & true would fail to infer ? any // null & true would fail to infer
@ -45,9 +52,18 @@ type InferPropType<T> = T extends null
? { [key: string]: any } ? { [key: string]: any }
: T extends Prop<infer V> ? V : T : T extends Prop<infer V> ? V : T
export type ExtractPropTypes<O> = O extends object export type ExtractPropTypes<
? { readonly [K in RequiredKeys<O>]: InferPropType<O[K]> } & O,
{ readonly [K in OptionalKeys<O>]?: InferPropType<O[K]> } 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 } : { [K in string]: any }
const enum BooleanFlags { const enum BooleanFlags {

View File

@ -12,7 +12,7 @@
"noImplicitAny": true, "noImplicitAny": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"removeComments": false, "removeComments": false,
"jsx": "preserve", "jsx": "react",
"lib": [ "lib": [
"esnext", "esnext",
"dom" "dom"