@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/dist/index.js CHANGED
@@ -4,7 +4,22 @@ import {
4
4
  eventDelegation,
5
5
  handlerRegistry,
6
6
  wrapEvent
7
- } from "./chunk-N36BSMJU.js";
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
- function getVNodeChildren(vNode) {
77
- if (!vNode || typeof vNode !== "object" || Array.isArray(vNode)) {
78
- return [];
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 tagName = Object.keys(vNode)[0];
81
- const props = vNode[tagName];
82
- if (!props || typeof props !== "object") {
83
- return [];
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 (props.children) {
86
- return Array.isArray(props.children) ? props.children : [props.children];
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
- if (props.text !== void 0) {
89
- return [String(props.text)];
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
- let actualText;
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
- virtualNode.forEach((child, index) => {
119
- const domChild = getDOMChildAtIndex(domElement, index);
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 (typeof virtualNode !== "object") {
402
+ if (!isElementVNode(virtualNode)) {
140
403
  return mismatches;
141
404
  }
142
- const tagName = Object.keys(virtualNode)[0];
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 attributeChecks = [
156
- { virtual: "className", dom: "class" },
157
- { virtual: "id", dom: "id" },
158
- { virtual: "type", dom: "type" },
159
- { virtual: "value", dom: "value" },
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
- const expectedStr = String(expectedValue);
170
- if (typeof expectedValue === "boolean") {
171
- const actualBool = actualValue !== null;
172
- if (expectedValue !== actualBool) {
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: expectedValue,
177
- actual: actualBool,
430
+ expected: expectedPresent,
431
+ actual: actualPresent,
178
432
  domPath: getDOMPath(domElement)
179
433
  });
180
434
  }
181
- return;
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 describeVNode(vNode) {
292
- if (typeof vNode === "string" || typeof vNode === "number") {
293
- return `text: "${String(vNode).substring(0, 50)}"`;
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 (typeof vNode === "object" && vNode !== null) {
299
- const tagName = Object.keys(vNode)[0];
300
- return `<${tagName}>`;
503
+ if (item.type === "element") {
504
+ return `<${Object.keys(item.vNode)[0]}>`;
301
505
  }
302
- return String(vNode);
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
- const eventListeners = [];
338
- const registeredHandlerIds = /* @__PURE__ */ new Set();
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
- const componentProps = { ...additionalProps, ...state };
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(container, virtualDOM, componentRef, registeredHandlerIds);
757
+ registerEventHandlers(root, virtualDOM, componentRef, registeredHandlerIds, boundAttributes);
366
758
  function doRerender() {
367
- const newProps = { ...additionalProps, ...state };
368
- virtualDOM = component(newProps);
369
- patchDOM(container, virtualDOM);
370
- registerEventHandlers(container, virtualDOM, componentRef, registeredHandlerIds);
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
- for (const handlerId of registeredHandlerIds) {
374
- handlerRegistry.unregister(handlerId);
779
+ if (!mounted) {
780
+ return;
375
781
  }
376
- registeredHandlerIds.clear();
377
- for (const { element, event, handler, options: options2 } of eventListeners) {
378
- element.removeEventListener(event, handler, options2);
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
- eventListeners.length = 0;
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
- container.setAttribute("data-coherent-hydrated", "true");
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 registerEventHandlers(domElement, vNode, componentRef, handlerIds) {
404
- if (!vNode || typeof vNode !== "object" || Array.isArray(vNode)) {
405
- return;
816
+ function isDevelopment() {
817
+ try {
818
+ return process.env.NODE_ENV === "development";
819
+ } catch {
820
+ return false;
406
821
  }
407
- const tagName = Object.keys(vNode)[0];
408
- const props = vNode[tagName];
409
- if (!props || typeof props !== "object") {
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.slice(2).toLowerCase();
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 children = getVNodeChildren2(props);
427
- const domChildren = getSignificantDOMChildren2(domElement);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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)">&times;</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,