Skip to content
43 changes: 20 additions & 23 deletions src/core/friendly_errors/param_validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function validateParams(p5, fn, lifecycles) {
* @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add`
* @returns {z.ZodSchema} Zod schema
*/
fn.generateZodSchemasForFunc = function (func) {
const generateZodSchemasForFunc = function (func) {
const { funcName, funcClass } = extractFuncNameAndClass(func);
let funcInfo = dataDoc[funcClass][funcName];

Expand Down Expand Up @@ -308,7 +308,7 @@ function validateParams(p5, fn, lifecycles) {
* @param {Array} args - User input arguments.
* @returns {z.ZodSchema} Closest schema matching the input arguments.
*/
fn.findClosestSchema = function (schema, args) {
const findClosestSchema = function (schema, args) {
if (!(schema instanceof z.ZodUnion)) {
return schema;
}
Expand Down Expand Up @@ -389,7 +389,7 @@ function validateParams(p5, fn, lifecycles) {
* @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add`
* @returns {String} The friendly error message.
*/
fn.friendlyParamError = function (zodErrorObj, func, args) {
const friendlyParamError = function (zodErrorObj, func, args) {
let message = '🌸 p5.js says: ';
let isVersionError = false;
// The `zodErrorObj` might contain multiple errors of equal importance
Expand Down Expand Up @@ -520,7 +520,7 @@ function validateParams(p5, fn, lifecycles) {
* @returns {any} [result.data] - The parsed data if validation was successful.
* @returns {String} [result.error] - The validation error message if validation has failed.
*/
fn.validate = function (func, args) {
const validate = function (func, args) {
if (p5.disableFriendlyErrors) {
return; // skip FES
}
Expand Down Expand Up @@ -548,7 +548,7 @@ function validateParams(p5, fn, lifecycles) {

let funcSchemas = schemaRegistry.get(func);
if (!funcSchemas) {
funcSchemas = fn.generateZodSchemasForFunc(func);
funcSchemas = generateZodSchemasForFunc(func);
if (!funcSchemas) return;
schemaRegistry.set(func, funcSchemas);
}
Expand All @@ -559,9 +559,9 @@ function validateParams(p5, fn, lifecycles) {
data: funcSchemas.parse(args)
};
} catch (error) {
const closestSchema = fn.findClosestSchema(funcSchemas, args);
const closestSchema = findClosestSchema(funcSchemas, args);
const zodError = closestSchema.safeParse(args).error;
const errorMessage = fn.friendlyParamError(zodError, func, args);
const errorMessage = friendlyParamError(zodError, func, args);

return {
success: false,
Expand All @@ -570,25 +570,22 @@ function validateParams(p5, fn, lifecycles) {
}
};

lifecycles.presetup = function(){
loadP5Constructors();
fn._validate = validate; // TEMP: For unit tests

if(
p5.disableParameterValidator !== true &&
p5.disableFriendlyErrors !== true
){
const excludes = ['validate'];
for(const f in this){
if(!excludes.includes(f) && !f.startsWith('_') && typeof this[f] === 'function'){
const copy = this[f];

this[f] = function(...args) {
this.validate(f, args);
return copy.call(this, ...args);
};
p5.decorateHelper(
/^(?!_).+$/,
function(target, { name }){
return function(...args){
if (!p5.disableFriendlyErrors && !p5.disableParameterValidator) {
validate(name, args);
}
}
return target.call(this, ...args);
};
}
);

lifecycles.presetup = function(){
loadP5Constructors();
};
}

Expand Down
246 changes: 128 additions & 118 deletions src/core/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ class p5 {
static _friendlyFileLoadError = () => {};

constructor(sketch, node) {
// Apply addon defined decorations
if(p5.decorations.size > 0){
for (const [patternArray, decoration] of p5.decorations) {
for(const member in p5.prototype) {
// Member must be a function
if (typeof p5.prototype[member] !== 'function') continue;

if (!patternArray.some(pattern => {
if (typeof pattern === 'string') {
return pattern === member;
} else if (pattern instanceof RegExp) {
return pattern.test(member);
}
})) continue;

p5.prototype[member] = decoration(p5.prototype[member], {
kind: 'method',
name: member,
access: {},
static: false,
private: false,
addInitializer(initializer){}
});
}
}

p5.decorations.clear();
}

//////////////////////////////////////////////
// PRIVATE p5 PROPERTIES AND METHODS
//////////////////////////////////////////////
Expand Down Expand Up @@ -77,122 +106,7 @@ class p5 {
// ensure correct reporting of window dimensions
this._updateWindowSize();

const bindGlobal = property => {
if (property === 'constructor') return;

// Common setter for all property types
const createSetter = () => newValue => {
Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
value: newValue,
writable: true
});
if (!p5.disableFriendlyErrors) {
console.log(`You just changed the value of "${property}", which was a p5 global value. This could cause problems later if you're not careful.`);
}
};

// Check if this property has a getter on the instance or prototype
const instanceDescriptor = Object.getOwnPropertyDescriptor(this, property);
const prototypeDescriptor = Object.getOwnPropertyDescriptor(p5.prototype, property);
const hasGetter = (instanceDescriptor && instanceDescriptor.get) ||
(prototypeDescriptor && prototypeDescriptor.get);

// Only check if it's a function if it doesn't have a getter
// to avoid actually evaluating getters before things like the
// renderer are fully constructed
let isPrototypeFunction = false;
let isConstant = false;
let constantValue;

if (!hasGetter) {
const prototypeValue = p5.prototype[property];
isPrototypeFunction = typeof prototypeValue === 'function';

// Check if this is a true constant from the constants module
if (!isPrototypeFunction && constants[property] !== undefined) {
isConstant = true;
constantValue = prototypeValue;
}
}

if (isPrototypeFunction) {
// For regular functions, cache the bound function
const boundFunction = p5.prototype[property].bind(this);
if (p5.disableFriendlyErrors) {
Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
value: boundFunction,
});
} else {
Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
get() {
return boundFunction;
},
set: createSetter()
});
}
} else if (isConstant) {
// For constants, cache the value directly
if (p5.disableFriendlyErrors) {
Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
value: constantValue,
});
} else {
Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
get() {
return constantValue;
},
set: createSetter()
});
}
} else if (hasGetter || !isPrototypeFunction) {
// For properties with getters or non-function properties, use lazy optimization
// On first access, determine the type and optimize subsequent accesses
let lastFunction = null;
let boundFunction = null;
let isFunction = null; // null = unknown, true = function, false = not function

Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
get: () => {
const currentValue = this[property];

if (isFunction === null) {
// First access - determine type and optimize
isFunction = typeof currentValue === 'function';
if (isFunction) {
lastFunction = currentValue;
boundFunction = currentValue.bind(this);
return boundFunction;
} else {
return currentValue;
}
} else if (isFunction) {
// Optimized function path - only rebind if function changed
if (currentValue !== lastFunction) {
lastFunction = currentValue;
boundFunction = currentValue.bind(this);
}
return boundFunction;
} else {
// Optimized non-function path
return currentValue;
}
},
set: createSetter()
});
}
};
const bindGlobal = createBindGlobal(this);
// If the user has created a global setup or draw function,
// assume "global" mode and make everything global (i.e. on the window)
if (!sketch) {
Expand Down Expand Up @@ -259,6 +173,7 @@ class p5 {

static registerAddon(addon) {
const lifecycles = {};

addon(p5, p5.prototype, lifecycles);

const validLifecycles = Object.keys(p5.lifecycleHooks);
Expand All @@ -269,6 +184,13 @@ class p5 {
}
}

static decorations = new Map();
static decorateHelper(pattern, decoration){
let patternArray = pattern;
if (!Array.isArray(pattern)) patternArray = [pattern];
p5.decorations.set(patternArray, decoration);
}

#customActions = {};
_customActions = new Proxy({}, {
get: (target, prop) => {
Expand Down Expand Up @@ -511,6 +433,96 @@ class p5 {
}
}

// Global helper function for binding properties to window in global mode
function createBindGlobal(instance) {
return function bindGlobal(property) {
if (property === 'constructor') return;

// Check if this property has a getter on the instance or prototype
const instanceDescriptor = Object.getOwnPropertyDescriptor(
instance,
property
);
const prototypeDescriptor = Object.getOwnPropertyDescriptor(
p5.prototype,
property
);
const hasGetter = (instanceDescriptor && instanceDescriptor.get) ||
(prototypeDescriptor && prototypeDescriptor.get);

// Only check if it's a function if it doesn't have a getter
// to avoid actually evaluating getters before things like the
// renderer are fully constructed
let isPrototypeFunction = false;
let isConstant = false;
let constantValue;

if (!hasGetter) {
const prototypeValue = p5.prototype[property];
isPrototypeFunction = typeof prototypeValue === 'function';

// Check if this is a true constant from the constants module
if (!isPrototypeFunction && constants[property] !== undefined) {
isConstant = true;
constantValue = prototypeValue;
}
}

if (isPrototypeFunction) {
// For regular functions, cache the bound function
const boundFunction = p5.prototype[property].bind(instance);
Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
value: boundFunction
});
} else if (isConstant) {
// For constants, cache the value directly
Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
value: constantValue
});
} else if (hasGetter || !isPrototypeFunction) {
// For properties with getters or non-function properties, use lazy optimization
// On first access, determine the type and optimize subsequent accesses
let lastFunction = null;
let boundFunction = null;
let isFunction = null; // null = unknown, true = function, false = not function

Object.defineProperty(window, property, {
configurable: true,
enumerable: true,
get: () => {
const currentValue = instance[property];

if (isFunction === null) {
// First access - determine type and optimize
isFunction = typeof currentValue === 'function';
if (isFunction) {
lastFunction = currentValue;
boundFunction = currentValue.bind(instance);
return boundFunction;
} else {
return currentValue;
}
} else if (isFunction) {
// Optimized function path - only rebind if function changed
if (currentValue !== lastFunction) {
lastFunction = currentValue;
boundFunction = currentValue.bind(instance);
}
return boundFunction;
} else {
// Optimized non-function path
return currentValue;
}
}
});
}
};
}

// Attach constants to p5 prototype
for (const k in constants) {
p5.prototype[k] = constants[k];
Expand Down Expand Up @@ -745,8 +757,6 @@ for (const k in constants) {
* </code>
* </div>
*/
p5.disableFriendlyErrors = false;

import transform from './transform';
import structure from './structure';
import environment from './environment';
Expand Down
Loading
Loading