why-hydration 0.1.5 → 0.3.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.
@@ -12,6 +12,230 @@ function readNodeEnv() {
12
12
  }
13
13
  var isDev = readNodeEnv() !== "production";
14
14
 
15
+ // src/core/classify/detectors.ts
16
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+ var REACT_ID_RE = /^:[rR][0-9a-z]*:$/;
18
+ var HEX_TOKEN_RE = /^[0-9a-f]{16,}$/i;
19
+ var RANDOM_DECIMAL_RE = /^0?\.\d{6,}$/;
20
+ var NANOID_RE = /^[A-Za-z0-9_-]{10,}$/;
21
+ var ARABIC_INDIC_DIGITS = /[٠-٩۰-۹]/;
22
+ var LATIN_DIGITS = /[0-9]/;
23
+ var BIDI_CONTROLS = /[\u200e\u200f\u061c\u2066-\u2069]/g;
24
+ function stripBidiControls(value2) {
25
+ return value2.replace(BIDI_CONTROLS, "");
26
+ }
27
+ function differsOnlyByBidiControls(a, b) {
28
+ const sa = stripBidiControls(a);
29
+ const sb = stripBidiControls(b);
30
+ return a !== b && sa === sb && sa.trim() !== "";
31
+ }
32
+ function normalizeNumerals(value2) {
33
+ return stripBidiControls(value2).replace(/[٠-٩۰-۹٫٬]/g, (ch) => {
34
+ if (ch === "\u066B") return ".";
35
+ if (ch === "\u066C") return ",";
36
+ const code2 = ch.charCodeAt(0);
37
+ const zero = code2 >= 1776 ? 1776 : 1632;
38
+ return String(code2 - zero);
39
+ });
40
+ }
41
+ function isRandomLike(value2) {
42
+ const v = value2.trim();
43
+ if (!v) return false;
44
+ if (UUID_RE.test(v)) return true;
45
+ if (REACT_ID_RE.test(v)) return true;
46
+ if (HEX_TOKEN_RE.test(v)) return true;
47
+ if (RANDOM_DECIMAL_RE.test(v)) return true;
48
+ if (NANOID_RE.test(v) && /[A-Za-z]/.test(v) && /[0-9]/.test(v)) return true;
49
+ return false;
50
+ }
51
+ function looksLikeTime(value2) {
52
+ return /\b\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp][Mm])?\b/.test(
53
+ normalizeNumerals(value2).trim()
54
+ );
55
+ }
56
+ var ISO_DATE = /^\d{4}-\d{1,2}(?:-\d{1,2})?(?:[T ]\d{1,2}:\d{2}(?::\d{2})?)?/;
57
+ var NUMERIC_DATE = /^\d{1,4}[/.-]\d{1,2}[/.-]\d{1,4}$/;
58
+ var TIME_OF_DAY = /^\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp]\.?[Mm]\.?)?$/;
59
+ var MONTH_NAME = /\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b/i;
60
+ function looksLikeDate(v) {
61
+ return ISO_DATE.test(v) || NUMERIC_DATE.test(v) || TIME_OF_DAY.test(v) || MONTH_NAME.test(v) && /\d/.test(v);
62
+ }
63
+ function toTimestamp(value2) {
64
+ const v = value2.trim();
65
+ if (!v) return null;
66
+ if (/^\d{10,13}$/.test(v)) {
67
+ const n = Number(v);
68
+ return v.length === 10 ? n * 1e3 : n;
69
+ }
70
+ if (!looksLikeDate(v)) return null;
71
+ const parsed = Date.parse(v);
72
+ if (!Number.isNaN(parsed)) return parsed;
73
+ if (looksLikeTime(v)) {
74
+ const anchored = Date.parse(`1970-01-01 ${v}`);
75
+ if (!Number.isNaN(anchored)) return anchored;
76
+ }
77
+ return null;
78
+ }
79
+ function hasArabicIndicDigits(value2) {
80
+ return ARABIC_INDIC_DIGITS.test(value2);
81
+ }
82
+ function hasLatinDigits(value2) {
83
+ return LATIN_DIGITS.test(value2);
84
+ }
85
+ var NUMERIC_LIKE = /^[+-]?[\d.,\s\u00a0\u2009]+$/;
86
+ function isSameNumberDifferentSeparators(a, b) {
87
+ const at = normalizeNumerals(a).trim();
88
+ const bt = normalizeNumerals(b).trim();
89
+ if (!NUMERIC_LIKE.test(at) || !NUMERIC_LIKE.test(bt)) return false;
90
+ const digitsOnly = (s) => s.replace(/\D/g, "");
91
+ const da = digitsOnly(at);
92
+ const db = digitsOnly(bt);
93
+ if (!da || da !== db) return false;
94
+ const hasSep = (s) => /[.,\s\u00a0\u2009]/.test(s);
95
+ return (hasSep(at) || hasSep(bt)) && a.trim() !== b.trim();
96
+ }
97
+ var CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
98
+ "value",
99
+ "placeholder",
100
+ "title",
101
+ "alt",
102
+ "label",
103
+ "aria-label",
104
+ "aria-valuetext",
105
+ "content",
106
+ "datetime"
107
+ ]);
108
+ function isContentAttribute(name) {
109
+ return CONTENT_ATTRIBUTES.has(name.toLowerCase());
110
+ }
111
+ var THIRD_PARTY_MARKERS = /googlefc|adsbygoogle|google_ads|googletag|__tcfapi|onetrust|optanon|cookiebot|usercentrics|iubenda|didomi|quantcast|grammarly|data-gramm|gtm-|_hjsettings|hotjar|fullstory|intercom|drift|zendesk|livechat|tawk|hubspot|turnstile|recaptcha/i;
112
+ function looksLikeThirdPartyNode(html, tagName) {
113
+ const tag = (tagName ?? "").toUpperCase();
114
+ if (tag === "IFRAME" || tag === "EMBED" || tag === "OBJECT") return true;
115
+ const h = (html ?? "").toLowerCase();
116
+ if (!h) return false;
117
+ return THIRD_PARTY_MARKERS.test(h) || h.includes("about:blank");
118
+ }
119
+ var DATE_PARTS_RE = /^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/;
120
+ function isSameDateDifferentOrder(a, b) {
121
+ const partsA = normalizeNumerals(a).trim().match(DATE_PARTS_RE);
122
+ const partsB = normalizeNumerals(b).trim().match(DATE_PARTS_RE);
123
+ if (!partsA || !partsB) return false;
124
+ const setA = [partsA[1], partsA[2], partsA[3]].sort().join("|");
125
+ const setB = [partsB[1], partsB[2], partsB[3]].sort().join("|");
126
+ return setA === setB && a.trim() !== b.trim();
127
+ }
128
+ var EXTENSION_ATTRIBUTES = /* @__PURE__ */ new Set([
129
+ "cz-shortcut-listen",
130
+ "data-gramm",
131
+ "data-gramm_editor",
132
+ "data-gramm_id",
133
+ "data-gr-c-s-loaded",
134
+ "data-lt-installed",
135
+ "data-new-gr-c-s-check-loaded",
136
+ "data-new-gr-c-s-loaded",
137
+ "spellcheck-extension",
138
+ "bis_register",
139
+ "__processed_by_react_dev_tools"
140
+ ]);
141
+ function isExtensionAttribute(name) {
142
+ const n = name.toLowerCase();
143
+ if (EXTENSION_ATTRIBUTES.has(n)) return true;
144
+ return n.startsWith("data-gr-") || n.startsWith("data-gramm") || n.startsWith("__bis") || n.startsWith("bis_");
145
+ }
146
+ var CLOSES_P = [
147
+ "address",
148
+ "article",
149
+ "aside",
150
+ "blockquote",
151
+ "center",
152
+ "details",
153
+ "dialog",
154
+ "dir",
155
+ "div",
156
+ "dl",
157
+ "fieldset",
158
+ "figcaption",
159
+ "figure",
160
+ "footer",
161
+ "form",
162
+ "h1",
163
+ "h2",
164
+ "h3",
165
+ "h4",
166
+ "h5",
167
+ "h6",
168
+ "header",
169
+ "hgroup",
170
+ "hr",
171
+ "li",
172
+ "dd",
173
+ "dt",
174
+ "listing",
175
+ "main",
176
+ "menu",
177
+ "nav",
178
+ "ol",
179
+ "p",
180
+ "plaintext",
181
+ "pre",
182
+ "search",
183
+ "section",
184
+ "summary",
185
+ "table",
186
+ "ul",
187
+ "xmp"
188
+ ];
189
+ var CLOSES_P_SET = new Set(CLOSES_P.map((t) => t.toUpperCase()));
190
+ var TABLE_CHILDREN = /* @__PURE__ */ new Set([
191
+ "CAPTION",
192
+ "COLGROUP",
193
+ "THEAD",
194
+ "TBODY",
195
+ "TFOOT"
196
+ ]);
197
+ var TABLE_SCAFFOLD = ["script", "template", "style"];
198
+ var TABLE_LEVEL = ["caption", "colgroup", "thead", "tbody", "tfoot"];
199
+ var SECTION_LEVEL = ["tr"];
200
+ var ROW_LEVEL = ["td", "th"];
201
+ var notIn = (tags) => `:not(${[...tags, ...TABLE_SCAFFOLD].join(", ")})`;
202
+ var PARSER_REPAIRS = {
203
+ P: CLOSES_P.join(", "),
204
+ A: "a",
205
+ BUTTON: "button",
206
+ FORM: "form",
207
+ TABLE: [
208
+ `:scope > ${notIn([...TABLE_LEVEL, ...SECTION_LEVEL])}`,
209
+ ...["thead", "tbody", "tfoot"].flatMap((section) => [
210
+ `:scope > ${section} > ${notIn(SECTION_LEVEL)}`,
211
+ `:scope > ${section} > tr > ${notIn(ROW_LEVEL)}`
212
+ ]),
213
+ `:scope > tr > ${notIn(ROW_LEVEL)}`
214
+ ].join(", ")
215
+ };
216
+ function isInvalidNesting(parentTag, childTag) {
217
+ if (!parentTag || !childTag) return false;
218
+ const p = parentTag.toUpperCase();
219
+ const c = childTag.toUpperCase();
220
+ if (p === "P") return CLOSES_P_SET.has(c);
221
+ if (p === "A") return c === "A";
222
+ if (p === "BUTTON") return c === "BUTTON" || c === "A";
223
+ if (p === "FORM") return c === "FORM";
224
+ if (p === "TABLE") return !TABLE_CHILDREN.has(c) && !isScaffold(c);
225
+ if (p === "THEAD" || p === "TBODY" || p === "TFOOT") {
226
+ return c !== "TR" && !isScaffold(c);
227
+ }
228
+ if (p === "TR") return c !== "TD" && c !== "TH" && !isScaffold(c);
229
+ return false;
230
+ }
231
+ function isScaffold(tag) {
232
+ return TABLE_SCAFFOLD.includes(tag.toLowerCase());
233
+ }
234
+ function messageIndicatesInvalidNesting(message) {
235
+ if (!message) return false;
236
+ return /validateDOMNesting/i.test(message) || /cannot (?:be a|contain).*(?:descendant|child)/i.test(message) || /cannot appear as a (?:child|descendant)/i.test(message);
237
+ }
238
+
15
239
  // src/core/diff.ts
