wip: support returning render fn from setup() + improve createComponent type inference
This commit is contained in:
parent
bfe6987323
commit
fce6a8fa51
@ -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)']
|
||||
}
|
||||
|
@ -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}/>)
|
||||
})
|
@ -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,
|
||||
RawBindings = Data,
|
||||
Props = ExtractPropTypes<RawProps>,
|
||||
ExposedProps = RawProps extends object ? Props : {}
|
||||
> {
|
||||
props?: RawProps
|
||||
setup?: (this: ComponentPublicProperties, props: Props) => RawBindings
|
||||
render?: <State extends UnwrapValue<RawBindings>>(
|
||||
this: ComponentPublicProperties<ExposedProps, State>,
|
||||
ctx: ComponentInstance<Props, State>
|
||||
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<PropsOptions>
|
||||
> {
|
||||
props: PropsOptions
|
||||
setup?: (
|
||||
this: ComponentRenderProxy<Props>,
|
||||
props: Props
|
||||
) => RawBindings | RenderFunction<Props>
|
||||
render?: RenderFunctionWithThis<Props, RawBindings>
|
||||
}
|
||||
|
||||
export interface FunctionalComponent<P = {}> {
|
||||
(ctx: ComponentInstance<P>): any
|
||||
interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
|
||||
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>
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -12,7 +12,7 @@
|
||||
"noImplicitAny": true,
|
||||
"experimentalDecorators": true,
|
||||
"removeComments": false,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react",
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
|
Loading…
Reference in New Issue
Block a user