@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.
- package/README.md +126 -66
- package/dist/chunk-EAOAAY2X.js +1550 -0
- package/dist/chunk-EAOAAY2X.js.map +7 -0
- package/dist/{chunk-N36BSMJU.js → chunk-UYL3RRRC.js} +134 -32
- package/dist/chunk-UYL3RRRC.js.map +7 -0
- package/dist/events/index.js +1 -1
- package/dist/hmr.js +34 -4
- package/dist/hmr.js.map +3 -3
- package/dist/index.js +598 -1732
- package/dist/index.js.map +4 -4
- package/dist/router.js +209 -31
- package/dist/router.js.map +2 -2
- package/package.json +4 -1
- package/types/hmr.d.ts +32 -1
- package/types/index.d.ts +258 -701
- package/types/router.d.ts +34 -11
- package/dist/chunk-N36BSMJU.js.map +0 -7
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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)">×</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
|