diff --git a/packages/lib/src/utils/validation.ts b/packages/lib/src/utils/validation.ts index eef1c68..81a1b64 100644 --- a/packages/lib/src/utils/validation.ts +++ b/packages/lib/src/utils/validation.ts @@ -43,6 +43,7 @@ export type PropValidator = { errorMsg: (value: unknown) => string; default: T | unknown; apply?: (container: InstanceType, props: Record, key: string) => void; + readOnly?: boolean; } // A more generic schema type that works with both functions @@ -219,37 +220,36 @@ export function validatePropsWithDefaults( * @param props The props to apply */ export function applyProps, InstanceType>( - instance: InstanceType, - schema: Schema, + instance: InstanceType, + schema: Schema, props: T -) { - Object.entries(props as Record).forEach(([key, value]) => { - if (key in schema) { - const propDef = schema[key as keyof T] as PropValidator; - if (propDef) { - if (propDef.apply) { - // Use type assertion to satisfy the type checker - propDef.apply(instance, props, key as string); - } else { - try { - (instance as Record)[key] = value; - } catch (error) { - console.error(`Error applying prop ${key}: ${error}`); - } - } - } - } - }); -} + ) { + Object.entries(props).forEach(([key, value]) => { + if (!(key in schema)) return; + const propDef = schema[key] as PropValidator | undefined; + if (!propDef || propDef.readOnly) return; + if (propDef.apply) { + propDef.apply(instance, props, key as string); + } else { + try { + (instance as Record)[key] = value; + } catch (error) { + console.error(`Error applying prop ${key}:`, error); + } + } + }); + } + /** * Property information including whether it's defined with a setter. */ export type PropertyInfo = { value: unknown; isDefinedWithSetter: boolean; -}; + readOnly?: boolean; // Mark properties that should not be assigned + }; /** * Get the pseudo public props of an instance with setter information. This is useful for creating a component definition from an instance. @@ -283,7 +283,7 @@ export function getPseudoPublicProps(container: Record): Record const hasGetter = typeof descriptor.get === 'function'; const hasSetter = typeof descriptor.set === 'function'; - if (hasSetter && !hasGetter) return; + if (hasSetter && !hasGetter) return; // Only setter-only props are skipped // If it's a getter/setter property, try to get the value if (descriptor.get) { @@ -369,8 +369,18 @@ export function createComponentDefinition( // Basic type detection entries.forEach(([key, propertyInfo]) => { - if(exclude.includes(String(key))) return; - const { value, isDefinedWithSetter } = propertyInfo; + if (exclude.includes(String(key))) return; + + // Mark getter-only properties as read-only + const descriptor = Object.getOwnPropertyDescriptor( + (instance as any).constructor.prototype, + key as string | symbol + ); + if (descriptor && descriptor.get && !descriptor.set) { + propertyInfo.readOnly = true; + } + + const { value, isDefinedWithSetter } = propertyInfo; // Colors if (value instanceof Color) { @@ -380,6 +390,9 @@ export function createComponentDefinition( errorMsg: (val: unknown) => `Invalid value for prop "${String(key)}": "${val}". ` + `Expected a hex like "#FF0000", CSS color name like "red", or an array "[1, 0, 0]").`, apply: (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } if(typeof props[key] === 'string') { const colorString = getColorFromName(props[key] as string) || props[key] as string; (instance[key as keyof InstanceType] as Color) = new Color().fromString(colorString); @@ -397,8 +410,14 @@ export function createComponentDefinition( errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` + `Expected an array of 2 numbers.`, apply: isDefinedWithSetter ? (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as Vec2) = new Vec2().fromArray(props[key] as number[]); } : (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as Vec2).set(...props[key] as [number, number]); } }; @@ -411,8 +430,14 @@ export function createComponentDefinition( errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` + `Expected an array of 3 numbers.`, apply: isDefinedWithSetter ? (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as Vec3) = new Vec3().fromArray(props[key] as number[]); } : (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as Vec3).set(...props[key] as [number, number, number]); } }; @@ -424,8 +449,14 @@ export function createComponentDefinition( default: [value.x, value.y, value.z, value.w], errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". Expected an array of 4 numbers.`, apply: isDefinedWithSetter ? (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as Vec4) = new Vec4().fromArray(props[key] as number[]); } : (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as Vec4).set(...props[key] as [number, number, number, number]); } }; @@ -439,8 +470,14 @@ export function createComponentDefinition( errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` + `Expected an array of 4 numbers.`, apply: isDefinedWithSetter ? (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as Quat) = new Quat().fromArray(props[key] as number[]); } : (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as Quat).set(...props[key] as [number, number, number, number]); } }; @@ -452,6 +489,7 @@ export function createComponentDefinition( default: Array.from((value.data)), errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` + `Expected an array of 16 numbers.`, + ...(propertyInfo.readOnly && { readOnly: true }), }; } // Numbers @@ -459,7 +497,8 @@ export function createComponentDefinition( schema[key] = { validate: (val) => typeof val === 'number', default: value, - errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a number.` + errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a number.`, + ...(propertyInfo.readOnly && { readOnly: true }), }; } // Strings @@ -467,7 +506,8 @@ export function createComponentDefinition( schema[key] = { validate: (val) => typeof val === 'string', default: value as string, - errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a string.` + errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a string.`, + ...(propertyInfo.readOnly && { readOnly: true }), }; } // Booleans @@ -475,7 +515,8 @@ export function createComponentDefinition( schema[key] = { validate: (val) => typeof val === 'boolean', default: value as boolean, - errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a boolean.` + errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a boolean.`, + ...(propertyInfo.readOnly && { readOnly: true }), }; } @@ -486,6 +527,9 @@ export function createComponentDefinition( default: value, errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". Expected an array.`, apply: (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } // For arrays, use a different approach to avoid spread operator issues const values = props[key] as unknown[]; @@ -505,6 +549,7 @@ export function createComponentDefinition( validate: (val) => val instanceof Material, default: value, errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". Expected a Material.`, + ...(propertyInfo.readOnly && { readOnly: true }), }; } @@ -515,6 +560,9 @@ export function createComponentDefinition( default: value, errorMsg: () => '', apply: (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as unknown) = props[key]; } };