All files / lib/core/tooling inspect.js

36.98% Statements 54/146
68.18% Branches 15/22
28.57% Functions 2/7
36.98% Lines 54/146

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 147403x 403x 403x 403x 403x 403x 403x 403x 18x 5x     5x 5x 13x 13x 18x     13x 18x 1x 1x 12x 18x           12x 12x 18x             12x 12x 12x 18x 9x 1x 9x   8x 8x 8x 9x 12x 12x 18x 403x 403x 403x 403x 403x 403x                                                                         403x 403x 403x 403x 403x 403x 108x 108x 108x                                                                                 108x  
/**
 * Recursively clones an object and strips non-cloneable elements
 * (like functions, circular references, and DOM nodes).
 * @param {*} val - The value to sanitize.
 * @param {WeakSet<object>} [seen] - WeakSet tracking visited objects to prevent circular loops.
 * @returns {*} The safe clone.
 */
export function serializeSafe(val, seen = new WeakSet()) {
  if (val === null || typeof val !== 'object') {
    if (typeof val === 'function') {
      return '[Function]';
    }
    return val;
  }
 
  // Handle DOM elements and window references
  if (typeof window !== 'undefined' && ((typeof Node !== 'undefined' && val instanceof Node) || val === window)) {
    return `[DOM Element: ${val.nodeName || 'Window'}]`;
  }
 
  if (seen.has(val)) {
    return '[Circular]';
  }
 
  if (Array.isArray(val)) {
    seen.add(val);
    const res = val.map(item => serializeSafe(item, seen));
    seen.delete(val);
    return res;
  }
 
  // Use toJSON if available
  if (typeof val.toJSON === 'function') {
    try {
      return val.toJSON();
    } catch {
      // Ignored fallback
    }
  }
 
  seen.add(val);
  const res = {};
  for (const [key, value] of Object.entries(val)) {
    if (typeof value === 'function') {
      res[key] = '[Function]';
    } else if (key.startsWith('__')) {
      res[key] = '[Internal]';
    } else {
      res[key] = serializeSafe(value, seen);
    }
  }
  seen.delete(val);
  return res;
}
 
/**
 * Collects all active component instances and application registration metadata.
 * @param {object} app - The AvenxApp instance.
 * @returns {object} Inspector data payload.
 */
function getInspectorData(app) {
  const activeComponents = [];
  if (typeof document !== 'undefined') {
    const elements = document.querySelectorAll('[data-avenx-comp], [data-avenx-comp-dynamic]');
    elements.forEach((el) => {
      if (el.__avenx_comp_instance) {
        const comp = el.__avenx_comp_instance;
        activeComponents.push({
          name: comp.constructor.name,
          state: comp.state || {},
          props: comp.props || {},
        });
      }
    });
  }

  const registeredBridges = {};
  for (const [name, bridge] of Object.entries(app.bridges || {})) {
    registeredBridges[name] = bridge;
  }

  const registeredComponents = app.components ? Array.from(app.components.keys()) : [];
  const registeredPages = app.pages ? Array.from(app.pages.keys()) : [];

  const routes = app.router ? app.router.routes : {};
  const currentRoute = app.router ? app.router.currentRoute : null;

  return serializeSafe({
    activeComponents,
    registeredBridges,
    registeredComponents,
    registeredPages,
    routes,
    currentRoute,
  });
}
 
/**
 * Initializes the inspector for an AvenxApp.
 * @param {object} app - The AvenxApp instance.
 */
export function initInspector(app) {
  if (typeof window === 'undefined' || !window.__avenx_inspect_enabled || typeof globalThis.BroadcastChannel === 'undefined') {
    return;
  }

  const channel = new globalThis.BroadcastChannel('avenx-inspector-channel');

  const broadcast = () => {
    try {
      channel.postMessage({
        type: 'inspect-data',
        data: getInspectorData(app),
      });
    } catch (err) {
      // Gracefully catch any remaining clone/postMessage errors
      console.warn('[Avenx Inspector] Failed to broadcast state:', err);
    }
  };

  channel.onmessage = (event) => {
    if (event.data === 'request-inspect-data') {
      broadcast();
    }
  };

  // Automatically broadcast on page transitions or component lifecycles/updates.
  const originalUpdateAll = app.updateAll;
  app.updateAll = function (...args) {
    const res = originalUpdateAll.apply(this, args);
    broadcast();
    return res;
  };

  const originalMountPage = app.mountPage;
  app.mountPage = function (...args) {
    const res = originalMountPage.apply(this, args);
    broadcast();
    return res;
  };

  // Intercept component lifecycles using capturing phase listeners (since they don't bubble)
  window.addEventListener('avenx:update', () => broadcast(), true);
  window.addEventListener('avenx:mount', () => broadcast(), true);
  window.addEventListener('avenx:unmount', () => broadcast(), true);
}