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 } 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' 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 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 } ? 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 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++) { 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, instance ) } } 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 || // 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, rawProps || EMPTY_OBJ, key, undefined, instance ) } } 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] } } } } // trigger updates for $attrs in case it's used in component slots 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 if (rawProps) { for (const key in rawProps) { // key, ref are reserved and never passed down if (isReservedProp(key)) { 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)))) { props[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 attrs[key] = value } } } if (needCastKeys) { const rawCurrentProps = toRaw(props) for (let i = 0; i < needCastKeys.length; i++) { const key = needCastKeys[i] props[key] = resolvePropValue( options!, rawCurrentProps, key, rawCurrentProps[key], instance ) } } } function resolvePropValue( options: NormalizedProps, props: Data, key: string, value: unknown, instance: ComponentInternalInstance ) { 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(props) setCurrentInstance(null) } } else { value = 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( comp: ConcreteComponent, appContext: AppContext, asMixin = false ): NormalizedPropsOptions { if (!appContext.deopt && comp.__props) { return comp.__props } 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) => { 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) { return (comp.__props = 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) } } } } } return (comp.__props = [normalized, needCastKeys]) } 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') }