Skip to content

Commit f76b2b6

Browse files
authored
Merge pull request #8099 from processing/addon-fn-proxy
Re-bind globals when assigned in addons
2 parents c35445c + fd9df8c commit f76b2b6

File tree

3 files changed

+172
-164
lines changed

3 files changed

+172
-164
lines changed

src/core/friendly_errors/param_validator.js

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ function validateParams(p5, fn, lifecycles) {
140140
* @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add`
141141
* @returns {z.ZodSchema} Zod schema
142142
*/
143-
fn.generateZodSchemasForFunc = function (func) {
143+
const generateZodSchemasForFunc = function (func) {
144144
const { funcName, funcClass } = extractFuncNameAndClass(func);
145145
let funcInfo = dataDoc[funcClass][funcName];
146146

@@ -308,7 +308,7 @@ function validateParams(p5, fn, lifecycles) {
308308
* @param {Array} args - User input arguments.
309309
* @returns {z.ZodSchema} Closest schema matching the input arguments.
310310
*/
311-
fn.findClosestSchema = function (schema, args) {
311+
const findClosestSchema = function (schema, args) {
312312
if (!(schema instanceof z.ZodUnion)) {
313313
return schema;
314314
}
@@ -389,7 +389,7 @@ function validateParams(p5, fn, lifecycles) {
389389
* @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add`
390390
* @returns {String} The friendly error message.
391391
*/
392-
fn.friendlyParamError = function (zodErrorObj, func, args) {
392+
const friendlyParamError = function (zodErrorObj, func, args) {
393393
let message = '🌸 p5.js says: ';
394394
let isVersionError = false;
395395
// The `zodErrorObj` might contain multiple errors of equal importance
@@ -520,7 +520,7 @@ function validateParams(p5, fn, lifecycles) {
520520
* @returns {any} [result.data] - The parsed data if validation was successful.
521521
* @returns {String} [result.error] - The validation error message if validation has failed.
522522
*/
523-
fn.validate = function (func, args) {
523+
const validate = function (func, args) {
524524
if (p5.disableFriendlyErrors) {
525525
return; // skip FES
526526
}
@@ -548,7 +548,7 @@ function validateParams(p5, fn, lifecycles) {
548548

549549
let funcSchemas = schemaRegistry.get(func);
550550
if (!funcSchemas) {
551-
funcSchemas = fn.generateZodSchemasForFunc(func);
551+
funcSchemas = generateZodSchemasForFunc(func);
552552
if (!funcSchemas) return;
553553
schemaRegistry.set(func, funcSchemas);
554554
}
@@ -559,9 +559,9 @@ function validateParams(p5, fn, lifecycles) {
559559
data: funcSchemas.parse(args)
560560
};
561561
} catch (error) {
562-
const closestSchema = fn.findClosestSchema(funcSchemas, args);
562+
const closestSchema = findClosestSchema(funcSchemas, args);
563563
const zodError = closestSchema.safeParse(args).error;
564-
const errorMessage = fn.friendlyParamError(zodError, func, args);
564+
const errorMessage = friendlyParamError(zodError, func, args);
565565

566566
return {
567567
success: false,
@@ -570,25 +570,22 @@ function validateParams(p5, fn, lifecycles) {
570570
}
571571
};
572572

573-
lifecycles.presetup = function(){
574-
loadP5Constructors();
573+
fn._validate = validate; // TEMP: For unit tests
575574

576-
if(
577-
p5.disableParameterValidator !== true &&
578-
p5.disableFriendlyErrors !== true
579-
){
580-
const excludes = ['validate'];
581-
for(const f in this){
582-
if(!excludes.includes(f) && !f.startsWith('_') && typeof this[f] === 'function'){
583-
const copy = this[f];
584-
585-
this[f] = function(...args) {
586-
this.validate(f, args);
587-
return copy.call(this, ...args);
588-
};
575+
p5.decorateHelper(
576+
/^(?!_).+$/,
577+
function(target, { name }){
578+
return function(...args){
579+
if (!p5.disableFriendlyErrors && !p5.disableParameterValidator) {
580+
validate(name, args);
589581
}
590-
}
582+
return target.call(this, ...args);
583+
};
591584
}
585+
);
586+
587+
lifecycles.presetup = function(){
588+
loadP5Constructors();
592589
};
593590
}
594591

src/core/main.js

Lines changed: 128 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,35 @@ class p5 {
4848
static _friendlyFileLoadError = () => {};
4949

5050
constructor(sketch, node) {
51+
// Apply addon defined decorations
52+
if(p5.decorations.size > 0){
53+
for (const [patternArray, decoration] of p5.decorations) {
54+
for(const member in p5.prototype) {
55+
// Member must be a function
56+
if (typeof p5.prototype[member] !== 'function') continue;
57+
58+
if (!patternArray.some(pattern => {
59+
if (typeof pattern === 'string') {
60+
return pattern === member;
61+
} else if (pattern instanceof RegExp) {
62+
return pattern.test(member);
63+
}
64+
})) continue;
65+
66+
p5.prototype[member] = decoration(p5.prototype[member], {
67+
kind: 'method',
68+
name: member,
69+
access: {},
70+
static: false,
71+
private: false,
72+
addInitializer(initializer){}
73+
});
74+
}
75+
}
76+
77+
p5.decorations.clear();
78+
}
79+
5180
//////////////////////////////////////////////
5281
// PRIVATE p5 PROPERTIES AND METHODS
5382
//////////////////////////////////////////////
@@ -77,122 +106,7 @@ class p5 {
77106
// ensure correct reporting of window dimensions
78107
this._updateWindowSize();
79108

80-
const bindGlobal = property => {
81-
if (property === 'constructor') return;
82-
83-
// Common setter for all property types
84-
const createSetter = () => newValue => {
85-
Object.defineProperty(window, property, {
86-
configurable: true,
87-
enumerable: true,
88-
value: newValue,
89-
writable: true
90-
});
91-
if (!p5.disableFriendlyErrors) {
92-
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.`);
93-
}
94-
};
95-
96-
// Check if this property has a getter on the instance or prototype
97-
const instanceDescriptor = Object.getOwnPropertyDescriptor(this, property);
98-
const prototypeDescriptor = Object.getOwnPropertyDescriptor(p5.prototype, property);
99-
const hasGetter = (instanceDescriptor && instanceDescriptor.get) ||
100-
(prototypeDescriptor && prototypeDescriptor.get);
101-
102-
// Only check if it's a function if it doesn't have a getter
103-
// to avoid actually evaluating getters before things like the
104-
// renderer are fully constructed
105-
let isPrototypeFunction = false;
106-
let isConstant = false;
107-
let constantValue;
108-
109-
if (!hasGetter) {
110-
const prototypeValue = p5.prototype[property];
111-
isPrototypeFunction = typeof prototypeValue === 'function';
112-
113-
// Check if this is a true constant from the constants module
114-
if (!isPrototypeFunction && constants[property] !== undefined) {
115-
isConstant = true;
116-
constantValue = prototypeValue;
117-
}
118-
}
119-
120-
if (isPrototypeFunction) {
121-
// For regular functions, cache the bound function
122-
const boundFunction = p5.prototype[property].bind(this);
123-
if (p5.disableFriendlyErrors) {
124-
Object.defineProperty(window, property, {
125-
configurable: true,
126-
enumerable: true,
127-
value: boundFunction,
128-
});
129-
} else {
130-
Object.defineProperty(window, property, {
131-
configurable: true,
132-
enumerable: true,
133-
get() {
134-
return boundFunction;
135-
},
136-
set: createSetter()
137-
});
138-
}
139-
} else if (isConstant) {
140-
// For constants, cache the value directly
141-
if (p5.disableFriendlyErrors) {
142-
Object.defineProperty(window, property, {
143-
configurable: true,
144-
enumerable: true,
145-
value: constantValue,
146-
});
147-
} else {
148-
Object.defineProperty(window, property, {
149-
configurable: true,
150-
enumerable: true,
151-
get() {
152-
return constantValue;
153-
},
154-
set: createSetter()
155-
});
156-
}
157-
} else if (hasGetter || !isPrototypeFunction) {
158-
// For properties with getters or non-function properties, use lazy optimization
159-
// On first access, determine the type and optimize subsequent accesses
160-
let lastFunction = null;
161-
let boundFunction = null;
162-
let isFunction = null; // null = unknown, true = function, false = not function
163-
164-
Object.defineProperty(window, property, {
165-
configurable: true,
166-
enumerable: true,
167-
get: () => {
168-
const currentValue = this[property];
169-
170-
if (isFunction === null) {
171-
// First access - determine type and optimize
172-
isFunction = typeof currentValue === 'function';
173-
if (isFunction) {
174-
lastFunction = currentValue;
175-
boundFunction = currentValue.bind(this);
176-
return boundFunction;
177-
} else {
178-
return currentValue;
179-
}
180-
} else if (isFunction) {
181-
// Optimized function path - only rebind if function changed
182-
if (currentValue !== lastFunction) {
183-
lastFunction = currentValue;
184-
boundFunction = currentValue.bind(this);
185-
}
186-
return boundFunction;
187-
} else {
188-
// Optimized non-function path
189-
return currentValue;
190-
}
191-
},
192-
set: createSetter()
193-
});
194-
}
195-
};
109+
const bindGlobal = createBindGlobal(this);
196110
// If the user has created a global setup or draw function,
197111
// assume "global" mode and make everything global (i.e. on the window)
198112
if (!sketch) {
@@ -259,6 +173,7 @@ class p5 {
259173

260174
static registerAddon(addon) {
261175
const lifecycles = {};
176+
262177
addon(p5, p5.prototype, lifecycles);
263178

264179
const validLifecycles = Object.keys(p5.lifecycleHooks);
@@ -269,6 +184,13 @@ class p5 {
269184
}
270185
}
271186

187+
static decorations = new Map();
188+
static decorateHelper(pattern, decoration){
189+
let patternArray = pattern;
190+
if (!Array.isArray(pattern)) patternArray = [pattern];
191+
p5.decorations.set(patternArray, decoration);
192+
}
193+
272194
#customActions = {};
273195
_customActions = new Proxy({}, {
274196
get: (target, prop) => {
@@ -511,6 +433,96 @@ class p5 {
511433
}
512434
}
513435

436+
// Global helper function for binding properties to window in global mode
437+
function createBindGlobal(instance) {
438+
return function bindGlobal(property) {
439+
if (property === 'constructor') return;
440+
441+
// Check if this property has a getter on the instance or prototype
442+
const instanceDescriptor = Object.getOwnPropertyDescriptor(
443+
instance,
444+
property
445+
);
446+
const prototypeDescriptor = Object.getOwnPropertyDescriptor(
447+
p5.prototype,
448+
property
449+
);
450+
const hasGetter = (instanceDescriptor && instanceDescriptor.get) ||
451+
(prototypeDescriptor && prototypeDescriptor.get);
452+
453+
// Only check if it's a function if it doesn't have a getter
454+
// to avoid actually evaluating getters before things like the
455+
// renderer are fully constructed
456+
let isPrototypeFunction = false;
457+
let isConstant = false;
458+
let constantValue;
459+
460+
if (!hasGetter) {
461+
const prototypeValue = p5.prototype[property];
462+
isPrototypeFunction = typeof prototypeValue === 'function';
463+
464+
// Check if this is a true constant from the constants module
465+
if (!isPrototypeFunction && constants[property] !== undefined) {
466+
isConstant = true;
467+
constantValue = prototypeValue;
468+
}
469+
}
470+
471+
if (isPrototypeFunction) {
472+
// For regular functions, cache the bound function
473+
const boundFunction = p5.prototype[property].bind(instance);
474+
Object.defineProperty(window, property, {
475+
configurable: true,
476+
enumerable: true,
477+
value: boundFunction
478+
});
479+
} else if (isConstant) {
480+
// For constants, cache the value directly
481+
Object.defineProperty(window, property, {
482+
configurable: true,
483+
enumerable: true,
484+
value: constantValue
485+
});
486+
} else if (hasGetter || !isPrototypeFunction) {
487+
// For properties with getters or non-function properties, use lazy optimization
488+
// On first access, determine the type and optimize subsequent accesses
489+
let lastFunction = null;
490+
let boundFunction = null;
491+
let isFunction = null; // null = unknown, true = function, false = not function
492+
493+
Object.defineProperty(window, property, {
494+
configurable: true,
495+
enumerable: true,
496+
get: () => {
497+
const currentValue = instance[property];
498+
499+
if (isFunction === null) {
500+
// First access - determine type and optimize
501+
isFunction = typeof currentValue === 'function';
502+
if (isFunction) {
503+
lastFunction = currentValue;
504+
boundFunction = currentValue.bind(instance);
505+
return boundFunction;
506+
} else {
507+
return currentValue;
508+
}
509+
} else if (isFunction) {
510+
// Optimized function path - only rebind if function changed
511+
if (currentValue !== lastFunction) {
512+
lastFunction = currentValue;
513+
boundFunction = currentValue.bind(instance);
514+
}
515+
return boundFunction;
516+
} else {
517+
// Optimized non-function path
518+
return currentValue;
519+
}
520+
}
521+
});
522+
}
523+
};
524+
}
525+
514526
// Attach constants to p5 prototype
515527
for (const k in constants) {
516528
p5.prototype[k] = constants[k];
@@ -745,8 +757,6 @@ for (const k in constants) {
745757
* </code>
746758
* </div>
747759
*/
748-
p5.disableFriendlyErrors = false;
749-
750760
import transform from './transform';
751761
import structure from './structure';
752762
import environment from './environment';

0 commit comments

Comments
 (0)