@coherent.js/client 1.1.1 → 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.
@@ -0,0 +1,1550 @@
1
+ // src/hmr/cleanup-tracker.js
2
+ function mergeSignals(controller, callerSignal) {
3
+ if (!callerSignal) {
4
+ return controller.signal;
5
+ }
6
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.any === "function") {
7
+ return AbortSignal.any([controller.signal, callerSignal]);
8
+ }
9
+ if (callerSignal.aborted) {
10
+ controller.abort(callerSignal.reason);
11
+ } else {
12
+ callerSignal.addEventListener("abort", () => controller.abort(callerSignal.reason), { once: true });
13
+ }
14
+ return controller.signal;
15
+ }
16
+ var CleanupTracker = class {
17
+ constructor() {
18
+ this.moduleResources = /* @__PURE__ */ new Map();
19
+ }
20
+ /**
21
+ * Create a tracked context for a module
22
+ *
23
+ * Returns an object with tracked versions of setTimeout, setInterval,
24
+ * addEventListener, and fetch that automatically clean up on module disposal.
25
+ *
26
+ * @param {string} moduleId - Unique identifier for the module
27
+ * @returns {Object} Tracked context with setTimeout, setInterval, etc.
28
+ */
29
+ createContext(moduleId) {
30
+ const resources = {
31
+ timers: /* @__PURE__ */ new Set(),
32
+ intervals: /* @__PURE__ */ new Set(),
33
+ listeners: [],
34
+ abortControllers: /* @__PURE__ */ new Set()
35
+ };
36
+ this.moduleResources.set(moduleId, resources);
37
+ const context = {
38
+ /**
39
+ * Tracked setTimeout - auto-removes from tracking on completion
40
+ * @param {Function} callback - Function to execute
41
+ * @param {number} delay - Delay in milliseconds
42
+ * @param {...*} args - Additional arguments to pass to callback
43
+ * @returns {number} Timer ID
44
+ */
45
+ setTimeout: (callback, delay, ...args) => {
46
+ const id = setTimeout(
47
+ (...a) => {
48
+ resources.timers.delete(id);
49
+ callback(...a);
50
+ },
51
+ delay,
52
+ ...args
53
+ );
54
+ resources.timers.add(id);
55
+ return id;
56
+ },
57
+ /**
58
+ * Tracked setInterval - stores in intervals set until cleared
59
+ * @param {Function} callback - Function to execute
60
+ * @param {number} delay - Interval in milliseconds
61
+ * @param {...*} args - Additional arguments to pass to callback
62
+ * @returns {number} Interval ID
63
+ */
64
+ setInterval: (callback, delay, ...args) => {
65
+ const id = setInterval(callback, delay, ...args);
66
+ resources.intervals.add(id);
67
+ return id;
68
+ },
69
+ /**
70
+ * Clear a tracked timeout
71
+ * @param {number} id - Timer ID to clear
72
+ */
73
+ clearTimeout: (id) => {
74
+ resources.timers.delete(id);
75
+ clearTimeout(id);
76
+ },
77
+ /**
78
+ * Clear a tracked interval
79
+ * @param {number} id - Interval ID to clear
80
+ */
81
+ clearInterval: (id) => {
82
+ resources.intervals.delete(id);
83
+ clearInterval(id);
84
+ },
85
+ /**
86
+ * Tracked addEventListener - stores listener info for removal on cleanup
87
+ * @param {EventTarget} target - Element or object to attach listener to
88
+ * @param {string} event - Event type
89
+ * @param {Function} handler - Event handler function
90
+ * @param {Object|boolean} [options] - Listener options
91
+ */
92
+ addEventListener: (target, event, handler, options) => {
93
+ target.addEventListener(event, handler, options);
94
+ resources.listeners.push({ target, event, handler, options });
95
+ },
96
+ /**
97
+ * Create a tracked AbortController
98
+ * @returns {AbortController} Tracked AbortController
99
+ */
100
+ createAbortController: () => {
101
+ const controller = new AbortController();
102
+ resources.abortControllers.add(controller);
103
+ return controller;
104
+ },
105
+ /**
106
+ * Tracked fetch - creates AbortController automatically, cleans up on completion
107
+ * @param {string|URL} url - URL to fetch
108
+ * @param {Object} [options] - Fetch options
109
+ * @returns {Promise<Response>} Fetch promise
110
+ */
111
+ fetch: (url, options = {}) => {
112
+ const controller = new AbortController();
113
+ resources.abortControllers.add(controller);
114
+ const signal = mergeSignals(controller, options.signal);
115
+ return fetch(url, { ...options, signal }).finally(() => {
116
+ resources.abortControllers.delete(controller);
117
+ });
118
+ }
119
+ };
120
+ return context;
121
+ }
122
+ /**
123
+ * Cleanup all resources for a module
124
+ *
125
+ * Called during HMR module disposal. Clears all timers, intervals,
126
+ * removes all event listeners, and aborts all pending fetch requests.
127
+ *
128
+ * @param {string} moduleId - Module identifier to clean up
129
+ */
130
+ cleanup(moduleId) {
131
+ const resources = this.moduleResources.get(moduleId);
132
+ if (!resources) return;
133
+ for (const id of resources.timers) {
134
+ clearTimeout(id);
135
+ }
136
+ resources.timers.clear();
137
+ for (const id of resources.intervals) {
138
+ clearInterval(id);
139
+ }
140
+ resources.intervals.clear();
141
+ for (const { target, event, handler, options } of resources.listeners) {
142
+ try {
143
+ target.removeEventListener(event, handler, options);
144
+ } catch {
145
+ }
146
+ }
147
+ resources.listeners.length = 0;
148
+ for (const controller of resources.abortControllers) {
149
+ try {
150
+ controller.abort();
151
+ } catch {
152
+ }
153
+ }
154
+ resources.abortControllers.clear();
155
+ this.moduleResources.delete(moduleId);
156
+ }
157
+ /**
158
+ * Check for potential resource leaks (for development mode)
159
+ *
160
+ * Logs warnings if resources weren't cleaned up before module disposal.
161
+ * Call this before cleanup() to detect potential leaks.
162
+ *
163
+ * @param {string} moduleId - Module identifier to check
164
+ */
165
+ checkForLeaks(moduleId) {
166
+ const resources = this.moduleResources.get(moduleId);
167
+ if (!resources) return;
168
+ const warnings = [];
169
+ if (resources.timers.size > 0) {
170
+ warnings.push(`${resources.timers.size} timer(s) not cleaned up`);
171
+ }
172
+ if (resources.intervals.size > 0) {
173
+ warnings.push(`${resources.intervals.size} interval(s) not cleaned up`);
174
+ }
175
+ if (resources.listeners.length > 0) {
176
+ warnings.push(`${resources.listeners.length} listener(s) not cleaned up`);
177
+ }
178
+ if (resources.abortControllers.size > 0) {
179
+ warnings.push(
180
+ `${resources.abortControllers.size} pending fetch(es) not aborted`
181
+ );
182
+ }
183
+ if (warnings.length > 0) {
184
+ console.warn(`[HMR] Potential leak in module ${moduleId}: ${warnings.join(", ")}`);
185
+ }
186
+ }
187
+ /**
188
+ * Check if a module has tracked resources
189
+ * @param {string} moduleId - Module identifier
190
+ * @returns {boolean} True if module has resources
191
+ */
192
+ hasResources(moduleId) {
193
+ return this.moduleResources.has(moduleId);
194
+ }
195
+ /**
196
+ * Get resource counts for a module (for testing/debugging)
197
+ * @param {string} moduleId - Module identifier
198
+ * @returns {Object|null} Resource counts or null if module not tracked
199
+ */
200
+ getResourceCounts(moduleId) {
201
+ const resources = this.moduleResources.get(moduleId);
202
+ if (!resources) return null;
203
+ return {
204
+ timers: resources.timers.size,
205
+ intervals: resources.intervals.size,
206
+ listeners: resources.listeners.length,
207
+ abortControllers: resources.abortControllers.size
208
+ };
209
+ }
210
+ };
211
+ var cleanupTracker = new CleanupTracker();
212
+
213
+ // src/hmr/state-capturer.js
214
+ var StateCapturer = class {
215
+ constructor() {
216
+ this.capturedInputs = /* @__PURE__ */ new Map();
217
+ this.scrollPositions = /* @__PURE__ */ new Map();
218
+ this.layoutSnapshot = null;
219
+ }
220
+ /**
221
+ * Generate a stable key for an input element
222
+ *
223
+ * Uses multiple factors to identify inputs across HMR updates:
224
+ * 1. ID (most stable)
225
+ * 2. Name + type (+ value for radios and checkboxes)
226
+ * 3. Form context
227
+ * 4. DOM path (fallback)
228
+ *
229
+ * @param {HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement} input - Input element
230
+ * @returns {string} Stable key for the input
231
+ */
232
+ getInputKey(input) {
233
+ const parts = [];
234
+ if (input.id) {
235
+ parts.push(`#${input.id}`);
236
+ return parts.join(":");
237
+ }
238
+ if (input.name) {
239
+ parts.push(`[name="${input.name}"]`);
240
+ }
241
+ if (input.type) {
242
+ parts.push(`[type="${input.type}"]`);
243
+ }
244
+ if (input.name && (input.type === "radio" || input.type === "checkbox")) {
245
+ parts.push(`[value="${input.value}"]`);
246
+ }
247
+ if (input.form?.id) {
248
+ parts.push(`form#${input.form.id}`);
249
+ }
250
+ if (parts.length === 0) {
251
+ parts.push(this.getElementPath(input));
252
+ }
253
+ return parts.join(":");
254
+ }
255
+ /**
256
+ * Build a CSS-like path for an element
257
+ *
258
+ * @param {HTMLElement} element - Element to build path for
259
+ * @returns {string} CSS-like path (e.g., "form > div:nth-of-type(2) > input")
260
+ */
261
+ getElementPath(element) {
262
+ const path = [];
263
+ let current = element;
264
+ while (current && current !== document.body && path.length < 10) {
265
+ let selector = current.tagName.toLowerCase();
266
+ if (current.className && typeof current.className === "string") {
267
+ const classes = current.className.trim().split(/\s+/).slice(0, 2);
268
+ if (classes.length > 0 && classes[0]) {
269
+ selector += `.${classes.join(".")}`;
270
+ }
271
+ }
272
+ if (current.parentElement) {
273
+ const siblings = current.parentElement.querySelectorAll(
274
+ `:scope > ${current.tagName.toLowerCase()}`
275
+ );
276
+ if (siblings.length > 1) {
277
+ const index = Array.from(siblings).indexOf(current);
278
+ selector += `:nth-of-type(${index + 1})`;
279
+ }
280
+ }
281
+ path.unshift(selector);
282
+ current = current.parentElement;
283
+ }
284
+ return path.join(" > ");
285
+ }
286
+ /**
287
+ * Capture all form input states
288
+ *
289
+ * Iterates through all input, textarea, and select elements,
290
+ * capturing their values, selection state, and checked state.
291
+ *
292
+ * @returns {Map<string, Object>} Map of input keys to their captured state
293
+ */
294
+ captureFormState() {
295
+ this.capturedInputs.clear();
296
+ const inputs = document.querySelectorAll("input, textarea, select");
297
+ for (const input of inputs) {
298
+ const key = this.getInputKey(input);
299
+ const state = {
300
+ value: input.value,
301
+ type: input.type || input.tagName.toLowerCase()
302
+ };
303
+ if (typeof input.selectionStart === "number" && (input.type === "text" || input.type === "search" || input.type === "url" || input.type === "tel" || input.type === "password" || input.tagName.toLowerCase() === "textarea")) {
304
+ state.selectionStart = input.selectionStart;
305
+ state.selectionEnd = input.selectionEnd;
306
+ }
307
+ if (input.type === "checkbox" || input.type === "radio") {
308
+ state.checked = input.checked;
309
+ }
310
+ this.capturedInputs.set(key, state);
311
+ }
312
+ return this.capturedInputs;
313
+ }
314
+ /**
315
+ * Restore form input states after HMR update
316
+ *
317
+ * Finds inputs by their captured keys and restores their values,
318
+ * only if the input type matches (to avoid corrupting data).
319
+ */
320
+ restoreFormState() {
321
+ for (const [key, state] of this.capturedInputs) {
322
+ const inputs = this.findInputsByKey(key);
323
+ for (const input of inputs) {
324
+ const currentType = input.type || input.tagName.toLowerCase();
325
+ if (currentType !== state.type) {
326
+ continue;
327
+ }
328
+ if (state.checked !== void 0) {
329
+ input.checked = state.checked;
330
+ continue;
331
+ }
332
+ input.value = state.value;
333
+ if (state.selectionStart !== void 0 && document.activeElement !== input) {
334
+ try {
335
+ input.setSelectionRange(state.selectionStart, state.selectionEnd);
336
+ } catch {
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+ /**
343
+ * Find inputs matching a captured key
344
+ *
345
+ * @param {string} key - Captured input key
346
+ * @returns {HTMLElement[]} Array of matching input elements
347
+ */
348
+ findInputsByKey(key) {
349
+ if (key.startsWith("#")) {
350
+ const id = key.slice(1);
351
+ const el = document.getElementById(id);
352
+ return el ? [el] : [];
353
+ }
354
+ const nameMatch = key.match(/\[name="([^"]+)"\]/);
355
+ if (nameMatch) {
356
+ const name = nameMatch[1];
357
+ const typeMatch = key.match(/\[type="([^"]+)"\]/);
358
+ const type = typeMatch ? typeMatch[1] : null;
359
+ let selector = `[name="${name}"]`;
360
+ if (type) {
361
+ selector += `[type="${type}"]`;
362
+ }
363
+ const inputs = Array.from(document.querySelectorAll(selector));
364
+ const valueMatch = key.match(/\[value="([^"]*)"\]/);
365
+ return valueMatch ? inputs.filter((input) => input.value === valueMatch[1]) : inputs;
366
+ }
367
+ try {
368
+ const el = document.querySelector(key);
369
+ return el ? [el] : [];
370
+ } catch {
371
+ return [];
372
+ }
373
+ }
374
+ /**
375
+ * Capture scroll positions for window and scrollable containers
376
+ *
377
+ * Captures scroll positions for:
378
+ * - Window (scrollX/scrollY)
379
+ * - Elements with [data-coherent-scroll-preserve] attribute
380
+ * - Elements with overflow that have actual scrolling
381
+ */
382
+ captureScrollPositions() {
383
+ this.scrollPositions.clear();
384
+ this.scrollPositions.set("window", {
385
+ top: window.scrollY,
386
+ left: window.scrollX
387
+ });
388
+ const markedScrollables = document.querySelectorAll(
389
+ "[data-coherent-scroll-preserve]"
390
+ );
391
+ for (const el of markedScrollables) {
392
+ const key = this.getScrollableKey(el);
393
+ this.scrollPositions.set(key, {
394
+ top: el.scrollTop,
395
+ left: el.scrollLeft
396
+ });
397
+ }
398
+ const overflowElements = document.querySelectorAll(
399
+ '[style*="overflow"], [class]'
400
+ );
401
+ for (const el of overflowElements) {
402
+ const style = window.getComputedStyle(el);
403
+ const hasOverflow = style.overflow === "auto" || style.overflow === "scroll" || style.overflowY === "auto" || style.overflowY === "scroll" || style.overflowX === "auto" || style.overflowX === "scroll";
404
+ if (hasOverflow && (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)) {
405
+ const key = this.getScrollableKey(el);
406
+ if (!this.scrollPositions.has(key)) {
407
+ this.scrollPositions.set(key, {
408
+ top: el.scrollTop,
409
+ left: el.scrollLeft
410
+ });
411
+ }
412
+ }
413
+ }
414
+ return this.scrollPositions;
415
+ }
416
+ /**
417
+ * Generate a stable key for a scrollable element
418
+ *
419
+ * @param {HTMLElement} el - Scrollable element
420
+ * @returns {string} Key for the element
421
+ */
422
+ getScrollableKey(el) {
423
+ if (el.id) {
424
+ return `#${el.id}`;
425
+ }
426
+ const component = el.getAttribute("data-coherent-component");
427
+ if (component) {
428
+ return `[data-coherent-component="${component}"]`;
429
+ }
430
+ return this.getElementPath(el);
431
+ }
432
+ /**
433
+ * Capture layout snapshot for change detection
434
+ *
435
+ * Captures body dimensions and positions of anchor elements
436
+ * (elements with data-coherent-component attribute).
437
+ */
438
+ captureLayout() {
439
+ this.layoutSnapshot = {
440
+ bodyHeight: document.body.scrollHeight,
441
+ bodyWidth: document.body.scrollWidth,
442
+ anchors: /* @__PURE__ */ new Map()
443
+ };
444
+ const components = document.querySelectorAll("[data-coherent-component]");
445
+ for (const el of components) {
446
+ const rect = el.getBoundingClientRect();
447
+ const key = this.getScrollableKey(el);
448
+ this.layoutSnapshot.anchors.set(key, {
449
+ top: rect.top,
450
+ left: rect.left,
451
+ width: rect.width,
452
+ height: rect.height
453
+ });
454
+ }
455
+ }
456
+ /**
457
+ * Check if layout changed significantly (>50px shift)
458
+ *
459
+ * Returns true if:
460
+ * - Body dimensions changed by more than 50px
461
+ * - Any anchor element position shifted by more than 50px
462
+ *
463
+ * @returns {boolean} True if layout changed significantly
464
+ */
465
+ layoutChangedSignificantly() {
466
+ if (!this.layoutSnapshot) {
467
+ return false;
468
+ }
469
+ const THRESHOLD = 50;
470
+ const heightDiff = Math.abs(
471
+ document.body.scrollHeight - this.layoutSnapshot.bodyHeight
472
+ );
473
+ const widthDiff = Math.abs(
474
+ document.body.scrollWidth - this.layoutSnapshot.bodyWidth
475
+ );
476
+ if (heightDiff > THRESHOLD || widthDiff > THRESHOLD) {
477
+ return true;
478
+ }
479
+ for (const [key, oldRect] of this.layoutSnapshot.anchors) {
480
+ const el = this.findElementByKey(key);
481
+ if (!el) {
482
+ continue;
483
+ }
484
+ const newRect = el.getBoundingClientRect();
485
+ const topDiff = Math.abs(newRect.top - oldRect.top);
486
+ const leftDiff = Math.abs(newRect.left - oldRect.left);
487
+ if (topDiff > THRESHOLD || leftDiff > THRESHOLD) {
488
+ return true;
489
+ }
490
+ }
491
+ return false;
492
+ }
493
+ /**
494
+ * Find an element by its scrollable key
495
+ *
496
+ * @param {string} key - Element key
497
+ * @returns {HTMLElement|null} Found element or null
498
+ */
499
+ findElementByKey(key) {
500
+ if (key === "window") {
501
+ return null;
502
+ }
503
+ if (key.startsWith("#")) {
504
+ return document.getElementById(key.slice(1));
505
+ }
506
+ try {
507
+ return document.querySelector(key);
508
+ } catch {
509
+ return null;
510
+ }
511
+ }
512
+ /**
513
+ * Restore scroll positions if layout hasn't changed significantly
514
+ *
515
+ * Logs a message if scroll restoration is skipped due to layout changes.
516
+ */
517
+ restoreScrollPositions() {
518
+ if (this.layoutChangedSignificantly()) {
519
+ console.log("[HMR] Layout changed significantly, not restoring scroll");
520
+ return;
521
+ }
522
+ const windowPos = this.scrollPositions.get("window");
523
+ if (windowPos) {
524
+ window.scrollTo(windowPos.left, windowPos.top);
525
+ }
526
+ for (const [key, pos] of this.scrollPositions) {
527
+ if (key === "window") {
528
+ continue;
529
+ }
530
+ const el = this.findElementByKey(key);
531
+ if (el) {
532
+ el.scrollTop = pos.top;
533
+ el.scrollLeft = pos.left;
534
+ }
535
+ }
536
+ }
537
+ /**
538
+ * Capture all state (form + scroll + layout)
539
+ *
540
+ * Convenience method that calls all capture methods.
541
+ */
542
+ captureAll() {
543
+ this.captureFormState();
544
+ this.captureScrollPositions();
545
+ this.captureLayout();
546
+ }
547
+ /**
548
+ * Restore all state (form + scroll)
549
+ *
550
+ * Convenience method that calls all restore methods.
551
+ */
552
+ restoreAll() {
553
+ this.restoreFormState();
554
+ this.restoreScrollPositions();
555
+ }
556
+ /**
557
+ * Clear all captured state
558
+ */
559
+ clear() {
560
+ this.capturedInputs.clear();
561
+ this.scrollPositions.clear();
562
+ this.layoutSnapshot = null;
563
+ }
564
+ };
565
+ var stateCapturer = new StateCapturer();
566
+
567
+ // src/hmr/overlay.js
568
+ function escapeHtml(str) {
569
+ return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
570
+ }
571
+ function toPositiveInt(value) {
572
+ const parsed = Number(value);
573
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
574
+ }
575
+ function formatCodeFrame(frame, highlightLine, startLine = 1) {
576
+ if (!frame) return "";
577
+ const firstLine = toPositiveInt(startLine) ?? 1;
578
+ const lines = frame.split("\n");
579
+ return lines.map((content, i) => {
580
+ const lineNum = firstLine + i;
581
+ const isHighlight = lineNum === highlightLine;
582
+ return `<div class="line${isHighlight ? " highlight" : ""}">
583
+ <span class="line-number">${lineNum}</span>
584
+ <span class="line-content">${escapeHtml(content)}</span>
585
+ </div>`;
586
+ }).join("");
587
+ }
588
+ var EDITOR_URLS = {
589
+ vscode: (file, line) => `vscode://file/${file}:${line}`,
590
+ cursor: (file, line) => `cursor://file/${file}:${line}`,
591
+ "vscode-insiders": (file, line) => `vscode-insiders://file/${file}:${line}`,
592
+ atom: (file, line) => `atom://core/open/file?filename=${file}&line=${line}`,
593
+ sublime: (file, line) => `subl://open?url=file://${file}&line=${line}`,
594
+ webstorm: (file, line) => `webstorm://open?file=${file}&line=${line}`,
595
+ idea: (file, line) => `idea://open?file=${file}&line=${line}`
596
+ };
597
+ var OVERLAY_STYLES = `
598
+ :host {
599
+ position: fixed;
600
+ top: 0;
601
+ left: 0;
602
+ width: 100%;
603
+ height: 100%;
604
+ z-index: 99999;
605
+ --bg: #181818;
606
+ --text: #f8f8f2;
607
+ --red: #ff5555;
608
+ --yellow: #f1fa8c;
609
+ --purple: #bd93f9;
610
+ --cyan: #8be9fd;
611
+ --code-bg: #282a36;
612
+ --line-num: #6272a4;
613
+ }
614
+ .backdrop {
615
+ position: absolute;
616
+ top: 0;
617
+ left: 0;
618
+ width: 100%;
619
+ height: 100%;
620
+ background: rgba(0, 0, 0, 0.66);
621
+ }
622
+ .container {
623
+ position: absolute;
624
+ top: 50%;
625
+ left: 50%;
626
+ transform: translate(-50%, -50%);
627
+ width: min(800px, 90vw);
628
+ max-height: 90vh;
629
+ overflow: auto;
630
+ background: var(--bg);
631
+ border-radius: 8px;
632
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
633
+ font-family: 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
634
+ }
635
+ .header {
636
+ padding: 16px 20px;
637
+ background: var(--red);
638
+ color: white;
639
+ display: flex;
640
+ justify-content: space-between;
641
+ align-items: center;
642
+ border-radius: 8px 8px 0 0;
643
+ }
644
+ .title {
645
+ font-weight: bold;
646
+ font-size: 16px;
647
+ }
648
+ .close-btn {
649
+ background: none;
650
+ border: none;
651
+ color: white;
652
+ font-size: 24px;
653
+ cursor: pointer;
654
+ padding: 0 8px;
655
+ line-height: 1;
656
+ }
657
+ .close-btn:hover {
658
+ opacity: 0.8;
659
+ }
660
+ .content {
661
+ padding: 20px;
662
+ color: var(--text);
663
+ }
664
+ .message {
665
+ font-size: 18px;
666
+ color: var(--red);
667
+ margin-bottom: 20px;
668
+ word-break: break-word;
669
+ }
670
+ .file {
671
+ color: var(--cyan);
672
+ margin-bottom: 16px;
673
+ cursor: pointer;
674
+ text-decoration: underline;
675
+ }
676
+ .file:hover {
677
+ color: var(--purple);
678
+ }
679
+ .code-frame {
680
+ background: var(--code-bg);
681
+ padding: 16px;
682
+ border-radius: 4px;
683
+ overflow-x: auto;
684
+ font-size: 14px;
685
+ line-height: 1.5;
686
+ margin-bottom: 16px;
687
+ }
688
+ .line {
689
+ display: flex;
690
+ }
691
+ .line-number {
692
+ width: 50px;
693
+ color: var(--line-num);
694
+ text-align: right;
695
+ padding-right: 16px;
696
+ user-select: none;
697
+ flex-shrink: 0;
698
+ }
699
+ .line-content {
700
+ flex: 1;
701
+ white-space: pre;
702
+ }
703
+ .line.highlight {
704
+ background: rgba(255, 85, 85, 0.2);
705
+ }
706
+ .line.highlight .line-content {
707
+ color: var(--red);
708
+ }
709
+ .stack {
710
+ margin-top: 20px;
711
+ font-size: 12px;
712
+ color: var(--line-num);
713
+ white-space: pre-wrap;
714
+ max-height: 200px;
715
+ overflow-y: auto;
716
+ }
717
+ .tip {
718
+ margin-top: 16px;
719
+ padding: 12px;
720
+ background: rgba(189, 147, 249, 0.1);
721
+ border-left: 3px solid var(--purple);
722
+ font-size: 13px;
723
+ color: var(--text);
724
+ }
725
+ .tip strong {
726
+ color: var(--purple);
727
+ }
728
+ `;
729
+ var ErrorOverlay = class {
730
+ constructor() {
731
+ this.overlay = null;
732
+ this.editor = this._getStoredEditor();
733
+ this.escapeHandler = null;
734
+ }
735
+ /**
736
+ * Get stored editor preference from localStorage.
737
+ * @returns {string} Editor name
738
+ * @private
739
+ */
740
+ _getStoredEditor() {
741
+ try {
742
+ return localStorage.getItem("coherent-editor") || "vscode";
743
+ } catch {
744
+ return "vscode";
745
+ }
746
+ }
747
+ /**
748
+ * Create the overlay element with Shadow DOM.
749
+ * @returns {{ host: HTMLElement, shadow: ShadowRoot }} Overlay elements
750
+ */
751
+ createOverlay() {
752
+ if (this.overlay) return this.overlay;
753
+ const host = document.createElement("div");
754
+ host.id = "coherent-error-overlay";
755
+ const shadow = host.attachShadow({ mode: "open" });
756
+ const style = document.createElement("style");
757
+ style.textContent = OVERLAY_STYLES;
758
+ shadow.appendChild(style);
759
+ this.overlay = { host, shadow };
760
+ return this.overlay;
761
+ }
762
+ /**
763
+ * Show the error overlay with error details.
764
+ * @param {Object} error - Error details
765
+ * @param {string} error.message - Error message
766
+ * @param {string} [error.file] - File path
767
+ * @param {number} [error.line] - Line number
768
+ * @param {number} [error.column] - Column number
769
+ * @param {string} [error.frame] - Code frame with context
770
+ * @param {string} [error.stack] - Stack trace
771
+ */
772
+ show(error) {
773
+ const { host, shadow } = this.createOverlay();
774
+ const existingWrapper = shadow.querySelector(".wrapper");
775
+ if (existingWrapper) existingWrapper.remove();
776
+ const line = toPositiveInt(error.line);
777
+ const column = toPositiveInt(error.column);
778
+ const frameLines = error.frame ? error.frame.split("\n").length : 0;
779
+ const startLine = line ? Math.max(1, line - Math.floor(frameLines / 2)) : 1;
780
+ const wrapper = document.createElement("div");
781
+ wrapper.className = "wrapper";
782
+ wrapper.innerHTML = `
783
+ <div class="backdrop"></div>
784
+ <div class="container">
785
+ <div class="header">
786
+ <span class="title">HMR Error</span>
787
+ <button class="close-btn" title="Close (Escape)">&times;</button>
788
+ </div>
789
+ <div class="content">
790
+ <div class="message">${escapeHtml(error.message || "Unknown error")}</div>
791
+ ${error.file ? `
792
+ <div class="file" data-file="${escapeHtml(error.file)}" data-line="${line || 1}">
793
+ ${escapeHtml(error.file)}${line ? `:${line}` : ""}${column ? `:${column}` : ""}
794
+ </div>
795
+ ` : ""}
796
+ ${error.frame ? `
797
+ <div class="code-frame">${formatCodeFrame(error.frame, line, startLine)}</div>
798
+ ` : ""}
799
+ ${error.stack ? `
800
+ <div class="stack">${escapeHtml(error.stack)}</div>
801
+ ` : ""}
802
+ <div class="tip">
803
+ Press <strong>Escape</strong> or click the X to dismiss.
804
+ ${error.file ? ` Click the file path to open in ${escapeHtml(this.editor)}.` : ""}
805
+ </div>
806
+ </div>
807
+ </div>
808
+ `;
809
+ shadow.appendChild(wrapper);
810
+ const closeBtn = wrapper.querySelector(".close-btn");
811
+ const backdrop = wrapper.querySelector(".backdrop");
812
+ const fileLink = wrapper.querySelector(".file");
813
+ closeBtn?.addEventListener("click", () => this.hide());
814
+ backdrop?.addEventListener("click", () => this.hide());
815
+ fileLink?.addEventListener("click", (e) => {
816
+ const target = e.target;
817
+ const file = target.dataset.file;
818
+ const line2 = parseInt(target.dataset.line, 10) || 1;
819
+ this.openInEditor(file, line2);
820
+ });
821
+ if (!this.escapeHandler) {
822
+ this.escapeHandler = (e) => {
823
+ if (e.key === "Escape") this.hide();
824
+ };
825
+ document.addEventListener("keydown", this.escapeHandler);
826
+ }
827
+ if (!host.parentNode) {
828
+ document.body.appendChild(host);
829
+ }
830
+ }
831
+ /**
832
+ * Hide and remove the error overlay.
833
+ */
834
+ hide() {
835
+ if (this.overlay?.host.parentNode) {
836
+ this.overlay.host.parentNode.removeChild(this.overlay.host);
837
+ }
838
+ if (this.escapeHandler) {
839
+ document.removeEventListener("keydown", this.escapeHandler);
840
+ this.escapeHandler = null;
841
+ }
842
+ this.overlay = null;
843
+ }
844
+ /**
845
+ * Open file in configured editor.
846
+ * @param {string} file - File path
847
+ * @param {number} [line=1] - Line number
848
+ */
849
+ openInEditor(file, line = 1) {
850
+ const urlGenerator = EDITOR_URLS[this.editor] || EDITOR_URLS.vscode;
851
+ const url = urlGenerator(file, line);
852
+ window.open(url, "_self");
853
+ }
854
+ /**
855
+ * Set preferred editor and store in localStorage.
856
+ * @param {string} editor - Editor name (vscode, cursor, vscode-insiders, atom, sublime, webstorm, idea)
857
+ */
858
+ setEditor(editor) {
859
+ this.editor = editor;
860
+ try {
861
+ localStorage.setItem("coherent-editor", editor);
862
+ } catch {
863
+ }
864
+ }
865
+ };
866
+ var errorOverlay = new ErrorOverlay();
867
+
868
+ // src/hmr/indicator.js
869
+ var STATUS_COLORS = {
870
+ connected: "#10b981",
871
+ // Green
872
+ disconnected: "#ef4444",
873
+ // Red
874
+ reconnecting: "#f59e0b",
875
+ // Yellow/amber
876
+ error: "#ef4444"
877
+ // Red
878
+ };
879
+ var STATUS_TITLES = {
880
+ connected: "HMR: Connected",
881
+ disconnected: "HMR: Disconnected",
882
+ reconnecting: "HMR: Reconnecting...",
883
+ error: "HMR: Error"
884
+ };
885
+ var DEFAULT_COLOR = "#666";
886
+ var ConnectionIndicator = class {
887
+ constructor() {
888
+ this.indicator = null;
889
+ }
890
+ /**
891
+ * Create the indicator element if it doesn't exist.
892
+ * Uses inline styles to avoid external CSS dependencies.
893
+ */
894
+ create() {
895
+ if (this.indicator) return;
896
+ const el = document.createElement("div");
897
+ el.id = "coherent-hmr-indicator";
898
+ el.style.cssText = `
899
+ position: fixed;
900
+ bottom: 8px;
901
+ right: 8px;
902
+ width: 8px;
903
+ height: 8px;
904
+ border-radius: 50%;
905
+ background: ${DEFAULT_COLOR};
906
+ z-index: 99998;
907
+ pointer-events: none;
908
+ transition: background 0.3s ease;
909
+ `;
910
+ el.title = "HMR: Initializing";
911
+ document.body.appendChild(el);
912
+ this.indicator = el;
913
+ }
914
+ /**
915
+ * Update the indicator status.
916
+ * Creates the element if it doesn't exist (lazy initialization).
917
+ *
918
+ * @param {string} status - Status string: 'connected', 'disconnected', 'reconnecting', or 'error'
919
+ */
920
+ update(status) {
921
+ if (!this.indicator) {
922
+ this.create();
923
+ }
924
+ const color = STATUS_COLORS[status] || STATUS_COLORS.disconnected;
925
+ const title = STATUS_TITLES[status] || "HMR: Unknown";
926
+ this.indicator.style.background = color;
927
+ this.indicator.title = title;
928
+ }
929
+ /**
930
+ * Remove the indicator from the DOM.
931
+ */
932
+ destroy() {
933
+ if (this.indicator?.parentNode) {
934
+ this.indicator.parentNode.removeChild(this.indicator);
935
+ }
936
+ this.indicator = null;
937
+ }
938
+ };
939
+ var connectionIndicator = new ConnectionIndicator();
940
+
941
+ // src/hmr/module-tracker.js
942
+ var ModuleTracker = class {
943
+ constructor() {
944
+ this.modules = /* @__PURE__ */ new Map();
945
+ this.socket = null;
946
+ }
947
+ /**
948
+ * Set WebSocket reference for invalidation messages
949
+ * @param {WebSocket|null} socket - WebSocket connection
950
+ */
951
+ setSocket(socket) {
952
+ this.socket = socket;
953
+ }
954
+ /**
955
+ * Create a hot context for a module (Vite-compatible API)
956
+ *
957
+ * Returns an object with:
958
+ * - data: Persistent object that survives HMR updates
959
+ * - accept(callback): Register self-update handler
960
+ * - acceptDeps(deps, callback): Register dependency update handler
961
+ * - dispose(callback): Register cleanup handler called before replacement
962
+ * - prune(callback): Register handler for when module is removed
963
+ * - invalidate(message): Signal that module cannot hot-update
964
+ *
965
+ * @param {string} moduleId - Unique identifier for the module
966
+ * @returns {Object} Hot context object
967
+ */
968
+ createHotContext(moduleId) {
969
+ let moduleData = this.modules.get(moduleId);
970
+ if (!moduleData) {
971
+ moduleData = {
972
+ accept: null,
973
+ acceptDeps: null,
974
+ dispose: null,
975
+ prune: null,
976
+ data: {}
977
+ };
978
+ this.modules.set(moduleId, moduleData);
979
+ }
980
+ const tracker = this;
981
+ return {
982
+ /**
983
+ * Persistent data object that survives HMR updates.
984
+ * Use this to preserve state across module replacements.
985
+ */
986
+ get data() {
987
+ return moduleData.data;
988
+ },
989
+ /**
990
+ * Accept self updates.
991
+ * Called when this module is updated and can handle its own replacement.
992
+ *
993
+ * @param {Function} [callback] - Optional callback receiving the new module
994
+ */
995
+ accept(callback) {
996
+ moduleData.accept = callback || (() => {
997
+ });
998
+ },
999
+ /**
1000
+ * Accept dependency updates.
1001
+ * Called when one of the specified dependencies is updated.
1002
+ *
1003
+ * @param {string|string[]} deps - Dependency module ID(s)
1004
+ * @param {Function} callback - Callback receiving updated dependencies
1005
+ */
1006
+ acceptDeps(deps, callback) {
1007
+ const depsArray = Array.isArray(deps) ? deps : [deps];
1008
+ moduleData.acceptDeps = { deps: depsArray, callback };
1009
+ },
1010
+ /**
1011
+ * Register disposal callback.
1012
+ * Called before the module is replaced, receives the data object
1013
+ * to allow saving state for the next version.
1014
+ *
1015
+ * @param {Function} callback - Cleanup handler, receives data object
1016
+ */
1017
+ dispose(callback) {
1018
+ moduleData.dispose = callback;
1019
+ },
1020
+ /**
1021
+ * Register prune callback.
1022
+ * Called when the module is completely removed from the module graph.
1023
+ *
1024
+ * @param {Function} callback - Prune handler
1025
+ */
1026
+ prune(callback) {
1027
+ moduleData.prune = callback;
1028
+ },
1029
+ /**
1030
+ * Invalidate this module.
1031
+ * Signals that the module cannot be hot-updated and should propagate
1032
+ * the update to its importers.
1033
+ *
1034
+ * @param {string} [message] - Optional message explaining why
1035
+ */
1036
+ invalidate(message) {
1037
+ const WS_OPEN = typeof WebSocket !== "undefined" ? WebSocket.OPEN : 1;
1038
+ if (tracker.socket?.readyState === WS_OPEN) {
1039
+ tracker.socket.send(JSON.stringify({
1040
+ type: "invalidate",
1041
+ moduleId,
1042
+ message
1043
+ }));
1044
+ }
1045
+ console.log(`[HMR] Module ${moduleId} invalidated${message ? `: ${message}` : ""}`);
1046
+ }
1047
+ };
1048
+ }
1049
+ /**
1050
+ * Check if a module can be hot-updated
1051
+ *
1052
+ * Returns true if the module has registered an accept handler.
1053
+ *
1054
+ * @param {string} moduleId - Module identifier
1055
+ * @returns {boolean} True if module accepts HMR updates
1056
+ */
1057
+ canHotUpdate(moduleId) {
1058
+ const moduleData = this.modules.get(moduleId);
1059
+ return !!(moduleData?.accept || moduleData?.acceptDeps);
1060
+ }
1061
+ /**
1062
+ * Check if a module is an HMR boundary
1063
+ *
1064
+ * A module is considered a boundary if:
1065
+ * - It has an accept handler registered
1066
+ * - It exports __hmrBoundary = true
1067
+ * - It is associated with a data-coherent-component element
1068
+ *
1069
+ * @param {string} moduleId - Module identifier
1070
+ * @param {Object} [moduleExports] - Optional module exports to check for __hmrBoundary
1071
+ * @returns {boolean} True if module is an HMR boundary
1072
+ */
1073
+ isHmrBoundary(moduleId, moduleExports) {
1074
+ if (this.canHotUpdate(moduleId)) {
1075
+ return true;
1076
+ }
1077
+ if (moduleExports?.__hmrBoundary === true) {
1078
+ return true;
1079
+ }
1080
+ const componentName = this.extractComponentName(moduleId);
1081
+ if (componentName && typeof document !== "undefined") {
1082
+ const hasComponent = document.querySelector(
1083
+ `[data-coherent-component="${componentName}"]`
1084
+ );
1085
+ if (hasComponent) {
1086
+ return true;
1087
+ }
1088
+ }
1089
+ return false;
1090
+ }
1091
+ /**
1092
+ * Extract a potential component name from module path
1093
+ *
1094
+ * @param {string} moduleId - Module path
1095
+ * @returns {string|null} Component name or null
1096
+ * @private
1097
+ */
1098
+ extractComponentName(moduleId) {
1099
+ const match = moduleId.match(/\/([^/]+?)(?:\.[^.]+)?$/);
1100
+ if (match) {
1101
+ return match[1];
1102
+ }
1103
+ return null;
1104
+ }
1105
+ /**
1106
+ * Execute dispose callback for a module
1107
+ *
1108
+ * Calls the registered dispose handler with the data object,
1109
+ * allowing the module to save state for the next version.
1110
+ *
1111
+ * @param {string} moduleId - Module identifier
1112
+ * @returns {Object|null} The data object (for passing to next version)
1113
+ */
1114
+ executeDispose(moduleId) {
1115
+ const moduleData = this.modules.get(moduleId);
1116
+ if (!moduleData) {
1117
+ return null;
1118
+ }
1119
+ if (typeof moduleData.dispose === "function") {
1120
+ try {
1121
+ moduleData.dispose(moduleData.data);
1122
+ } catch (err) {
1123
+ console.error(`[HMR] Error in dispose handler for ${moduleId}:`, err);
1124
+ }
1125
+ }
1126
+ return moduleData.data;
1127
+ }
1128
+ /**
1129
+ * Execute accept callback for a module
1130
+ *
1131
+ * Calls the registered accept handler with the new module.
1132
+ *
1133
+ * @param {string} moduleId - Module identifier
1134
+ * @param {Object} [newModule] - The newly imported module
1135
+ * @returns {boolean} True if accept handler was called
1136
+ */
1137
+ executeAccept(moduleId, newModule) {
1138
+ const moduleData = this.modules.get(moduleId);
1139
+ if (!moduleData?.accept) {
1140
+ return false;
1141
+ }
1142
+ try {
1143
+ moduleData.accept(newModule);
1144
+ return true;
1145
+ } catch (err) {
1146
+ console.error(`[HMR] Error in accept handler for ${moduleId}:`, err);
1147
+ return false;
1148
+ }
1149
+ }
1150
+ /**
1151
+ * Execute acceptDeps callback for a module
1152
+ *
1153
+ * Calls the registered acceptDeps handler with the updated dependencies.
1154
+ *
1155
+ * @param {string} moduleId - Module identifier
1156
+ * @param {Object} updatedDeps - Map of dependency moduleId -> new module
1157
+ * @returns {boolean} True if acceptDeps handler was called
1158
+ */
1159
+ executeAcceptDeps(moduleId, updatedDeps) {
1160
+ const moduleData = this.modules.get(moduleId);
1161
+ if (!moduleData?.acceptDeps) {
1162
+ return false;
1163
+ }
1164
+ try {
1165
+ const { deps, callback } = moduleData.acceptDeps;
1166
+ const modules = deps.map((dep) => updatedDeps[dep]);
1167
+ callback(modules);
1168
+ return true;
1169
+ } catch (err) {
1170
+ console.error(`[HMR] Error in acceptDeps handler for ${moduleId}:`, err);
1171
+ return false;
1172
+ }
1173
+ }
1174
+ /**
1175
+ * Execute prune callback for a module
1176
+ *
1177
+ * Called when a module is removed from the module graph.
1178
+ *
1179
+ * @param {string} moduleId - Module identifier
1180
+ */
1181
+ executePrune(moduleId) {
1182
+ const moduleData = this.modules.get(moduleId);
1183
+ if (!moduleData?.prune) {
1184
+ return;
1185
+ }
1186
+ try {
1187
+ moduleData.prune();
1188
+ } catch (err) {
1189
+ console.error(`[HMR] Error in prune handler for ${moduleId}:`, err);
1190
+ }
1191
+ this.modules.delete(moduleId);
1192
+ }
1193
+ /**
1194
+ * Check if module is registered
1195
+ *
1196
+ * @param {string} moduleId - Module identifier
1197
+ * @returns {boolean} True if module is registered
1198
+ */
1199
+ hasModule(moduleId) {
1200
+ return this.modules.has(moduleId);
1201
+ }
1202
+ /**
1203
+ * Get module data (for testing/debugging)
1204
+ *
1205
+ * @param {string} moduleId - Module identifier
1206
+ * @returns {Object|null} Module data or null
1207
+ */
1208
+ getModuleData(moduleId) {
1209
+ return this.modules.get(moduleId) || null;
1210
+ }
1211
+ /**
1212
+ * Clear all module registrations (for testing)
1213
+ */
1214
+ clear() {
1215
+ this.modules.clear();
1216
+ }
1217
+ };
1218
+ var moduleTracker = new ModuleTracker();
1219
+ function createHotContext(moduleId) {
1220
+ return moduleTracker.createHotContext(moduleId);
1221
+ }
1222
+
1223
+ // src/hmr/client.js
1224
+ var MAX_STACK_LINE_LENGTH = 1024;
1225
+ var MAX_STACK_LINES = 50;
1226
+ function parseErrorLocation(error) {
1227
+ const result = { file: null, line: null, column: null };
1228
+ if (!error.stack) {
1229
+ return result;
1230
+ }
1231
+ const patterns = [
1232
+ /at\s[^(]*\(([^()]+):(\d+):(\d+)\)/,
1233
+ // Chrome/Node with parens
1234
+ /at\s+([^\s].*):(\d+):(\d+)/,
1235
+ // Chrome/Node without parens
1236
+ /@([^@]+):(\d+):(\d+)/,
1237
+ // Firefox
1238
+ /^(.+?):(\d+):(\d+)/
1239
+ // Safari
1240
+ ];
1241
+ const lines = error.stack.split("\n", MAX_STACK_LINES);
1242
+ for (const line of lines) {
1243
+ if (line.length > MAX_STACK_LINE_LENGTH) continue;
1244
+ for (const pattern of patterns) {
1245
+ const match = line.match(pattern);
1246
+ if (match) {
1247
+ result.file = match[1];
1248
+ result.line = parseInt(match[2], 10);
1249
+ result.column = parseInt(match[3], 10);
1250
+ return result;
1251
+ }
1252
+ }
1253
+ }
1254
+ return result;
1255
+ }
1256
+ var HMRClient = class {
1257
+ constructor() {
1258
+ this.socket = null;
1259
+ this.connected = false;
1260
+ this.reconnectAttempts = 0;
1261
+ this.maxReconnectAttempts = 10;
1262
+ this.reconnectDelay = 1e3;
1263
+ this.hadDisconnect = false;
1264
+ this.reconnectTimeout = null;
1265
+ this.initialized = false;
1266
+ }
1267
+ /**
1268
+ * Connect to the dev server WebSocket
1269
+ *
1270
+ * Establishes WebSocket connection with automatic reconnection using
1271
+ * exponential backoff with jitter.
1272
+ *
1273
+ * @returns {void}
1274
+ */
1275
+ connect() {
1276
+ if (typeof window === "undefined") {
1277
+ return;
1278
+ }
1279
+ if (this.reconnectTimeout !== null) {
1280
+ clearTimeout(this.reconnectTimeout);
1281
+ this.reconnectTimeout = null;
1282
+ }
1283
+ try {
1284
+ const protocol = location.protocol === "https:" ? "wss" : "ws";
1285
+ const wsUrl = `${protocol}://${location.host}`;
1286
+ const socket = new WebSocket(wsUrl);
1287
+ this.socket = socket;
1288
+ moduleTracker.setSocket(socket);
1289
+ const isCurrent = () => this.socket === socket;
1290
+ socket.addEventListener("open", () => {
1291
+ if (!isCurrent()) return;
1292
+ console.log("[HMR] Connected");
1293
+ this.connected = true;
1294
+ this.reconnectAttempts = 0;
1295
+ connectionIndicator.update("connected");
1296
+ socket.send(JSON.stringify({ type: "connected" }));
1297
+ if (this.hadDisconnect) {
1298
+ console.log("[HMR] Reconnected after disconnect, reloading page");
1299
+ setTimeout(() => this.reload(), 200);
1300
+ return;
1301
+ }
1302
+ });
1303
+ socket.addEventListener("close", () => {
1304
+ if (!isCurrent()) return;
1305
+ this.connected = false;
1306
+ this.hadDisconnect = true;
1307
+ connectionIndicator.update("disconnected");
1308
+ moduleTracker.setSocket(null);
1309
+ this.scheduleReconnect();
1310
+ });
1311
+ socket.addEventListener("error", (event) => {
1312
+ if (!isCurrent()) return;
1313
+ console.warn("[HMR] WebSocket error:", event);
1314
+ connectionIndicator.update("error");
1315
+ try {
1316
+ socket.close();
1317
+ } catch {
1318
+ }
1319
+ });
1320
+ socket.addEventListener("message", (event) => {
1321
+ if (!isCurrent()) return;
1322
+ this.handleMessage(event);
1323
+ });
1324
+ } catch (error) {
1325
+ console.warn("[HMR] Failed to connect:", error);
1326
+ this.scheduleReconnect();
1327
+ }
1328
+ }
1329
+ /**
1330
+ * Schedule a reconnection attempt with exponential backoff
1331
+ *
1332
+ * @private
1333
+ */
1334
+ scheduleReconnect() {
1335
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
1336
+ console.warn("[HMR] Max reconnection attempts reached");
1337
+ connectionIndicator.update("disconnected");
1338
+ return;
1339
+ }
1340
+ connectionIndicator.update("reconnecting");
1341
+ const delay = Math.min(
1342
+ this.reconnectDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1e3,
1343
+ 3e4
1344
+ );
1345
+ this.reconnectAttempts++;
1346
+ console.log(`[HMR] Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
1347
+ this.reconnectTimeout = setTimeout(() => {
1348
+ this.reconnectTimeout = null;
1349
+ this.connect();
1350
+ }, delay);
1351
+ }
1352
+ /**
1353
+ * Handle incoming WebSocket message
1354
+ *
1355
+ * @param {MessageEvent} event - WebSocket message event
1356
+ * @private
1357
+ */
1358
+ handleMessage(event) {
1359
+ let data;
1360
+ try {
1361
+ data = JSON.parse(event.data);
1362
+ } catch {
1363
+ return;
1364
+ }
1365
+ console.log("[HMR] message", data.type, data.filePath || data.webPath || "");
1366
+ switch (data.type) {
1367
+ case "connected":
1368
+ break;
1369
+ case "hmr-full-reload":
1370
+ case "reload":
1371
+ console.warn("[HMR] Server requested full reload");
1372
+ this.reload();
1373
+ break;
1374
+ case "hmr-component-update":
1375
+ case "hmr-update":
1376
+ this.handleUpdate(data);
1377
+ break;
1378
+ case "hmr-error":
1379
+ this.showError(data.error || data);
1380
+ break;
1381
+ case "preview-update":
1382
+ break;
1383
+ default:
1384
+ break;
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Handle module update
1389
+ *
1390
+ * Orchestrates the full HMR update cycle:
1391
+ * 1. Capture form/scroll state
1392
+ * 2. Execute dispose handlers
1393
+ * 3. Clean up module resources
1394
+ * 4. Re-import module
1395
+ * 5. Execute accept handlers, or reload the page when the module does not
1396
+ * accept updates (nothing else can apply its new code)
1397
+ * 6. Restore state
1398
+ *
1399
+ * @param {Object} data - Update message data
1400
+ * @param {string} [data.filePath] - File path that changed
1401
+ * @param {string} [data.webPath] - Web-accessible path
1402
+ * @param {string} [data.updateType] - Type of update (component, style, etc.)
1403
+ */
1404
+ async handleUpdate(data) {
1405
+ const filePath = data.webPath || data.filePath || "";
1406
+ const moduleId = filePath;
1407
+ try {
1408
+ stateCapturer.captureAll();
1409
+ if (moduleTracker.hasModule(moduleId)) {
1410
+ moduleTracker.executeDispose(moduleId);
1411
+ }
1412
+ if (cleanupTracker.hasResources(moduleId)) {
1413
+ cleanupTracker.checkForLeaks(moduleId);
1414
+ cleanupTracker.cleanup(moduleId);
1415
+ }
1416
+ const importPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
1417
+ const newModule = await this.importModule(`${importPath}?t=${Date.now()}`);
1418
+ if (!moduleTracker.canHotUpdate(moduleId)) {
1419
+ console.log(`[HMR] ${filePath} does not accept hot updates, reloading`);
1420
+ this.reload();
1421
+ return;
1422
+ }
1423
+ moduleTracker.executeAccept(moduleId, newModule);
1424
+ stateCapturer.restoreAll();
1425
+ errorOverlay.hide();
1426
+ console.log(`[HMR] Updated: ${data.updateType || "module"} ${filePath}`);
1427
+ } catch (error) {
1428
+ this.handleUpdateError(error, filePath);
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Import an updated module (overridable, e.g. in tests)
1433
+ *
1434
+ * @param {string} url - Module URL with cache-busting query
1435
+ * @returns {Promise<Object>} Module namespace
1436
+ */
1437
+ importModule(url) {
1438
+ return import(
1439
+ /* @vite-ignore */
1440
+ url
1441
+ );
1442
+ }
1443
+ /**
1444
+ * Reload the page
1445
+ */
1446
+ reload() {
1447
+ location.reload();
1448
+ }
1449
+ /**
1450
+ * Handle update error
1451
+ *
1452
+ * @param {Error} error - Error that occurred
1453
+ * @param {string} filePath - File that was being updated
1454
+ * @private
1455
+ */
1456
+ handleUpdateError(error, filePath) {
1457
+ console.error("[HMR] Update failed:", error);
1458
+ const location2 = parseErrorLocation(error);
1459
+ const errorDetails = {
1460
+ message: error.message || "Unknown error during HMR update",
1461
+ file: location2.file || filePath,
1462
+ line: location2.line,
1463
+ column: location2.column,
1464
+ stack: error.stack
1465
+ };
1466
+ this.showError(errorDetails);
1467
+ }
1468
+ /**
1469
+ * Show error overlay
1470
+ *
1471
+ * @param {Object} error - Error details
1472
+ */
1473
+ showError(error) {
1474
+ errorOverlay.show(error);
1475
+ }
1476
+ /**
1477
+ * Hide error overlay
1478
+ */
1479
+ hideError() {
1480
+ errorOverlay.hide();
1481
+ }
1482
+ /**
1483
+ * Initialize HMR client
1484
+ *
1485
+ * Guards against double initialization and connects to dev server.
1486
+ *
1487
+ * @returns {void}
1488
+ */
1489
+ initialize() {
1490
+ if (typeof window === "undefined") {
1491
+ return;
1492
+ }
1493
+ if (window.__coherent_hmr_initialized || this.initialized) {
1494
+ return;
1495
+ }
1496
+ window.__coherent_hmr_initialized = true;
1497
+ this.initialized = true;
1498
+ this.connect();
1499
+ }
1500
+ /**
1501
+ * Disconnect and clean up
1502
+ *
1503
+ * @returns {void}
1504
+ */
1505
+ disconnect() {
1506
+ if (this.reconnectTimeout !== null) {
1507
+ clearTimeout(this.reconnectTimeout);
1508
+ this.reconnectTimeout = null;
1509
+ }
1510
+ if (this.socket) {
1511
+ const socket = this.socket;
1512
+ this.socket = null;
1513
+ try {
1514
+ socket.close();
1515
+ } catch {
1516
+ }
1517
+ }
1518
+ this.connected = false;
1519
+ moduleTracker.setSocket(null);
1520
+ connectionIndicator.destroy();
1521
+ }
1522
+ /**
1523
+ * Check if client is connected
1524
+ *
1525
+ * @returns {boolean} True if connected
1526
+ */
1527
+ isConnected() {
1528
+ return this.connected;
1529
+ }
1530
+ };
1531
+ var hmrClient = new HMRClient();
1532
+
1533
+ export {
1534
+ CleanupTracker,
1535
+ cleanupTracker,
1536
+ StateCapturer,
1537
+ stateCapturer,
1538
+ escapeHtml,
1539
+ formatCodeFrame,
1540
+ ErrorOverlay,
1541
+ errorOverlay,
1542
+ ConnectionIndicator,
1543
+ connectionIndicator,
1544
+ ModuleTracker,
1545
+ moduleTracker,
1546
+ createHotContext,
1547
+ HMRClient,
1548
+ hmrClient
1549
+ };
1550
+ //# sourceMappingURL=chunk-EAOAAY2X.js.map