why-hydration 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +394 -0
  3. package/dist/chunk-EPJM3LH3.cjs +717 -0
  4. package/dist/chunk-EPJM3LH3.cjs.map +1 -0
  5. package/dist/chunk-FV3PEJQE.js +42 -0
  6. package/dist/chunk-FV3PEJQE.js.map +1 -0
  7. package/dist/chunk-HYG6GVWO.js +16 -0
  8. package/dist/chunk-HYG6GVWO.js.map +1 -0
  9. package/dist/chunk-LNVWRW65.js +700 -0
  10. package/dist/chunk-LNVWRW65.js.map +1 -0
  11. package/dist/chunk-OGQEPU7G.cjs +49 -0
  12. package/dist/chunk-OGQEPU7G.cjs.map +1 -0
  13. package/dist/chunk-PVXOK7GJ.js +418 -0
  14. package/dist/chunk-PVXOK7GJ.js.map +1 -0
  15. package/dist/chunk-QGAEURQY.cjs +38 -0
  16. package/dist/chunk-QGAEURQY.cjs.map +1 -0
  17. package/dist/chunk-R6344ZUK.cjs +441 -0
  18. package/dist/chunk-R6344ZUK.cjs.map +1 -0
  19. package/dist/index.cjs +97 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.cts +65 -0
  22. package/dist/index.d.ts +65 -0
  23. package/dist/index.js +4 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/next/index.cjs +23 -0
  26. package/dist/next/index.cjs.map +1 -0
  27. package/dist/next/index.d.cts +4 -0
  28. package/dist/next/index.d.ts +4 -0
  29. package/dist/next/index.js +6 -0
  30. package/dist/next/index.js.map +1 -0
  31. package/dist/next/script.cjs +13 -0
  32. package/dist/next/script.cjs.map +1 -0
  33. package/dist/next/script.d.cts +9 -0
  34. package/dist/next/script.d.ts +9 -0
  35. package/dist/next/script.js +4 -0
  36. package/dist/next/script.js.map +1 -0
  37. package/dist/react.cjs +18 -0
  38. package/dist/react.cjs.map +1 -0
  39. package/dist/react.d.cts +38 -0
  40. package/dist/react.d.ts +38 -0
  41. package/dist/react.js +5 -0
  42. package/dist/react.js.map +1 -0
  43. package/dist/types-DTlltJb9.d.cts +53 -0
  44. package/dist/types-DTlltJb9.d.ts +53 -0
  45. package/package.json +134 -0
