why-hydration 0.1.5 → 0.3.0
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 +116 -0
- package/README.md +155 -35
- package/dist/chunk-CER5NQRR.js +1333 -0
- package/dist/chunk-CER5NQRR.js.map +1 -0
- package/dist/{chunk-AS5DZZHI.js → chunk-KP4S6CRL.js} +560 -230
- package/dist/chunk-KP4S6CRL.js.map +1 -0
- package/dist/{chunk-AT6A77X3.cjs → chunk-QFHUKJ5K.cjs} +568 -229
- package/dist/chunk-QFHUKJ5K.cjs.map +1 -0
- package/dist/chunk-RWMRFTQ4.cjs +1356 -0
- package/dist/chunk-RWMRFTQ4.cjs.map +1 -0
- package/dist/index.cjs +25 -17
- package/dist/index.d.cts +37 -6
- package/dist/index.d.ts +37 -6
- 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 +24 -3
- package/dist/react.d.ts +24 -3
- package/dist/react.js +2 -2
- package/dist/{types-6wTih14e.d.cts → types-C14vcpBO.d.cts} +9 -0
- package/dist/{types-6wTih14e.d.ts → types-C14vcpBO.d.ts} +9 -0
- package/package.json +22 -4
- package/dist/chunk-3I6F4K4L.js +0 -642
- package/dist/chunk-3I6F4K4L.js.map +0 -1
- package/dist/chunk-AS5DZZHI.js.map +0 -1
- package/dist/chunk-AT6A77X3.cjs.map +0 -1
- package/dist/chunk-OFVFNPE6.cjs +0 -665
- package/dist/chunk-OFVFNPE6.cjs.map +0 -1
|
@@ -10,6 +10,230 @@ function readNodeEnv() {
|
|
|
10
10
|
}
|
|
11
11
|
var isDev = readNodeEnv() !== "production";
|
|
12
12
|
|
|
13
|
+
// src/core/classify/detectors.ts
|
|
14
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15
|
+
var REACT_ID_RE = /^:[rR][0-9a-z]*:$/;
|
|
16
|
+
var HEX_TOKEN_RE = /^[0-9a-f]{16,}$/i;
|
|
17
|
+
var RANDOM_DECIMAL_RE = /^0?\.\d{6,}$/;
|
|
18
|
+
var NANOID_RE = /^[A-Za-z0-9_-]{10,}$/;
|
|
19
|
+
var ARABIC_INDIC_DIGITS = /[٠-٩۰-۹]/;
|
|
20
|
+
var LATIN_DIGITS = /[0-9]/;
|
|
21
|
+
var BIDI_CONTROLS = /[\u200e\u200f\u061c\u2066-\u2069]/g;
|
|
22
|
+
function stripBidiControls(value2) {
|
|
23
|
+
return value2.replace(BIDI_CONTROLS, "");
|
|
24
|
+
}
|
|
25
|
+
function differsOnlyByBidiControls(a, b) {
|
|
26
|
+
const sa = stripBidiControls(a);
|
|
27
|
+
const sb = stripBidiControls(b);
|
|
28
|
+
return a !== b && sa === sb && sa.trim() !== "";
|
|
29
|
+
}
|
|
30
|
+
function normalizeNumerals(value2) {
|
|
31
|
+
return stripBidiControls(value2).replace(/[٠-٩۰-۹٫٬]/g, (ch) => {
|
|
32
|
+
if (ch === "\u066B") return ".";
|
|
33
|
+
if (ch === "\u066C") return ",";
|
|
34
|
+
const code2 = ch.charCodeAt(0);
|
|
35
|
+
const zero = code2 >= 1776 ? 1776 : 1632;
|
|
36
|
+
return String(code2 - zero);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function isRandomLike(value2) {
|
|
40
|
+
const v = value2.trim();
|
|
41
|
+
if (!v) return false;
|
|
42
|
+
if (UUID_RE.test(v)) return true;
|
|
43
|
+
if (REACT_ID_RE.test(v)) return true;
|
|
44
|
+
if (HEX_TOKEN_RE.test(v)) return true;
|
|
45
|
+
if (RANDOM_DECIMAL_RE.test(v)) return true;
|
|
46
|
+
if (NANOID_RE.test(v) && /[A-Za-z]/.test(v) && /[0-9]/.test(v)) return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function looksLikeTime(value2) {
|
|
50
|
+
return /\b\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp][Mm])?\b/.test(
|
|
51
|
+
normalizeNumerals(value2).trim()
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
var ISO_DATE = /^\d{4}-\d{1,2}(?:-\d{1,2})?(?:[T ]\d{1,2}:\d{2}(?::\d{2})?)?/;
|
|
55
|
+
var NUMERIC_DATE = /^\d{1,4}[/.-]\d{1,2}[/.-]\d{1,4}$/;
|
|
56
|
+
var TIME_OF_DAY = /^\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp]\.?[Mm]\.?)?$/;
|
|
57
|
+
var MONTH_NAME = /\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b/i;
|
|
58
|
+
function looksLikeDate(v) {
|
|
59
|
+
return ISO_DATE.test(v) || NUMERIC_DATE.test(v) || TIME_OF_DAY.test(v) || MONTH_NAME.test(v) && /\d/.test(v);
|
|
60
|
+
}
|
|
61
|
+
function toTimestamp(value2) {
|
|
62
|
+
const v = value2.trim();
|
|
63
|
+
if (!v) return null;
|
|
64
|
+
if (/^\d{10,13}$/.test(v)) {
|
|
65
|
+
const n = Number(v);
|
|
66
|
+
return v.length === 10 ? n * 1e3 : n;
|
|
67
|
+
}
|
|
68
|
+
if (!looksLikeDate(v)) return null;
|
|
69
|
+
const parsed = Date.parse(v);
|
|
70
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
71
|
+
if (looksLikeTime(v)) {
|
|
72
|
+
const anchored = Date.parse(`1970-01-01 ${v}`);
|
|
73
|
+
if (!Number.isNaN(anchored)) return anchored;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function hasArabicIndicDigits(value2) {
|
|
78
|
+
return ARABIC_INDIC_DIGITS.test(value2);
|
|
79
|
+
}
|
|
80
|
+
function hasLatinDigits(value2) {
|
|
81
|
+
return LATIN_DIGITS.test(value2);
|
|
82
|
+
}
|
|
83
|
+
var NUMERIC_LIKE = /^[+-]?[\d.,\s\u00a0\u2009]+$/;
|
|
84
|
+
function isSameNumberDifferentSeparators(a, b) {
|
|
85
|
+
const at = normalizeNumerals(a).trim();
|
|
86
|
+
const bt = normalizeNumerals(b).trim();
|
|
87
|
+
if (!NUMERIC_LIKE.test(at) || !NUMERIC_LIKE.test(bt)) return false;
|
|
88
|
+
const digitsOnly = (s) => s.replace(/\D/g, "");
|
|
89
|
+
const da = digitsOnly(at);
|
|
90
|
+
const db = digitsOnly(bt);
|
|
91
|
+
if (!da || da !== db) return false;
|
|
92
|
+
const hasSep = (s) => /[.,\s\u00a0\u2009]/.test(s);
|
|
93
|
+
return (hasSep(at) || hasSep(bt)) && a.trim() !== b.trim();
|
|
94
|
+
}
|
|
95
|
+
var CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
96
|
+
"value",
|
|
97
|
+
"placeholder",
|
|
98
|
+
"title",
|
|
99
|
+
"alt",
|
|
100
|
+
"label",
|
|
101
|
+
"aria-label",
|
|
102
|
+
"aria-valuetext",
|
|
103
|
+
"content",
|
|
104
|
+
"datetime"
|
|
105
|
+
]);
|
|
106
|
+
function isContentAttribute(name) {
|
|
107
|
+
return CONTENT_ATTRIBUTES.has(name.toLowerCase());
|
|
108
|
+
}
|
|
109
|
+
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;
|
|
110
|
+
function looksLikeThirdPartyNode(html, tagName) {
|
|
111
|
+
const tag = (tagName ?? "").toUpperCase();
|
|
112
|
+
if (tag === "IFRAME" || tag === "EMBED" || tag === "OBJECT") return true;
|
|
113
|
+
const h = (html ?? "").toLowerCase();
|
|
114
|
+
if (!h) return false;
|
|
115
|
+
return THIRD_PARTY_MARKERS.test(h) || h.includes("about:blank");
|
|
116
|
+
}
|
|
117
|
+
var DATE_PARTS_RE = /^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/;
|
|
118
|
+
function isSameDateDifferentOrder(a, b) {
|
|
119
|
+
const partsA = normalizeNumerals(a).trim().match(DATE_PARTS_RE);
|
|
120
|
+
const partsB = normalizeNumerals(b).trim().match(DATE_PARTS_RE);
|
|
121
|
+
if (!partsA || !partsB) return false;
|
|
122
|
+
const setA = [partsA[1], partsA[2], partsA[3]].sort().join("|");
|
|
123
|
+
const setB = [partsB[1], partsB[2], partsB[3]].sort().join("|");
|
|
124
|
+
return setA === setB && a.trim() !== b.trim();
|
|
125
|
+
}
|
|
126
|
+
var EXTENSION_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
127
|
+
"cz-shortcut-listen",
|
|
128
|
+
"data-gramm",
|
|
129
|
+
"data-gramm_editor",
|
|
130
|
+
"data-gramm_id",
|
|
131
|
+
"data-gr-c-s-loaded",
|
|
132
|
+
"data-lt-installed",
|
|
133
|
+
"data-new-gr-c-s-check-loaded",
|
|
134
|
+
"data-new-gr-c-s-loaded",
|
|
135
|
+
"spellcheck-extension",
|
|
136
|
+
"bis_register",
|
|
137
|
+
"__processed_by_react_dev_tools"
|
|
138
|
+
]);
|
|
139
|
+
function isExtensionAttribute(name) {
|
|
140
|
+
const n = name.toLowerCase();
|
|
141
|
+
if (EXTENSION_ATTRIBUTES.has(n)) return true;
|
|
142
|
+
return n.startsWith("data-gr-") || n.startsWith("data-gramm") || n.startsWith("__bis") || n.startsWith("bis_");
|
|
143
|
+
}
|
|
144
|
+
var CLOSES_P = [
|
|
145
|
+
"address",
|
|
146
|
+
"article",
|
|
147
|
+
"aside",
|
|
148
|
+
"blockquote",
|
|
149
|
+
"center",
|
|
150
|
+
"details",
|
|
151
|
+
"dialog",
|
|
152
|
+
"dir",
|
|
153
|
+
"div",
|
|
154
|
+
"dl",
|
|
155
|
+
"fieldset",
|
|
156
|
+
"figcaption",
|
|
157
|
+
"figure",
|
|
158
|
+
"footer",
|
|
159
|
+
"form",
|
|
160
|
+
"h1",
|
|
161
|
+
"h2",
|
|
162
|
+
"h3",
|
|
163
|
+
"h4",
|
|
164
|
+
"h5",
|
|
165
|
+
"h6",
|
|
166
|
+
"header",
|
|
167
|
+
"hgroup",
|
|
168
|
+
"hr",
|
|
169
|
+
"li",
|
|
170
|
+
"dd",
|
|
171
|
+
"dt",
|
|
172
|
+
"listing",
|
|
173
|
+
"main",
|
|
174
|
+
"menu",
|
|
175
|
+
"nav",
|
|
176
|
+
"ol",
|
|
177
|
+
"p",
|
|
178
|
+
"plaintext",
|
|
179
|
+
"pre",
|
|
180
|
+
"search",
|
|
181
|
+
"section",
|
|
182
|
+
"summary",
|
|
183
|
+
"table",
|
|
184
|
+
"ul",
|
|
185
|
+
"xmp"
|
|
186
|
+
];
|
|
187
|
+
var CLOSES_P_SET = new Set(CLOSES_P.map((t) => t.toUpperCase()));
|
|
188
|
+
var TABLE_CHILDREN = /* @__PURE__ */ new Set([
|
|
189
|
+
"CAPTION",
|
|
190
|
+
"COLGROUP",
|
|
191
|
+
"THEAD",
|
|
192
|
+
"TBODY",
|
|
193
|
+
"TFOOT"
|
|
194
|
+
]);
|
|
195
|
+
var TABLE_SCAFFOLD = ["script", "template", "style"];
|
|
196
|
+
var TABLE_LEVEL = ["caption", "colgroup", "thead", "tbody", "tfoot"];
|
|
197
|
+
var SECTION_LEVEL = ["tr"];
|
|
198
|
+
var ROW_LEVEL = ["td", "th"];
|
|
199
|
+
var notIn = (tags) => `:not(${[...tags, ...TABLE_SCAFFOLD].join(", ")})`;
|
|
200
|
+
var PARSER_REPAIRS = {
|
|
201
|
+
P: CLOSES_P.join(", "),
|
|
202
|
+
A: "a",
|
|
203
|
+
BUTTON: "button",
|
|
204
|
+
FORM: "form",
|
|
205
|
+
TABLE: [
|
|
206
|
+
`:scope > ${notIn([...TABLE_LEVEL, ...SECTION_LEVEL])}`,
|
|
207
|
+
...["thead", "tbody", "tfoot"].flatMap((section) => [
|
|
208
|
+
`:scope > ${section} > ${notIn(SECTION_LEVEL)}`,
|
|
209
|
+
`:scope > ${section} > tr > ${notIn(ROW_LEVEL)}`
|
|
210
|
+
]),
|
|
211
|
+
`:scope > tr > ${notIn(ROW_LEVEL)}`
|
|
212
|
+
].join(", ")
|
|
213
|
+
};
|
|
214
|
+
function isInvalidNesting(parentTag, childTag) {
|
|
215
|
+
if (!parentTag || !childTag) return false;
|
|
216
|
+
const p = parentTag.toUpperCase();
|
|
217
|
+
const c = childTag.toUpperCase();
|
|
218
|
+
if (p === "P") return CLOSES_P_SET.has(c);
|
|
219
|
+
if (p === "A") return c === "A";
|
|
220
|
+
if (p === "BUTTON") return c === "BUTTON" || c === "A";
|
|
221
|
+
if (p === "FORM") return c === "FORM";
|
|
222
|
+
if (p === "TABLE") return !TABLE_CHILDREN.has(c) && !isScaffold(c);
|
|
223
|
+
if (p === "THEAD" || p === "TBODY" || p === "TFOOT") {
|
|
224
|
+
return c !== "TR" && !isScaffold(c);
|
|
225
|
+
}
|
|
226
|
+
if (p === "TR") return c !== "TD" && c !== "TH" && !isScaffold(c);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
function isScaffold(tag) {
|
|
230
|
+
return TABLE_SCAFFOLD.includes(tag.toLowerCase());
|
|
231
|
+
}
|
|
232
|
+
function messageIndicatesInvalidNesting(message) {
|
|
233
|
+
if (!message) return false;
|
|
234
|
+
return /validateDOMNesting/i.test(message) || /cannot (?:be a|contain).*(?:descendant|child)/i.test(message) || /cannot appear as a (?:child|descendant)/i.test(message);
|
|
235
|
+
}
|
|
236
|
+
|
|
13
237
|
// src/core/diff.ts
|
|
14
238
|
var IGNORED_ATTRIBUTES = /* @__PURE__ */ new Set(["data-reactroot"]);
|
|
15
239
|
function parseServerHtml(html, rootTagName) {
|
|
@@ -36,9 +260,15 @@ function diffSnapshotAgainstDom(serverHtml, clientRoot) {
|
|
|
36
260
|
function collectChildren(serverParent, clientParent, parentPath, out, limit) {
|
|
37
261
|
if (out.length >= limit) return;
|
|
38
262
|
const serverChildren = meaningfulChildNodes(serverParent);
|
|
39
|
-
const clientChildren = meaningfulChildNodes(clientParent);
|
|
40
263
|
const parentPending = hasPendingSuspense(serverParent);
|
|
41
264
|
const parentTag = elementTag(clientParent);
|
|
265
|
+
const clientChildren = repairClientChildren(
|
|
266
|
+
meaningfulChildNodes(clientParent),
|
|
267
|
+
parentTag,
|
|
268
|
+
parentPath,
|
|
269
|
+
out,
|
|
270
|
+
limit
|
|
271
|
+
);
|
|
42
272
|
const pairs = alignChildren(serverChildren, clientChildren);
|
|
43
273
|
let index = 0;
|
|
44
274
|
for (let k = 0; k < pairs.length; k++) {
|
|
@@ -86,7 +316,10 @@ function collectChildren(serverParent, clientParent, parentPath, out, limit) {
|
|
|
86
316
|
parentTagName: parentTag,
|
|
87
317
|
server: null,
|
|
88
318
|
client: serialize(clientNode),
|
|
89
|
-
element
|
|
319
|
+
// An added text node has no element of its own, so point at its
|
|
320
|
+
// parent, as text changes do. With `null` the component lookup found
|
|
321
|
+
// nothing and fell back to whichever error was reported last.
|
|
322
|
+
element: asElement(clientNode) ?? asElement(clientParent)
|
|
90
323
|
});
|
|
91
324
|
} else if (serverNode && clientNode) {
|
|
92
325
|
collectNode(serverNode, clientNode, path, parentTag, out, limit);
|
|
@@ -132,7 +365,7 @@ function collectNode(serverNode, clientNode, path, parentTag, out, limit) {
|
|
|
132
365
|
parentTagName: parentTag,
|
|
133
366
|
server: serialize(serverEl),
|
|
134
367
|
client: serialize(clientEl),
|
|
135
|
-
element: clientEl
|
|
368
|
+
element: liveElement(clientEl)
|
|
136
369
|
});
|
|
137
370
|
return;
|
|
138
371
|
}
|
|
@@ -160,26 +393,26 @@ function diffAttributes(serverEl, clientEl, path, parentTag) {
|
|
|
160
393
|
attribute: name,
|
|
161
394
|
server: serverValue,
|
|
162
395
|
client: clientValue,
|
|
163
|
-
element: clientEl
|
|
396
|
+
element: liveElement(clientEl)
|
|
164
397
|
};
|
|
165
398
|
}
|
|
166
399
|
return null;
|
|
167
400
|
}
|
|
168
|
-
function normalizeAttr(name,
|
|
169
|
-
if (
|
|
401
|
+
function normalizeAttr(name, value2) {
|
|
402
|
+
if (value2 == null) return null;
|
|
170
403
|
if (name === "class") {
|
|
171
|
-
return
|
|
404
|
+
return value2.trim().split(/\s+/).filter(Boolean).sort().join(" ");
|
|
172
405
|
}
|
|
173
406
|
if (name === "style") {
|
|
174
|
-
return normalizeStyle(
|
|
407
|
+
return normalizeStyle(value2);
|
|
175
408
|
}
|
|
176
|
-
return
|
|
409
|
+
return value2;
|
|
177
410
|
}
|
|
178
|
-
function normalizeStyle(
|
|
411
|
+
function normalizeStyle(value2) {
|
|
179
412
|
if (typeof document !== "undefined") {
|
|
180
413
|
try {
|
|
181
414
|
const el = document.createElement("div");
|
|
182
|
-
el.style.cssText =
|
|
415
|
+
el.style.cssText = value2;
|
|
183
416
|
const decls = [];
|
|
184
417
|
for (let i = 0; i < el.style.length; i++) {
|
|
185
418
|
const prop = el.style.item(i);
|
|
@@ -189,7 +422,7 @@ function normalizeStyle(value) {
|
|
|
189
422
|
} catch {
|
|
190
423
|
}
|
|
191
424
|
}
|
|
192
|
-
return
|
|
425
|
+
return value2.split(";").map((s) => s.trim()).filter(Boolean).sort().join(";");
|
|
193
426
|
}
|
|
194
427
|
function nodeKey(node) {
|
|
195
428
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -307,7 +540,8 @@ function meaningfulChildNodes(parent) {
|
|
|
307
540
|
for (const node of raw) {
|
|
308
541
|
if (node.nodeType === Node.COMMENT_NODE) {
|
|
309
542
|
const data = node.nodeValue ?? "";
|
|
310
|
-
if (data === "$?" || data === "$" || data === "$!")
|
|
543
|
+
if (data === "$?" || data === "$" || data === "$!")
|
|
544
|
+
boundaryStack.push(data);
|
|
311
545
|
else if (data === "/$") boundaryStack.pop();
|
|
312
546
|
continue;
|
|
313
547
|
}
|
|
@@ -330,7 +564,94 @@ function serialize(node) {
|
|
|
330
564
|
}
|
|
331
565
|
function asElement(node) {
|
|
332
566
|
if (!node) return null;
|
|
333
|
-
return node.nodeType === Node.ELEMENT_NODE ? node : null;
|
|
567
|
+
return node.nodeType === Node.ELEMENT_NODE ? liveElement(node) : null;
|
|
568
|
+
}
|
|
569
|
+
var LIVE = /* @__PURE__ */ new WeakMap();
|
|
570
|
+
function liveElement(el) {
|
|
571
|
+
return LIVE.get(el) ?? el;
|
|
572
|
+
}
|
|
573
|
+
function repairClientChildren(children, parentTag, parentPath, out, limit) {
|
|
574
|
+
let repaired = null;
|
|
575
|
+
children.forEach((child, i) => {
|
|
576
|
+
const expansion = repairedForm(child, parentTag);
|
|
577
|
+
if (!expansion) {
|
|
578
|
+
repaired?.push(child);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
repaired ??= children.slice(0, i);
|
|
582
|
+
repaired.push(...expansion.nodes);
|
|
583
|
+
if (out.length < limit) {
|
|
584
|
+
out.push({
|
|
585
|
+
kind: "structure",
|
|
586
|
+
path: childPath(parentPath, child, i),
|
|
587
|
+
tagName: expansion.offender.tagName,
|
|
588
|
+
parentTagName: invalidParent(expansion.offender, child),
|
|
589
|
+
// No values, on purpose: React reports the same nesting in its own
|
|
590
|
+
// warning with none, and matching it lets the two collapse into one.
|
|
591
|
+
server: null,
|
|
592
|
+
client: null,
|
|
593
|
+
element: expansion.offender
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
return repaired ?? children;
|
|
598
|
+
}
|
|
599
|
+
function invalidParent(offender, top) {
|
|
600
|
+
for (let el = offender.parentElement; el; el = el.parentElement) {
|
|
601
|
+
if (isInvalidNesting(el.tagName, offender.tagName)) return el.tagName;
|
|
602
|
+
if (el === top) break;
|
|
603
|
+
}
|
|
604
|
+
return top.tagName;
|
|
605
|
+
}
|
|
606
|
+
function repairedForm(node, parentTag) {
|
|
607
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
608
|
+
const el = node;
|
|
609
|
+
const selector = PARSER_REPAIRS[el.tagName];
|
|
610
|
+
if (!selector) return null;
|
|
611
|
+
let offender = null;
|
|
612
|
+
try {
|
|
613
|
+
offender = el.querySelector(selector);
|
|
614
|
+
} catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
if (!offender) return null;
|
|
618
|
+
const html = serializeKeepingTextBoundaries(el);
|
|
619
|
+
const container = parseServerHtml(html, parentTag ?? "div");
|
|
620
|
+
if (container.innerHTML === html) return null;
|
|
621
|
+
const nodes = meaningfulChildNodes(container);
|
|
622
|
+
mapToLive(el, nodes);
|
|
623
|
+
return { nodes, offender };
|
|
624
|
+
}
|
|
625
|
+
function serializeKeepingTextBoundaries(el) {
|
|
626
|
+
const copy = el.cloneNode(true);
|
|
627
|
+
const walker = copy.ownerDocument.createTreeWalker(
|
|
628
|
+
copy,
|
|
629
|
+
NodeFilter.SHOW_TEXT
|
|
630
|
+
);
|
|
631
|
+
const texts = [];
|
|
632
|
+
while (walker.nextNode()) texts.push(walker.currentNode);
|
|
633
|
+
for (const t of texts) {
|
|
634
|
+
if (t.previousSibling?.nodeType === Node.TEXT_NODE) {
|
|
635
|
+
t.parentNode.insertBefore(copy.ownerDocument.createComment(" "), t);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return copy.outerHTML;
|
|
639
|
+
}
|
|
640
|
+
function mapToLive(live, repaired) {
|
|
641
|
+
const liveEls = [live, ...Array.from(live.querySelectorAll("*"))];
|
|
642
|
+
let i = 0;
|
|
643
|
+
for (const root of repaired) {
|
|
644
|
+
if (root.nodeType !== Node.ELEMENT_NODE) continue;
|
|
645
|
+
const el = root;
|
|
646
|
+
for (const r of [el, ...Array.from(el.querySelectorAll("*"))]) {
|
|
647
|
+
let j = i;
|
|
648
|
+
while (j < liveEls.length && liveEls[j].tagName !== r.tagName) j++;
|
|
649
|
+
if (j < liveEls.length) {
|
|
650
|
+
LIVE.set(r, liveEls[j]);
|
|
651
|
+
i = j + 1;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
334
655
|
}
|
|
335
656
|
function elementTag(node) {
|
|
336
657
|
return node.nodeType === Node.ELEMENT_NODE ? node.tagName : void 0;
|
|
@@ -351,177 +672,161 @@ function childPath(parentPath, node, index) {
|
|
|
351
672
|
return `${parentPath} > #text[${index}]`;
|
|
352
673
|
}
|
|
353
674
|
|
|
354
|
-
// src/core/classify/
|
|
355
|
-
var
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
var ARABIC_INDIC_DIGITS = /[٠-٩۰-۹]/;
|
|
361
|
-
var LATIN_DIGITS = /[0-9]/;
|
|
362
|
-
var BIDI_CONTROLS = /[-]/g;
|
|
363
|
-
function stripBidiControls(value) {
|
|
364
|
-
return value.replace(BIDI_CONTROLS, "");
|
|
675
|
+
// src/core/classify/messages.ts
|
|
676
|
+
var code = (text) => ({ code: text });
|
|
677
|
+
var value = (text) => ({ value: text });
|
|
678
|
+
function param(params, name) {
|
|
679
|
+
const v = params[name];
|
|
680
|
+
return typeof v === "string" ? v : "";
|
|
365
681
|
}
|
|
366
|
-
function
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
return a !== b && sa === sb && sa.trim() !== "";
|
|
682
|
+
function paramList(params, name) {
|
|
683
|
+
const v = params[name];
|
|
684
|
+
return Array.isArray(v) ? v : [];
|
|
370
685
|
}
|
|
371
|
-
function
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
const zero = code >= 1776 ? 1776 : 1632;
|
|
377
|
-
return String(code - zero);
|
|
686
|
+
function valueList(items, separator) {
|
|
687
|
+
const out = [];
|
|
688
|
+
items.forEach((item, i) => {
|
|
689
|
+
if (i > 0) out.push(separator);
|
|
690
|
+
out.push(value(item));
|
|
378
691
|
});
|
|
692
|
+
return out;
|
|
379
693
|
}
|
|
380
|
-
function
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
694
|
+
function renderMessage(template, params = {}) {
|
|
695
|
+
if (typeof template === "function") return template(params);
|
|
696
|
+
const out = [];
|
|
697
|
+
template.split("`").forEach((part, i) => {
|
|
698
|
+
if (i % 2 === 1) {
|
|
699
|
+
out.push(
|
|
700
|
+
code(part.replace(/\{(\w+)\}/g, (_, n) => param(params, n)))
|
|
701
|
+
);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
let last = 0;
|
|
705
|
+
for (const m of part.matchAll(/\{(\w+)\}/g)) {
|
|
706
|
+
const at = m.index ?? 0;
|
|
707
|
+
if (at > last) out.push(part.slice(last, at));
|
|
708
|
+
out.push(value(param(params, m[1])));
|
|
709
|
+
last = at + m[0].length;
|
|
710
|
+
}
|
|
711
|
+
if (last < part.length) out.push(part.slice(last));
|
|
712
|
+
});
|
|
713
|
+
return out.filter((s) => s !== "");
|
|
394
714
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
function looksLikeDate(v) {
|
|
400
|
-
return ISO_DATE.test(v) || NUMERIC_DATE.test(v) || TIME_OF_DAY.test(v) || MONTH_NAME.test(v) && /\d/.test(v);
|
|
715
|
+
function plainText(segments) {
|
|
716
|
+
return segments.map(
|
|
717
|
+
(s) => typeof s === "string" ? s : "code" in s ? `\`${s.code}\`` : s.value
|
|
718
|
+
).join("");
|
|
401
719
|
}
|
|
402
|
-
function
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
|
|
720
|
+
function classDetail(params, words) {
|
|
721
|
+
const added = paramList(params, "added");
|
|
722
|
+
const removed = paramList(params, "removed");
|
|
723
|
+
if (!added.length && !removed.length) return [];
|
|
724
|
+
const out = [words.open];
|
|
725
|
+
if (added.length) {
|
|
726
|
+
out.push(words.added, ...valueList(added, words.listSeparator));
|
|
408
727
|
}
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (looksLikeTime(v)) {
|
|
413
|
-
const anchored = Date.parse(`1970-01-01 ${v}`);
|
|
414
|
-
if (!Number.isNaN(anchored)) return anchored;
|
|
728
|
+
if (removed.length) {
|
|
729
|
+
if (added.length) out.push(words.partSeparator);
|
|
730
|
+
out.push(words.removed, ...valueList(removed, words.listSeparator));
|
|
415
731
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
function hasArabicIndicDigits(value) {
|
|
419
|
-
return ARABIC_INDIC_DIGITS.test(value);
|
|
420
|
-
}
|
|
421
|
-
function hasLatinDigits(value) {
|
|
422
|
-
return LATIN_DIGITS.test(value);
|
|
423
|
-
}
|
|
424
|
-
var NUMERIC_LIKE = /^[+-]?[\d.,\s\u00a0\u2009]+$/;
|
|
425
|
-
function isSameNumberDifferentSeparators(a, b) {
|
|
426
|
-
const at = normalizeNumerals(a).trim();
|
|
427
|
-
const bt = normalizeNumerals(b).trim();
|
|
428
|
-
if (!NUMERIC_LIKE.test(at) || !NUMERIC_LIKE.test(bt)) return false;
|
|
429
|
-
const digitsOnly = (s) => s.replace(/\D/g, "");
|
|
430
|
-
const da = digitsOnly(at);
|
|
431
|
-
const db = digitsOnly(bt);
|
|
432
|
-
if (!da || da !== db) return false;
|
|
433
|
-
const hasSep = (s) => /[.,\s\u00a0\u2009]/.test(s);
|
|
434
|
-
return (hasSep(at) || hasSep(bt)) && a.trim() !== b.trim();
|
|
435
|
-
}
|
|
436
|
-
var CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
437
|
-
"value",
|
|
438
|
-
"placeholder",
|
|
439
|
-
"title",
|
|
440
|
-
"alt",
|
|
441
|
-
"label",
|
|
442
|
-
"aria-label",
|
|
443
|
-
"aria-valuetext",
|
|
444
|
-
"content",
|
|
445
|
-
"datetime"
|
|
446
|
-
]);
|
|
447
|
-
function isContentAttribute(name) {
|
|
448
|
-
return CONTENT_ATTRIBUTES.has(name.toLowerCase());
|
|
449
|
-
}
|
|
450
|
-
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;
|
|
451
|
-
function looksLikeThirdPartyNode(html, tagName) {
|
|
452
|
-
const tag = (tagName ?? "").toUpperCase();
|
|
453
|
-
if (tag === "IFRAME" || tag === "EMBED" || tag === "OBJECT") return true;
|
|
454
|
-
const h = (html ?? "").toLowerCase();
|
|
455
|
-
if (!h) return false;
|
|
456
|
-
return THIRD_PARTY_MARKERS.test(h) || h.includes("about:blank");
|
|
457
|
-
}
|
|
458
|
-
var DATE_PARTS_RE = /^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/;
|
|
459
|
-
function isSameDateDifferentOrder(a, b) {
|
|
460
|
-
const partsA = normalizeNumerals(a).trim().match(DATE_PARTS_RE);
|
|
461
|
-
const partsB = normalizeNumerals(b).trim().match(DATE_PARTS_RE);
|
|
462
|
-
if (!partsA || !partsB) return false;
|
|
463
|
-
const setA = [partsA[1], partsA[2], partsA[3]].sort().join("|");
|
|
464
|
-
const setB = [partsB[1], partsB[2], partsB[3]].sort().join("|");
|
|
465
|
-
return setA === setB && a.trim() !== b.trim();
|
|
466
|
-
}
|
|
467
|
-
var EXTENSION_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
468
|
-
"cz-shortcut-listen",
|
|
469
|
-
"data-gramm",
|
|
470
|
-
"data-gramm_editor",
|
|
471
|
-
"data-gramm_id",
|
|
472
|
-
"data-gr-c-s-loaded",
|
|
473
|
-
"data-lt-installed",
|
|
474
|
-
"data-new-gr-c-s-check-loaded",
|
|
475
|
-
"data-new-gr-c-s-loaded",
|
|
476
|
-
"spellcheck-extension",
|
|
477
|
-
"bis_register",
|
|
478
|
-
"__processed_by_react_dev_tools"
|
|
479
|
-
]);
|
|
480
|
-
function isExtensionAttribute(name) {
|
|
481
|
-
const n = name.toLowerCase();
|
|
482
|
-
if (EXTENSION_ATTRIBUTES.has(n)) return true;
|
|
483
|
-
return n.startsWith("data-gr-") || n.startsWith("data-gramm") || n.startsWith("__bis") || n.startsWith("bis_");
|
|
732
|
+
out.push(words.close);
|
|
733
|
+
return out;
|
|
484
734
|
}
|
|
485
|
-
var
|
|
486
|
-
"
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
"
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
"
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
"
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
"
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
735
|
+
var EN_MESSAGES = {
|
|
736
|
+
"non-deterministic-value": {
|
|
737
|
+
explanation: "The server and client rendered different random-looking values (an id, token, or Math.random() output). Anything non-deterministic in render produces a different value on each side.",
|
|
738
|
+
suggestion: "Use React `useId()` for ids. For random values, generate them after mount (in `useEffect`) or pass a value down from the server so both sides agree. Never call `Math.random()`/`crypto` during render."
|
|
739
|
+
},
|
|
740
|
+
"date-time": {
|
|
741
|
+
explanation: "The values are dates/times that differ between server render and client render \u2014 the clock moved (or the timezone differs) between the two environments.",
|
|
742
|
+
suggestion: "Render the current time after mount, or pass a single server timestamp down and format it identically on both sides. Pin an explicit timezone when formatting."
|
|
743
|
+
},
|
|
744
|
+
"locale-format.bidi": {
|
|
745
|
+
explanation: "The values differ only by invisible bidirectional control marks (LRM/RLM/isolates). `Intl` adds these around numbers and dates in RTL locales, and different ICU versions \u2014 Node vs the browser \u2014 emit different ones for the same input.",
|
|
746
|
+
suggestion: "Format the value in one place and pass the string down, or pin the same locale and timezone on both sides. If the marks are harmless, add `suppressHydrationWarning` to the element."
|
|
747
|
+
},
|
|
748
|
+
"locale-format.script": {
|
|
749
|
+
explanation: "The same value was formatted with different digit scripts (Arabic-Indic \u0660\u0661\u0662 vs Latin 012). The server and client resolved to different locales.",
|
|
750
|
+
suggestion: "Pass an explicit `locale` (and timezone) to `Intl.NumberFormat` / `toLocaleString` on both server and client, or format the value after mount so only the client locale is ever used."
|
|
751
|
+
},
|
|
752
|
+
"locale-format.separators": {
|
|
753
|
+
explanation: "The same number was formatted with different grouping/decimal separators between server and client (e.g. 1,234.56 vs 1.234,56).",
|
|
754
|
+
suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match."
|
|
755
|
+
},
|
|
756
|
+
"locale-format.date-order": {
|
|
757
|
+
explanation: "The same date was rendered in a different field order (MM/DD vs DD/MM) between server and client.",
|
|
758
|
+
suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides."
|
|
759
|
+
},
|
|
760
|
+
"browser-only-api": {
|
|
761
|
+
explanation: "The client rendered content the server left empty \u2014 the signature of reading a browser-only API (`window`, `document`, `localStorage`, `navigator`, `matchMedia`) during render.",
|
|
762
|
+
suggestion: "Gate browser-only reads behind a mounted flag or `useEffect`, or use `useSyncExternalStore` with a server snapshot so the first client render matches the server."
|
|
763
|
+
},
|
|
764
|
+
"viewport-branching": {
|
|
765
|
+
explanation: "A whole subtree was added, removed, or swapped between server and client \u2014 typically a JavaScript width/viewport check that branches the tree at first render.",
|
|
766
|
+
suggestion: "Render both branches and switch between them with CSS media queries at first paint instead of branching in JavaScript, or defer the JS-driven branch until after mount."
|
|
767
|
+
},
|
|
768
|
+
"invalid-html-nesting": {
|
|
769
|
+
explanation: "A node was moved or ejected because the markup is invalid HTML (e.g. a `<div>` inside a `<p>`, or nested `<a>`). The browser repairs the server DOM, so it no longer matches what React expects.",
|
|
770
|
+
suggestion: "Fix the markup validity: block elements cannot live inside `<p>`, anchors cannot nest, etc. Replace the invalid parent with a `<div>` or restructure the tree."
|
|
771
|
+
},
|
|
772
|
+
"whitespace-minification": {
|
|
773
|
+
explanation: "The mismatch is whitespace-only \u2014 the text is identical apart from spaces/newlines. An HTML minifier likely collapsed whitespace around the hydration root differently from React.",
|
|
774
|
+
suggestion: "Check your HTML minifier settings (e.g. `conservativeCollapse`) around the app root, or avoid minifying whitespace inside hydrated markup."
|
|
775
|
+
},
|
|
776
|
+
"third-party-dom-mutation.extension-attribute": {
|
|
777
|
+
explanation: "The attribute `{attribute}` was injected by a browser extension or third-party script (e.g. Grammarly, ColorZilla) before hydration, so the client DOM no longer matches the server.",
|
|
778
|
+
suggestion: "This is usually harmless. Add `suppressHydrationWarning` to the affected element, or defer third-party script init until after hydration."
|
|
779
|
+
},
|
|
780
|
+
"third-party-dom-mutation.root-attribute": {
|
|
781
|
+
explanation: "An attribute (`{attribute}`) appeared on a root element that the server never sent \u2014 a hallmark of an extension or early third-party script mutating the DOM.",
|
|
782
|
+
suggestion: "Add `suppressHydrationWarning` to the root element, or defer the third-party script until after hydration."
|
|
783
|
+
},
|
|
784
|
+
"third-party-dom-mutation.injected-node": {
|
|
785
|
+
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.",
|
|
786
|
+
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">`).'
|
|
787
|
+
},
|
|
788
|
+
"attribute-mismatch.class": {
|
|
789
|
+
explanation: (p) => [
|
|
790
|
+
"The ",
|
|
791
|
+
code("class"),
|
|
792
|
+
" differs between server and client",
|
|
793
|
+
...classDetail(p, {
|
|
794
|
+
added: "added on client: ",
|
|
795
|
+
removed: "removed on client: ",
|
|
796
|
+
listSeparator: ", ",
|
|
797
|
+
partSeparator: "; ",
|
|
798
|
+
open: " (",
|
|
799
|
+
close: ")"
|
|
800
|
+
}),
|
|
801
|
+
". 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."
|
|
802
|
+
],
|
|
803
|
+
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."
|
|
804
|
+
},
|
|
805
|
+
"attribute-mismatch.style": {
|
|
806
|
+
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.",
|
|
807
|
+
suggestion: "Compute the style after mount (`useEffect`) so the first client render matches the server, or move it to a CSS class / media query."
|
|
808
|
+
},
|
|
809
|
+
"attribute-mismatch.generic": {
|
|
810
|
+
explanation: "The `{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.",
|
|
811
|
+
suggestion: "Make the attribute deterministic across server and client, or set it after mount so the first client render matches the server HTML."
|
|
812
|
+
},
|
|
813
|
+
unknown: {
|
|
814
|
+
explanation: "A hydration mismatch was detected but could not be matched to a known cause. Inspect the server vs client values above.",
|
|
815
|
+
suggestion: "Compare the server and client values. Common causes are non-deterministic values, dates/locales, and browser-only APIs used during render."
|
|
816
|
+
},
|
|
817
|
+
"unknown.no-location": {
|
|
818
|
+
explanation: "React reported that hydration failed but did not say which node differed, and the DOM diff found no difference to point at.",
|
|
819
|
+
suggestion: "Make sure `<HydrationSnapshotScript>` (or the manual snapshot script) is in `<head>` so the DOM diff can locate the node, and read React's full warning in the browser console."
|
|
519
820
|
}
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
821
|
+
};
|
|
822
|
+
function describe(id, params = {}, catalog = EN_MESSAGES) {
|
|
823
|
+
const entry = catalog[id];
|
|
824
|
+
return {
|
|
825
|
+
messageId: id,
|
|
826
|
+
...Object.keys(params).length ? { params } : {},
|
|
827
|
+
explanation: plainText(renderMessage(entry.explanation, params)),
|
|
828
|
+
suggestion: plainText(renderMessage(entry.suggestion, params))
|
|
829
|
+
};
|
|
525
830
|
}
|
|
526
831
|
|
|
527
832
|
// src/core/classify/rules.ts
|
|
@@ -546,8 +851,7 @@ var nonDeterministic = (d) => {
|
|
|
546
851
|
return {
|
|
547
852
|
category: "non-deterministic-value",
|
|
548
853
|
confidence: 0.9,
|
|
549
|
-
|
|
550
|
-
suggestion: "Use React `useId()` for ids. For random values, generate them after mount (in `useEffect`) or pass a value down from the server so both sides agree. Never call `Math.random()`/`crypto` during render.",
|
|
854
|
+
...describe("non-deterministic-value"),
|
|
551
855
|
docsUrl: docs("non-deterministic-value")
|
|
552
856
|
};
|
|
553
857
|
};
|
|
@@ -565,8 +869,7 @@ var dateTime = (d) => {
|
|
|
565
869
|
return {
|
|
566
870
|
category: "date-time",
|
|
567
871
|
confidence: smallDelta || bothTimes ? 0.85 : 0.7,
|
|
568
|
-
|
|
569
|
-
suggestion: "Render the current time after mount, or pass a single server timestamp down and format it identically on both sides. Pin an explicit timezone when formatting.",
|
|
872
|
+
...describe("date-time"),
|
|
570
873
|
docsUrl: docs("date-time")
|
|
571
874
|
};
|
|
572
875
|
};
|
|
@@ -578,8 +881,7 @@ var localeFormat = (d) => {
|
|
|
578
881
|
return {
|
|
579
882
|
category: "locale-format",
|
|
580
883
|
confidence: 0.88,
|
|
581
|
-
|
|
582
|
-
suggestion: "Format the value in one place and pass the string down, or pin the same locale and timezone on both sides. If the marks are harmless, add `suppressHydrationWarning` to the element.",
|
|
884
|
+
...describe("locale-format.bidi"),
|
|
583
885
|
docsUrl: docs("locale-format")
|
|
584
886
|
};
|
|
585
887
|
}
|
|
@@ -588,8 +890,7 @@ var localeFormat = (d) => {
|
|
|
588
890
|
return {
|
|
589
891
|
category: "locale-format",
|
|
590
892
|
confidence: 0.92,
|
|
591
|
-
|
|
592
|
-
suggestion: "Pass an explicit `locale` (and timezone) to `Intl.NumberFormat` / `toLocaleString` on both server and client, or format the value after mount so only the client locale is ever used.",
|
|
893
|
+
...describe("locale-format.script"),
|
|
593
894
|
docsUrl: docs("locale-format")
|
|
594
895
|
};
|
|
595
896
|
}
|
|
@@ -597,8 +898,7 @@ var localeFormat = (d) => {
|
|
|
597
898
|
return {
|
|
598
899
|
category: "locale-format",
|
|
599
900
|
confidence: 0.82,
|
|
600
|
-
|
|
601
|
-
suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match.",
|
|
901
|
+
...describe("locale-format.separators"),
|
|
602
902
|
docsUrl: docs("locale-format")
|
|
603
903
|
};
|
|
604
904
|
}
|
|
@@ -606,8 +906,7 @@ var localeFormat = (d) => {
|
|
|
606
906
|
return {
|
|
607
907
|
category: "locale-format",
|
|
608
908
|
confidence: 0.75,
|
|
609
|
-
|
|
610
|
-
suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides.",
|
|
909
|
+
...describe("locale-format.date-order"),
|
|
611
910
|
docsUrl: docs("locale-format")
|
|
612
911
|
};
|
|
613
912
|
}
|
|
@@ -621,8 +920,7 @@ var browserOnlyApi = (d) => {
|
|
|
621
920
|
return {
|
|
622
921
|
category: "browser-only-api",
|
|
623
922
|
confidence: 0.75,
|
|
624
|
-
|
|
625
|
-
suggestion: "Gate browser-only reads behind a mounted flag or `useEffect`, or use `useSyncExternalStore` with a server snapshot so the first client render matches the server.",
|
|
923
|
+
...describe("browser-only-api"),
|
|
626
924
|
docsUrl: docs("browser-only-api")
|
|
627
925
|
};
|
|
628
926
|
};
|
|
@@ -636,8 +934,7 @@ var viewportBranching = (d) => {
|
|
|
636
934
|
return {
|
|
637
935
|
category: "viewport-branching",
|
|
638
936
|
confidence: 0.6,
|
|
639
|
-
|
|
640
|
-
suggestion: "Render both branches and switch between them with CSS media queries at first paint instead of branching in JavaScript, or defer the JS-driven branch until after mount.",
|
|
937
|
+
...describe("viewport-branching"),
|
|
641
938
|
docsUrl: docs("viewport-branching")
|
|
642
939
|
};
|
|
643
940
|
};
|
|
@@ -648,8 +945,7 @@ var invalidNesting = (d) => {
|
|
|
648
945
|
return {
|
|
649
946
|
category: "invalid-html-nesting",
|
|
650
947
|
confidence: byMessage ? 0.9 : 0.72,
|
|
651
|
-
|
|
652
|
-
suggestion: "Fix the markup validity: block elements cannot live inside `<p>`, anchors cannot nest, etc. Replace the invalid parent with a `<div>` or restructure the tree.",
|
|
948
|
+
...describe("invalid-html-nesting"),
|
|
653
949
|
docsUrl: docs("invalid-html-nesting")
|
|
654
950
|
};
|
|
655
951
|
};
|
|
@@ -662,8 +958,7 @@ var whitespaceMinification = (d) => {
|
|
|
662
958
|
return {
|
|
663
959
|
category: "whitespace-minification",
|
|
664
960
|
confidence: 0.7,
|
|
665
|
-
|
|
666
|
-
suggestion: "Check your HTML minifier settings (e.g. `conservativeCollapse`) around the app root, or avoid minifying whitespace inside hydrated markup.",
|
|
961
|
+
...describe("whitespace-minification"),
|
|
667
962
|
docsUrl: docs("whitespace-minification")
|
|
668
963
|
};
|
|
669
964
|
};
|
|
@@ -673,8 +968,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
673
968
|
return {
|
|
674
969
|
category: "third-party-dom-mutation",
|
|
675
970
|
confidence: 0.88,
|
|
676
|
-
|
|
677
|
-
|
|
971
|
+
...describe("third-party-dom-mutation.extension-attribute", {
|
|
972
|
+
attribute: d.attribute
|
|
973
|
+
}),
|
|
678
974
|
docsUrl: docs("third-party-dom-mutation")
|
|
679
975
|
};
|
|
680
976
|
}
|
|
@@ -684,8 +980,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
684
980
|
return {
|
|
685
981
|
category: "third-party-dom-mutation",
|
|
686
982
|
confidence: 0.6,
|
|
687
|
-
|
|
688
|
-
|
|
983
|
+
...describe("third-party-dom-mutation.root-attribute", {
|
|
984
|
+
attribute: d.attribute
|
|
985
|
+
}),
|
|
689
986
|
docsUrl: docs("third-party-dom-mutation")
|
|
690
987
|
};
|
|
691
988
|
}
|
|
@@ -695,8 +992,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
695
992
|
return {
|
|
696
993
|
category: "third-party-dom-mutation",
|
|
697
994
|
confidence: 0.7,
|
|
698
|
-
|
|
699
|
-
|
|
995
|
+
...describe("third-party-dom-mutation.injected-node", {
|
|
996
|
+
tag: `<${tag}>`
|
|
997
|
+
}),
|
|
700
998
|
docsUrl: docs("third-party-dom-mutation")
|
|
701
999
|
};
|
|
702
1000
|
}
|
|
@@ -712,15 +1010,10 @@ var attributeMismatch = (d) => {
|
|
|
712
1010
|
const clientSet = new Set(client.split(/\s+/).filter(Boolean));
|
|
713
1011
|
const added = [...clientSet].filter((c) => !serverSet.has(c));
|
|
714
1012
|
const removed = [...serverSet].filter((c) => !clientSet.has(c));
|
|
715
|
-
const parts = [];
|
|
716
|
-
if (added.length) parts.push(`added on client: ${added.join(", ")}`);
|
|
717
|
-
if (removed.length) parts.push(`removed on client: ${removed.join(", ")}`);
|
|
718
|
-
const detail = parts.length ? ` (${parts.join("; ")})` : "";
|
|
719
1013
|
return {
|
|
720
1014
|
category: "attribute-mismatch",
|
|
721
1015
|
confidence: 0.8,
|
|
722
|
-
|
|
723
|
-
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.",
|
|
1016
|
+
...describe("attribute-mismatch.class", { added, removed }),
|
|
724
1017
|
docsUrl: docs("attribute-mismatch")
|
|
725
1018
|
};
|
|
726
1019
|
}
|
|
@@ -728,16 +1021,18 @@ var attributeMismatch = (d) => {
|
|
|
728
1021
|
return {
|
|
729
1022
|
category: "attribute-mismatch",
|
|
730
1023
|
confidence: 0.75,
|
|
731
|
-
|
|
732
|
-
suggestion: "Compute the style after mount (`useEffect`) so the first client render matches the server, or move it to a CSS class / media query.",
|
|
1024
|
+
...describe("attribute-mismatch.style"),
|
|
733
1025
|
docsUrl: docs("attribute-mismatch")
|
|
734
1026
|
};
|
|
735
1027
|
}
|
|
736
1028
|
return {
|
|
737
1029
|
category: "attribute-mismatch",
|
|
738
1030
|
confidence: 0.6,
|
|
739
|
-
|
|
740
|
-
|
|
1031
|
+
...describe("attribute-mismatch.generic", {
|
|
1032
|
+
attribute: d.attribute,
|
|
1033
|
+
server,
|
|
1034
|
+
client
|
|
1035
|
+
}),
|
|
741
1036
|
docsUrl: docs("attribute-mismatch")
|
|
742
1037
|
};
|
|
743
1038
|
};
|
|
@@ -759,13 +1054,21 @@ var BUILT_IN_RULES = [
|
|
|
759
1054
|
var UNKNOWN_CAUSE = {
|
|
760
1055
|
category: "unknown",
|
|
761
1056
|
confidence: 0,
|
|
762
|
-
|
|
763
|
-
|
|
1057
|
+
...describe("unknown"),
|
|
1058
|
+
docsUrl: docs("unknown")
|
|
1059
|
+
};
|
|
1060
|
+
var UNKNOWN_NO_LOCATION_CAUSE = {
|
|
1061
|
+
category: "unknown",
|
|
1062
|
+
confidence: 0,
|
|
1063
|
+
...describe("unknown.no-location"),
|
|
764
1064
|
docsUrl: docs("unknown")
|
|
765
1065
|
};
|
|
766
1066
|
|
|
767
1067
|
// src/core/classify/index.ts
|
|
768
1068
|
var CONFIDENCE_THRESHOLD = 0.5;
|
|
1069
|
+
function isLocationless(d) {
|
|
1070
|
+
return d.server == null && d.client == null && !d.tagName && !d.attribute;
|
|
1071
|
+
}
|
|
769
1072
|
function classify(divergence, options = {}) {
|
|
770
1073
|
const threshold = options.threshold ?? CONFIDENCE_THRESHOLD;
|
|
771
1074
|
const rules = [
|
|
@@ -783,7 +1086,7 @@ function classify(divergence, options = {}) {
|
|
|
783
1086
|
return result;
|
|
784
1087
|
}
|
|
785
1088
|
}
|
|
786
|
-
return UNKNOWN_CAUSE;
|
|
1089
|
+
return isLocationless(divergence) ? UNKNOWN_NO_LOCATION_CAUSE : UNKNOWN_CAUSE;
|
|
787
1090
|
}
|
|
788
1091
|
|
|
789
1092
|
// src/core/report.ts
|
|
@@ -792,12 +1095,12 @@ function nextId() {
|
|
|
792
1095
|
counter += 1;
|
|
793
1096
|
return `wh_${Date.now().toString(36)}_${counter}`;
|
|
794
1097
|
}
|
|
795
|
-
function truncate(
|
|
796
|
-
if (
|
|
797
|
-
if (
|
|
798
|
-
const last =
|
|
1098
|
+
function truncate(value2, max = 300) {
|
|
1099
|
+
if (value2 == null) return null;
|
|
1100
|
+
if (value2.length <= max) return value2;
|
|
1101
|
+
const last = value2.charCodeAt(max - 1);
|
|
799
1102
|
const end = last >= 55296 && last <= 56319 ? max - 1 : max;
|
|
800
|
-
return `${
|
|
1103
|
+
return `${value2.slice(0, end)}\u2026`;
|
|
801
1104
|
}
|
|
802
1105
|
function buildReport(divergence, cause, context = {}) {
|
|
803
1106
|
return {
|
|
@@ -835,6 +1138,16 @@ var ReportCollector = class {
|
|
|
835
1138
|
this.options = options;
|
|
836
1139
|
this.maxReports = options.maxReports ?? 25;
|
|
837
1140
|
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Swap the rules, threshold, ignore list and cap for everything reported
|
|
1143
|
+
* from now on. Reports already collected are kept as they are: they were
|
|
1144
|
+
* correct under the options in force when they were made, and re-deciding
|
|
1145
|
+
* them would re-fire `onReport` for mismatches the caller already saw.
|
|
1146
|
+
*/
|
|
1147
|
+
configure(options) {
|
|
1148
|
+
this.options = options;
|
|
1149
|
+
this.maxReports = options.maxReports ?? 25;
|
|
1150
|
+
}
|
|
838
1151
|
/**
|
|
839
1152
|
* Register a sink. Pass `replay` for sinks that render *state* (the overlay)
|
|
840
1153
|
* rather than react to *events* (`onReport`, the console): they need the
|
|
@@ -857,16 +1170,20 @@ var ReportCollector = class {
|
|
|
857
1170
|
getReports() {
|
|
858
1171
|
return this.reports;
|
|
859
1172
|
}
|
|
1173
|
+
/** The cause this collector would assign — same rules, same threshold. */
|
|
1174
|
+
classify(divergence) {
|
|
1175
|
+
return classify(divergence, {
|
|
1176
|
+
extra: this.options.extra,
|
|
1177
|
+
threshold: this.options.threshold
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
860
1180
|
get isFull() {
|
|
861
1181
|
return this.reports.length >= this.maxReports;
|
|
862
1182
|
}
|
|
863
1183
|
report(divergence, context = {}) {
|
|
864
1184
|
if (this.isFull) return null;
|
|
865
1185
|
if (this.options.ignore?.(divergence)) return null;
|
|
866
|
-
const cause = classify(divergence
|
|
867
|
-
extra: this.options.extra,
|
|
868
|
-
threshold: this.options.threshold
|
|
869
|
-
});
|
|
1186
|
+
const cause = this.classify(divergence);
|
|
870
1187
|
const report = buildReport(divergence, cause, context);
|
|
871
1188
|
const signature = signatureOf(report);
|
|
872
1189
|
if (this.seen.has(signature)) return null;
|
|
@@ -916,11 +1233,19 @@ function extractComponentFromMessage(message) {
|
|
|
916
1233
|
}
|
|
917
1234
|
return names.length ? names[names.length - 1] : void 0;
|
|
918
1235
|
}
|
|
1236
|
+
function diffLines(message) {
|
|
1237
|
+
const lines = message.split("\n");
|
|
1238
|
+
let link = -1;
|
|
1239
|
+
lines.forEach((line, i) => {
|
|
1240
|
+
if (/react\.dev\/link\/hydration-mismatch/.test(line)) link = i;
|
|
1241
|
+
});
|
|
1242
|
+
return link >= 0 ? lines.slice(link + 1) : lines;
|
|
1243
|
+
}
|
|
919
1244
|
var ATTR_RE = /^([\w:-]+)=(?:"([\s\S]*)"|\{([\s\S]*)\})$/;
|
|
920
1245
|
function parseModernDiff(message) {
|
|
921
1246
|
const plus = [];
|
|
922
1247
|
const minus = [];
|
|
923
|
-
for (const raw of message
|
|
1248
|
+
for (const raw of diffLines(message)) {
|
|
924
1249
|
const line = raw.trim();
|
|
925
1250
|
const p = /^\+\s+(.+)$/.exec(line);
|
|
926
1251
|
const mn = /^-\s+(.+)$/.exec(line);
|
|
@@ -1052,7 +1377,7 @@ function parseAllHydrationDivergences(message) {
|
|
|
1052
1377
|
};
|
|
1053
1378
|
const plus = [];
|
|
1054
1379
|
const minus = [];
|
|
1055
|
-
for (const raw of message
|
|
1380
|
+
for (const raw of diffLines(message)) {
|
|
1056
1381
|
const line = raw.trim();
|
|
1057
1382
|
const p = /^\+\s+(.+)$/.exec(line);
|
|
1058
1383
|
const mn = /^-\s+(.+)$/.exec(line);
|
|
@@ -1135,8 +1460,13 @@ function serverTreeFor(root, html) {
|
|
|
1135
1460
|
serverTrees.set(root, { html, tree });
|
|
1136
1461
|
return tree;
|
|
1137
1462
|
}
|
|
1138
|
-
function reportFromMessage(message, collector, context = {}) {
|
|
1139
|
-
const
|
|
1463
|
+
function reportFromMessage(message, collector, context = {}, options = {}) {
|
|
1464
|
+
const mode = options.locationless ?? "include";
|
|
1465
|
+
const divergences = parseAllHydrationDivergences(message).filter((d) => {
|
|
1466
|
+
if (mode === "include") return true;
|
|
1467
|
+
const bare = isLocationless(d) && collector.classify(d).messageId === "unknown.no-location";
|
|
1468
|
+
return mode === "skip" ? !bare : bare;
|
|
1469
|
+
});
|
|
1140
1470
|
const component = extractComponentFromMessage(message) ?? context.component;
|
|
1141
1471
|
let reported = 0;
|
|
1142
1472
|
for (const divergence of divergences) {
|
|
@@ -1151,6 +1481,6 @@ function reportFromMessage(message, collector, context = {}) {
|
|
|
1151
1481
|
return reported;
|
|
1152
1482
|
}
|
|
1153
1483
|
|
|
1154
|
-
export { BUILT_IN_RULES, CONFIDENCE_THRESHOLD, ReportCollector, UNKNOWN_CAUSE, buildReport, classify, diffSnapshotAgainstDom, diffTrees, formatConsoleArgs, inspectRoot, isDev, isHydrationMessage, parseHydrationMessage, parseServerHtml, reportFromMessage, signatureOf };
|
|
1155
|
-
//# sourceMappingURL=chunk-
|
|
1156
|
-
//# sourceMappingURL=chunk-
|
|
1484
|
+
export { BUILT_IN_RULES, CONFIDENCE_THRESHOLD, EN_MESSAGES, ReportCollector, UNKNOWN_CAUSE, UNKNOWN_NO_LOCATION_CAUSE, buildReport, classDetail, classify, code, diffSnapshotAgainstDom, diffTrees, formatConsoleArgs, inspectRoot, isDev, isHydrationMessage, isInternalComponent, isLocationless, param, parseHydrationMessage, parseServerHtml, plainText, renderMessage, reportFromMessage, signatureOf };
|
|
1485
|
+
//# sourceMappingURL=chunk-KP4S6CRL.js.map
|
|
1486
|
+
//# sourceMappingURL=chunk-KP4S6CRL.js.map
|