wip: vapor component props validation

This commit is contained in:
Evan You 2024-12-05 16:14:24 +08:00
parent 8725954244
commit 93a16af08e
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
4 changed files with 80 additions and 38 deletions

View File

@ -217,7 +217,7 @@ export function initProps(
// validation
if (__DEV__) {
validateProps(rawProps || {}, props, instance)
validateProps(rawProps || {}, props, instance.propsOptions[0]!)
}
if (isStateful) {
@ -371,7 +371,7 @@ export function updateProps(
}
if (__DEV__) {
validateProps(rawProps || {}, props, instance)
validateProps(rawProps || {}, props, instance.propsOptions[0]!)
}
}
@ -691,23 +691,23 @@ function getType(ctor: Prop<any> | null): string {
/**
* dev only
* @internal
*/
function validateProps(
export function validateProps(
rawProps: Data,
props: Data,
instance: ComponentInternalInstance,
) {
const resolvedValues = toRaw(props)
const options = instance.propsOptions[0]
resolvedProps: Data,
options: NormalizedProps,
): void {
resolvedProps = toRaw(resolvedProps)
const camelizePropsKey = Object.keys(rawProps).map(key => camelize(key))
for (const key in options) {
let opt = options[key]
if (opt == null) continue
validateProp(
key,
resolvedValues[key],
resolvedProps[key],
opt,
__DEV__ ? shallowReadonly(resolvedValues) : resolvedValues,
__DEV__ ? shallowReadonly(resolvedProps) : resolvedProps,
!camelizePropsKey.includes(key),
)
}
@ -717,16 +717,16 @@ function validateProps(
* dev only
*/
function validateProp(
name: string,
key: string,
value: unknown,
prop: PropOptions,
props: Data,
propOptions: PropOptions,
resolvedProps: Data,
isAbsent: boolean,
) {
const { type, required, validator, skipCheck } = prop
const { type, required, validator, skipCheck } = propOptions
// required!
if (required && isAbsent) {
warn('Missing required prop: "' + name + '"')
warn('Missing required prop: "' + key + '"')
return
}
// missing but optional
@ -745,13 +745,13 @@ function validateProp(
isValid = valid
}
if (!isValid) {
warn(getInvalidTypeMessage(name, value, expectedTypes))
warn(getInvalidTypeMessage(key, value, expectedTypes))
return
}
}
// custom validator
if (validator && !validator(value, props)) {
warn('Invalid prop: custom validator check failed for prop "' + name + '".')
if (validator && !validator(value, resolvedProps)) {
warn('Invalid prop: custom validator check failed for prop "' + key + '".')
}
}

View File

@ -491,6 +491,7 @@ export {
type NormalizedPropsOptions,
baseNormalizePropsOptions,
resolvePropValue,
validateProps,
} from './componentProps'
export { baseEmit, isEmitListener } from './componentEmits'
export { type SchedulerJob, queueJob } from './scheduler'

View File

@ -16,11 +16,13 @@ import {
} from '@vue/runtime-dom'
import { type Block, isBlock } from './block'
import { pauseTracking, resetTracking } from '@vue/reactivity'
import { EMPTY_OBJ, hasOwn, isFunction } from '@vue/shared'
import { EMPTY_OBJ, isFunction } from '@vue/shared'
import {
type RawProps,
getPropsProxyHandlers,
hasFallthroughAttrs,
normalizePropsOptions,
setupPropsValidation,
} from './componentProps'
import { setDynamicProp } from './dom/prop'
import { renderEffect } from './renderEffect'
@ -208,31 +210,16 @@ export class VaporComponentInstance implements GenericComponentInstance {
const handlers = getPropsProxyHandlers(comp, this)
this.props = comp.props ? new Proxy(target, handlers[0]!) : {}
this.attrs = new Proxy(target, handlers[1])
this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
if (__DEV__) {
// validate props
if (rawProps) setupPropsValidation(this)
// cache normalized options for dev only emit check
this.propsOptions = normalizePropsOptions(comp)
this.emitsOptions = normalizeEmitsOptions(comp)
}
// determine fallthrough
this.hasFallthrough = false
if (rawProps) {
if (rawProps.$ || !comp.props) {
this.hasFallthrough = true
} else {
// check if rawProps contains any keys not declared
const propsOptions = normalizePropsOptions(comp)[0]
for (const key in rawProps) {
if (!hasOwn(propsOptions!, key)) {
this.hasFallthrough = true
break
}
}
}
}
// TODO validate props
// TODO init slots
}
}

View File

@ -1,12 +1,16 @@
import { EMPTY_ARR, NO, YES, hasOwn, isFunction } from '@vue/shared'
import { EMPTY_ARR, NO, YES, extend, hasOwn, isFunction } from '@vue/shared'
import type { VaporComponent, VaporComponentInstance } from './component'
import {
type NormalizedPropsOptions,
baseNormalizePropsOptions,
isEmitListener,
popWarningContext,
pushWarningContext,
resolvePropValue,
validateProps,
} from '@vue/runtime-dom'
import { normalizeEmitsOptions } from './componentEmits'
import { renderEffect } from './renderEffect'
export type RawProps = Record<string, () => unknown> & {
$?: DynamicPropsSource[]
@ -174,3 +178,53 @@ function resolveDefault(
) {
return factory.call(null, instance.props)
}
export function hasFallthroughAttrs(
comp: VaporComponent,
rawProps: RawProps | undefined,
): boolean {
if (rawProps) {
// determine fallthrough
if (rawProps.$ || !comp.props) {
return true
} else {
// check if rawProps contains any keys not declared
const propsOptions = normalizePropsOptions(comp)[0]
for (const key in rawProps) {
if (!hasOwn(propsOptions!, key)) {
return true
}
}
}
}
return false
}
/**
* dev only
*/
export function setupPropsValidation(instance: VaporComponentInstance): void {
const rawProps = instance.rawProps
if (!rawProps) return
renderEffect(() => {
const mergedRawProps = extend({}, rawProps)
if (rawProps.$) {
for (const source of rawProps.$) {
const isDynamic = isFunction(source)
const resolved = isDynamic ? source() : source
for (const key in resolved) {
mergedRawProps[key] = isDynamic
? resolved[key]
: (resolved[key] as Function)()
}
}
}
pushWarningContext(instance)
validateProps(
mergedRawProps,
instance.props,
normalizePropsOptions(instance.type)[0]!,
)
popWarningContext()
})
}