| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- import { toRaw, lock, unlock } from '@vue/reactivity'
- import {
- EMPTY_OBJ,
- camelize,
- hyphenate,
- capitalize,
- isString,
- isFunction,
- isArray,
- isObject,
- hasOwn,
- toRawType,
- PatchFlags,
- makeMap,
- isReservedProp
- } from '@vue/shared'
- import { warn } from './warning'
- import { Data, ComponentInternalInstance } from './component'
- export type ComponentPropsOptions<P = Data> =
- | ComponentObjectPropsOptions<P>
- | string[]
- export type ComponentObjectPropsOptions<P = Data> = {
- [K in keyof P]: Prop<P[K]> | null
- }
- export type Prop<T> = PropOptions<T> | PropType<T>
- type DefaultFactory<T> = () => T | null | undefined
- interface PropOptions<T = any> {
- type?: PropType<T> | true | null
- required?: boolean
- default?: T | DefaultFactory<T> | null | undefined
- validator?(value: unknown): boolean
- }
- export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
- type PropConstructor<T = any> = { new (...args: any[]): T & object } | { (): T }
- type RequiredKeys<T, MakeDefaultRequired> = {
- [K in keyof T]: T[K] extends
- | { required: true }
- | (MakeDefaultRequired extends true ? { default: any } : never)
- ? K
- : never
- }[keyof T]
- type OptionalKeys<T, MakeDefaultRequired> = Exclude<
- keyof T,
- RequiredKeys<T, MakeDefaultRequired>
- >
- type InferPropType<T> = 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<infer V> ? V : T
- export type ExtractPropTypes<
- O,
- MakeDefaultRequired extends boolean = true
- > = O extends object
- ? { [K in RequiredKeys<O, MakeDefaultRequired>]: InferPropType<O[K]> } &
- { [K in OptionalKeys<O, MakeDefaultRequired>]?: InferPropType<O[K]> }
- : { [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, NormalizedProp>, string[]]
- // resolve raw VNode data.
- // - filter out reserved keys (key, ref)
- // - extract class and style into $attrs (to be merged onto child
- // component root)
- // - for the rest:
- // - if has declared props: put declared ones in `props`, the rest in `attrs`
- // - else: everything goes in `props`.
- export function resolveProps(
- instance: ComponentInternalInstance,
- rawProps: Data | null,
- _options: ComponentPropsOptions | void
- ) {
- const hasDeclaredProps = _options != null
- if (!rawProps && !hasDeclaredProps) {
- return
- }
- const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
- const props: Data = {}
- let attrs: Data | undefined = undefined
- // update the instance propsProxy (passed to setup()) to trigger potential
- // changes
- const propsProxy = instance.propsProxy
- const setProp = propsProxy
- ? (key: string, val: unknown) => {
- props[key] = val
- propsProxy[key] = val
- }
- : (key: string, val: unknown) => {
- props[key] = val
- }
- // allow mutation of propsProxy (which is readonly by default)
- unlock()
- if (rawProps != null) {
- 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.
- if (hasDeclaredProps) {
- const camelKey = camelize(key)
- if (hasOwn(options, camelKey)) {
- setProp(camelKey, value)
- } else {
- // Any non-declared props are put into a separate `attrs` object
- // for spreading. Make sure to preserve original key casing
- ;(attrs || (attrs = {}))[key] = value
- }
- } else {
- setProp(key, value)
- }
- }
- }
- if (hasDeclaredProps) {
- // set default values & cast booleans
- for (let i = 0; i < needCastKeys.length; i++) {
- const key = needCastKeys[i]
- let opt = options[key]
- if (opt == null) continue
- const hasDefault = hasOwn(opt, 'default')
- const currentValue = props[key]
- // default values
- if (hasDefault && currentValue === undefined) {
- const defaultValue = opt.default
- setProp(key, isFunction(defaultValue) ? defaultValue() : defaultValue)
- }
- // boolean casting
- if (opt[BooleanFlags.shouldCast]) {
- if (!hasOwn(props, key) && !hasDefault) {
- setProp(key, false)
- } else if (
- opt[BooleanFlags.shouldCastTrue] &&
- (currentValue === '' || currentValue === hyphenate(key))
- ) {
- setProp(key, true)
- }
- }
- }
- // validation
- if (__DEV__ && rawProps) {
- for (const key in options) {
- let opt = options[key]
- if (opt == null) continue
- validateProp(key, props[key], opt, !hasOwn(props, key))
- }
- }
- } else {
- // if component has no declared props, $attrs === $props
- attrs = props
- }
- // in case of dynamic props, check if we need to delete keys from
- // the props proxy
- const { patchFlag } = instance.vnode
- if (
- propsProxy !== null &&
- (patchFlag === 0 || patchFlag & PatchFlags.FULL_PROPS)
- ) {
- const rawInitialProps = toRaw(propsProxy)
- for (const key in rawInitialProps) {
- if (!hasOwn(props, key)) {
- delete propsProxy[key]
- }
- }
- }
- // lock readonly
- lock()
- instance.props = props
- instance.attrs = attrs || EMPTY_OBJ
- }
- const normalizationMap = new WeakMap<
- ComponentPropsOptions,
- NormalizedPropsOptions
- >()
- function validatePropName(key: string) {
- if (key[0] !== '$') {
- return true
- } else if (__DEV__) {
- warn(`Invalid prop name: "${key}" is a reserved property.`)
- }
- return false
- }
- function normalizePropsOptions(
- raw: ComponentPropsOptions | void
- ): NormalizedPropsOptions {
- if (!raw) {
- return [] as any
- }
- if (normalizationMap.has(raw)) {
- return normalizationMap.get(raw)!
- }
- const options: 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)) {
- options[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 = (options[normalizedKey] =
- isArray(opt) || isFunction(opt) ? { type: opt } : opt)
- if (prop != null) {
- 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 normalized: NormalizedPropsOptions = [options, needCastKeys]
- normalizationMap.set(raw, normalized)
- return normalized
- }
- // use function string name to check type constructors
- // so that it works across vms / iframes.
- function getType(ctor: Prop<any>): string {
- const match = ctor && ctor.toString().match(/^\s*function (\w+)/)
- return match ? match[1] : ''
- }
- function isSameType(a: Prop<any>, b: Prop<any>): boolean {
- return getType(a) === getType(b)
- }
- function getTypeIndex(
- type: Prop<any>,
- expectedTypes: PropType<any> | 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
- }
- type AssertionResult = {
- valid: boolean
- expectedType: string
- }
- 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'
- )
- 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')
- }
|