import { toRaw, shallowReactive, trigger, TriggerOpTypes } from '@vue/reactivity' import { EMPTY_OBJ, camelize, hyphenate, capitalize, isString, isFunction, isArray, isObject, hasOwn, toRawType, PatchFlags, makeMap, isReservedProp, EMPTY_ARR, def, extend, isOn } from '@vue/shared' import { warn } from './warning' import { Data, ComponentInternalInstance, ComponentOptions, ConcreteComponent, setCurrentInstance } from './component' import { isEmitListener } from './componentEmits' import { InternalObjectKey } from './vnode' import { AppContext } from './apiCreateApp' import { createPropsDefaultThis } from './compat/props' import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig' import { DeprecationTypes } from './compat/compatConfig' import { shouldSkipAttr } from './compat/attrsFallthrough' 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 } export type PropType = PropConstructor | PropConstructor[] type PropConstructor = | { new (...args: any[]): T & {} } | { (): T } | PropMethod type PropMethod = [T] extends [(...args: any) => any] // if is function with args ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor : never type RequiredKeys = { [K in keyof T]: T[K] extends | { required: true } | { default: any } // don't mark Boolean props as undefined | BooleanConstructor | { type: BooleanConstructor } ? T[K] extends { default: undefined | (() => undefined) } ? never : K : never }[keyof T] type OptionalKeys = Exclude> type DefaultKeys = { [K in keyof T]: T[K] extends | { default: any } // Boolean implicitly defaults to false | BooleanConstructor | { type: BooleanConstructor } ? T[K] extends { type: BooleanConstructor; required: true } // not default if Boolean is marked as required ? never : K : never }[keyof T] type InferPropType = [T] extends [null] ? any // null & true would fail to infer : [T] extends [{ type: null | true }] ? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean` : [T] extends [ObjectConstructor | { type: ObjectConstructor }] ? Record : [T] extends [BooleanConstructor | { type: BooleanConstructor }] ? boolean : [T] extends [DateConstructor | { type: DateConstructor }] ? Date : [T] extends [Prop] ? (unknown extends V ? D : V) : T export type ExtractPropTypes = O extends object ? { [K in RequiredKeys]: InferPropType } & { [K in OptionalKeys]?: InferPropType } : { [K in string]: any } const enum BooleanFlags { shouldCast, shouldCastTrue } // extract props which defined with default from prop options export type ExtractDefaultPropTypes = O extends object ? { [K in DefaultKeys]: InferPropType } : {} type NormalizedProp = | null | (PropOptions & { [BooleanFlags.shouldCast]?: boolean [BooleanFlags.shouldCastTrue]?: boolean }) // normalized value is a tuple of the actual normalized options // and an array of prop keys that need value casting (booleans and defaults) export type NormalizedProps = Record export type NormalizedPropsOptions = [NormalizedProps, string[]] | [] export function initProps( instance: ComponentInternalInstance, rawProps: Data | null, isStateful: number, // result of bitwise flag comparison isSSR = false ) { const props: Data = {} const attrs: Data = {} def(attrs, InternalObjectKey, 1) instance.propsDefaults = Object.create(null) setFullProps(instance, rawProps, props, attrs) // ensure all declared prop keys are present for (const key in instance.propsOptions[0]) { if (!(key in props)) { props[key] = undefined } } // validation if (__DEV__) { validateProps(rawProps || {}, props, instance) } if (isStateful) { // stateful instance.props = isSSR ? props : shallowReactive(props) } else { if (!instance.type.props) { // functional w/ optional props, props === attrs instance.props = attrs } else { // functional w/ declared props instance.props = props } } instance.attrs = attrs } export function updateProps( instance: ComponentInternalInstance, rawProps: Data | null, rawPrevProps: Data | null, optimized: boolean ) { const { props, attrs, vnode: { patchFlag } } = instance const rawCurrentProps = toRaw(props) const [options] = instance.propsOptions let hasAttrsChanged = false if ( // always force full diff in dev // - #1942 if hmr is enabled with sfc component // - vite#872 non-sfc component used by sfc component !( __DEV__ && (instance.type.__hmrId || (instance.parent && instance.parent.type.__hmrId)) ) && (optimized || patchFlag > 0) && !(patchFlag & PatchFlags.FULL_PROPS) ) { if (patchFlag & PatchFlags.PROPS) { // Compiler-generated props & no keys change, just set the updated // the props. const propsToUpdate = instance.vnode.dynamicProps! for (let i = 0; i < propsToUpdate.length; i++) { let key = propsToUpdate[i] // PROPS flag guarantees rawProps to be non-null const value = rawProps![key] if (options) { // attr / props separation was done on init and will be consistent // in this code path, so just check if attrs have it. if (hasOwn(attrs, key)) { if (value !== attrs[key]) { attrs[key] = value hasAttrsChanged = true } } else { const camelizedKey = camelize(key) props[camelizedKey] = resolvePropValue( options, rawCurrentProps, camelizedKey, value, instance, false /* isAbsent */ ) } } else { if (__COMPAT__) { if (isOn(key) && key.endsWith('Native')) { key = key.slice(0, -6) // remove Native postfix } else if (shouldSkipAttr(key, instance)) { continue } } if (value !== attrs[key]) { attrs[key] = value hasAttrsChanged = true } } } } } else { // full props update. if (setFullProps(instance, rawProps, props, attrs)) { hasAttrsChanged = true } // in case of dynamic props, check if we need to delete keys from // the props object let kebabKey: string for (const key in rawCurrentProps) { if ( !rawProps || // for camelCase (!hasOwn(rawProps, key) && // it's possible the original props was passed in as kebab-case // and converted to camelCase (#955) ((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey))) ) { if (options) { if ( rawPrevProps && // for camelCase (rawPrevProps[key] !== undefined || // for kebab-case rawPrevProps[kebabKey!] !== undefined) ) { props[key] = resolvePropValue( options, rawCurrentProps, key, undefined, instance, true /* isAbsent */ ) } } else { delete props[key] } } } // in the case of functional component w/o props declaration, props and // attrs point to the same object so it should already have been updated. if (attrs !== rawCurrentProps) { for (const key in attrs) { if (!rawProps || !hasOwn(rawProps, key)) { delete attrs[key] hasAttrsChanged = true } } } } // trigger updates for $attrs in case it's used in component slots if (hasAttrsChanged) { trigger(instance, TriggerOpTypes.SET, '$attrs') } if (__DEV__) { validateProps(rawProps || {}, props, instance) } } function setFullProps( instance: ComponentInternalInstance, rawProps: Data | null, props: Data, attrs: Data ) { const [options, needCastKeys] = instance.propsOptions let hasAttrsChanged = false let rawCastValues: Data | undefined if (rawProps) { for (let key in rawProps) { // key, ref are reserved and never passed down if (isReservedProp(key)) { continue } if (__COMPAT__) { if (key.startsWith('onHook:')) { softAssertCompatEnabled( DeprecationTypes.INSTANCE_EVENT_HOOKS, instance, key.slice(2).toLowerCase() ) } if (key === 'inline-template') { continue } } const value = rawProps[key] // prop option names are camelized during normalization, so to support // kebab -> camel conversion here we need to camelize the key. let camelKey if (options && hasOwn(options, (camelKey = camelize(key)))) { if (!needCastKeys || !needCastKeys.includes(camelKey)) { props[camelKey] = value } else { ;(rawCastValues || (rawCastValues = {}))[camelKey] = value } } else if (!isEmitListener(instance.emitsOptions, key)) { // Any non-declared (either as a prop or an emitted event) props are put // into a separate `attrs` object for spreading. Make sure to preserve // original key casing if (__COMPAT__) { if (isOn(key) && key.endsWith('Native')) { key = key.slice(0, -6) // remove Native postfix } else if (shouldSkipAttr(key, instance)) { continue } } if (value !== attrs[key]) { attrs[key] = value hasAttrsChanged = true } } } } if (needCastKeys) { const rawCurrentProps = toRaw(props) const castValues = rawCastValues || EMPTY_OBJ for (let i = 0; i < needCastKeys.length; i++) { const key = needCastKeys[i] props[key] = resolvePropValue( options!, rawCurrentProps, key, castValues[key], instance, !hasOwn(castValues, key) ) } } return hasAttrsChanged } 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 && isFunction(defaultValue)) { 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 ) setCurrentInstance(null) } } 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: ConcreteComponent, appContext: AppContext, asMixin = false ): NormalizedPropsOptions { const cache = appContext.propsCache const cached = cache.get(comp) if (cached) { return cached } const raw = comp.props const normalized: NormalizedPropsOptions[0] = {} const needCastKeys: NormalizedPropsOptions[1] = [] // apply mixin/extends props let hasExtends = false if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) { const extendProps = (raw: ComponentOptions) => { if (__COMPAT__ && isFunction(raw)) { raw = raw.options } hasExtends = true const [props, keys] = normalizePropsOptions(raw, appContext, true) extend(normalized, props) if (keys) needCastKeys.push(...keys) } if (!asMixin && appContext.mixins.length) { appContext.mixins.forEach(extendProps) } if (comp.extends) { extendProps(comp.extends) } if (comp.mixins) { comp.mixins.forEach(extendProps) } } if (!raw && !hasExtends) { cache.set(comp, EMPTY_ARR as any) return EMPTY_ARR as any } if (isArray(raw)) { for (let i = 0; i < raw.length; i++) { if (__DEV__ && !isString(raw[i])) { warn(`props must be strings when using array syntax.`, raw[i]) } const normalizedKey = camelize(raw[i]) if (validatePropName(normalizedKey)) { normalized[normalizedKey] = EMPTY_OBJ } } } else if (raw) { if (__DEV__ && !isObject(raw)) { warn(`invalid props options`, 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 } : 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] cache.set(comp, res) return res } function validatePropName(key: string) { if (key[0] !== '$') { return true } else if (__DEV__) { warn(`Invalid prop name: "${key}" is a reserved property.`) } return false } // use function string name to check type constructors // so that it works across vms / iframes. function getType(ctor: Prop): string { const match = ctor && ctor.toString().match(/^\s*function (\w+)/) return match ? match[1] : '' } 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 } /** * dev only */ function validateProps( rawProps: Data, props: Data, instance: ComponentInternalInstance ) { const resolvedValues = toRaw(props) const options = instance.propsOptions[0] for (const key in options) { let opt = options[key] if (opt == null) continue validateProp( key, resolvedValues[key], opt, !hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key)) ) } } /** * dev only */ function validateProp( name: string, value: unknown, prop: PropOptions, isAbsent: boolean ) { const { type, required, validator } = prop // required! if (required && isAbsent) { warn('Missing required prop: "' + name + '"') return } // missing but optional if (value == null && !prop.required) { return } // type check if (type != null && type !== true) { let isValid = false const types = isArray(type) ? type : [type] const expectedTypes = [] // value is valid as long as one of the specified types match for (let i = 0; i < types.length && !isValid; i++) { const { valid, expectedType } = assertType(value, types[i]) expectedTypes.push(expectedType || '') isValid = valid } if (!isValid) { warn(getInvalidTypeMessage(name, value, expectedTypes)) return } } // custom validator if (validator && !validator(value)) { warn('Invalid prop: custom validator check failed for prop "' + name + '".') } } const isSimpleType = /*#__PURE__*/ makeMap( 'String,Number,Boolean,Function,Symbol,BigInt' ) type AssertionResult = { valid: boolean expectedType: string } /** * dev only */ function assertType(value: unknown, type: PropConstructor): AssertionResult { let valid const expectedType = getType(type) if (isSimpleType(expectedType)) { const t = typeof value valid = t === expectedType.toLowerCase() // for primitive wrapper objects if (!valid && t === 'object') { valid = value instanceof type } } else if (expectedType === 'Object') { valid = isObject(value) } else if (expectedType === 'Array') { valid = isArray(value) } else { valid = value instanceof type } return { valid, expectedType } } /** * dev only */ function getInvalidTypeMessage( name: string, value: unknown, expectedTypes: string[] ): string { let message = `Invalid prop: type check failed for prop "${name}".` + ` Expected ${expectedTypes.map(capitalize).join(', ')}` const expectedType = expectedTypes[0] const receivedType = toRawType(value) const expectedValue = styleValue(value, expectedType) const receivedValue = styleValue(value, receivedType) // check if we need to specify expected value if ( expectedTypes.length === 1 && isExplicable(expectedType) && !isBoolean(expectedType, receivedType) ) { message += ` with value ${expectedValue}` } message += `, got ${receivedType} ` // check if we need to specify received value if (isExplicable(receivedType)) { message += `with value ${receivedValue}.` } return message } /** * dev only */ function styleValue(value: unknown, type: string): string { if (type === 'String') { return `"${value}"` } else if (type === 'Number') { return `${Number(value)}` } else { return `${value}` } } /** * dev only */ function isExplicable(type: string): boolean { const explicitTypes = ['string', 'number', 'boolean'] return explicitTypes.some(elem => type.toLowerCase() === elem) } /** * dev only */ function isBoolean(...args: string[]): boolean { return args.some(elem => elem.toLowerCase() === 'boolean') }