why-hydration 0.1.2 → 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,81 @@
1
+ # Changelog
2
+
3
+ ## 0.1.3
4
+
5
+ Stability + accuracy overhaul from running on a large production Next.js app.
6
+ Detection is now **deterministic** and reports **every** mismatch at once.
7
+
8
+ - **LCS child alignment.** Children are matched by an LCS (tag+id) instead of by
9
+ index, so a node the client injects mid-tree (react-toastify's
10
+ `<section class="Toastify">`, a portal, a modal, an ad) is treated as an
11
+ insertion — it no longer shifts every sibling and cascade into a different set
12
+ of false positives on each refresh. This makes results **consistent across
13
+ refreshes**.
14
+ - **Collect all mismatches.** The diff now returns every divergence on the page
15
+ (deduped by value), so the overlay shows them together instead of one-per-
16
+ refresh. The panel is scrollable and shows a "↓ N issues — scroll to see all"
17
+ hint (with a ✕ close button) when there are more than fit.
18
+ - **Skip client-injected containers** — toasts, modals, portals, overlays,
19
+ tooltips, consent banners, chat/analytics widgets (by class/role/`aria-live`)
20
+ are never reported and never mask a real mismatch.
21
+ - **Skip pending Suspense fallbacks** — content inside a streaming `<!--$?-->`
22
+ boundary (server `Loading…` vs client content) is expected, not a mismatch.
23
+ - **No duplicates** — value-based dedup means the same mismatch (e.g. two links
24
+ with the same conditional class) appears once.
25
+ - **All class/style mismatches captured.** React can emit several changes in one
26
+ hydration message; the parser now extracts all of them (not just the first).
27
+ - Adjacent delete+insert is coalesced into a single `structure` report.
28
+ - License: removed MIT (now `UNLICENSED`); added the author's LinkedIn.
29
+
30
+ ## 0.1.2
31
+
32
+ Accuracy overhaul from running on a large production Next.js app — fixes real
33
+ false positives and misclassifications.
34
+
35
+ - **New `attribute-mismatch` category.** A `class`/`style` difference (e.g. a
36
+ conditional `forceHide` class) is now reported correctly with the exact
37
+ changed tokens — previously misclassified as "locale-format" because the class
38
+ list contained digits.
39
+ - **Component name + source file:line.** Reports now name the offending
40
+ component (e.g. `<PriceTag>`) and, where React exposes it, the source
41
+ `file:line`, read from React's fiber and its hydration diff.
42
+ - **Modern React message parsing.** React 18.3+/19 print a JSX diff tree
43
+ (`+`/`-` lines) instead of "Prop X did not match"; the parser now understands
44
+ it and extracts the attribute/value and component. This is required because
45
+ React does **not** patch mismatched attributes, so the DOM diff alone can't
46
+ see them.
47
+ - **Third-party noise is skipped.** Hidden ads/consent/analytics iframes (e.g.
48
+ Google Funding Choices `googlefcInactive`, `about:blank`) no longer produce
49
+ false "browser-only API" reports and no longer mask your real mismatch.
50
+ - **Stricter locale detection** — number-format matching now requires actual
51
+ numeric values, so class lists / ids with digits no longer false-match.
52
+ - **Reliable timing.** Detection re-checks across a short settling window
53
+ because React applies client values to mismatched subtrees a few hundred ms
54
+ after hydration; value-based dedup prevents double reports.
55
+ - Docs: detection scope (full load vs client navigation), `attribute-mismatch`
56
+ category, refreshed screenshots showing component names.
57
+
58
+ ## 0.1.1
59
+
60
+ Fixes found by end-to-end testing in a real Next.js app (App Router + Pages
61
+ Router, React 18 & 19). **0.1.0 is broken in Next.js — use 0.1.1 or later.**
62
+
63
+ - **`'use client'` directive** is now injected into the `react` and `next`
64
+ bundles. Without it, `<HydrationInspector>` crashed in a Server Component
65
+ layout (`useRef only works in Client Components`). `why-hydration/next` is now
66
+ client-only; import `HydrationSnapshotScript` from `why-hydration/next/script`.
67
+ - **Diff skips framework comment markers** (React `<!--$-->`, RSC payload) that
68
+ were reported as false `viewport-branching` mismatches.
69
+ - **Inline `style` is normalized through the CSSOM**, so the server snapshot and
70
+ the browser-normalized live DOM (`#hex` → `rgb()`, spacing) no longer produce
71
+ a false `unknown` mismatch.
72
+ - **`typesVersions`** added so subpath types resolve under `moduleResolution:
73
+ "node"` (fixes `next build` type errors in apps using classic resolution).
74
+
75
+ Verified live: locale-format, non-deterministic-value, date-time, and
76
+ browser-only-api classify correctly in App Router and Pages Router; the tool is
77
+ a zero-code no-op in a production `next build`.
78
+
79
+ ## 0.1.0
80
+
81
+ Initial release.
package/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  [![npm version](https://img.shields.io/npm/v/why-hydration.svg)](https://www.npmjs.com/package/why-hydration)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/why-hydration.svg)](https://www.npmjs.com/package/why-hydration)
5
5
  [![minzipped size](https://img.shields.io/bundlephobia/minzip/why-hydration.svg)](https://bundlephobia.com/package/why-hydration)
6
- [![license: MIT](https://img.shields.io/npm/l/why-hydration.svg)](LICENSE)
7
6
 
8
7
  📦 **npm:** https://www.npmjs.com/package/why-hydration
9
8
 
@@ -428,14 +427,24 @@ field in `package.json` to match your URL.
428
427
  **Can I send reports to my logging?** Yes — pass `onReport`; you receive the full
429
428
  `HydrationReport`.
430
429
 
431
- **Does it slow my app down?** No. It's dev-only, runs the diff once after
432
- hydration (plus once per real React signal), and holds no standing observers.
430
+ **Why do results now show all at once and stay the same on refresh?** The diff
431
+ aligns children with an LCS and collects every mismatch deterministically, so
432
+ injected nodes (toasts, portals, ads) can't shift the comparison and change the
433
+ results between refreshes.
434
+
435
+ **Does it slow my app down?** No. It's dev-only, diffs across a short settling
436
+ window right after hydration (React applies client values a few hundred ms
437
+ later), then stops — no standing observers.
433
438
 
434
439
  ## Contributing
435
440
 
436
441
  Adding a cause category is a self-contained change — see
437
442
  [CONTRIBUTING.md](CONTRIBUTING.md).
438
443
 
439
- ## License
444
+ ## Author
445
+
446
+ Built by **[Razan Aboushi](https://www.linkedin.com/in/razan-aboushi/)** ·
447
+ [GitHub](https://github.com/razan-aboushi) ·
448
+ [LinkedIn](https://www.linkedin.com/in/razan-aboushi/)
440
449
 
441
- [MIT](LICENSE) © [Razan Aboushi](https://github.com/razan-aboushi)
450
+ © 2026 Razan Aboushi. All rights reserved.
@@ -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;
26
+ }
27
+ function diffSnapshotAgainstDom(serverHtml, clientRoot) {
28
+ return collectSnapshotAgainstDom(serverHtml, clientRoot, 1)[0] ?? null;
21
29
  }
22
- function diffChildren(serverParent, clientParent, parentPath) {
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",
@@ -165,6 +243,7 @@ var NOISE_TAGS = /* @__PURE__ */ new Set([
165
243
  "NOSCRIPT"
166
244
  ]);
167
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;
168
247
  function isThirdPartyNoiseElement(el) {
169
248
  const identity = `${el.getAttribute("name") ?? ""} ${el.id} ${typeof el.className === "string" ? el.className : ""}`;
170
249
  if (TRACKER_MARKER.test(identity)) return true;
@@ -176,6 +255,25 @@ function isThirdPartyNoiseElement(el) {
176
255
  }
177
256
  return false;
178
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
+ }
179
277
  function isNoiseElement(node) {
180
278
  if (node.nodeType !== Node.ELEMENT_NODE) return false;
181
279
  const el = node;
@@ -185,19 +283,38 @@ function isNoiseElement(node) {
185
283
  if (isThirdPartyNoiseElement(el)) return true;
186
284
  return false;
187
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
+ }
292
+ return false;
293
+ }
188
294
  function meaningfulChildNodes(parent) {
189
- const children = Array.from(parent.childNodes);
190
- const hasElement = children.some(
295
+ const raw = Array.from(parent.childNodes);
296
+ const hasElement = raw.some(
191
297
  (n) => n.nodeType === Node.ELEMENT_NODE && !isNoiseElement(n)
192
298
  );
193
- return children.filter((node) => {
194
- if (isNoiseElement(node)) return false;
195
- if (node.nodeType === Node.COMMENT_NODE) return false;
196
- 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
+ }
197
314
  const text = node.textContent ?? "";
198
- if (text.trim() !== "") return true;
199
- return !hasElement;
200
- });
315
+ if (text.trim() !== "" || !hasElement) result.push(node);
316
+ }
317
+ return result;
201
318
  }
202
319
  function serialize(node) {
203
320
  if (node.nodeType === Node.ELEMENT_NODE) {
@@ -217,8 +334,10 @@ function tagPath(el) {
217
334
  }
218
335
  function childPath(parentPath, node, index) {
219
336
  if (node.nodeType === Node.ELEMENT_NODE) {
220
- const tag = node.tagName.toLowerCase();
221
- 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})`;
222
341
  }
223
342
  if (node.nodeType === Node.COMMENT_NODE) {
224
343
  return `${parentPath} > #comment[${index}]`;
@@ -866,27 +985,87 @@ function parseHydrationMessage(message) {
866
985
  reactMessage: message
867
986
  };
868
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
+ }
869
1041
 
870
1042
  // src/core/inspect.ts
871
1043
  function inspectRoot(root, collector, context = {}, enrich) {
872
1044
  const serverHtml = chunkOGQEPU7G_cjs.getServerHtmlForRoot(root);
873
- if (serverHtml == null) return false;
874
- const divergence = diffSnapshotAgainstDom(serverHtml, root);
875
- if (!divergence) return false;
876
- if (context.reactMessage && !divergence.reactMessage) {
877
- 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
+ }
878
1056
  }
879
- const enriched = enrich ? enrich(divergence) : {};
880
- return collector.report(divergence, { ...context, ...enriched }) != null;
1057
+ return reported;
881
1058
  }
882
1059
  function reportFromMessage(message, collector, context = {}) {
883
- const divergence = parseHydrationMessage(message);
884
- if (!divergence) return false;
885
- return collector.report(divergence, {
886
- ...context,
887
- component: extractComponentFromMessage(message) ?? context.component,
888
- reactMessage: message
889
- }) != 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;
890
1069
  }
891
1070
 
892
1071
  exports.BUILT_IN_RULES = BUILT_IN_RULES;
@@ -905,5 +1084,5 @@ exports.parseHydrationMessage = parseHydrationMessage;
905
1084
  exports.parseServerHtml = parseServerHtml;
906
1085
  exports.reportFromMessage = reportFromMessage;
907
1086
  exports.signatureOf = signatureOf;
908
- //# sourceMappingURL=chunk-AN3N7274.cjs.map
909
- //# sourceMappingURL=chunk-AN3N7274.cjs.map
1087
+ //# sourceMappingURL=chunk-DSO337ME.cjs.map
1088
+ //# sourceMappingURL=chunk-DSO337ME.cjs.map