import { EMPTY_ARR, NO, YES, camelize, hasOwn, isArray, isFunction, isString, } from '@vue/shared' import type { VaporComponent, VaporComponentInstance } from './component' import { type NormalizedPropsOptions, baseNormalizePropsOptions, currentInstance, isEmitListener, popWarningContext, pushWarningContext, resolvePropValue, simpleSetCurrentInstance, validateProps, warn, } from '@vue/runtime-dom' import { normalizeEmitsOptions } from './componentEmits' import { renderEffect } from './renderEffect' export type RawProps = Record unknown> & { // generated by compiler for :[key]="x" or v-bind="x" $?: DynamicPropsSource[] } export type DynamicPropsSource = | (() => Record) | Record unknown> // TODO optimization: maybe convert functions into computeds export function resolveSource( source: Record | (() => Record), ): Record { return isFunction(source) ? source() : source } export function getPropsProxyHandlers( comp: VaporComponent, ): [ ProxyHandler | null, ProxyHandler, ] { if (comp.__propsHandlers) { return comp.__propsHandlers } const propsOptions = normalizePropsOptions(comp)[0] const emitsOptions = normalizeEmitsOptions(comp) const isProp = ( propsOptions ? (key: string | symbol) => isString(key) && hasOwn(propsOptions, camelize(key)) : NO ) as (key: string | symbol) => key is string const isAttr = propsOptions ? (key: string) => key !== '$' && !isProp(key) && !isEmitListener(emitsOptions, key) : YES const getProp = (instance: VaporComponentInstance, key: string | symbol) => { if (!isProp(key)) return const rawProps = instance.rawProps const dynamicSources = rawProps.$ if (dynamicSources) { let i = dynamicSources.length let source, isDynamic, rawKey while (i--) { source = dynamicSources[i] isDynamic = isFunction(source) source = isDynamic ? (source as Function)() : source for (rawKey in source) { if (camelize(rawKey) === key) { return resolvePropValue( propsOptions!, key, isDynamic ? source[rawKey] : source[rawKey](), instance, resolveDefault, ) } } } } for (const rawKey in rawProps) { if (camelize(rawKey) === key) { return resolvePropValue( propsOptions!, key, rawProps[rawKey](), instance, resolveDefault, ) } } return resolvePropValue( propsOptions!, key, undefined, instance, resolveDefault, true, ) } const propsHandlers = propsOptions ? ({ get: (target, key) => getProp(target, key), has: (_, key) => isProp(key), ownKeys: () => Object.keys(propsOptions), getOwnPropertyDescriptor(target, key) { if (isProp(key)) { return { configurable: true, enumerable: true, get: () => getProp(target, key), } } }, } satisfies ProxyHandler) : null if (__DEV__ && propsOptions) { Object.assign(propsHandlers!, { set: propsSetDevTrap, deleteProperty: propsDeleteDevTrap, }) } const getAttr = (target: RawProps, key: string) => { if (!isProp(key) && !isEmitListener(emitsOptions, key)) { return getAttrFromRawProps(target, key) } } const hasAttr = (target: RawProps, key: string) => { if (isAttr(key)) { return hasAttrFromRawProps(target, key) } else { return false } } const attrsHandlers = { get: (target, key: string) => getAttr(target.rawProps, key), has: (target, key: string) => hasAttr(target.rawProps, key), ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr), getOwnPropertyDescriptor(target, key: string) { if (hasAttr(target.rawProps, key)) { return { configurable: true, enumerable: true, get: () => getAttr(target.rawProps, key), } } }, } satisfies ProxyHandler if (__DEV__) { Object.assign(attrsHandlers, { set: propsSetDevTrap, deleteProperty: propsDeleteDevTrap, }) } return (comp.__propsHandlers = [propsHandlers, attrsHandlers]) } export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown { if (key === '$') return // need special merging behavior for class & style const merged = key === 'class' || key === 'style' ? ([] as any[]) : undefined const dynamicSources = rawProps.$ if (dynamicSources) { let i = dynamicSources.length let source, isDynamic while (i--) { source = dynamicSources[i] isDynamic = isFunction(source) source = isDynamic ? (source as Function)() : source if (hasOwn(source, key)) { const value = isDynamic ? source[key] : source[key]() if (merged) { merged.push(value) } else { return value } } } } if (hasOwn(rawProps, key)) { if (merged) { merged.push(rawProps[key]()) } else { return rawProps[key]() } } return merged } export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean { if (key === '$') return false const dynamicSources = rawProps.$ if (dynamicSources) { let i = dynamicSources.length while (i--) { if (hasOwn(resolveSource(dynamicSources[i]), key)) { return true } } } return hasOwn(rawProps, key) } export function getKeysFromRawProps(rawProps: RawProps): string[] { const keys: string[] = [] for (const key in rawProps) { if (key !== '$') keys.push(key) } const dynamicSources = rawProps.$ if (dynamicSources) { let i = dynamicSources.length let source while (i--) { source = resolveSource(dynamicSources[i]) for (const key in source) { keys.push(key) } } } return Array.from(new Set(keys)) } export function normalizePropsOptions( comp: VaporComponent, ): NormalizedPropsOptions { const cached = comp.__propsOptions if (cached) return cached const raw = comp.props if (!raw) return EMPTY_ARR as [] const normalized: NormalizedPropsOptions[0] = {} const needCastKeys: NormalizedPropsOptions[1] = [] baseNormalizePropsOptions(raw, normalized, needCastKeys) return (comp.__propsOptions = [normalized, needCastKeys]) } function resolveDefault( factory: (props: Record) => unknown, instance: VaporComponentInstance, ) { const prev = currentInstance simpleSetCurrentInstance(instance) const res = factory.call(null, instance.props) simpleSetCurrentInstance(prev, instance) return res } export function hasFallthroughAttrs( comp: VaporComponent, rawProps: RawProps | null | undefined, ): boolean { if (rawProps) { // determine fallthrough if (rawProps.$ || !comp.props) { return true } else { // check if rawProps contains any keys not declared const propsOptions = normalizePropsOptions(comp)[0]! for (const key in rawProps) { if (!hasOwn(propsOptions, camelize(key))) { return true } } } } return false } /** * dev only */ export function setupPropsValidation(instance: VaporComponentInstance): void { const rawProps = instance.rawProps if (!rawProps) return renderEffect(() => { pushWarningContext(instance) validateProps( resolveDynamicProps(rawProps), instance.props, normalizePropsOptions(instance.type)[0]!, ) popWarningContext() }, true /* noLifecycle */) } export function resolveDynamicProps(props: RawProps): Record { const mergedRawProps: Record = {} for (const key in props) { if (key !== '$') { mergedRawProps[key] = props[key]() } } if (props.$) { for (const source of props.$) { const isDynamic = isFunction(source) const resolved = isDynamic ? source() : source for (const key in resolved) { const value = isDynamic ? resolved[key] : (resolved[key] as Function)() if (key === 'class' || key === 'style') { const existing = mergedRawProps[key] if (isArray(existing)) { existing.push(value) } else { mergedRawProps[key] = [existing, value] } } else { mergedRawProps[key] = value } } } } return mergedRawProps } function propsSetDevTrap(_: any, key: string | symbol) { warn( `Attempt to mutate prop ${JSON.stringify(key)} failed. Props are readonly.`, ) return true } function propsDeleteDevTrap(_: any, key: string | symbol) { warn( `Attempt to delete prop ${JSON.stringify(key)} failed. Props are readonly.`, ) return true }