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