/**
* Manages runtime style injection for component classes.
* Ensures only one <style> element per component class is ever present
* in the document <head>, using reference counting to safely remove
* styles only when all instances of that class have been unmounted.
*/
export class StyleMountManager {
/**
* Maps a component class style identifier to its metadata.
* @type {Map<string, { element: Element, refCount: number }>}
* @private
*/
#registry = new Map();
/**
* CSP nonce applied to runtime-created <style> elements.
* @type {string|null}
* @private
*/
#cspNonce = null;
/**
* Configures the CSP nonce used for runtime-created <style> elements.
* Passing null, undefined, or an empty value disables nonce application.
* @param {string|null|undefined} nonce
*/
setCspNonce(nonce) {
this.#cspNonce =
typeof nonce === 'string' && nonce.trim()
? nonce
: null;
}
/**
* Caches generated style IDs for anonymous (unnamed) component classes.
* @type {WeakMap<Function, string>}
* @private
*/
#anonymousIdMap = new WeakMap();
/**
* Auto-incrementing counter for generating unique style IDs for anonymous component classes.
* @type {number}
* @private
*/
#anonymousCounter = 0;
/**
* Mounts runtime styles for a component class into the document <head>.
* If the styles for this class are already mounted, increments the
* reference count without creating a duplicate <style> element.
* @param {Function} componentClass - The component class (constructor).
*/
mount(componentClass) {
const styles = componentClass.styles;
if (!styles || typeof styles !== 'string' || !styles.trim()) return;
const styleId = this.#getStyleId(componentClass);
if (this.#registry.has(styleId)) {
this.#registry.get(styleId).refCount++;
return;
}
// Check if a style element with this ID already exists in the DOM
// (e.g. from a previous app lifecycle or SSR hydration)
if (typeof document !== 'undefined' && document.head) {
const existing = document.head.querySelector(`[data-avenx-style="${styleId}"]`);
if (existing) {
this.#registry.set(styleId, { element: existing, refCount: 1 });
return;
}
}
// Create and append a new <style> element
if (typeof document !== 'undefined' && document.head) {
const styleEl = document.createElement('style');
styleEl.setAttribute('data-avenx-style', styleId);
if (this.#cspNonce) {
styleEl.setAttribute('nonce', this.#cspNonce);
}
styleEl.textContent = styles;
document.head.appendChild(styleEl);
this.#registry.set(styleId, { element: styleEl, refCount: 1 });
}
}
/**
* Decrements the reference count for a component class's styles.
* Removes the <style> element from the DOM only when no more
* instances of that class are active.
* @param {Function} componentClass - The component class (constructor).
*/
unmount(componentClass) {
const styles = componentClass.styles;
if (!styles || typeof styles !== 'string' || !styles.trim()) return;
const styleId = this.#getStyleId(componentClass);
const entry = this.#registry.get(styleId);
if (entry) {
entry.refCount--;
}
const refCount = entry ? entry.refCount : 0;
const hasActiveDOM = this.#hasActiveInstancesInDOM(componentClass);
if (refCount <= 0 || !hasActiveDOM) {
this.#removeStyleElements(styleId, entry ? entry.element : null);
this.#registry.delete(styleId);
}
}
/**
* Cleans up all style elements matching a style ID from document head.
* @param {string} styleId
* @param {Element|null} [fallbackElement]
* @private
*/
#removeStyleElements(styleId, fallbackElement) {
if (typeof document !== 'undefined' && document.head) {
let removedAny = false;
if (document.head.querySelectorAll) {
const matching = document.head.querySelectorAll(`[data-avenx-style="${styleId}"]`);
if (matching && matching.length > 0) {
Array.from(matching).forEach((el) => {
if (el && el.parentNode) {
el.parentNode.removeChild(el);
removedAny = true;
}
});
}
} else if (document.head.querySelector) {
let matching = document.head.querySelector(`[data-avenx-style="${styleId}"]`);
while (matching) {
if (matching.parentNode) {
matching.parentNode.removeChild(matching);
removedAny = true;
} else {
break;
}
matching = document.head.querySelector(`[data-avenx-style="${styleId}"]`);
}
}
if (!removedAny && fallbackElement && fallbackElement.parentNode) {
fallbackElement.parentNode.removeChild(fallbackElement);
}
}
}
/**
* Checks if any active instances of the component class currently exist in the DOM tree.
* @param {Function} componentClass
* @returns {boolean}
* @private
*/
#hasActiveInstancesInDOM(componentClass) {
if (typeof document === 'undefined' || (!document.body && !document.documentElement)) {
return true;
}
const targetName = componentClass.name;
const isMatchingInstance = (el) => {
if (!el || !el.__avenx_comp_instance) return false;
const inst = el.__avenx_comp_instance;
return inst.constructor === componentClass || (targetName && inst.constructor.name === targetName);
};
const traverse = (root) => {
if (!root) return false;
if (isMatchingInstance(root)) return true;
if (root.children) {
for (let i = 0; i < root.children.length; i++) {
if (traverse(root.children[i])) return true;
}
}
return false;
};
return traverse(document.body) || traverse(document.documentElement);
}
/**
* Generates a unique style identifier for a component class.
* Uses the class name as the key.
* @param {Function} componentClass - The component class.
* @returns {string} The style identifier.
* @private
*/
#getStyleId(componentClass) {
if (!componentClass) return 'avenx-style-unknown';
const className = componentClass.name ? componentClass.name.trim() : '';
if (className) {
return `avenx-style-${className}`;
}
if (!this.#anonymousIdMap.has(componentClass)) {
this.#anonymousCounter += 1;
this.#anonymousIdMap.set(componentClass, `avenx-style-anonymous-${this.#anonymousCounter}`);
}
return this.#anonymousIdMap.get(componentClass);
}
/**
* Returns the current reference count for a component class's styles.
* Useful for testing purposes.
* @param {Function} componentClass - The component class.
* @returns {number} The reference count, or 0 if not mounted.
*/
getRefCount(componentClass) {
const styleId = this.#getStyleId(componentClass);
const entry = this.#registry.get(styleId);
return entry ? entry.refCount : 0;
}
}
/**
* The singleton instance used by all components.
* @type {StyleMountManager}
*/
export const styleMountManager = new StyleMountManager();