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
|
@@ -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;
|
|
21
26
|
}
|
|
22
|
-
function
|
|
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
|
|
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",
|
|
@@ -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
|
|
177
|
-
const hasElement =
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
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
|
+
}
|
|
184
314
|
const text = node.textContent ?? "";
|
|
185
|
-
if (text.trim() !== "")
|
|
186
|
-
|
|
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
|
|
208
|
-
|
|
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
|
|
260
|
-
const
|
|
261
|
-
|
|
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
|
|
264
|
-
return (hasSep(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
1057
|
+
return reported;
|
|
707
1058
|
}
|
|
708
1059
|
function reportFromMessage(message, collector, context = {}) {
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
reactMessage: message
|
|
714
|
-
|
|
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-
|
|
734
|
-
//# sourceMappingURL=chunk-
|
|
1087
|
+
//# sourceMappingURL=chunk-DSO337ME.cjs.map
|
|
1088
|
+
//# sourceMappingURL=chunk-DSO337ME.cjs.map
|