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.
- package/CHANGELOG.md +81 -0
- package/README.md +56 -7
- package/dist/{chunk-CDNKC2LD.cjs → chunk-DSO337ME.cjs} +445 -91
- package/dist/chunk-DSO337ME.cjs.map +1 -0
- package/dist/{chunk-VUGHOMMU.cjs → chunk-MTAU6G26.cjs} +164 -24
- package/dist/chunk-MTAU6G26.cjs.map +1 -0
- package/dist/{chunk-ZMAUNJJR.js → chunk-NQX4TBV5.js} +159 -19
- package/dist/chunk-NQX4TBV5.js.map +1 -0
- package/dist/{chunk-WJ3ZOW4D.js → chunk-WQLUD25W.js} +445 -91
- package/dist/chunk-WQLUD25W.js.map +1 -0
- package/dist/index.cjs +17 -17
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1 -1
- package/dist/next/index.cjs +4 -4
- package/dist/next/index.d.cts +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +2 -2
- package/dist/react.cjs +4 -4
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{types-CDtDTiIa.d.cts → types-6wTih14e.d.cts} +1 -1
- package/dist/{types-CDtDTiIa.d.ts → types-6wTih14e.d.ts} +1 -1
- package/package.json +5 -4
- package/LICENSE +0 -21
- package/dist/chunk-CDNKC2LD.cjs.map +0 -1
- package/dist/chunk-VUGHOMMU.cjs.map +0 -1
- package/dist/chunk-WJ3ZOW4D.js.map +0 -1
- package/dist/chunk-ZMAUNJJR.js.map +0 -1
|
@@ -10,57 +10,84 @@ function parseServerHtml(html, rootTagName) {
|
|
|
10
10
|
container.innerHTML = html;
|
|
11
11
|
return container;
|
|
12
12
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
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
|
|
18
|
+
function collectSnapshotAgainstDom(serverHtml, clientRoot, limit = 60) {
|
|
17
19
|
const serverRoot = parseServerHtml(serverHtml, clientRoot.tagName);
|
|
18
|
-
return
|
|
20
|
+
return collectDivergences(serverRoot, clientRoot, limit);
|
|
21
|
+
}
|
|
22
|
+
function diffTrees(serverRoot, clientRoot) {
|
|
23
|
+
return collectDivergences(serverRoot, clientRoot, 1)[0] ?? null;
|
|
19
24
|
}
|
|
20
|
-
function
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
62
|
+
out.push({
|
|
30
63
|
kind: "node-removed",
|
|
31
64
|
path,
|
|
32
65
|
tagName: elementTag(serverNode),
|
|
33
|
-
parentTagName:
|
|
66
|
+
parentTagName: parentTag,
|
|
34
67
|
server: serialize(serverNode),
|
|
35
68
|
client: null,
|
|
36
69
|
element: asElement(clientParent)
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
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:
|
|
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
|
|
88
|
+
function collectNode(serverNode, clientNode, path, parentTag, out, limit) {
|
|
62
89
|
if (serverNode.nodeType !== clientNode.nodeType) {
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
104
|
-
|
|
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
|
|
175
|
-
const hasElement =
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (node.nodeType
|
|
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() !== "")
|
|
184
|
-
|
|
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
|
|
206
|
-
|
|
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
|
|
258
|
-
const
|
|
259
|
-
|
|
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
|
|
262
|
-
return (hasSep(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
|
1055
|
+
return reported;
|
|
705
1056
|
}
|
|
706
1057
|
function reportFromMessage(message, collector, context = {}) {
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
reactMessage: message
|
|
712
|
-
|
|
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-
|
|
717
|
-
//# sourceMappingURL=chunk-
|
|
1070
|
+
//# sourceMappingURL=chunk-WQLUD25W.js.map
|
|
1071
|
+
//# sourceMappingURL=chunk-WQLUD25W.js.map
|