From e2edd62f663fd611e048cdcb0644a78e4d48adc6 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Tue, 5 Dec 2023 00:50:34 +0900 Subject: [PATCH 1/6] wip: component props --- packages/runtime-vapor/src/component.ts | 36 ++- packages/runtime-vapor/src/componentProps.ts | 270 +++++++++++++++++++ packages/runtime-vapor/src/render.ts | 30 ++- playground/src/main.ts | 21 +- playground/src/props.js | 94 +++++++ 5 files changed, 433 insertions(+), 18 deletions(-) create mode 100644 packages/runtime-vapor/src/componentProps.ts create mode 100644 playground/src/props.js diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index c6195f36e..da30df343 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,6 +1,18 @@ import { EffectScope } from '@vue/reactivity' +import { EMPTY_OBJ } from '@vue/shared' import { Block, BlockFn } from './render' import { DirectiveBinding } from './directives' +import { + ComponentPropsOptions, + NormalizedPropsOptions, + normalizePropsOptions, +} from './componentProps' + +// Conventional ConcreteComponent +export interface Component

{ + props?: ComponentPropsOptions

+ blockFn: BlockFn +} export interface ComponentInternalInstance { uid: number @@ -8,11 +20,17 @@ export interface ComponentInternalInstance { block: Block | null scope: EffectScope - component: BlockFn - isMounted: boolean + blockFn: BlockFn + propsOptions: NormalizedPropsOptions + + // state + props: Data /** directives */ dirs: Map + + // lifecycle + isMounted: boolean // TODO: registory of provides, appContext, lifecycles, ... } @@ -36,18 +54,26 @@ export interface ComponentPublicInstance {} let uid = 0 export const createComponentInstance = ( - component: BlockFn, + component: Component, ): ComponentInternalInstance => { const instance: ComponentInternalInstance = { uid: uid++, block: null, container: null!, // set on mount scope: new EffectScope(true /* detached */)!, + blockFn: component.blockFn, - component, - isMounted: false, + // resolved props and emits options + propsOptions: normalizePropsOptions(component), + // emitsOptions: normalizeEmitsOptions(type, appContext), // TODO: + + // state + props: EMPTY_OBJ, dirs: new Map(), + + // lifecycle hooks + isMounted: false, // TODO: registory of provides, appContext, lifecycles, ... } return instance diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts new file mode 100644 index 000000000..eb4c21bf8 --- /dev/null +++ b/packages/runtime-vapor/src/componentProps.ts @@ -0,0 +1,270 @@ +// NOTE: runtime-core/src/componentProps.ts + +import { + EMPTY_ARR, + EMPTY_OBJ, + camelize, + extend, + hasOwn, + hyphenate, + isArray, + isFunction, + isReservedProp, +} from '@vue/shared' +import { shallowReactive, toRaw } from '@vue/reactivity' +import { Component, ComponentInternalInstance, Data } from './component' + +export type ComponentPropsOptions

= + | ComponentObjectPropsOptions

+ | string[] + +export type ComponentObjectPropsOptions

= { + [K in keyof P]: Prop | null +} + +export type Prop = PropOptions | PropType + +type DefaultFactory = (props: Data) => T | null | undefined + +export interface PropOptions { + type?: PropType | true | null + required?: boolean + default?: D | DefaultFactory | null | undefined | object + validator?(value: unknown): boolean + /** + * @internal + */ + skipCheck?: boolean + /** + * @internal + */ + skipFactory?: boolean +} + +export type PropType = PropConstructor | PropConstructor[] + +type PropConstructor = + | { new (...args: any[]): T & {} } + | { (): T } + | PropMethod + +type PropMethod = [T] extends [ + ((...args: any) => any) | undefined, +] // if is function with args, allowing non-required functions + ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor + : never + +enum BooleanFlags { + shouldCast, + shouldCastTrue, +} + +type NormalizedProp = + | null + | (PropOptions & { + [BooleanFlags.shouldCast]?: boolean + [BooleanFlags.shouldCastTrue]?: boolean + }) + +export type NormalizedProps = Record +export type NormalizedPropsOptions = [NormalizedProps, string[]] | [] + +export function initProps( + instance: ComponentInternalInstance, + rawProps: Data | null, +) { + const props: Data = {} + + const [options, needCastKeys] = instance.propsOptions + let rawCastValues: Data | undefined + if (rawProps) { + for (let key in rawProps) { + // key, ref are reserved and never passed down + if (isReservedProp(key)) { + continue + } + + const valueGetter = () => rawProps[key] + let camelKey + if (options && hasOwn(options, (camelKey = camelize(key)))) { + if (!needCastKeys || !needCastKeys.includes(camelKey)) { + // NOTE: must getter + // props[camelKey] = value + Object.defineProperty(props, camelKey, { + get() { + return valueGetter() + }, + }) + } else { + // NOTE: must getter + // ;(rawCastValues || (rawCastValues = {}))[camelKey] = value + rawCastValues || (rawCastValues = {}) + Object.defineProperty(rawCastValues, camelKey, { + get() { + return valueGetter() + }, + }) + } + } else { + // TODO: + } + } + } + + if (needCastKeys) { + const rawCurrentProps = toRaw(props) + const castValues = rawCastValues || EMPTY_OBJ + for (let i = 0; i < needCastKeys.length; i++) { + const key = needCastKeys[i] + + // NOTE: must getter + // props[key] = resolvePropValue( + // options!, + // rawCurrentProps, + // key, + // castValues[key], + // instance, + // !hasOwn(castValues, key), + // ) + Object.defineProperty(props, key, { + get() { + return resolvePropValue( + options!, + rawCurrentProps, + key, + castValues[key], + instance, + !hasOwn(castValues, key), + ) + }, + }) + } + } + + instance.props = shallowReactive(props) +} + +function resolvePropValue( + options: NormalizedProps, + props: Data, + key: string, + value: unknown, + instance: ComponentInternalInstance, + isAbsent: boolean, +) { + const opt = options[key] + if (opt != null) { + const hasDefault = hasOwn(opt, 'default') + // default values + if (hasDefault && value === undefined) { + const defaultValue = opt.default + if ( + opt.type !== Function && + !opt.skipFactory && + isFunction(defaultValue) + ) { + // TODO: caching? + // const { propsDefaults } = instance + // if (key in propsDefaults) { + // value = propsDefaults[key] + // } else { + // setCurrentInstance(instance) + // value = propsDefaults[key] = defaultValue.call( + // __COMPAT__ && + // isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance) + // ? createPropsDefaultThis(instance, props, key) + // : null, + // props, + // ) + // unsetCurrentInstance() + // } + } else { + value = defaultValue + } + } + // boolean casting + if (opt[BooleanFlags.shouldCast]) { + if (isAbsent && !hasDefault) { + value = false + } else if ( + opt[BooleanFlags.shouldCastTrue] && + (value === '' || value === hyphenate(key)) + ) { + value = true + } + } + } + return value +} + +export function normalizePropsOptions(comp: Component): NormalizedPropsOptions { + // TODO: cahching? + + const raw = comp.props as any + const normalized: NormalizedPropsOptions[0] = {} + const needCastKeys: NormalizedPropsOptions[1] = [] + + if (!raw) { + return EMPTY_ARR as any + } + + if (isArray(raw)) { + for (let i = 0; i < raw.length; i++) { + const normalizedKey = camelize(raw[i]) + if (validatePropName(normalizedKey)) { + normalized[normalizedKey] = EMPTY_OBJ + } + } + } else if (raw) { + for (const key in raw) { + const normalizedKey = camelize(key) + if (validatePropName(normalizedKey)) { + const opt = raw[key] + const prop: NormalizedProp = (normalized[normalizedKey] = + isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt)) + if (prop) { + const booleanIndex = getTypeIndex(Boolean, prop.type) + const stringIndex = getTypeIndex(String, prop.type) + prop[BooleanFlags.shouldCast] = booleanIndex > -1 + prop[BooleanFlags.shouldCastTrue] = + stringIndex < 0 || booleanIndex < stringIndex + // if the prop needs boolean casting or default value + if (booleanIndex > -1 || hasOwn(prop, 'default')) { + needCastKeys.push(normalizedKey) + } + } + } + } + } + + const res: NormalizedPropsOptions = [normalized, needCastKeys] + return res +} + +function validatePropName(key: string) { + if (key[0] !== '$') { + return true + } + return false +} + +function getType(ctor: Prop): string { + const match = ctor && ctor.toString().match(/^\s*(function|class) (\w+)/) + return match ? match[2] : ctor === null ? 'null' : '' +} + +function isSameType(a: Prop, b: Prop): boolean { + return getType(a) === getType(b) +} + +function getTypeIndex( + type: Prop, + expectedTypes: PropType | void | null | true, +): number { + if (isArray(expectedTypes)) { + return expectedTypes.findIndex((t) => isSameType(t, type)) + } else if (isFunction(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + return -1 +} diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 82a3bfde7..c033d5518 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -6,10 +6,13 @@ import { } from '@vue/shared' import { + Component, ComponentInternalInstance, createComponentInstance, setCurrentInstance, + unsetCurrentInstance, } from './component' +import { initProps } from './componentProps' export type Block = Node | Fragment | Block[] export type ParentBlock = ParentNode | Node[] @@ -17,13 +20,15 @@ export type Fragment = { nodes: Block; anchor: Node } export type BlockFn = (props?: any) => Block export function render( - comp: BlockFn, + comp: Component, + props: any, container: string | ParentNode, ): ComponentInternalInstance { - const instance = createComponentInstance(comp) - setCurrentInstance(instance) - mountComponent(instance, (container = normalizeContainer(container))) - return instance + return mountComponent( + comp, + props, + (container = normalizeContainer(container)), + ) } export function normalizeContainer(container: string | ParentNode): ParentNode { @@ -33,18 +38,27 @@ export function normalizeContainer(container: string | ParentNode): ParentNode { } export const mountComponent = ( - instance: ComponentInternalInstance, + comp: Component, + props: any, container: ParentNode, -) => { +): ComponentInternalInstance => { + const instance = createComponentInstance(comp) + initProps(instance, props) + + setCurrentInstance(instance) instance.container = container const block = instance.scope.run( - () => (instance.block = instance.component()), + () => (instance.block = instance.blockFn(instance.props)), )! insert(block, instance.container) instance.isMounted = true + unsetCurrentInstance() + // TODO: lifecycle hooks (mounted, ...) // const { m } = instance // m && invoke(m) + + return instance } export const unmountComponent = (instance: ComponentInternalInstance) => { diff --git a/playground/src/main.ts b/playground/src/main.ts index 06b2c60ad..0c7a34b16 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,11 +1,22 @@ +import { extend } from '@vue/shared' import { render } from 'vue/vapor' -const modules = import.meta.glob('./*.vue') +const modules = import.meta.glob('./*.(vue|js)') const mod = (modules['.' + location.pathname] || modules['./App.vue'])() mod.then(({ default: m }) => { - render(() => { - const returned = m.setup?.({}, { expose() {} }) - return m.render(returned) - }, '#app') + render( + { + props: m.props, + blockFn: props => { + const returned = m.setup?.(props, { expose() {} }) + const ctx = extend(props, returned) // TODO: merge + return m.render(ctx) + } + }, + { + /* TODO: raw props */ + }, + '#app' + ) }) diff --git a/playground/src/props.js b/playground/src/props.js new file mode 100644 index 000000000..2aa692f84 --- /dev/null +++ b/playground/src/props.js @@ -0,0 +1,94 @@ +import { extend } from '@vue/shared' +import { watch } from 'vue' +import { + children, + on, + ref, + template, + effect, + setText, + render as renderComponent // TODO: +} from '@vue/vapor' + +export default { + props: undefined, + + setup(_, {}) { + const count = ref(1) + const handleClick = () => { + count.value++ + } + return { count, handleClick } + }, + + render(_ctx) { + const t0 = template('') + const n0 = t0() + const { + 0: [n1] + } = children(n0) + on(n1, 'click', _ctx.handleClick) + effect(() => { + setText(n1, void 0, _ctx.count.value) + }) + + // TODO: create component fn? + // const c0 = createComponent(...) + // insert(n0, c0) + renderComponent( + { + props: child.props, + blockFn: props => { + const returned = child.setup?.(props, { expose() {} }) + const ctx = extend(props, returned) // TODO: merge + return child.render(ctx) + } + }, + // TODO: proxy?? + { + /* */ + get count() { + return _ctx.count.value + }, + + /* */ + get inlineDouble() { + return _ctx.count.value * 2 + } + }, + n0 + ) + + return n0 + } +} + +const child = { + props: { + count: { type: Number, default: 1 }, + inlineDouble: { type: Number, default: 2 } + }, + + setup(props) { + watch( + () => props.count, + v => console.log('count changed', v) + ) + watch( + () => props.inlineDouble, + v => console.log('inlineDouble changed', v) + ) + }, + + render(_ctx) { + const t0 = template('

') + const n0 = t0() + const { + 0: [n1] + } = children(n0) + effect(() => { + setText(n1, void 0, _ctx.count + ' * 2 = ' + _ctx.inlineDouble) + }) + return n0 + } +} From 79aa1d9d38048390409d68080273888ab4f9800e Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Thu, 7 Dec 2023 23:59:15 +0900 Subject: [PATCH 2/6] refactor(runtime-vapor): remove dead console.log --- packages/runtime-vapor/src/render.ts | 5 ----- playground/src/props.js | 1 - 2 files changed, 6 deletions(-) diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index c1c8c6615..55c09c796 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -68,11 +68,6 @@ export function mountComponent( const setupFn = typeof component === 'function' ? component : component.setup - console.log( - '🚀 ~ file: render.ts:70 ~ block ~ setupFn:', - component, - setupFn, - ) const state = setupFn(props, ctx) diff --git a/playground/src/props.js b/playground/src/props.js index 4ac1750cc..be43c6dd2 100644 --- a/playground/src/props.js +++ b/playground/src/props.js @@ -1,4 +1,3 @@ -import { extend } from '@vue/shared' import { watch } from 'vue' import { children, From b6e699a1d11f6b188701bd3fdbc8058ff52eeff8 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Fri, 8 Dec 2023 00:17:43 +0900 Subject: [PATCH 3/6] feat(runtime-vapor): componentPublicInstance --- packages/runtime-vapor/src/component.ts | 7 ++++ .../src/componentPublicInstance.ts | 22 +++++++++++ packages/runtime-vapor/src/render.ts | 37 ++++--------------- playground/src/props.js | 6 +-- 4 files changed, 40 insertions(+), 32 deletions(-) create mode 100644 packages/runtime-vapor/src/componentPublicInstance.ts diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 972210309..ad9f116e4 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -31,8 +31,12 @@ export interface ComponentInternalInstance { component: FunctionalComponent | ObjectComponent propsOptions: NormalizedPropsOptions + // TODO: type + proxy: Data | null + // state props: Data + setupState: Data /** directives */ dirs: Map @@ -71,8 +75,11 @@ export const createComponentInstance = ( propsOptions: normalizePropsOptions(component), // emitsOptions: normalizeEmitsOptions(type, appContext), // TODO: + proxy: null, + // state props: EMPTY_OBJ, + setupState: EMPTY_OBJ, dirs: new Map(), diff --git a/packages/runtime-vapor/src/componentPublicInstance.ts b/packages/runtime-vapor/src/componentPublicInstance.ts new file mode 100644 index 000000000..8bfacf981 --- /dev/null +++ b/packages/runtime-vapor/src/componentPublicInstance.ts @@ -0,0 +1,22 @@ +import { hasOwn } from '@vue/shared' +import { type ComponentInternalInstance } from './component' + +export interface ComponentRenderContext { + [key: string]: any + _: ComponentInternalInstance +} + +export const PublicInstanceProxyHandlers: ProxyHandler = { + get({ _: instance }: ComponentRenderContext, key: string) { + let normalizedProps + const { setupState, props } = instance + if (hasOwn(setupState, key)) { + return setupState[key] + } else if ( + (normalizedProps = instance.propsOptions[0]) && + hasOwn(normalizedProps, key) + ) { + return props![key] + } + }, +} diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 55c09c796..9d0a08d8e 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -1,5 +1,5 @@ -import { reactive } from '@vue/reactivity' -import { Data, extend } from '@vue/shared' +import { markRaw, proxyRefs } from '@vue/reactivity' +import { Data } from '@vue/shared' import { type Component, @@ -13,6 +13,7 @@ import { initProps } from './componentProps' import { invokeDirectiveHook } from './directives' import { insert, remove } from './dom' +import { PublicInstanceProxyHandlers } from './componentPublicInstance' export type Block = Node | Fragment | Block[] export type ParentBlock = ParentNode | Node[] @@ -35,26 +36,6 @@ export function normalizeContainer(container: string | ParentNode): ParentNode { : container } -// export const mountComponent = ( -// comp: Component, -// props: any, -// container: ParentNode, -// ): ComponentInternalInstance => { -// const instance = createComponentInstance(comp) -// initProps(instance, props) - -// setCurrentInstance(instance) -// instance.container = container -// const block = instance.scope.run( -// () => (instance.block = instance.blockFn(instance.props)), -// )! -// insert(block, instance.container) -// instance.isMounted = true -// unsetCurrentInstance() - -// return instance -// } - export function mountComponent( instance: ComponentInternalInstance, container: ParentNode, @@ -70,14 +51,12 @@ export function mountComponent( typeof component === 'function' ? component : component.setup const state = setupFn(props, ctx) - + instance.proxy = markRaw( + new Proxy({ _: instance }, PublicInstanceProxyHandlers), + ) if (state && '__isScriptSetup' in state) { - return (instance.block = component.render( - reactive( - // TODO: merge - extend(props, state), - ), - )) + instance.setupState = proxyRefs(state) + return (instance.block = component.render(instance.proxy)) } else { return (instance.block = state as Block) } diff --git a/playground/src/props.js b/playground/src/props.js index be43c6dd2..b80768dcc 100644 --- a/playground/src/props.js +++ b/playground/src/props.js @@ -36,7 +36,7 @@ export default { } = children(n0) on(n1, 'click', _ctx.handleClick) effect(() => { - setText(n1, void 0, _ctx.count.value) + setText(n1, void 0, _ctx.count) }) // TODO: create component fn? @@ -49,12 +49,12 @@ export default { { /* */ get count() { - return _ctx.count.value + return _ctx.count }, /* */ get inlineDouble() { - return _ctx.count.value * 2 + return _ctx.count * 2 } }, n0 From 2460435eaedbb16ebc9bbbf7739e2324e29a08ad Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Fri, 8 Dec 2023 00:36:23 +0900 Subject: [PATCH 4/6] chore(runtime-vapor): fix type import --- packages/runtime-vapor/src/component.ts | 6 +++--- packages/runtime-vapor/src/componentProps.ts | 2 +- packages/runtime-vapor/src/render.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index ad9f116e4..608acc6a1 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -2,10 +2,10 @@ import { EffectScope } from '@vue/reactivity' import { EMPTY_OBJ } from '@vue/shared' import { Block } from './render' -import { DirectiveBinding } from './directives' +import { type DirectiveBinding } from './directives' import { - ComponentPropsOptions, - NormalizedPropsOptions, + type ComponentPropsOptions, + type NormalizedPropsOptions, normalizePropsOptions, } from './componentProps' import type { Data } from '@vue/shared' diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 693de1378..c12ce1f13 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -13,7 +13,7 @@ import { isReservedProp, } from '@vue/shared' import { shallowReactive, toRaw } from '@vue/reactivity' -import { ComponentInternalInstance, Component } from './component' +import { type ComponentInternalInstance, type Component } from './component' export type ComponentPropsOptions

= | ComponentObjectPropsOptions

diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 9d0a08d8e..bc1756363 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -1,5 +1,5 @@ import { markRaw, proxyRefs } from '@vue/reactivity' -import { Data } from '@vue/shared' +import { type Data } from '@vue/shared' import { type Component, From 551e5c10301822736e017ab18cb6637fc7407665 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Fri, 8 Dec 2023 19:43:30 +0900 Subject: [PATCH 5/6] chore(runtime-vapor): remove props type check option --- packages/runtime-vapor/src/componentProps.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index c12ce1f13..5cd0f1d21 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -32,10 +32,6 @@ export interface PropOptions { required?: boolean default?: D | DefaultFactory | null | undefined | object validator?(value: unknown): boolean - /** - * @internal - */ - skipCheck?: boolean /** * @internal */ From 7bcb160c0253c4464a87896b96590e592cc10a69 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Fri, 8 Dec 2023 19:45:36 +0900 Subject: [PATCH 6/6] chore(runtime-vapor): remove dead props --- packages/runtime-vapor/src/component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index ac8746f4d..5ff3b86b8 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -45,7 +45,6 @@ export interface ComponentInternalInstance { // lifecycle get isMounted(): boolean isMountedRef: Ref - isMounted: boolean // TODO: registory of provides, appContext, lifecycles, ... }