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
|
@@ -165,21 +165,21 @@ function diffAttributes(serverEl, clientEl, path, parentTag) {
|
|
|
165
165
|
}
|
|
166
166
|
return null;
|
|
167
167
|
}
|
|
168
|
-
function normalizeAttr(name,
|
|
169
|
-
if (
|
|
168
|
+
function normalizeAttr(name, value2) {
|
|
169
|
+
if (value2 == null) return null;
|
|
170
170
|
if (name === "class") {
|
|
171
|
-
return
|
|
171
|
+
return value2.trim().split(/\s+/).filter(Boolean).sort().join(" ");
|
|
172
172
|
}
|
|
173
173
|
if (name === "style") {
|
|
174
|
-
return normalizeStyle(
|
|
174
|
+
return normalizeStyle(value2);
|
|
175
175
|
}
|
|
176
|
-
return
|
|
176
|
+
return value2;
|
|
177
177
|
}
|
|
178
|
-
function normalizeStyle(
|
|
178
|
+
function normalizeStyle(value2) {
|
|
179
179
|
if (typeof document !== "undefined") {
|
|
180
180
|
try {
|
|
181
181
|
const el = document.createElement("div");
|
|
182
|
-
el.style.cssText =
|
|
182
|
+
el.style.cssText = value2;
|
|
183
183
|
const decls = [];
|
|
184
184
|
for (let i = 0; i < el.style.length; i++) {
|
|
185
185
|
const prop = el.style.item(i);
|
|
@@ -189,7 +189,7 @@ function normalizeStyle(value) {
|
|
|
189
189
|
} catch {
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
-
return
|
|
192
|
+
return value2.split(";").map((s) => s.trim()).filter(Boolean).sort().join(";");
|
|
193
193
|
}
|
|
194
194
|
function nodeKey(node) {
|
|
195
195
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -359,26 +359,26 @@ var RANDOM_DECIMAL_RE = /^0?\.\d{6,}$/;
|
|
|
359
359
|
var NANOID_RE = /^[A-Za-z0-9_-]{10,}$/;
|
|
360
360
|
var ARABIC_INDIC_DIGITS = /[٠-٩۰-۹]/;
|
|
361
361
|
var LATIN_DIGITS = /[0-9]/;
|
|
362
|
-
var BIDI_CONTROLS = /[
|
|
363
|
-
function stripBidiControls(
|
|
364
|
-
return
|
|
362
|
+
var BIDI_CONTROLS = /[\u200e\u200f\u061c\u2066-\u2069]/g;
|
|
363
|
+
function stripBidiControls(value2) {
|
|
364
|
+
return value2.replace(BIDI_CONTROLS, "");
|
|
365
365
|
}
|
|
366
366
|
function differsOnlyByBidiControls(a, b) {
|
|
367
367
|
const sa = stripBidiControls(a);
|
|
368
368
|
const sb = stripBidiControls(b);
|
|
369
369
|
return a !== b && sa === sb && sa.trim() !== "";
|
|
370
370
|
}
|
|
371
|
-
function normalizeNumerals(
|
|
372
|
-
return stripBidiControls(
|
|
371
|
+
function normalizeNumerals(value2) {
|
|
372
|
+
return stripBidiControls(value2).replace(/[٠-٩۰-۹٫٬]/g, (ch) => {
|
|
373
373
|
if (ch === "\u066B") return ".";
|
|
374
374
|
if (ch === "\u066C") return ",";
|
|
375
|
-
const
|
|
376
|
-
const zero =
|
|
377
|
-
return String(
|
|
375
|
+
const code2 = ch.charCodeAt(0);
|
|
376
|
+
const zero = code2 >= 1776 ? 1776 : 1632;
|
|
377
|
+
return String(code2 - zero);
|
|
378
378
|
});
|
|
379
379
|
}
|
|
380
|
-
function isRandomLike(
|
|
381
|
-
const v =
|
|
380
|
+
function isRandomLike(value2) {
|
|
381
|
+
const v = value2.trim();
|
|
382
382
|
if (!v) return false;
|
|
383
383
|
if (UUID_RE.test(v)) return true;
|
|
384
384
|
if (REACT_ID_RE.test(v)) return true;
|
|
@@ -387,9 +387,9 @@ function isRandomLike(value) {
|
|
|
387
387
|
if (NANOID_RE.test(v) && /[A-Za-z]/.test(v) && /[0-9]/.test(v)) return true;
|
|
388
388
|
return false;
|
|
389
389
|
}
|
|
390
|
-
function looksLikeTime(
|
|
390
|
+
function looksLikeTime(value2) {
|
|
391
391
|
return /\b\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp][Mm])?\b/.test(
|
|
392
|
-
normalizeNumerals(
|
|
392
|
+
normalizeNumerals(value2).trim()
|
|
393
393
|
);
|
|
394
394
|
}
|
|
395
395
|
var ISO_DATE = /^\d{4}-\d{1,2}(?:-\d{1,2})?(?:[T ]\d{1,2}:\d{2}(?::\d{2})?)?/;
|
|
@@ -399,8 +399,8 @@ var MONTH_NAME = /\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b/
|
|
|
399
399
|
function looksLikeDate(v) {
|
|
400
400
|
return ISO_DATE.test(v) || NUMERIC_DATE.test(v) || TIME_OF_DAY.test(v) || MONTH_NAME.test(v) && /\d/.test(v);
|
|
401
401
|
}
|
|
402
|
-
function toTimestamp(
|
|
403
|
-
const v =
|
|
402
|
+
function toTimestamp(value2) {
|
|
403
|
+
const v = value2.trim();
|
|
404
404
|
if (!v) return null;
|
|
405
405
|
if (/^\d{10,13}$/.test(v)) {
|
|
406
406
|
const n = Number(v);
|
|
@@ -415,11 +415,11 @@ function toTimestamp(value) {
|
|
|
415
415
|
}
|
|
416
416
|
return null;
|
|
417
417
|
}
|
|
418
|
-
function hasArabicIndicDigits(
|
|
419
|
-
return ARABIC_INDIC_DIGITS.test(
|
|
418
|
+
function hasArabicIndicDigits(value2) {
|
|
419
|
+
return ARABIC_INDIC_DIGITS.test(value2);
|
|
420
420
|
}
|
|
421
|
-
function hasLatinDigits(
|
|
422
|
-
return LATIN_DIGITS.test(
|
|
421
|
+
function hasLatinDigits(value2) {
|
|
422
|
+
return LATIN_DIGITS.test(value2);
|
|
423
423
|
}
|
|
424
424
|
var NUMERIC_LIKE = /^[+-]?[\d.,\s\u00a0\u2009]+$/;
|
|
425
425
|
function isSameNumberDifferentSeparators(a, b) {
|
|
@@ -524,6 +524,163 @@ function messageIndicatesInvalidNesting(message) {
|
|
|
524
524
|
return /validateDOMNesting/i.test(message) || /cannot (?:be a|contain).*(?:descendant|child)/i.test(message) || /cannot appear as a (?:child|descendant)/i.test(message);
|
|
525
525
|
}
|
|
526
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
|
+
|
|
527
684
|
// src/core/classify/rules.ts
|
|
528
685
|
var DOCS_BASE = "https://github.com/razan-aboushi/why-hydration#cause-";
|
|
529
686
|
function docs(category) {
|
|
@@ -546,8 +703,7 @@ var nonDeterministic = (d) => {
|
|
|
546
703
|
return {
|
|
547
704
|
category: "non-deterministic-value",
|
|
548
705
|
confidence: 0.9,
|
|
549
|
-
|
|
550
|
-
suggestion: "Use React `useId()` for ids. For random values, generate them after mount (in `useEffect`) or pass a value down from the server so both sides agree. Never call `Math.random()`/`crypto` during render.",
|
|
706
|
+
...describe("non-deterministic-value"),
|
|
551
707
|
docsUrl: docs("non-deterministic-value")
|
|
552
708
|
};
|
|
553
709
|
};
|
|
@@ -565,8 +721,7 @@ var dateTime = (d) => {
|
|
|
565
721
|
return {
|
|
566
722
|
category: "date-time",
|
|
567
723
|
confidence: smallDelta || bothTimes ? 0.85 : 0.7,
|
|
568
|
-
|
|
569
|
-
suggestion: "Render the current time after mount, or pass a single server timestamp down and format it identically on both sides. Pin an explicit timezone when formatting.",
|
|
724
|
+
...describe("date-time"),
|
|
570
725
|
docsUrl: docs("date-time")
|
|
571
726
|
};
|
|
572
727
|
};
|
|
@@ -578,8 +733,7 @@ var localeFormat = (d) => {
|
|
|
578
733
|
return {
|
|
579
734
|
category: "locale-format",
|
|
580
735
|
confidence: 0.88,
|
|
581
|
-
|
|
582
|
-
suggestion: "Format the value in one place and pass the string down, or pin the same locale and timezone on both sides. If the marks are harmless, add `suppressHydrationWarning` to the element.",
|
|
736
|
+
...describe("locale-format.bidi"),
|
|
583
737
|
docsUrl: docs("locale-format")
|
|
584
738
|
};
|
|
585
739
|
}
|
|
@@ -588,8 +742,7 @@ var localeFormat = (d) => {
|
|
|
588
742
|
return {
|
|
589
743
|
category: "locale-format",
|
|
590
744
|
confidence: 0.92,
|
|
591
|
-
|
|
592
|
-
suggestion: "Pass an explicit `locale` (and timezone) to `Intl.NumberFormat` / `toLocaleString` on both server and client, or format the value after mount so only the client locale is ever used.",
|
|
745
|
+
...describe("locale-format.script"),
|
|
593
746
|
docsUrl: docs("locale-format")
|
|
594
747
|
};
|
|
595
748
|
}
|
|
@@ -597,8 +750,7 @@ var localeFormat = (d) => {
|
|
|
597
750
|
return {
|
|
598
751
|
category: "locale-format",
|
|
599
752
|
confidence: 0.82,
|
|
600
|
-
|
|
601
|
-
suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match.",
|
|
753
|
+
...describe("locale-format.separators"),
|
|
602
754
|
docsUrl: docs("locale-format")
|
|
603
755
|
};
|
|
604
756
|
}
|
|
@@ -606,8 +758,7 @@ var localeFormat = (d) => {
|
|
|
606
758
|
return {
|
|
607
759
|
category: "locale-format",
|
|
608
760
|
confidence: 0.75,
|
|
609
|
-
|
|
610
|
-
suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides.",
|
|
761
|
+
...describe("locale-format.date-order"),
|
|
611
762
|
docsUrl: docs("locale-format")
|
|
612
763
|
};
|
|
613
764
|
}
|
|
@@ -621,8 +772,7 @@ var browserOnlyApi = (d) => {
|
|
|
621
772
|
return {
|
|
622
773
|
category: "browser-only-api",
|
|
623
774
|
confidence: 0.75,
|
|
624
|
-
|
|
625
|
-
suggestion: "Gate browser-only reads behind a mounted flag or `useEffect`, or use `useSyncExternalStore` with a server snapshot so the first client render matches the server.",
|
|
775
|
+
...describe("browser-only-api"),
|
|
626
776
|
docsUrl: docs("browser-only-api")
|
|
627
777
|
};
|
|
628
778
|
};
|
|
@@ -636,8 +786,7 @@ var viewportBranching = (d) => {
|
|
|
636
786
|
return {
|
|
637
787
|
category: "viewport-branching",
|
|
638
788
|
confidence: 0.6,
|
|
639
|
-
|
|
640
|
-
suggestion: "Render both branches and switch between them with CSS media queries at first paint instead of branching in JavaScript, or defer the JS-driven branch until after mount.",
|
|
789
|
+
...describe("viewport-branching"),
|
|
641
790
|
docsUrl: docs("viewport-branching")
|
|
642
791
|
};
|
|
643
792
|
};
|
|
@@ -648,8 +797,7 @@ var invalidNesting = (d) => {
|
|
|
648
797
|
return {
|
|
649
798
|
category: "invalid-html-nesting",
|
|
650
799
|
confidence: byMessage ? 0.9 : 0.72,
|
|
651
|
-
|
|
652
|
-
suggestion: "Fix the markup validity: block elements cannot live inside `<p>`, anchors cannot nest, etc. Replace the invalid parent with a `<div>` or restructure the tree.",
|
|
800
|
+
...describe("invalid-html-nesting"),
|
|
653
801
|
docsUrl: docs("invalid-html-nesting")
|
|
654
802
|
};
|
|
655
803
|
};
|
|
@@ -662,8 +810,7 @@ var whitespaceMinification = (d) => {
|
|
|
662
810
|
return {
|
|
663
811
|
category: "whitespace-minification",
|
|
664
812
|
confidence: 0.7,
|
|
665
|
-
|
|
666
|
-
suggestion: "Check your HTML minifier settings (e.g. `conservativeCollapse`) around the app root, or avoid minifying whitespace inside hydrated markup.",
|
|
813
|
+
...describe("whitespace-minification"),
|
|
667
814
|
docsUrl: docs("whitespace-minification")
|
|
668
815
|
};
|
|
669
816
|
};
|
|
@@ -673,8 +820,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
673
820
|
return {
|
|
674
821
|
category: "third-party-dom-mutation",
|
|
675
822
|
confidence: 0.88,
|
|
676
|
-
|
|
677
|
-
|
|
823
|
+
...describe("third-party-dom-mutation.extension-attribute", {
|
|
824
|
+
attribute: d.attribute
|
|
825
|
+
}),
|
|
678
826
|
docsUrl: docs("third-party-dom-mutation")
|
|
679
827
|
};
|
|
680
828
|
}
|
|
@@ -684,8 +832,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
684
832
|
return {
|
|
685
833
|
category: "third-party-dom-mutation",
|
|
686
834
|
confidence: 0.6,
|
|
687
|
-
|
|
688
|
-
|
|
835
|
+
...describe("third-party-dom-mutation.root-attribute", {
|
|
836
|
+
attribute: d.attribute
|
|
837
|
+
}),
|
|
689
838
|
docsUrl: docs("third-party-dom-mutation")
|
|
690
839
|
};
|
|
691
840
|
}
|
|
@@ -695,8 +844,9 @@ var thirdPartyDomMutation = (d) => {
|
|
|
695
844
|
return {
|
|
696
845
|
category: "third-party-dom-mutation",
|
|
697
846
|
confidence: 0.7,
|
|
698
|
-
|
|
699
|
-
|
|
847
|
+
...describe("third-party-dom-mutation.injected-node", {
|
|
848
|
+
tag: `<${tag}>`
|
|
849
|
+
}),
|
|
700
850
|
docsUrl: docs("third-party-dom-mutation")
|
|
701
851
|
};
|
|
702
852
|
}
|
|
@@ -712,15 +862,10 @@ var attributeMismatch = (d) => {
|
|
|
712
862
|
const clientSet = new Set(client.split(/\s+/).filter(Boolean));
|
|
713
863
|
const added = [...clientSet].filter((c) => !serverSet.has(c));
|
|
714
864
|
const removed = [...serverSet].filter((c) => !clientSet.has(c));
|
|
715
|
-
const parts = [];
|
|
716
|
-
if (added.length) parts.push(`added on client: ${added.join(", ")}`);
|
|
717
|
-
if (removed.length) parts.push(`removed on client: ${removed.join(", ")}`);
|
|
718
|
-
const detail = parts.length ? ` (${parts.join("; ")})` : "";
|
|
719
865
|
return {
|
|
720
866
|
category: "attribute-mismatch",
|
|
721
867
|
confidence: 0.8,
|
|
722
|
-
|
|
723
|
-
suggestion: "Render the same className on the server and the first client paint. Move client-only conditions into `useEffect`/a mounted flag, or drive the visual change with CSS media queries instead of a JS class toggle.",
|
|
868
|
+
...describe("attribute-mismatch.class", { added, removed }),
|
|
724
869
|
docsUrl: docs("attribute-mismatch")
|
|
725
870
|
};
|
|
726
871
|
}
|
|
@@ -728,16 +873,18 @@ var attributeMismatch = (d) => {
|
|
|
728
873
|
return {
|
|
729
874
|
category: "attribute-mismatch",
|
|
730
875
|
confidence: 0.75,
|
|
731
|
-
|
|
732
|
-
suggestion: "Compute the style after mount (`useEffect`) so the first client render matches the server, or move it to a CSS class / media query.",
|
|
876
|
+
...describe("attribute-mismatch.style"),
|
|
733
877
|
docsUrl: docs("attribute-mismatch")
|
|
734
878
|
};
|
|
735
879
|
}
|
|
736
880
|
return {
|
|
737
881
|
category: "attribute-mismatch",
|
|
738
882
|
confidence: 0.6,
|
|
739
|
-
|
|
740
|
-
|
|
883
|
+
...describe("attribute-mismatch.generic", {
|
|
884
|
+
attribute: d.attribute,
|
|
885
|
+
server,
|
|
886
|
+
client
|
|
887
|
+
}),
|
|
741
888
|
docsUrl: docs("attribute-mismatch")
|
|
742
889
|
};
|
|
743
890
|
};
|
|
@@ -759,13 +906,21 @@ var BUILT_IN_RULES = [
|
|
|
759
906
|
var UNKNOWN_CAUSE = {
|
|
760
907
|
category: "unknown",
|
|
761
908
|
confidence: 0,
|
|
762
|
-
|
|
763
|
-
|
|
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"),
|
|
764
916
|
docsUrl: docs("unknown")
|
|
765
917
|
};
|
|
766
918
|
|
|
767
919
|
// src/core/classify/index.ts
|
|
768
920
|
var CONFIDENCE_THRESHOLD = 0.5;
|
|
921
|
+
function isLocationless(d) {
|
|
922
|
+
return d.server == null && d.client == null && !d.tagName && !d.attribute;
|
|
923
|
+
}
|
|
769
924
|
function classify(divergence, options = {}) {
|
|
770
925
|
const threshold = options.threshold ?? CONFIDENCE_THRESHOLD;
|
|
771
926
|
const rules = [
|
|
@@ -783,7 +938,7 @@ function classify(divergence, options = {}) {
|
|
|
783
938
|
return result;
|
|
784
939
|
}
|
|
785
940
|
}
|
|
786
|
-
return UNKNOWN_CAUSE;
|
|
941
|
+
return isLocationless(divergence) ? UNKNOWN_NO_LOCATION_CAUSE : UNKNOWN_CAUSE;
|
|
787
942
|
}
|
|
788
943
|
|
|
789
944
|
// src/core/report.ts
|
|
@@ -792,12 +947,12 @@ function nextId() {
|
|
|
792
947
|
counter += 1;
|
|
793
948
|
return `wh_${Date.now().toString(36)}_${counter}`;
|
|
794
949
|
}
|
|
795
|
-
function truncate(
|
|
796
|
-
if (
|
|
797
|
-
if (
|
|
798
|
-
const last =
|
|
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);
|
|
799
954
|
const end = last >= 55296 && last <= 56319 ? max - 1 : max;
|
|
800
|
-
return `${
|
|
955
|
+
return `${value2.slice(0, end)}\u2026`;
|
|
801
956
|
}
|
|
802
957
|
function buildReport(divergence, cause, context = {}) {
|
|
803
958
|
return {
|
|
@@ -857,16 +1012,20 @@ var ReportCollector = class {
|
|
|
857
1012
|
getReports() {
|
|
858
1013
|
return this.reports;
|
|
859
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
|
+
}
|
|
860
1022
|
get isFull() {
|
|
861
1023
|
return this.reports.length >= this.maxReports;
|
|
862
1024
|
}
|
|
863
1025
|
report(divergence, context = {}) {
|
|
864
1026
|
if (this.isFull) return null;
|
|
865
1027
|
if (this.options.ignore?.(divergence)) return null;
|
|
866
|
-
const cause = classify(divergence
|
|
867
|
-
extra: this.options.extra,
|
|
868
|
-
threshold: this.options.threshold
|
|
869
|
-
});
|
|
1028
|
+
const cause = this.classify(divergence);
|
|
870
1029
|
const report = buildReport(divergence, cause, context);
|
|
871
1030
|
const signature = signatureOf(report);
|
|
872
1031
|
if (this.seen.has(signature)) return null;
|
|
@@ -916,11 +1075,19 @@ function extractComponentFromMessage(message) {
|
|
|
916
1075
|
}
|
|
917
1076
|
return names.length ? names[names.length - 1] : void 0;
|
|
918
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
|
+
}
|
|
919
1086
|
var ATTR_RE = /^([\w:-]+)=(?:"([\s\S]*)"|\{([\s\S]*)\})$/;
|
|
920
1087
|
function parseModernDiff(message) {
|
|
921
1088
|
const plus = [];
|
|
922
1089
|
const minus = [];
|
|
923
|
-
for (const raw of message
|
|
1090
|
+
for (const raw of diffLines(message)) {
|
|
924
1091
|
const line = raw.trim();
|
|
925
1092
|
const p = /^\+\s+(.+)$/.exec(line);
|
|
926
1093
|
const mn = /^-\s+(.+)$/.exec(line);
|
|
@@ -1052,7 +1219,7 @@ function parseAllHydrationDivergences(message) {
|
|
|
1052
1219
|
};
|
|
1053
1220
|
const plus = [];
|
|
1054
1221
|
const minus = [];
|
|
1055
|
-
for (const raw of message
|
|
1222
|
+
for (const raw of diffLines(message)) {
|
|
1056
1223
|
const line = raw.trim();
|
|
1057
1224
|
const p = /^\+\s+(.+)$/.exec(line);
|
|
1058
1225
|
const mn = /^-\s+(.+)$/.exec(line);
|
|
@@ -1135,8 +1302,13 @@ function serverTreeFor(root, html) {
|
|
|
1135
1302
|
serverTrees.set(root, { html, tree });
|
|
1136
1303
|
return tree;
|
|
1137
1304
|
}
|
|
1138
|
-
function reportFromMessage(message, collector, context = {}) {
|
|
1139
|
-
const
|
|
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
|
+
});
|
|
1140
1312
|
const component = extractComponentFromMessage(message) ?? context.component;
|
|
1141
1313
|
let reported = 0;
|
|
1142
1314
|
for (const divergence of divergences) {
|
|
@@ -1151,6 +1323,6 @@ function reportFromMessage(message, collector, context = {}) {
|
|
|
1151
1323
|
return reported;
|
|
1152
1324
|
}
|
|
1153
1325
|
|
|
1154
|
-
export { BUILT_IN_RULES, CONFIDENCE_THRESHOLD, ReportCollector, UNKNOWN_CAUSE, buildReport, classify, diffSnapshotAgainstDom, diffTrees, formatConsoleArgs, inspectRoot, isDev, isHydrationMessage, parseHydrationMessage, parseServerHtml, reportFromMessage, signatureOf };
|
|
1155
|
-
//# sourceMappingURL=chunk-
|
|
1156
|
-
//# sourceMappingURL=chunk-
|
|
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
|