feat: runtime prop validation
This commit is contained in:
		
							parent
							
								
									e93e85bb29
								
							
						
					
					
						commit
						2241ad7765
					
				@ -29,7 +29,7 @@ export type PropType<T> = Prop<T> | Prop<T>[]
 | 
				
			|||||||
export type PropValidator<T> = PropOptions<T> | PropType<T>
 | 
					export type PropValidator<T> = PropOptions<T> | PropType<T>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface PropOptions<T = any> {
 | 
					export interface PropOptions<T = any> {
 | 
				
			||||||
  type?: PropType<T>
 | 
					  type?: PropType<T> | true | null
 | 
				
			||||||
  required?: boolean
 | 
					  required?: boolean
 | 
				
			||||||
  default?: T | null | undefined | (() => T | null | undefined)
 | 
					  default?: T | null | undefined | (() => T | null | undefined)
 | 
				
			||||||
  validator?(value: T): boolean
 | 
					  validator?(value: T): boolean
 | 
				
			||||||
 | 
				
			|||||||
@ -8,12 +8,11 @@ import { immutable, unwrap, lock, unlock } from '@vue/observer'
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  Data,
 | 
					  Data,
 | 
				
			||||||
  ComponentPropsOptions,
 | 
					  ComponentPropsOptions,
 | 
				
			||||||
  PropValidator,
 | 
					 | 
				
			||||||
  PropOptions,
 | 
					  PropOptions,
 | 
				
			||||||
  Prop,
 | 
					  Prop,
 | 
				
			||||||
  PropType
 | 
					  PropType
 | 
				
			||||||
} from './componentOptions'
 | 
					} from './componentOptions'
 | 
				
			||||||
import { camelize, hyphenate } from './utils'
 | 
					import { camelize, hyphenate, capitalize } from './utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function initializeProps(instance: MountedComponent, data: Data | null) {
 | 
					export function initializeProps(instance: MountedComponent, data: Data | null) {
 | 
				
			||||||
  const { props, attrs } = resolveProps(
 | 
					  const { props, attrs } = resolveProps(
 | 
				
			||||||
@ -136,7 +135,7 @@ export function resolveProps(
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      // runtime validation
 | 
					      // runtime validation
 | 
				
			||||||
      if (__DEV__) {
 | 
					      if (__DEV__) {
 | 
				
			||||||
        validateProp(key, rawData[key], opt, Component)
 | 
					        validateProp(key, rawData[key], opt, Component, isAbsent)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -211,7 +210,7 @@ function isSameType(a: Prop<any>, b: Prop<any>): boolean {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function getTypeIndex(
 | 
					function getTypeIndex(
 | 
				
			||||||
  type: Prop<any>,
 | 
					  type: Prop<any>,
 | 
				
			||||||
  expectedTypes: PropType<any> | void
 | 
					  expectedTypes: PropType<any> | void | null | true
 | 
				
			||||||
): number {
 | 
					): number {
 | 
				
			||||||
  if (Array.isArray(expectedTypes)) {
 | 
					  if (Array.isArray(expectedTypes)) {
 | 
				
			||||||
    for (let i = 0, len = expectedTypes.length; i < len; i++) {
 | 
					    for (let i = 0, len = expectedTypes.length; i < len; i++) {
 | 
				
			||||||
@ -219,17 +218,130 @@ function getTypeIndex(
 | 
				
			|||||||
        return i
 | 
					        return i
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } else if (expectedTypes != null) {
 | 
					  } else if (expectedTypes != null && typeof expectedTypes === 'object') {
 | 
				
			||||||
    return isSameType(expectedTypes, type) ? 0 : -1
 | 
					    return isSameType(expectedTypes, type) ? 0 : -1
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return -1
 | 
					  return -1
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function validateProp(
 | 
					type AssertionResult = {
 | 
				
			||||||
  key: string,
 | 
					  valid: boolean
 | 
				
			||||||
  value: any,
 | 
					  expectedType: string
 | 
				
			||||||
  validator: PropValidator<any>,
 | 
					}
 | 
				
			||||||
  Component: ComponentClass | FunctionalComponent
 | 
					
 | 
				
			||||||
) {
 | 
					function validateProp(
 | 
				
			||||||
  // TODO
 | 
					  name: string,
 | 
				
			||||||
 | 
					  value: any,
 | 
				
			||||||
 | 
					  prop: PropOptions<any>,
 | 
				
			||||||
 | 
					  Component: ComponentClass | FunctionalComponent,
 | 
				
			||||||
 | 
					  isAbsent: boolean
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const { type, required, validator } = prop
 | 
				
			||||||
 | 
					  // required!
 | 
				
			||||||
 | 
					  if (required && isAbsent) {
 | 
				
			||||||
 | 
					    console.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 = Array.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) {
 | 
				
			||||||
 | 
					      console.warn(getInvalidTypeMessage(name, value, expectedTypes))
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // custom validator
 | 
				
			||||||
 | 
					  if (validator && !validator(value)) {
 | 
				
			||||||
 | 
					    console.warn(
 | 
				
			||||||
 | 
					      'Invalid prop: custom validator check failed for prop "' + name + '".'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function assertType(value: any, type: Prop<any>): AssertionResult {
 | 
				
			||||||
 | 
					  let valid
 | 
				
			||||||
 | 
					  const expectedType = getType(type)
 | 
				
			||||||
 | 
					  if (simpleCheckRE.test(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 = Array.isArray(value)
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    valid = value instanceof type
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    valid,
 | 
				
			||||||
 | 
					    expectedType
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getInvalidTypeMessage(
 | 
				
			||||||
 | 
					  name: string,
 | 
				
			||||||
 | 
					  value: any,
 | 
				
			||||||
 | 
					  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: any, type: string): string {
 | 
				
			||||||
 | 
					  if (type === 'String') {
 | 
				
			||||||
 | 
					    return `"${value}"`
 | 
				
			||||||
 | 
					  } else if (type === 'Number') {
 | 
				
			||||||
 | 
					    return `${Number(value)}`
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return `${value}`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toRawType(value: any): string {
 | 
				
			||||||
 | 
					  return Object.prototype.toString.call(value).slice(8, -1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -55,6 +55,10 @@ export const hyphenate = (str: string): string => {
 | 
				
			|||||||
  return str.replace(hyphenateRE, '-$1').toLowerCase()
 | 
					  return str.replace(hyphenateRE, '-$1').toLowerCase()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const capitalize = (str: string): string => {
 | 
				
			||||||
 | 
					  return str.charAt(0).toUpperCase() + str.slice(1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
 | 
					// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
 | 
				
			||||||
export function lis(arr: number[]): number[] {
 | 
					export function lis(arr: number[]): number[] {
 | 
				
			||||||
  const p = arr.slice()
 | 
					  const p = arr.slice()
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user