16
240
  var IGNORED_ATTRIBUTES = /* @__PURE__ */ new Set(["data-reactroot"]);
17
241
  function parseServerHtml(html, rootTagName) {
@@ -38,9 +262,15 @@ function diffSnapshotAgainstDom(serverHtml, clientRoot) {
38
262
  function collectChildren(serverParent, clientParent, parentPath, out, limit) {
39
263
  if (out.length >= limit) return;
40
264
  const serverChildren = meaningfulChildNodes(serverParent);
41
- const clientChildren = meaningfulChildNodes(clientParent);
42
265
  const parentPending = hasPendingSuspense(serverParent);
43
266
  const parentTag = elementTag(clientParent);
267
+ const clientChildren = repairClientChildren(
268
+ meaningfulChildNodes(clientParent),
269
+ parentTag,
270
+ parentPath,
271
+ out,
272
+ limit
273
+ );
44
274
  const pairs = alignChildren(serverChildren, clientChildren);
45
275
  let index = 0;
46
276
  for (let k = 0; k < pairs.length; k++) {
@@ -88,7 +318,10 @@ function collectChildren(serverParent, clientParent, parentPath, out, limit) {
88
318
  parentTagName: parentTag,
89
319
  server: null,
90
320
  client: serialize(clientNode),
91
- element: asElement(clientNode)
321
+ // An added text node has no element of its own, so point at its
322
+ // parent, as text changes do. With `null` the component lookup found
323
+ // nothing and fell back to whichever error was reported last.
324
+ element: asElement(clientNode) ?? asElement(clientParent)
92
325
  });
93
326
  } else if (serverNode && clientNode) {
94
327
  collectNode(serverNode, clientNode, path, parentTag, out, limit);
@@ -134,7 +367,7 @@ function collectNode(serverNode, clientNode, path, parentTag, out, limit) {
134
367
  parentTagName: parentTag,
135
368
  server: serialize(serverEl),
136
369
  client: serialize(clientEl),
137
- element: clientEl
370
+ element: liveElement(clientEl)
138
371
  });
139
372
  return;
140
373
  }
@@ -162,26 +395,26 @@ function diffAttributes(serverEl, clientEl, path, parentTag) {
162
395
  attribute: name,
163
396
  server: serverValue,
164
397
  client: clientValue,
165
- element: clientEl
398
+ element: liveElement(clientEl)
166
399
  };
167
400
  }
168
401
  return null;
169
402
  }
