@coherent.js/devtools 1.1.2 → 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;
@@ -877,7 +904,7 @@ function createProfiler(options = {}) {
877
904
  return new PerformanceProfiler(options);
878
905
  }
879
906
  async function measure(name, fn, profiler = null) {
880
- const prof = profiler || new PerformanceProfiler();
907
+ const prof = profiler || new PerformanceProfiler({ enabled: true });
881
908
  const sessionId = prof.start(name);
882
909
  try {
883
910
  const value = await fn();
@@ -885,14 +912,48 @@ async function measure(name, fn, profiler = null) {
885
912
  return { value, duration: result?.duration || 0 };
886
913
  } catch (error) {
887
914
  const result = prof.stop(sessionId);
888
- 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;
889
921
  }
890
922
  }
891
- function profile(fn) {
892
- return function(...args) {
893
- 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);
894
952
  return result;
895
- };
953
+ }
954
+ Object.defineProperty(profiled, "name", { value: name });
955
+ profiled.profiler = profiler;
956
+ return profiled;
896
957
  }
897
958
 
898
959
  // src/logger.js
@@ -1351,28 +1412,53 @@ function createConsoleLogger(prefix = "") {
1351
1412
 
1352
1413
  // src/dev-tools.js
1353
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"]);
1354
1416
  var DevTools = class {
1355
- 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 = {}) {
1356
1431
  this.coherent = coherentInstance;
1357
- 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();
1358
1441
  this.renderHistory = [];
1359
1442
  this.componentRegistry = /* @__PURE__ */ new Map();
1360
1443
  this.warnings = [];
1361
1444
  this.errors = [];
1362
1445
  this.hotReloadEnabled = false;
1446
+ this._teardown = [];
1363
1447
  if (this.isEnabled) {
1364
1448
  this.initialize();
1365
1449
  }
1366
1450
  }
1367
1451
  /**
1368
- * 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.
1369
1455
  */
1370
1456
  shouldEnable() {
1371
- if (typeof process !== "undefined") {
1457
+ if (typeof process !== "undefined" && process?.env) {
1372
1458
  return process.env.NODE_ENV === "development";
1373
1459
  }
1374
- if (typeof window !== "undefined") {
1375
- 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);
1376
1462
  }
1377
1463
  return false;
1378
1464
  }
@@ -1381,17 +1467,85 @@ var DevTools = class {
1381
1467
  */
1382
1468
  initialize() {
1383
1469
  console.log("\u{1F6E0}\uFE0F Coherent.js Dev Tools Enabled");
1384
- this.setupGlobalHelpers();
1385
- this.setupRenderInterception();
1470
+ if (this.options.globalHelpers) {
1471
+ this.setupGlobalHelpers();
1472
+ }
1386
1473
  this.setupErrorHandling();
1387
1474
  this.setupHotReload();
1388
- this.setupComponentInspector();
1389
- if (typeof window !== "undefined") {
1475
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
1390
1476
  this.setupBrowserDevTools();
1391
1477
  }
1392
- if (typeof process !== "undefined") {
1393
- 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;
1529
+ }
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
+ });
1394
1547
  }
1548
+ return component;
1395
1549
  }
1396
1550
  /**
1397
1551
  * Set up global helper functions
@@ -1415,34 +1569,15 @@ var DevTools = class {
1415
1569
  // Get warnings and errors
1416
1570
  $issues: () => ({ warnings: this.warnings, errors: this.errors })
1417
1571
  };
1418
- if (typeof window !== "undefined") {
1419
- Object.assign(window, helpers);
1420
- } else if (typeof global !== "undefined") {
1421
- Object.assign(global, helpers);
1422
- }
1423
- }
1424
- /**
1425
- * Intercept render calls for debugging
1426
- */
1427
- setupRenderInterception() {
1428
- const originalRender = this.coherent.render;
1429
- this.coherent.render = (component, context = {}, options = {}) => {
1430
- const renderStart = performance.now();
1431
- const renderId = this.generateRenderId();
1432
- try {
1433
- this.preRenderAnalysis(component, context, renderId);
1434
- const result = originalRender.call(this.coherent, component, context, {
1435
- ...options,
1436
- _devRenderId: renderId
1437
- });
1438
- const renderTime = performance.now() - renderStart;
1439
- this.postRenderAnalysis(component, result, renderTime, renderId);
1440
- return result;
1441
- } catch (_error) {
1442
- this.handleRenderError(_error, component, context, renderId);
1443
- 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;
1444
1579
  }
1445
- };
1580
+ });
1446
1581
  }
1447
1582
  /**
1448
1583
  * Pre-render analysis and validation
@@ -1450,7 +1585,7 @@ var DevTools = class {
1450
1585
  preRenderAnalysis(component, context, renderId) {
1451
1586
  const validation = this.deepValidateComponent(component);
1452
1587
  if (!validation.isValid) {
1453
- this.warnings.push({
1588
+ this.record(this.warnings, {
1454
1589
  type: "validation",
1455
1590
  message: validation.message,
1456
1591
  component: this.serializeComponent(component),
@@ -1460,14 +1595,14 @@ var DevTools = class {
1460
1595
  }
1461
1596
  const complexity = this.analyzeComplexity(component);
1462
1597
  if (complexity > 1e3) {
1463
- this.warnings.push({
1598
+ this.record(this.warnings, {
1464
1599
  type: "performance",
1465
1600
  message: `High complexity component detected (${complexity} nodes)`,
1466
1601
  renderId,
1467
1602
  timestamp: Date.now()
1468
1603
  });
1469
1604
  }
1470
- this.analyzeContext(context, renderId);
1605
+ this.analyzeContext(context ?? {}, renderId);
1471
1606
  }
1472
1607
  /**
1473
1608
  * Post-render analysis
@@ -1478,7 +1613,7 @@ var DevTools = class {
1478
1613
  timestamp: Date.now(),
1479
1614
  component: this.serializeComponent(component),
1480
1615
  renderTime,
1481
- outputSize: result.length,
1616
+ outputSize: typeof result === "string" ? result.length : 0,
1482
1617
  complexity: this.analyzeComplexity(component)
1483
1618
  };
1484
1619
  this.renderHistory.push(renderRecord);
@@ -1486,7 +1621,7 @@ var DevTools = class {
1486
1621
  this.renderHistory.shift();
1487
1622
  }
1488
1623
  if (renderTime > 10) {
1489
- this.warnings.push({
1624
+ this.record(this.warnings, {
1490
1625
  type: "performance",
1491
1626
  message: `Slow render detected: ${renderTime.toFixed(2)}ms`,
1492
1627
  renderId,
@@ -1500,11 +1635,11 @@ var DevTools = class {
1500
1635
  /**
1501
1636
  * Deep component validation
1502
1637
  */
1503
- deepValidateComponent(component, path2 = "root", depth = 0) {
1638
+ deepValidateComponent(component, path = "root", depth = 0) {
1504
1639
  if (depth > 100) {
1505
1640
  return {
1506
1641
  isValid: false,
1507
- message: `Component nesting too deep at ${path2}`
1642
+ message: `Component nesting too deep at ${path}`
1508
1643
  };
1509
1644
  }
1510
1645
  try {
@@ -1512,14 +1647,14 @@ var DevTools = class {
1512
1647
  } catch (_error) {
1513
1648
  return {
1514
1649
  isValid: false,
1515
- message: `Invalid component at ${path2}: ${_error.message}`
1650
+ message: `Invalid component at ${path}: ${_error.message}`
1516
1651
  };
1517
1652
  }
1518
1653
  if (Array.isArray(component)) {
1519
1654
  for (let i = 0; i < component.length; i++) {
1520
1655
  const childValidation = this.deepValidateComponent(
1521
1656
  component[i],
1522
- `${path2}[${i}]`,
1657
+ `${path}[${i}]`,
1523
1658
  depth + 1
1524
1659
  );
1525
1660
  if (!childValidation.isValid) {
@@ -1531,7 +1666,7 @@ var DevTools = class {
1531
1666
  if (props && typeof props === "object" && props.children) {
1532
1667
  const childValidation = this.deepValidateComponent(
1533
1668
  props.children,
1534
- `${path2}.${tag}.children`,
1669
+ `${path}.${tag}.children`,
1535
1670
  depth + 1
1536
1671
  );
1537
1672
  if (!childValidation.isValid) {
@@ -1573,26 +1708,27 @@ var DevTools = class {
1573
1708
  * Context analysis
1574
1709
  */
1575
1710
  analyzeContext(context, renderId) {
1576
- const contextSize = JSON.stringify(context).length;
1577
- if (contextSize > 1e4) {
1578
- this.warnings.push({
1579
- type: "context",
1580
- message: `Large context object: ${contextSize} characters`,
1581
- renderId,
1582
- timestamp: Date.now()
1583
- });
1584
- }
1711
+ let contextSize;
1585
1712
  try {
1586
- JSON.stringify(context);
1713
+ contextSize = JSON.stringify(context)?.length ?? 0;
1587
1714
  } catch (_error) {
1588
- if (_error.message.includes("circular")) {
1589
- this.warnings.push({
1715
+ if (/circular/i.test(_error.message)) {
1716
+ this.record(this.warnings, {
1590
1717
  type: "context",
1591
1718
  message: "Circular reference detected in context",
1592
1719
  renderId,
1593
1720
  timestamp: Date.now()
1594
1721
  });
1595
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
+ });
1596
1732
  }
1597
1733
  }
1598
1734
  /**
@@ -1706,48 +1842,63 @@ ${indent}</${tag}>`;
1706
1842
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1707
1843
  }
1708
1844
  /**
1709
- * 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.
1710
1848
  */
1711
1849
  setupErrorHandling() {
1712
- const originalConsoleError = console.error;
1713
- console.error = (...args) => {
1714
- this.errors.push({
1715
- type: "console",
1716
- message: args.join(" "),
1717
- timestamp: Date.now(),
1718
- 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;
1719
1864
  });
1720
- originalConsoleError.apply(console, args);
1721
- };
1722
- if (typeof process !== "undefined") {
1723
- process.on("unhandledRejection", (reason, promise) => {
1724
- 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, {
1725
1869
  type: "unhandled-rejection",
1726
- message: reason.toString(),
1727
- promise: promise.toString(),
1870
+ message: reason instanceof Error ? reason.message : String(reason),
1871
+ stack: reason instanceof Error ? reason.stack : void 0,
1728
1872
  timestamp: Date.now()
1729
1873
  });
1730
- });
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));
1731
1880
  }
1732
- if (typeof window !== "undefined") {
1733
- window.addEventListener("_error", (event) => {
1734
- this.errors.push({
1735
- 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",
1736
1885
  message: event.message,
1737
1886
  filename: event.filename,
1738
1887
  lineno: event.lineno,
1739
1888
  colno: event.colno,
1740
1889
  timestamp: Date.now()
1741
1890
  });
1742
- });
1891
+ };
1892
+ window.addEventListener("error", listener);
1893
+ this._teardown.push(() => window.removeEventListener("error", listener));
1743
1894
  }
1744
1895
  }
1745
1896
  /**
1746
1897
  * Handle render errors specifically
1747
1898
  */
1748
1899
  handleRenderError(_error, component, context, renderId) {
1749
- this.errors.push({
1750
- type: "render-_error",
1900
+ this.record(this.errors, {
1901
+ type: "render-error",
1751
1902
  message: _error.message,
1752
1903
  stack: _error.stack,
1753
1904
  component: this.serializeComponent(component),
@@ -1759,23 +1910,27 @@ ${indent}</${tag}>`;
1759
1910
  console.error("Component:", this.serializeComponent(component));
1760
1911
  }
1761
1912
  /**
1762
- * Setup hot reload capability
1913
+ * Connect to a hot-reload WebSocket, only when `hotReloadUrl` is set
1914
+ * (browser only).
1763
1915
  */
1764
1916
  setupHotReload() {
1765
- if (typeof window !== "undefined" && "WebSocket" in window) {
1766
- this.setupBrowserHotReload();
1767
- } else if (typeof __require !== "undefined") {
1768
- this.setupNodeHotReload();
1917
+ if (this.options.hotReloadUrl && typeof window !== "undefined" && "WebSocket" in window) {
1918
+ this.setupBrowserHotReload(this.options.hotReloadUrl);
1769
1919
  }
1770
1920
  }
1771
1921
  /**
1772
1922
  * Browser hot reload setup
1773
1923
  */
1774
- setupBrowserHotReload() {
1924
+ setupBrowserHotReload(url) {
1775
1925
  try {
1776
- const ws = new WebSocket("ws://localhost:3001/coherent-dev");
1926
+ const ws = new window.WebSocket(url);
1777
1927
  ws.onmessage = (event) => {
1778
- const data = JSON.parse(event.data);
1928
+ let data;
1929
+ try {
1930
+ data = JSON.parse(event.data);
1931
+ } catch {
1932
+ return;
1933
+ }
1779
1934
  if (data.type === "component-updated") {
1780
1935
  console.log("\u{1F504} Component updated:", data.componentName);
1781
1936
  this.handleComponentUpdate(data);
@@ -1791,24 +1946,7 @@ ${indent}</${tag}>`;
1791
1946
  console.log("\u{1F50C} Disconnected from dev server");
1792
1947
  this.hotReloadEnabled = false;
1793
1948
  };
1794
- } catch {
1795
- }
1796
- }
1797
- /**
1798
- * Node.js hot reload setup
1799
- */
1800
- setupNodeHotReload() {
1801
- try {
1802
- const fs = __require("fs");
1803
- const path2 = __require("path");
1804
- const watchDir = path2.join(process.cwd(), "src");
1805
- fs.watch(watchDir, { recursive: true }, (eventType, filename) => {
1806
- if (filename && filename.endsWith(".js")) {
1807
- console.log(`\u{1F504} File changed: ${filename}`);
1808
- this.handleFileChange(filename, eventType);
1809
- }
1810
- });
1811
- this.hotReloadEnabled = true;
1949
+ this._teardown.push(() => ws.close());
1812
1950
  } catch {
1813
1951
  }
1814
1952
  }
@@ -1816,7 +1954,7 @@ ${indent}</${tag}>`;
1816
1954
  * Handle component updates
1817
1955
  */
1818
1956
  handleComponentUpdate(updateData) {
1819
- if (this.coherent.cache) {
1957
+ if (this.coherent?.cache?.invalidatePattern) {
1820
1958
  this.coherent.cache.invalidatePattern(updateData.componentName);
1821
1959
  }
1822
1960
  this.componentRegistry.set(updateData.componentName, {
@@ -1827,22 +1965,12 @@ ${indent}</${tag}>`;
1827
1965
  window.location.reload();
1828
1966
  }
1829
1967
  }
1830
- /**
1831
- * Handle file changes
1832
- */
1833
- handleFileChange(filename, eventType) {
1834
- if (typeof __require !== "undefined" && __require.cache) {
1835
- const fullPath = __require.resolve(path.resolve(filename));
1836
- delete __require.cache[fullPath];
1837
- }
1838
- console.log(`\u{1F4DD} ${eventType}: ${filename}`);
1839
- }
1840
1968
  /**
1841
1969
  * Setup browser-specific dev tools
1842
1970
  */
1843
1971
  setupBrowserDevTools() {
1844
1972
  this.createDevPanel();
1845
- document.addEventListener("keydown", (e) => {
1973
+ const onKeyDown = (e) => {
1846
1974
  if (e.ctrlKey && e.shiftKey && e.code === "KeyC") {
1847
1975
  this.toggleDevPanel();
1848
1976
  e.preventDefault();
@@ -1851,7 +1979,9 @@ ${indent}</${tag}>`;
1851
1979
  console.table(this.getPerformanceInsights());
1852
1980
  e.preventDefault();
1853
1981
  }
1854
- });
1982
+ };
1983
+ document.addEventListener("keydown", onKeyDown);
1984
+ this._teardown.push(() => document.removeEventListener("keydown", onKeyDown));
1855
1985
  }
1856
1986
  /**
1857
1987
  * Create development panel in browser
@@ -1877,6 +2007,10 @@ ${indent}</${tag}>`;
1877
2007
  `;
1878
2008
  document.body.appendChild(panel);
1879
2009
  this.devPanel = panel;
2010
+ this._teardown.push(() => {
2011
+ panel.remove();
2012
+ this.devPanel = null;
2013
+ });
1880
2014
  this.updateDevPanel();
1881
2015
  }
1882
2016
  /**
@@ -1940,15 +2074,6 @@ ${indent}</${tag}>`;
1940
2074
  </div>
1941
2075
  `;
1942
2076
  }
1943
- /**
1944
- * Setup Node.js specific dev tools
1945
- */
1946
- setupNodeDevTools() {
1947
- process.on("SIGINT", () => {
1948
- this.printDevSummary();
1949
- process.exit();
1950
- });
1951
- }
1952
2077
  /**
1953
2078
  * Print development summary
1954
2079
  */
@@ -1983,7 +2108,7 @@ ${indent}</${tag}>`;
1983
2108
  insights.slowestRender = Math.max(...times);
1984
2109
  insights.fastestRender = Math.min(...times);
1985
2110
  }
1986
- if (this.coherent.cache && this.coherent.cache.getStats) {
2111
+ if (this.coherent?.cache?.getStats) {
1987
2112
  const cacheStats = this.coherent.cache.getStats();
1988
2113
  insights.cacheHits = cacheStats.hits;
1989
2114
  insights.cacheHitRate = cacheStats.hitRate;
@@ -2043,7 +2168,7 @@ ${indent}</${tag}>`;
2043
2168
  toggleFeature(feature) {
2044
2169
  switch (feature) {
2045
2170
  case "cache":
2046
- if (this.coherent.cache) {
2171
+ if (this.coherent?.cache) {
2047
2172
  this.coherent.cache.enabled = !this.coherent.cache.enabled;
2048
2173
  console.log(`Cache ${this.coherent.cache.enabled ? "enabled" : "disabled"}`);
2049
2174
  }
@@ -2065,23 +2190,9 @@ ${indent}</${tag}>`;
2065
2190
  validateComponent(component) {
2066
2191
  return this.deepValidateComponent(component);
2067
2192
  }
2068
- setupComponentInspector() {
2069
- const originalCreateComponent = this.coherent.createComponent;
2070
- if (originalCreateComponent) {
2071
- this.coherent.createComponent = (config) => {
2072
- const component = originalCreateComponent.call(this.coherent, config);
2073
- this.componentRegistry.set(config.name || "anonymous", {
2074
- config,
2075
- component,
2076
- registeredAt: Date.now()
2077
- });
2078
- return component;
2079
- };
2080
- }
2081
- }
2082
2193
  };
2083
- function createDevTools(coherentInstance) {
2084
- return new DevTools(coherentInstance);
2194
+ function createDevTools(coherentInstance, options) {
2195
+ return new DevTools(coherentInstance, options);
2085
2196
  }
2086
2197
 
2087
2198
  // src/component-visualizer.js
@@ -2590,8 +2701,8 @@ var PerformanceDashboard = class {
2590
2701
  * Update metrics from external sources
2591
2702
  */
2592
2703
  updateMetrics() {
2593
- const now = Date.now();
2594
- const uptime = now - this.startTime;
2704
+ const now2 = Date.now();
2705
+ const uptime = now2 - this.startTime;
2595
2706
  const apiRate = this.metrics.api.requests / (uptime / 1e3);
2596
2707
  const componentRate = this.metrics.components.renders / (uptime / 1e3);
2597
2708
  const fullStackRate = this.metrics.fullstack.totalRequests / (uptime / 1e3);
@@ -2925,10 +3036,10 @@ var EnhancedErrorHandler = class {
2925
3036
  /**
2926
3037
  * Get component context tree
2927
3038
  */
2928
- getComponentContext(component, path2 = []) {
3039
+ getComponentContext(component, path = []) {
2929
3040
  const context = {
2930
- path: path2.join("."),
2931
- depth: path2.length,
3041
+ path: path.join("."),
3042
+ depth: path.length,
2932
3043
  component: this.summarizeComponent(component),
2933
3044
  children: []
2934
3045
  };
@@ -2940,7 +3051,7 @@ var EnhancedErrorHandler = class {
2940
3051
  const children = Array.isArray(props.children) ? props.children : [props.children];
2941
3052
  children.forEach((child, index) => {
2942
3053
  if (child && typeof child === "object") {
2943
- const childContext = this.getComponentContext(child, [...path2, `${tagName}[${index}]`]);
3054
+ const childContext = this.getComponentContext(child, [...path, `${tagName}[${index}]`]);
2944
3055
  context.children.push(childContext);
2945
3056
  }
2946
3057
  });