Skip to content

Commit 904a947

Browse files
committed
fix: Handle case-insensitive plugin global mapping and add support for function reference assignments in plugin properties.
1 parent 0375395 commit 904a947

1 file changed

Lines changed: 176 additions & 5 deletions

File tree

src/index.js

Lines changed: 176 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {dotNotationToObject} from "@cocreate/utils";
33

44
/**
55
* @typedef {Object} PluginDefinition
6-
* @property {Array<string|Object>} [js] - List of JS files to load. Can be strings (URLs) or objects with src, integrity, etc.
6+
* @property {Array<string|Object>} [js] - List of JS files to load. Can be strings (URLs) or objects with src, integrity, etc
77
* @property {Array<string>} [css] - List of CSS files to load.
88
*/
99

@@ -107,6 +107,8 @@ async function processPlugin(el) {
107107

108108
// Load JS with Promise Cache
109109
if (pluginDef.js) {
110+
const preWindowKeys = (typeof window !== 'undefined') ? new Set(Object.keys(window)) : new Set();
111+
110112
for (const item of pluginDef.js) {
111113
const src = typeof item === 'string' ? item : item.src;
112114
const integrity = typeof item === 'object' ? item.integrity : null;
@@ -152,6 +154,36 @@ async function processPlugin(el) {
152154
console.error(`Failed to load script: ${src}`, e);
153155
}
154156
}
157+
158+
// After loading JS files, map newly-added globals to the expected plugin name.
159+
// Exact (case-insensitive) matching only.
160+
try {
161+
if (typeof window !== 'undefined') {
162+
const expectedName = pluginName;
163+
const lower = expectedName.toLowerCase();
164+
165+
const allKeys = Object.keys(window);
166+
const newKeys = allKeys.filter(k => !preWindowKeys.has(k));
167+
let mappedKey = null;
168+
169+
for (const k of newKeys) {
170+
if (k.toLowerCase() === lower) { mappedKey = k; break; }
171+
}
172+
173+
if (!mappedKey) {
174+
for (const k of allKeys) {
175+
if (k.toLowerCase() === lower) { mappedKey = k; break; }
176+
}
177+
}
178+
179+
if (mappedKey && !window[expectedName]) {
180+
window[expectedName] = window[mappedKey];
181+
console.debug(`Mapped plugin global: window.${expectedName} <- window.${mappedKey}`);
182+
}
183+
}
184+
} catch (err) {
185+
// Non-fatal
186+
}
155187
}
156188
}
157189

@@ -234,7 +266,7 @@ function executeGenericPlugin(el, name) {
234266

235267
// Pass context: Window as parent, Plugin Name as property (for potential context binding)
236268
// el and name used to store the result on the element.
237-
update(Target, val, window, name, el, name);
269+
update(Target, val, window, name, el, name, el);
238270

239271
console.log(`Processed ${name}`);
240272
} catch (e) {
@@ -244,7 +276,126 @@ function executeGenericPlugin(el, name) {
244276
}
245277
}
246278

