UNPKG

@playcanvas/react

Version:

A React renderer for PlayCanvas – build interactive 3D applications using React's declarative paradigm.

233 lines 10.3 kB
import { Color, Quat, Vec2, Vec3, Vec4, Mat4, Application, NullGraphicsDevice, Material } from "playcanvas"; // Limit the size of the warned set to prevent memory leaks const MAX_WARNED_SIZE = 1000; const warned = new Set(); export const warnOnce = (message) => { if (!warned.has(message)) { if (process.env.NODE_ENV === 'development') { // Use setTimeout to break the call stack setTimeout(() => { // Apply styling to make the warning stand out console.warn('%c[PlayCanvas React]:', 'color: #ff9800; font-weight: bold; font-size: 12px;', message); }, 0); } else { // Error in production for critical issues console.error(`[PlayCanvas React]:\n\n` + `${message}`); } warned.add(message); // Prevent the warned set from growing too large if (warned.size > MAX_WARNED_SIZE) { // Remove oldest entries when the set gets too large const entriesToRemove = Array.from(warned).slice(0, warned.size - MAX_WARNED_SIZE); entriesToRemove.forEach(entry => warned.delete(entry)); } } }; export function validateAndSanitize(value, propDef, propName, componentName, apiName) { const isValid = value !== undefined && propDef.validate(value); if (!isValid && value !== undefined && process.env.NODE_ENV !== 'production') { warnOnce(`Invalid prop "${propName}" in \`<${componentName} ${propName}={${JSON.stringify(value)}} />\`\n` + ` ${propDef.errorMsg(value)}\n` + (propDef.default !== undefined ? ` Using default: ${JSON.stringify(propDef.default)}` : '') + `\n\nPlease see the documentation https://api.playcanvas.com/engine/classes/${apiName ?? componentName}.html.`); } return isValid ? value : propDef.default; } export function validateAndSanitizeProps(rawProps, componentDef, warnUnknownProps = true) { // Start with a copy of the raw props const { schema, name, apiName } = componentDef; const result = { ...rawProps }; // Track unknown props for warning const unknownProps = []; // Process each prop once Object.keys(rawProps).forEach((key) => { // Skip 'children' as it's a special React prop if (key === 'children') return; // Check if this prop is in the schema if (key in schema) { // Validate and sanitize props that are in the schema const propDef = schema[key]; if (propDef) { result[key] = validateAndSanitize(rawProps[key], propDef, key, name, apiName); } } else { // Collect unknown props for warning unknownProps.push(key); } }); // Warn about unknown props in development mode if (process.env.NODE_ENV !== 'production' && warnUnknownProps && unknownProps.length > 0) { warnOnce(`Unknown props in "<${name}/>."\n` + `The following props are invalid and will be ignored: "${unknownProps.join('", "')}"\n\n` + `Please see the documentation https://api.playcanvas.com/engine/classes/${apiName ?? name}.html.`); } return result; } export function getPseudoPublicProps(container) { // Get regular enumerable properties const entries = Object.entries(container) .filter(([key]) => !key.startsWith('_') && typeof container[key] !== 'function'); // Get getters and setters from the prototype const prototype = Object.getPrototypeOf(container); if (prototype && prototype !== Object.prototype) { const descriptors = Object.getOwnPropertyDescriptors(prototype); const getterSetterEntries = Object.entries(descriptors) .filter(([key, descriptor]) => { // Include properties that have setters (and optionally getters) and don't start with _ return (descriptor.get && descriptor.set) && !key.startsWith('_') && key !== 'constructor'; }) .map(([key, descriptor]) => { // For properties with both getter and setter, try to get the value if possible if (descriptor.get) { try { const value = descriptor.get.call(container); // Create a shallow copy of the value to avoid reference issues const safeValue = value !== null && typeof value === 'object' ? value.clone ? value.clone() : { ...value } : value; return [key, safeValue]; } catch { // If we can't get the value, just use the key return [key, undefined]; } } // For setters only, we can't get the value return [key, undefined]; }); // Combine regular properties with getter/setter properties return Object.fromEntries([...entries, ...getterSetterEntries]); } // If no prototype or it's just Object.prototype, just return the regular properties return Object.fromEntries(entries); } export function createComponentDefinition(name, createInstance, cleanup, apiName) { const instance = createInstance(); const schema = {}; const props = getPseudoPublicProps(instance); const entries = Object.entries(props); // Basic type detection entries.forEach(([key, value]) => { // Colors if (value instanceof Color) { schema[key] = { validate: (val) => val instanceof Color || typeof val === 'string', default: value.toString(true), errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". ` + `Expected a hex like "#FF0000" or CSS color name like "red").` }; } // Vec2 else if (value instanceof Vec2) { schema[key] = { validate: (val) => Array.isArray(val) && val.length === 2, default: [value.x, value.y], errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` + `Expected an array of 2 numbers.` }; } // Vec3 else if (value instanceof Vec3) { schema[key] = { validate: (val) => Array.isArray(val) && val.length === 3, default: [value.x, value.y, value.z], errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` + `Expected an array of 3 numbers.` }; } // Vec4 else if (value instanceof Vec4) { schema[key] = { validate: (val) => Array.isArray(val) && val.length === 4, 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.` }; } // Quaternions else if (value instanceof Quat) { schema[key] = { validate: (val) => Array.isArray(val) && val.length === 4, 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.` }; } // Mat4 else if (value instanceof Mat4) { schema[key] = { validate: (val) => val instanceof Mat4 || (Array.isArray(val) && val.length === 16), default: value.toString(), errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` + `Expected an array of 16 numbers.` }; } // Numbers else if (typeof value === 'number') { schema[key] = { validate: (val) => typeof val === 'number', default: value, errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a number.` }; } // Strings else if (typeof value === 'string') { schema[key] = { validate: (val) => typeof val === 'string', default: value, errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a string.` }; } // Booleans else if (typeof value === 'boolean') { schema[key] = { validate: (val) => typeof val === 'boolean', default: value, errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a boolean.` }; } // Arrays else if (Array.isArray(value)) { schema[key] = { validate: (val) => Array.isArray(val), default: value, errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". Expected an array.` }; } // Materials else if (value instanceof Material) { schema[key] = { validate: (val) => val instanceof Material, default: null, errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". Expected a Material.` }; } }); if (cleanup) cleanup(instance); const componentDef = { name, apiName, schema }; return componentDef; } /** * This is a mock application that is used to render the application without a canvas. * @private * @returns A mock application that is used to render the application without a canvas. */ export function getNullApplication() { const mockCanvas = { id: 'pc-react-mock-canvas' }; // @ts-expect-error - Mock canvas is not a real canvas return new Application(mockCanvas, { graphicsDevice: new NullGraphicsDevice(mockCanvas) }); } const localApp = getNullApplication(); export const getStaticNullApplication = () => localApp; //# sourceMappingURL=validation.js.map