From d54ff0a0a8221cb1ac745ccec99cbc58ea60a4a7 Mon Sep 17 00:00:00 2001 From: Nolan Date: Fri, 27 Feb 2026 11:47:08 -0800 Subject: [PATCH 1/3] Read Only Implementation For Getter-Only Properties (Schema Generator) --- package.json | 1 + packages/lib/src/utils/validation.ts | 91 ++++++++++++++++++++-------- pnpm-lock.yaml | 4 ++ 3 files changed, 71 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 98f86c93..5d5c0452 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/react-dom": "19.2.2", "@vitejs/plugin-react": "5.1.0", "eslint": "9.39.1", + "playcanvas": "^2.11.8", "eslint-plugin-import": "2.32.0", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411", diff --git a/packages/lib/src/utils/validation.ts b/packages/lib/src/utils/validation.ts index eef1c68e..51cd8db4 100644 --- a/packages/lib/src/utils/validation.ts +++ b/packages/lib/src/utils/validation.ts @@ -219,37 +219,35 @@ 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; + + 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 +281,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 +367,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 +388,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 +408,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 +428,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 +447,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 +468,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]); } }; @@ -486,6 +521,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[]; @@ -515,6 +553,9 @@ export function createComponentDefinition( default: value, errorMsg: () => '', apply: (instance, props, key) => { + if (propertyInfo.readOnly) { + return; + } (instance[key as keyof InstanceType] as unknown) = props[key]; } }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 792f4dab..64b1473f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: pkg-pr-new: specifier: 0.0.60 version: 0.0.60 + playcanvas: + specifier: ^2.11.8 + version: 2.11.8 type-fest: specifier: 5.2.0 version: 5.2.0 @@ -2874,6 +2877,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: From 087ddbc8a141b8d8ca548afa78c1cb5a0f8775c9 Mon Sep 17 00:00:00 2001 From: Nolan Date: Fri, 27 Feb 2026 12:43:20 -0800 Subject: [PATCH 2/3] Corrected Logic For Blocks Like Mat4 That Do Not Have An Apply Block --- package.json | 1 - packages/lib/src/utils/validation.ts | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 5d5c0452..98f86c93 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@types/react-dom": "19.2.2", "@vitejs/plugin-react": "5.1.0", "eslint": "9.39.1", - "playcanvas": "^2.11.8", "eslint-plugin-import": "2.32.0", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411", diff --git a/packages/lib/src/utils/validation.ts b/packages/lib/src/utils/validation.ts index 51cd8db4..81a1b643 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 @@ -225,8 +226,9 @@ export function applyProps, InstanceType>( ) { Object.entries(props).forEach(([key, value]) => { if (!(key in schema)) return; - const propDef = schema[key] as PropValidator; - + const propDef = schema[key] as PropValidator | undefined; + if (!propDef || propDef.readOnly) return; + if (propDef.apply) { propDef.apply(instance, props, key as string); } else { @@ -390,7 +392,7 @@ export function createComponentDefinition( 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); @@ -487,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 @@ -494,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 @@ -502,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 @@ -510,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 }), }; } @@ -543,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 }), }; } From d80fca70453b3493658ad022d157382a120427ef Mon Sep 17 00:00:00 2001 From: Nolan Date: Fri, 27 Feb 2026 12:57:00 -0800 Subject: [PATCH 3/3] reverting pnpm-lock.yaml to prefork state --- pnpm-lock.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64b1473f..792f4dab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,6 @@ importers: pkg-pr-new: specifier: 0.0.60 version: 0.0.60 - playcanvas: - specifier: ^2.11.8 - version: 2.11.8 type-fest: specifier: 5.2.0 version: 5.2.0 @@ -2877,7 +2874,6 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} - deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: