@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/README.md +16 -12
- package/dist/index.js +317 -206
- package/dist/index.js.map +3 -3
- package/dist/profiler.js +107 -39
- package/dist/profiler.js.map +2 -2
- package/package.json +2 -2
- package/types/index.d.ts +74 -12
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,
|
|
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 ${
|
|
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, [...
|
|
110
|
+
obj.forEach((item, index) => checkCircular(item, [...path, `[${index}]`]));
|
|
118
111
|
} else {
|
|
119
112
|
Object.keys(obj).forEach((key) => {
|
|
120
|
-
checkCircular(obj[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
|
-
|
|
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:
|
|
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 (
|
|
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 =
|
|
463
|
+
session.endTime = now();
|
|
447
464
|
session.duration = session.endTime - session.startTime;
|
|
448
465
|
session.active = false;
|
|
449
|
-
|
|
450
|
-
|
|
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.
|
|
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:
|
|
488
|
+
startTime: now(),
|
|
473
489
|
startMemory: this.getMemoryUsage(),
|
|
474
490
|
phase: "render"
|
|
475
491
|
};
|
|
476
492
|
this.marks.set(measurementId, measurement);
|
|
477
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
1385
|
-
|
|
1470
|
+
if (this.options.globalHelpers) {
|
|
1471
|
+
this.setupGlobalHelpers();
|
|
1472
|
+
}
|
|
1386
1473
|
this.setupErrorHandling();
|
|
1387
1474
|
this.setupHotReload();
|
|
1388
|
-
|
|
1389
|
-
if (typeof window !== "undefined") {
|
|
1475
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
1390
1476
|
this.setupBrowserDevTools();
|
|
1391
1477
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 ${
|
|
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 ${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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
|
-
|
|
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
|
|
1589
|
-
this.warnings
|
|
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
|
-
*
|
|
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
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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.
|
|
1727
|
-
|
|
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
|
-
|
|
1734
|
-
this.errors
|
|
1735
|
-
type: "browser-
|
|
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
|
|
1750
|
-
type: "render-
|
|
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
|
-
*
|
|
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(
|
|
1926
|
+
const ws = new window.WebSocket(url);
|
|
1777
1927
|
ws.onmessage = (event) => {
|
|
1778
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2594
|
-
const uptime =
|
|
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,
|
|
3039
|
+
getComponentContext(component, path = []) {
|
|
2929
3040
|
const context = {
|
|
2930
|
-
path:
|
|
2931
|
-
depth:
|
|
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, [...
|
|
3054
|
+
const childContext = this.getComponentContext(child, [...path, `${tagName}[${index}]`]);
|
|
2944
3055
|
context.children.push(childContext);
|
|
2945
3056
|
}
|
|
2946
3057
|
});
|