@@ -0,0 +1,717 @@
1
+ 'use strict';
2
+
3
+ var chunkOGQEPU7G_cjs = require('./chunk-OGQEPU7G.cjs');
4
+
5
+ // src/core/env.ts
6
+ var isDev = typeof process !== "undefined" && process.env != null && process.env.NODE_ENV !== "production";
7
+
8
+ // src/core/diff.ts
9
+ var IGNORED_ATTRIBUTES = /* @__PURE__ */ new Set(["data-reactroot"]);
10
+ function parseServerHtml(html, rootTagName) {
11
+ const container = document.createElement(rootTagName || "div");
12
+ container.innerHTML = html;
13
+ return container;
14
+ }
15
+ function diffTrees(serverRoot, clientRoot, basePath = tagPath(clientRoot)) {
16
+ return diffChildren(serverRoot, clientRoot, basePath);
17
+ }
18
+ function diffSnapshotAgainstDom(serverHtml, clientRoot) {
19
+ const serverRoot = parseServerHtml(serverHtml, clientRoot.tagName);
20
+ return diffTrees(serverRoot, clientRoot);
21
+ }
22
+ function diffChildren(serverParent, clientParent, parentPath) {
23
+ const serverChildren = meaningfulChildNodes(serverParent);
24
+ const clientChildren = meaningfulChildNodes(clientParent);
25
+ const max = Math.max(serverChildren.length, clientChildren.length);
26
+ for (let i = 0; i < max; i++) {
27
+ const serverNode = serverChildren[i] ?? null;
28
+ const clientNode = clientChildren[i] ?? null;
29
+ const path = childPath(parentPath, clientNode ?? serverNode, i);
30
+ if (serverNode && !clientNode) {
31
+ return {
32
+ kind: "node-removed",
33
+ path,
34
+ tagName: elementTag(serverNode),
35
+ parentTagName: elementTag(clientParent),
36
+ server: serialize(serverNode),
37
+ client: null,
38
+ element: asElement(clientParent)
39
+ };
40
+ }
41
+ if (!serverNode && clientNode) {
42
+ return {
43
+ kind: "node-added",
44
+ path,
45
+ tagName: elementTag(clientNode),
46
+ parentTagName: elementTag(clientParent),
47
+ server: null,
48
+ client: serialize(clientNode),
49
+ element: asElement(clientNode)
50
+ };
51
+ }
52
+ if (!serverNode || !clientNode) continue;
53
+ const divergence = diffNode(
54
+ serverNode,
55
+ clientNode,
56
+ path,
57
+ elementTag(clientParent)
58
+ );
59
+ if (divergence) return divergence;
60
+ }
61
+ return null;
62
+ }
63
+ function diffNode(serverNode, clientNode, path, parentTag) {
64
+ if (serverNode.nodeType !== clientNode.nodeType) {
65
+ return {
66
+ kind: "structure",
67
+ path,
68
+ tagName: elementTag(clientNode),
69
+ parentTagName: parentTag,
70
+ server: serialize(serverNode),
71
+ client: serialize(clientNode),
72
+ element: asElement(clientNode)
73
+ };
74
+ }
75
+ if (serverNode.nodeType === Node.TEXT_NODE || serverNode.nodeType === Node.COMMENT_NODE) {
76
+ const serverText = serverNode.textContent ?? "";
77
+ const clientText = clientNode.textContent ?? "";
78
+ if (serverText !== clientText) {
79
+ return {
80
+ kind: "text",
81
+ path,
82
+ parentTagName: parentTag,
83
+ server: serverText,
84
+ client: clientText,
85
+ element: asElement(clientNode.parentNode)
86
+ };
87
+ }
88
+ return null;
89
+ }
90
+ if (serverNode.nodeType === Node.ELEMENT_NODE) {
91
+ const serverEl = serverNode;
92
+ const clientEl = clientNode;
93
+ if (serverEl.tagName !== clientEl.tagName) {
94
+ return {
95
+ kind: "structure",
96
+ path,
97
+ tagName: clientEl.tagName,
98
+ parentTagName: parentTag,
99
+ server: serialize(serverEl),
100
+ client: serialize(clientEl),
101
+ element: clientEl
102
+ };
103
+ }
104
+ const attrDivergence = diffAttributes(serverEl, clientEl, path, parentTag);
105
+ if (attrDivergence) return attrDivergence;
106
+ return diffChildren(serverEl, clientEl, path);
107
+ }
108
+ return null;
109
+ }
110
+ function diffAttributes(serverEl, clientEl, path, parentTag) {
111
+ const names = /* @__PURE__ */ new Set();
112
+ for (const a of Array.from(serverEl.attributes)) names.add(a.name);
113
+ for (const a of Array.from(clientEl.attributes)) names.add(a.name);
114
+ for (const name of names) {
115
+ if (IGNORED_ATTRIBUTES.has(name)) continue;
116
+ const serverValue = serverEl.getAttribute(name);
117
+ const clientValue = clientEl.getAttribute(name);
118
+ if (normalizeAttr(name, serverValue) === normalizeAttr(name, clientValue)) {
119
+ continue;
120
+ }
121
+ return {
122
+ kind: "attribute",
123
+ path,
124
+ tagName: clientEl.tagName,
125
+ parentTagName: parentTag,
126
+ attribute: name,
127
+ server: serverValue,
128
+ client: clientValue,
129
+ element: clientEl
130
+ };
131
+ }
132
+ return null;
133
+ }
134
+ function normalizeAttr(name, value) {
135
+ if (value == null) return null;
136
+ if (name === "class") {
137
+ return value.trim().split(/\s+/).filter(Boolean).sort().join(" ");
138
+ }
139
+ if (name === "style") {
140
+ return value.split(";").map((s) => s.trim()).filter(Boolean).sort().join(";");
141
+ }
142
+ return value;
143
+ }
144
+ var NOISE_TAGS = /* @__PURE__ */ new Set([
145
+ "SCRIPT",
146
+ "STYLE",
147
+ "LINK",
148
+ "TEMPLATE",
149
+ "NOSCRIPT"
150
+ ]);
151
+ function isNoiseElement(node) {
152
+ if (node.nodeType !== Node.ELEMENT_NODE) return false;
153
+ const el = node;
154
+ if (el.hasAttribute("data-why-hydration")) return true;
155
+ if (NOISE_TAGS.has(el.tagName)) return true;
156
+ if (el.tagName.includes("-ROUTE-ANNOUNCER")) return true;
157
+ return false;
158
+ }
159
+ function meaningfulChildNodes(parent) {
160
+ const children = Array.from(parent.childNodes);
161
+ const hasElement = children.some(
162
+ (n) => n.nodeType === Node.ELEMENT_NODE && !isNoiseElement(n)
163
+ );
164
+ return children.filter((node) => {
165
+ if (isNoiseElement(node)) return false;
166
+ if (node.nodeType !== Node.TEXT_NODE) return true;
167
+ const text = node.textContent ?? "";
168
+ if (text.trim() !== "") return true;
169
+ return !hasElement;
170
+ });
171
+ }
172
+ function serialize(node) {
173
+ if (node.nodeType === Node.ELEMENT_NODE) {
174
+ return node.outerHTML;
175
+ }
176
+ return node.textContent ?? "";
177
+ }
178
+ function asElement(node) {
179
+ if (!node) return null;
180
+ return node.nodeType === Node.ELEMENT_NODE ? node : null;
181
+ }
182
+ function elementTag(node) {
183
+ return node.nodeType === Node.ELEMENT_NODE ? node.tagName : void 0;
184
+ }
185
+ function tagPath(el) {
186
+ return el.tagName.toLowerCase();
187
+ }
188
+ function childPath(parentPath, node, index) {
189
+ if (node.nodeType === Node.ELEMENT_NODE) {
190
+ const tag = node.tagName.toLowerCase();
191
+ return `${parentPath} > ${tag}:nth-child(${index + 1})`;
192
+ }
193
+ if (node.nodeType === Node.COMMENT_NODE) {
194
+ return `${parentPath} > #comment[${index}]`;
195
+ }
196
+ return `${parentPath} > #text[${index}]`;
197
+ }
198
+
199
+ // src/core/classify/detectors.ts
200
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
201
+ var REACT_ID_RE = /^:[rR][0-9a-z]*:$/;
202
+ var HEX_TOKEN_RE = /^[0-9a-f]{16,}$/i;
203
+ var RANDOM_DECIMAL_RE = /^0?\.\d{6,}$/;
204
+ var NANOID_RE = /^[A-Za-z0-9_-]{10,}$/;
205
+ var ARABIC_INDIC_DIGITS = /[٠-٩۰-۹]/;
206
+ var LATIN_DIGITS = /[0-9]/;
207
+ function isRandomLike(value) {
208
+ const v = value.trim();
209
+ if (!v) return false;
210
+ if (UUID_RE.test(v)) return true;
211
+ if (REACT_ID_RE.test(v)) return true;
212
+ if (HEX_TOKEN_RE.test(v)) return true;
213
+ if (RANDOM_DECIMAL_RE.test(v)) return true;
214
+ if (NANOID_RE.test(v) && /[A-Za-z]/.test(v) && /[0-9]/.test(v)) return true;
215
+ return false;
216
+ }
217
+ function looksLikeTime(value) {
218
+ return /\b\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AaPp][Mm])?\b/.test(value.trim());
219
+ }
220
+ function toTimestamp(value) {
221
+ const v = value.trim();
222
+ if (!v) return null;
223
+ if (/^\d{10,13}$/.test(v)) {
224
+ const n = Number(v);
225
+ return v.length === 10 ? n * 1e3 : n;
226
+ }
227
+ const parsed = Date.parse(v);
228
+ if (!Number.isNaN(parsed)) return parsed;
229
+ if (looksLikeTime(v)) {
230
+ const anchored = Date.parse(`1970-01-01 ${v}`);
231
+ if (!Number.isNaN(anchored)) return anchored;
232
+ }
233
+ return null;
234
+ }
235
+ function hasArabicIndicDigits(value) {
236
+ return ARABIC_INDIC_DIGITS.test(value);
237
+ }
238
+ function hasLatinDigits(value) {
239
+ return LATIN_DIGITS.test(value);
240
+ }
241
+ function isSameNumberDifferentSeparators(a, b) {
242
+ const digitsOnly = (s) => s.replace(/[^\d]/g, "");
243
+ const da = digitsOnly(a);
244
+ const db = digitsOnly(b);
245
+ if (!da || da !== db) return false;
246
+ const hasSep = (s) => /[.,\s\u00a0\u2009]/.test(s.trim());
247
+ return (hasSep(a) || hasSep(b)) && a.trim() !== b.trim();
248
+ }
249
+ function isSameDateDifferentOrder(a, b) {
250
+ const partsA = a.trim().match(/^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/);
251
+ const partsB = b.trim().match(/^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/);
252
+ if (!partsA || !partsB) return false;
253
+ const setA = [partsA[1], partsA[2], partsA[3]].sort().join("|");
254
+ const setB = [partsB[1], partsB[2], partsB[3]].sort().join("|");
255
+ return setA === setB && a.trim() !== b.trim();
256
+ }
257
+ var EXTENSION_ATTRIBUTES = /* @__PURE__ */ new Set([
258
+ "cz-shortcut-listen",
259
+ "data-gramm",
260
+ "data-gramm_editor",
261
+ "data-gramm_id",
262
+ "data-gr-c-s-loaded",
263
+ "data-lt-installed",
264
+ "data-new-gr-c-s-check-loaded",
265
+ "data-new-gr-c-s-loaded",
266
+ "spellcheck-extension",
267
+ "bis_register",
268
+ "__processed_by_react_dev_tools"
269
+ ]);
270
+ function isExtensionAttribute(name) {
271
+ const n = name.toLowerCase();
272
+ if (EXTENSION_ATTRIBUTES.has(n)) return true;
273
+ return n.startsWith("data-gr-") || n.startsWith("data-gramm") || n.startsWith("__bis") || n.startsWith("bis_");
274
+ }
275
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
276
+ "DIV",
277
+ "P",
278
+ "SECTION",
279
+ "ARTICLE",
280
+ "UL",
281
+ "OL",
282
+ "LI",
283
+ "TABLE",
284
+ "HEADER",
285
+ "FOOTER",
286
+ "MAIN",
287
+ "ASIDE",
288
+ "NAV",
289
+ "H1",
290
+ "H2",
291
+ "H3",
292
+ "H4",
293
+ "H5",
294
+ "H6",
295
+ "FORM",
296
+ "BLOCKQUOTE",
297
+ "PRE",
298
+ "HR"
299
+ ]);
300
+ function isInvalidNesting(parentTag, childTag) {
301
+ if (!parentTag || !childTag) return false;
302
+ const p = parentTag.toUpperCase();
303
+ const c = childTag.toUpperCase();
304
+ if (p === "P" && BLOCK_TAGS.has(c)) return true;
305
+ if (p === "A" && c === "A") return true;
306
+ if (p === "BUTTON" && (c === "BUTTON" || c === "A")) return true;
307
+ if ((p === "TABLE" || p === "THEAD" || p === "TBODY") && c === "DIV") {
308
+ return true;
309
+ }
310
+ return false;
311
+ }
312
+ function messageIndicatesInvalidNesting(message) {
313
+ if (!message) return false;
314
+ return /validateDOMNesting/i.test(message) || /cannot (?:be a|contain).*(?:descendant|child)/i.test(message) || /cannot appear as a (?:child|descendant)/i.test(message);
315
+ }
316
+
317
+ // src/core/classify/rules.ts
318
+ var DOCS_BASE = "https://github.com/razan-aboushi/why-hydration#cause-";
319
+ function docs(category) {
320
+ return `${DOCS_BASE}${category}`;
321
+ }
322
+ function bothDiffer(d) {
323
+ return d.server != null && d.client != null && d.server.trim() !== d.client.trim();
324
+ }
325
+ var nonDeterministic = (d) => {
326
+ if (d.kind !== "text" && d.kind !== "attribute") return null;
327
+ if (!bothDiffer(d)) return null;
328
+ if (!isRandomLike(d.server) || !isRandomLike(d.client)) return null;
329
+ return {
330
+ category: "non-deterministic-value",
331
+ confidence: 0.9,
332
+ 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.",
333
+ 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.",
334
+ docsUrl: docs("non-deterministic-value")
335
+ };
336
+ };
337
+ var dateTime = (d) => {
338
+ if (d.kind !== "text" && d.kind !== "attribute") return null;
339
+ if (!bothDiffer(d)) return null;
340
+ const serverTs = toTimestamp(d.server);
341
+ const clientTs = toTimestamp(d.client);
342
+ const bothTimes = looksLikeTime(d.server) && looksLikeTime(d.client);
343
+ if (serverTs == null || clientTs == null) {
344
+ if (!bothTimes) return null;
345
+ }
346
+ const delta = serverTs != null && clientTs != null ? Math.abs(serverTs - clientTs) : 0;
347
+ const smallDelta = delta > 0 && delta < 24 * 60 * 60 * 1e3;
348
+ return {
349
+ category: "date-time",
350
+ confidence: smallDelta || bothTimes ? 0.85 : 0.7,
351
+ 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.",
352
+ 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.",
353
+ docsUrl: docs("date-time")
354
+ };
355
+ };
356
+ var localeFormat = (d) => {
357
+ if (d.kind !== "text" && d.kind !== "attribute") return null;
358
+ if (!bothDiffer(d)) return null;
359
+ const { server, client } = d;
360
+ const scriptMismatch = hasArabicIndicDigits(server) && hasLatinDigits(client) || hasLatinDigits(server) && hasArabicIndicDigits(client);
361
+ if (scriptMismatch) {
362
+ return {
363
+ category: "locale-format",
364
+ confidence: 0.92,
365
+ 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.",
366
+ 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.",
367
+ docsUrl: docs("locale-format")
368
+ };
369
+ }
370
+ if (isSameNumberDifferentSeparators(server, client)) {
371
+ return {
372
+ category: "locale-format",
373
+ confidence: 0.82,
374
+ explanation: "The same number was formatted with different grouping/decimal separators between server and client (e.g. 1,234.56 vs 1.234,56).",
375
+ suggestion: "Pass an explicit locale to `Intl.NumberFormat`/`toLocaleString` on both sides so the separators match.",
376
+ docsUrl: docs("locale-format")
377
+ };
378
+ }
379
+ if (isSameDateDifferentOrder(server, client)) {
380
+ return {
381
+ category: "locale-format",
382
+ confidence: 0.75,
383
+ explanation: "The same date was rendered in a different field order (MM/DD vs DD/MM) between server and client.",
384
+ suggestion: "Format dates with an explicit locale and timezone via `Intl` on both sides.",
385
+ docsUrl: docs("locale-format")
386
+ };
387
+ }
388
+ return null;
389
+ };
390
+ var browserOnlyApi = (d) => {
391
+ const serverEmpty = d.server == null || d.server.trim() === "";
392
+ const clientHasContent = d.client != null && d.client.trim() !== "";
393
+ const shape = (d.kind === "text" || d.kind === "node-added") && serverEmpty && clientHasContent;
394
+ if (!shape) return null;
395
+ return {
396
+ category: "browser-only-api",
397
+ confidence: 0.75,
398
+ 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.",
399
+ 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.",
400
+ docsUrl: docs("browser-only-api")
401
+ };
402
+ };
403
+ var viewportBranching = (d) => {
404
+ const structural = d.kind === "structure" || d.kind === "node-added" || d.kind === "node-removed";
405
+ if (!structural) return null;
406
+ if (isInvalidNesting(d.parentTagName, d.tagName) || messageIndicatesInvalidNesting(d.reactMessage)) {
407
+ return null;
408
+ }
409
+ return {
410
+ category: "viewport-branching",
411
+ confidence: 0.6,
412
+ 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.",
413
+ 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.",
414
+ docsUrl: docs("viewport-branching")
415
+ };
416
+ };
417
+ var invalidNesting = (d) => {
418
+ const byMessage = messageIndicatesInvalidNesting(d.reactMessage);
419
+ const byShape = isInvalidNesting(d.parentTagName, d.tagName);
420
+ if (!byMessage && !byShape) return null;
421
+ return {
422
+ category: "invalid-html-nesting",
423
+ confidence: byMessage ? 0.9 : 0.72,
424
+ 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.",
425
+ 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.",
426
+ docsUrl: docs("invalid-html-nesting")
427
+ };
428
+ };
429
+ var whitespaceMinification = (d) => {
430
+ if (d.kind !== "text") return null;
431
+ if (d.server == null || d.client == null) return null;
432
+ if (d.server === d.client) return null;
433
+ const collapse = (s) => s.replace(/\s+/g, " ").trim();
434
+ if (collapse(d.server) !== collapse(d.client)) return null;
435
+ return {
436
+ category: "whitespace-minification",
437
+ confidence: 0.7,
438
+ 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.",
439
+ suggestion: "Check your HTML minifier settings (e.g. `conservativeCollapse`) around the app root, or avoid minifying whitespace inside hydrated markup.",
440
+ docsUrl: docs("whitespace-minification")
441
+ };
442
+ };
443
+ var thirdPartyDomMutation = (d) => {
444
+ if (d.kind === "attribute" && d.attribute) {
445
+ if (isExtensionAttribute(d.attribute)) {
446
+ return {
447
+ category: "third-party-dom-mutation",
448
+ confidence: 0.88,
449
+ explanation: `The attribute \`${d.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.`,
450
+ suggestion: "This is usually harmless. Add `suppressHydrationWarning` to the affected element, or defer third-party script init until after hydration.",
451
+ docsUrl: docs("third-party-dom-mutation")
452
+ };
453
+ }
454
+ const atRoot = /^(html|body)\b/.test(d.path) && d.server == null;
455
+ if (atRoot) {
456
+ return {
457
+ category: "third-party-dom-mutation",
458
+ confidence: 0.6,
459
+ explanation: `An attribute (\`${d.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.`,
460
+ suggestion: "Add `suppressHydrationWarning` to the root element, or defer the third-party script until after hydration.",
461
+ docsUrl: docs("third-party-dom-mutation")
462
+ };
463
+ }
464
+ }
465
+ return null;
466
+ };
467
+ var BUILT_IN_RULES = [
468
+ nonDeterministic,
469
+ dateTime,
470
+ localeFormat,
471
+ browserOnlyApi,
472
+ viewportBranching,
473
+ invalidNesting,
474
+ whitespaceMinification,
475
+ thirdPartyDomMutation
476
+ ];
477
+ var UNKNOWN_CAUSE = {
478
+ category: "unknown",
479
+ confidence: 0,
480
+ explanation: "A hydration mismatch was detected but could not be matched to a known cause. Inspect the server vs client values above.",
481
+ suggestion: "Compare the server and client values. Common causes are non-deterministic values, dates/locales, and browser-only APIs used during render.",
482
+ docsUrl: docs("unknown")
483
+ };
484
+
485
+ // src/core/classify/index.ts
486
+ var CONFIDENCE_THRESHOLD = 0.5;
487
+ function classify(divergence, options = {}) {
488
+ const threshold = options.threshold ?? CONFIDENCE_THRESHOLD;
489
+ const rules = [
490
+ ...options.extra ?? [],
491
+ ...BUILT_IN_RULES
492
+ ];
493
+ for (const rule of rules) {
494
+ let result = null;
495
+ try {
496
+ result = rule(divergence);
497
+ } catch {
498
+ result = null;
499
+ }
500
+ if (result && result.confidence >= threshold) {
501
+ return result;
502
+ }
503
+ }
504
+ return UNKNOWN_CAUSE;
505
+ }
506
+
507
+ // src/core/report.ts
508
+ var counter = 0;
509
+ function nextId() {
510
+ counter += 1;
511
+ return `wh_${Date.now().toString(36)}_${counter}`;
512
+ }
513
+ function truncate(value, max = 300) {
514
+ if (value == null) return null;
515
+ return value.length > max ? `${value.slice(0, max)}\u2026` : value;
516
+ }
517
+ function buildReport(divergence, cause, context = {}) {
518
+ return {
519
+ id: nextId(),
520
+ timestamp: Date.now(),
521
+ component: context.component,
522
+ componentStack: context.componentStack,
523
+ location: context.location,
524
+ node: {
525
+ path: divergence.path,
526
+ tagName: divergence.tagName,
527
+ attribute: divergence.attribute,
528
+ kind: divergence.kind
529
+ },
530
+ server: truncate(divergence.server),
531
+ client: truncate(divergence.client),
532
+ cause,
533
+ raw: context.reactMessage ? { reactMessage: context.reactMessage } : void 0
534
+ };
535
+ }
536
+ function signatureOf(report) {
537
+ return [
538
+ report.node.kind,
539
+ report.node.path,
540
+ report.node.attribute ?? "",
541
+ report.cause.category,
542
+ report.server ?? "",
543
+ report.client ?? ""
544
+ ].join("|");
545
+ }
546
+ var ReportCollector = class {
547
+ constructor(options = {}) {
548
+ this.seen = /* @__PURE__ */ new Set();
549
+ this.sinks = /* @__PURE__ */ new Set();
550
+ this.reports = [];
551
+ this.options = options;
552
+ this.maxReports = options.maxReports ?? 25;
553
+ }
554
+ addSink(sink) {
555
+ this.sinks.add(sink);
556
+ return () => this.sinks.delete(sink);
557
+ }
558
+ getReports() {
559
+ return this.reports;
560
+ }
561
+ get isFull() {
562
+ return this.reports.length >= this.maxReports;
563
+ }
564
+ report(divergence, context = {}) {
565
+ if (this.isFull) return null;
566
+ if (this.options.ignore?.(divergence)) return null;
567
+ const cause = classify(divergence, {
568
+ extra: this.options.extra,
569
+ threshold: this.options.threshold
570
+ });
571
+ const report = buildReport(divergence, cause, context);
572
+ const signature = signatureOf(report);
573
+ if (this.seen.has(signature)) return null;
574
+ this.seen.add(signature);
575
+ this.reports.push(report);
576
+ for (const sink of this.sinks) {
577
+ try {
578
+ sink(report);
579
+ } catch {
580
+ }
581
+ }
582
+ return report;
583
+ }
584
+ reset() {
585
+ this.seen.clear();
586
+ this.reports.length = 0;
587
+ }
588
+ };
589
+
590
+ // src/core/react-message.ts
591
+ function isHydrationMessage(message) {
592
+ return /hydrat/i.test(message) || /did not match/i.test(message) || /server (?:HTML|rendered)/i.test(message) || /validateDOMNesting/i.test(message) || /Text content does not match/i.test(message);
593
+ }
594
+ function formatConsoleArgs(args) {
595
+ if (args.length === 0) return "";
596
+ const [first, ...rest] = args;
597
+ if (typeof first === "string" && /%[sco]/.test(first)) {
598
+ let i = 0;
599
+ return first.replace(/%[sco]/g, () => String(rest[i++] ?? "")).trim();
600
+ }
601
+ return args.map((a) => typeof a === "string" ? a : String(a)).join(" ");
602
+ }
603
+ function parseHydrationMessage(message) {
604
+ if (!isHydrationMessage(message)) return null;
605
+ const text = /Text content (?:did not match|does not match)[^:]*Server:\s*"?(.*?)"?\s+Client:\s*"?(.*?)"?\s*$/i.exec(
606
+ message
607
+ );
608
+ if (text) {
609
+ return {
610
+ kind: "text",
611
+ path: "body",
612
+ server: text[1] ?? null,
613
+ client: text[2] ?? null,
614
+ reactMessage: message
615
+ };
616
+ }
617
+ const prop = /Prop [`'"]([^`'"]+)[`'"] did not match\.?\s*Server:\s*"?(.*?)"?\s+Client:\s*"?(.*?)"?\s*$/i.exec(
618
+ message
619
+ );
620
+ if (prop) {
621
+ return {
622
+ kind: "attribute",
623
+ path: "body",
624
+ attribute: prop[1],
625
+ server: prop[2] ?? null,
626
+ client: prop[3] ?? null,
627
+ reactMessage: message
628
+ };
629
+ }
630
+ const nesting = /<(\w+)>\s*cannot (?:appear as a|be a) (?:child|descendant) of <?(\w+)>?/i.exec(
631
+ message
632
+ );
633
+ if (nesting || /validateDOMNesting/i.test(message)) {
634
+ return {
635
+ kind: "structure",
636
+ path: "body",
637
+ tagName: nesting ? nesting[1]?.toUpperCase() : void 0,
638
+ parentTagName: nesting ? nesting[2]?.toUpperCase() : void 0,
639
+ server: null,
640
+ client: null,
641
+ reactMessage: message
642
+ };
643
+ }
644
+ const expected = /Expected server HTML to contain a matching <(\w+)>(?: in <(\w+)>)?/i.exec(
645
+ message
646
+ );
647
+ if (expected) {
648
+ return {
649
+ kind: "node-added",
650
+ path: "body",
651
+ tagName: expected[1]?.toUpperCase(),
652
+ parentTagName: expected[2]?.toUpperCase(),
653
+ server: null,
654
+ client: `<${expected[1]?.toLowerCase()}>`,
655
+ reactMessage: message
656
+ };
657
+ }
658
+ const notExpected = /Did not expect server HTML to contain(?: the text node)?(?: a)? <?(\w+)>?/i.exec(
659
+ message
660
+ );
661
+ if (notExpected) {
662
+ return {
663
+ kind: "node-removed",
664
+ path: "body",
665
+ tagName: notExpected[1]?.toUpperCase(),
666
+ server: `<${notExpected[1]?.toLowerCase()}>`,
667
+ client: null,
668
+ reactMessage: message
669
+ };
670
+ }
671
+ return {
672
+ kind: "structure",
673
+ path: "body",
674
+ server: null,
675
+ client: null,
676
+ reactMessage: message
677
+ };
678
+ }
679
+
680
+ // src/core/inspect.ts
681
+ function inspectRoot(root, collector, context = {}) {
682
+ const serverHtml = chunkOGQEPU7G_cjs.getServerHtmlForRoot(root);
683
+ if (serverHtml == null) return false;
684
+ const divergence = diffSnapshotAgainstDom(serverHtml, root);
685
+ if (!divergence) return false;
686
+ if (context.reactMessage && !divergence.reactMessage) {
687
+ divergence.reactMessage = context.reactMessage;
688
+ }
689
+ return collector.report(divergence, context) != null;
690
+ }
691
+ function reportFromMessage(message, collector, context = {}) {
692
+ const divergence = parseHydrationMessage(message);
693
+ if (!divergence) return false;
694
+ return collector.report(divergence, {
695
+ ...context,
696
+ reactMessage: message
697
+ }) != null;
698
+ }
699
+
700
+ exports.BUILT_IN_RULES = BUILT_IN_RULES;
701
+ exports.CONFIDENCE_THRESHOLD = CONFIDENCE_THRESHOLD;
702
+ exports.ReportCollector = ReportCollector;
703
+ exports.UNKNOWN_CAUSE = UNKNOWN_CAUSE;
704
+ exports.buildReport = buildReport;
705
+ exports.classify = classify;
706
+ exports.diffSnapshotAgainstDom = diffSnapshotAgainstDom;
707
+ exports.diffTrees = diffTrees;
708
+ exports.formatConsoleArgs = formatConsoleArgs;
709
+ exports.inspectRoot = inspectRoot;
710
+ exports.isDev = isDev;
711
+ exports.isHydrationMessage = isHydrationMessage;
712
+ exports.parseHydrationMessage = parseHydrationMessage;
713
+ exports.parseServerHtml = parseServerHtml;
714
+ exports.reportFromMessage = reportFromMessage;
715
+ exports.signatureOf = signatureOf;
716
+ //# sourceMappingURL=chunk-EPJM3LH3.cjs.map
717
+ //# sourceMappingURL=chunk-EPJM3LH3.cjs.map