why-hydration 0.1.1 → 0.1.3

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.
@@ -10,57 +10,84 @@ function parseServerHtml(html, rootTagName) {
10
10
  container.innerHTML = html;
11
11
  return container;
12
12
  }
13
- function diffTrees(serverRoot, clientRoot, basePath = tagPath(clientRoot)) {
14
- return diffChildren(serverRoot, clientRoot, basePath);
13
+ function collectDivergences(serverRoot, clientRoot, limit = 60) {
14
+ const out = [];
15
+ collectChildren(serverRoot, clientRoot, tagPath(clientRoot), out, limit);
16
+ return out;
15
17
  }
16
- function diffSnapshotAgainstDom(serverHtml, clientRoot) {
18
+ function collectSnapshotAgainstDom(serverHtml, clientRoot, limit = 60) {
17
19
  const serverRoot = parseServerHtml(serverHtml, clientRoot.tagName);
18
- return diffTrees(serverRoot, clientRoot);
20
+ return collectDivergences(serverRoot, clientRoot, limit);
21
+ }
22
+ function diffTrees(serverRoot, clientRoot) {
23
+ return collectDivergences(serverRoot, clientRoot, 1)[0] ?? null;
19
24
  }
20
- function diffChildren(serverParent, clientParent, parentPath) {
25
+ function diffSnapshotAgainstDom(serverHtml, clientRoot) {
26
+ return collectSnapshotAgainstDom(serverHtml, clientRoot, 1)[0] ?? null;
27
+ }
28
+ function collectChildren(serverParent, clientParent, parentPath, out, limit) {
29
+ if (out.length >= limit) return;
21
30
  const serverChildren = meaningfulChildNodes(serverParent);
22
31
  const clientChildren = meaningfulChildNodes(clientParent);
23
- const max = Math.max(serverChildren.length, clientChildren.length);
24
- for (let i = 0; i < max; i++) {
25
- const serverNode = serverChildren[i] ?? null;
26
- const clientNode = clientChildren[i] ?? null;
27
- const path = childPath(parentPath, clientNode ?? serverNode, i);
32
+ const parentPending = hasPendingSuspense(serverParent);
33
+ const parentTag = elementTag(clientParent);
34
+ const pairs = alignChildren(serverChildren, clientChildren);
35
+ let index = 0;
36
+ for (let k = 0; k < pairs.length; k++) {
37
+ if (out.length >= limit) return;
38
+ const [serverNode, clientNode] = pairs[k];
39
+ if (serverNode && !clientNode && k + 1 < pairs.length) {
40
+ const [nextServer, nextClient] = pairs[k + 1];
41
+ if (!nextServer && nextClient && !isInjectedContainer(nextClient)) {
42
+ const path2 = childPath(parentPath, nextClient, index);
43
+ index += 1;
44
+ k += 1;
45
+ out.push({
46
+ kind: "structure",
47
+ path: path2,
48
+ tagName: elementTag(nextClient),
49
+ parentTagName: parentTag,
50
+ server: serialize(serverNode),
51
+ client: serialize(nextClient),
52
+ element: asElement(nextClient)
53
+ });
54
+ continue;
55
+ }
56
+ }
57
+ const anchor = clientNode ?? serverNode;
58
+ if (!anchor) continue;
59
+ const path = childPath(parentPath, anchor, index);
60
+ index += 1;
28
61
  if (serverNode && !clientNode) {
29
- return {
62
+ out.push({
30
63
  kind: "node-removed",
31
64
  path,
32
65
  tagName: elementTag(serverNode),
33
- parentTagName: elementTag(clientParent),
66
+ parentTagName: parentTag,
34
67
  server: serialize(serverNode),
35
68
  client: null,
36
69
  element: asElement(clientParent)
37
- };
38
- }
39
- if (!serverNode && clientNode) {
40
- return {
70
+ });
71
+ } else if (!serverNode && clientNode) {
72
+ if (isInjectedContainer(clientNode)) continue;
73
+ if (clientNode.nodeType === Node.ELEMENT_NODE && parentPending) continue;
74
+ out.push({
41
75
  kind: "node-added",
42
76
  path,
43
77
  tagName: elementTag(clientNode),
44
- parentTagName: elementTag(clientParent),
78
+ parentTagName: parentTag,
45
79
  server: null,
46
80
  client: serialize(clientNode),
47
81
  element: asElement(clientNode)
48
- };
82
+ });
83
+ } else if (serverNode && clientNode) {
84
+ collectNode(serverNode, clientNode, path, parentTag, out, limit);
49
85
  }
50
- if (!serverNode || !clientNode) continue;
51
- const divergence = diffNode(
52
- serverNode,
53
- clientNode,
54
- path,
55
- elementTag(clientParent)
56
- );
57
- if (divergence) return divergence;
58
86
  }
59
- return null;
60
87
  }
61
- function diffNode(serverNode, clientNode, path, parentTag) {
88
+ function collectNode(serverNode, clientNode, path, parentTag, out, limit) {
62
89
  if (serverNode.nodeType !== clientNode.nodeType) {
63
- return {
90
+ out.push({
64
91
  kind: "structure",
65
92
  path,
66
93
  tagName: elementTag(clientNode),
@@ -68,28 +95,29 @@ function diffNode(serverNode, clientNode, path, parentTag) {
68
95
  server: serialize(serverNode),
69
96
  client: serialize(clientNode),
70
97
  element: asElement(clientNode)
71
- };
98
+ });
99
+ return;
72
100
  }
73
101
  if (serverNode.nodeType === Node.TEXT_NODE || serverNode.nodeType === Node.COMMENT_NODE) {
74
102
  const serverText = serverNode.textContent ?? "";
75
103
  const clientText = clientNode.textContent ?? "";
76
104
  if (serverText !== clientText) {
77
- return {
105
+ out.push({
78
106
  kind: "text",
79
107
  path,
80
108
  parentTagName: parentTag,
81
109
  server: serverText,
82
110
  client: clientText,
83
111
  element: asElement(clientNode.parentNode)
84
- };
112
+ });
85
113
  }
86
- return null;
114
+ return;
87
115
  }
88
116
  if (serverNode.nodeType === Node.ELEMENT_NODE) {
89
117
  const serverEl = serverNode;
90
118
  const clientEl = clientNode;
91
119
  if (serverEl.tagName !== clientEl.tagName) {
92
- return {
120
+ out.push({
93
121
  kind: "structure",
94
122
  path,
95
123
  tagName: clientEl.tagName,
@@ -97,13 +125,13 @@ function diffNode(serverNode, clientNode, path, parentTag) {
97
125
  server: serialize(serverEl),
98
126
  client: serialize(clientEl),
99
127
  element: clientEl
100
- };
128
+ });
129
+ return;
101
130
  }
102
131
  const attrDivergence = diffAttributes(serverEl, clientEl, path, parentTag);
103
- if (attrDivergence) return attrDivergence;
104
- return diffChildren(serverEl, clientEl, path);
132
+ if (attrDivergence) out.push(attrDivergence);
133
+ collectChildren(serverEl, clientEl, path, out, limit);
105
134
  }
106
- return null;
107
135
  }
108
136
  function diffAttributes(serverEl, clientEl, path, parentTag) {
109
137
  const names = /* @__PURE__ */ new Set();
@@ -155,6 +183,56 @@ function normalizeStyle(value) {
155
183
  }
156
184
  return value.split(";").map((s) => s.trim()).filter(Boolean).sort().join(";");
157
185
  }
186
+ function nodeKey(node) {
187
+ if (node.nodeType === Node.ELEMENT_NODE) {
188
+ const el = node;
189
+ return el.id ? `${el.tagName}#${el.id}` : el.tagName;
190
+ }
191
+ if (node.nodeType === Node.TEXT_NODE) return "#text";
192
+ return "#other";
193
+ }
194
+ function alignChildren(server, client) {
195
+ const m = server.length;
196
+ const n = client.length;
197
+ if (m === 0 || n === 0 || m > 200 || n > 200 || m * n > 1e4) {
198
+ const pairs2 = [];
199
+ const max = Math.max(m, n);
200
+ for (let i2 = 0; i2 < max; i2++) {
201
+ pairs2.push([server[i2] ?? null, client[i2] ?? null]);
202
+ }
203
+ return pairs2;
204
+ }
205
+ const sk = server.map(nodeKey);
206
+ const ck = client.map(nodeKey);
207
+ const dp = Array.from(
208
+ { length: m + 1 },
209
+ () => new Array(n + 1).fill(0)
210
+ );
211
+ for (let i2 = m - 1; i2 >= 0; i2--) {
212
+ for (let j2 = n - 1; j2 >= 0; j2--) {
213
+ dp[i2][j2] = sk[i2] === ck[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
214
+ }
215
+ }
216
+ const pairs = [];
217
+ let i = 0;
218
+ let j = 0;
219
+ while (i < m && j < n) {
220
+ if (sk[i] === ck[j]) {
221
+ pairs.push([server[i], client[j]]);
222
+ i++;
223
+ j++;
224
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
225
+ pairs.push([server[i], null]);
226
+ i++;
227
+ } else {
228
+ pairs.push([null, client[j]]);
229
+ j++;
230
+ }
231
+ }
232
+ while (i < m) pairs.push([server[i++], null]);
233
+ while (j < n) pairs.push([null, client[j++]]);
234
+ return pairs;
235
+ }
158
236
  var NOISE_TAGS = /* @__PURE__ */ new Set([
159
237
  "SCRIPT",
160
238
  "STYLE",
@@ -162,27 +240,79 @@ var NOISE_TAGS = /* @__PURE__ */ new Set([
162
240
  "TEMPLATE",
163
241
  "NOSCRIPT"
164
242
  ]);
243
+ var TRACKER_MARKER = /googlefc|adsbygoogle|google_ads|googletag|__tcfapi|onetrust|optanon|cookiebot|usercentrics|didomi|quantcast|grammarly|gtm|hotjar|fullstory|intercom|drift|zendesk|livechat|tawk|hubspot|turnstile|recaptcha/i;
244
+ var INJECTED_MARKER = /toastify|toast|modal|portal|overlay|backdrop|drawer|dialog|popover|popper|tooltip|snackbar|notification|consent|gdpr|cookie-?(?:banner|consent)|intercom|drift|crisp|tawk|zendesk|onetrust|usercentrics/i;
245
+ function isThirdPartyNoiseElement(el) {
246
+ const identity = `${el.getAttribute("name") ?? ""} ${el.id} ${typeof el.className === "string" ? el.className : ""}`;
247
+ if (TRACKER_MARKER.test(identity)) return true;
248
+ if (el.tagName === "IFRAME") {
249
+ const src = el.getAttribute("src") ?? "";
250
+ const style = (el.getAttribute("style") ?? "").toLowerCase();
251
+ const hidden = /display\s*:\s*none/.test(style) || /visibility\s*:\s*hidden/.test(style) || /(?:left|top)\s*:\s*-\d{3,}px/.test(style) || /(?:width|height)\s*:\s*0(?:px)?\b/.test(style) || el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true";
252
+ if (src === "about:blank" || hidden) return true;
253
+ }
254
+ return false;
255
+ }
256
+ function isInjectedContainer(node) {
257
+ if (node.nodeType !== Node.ELEMENT_NODE) return false;
258
+ const el = node;
259
+ if (isNoiseElement(el)) return true;
260
+ const cls = typeof el.className === "string" ? el.className : "";
261
+ if (INJECTED_MARKER.test(`${el.id} ${cls}`)) return true;
262
+ if (el.hasAttribute("aria-live")) return true;
263
+ const role = el.getAttribute("role");
264
+ if (role && /^(dialog|alertdialog|tooltip|status|alert)$/.test(role.trim())) {
265
+ return true;
266
+ }
267
+ if (el.tagName.includes("-PORTAL") || el.tagName.includes("-OVERLAY")) {
268
+ return true;
269
+ }
270
+ for (const attr of Array.from(el.attributes)) {
271
+ if (/portal|radix|headlessui|floating-ui/i.test(attr.name)) return true;
272
+ }
273
+ return false;
274
+ }
165
275
  function isNoiseElement(node) {
166
276
  if (node.nodeType !== Node.ELEMENT_NODE) return false;
167
277
  const el = node;
168
278
  if (el.hasAttribute("data-why-hydration")) return true;
169
279
  if (NOISE_TAGS.has(el.tagName)) return true;
170
280
  if (el.tagName.includes("-ROUTE-ANNOUNCER")) return true;
281
+ if (isThirdPartyNoiseElement(el)) return true;
282
+ return false;
283
+ }
284
+ function hasPendingSuspense(parent) {
285
+ for (const n of Array.from(parent.childNodes)) {
286
+ if (n.nodeType === Node.COMMENT_NODE && (n.nodeValue ?? "").startsWith("$?")) {
287
+ return true;
288
+ }
289
+ }
171
290
  return false;
172
291
  }
173
292
  function meaningfulChildNodes(parent) {
174
- const children = Array.from(parent.childNodes);
175
- const hasElement = children.some(
293
+ const raw = Array.from(parent.childNodes);
294
+ const hasElement = raw.some(
176
295
  (n) => n.nodeType === Node.ELEMENT_NODE && !isNoiseElement(n)
177
296
  );
178
- return children.filter((node) => {
179
- if (isNoiseElement(node)) return false;
180
- if (node.nodeType === Node.COMMENT_NODE) return false;
181
- if (node.nodeType !== Node.TEXT_NODE) return true;
297
+ const result = [];
298
+ const boundaryStack = [];
299
+ for (const node of raw) {
300
+ if (node.nodeType === Node.COMMENT_NODE) {
301
+ const data = node.nodeValue ?? "";
302
+ if (data === "$?" || data === "$" || data === "$!") boundaryStack.push(data);
303
+ else if (data === "/$") boundaryStack.pop();
304
+ continue;
305
+ }
306
+ if (boundaryStack.includes("$?")) continue;
307
+ if (isNoiseElement(node)) continue;
308
+ if (node.nodeType !== Node.TEXT_NODE) {
309
+ result.push(node);
310
+ continue;
311
+ }
182
312
  const text = node.textContent ?? "";
183
- if (text.trim() !== "") return true;
184
- return !hasElement;
185
- });
313
+ if (text.trim() !== "" || !hasElement) result.push(node);
314
+ }
315
+ return result;
186
316
  }
187
317
  function serialize(node) {
188
318
  if (node.nodeType === Node.ELEMENT_NODE) {
@@ -202,8 +332,10 @@ function tagPath(el) {
202
332
  }
203
333
  function childPath(parentPath, node, index) {
204
334
  if (node.nodeType === Node.ELEMENT_NODE) {
205
- const tag = node.tagName.toLowerCase();
206
- return `${parentPath} > ${tag}:nth-child(${index + 1})`;
335
+ const el = node;
336
+ const tag = el.tagName.toLowerCase();
337
+ const id = el.id ? `#${el.id}` : "";
338
+ return `${parentPath} > ${tag}${id}:nth-child(${index + 1})`;
207
339
  }
208
340
  if (node.nodeType === Node.COMMENT_NODE) {
209
341
  return `${parentPath} > #comment[${index}]`;
@@ -253,13 +385,39 @@ function hasArabicIndicDigits(value) {
253
385
  function hasLatinDigits(value) {
254
386
  return LATIN_DIGITS.test(value);
255
387
  }
388
+ var NUMERIC_LIKE = /^[+-]?[\d.,\s\u00a0\u2009]+$/;
256
389
  function isSameNumberDifferentSeparators(a, b) {
257
- const digitsOnly = (s) => s.replace(/[^\d]/g, "");
258
- const da = digitsOnly(a);
259
- const db = digitsOnly(b);
390
+ const at = a.trim();
391
+ const bt = b.trim();
392
+ if (!NUMERIC_LIKE.test(at) || !NUMERIC_LIKE.test(bt)) return false;
393
+ const digitsOnly = (s) => s.replace(/\D/g, "");
394
+ const da = digitsOnly(at);
395
+ const db = digitsOnly(bt);
260
396
  if (!da || da !== db) return false;
261
- const hasSep = (s) => /[.,\s\u00a0\u2009]/.test(s.trim());
262
- return (hasSep(a) || hasSep(b)) && a.trim() !== b.trim();
397
+ const hasSep = (s) => /[.,\s\u00a0\u2009]/.test(s);
398
+ return (hasSep(at) || hasSep(bt)) && at !== bt;
399
+ }
400
+ var CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
401
+ "value",
402
+ "placeholder",
403
+ "title",
404
+ "alt",
405
+ "label",
406
+ "aria-label",
407
+ "aria-valuetext",
408
+ "content",
409
+ "datetime"
410
+ ]);
411
+ function isContentAttribute(name) {
412
+ return CONTENT_ATTRIBUTES.has(name.toLowerCase());
413
+ }
414
+ 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;
415
+ function looksLikeThirdPartyNode(html, tagName) {
416
+ const tag = (tagName ?? "").toUpperCase();
417
+ if (tag === "IFRAME" || tag === "EMBED" || tag === "OBJECT") return true;
418
+ const h = (html ?? "").toLowerCase();
419
+ if (!h) return false;
420
+ return THIRD_PARTY_MARKERS.test(h) || h.includes("about:blank");
263
421
  }
264
422
  function isSameDateDifferentOrder(a, b) {
265
423
  const partsA = a.trim().match(/^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/);
@@ -337,8 +495,15 @@ function docs(category) {
337
495
  function bothDiffer(d) {
338
496
  return d.server != null && d.client != null && d.server.trim() !== d.client.trim();
339
497
  }
498
+ function isValueDivergence(d) {
499
+ if (d.kind === "text") return true;
500
+ if (d.kind === "attribute" && d.attribute) {
501
+ return isContentAttribute(d.attribute);
502
+ }
503
+ return false;
504
+ }
340
505
  var nonDeterministic = (d) => {
341
- if (d.kind !== "text" && d.kind !== "attribute") return null;
506
+ if (!isValueDivergence(d)) return null;
342
507
  if (!bothDiffer(d)) return null;
343
508
  if (!isRandomLike(d.server) || !isRandomLike(d.client)) return null;
344
509
  return {
@@ -350,7 +515,7 @@ var nonDeterministic = (d) => {
350
515
  };
351
516
  };
352
517
  var dateTime = (d) => {
353
- if (d.kind !== "text" && d.kind !== "attribute") return null;
518
+ if (!isValueDivergence(d)) return null;
354
519
  if (!bothDiffer(d)) return null;
355
520
  const serverTs = toTimestamp(d.server);
356
521
  const clientTs = toTimestamp(d.client);
@@ -369,7 +534,7 @@ var dateTime = (d) => {
369
534
  };
370
535
  };
371
536
  var localeFormat = (d) => {
372
- if (d.kind !== "text" && d.kind !== "attribute") return null;
537
+ if (!isValueDivergence(d)) return null;
373
538
  if (!bothDiffer(d)) return null;
374
539
  const { server, client } = d;
375
540
  const scriptMismatch = hasArabicIndicDigits(server) && hasLatinDigits(client) || hasLatinDigits(server) && hasArabicIndicDigits(client);
@@ -477,17 +642,71 @@ var thirdPartyDomMutation = (d) => {
477
642
  };
478
643
  }
479
644
  }
645
+ if ((d.kind === "node-added" || d.kind === "node-removed") && looksLikeThirdPartyNode(d.client ?? d.server, d.tagName)) {
646
+ const tag = (d.tagName ?? "element").toLowerCase();
647
+ return {
648
+ category: "third-party-dom-mutation",
649
+ confidence: 0.7,
650
+ 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.`,
651
+ 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">`).',
652
+ docsUrl: docs("third-party-dom-mutation")
653
+ };
654
+ }
480
655
  return null;
481
656
  };
657
+ var attributeMismatch = (d) => {
658
+ if (d.kind !== "attribute" || !d.attribute) return null;
659
+ const attr = d.attribute.toLowerCase();
660
+ const server = d.server ?? "";
661
+ const client = d.client ?? "";
662
+ if (attr === "class" || attr === "classname") {
663
+ const serverSet = new Set(server.split(/\s+/).filter(Boolean));
664
+ const clientSet = new Set(client.split(/\s+/).filter(Boolean));
665
+ const added = [...clientSet].filter((c) => !serverSet.has(c));
666
+ const removed = [...serverSet].filter((c) => !clientSet.has(c));
667
+ const parts = [];
668
+ if (added.length) parts.push(`added on client: ${added.join(", ")}`);
669
+ if (removed.length) parts.push(`removed on client: ${removed.join(", ")}`);
670
+ const detail = parts.length ? ` (${parts.join("; ")})` : "";
671
+ return {
672
+ category: "attribute-mismatch",
673
+ confidence: 0.8,
674
+ 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.`,
675
+ 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.",
676
+ docsUrl: docs("attribute-mismatch")
677
+ };
678
+ }
679
+ if (attr === "style") {
680
+ return {
681
+ category: "attribute-mismatch",
682
+ confidence: 0.75,
683
+ 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.",
684
+ suggestion: "Compute the style after mount (`useEffect`) so the first client render matches the server, or move it to a CSS class / media query.",
685
+ docsUrl: docs("attribute-mismatch")
686
+ };
687
+ }
688
+ return {
689
+ category: "attribute-mismatch",
690
+ confidence: 0.6,
691
+ 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.`,
692
+ suggestion: "Make the attribute deterministic across server and client, or set it after mount so the first client render matches the server HTML.",
693
+ docsUrl: docs("attribute-mismatch")
694
+ };
695
+ };
482
696
  var BUILT_IN_RULES = [
483
697
  nonDeterministic,
484
698
  dateTime,
485
699
  localeFormat,
700
+ // Third-party runs before browser-only/viewport so an injected iframe is
701
+ // labelled correctly instead of "browser-only API" or "viewport branching".
702
+ thirdPartyDomMutation,
486
703
  browserOnlyApi,
487
704
  viewportBranching,
488
705
  invalidNesting,
489
706
  whitespaceMinification,
490
- thirdPartyDomMutation
707
+ // Catch-all for class/style/generic attribute diffs — after the specific
708
+ // rules so extension attributes and content values are handled first.
709
+ attributeMismatch
491
710
  ];
492
711
  var UNKNOWN_CAUSE = {
493
712
  category: "unknown",
@@ -551,7 +770,6 @@ function buildReport(divergence, cause, context = {}) {
551
770
  function signatureOf(report) {
552
771
  return [
553
772
  report.node.kind,
554
- report.node.path,
555
773
  report.node.attribute ?? "",
556
774
  report.cause.category,
557
775
  report.server ?? "",
@@ -604,19 +822,107 @@ var ReportCollector = class {
604
822
 
605
823
  // src/core/react-message.ts
606
824
  function isHydrationMessage(message) {
607
- return /hydrat/i.test(message) || /did not match/i.test(message) || /server (?:HTML|rendered)/i.test(message) || /validateDOMNesting/i.test(message) || /Text content does not match/i.test(message);
825
+ return /hydrat/i.test(message) || /did(?:n't| not) match/i.test(message) || /won't be patched up/i.test(message) || /hydration-mismatch/i.test(message) || /server (?:HTML|rendered)/i.test(message) || /validateDOMNesting/i.test(message) || /Text content does not match/i.test(message);
826
+ }
827
+ function stringify(v) {
828
+ return typeof v === "string" ? v : String(v);
608
829
  }
609
830
  function formatConsoleArgs(args) {
610
831
  if (args.length === 0) return "";
611
832
  const [first, ...rest] = args;
612
833
  if (typeof first === "string" && /%[sco]/.test(first)) {
613
834
  let i = 0;
614
- return first.replace(/%[sco]/g, () => String(rest[i++] ?? "")).trim();
835
+ const expanded = first.replace(/%[sco]/g, () => stringify(rest[i++] ?? ""));
836
+ const leftover = rest.slice(i).map(stringify).filter(Boolean);
837
+ return [expanded, ...leftover].join("\n").trim();
615
838
  }
616
- return args.map((a) => typeof a === "string" ? a : String(a)).join(" ");
839
+ return args.map(stringify).join(" ");
840
+ }
841
+ function isInternalComponent(name) {
842
+ return /^(Inner|Outer|Segment|Client|Server|Redirect|Error|Loading|HTTPAccess|RenderFrom|ScrollAndFocus|Metadata|Outlet|ViewTransition|NotFound|Hot|DevRoot|App)$/.test(
843
+ name
844
+ ) || /^(Inner|Outer|Segment|Client|Server|Redirect|Error|Loading|HTTPAccess|RenderFrom|ScrollAndFocus|Metadata|Outlet|ViewTransition|NotFound|Hot|DevRoot)[A-Z]/.test(
845
+ name
846
+ ) || /(Boundary|Router|Handler|Provider|Context|Root|Node)$/.test(name) || name === "Fragment" || name === "Suspense";
847
+ }
848
+ function extractComponentFromMessage(message) {
849
+ const names = [];
850
+ const re = /<([A-Z][A-Za-z0-9_]*)\b/g;
851
+ let m;
852
+ while ((m = re.exec(message)) !== null) {
853
+ const name = m[1];
854
+ if (name && !isInternalComponent(name)) names.push(name);
855
+ }
856
+ return names.length ? names[names.length - 1] : void 0;
857
+ }
858
+ var ATTR_RE = /^([\w:-]+)=(?:"([\s\S]*)"|\{([\s\S]*)\})$/;
859
+ function parseModernDiff(message) {
860
+ const plus = [];
861
+ const minus = [];
862
+ for (const raw of message.split("\n")) {
863
+ const line = raw.trim();
864
+ const p = /^\+\s+(.+)$/.exec(line);
865
+ const mn = /^-\s+(.+)$/.exec(line);
866
+ if (p && p[1]) plus.push(p[1].trim());
867
+ else if (mn && mn[1]) minus.push(mn[1].trim());
868
+ }
869
+ if (plus.length === 0 && minus.length === 0) return null;
870
+ for (const p of plus) {
871
+ const pm = ATTR_RE.exec(p);
872
+ if (!pm) continue;
873
+ const name = pm[1];
874
+ const clientValue = pm[2] ?? pm[3] ?? "";
875
+ const matched = minus.find((mm) => {
876
+ const parsed = ATTR_RE.exec(mm);
877
+ return parsed && parsed[1] === name;
878
+ });
879
+ if (matched) {
880
+ const parsed = ATTR_RE.exec(matched);
881
+ const serverValue = parsed ? parsed[2] ?? parsed[3] ?? "" : "";
882
+ return {
883
+ kind: "attribute",
884
+ path: "body",
885
+ attribute: name,
886
+ server: serverValue,
887
+ client: clientValue,
888
+ reactMessage: message
889
+ };
890
+ }
891
+ }
892
+ const client = plus.find((p) => !ATTR_RE.test(p)) ?? null;
893
+ const server = minus.find((m) => !ATTR_RE.test(m)) ?? null;
894
+ if (client !== null || server !== null) {
895
+ return {
896
+ kind: "text",
897
+ path: "body",
898
+ server,
899
+ client,
900
+ reactMessage: message
901
+ };
902
+ }
903
+ return null;
617
904
  }
618
905
  function parseHydrationMessage(message) {
619
906
  if (!isHydrationMessage(message)) return null;
907
+ const nesting = /<(\w+)>\s*cannot (?:appear as a|be a|contain a) (?:child|descendant)/i.exec(
908
+ message
909
+ );
910
+ if (nesting || /validateDOMNesting/i.test(message)) {
911
+ const pair = /<(\w+)>\s*cannot (?:appear as a|be a) (?:child|descendant) of <?(\w+)>?/i.exec(
912
+ message
913
+ );
914
+ return {
915
+ kind: "structure",
916
+ path: "body",
917
+ tagName: pair ? pair[1]?.toUpperCase() : void 0,
918
+ parentTagName: pair ? pair[2]?.toUpperCase() : void 0,
919
+ server: null,
920
+ client: null,
921
+ reactMessage: message
922
+ };
923
+ }
924
+ const modern = parseModernDiff(message);
925
+ if (modern) return modern;
620
926
  const text = /Text content (?:did not match|does not match)[^:]*Server:\s*"?(.*?)"?\s+Client:\s*"?(.*?)"?\s*$/i.exec(
621
927
  message
622
928
  );
@@ -642,20 +948,6 @@ function parseHydrationMessage(message) {
642
948
  reactMessage: message
643
949
  };
644
950
  }
645
- const nesting = /<(\w+)>\s*cannot (?:appear as a|be a) (?:child|descendant) of <?(\w+)>?/i.exec(
646
- message
647
- );
648
- if (nesting || /validateDOMNesting/i.test(message)) {
649
- return {
650
- kind: "structure",
651
- path: "body",
652
- tagName: nesting ? nesting[1]?.toUpperCase() : void 0,
653
- parentTagName: nesting ? nesting[2]?.toUpperCase() : void 0,
654
- server: null,
655
- client: null,
656
- reactMessage: message
657
- };
658
- }
659
951
  const expected = /Expected server HTML to contain a matching <(\w+)>(?: in <(\w+)>)?/i.exec(
660
952
  message
661
953
  );
@@ -691,27 +983,89 @@ function parseHydrationMessage(message) {
691
983
  reactMessage: message
692
984
  };
693
985
  }
986
+ function parseAllHydrationDivergences(message) {
987
+ if (!isHydrationMessage(message)) return [];
988
+ const plus = [];
989
+ const minus = [];
990
+ for (const raw of message.split("\n")) {
991
+ const line = raw.trim();
992
+ const p = /^\+\s+(.+)$/.exec(line);
993
+ const mn = /^-\s+(.+)$/.exec(line);
994
+ if (p && p[1]) plus.push(p[1].trim());
995
+ else if (mn && mn[1]) minus.push(mn[1].trim());
996
+ }
997
+ if (plus.length === 0 && minus.length === 0) {
998
+ const single = parseHydrationMessage(message);
999
+ return single ? [single] : [];
1000
+ }
1001
+ const out = [];
1002
+ const attrMinus = minus.filter((m) => ATTR_RE.test(m));
1003
+ const usedMinus = /* @__PURE__ */ new Set();
1004
+ for (const p of plus) {
1005
+ const pm = ATTR_RE.exec(p);
1006
+ if (!pm) continue;
1007
+ const name = pm[1];
1008
+ const clientValue = pm[2] ?? pm[3] ?? "";
1009
+ const idx = attrMinus.findIndex((m, k) => {
1010
+ if (usedMinus.has(k)) return false;
1011
+ const parsed = ATTR_RE.exec(m);
1012
+ return parsed?.[1] === name;
1013
+ });
1014
+ if (idx >= 0) {
1015
+ usedMinus.add(idx);
1016
+ const parsed = ATTR_RE.exec(attrMinus[idx]);
1017
+ out.push({
1018
+ kind: "attribute",
1019
+ path: "body",
1020
+ attribute: name,
1021
+ server: parsed ? parsed[2] ?? parsed[3] ?? "" : "",
1022
+ client: clientValue,
1023
+ reactMessage: message
1024
+ });
1025
+ }
1026
+ }
1027
+ const textPlus = plus.filter((p) => !ATTR_RE.test(p));
1028
+ const textMinus = minus.filter((m) => !ATTR_RE.test(m));
1029
+ const len = Math.max(textPlus.length, textMinus.length);
1030
+ for (let i = 0; i < len; i++) {
1031
+ const client = textPlus[i] ?? null;
1032
+ const server = textMinus[i] ?? null;
1033
+ if (client !== null || server !== null) {
1034
+ out.push({ kind: "text", path: "body", server, client, reactMessage: message });
1035
+ }
1036
+ }
1037
+ return out.length ? out : parseHydrationMessage(message) ? [parseHydrationMessage(message)] : [];
1038
+ }
694
1039
 
695
1040
  // src/core/inspect.ts
696
- function inspectRoot(root, collector, context = {}) {
1041
+ function inspectRoot(root, collector, context = {}, enrich) {
697
1042
  const serverHtml = getServerHtmlForRoot(root);
698
- if (serverHtml == null) return false;
699
- const divergence = diffSnapshotAgainstDom(serverHtml, root);
700
- if (!divergence) return false;
701
- if (context.reactMessage && !divergence.reactMessage) {
702
- divergence.reactMessage = context.reactMessage;
1043
+ if (serverHtml == null) return 0;
1044
+ const divergences = collectSnapshotAgainstDom(serverHtml, root);
1045
+ let reported = 0;
1046
+ for (const divergence of divergences) {
1047
+ if (context.reactMessage && !divergence.reactMessage) {
1048
+ divergence.reactMessage = context.reactMessage;
1049
+ }
1050
+ const enriched = enrich ? enrich(divergence) : {};
1051
+ if (collector.report(divergence, { ...context, ...enriched }) != null) {
1052
+ reported += 1;
1053
+ }
703
1054
  }
704
- return collector.report(divergence, context) != null;
1055
+ return reported;
705
1056
  }
706
1057
  function reportFromMessage(message, collector, context = {}) {
707
- const divergence = parseHydrationMessage(message);
708
- if (!divergence) return false;
709
- return collector.report(divergence, {
710
- ...context,
711
- reactMessage: message
712
- }) != null;
1058
+ const divergences = parseAllHydrationDivergences(message);
1059
+ const component = extractComponentFromMessage(message) ?? context.component;
1060
+ let reported = 0;
1061
+ for (const divergence of divergences) {
1062
+ if (collector.report(divergence, { ...context, component, reactMessage: message }) != null) {
1063
+ reported += 1;
1064
+ }
1065
+ }
1066
+ return reported;
713
1067
  }
714
1068
 
715
1069
  export { BUILT_IN_RULES, CONFIDENCE_THRESHOLD, ReportCollector, UNKNOWN_CAUSE, buildReport, classify, diffSnapshotAgainstDom, diffTrees, formatConsoleArgs, inspectRoot, isDev, isHydrationMessage, parseHydrationMessage, parseServerHtml, reportFromMessage, signatureOf };
716
- //# sourceMappingURL=chunk-WJ3ZOW4D.js.map
717
- //# sourceMappingURL=chunk-WJ3ZOW4D.js.map
1070
+ //# sourceMappingURL=chunk-WQLUD25W.js.map
1071
+ //# sourceMappingURL=chunk-WQLUD25W.js.map