247-
function update(Target, val, parent, property, elParent, elProperty) {
279+
function resolvePathWithParent(root, path) {
280+
if (!root || !path || typeof path !== "string") return { parent: null, value: undefined };
281+
const parts = path.split(".").filter(Boolean);
282+
if (!parts.length) return { parent: null, value: undefined };
283+
284+
let parent = null;
285+
let current = root;
286+
for (let i = 0; i < parts.length; i++) {
287+
const part = parts[i];
288+
if (current == null) return { parent: null, value: undefined };
289+
parent = current;
290+
current = current[part];
291+
}
292+
return { parent, value: current };
293+
}
294+
295+
function normalizeCrudPayload(value) {
296+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
297+
298+
if (value.type && Array.isArray(value[value.type])) return value[value.type];
299+
if (value.method && typeof value.method === "string") {
300+
const type = value.method.split(".")[0];
301+
if (type && Array.isArray(value[type])) return value[type];
302+
}
303+
return value;
304+
}
305+
306+
function getPluginInstancesFromElement(el) {
307+
if (!el || !el.__cocreatePluginInstances) return [];
308+
return Object.values(el.__cocreatePluginInstances).filter(Boolean);
309+
}
310+
311+
function isReferenceAssignment(val) {
312+
return typeof val === "string" && val.trim().startsWith("=");
313+
}
314+
315+
function normalizeReferencePath(refPath) {
316+
if (typeof refPath !== "string") return "";
317+
return refPath.trim().replace(/^=\s*/, "");
318+
}
319+
320+
function resolveCallableReference(refPath, parent, hostElement) {
321+
const normalized = normalizeReferencePath(refPath);
322+
if (!normalized) return { fn: undefined, context: undefined, methodName: undefined };
323+
324+
const methodName = normalized.split(".").pop();
325+
const startsWithThis = normalized === "$this" || normalized.startsWith("$this.");
326+
const startsWithWindow = normalized === "$window" || normalized.startsWith("$window.");
327+
const startsWithToken = normalized.startsWith("$");
328+
329+
const candidates = [];
330+
if (startsWithThis) {
331+
const path = normalized.replace(/^\$this\.?/, "");
332+
candidates.push({ root: hostElement || parent, path });
333+
} else if (startsWithWindow) {
334+
const path = normalized.replace(/^\$window\.?/, "");
335+
candidates.push({ root: window, path });
336+
} else if (startsWithToken) {
337+
const path = normalized.replace(/^\$/, "");
338+
candidates.push({ root: hostElement, path });
339+
candidates.push({ root: parent, path });
340+
candidates.push({ root: window, path });
341+
} else {
342+
candidates.push({ root: hostElement, path: normalized });
343+
candidates.push({ root: parent, path: normalized });
344+
candidates.push({ root: window, path: normalized });
345+
}
346+
347+
for (const candidate of candidates) {
348+
if (!candidate.root) continue;
349+
const { parent: resolvedParent, value } = resolvePathWithParent(candidate.root, candidate.path);
350+
if (typeof value === "function") {
351+
return { fn: value, context: resolvedParent, methodName };
352+
}
353+
}
354+
355+
if (methodName) {
356+
const instances = getPluginInstancesFromElement(hostElement || parent);
357+
for (const instance of instances) {
358+
if (instance && typeof instance[methodName] === "function") {
359+
return { fn: instance[methodName], context: instance, methodName };
360+
}
361+
}
362+
}
363+
364+
return { fn: undefined, context: undefined, methodName };
365+
}
366+
367+
function createFunctionAdapter(refPath, parent, property, hostElement) {
368+
const normalizedRefPath = normalizeReferencePath(refPath);
369+
const methodName = normalizedRefPath.split(".").pop();
370+
371+
return function (...args) {
372+
const resolved = resolveCallableReference(normalizedRefPath, parent, hostElement);
373+
const fn = resolved.fn;
374+
const context = resolved.context;
375+
376+
if (typeof fn !== "function") {
377+
console.error(`Plugin adapter failed: "${normalizedRefPath}" did not resolve to a function for ${property}.`);
378+
return;
379+
}
380+
381+
if (property === "setValue") {
382+
const payload = normalizeCrudPayload(args[0]);
383+
384+
if (methodName === "addEventSource" && context && typeof context.getEventSources === "function") {
385+
const sources = context.getEventSources();
386+
if (Array.isArray(sources)) {
387+
sources.forEach(source => source && typeof source.remove === "function" && source.remove());
388+
}
389+
}
390+
391+
return fn.call(context || this, payload);
392+
}
393+
394+
return fn.apply(context || this, args);
395+
};
396+
}
397+
398+
function update(Target, val, parent, property, elParent, elProperty, hostElement) {
248399
// RESOLUTION: Handle case-insensitivity before processing targets.
249400
// If Target is missing, check parent for a property matching 'property' (case-insensitive).
250401
if (!Target && parent && property) {
@@ -261,6 +412,13 @@ function update(Target, val, parent, property, elParent, elProperty) {
261412

262413
let instance;
263414
if (typeof Target === 'function') {
415+
if (isReferenceAssignment(val) && parent && property) {
416+
instance = createFunctionAdapter(val, parent, property, hostElement);
417+
parent[property] = instance;
418+
if (elParent && elProperty) elParent[elProperty] = instance;
419+
return;
420+
}
421+
264422
if (!isConstructor(Target, property)) {
265423
// Call as a function (method or standalone)
266424
// Use 'parent' as context (this) if available to maintain class references
@@ -291,6 +449,12 @@ function update(Target, val, parent, property, elParent, elProperty) {
291449
elParent[elProperty] = instance;
292450
}
293451

452+
if (instance && instance.el && typeof instance.el === "object") {
453+
if (!instance.el.__cocreatePluginInstances) instance.el.__cocreatePluginInstances = {};
454+
const key = property || (Target && Target.name) || "instance";
455+
instance.el.__cocreatePluginInstances[key] = instance;
456+
}
457+
294458
} else if (typeof Target === 'object' && Target !== null && typeof val === 'object' && val !== null && !Array.isArray(val)) {
295459
// Prepare the next level of the element structure
296460
if (elParent && elProperty) {
@@ -300,10 +464,17 @@ function update(Target, val, parent, property, elParent, elProperty) {
300464
const nextElParent = elParent[elProperty];
301465

302466
for (let key in val) {
303-
update(Target[key], val[key], Target, key, nextElParent, key);
467+
update(Target[key], val[key], Target, key, nextElParent, key, hostElement);
304468
}
305469
}
306470
} else if (parent && property) {
471+
if (isReferenceAssignment(val)) {
472+
const adapter = createFunctionAdapter(val, parent, property, hostElement);
473+
parent[property] = adapter;
474+
if (elParent && elProperty) elParent[elProperty] = adapter;
475+
return;
476+
}
477+
307478
// If it's not a function, we are setting a value on the plugin object
308479
parent[property] = val;
309480

@@ -418,4 +589,4 @@ if (typeof document !== 'undefined') {
418589
});
419590
}
420591

421-
export default { init, plugins }
592+
export default { init, plugins }

0 commit comments

Comments
 (0)