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