why-hydration 0.1.4 → 0.2.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 +108 -1
- package/README.md +220 -33
- package/dist/{chunk-WQLUD25W.js → chunk-3OAI2ZHY.js} +375 -118
- package/dist/chunk-3OAI2ZHY.js.map +1 -0
- package/dist/{chunk-OGQEPU7G.cjs → chunk-DXPTZB3Z.cjs} +44 -2
- package/dist/chunk-DXPTZB3Z.cjs.map +1 -0
- package/dist/{chunk-DSO337ME.cjs → chunk-KX6G7I4Y.cjs} +384 -118
- package/dist/chunk-KX6G7I4Y.cjs.map +1 -0
- package/dist/chunk-MK3TYHKM.js +966 -0
- package/dist/chunk-MK3TYHKM.js.map +1 -0
- package/dist/chunk-R7IWOK5M.cjs +989 -0
- package/dist/chunk-R7IWOK5M.cjs.map +1 -0
- package/dist/{chunk-FV3PEJQE.js → chunk-XIM33ZGB.js} +44 -2
- package/dist/chunk-XIM33ZGB.js.map +1 -0
- package/dist/index.cjs +32 -24
- package/dist/index.d.cts +43 -5
- package/dist/index.d.ts +43 -5
- package/dist/index.js +2 -2
- package/dist/next/index.cjs +5 -5
- package/dist/next/index.d.cts +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +3 -3
- package/dist/next/script.cjs +2 -2
- package/dist/next/script.js +1 -1
- package/dist/react.cjs +5 -5
- package/dist/react.d.cts +23 -3
- package/dist/react.d.ts +23 -3
- package/dist/react.js +3 -3
- 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 +1 -1
- package/dist/chunk-3KULWJ7A.js +0 -569
- package/dist/chunk-3KULWJ7A.js.map +0 -1
- package/dist/chunk-5CBGYE7A.cjs +0 -592
- package/dist/chunk-5CBGYE7A.cjs.map +0 -1
- package/dist/chunk-DSO337ME.cjs.map +0 -1
- package/dist/chunk-FV3PEJQE.js.map +0 -1
- package/dist/chunk-OGQEPU7G.cjs.map +0 -1
- package/dist/chunk-WQLUD25W.js.map +0 -1
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { getServerHtmlForRoot } from './chunk-
|
|
1
|
+
import { getServerHtmlForRoot } from './chunk-XIM33ZGB.js';
|
|
2
2
|
|
|
3
3
|
// src/core/env.ts
|
|
4
|
-
|
|
4
|
+
function readNodeEnv() {
|
|
5
|
+
try {
|
|
6
|
+
return process.env.NODE_ENV;
|
|
7
|
+
} catch {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var isDev = readNodeEnv() !== "production";
|
|
5
12
|
|
|
6
13
|
// src/core/diff.ts
|
|
7
14
|
var IGNORED_ATTRIBUTES = /* @__PURE__ */ new Set(["data-reactroot"]);
|
|
8
15
|
function parseServerHtml(html, rootTagName) {
|
|
9
|
-
const
|
|
16
|
+
const doc = document.implementation.createHTMLDocument("");
|
|
17
|
+
const container = doc.createElement(rootTagName || "div");
|
|
10
18
|
container.innerHTML = html;
|
|
11
19
|
return container;
|
|
12
20
|
}
|
|
@@ -157,21 +165,21 @@ function diffAttributes(serverEl, clientEl, path, parentTag) {
|
|
|
157
165
|
}
|
|
158
166
|
return null;
|
|
159
167
|
}
|
|
160
|
-
function normalizeAttr(name,
|
|
161
|
-
if (
|
|
168
|
+
function normalizeAttr(name, value2) {
|
|
169
|
+
if (value2 == null) return null;
|
|
162
170
|
if (name === "class") {
|
|
163
|
-
return
|
|
171
|
+
return value2.trim().split(/\s+/).filter(Boolean).sort().join(" ");
|
|
164
172
|
}
|
|
165
173
|
if (name === "style") {
|
|
166
|
-
return normalizeStyle(
|
|
174
|
+
return normalizeStyle(value2);
|
|
167
175
|
}
|
|
168
|
-
return
|
|
176
|
+
return value2;
|
|
169
177
|
}
|
|
170
|
-
function normalizeStyle(
|
|
178
|
+
function normalizeStyle(value2) {
|
|
171
179
|
if (typeof document !== "undefined") {
|
|
172
180
|
try {
|
|
173
181
|
const el = document.createElement("div");
|
|
174
|
-
el.style.cssText =
|
|
182
|
+
el.style.cssText = value2;
|
|
175
183
|
const decls = [];
|
|
176
184
|
for (let i = 0; i < el.style.length; i++) {
|
|
177
185
|
const prop = el.style.item(i);
|
|
@@ -181,7 +189,7 @@ function normalizeStyle(value) {
|
|
|
181
189
|
} catch {
|
|
182
190
|
}
|
|
183
191
|
}
|
|
184
|
-
return
|
|
192
|
+
return value2.split(";").map((s) => s.trim()).filter(Boolean).sort().join(";");
|
|
185
193
|
}
|
|
186
194
|
function nodeKey(node) {
|
|
187
195
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -351,8 +359,26 @@ var RANDOM_DECIMAL_RE = /^0?\.\d{6,}$/;
|
|
|
351
359
|
var NANOID_RE = /^[A-Za-z0-9_-]{10,}$/;
|
|
352
360
|
var ARABIC_INDIC_DIGITS = /[٠-٩۰-۹]/;
|
|
353
361
|
var LATIN_DIGITS = /[0-9]/;
|
|
354
|
-
|
|
355
|
-
|
|
362
|
+
var BIDI_CONTROLS = /[\u200e\u200f\u061c\u2066-\u2069]/g;
|
|
363
|
+
function stripBidiControls(value2) {
|
|
364
|
+
return value2.replace(BIDI_CONTROLS, "");
|
|
365
|
+
}
|
|
366
|
+
function differsOnlyByBidiControls(a, b) {
|
|
367
|
+
const sa = stripBidiControls(a);
|
|
368
|
+
const sb = stripBidiControls(b);
|
|
369
|
+
return a !== b && sa === sb && sa.trim() !== "";
|
|
370
|
+
}
|
|
371
|
+
function normalizeNumerals(value2) {
|
|
372
|
+
return stripBidiControls(value2).replace(/[٠-٩۰-۹٫٬]/g, (ch) => {
|
|
373
|
+
if (ch === "\u066B") return ".";
|
|
374
|
+
if (ch === "\u066C") return ",";
|
|
375
|
+
const code2 = ch.charCodeAt(0);
|
|
376
|
+
const zero = code2 >= 1776 ? 1776 : 1632;
|
|
377
|
+
return String(code2 - zero);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function isRandomLike(value2) {
|
|
381
|
+
const v = value2.trim();
|
|
356
382
|
if (!v) return false;
|
|
357
383
|
if (UUID_RE.test(v)) return true;
|
|
358
384
|
if (REACT_ID_RE.test(v)) return true;
|
|
@@ -361,16 +387,26 @@ function isRandomLike(value) {
|
|
|
361
387
|
if (NANOID_RE.test(v) && /[A-Za-z]/.test(v) && /[0-9]/.test(v)) return true;
|
|
362
388
|
return false;
|
|
363
389
|
}
|
|
364
|
-
function looksLikeTime(
|
|
365
|
-
return /\b\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp][Mm])?\b/.test(
|
|
390
|
+
function looksLikeTime(value2) {
|
|
391
|
+
return /\b\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp][Mm])?\b/.test(
|
|
392
|
+
normalizeNumerals(value2).trim()
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
var ISO_DATE = /^\d{4}-\d{1,2}(?:-\d{1,2})?(?:[T ]\d{1,2}:\d{2}(?::\d{2})?)?/;
|
|
396
|
+
var NUMERIC_DATE = /^\d{1,4}[/.-]\d{1,2}[/.-]\d{1,4}$/;
|
|
397
|
+
var TIME_OF_DAY = /^\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp]\.?[Mm]\.?)?$/;
|
|
398
|
+
var MONTH_NAME = /\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b/i;
|
|
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);
|
|
366
401
|
}
|
|
367
|
-
function toTimestamp(
|
|
368
|
-
const v =
|
|
402
|
+
function toTimestamp(value2) {
|
|
403
|
+
const v = value2.trim();
|
|
369
404
|
if (!v) return null;
|
|
370
405
|
if (/^\d{10,13}$/.test(v)) {
|
|
371
406
|
const n = Number(v);
|
|
372
407
|
return v.length === 10 ? n * 1e3 : n;
|
|
373
408
|
}
|
|
409
|
+
if (!looksLikeDate(v)) return null;
|
|
374
410
|
const parsed = Date.parse(v);
|
|
375
411
|
if (!Number.isNaN(parsed)) return parsed;
|
|
376
412
|
if (looksLikeTime(v)) {
|
|
@@ -379,23 +415,23 @@ function toTimestamp(value) {
|
|
|
379
415
|
}
|
|
380
416
|
return null;
|
|
381
417
|
}
|
|
382
|
-
function hasArabicIndicDigits(
|
|
383
|
-
return ARABIC_INDIC_DIGITS.test(
|
|
418
|
+
function hasArabicIndicDigits(value2) {
|
|
419
|
+
return ARABIC_INDIC_DIGITS.test(value2);
|
|
384
420
|
}
|
|
385
|
-
function hasLatinDigits(
|
|
386
|
-
return LATIN_DIGITS.test(
|
|
421
|
+
function hasLatinDigits(value2) {
|
|
422
|
+
return LATIN_DIGITS.test(value2);
|
|
387
423
|
}
|
|
388
424
|
var NUMERIC_LIKE = /^[+-]?[\d.,\s\u00a0\u2009]+$/;
|
|
389
425
|
function isSameNumberDifferentSeparators(a, b) {
|
|
390
|
-
const at = a.trim();
|
|
391
|
-
const bt = b.trim();
|
|
426
|
+
const at = normalizeNumerals(a).trim();
|
|
427
|
+
const bt = normalizeNumerals(b).trim();
|
|
392
428
|
if (!NUMERIC_LIKE.test(at) || !NUMERIC_LIKE.test(bt)) return false;
|
|
393
429
|
const digitsOnly = (s) => s.replace(/\D/g, "");
|
|
394
430
|
const da = digitsOnly(at);
|
|
395
431
|
const db = digitsOnly(bt);
|
|
396
432
|
if (!da || da !== db) return false;
|
|
397
433
|
const hasSep = (s) => /[.,\s\u00a0\u2009]/.test(s);
|
|
398
|
-
return (hasSep(at) || hasSep(bt)) &&
|
|
434
|
+
return (hasSep(at) || hasSep(bt)) && a.trim() !== b.trim();
|
|
399
435
|
}
|
|
400
436
|
var CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
401
437
|
"value",
|
|
@@ -419,9 +455,10 @@ function looksLikeThirdPartyNode(html, tagName) {
|
|
|
419
455
|
if (!h) return false;
|
|
420
456
|
return THIRD_PARTY_MARKERS.test(h) || h.includes("about:blank");
|
|
421
457
|
}
|
|
458
|
+
var DATE_PARTS_RE = /^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/;
|
|
422
459
|
function isSameDateDifferentOrder(a, b) {
|
|
423
|
-
const partsA = a.trim().match(
|
|
424
|
-
const partsB = b.trim().match(
|
|
460
|
+
const partsA = normalizeNumerals(a).trim().match(DATE_PARTS_RE);
|
|
461
|
+
const partsB = normalizeNumerals(b).trim().match(DATE_PARTS_RE);
|
|
425
462
|
if (!partsA || !partsB) return false;
|
|
426
463
|
const setA = [partsA[1], partsA[2], partsA[3]].sort().join("|");
|
|
427
464
|
const setB = [partsB[1], partsB[2], partsB[3]].sort().join("|");
|
|
@@ -487,6 +524,163 @@ function messageIndicatesInvalidNesting(message) {
|
|
|
487
524
|
return /validateDOMNesting/i.test(message) || /cannot (?:be a|contain).*(?:descendant|child)/i.test(message) || /cannot appear as a (?:child|descendant)/i.test(message);
|
|
488
525
|
}
|
|
489
526
|
|
|
527
|
+
// src/core/classify/messages.ts
|
|
528
|
+
var code = (text) => ({ code: text });
|
|
529
|
+
var value = (text) => ({ value: text });
|
|
530
|
+
function param(params, name) {
|
|
531
|
+
const v = params[name];
|
|
532
|
+
return typeof v === "string" ? v : "";
|
|
533
|
+
}
|
|
534
|
+
function paramList(params, name) {
|
|
535
|
+
const v = params[name];
|
|
536
|
+
return Array.isArray(v) ? v : [];
|
|
537
|
+
}
|
|
538
|
+
function valueList(items, separator) {
|
|
539
|
+
const out = [];
|
|
540
|
+
items.forEach((item, i) => {
|
|
541
|
+
if (i > 0) out.push(separator);
|
|
542
|
+
out.push(value(item));
|
|
543
|
+
});
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
function renderMessage(template, params = {}) {
|
|
547
|
+
if (typeof template === "function") return template(params);
|
|
548
|
+
const out = [];
|
|
549
|
+
template.split("`").forEach((part, i) => {
|
|
550
|
+
if (i % 2 === 1) {
|
|
551
|
+
out.push(
|
|
552
|
+
code(part.replace(/\{(\w+)\}/g, (_, n) => param(params, n)))
|
|
553
|
+
);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
let last = 0;
|
|
557
|
+
for (const m of part.matchAll(/\{(\w+)\}/g)) {
|
|
558
|
+
const at = m.index ?? 0;
|
|
559
|
+
if (at > last) out.push(part.slice(last, at));
|
|
560
|
+
out.push(value(param(params, m[1])));
|
|
561
|
+
last = at + m[0].length;
|
|
562
|
+
}
|
|
563
|
+
if (last < part.length) out.push(part.slice(last));
|
|
564
|
+
});
|
|
565
|
+
return out.filter((s) => s !== "");
|
|
566
|
+
}
|
|
567
|
+
function plainText(segments) {
|
|
568
|
+
return segments.map(
|
|
569
|
+
(s) => typeof s === "string" ? s : "code" in s ? `\`${s.code}\`` : s.value
|
|
570
|
+
).join("");
|
|
571
|
+
}
|
|
572
|
+
function classDetail(params, words) {
|
|
573
|
+
const added = paramList(params, "added");
|
|
574
|
+
const removed = paramList(params, "removed");
|
|
575
|
+
if (!added.length && !removed.length) return [];
|
|
576
|
+
const out = [words.open];
|
|
577
|
+
if (added.length) {
|
|
578
|
+
out.push(words.added, ...valueList(added, words.listSeparator));
|
|
579
|
+
}
|
|
580
|
+
if (removed.length) {
|
|
581
|
+
if (added.length) out.push(words.partSeparator);
|
|
582
|
+
out.push(words.removed, ...valueList(removed, words.listSeparator));
|
|
583
|
+
}
|
|
584
|
+
out.push(words.close);
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
var EN_MESSAGES = {
|
|
588
|
+
"non-deterministic-value": {
|
|
589
|
+
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.",
|
|
590
|
+
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."
|
|
591
|
+
},
|
|
592
|
+
"date-time": {
|
|
593
|
+
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.",
|
|
594
|
+
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."
|
|
595
|
+
},
|
|
596
|
+
"locale-format.bidi": {
|
|
597
|
+
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.",
|
|
598
|
+
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."
|
|
599
|
+
},
|
|
600
|
+
"locale-format.script": {
|
|
601
|
+
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.",
|
|
602
|
+
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."
|
|
603
|
+
},
|
|
604
|
+
"locale-format.separators": {
|
|
605
|
+
explanation: "The same number was formatted with different grouping/decimal separators between server and client (e.g. 1,234.56 vs 1.234,56).",
|
|
606
|
+
suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match."
|
|
607
|
+
},
|
|
608
|
+
"locale-format.date-order": {
|
|
609
|
+
explanation: "The same date was rendered in a different field order (MM/DD vs DD/MM) between server and client.",
|
|
610
|
+
suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides."
|
|
611
|
+
},
|
|
612
|
+
"browser-only-api": {
|
|
613
|
+
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.",
|
|
614
|
+
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."
|
|
615
|
+
},
|
|
616
|
+
"viewport-branching": {
|
|
617
|
+
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.",
|
|
618
|
+
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."
|
|
619
|
+
},
|
|
620
|
+
"invalid-html-nesting": {
|
|
621
|
+
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.",
|
|
622
|
+
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."
|
|
623
|
+
},
|
|
624
|
+
"whitespace-minification": {
|
|
625
|
+
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.",
|
|
626
|
+
suggestion: "Check your HTML minifier settings (e.g. `conservativeCollapse`) around the app root, or avoid minifying whitespace inside hydrated markup."
|
|
627
|
+
},
|
|
628
|
+
"third-party-dom-mutation.extension-attribute": {
|
|
629
|
+
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.",
|
|
630
|
+
suggestion: "This is usually harmless. Add `suppressHydrationWarning` to the affected element, or defer third-party script init until after hydration."
|
|
631
|
+
},
|
|
632
|
+
"third-party-dom-mutation.root-attribute": {
|
|
633
|
+
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.",
|
|
634
|
+
suggestion: "Add `suppressHydrationWarning` to the root element, or defer the third-party script until after hydration."
|
|
635
|
+
},
|
|
636
|
+
"third-party-dom-mutation.injected-node": {
|
|
637
|
+
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.",
|
|
638
|
+
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">`).'
|
|
639
|
+
},
|
|
640
|
+
"attribute-mismatch.class": {
|
|
641
|
+
explanation: (p) => [
|
|
642
|
+
"The ",
|
|
643
|
+
code("class"),
|
|
644
|
+
" differs between server and client",
|
|
645
|
+
...classDetail(p, {
|
|
646
|
+
added: "added on client: ",
|
|
647
|
+
removed: "removed on client: ",
|
|
648
|
+
listSeparator: ", ",
|
|
649
|
+
partSeparator: "; ",
|
|
650
|
+
open: " (",
|
|
651
|
+
close: ")"
|
|
652
|
+
}),
|
|
653
|
+
". 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."
|
|
654
|
+
],
|
|
655
|
+
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."
|
|
656
|
+
},
|
|
657
|
+
"attribute-mismatch.style": {
|
|
658
|
+
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.",
|
|
659
|
+
suggestion: "Compute the style after mount (`useEffect`) so the first client render matches the server, or move it to a CSS class / media query."
|
|
660
|
+
},
|
|
661
|
+
"attribute-mismatch.generic": {
|
|
662
|
+
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.",
|
|
663
|
+
suggestion: "Make the attribute deterministic across server and client, or set it after mount so the first client render matches the server HTML."
|
|
664
|
+
},
|
|
665
|
+
unknown: {
|
|
666
|
+
explanation: "A hydration mismatch was detected but could not be matched to a known cause. Inspect the server vs client values above.",
|
|
667
|
+
suggestion: "Compare the server and client values. Common causes are non-deterministic values, dates/locales, and browser-only APIs used during render."
|
|
668
|
+
},
|
|
669
|
+
"unknown.no-location": {
|
|
670
|
+
explanation: "React reported that hydration failed but did not say which node differed, and the DOM diff found no difference to point at.",
|
|
671
|
+
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."
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
function describe(id, params = {}, catalog = EN_MESSAGES) {
|
|
675
|
+
const entry = catalog[id];
|
|
676
|
+
return {
|
|
677
|
+
messageId: id,
|
|
678
|
+
...Object.keys(params).length ? { params } : {},
|
|
679
|
+
explanation: plainText(renderMessage(entry.explanation, params)),
|
|
680
|
+
suggestion: plainText(renderMessage(entry.suggestion, params))
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
490
684
|
// src/core/classify/rules.ts
|
|
491
685
|
var DOCS_BASE = "https://github.com/razan-aboushi/why-hydration#cause-";
|
|
492
686
|
function docs(category) {
|
|
@@ -509,8 +703,7 @@ var nonDeterministic = (d) => {
|
|
|
509
703
|
return {
|
|
510
704
|
category: "non-deterministic-value",
|
|
511
705
|
confidence: 0.9,
|
|
512
|
-
|
|
513
|
-
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.",
|
|
706
|
+
...describe("non-deterministic-value"),
|
|
514
707
|
docsUrl: docs("non-deterministic-value")
|
|
515
708
|
};
|
|
516
709
|
};
|
|
@@ -528,8 +721,7 @@ var dateTime = (d) => {
|
|
|
528
721
|
return {
|
|
529
722
|
category: "date-time",
|
|
530
723
|
confidence: smallDelta || bothTimes ? 0.85 : 0.7,
|
|
531
|
-
|
|
532
|
-
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.",
|
|
724
|
+
...describe("date-time"),
|
|
533
725
|
docsUrl: docs("date-time")
|
|
534
726
|
};
|
|
535
727
|
};
|
|
@@ -537,13 +729,20 @@ var localeFormat = (d) => {
|
|
|
537
729
|
if (!isValueDivergence(d)) return null;
|
|
538
730
|
if (!bothDiffer(d)) return null;
|
|
539
731
|
const { server, client } = d;
|
|
732
|
+
if (differsOnlyByBidiControls(server, client)) {
|
|
733
|
+
return {
|
|
734
|
+
category: "locale-format",
|
|
735
|
+
confidence: 0.88,
|
|
736
|
+
...describe("locale-format.bidi"),
|
|
737
|
+
docsUrl: docs("locale-format")
|
|
738
|
+
};
|
|
739
|
+
}
|
|
540
740
|
const scriptMismatch = hasArabicIndicDigits(server) && hasLatinDigits(client) || hasLatinDigits(server) && hasArabicIndicDigits(client);
|
|
541
741
|
if (scriptMismatch) {
|
|
542
742
|
return {
|
|
543
743
|
category: "locale-format",
|
|
544
744
|
confidence: 0.92,
|
|
545
|
-
|
|
546
|
-
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.",
|
|
745
|
+
...describe("locale-format.script"),
|
|
547
746
|
docsUrl: docs("locale-format")
|
|
548
747
|
};
|
|
549
748
|
}
|
|
@@ -551,8 +750,7 @@ var localeFormat = (d) => {
|
|
|
551
750
|
return {
|
|
552
751
|
category: "locale-format",
|
|
553
752
|
confidence: 0.82,
|
|
554
|
-
|
|
555
|
-
suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match.",
|
|
753
|
+
...describe("locale-format.separators"),
|
|
556
754
|
docsUrl: docs("locale-format")
|
|
557
755
|
};
|
|
558
756
|
}
|
|
@@ -560,8 +758,7 @@ var localeFormat = (d) => {
|
|
|
560
758
|
return {
|
|
561
759
|
category: "locale-format",
|
|
562
760
|
confidence: 0.75,
|
|
563
|
-
|
|
564
|
-
suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides.",
|
|
761
|
+
...describe("locale-format.date-order"),
|
|
565
762
|
docsUrl: docs("locale-format")
|
|
566
763
|
};
|
|
567
764
|
}
|
|
@@ -575,8 +772,7 @@ var browserOnlyApi = (d) => {
|
|
|
575
772
|
return {
|
|
576
773
|
category: "browser-only-api",
|
|
577
774
|
confidence: 0.75,
|
|
578
|
-
|
|
579
|
-
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.",
|
|
775
|
+
...describe("browser-only-api"),
|
|
580
776
|
docsUrl: docs("browser-only-api")
|
|
581
777
|
};
|
|
582
778
|
};
|
|
@@ -586,11 +782,11 @@ var viewportBranching = (d) => {
|
|
|
586
782
|
if (isInvalidNesting(d.parentTagName, d.tagName) || messageIndicatesInvalidNesting(d.reactMessage)) {
|
|
587
783
|
return null;
|
|
588
784
|
}
|
|
785
|
+
if (d.server == null && d.client == null && !d.tagName) return null;
|
|
589
786
|
return {
|
|
590
787
|
category: "viewport-branching",
|
|
591
788
|
confidence: 0.6,
|
|
592
|
-
|
|
593
|
-
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.",
|
|
789
|
+
...describe("viewport-branching"),
|
|
594
790
|
docsUrl: docs("viewport-branching")
|
|
595
791
|
};
|
|
596
792
|
};
|
|
@@ -601,8 +797,7 @@ var invalidNesting = (d) => {
|
|
|
601
797
|
return {
|
|
602
798
|
category: "invalid-html-nesting",
|
|
603
799
|
confidence: byMessage ? 0.9 : 0.72,
|
|
604
|
-
|
|
605
|
-
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.",
|
|
800
|
+
...describe("invalid-html-nesting"),
|
|
606
801
|
docsUrl: docs("invalid-html-nesting")
|
|
607
802
|
};
|
|
608
803
|
};
|
|
@@ -615,8 +810,7 @@ var whitespaceMinification = (d) => {
|
|
|
615
810
|
return {
|
|
616
811
|
category: "whitespace-minification",
|
|
617
812
|
confidence: 0.7,
|
|
618
|
-
|
|
619
|
-
suggestion: "Check your HTML minifier settings (e.g. `conservativeCollapse`) around the app root, or avoid minifying whitespace inside hydrated markup.",
|
|
813
|
+
...describe("whitespace-minification"),
|
|
620
814
|
docsUrl: docs("whitespace-minification")
|
|
621
815
|
};
|
|
622
816
|
};
|
|
@@ -626,18 +820,21 @@ var thirdPartyDomMutation = (d) => {
|
|
|
626
820
|
return {
|
|
627
821
|
category: "third-party-dom-mutation",
|
|
628
822
|
confidence: 0.88,
|
|
629
|
-
|
|
630
|
-
|
|
823
|
+
...describe("third-party-dom-mutation.extension-attribute", {
|
|
824
|
+
attribute: d.attribute
|
|
825
|
+
}),
|
|
631
826
|
docsUrl: docs("third-party-dom-mutation")
|
|
632
827
|
};
|
|
633
828
|
}
|
|
634
|
-
const
|
|
829
|
+
const el = d.element;
|
|
830
|
+
const atRoot = d.server == null && el != null && /^(?:HTML|BODY)$/.test(el.tagName);
|
|
635
831
|
if (atRoot) {
|
|
636
832
|
return {
|
|
637
833
|
category: "third-party-dom-mutation",
|
|
638
834
|
confidence: 0.6,
|
|
639
|
-
|
|
640
|
-
|
|
835
|
+
...describe("third-party-dom-mutation.root-attribute", {
|
|
836
|
+
attribute: d.attribute
|
|
837
|
+
}),
|
|
641
838
|
docsUrl: docs("third-party-dom-mutation")
|
|
642
839
|
};
|
|
643
840
|
}
|
|
@@ -647,8 +844,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
647
844
|
return {
|
|
648
845
|
category: "third-party-dom-mutation",
|
|
649
846
|
confidence: 0.7,
|
|
650
|
-
|
|
651
|
-
|
|
847
|
+
...describe("third-party-dom-mutation.injected-node", {
|
|
848
|
+
tag: `<${tag}>`
|
|
849
|
+
}),
|
|
652
850
|
docsUrl: docs("third-party-dom-mutation")
|
|
653
851
|
};
|
|
654
852
|
}
|
|
@@ -664,15 +862,10 @@ var attributeMismatch = (d) => {
|
|
|
664
862
|
const clientSet = new Set(client.split(/\s+/).filter(Boolean));
|
|
665
863
|
const added = [...clientSet].filter((c) => !serverSet.has(c));
|
|
666
864
|
const removed = [...serverSet].filter((c) => !clientSet.has(c));
|
|
667
|
-
const parts = [];
|
|
668
|
-
if (added.length) parts.push(`added on client: ${added.join(", ")}`);
|
|
669
|
-
if (removed.length) parts.push(`removed on client: ${removed.join(", ")}`);
|
|
670
|
-
const detail = parts.length ? ` (${parts.join("; ")})` : "";
|
|
671
865
|
return {
|
|
672
866
|
category: "attribute-mismatch",
|
|
673
867
|
confidence: 0.8,
|
|
674
|
-
|
|
675
|
-
suggestion: "Render the same className on the server and the first client paint. Move client-only conditions into `useEffect`/a mounted flag, or drive the visual change with CSS media queries instead of a JS class toggle.",
|
|
868
|
+
...describe("attribute-mismatch.class", { added, removed }),
|
|
676
869
|
docsUrl: docs("attribute-mismatch")
|
|
677
870
|
};
|
|
678
871
|
}
|
|
@@ -680,16 +873,18 @@ var attributeMismatch = (d) => {
|
|
|
680
873
|
return {
|
|
681
874
|
category: "attribute-mismatch",
|
|
682
875
|
confidence: 0.75,
|
|
683
|
-
|
|
684
|
-
suggestion: "Compute the style after mount (`useEffect`) so the first client render matches the server, or move it to a CSS class / media query.",
|
|
876
|
+
...describe("attribute-mismatch.style"),
|
|
685
877
|
docsUrl: docs("attribute-mismatch")
|
|
686
878
|
};
|
|
687
879
|
}
|
|
688
880
|
return {
|
|
689
881
|
category: "attribute-mismatch",
|
|
690
882
|
confidence: 0.6,
|
|
691
|
-
|
|
692
|
-
|
|
883
|
+
...describe("attribute-mismatch.generic", {
|
|
884
|
+
attribute: d.attribute,
|
|
885
|
+
server,
|
|
886
|
+
client
|
|
887
|
+
}),
|
|
693
888
|
docsUrl: docs("attribute-mismatch")
|
|
694
889
|
};
|
|
695
890
|
};
|
|
@@ -711,13 +906,21 @@ var BUILT_IN_RULES = [
|
|
|
711
906
|
var UNKNOWN_CAUSE = {
|
|
712
907
|
category: "unknown",
|
|
713
908
|
confidence: 0,
|
|
714
|
-
|
|
715
|
-
|
|
909
|
+
...describe("unknown"),
|
|
910
|
+
docsUrl: docs("unknown")
|
|
911
|
+
};
|
|
912
|
+
var UNKNOWN_NO_LOCATION_CAUSE = {
|
|
913
|
+
category: "unknown",
|
|
914
|
+
confidence: 0,
|
|
915
|
+
...describe("unknown.no-location"),
|
|
716
916
|
docsUrl: docs("unknown")
|
|
717
917
|
};
|
|
718
918
|
|
|
719
919
|
// src/core/classify/index.ts
|
|
720
920
|
var CONFIDENCE_THRESHOLD = 0.5;
|
|
921
|
+
function isLocationless(d) {
|
|
922
|
+
return d.server == null && d.client == null && !d.tagName && !d.attribute;
|
|
923
|
+
}
|
|
721
924
|
function classify(divergence, options = {}) {
|
|
722
925
|
const threshold = options.threshold ?? CONFIDENCE_THRESHOLD;
|
|
723
926
|
const rules = [
|
|
@@ -735,7 +938,7 @@ function classify(divergence, options = {}) {
|
|
|
735
938
|
return result;
|
|
736
939
|
}
|
|
737
940
|
}
|
|
738
|
-
return UNKNOWN_CAUSE;
|
|
941
|
+
return isLocationless(divergence) ? UNKNOWN_NO_LOCATION_CAUSE : UNKNOWN_CAUSE;
|
|
739
942
|
}
|
|
740
943
|
|
|
741
944
|
// src/core/report.ts
|
|
@@ -744,9 +947,12 @@ function nextId() {
|
|
|
744
947
|
counter += 1;
|
|
745
948
|
return `wh_${Date.now().toString(36)}_${counter}`;
|
|
746
949
|
}
|
|
747
|
-
function truncate(
|
|
748
|
-
if (
|
|
749
|
-
|
|
950
|
+
function truncate(value2, max = 300) {
|
|
951
|
+
if (value2 == null) return null;
|
|
952
|
+
if (value2.length <= max) return value2;
|
|
953
|
+
const last = value2.charCodeAt(max - 1);
|
|
954
|
+
const end = last >= 55296 && last <= 56319 ? max - 1 : max;
|
|
955
|
+
return `${value2.slice(0, end)}\u2026`;
|
|
750
956
|
}
|
|
751
957
|
function buildReport(divergence, cause, context = {}) {
|
|
752
958
|
return {
|
|
@@ -784,34 +990,48 @@ var ReportCollector = class {
|
|
|
784
990
|
this.options = options;
|
|
785
991
|
this.maxReports = options.maxReports ?? 25;
|
|
786
992
|
}
|
|
787
|
-
|
|
993
|
+
/**
|
|
994
|
+
* Register a sink. Pass `replay` for sinks that render *state* (the overlay)
|
|
995
|
+
* rather than react to *events* (`onReport`, the console): they need the
|
|
996
|
+
* reports collected before they were attached, which matters when a sink is
|
|
997
|
+
* re-attached after a stop/start cycle.
|
|
998
|
+
*/
|
|
999
|
+
addSink(sink, options = {}) {
|
|
788
1000
|
this.sinks.add(sink);
|
|
1001
|
+
if (options.replay) {
|
|
1002
|
+
for (const report of this.reports) this.emit(sink, report);
|
|
1003
|
+
}
|
|
789
1004
|
return () => this.sinks.delete(sink);
|
|
790
1005
|
}
|
|
1006
|
+
emit(sink, report) {
|
|
1007
|
+
try {
|
|
1008
|
+
sink(report);
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
791
1012
|
getReports() {
|
|
792
1013
|
return this.reports;
|
|
793
1014
|
}
|
|
1015
|
+
/** The cause this collector would assign — same rules, same threshold. */
|
|
1016
|
+
classify(divergence) {
|
|
1017
|
+
return classify(divergence, {
|
|
1018
|
+
extra: this.options.extra,
|
|
1019
|
+
threshold: this.options.threshold
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
794
1022
|
get isFull() {
|
|
795
1023
|
return this.reports.length >= this.maxReports;
|
|
796
1024
|
}
|
|
797
1025
|
report(divergence, context = {}) {
|
|
798
1026
|
if (this.isFull) return null;
|
|
799
1027
|
if (this.options.ignore?.(divergence)) return null;
|
|
800
|
-
const cause = classify(divergence
|
|
801
|
-
extra: this.options.extra,
|
|
802
|
-
threshold: this.options.threshold
|
|
803
|
-
});
|
|
1028
|
+
const cause = this.classify(divergence);
|
|
804
1029
|
const report = buildReport(divergence, cause, context);
|
|
805
1030
|
const signature = signatureOf(report);
|
|
806
1031
|
if (this.seen.has(signature)) return null;
|
|
807
1032
|
this.seen.add(signature);
|
|
808
1033
|
this.reports.push(report);
|
|
809
|
-
for (const sink of this.sinks)
|
|
810
|
-
try {
|
|
811
|
-
sink(report);
|
|
812
|
-
} catch {
|
|
813
|
-
}
|
|
814
|
-
}
|
|
1034
|
+
for (const sink of this.sinks) this.emit(sink, report);
|
|
815
1035
|
return report;
|
|
816
1036
|
}
|
|
817
1037
|
reset() {
|
|
@@ -855,11 +1075,19 @@ function extractComponentFromMessage(message) {
|
|
|
855
1075
|
}
|
|
856
1076
|
return names.length ? names[names.length - 1] : void 0;
|
|
857
1077
|
}
|
|
1078
|
+
function diffLines(message) {
|
|
1079
|
+
const lines = message.split("\n");
|
|
1080
|
+
let link = -1;
|
|
1081
|
+
lines.forEach((line, i) => {
|
|
1082
|
+
if (/react\.dev\/link\/hydration-mismatch/.test(line)) link = i;
|
|
1083
|
+
});
|
|
1084
|
+
return link >= 0 ? lines.slice(link + 1) : lines;
|
|
1085
|
+
}
|
|
858
1086
|
var ATTR_RE = /^([\w:-]+)=(?:"([\s\S]*)"|\{([\s\S]*)\})$/;
|
|
859
1087
|
function parseModernDiff(message) {
|
|
860
1088
|
const plus = [];
|
|
861
1089
|
const minus = [];
|
|
862
|
-
for (const raw of message
|
|
1090
|
+
for (const raw of diffLines(message)) {
|
|
863
1091
|
const line = raw.trim();
|
|
864
1092
|
const p = /^\+\s+(.+)$/.exec(line);
|
|
865
1093
|
const mn = /^-\s+(.+)$/.exec(line);
|
|
@@ -985,45 +1213,47 @@ function parseHydrationMessage(message) {
|
|
|
985
1213
|
}
|
|
986
1214
|
function parseAllHydrationDivergences(message) {
|
|
987
1215
|
if (!isHydrationMessage(message)) return [];
|
|
1216
|
+
const fallback = () => {
|
|
1217
|
+
const single = parseHydrationMessage(message);
|
|
1218
|
+
return single ? [single] : [];
|
|
1219
|
+
};
|
|
988
1220
|
const plus = [];
|
|
989
1221
|
const minus = [];
|
|
990
|
-
for (const raw of message
|
|
1222
|
+
for (const raw of diffLines(message)) {
|
|
991
1223
|
const line = raw.trim();
|
|
992
1224
|
const p = /^\+\s+(.+)$/.exec(line);
|
|
993
1225
|
const mn = /^-\s+(.+)$/.exec(line);
|
|
994
1226
|
if (p && p[1]) plus.push(p[1].trim());
|
|
995
1227
|
else if (mn && mn[1]) minus.push(mn[1].trim());
|
|
996
1228
|
}
|
|
997
|
-
if (plus.length === 0 && minus.length === 0)
|
|
998
|
-
const single = parseHydrationMessage(message);
|
|
999
|
-
return single ? [single] : [];
|
|
1000
|
-
}
|
|
1229
|
+
if (plus.length === 0 && minus.length === 0) return fallback();
|
|
1001
1230
|
const out = [];
|
|
1231
|
+
const attrPlus = plus.filter((p) => ATTR_RE.test(p));
|
|
1002
1232
|
const attrMinus = minus.filter((m) => ATTR_RE.test(m));
|
|
1003
1233
|
const usedMinus = /* @__PURE__ */ new Set();
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
server: parsed ? parsed[2] ?? parsed[3] ?? "" : "",
|
|
1022
|
-
client: clientValue,
|
|
1023
|
-
reactMessage: message
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1234
|
+
const attribute = (name, server, client) => ({
|
|
1235
|
+
kind: "attribute",
|
|
1236
|
+
path: "body",
|
|
1237
|
+
attribute: name,
|
|
1238
|
+
server,
|
|
1239
|
+
client,
|
|
1240
|
+
reactMessage: message
|
|
1241
|
+
});
|
|
1242
|
+
for (const line of attrPlus) {
|
|
1243
|
+
const name = attrName(line);
|
|
1244
|
+
if (!name) continue;
|
|
1245
|
+
const idx = attrMinus.findIndex(
|
|
1246
|
+
(m, k) => !usedMinus.has(k) && attrName(m) === name
|
|
1247
|
+
);
|
|
1248
|
+
if (idx >= 0) usedMinus.add(idx);
|
|
1249
|
+
const server = idx >= 0 ? attrValue(attrMinus[idx]) : null;
|
|
1250
|
+
out.push(attribute(name, server, attrValue(line)));
|
|
1026
1251
|
}
|
|
1252
|
+
attrMinus.forEach((line, k) => {
|
|
1253
|
+
if (usedMinus.has(k)) return;
|
|
1254
|
+
const name = attrName(line);
|
|
1255
|
+
if (name) out.push(attribute(name, attrValue(line), null));
|
|
1256
|
+
});
|
|
1027
1257
|
const textPlus = plus.filter((p) => !ATTR_RE.test(p));
|
|
1028
1258
|
const textMinus = minus.filter((m) => !ATTR_RE.test(m));
|
|
1029
1259
|
const len = Math.max(textPlus.length, textMinus.length);
|
|
@@ -1031,22 +1261,32 @@ function parseAllHydrationDivergences(message) {
|
|
|
1031
1261
|
const client = textPlus[i] ?? null;
|
|
1032
1262
|
const server = textMinus[i] ?? null;
|
|
1033
1263
|
if (client !== null || server !== null) {
|
|
1034
|
-
out.push({
|
|
1264
|
+
out.push({
|
|
1265
|
+
kind: "text",
|
|
1266
|
+
path: "body",
|
|
1267
|
+
server,
|
|
1268
|
+
client,
|
|
1269
|
+
reactMessage: message
|
|
1270
|
+
});
|
|
1035
1271
|
}
|
|
1036
1272
|
}
|
|
1037
|
-
return out.length ? out :
|
|
1273
|
+
return out.length ? out : fallback();
|
|
1274
|
+
}
|
|
1275
|
+
function attrName(line) {
|
|
1276
|
+
return ATTR_RE.exec(line)?.[1];
|
|
1277
|
+
}
|
|
1278
|
+
function attrValue(line) {
|
|
1279
|
+
const parsed = ATTR_RE.exec(line);
|
|
1280
|
+
return parsed ? parsed[2] ?? parsed[3] ?? "" : "";
|
|
1038
1281
|
}
|
|
1039
1282
|
|
|
1040
1283
|
// src/core/inspect.ts
|
|
1041
1284
|
function inspectRoot(root, collector, context = {}, enrich) {
|
|
1042
1285
|
const serverHtml = getServerHtmlForRoot(root);
|
|
1043
1286
|
if (serverHtml == null) return 0;
|
|
1044
|
-
const divergences =
|
|
1287
|
+
const divergences = collectDivergences(serverTreeFor(root, serverHtml), root);
|
|
1045
1288
|
let reported = 0;
|
|
1046
1289
|
for (const divergence of divergences) {
|
|
1047
|
-
if (context.reactMessage && !divergence.reactMessage) {
|
|
1048
|
-
divergence.reactMessage = context.reactMessage;
|
|
1049
|
-
}
|
|
1050
1290
|
const enriched = enrich ? enrich(divergence) : {};
|
|
1051
1291
|
if (collector.report(divergence, { ...context, ...enriched }) != null) {
|
|
1052
1292
|
reported += 1;
|
|
@@ -1054,18 +1294,35 @@ function inspectRoot(root, collector, context = {}, enrich) {
|
|
|
1054
1294
|
}
|
|
1055
1295
|
return reported;
|
|
1056
1296
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1297
|
+
var serverTrees = /* @__PURE__ */ new WeakMap();
|
|
1298
|
+
function serverTreeFor(root, html) {
|
|
1299
|
+
const cached = serverTrees.get(root);
|
|
1300
|
+
if (cached && cached.html === html) return cached.tree;
|
|
1301
|
+
const tree = parseServerHtml(html, root.tagName);
|
|
1302
|
+
serverTrees.set(root, { html, tree });
|
|
1303
|
+
return tree;
|
|
1304
|
+
}
|
|
1305
|
+
function reportFromMessage(message, collector, context = {}, options = {}) {
|
|
1306
|
+
const mode = options.locationless ?? "include";
|
|
1307
|
+
const divergences = parseAllHydrationDivergences(message).filter((d) => {
|
|
1308
|
+
if (mode === "include") return true;
|
|
1309
|
+
const bare = isLocationless(d) && collector.classify(d).messageId === "unknown.no-location";
|
|
1310
|
+
return mode === "skip" ? !bare : bare;
|
|
1311
|
+
});
|
|
1059
1312
|
const component = extractComponentFromMessage(message) ?? context.component;
|
|
1060
1313
|
let reported = 0;
|
|
1061
1314
|
for (const divergence of divergences) {
|
|
1062
|
-
if (collector.report(divergence, {
|
|
1315
|
+
if (collector.report(divergence, {
|
|
1316
|
+
...context,
|
|
1317
|
+
component,
|
|
1318
|
+
reactMessage: message
|
|
1319
|
+
}) != null) {
|
|
1063
1320
|
reported += 1;
|
|
1064
1321
|
}
|
|
1065
1322
|
}
|
|
1066
1323
|
return reported;
|
|
1067
1324
|
}
|
|
1068
1325
|
|
|
1069
|
-
export { BUILT_IN_RULES, CONFIDENCE_THRESHOLD, ReportCollector, UNKNOWN_CAUSE, buildReport, classify, diffSnapshotAgainstDom, diffTrees, formatConsoleArgs, inspectRoot, isDev, isHydrationMessage, parseHydrationMessage, parseServerHtml, reportFromMessage, signatureOf };
|
|
1070
|
-
//# sourceMappingURL=chunk-
|
|
1071
|
-
//# sourceMappingURL=chunk-
|
|
1326
|
+
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 };
|
|
1327
|
+
//# sourceMappingURL=chunk-3OAI2ZHY.js.map
|
|
1328
|
+
//# sourceMappingURL=chunk-3OAI2ZHY.js.map
|