@coherent.js/devtools 1.1.0 → 2.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,3 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // src/inspector.js
9
2
  var ComponentInspector = class {
10
3
  constructor(options = {}) {
@@ -104,20 +97,20 @@ var ComponentInspector = class {
104
97
  const warnings = [];
105
98
  const info = [];
106
99
  const seen = /* @__PURE__ */ new WeakSet();
107
- const checkCircular = (obj, path2 = []) => {
100
+ const checkCircular = (obj, path = []) => {
108
101
  if (obj === null || typeof obj !== "object") {
109
102
  return;
110
103
  }
111
104
  if (seen.has(obj)) {
112
- warnings.push(`circular reference detected at ${path2.join(".")}`);
105
+ warnings.push(`circular reference detected at ${path.join(".")}`);
113
106
  return;
114
107
  }
115
108
  seen.add(obj);
116
109
  if (Array.isArray(obj)) {
117
- obj.forEach((item, index) => checkCircular(item, [...path2, `[${index}]`]));
110
+ obj.forEach((item, index) => checkCircular(item, [...path, `[${index}]`]));
118
111
  } else {
119
112
  Object.keys(obj).forEach((key) => {
120
- checkCircular(obj[key], [...path2, key]);
113
+ checkCircular(obj[key], [...path, key]);
121
114
  });
122
115
  }
123
116
  };
@@ -390,10 +383,34 @@ function validateComponent(component) {
390
383
  }
391
384
 
392
385
  // src/profiler.js
386
+ var hasPerformanceTimeline = () => typeof performance !== "undefined" && typeof performance.mark === "function";
387
+ function now() {
388
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
389
+ }
390
+ function clearTimelineEntries(marks, measureName) {
391
+ if (!hasPerformanceTimeline()) return;
392
+ for (const mark of marks) {
393
+ performance.clearMarks?.(mark);
394
+ }
395
+ if (measureName) {
396
+ performance.clearMeasures?.(measureName);
397
+ }
398
+ }
399
+ function recordTimelineMeasure(name, startMark, endMark) {
400
+ if (!hasPerformanceTimeline()) return;
401
+ try {
402
+ performance.mark(endMark);
403
+ performance.measure?.(name, startMark, endMark);
404
+ } catch {
405
+ } finally {
406
+ clearTimelineEntries([startMark, endMark], name);
407
+ }
408
+ }
393
409
  var PerformanceProfiler = class {
394
410
  constructor(options = {}) {
395
411
  this.options = {
396
- enabled: true,
412
+ // Opt-in: a profiler that is merely constructed must cost nothing.
413
+ enabled: false,
397
414
  sampleRate: 1,
398
415
  // 1.0 = 100% sampling
399
416
  slowThreshold: 16,
@@ -420,14 +437,14 @@ var PerformanceProfiler = class {
420
437
  const session = {
421
438
  id: this.generateId(),
422
439
  name,
423
- startTime: Date.now(),
440
+ startTime: now(),
424
441
  measurements: [],
425
442
  marks: [],
426
443
  active: true
427
444
  };
428
445
  this.sessions.set(session.id, session);
429
446
  this.currentSession = session;
430
- if (typeof performance !== "undefined" && performance.mark) {
447
+ if (hasPerformanceTimeline()) {
431
448
  performance.mark(`coherent-session-start-${session.id}`);
432
449
  }
433
450
  return session.id;
@@ -443,19 +460,18 @@ var PerformanceProfiler = class {
443
460
  if (!session) {
444
461
  return null;
445
462
  }
446
- session.endTime = Date.now();
463
+ session.endTime = now();
447
464
  session.duration = session.endTime - session.startTime;
448
465
  session.active = false;
449
- if (typeof performance !== "undefined" && performance.mark) {
450
- performance.mark(`coherent-session-end-${session.id}`);
451
- }
466
+ recordTimelineMeasure(
467
+ `coherent-session-${session.id}`,
468
+ `coherent-session-start-${session.id}`,
469
+ `coherent-session-end-${session.id}`
470
+ );
452
471
  if (this.currentSession === session) {
453
472
  this.currentSession = null;
454
473
  }
455
- this.measurements.push(session);
456
- if (this.measurements.length > this.options.maxSamples) {
457
- this.measurements.shift();
458
- }
474
+ this.addMeasurement(session);
459
475
  return this.analyzeSession(session);
460
476
  }
461
477
  /**
@@ -469,12 +485,12 @@ var PerformanceProfiler = class {
469
485
  id: measurementId,
470
486
  componentName,
471
487
  props,
472
- startTime: Date.now(),
488
+ startTime: now(),
473
489
  startMemory: this.getMemoryUsage(),
474
490
  phase: "render"
475
491
  };
476
492
  this.marks.set(measurementId, measurement);
477
- if (typeof performance !== "undefined" && performance.mark) {
493
+ if (hasPerformanceTimeline()) {
478
494
  performance.mark(`coherent-render-start-${measurementId}`);
479
495
  }
480
496
  return measurementId;
@@ -485,28 +501,21 @@ var PerformanceProfiler = class {
485
501
  endRender(measurementId, result = {}) {
486
502
  if (!measurementId || !this.marks.has(measurementId)) return null;
487
503
  const measurement = this.marks.get(measurementId);
488
- measurement.endTime = Date.now();
504
+ measurement.endTime = now();
489
505
  measurement.duration = measurement.endTime - measurement.startTime;
490
506
  measurement.endMemory = this.getMemoryUsage();
491
- measurement.memoryDelta = measurement.endMemory - measurement.startMemory;
507
+ measurement.memoryDelta = measurement.startMemory && measurement.endMemory ? measurement.endMemory.used - measurement.startMemory.used : null;
492
508
  measurement.result = result;
493
509
  measurement.slow = measurement.duration > this.options.slowThreshold;
494
- if (typeof performance !== "undefined" && performance.mark) {
495
- performance.mark(`coherent-render-end-${measurementId}`);
496
- if (performance.measure) {
497
- try {
498
- performance.measure(
499
- `coherent-render-${measurementId}`,
500
- `coherent-render-start-${measurementId}`,
501
- `coherent-render-end-${measurementId}`
502
- );
503
- } catch {
504
- }
505
- }
506
- }
507
- this.measurements.push(measurement);
510
+ recordTimelineMeasure(
511
+ `coherent-render-${measurementId}`,
512
+ `coherent-render-start-${measurementId}`,
513
+ `coherent-render-end-${measurementId}`
514
+ );
515
+ this.addMeasurement(measurement);
508
516
  if (this.currentSession) {
509
517
  this.currentSession.measurements.push(measurement);
518
+ this.trim(this.currentSession.measurements);
510
519
  }
511
520
  this.marks.delete(measurementId);
512
521
  return measurement;
@@ -517,15 +526,13 @@ var PerformanceProfiler = class {
517
526
  mark(name, data = {}) {
518
527
  const mark = {
519
528
  name,
520
- timestamp: Date.now(),
529
+ timestamp: now(),
521
530
  data,
522
531
  memory: this.getMemoryUsage()
523
532
  };
524
533
  if (this.currentSession) {
525
534
  this.currentSession.marks.push(mark);
526
- }
527
- if (typeof performance !== "undefined" && performance.mark) {
528
- performance.mark(`coherent-mark-${name}`);
535
+ this.trim(this.currentSession.marks);
529
536
  }
530
537
  return mark;
531
538
  }
@@ -544,6 +551,22 @@ var PerformanceProfiler = class {
544
551
  endMark: end.name
545
552
  };
546
553
  }
554
+ /**
555
+ * Append a measurement, dropping the oldest beyond `maxSamples`.
556
+ */
557
+ addMeasurement(measurement) {
558
+ this.measurements.push(measurement);
559
+ this.trim(this.measurements);
560
+ }
561
+ /**
562
+ * Drop the oldest entries of `list` beyond `maxSamples`.
563
+ */
564
+ trim(list) {
565
+ const max = Math.max(1, Number(this.options.maxSamples) || 1e3);
566
+ if (list.length > max) {
567
+ list.splice(0, list.length - max);
568
+ }
569
+ }
547
570
  /**
548
571
  * Get memory usage
549
572
  */
@@ -838,6 +861,10 @@ var PerformanceProfiler = class {
838
861
  * Clear all data
839
862
  */
840
863
  clear() {
864
+ clearTimelineEntries([
865
+ ...[...this.marks.keys()].map((id) => `coherent-render-start-${id}`),
866
+ ...[...this.sessions.values()].filter((session) => session.active).map((session) => `coherent-session-start-${session.id}`)
867
+ ]);
841
868
  this.measurements = [];
842
869
  this.sessions.clear();
843
870
  this.currentSession = null;
@@ -856,17 +883,28 @@ var PerformanceProfiler = class {
856
883
  this.options.enabled = false;
857
884
  }
858
885
  /**
859
- * Generate unique ID
886
+ * Generate unique ID.
887
+ *
888
+ * Uses crypto.getRandomValues rather than Math.random: the ids key the
889
+ * session and measurement maps, so a predictable suffix lets one caller
890
+ * guess or collide with another's entry. getRandomValues is available in
891
+ * Node 19+ and in browsers without requiring a secure context, unlike
892
+ * randomUUID.
893
+ *
894
+ * @returns {string} A unique profiling id
860
895
  */
861
896
  generateId() {
862
- return `prof-${Date.now()}-${Math.random().toString(36).substring(7)}`;
897
+ const bytes = new Uint8Array(8);
898
+ globalThis.crypto.getRandomValues(bytes);
899
+ const suffix = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
900
+ return `prof-${Date.now()}-${suffix}`;
863
901
  }
864
902
  };
865
903
  function createProfiler(options = {}) {
866
904
  return new PerformanceProfiler(options);
867
905
  }
868
906
  async function measure(name, fn, profiler = null) {
869
- const prof = profiler || new PerformanceProfiler();
907
+ const prof = profiler || new PerformanceProfiler({ enabled: true });
870
908
  const sessionId = prof.start(name);
871
909
  try {
872
910
  const value = await fn();
@@ -874,14 +912,48 @@ async function measure(name, fn, profiler = null) {
874
912
  return { value, duration: result?.duration || 0 };
875
913
  } catch (error) {
876
914
  const result = prof.stop(sessionId);
877
- throw { error, duration: result?.duration || 0 };
915
+ const failure = error instanceof Error ? error : new Error(`${name} failed: ${String(error)}`, { cause: error });
916
+ try {
917
+ failure.duration = result?.duration || 0;
918
+ } catch {
919
+ }
920
+ throw failure;
878
921
  }
879
922
  }
880
- function profile(fn) {
881
- return function(...args) {
882
- const result = fn(...args);
923
+ function profile(fn, options = {}) {
924
+ if (typeof fn !== "function") {
925
+ throw new TypeError("profile() expects a function");
926
+ }
927
+ const opts = options instanceof PerformanceProfiler ? { profiler: options } : options;
928
+ const profiler = opts.profiler || new PerformanceProfiler({ enabled: true });
929
+ const name = opts.name || fn.name || "anonymous";
930
+ function profiled(...args) {
931
+ const id = profiler.startRender(name);
932
+ let result;
933
+ try {
934
+ result = fn.apply(this, args);
935
+ } catch (error) {
936
+ profiler.endRender(id, { error: true });
937
+ throw error;
938
+ }
939
+ if (result && typeof result.then === "function") {
940
+ return Promise.resolve(result).then(
941
+ (value) => {
942
+ profiler.endRender(id);
943
+ return value;
944
+ },
945
+ (error) => {
946
+ profiler.endRender(id, { error: true });
947
+ throw error;
948
+ }
949
+ );
950
+ }
951
+ profiler.endRender(id);
883
952
  return result;
884
- };
953
+ }
954
+ Object.defineProperty(profiled, "name", { value: name });
955
+ profiled.profiler = profiler;
956
+ return profiled;
885
957
  }
886
958
 
887
959
  // src/logger.js
@@ -1340,28 +1412,53 @@ function createConsoleLogger(prefix = "") {
1340
1412
 
1341
1413
  // src/dev-tools.js
1342
1414
  import { performanceMonitor, validateComponent as validateComponent2, isCoherentObject } from "@coherent.js/core";
1415
+ var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
1343
1416
  var DevTools = class {
1344
- constructor(coherentInstance) {
1417
+ /**
1418
+ * @param {Object} [coherentInstance] - Object exposing `render` (and optionally
1419
+ * `createComponent` / `cache`), e.g. `import * as coherent from '@coherent.js/core'`.
1420
+ * @param {Object} [options]
1421
+ * @param {boolean} [options.enabled] - Force on or off; defaults to {@link DevTools#shouldEnable}.
1422
+ * @param {number} [options.maxEntries=100] - Cap on retained warnings and errors each.
1423
+ * @param {boolean} [options.captureConsoleErrors=true] - Record console.error calls.
1424
+ * @param {boolean} [options.trackUnhandledRejections=false] - Record unhandled promise
1425
+ * rejections (Node). They still crash the process as they would without DevTools.
1426
+ * @param {string} [options.hotReloadUrl] - Browser only: WebSocket URL of a dev server
1427
+ * sending `component-updated` / `full-reload` messages. No connection is made without it.
1428
+ * @param {boolean} [options.globalHelpers=true] - Expose `$inspect`, `$history`, … globally.
1429
+ */
1430
+ constructor(coherentInstance = null, options = {}) {
1345
1431
  this.coherent = coherentInstance;
1346
- this.isEnabled = this.shouldEnable();
1432
+ this.options = {
1433
+ maxEntries: 100,
1434
+ captureConsoleErrors: true,
1435
+ trackUnhandledRejections: false,
1436
+ hotReloadUrl: null,
1437
+ globalHelpers: true,
1438
+ ...options
1439
+ };
1440
+ this.isEnabled = typeof options.enabled === "boolean" ? options.enabled : this.shouldEnable();
1347
1441
  this.renderHistory = [];
1348
1442
  this.componentRegistry = /* @__PURE__ */ new Map();
1349
1443
  this.warnings = [];
1350
1444
  this.errors = [];
1351
1445
  this.hotReloadEnabled = false;
1446
+ this._teardown = [];
1352
1447
  if (this.isEnabled) {
1353
1448
  this.initialize();
1354
1449
  }
1355
1450
  }
1356
1451
  /**
1357
- * Check if dev tools should be enabled
1452
+ * Check if dev tools should be enabled: NODE_ENV=development in Node, a
1453
+ * localhost page in the browser. Anything else (a query parameter on a
1454
+ * production host, say) needs the explicit `enabled: true` option.
1358
1455
  */
1359
1456
  shouldEnable() {
1360
- if (typeof process !== "undefined") {
1457
+ if (typeof process !== "undefined" && process?.env) {
1361
1458
  return process.env.NODE_ENV === "development";
1362
1459
  }
1363
- if (typeof window !== "undefined") {
1364
- return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.search.includes("dev=true");
1460
+ if (typeof window !== "undefined" && window.location) {
1461
+ return LOCAL_HOSTNAMES.has(window.location.hostname);
1365
1462
  }
1366
1463
  return false;
1367
1464
  }
@@ -1370,18 +1467,86 @@ var DevTools = class {
1370
1467
  */
1371
1468
  initialize() {
1372
1469
  console.log("\u{1F6E0}\uFE0F Coherent.js Dev Tools Enabled");
1373
- this.setupGlobalHelpers();
1374
- this.setupRenderInterception();
1470
+ if (this.options.globalHelpers) {
1471
+ this.setupGlobalHelpers();
1472
+ }
1375
1473
  this.setupErrorHandling();
1376
1474
  this.setupHotReload();
1377
- this.setupComponentInspector();
1378
- if (typeof window !== "undefined") {
1475
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
1379
1476
  this.setupBrowserDevTools();
1380
1477
  }
1381
- if (typeof process !== "undefined") {
1382
- this.setupNodeDevTools();
1478
+ }
1479
+ /**
1480
+ * Undo everything initialize() installed: global helpers, the
1481
+ * console.error hook, the unhandled-rejection listener, the hot-reload
1482
+ * socket and the browser panel.
1483
+ */
1484
+ destroy() {
1485
+ while (this._teardown.length) {
1486
+ try {
1487
+ this._teardown.pop()();
1488
+ } catch {
1489
+ }
1490
+ }
1491
+ }
1492
+ /**
1493
+ * Append to a bounded list (warnings, errors), dropping the oldest.
1494
+ */
1495
+ record(list, entry) {
1496
+ list.push(entry);
1497
+ const max = Math.max(1, this.options.maxEntries);
1498
+ if (list.length > max) {
1499
+ list.splice(0, list.length - max);
1500
+ }
1501
+ return entry;
1502
+ }
1503
+ /**
1504
+ * Render through the wrapped Coherent instance, recording timing,
1505
+ * validation warnings and errors.
1506
+ */
1507
+ render(component, context = {}, options = {}) {
1508
+ const render = this.coherent?.render;
1509
+ if (typeof render !== "function") {
1510
+ throw new TypeError("DevTools.render() needs a Coherent instance with a render() function: createDevTools(coherent)");
1511
+ }
1512
+ if (!this.isEnabled) {
1513
+ return render.call(this.coherent, component, context, options);
1514
+ }
1515
+ const renderStart = performance.now();
1516
+ const renderId = this.generateRenderId();
1517
+ try {
1518
+ this.preRenderAnalysis(component, context, renderId);
1519
+ const result = render.call(this.coherent, component, context, {
1520
+ ...options,
1521
+ _devRenderId: renderId
1522
+ });
1523
+ const renderTime = performance.now() - renderStart;
1524
+ this.postRenderAnalysis(component, result, renderTime, renderId);
1525
+ return result;
1526
+ } catch (_error) {
1527
+ this.handleRenderError(_error, component, context, renderId);
1528
+ throw _error;
1383
1529
  }
1384
1530
  }
1531
+ /**
1532
+ * createComponent() of the wrapped instance, registering the result for
1533
+ * inspection.
1534
+ */
1535
+ createComponent(config) {
1536
+ const create = this.coherent?.createComponent;
1537
+ if (typeof create !== "function") {
1538
+ throw new TypeError("DevTools.createComponent() needs a Coherent instance with createComponent()");
1539
+ }
1540
+ const component = create.call(this.coherent, config);
1541
+ if (this.isEnabled) {
1542
+ this.componentRegistry.set(config?.name || "anonymous", {
1543
+ config,
1544
+ component,
1545
+ registeredAt: Date.now()
1546
+ });
1547
+ }
1548
+ return component;
1549
+ }
1385
1550
  /**
1386
1551
  * Set up global helper functions
1387
1552
  */
@@ -1404,34 +1569,15 @@ var DevTools = class {
1404
1569
  // Get warnings and errors
1405
1570
  $issues: () => ({ warnings: this.warnings, errors: this.errors })
1406
1571
  };
1407
- if (typeof window !== "undefined") {
1408
- Object.assign(window, helpers);
1409
- } else if (typeof global !== "undefined") {
1410
- Object.assign(global, helpers);
1411
- }
1412
- }
1413
- /**
1414
- * Intercept render calls for debugging
1415
- */
1416
- setupRenderInterception() {
1417
- const originalRender = this.coherent.render;
1418
- this.coherent.render = (component, context = {}, options = {}) => {
1419
- const renderStart = performance.now();
1420
- const renderId = this.generateRenderId();
1421
- try {
1422
- this.preRenderAnalysis(component, context, renderId);
1423
- const result = originalRender.call(this.coherent, component, context, {
1424
- ...options,
1425
- _devRenderId: renderId
1426
- });
1427
- const renderTime = performance.now() - renderStart;
1428
- this.postRenderAnalysis(component, result, renderTime, renderId);
1429
- return result;
1430
- } catch (_error) {
1431
- this.handleRenderError(_error, component, context, renderId);
1432
- throw _error;
1572
+ const target = globalThis;
1573
+ const previous = Object.fromEntries(Object.keys(helpers).map((key) => [key, target[key]]));
1574
+ Object.assign(target, helpers);
1575
+ this._teardown.push(() => {
1576
+ for (const [key, value] of Object.entries(previous)) {
1577
+ if (value === void 0) delete target[key];
1578
+ else target[key] = value;
1433
1579
  }
1434
- };
1580
+ });
1435
1581
  }
1436
1582
  /**
1437
1583
  * Pre-render analysis and validation
@@ -1439,7 +1585,7 @@ var DevTools = class {
1439
1585
  preRenderAnalysis(component, context, renderId) {
1440
1586
  const validation = this.deepValidateComponent(component);
1441
1587
  if (!validation.isValid) {
1442
- this.warnings.push({
1588
+ this.record(this.warnings, {
1443
1589
  type: "validation",
1444
1590
  message: validation.message,
1445
1591
  component: this.serializeComponent(component),
@@ -1449,14 +1595,14 @@ var DevTools = class {
1449
1595
  }
1450
1596
  const complexity = this.analyzeComplexity(component);
1451
1597
  if (complexity > 1e3) {
1452
- this.warnings.push({
1598
+ this.record(this.warnings, {
1453
1599
  type: "performance",
1454
1600
  message: `High complexity component detected (${complexity} nodes)`,
1455
1601
  renderId,
1456
1602
  timestamp: Date.now()
1457
1603
  });
1458
1604
  }
1459
- this.analyzeContext(context, renderId);
1605
+ this.analyzeContext(context ?? {}, renderId);
1460
1606
  }
1461
1607
  /**
1462
1608
  * Post-render analysis
@@ -1467,7 +1613,7 @@ var DevTools = class {
1467
1613
  timestamp: Date.now(),
1468
1614
  component: this.serializeComponent(component),
1469
1615
  renderTime,
1470
- outputSize: result.length,
1616
+ outputSize: typeof result === "string" ? result.length : 0,
1471
1617
  complexity: this.analyzeComplexity(component)
1472
1618
  };
1473
1619
  this.renderHistory.push(renderRecord);
@@ -1475,7 +1621,7 @@ var DevTools = class {
1475
1621
  this.renderHistory.shift();
1476
1622
  }
1477
1623
  if (renderTime > 10) {
1478
- this.warnings.push({
1624
+ this.record(this.warnings, {
1479
1625
  type: "performance",
1480
1626
  message: `Slow render detected: ${renderTime.toFixed(2)}ms`,
1481
1627
  renderId,
@@ -1489,11 +1635,11 @@ var DevTools = class {
1489
1635
  /**
1490
1636
  * Deep component validation
1491
1637
  */
1492
- deepValidateComponent(component, path2 = "root", depth = 0) {
1638
+ deepValidateComponent(component, path = "root", depth = 0) {
1493
1639
  if (depth > 100) {
1494
1640
  return {
1495
1641
  isValid: false,
1496
- message: `Component nesting too deep at ${path2}`
1642
+ message: `Component nesting too deep at ${path}`
1497
1643
  };
1498
1644
  }
1499
1645
  try {
@@ -1501,14 +1647,14 @@ var DevTools = class {
1501
1647
  } catch (_error) {
1502
1648
  return {
1503
1649
  isValid: false,
1504
- message: `Invalid component at ${path2}: ${_error.message}`
1650
+ message: `Invalid component at ${path}: ${_error.message}`
1505
1651
  };
1506
1652
  }
1507
1653
  if (Array.isArray(component)) {
1508
1654
  for (let i = 0; i < component.length; i++) {
1509
1655
  const childValidation = this.deepValidateComponent(
1510
1656
  component[i],
1511
- `${path2}[${i}]`,
1657
+ `${path}[${i}]`,
1512
1658
  depth + 1
1513
1659
  );
1514
1660
  if (!childValidation.isValid) {
@@ -1520,7 +1666,7 @@ var DevTools = class {
1520
1666
  if (props && typeof props === "object" && props.children) {
1521
1667
  const childValidation = this.deepValidateComponent(
1522
1668
  props.children,
1523
- `${path2}.${tag}.children`,
1669
+ `${path}.${tag}.children`,
1524
1670
  depth + 1
1525
1671
  );
1526
1672
  if (!childValidation.isValid) {
@@ -1562,26 +1708,27 @@ var DevTools = class {
1562
1708
  * Context analysis
1563
1709
  */
1564
1710
  analyzeContext(context, renderId) {
1565
- const contextSize = JSON.stringify(context).length;
1566
- if (contextSize > 1e4) {
1567
- this.warnings.push({
1568
- type: "context",
1569
- message: `Large context object: ${contextSize} characters`,
1570
- renderId,
1571
- timestamp: Date.now()
1572
- });
1573
- }
1711
+ let contextSize;
1574
1712
  try {
1575
- JSON.stringify(context);
1713
+ contextSize = JSON.stringify(context)?.length ?? 0;
1576
1714
  } catch (_error) {
1577
- if (_error.message.includes("circular")) {
1578
- this.warnings.push({
1715
+ if (/circular/i.test(_error.message)) {
1716
+ this.record(this.warnings, {
1579
1717
  type: "context",
1580
1718
  message: "Circular reference detected in context",
1581
1719
  renderId,
1582
1720
  timestamp: Date.now()
1583
1721
  });
1584
1722
  }
1723
+ return;
1724
+ }
1725
+ if (contextSize > 1e4) {
1726
+ this.record(this.warnings, {
1727
+ type: "context",
1728
+ message: `Large context object: ${contextSize} characters`,
1729
+ renderId,
1730
+ timestamp: Date.now()
1731
+ });
1585
1732
  }
1586
1733
  }
1587
1734
  /**
@@ -1695,48 +1842,63 @@ ${indent}</${tag}>`;
1695
1842
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1696
1843
  }
1697
1844
  /**
1698
- * Setup _error handling
1845
+ * Record errors. console.error calls are captured (and still printed);
1846
+ * unhandled rejections only when `trackUnhandledRejections` is set, and
1847
+ * then they still crash the process exactly as they would without us.
1699
1848
  */
1700
1849
  setupErrorHandling() {
1701
- const originalConsoleError = console.error;
1702
- console.error = (...args) => {
1703
- this.errors.push({
1704
- type: "console",
1705
- message: args.join(" "),
1706
- timestamp: Date.now(),
1707
- stack: new Error().stack
1850
+ if (this.options.captureConsoleErrors) {
1851
+ const originalConsoleError = console.error;
1852
+ const hooked = (...args) => {
1853
+ this.record(this.errors, {
1854
+ type: "console",
1855
+ message: args.map((arg) => String(arg)).join(" "),
1856
+ timestamp: Date.now(),
1857
+ stack: new Error().stack
1858
+ });
1859
+ originalConsoleError.apply(console, args);
1860
+ };
1861
+ console.error = hooked;
1862
+ this._teardown.push(() => {
1863
+ if (console.error === hooked) console.error = originalConsoleError;
1708
1864
  });
1709
- originalConsoleError.apply(console, args);
1710
- };
1711
- if (typeof process !== "undefined") {
1712
- process.on("unhandledRejection", (reason, promise) => {
1713
- this.errors.push({
1865
+ }
1866
+ if (this.options.trackUnhandledRejections && typeof process !== "undefined" && typeof process.on === "function") {
1867
+ const listener = (reason) => {
1868
+ this.record(this.errors, {
1714
1869
  type: "unhandled-rejection",
1715
- message: reason.toString(),
1716
- promise: promise.toString(),
1870
+ message: reason instanceof Error ? reason.message : String(reason),
1871
+ stack: reason instanceof Error ? reason.stack : void 0,
1717
1872
  timestamp: Date.now()
1718
1873
  });
1719
- });
1874
+ if (process.listenerCount("unhandledRejection") === 1) {
1875
+ throw reason instanceof Error ? reason : new Error(`Unhandled promise rejection: ${String(reason)}`);
1876
+ }
1877
+ };
1878
+ process.on("unhandledRejection", listener);
1879
+ this._teardown.push(() => process.off("unhandledRejection", listener));
1720
1880
  }
1721
- if (typeof window !== "undefined") {
1722
- window.addEventListener("_error", (event) => {
1723
- this.errors.push({
1724
- type: "browser-_error",
1881
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
1882
+ const listener = (event) => {
1883
+ this.record(this.errors, {
1884
+ type: "browser-error",
1725
1885
  message: event.message,
1726
1886
  filename: event.filename,
1727
1887
  lineno: event.lineno,
1728
1888
  colno: event.colno,
1729
1889
  timestamp: Date.now()
1730
1890
  });
1731
- });
1891
+ };
1892
+ window.addEventListener("error", listener);
1893
+ this._teardown.push(() => window.removeEventListener("error", listener));
1732
1894
  }
1733
1895
  }
1734
1896
  /**
1735
1897
  * Handle render errors specifically
1736
1898
  */
1737
1899
  handleRenderError(_error, component, context, renderId) {
1738
- this.errors.push({
1739
- type: "render-_error",
1900
+ this.record(this.errors, {
1901
+ type: "render-error",
1740
1902
  message: _error.message,
1741
1903
  stack: _error.stack,
1742
1904
  component: this.serializeComponent(component),
@@ -1748,23 +1910,27 @@ ${indent}</${tag}>`;
1748
1910
  console.error("Component:", this.serializeComponent(component));
1749
1911
  }
1750
1912
  /**
1751
- * Setup hot reload capability
1913
+ * Connect to a hot-reload WebSocket, only when `hotReloadUrl` is set
1914
+ * (browser only).
1752
1915
  */
1753
1916
  setupHotReload() {
1754
- if (typeof window !== "undefined" && "WebSocket" in window) {
1755
- this.setupBrowserHotReload();
1756
- } else if (typeof __require !== "undefined") {
1757
- this.setupNodeHotReload();
1917
+ if (this.options.hotReloadUrl && typeof window !== "undefined" && "WebSocket" in window) {
1918
+ this.setupBrowserHotReload(this.options.hotReloadUrl);
1758
1919
  }
1759
1920
  }
1760
1921
  /**
1761
1922
  * Browser hot reload setup
1762
1923
  */
1763
- setupBrowserHotReload() {
1924
+ setupBrowserHotReload(url) {
1764
1925
  try {
1765
- const ws = new WebSocket("ws://localhost:3001/coherent-dev");
1926
+ const ws = new window.WebSocket(url);
1766
1927
  ws.onmessage = (event) => {
1767
- const data = JSON.parse(event.data);
1928
+ let data;
1929
+ try {
1930
+ data = JSON.parse(event.data);
1931
+ } catch {
1932
+ return;
1933
+ }
1768
1934
  if (data.type === "component-updated") {
1769
1935
  console.log("\u{1F504} Component updated:", data.componentName);
1770
1936
  this.handleComponentUpdate(data);
@@ -1780,24 +1946,7 @@ ${indent}</${tag}>`;
1780
1946
  console.log("\u{1F50C} Disconnected from dev server");
1781
1947
  this.hotReloadEnabled = false;
1782
1948
  };
1783
- } catch {
1784
- }
1785
- }
1786
- /**
1787
- * Node.js hot reload setup
1788
- */
1789
- setupNodeHotReload() {
1790
- try {
1791
- const fs = __require("fs");
1792
- const path2 = __require("path");
1793
- const watchDir = path2.join(process.cwd(), "src");
1794
- fs.watch(watchDir, { recursive: true }, (eventType, filename) => {
1795
- if (filename && filename.endsWith(".js")) {
1796
- console.log(`\u{1F504} File changed: ${filename}`);
1797
- this.handleFileChange(filename, eventType);
1798
- }
1799
- });
1800
- this.hotReloadEnabled = true;
1949
+ this._teardown.push(() => ws.close());
1801
1950
  } catch {
1802
1951
  }
1803
1952
  }
@@ -1805,7 +1954,7 @@ ${indent}</${tag}>`;
1805
1954
  * Handle component updates
1806
1955
  */
1807
1956
  handleComponentUpdate(updateData) {
1808
- if (this.coherent.cache) {
1957
+ if (this.coherent?.cache?.invalidatePattern) {
1809
1958
  this.coherent.cache.invalidatePattern(updateData.componentName);
1810
1959
  }
1811
1960
  this.componentRegistry.set(updateData.componentName, {
@@ -1816,22 +1965,12 @@ ${indent}</${tag}>`;
1816
1965
  window.location.reload();
1817
1966
  }
1818
1967
  }
1819
- /**
1820
- * Handle file changes
1821
- */
1822
- handleFileChange(filename, eventType) {
1823
- if (typeof __require !== "undefined" && __require.cache) {
1824
- const fullPath = __require.resolve(path.resolve(filename));
1825
- delete __require.cache[fullPath];
1826
- }
1827
- console.log(`\u{1F4DD} ${eventType}: ${filename}`);
1828
- }
1829
1968
  /**
1830
1969
  * Setup browser-specific dev tools
1831
1970
  */
1832
1971
  setupBrowserDevTools() {
1833
1972
  this.createDevPanel();
1834
- document.addEventListener("keydown", (e) => {
1973
+ const onKeyDown = (e) => {
1835
1974
  if (e.ctrlKey && e.shiftKey && e.code === "KeyC") {
1836
1975
  this.toggleDevPanel();
1837
1976
  e.preventDefault();
@@ -1840,7 +1979,9 @@ ${indent}</${tag}>`;
1840
1979
  console.table(this.getPerformanceInsights());
1841
1980
  e.preventDefault();
1842
1981
  }
1843
- });
1982
+ };
1983
+ document.addEventListener("keydown", onKeyDown);
1984
+ this._teardown.push(() => document.removeEventListener("keydown", onKeyDown));
1844
1985
  }
1845
1986
  /**
1846
1987
  * Create development panel in browser
@@ -1866,6 +2007,10 @@ ${indent}</${tag}>`;
1866
2007
  `;
1867
2008
  document.body.appendChild(panel);
1868
2009
  this.devPanel = panel;
2010
+ this._teardown.push(() => {
2011
+ panel.remove();
2012
+ this.devPanel = null;
2013
+ });
1869
2014
  this.updateDevPanel();
1870
2015
  }
1871
2016
  /**
@@ -1929,15 +2074,6 @@ ${indent}</${tag}>`;
1929
2074
  </div>
1930
2075
  `;
1931
2076
  }
1932
- /**
1933
- * Setup Node.js specific dev tools
1934
- */
1935
- setupNodeDevTools() {
1936
- process.on("SIGINT", () => {
1937
- this.printDevSummary();
1938
- process.exit();
1939
- });
1940
- }
1941
2077
  /**
1942
2078
  * Print development summary
1943
2079
  */
@@ -1972,7 +2108,7 @@ ${indent}</${tag}>`;
1972
2108
  insights.slowestRender = Math.max(...times);
1973
2109
  insights.fastestRender = Math.min(...times);
1974
2110
  }
1975
- if (this.coherent.cache && this.coherent.cache.getStats) {
2111
+ if (this.coherent?.cache?.getStats) {
1976
2112
  const cacheStats = this.coherent.cache.getStats();
1977
2113
  insights.cacheHits = cacheStats.hits;
1978
2114
  insights.cacheHitRate = cacheStats.hitRate;
@@ -2032,7 +2168,7 @@ ${indent}</${tag}>`;
2032
2168
  toggleFeature(feature) {
2033
2169
  switch (feature) {
2034
2170
  case "cache":
2035
- if (this.coherent.cache) {
2171
+ if (this.coherent?.cache) {
2036
2172
  this.coherent.cache.enabled = !this.coherent.cache.enabled;
2037
2173
  console.log(`Cache ${this.coherent.cache.enabled ? "enabled" : "disabled"}`);
2038
2174
  }
@@ -2054,23 +2190,9 @@ ${indent}</${tag}>`;
2054
2190
  validateComponent(component) {
2055
2191
  return this.deepValidateComponent(component);
2056
2192
  }
2057
- setupComponentInspector() {
2058
- const originalCreateComponent = this.coherent.createComponent;
2059
- if (originalCreateComponent) {
2060
- this.coherent.createComponent = (config) => {
2061
- const component = originalCreateComponent.call(this.coherent, config);
2062
- this.componentRegistry.set(config.name || "anonymous", {
2063
- config,
2064
- component,
2065
- registeredAt: Date.now()
2066
- });
2067
- return component;
2068
- };
2069
- }
2070
- }
2071
2193
  };
2072
- function createDevTools(coherentInstance) {
2073
- return new DevTools(coherentInstance);
2194
+ function createDevTools(coherentInstance, options) {
2195
+ return new DevTools(coherentInstance, options);
2074
2196
  }
2075
2197
 
2076
2198
  // src/component-visualizer.js
@@ -2579,8 +2701,8 @@ var PerformanceDashboard = class {
2579
2701
  * Update metrics from external sources
2580
2702
  */
2581
2703
  updateMetrics() {
2582
- const now = Date.now();
2583
- const uptime = now - this.startTime;
2704
+ const now2 = Date.now();
2705
+ const uptime = now2 - this.startTime;
2584
2706
  const apiRate = this.metrics.api.requests / (uptime / 1e3);
2585
2707
  const componentRate = this.metrics.components.renders / (uptime / 1e3);
2586
2708
  const fullStackRate = this.metrics.fullstack.totalRequests / (uptime / 1e3);
@@ -2914,10 +3036,10 @@ var EnhancedErrorHandler = class {
2914
3036
  /**
2915
3037
  * Get component context tree
2916
3038
  */
2917
- getComponentContext(component, path2 = []) {
3039
+ getComponentContext(component, path = []) {
2918
3040
  const context = {
2919
- path: path2.join("."),
2920
- depth: path2.length,
3041
+ path: path.join("."),
3042
+ depth: path.length,
2921
3043
  component: this.summarizeComponent(component),
2922
3044
  children: []
2923
3045
  };
@@ -2929,7 +3051,7 @@ var EnhancedErrorHandler = class {
2929
3051
  const children = Array.isArray(props.children) ? props.children : [props.children];
2930
3052
  children.forEach((child, index) => {
2931
3053
  if (child && typeof child === "object") {
2932
- const childContext = this.getComponentContext(child, [...path2, `${tagName}[${index}]`]);
3054
+ const childContext = this.getComponentContext(child, [...path, `${tagName}[${index}]`]);
2933
3055
  context.children.push(childContext);
2934
3056
  }
2935
3057
  });