why-hydration 0.1.5 → 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 +50 -0
- package/README.md +85 -29
- package/dist/{chunk-AS5DZZHI.js → chunk-3OAI2ZHY.js} +253 -81
- package/dist/chunk-3OAI2ZHY.js.map +1 -0
- package/dist/{chunk-AT6A77X3.cjs → chunk-KX6G7I4Y.cjs} +261 -80
- 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/index.cjs +25 -17
- package/dist/index.d.cts +28 -4
- package/dist/index.d.ts +28 -4
- 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 +23 -3
- package/dist/react.d.ts +23 -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 +1 -1
- 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
|
@@ -167,21 +167,21 @@ function diffAttributes(serverEl, clientEl, path, parentTag) {
|
|
|
167
167
|
}
|
|
168
168
|
return null;
|
|
169
169
|
}
|
|
170
|
-
function normalizeAttr(name,
|
|
171
|
-
if (
|
|
170
|
+
function normalizeAttr(name, value2) {
|
|
171
|
+
if (value2 == null) return null;
|
|
172
172
|
if (name === "class") {
|
|
173
|
-
return
|
|
173
|
+
return value2.trim().split(/\s+/).filter(Boolean).sort().join(" ");
|
|
174
174
|
}
|
|
175
175
|
if (name === "style") {
|
|
176
|
-
return normalizeStyle(
|
|
176
|
+
return normalizeStyle(value2);
|
|
177
177
|
}
|
|
178
|
-
return
|
|
178
|
+
return value2;
|
|
179
179
|
}
|
|
180
|
-
function normalizeStyle(
|
|
180
|
+
function normalizeStyle(value2) {
|
|
181
181
|
if (typeof document !== "undefined") {
|
|
182
182
|
try {
|
|
183
183
|
const el = document.createElement("div");
|
|
184
|
-
el.style.cssText =
|
|
184
|
+
el.style.cssText = value2;
|
|
185
185
|
const decls = [];
|
|
186
186
|
for (let i = 0; i < el.style.length; i++) {
|
|
187
187
|
const prop = el.style.item(i);
|
|
@@ -191,7 +191,7 @@ function normalizeStyle(value) {
|
|
|
191
191
|
} catch {
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
-
return
|
|
194
|
+
return value2.split(";").map((s) => s.trim()).filter(Boolean).sort().join(";");
|
|
195
195
|
}
|
|
196
196
|
function nodeKey(node) {
|
|
197
197
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -361,26 +361,26 @@ var RANDOM_DECIMAL_RE = /^0?\.\d{6,}$/;
|
|
|
361
361
|
var NANOID_RE = /^[A-Za-z0-9_-]{10,}$/;
|
|
362
362
|
var ARABIC_INDIC_DIGITS = /[٠-٩۰-۹]/;
|
|
363
363
|
var LATIN_DIGITS = /[0-9]/;
|
|
364
|
-
var BIDI_CONTROLS = /[
|
|
365
|
-
function stripBidiControls(
|
|
366
|
-
return
|
|
364
|
+
var BIDI_CONTROLS = /[\u200e\u200f\u061c\u2066-\u2069]/g;
|
|
365
|
+
function stripBidiControls(value2) {
|
|
366
|
+
return value2.replace(BIDI_CONTROLS, "");
|
|
367
367
|
}
|
|
368
368
|
function differsOnlyByBidiControls(a, b) {
|
|
369
369
|
const sa = stripBidiControls(a);
|
|
370
370
|
const sb = stripBidiControls(b);
|
|
371
371
|
return a !== b && sa === sb && sa.trim() !== "";
|
|
372
372
|
}
|
|
373
|
-
function normalizeNumerals(
|
|
374
|
-
return stripBidiControls(
|
|
373
|
+
function normalizeNumerals(value2) {
|
|
374
|
+
return stripBidiControls(value2).replace(/[٠-٩۰-۹٫٬]/g, (ch) => {
|
|
375
375
|
if (ch === "\u066B") return ".";
|
|
376
376
|
if (ch === "\u066C") return ",";
|
|
377
|
-
const
|
|
378
|
-
const zero =
|
|
379
|
-
return String(
|
|
377
|
+
const code2 = ch.charCodeAt(0);
|
|
378
|
+
const zero = code2 >= 1776 ? 1776 : 1632;
|
|
379
|
+
return String(code2 - zero);
|
|
380
380
|
});
|
|
381
381
|
}
|
|
382
|
-
function isRandomLike(
|
|
383
|
-
const v =
|
|
382
|
+
function isRandomLike(value2) {
|
|
383
|
+
const v = value2.trim();
|
|
384
384
|
if (!v) return false;
|
|
385
385
|
if (UUID_RE.test(v)) return true;
|
|
386
386
|
if (REACT_ID_RE.test(v)) return true;
|
|
@@ -389,9 +389,9 @@ function isRandomLike(value) {
|
|
|
389
389
|
if (NANOID_RE.test(v) && /[A-Za-z]/.test(v) && /[0-9]/.test(v)) return true;
|
|
390
390
|
return false;
|
|
391
391
|
}
|
|
392
|
-
function looksLikeTime(
|
|
392
|
+
function looksLikeTime(value2) {
|
|
393
393
|
return /\b\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp][Mm])?\b/.test(
|
|
394
|
-
normalizeNumerals(
|
|
394
|
+
normalizeNumerals(value2).trim()
|
|
395
395
|
);
|
|
396
396
|
}
|
|
397
397
|
var ISO_DATE = /^\d{4}-\d{1,2}(?:-\d{1,2})?(?:[T ]\d{1,2}:\d{2}(?::\d{2})?)?/;
|
|
@@ -401,8 +401,8 @@ var MONTH_NAME = /\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b/
|
|
|
401
401
|
function looksLikeDate(v) {
|
|
402
402
|
return ISO_DATE.test(v) || NUMERIC_DATE.test(v) || TIME_OF_DAY.test(v) || MONTH_NAME.test(v) && /\d/.test(v);
|
|
403
403
|
}
|
|
404
|
-
function toTimestamp(
|
|
405
|
-
const v =
|
|
404
|
+
function toTimestamp(value2) {
|
|
405
|
+
const v = value2.trim();
|
|
406
406
|
if (!v) return null;
|
|
407
407
|
if (/^\d{10,13}$/.test(v)) {
|
|
408
408
|
const n = Number(v);
|
|
@@ -417,11 +417,11 @@ function toTimestamp(value) {
|
|
|
417
417
|
}
|
|
418
418
|
return null;
|
|
419
419
|
}
|
|
420
|
-
function hasArabicIndicDigits(
|
|
421
|
-
return ARABIC_INDIC_DIGITS.test(
|
|
420
|
+
function hasArabicIndicDigits(value2) {
|
|
421
|
+
return ARABIC_INDIC_DIGITS.test(value2);
|
|
422
422
|
}
|
|
423
|
-
function hasLatinDigits(
|
|
424
|
-
return LATIN_DIGITS.test(
|
|
423
|
+
function hasLatinDigits(value2) {
|
|
424
|
+
return LATIN_DIGITS.test(value2);
|
|
425
425
|
}
|
|
426
426
|
var NUMERIC_LIKE = /^[+-]?[\d.,\s\u00a0\u2009]+$/;
|
|
427
427
|
function isSameNumberDifferentSeparators(a, b) {
|
|
@@ -526,6 +526,163 @@ function messageIndicatesInvalidNesting(message) {
|
|
|
526
526
|
return /validateDOMNesting/i.test(message) || /cannot (?:be a|contain).*(?:descendant|child)/i.test(message) || /cannot appear as a (?:child|descendant)/i.test(message);
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
// src/core/classify/messages.ts
|
|
530
|
+
var code = (text) => ({ code: text });
|
|
531
|
+
var value = (text) => ({ value: text });
|
|
532
|
+
function param(params, name) {
|
|
533
|
+
const v = params[name];
|
|
534
|
+
return typeof v === "string" ? v : "";
|
|
535
|
+
}
|
|
536
|
+
function paramList(params, name) {
|
|
537
|
+
const v = params[name];
|
|
538
|
+
return Array.isArray(v) ? v : [];
|
|
539
|
+
}
|
|
540
|
+
function valueList(items, separator) {
|
|
541
|
+
const out = [];
|
|
542
|
+
items.forEach((item, i) => {
|
|
543
|
+
if (i > 0) out.push(separator);
|
|
544
|
+
out.push(value(item));
|
|
545
|
+
});
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
function renderMessage(template, params = {}) {
|
|
549
|
+
if (typeof template === "function") return template(params);
|
|
550
|
+
const out = [];
|
|
551
|
+
template.split("`").forEach((part, i) => {
|
|
552
|
+
if (i % 2 === 1) {
|
|
553
|
+
out.push(
|
|
554
|
+
code(part.replace(/\{(\w+)\}/g, (_, n) => param(params, n)))
|
|
555
|
+
);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
let last = 0;
|
|
559
|
+
for (const m of part.matchAll(/\{(\w+)\}/g)) {
|
|
560
|
+
const at = m.index ?? 0;
|
|
561
|
+
if (at > last) out.push(part.slice(last, at));
|
|
562
|
+
out.push(value(param(params, m[1])));
|
|
563
|
+
last = at + m[0].length;
|
|
564
|
+
}
|
|
565
|
+
if (last < part.length) out.push(part.slice(last));
|
|
566
|
+
});
|
|
567
|
+
return out.filter((s) => s !== "");
|
|
568
|
+
}
|
|
569
|
+
function plainText(segments) {
|
|
570
|
+
return segments.map(
|
|
571
|
+
(s) => typeof s === "string" ? s : "code" in s ? `\`${s.code}\`` : s.value
|
|
572
|
+
).join("");
|
|
573
|
+
}
|
|
574
|
+
function classDetail(params, words) {
|
|
575
|
+
const added = paramList(params, "added");
|
|
576
|
+
const removed = paramList(params, "removed");
|
|
577
|
+
if (!added.length && !removed.length) return [];
|
|
578
|
+
const out = [words.open];
|
|
579
|
+
if (added.length) {
|
|
580
|
+
out.push(words.added, ...valueList(added, words.listSeparator));
|
|
581
|
+
}
|
|
582
|
+
if (removed.length) {
|
|
583
|
+
if (added.length) out.push(words.partSeparator);
|
|
584
|
+
out.push(words.removed, ...valueList(removed, words.listSeparator));
|
|
585
|
+
}
|
|
586
|
+
out.push(words.close);
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
var EN_MESSAGES = {
|
|
590
|
+
"non-deterministic-value": {
|
|
591
|
+
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.",
|
|
592
|
+
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."
|
|
593
|
+
},
|
|
594
|
+
"date-time": {
|
|
595
|
+
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.",
|
|
596
|
+
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."
|
|
597
|
+
},
|
|
598
|
+
"locale-format.bidi": {
|
|
599
|
+
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.",
|
|
600
|
+
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."
|
|
601
|
+
},
|
|
602
|
+
"locale-format.script": {
|
|
603
|
+
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.",
|
|
604
|
+
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."
|
|
605
|
+
},
|
|
606
|
+
"locale-format.separators": {
|
|
607
|
+
explanation: "The same number was formatted with different grouping/decimal separators between server and client (e.g. 1,234.56 vs 1.234,56).",
|
|
608
|
+
suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match."
|
|
609
|
+
},
|
|
610
|
+
"locale-format.date-order": {
|
|
611
|
+
explanation: "The same date was rendered in a different field order (MM/DD vs DD/MM) between server and client.",
|
|
612
|
+
suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides."
|
|
613
|
+
},
|
|
614
|
+
"browser-only-api": {
|
|
615
|
+
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.",
|
|
616
|
+
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."
|
|
617
|
+
},
|
|
618
|
+
"viewport-branching": {
|
|
619
|
+
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.",
|
|
620
|
+
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."
|
|
621
|
+
},
|
|
622
|
+
"invalid-html-nesting": {
|
|
623
|
+
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.",
|
|
624
|
+
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."
|
|
625
|
+
},
|
|
626
|
+
"whitespace-minification": {
|
|
627
|
+
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.",
|
|
628
|
+
suggestion: "Check your HTML minifier settings (e.g. `conservativeCollapse`) around the app root, or avoid minifying whitespace inside hydrated markup."
|
|
629
|
+
},
|
|
630
|
+
"third-party-dom-mutation.extension-attribute": {
|
|
631
|
+
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.",
|
|
632
|
+
suggestion: "This is usually harmless. Add `suppressHydrationWarning` to the affected element, or defer third-party script init until after hydration."
|
|
633
|
+
},
|
|
634
|
+
"third-party-dom-mutation.root-attribute": {
|
|
635
|
+
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.",
|
|
636
|
+
suggestion: "Add `suppressHydrationWarning` to the root element, or defer the third-party script until after hydration."
|
|
637
|
+
},
|
|
638
|
+
"third-party-dom-mutation.injected-node": {
|
|
639
|
+
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.",
|
|
640
|
+
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">`).'
|
|
641
|
+
},
|
|
642
|
+
"attribute-mismatch.class": {
|
|
643
|
+
explanation: (p) => [
|
|
644
|
+
"The ",
|
|
645
|
+
code("class"),
|
|
646
|
+
" differs between server and client",
|
|
647
|
+
...classDetail(p, {
|
|
648
|
+
added: "added on client: ",
|
|
649
|
+
removed: "removed on client: ",
|
|
650
|
+
listSeparator: ", ",
|
|
651
|
+
partSeparator: "; ",
|
|
652
|
+
open: " (",
|
|
653
|
+
close: ")"
|
|
654
|
+
}),
|
|
655
|
+
". 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."
|
|
656
|
+
],
|
|
657
|
+
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."
|
|
658
|
+
},
|
|
659
|
+
"attribute-mismatch.style": {
|
|
660
|
+
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.",
|
|
661
|
+
suggestion: "Compute the style after mount (`useEffect`) so the first client render matches the server, or move it to a CSS class / media query."
|
|
662
|
+
},
|
|
663
|
+
"attribute-mismatch.generic": {
|
|
664
|
+
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.",
|
|
665
|
+
suggestion: "Make the attribute deterministic across server and client, or set it after mount so the first client render matches the server HTML."
|
|
666
|
+
},
|
|
667
|
+
unknown: {
|
|
668
|
+
explanation: "A hydration mismatch was detected but could not be matched to a known cause. Inspect the server vs client values above.",
|
|
669
|
+
suggestion: "Compare the server and client values. Common causes are non-deterministic values, dates/locales, and browser-only APIs used during render."
|
|
670
|
+
},
|
|
671
|
+
"unknown.no-location": {
|
|
672
|
+
explanation: "React reported that hydration failed but did not say which node differed, and the DOM diff found no difference to point at.",
|
|
673
|
+
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."
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
function describe(id, params = {}, catalog = EN_MESSAGES) {
|
|
677
|
+
const entry = catalog[id];
|
|
678
|
+
return {
|
|
679
|
+
messageId: id,
|
|
680
|
+
...Object.keys(params).length ? { params } : {},
|
|
681
|
+
explanation: plainText(renderMessage(entry.explanation, params)),
|
|
682
|
+
suggestion: plainText(renderMessage(entry.suggestion, params))
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
529
686
|
// src/core/classify/rules.ts
|
|
530
687
|
var DOCS_BASE = "https://github.com/razan-aboushi/why-hydration#cause-";
|
|
531
688
|
function docs(category) {
|
|
@@ -548,8 +705,7 @@ var nonDeterministic = (d) => {
|
|
|
548
705
|
return {
|
|
549
706
|
category: "non-deterministic-value",
|
|
550
707
|
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.",
|
|
708
|
+
...describe("non-deterministic-value"),
|
|
553
709
|
docsUrl: docs("non-deterministic-value")
|
|
554
710
|
};
|
|
555
711
|
};
|
|
@@ -567,8 +723,7 @@ var dateTime = (d) => {
|
|
|
567
723
|
return {
|
|
568
724
|
category: "date-time",
|
|
569
725
|
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.",
|
|
726
|
+
...describe("date-time"),
|
|
572
727
|
docsUrl: docs("date-time")
|
|
573
728
|
};
|
|
574
729
|
};
|
|
@@ -580,8 +735,7 @@ var localeFormat = (d) => {
|
|
|
580
735
|
return {
|
|
581
736
|
category: "locale-format",
|
|
582
737
|
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.",
|
|
738
|
+
...describe("locale-format.bidi"),
|
|
585
739
|
docsUrl: docs("locale-format")
|
|
586
740
|
};
|
|
587
741
|
}
|
|
@@ -590,8 +744,7 @@ var localeFormat = (d) => {
|
|
|
590
744
|
return {
|
|
591
745
|
category: "locale-format",
|
|
592
746
|
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.",
|
|
747
|
+
...describe("locale-format.script"),
|
|
595
748
|
docsUrl: docs("locale-format")
|
|
596
749
|
};
|
|
597
750
|
}
|
|
@@ -599,8 +752,7 @@ var localeFormat = (d) => {
|
|
|
599
752
|
return {
|
|
600
753
|
category: "locale-format",
|
|
601
754
|
confidence: 0.82,
|
|
602
|
-
|
|
603
|
-
suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match.",
|
|
755
|
+
...describe("locale-format.separators"),
|
|
604
756
|
docsUrl: docs("locale-format")
|
|
605
757
|
};
|
|
606
758
|
}
|
|
@@ -608,8 +760,7 @@ var localeFormat = (d) => {
|
|
|
608
760
|
return {
|
|
609
761
|
category: "locale-format",
|
|
610
762
|
confidence: 0.75,
|
|
611
|
-
|
|
612
|
-
suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides.",
|
|
763
|
+
...describe("locale-format.date-order"),
|
|
613
764
|
docsUrl: docs("locale-format")
|
|
614
765
|
};
|
|
615
766
|
}
|
|
@@ -623,8 +774,7 @@ var browserOnlyApi = (d) => {
|
|
|
623
774
|
return {
|
|
624
775
|
category: "browser-only-api",
|
|
625
776
|
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.",
|
|
777
|
+
...describe("browser-only-api"),
|
|
628
778
|
docsUrl: docs("browser-only-api")
|
|
629
779
|
};
|
|
630
780
|
};
|
|
@@ -638,8 +788,7 @@ var viewportBranching = (d) => {
|
|
|
638
788
|
return {
|
|
639
789
|
category: "viewport-branching",
|
|
640
790
|
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.",
|
|
791
|
+
...describe("viewport-branching"),
|
|
643
792
|
docsUrl: docs("viewport-branching")
|
|
644
793
|
};
|
|
645
794
|
};
|
|
@@ -650,8 +799,7 @@ var invalidNesting = (d) => {
|
|
|
650
799
|
return {
|
|
651
800
|
category: "invalid-html-nesting",
|
|
652
801
|
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.",
|
|
802
|
+
...describe("invalid-html-nesting"),
|
|
655
803
|
docsUrl: docs("invalid-html-nesting")
|
|
656
804
|
};
|
|
657
805
|
};
|
|
@@ -664,8 +812,7 @@ var whitespaceMinification = (d) => {
|
|
|
664
812
|
return {
|
|
665
813
|
category: "whitespace-minification",
|
|
666
814
|
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.",
|
|
815
|
+
...describe("whitespace-minification"),
|
|
669
816
|
docsUrl: docs("whitespace-minification")
|
|
670
817
|
};
|
|
671
818
|
};
|
|
@@ -675,8 +822,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
675
822
|
return {
|
|
676
823
|
category: "third-party-dom-mutation",
|
|
677
824
|
confidence: 0.88,
|
|
678
|
-
|
|
679
|
-
|
|
825
|
+
...describe("third-party-dom-mutation.extension-attribute", {
|
|
826
|
+
attribute: d.attribute
|
|
827
|
+
}),
|
|
680
828
|
docsUrl: docs("third-party-dom-mutation")
|
|
681
829
|
};
|
|
682
830
|
}
|
|
@@ -686,8 +834,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
686
834
|
return {
|
|
687
835
|
category: "third-party-dom-mutation",
|
|
688
836
|
confidence: 0.6,
|
|
689
|
-
|
|
690
|
-
|
|
837
|
+
...describe("third-party-dom-mutation.root-attribute", {
|
|
838
|
+
attribute: d.attribute
|
|
839
|
+
}),
|
|
691
840
|
docsUrl: docs("third-party-dom-mutation")
|
|
692
841
|
};
|
|
693
842
|
}
|
|
@@ -697,8 +846,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
697
846
|
return {
|
|
698
847
|
category: "third-party-dom-mutation",
|
|
699
848
|
confidence: 0.7,
|
|
700
|
-
|
|
701
|
-
|
|
849
|
+
...describe("third-party-dom-mutation.injected-node", {
|
|
850
|
+
tag: `<${tag}>`
|
|
851
|
+
}),
|
|
702
852
|
docsUrl: docs("third-party-dom-mutation")
|
|
703
853
|
};
|
|
704
854
|
}
|
|
@@ -714,15 +864,10 @@ var attributeMismatch = (d) => {
|
|
|
714
864
|
const clientSet = new Set(client.split(/\s+/).filter(Boolean));
|
|
715
865
|
const added = [...clientSet].filter((c) => !serverSet.has(c));
|
|
716
866
|
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
867
|
return {
|
|
722
868
|
category: "attribute-mismatch",
|
|
723
869
|
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.",
|
|
870
|
+
...describe("attribute-mismatch.class", { added, removed }),
|
|
726
871
|
docsUrl: docs("attribute-mismatch")
|
|
727
872
|
};
|
|
728
873
|
}
|
|
@@ -730,16 +875,18 @@ var attributeMismatch = (d) => {
|
|
|
730
875
|
return {
|
|
731
876
|
category: "attribute-mismatch",
|
|
732
877
|
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.",
|
|
878
|
+
...describe("attribute-mismatch.style"),
|
|
735
879
|
docsUrl: docs("attribute-mismatch")
|
|
736
880
|
};
|
|
737
881
|
}
|
|
738
882
|
return {
|
|
739
883
|
category: "attribute-mismatch",
|
|
740
884
|
confidence: 0.6,
|
|
741
|
-
|
|
742
|
-
|
|
885
|
+
...describe("attribute-mismatch.generic", {
|
|
886
|
+
attribute: d.attribute,
|
|
887
|
+
server,
|
|
888
|
+
client
|
|
889
|
+
}),
|
|
743
890
|
docsUrl: docs("attribute-mismatch")
|
|
744
891
|
};
|
|
745
892
|
};
|
|
@@ -761,13 +908,21 @@ var BUILT_IN_RULES = [
|
|
|
761
908
|
var UNKNOWN_CAUSE = {
|
|
762
909
|
category: "unknown",
|
|
763
910
|
confidence: 0,
|
|
764
|
-
|
|
765
|
-
|
|
911
|
+
...describe("unknown"),
|
|
912
|
+
docsUrl: docs("unknown")
|
|
913
|
+
};
|
|
914
|
+
var UNKNOWN_NO_LOCATION_CAUSE = {
|
|
915
|
+
category: "unknown",
|
|
916
|
+
confidence: 0,
|
|
917
|
+
...describe("unknown.no-location"),
|
|
766
918
|
docsUrl: docs("unknown")
|
|
767
919
|
};
|
|
768
920
|
|
|
769
921
|
// src/core/classify/index.ts
|
|
770
922
|
var CONFIDENCE_THRESHOLD = 0.5;
|
|
923
|
+
function isLocationless(d) {
|
|
924
|
+
return d.server == null && d.client == null && !d.tagName && !d.attribute;
|
|
925
|
+
}
|
|
771
926
|
function classify(divergence, options = {}) {
|
|
772
927
|
const threshold = options.threshold ?? CONFIDENCE_THRESHOLD;
|
|
773
928
|
const rules = [
|
|
@@ -785,7 +940,7 @@ function classify(divergence, options = {}) {
|
|
|
785
940
|
return result;
|
|
786
941
|
}
|
|
787
942
|
}
|
|
788
|
-
return UNKNOWN_CAUSE;
|
|
943
|
+
return isLocationless(divergence) ? UNKNOWN_NO_LOCATION_CAUSE : UNKNOWN_CAUSE;
|
|
789
944
|
}
|
|
790
945
|
|
|
791
946
|
// src/core/report.ts
|
|
@@ -794,12 +949,12 @@ function nextId() {
|
|
|
794
949
|
counter += 1;
|
|
795
950
|
return `wh_${Date.now().toString(36)}_${counter}`;
|
|
796
951
|
}
|
|
797
|
-
function truncate(
|
|
798
|
-
if (
|
|
799
|
-
if (
|
|
800
|
-
const last =
|
|
952
|
+
function truncate(value2, max = 300) {
|
|
953
|
+
if (value2 == null) return null;
|
|
954
|
+
if (value2.length <= max) return value2;
|
|
955
|
+
const last = value2.charCodeAt(max - 1);
|
|
801
956
|
const end = last >= 55296 && last <= 56319 ? max - 1 : max;
|
|
802
|
-
return `${
|
|
957
|
+
return `${value2.slice(0, end)}\u2026`;
|
|
803
958
|
}
|
|
804
959
|
function buildReport(divergence, cause, context = {}) {
|
|
805
960
|
return {
|
|
@@ -859,16 +1014,20 @@ var ReportCollector = class {
|
|
|
859
1014
|
getReports() {
|
|
860
1015
|
return this.reports;
|
|
861
1016
|
}
|
|
1017
|
+
/** The cause this collector would assign — same rules, same threshold. */
|
|
1018
|
+
classify(divergence) {
|
|
1019
|
+
return classify(divergence, {
|
|
1020
|
+
extra: this.options.extra,
|
|
1021
|
+
threshold: this.options.threshold
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
862
1024
|
get isFull() {
|
|
863
1025
|
return this.reports.length >= this.maxReports;
|
|
864
1026
|
}
|
|
865
1027
|
report(divergence, context = {}) {
|
|
866
1028
|
if (this.isFull) return null;
|
|
867
1029
|
if (this.options.ignore?.(divergence)) return null;
|
|
868
|
-
const cause = classify(divergence
|
|
869
|
-
extra: this.options.extra,
|
|
870
|
-
threshold: this.options.threshold
|
|
871
|
-
});
|
|
1030
|
+
const cause = this.classify(divergence);
|
|
872
1031
|
const report = buildReport(divergence, cause, context);
|
|
873
1032
|
const signature = signatureOf(report);
|
|
874
1033
|
if (this.seen.has(signature)) return null;
|
|
@@ -918,11 +1077,19 @@ function extractComponentFromMessage(message) {
|
|
|
918
1077
|
}
|
|
919
1078
|
return names.length ? names[names.length - 1] : void 0;
|
|
920
1079
|
}
|
|
1080
|
+
function diffLines(message) {
|
|
1081
|
+
const lines = message.split("\n");
|
|
1082
|
+
let link = -1;
|
|
1083
|
+
lines.forEach((line, i) => {
|
|
1084
|
+
if (/react\.dev\/link\/hydration-mismatch/.test(line)) link = i;
|
|
1085
|
+
});
|
|
1086
|
+
return link >= 0 ? lines.slice(link + 1) : lines;
|
|
1087
|
+
}
|
|
921
1088
|
var ATTR_RE = /^([\w:-]+)=(?:"([\s\S]*)"|\{([\s\S]*)\})$/;
|
|
922
1089
|
function parseModernDiff(message) {
|
|
923
1090
|
const plus = [];
|
|
924
1091
|
const minus = [];
|
|
925
|
-
for (const raw of message
|
|
1092
|
+
for (const raw of diffLines(message)) {
|
|
926
1093
|
const line = raw.trim();
|
|
927
1094
|
const p = /^\+\s+(.+)$/.exec(line);
|
|
928
1095
|
const mn = /^-\s+(.+)$/.exec(line);
|
|
@@ -1054,7 +1221,7 @@ function parseAllHydrationDivergences(message) {
|
|
|
1054
1221
|
};
|
|
1055
1222
|
const plus = [];
|
|
1056
1223
|
const minus = [];
|
|
1057
|
-
for (const raw of message
|
|
1224
|
+
for (const raw of diffLines(message)) {
|
|
1058
1225
|
const line = raw.trim();
|
|
1059
1226
|
const p = /^\+\s+(.+)$/.exec(line);
|
|
1060
1227
|
const mn = /^-\s+(.+)$/.exec(line);
|
|
@@ -1137,8 +1304,13 @@ function serverTreeFor(root, html) {
|
|
|
1137
1304
|
serverTrees.set(root, { html, tree });
|
|
1138
1305
|
return tree;
|
|
1139
1306
|
}
|
|
1140
|
-
function reportFromMessage(message, collector, context = {}) {
|
|
1141
|
-
const
|
|
1307
|
+
function reportFromMessage(message, collector, context = {}, options = {}) {
|
|
1308
|
+
const mode = options.locationless ?? "include";
|
|
1309
|
+
const divergences = parseAllHydrationDivergences(message).filter((d) => {
|
|
1310
|
+
if (mode === "include") return true;
|
|
1311
|
+
const bare = isLocationless(d) && collector.classify(d).messageId === "unknown.no-location";
|
|
1312
|
+
return mode === "skip" ? !bare : bare;
|
|
1313
|
+
});
|
|
1142
1314
|
const component = extractComponentFromMessage(message) ?? context.component;
|
|
1143
1315
|
let reported = 0;
|
|
1144
1316
|
for (const divergence of divergences) {
|
|
@@ -1155,19 +1327,28 @@ function reportFromMessage(message, collector, context = {}) {
|
|
|
1155
1327
|
|
|
1156
1328
|
exports.BUILT_IN_RULES = BUILT_IN_RULES;
|
|
1157
1329
|
exports.CONFIDENCE_THRESHOLD = CONFIDENCE_THRESHOLD;
|
|
1330
|
+
exports.EN_MESSAGES = EN_MESSAGES;
|
|
1158
1331
|
exports.ReportCollector = ReportCollector;
|
|
1159
1332
|
exports.UNKNOWN_CAUSE = UNKNOWN_CAUSE;
|
|
1333
|
+
exports.UNKNOWN_NO_LOCATION_CAUSE = UNKNOWN_NO_LOCATION_CAUSE;
|
|
1160
1334
|
exports.buildReport = buildReport;
|
|
1335
|
+
exports.classDetail = classDetail;
|
|
1161
1336
|
exports.classify = classify;
|
|
1337
|
+
exports.code = code;
|
|
1162
1338
|
exports.diffSnapshotAgainstDom = diffSnapshotAgainstDom;
|
|
1163
1339
|
exports.diffTrees = diffTrees;
|
|
1164
1340
|
exports.formatConsoleArgs = formatConsoleArgs;
|
|
1165
1341
|
exports.inspectRoot = inspectRoot;
|
|
1166
1342
|
exports.isDev = isDev;
|
|
1167
1343
|
exports.isHydrationMessage = isHydrationMessage;
|
|
1344
|
+
exports.isInternalComponent = isInternalComponent;
|
|
1345
|
+
exports.isLocationless = isLocationless;
|
|
1346
|
+
exports.param = param;
|
|
1168
1347
|
exports.parseHydrationMessage = parseHydrationMessage;
|
|
1169
1348
|
exports.parseServerHtml = parseServerHtml;
|
|
1349
|
+
exports.plainText = plainText;
|
|
1350
|
+
exports.renderMessage = renderMessage;
|
|
1170
1351
|
exports.reportFromMessage = reportFromMessage;
|
|
1171
1352
|
exports.signatureOf = signatureOf;
|
|
1172
|
-
//# sourceMappingURL=chunk-
|
|
1173
|
-
//# sourceMappingURL=chunk-
|
|
1353
|
+
//# sourceMappingURL=chunk-KX6G7I4Y.cjs.map
|
|
1354
|
+
//# sourceMappingURL=chunk-KX6G7I4Y.cjs.map
|