All files / lib/core/runtime StyleMountManager.js

81.65% Statements 187/229
80.85% Branches 38/47
70% Functions 7/10
81.65% Lines 187/229

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x           403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 332x 332x 12x 12x 12x 13x 5x 5x 5x 7x 7x 7x 332x 7x 7x       7x 7x 7x 332x 7x 7x 7x 7x 7x     7x 7x 7x 7x 7x 7x 332x 403x 403x 403x 403x 403x 403x 403x 403x 193x 193x 13x 13x 13x 13x 14x 12x 12x 13x 193x 193x 193x 193x 8x 8x 8x 193x 403x 403x 403x 403x 403x 403x 403x 403x 8x 8x 8x 8x                   8x 8x 8x 8x 8x 8x 8x     8x 8x 8x 8x 8x     8x 8x 403x 403x 403x 403x 403x 403x 403x 403x 13x 13x 13x                                       13x 13x 403x 403x 403x 403x 403x 403x 403x 403x 403x 34x 34x 34x 34x 24x 24x 10x 34x 2x 2x 2x 10x 10x 34x 403x 403x 403x 403x 403x 403x 403x 403x 9x 9x 9x 9x 403x 403x 403x 403x 403x 403x 403x  
/**
 * 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();