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