170
- function normalizeAttr(name, value) {
171
- if (value == null) return null;
403
+ function normalizeAttr(name, value2) {
404
+ if (value2 == null) return null;
172
405
  if (name === "class") {
173
- return value.trim().split(/\s+/).filter(Boolean).sort().join(" ");
406
+ return value2.trim().split(/\s+/).filter(Boolean).sort().join(" ");
174
407
  }
175
408
  if (name === "style") {
176
- return normalizeStyle(value);
409
+ return normalizeStyle(value2);
177
410
  }
178
- return value;
411
+ return value2;
179
412
  }
180
- function normalizeStyle(value) {
413
+ function normalizeStyle(value2) {
181
414
  if (typeof document !== "undefined") {
182
415
  try {
183
416
  const el = document.createElement("div");
184
- el.style.cssText = value;
417
+ el.style.cssText = value2;
185
418
  const decls = [];
186
419
  for (let i = 0; i < el.style.length; i++) {
187
420
  const prop = el.style.item(i);
@@ -191,7 +424,7 @@ function normalizeStyle(value) {
191
424
  } catch {
192
425
  }
193
426
  }
194
- return value.split(";").map((s) => s.trim()).filter(Boolean).sort().join(";");
427
+ return value2.split(";").map((s) => s.trim()).filter(Boolean).sort().join(";");
195
428
  }
196
429
  function nodeKey(node) {
197
430
  if (node.nodeType === Node.ELEMENT_NODE) {
@@ -309,7 +542,8 @@ function meaningfulChildNodes(parent) {
309
542
  for (const node of raw) {
310
543
  if (node.nodeType === Node.COMMENT_NODE) {
311
544
  const data = node.nodeValue ?? "";
312
- if (data === "$?" || data === "$" || data === "$!") boundaryStack.push(data);
545
+ if (data === "$?" || data === "$" || data === "$!")
546
+ boundaryStack.push(data);
313
547
  else if (data === "/$") boundaryStack.pop();
314
548
  continue;
315
549
  }
@@ -332,7 +566,94 @@ function serialize(node) {
332
566
  }
333
567
  function asElement(node) {
334
568
  if (!node) return null;
335
- return node.nodeType === Node.ELEMENT_NODE ? node : null;
569
+ return node.nodeType === Node.ELEMENT_NODE ? liveElement(node) : null;
570
+ }
571
+ var LIVE = /* @__PURE__ */ new WeakMap();
572
+ function liveElement(el) {
573
+ return LIVE.get(el) ?? el;
574
+ }
575
+ function repairClientChildren(children, parentTag, parentPath, out, limit) {
576
+ let repaired = null;
577
+ children.forEach((child, i) => {
578
+ const expansion = repairedForm(child, parentTag);
579
+ if (!expansion) {
580
+ repaired?.push(child);
581
+ return;
582
+ }
583
+ repaired ??= children.slice(0, i);
584
+ repaired.push(...expansion.nodes);
585
+ if (out.length < limit) {
586
+ out.push({
587
+ kind: "structure",
588
+ path: childPath(parentPath, child, i),
589
+ tagName: expansion.offender.tagName,
590
+ parentTagName: invalidParent(expansion.offender, child),
591
+ // No values, on purpose: React reports the same nesting in its own
592
+ // warning with none, and matching it lets the two collapse into one.
593
+ server: null,
594
+ client: null,
595
+ element: expansion.offender
596
+ });
597
+ }
598
+ });
599
+ return repaired ?? children;
600
+ }
601
+ function invalidParent(offender, top) {
602
+ for (let el = offender.parentElement; el; el = el.parentElement) {
603
+ if (isInvalidNesting(el.tagName, offender.tagName)) return el.tagName;
604
+ if (el === top) break;
605
+ }
606
+ return top.tagName;
607
+ }
608
+ function repairedForm(node, parentTag) {
609
+ if (node.nodeType !== Node.ELEMENT_NODE) return null;
610
+ const el = node;
611
+ const selector = PARSER_REPAIRS[el.tagName];
612
+ if (!selector) return null;
613
+ let offender = null;
614
+ try {
615
+ offender = el.querySelector(selector);
616
+ } catch {
617
+ return null;
618
+ }
619
+ if (!offender) return null;
620
+ const html = serializeKeepingTextBoundaries(el);
621
+ const container = parseServerHtml(html, parentTag ?? "div");
622
+ if (container.innerHTML === html) return null;
623
+ const nodes = meaningfulChildNodes(container);
624
+ mapToLive(el, nodes);
625
+ return { nodes, offender };
626
+ }
627
+ function serializeKeepingTextBoundaries(el) {
628
+ const copy = el.cloneNode(true);
629
+ const walker = copy.ownerDocument.createTreeWalker(
630
+ copy,
631
+ NodeFilter.SHOW_TEXT
632
+ );
633
+ const texts = [];
634
+ while (walker.nextNode()) texts.push(walker.currentNode);
635
+ for (const t of texts) {
636
+ if (t.previousSibling?.nodeType === Node.TEXT_NODE) {
637
+ t.parentNode.insertBefore(copy.ownerDocument.createComment(" "), t);
638
+ }
639
+ }
640
+ return copy.outerHTML;
641
+ }
642
+ function mapToLive(live, repaired) {
643
+ const liveEls = [live, ...Array.from(live.querySelectorAll("*"))];
644
+ let i = 0;
645
+ for (const root of repaired) {
646
+ if (root.nodeType !== Node.ELEMENT_NODE) continue;
647
+ const el = root;
648
+ for (const r of [el, ...Array.from(el.querySelectorAll("*"))]) {
649
+ let j = i;
650
+ while (j < liveEls.length && liveEls[j].tagName !== r.tagName) j++;
651
+ if (j < liveEls.length) {
652
+ LIVE.set(r, liveEls[j]);
653
+ i = j + 1;
654
+ }
655
+ }
656
+ }
336
657
  }
337
658
  function elementTag(node) {
338
659
  return node.nodeType === Node.ELEMENT_NODE ? node.tagName : void 0;
@@ -353,177 +674,161 @@ function childPath(parentPath, node, index) {
353
674
  return `${parentPath} > #text[${index}]`;
354
675
  }
355
676
 
356
- // src/core/classify/detectors.ts
357
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
358
- var REACT_ID_RE = /^:[rR][0-9a-z]*:$/;
359
- var HEX_TOKEN_RE = /^[0-9a-f]{16,}$/i;
360
- var RANDOM_DECIMAL_RE = /^0?\.\d{6,}$/;
361
- var NANOID_RE = /^[A-Za-z0-9_-]{10,}$/;
362
- var ARABIC_INDIC_DIGITS = /[٠-٩۰-۹]/;
363
- var LATIN_DIGITS = /[0-9]/;
364
- var BIDI_CONTROLS = /[‎‏؜⁦-⁩]/g;
365
- function stripBidiControls(value) {
366
- return value.replace(BIDI_CONTROLS, "");
677
+ // src/core/classify/messages.ts
678
+ var code = (text) => ({ code: text });
679
+ var value = (text) => ({ value: text });
680
+ function param(params, name) {
681
+ const v = params[name];
682
+ return typeof v === "string" ? v : "";
367
683
  }
368
- function differsOnlyByBidiControls(a, b) {
369
- const sa = stripBidiControls(a);
370
- const sb = stripBidiControls(b);
371
- return a !== b && sa === sb && sa.trim() !== "";
684
+ function paramList(params, name) {
685
+ const v = params[name];
686
+ return Array.isArray(v) ? v : [];
372
687
  }
373
- function normalizeNumerals(value) {
374
- return stripBidiControls(value).replace(/[٠-٩۰-۹٫٬]/g, (ch) => {
375
- if (ch === "\u066B") return ".";
376
- if (ch === "\u066C") return ",";
377
- const code = ch.charCodeAt(0);
378
- const zero = code >= 1776 ? 1776 : 1632;
379
- return String(code - zero);
688
+ function valueList(items, separator) {
689
+ const out = [];
690
+ items.forEach((item, i) => {
691
+ if (i > 0) out.push(separator);
692
+ out.push(value(item));
380
693
  });
694
+ return out;
381
695
  }
382
- function isRandomLike(value) {
383
- const v = value.trim();
384
- if (!v) return false;
385
- if (UUID_RE.test(v)) return true;
386
- if (REACT_ID_RE.test(v)) return true;
387
- if (HEX_TOKEN_RE.test(v)) return true;
388
- if (RANDOM_DECIMAL_RE.test(v)) return true;
389
- if (NANOID_RE.test(v) && /[A-Za-z]/.test(v) && /[0-9]/.test(v)) return true;
390
- return false;
391
- }
392
- function looksLikeTime(value) {
393
- return /\b\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp][Mm])?\b/.test(
394
- normalizeNumerals(value).trim()
395
- );
696
+ function renderMessage(template, params = {}) {
697
+ if (typeof template === "function") return template(params);
698
+ const out = [];
699
+ template.split("`").forEach((part, i) => {
700
+ if (i % 2 === 1) {
701
+ out.push(
702
+ code(part.replace(/\{(\w+)\}/g, (_, n) => param(params, n)))
703
+ );
704
+ return;
705
+ }
706
+ let last = 0;
707
+ for (const m of part.matchAll(/\{(\w+)\}/g)) {
708
+ const at = m.index ?? 0;
709
+ if (at > last) out.push(part.slice(last, at));
710
+ out.push(value(param(params, m[1])));
711
+ last = at + m[0].length;
712
+ }
713
+ if (last < part.length) out.push(part.slice(last));
714
+ });
715
+ return out.filter((s) => s !== "");
396
716
  }
397
- var ISO_DATE = /^\d{4}-\d{1,2}(?:-\d{1,2})?(?:[T ]\d{1,2}:\d{2}(?::\d{2})?)?/;
398
- var NUMERIC_DATE = /^\d{1,4}[/.-]\d{1,2}[/.-]\d{1,4}$/;
399
- var TIME_OF_DAY = /^\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp]\.?[Mm]\.?)?$/;
400
- var MONTH_NAME = /\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b/i;
401
- function looksLikeDate(v) {
402
- return ISO_DATE.test(v) || NUMERIC_DATE.test(v) || TIME_OF_DAY.test(v) || MONTH_NAME.test(v) && /\d/.test(v);
717
+ function plainText(segments) {
718
+ return segments.map(
719
+ (s) => typeof s === "string" ? s : "code" in s ? `\`${s.code}\`` : s.value
720
+ ).join("");
403
721
  }
404
- function toTimestamp(value) {
405
- const v = value.trim();
406
- if (!v) return null;
407
- if (/^\d{10,13}$/.test(v)) {
408
- const n = Number(v);
409
- return v.length === 10 ? n * 1e3 : n;
722
+ function classDetail(params, words) {
723
+ const added = paramList(params, "added");
724
+ const removed = paramList(params, "removed");
725
+ if (!added.length && !removed.length) return [];
726
+ const out = [words.open];
727
+ if (added.length) {
728
+ out.push(words.added, ...valueList(added, words.listSeparator));
410
729
  }
411
- if (!looksLikeDate(v)) return null;
412
- const parsed = Date.parse(v);
413
- if (!Number.isNaN(parsed)) return parsed;
414
- if (looksLikeTime(v)) {
415
- const anchored = Date.parse(`1970-01-01 ${v}`);
416
- if (!Number.isNaN(anchored)) return anchored;
730
+ if (removed.length) {
731
+ if (added.length) out.push(words.partSeparator);
732
+ out.push(words.removed, ...valueList(removed, words.listSeparator));
417
733
  }
418
- return null;
419
- }
420
- function hasArabicIndicDigits(value) {
421
- return ARABIC_INDIC_DIGITS.test(value);
422
- }
423
- function hasLatinDigits(value) {
424
- return LATIN_DIGITS.test(value);
425
- }
426
- var NUMERIC_LIKE = /^[+-]?[\d.,\s\u00a0\u2009]+$/;
427
- function isSameNumberDifferentSeparators(a, b) {
428
- const at = normalizeNumerals(a).trim();
429
- const bt = normalizeNumerals(b).trim();
430
- if (!NUMERIC_LIKE.test(at) || !NUMERIC_LIKE.test(bt)) return false;
431
- const digitsOnly = (s) => s.replace(/\D/g, "");
432
- const da = digitsOnly(at);
433
- const db = digitsOnly(bt);
434
- if (!da || da !== db) return false;
435
- const hasSep = (s) => /[.,\s\u00a0\u2009]/.test(s);
436
- return (hasSep(at) || hasSep(bt)) && a.trim() !== b.trim();
437
- }
438
- var CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
439
- "value",
440
- "placeholder",
441
- "title",
442
- "alt",
443
- "label",
444
- "aria-label",
445
- "aria-valuetext",
446
- "content",
447
- "datetime"
448
- ]);
449
- function isContentAttribute(name) {
450
- return CONTENT_ATTRIBUTES.has(name.toLowerCase());
451
- }
452
- var THIRD_PARTY_MARKERS = /googlefc|adsbygoogle|google_ads|googletag|__tcfapi|onetrust|optanon|cookiebot|usercentrics|iubenda|didomi|quantcast|grammarly|data-gramm|gtm-|_hjsettings|hotjar|fullstory|intercom|drift|zendesk|livechat|tawk|hubspot|turnstile|recaptcha/i;
453
- function looksLikeThirdPartyNode(html, tagName) {
454
- const tag = (tagName ?? "").toUpperCase();
455
- if (tag === "IFRAME" || tag === "EMBED" || tag === "OBJECT") return true;
456
- const h = (html ?? "").toLowerCase();
457
- if (!h) return false;
458
- return THIRD_PARTY_MARKERS.test(h) || h.includes("about:blank");
459
- }
460
- var DATE_PARTS_RE = /^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/;
461
- function isSameDateDifferentOrder(a, b) {
462
- const partsA = normalizeNumerals(a).trim().match(DATE_PARTS_RE);
463
- const partsB = normalizeNumerals(b).trim().match(DATE_PARTS_RE);
464
- if (!partsA || !partsB) return false;
465
- const setA = [partsA[1], partsA[2], partsA[3]].sort().join("|");
466
- const setB = [partsB[1], partsB[2], partsB[3]].sort().join("|");
467
- return setA === setB && a.trim() !== b.trim();
468
- }
469
- var EXTENSION_ATTRIBUTES = /* @__PURE__ */ new Set([
470
- "cz-shortcut-listen",
471
- "data-gramm",
472
- "data-gramm_editor",
473
- "data-gramm_id",
474
- "data-gr-c-s-loaded",
475
- "data-lt-installed",
476
- "data-new-gr-c-s-check-loaded",
477
- "data-new-gr-c-s-loaded",
478
- "spellcheck-extension",
479
- "bis_register",
480
- "__processed_by_react_dev_tools"
481
- ]);
482
- function isExtensionAttribute(name) {
483
- const n = name.toLowerCase();
484
- if (EXTENSION_ATTRIBUTES.has(n)) return true;
485
- return n.startsWith("data-gr-") || n.startsWith("data-gramm") || n.startsWith("__bis") || n.startsWith("bis_");
734
+ out.push(words.close);
735
+ return out;
486
736
  }
487
- var BLOCK_TAGS = /* @__PURE__ */ new Set([
488
- "DIV",
489
- "P",
490
- "SECTION",
491
- "ARTICLE",
492
- "UL",
493
- "OL",
494
- "LI",
495
- "TABLE",
496
- "HEADER",
497
- "FOOTER",
498
- "MAIN",
499
- "ASIDE",
500
- "NAV",
501
- "H1",
502
- "H2",
503
- "H3",
504
- "H4",
505
- "H5",
506
- "H6",
507
- "FORM",
508
- "BLOCKQUOTE",
509
- "PRE",
510
- "HR"
511
- ]);
512
- function isInvalidNesting(parentTag, childTag) {
513
- if (!parentTag || !childTag) return false;
514
- const p = parentTag.toUpperCase();
515
- const c = childTag.toUpperCase();
516
- if (p === "P" && BLOCK_TAGS.has(c)) return true;
517
- if (p === "A" && c === "A") return true;
518
- if (p === "BUTTON" && (c === "BUTTON" || c === "A")) return true;
519
- if ((p === "TABLE" || p === "THEAD" || p === "TBODY") && c === "DIV") {
520
- return true;
737
+ var EN_MESSAGES = {
738
+ "non-deterministic-value": {
739
+ explanation: "The server and client rendered different random-looking values (an id, token, or Math.random() output). Anything non-deterministic in render produces a different value on each side.",
740
+ suggestion: "Use React `useId()` for ids. For random values, generate them after mount (in `useEffect`) or pass a value down from the server so both sides agree. Never call `Math.random()`/`crypto` during render."
741
+ },
742
+ "date-time": {
743
+ explanation: "The values are dates/times that differ between server render and client render \u2014 the clock moved (or the timezone differs) between the two environments.",
744
+ suggestion: "Render the current time after mount, or pass a single server timestamp down and format it identically on both sides. Pin an explicit timezone when formatting."
745
+ },
746
+ "locale-format.bidi": {
747
+ explanation: "The values differ only by invisible bidirectional control marks (LRM/RLM/isolates). `Intl` adds these around numbers and dates in RTL locales, and different ICU versions \u2014 Node vs the browser \u2014 emit different ones for the same input.",
748
+ suggestion: "Format the value in one place and pass the string down, or pin the same locale and timezone on both sides. If the marks are harmless, add `suppressHydrationWarning` to the element."
749
+ },
750
+ "locale-format.script": {
751
+ explanation: "The same value was formatted with different digit scripts (Arabic-Indic \u0660\u0661\u0662 vs Latin 012). The server and client resolved to different locales.",
752
+ suggestion: "Pass an explicit `locale` (and timezone) to `Intl.NumberFormat` / `toLocaleString` on both server and client, or format the value after mount so only the client locale is ever used."
753
+ },
754
+ "locale-format.separators": {
755
+ explanation: "The same number was formatted with different grouping/decimal separators between server and client (e.g. 1,234.56 vs 1.234,56).",
756
+ suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match."
757
+ },
758
+ "locale-format.date-order": {
759
+ explanation: "The same date was rendered in a different field order (MM/DD vs DD/MM) between server and client.",
760
+ suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides."
761
+ },
762
+ "browser-only-api": {
763
+ explanation: "The client rendered content the server left empty \u2014 the signature of reading a browser-only API (`window`, `document`, `localStorage`, `navigator`, `matchMedia`) during render.",
764
+ suggestion: "Gate browser-only reads behind a mounted flag or `useEffect`, or use `useSyncExternalStore` with a server snapshot so the first client render matches the server."
765
+ },
766
+ "viewport-branching": {
767
+ explanation: "A whole subtree was added, removed, or swapped between server and client \u2014 typically a JavaScript width/viewport check that branches the tree at first render.",
768
+ suggestion: "Render both branches and switch between them with CSS media queries at first paint instead of branching in JavaScript, or defer the JS-driven branch until after mount."
769
+ },
770
+ "invalid-html-nesting": {
771
+ explanation: "A node was moved or ejected because the markup is invalid HTML (e.g. a `<div>` inside a `<p>`, or nested `<a>`). The browser repairs the server DOM, so it no longer matches what React expects.",
772
+ suggestion: "Fix the markup validity: block elements cannot live inside `<p>`, anchors cannot nest, etc. Replace the invalid parent with a `<div>` or restructure the tree."
773
+ },
774
+ "whitespace-minification": {
775
+ explanation: "The mismatch is whitespace-only \u2014 the text is identical apart from spaces/newlines. An HTML minifier likely collapsed whitespace around the hydration root differently from React.",
776
+ suggestion: "Check your HTML minifier settings (e.g. `conservativeCollapse`) around the app root, or avoid minifying whitespace inside hydrated markup."
777
+ },
778
+ "third-party-dom-mutation.extension-attribute": {
779
+ explanation: "The attribute `{attribute}` was injected by a browser extension or third-party script (e.g. Grammarly, ColorZilla) before hydration, so the client DOM no longer matches the server.",
780
+ suggestion: "This is usually harmless. Add `suppressHydrationWarning` to the affected element, or defer third-party script init until after hydration."
781
+ },
782
+ "third-party-dom-mutation.root-attribute": {
783
+ explanation: "An attribute (`{attribute}`) appeared on a root element that the server never sent \u2014 a hallmark of an extension or early third-party script mutating the DOM.",
784
+ suggestion: "Add `suppressHydrationWarning` to the root element, or defer the third-party script until after hydration."
785
+ },
786
+ "third-party-dom-mutation.injected-node": {
787
+ explanation: "A {tag} was injected by a third-party script or browser extension (ads, consent, analytics, chat) after the server render. It is not part of your app's hydration, so this is usually harmless noise.",
788
+ suggestion: 'If React warns about it, add `suppressHydrationWarning` to the nearest server-rendered wrapper, or load the third-party script after hydration (e.g. Next.js `<Script strategy="afterInteractive">`).'
789
+ },
790
+ "attribute-mismatch.class": {
791
+ explanation: (p) => [
792
+ "The ",
793
+ code("class"),
794
+ " differs between server and client",
795
+ ...classDetail(p, {
796
+ added: "added on client: ",
797
+ removed: "removed on client: ",
798
+ listSeparator: ", ",
799
+ partSeparator: "; ",
800
+ open: " (",
801
+ close: ")"
802
+ }),
803
+ ". A class was applied conditionally on the client \u2014 commonly a viewport, media-query, theme, or feature-flag check that runs during the first render."
804
+ ],
805
+ suggestion: "Render the same className on the server and the first client paint. Move client-only conditions into `useEffect`/a mounted flag, or drive the visual change with CSS media queries instead of a JS class toggle."
806
+ },
807
+ "attribute-mismatch.style": {
808
+ explanation: "The inline `style` differs between server and client \u2014 an inline style was computed from client-only state (viewport size, theme, scroll position) during render.",
809
+ suggestion: "Compute the style after mount (`useEffect`) so the first client render matches the server, or move it to a CSS class / media query."
810
+ },
811
+ "attribute-mismatch.generic": {
812
+ explanation: "The `{attribute}` attribute differs between server (`{server}`) and client (`{client}`) \u2014 its value was derived from something that differs between the server and the first client render.",
813
+ suggestion: "Make the attribute deterministic across server and client, or set it after mount so the first client render matches the server HTML."
814
+ },
815
+ unknown: {
816
+ explanation: "A hydration mismatch was detected but could not be matched to a known cause. Inspect the server vs client values above.",
817
+ suggestion: "Compare the server and client values. Common causes are non-deterministic values, dates/locales, and browser-only APIs used during render."
818
+ },
819
+ "unknown.no-location": {
820
+ explanation: "React reported that hydration failed but did not say which node differed, and the DOM diff found no difference to point at.",
821
+ suggestion: "Make sure `<HydrationSnapshotScript>` (or the manual snapshot script) is in `<head>` so the DOM diff can locate the node, and read React's full warning in the browser console."
521
822
  }
522
- return false;
523
- }
524
- function messageIndicatesInvalidNesting(message) {
525
- if (!message) return false;
526
- return /validateDOMNesting/i.test(message) || /cannot (?:be a|contain).*(?:descendant|child)/i.test(message) || /cannot appear as a (?:child|descendant)/i.test(message);
823
+ };
824
+ function describe(id, params = {}, catalog = EN_MESSAGES) {
825
+ const entry = catalog[id];
826
+ return {
827
+ messageId: id,
828
+ ...Object.keys(params).length ? { params } : {},
829
+ explanation: plainText(renderMessage(entry.explanation, params)),
830
+ suggestion: plainText(renderMessage(entry.suggestion, params))
831
+ };
527
832
  }
528
833
 
529
834
  // src/core/classify/rules.ts
@@ -548,8 +853,7 @@ var nonDeterministic = (d) => {
548
853
  return {
549
854
  category: "non-deterministic-value",
550
855
  confidence: 0.9,
551
- explanation: "The server and client rendered different random-looking values (an id, token, or Math.random() output). Anything non-deterministic in render produces a different value on each side.",
552
- suggestion: "Use React `useId()` for ids. For random values, generate them after mount (in `useEffect`) or pass a value down from the server so both sides agree. Never call `Math.random()`/`crypto` during render.",
856
+ ...describe("non-deterministic-value"),
553
857
  docsUrl: docs("non-deterministic-value")
554
858
  };
555
859
  };
@@ -567,8 +871,7 @@ var dateTime = (d) => {
567
871
  return {
568
872
  category: "date-time",
569
873
  confidence: smallDelta || bothTimes ? 0.85 : 0.7,
570
- explanation: "The values are dates/times that differ between server render and client render \u2014 the clock moved (or the timezone differs) between the two environments.",
571
- suggestion: "Render the current time after mount, or pass a single server timestamp down and format it identically on both sides. Pin an explicit timezone when formatting.",
874
+ ...describe("date-time"),
572
875
  docsUrl: docs("date-time")
573
876
  };
574
877
  };
@@ -580,8 +883,7 @@ var localeFormat = (d) => {
580
883
  return {
581
884
  category: "locale-format",
582
885
  confidence: 0.88,
583
- explanation: "The values differ only by invisible bidirectional control marks (LRM/RLM/isolates). `Intl` adds these around numbers and dates in RTL locales, and different ICU versions \u2014 Node vs the browser \u2014 emit different ones for the same input.",
584
- suggestion: "Format the value in one place and pass the string down, or pin the same locale and timezone on both sides. If the marks are harmless, add `suppressHydrationWarning` to the element.",
886
+ ...describe("locale-format.bidi"),
585
887
  docsUrl: docs("locale-format")
586
888
  };
587
889
  }
@@ -590,8 +892,7 @@ var localeFormat = (d) => {
590
892
  return {
591
893
  category: "locale-format",
592
894
  confidence: 0.92,
593
- explanation: "The same value was formatted with different digit scripts (Arabic-Indic \u0660\u0661\u0662 vs Latin 012). The server and client resolved to different locales.",
594
- suggestion: "Pass an explicit `locale` (and timezone) to `Intl.NumberFormat` / `toLocaleString` on both server and client, or format the value after mount so only the client locale is ever used.",
895
+ ...describe("locale-format.script"),
595
896
  docsUrl: docs("locale-format")
596
897
  };
597
898
  }
@@ -599,8 +900,7 @@ var localeFormat = (d) => {
599
900
  return {
600
901
  category: "locale-format",
601
902
  confidence: 0.82,
602
- explanation: "The same number was formatted with different grouping/decimal separators between server and client (e.g. 1,234.56 vs 1.234,56).",
603
- suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match.",
903
+ ...describe("locale-format.separators"),
604
904
  docsUrl: docs("locale-format")
605
905
  };
606
906
  }
@@ -608,8 +908,7 @@ var localeFormat = (d) => {
608
908
  return {
609
909
  category: "locale-format",
610
910
  confidence: 0.75,
611
- explanation: "The same date was rendered in a different field order (MM/DD vs DD/MM) between server and client.",
612
- suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides.",
911
+ ...describe("locale-format.date-order"),
613
912
  docsUrl: docs("locale-format")
614
913
  };
615
914
  }
@@ -623,8 +922,7 @@ var browserOnlyApi = (d) => {
623
922
  return {
624
923
  category: "browser-only-api",
625
924
  confidence: 0.75,
626
- explanation: "The client rendered content the server left empty \u2014 the signature of reading a browser-only API (`window`, `document`, `localStorage`, `navigator`, `matchMedia`) during render.",
627
- suggestion: "Gate browser-only reads behind a mounted flag or `useEffect`, or use `useSyncExternalStore` with a server snapshot so the first client render matches the server.",
925
+ ...describe("browser-only-api"),
628
926
  docsUrl: docs("browser-only-api")
629
927
  };
630
928
  };
@@ -638,8 +936,7 @@ var viewportBranching = (d) => {
638
936
  return {
639
937
  category: "viewport-branching",
640
938
  confidence: 0.6,
641
- explanation: "A whole subtree was added, removed, or swapped between server and client \u2014 typically a JavaScript width/viewport check that branches the tree at first render.",
642
- suggestion: "Render both branches and switch between them with CSS media queries at first paint instead of branching in JavaScript, or defer the JS-driven branch until after mount.",
939
+ ...describe("viewport-branching"),
643
940
  docsUrl: docs("viewport-branching")
644
941
  };
645
942
  };
@@ -650,8 +947,7 @@ var invalidNesting = (d) => {
650
947
  return {
651
948
  category: "invalid-html-nesting",
652
949
  confidence: byMessage ? 0.9 : 0.72,
653
- explanation: "A node was moved or ejected because the markup is invalid HTML (e.g. a `<div>` inside a `<p>`, or nested `<a>`). The browser repairs the server DOM, so it no longer matches what React expects.",
654
- suggestion: "Fix the markup validity: block elements cannot live inside `<p>`, anchors cannot nest, etc. Replace the invalid parent with a `<div>` or restructure the tree.",
950
+ ...describe("invalid-html-nesting"),
655
951
  docsUrl: docs("invalid-html-nesting")
656
952
  };
657
953
  };
@@ -664,8 +960,7 @@ var whitespaceMinification = (d) => {
664
960
  return {
665
961
  category: "whitespace-minification",
666
962
  confidence: 0.7,
667
- explanation: "The mismatch is whitespace-only \u2014 the text is identical apart from spaces/newlines. An HTML minifier likely collapsed whitespace around the hydration root differently from React.",
668
- suggestion: "Check your HTML minifier settings (e.g. `conservativeCollapse`) around the app root, or avoid minifying whitespace inside hydrated markup.",
963
+ ...describe("whitespace-minification"),
669
964
  docsUrl: docs("whitespace-minification")
670
965
  };
671
966
  };
@@ -675,8 +970,9 @@ var thirdPartyDomMutation = (d) => {
675
970
  return {
676
971
  category: "third-party-dom-mutation",
677
972
  confidence: 0.88,
678
- explanation: `The attribute \`${d.attribute}\` was injected by a browser extension or third-party script (e.g. Grammarly, ColorZilla) before hydration, so the client DOM no longer matches the server.`,
679
- suggestion: "This is usually harmless. Add `suppressHydrationWarning` to the affected element, or defer third-party script init until after hydration.",
973
+ ...describe("third-party-dom-mutation.extension-attribute", {
974
+ attribute: d.attribute
975
+ }),
680
976
  docsUrl: docs("third-party-dom-mutation")
681
977
  };
682
978
  }
@@ -686,8 +982,9 @@ var thirdPartyDomMutation = (d) => {
686
982
  return {
687
983
  category: "third-party-dom-mutation",
688
984
  confidence: 0.6,
689
- explanation: `An attribute (\`${d.attribute}\`) appeared on a root element that the server never sent \u2014 a hallmark of an extension or early third-party script mutating the DOM.`,
690
- suggestion: "Add `suppressHydrationWarning` to the root element, or defer the third-party script until after hydration.",
985
+ ...describe("third-party-dom-mutation.root-attribute", {
986
+ attribute: d.attribute
987
+ }),
691
988
  docsUrl: docs("third-party-dom-mutation")
692
989
  };
693
990
  }
@@ -697,8 +994,9 @@ var thirdPartyDomMutation = (d) => {
697
994
  return {
698
995
  category: "third-party-dom-mutation",
699
996
  confidence: 0.7,
700
- explanation: `A <${tag}> was injected by a third-party script or browser extension (ads, consent, analytics, chat) after the server render. It is not part of your app's hydration, so this is usually harmless noise.`,
701
- suggestion: 'If React warns about it, add `suppressHydrationWarning` to the nearest server-rendered wrapper, or load the third-party script after hydration (e.g. Next.js `<Script strategy="afterInteractive">`).',
997
+ ...describe("third-party-dom-mutation.injected-node", {
998
+ tag: `<${tag}>`
999
+ }),
702
1000
  docsUrl: docs("third-party-dom-mutation")
703
1001
  };
704
1002
  }
@@ -714,15 +1012,10 @@ var attributeMismatch = (d) => {
714
1012
  const clientSet = new Set(client.split(/\s+/).filter(Boolean));
715
1013
  const added = [...clientSet].filter((c) => !serverSet.has(c));
716
1014
  const removed = [...serverSet].filter((c) => !clientSet.has(c));
717
- const parts = [];
718
- if (added.length) parts.push(`added on client: ${added.join(", ")}`);
719
- if (removed.length) parts.push(`removed on client: ${removed.join(", ")}`);
720
- const detail = parts.length ? ` (${parts.join("; ")})` : "";
721
1015
  return {
722
1016
  category: "attribute-mismatch",
723
1017
  confidence: 0.8,
724
- explanation: `The \`class\` differs between server and client${detail}. A class was applied conditionally on the client \u2014 commonly a viewport, media-query, theme, or feature-flag check that runs during the first render.`,
725
- suggestion: "Render the same className on the server and the first client paint. Move client-only conditions into `useEffect`/a mounted flag, or drive the visual change with CSS media queries instead of a JS class toggle.",
1018
+ ...describe("attribute-mismatch.class", { added, removed }),
726
1019
  docsUrl: docs("attribute-mismatch")
727
1020
  };
728
1021
  }
@@ -730,16 +1023,18 @@ var attributeMismatch = (d) => {
730
1023
  return {
731
1024
  category: "attribute-mismatch",
732
1025
  confidence: 0.75,
733
- explanation: "The inline `style` differs between server and client \u2014 an inline style was computed from client-only state (viewport size, theme, scroll position) during render.",
734
- suggestion: "Compute the style after mount (`useEffect`) so the first client render matches the server, or move it to a CSS class / media query.",
1026
+ ...describe("attribute-mismatch.style"),
735
1027
  docsUrl: docs("attribute-mismatch")
736
1028
  };
737
1029
  }
738
1030
  return {
739
1031
  category: "attribute-mismatch",
740
1032
  confidence: 0.6,
741
- explanation: `The \`${d.attribute}\` attribute differs between server (\`${server}\`) and client (\`${client}\`) \u2014 its value was derived from something that differs between the server and the first client render.`,
742
- suggestion: "Make the attribute deterministic across server and client, or set it after mount so the first client render matches the server HTML.",
1033
+ ...describe("attribute-mismatch.generic", {
1034
+ attribute: d.attribute,
1035
+ server,
1036
+ client
1037
+ }),
743
1038
  docsUrl: docs("attribute-mismatch")
744
1039
  };
745
1040
  };
@@ -761,13 +1056,21 @@ var BUILT_IN_RULES = [
761
1056
  var UNKNOWN_CAUSE = {
762
1057
  category: "unknown",
763
1058
  confidence: 0,
764
- explanation: "A hydration mismatch was detected but could not be matched to a known cause. Inspect the server vs client values above.",
765
- suggestion: "Compare the server and client values. Common causes are non-deterministic values, dates/locales, and browser-only APIs used during render.",
1059
+ ...describe("unknown"),
1060
+ docsUrl: docs("unknown")
1061
+ };
1062
+ var UNKNOWN_NO_LOCATION_CAUSE = {
1063
+ category: "unknown",
1064
+ confidence: 0,
1065
+ ...describe("unknown.no-location"),
766
1066
  docsUrl: docs("unknown")
767
1067
  };
768
1068
 
769
1069
  // src/core/classify/index.ts
770
1070
  var CONFIDENCE_THRESHOLD = 0.5;
1071
+ function isLocationless(d) {
1072
+ return d.server == null && d.client == null && !d.tagName && !d.attribute;
1073
+ }
771
1074
  function classify(divergence, options = {}) {
772
1075
  const threshold = options.threshold ?? CONFIDENCE_THRESHOLD;
773
1076
  const rules = [
@@ -785,7 +1088,7 @@ function classify(divergence, options = {}) {
785
1088
  return result;
786
1089
  }
787
1090
  }
788
- return UNKNOWN_CAUSE;
1091
+ return isLocationless(divergence) ? UNKNOWN_NO_LOCATION_CAUSE : UNKNOWN_CAUSE;
789
1092
  }
790
1093
 
791
1094
  // src/core/report.ts
@@ -794,12 +1097,12 @@ function nextId() {
794
1097
  counter += 1;
795
1098
  return `wh_${Date.now().toString(36)}_${counter}`;
796
1099
  }
797
- function truncate(value, max = 300) {
798
- if (value == null) return null;
799
- if (value.length <= max) return value;
800
- const last = value.charCodeAt(max - 1);
1100
+ function truncate(value2, max = 300) {
1101
+ if (value2 == null) return null;
1102
+ if (value2.length <= max) return value2;
1103
+ const last = value2.charCodeAt(max - 1);
801
1104
  const end = last >= 55296 && last <= 56319 ? max - 1 : max;
802
- return `${value.slice(0, end)}\u2026`;
1105
+ return `${value2.slice(0, end)}\u2026`;
803
1106
  }
804
1107
  function buildReport(divergence, cause, context = {}) {
805
1108
  return {
@@ -837,6 +1140,16 @@ var ReportCollector = class {
837
1140
  this.options = options;
838
1141
  this.maxReports = options.maxReports ?? 25;
839
1142
  }
1143
+ /**
1144
+ * Swap the rules, threshold, ignore list and cap for everything reported
1145
+ * from now on. Reports already collected are kept as they are: they were
1146
+ * correct under the options in force when they were made, and re-deciding
1147
+ * them would re-fire `onReport` for mismatches the caller already saw.
1148
+ */
1149
+ configure(options) {
1150
+ this.options = options;
1151
+ this.maxReports = options.maxReports ?? 25;
1152
+ }
840
1153
  /**
841
1154
  * Register a sink. Pass `replay` for sinks that render *state* (the overlay)
842
1155
  * rather than react to *events* (`onReport`, the console): they need the
@@ -859,16 +1172,20 @@ var ReportCollector = class {
859
1172
  getReports() {
860
1173
  return this.reports;
861
1174
  }
1175
+ /** The cause this collector would assign — same rules, same threshold. */
1176
+ classify(divergence) {
1177
+ return classify(divergence, {
1178
+ extra: this.options.extra,
1179
+ threshold: this.options.threshold
1180
+ });
1181
+ }
862
1182
  get isFull() {
863
1183
  return this.reports.length >= this.maxReports;
864
1184
  }
865
1185
  report(divergence, context = {}) {
866
1186
  if (this.isFull) return null;
867
1187
  if (this.options.ignore?.(divergence)) return null;
868
- const cause = classify(divergence, {
869
- extra: this.options.extra,
870
- threshold: this.options.threshold
871
- });
1188
+ const cause = this.classify(divergence);
872
1189
  const report = buildReport(divergence, cause, context);
873
1190
  const signature = signatureOf(report);
874
1191
  if (this.seen.has(signature)) return null;
@@ -918,11 +1235,19 @@ function extractComponentFromMessage(message) {
918
1235
  }
919
1236
  return names.length ? names[names.length - 1] : void 0;
920
1237
  }
1238
+ function diffLines(message) {
1239
+ const lines = message.split("\n");
1240
+ let link = -1;
1241
+ lines.forEach((line, i) => {
1242
+ if (/react\.dev\/link\/hydration-mismatch/.test(line)) link = i;
1243
+ });
1244
+ return link >= 0 ? lines.slice(link + 1) : lines;
1245
+ }
921
1246
  var ATTR_RE = /^([\w:-]+)=(?:"([\s\S]*)"|\{([\s\S]*)\})$/;
922
1247
  function parseModernDiff(message) {
923
1248
  const plus = [];
924
1249
  const minus = [];
925
- for (const raw of message.split("\n")) {
1250
+ for (const raw of diffLines(message)) {
926
1251
  const line = raw.trim();
927
1252
  const p = /^\+\s+(.+)$/.exec(line);
928
1253
  const mn = /^-\s+(.+)$/.exec(line);
@@ -1054,7 +1379,7 @@ function parseAllHydrationDivergences(message) {
1054
1379
  };
1055
1380
  const plus = [];
1056
1381
  const minus = [];
1057
- for (const raw of message.split("\n")) {
1382
+ for (const raw of diffLines(message)) {
1058
1383
  const line = raw.trim();
1059
1384
  const p = /^\+\s+(.+)$/.exec(line);
1060
1385
  const mn = /^-\s+(.+)$/.exec(line);
@@ -1137,8 +1462,13 @@ function serverTreeFor(root, html) {
1137
1462
  serverTrees.set(root, { html, tree });
1138
1463
  return tree;
1139
1464
  }
1140
- function reportFromMessage(message, collector, context = {}) {
1141
- const divergences = parseAllHydrationDivergences(message);
1465
+ function reportFromMessage(message, collector, context = {}, options = {}) {
1466
+ const mode = options.locationless ?? "include";
1467
+ const divergences = parseAllHydrationDivergences(message).filter((d) => {
1468
+ if (mode === "include") return true;
1469
+ const bare = isLocationless(d) && collector.classify(d).messageId === "unknown.no-location";
1470
+ return mode === "skip" ? !bare : bare;
1471
+ });
1142
1472
  const component = extractComponentFromMessage(message) ?? context.component;
1143
1473
  let reported = 0;
1144
1474
  for (const divergence of divergences) {
@@ -1155,19 +1485,28 @@ function reportFromMessage(message, collector, context = {}) {
1155
1485
 
1156
1486
  exports.BUILT_IN_RULES = BUILT_IN_RULES;
1157
1487
  exports.CONFIDENCE_THRESHOLD = CONFIDENCE_THRESHOLD;
1488
+ exports.EN_MESSAGES = EN_MESSAGES;
1158
1489
  exports.ReportCollector = ReportCollector;
1159
1490
  exports.UNKNOWN_CAUSE = UNKNOWN_CAUSE;
1491
+ exports.UNKNOWN_NO_LOCATION_CAUSE = UNKNOWN_NO_LOCATION_CAUSE;
1160
1492
  exports.buildReport = buildReport;
1493
+ exports.classDetail = classDetail;
1161
1494
  exports.classify = classify;
1495
+ exports.code = code;
1162
1496
  exports.diffSnapshotAgainstDom = diffSnapshotAgainstDom;
1163
1497
  exports.diffTrees = diffTrees;
1164
1498
  exports.formatConsoleArgs = formatConsoleArgs;
1165
1499
  exports.inspectRoot = inspectRoot;
1166
1500
  exports.isDev = isDev;
1167
1501
  exports.isHydrationMessage = isHydrationMessage;
1502
+ exports.isInternalComponent = isInternalComponent;
1503
+ exports.isLocationless = isLocationless;
1504
+ exports.param = param;
1168
1505
  exports.parseHydrationMessage = parseHydrationMessage;
1169
1506
  exports.parseServerHtml = parseServerHtml;
1507
+ exports.plainText = plainText;
1508
+ exports.renderMessage = renderMessage;
1170
1509
  exports.reportFromMessage = reportFromMessage;
1171
1510
  exports.signatureOf = signatureOf;
1172
- //# sourceMappingURL=chunk-AT6A77X3.cjs.map
1173
- //# sourceMappingURL=chunk-AT6A77X3.cjs.map
1511
+ //# sourceMappingURL=chunk-QFHUKJ5K.cjs.map
1512
+ //# sourceMappingURL=chunk-QFHUKJ5K.cjs.map