import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity' import { EMPTY_OBJ, camelize, hyphenate, capitalize, isString, isFunction, isArray, isObject, hasOwn, toRawType, PatchFlags, makeMap, isReservedProp, EMPTY_ARR, def } from '@vue/shared' import { warn } from './warning' import { Data, ComponentInternalInstance } from './component' import { isEmitListener } from './componentEmits' import { InternalObjectSymbol } from './vnode' export type ComponentPropsOptions

= | ComponentObjectPropsOptions

| string[] export type ComponentObjectPropsOptions

= { [K in keyof P]: Prop | null } export type Prop = PropOptions | PropType type DefaultFactory = () => T | null | undefined interface PropOptions { type?: PropType | true | null required?: boolean default?: T | DefaultFactory | null | undefined validator?(value: unknown): boolean } export type PropType = PropConstructor | PropConstructor[] type PropConstructor = | { new (...args: any[]): T & object } | { (): T } | PropMethod type PropMethod = T extends (...args: any) => any // if is function with args ? { new (): T; (): T; readonly proptotype: Function } // Create Function like contructor : never type RequiredKeys = { [K in keyof T]: T[K] extends | { required: true } | (MakeDefaultRequired extends true ? { default: any } : never) ? K : never }[keyof T] type OptionalKeys = Exclude< keyof T, RequiredKeys > type InferPropType = T extends null ? any // null & true would fail to infer : T extends { type: null | true } ? any // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` : T extends ObjectConstructor | { type: ObjectConstructor } ? { [key: string]: any } : T extends Prop ? V : T export type ExtractPropTypes< O, MakeDefaultRequired extends boolean = true > = O extends object ? { [K in RequiredKeys]: InferPropType } & { [K in OptionalKeys]?: InferPropType } : { [K in string]: any } const enum BooleanFlags { shouldCast, shouldCastTrue } 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) type NormalizedPropsOptions = [Record, 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, InternalObjectSymbol, true) setFullProps(instance, rawProps, props, attrs) const options = instance.type.props // validation if (__DEV__ && options && rawProps) { validateProps(props, options) } if (isStateful) { // stateful instance.props = isSSR ? props : shallowReadonly(props) } else { if (!options) { // 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, optimized: boolean ) { // allow mutation of propsProxy (which is readonly by default) unlock() const { props, attrs, vnode: { patchFlag } } = instance const rawOptions = instance.type.props const rawCurrentProps = toRaw(props) const { 0: options } = normalizePropsOptions(rawOptions) if ((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++) { const 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)) { attrs[key] = value } else { const camelizedKey = camelize(key) props[camelizedKey] = resolvePropValue( options, rawCurrentProps, camelizedKey, value ) } } else { attrs[key] = value } } } } else { // full props update. setFullProps(instance, rawProps, props, attrs) // 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 || (!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) { props[key] = resolvePropValue( options, rawProps || EMPTY_OBJ, key, undefined ) } else { delete props[key] } } } for (const key in attrs) { if (!rawProps || !hasOwn(rawProps, key)) { delete attrs[key] } } } // lock readonly lock() if (__DEV__ && rawOptions && rawProps) { validateProps(props, rawOptions) } } function setFullProps( instance: ComponentInternalInstance, rawProps: Data | null, props: Data, attrs: Data ) { const { 0: options, 1: needCastKeys } = normalizePropsOptions( instance.type.props ) const emits = instance.type.emits if (rawProps) { for (const key in rawProps) { const value = rawProps[key] // key, ref are reserved and never passed down if (isReservedProp(key)) { continue } // 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)))) { props[camelKey] = value } else if (!emits || !isEmitListener(emits, 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 attrs[key] = value } } } if (needCastKeys) { for (let i = 0; i < needCastKeys.length; i++) { const key = needCastKeys[i] props[key] = resolvePropValue(options!, props, key, props[key]) } } } function resolvePropValue( options: NormalizedPropsOptions[0], props: Data, key: string, value: unknown ) { const opt = options[key] if (opt != null) { const hasDefault = hasOwn(opt, 'default') // default values if (hasDefault && value === undefined) { const defaultValue = opt.default value = isFunction(defaultValue) ? defaultValue() : defaultValue } // boolean casting if (opt[BooleanFlags.shouldCast]) { if (!hasOwn(props, key) && !hasDefault) { value = false } else if ( opt[BooleanFlags.shouldCastTrue] && (value === '' || value === hyphenate(key)) ) { value = true } } } return value } export function normalizePropsOptions( raw: ComponentPropsOptions | undefined ): NormalizedPropsOptions | [] { if (!raw) { return EMPTY_ARR as any } if ((raw as any)._n) { return (raw as any)._n } const normalized: NormalizedPropsOptions[0] = {} const needCastKeys: NormalizedPropsOptions[1] = [] 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 (__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 normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys] def(raw, '_n', normalizedEntry) return normalizedEntry } // 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)) { for (let i = 0, len = expectedTypes.length; i < len; i++) { if (isSameType(expectedTypes[i], type)) { return i } } } else if (isFunction(expectedTypes)) { return isSameType(expectedTypes, type) ? 0 : -1 } return -1 } function validateProps(props: Data, rawOptions: ComponentPropsOptions) { const rawValues = toRaw(props) const options = normalizePropsOptions(rawOptions)[0] for (const key in options) { let opt = options[key] if (opt == null) continue validateProp(key, rawValues[key], opt, !hasOwn(rawValues, key)) } } function validatePropName(key: string) { if (key[0] !== '$') { return true } else if (__DEV__) { warn(`Invalid prop name: "${key}" is a reserved property.`) } return false } 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' ) type AssertionResult = { valid: boolean expectedType: string } 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 = toRawType(value) === 'Object' } else if (expectedType === 'Array') { valid = isArray(value) } else { valid = value instanceof type } return { valid, expectedType } } 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 } function styleValue(value: unknown, type: string): string { if (type === 'String') { return `"${value}"` } else if (type === 'Number') { return `${Number(value)}` } else { return `${value}` } } function isExplicable(type: string): boolean { const explicitTypes = ['string', 'number', 'boolean'] return explicitTypes.some(elem => type.toLowerCase() === elem) } function isBoolean(...args: string[]): boolean { return args.some(elem => elem.toLowerCase() === 'boolean') }