diff --git a/README.md b/README.md index 8ff103f92..bfc5cfdc4 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,9 @@ The code provided here is a duplicate from `runtime-core` as Vapor cannot import - packages/runtime-vapor/src/apiWatch.ts - packages/runtime-vapor/src/component.ts +- packages/runtime-vapor/src/componentEmits.ts - packages/runtime-vapor/src/componentProps.ts +- packages/runtime-vapor/src/componentPublicInstance.ts - packages/runtime-vapor/src/enums.ts - packages/runtime-vapor/src/errorHandling.ts - packages/runtime-vapor/src/scheduler.ts diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index a3fdde483..6ec3dacc9 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -501,12 +501,11 @@ export type ShallowUnwrapRef = { type DistrubuteRef = T extends Ref ? V : T -export type UnwrapRef = - T extends ShallowRef - ? V - : T extends Ref - ? UnwrapRefSimple - : UnwrapRefSimple +export type UnwrapRef = T extends ShallowRef + ? V + : T extends Ref + ? UnwrapRefSimple + : UnwrapRefSimple export type UnwrapRefSimple = T extends | Function diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index 4551235bc..3c0a111aa 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -54,30 +54,28 @@ export type EmitsToProps = T extends string[] } : {} -export type ShortEmitsToObject = - E extends Record - ? { - [K in keyof E]: (...args: E[K]) => any - } - : E +export type ShortEmitsToObject = E extends Record + ? { + [K in keyof E]: (...args: E[K]) => any + } + : E export type EmitFn< Options = ObjectEmitsOptions, Event extends keyof Options = keyof Options, -> = - Options extends Array - ? (event: V, ...args: any[]) => void - : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function - ? (event: string, ...args: any[]) => void - : UnionToIntersection< - { - [key in Event]: Options[key] extends (...args: infer Args) => any - ? (event: key, ...args: Args) => void - : Options[key] extends any[] - ? (event: key, ...args: Options[key]) => void - : (event: key, ...args: any[]) => void - }[Event] - > +> = Options extends Array + ? (event: V, ...args: any[]) => void + : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function + ? (event: string, ...args: any[]) => void + : UnionToIntersection< + { + [key in Event]: Options[key] extends (...args: infer Args) => any + ? (event: key, ...args: Args) => void + : Options[key] extends any[] + ? (event: key, ...args: Options[key]) => void + : (event: key, ...args: any[]) => void + }[Event] + > export function emit( instance: ComponentInternalInstance, diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 03cb07d94..c190810c6 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -84,36 +84,34 @@ type IsDefaultMixinComponent = T extends ComponentOptionsMixin : false : false -type MixinToOptionTypes = - T extends ComponentOptionsBase< - infer P, - infer B, - infer D, - infer C, - infer M, - infer Mixin, - infer Extends, - any, - any, - infer Defaults, - any, - any, - any - > - ? OptionTypesType

& - IntersectionMixin & - IntersectionMixin - : never +type MixinToOptionTypes = T extends ComponentOptionsBase< + infer P, + infer B, + infer D, + infer C, + infer M, + infer Mixin, + infer Extends, + any, + any, + infer Defaults, + any, + any, + any +> + ? OptionTypesType

& + IntersectionMixin & + IntersectionMixin + : never // ExtractMixin(map type) is used to resolve circularly references type ExtractMixin = { Mixin: MixinToOptionTypes }[T extends ComponentOptionsMixin ? 'Mixin' : never] -export type IntersectionMixin = - IsDefaultMixinComponent extends true - ? OptionTypesType - : UnionToIntersection> +export type IntersectionMixin = IsDefaultMixinComponent extends true + ? OptionTypesType + : UnionToIntersection> export type UnwrapMixinsType< T, diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 7ed267668..2099d3626 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,15 +1,17 @@ +import type { Data } from '@vue/shared' +import { EMPTY_OBJ } from '@vue/shared' import { EffectScope } from '@vue/reactivity' -import { EMPTY_OBJ } from '@vue/shared' import type { Block } from './render' import type { DirectiveBinding } from './directive' -import { - type ComponentPropsOptions, - type NormalizedPropsOptions, - normalizePropsOptions, +import type { + ComponentPropsOptions, + NormalizedPropsOptions, } from './componentProps' +import { normalizePropsOptions } from './componentProps' -import type { Data } from '@vue/shared' +import type { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits' +import { emit, normalizeEmitsOptions } from './componentEmits' import { VaporLifecycleHooks } from './enums' export type Component = FunctionalComponent | ObjectComponent @@ -17,10 +19,12 @@ export type Component = FunctionalComponent | ObjectComponent export type SetupFn = (props: any, ctx: any) => Block | Data export type FunctionalComponent = SetupFn & { props: ComponentPropsOptions + emits: EmitsOptions render(ctx: any): Block } export interface ObjectComponent { props: ComponentPropsOptions + emits: EmitsOptions setup?: SetupFn render(ctx: any): Block } @@ -36,14 +40,25 @@ export interface ComponentInternalInstance { container: ParentNode block: Block | null scope: EffectScope + + // conventional vnode.type component: FunctionalComponent | ObjectComponent + rawProps: Data + + // normalized options propsOptions: NormalizedPropsOptions + emitsOptions: ObjectEmitsOptions | null parent: ComponentInternalInstance | null + // TODO: type + proxy: Data | null + // state props: Data setupState: Data + emit: EmitFn + emitted: Record | null refs: Data metadata: WeakMap @@ -52,11 +67,13 @@ export interface ComponentInternalInstance { /** directives */ dirs: Map + // TODO: registory of provides, appContext, ... + // lifecycle isMounted: boolean isUnmounted: boolean isUpdating: boolean - // TODO: registory of provides, lifecycles, ... + /** * @internal */ @@ -139,6 +156,7 @@ export const unsetCurrentInstance = () => { let uid = 0 export const createComponentInstance = ( component: ObjectComponent | FunctionalComponent, + rawProps: Data, ): ComponentInternalInstance => { const instance: ComponentInternalInstance = { uid: uid++, @@ -146,12 +164,22 @@ export const createComponentInstance = ( container: null!, // set on mountComponent scope: new EffectScope(true /* detached */)!, component, + rawProps, // TODO: registory of parent parent: null, // resolved props and emits options propsOptions: normalizePropsOptions(component), + + emitsOptions: normalizeEmitsOptions(component), + + // emit + emit: null!, // to be set immediately + emitted: null, + + proxy: null, + // emitsOptions: normalizeEmitsOptions(type, appContext), // TODO: // state @@ -163,11 +191,14 @@ export const createComponentInstance = ( dirs: new Map(), + // TODO: registory of provides, appContext, ... + // lifecycle + isMounted: false, isUnmounted: false, isUpdating: false, - // TODO: registory of provides, appContext, lifecycles, ... + /** * @internal */ @@ -225,5 +256,8 @@ export const createComponentInstance = ( */ // [VaporLifecycleHooks.SERVER_PREFETCH]: null, } + + instance.emit = emit.bind(null, instance) + return instance } diff --git a/packages/runtime-vapor/src/componentEmits.ts b/packages/runtime-vapor/src/componentEmits.ts new file mode 100644 index 000000000..c1e55b06e --- /dev/null +++ b/packages/runtime-vapor/src/componentEmits.ts @@ -0,0 +1,95 @@ +// NOTE: runtime-core/src/componentEmits.ts + +import { + type UnionToIntersection, + camelize, + extend, + hyphenate, + isArray, + isFunction, + toHandlerKey, +} from '@vue/shared' +import type { Component, ComponentInternalInstance } from './component' + +export type ObjectEmitsOptions = Record< + string, + ((...args: any[]) => any) | null // TODO: call validation? +> + +export type EmitsOptions = ObjectEmitsOptions | string[] + +export type EmitFn< + Options = ObjectEmitsOptions, + Event extends keyof Options = keyof Options, +> = Options extends Array + ? (event: V, ...args: any[]) => void + : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function + ? (event: string, ...args: any[]) => void + : UnionToIntersection< + { + [key in Event]: Options[key] extends (...args: infer Args) => any + ? (event: key, ...args: Args) => void + : (event: key, ...args: any[]) => void + }[Event] + > + +export function emit( + instance: ComponentInternalInstance, + event: string, + ...rawArgs: any[] +) { + if (instance.isUnmounted) return + const props = instance.rawProps // TODO: raw props + + let args = rawArgs + + // TODO: modelListener + + let handlerName + let handler = + props[(handlerName = toHandlerKey(event))] || + // also try camelCase event handler (#2249) + props[(handlerName = toHandlerKey(camelize(event)))] + // for v-model update:xxx events, also trigger kebab-case equivalent + // for props passed via kebab-case + if (!handler) { + handler = props[(handlerName = toHandlerKey(hyphenate(event)))] + } + + if (handler && isFunction(handler)) { + // TODO: callWithAsyncErrorHandling + handler(...args) + } + + const onceHandler = props[handlerName + `Once`] + if (onceHandler) { + if (!instance.emitted) { + instance.emitted = {} + } else if (instance.emitted[handlerName]) { + return + } + + if (isFunction(onceHandler)) { + instance.emitted[handlerName] = true + // TODO: callWithAsyncErrorHandling + onceHandler(...args) + } + } +} + +export function normalizeEmitsOptions( + comp: Component, +): ObjectEmitsOptions | null { + // TODO: caching? + + const raw = comp.emits + let normalized: ObjectEmitsOptions = {} + + if (isArray(raw)) { + raw.forEach((key) => (normalized[key] = null)) + } else { + extend(normalized, raw) + } + + return normalized +} diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 18f51a568..a7cf46461 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -77,6 +77,7 @@ export function initProps( if (rawProps) { for (let key in rawProps) { // key, ref are reserved and never passed down + // TODO: remove vnode if (isReservedProp(key)) { continue } diff --git a/packages/runtime-vapor/src/componentPublicInstance.ts b/packages/runtime-vapor/src/componentPublicInstance.ts new file mode 100644 index 000000000..5589dbbb2 --- /dev/null +++ b/packages/runtime-vapor/src/componentPublicInstance.ts @@ -0,0 +1,24 @@ +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] + } + }, +} + +// TODO: publicPropertiesMap diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 10b121a9e..0df3d3941 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -1,5 +1,6 @@ import { proxyRefs } from '@vue/reactivity' import { type Data, invokeArrayFns, isArray, isObject } from '@vue/shared' + import { type Component, type ComponentInternalInstance, @@ -22,7 +23,7 @@ export function render( props: Data, container: string | ParentNode, ): ComponentInternalInstance { - const instance = createComponentInstance(comp) + const instance = createComponentInstance(comp, props) initProps(instance, props) return mountComponent(instance, (container = normalizeContainer(container))) } @@ -42,8 +43,8 @@ export function mountComponent( const reset = setCurrentInstance(instance) const block = instance.scope.run(() => { - const { component, props } = instance - const ctx = { expose: () => {} } + const { component, props, emit } = instance + const ctx = { emit, expose: () => {} } const setupFn = typeof component === 'function' ? component : component.setup diff --git a/playground/src/emits.js b/playground/src/emits.js new file mode 100644 index 000000000..4d019fc1a --- /dev/null +++ b/playground/src/emits.js @@ -0,0 +1,100 @@ +import { + children, + on, + ref, + render as renderComponent, // TODO: + setText, + template, + watchEffect, +} from '@vue/vapor' + +export default { + props: undefined, + + setup(_, {}) { + const count = ref(1) + const setCount = v => { + count.value = v + } + + const __returned__ = { count, setCount } + + Object.defineProperty(__returned__, '__isScriptSetup', { + enumerable: false, + value: true, + }) + + return __returned__ + }, + + render(_ctx) { + const t0 = template('

') + const n0 = t0() + const { + 0: [n1], + } = children(n0) + + watchEffect(() => { + setText(n1, void 0, _ctx.count) + }) + + renderComponent( + child, + { + get count() { + return _ctx.count + }, + get ['onClick:child']() { + return _ctx.setCount + }, + }, + n0, + ) + + renderComponent( + child, + { + get count() { + return _ctx.count + }, + get ['onClick:childOnce']() { + return _ctx.setCount + }, + }, + n0, + ) + + return n0 + }, +} + +const child = { + props: { + count: { type: Number, default: 1 }, + }, + + setup(props, { emit }) { + const handleClick = () => { + emit('click:child', props.count * 2) + } + + const __returned__ = { handleClick } + + Object.defineProperty(__returned__, '__isScriptSetup', { + enumerable: false, + value: true, + }) + + return __returned__ + }, + + render(_ctx) { + const t0 = template('') + const n0 = t0() + const { + 0: [n1], + } = children(n0) + on(n1, 'click', _ctx.handleClick) + return n0 + }, +} diff --git a/playground/src/props.js b/playground/src/props.js index e2d0d0c76..2020111b7 100644 --- a/playground/src/props.js +++ b/playground/src/props.js @@ -60,6 +60,20 @@ export default { n0, ) + // test default value + renderComponent( + child, + { + get count() { + return _ctx.count % 2 === 0 ? _ctx.count : undefined + }, + get inlineDouble() { + return _ctx.count % 2 === 0 ? undefined : _ctx.count * 2 + }, + }, + n0, + ) + return n0 }, } diff --git a/tsconfig.json b/tsconfig.json index 159200b40..ced7bbc1e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,8 +25,8 @@ "@vue/compat": ["packages/vue-compat/src"], "@vue/vapor": ["packages/vue-vapor/src"], "@vue/*": ["packages/*/src"], - "vue": ["packages/vue/src"], - }, + "vue": ["packages/vue/src"] + } }, "include": [ "packages/global.d.ts", @@ -37,6 +37,6 @@ "packages/vue/jsx-runtime", "scripts/*", "rollup.*.js", - "playground", - ], + "playground" + ] }