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 +81 -0
- package/README.md +14 -5
- package/dist/{chunk-AN3N7274.cjs → chunk-DSO337ME.cjs} +244 -65
- package/dist/chunk-DSO337ME.cjs.map +1 -0
- package/dist/{chunk-I45YRDCA.cjs → chunk-MTAU6G26.cjs} +61 -21
- package/dist/chunk-MTAU6G26.cjs.map +1 -0
- package/dist/{chunk-QYZQHTZS.js → chunk-NQX4TBV5.js} +55 -15
- package/dist/chunk-NQX4TBV5.js.map +1 -0
- package/dist/{chunk-HRNL75RP.js → chunk-WQLUD25W.js} +244 -65
- package/dist/chunk-WQLUD25W.js.map +1 -0
- package/dist/index.cjs +17 -17
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/next/index.cjs +4 -4
- package/dist/next/index.js +2 -2
- package/dist/react.cjs +4 -4
- package/dist/react.js +2 -2
- package/package.json +5 -4
- package/LICENSE +0 -21
- package/dist/chunk-AN3N7274.cjs.map +0 -1
- package/dist/chunk-HRNL75RP.js.map +0 -1
- package/dist/chunk-I45YRDCA.cjs.map +0 -1
- package/dist/chunk-QYZQHTZS.js.map +0 -1
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
|
[](https://www.npmjs.com/package/why-hydration)
|
|
4
4
|
[](https://www.npmjs.com/package/why-hydration)
|
|
5
5
|
[](https://bundlephobia.com/package/why-hydration)
|
|
6
|
-
[](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
|
-
**
|
|
432
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
16
|
-
|
|
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
|
|
20
|
+
function collectSnapshotAgainstDom(serverHtml, clientRoot, limit = 60) {
|
|
19
21
|
const serverRoot = parseServerHtml(serverHtml, clientRoot.tagName);
|
|
20
|
-
return
|
|
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
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
64
|
+
out.push({
|
|
32
65
|
kind: "node-removed",
|
|
33
66
|
path,
|
|
34
67
|
tagName: elementTag(serverNode),
|
|
35
|
-
parentTagName:
|
|
68
|
+
parentTagName: parentTag,
|
|
36
69
|
server: serialize(serverNode),
|
|
37
70
|
client: null,
|
|
38
71
|
element: asElement(clientParent)
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
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:
|
|
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
|
|
90
|
+
function collectNode(serverNode, clientNode, path, parentTag, out, limit) {
|
|
64
91
|
if (serverNode.nodeType !== clientNode.nodeType) {
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
106
|
-
|
|
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
|
|
190
|
-
const hasElement =
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (node.nodeType
|
|
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() !== "")
|
|
199
|
-
|
|
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
|
|
221
|
-
|
|
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
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
880
|
-
return collector.report(divergence, { ...context, ...enriched }) != null;
|
|
1057
|
+
return reported;
|
|
881
1058
|
}
|
|
882
1059
|
function reportFromMessage(message, collector, context = {}) {
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
component:
|
|
888
|
-
|
|
889
|
-
|
|
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-
|
|
909
|
-
//# sourceMappingURL=chunk-
|
|
1087
|
+
//# sourceMappingURL=chunk-DSO337ME.cjs.map
|
|
1088
|
+
//# sourceMappingURL=chunk-DSO337ME.cjs.map
|