@coherent.js/client 1.0.0-beta.7 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2122 -931
- package/dist/index.js.map +4 -4
- package/package.json +2 -6
- package/src/index.js +1 -10
- package/types/index.d.ts +0 -53
- package/dist/client/hmr.d.ts +0 -1
- package/dist/client/hmr.d.ts.map +0 -1
- package/dist/client/hmr.js +0 -107
- package/dist/client/hmr.js.map +0 -1
- package/dist/client/hydration.d.ts +0 -55
- package/dist/client/hydration.d.ts.map +0 -1
- package/dist/client/hydration.js +0 -1593
- package/dist/client/hydration.js.map +0 -1
- package/types/hydration.d.ts +0 -66
package/dist/index.js
CHANGED
|
@@ -1,1017 +1,2208 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return void 0;
|
|
1
|
+
// src/events/registry.js
|
|
2
|
+
var HandlerRegistry = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
6
5
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Register a handler with optional component context
|
|
8
|
+
* @param {string} handlerId - Unique identifier for the handler
|
|
9
|
+
* @param {Function} handler - The event handler function
|
|
10
|
+
* @param {object|null} componentRef - Optional component reference with state/setState
|
|
11
|
+
*/
|
|
12
|
+
register(handlerId, handler, componentRef = null) {
|
|
13
|
+
if (typeof handler !== "function") {
|
|
14
|
+
throw new Error(`Handler must be a function, received: ${typeof handler}`);
|
|
15
|
+
}
|
|
16
|
+
this.handlers.set(handlerId, { handler, componentRef });
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Unregister a handler by ID
|
|
20
|
+
* @param {string} handlerId - The handler ID to remove
|
|
21
|
+
* @returns {boolean} True if handler was removed, false if not found
|
|
22
|
+
*/
|
|
23
|
+
unregister(handlerId) {
|
|
24
|
+
return this.handlers.delete(handlerId);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get a handler entry by ID
|
|
28
|
+
* @param {string} handlerId - The handler ID to look up
|
|
29
|
+
* @returns {{handler: Function, componentRef: object|null}|undefined} Handler entry or undefined
|
|
30
|
+
*/
|
|
31
|
+
get(handlerId) {
|
|
32
|
+
return this.handlers.get(handlerId);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if a handler is registered
|
|
36
|
+
* @param {string} handlerId - The handler ID to check
|
|
37
|
+
* @returns {boolean} True if handler exists
|
|
38
|
+
*/
|
|
39
|
+
has(handlerId) {
|
|
40
|
+
return this.handlers.has(handlerId);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Clear all registered handlers
|
|
44
|
+
*/
|
|
45
|
+
clear() {
|
|
46
|
+
this.handlers.clear();
|
|
11
47
|
}
|
|
12
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Get all handler IDs registered for a specific component
|
|
50
|
+
* @param {object} componentRef - The component reference to search for
|
|
51
|
+
* @returns {string[]} Array of handler IDs belonging to this component
|
|
52
|
+
*/
|
|
53
|
+
getByComponent(componentRef) {
|
|
54
|
+
if (!componentRef) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const handlerIds = [];
|
|
58
|
+
for (const [handlerId, entry] of this.handlers) {
|
|
59
|
+
if (entry.componentRef === componentRef) {
|
|
60
|
+
handlerIds.push(handlerId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return handlerIds;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the number of registered handlers
|
|
67
|
+
* @returns {number} Count of registered handlers
|
|
68
|
+
*/
|
|
69
|
+
get size() {
|
|
70
|
+
return this.handlers.size;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var handlerRegistry = new HandlerRegistry();
|
|
74
|
+
|
|
75
|
+
// src/events/wrapper.js
|
|
76
|
+
function wrapEvent(originalEvent, target, componentRef = null) {
|
|
77
|
+
return {
|
|
78
|
+
// Native event access
|
|
79
|
+
originalEvent,
|
|
80
|
+
target,
|
|
81
|
+
// Delegate common methods
|
|
82
|
+
preventDefault() {
|
|
83
|
+
originalEvent.preventDefault();
|
|
84
|
+
},
|
|
85
|
+
stopPropagation() {
|
|
86
|
+
originalEvent.stopPropagation();
|
|
87
|
+
},
|
|
88
|
+
// Component context (null if no componentRef provided)
|
|
89
|
+
component: componentRef?.component ?? null,
|
|
90
|
+
state: componentRef?.state ?? null,
|
|
91
|
+
setState: componentRef?.setState ?? null,
|
|
92
|
+
props: componentRef?.props ?? null
|
|
93
|
+
};
|
|
13
94
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
95
|
+
|
|
96
|
+
// src/events/delegation.js
|
|
97
|
+
var EventDelegation = class {
|
|
98
|
+
/**
|
|
99
|
+
* @param {import('./registry.js').HandlerRegistry} [registry] - Handler registry instance
|
|
100
|
+
*/
|
|
101
|
+
constructor(registry = handlerRegistry) {
|
|
102
|
+
this.registry = registry;
|
|
103
|
+
this.initialized = false;
|
|
104
|
+
this.root = null;
|
|
105
|
+
this.boundHandlers = /* @__PURE__ */ new Map();
|
|
106
|
+
this.eventTypes = [
|
|
107
|
+
"click",
|
|
108
|
+
"change",
|
|
109
|
+
"input",
|
|
110
|
+
"submit",
|
|
111
|
+
"focus",
|
|
112
|
+
"blur",
|
|
113
|
+
"keydown",
|
|
114
|
+
"keyup",
|
|
115
|
+
"keypress"
|
|
116
|
+
];
|
|
17
117
|
}
|
|
18
|
-
|
|
19
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Initialize event delegation by attaching listeners to the root element
|
|
120
|
+
* @param {Document|Element} [root=document] - Root element for event delegation
|
|
121
|
+
*/
|
|
122
|
+
initialize(root = typeof document !== "undefined" ? document : null) {
|
|
123
|
+
if (this.initialized) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!root) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.root = root;
|
|
130
|
+
for (const eventType of this.eventTypes) {
|
|
131
|
+
const handler = (event) => this.handleEvent(event, eventType);
|
|
132
|
+
const useCapture = eventType === "focus" || eventType === "blur";
|
|
133
|
+
const options = {
|
|
134
|
+
capture: useCapture,
|
|
135
|
+
passive: eventType !== "submit"
|
|
136
|
+
};
|
|
137
|
+
root.addEventListener(eventType, handler, options);
|
|
138
|
+
this.boundHandlers.set(eventType, { handler, options });
|
|
139
|
+
}
|
|
140
|
+
this.initialized = true;
|
|
20
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Handle a delegated event
|
|
144
|
+
* @param {Event} event - The DOM event
|
|
145
|
+
* @param {string} eventType - The type of event (click, change, etc.)
|
|
146
|
+
*/
|
|
147
|
+
handleEvent(event, eventType) {
|
|
148
|
+
const target = event.target;
|
|
149
|
+
if (!target || typeof target.closest !== "function") {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const attrName = `data-coherent-${eventType}`;
|
|
153
|
+
const delegateTarget = target.closest(`[${attrName}]`);
|
|
154
|
+
if (!delegateTarget) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const handlerId = delegateTarget.getAttribute(attrName);
|
|
158
|
+
if (!handlerId) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const entry = this.registry.get(handlerId);
|
|
162
|
+
if (!entry) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const wrappedEvent = wrapEvent(event, delegateTarget, entry.componentRef);
|
|
166
|
+
entry.handler(wrappedEvent);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Destroy the event delegation system
|
|
170
|
+
* Removes all listeners and clears the registry
|
|
171
|
+
*/
|
|
172
|
+
destroy() {
|
|
173
|
+
if (!this.initialized || !this.root) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
for (const [eventType, { handler, options }] of this.boundHandlers) {
|
|
177
|
+
this.root.removeEventListener(eventType, handler, options);
|
|
178
|
+
}
|
|
179
|
+
this.boundHandlers.clear();
|
|
180
|
+
this.registry.clear();
|
|
181
|
+
this.initialized = false;
|
|
182
|
+
this.root = null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Check if the delegation system is initialized
|
|
186
|
+
* @returns {boolean} True if initialized
|
|
187
|
+
*/
|
|
188
|
+
isInitialized() {
|
|
189
|
+
return this.initialized;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var eventDelegation = new EventDelegation();
|
|
193
|
+
|
|
194
|
+
// src/hydration/state-serializer.js
|
|
195
|
+
function serializeState(state) {
|
|
196
|
+
if (!state || typeof state !== "object") return null;
|
|
197
|
+
const serializable = {};
|
|
198
|
+
let hasSerializable = false;
|
|
199
|
+
for (const [key, value] of Object.entries(state)) {
|
|
200
|
+
if (isSerializable(value)) {
|
|
201
|
+
serializable[key] = value;
|
|
202
|
+
hasSerializable = true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!hasSerializable) return null;
|
|
21
206
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let hasState = false;
|
|
28
|
-
const countAttr = element.getAttribute("data-count");
|
|
29
|
-
if (countAttr !== null) {
|
|
30
|
-
state.count = parseInt(countAttr, 10) || 0;
|
|
31
|
-
hasState = true;
|
|
32
|
-
}
|
|
33
|
-
const stepAttr = element.getAttribute("data-step");
|
|
34
|
-
if (stepAttr !== null) {
|
|
35
|
-
state.step = parseInt(stepAttr, 10) || 1;
|
|
36
|
-
hasState = true;
|
|
37
|
-
}
|
|
38
|
-
const todosAttr = element.getAttribute("data-todos");
|
|
39
|
-
if (todosAttr) {
|
|
40
|
-
state.todos = JSON.parse(todosAttr);
|
|
41
|
-
hasState = true;
|
|
42
|
-
}
|
|
43
|
-
const valueAttr = element.getAttribute("data-value");
|
|
44
|
-
if (valueAttr !== null) {
|
|
45
|
-
state.value = valueAttr;
|
|
46
|
-
hasState = true;
|
|
47
|
-
}
|
|
48
|
-
if (options.initialState) {
|
|
49
|
-
return { ...options.initialState, ...state };
|
|
50
|
-
}
|
|
51
|
-
return hasState ? state : null;
|
|
52
|
-
} catch (_error) {
|
|
53
|
-
console.warn("Error extracting initial state:", _error);
|
|
54
|
-
return options.initialState || null;
|
|
207
|
+
const json = JSON.stringify(serializable);
|
|
208
|
+
return btoa(encodeURIComponent(json));
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.warn("[Coherent.js] Failed to serialize state:", e);
|
|
211
|
+
return null;
|
|
55
212
|
}
|
|
56
213
|
}
|
|
57
|
-
function
|
|
58
|
-
if (typeof
|
|
59
|
-
|
|
214
|
+
function deserializeState(encoded) {
|
|
215
|
+
if (!encoded || typeof encoded !== "string") return null;
|
|
216
|
+
try {
|
|
217
|
+
const json = decodeURIComponent(atob(encoded));
|
|
218
|
+
return JSON.parse(json);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.warn("[Coherent.js] Failed to deserialize state:", e);
|
|
60
221
|
return null;
|
|
61
222
|
}
|
|
62
|
-
|
|
63
|
-
|
|
223
|
+
}
|
|
224
|
+
function extractState(element) {
|
|
225
|
+
if (!element || typeof element.getAttribute !== "function") {
|
|
64
226
|
return null;
|
|
65
227
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const text = child.textContent.trim();
|
|
137
|
-
if (text) children.push(text);
|
|
138
|
-
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
139
|
-
const childVNode = this.virtualElementFromDOM(child);
|
|
140
|
-
if (childVNode) children.push(childVNode);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
if (children.length > 0) {
|
|
145
|
-
props2.children = children;
|
|
146
|
-
}
|
|
147
|
-
return { [tagName]: props2 };
|
|
148
|
-
},
|
|
149
|
-
// Intelligent DOM patching with minimal changes
|
|
150
|
-
patchDOM(domElement, oldVNode, newVNode) {
|
|
151
|
-
if (typeof newVNode === "string" || typeof newVNode === "number") {
|
|
152
|
-
const newText = String(newVNode);
|
|
153
|
-
if (typeof window === "undefined" || typeof Node === "undefined" || typeof document === "undefined") {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (domElement.nodeType === Node.TEXT_NODE) {
|
|
157
|
-
if (domElement.textContent !== newText) {
|
|
158
|
-
domElement.textContent = newText;
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
const textNode = document.createTextNode(newText);
|
|
162
|
-
if (domElement.parentNode) {
|
|
163
|
-
domElement.parentNode.replaceChild(textNode, domElement);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (!newVNode) {
|
|
169
|
-
domElement.remove();
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (Array.isArray(newVNode)) {
|
|
173
|
-
console.warn("Array virtual node at root level");
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const newTagName = Object.keys(newVNode)[0];
|
|
177
|
-
if (domElement.tagName.toLowerCase() !== newTagName.toLowerCase()) {
|
|
178
|
-
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const newElement = this.createDOMElement(newVNode);
|
|
182
|
-
if (domElement.parentNode) {
|
|
183
|
-
domElement.parentNode.replaceChild(newElement, domElement);
|
|
184
|
-
}
|
|
185
|
-
attachEventListeners(newElement, this);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
this.patchAttributes(domElement, oldVNode, newVNode);
|
|
189
|
-
this.patchChildren(domElement, oldVNode, newVNode);
|
|
190
|
-
attachEventListeners(domElement, this);
|
|
191
|
-
},
|
|
192
|
-
// Patch element attributes efficiently
|
|
193
|
-
patchAttributes(domElement, oldVNode, newVNode) {
|
|
194
|
-
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (!domElement || typeof domElement.setAttribute !== "function" || typeof domElement.removeAttribute !== "function") {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const oldTagName = oldVNode ? Object.keys(oldVNode)[0] : null;
|
|
201
|
-
const newTagName = Object.keys(newVNode)[0];
|
|
202
|
-
const oldProps = oldVNode && oldTagName ? oldVNode[oldTagName] || {} : {};
|
|
203
|
-
const newProps = newVNode[newTagName] || {};
|
|
204
|
-
Object.keys(oldProps).forEach((key) => {
|
|
205
|
-
if (key === "children" || key === "text") return;
|
|
206
|
-
if (!(key in newProps)) {
|
|
207
|
-
const attrName = key === "className" ? "class" : key;
|
|
208
|
-
domElement.removeAttribute(attrName);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
Object.keys(newProps).forEach((key) => {
|
|
212
|
-
if (key === "children" || key === "text") return;
|
|
213
|
-
const newValue = newProps[key];
|
|
214
|
-
const oldValue = oldProps[key];
|
|
215
|
-
if (newValue !== oldValue) {
|
|
216
|
-
const attrName = key === "className" ? "class" : key;
|
|
217
|
-
if (newValue === true) {
|
|
218
|
-
domElement.setAttribute(attrName, "");
|
|
219
|
-
} else if (newValue === false || newValue === null) {
|
|
220
|
-
domElement.removeAttribute(attrName);
|
|
221
|
-
} else {
|
|
222
|
-
domElement.setAttribute(attrName, String(newValue));
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
},
|
|
227
|
-
// Get children array from a vNode
|
|
228
|
-
getVNodeChildren(vNode) {
|
|
229
|
-
if (!vNode || typeof vNode !== "object" || Array.isArray(vNode)) {
|
|
230
|
-
return [];
|
|
231
|
-
}
|
|
232
|
-
const tagName = Object.keys(vNode)[0];
|
|
233
|
-
const props2 = vNode[tagName];
|
|
234
|
-
if (!props2 || typeof props2 !== "object") {
|
|
235
|
-
return [];
|
|
236
|
-
}
|
|
237
|
-
if (props2.children) {
|
|
238
|
-
return Array.isArray(props2.children) ? props2.children : [props2.children];
|
|
239
|
-
}
|
|
240
|
-
if (props2.text !== void 0) {
|
|
241
|
-
return [props2.text];
|
|
242
|
-
}
|
|
243
|
-
return [];
|
|
244
|
-
},
|
|
245
|
-
// Patch children with key-based reconciliation
|
|
246
|
-
patchChildren(domElement, oldVNode, newVNode) {
|
|
247
|
-
if (typeof window === "undefined" || typeof Node === "undefined" || typeof document === "undefined") {
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
if (!domElement || typeof domElement.childNodes === "undefined" || typeof domElement.appendChild !== "function") {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
const oldChildren = this.getVNodeChildren(oldVNode);
|
|
254
|
-
const newChildren = this.getVNodeChildren(newVNode);
|
|
255
|
-
let domChildren = [];
|
|
256
|
-
if (typeof Array.from === "function" && domElement.childNodes) {
|
|
257
|
-
try {
|
|
258
|
-
domChildren = Array.from(domElement.childNodes).filter((node) => {
|
|
259
|
-
return node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim();
|
|
260
|
-
});
|
|
261
|
-
} catch (_error) {
|
|
262
|
-
console.warn("Failed to convert childNodes to array:", _error);
|
|
263
|
-
domChildren = [];
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const oldKeyMap = /* @__PURE__ */ new Map();
|
|
267
|
-
const oldIndexMap = /* @__PURE__ */ new Map();
|
|
268
|
-
oldChildren.forEach((child, i) => {
|
|
269
|
-
const key = getKey(child);
|
|
270
|
-
if (key !== void 0) {
|
|
271
|
-
oldKeyMap.set(key, { vNode: child, index: i, domNode: domChildren[i] });
|
|
272
|
-
} else {
|
|
273
|
-
oldIndexMap.set(i, { vNode: child, index: i, domNode: domChildren[i] });
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
const usedOldNodes = /* @__PURE__ */ new Set();
|
|
277
|
-
newChildren.forEach((newChild, newIndex) => {
|
|
278
|
-
const newKey = getKey(newChild);
|
|
279
|
-
let oldEntry = null;
|
|
280
|
-
if (newKey !== void 0 && oldKeyMap.has(newKey)) {
|
|
281
|
-
oldEntry = oldKeyMap.get(newKey);
|
|
282
|
-
usedOldNodes.add(newKey);
|
|
283
|
-
} else if (newKey === void 0 && oldIndexMap.has(newIndex)) {
|
|
284
|
-
oldEntry = oldIndexMap.get(newIndex);
|
|
285
|
-
usedOldNodes.add(`index:${newIndex}`);
|
|
286
|
-
}
|
|
287
|
-
if (oldEntry && oldEntry.domNode) {
|
|
288
|
-
this.patchDOM(oldEntry.domNode, oldEntry.vNode, newChild);
|
|
289
|
-
const currentPosition = Array.from(domElement.childNodes).filter((node) => {
|
|
290
|
-
return node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim();
|
|
291
|
-
}).indexOf(oldEntry.domNode);
|
|
292
|
-
if (currentPosition !== newIndex) {
|
|
293
|
-
const referenceNode = domElement.childNodes[newIndex];
|
|
294
|
-
if (referenceNode) {
|
|
295
|
-
domElement.insertBefore(oldEntry.domNode, referenceNode);
|
|
296
|
-
} else {
|
|
297
|
-
domElement.appendChild(oldEntry.domNode);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
} else {
|
|
301
|
-
const newElement = this.createDOMElement(newChild);
|
|
302
|
-
if (newElement) {
|
|
303
|
-
const referenceNode = domElement.childNodes[newIndex];
|
|
304
|
-
if (referenceNode) {
|
|
305
|
-
domElement.insertBefore(newElement, referenceNode);
|
|
306
|
-
} else {
|
|
307
|
-
domElement.appendChild(newElement);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
oldKeyMap.forEach((entry, key) => {
|
|
313
|
-
if (!usedOldNodes.has(key) && entry.domNode && entry.domNode.parentNode) {
|
|
314
|
-
entry.domNode.remove();
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
oldIndexMap.forEach((entry, index) => {
|
|
318
|
-
if (!usedOldNodes.has(`index:${index}`) && entry.domNode && entry.domNode.parentNode) {
|
|
319
|
-
entry.domNode.remove();
|
|
320
|
-
}
|
|
228
|
+
const encoded = element.getAttribute("data-state");
|
|
229
|
+
return deserializeState(encoded);
|
|
230
|
+
}
|
|
231
|
+
function isSerializable(value) {
|
|
232
|
+
if (value === void 0) return false;
|
|
233
|
+
if (value === null) return true;
|
|
234
|
+
if (typeof value === "function") return false;
|
|
235
|
+
if (typeof value === "symbol") return false;
|
|
236
|
+
if (typeof value === "bigint") return false;
|
|
237
|
+
if (Array.isArray(value)) {
|
|
238
|
+
return value.every(isSerializable);
|
|
239
|
+
}
|
|
240
|
+
if (typeof value === "object") {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
var STATE_SIZE_WARNING_THRESHOLD = 10 * 1024;
|
|
246
|
+
function serializeStateWithWarning(state, componentName = "Unknown") {
|
|
247
|
+
const encoded = serializeState(state);
|
|
248
|
+
if (encoded && encoded.length > STATE_SIZE_WARNING_THRESHOLD) {
|
|
249
|
+
console.warn(
|
|
250
|
+
`[Coherent.js] Large state detected for component "${componentName}": ${Math.round(encoded.length / 1024)}KB. Consider using a state management solution for large datasets.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return encoded;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/hydration/mismatch-detector.js
|
|
257
|
+
function formatPath(segments) {
|
|
258
|
+
if (!segments || segments.length === 0) return "root";
|
|
259
|
+
return segments.join(".");
|
|
260
|
+
}
|
|
261
|
+
function getVNodeChildren(vNode) {
|
|
262
|
+
if (!vNode || typeof vNode !== "object" || Array.isArray(vNode)) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
const tagName = Object.keys(vNode)[0];
|
|
266
|
+
const props = vNode[tagName];
|
|
267
|
+
if (!props || typeof props !== "object") {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
if (props.children) {
|
|
271
|
+
return Array.isArray(props.children) ? props.children : [props.children];
|
|
272
|
+
}
|
|
273
|
+
if (props.text !== void 0) {
|
|
274
|
+
return [String(props.text)];
|
|
275
|
+
}
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
function detectMismatch(domElement, virtualNode, path = []) {
|
|
279
|
+
const mismatches = [];
|
|
280
|
+
if (virtualNode === null || virtualNode === void 0) {
|
|
281
|
+
return mismatches;
|
|
282
|
+
}
|
|
283
|
+
if (typeof virtualNode === "string" || typeof virtualNode === "number") {
|
|
284
|
+
const expectedText = String(virtualNode).trim();
|
|
285
|
+
let actualText;
|
|
286
|
+
if (domElement.nodeType === 3) {
|
|
287
|
+
actualText = domElement.textContent?.trim() || "";
|
|
288
|
+
} else {
|
|
289
|
+
actualText = domElement.textContent?.trim() || "";
|
|
290
|
+
}
|
|
291
|
+
if (actualText !== expectedText) {
|
|
292
|
+
mismatches.push({
|
|
293
|
+
path: formatPath(path),
|
|
294
|
+
type: "text",
|
|
295
|
+
expected: expectedText,
|
|
296
|
+
actual: actualText,
|
|
297
|
+
domPath: getDOMPath(domElement)
|
|
321
298
|
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
299
|
+
}
|
|
300
|
+
return mismatches;
|
|
301
|
+
}
|
|
302
|
+
if (Array.isArray(virtualNode)) {
|
|
303
|
+
virtualNode.forEach((child, index) => {
|
|
304
|
+
const domChild = getDOMChildAtIndex(domElement, index);
|
|
305
|
+
if (domChild) {
|
|
306
|
+
const childMismatches = detectMismatch(
|
|
307
|
+
domChild,
|
|
308
|
+
child,
|
|
309
|
+
[...path, `[${index}]`]
|
|
310
|
+
);
|
|
311
|
+
mismatches.push(...childMismatches);
|
|
312
|
+
} else {
|
|
313
|
+
mismatches.push({
|
|
314
|
+
path: formatPath([...path, `[${index}]`]),
|
|
315
|
+
type: "missing_element",
|
|
316
|
+
expected: describeVNode(child),
|
|
317
|
+
actual: null,
|
|
318
|
+
domPath: `${getDOMPath(domElement)} > child[${index}]`
|
|
335
319
|
});
|
|
336
|
-
return fragment;
|
|
337
320
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
321
|
+
});
|
|
322
|
+
return mismatches;
|
|
323
|
+
}
|
|
324
|
+
if (typeof virtualNode !== "object") {
|
|
325
|
+
return mismatches;
|
|
326
|
+
}
|
|
327
|
+
const tagName = Object.keys(virtualNode)[0];
|
|
328
|
+
const props = virtualNode[tagName] || {};
|
|
329
|
+
const domTagName = domElement.tagName?.toLowerCase();
|
|
330
|
+
if (domTagName !== tagName.toLowerCase()) {
|
|
331
|
+
mismatches.push({
|
|
332
|
+
path: formatPath(path),
|
|
333
|
+
type: "tagName",
|
|
334
|
+
expected: tagName,
|
|
335
|
+
actual: domTagName,
|
|
336
|
+
domPath: getDOMPath(domElement)
|
|
337
|
+
});
|
|
338
|
+
return mismatches;
|
|
339
|
+
}
|
|
340
|
+
const attributeChecks = [
|
|
341
|
+
{ virtual: "className", dom: "class" },
|
|
342
|
+
{ virtual: "id", dom: "id" },
|
|
343
|
+
{ virtual: "type", dom: "type" },
|
|
344
|
+
{ virtual: "value", dom: "value" },
|
|
345
|
+
{ virtual: "checked", dom: "checked" },
|
|
346
|
+
{ virtual: "disabled", dom: "disabled" },
|
|
347
|
+
{ virtual: "href", dom: "href" },
|
|
348
|
+
{ virtual: "src", dom: "src" }
|
|
349
|
+
];
|
|
350
|
+
attributeChecks.forEach(({ virtual, dom }) => {
|
|
351
|
+
const expectedValue = props[virtual];
|
|
352
|
+
if (expectedValue === void 0) return;
|
|
353
|
+
const actualValue = domElement.getAttribute(dom);
|
|
354
|
+
const expectedStr = String(expectedValue);
|
|
355
|
+
if (typeof expectedValue === "boolean") {
|
|
356
|
+
const actualBool = actualValue !== null;
|
|
357
|
+
if (expectedValue !== actualBool) {
|
|
358
|
+
mismatches.push({
|
|
359
|
+
path: formatPath([...path, `@${dom}`]),
|
|
360
|
+
type: "attribute",
|
|
361
|
+
expected: expectedValue,
|
|
362
|
+
actual: actualBool,
|
|
363
|
+
domPath: getDOMPath(domElement)
|
|
355
364
|
});
|
|
356
|
-
} else if (props2.text) {
|
|
357
|
-
element2.appendChild(document.createTextNode(String(props2.text)));
|
|
358
|
-
}
|
|
359
|
-
return element2;
|
|
360
|
-
},
|
|
361
|
-
// Render virtual element to HTML string
|
|
362
|
-
renderVirtualElement(element2) {
|
|
363
|
-
if (typeof element2 === "string" || typeof element2 === "number") {
|
|
364
|
-
return String(element2);
|
|
365
|
-
}
|
|
366
|
-
if (!element2 || typeof element2 !== "object") {
|
|
367
|
-
return "";
|
|
368
365
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const children = [];
|
|
379
|
-
Object.keys(props2).forEach((key) => {
|
|
380
|
-
if (key === "children") {
|
|
381
|
-
if (Array.isArray(props2.children)) {
|
|
382
|
-
children.push(...props2.children);
|
|
383
|
-
} else {
|
|
384
|
-
children.push(props2.children);
|
|
385
|
-
}
|
|
386
|
-
} else if (key === "text") {
|
|
387
|
-
children.push(props2.text);
|
|
388
|
-
} else {
|
|
389
|
-
const attrName = key === "className" ? "class" : key;
|
|
390
|
-
const value = props2[key];
|
|
391
|
-
if (value === true) {
|
|
392
|
-
attributes += ` ${attrName}`;
|
|
393
|
-
} else if (value !== false && value !== null && value !== void 0) {
|
|
394
|
-
attributes += ` ${attrName}="${String(value).replace(/"/g, """)}"`;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (expectedStr !== actualValue) {
|
|
369
|
+
mismatches.push({
|
|
370
|
+
path: formatPath([...path, `@${dom}`]),
|
|
371
|
+
type: "attribute",
|
|
372
|
+
expected: expectedStr,
|
|
373
|
+
actual: actualValue,
|
|
374
|
+
domPath: getDOMPath(domElement)
|
|
397
375
|
});
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
const vChildren = getVNodeChildren({ [tagName]: props });
|
|
379
|
+
const dChildren = getSignificantDOMChildren(domElement);
|
|
380
|
+
if (vChildren.length !== dChildren.length) {
|
|
381
|
+
mismatches.push({
|
|
382
|
+
path: formatPath([...path, "children"]),
|
|
383
|
+
type: "children_count",
|
|
384
|
+
expected: vChildren.length,
|
|
385
|
+
actual: dChildren.length,
|
|
386
|
+
domPath: getDOMPath(domElement)
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const maxChildren = Math.max(vChildren.length, dChildren.length);
|
|
390
|
+
for (let i = 0; i < maxChildren; i++) {
|
|
391
|
+
const vChild = vChildren[i];
|
|
392
|
+
const dChild = dChildren[i];
|
|
393
|
+
if (vChild && dChild) {
|
|
394
|
+
const childMismatches = detectMismatch(
|
|
395
|
+
dChild,
|
|
396
|
+
vChild,
|
|
397
|
+
[...path, `children[${i}]`]
|
|
398
|
+
);
|
|
399
|
+
mismatches.push(...childMismatches);
|
|
400
|
+
} else if (vChild && !dChild) {
|
|
401
|
+
mismatches.push({
|
|
402
|
+
path: formatPath([...path, `children[${i}]`]),
|
|
403
|
+
type: "missing_dom_child",
|
|
404
|
+
expected: describeVNode(vChild),
|
|
405
|
+
actual: null,
|
|
406
|
+
domPath: getDOMPath(domElement)
|
|
407
|
+
});
|
|
408
|
+
} else if (!vChild && dChild) {
|
|
409
|
+
mismatches.push({
|
|
410
|
+
path: formatPath([...path, `children[${i}]`]),
|
|
411
|
+
type: "extra_dom_child",
|
|
412
|
+
expected: null,
|
|
413
|
+
actual: describeNode(dChild),
|
|
414
|
+
domPath: getDOMPath(domElement)
|
|
411
415
|
});
|
|
412
|
-
this.state = null;
|
|
413
|
-
this.isHydrated = false;
|
|
414
|
-
componentInstances.delete(this.element);
|
|
415
|
-
},
|
|
416
|
-
// Set state (for components with state)
|
|
417
|
-
setState(newState) {
|
|
418
|
-
if (!this.state) {
|
|
419
|
-
this.state = {};
|
|
420
|
-
}
|
|
421
|
-
const oldState = { ...this.state };
|
|
422
|
-
this.state = typeof newState === "function" ? { ...this.state, ...newState(this.state) } : { ...this.state, ...newState };
|
|
423
|
-
this.rerender();
|
|
424
|
-
if (this.onStateChange) {
|
|
425
|
-
this.onStateChange(this.state, oldState);
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
// Add event listener that will be cleaned up on destroy
|
|
429
|
-
addEventListener(targetElement, event, handler) {
|
|
430
|
-
if (targetElement.addEventListener) {
|
|
431
|
-
targetElement.addEventListener(event, handler);
|
|
432
|
-
this.eventListeners.push({ element: targetElement, event, handler });
|
|
433
|
-
}
|
|
434
416
|
}
|
|
435
|
-
};
|
|
436
|
-
componentInstances.set(element, instance);
|
|
437
|
-
if (element && typeof element.setAttribute === "function") {
|
|
438
|
-
element.__coherentInstance = instance;
|
|
439
|
-
if (!element.hasAttribute("data-coherent-component")) {
|
|
440
|
-
element.setAttribute("data-coherent-component", "true");
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
const componentProps = { ...instance.props };
|
|
444
|
-
if (instance.component.__stateContainer) {
|
|
445
|
-
if (instance.state) {
|
|
446
|
-
instance.component.__stateContainer.setState(instance.state);
|
|
447
|
-
}
|
|
448
|
-
instance.setState = (newState) => {
|
|
449
|
-
instance.component.__stateContainer.setState(newState);
|
|
450
|
-
const updatedState = instance.component.__stateContainer.getState();
|
|
451
|
-
instance.state = updatedState;
|
|
452
|
-
instance.rerender();
|
|
453
|
-
};
|
|
454
417
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
418
|
+
return mismatches;
|
|
419
|
+
}
|
|
420
|
+
function reportMismatches(mismatches, options = {}) {
|
|
421
|
+
if (!mismatches || mismatches.length === 0) return;
|
|
422
|
+
const { componentName = "Unknown", strict = false } = options;
|
|
423
|
+
const header = `[Coherent.js] Hydration mismatch detected in "${componentName}"!
|
|
424
|
+
Found ${mismatches.length} difference(s) between server and client:
|
|
425
|
+
`;
|
|
426
|
+
const details = mismatches.map((m, i) => {
|
|
427
|
+
return `
|
|
428
|
+
${i + 1}. ${m.type} at ${m.path}
|
|
429
|
+
DOM path: ${m.domPath}
|
|
430
|
+
Expected: ${JSON.stringify(m.expected)}
|
|
431
|
+
Actual: ${JSON.stringify(m.actual)}`;
|
|
432
|
+
}).join("");
|
|
433
|
+
const advice = "\n\nThis usually happens when:\n - Server renders with different data than client\n - Using Date.now(), Math.random(), or browser-only APIs during render\n - Component is not pure (has side effects during render)\n";
|
|
434
|
+
console.warn(header + details + advice);
|
|
435
|
+
if (strict) {
|
|
436
|
+
throw new Error(`Hydration failed: ${mismatches.length} mismatch(es) found. See console for details.`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function getSignificantDOMChildren(element) {
|
|
440
|
+
if (!element || !element.childNodes) return [];
|
|
441
|
+
return Array.from(element.childNodes).filter((node) => {
|
|
442
|
+
if (node.nodeType === 1) return true;
|
|
443
|
+
if (node.nodeType === 3) {
|
|
444
|
+
return node.textContent && node.textContent.trim().length > 0;
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
function getDOMChildAtIndex(parent, index) {
|
|
450
|
+
const children = getSignificantDOMChildren(parent);
|
|
451
|
+
return children[index] || null;
|
|
452
|
+
}
|
|
453
|
+
function getDOMPath(element) {
|
|
454
|
+
if (!element || !element.tagName) return "(unknown)";
|
|
455
|
+
const parts = [];
|
|
456
|
+
let current = element;
|
|
457
|
+
while (current && current.tagName) {
|
|
458
|
+
let selector = current.tagName.toLowerCase();
|
|
459
|
+
if (current.id) {
|
|
460
|
+
selector += `#${current.id}`;
|
|
461
|
+
} else if (current.className && typeof current.className === "string") {
|
|
462
|
+
const classes = current.className.trim().split(/\s+/).slice(0, 2);
|
|
463
|
+
if (classes.length > 0 && classes[0]) {
|
|
464
|
+
selector += `.${classes.join(".")}`;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
parts.unshift(selector);
|
|
468
|
+
current = current.parentElement;
|
|
469
|
+
if (parts.length > 5) {
|
|
470
|
+
parts.unshift("...");
|
|
471
|
+
break;
|
|
459
472
|
}
|
|
460
473
|
}
|
|
461
|
-
|
|
462
|
-
return instance;
|
|
474
|
+
return parts.join(" > ");
|
|
463
475
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
476
|
+
function describeVNode(vNode) {
|
|
477
|
+
if (typeof vNode === "string" || typeof vNode === "number") {
|
|
478
|
+
return `text: "${String(vNode).substring(0, 50)}"`;
|
|
479
|
+
}
|
|
480
|
+
if (Array.isArray(vNode)) {
|
|
481
|
+
return `array[${vNode.length}]`;
|
|
482
|
+
}
|
|
483
|
+
if (typeof vNode === "object" && vNode !== null) {
|
|
484
|
+
const tagName = Object.keys(vNode)[0];
|
|
485
|
+
return `<${tagName}>`;
|
|
486
|
+
}
|
|
487
|
+
return String(vNode);
|
|
467
488
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
489
|
+
function describeNode(node) {
|
|
490
|
+
if (!node) return "(null)";
|
|
491
|
+
if (node.nodeType === 3) {
|
|
492
|
+
return `text: "${(node.textContent || "").substring(0, 50)}"`;
|
|
493
|
+
}
|
|
494
|
+
if (node.nodeType === 1) {
|
|
495
|
+
return `<${node.tagName.toLowerCase()}>`;
|
|
496
|
+
}
|
|
497
|
+
return `node(type=${node.nodeType})`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/hydrate.js
|
|
501
|
+
function hydrate(component, container, options = {}) {
|
|
502
|
+
if (typeof component !== "function") {
|
|
503
|
+
throw new Error(
|
|
504
|
+
`hydrate() requires a component function, received: ${typeof component}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
if (!container || typeof container.getAttribute !== "function") {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`hydrate() requires a valid DOM element as container, received: ${container === null ? "null" : typeof container}`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
eventDelegation.initialize();
|
|
513
|
+
const {
|
|
514
|
+
initialState: providedState,
|
|
515
|
+
detectMismatch: shouldDetectMismatch = true,
|
|
516
|
+
strict = false,
|
|
517
|
+
onMismatch,
|
|
518
|
+
props: additionalProps = {}
|
|
519
|
+
} = options;
|
|
520
|
+
let state = providedState ?? extractState(container) ?? {};
|
|
521
|
+
const eventListeners = [];
|
|
522
|
+
const registeredHandlerIds = /* @__PURE__ */ new Set();
|
|
523
|
+
const componentRef = {
|
|
524
|
+
getState: () => state,
|
|
525
|
+
setState: (newState) => {
|
|
526
|
+
if (typeof newState === "function") {
|
|
527
|
+
state = { ...state, ...newState(state) };
|
|
528
|
+
} else {
|
|
529
|
+
state = { ...state, ...newState };
|
|
480
530
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
531
|
+
doRerender();
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
const componentProps = { ...additionalProps, ...state };
|
|
535
|
+
let virtualDOM = component(componentProps);
|
|
536
|
+
if (shouldDetectMismatch) {
|
|
537
|
+
const mismatches = detectMismatch(container, virtualDOM);
|
|
538
|
+
if (mismatches.length > 0) {
|
|
539
|
+
if (onMismatch) {
|
|
540
|
+
onMismatch(mismatches);
|
|
491
541
|
} else {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
542
|
+
reportMismatches(mismatches, {
|
|
543
|
+
componentName: component.name || "Anonymous",
|
|
544
|
+
strict
|
|
545
|
+
});
|
|
497
546
|
}
|
|
498
|
-
} else {
|
|
499
|
-
console.warn(`Event handler not found for ID: ${eventId}`);
|
|
500
547
|
}
|
|
548
|
+
}
|
|
549
|
+
registerEventHandlers(container, virtualDOM, componentRef, registeredHandlerIds);
|
|
550
|
+
function doRerender() {
|
|
551
|
+
const newProps = { ...additionalProps, ...state };
|
|
552
|
+
virtualDOM = component(newProps);
|
|
553
|
+
patchDOM(container, virtualDOM);
|
|
554
|
+
registerEventHandlers(container, virtualDOM, componentRef, registeredHandlerIds);
|
|
555
|
+
}
|
|
556
|
+
function unmount() {
|
|
557
|
+
for (const handlerId of registeredHandlerIds) {
|
|
558
|
+
handlerRegistry.unregister(handlerId);
|
|
559
|
+
}
|
|
560
|
+
registeredHandlerIds.clear();
|
|
561
|
+
for (const { element, event, handler, options: options2 } of eventListeners) {
|
|
562
|
+
element.removeEventListener(event, handler, options2);
|
|
563
|
+
}
|
|
564
|
+
eventListeners.length = 0;
|
|
565
|
+
container.removeAttribute("data-coherent-hydrated");
|
|
566
|
+
}
|
|
567
|
+
function rerender(newProps) {
|
|
568
|
+
if (newProps) {
|
|
569
|
+
Object.assign(additionalProps, newProps);
|
|
570
|
+
}
|
|
571
|
+
doRerender();
|
|
572
|
+
}
|
|
573
|
+
function getState() {
|
|
574
|
+
return { ...state };
|
|
575
|
+
}
|
|
576
|
+
function setState(newState) {
|
|
577
|
+
componentRef.setState(newState);
|
|
578
|
+
}
|
|
579
|
+
container.setAttribute("data-coherent-hydrated", "true");
|
|
580
|
+
return {
|
|
581
|
+
unmount,
|
|
582
|
+
rerender,
|
|
583
|
+
getState,
|
|
584
|
+
setState
|
|
501
585
|
};
|
|
502
586
|
}
|
|
503
|
-
function
|
|
504
|
-
if (!
|
|
587
|
+
function registerEventHandlers(domElement, vNode, componentRef, handlerIds) {
|
|
588
|
+
if (!vNode || typeof vNode !== "object" || Array.isArray(vNode)) {
|
|
505
589
|
return;
|
|
506
590
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
591
|
+
const tagName = Object.keys(vNode)[0];
|
|
592
|
+
const props = vNode[tagName];
|
|
593
|
+
if (!props || typeof props !== "object") {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const eventProps = Object.keys(props).filter(
|
|
597
|
+
(key) => key.startsWith("on") && typeof props[key] === "function"
|
|
598
|
+
);
|
|
599
|
+
for (const eventProp of eventProps) {
|
|
600
|
+
const eventType = eventProp.slice(2).toLowerCase();
|
|
601
|
+
const handler = props[eventProp];
|
|
602
|
+
const handlerId = `${tagName}-${eventType}-${Math.random().toString(36).slice(2, 9)}`;
|
|
603
|
+
handlerRegistry.register(handlerId, handler, componentRef);
|
|
604
|
+
handlerIds.add(handlerId);
|
|
605
|
+
const attrName = `data-coherent-${eventType}`;
|
|
606
|
+
if (domElement.setAttribute) {
|
|
607
|
+
domElement.setAttribute(attrName, handlerId);
|
|
517
608
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
609
|
+
}
|
|
610
|
+
const children = getVNodeChildren2(props);
|
|
611
|
+
const domChildren = getSignificantDOMChildren2(domElement);
|
|
612
|
+
children.forEach((child, index) => {
|
|
613
|
+
if (child && typeof child === "object" && !Array.isArray(child) && domChildren[index]) {
|
|
614
|
+
registerEventHandlers(domChildren[index], child, componentRef, handlerIds);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function patchDOM(domElement, vNode) {
|
|
619
|
+
if (!vNode || !domElement) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (typeof vNode === "string" || typeof vNode === "number") {
|
|
623
|
+
if (domElement.textContent !== String(vNode)) {
|
|
624
|
+
domElement.textContent = String(vNode);
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (Array.isArray(vNode)) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (typeof vNode !== "object") {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const tagName = Object.keys(vNode)[0];
|
|
635
|
+
const props = vNode[tagName] || {};
|
|
636
|
+
const attributeMap = {
|
|
637
|
+
className: "class",
|
|
638
|
+
htmlFor: "for"
|
|
639
|
+
};
|
|
640
|
+
for (const [key, value] of Object.entries(props)) {
|
|
641
|
+
if (key === "children" || key === "text" || key.startsWith("on")) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const attrName = attributeMap[key] || key;
|
|
645
|
+
if (value === true) {
|
|
646
|
+
domElement.setAttribute(attrName, "");
|
|
647
|
+
} else if (value === false || value === null || value === void 0) {
|
|
648
|
+
domElement.removeAttribute(attrName);
|
|
649
|
+
} else if (domElement.getAttribute(attrName) !== String(value)) {
|
|
650
|
+
domElement.setAttribute(attrName, String(value));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (props.text !== void 0) {
|
|
654
|
+
const textContent = String(props.text);
|
|
655
|
+
if (domElement.textContent !== textContent) {
|
|
656
|
+
domElement.textContent = textContent;
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const children = getVNodeChildren2(props);
|
|
661
|
+
const domChildren = getSignificantDOMChildren2(domElement);
|
|
662
|
+
children.forEach((child, index) => {
|
|
663
|
+
if (domChildren[index]) {
|
|
664
|
+
patchDOM(domChildren[index], child);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
function getVNodeChildren2(props) {
|
|
669
|
+
if (!props) return [];
|
|
670
|
+
if (props.children) {
|
|
671
|
+
return Array.isArray(props.children) ? props.children : [props.children];
|
|
672
|
+
}
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
function getSignificantDOMChildren2(element) {
|
|
676
|
+
if (!element || !element.childNodes) return [];
|
|
677
|
+
return Array.from(element.childNodes).filter((node) => {
|
|
678
|
+
if (node.nodeType === 1) return true;
|
|
679
|
+
if (node.nodeType === 3) {
|
|
680
|
+
return node.textContent && node.textContent.trim().length > 0;
|
|
681
|
+
}
|
|
682
|
+
return false;
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/hmr/cleanup-tracker.js
|
|
687
|
+
var CleanupTracker = class {
|
|
688
|
+
constructor() {
|
|
689
|
+
this.moduleResources = /* @__PURE__ */ new Map();
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Create a tracked context for a module
|
|
693
|
+
*
|
|
694
|
+
* Returns an object with tracked versions of setTimeout, setInterval,
|
|
695
|
+
* addEventListener, and fetch that automatically clean up on module disposal.
|
|
696
|
+
*
|
|
697
|
+
* @param {string} moduleId - Unique identifier for the module
|
|
698
|
+
* @returns {Object} Tracked context with setTimeout, setInterval, etc.
|
|
699
|
+
*/
|
|
700
|
+
createContext(moduleId) {
|
|
701
|
+
const resources = {
|
|
702
|
+
timers: /* @__PURE__ */ new Set(),
|
|
703
|
+
intervals: /* @__PURE__ */ new Set(),
|
|
704
|
+
listeners: [],
|
|
705
|
+
abortControllers: /* @__PURE__ */ new Set()
|
|
706
|
+
};
|
|
707
|
+
this.moduleResources.set(moduleId, resources);
|
|
708
|
+
const context = {
|
|
709
|
+
/**
|
|
710
|
+
* Tracked setTimeout - auto-removes from tracking on completion
|
|
711
|
+
* @param {Function} callback - Function to execute
|
|
712
|
+
* @param {number} delay - Delay in milliseconds
|
|
713
|
+
* @param {...*} args - Additional arguments to pass to callback
|
|
714
|
+
* @returns {number} Timer ID
|
|
715
|
+
*/
|
|
716
|
+
setTimeout: (callback, delay, ...args) => {
|
|
717
|
+
const id = setTimeout(
|
|
718
|
+
(...a) => {
|
|
719
|
+
resources.timers.delete(id);
|
|
720
|
+
callback(...a);
|
|
721
|
+
},
|
|
722
|
+
delay,
|
|
723
|
+
...args
|
|
724
|
+
);
|
|
725
|
+
resources.timers.add(id);
|
|
726
|
+
return id;
|
|
727
|
+
},
|
|
728
|
+
/**
|
|
729
|
+
* Tracked setInterval - stores in intervals set until cleared
|
|
730
|
+
* @param {Function} callback - Function to execute
|
|
731
|
+
* @param {number} delay - Interval in milliseconds
|
|
732
|
+
* @param {...*} args - Additional arguments to pass to callback
|
|
733
|
+
* @returns {number} Interval ID
|
|
734
|
+
*/
|
|
735
|
+
setInterval: (callback, delay, ...args) => {
|
|
736
|
+
const id = setInterval(callback, delay, ...args);
|
|
737
|
+
resources.intervals.add(id);
|
|
738
|
+
return id;
|
|
739
|
+
},
|
|
740
|
+
/**
|
|
741
|
+
* Clear a tracked timeout
|
|
742
|
+
* @param {number} id - Timer ID to clear
|
|
743
|
+
*/
|
|
744
|
+
clearTimeout: (id) => {
|
|
745
|
+
resources.timers.delete(id);
|
|
746
|
+
clearTimeout(id);
|
|
747
|
+
},
|
|
748
|
+
/**
|
|
749
|
+
* Clear a tracked interval
|
|
750
|
+
* @param {number} id - Interval ID to clear
|
|
751
|
+
*/
|
|
752
|
+
clearInterval: (id) => {
|
|
753
|
+
resources.intervals.delete(id);
|
|
754
|
+
clearInterval(id);
|
|
755
|
+
},
|
|
756
|
+
/**
|
|
757
|
+
* Tracked addEventListener - stores listener info for removal on cleanup
|
|
758
|
+
* @param {EventTarget} target - Element or object to attach listener to
|
|
759
|
+
* @param {string} event - Event type
|
|
760
|
+
* @param {Function} handler - Event handler function
|
|
761
|
+
* @param {Object|boolean} [options] - Listener options
|
|
762
|
+
*/
|
|
763
|
+
addEventListener: (target, event, handler, options) => {
|
|
764
|
+
target.addEventListener(event, handler, options);
|
|
765
|
+
resources.listeners.push({ target, event, handler, options });
|
|
766
|
+
},
|
|
767
|
+
/**
|
|
768
|
+
* Create a tracked AbortController
|
|
769
|
+
* @returns {AbortController} Tracked AbortController
|
|
770
|
+
*/
|
|
771
|
+
createAbortController: () => {
|
|
772
|
+
const controller = new AbortController();
|
|
773
|
+
resources.abortControllers.add(controller);
|
|
774
|
+
return controller;
|
|
775
|
+
},
|
|
776
|
+
/**
|
|
777
|
+
* Tracked fetch - creates AbortController automatically, cleans up on completion
|
|
778
|
+
* @param {string|URL} url - URL to fetch
|
|
779
|
+
* @param {Object} [options] - Fetch options
|
|
780
|
+
* @returns {Promise<Response>} Fetch promise
|
|
781
|
+
*/
|
|
782
|
+
fetch: (url, options = {}) => {
|
|
783
|
+
const controller = new AbortController();
|
|
784
|
+
resources.abortControllers.add(controller);
|
|
785
|
+
const mergedOptions = {
|
|
786
|
+
...options,
|
|
787
|
+
signal: controller.signal
|
|
788
|
+
};
|
|
789
|
+
return fetch(url, mergedOptions).finally(() => {
|
|
790
|
+
resources.abortControllers.delete(controller);
|
|
599
791
|
});
|
|
600
792
|
}
|
|
793
|
+
};
|
|
794
|
+
return context;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Cleanup all resources for a module
|
|
798
|
+
*
|
|
799
|
+
* Called during HMR module disposal. Clears all timers, intervals,
|
|
800
|
+
* removes all event listeners, and aborts all pending fetch requests.
|
|
801
|
+
*
|
|
802
|
+
* @param {string} moduleId - Module identifier to clean up
|
|
803
|
+
*/
|
|
804
|
+
cleanup(moduleId) {
|
|
805
|
+
const resources = this.moduleResources.get(moduleId);
|
|
806
|
+
if (!resources) return;
|
|
807
|
+
for (const id of resources.timers) {
|
|
808
|
+
clearTimeout(id);
|
|
809
|
+
}
|
|
810
|
+
resources.timers.clear();
|
|
811
|
+
for (const id of resources.intervals) {
|
|
812
|
+
clearInterval(id);
|
|
813
|
+
}
|
|
814
|
+
resources.intervals.clear();
|
|
815
|
+
for (const { target, event, handler, options } of resources.listeners) {
|
|
816
|
+
try {
|
|
817
|
+
target.removeEventListener(event, handler, options);
|
|
818
|
+
} catch {
|
|
819
|
+
}
|
|
601
820
|
}
|
|
821
|
+
resources.listeners.length = 0;
|
|
822
|
+
for (const controller of resources.abortControllers) {
|
|
823
|
+
try {
|
|
824
|
+
controller.abort();
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
resources.abortControllers.clear();
|
|
829
|
+
this.moduleResources.delete(moduleId);
|
|
602
830
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
831
|
+
/**
|
|
832
|
+
* Check for potential resource leaks (for development mode)
|
|
833
|
+
*
|
|
834
|
+
* Logs warnings if resources weren't cleaned up before module disposal.
|
|
835
|
+
* Call this before cleanup() to detect potential leaks.
|
|
836
|
+
*
|
|
837
|
+
* @param {string} moduleId - Module identifier to check
|
|
838
|
+
*/
|
|
839
|
+
checkForLeaks(moduleId) {
|
|
840
|
+
const resources = this.moduleResources.get(moduleId);
|
|
841
|
+
if (!resources) return;
|
|
842
|
+
const warnings = [];
|
|
843
|
+
if (resources.timers.size > 0) {
|
|
844
|
+
warnings.push(`${resources.timers.size} timer(s) not cleaned up`);
|
|
614
845
|
}
|
|
615
|
-
if (
|
|
616
|
-
|
|
846
|
+
if (resources.intervals.size > 0) {
|
|
847
|
+
warnings.push(`${resources.intervals.size} interval(s) not cleaned up`);
|
|
617
848
|
}
|
|
618
|
-
if (
|
|
619
|
-
|
|
849
|
+
if (resources.listeners.length > 0) {
|
|
850
|
+
warnings.push(`${resources.listeners.length} listener(s) not cleaned up`);
|
|
620
851
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
852
|
+
if (resources.abortControllers.size > 0) {
|
|
853
|
+
warnings.push(
|
|
854
|
+
`${resources.abortControllers.size} pending fetch(es) not aborted`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (warnings.length > 0) {
|
|
858
|
+
console.warn(`[HMR] Potential leak in module ${moduleId}: ${warnings.join(", ")}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Check if a module has tracked resources
|
|
863
|
+
* @param {string} moduleId - Module identifier
|
|
864
|
+
* @returns {boolean} True if module has resources
|
|
865
|
+
*/
|
|
866
|
+
hasResources(moduleId) {
|
|
867
|
+
return this.moduleResources.has(moduleId);
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Get resource counts for a module (for testing/debugging)
|
|
871
|
+
* @param {string} moduleId - Module identifier
|
|
872
|
+
* @returns {Object|null} Resource counts or null if module not tracked
|
|
873
|
+
*/
|
|
874
|
+
getResourceCounts(moduleId) {
|
|
875
|
+
const resources = this.moduleResources.get(moduleId);
|
|
876
|
+
if (!resources) return null;
|
|
877
|
+
return {
|
|
878
|
+
timers: resources.timers.size,
|
|
879
|
+
intervals: resources.intervals.size,
|
|
880
|
+
listeners: resources.listeners.length,
|
|
881
|
+
abortControllers: resources.abortControllers.size
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
var cleanupTracker = new CleanupTracker();
|
|
886
|
+
|
|
887
|
+
// src/hmr/state-capturer.js
|
|
888
|
+
var StateCapturer = class {
|
|
889
|
+
constructor() {
|
|
890
|
+
this.capturedInputs = /* @__PURE__ */ new Map();
|
|
891
|
+
this.scrollPositions = /* @__PURE__ */ new Map();
|
|
892
|
+
this.layoutSnapshot = null;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Generate a stable key for an input element
|
|
896
|
+
*
|
|
897
|
+
* Uses multiple factors to identify inputs across HMR updates:
|
|
898
|
+
* 1. ID (most stable)
|
|
899
|
+
* 2. Name + type
|
|
900
|
+
* 3. Form context
|
|
901
|
+
* 4. DOM path (fallback)
|
|
902
|
+
*
|
|
903
|
+
* @param {HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement} input - Input element
|
|
904
|
+
* @returns {string} Stable key for the input
|
|
905
|
+
*/
|
|
906
|
+
getInputKey(input) {
|
|
907
|
+
const parts = [];
|
|
908
|
+
if (input.id) {
|
|
909
|
+
parts.push(`#${input.id}`);
|
|
910
|
+
return parts.join(":");
|
|
911
|
+
}
|
|
912
|
+
if (input.name) {
|
|
913
|
+
parts.push(`[name="${input.name}"]`);
|
|
914
|
+
}
|
|
915
|
+
if (input.type) {
|
|
916
|
+
parts.push(`[type="${input.type}"]`);
|
|
917
|
+
}
|
|
918
|
+
if (input.form?.id) {
|
|
919
|
+
parts.push(`form#${input.form.id}`);
|
|
920
|
+
}
|
|
921
|
+
if (parts.length === 0) {
|
|
922
|
+
parts.push(this.getElementPath(input));
|
|
923
|
+
}
|
|
924
|
+
return parts.join(":");
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Build a CSS-like path for an element
|
|
928
|
+
*
|
|
929
|
+
* @param {HTMLElement} element - Element to build path for
|
|
930
|
+
* @returns {string} CSS-like path (e.g., "form > div:nth-of-type(2) > input")
|
|
931
|
+
*/
|
|
932
|
+
getElementPath(element) {
|
|
933
|
+
const path = [];
|
|
934
|
+
let current = element;
|
|
935
|
+
while (current && current !== document.body && path.length < 10) {
|
|
936
|
+
let selector = current.tagName.toLowerCase();
|
|
937
|
+
if (current.className && typeof current.className === "string") {
|
|
938
|
+
const classes = current.className.trim().split(/\s+/).slice(0, 2);
|
|
939
|
+
if (classes.length > 0 && classes[0]) {
|
|
940
|
+
selector += `.${classes.join(".")}`;
|
|
643
941
|
}
|
|
644
942
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const eventAttr = elementWithEvent.getAttribute(eventName);
|
|
653
|
-
if (eventAttr) {
|
|
654
|
-
console.warn(
|
|
655
|
-
`[Coherent.js] Inline event attribute "${eventName}="${eventAttr}" found but not supported.
|
|
656
|
-
For security and CSP compliance, use one of these alternatives:
|
|
657
|
-
1. Function-based handlers: Pass functions directly in virtual DOM
|
|
658
|
-
2. Data-action registry: <button data-action="actionId" data-event="click">
|
|
659
|
-
3. Event registry: <button data-coherent-event="handlerId" data-coherent-event-type="click">
|
|
660
|
-
See documentation for details.`
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
});
|
|
665
|
-
const dataActionElements = element.querySelectorAll("[data-action]");
|
|
666
|
-
dataActionElements.forEach((actionElement) => {
|
|
667
|
-
if (!actionElement || typeof actionElement.getAttribute !== "function") return;
|
|
668
|
-
const actionId = actionElement.getAttribute("data-action");
|
|
669
|
-
const eventType = actionElement.getAttribute("data-event") || "click";
|
|
670
|
-
if (actionId) {
|
|
671
|
-
let handlerFunc = null;
|
|
672
|
-
if (typeof window !== "undefined" && window.__coherentActionRegistry && window.__coherentActionRegistry[actionId]) {
|
|
673
|
-
handlerFunc = window.__coherentActionRegistry[actionId];
|
|
674
|
-
} else {
|
|
675
|
-
console.warn(`No handler found for action ${actionId}`, window.__coherentActionRegistry);
|
|
676
|
-
}
|
|
677
|
-
if (handlerFunc && typeof handlerFunc === "function") {
|
|
678
|
-
if (typeof actionElement.hasAttribute === "function" && !actionElement.hasAttribute(`data-hydrated-${eventType}`)) {
|
|
679
|
-
actionElement.setAttribute(`data-hydrated-${eventType}`, "true");
|
|
680
|
-
const handler = (e) => {
|
|
681
|
-
try {
|
|
682
|
-
let componentElement = actionElement;
|
|
683
|
-
while (componentElement && !componentElement.hasAttribute("data-coherent-component")) {
|
|
684
|
-
componentElement = componentElement.parentElement;
|
|
685
|
-
}
|
|
686
|
-
if (componentElement && componentElement.__coherentInstance) {
|
|
687
|
-
const instance2 = componentElement.__coherentInstance;
|
|
688
|
-
const state = instance2.state || {};
|
|
689
|
-
const setState = instance2.setState || (() => {
|
|
690
|
-
});
|
|
691
|
-
handlerFunc.call(actionElement, e, state, setState);
|
|
692
|
-
} else {
|
|
693
|
-
handlerFunc.call(actionElement, e);
|
|
694
|
-
}
|
|
695
|
-
} catch (_error) {
|
|
696
|
-
console.warn(`Error executing action handler for ${actionId}:`, _error);
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
if (typeof actionElement.addEventListener === "function") {
|
|
700
|
-
actionElement.addEventListener(eventType, handler);
|
|
701
|
-
if (instance && instance.eventListeners && Array.isArray(instance.eventListeners)) {
|
|
702
|
-
instance.eventListeners.push({
|
|
703
|
-
element: actionElement,
|
|
704
|
-
event: eventType,
|
|
705
|
-
handler
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
943
|
+
if (current.parentElement) {
|
|
944
|
+
const siblings = current.parentElement.querySelectorAll(
|
|
945
|
+
`:scope > ${current.tagName.toLowerCase()}`
|
|
946
|
+
);
|
|
947
|
+
if (siblings.length > 1) {
|
|
948
|
+
const index = Array.from(siblings).indexOf(current);
|
|
949
|
+
selector += `:nth-of-type(${index + 1})`;
|
|
710
950
|
}
|
|
711
951
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
952
|
+
path.unshift(selector);
|
|
953
|
+
current = current.parentElement;
|
|
954
|
+
}
|
|
955
|
+
return path.join(" > ");
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Capture all form input states
|
|
959
|
+
*
|
|
960
|
+
* Iterates through all input, textarea, and select elements,
|
|
961
|
+
* capturing their values, selection state, and checked state.
|
|
962
|
+
*
|
|
963
|
+
* @returns {Map<string, Object>} Map of input keys to their captured state
|
|
964
|
+
*/
|
|
965
|
+
captureFormState() {
|
|
966
|
+
this.capturedInputs.clear();
|
|
967
|
+
const inputs = document.querySelectorAll("input, textarea, select");
|
|
968
|
+
for (const input of inputs) {
|
|
969
|
+
const key = this.getInputKey(input);
|
|
970
|
+
const state = {
|
|
971
|
+
value: input.value,
|
|
972
|
+
type: input.type || input.tagName.toLowerCase()
|
|
973
|
+
};
|
|
974
|
+
if (typeof input.selectionStart === "number" && (input.type === "text" || input.type === "search" || input.type === "url" || input.type === "tel" || input.type === "password" || input.tagName.toLowerCase() === "textarea")) {
|
|
975
|
+
state.selectionStart = input.selectionStart;
|
|
976
|
+
state.selectionEnd = input.selectionEnd;
|
|
977
|
+
}
|
|
978
|
+
if (input.type === "checkbox" || input.type === "radio") {
|
|
979
|
+
state.checked = input.checked;
|
|
980
|
+
}
|
|
981
|
+
this.capturedInputs.set(key, state);
|
|
982
|
+
}
|
|
983
|
+
return this.capturedInputs;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Restore form input states after HMR update
|
|
987
|
+
*
|
|
988
|
+
* Finds inputs by their captured keys and restores their values,
|
|
989
|
+
* only if the input type matches (to avoid corrupting data).
|
|
990
|
+
*/
|
|
991
|
+
restoreFormState() {
|
|
992
|
+
for (const [key, state] of this.capturedInputs) {
|
|
993
|
+
const inputs = this.findInputsByKey(key);
|
|
994
|
+
for (const input of inputs) {
|
|
995
|
+
const currentType = input.type || input.tagName.toLowerCase();
|
|
996
|
+
if (currentType !== state.type) {
|
|
997
|
+
continue;
|
|
722
998
|
}
|
|
723
|
-
if (
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
} catch (_error) {
|
|
733
|
-
console.warn(`Error executing coherent event handler:`, _error);
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
if (typeof elementWithCoherentEvent.addEventListener === "function") {
|
|
737
|
-
elementWithCoherentEvent.addEventListener(eventType, handler);
|
|
738
|
-
if (instance.eventListeners && Array.isArray(instance.eventListeners)) {
|
|
739
|
-
instance.eventListeners.push({
|
|
740
|
-
element: elementWithCoherentEvent,
|
|
741
|
-
event: eventType,
|
|
742
|
-
handler
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
}
|
|
999
|
+
if (state.checked !== void 0) {
|
|
1000
|
+
input.checked = state.checked;
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
input.value = state.value;
|
|
1004
|
+
if (state.selectionStart !== void 0 && document.activeElement !== input) {
|
|
1005
|
+
try {
|
|
1006
|
+
input.setSelectionRange(state.selectionStart, state.selectionEnd);
|
|
1007
|
+
} catch {
|
|
746
1008
|
}
|
|
747
1009
|
}
|
|
748
1010
|
}
|
|
749
|
-
}
|
|
750
|
-
} catch (_error) {
|
|
751
|
-
console.warn("Error attaching event listeners:", _error);
|
|
1011
|
+
}
|
|
752
1012
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Find inputs matching a captured key
|
|
1015
|
+
*
|
|
1016
|
+
* @param {string} key - Captured input key
|
|
1017
|
+
* @returns {HTMLElement[]} Array of matching input elements
|
|
1018
|
+
*/
|
|
1019
|
+
findInputsByKey(key) {
|
|
1020
|
+
if (key.startsWith("#")) {
|
|
1021
|
+
const id = key.slice(1);
|
|
1022
|
+
const el = document.getElementById(id);
|
|
1023
|
+
return el ? [el] : [];
|
|
1024
|
+
}
|
|
1025
|
+
const nameMatch = key.match(/\[name="([^"]+)"\]/);
|
|
1026
|
+
if (nameMatch) {
|
|
1027
|
+
const name = nameMatch[1];
|
|
1028
|
+
const typeMatch = key.match(/\[type="([^"]+)"\]/);
|
|
1029
|
+
const type = typeMatch ? typeMatch[1] : null;
|
|
1030
|
+
let selector = `[name="${name}"]`;
|
|
1031
|
+
if (type) {
|
|
1032
|
+
selector += `[type="${type}"]`;
|
|
774
1033
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1034
|
+
return Array.from(document.querySelectorAll(selector));
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
const el = document.querySelector(key);
|
|
1038
|
+
return el ? [el] : [];
|
|
1039
|
+
} catch {
|
|
1040
|
+
return [];
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Capture scroll positions for window and scrollable containers
|
|
1045
|
+
*
|
|
1046
|
+
* Captures scroll positions for:
|
|
1047
|
+
* - Window (scrollX/scrollY)
|
|
1048
|
+
* - Elements with [data-coherent-scroll-preserve] attribute
|
|
1049
|
+
* - Elements with overflow that have actual scrolling
|
|
1050
|
+
*/
|
|
1051
|
+
captureScrollPositions() {
|
|
1052
|
+
this.scrollPositions.clear();
|
|
1053
|
+
this.scrollPositions.set("window", {
|
|
1054
|
+
top: window.scrollY,
|
|
1055
|
+
left: window.scrollX
|
|
1056
|
+
});
|
|
1057
|
+
const markedScrollables = document.querySelectorAll(
|
|
1058
|
+
"[data-coherent-scroll-preserve]"
|
|
1059
|
+
);
|
|
1060
|
+
for (const el of markedScrollables) {
|
|
1061
|
+
const key = this.getScrollableKey(el);
|
|
1062
|
+
this.scrollPositions.set(key, {
|
|
1063
|
+
top: el.scrollTop,
|
|
1064
|
+
left: el.scrollLeft
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
const overflowElements = document.querySelectorAll(
|
|
1068
|
+
'[style*="overflow"], [class]'
|
|
1069
|
+
);
|
|
1070
|
+
for (const el of overflowElements) {
|
|
1071
|
+
const style = window.getComputedStyle(el);
|
|
1072
|
+
const hasOverflow = style.overflow === "auto" || style.overflow === "scroll" || style.overflowY === "auto" || style.overflowY === "scroll" || style.overflowX === "auto" || style.overflowX === "scroll";
|
|
1073
|
+
if (hasOverflow && (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)) {
|
|
1074
|
+
const key = this.getScrollableKey(el);
|
|
1075
|
+
if (!this.scrollPositions.has(key)) {
|
|
1076
|
+
this.scrollPositions.set(key, {
|
|
1077
|
+
top: el.scrollTop,
|
|
1078
|
+
left: el.scrollLeft
|
|
1079
|
+
});
|
|
783
1080
|
}
|
|
784
1081
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1082
|
+
}
|
|
1083
|
+
return this.scrollPositions;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Generate a stable key for a scrollable element
|
|
1087
|
+
*
|
|
1088
|
+
* @param {HTMLElement} el - Scrollable element
|
|
1089
|
+
* @returns {string} Key for the element
|
|
1090
|
+
*/
|
|
1091
|
+
getScrollableKey(el) {
|
|
1092
|
+
if (el.id) {
|
|
1093
|
+
return `#${el.id}`;
|
|
1094
|
+
}
|
|
1095
|
+
const component = el.getAttribute("data-coherent-component");
|
|
1096
|
+
if (component) {
|
|
1097
|
+
return `[data-coherent-component="${component}"]`;
|
|
1098
|
+
}
|
|
1099
|
+
return this.getElementPath(el);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Capture layout snapshot for change detection
|
|
1103
|
+
*
|
|
1104
|
+
* Captures body dimensions and positions of anchor elements
|
|
1105
|
+
* (elements with data-coherent-component attribute).
|
|
1106
|
+
*/
|
|
1107
|
+
captureLayout() {
|
|
1108
|
+
this.layoutSnapshot = {
|
|
1109
|
+
bodyHeight: document.body.scrollHeight,
|
|
1110
|
+
bodyWidth: document.body.scrollWidth,
|
|
1111
|
+
anchors: /* @__PURE__ */ new Map()
|
|
1112
|
+
};
|
|
1113
|
+
const components = document.querySelectorAll("[data-coherent-component]");
|
|
1114
|
+
for (const el of components) {
|
|
1115
|
+
const rect = el.getBoundingClientRect();
|
|
1116
|
+
const key = this.getScrollableKey(el);
|
|
1117
|
+
this.layoutSnapshot.anchors.set(key, {
|
|
1118
|
+
top: rect.top,
|
|
1119
|
+
left: rect.left,
|
|
1120
|
+
width: rect.width,
|
|
1121
|
+
height: rect.height
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Check if layout changed significantly (>50px shift)
|
|
1127
|
+
*
|
|
1128
|
+
* Returns true if:
|
|
1129
|
+
* - Body dimensions changed by more than 50px
|
|
1130
|
+
* - Any anchor element position shifted by more than 50px
|
|
1131
|
+
*
|
|
1132
|
+
* @returns {boolean} True if layout changed significantly
|
|
1133
|
+
*/
|
|
1134
|
+
layoutChangedSignificantly() {
|
|
1135
|
+
if (!this.layoutSnapshot) {
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
const THRESHOLD = 50;
|
|
1139
|
+
const heightDiff = Math.abs(
|
|
1140
|
+
document.body.scrollHeight - this.layoutSnapshot.bodyHeight
|
|
1141
|
+
);
|
|
1142
|
+
const widthDiff = Math.abs(
|
|
1143
|
+
document.body.scrollWidth - this.layoutSnapshot.bodyWidth
|
|
1144
|
+
);
|
|
1145
|
+
if (heightDiff > THRESHOLD || widthDiff > THRESHOLD) {
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
for (const [key, oldRect] of this.layoutSnapshot.anchors) {
|
|
1149
|
+
const el = this.findElementByKey(key);
|
|
1150
|
+
if (!el) {
|
|
1151
|
+
continue;
|
|
797
1152
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
if (
|
|
802
|
-
|
|
803
|
-
newTodos[todoIndex].completed = !newTodos[todoIndex].completed;
|
|
804
|
-
instance.setState({ todos: newTodos });
|
|
1153
|
+
const newRect = el.getBoundingClientRect();
|
|
1154
|
+
const topDiff = Math.abs(newRect.top - oldRect.top);
|
|
1155
|
+
const leftDiff = Math.abs(newRect.left - oldRect.left);
|
|
1156
|
+
if (topDiff > THRESHOLD || leftDiff > THRESHOLD) {
|
|
1157
|
+
return true;
|
|
805
1158
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1159
|
+
}
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Find an element by its scrollable key
|
|
1164
|
+
*
|
|
1165
|
+
* @param {string} key - Element key
|
|
1166
|
+
* @returns {HTMLElement|null} Found element or null
|
|
1167
|
+
*/
|
|
1168
|
+
findElementByKey(key) {
|
|
1169
|
+
if (key === "window") {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
if (key.startsWith("#")) {
|
|
1173
|
+
return document.getElementById(key.slice(1));
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
return document.querySelector(key);
|
|
1177
|
+
} catch {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Restore scroll positions if layout hasn't changed significantly
|
|
1183
|
+
*
|
|
1184
|
+
* Logs a message if scroll restoration is skipped due to layout changes.
|
|
1185
|
+
*/
|
|
1186
|
+
restoreScrollPositions() {
|
|
1187
|
+
if (this.layoutChangedSignificantly()) {
|
|
1188
|
+
console.log("[HMR] Layout changed significantly, not restoring scroll");
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
const windowPos = this.scrollPositions.get("window");
|
|
1192
|
+
if (windowPos) {
|
|
1193
|
+
window.scrollTo(windowPos.left, windowPos.top);
|
|
1194
|
+
}
|
|
1195
|
+
for (const [key, pos] of this.scrollPositions) {
|
|
1196
|
+
if (key === "window") {
|
|
1197
|
+
continue;
|
|
820
1198
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
instance[action](event, target);
|
|
826
|
-
} catch (_error) {
|
|
827
|
-
console.warn(`Error executing custom action ${action}:`, _error);
|
|
828
|
-
}
|
|
829
|
-
} else {
|
|
830
|
-
if (typeof window !== "undefined" && window.__coherentActionRegistry && window.__coherentActionRegistry[action]) {
|
|
831
|
-
const handlerFunc = window.__coherentActionRegistry[action];
|
|
832
|
-
const state = instance ? instance.state || {} : {};
|
|
833
|
-
const setState = instance && instance.setState ? instance.setState.bind(instance) : (() => {
|
|
834
|
-
});
|
|
835
|
-
try {
|
|
836
|
-
handlerFunc(event, state, setState);
|
|
837
|
-
} catch (_error) {
|
|
838
|
-
console.warn(`Error executing action handler ${action}:`, _error);
|
|
839
|
-
}
|
|
840
|
-
} else {
|
|
841
|
-
}
|
|
1199
|
+
const el = this.findElementByKey(key);
|
|
1200
|
+
if (el) {
|
|
1201
|
+
el.scrollTop = pos.top;
|
|
1202
|
+
el.scrollLeft = pos.left;
|
|
842
1203
|
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Capture all state (form + scroll + layout)
|
|
1208
|
+
*
|
|
1209
|
+
* Convenience method that calls all capture methods.
|
|
1210
|
+
*/
|
|
1211
|
+
captureAll() {
|
|
1212
|
+
this.captureFormState();
|
|
1213
|
+
this.captureScrollPositions();
|
|
1214
|
+
this.captureLayout();
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Restore all state (form + scroll)
|
|
1218
|
+
*
|
|
1219
|
+
* Convenience method that calls all restore methods.
|
|
1220
|
+
*/
|
|
1221
|
+
restoreAll() {
|
|
1222
|
+
this.restoreFormState();
|
|
1223
|
+
this.restoreScrollPositions();
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Clear all captured state
|
|
1227
|
+
*/
|
|
1228
|
+
clear() {
|
|
1229
|
+
this.capturedInputs.clear();
|
|
1230
|
+
this.scrollPositions.clear();
|
|
1231
|
+
this.layoutSnapshot = null;
|
|
843
1232
|
}
|
|
1233
|
+
};
|
|
1234
|
+
var stateCapturer = new StateCapturer();
|
|
1235
|
+
|
|
1236
|
+
// src/hmr/overlay.js
|
|
1237
|
+
function escapeHtml(str) {
|
|
1238
|
+
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
844
1239
|
}
|
|
845
|
-
function
|
|
846
|
-
if (
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1240
|
+
function formatCodeFrame(frame, highlightLine, startLine = 1) {
|
|
1241
|
+
if (!frame) return "";
|
|
1242
|
+
const lines = frame.split("\n");
|
|
1243
|
+
return lines.map((content, i) => {
|
|
1244
|
+
const lineNum = startLine + i;
|
|
1245
|
+
const isHighlight = lineNum === highlightLine;
|
|
1246
|
+
return `<div class="line${isHighlight ? " highlight" : ""}">
|
|
1247
|
+
<span class="line-number">${lineNum}</span>
|
|
1248
|
+
<span class="line-content">${escapeHtml(content)}</span>
|
|
1249
|
+
</div>`;
|
|
1250
|
+
}).join("");
|
|
854
1251
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1252
|
+
var EDITOR_URLS = {
|
|
1253
|
+
vscode: (file, line) => `vscode://file/${file}:${line}`,
|
|
1254
|
+
cursor: (file, line) => `cursor://file/${file}:${line}`,
|
|
1255
|
+
"vscode-insiders": (file, line) => `vscode-insiders://file/${file}:${line}`,
|
|
1256
|
+
atom: (file, line) => `atom://core/open/file?filename=${file}&line=${line}`,
|
|
1257
|
+
sublime: (file, line) => `subl://open?url=file://${file}&line=${line}`,
|
|
1258
|
+
webstorm: (file, line) => `webstorm://open?file=${file}&line=${line}`,
|
|
1259
|
+
idea: (file, line) => `idea://open?file=${file}&line=${line}`
|
|
1260
|
+
};
|
|
1261
|
+
var OVERLAY_STYLES = `
|
|
1262
|
+
:host {
|
|
1263
|
+
position: fixed;
|
|
1264
|
+
top: 0;
|
|
1265
|
+
left: 0;
|
|
1266
|
+
width: 100%;
|
|
1267
|
+
height: 100%;
|
|
1268
|
+
z-index: 99999;
|
|
1269
|
+
--bg: #181818;
|
|
1270
|
+
--text: #f8f8f2;
|
|
1271
|
+
--red: #ff5555;
|
|
1272
|
+
--yellow: #f1fa8c;
|
|
1273
|
+
--purple: #bd93f9;
|
|
1274
|
+
--cyan: #8be9fd;
|
|
1275
|
+
--code-bg: #282a36;
|
|
1276
|
+
--line-num: #6272a4;
|
|
858
1277
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1278
|
+
.backdrop {
|
|
1279
|
+
position: absolute;
|
|
1280
|
+
top: 0;
|
|
1281
|
+
left: 0;
|
|
1282
|
+
width: 100%;
|
|
1283
|
+
height: 100%;
|
|
1284
|
+
background: rgba(0, 0, 0, 0.66);
|
|
865
1285
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
|
|
1286
|
+
.container {
|
|
1287
|
+
position: absolute;
|
|
1288
|
+
top: 50%;
|
|
1289
|
+
left: 50%;
|
|
1290
|
+
transform: translate(-50%, -50%);
|
|
1291
|
+
width: min(800px, 90vw);
|
|
1292
|
+
max-height: 90vh;
|
|
1293
|
+
overflow: auto;
|
|
1294
|
+
background: var(--bg);
|
|
1295
|
+
border-radius: 8px;
|
|
1296
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
|
|
1297
|
+
font-family: 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
1298
|
+
}
|
|
1299
|
+
.header {
|
|
1300
|
+
padding: 16px 20px;
|
|
1301
|
+
background: var(--red);
|
|
1302
|
+
color: white;
|
|
1303
|
+
display: flex;
|
|
1304
|
+
justify-content: space-between;
|
|
1305
|
+
align-items: center;
|
|
1306
|
+
border-radius: 8px 8px 0 0;
|
|
1307
|
+
}
|
|
1308
|
+
.title {
|
|
1309
|
+
font-weight: bold;
|
|
1310
|
+
font-size: 16px;
|
|
1311
|
+
}
|
|
1312
|
+
.close-btn {
|
|
1313
|
+
background: none;
|
|
1314
|
+
border: none;
|
|
1315
|
+
color: white;
|
|
1316
|
+
font-size: 24px;
|
|
1317
|
+
cursor: pointer;
|
|
1318
|
+
padding: 0 8px;
|
|
1319
|
+
line-height: 1;
|
|
1320
|
+
}
|
|
1321
|
+
.close-btn:hover {
|
|
1322
|
+
opacity: 0.8;
|
|
1323
|
+
}
|
|
1324
|
+
.content {
|
|
1325
|
+
padding: 20px;
|
|
1326
|
+
color: var(--text);
|
|
1327
|
+
}
|
|
1328
|
+
.message {
|
|
1329
|
+
font-size: 18px;
|
|
1330
|
+
color: var(--red);
|
|
1331
|
+
margin-bottom: 20px;
|
|
1332
|
+
word-break: break-word;
|
|
1333
|
+
}
|
|
1334
|
+
.file {
|
|
1335
|
+
color: var(--cyan);
|
|
1336
|
+
margin-bottom: 16px;
|
|
1337
|
+
cursor: pointer;
|
|
1338
|
+
text-decoration: underline;
|
|
1339
|
+
}
|
|
1340
|
+
.file:hover {
|
|
1341
|
+
color: var(--purple);
|
|
1342
|
+
}
|
|
1343
|
+
.code-frame {
|
|
1344
|
+
background: var(--code-bg);
|
|
1345
|
+
padding: 16px;
|
|
1346
|
+
border-radius: 4px;
|
|
1347
|
+
overflow-x: auto;
|
|
1348
|
+
font-size: 14px;
|
|
1349
|
+
line-height: 1.5;
|
|
1350
|
+
margin-bottom: 16px;
|
|
881
1351
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
hydratableComponent.__stateContainer = component.__stateContainer;
|
|
1352
|
+
.line {
|
|
1353
|
+
display: flex;
|
|
885
1354
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1355
|
+
.line-number {
|
|
1356
|
+
width: 50px;
|
|
1357
|
+
color: var(--line-num);
|
|
1358
|
+
text-align: right;
|
|
1359
|
+
padding-right: 16px;
|
|
1360
|
+
user-select: none;
|
|
1361
|
+
flex-shrink: 0;
|
|
1362
|
+
}
|
|
1363
|
+
.line-content {
|
|
1364
|
+
flex: 1;
|
|
1365
|
+
white-space: pre;
|
|
1366
|
+
}
|
|
1367
|
+
.line.highlight {
|
|
1368
|
+
background: rgba(255, 85, 85, 0.2);
|
|
1369
|
+
}
|
|
1370
|
+
.line.highlight .line-content {
|
|
1371
|
+
color: var(--red);
|
|
1372
|
+
}
|
|
1373
|
+
.stack {
|
|
1374
|
+
margin-top: 20px;
|
|
1375
|
+
font-size: 12px;
|
|
1376
|
+
color: var(--line-num);
|
|
1377
|
+
white-space: pre-wrap;
|
|
1378
|
+
max-height: 200px;
|
|
1379
|
+
overflow-y: auto;
|
|
1380
|
+
}
|
|
1381
|
+
.tip {
|
|
1382
|
+
margin-top: 16px;
|
|
1383
|
+
padding: 12px;
|
|
1384
|
+
background: rgba(189, 147, 249, 0.1);
|
|
1385
|
+
border-left: 3px solid var(--purple);
|
|
1386
|
+
font-size: 13px;
|
|
1387
|
+
color: var(--text);
|
|
1388
|
+
}
|
|
1389
|
+
.tip strong {
|
|
1390
|
+
color: var(--purple);
|
|
1391
|
+
}
|
|
1392
|
+
`;
|
|
1393
|
+
var ErrorOverlay = class {
|
|
1394
|
+
constructor() {
|
|
1395
|
+
this.overlay = null;
|
|
1396
|
+
this.editor = this._getStoredEditor();
|
|
1397
|
+
this.escapeHandler = null;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Get stored editor preference from localStorage.
|
|
1401
|
+
* @returns {string} Editor name
|
|
1402
|
+
* @private
|
|
1403
|
+
*/
|
|
1404
|
+
_getStoredEditor() {
|
|
1405
|
+
try {
|
|
1406
|
+
return localStorage.getItem("coherent-editor") || "vscode";
|
|
1407
|
+
} catch {
|
|
1408
|
+
return "vscode";
|
|
891
1409
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Create the overlay element with Shadow DOM.
|
|
1413
|
+
* @returns {{ host: HTMLElement, shadow: ShadowRoot }} Overlay elements
|
|
1414
|
+
*/
|
|
1415
|
+
createOverlay() {
|
|
1416
|
+
if (this.overlay) return this.overlay;
|
|
1417
|
+
const host = document.createElement("div");
|
|
1418
|
+
host.id = "coherent-error-overlay";
|
|
1419
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
1420
|
+
const style = document.createElement("style");
|
|
1421
|
+
style.textContent = OVERLAY_STYLES;
|
|
1422
|
+
shadow.appendChild(style);
|
|
1423
|
+
this.overlay = { host, shadow };
|
|
1424
|
+
return this.overlay;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Show the error overlay with error details.
|
|
1428
|
+
* @param {Object} error - Error details
|
|
1429
|
+
* @param {string} error.message - Error message
|
|
1430
|
+
* @param {string} [error.file] - File path
|
|
1431
|
+
* @param {number} [error.line] - Line number
|
|
1432
|
+
* @param {number} [error.column] - Column number
|
|
1433
|
+
* @param {string} [error.frame] - Code frame with context
|
|
1434
|
+
* @param {string} [error.stack] - Stack trace
|
|
1435
|
+
*/
|
|
1436
|
+
show(error) {
|
|
1437
|
+
const { host, shadow } = this.createOverlay();
|
|
1438
|
+
const existingWrapper = shadow.querySelector(".wrapper");
|
|
1439
|
+
if (existingWrapper) existingWrapper.remove();
|
|
1440
|
+
const frameLines = error.frame ? error.frame.split("\n").length : 0;
|
|
1441
|
+
const startLine = error.line ? Math.max(1, error.line - Math.floor(frameLines / 2)) : 1;
|
|
1442
|
+
const wrapper = document.createElement("div");
|
|
1443
|
+
wrapper.className = "wrapper";
|
|
1444
|
+
wrapper.innerHTML = `
|
|
1445
|
+
<div class="backdrop"></div>
|
|
1446
|
+
<div class="container">
|
|
1447
|
+
<div class="header">
|
|
1448
|
+
<span class="title">HMR Error</span>
|
|
1449
|
+
<button class="close-btn" title="Close (Escape)">×</button>
|
|
1450
|
+
</div>
|
|
1451
|
+
<div class="content">
|
|
1452
|
+
<div class="message">${escapeHtml(error.message || "Unknown error")}</div>
|
|
1453
|
+
${error.file ? `
|
|
1454
|
+
<div class="file" data-file="${escapeHtml(error.file)}" data-line="${error.line || 1}">
|
|
1455
|
+
${escapeHtml(error.file)}${error.line ? `:${error.line}` : ""}${error.column ? `:${error.column}` : ""}
|
|
1456
|
+
</div>
|
|
1457
|
+
` : ""}
|
|
1458
|
+
${error.frame ? `
|
|
1459
|
+
<div class="code-frame">${formatCodeFrame(error.frame, error.line, startLine)}</div>
|
|
1460
|
+
` : ""}
|
|
1461
|
+
${error.stack ? `
|
|
1462
|
+
<div class="stack">${escapeHtml(error.stack)}</div>
|
|
1463
|
+
` : ""}
|
|
1464
|
+
<div class="tip">
|
|
1465
|
+
Press <strong>Escape</strong> or click the X to dismiss.
|
|
1466
|
+
${error.file ? ` Click the file path to open in ${this.editor}.` : ""}
|
|
1467
|
+
</div>
|
|
1468
|
+
</div>
|
|
1469
|
+
</div>
|
|
1470
|
+
`;
|
|
1471
|
+
shadow.appendChild(wrapper);
|
|
1472
|
+
const closeBtn = wrapper.querySelector(".close-btn");
|
|
1473
|
+
const backdrop = wrapper.querySelector(".backdrop");
|
|
1474
|
+
const fileLink = wrapper.querySelector(".file");
|
|
1475
|
+
closeBtn?.addEventListener("click", () => this.hide());
|
|
1476
|
+
backdrop?.addEventListener("click", () => this.hide());
|
|
1477
|
+
fileLink?.addEventListener("click", (e) => {
|
|
1478
|
+
const target = e.target;
|
|
1479
|
+
const file = target.dataset.file;
|
|
1480
|
+
const line = parseInt(target.dataset.line, 10) || 1;
|
|
1481
|
+
this.openInEditor(file, line);
|
|
1482
|
+
});
|
|
1483
|
+
this.escapeHandler = (e) => {
|
|
1484
|
+
if (e.key === "Escape") this.hide();
|
|
1485
|
+
};
|
|
1486
|
+
document.addEventListener("keydown", this.escapeHandler);
|
|
1487
|
+
if (!host.parentNode) {
|
|
1488
|
+
document.body.appendChild(host);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Hide and remove the error overlay.
|
|
1493
|
+
*/
|
|
1494
|
+
hide() {
|
|
1495
|
+
if (this.overlay?.host.parentNode) {
|
|
1496
|
+
this.overlay.host.parentNode.removeChild(this.overlay.host);
|
|
1497
|
+
}
|
|
1498
|
+
if (this.escapeHandler) {
|
|
1499
|
+
document.removeEventListener("keydown", this.escapeHandler);
|
|
1500
|
+
this.escapeHandler = null;
|
|
1501
|
+
}
|
|
1502
|
+
this.overlay = null;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Open file in configured editor.
|
|
1506
|
+
* @param {string} file - File path
|
|
1507
|
+
* @param {number} [line=1] - Line number
|
|
1508
|
+
*/
|
|
1509
|
+
openInEditor(file, line = 1) {
|
|
1510
|
+
const urlGenerator = EDITOR_URLS[this.editor] || EDITOR_URLS.vscode;
|
|
1511
|
+
const url = urlGenerator(file, line);
|
|
1512
|
+
window.open(url, "_self");
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Set preferred editor and store in localStorage.
|
|
1516
|
+
* @param {string} editor - Editor name (vscode, cursor, vscode-insiders, atom, sublime, webstorm, idea)
|
|
1517
|
+
*/
|
|
1518
|
+
setEditor(editor) {
|
|
1519
|
+
this.editor = editor;
|
|
1520
|
+
try {
|
|
1521
|
+
localStorage.setItem("coherent-editor", editor);
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
var errorOverlay = new ErrorOverlay();
|
|
1527
|
+
|
|
1528
|
+
// src/hmr/indicator.js
|
|
1529
|
+
var STATUS_COLORS = {
|
|
1530
|
+
connected: "#10b981",
|
|
1531
|
+
// Green
|
|
1532
|
+
disconnected: "#ef4444",
|
|
1533
|
+
// Red
|
|
1534
|
+
reconnecting: "#f59e0b",
|
|
1535
|
+
// Yellow/amber
|
|
1536
|
+
error: "#ef4444"
|
|
1537
|
+
// Red
|
|
1538
|
+
};
|
|
1539
|
+
var STATUS_TITLES = {
|
|
1540
|
+
connected: "HMR: Connected",
|
|
1541
|
+
disconnected: "HMR: Disconnected",
|
|
1542
|
+
reconnecting: "HMR: Reconnecting...",
|
|
1543
|
+
error: "HMR: Error"
|
|
1544
|
+
};
|
|
1545
|
+
var DEFAULT_COLOR = "#666";
|
|
1546
|
+
var ConnectionIndicator = class {
|
|
1547
|
+
constructor() {
|
|
1548
|
+
this.indicator = null;
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Create the indicator element if it doesn't exist.
|
|
1552
|
+
* Uses inline styles to avoid external CSS dependencies.
|
|
1553
|
+
*/
|
|
1554
|
+
create() {
|
|
1555
|
+
if (this.indicator) return;
|
|
1556
|
+
const el = document.createElement("div");
|
|
1557
|
+
el.id = "coherent-hmr-indicator";
|
|
1558
|
+
el.style.cssText = `
|
|
1559
|
+
position: fixed;
|
|
1560
|
+
bottom: 8px;
|
|
1561
|
+
right: 8px;
|
|
1562
|
+
width: 8px;
|
|
1563
|
+
height: 8px;
|
|
1564
|
+
border-radius: 50%;
|
|
1565
|
+
background: ${DEFAULT_COLOR};
|
|
1566
|
+
z-index: 99998;
|
|
1567
|
+
pointer-events: none;
|
|
1568
|
+
transition: background 0.3s ease;
|
|
1569
|
+
`;
|
|
1570
|
+
el.title = "HMR: Initializing";
|
|
1571
|
+
document.body.appendChild(el);
|
|
1572
|
+
this.indicator = el;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Update the indicator status.
|
|
1576
|
+
* Creates the element if it doesn't exist (lazy initialization).
|
|
1577
|
+
*
|
|
1578
|
+
* @param {string} status - Status string: 'connected', 'disconnected', 'reconnecting', or 'error'
|
|
1579
|
+
*/
|
|
1580
|
+
update(status) {
|
|
1581
|
+
if (!this.indicator) {
|
|
1582
|
+
this.create();
|
|
1583
|
+
}
|
|
1584
|
+
const color = STATUS_COLORS[status] || STATUS_COLORS.disconnected;
|
|
1585
|
+
const title = STATUS_TITLES[status] || "HMR: Unknown";
|
|
1586
|
+
this.indicator.style.background = color;
|
|
1587
|
+
this.indicator.title = title;
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Remove the indicator from the DOM.
|
|
1591
|
+
*/
|
|
1592
|
+
destroy() {
|
|
1593
|
+
if (this.indicator?.parentNode) {
|
|
1594
|
+
this.indicator.parentNode.removeChild(this.indicator);
|
|
1595
|
+
}
|
|
1596
|
+
this.indicator = null;
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
var connectionIndicator = new ConnectionIndicator();
|
|
1600
|
+
|
|
1601
|
+
// src/hmr/module-tracker.js
|
|
1602
|
+
var ModuleTracker = class {
|
|
1603
|
+
constructor() {
|
|
1604
|
+
this.modules = /* @__PURE__ */ new Map();
|
|
1605
|
+
this.socket = null;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Set WebSocket reference for invalidation messages
|
|
1609
|
+
* @param {WebSocket|null} socket - WebSocket connection
|
|
1610
|
+
*/
|
|
1611
|
+
setSocket(socket) {
|
|
1612
|
+
this.socket = socket;
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Create a hot context for a module (Vite-compatible API)
|
|
1616
|
+
*
|
|
1617
|
+
* Returns an object with:
|
|
1618
|
+
* - data: Persistent object that survives HMR updates
|
|
1619
|
+
* - accept(callback): Register self-update handler
|
|
1620
|
+
* - acceptDeps(deps, callback): Register dependency update handler
|
|
1621
|
+
* - dispose(callback): Register cleanup handler called before replacement
|
|
1622
|
+
* - prune(callback): Register handler for when module is removed
|
|
1623
|
+
* - invalidate(message): Signal that module cannot hot-update
|
|
1624
|
+
*
|
|
1625
|
+
* @param {string} moduleId - Unique identifier for the module
|
|
1626
|
+
* @returns {Object} Hot context object
|
|
1627
|
+
*/
|
|
1628
|
+
createHotContext(moduleId) {
|
|
1629
|
+
let moduleData = this.modules.get(moduleId);
|
|
1630
|
+
if (!moduleData) {
|
|
1631
|
+
moduleData = {
|
|
1632
|
+
accept: null,
|
|
1633
|
+
acceptDeps: null,
|
|
1634
|
+
dispose: null,
|
|
1635
|
+
prune: null,
|
|
1636
|
+
data: {}
|
|
1637
|
+
};
|
|
1638
|
+
this.modules.set(moduleId, moduleData);
|
|
1639
|
+
}
|
|
1640
|
+
const tracker = this;
|
|
896
1641
|
return {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1642
|
+
/**
|
|
1643
|
+
* Persistent data object that survives HMR updates.
|
|
1644
|
+
* Use this to preserve state across module replacements.
|
|
1645
|
+
*/
|
|
1646
|
+
get data() {
|
|
1647
|
+
return moduleData.data;
|
|
1648
|
+
},
|
|
1649
|
+
/**
|
|
1650
|
+
* Accept self updates.
|
|
1651
|
+
* Called when this module is updated and can handle its own replacement.
|
|
1652
|
+
*
|
|
1653
|
+
* @param {Function} [callback] - Optional callback receiving the new module
|
|
1654
|
+
*/
|
|
1655
|
+
accept(callback) {
|
|
1656
|
+
moduleData.accept = callback || (() => {
|
|
1657
|
+
});
|
|
1658
|
+
},
|
|
1659
|
+
/**
|
|
1660
|
+
* Accept dependency updates.
|
|
1661
|
+
* Called when one of the specified dependencies is updated.
|
|
1662
|
+
*
|
|
1663
|
+
* @param {string|string[]} deps - Dependency module ID(s)
|
|
1664
|
+
* @param {Function} callback - Callback receiving updated dependencies
|
|
1665
|
+
*/
|
|
1666
|
+
acceptDeps(deps, callback) {
|
|
1667
|
+
const depsArray = Array.isArray(deps) ? deps : [deps];
|
|
1668
|
+
moduleData.acceptDeps = { deps: depsArray, callback };
|
|
1669
|
+
},
|
|
1670
|
+
/**
|
|
1671
|
+
* Register disposal callback.
|
|
1672
|
+
* Called before the module is replaced, receives the data object
|
|
1673
|
+
* to allow saving state for the next version.
|
|
1674
|
+
*
|
|
1675
|
+
* @param {Function} callback - Cleanup handler, receives data object
|
|
1676
|
+
*/
|
|
1677
|
+
dispose(callback) {
|
|
1678
|
+
moduleData.dispose = callback;
|
|
1679
|
+
},
|
|
1680
|
+
/**
|
|
1681
|
+
* Register prune callback.
|
|
1682
|
+
* Called when the module is completely removed from the module graph.
|
|
1683
|
+
*
|
|
1684
|
+
* @param {Function} callback - Prune handler
|
|
1685
|
+
*/
|
|
1686
|
+
prune(callback) {
|
|
1687
|
+
moduleData.prune = callback;
|
|
1688
|
+
},
|
|
1689
|
+
/**
|
|
1690
|
+
* Invalidate this module.
|
|
1691
|
+
* Signals that the module cannot be hot-updated and should propagate
|
|
1692
|
+
* the update to its importers.
|
|
1693
|
+
*
|
|
1694
|
+
* @param {string} [message] - Optional message explaining why
|
|
1695
|
+
*/
|
|
1696
|
+
invalidate(message) {
|
|
1697
|
+
const WS_OPEN = typeof WebSocket !== "undefined" ? WebSocket.OPEN : 1;
|
|
1698
|
+
if (tracker.socket?.readyState === WS_OPEN) {
|
|
1699
|
+
tracker.socket.send(JSON.stringify({
|
|
1700
|
+
type: "invalidate",
|
|
1701
|
+
moduleId,
|
|
1702
|
+
message
|
|
1703
|
+
}));
|
|
912
1704
|
}
|
|
913
|
-
|
|
1705
|
+
console.log(`[HMR] Module ${moduleId} invalidated${message ? `: ${message}` : ""}`);
|
|
914
1706
|
}
|
|
915
1707
|
};
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Check if a module can be hot-updated
|
|
1711
|
+
*
|
|
1712
|
+
* Returns true if the module has registered an accept handler.
|
|
1713
|
+
*
|
|
1714
|
+
* @param {string} moduleId - Module identifier
|
|
1715
|
+
* @returns {boolean} True if module accepts HMR updates
|
|
1716
|
+
*/
|
|
1717
|
+
canHotUpdate(moduleId) {
|
|
1718
|
+
const moduleData = this.modules.get(moduleId);
|
|
1719
|
+
return !!(moduleData?.accept || moduleData?.acceptDeps);
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Check if a module is an HMR boundary
|
|
1723
|
+
*
|
|
1724
|
+
* A module is considered a boundary if:
|
|
1725
|
+
* - It has an accept handler registered
|
|
1726
|
+
* - It exports __hmrBoundary = true
|
|
1727
|
+
* - It is associated with a data-coherent-component element
|
|
1728
|
+
*
|
|
1729
|
+
* @param {string} moduleId - Module identifier
|
|
1730
|
+
* @param {Object} [moduleExports] - Optional module exports to check for __hmrBoundary
|
|
1731
|
+
* @returns {boolean} True if module is an HMR boundary
|
|
1732
|
+
*/
|
|
1733
|
+
isHmrBoundary(moduleId, moduleExports) {
|
|
1734
|
+
if (this.canHotUpdate(moduleId)) {
|
|
1735
|
+
return true;
|
|
1736
|
+
}
|
|
1737
|
+
if (moduleExports?.__hmrBoundary === true) {
|
|
1738
|
+
return true;
|
|
1739
|
+
}
|
|
1740
|
+
const componentName = this.extractComponentName(moduleId);
|
|
1741
|
+
if (componentName && typeof document !== "undefined") {
|
|
1742
|
+
const hasComponent = document.querySelector(
|
|
1743
|
+
`[data-coherent-component="${componentName}"]`
|
|
1744
|
+
);
|
|
1745
|
+
if (hasComponent) {
|
|
1746
|
+
return true;
|
|
927
1747
|
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1748
|
+
}
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Extract a potential component name from module path
|
|
1753
|
+
*
|
|
1754
|
+
* @param {string} moduleId - Module path
|
|
1755
|
+
* @returns {string|null} Component name or null
|
|
1756
|
+
* @private
|
|
1757
|
+
*/
|
|
1758
|
+
extractComponentName(moduleId) {
|
|
1759
|
+
const match = moduleId.match(/\/([^/]+?)(?:\.[^.]+)?$/);
|
|
1760
|
+
if (match) {
|
|
1761
|
+
return match[1];
|
|
1762
|
+
}
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Execute dispose callback for a module
|
|
1767
|
+
*
|
|
1768
|
+
* Calls the registered dispose handler with the data object,
|
|
1769
|
+
* allowing the module to save state for the next version.
|
|
1770
|
+
*
|
|
1771
|
+
* @param {string} moduleId - Module identifier
|
|
1772
|
+
* @returns {Object|null} The data object (for passing to next version)
|
|
1773
|
+
*/
|
|
1774
|
+
executeDispose(moduleId) {
|
|
1775
|
+
const moduleData = this.modules.get(moduleId);
|
|
1776
|
+
if (!moduleData) {
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
if (typeof moduleData.dispose === "function") {
|
|
934
1780
|
try {
|
|
935
|
-
|
|
936
|
-
} catch (
|
|
937
|
-
console.
|
|
1781
|
+
moduleData.dispose(moduleData.data);
|
|
1782
|
+
} catch (err) {
|
|
1783
|
+
console.error(`[HMR] Error in dispose handler for ${moduleId}:`, err);
|
|
938
1784
|
}
|
|
939
1785
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1786
|
+
return moduleData.data;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Execute accept callback for a module
|
|
1790
|
+
*
|
|
1791
|
+
* Calls the registered accept handler with the new module.
|
|
1792
|
+
*
|
|
1793
|
+
* @param {string} moduleId - Module identifier
|
|
1794
|
+
* @param {Object} [newModule] - The newly imported module
|
|
1795
|
+
* @returns {boolean} True if accept handler was called
|
|
1796
|
+
*/
|
|
1797
|
+
executeAccept(moduleId, newModule) {
|
|
1798
|
+
const moduleData = this.modules.get(moduleId);
|
|
1799
|
+
if (!moduleData?.accept) {
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
moduleData.accept(newModule);
|
|
1804
|
+
return true;
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
console.error(`[HMR] Error in accept handler for ${moduleId}:`, err);
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Execute acceptDeps callback for a module
|
|
1812
|
+
*
|
|
1813
|
+
* Calls the registered acceptDeps handler with the updated dependencies.
|
|
1814
|
+
*
|
|
1815
|
+
* @param {string} moduleId - Module identifier
|
|
1816
|
+
* @param {Object} updatedDeps - Map of dependency moduleId -> new module
|
|
1817
|
+
* @returns {boolean} True if acceptDeps handler was called
|
|
1818
|
+
*/
|
|
1819
|
+
executeAcceptDeps(moduleId, updatedDeps) {
|
|
1820
|
+
const moduleData = this.modules.get(moduleId);
|
|
1821
|
+
if (!moduleData?.acceptDeps) {
|
|
1822
|
+
return false;
|
|
1823
|
+
}
|
|
1824
|
+
try {
|
|
1825
|
+
const { deps, callback } = moduleData.acceptDeps;
|
|
1826
|
+
const modules = deps.map((dep) => updatedDeps[dep]);
|
|
1827
|
+
callback(modules);
|
|
1828
|
+
return true;
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
console.error(`[HMR] Error in acceptDeps handler for ${moduleId}:`, err);
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Execute prune callback for a module
|
|
1836
|
+
*
|
|
1837
|
+
* Called when a module is removed from the module graph.
|
|
1838
|
+
*
|
|
1839
|
+
* @param {string} moduleId - Module identifier
|
|
1840
|
+
*/
|
|
1841
|
+
executePrune(moduleId) {
|
|
1842
|
+
const moduleData = this.modules.get(moduleId);
|
|
1843
|
+
if (!moduleData?.prune) {
|
|
1844
|
+
return;
|
|
952
1845
|
}
|
|
1846
|
+
try {
|
|
1847
|
+
moduleData.prune();
|
|
1848
|
+
} catch (err) {
|
|
1849
|
+
console.error(`[HMR] Error in prune handler for ${moduleId}:`, err);
|
|
1850
|
+
}
|
|
1851
|
+
this.modules.delete(moduleId);
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Check if module is registered
|
|
1855
|
+
*
|
|
1856
|
+
* @param {string} moduleId - Module identifier
|
|
1857
|
+
* @returns {boolean} True if module is registered
|
|
1858
|
+
*/
|
|
1859
|
+
hasModule(moduleId) {
|
|
1860
|
+
return this.modules.has(moduleId);
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Get module data (for testing/debugging)
|
|
1864
|
+
*
|
|
1865
|
+
* @param {string} moduleId - Module identifier
|
|
1866
|
+
* @returns {Object|null} Module data or null
|
|
1867
|
+
*/
|
|
1868
|
+
getModuleData(moduleId) {
|
|
1869
|
+
return this.modules.get(moduleId) || null;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Clear all module registrations (for testing)
|
|
1873
|
+
*/
|
|
1874
|
+
clear() {
|
|
1875
|
+
this.modules.clear();
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
var moduleTracker = new ModuleTracker();
|
|
1879
|
+
function createHotContext(moduleId) {
|
|
1880
|
+
return moduleTracker.createHotContext(moduleId);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/hmr/client.js
|
|
1884
|
+
function parseErrorLocation(error) {
|
|
1885
|
+
const result = { file: null, line: null, column: null };
|
|
1886
|
+
if (!error.stack) {
|
|
953
1887
|
return result;
|
|
954
|
-
}
|
|
955
|
-
|
|
1888
|
+
}
|
|
1889
|
+
const patterns = [
|
|
1890
|
+
/at\s+.*?\((.+?):(\d+):(\d+)\)/,
|
|
1891
|
+
// Chrome/Node with parens
|
|
1892
|
+
/at\s+(.+?):(\d+):(\d+)/,
|
|
1893
|
+
// Chrome/Node without parens
|
|
1894
|
+
/@(.+?):(\d+):(\d+)/,
|
|
1895
|
+
// Firefox
|
|
1896
|
+
/^(.+?):(\d+):(\d+)/
|
|
1897
|
+
// Safari
|
|
1898
|
+
];
|
|
1899
|
+
const lines = error.stack.split("\n");
|
|
1900
|
+
for (const line of lines) {
|
|
1901
|
+
for (const pattern of patterns) {
|
|
1902
|
+
const match = line.match(pattern);
|
|
1903
|
+
if (match) {
|
|
1904
|
+
result.file = match[1];
|
|
1905
|
+
result.line = parseInt(match[2], 10);
|
|
1906
|
+
result.column = parseInt(match[3], 10);
|
|
1907
|
+
return result;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return result;
|
|
956
1912
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1913
|
+
var HMRClient = class {
|
|
1914
|
+
constructor() {
|
|
1915
|
+
this.socket = null;
|
|
1916
|
+
this.connected = false;
|
|
1917
|
+
this.reconnectAttempts = 0;
|
|
1918
|
+
this.maxReconnectAttempts = 10;
|
|
1919
|
+
this.reconnectDelay = 1e3;
|
|
1920
|
+
this.hadDisconnect = false;
|
|
1921
|
+
this.reconnectTimeout = null;
|
|
1922
|
+
this.initialized = false;
|
|
960
1923
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1924
|
+
/**
|
|
1925
|
+
* Connect to the dev server WebSocket
|
|
1926
|
+
*
|
|
1927
|
+
* Establishes WebSocket connection with automatic reconnection using
|
|
1928
|
+
* exponential backoff with jitter.
|
|
1929
|
+
*
|
|
1930
|
+
* @returns {void}
|
|
1931
|
+
*/
|
|
1932
|
+
connect() {
|
|
1933
|
+
if (typeof window === "undefined") {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
if (this.reconnectTimeout !== null) {
|
|
1937
|
+
clearTimeout(this.reconnectTimeout);
|
|
1938
|
+
this.reconnectTimeout = null;
|
|
1939
|
+
}
|
|
1940
|
+
try {
|
|
1941
|
+
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
|
1942
|
+
const wsUrl = `${protocol}://${location.host}`;
|
|
1943
|
+
this.socket = new WebSocket(wsUrl);
|
|
1944
|
+
moduleTracker.setSocket(this.socket);
|
|
1945
|
+
this.socket.addEventListener("open", () => {
|
|
1946
|
+
console.log("[HMR] Connected");
|
|
1947
|
+
this.connected = true;
|
|
1948
|
+
this.reconnectAttempts = 0;
|
|
1949
|
+
connectionIndicator.update("connected");
|
|
1950
|
+
this.socket.send(JSON.stringify({ type: "connected" }));
|
|
1951
|
+
if (this.hadDisconnect) {
|
|
1952
|
+
console.log("[HMR] Reconnected after disconnect, reloading page");
|
|
1953
|
+
setTimeout(() => location.reload(), 200);
|
|
1954
|
+
return;
|
|
978
1955
|
}
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1956
|
+
});
|
|
1957
|
+
this.socket.addEventListener("close", () => {
|
|
1958
|
+
this.connected = false;
|
|
1959
|
+
this.hadDisconnect = true;
|
|
1960
|
+
connectionIndicator.update("disconnected");
|
|
1961
|
+
moduleTracker.setSocket(null);
|
|
1962
|
+
this.scheduleReconnect();
|
|
1963
|
+
});
|
|
1964
|
+
this.socket.addEventListener("error", (event) => {
|
|
1965
|
+
console.warn("[HMR] WebSocket error:", event);
|
|
1966
|
+
connectionIndicator.update("error");
|
|
985
1967
|
try {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const stateAttr = element.getAttribute("data-coherent-state");
|
|
989
|
-
const initialState = stateAttr ? JSON.parse(stateAttr) : null;
|
|
990
|
-
const instance = hydrate(element, component, props, { initialState });
|
|
991
|
-
if (instance) {
|
|
992
|
-
} else {
|
|
993
|
-
console.warn(`\u274C Failed to hydrate component: ${componentName}`);
|
|
994
|
-
}
|
|
995
|
-
} catch (_error) {
|
|
996
|
-
console.error(`\u274C Failed to auto-hydrate component ${componentName}:`, _error);
|
|
1968
|
+
this.socket.close();
|
|
1969
|
+
} catch {
|
|
997
1970
|
}
|
|
1971
|
+
});
|
|
1972
|
+
this.socket.addEventListener("message", (event) => {
|
|
1973
|
+
this.handleMessage(event);
|
|
1974
|
+
});
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
console.warn("[HMR] Failed to connect:", error);
|
|
1977
|
+
this.scheduleReconnect();
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Schedule a reconnection attempt with exponential backoff
|
|
1982
|
+
*
|
|
1983
|
+
* @private
|
|
1984
|
+
*/
|
|
1985
|
+
scheduleReconnect() {
|
|
1986
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1987
|
+
console.warn("[HMR] Max reconnection attempts reached");
|
|
1988
|
+
connectionIndicator.update("disconnected");
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
connectionIndicator.update("reconnecting");
|
|
1992
|
+
const delay = Math.min(
|
|
1993
|
+
this.reconnectDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1e3,
|
|
1994
|
+
3e4
|
|
1995
|
+
);
|
|
1996
|
+
this.reconnectAttempts++;
|
|
1997
|
+
console.log(`[HMR] Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
1998
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
1999
|
+
this.reconnectTimeout = null;
|
|
2000
|
+
this.connect();
|
|
2001
|
+
}, delay);
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Handle incoming WebSocket message
|
|
2005
|
+
*
|
|
2006
|
+
* @param {MessageEvent} event - WebSocket message event
|
|
2007
|
+
* @private
|
|
2008
|
+
*/
|
|
2009
|
+
handleMessage(event) {
|
|
2010
|
+
let data;
|
|
2011
|
+
try {
|
|
2012
|
+
data = JSON.parse(event.data);
|
|
2013
|
+
} catch {
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
console.log("[HMR] message", data.type, data.filePath || data.webPath || "");
|
|
2017
|
+
switch (data.type) {
|
|
2018
|
+
case "connected":
|
|
2019
|
+
break;
|
|
2020
|
+
case "hmr-full-reload":
|
|
2021
|
+
case "reload":
|
|
2022
|
+
console.warn("[HMR] Server requested full reload");
|
|
2023
|
+
location.reload();
|
|
2024
|
+
break;
|
|
2025
|
+
case "hmr-component-update":
|
|
2026
|
+
case "hmr-update":
|
|
2027
|
+
this.handleUpdate(data);
|
|
2028
|
+
break;
|
|
2029
|
+
case "hmr-error":
|
|
2030
|
+
this.showError(data.error || data);
|
|
2031
|
+
break;
|
|
2032
|
+
case "preview-update":
|
|
2033
|
+
break;
|
|
2034
|
+
default:
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Handle module update
|
|
2040
|
+
*
|
|
2041
|
+
* Orchestrates the full HMR update cycle:
|
|
2042
|
+
* 1. Capture form/scroll state
|
|
2043
|
+
* 2. Execute dispose handlers
|
|
2044
|
+
* 3. Clean up module resources
|
|
2045
|
+
* 4. Re-import module
|
|
2046
|
+
* 5. Execute accept handlers
|
|
2047
|
+
* 6. Restore state
|
|
2048
|
+
*
|
|
2049
|
+
* @param {Object} data - Update message data
|
|
2050
|
+
* @param {string} [data.filePath] - File path that changed
|
|
2051
|
+
* @param {string} [data.webPath] - Web-accessible path
|
|
2052
|
+
* @param {string} [data.updateType] - Type of update (component, style, etc.)
|
|
2053
|
+
*/
|
|
2054
|
+
async handleUpdate(data) {
|
|
2055
|
+
const filePath = data.webPath || data.filePath || "";
|
|
2056
|
+
const moduleId = filePath;
|
|
2057
|
+
try {
|
|
2058
|
+
stateCapturer.captureAll();
|
|
2059
|
+
if (moduleTracker.hasModule(moduleId)) {
|
|
2060
|
+
moduleTracker.executeDispose(moduleId);
|
|
998
2061
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
2062
|
+
if (cleanupTracker.hasResources(moduleId)) {
|
|
2063
|
+
cleanupTracker.checkForLeaks(moduleId);
|
|
2064
|
+
cleanupTracker.cleanup(moduleId);
|
|
2065
|
+
}
|
|
2066
|
+
const importPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
|
|
2067
|
+
const newModule = await import(`${importPath}?t=${Date.now()}`);
|
|
2068
|
+
const accepted = moduleTracker.canHotUpdate(moduleId);
|
|
2069
|
+
if (accepted) {
|
|
2070
|
+
moduleTracker.executeAccept(moduleId, newModule);
|
|
2071
|
+
} else {
|
|
2072
|
+
await this.fallbackHydrate();
|
|
2073
|
+
}
|
|
2074
|
+
stateCapturer.restoreAll();
|
|
2075
|
+
errorOverlay.hide();
|
|
2076
|
+
console.log(`[HMR] Updated: ${data.updateType || "module"} ${filePath}`);
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
this.handleUpdateError(error, filePath);
|
|
2079
|
+
}
|
|
1006
2080
|
}
|
|
1007
|
-
|
|
2081
|
+
/**
|
|
2082
|
+
* Fall back to autoHydrate for non-HMR-aware modules
|
|
2083
|
+
*
|
|
2084
|
+
* @private
|
|
2085
|
+
*/
|
|
2086
|
+
async fallbackHydrate() {
|
|
2087
|
+
try {
|
|
2088
|
+
const { autoHydrate } = await import("../hydration.js");
|
|
2089
|
+
if (typeof window !== "undefined" && window.componentRegistry) {
|
|
2090
|
+
autoHydrate(window.componentRegistry);
|
|
2091
|
+
} else {
|
|
2092
|
+
autoHydrate();
|
|
2093
|
+
}
|
|
2094
|
+
} catch {
|
|
2095
|
+
console.warn("[HMR] autoHydrate not available, component may need manual refresh");
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Handle update error
|
|
2100
|
+
*
|
|
2101
|
+
* @param {Error} error - Error that occurred
|
|
2102
|
+
* @param {string} filePath - File that was being updated
|
|
2103
|
+
* @private
|
|
2104
|
+
*/
|
|
2105
|
+
handleUpdateError(error, filePath) {
|
|
2106
|
+
console.error("[HMR] Update failed:", error);
|
|
2107
|
+
const location2 = parseErrorLocation(error);
|
|
2108
|
+
const errorDetails = {
|
|
2109
|
+
message: error.message || "Unknown error during HMR update",
|
|
2110
|
+
file: location2.file || filePath,
|
|
2111
|
+
line: location2.line,
|
|
2112
|
+
column: location2.column,
|
|
2113
|
+
stack: error.stack
|
|
2114
|
+
};
|
|
2115
|
+
this.showError(errorDetails);
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Show error overlay
|
|
2119
|
+
*
|
|
2120
|
+
* @param {Object} error - Error details
|
|
2121
|
+
*/
|
|
2122
|
+
showError(error) {
|
|
2123
|
+
errorOverlay.show(error);
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Hide error overlay
|
|
2127
|
+
*/
|
|
2128
|
+
hideError() {
|
|
2129
|
+
errorOverlay.hide();
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Initialize HMR client
|
|
2133
|
+
*
|
|
2134
|
+
* Guards against double initialization and connects to dev server.
|
|
2135
|
+
*
|
|
2136
|
+
* @returns {void}
|
|
2137
|
+
*/
|
|
2138
|
+
initialize() {
|
|
2139
|
+
if (typeof window === "undefined") {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
if (window.__coherent_hmr_initialized || this.initialized) {
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
window.__coherent_hmr_initialized = true;
|
|
2146
|
+
this.initialized = true;
|
|
2147
|
+
this.connect();
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Disconnect and clean up
|
|
2151
|
+
*
|
|
2152
|
+
* @returns {void}
|
|
2153
|
+
*/
|
|
2154
|
+
disconnect() {
|
|
2155
|
+
if (this.reconnectTimeout !== null) {
|
|
2156
|
+
clearTimeout(this.reconnectTimeout);
|
|
2157
|
+
this.reconnectTimeout = null;
|
|
2158
|
+
}
|
|
2159
|
+
if (this.socket) {
|
|
2160
|
+
try {
|
|
2161
|
+
this.socket.close();
|
|
2162
|
+
} catch {
|
|
2163
|
+
}
|
|
2164
|
+
this.socket = null;
|
|
2165
|
+
}
|
|
2166
|
+
this.connected = false;
|
|
2167
|
+
moduleTracker.setSocket(null);
|
|
2168
|
+
connectionIndicator.destroy();
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Check if client is connected
|
|
2172
|
+
*
|
|
2173
|
+
* @returns {boolean} True if connected
|
|
2174
|
+
*/
|
|
2175
|
+
isConnected() {
|
|
2176
|
+
return this.connected;
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
var hmrClient = new HMRClient();
|
|
1008
2180
|
export {
|
|
1009
|
-
|
|
1010
|
-
|
|
2181
|
+
CleanupTracker,
|
|
2182
|
+
ConnectionIndicator,
|
|
2183
|
+
ErrorOverlay,
|
|
2184
|
+
EventDelegation,
|
|
2185
|
+
HMRClient,
|
|
2186
|
+
HandlerRegistry,
|
|
2187
|
+
ModuleTracker,
|
|
2188
|
+
StateCapturer,
|
|
2189
|
+
cleanupTracker,
|
|
2190
|
+
connectionIndicator,
|
|
2191
|
+
createHotContext,
|
|
2192
|
+
deserializeState,
|
|
2193
|
+
detectMismatch,
|
|
2194
|
+
errorOverlay,
|
|
2195
|
+
eventDelegation,
|
|
2196
|
+
extractState,
|
|
2197
|
+
formatPath,
|
|
2198
|
+
handlerRegistry,
|
|
2199
|
+
hmrClient,
|
|
1011
2200
|
hydrate,
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
2201
|
+
moduleTracker,
|
|
2202
|
+
reportMismatches,
|
|
2203
|
+
serializeState,
|
|
2204
|
+
serializeStateWithWarning,
|
|
2205
|
+
stateCapturer,
|
|
2206
|
+
wrapEvent
|
|
1016
2207
|
};
|
|
1017
2208
|
//# sourceMappingURL=index.js.map
|