why-hydration 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +108 -1
  2. package/README.md +220 -33
  3. package/dist/{chunk-WQLUD25W.js → chunk-3OAI2ZHY.js} +375 -118
  4. package/dist/chunk-3OAI2ZHY.js.map +1 -0
  5. package/dist/{chunk-OGQEPU7G.cjs → chunk-DXPTZB3Z.cjs} +44 -2
  6. package/dist/chunk-DXPTZB3Z.cjs.map +1 -0
  7. package/dist/{chunk-DSO337ME.cjs → chunk-KX6G7I4Y.cjs} +384 -118
  8. package/dist/chunk-KX6G7I4Y.cjs.map +1 -0
  9. package/dist/chunk-MK3TYHKM.js +966 -0
  10. package/dist/chunk-MK3TYHKM.js.map +1 -0
  11. package/dist/chunk-R7IWOK5M.cjs +989 -0
  12. package/dist/chunk-R7IWOK5M.cjs.map +1 -0
  13. package/dist/{chunk-FV3PEJQE.js → chunk-XIM33ZGB.js} +44 -2
  14. package/dist/chunk-XIM33ZGB.js.map +1 -0
  15. package/dist/index.cjs +32 -24
  16. package/dist/index.d.cts +43 -5
  17. package/dist/index.d.ts +43 -5
  18. package/dist/index.js +2 -2
  19. package/dist/next/index.cjs +5 -5
  20. package/dist/next/index.d.cts +1 -1
  21. package/dist/next/index.d.ts +1 -1
  22. package/dist/next/index.js +3 -3
  23. package/dist/next/script.cjs +2 -2
  24. package/dist/next/script.js +1 -1
  25. package/dist/react.cjs +5 -5
  26. package/dist/react.d.cts +23 -3
  27. package/dist/react.d.ts +23 -3
  28. package/dist/react.js +3 -3
  29. package/dist/{types-6wTih14e.d.cts → types-C14vcpBO.d.cts} +9 -0
  30. package/dist/{types-6wTih14e.d.ts → types-C14vcpBO.d.ts} +9 -0
  31. package/package.json +1 -1
  32. package/dist/chunk-3KULWJ7A.js +0 -569
  33. package/dist/chunk-3KULWJ7A.js.map +0 -1
  34. package/dist/chunk-5CBGYE7A.cjs +0 -592
  35. package/dist/chunk-5CBGYE7A.cjs.map +0 -1
  36. package/dist/chunk-DSO337ME.cjs.map +0 -1
  37. package/dist/chunk-FV3PEJQE.js.map +0 -1
  38. package/dist/chunk-OGQEPU7G.cjs.map +0 -1
  39. package/dist/chunk-WQLUD25W.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,112 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - The overlay now speaks Arabic, and renders right-to-left data correctly in any language.
8
+
9
+ **Arabic overlay**
10
+
11
+ - On a page whose `<html lang>` is Arabic, the whole panel is in Arabic and
12
+ laid out right-to-left — title, buttons, category names, explanations, fixes,
13
+ and the scroll hint, with correct Arabic plurals. Every other page, including
14
+ right-to-left pages in languages the overlay does not translate, gets the
15
+ English panel as before.
16
+ - New `overlay={{ locale: 'auto' | 'en' | 'ar' }}` option. `'auto'` is the
17
+ default and follows `<html lang>`.
18
+ - The console output and the `explanation`/`suggestion` strings in `onReport`
19
+ stay English and are byte-identical to 0.1.5. Every built-in cause now also
20
+ carries `messageId` and `params`, which is how the overlay finds the
21
+ translation.
22
+
23
+ **Right-to-left rendering fixes**
24
+
25
+ - Arabic values no longer have their sentence punctuation moved to the wrong
26
+ end. Each value cell takes its direction from its own content, in either
27
+ panel language.
28
+ - Arabic values are drawn in a proportional face instead of a monospace
29
+ fallback, which rendered them with their letters disconnected.
30
+ - Selectors, component names, file paths and attribute names are isolated
31
+ left-to-right, and quoted values inside explanations are isolated one by one,
32
+ so brackets cannot mirror and tokens cannot swap places.
33
+ - The panel declares its own `lang`. Previously an English panel on an Arabic
34
+ page inherited `lang="ar"`, so screen readers voiced it with an Arabic voice.
35
+
36
+ **Other fixes**
37
+
38
+ - Invisible bidi marks (LRM, RLM, isolates…) are drawn as labelled badges in
39
+ the value cells, so the bidi-mark case no longer shows two identical-looking
40
+ values. Value cells also preserve whitespace, so whitespace-only mismatches
41
+ are visible.
42
+ - Component attribution from a React component stack no longer names a DOM tag
43
+ such as `<p>`; it names the nearest component you wrote.
44
+ - React's bare "hydration failed" message no longer adds an empty card next to
45
+ a real mismatch. When it is the only signal it is still reported, with text
46
+ that says React did not name the node, instead of "inspect the values above".
47
+ - React 19 hydration errors no longer produce junk reports. React 19 prints a
48
+ bulleted list of possible causes above its diff tree, and each bullet was
49
+ read as a removed ("server") diff line — five bogus "Unknown" reports per
50
+ mismatch, with the real client value paired against the first bullet. Only
51
+ the tree after React's hydration-mismatch link is read as a diff now.
52
+
53
+ ## 0.1.5
54
+
55
+ ### Patch Changes
56
+
57
+ - 7b16e20: Fix the inspector lifecycle, bound detection cost, and close detection gaps.
58
+
59
+ **Lifecycle**
60
+
61
+ - `createHydrationInspector()` is no longer dead after a React Strict Mode
62
+ remount. Its `<Provider>` only ever stopped the controller, so Strict Mode's
63
+ setup → cleanup → setup left the documented Vite/CRA/Remix integration
64
+ silently doing nothing for the rest of the session.
65
+ - The dev check no longer requires a `process` global. Vite, Rollup and esbuild
66
+ substitute `process.env.NODE_ENV` without defining `process` itself, which
67
+ compiled the old `typeof process !== 'undefined'` guard down to `false` and
68
+ disabled the inspector entirely in those bundlers.
69
+ - Scheduling races an animation frame against a timer, so a page that is hidden
70
+ at load — where the browser suspends `requestAnimationFrame` outright — is
71
+ still inspected.
72
+ - The console capture hands `console.error` back correctly when the same
73
+ listener subscribes twice, and its message cap now drops past-cap messages
74
+ instead of recording-but-forwarding them (which broke dedup exactly when the
75
+ cap was meant to engage).
76
+
77
+ **Cost**
78
+
79
+ - A burst of React warnings arriving in one frame triggers one diff pass rather
80
+ than one per warning.
81
+ - Captured server markup is parsed once per root and reused across the settling
82
+ window instead of being re-parsed on every pass.
83
+ - Retained warning text is bounded, so an app erroring in a render loop cannot
84
+ grow the per-pass cost without limit.
85
+
86
+ **Detection**
87
+
88
+ - `date-time` no longer fires on ordinary values. `Date.parse` accepts almost
89
+ anything — `100` is the year 100, `server-0` is the year 2000 — so prices,
90
+ counts and ids were being diagnosed as a clock or timezone drift. A value must
91
+ now look like a date by shape.
92
+ - Arabic formatting mismatches are detected when both sides use the same digit
93
+ script: grouping/decimal separators, field order and times in Arabic-Indic and
94
+ Persian digits previously fell through to `unknown`.
95
+ - Values differing only by invisible bidirectional control marks (LRM/RLM/ALM,
96
+ isolates) are detected and named. Different ICU versions emit different marks
97
+ for the same `Intl` call, so Node and the browser routinely produce strings
98
+ that look identical and are not.
99
+ - Report values are no longer truncated mid surrogate pair.
100
+
101
+ **Overlay**
102
+
103
+ - A panel re-mounted after being dismissed no longer reports a stale count or
104
+ promise scrolling it cannot do.
105
+
106
+ Note for anyone consuming `report.cause.category` in an `onReport` sink: the
107
+ `date-time` and `locale-format` fixes change which category some mismatches
108
+ resolve to.
109
+
3
110
  ## 0.1.4
4
111
 
5
112
  - **License: restored MIT.** Re-added `LICENSE` (MIT) and set
@@ -93,7 +200,7 @@ Router, React 18 & 19). **0.1.0 is broken in Next.js — use 0.1.1 or later.**
93
200
  the browser-normalized live DOM (`#hex` → `rgb()`, spacing) no longer produce
94
201
  a false `unknown` mismatch.
95
202
  - **`typesVersions`** added so subpath types resolve under `moduleResolution:
96
- "node"` (fixes `next build` type errors in apps using classic resolution).
203
+ "node"` (fixes `next build` type errors in apps using classic resolution).
97
204
 
98
205
  Verified live: locale-format, non-deterministic-value, date-time, and
99
206
  browser-only-api classify correctly in App Router and Pages Router; the tool is
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/why-hydration.svg)](https://www.npmjs.com/package/why-hydration)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/why-hydration.svg)](https://www.npmjs.com/package/why-hydration)
5
- [![minzipped size](https://img.shields.io/bundlephobia/minzip/why-hydration.svg)](https://bundlephobia.com/package/why-hydration)
5
+ [![CI](https://github.com/razan-aboushi/why-hydration/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/razan-aboushi/why-hydration/actions/workflows/ci.yml)
6
+ [![prod bundle: 99 B gzipped](https://img.shields.io/badge/prod%20bundle-99%20B%20gzipped-brightgreen)](#production-behavior)
7
+ [![node: >=18](https://img.shields.io/node/v/why-hydration)](#install)
6
8
  [![license: MIT](https://img.shields.io/npm/l/why-hydration.svg)](LICENSE)
7
9
 
8
10
  📦 **npm:** https://www.npmjs.com/package/why-hydration &nbsp;·&nbsp; 🐙 **GitHub:** https://github.com/razan-aboushi/why-hydration &nbsp;·&nbsp; 💼 **Author:** [Razan Aboushi](https://www.linkedin.com/in/razan-aboushi/)
@@ -58,9 +60,10 @@ together.
58
60
  incidental page noise.
59
61
  - 🌐 **Arabic-first locale detection** — digit-script mismatches (٠١٢ vs 012)
60
62
  are a first-class cause, not an afterthought.
61
- - 🔤 **RTL-safe overlay** — the overlay always renders left-to-right, even on
62
- pages with `<html dir="rtl">`, since its content (paths, values, code) is
63
- English.
63
+ - 🔤 **Speaks Arabic, right-to-left** — on an Arabic page (`<html lang="ar">`)
64
+ the overlay is in Arabic and laid out right-to-left; everywhere else it is in
65
+ English. Page data renders in its own direction in both, and invisible bidi
66
+ marks are shown instead of hidden.
64
67
  - 🫧 **Zero production cost** — every code path is gated behind
65
68
  `process.env.NODE_ENV`, and CI fails the build if the production bundle for
66
69
  any entry point isn't tree-shaken to a no-op.
@@ -288,6 +291,14 @@ React/Next.js's own internal markup markers.
288
291
  misattribute every following sibling. This is a bounded check, not a
289
292
  standing `MutationObserver`, so legitimate DOM changes from your app's own
290
293
  state updates after this window are never mistaken for a hydration issue.
294
+
295
+ The passes are also bounded in cost. React logs its warnings in bursts, so
296
+ every signal that lands in the same frame is coalesced into **one** diff
297
+ rather than one diff each, and the captured server markup — re-materialising
298
+ it is a full HTML parse of your server render — is parsed **once per root**
299
+ and reused across every pass. Scheduling races an animation frame against a
300
+ 50 ms timer, so a page that is hidden at load (where the browser suspends
301
+ `requestAnimationFrame` entirely) still gets inspected.
291
302
  4. **Classify.** Each divergence is passed through an ordered list of rules
292
303
  (see [Cause categories](#cause-categories)); the first rule whose
293
304
  confidence clears the threshold wins, otherwise the mismatch is reported as
@@ -321,6 +332,28 @@ production):
321
332
 
322
333
  A page with no mismatches renders **nothing** — no overlay, no console output.
323
334
 
335
+ ### Controlling the overlay
336
+
337
+ | Action | Effect |
338
+ | ------ | ------ |
339
+ | **Dismiss** button, or **Esc** | Removes the panel for the rest of the page load. |
340
+ | **✕** on the hint bar | Closes just the "scroll to see all" hint; the panel stays. |
341
+ | `overlay={false}` | Never mounts it at all — `onReport` and the console output still work. |
342
+ | `overlay={{ position }}` | `bottom-right` (default), `bottom-left`, `top-right`, `top-left`. |
343
+ | `overlay={{ locale }}` | `'auto'` (default) follows `<html lang>`; `'en'` or `'ar'` pins the language. See [RTL and Arabic support](#rtl-and-arabic-support). |
344
+
345
+ The overlay is a *view* over the collected reports, not the collector itself:
346
+ dismissing it does not stop detection, and `onReport` keeps firing. If a
347
+ mismatch is found after you dismissed it — a late signal, or a second hydration
348
+ error — the panel returns showing that mismatch, starting from a clean count
349
+ rather than resuming a stale one.
350
+
351
+ It renders in an isolated Shadow DOM, is never part of your app's tree, and is
352
+ excluded from its own diff, so it can never be mistaken for a mismatch.
353
+ Mismatched values are rendered as **text**, and "Learn more" links are
354
+ restricted to `http(s)` URLs, so nothing in a mismatched value can inject markup
355
+ or script into the panel.
356
+
324
357
  ### In a real app
325
358
 
326
359
  Captured from a production Next.js app: every mismatch on the page collected
@@ -337,21 +370,82 @@ there are more than fit:
337
370
 
338
371
  ## RTL and Arabic support
339
372
 
340
- The overlay works correctly in apps that render right-to-left, e.g. pages with
341
- `<html dir="rtl">` for Arabic, Hebrew, or other RTL locales:
342
-
343
- - The overlay's own layout **always renders left-to-right**. Its content —
344
- file paths, DOM selectors, code values, category names — is English, so
345
- keeping it LTR keeps it readable regardless of the host page's direction.
346
- - This is automatic. The overlay renders inside an isolated Shadow DOM and
347
- explicitly sets its own `direction`, so it does not inherit `dir="rtl"`
348
- from the host page and does not mirror its layout. No configuration is
349
- needed.
350
- - **Detection itself is locale-agnostic.** The [`locale-format`](#cause-locale-format)
351
- category specifically detects Arabic-Indic vs. Latin digit-script mismatches
352
- (`٠١٢` vs `012`), which is a common real-world source of hydration
353
- mismatches in Arabic-first apps that format numbers with `Intl` or
354
- `toLocaleString` without pinning an explicit locale.
373
+ An Arabic app gets the same diagnosis quality as an English one. That covers
374
+ both how the overlay renders and what the engine can actually detect.
375
+
376
+ ### The overlay
377
+
378
+ **The overlay speaks the page's language.** On a page whose `<html lang>` is
379
+ Arabic (`ar`, `ar-SA`, `ar-EG`, …) the whole panel is in Arabic and laid out
380
+ right-to-left: title, buttons, category names, the explanation, the fix, and
381
+ the scroll hint, with correct Arabic plurals. Every other page gets the English
382
+ panel — including right-to-left pages in languages the overlay does not
383
+ translate, such as Hebrew or Persian, where it stays left-to-right rather than
384
+ mirroring English text. Pin it either way with `overlay={{ locale: 'en' }}` or
385
+ `overlay={{ locale: 'ar' }}`.
386
+
387
+ What stays English: the console output, and `cause.explanation` /
388
+ `cause.suggestion` in the report `onReport` receives. Those are what people log,
389
+ grep and paste into issues, so they do not change with the page. Each report
390
+ also carries `cause.messageId` and `cause.params`, which is how the overlay
391
+ finds the translation — and how your own tooling can, too. The "Learn more"
392
+ links point at this README, which is in English, and say so.
393
+
394
+ **Page data renders in its own direction, in either panel.** A mismatched value
395
+ can be in any script, so each value cell takes its direction from its own first
396
+ strong character. `السعر: ١٬٤٠٠ د.ك.` reads right-to-left with its full stop at
397
+ the end even inside the English panel, and `Price: 1,400 KWD.` reads
398
+ left-to-right inside the Arabic one. Selectors, component names, file paths and
399
+ attribute names are code, so they are always isolated left-to-right — otherwise
400
+ a right-to-left line would mirror their brackets and reorder their segments.
401
+ Class tokens and other values quoted inside a sentence are isolated one by one,
402
+ so two Arabic tokens cannot swap places. Arabic is drawn in a proportional face
403
+ (a monospace fallback renders it with its letters disconnected), and Arabic
404
+ labels are never letter-spaced or uppercased, which breaks their joining.
405
+
406
+ **What differs is what you see.** Value cells preserve whitespace, so a
407
+ whitespace-only mismatch no longer looks like two identical strings. Invisible
408
+ characters that change layout — `LRM`, `RLM`, `ALM`, the bidi embeddings and
409
+ isolates, zero-width space, BOM — are drawn as small labelled badges rather than
410
+ applied, so the [bidi-mark case](#cause-locale-format) shows a visible
411
+ difference instead of two cells that look the same.
412
+
413
+ **It is isolated from the page.** The panel lives in a Shadow DOM, and its
414
+ direction is set inside it, so nothing in the host page's CSS can flip it — the
415
+ `all` shorthand deliberately excludes `direction` (per spec), so
416
+ `:host { all: initial }` alone would not be enough. The panel also declares its
417
+ own `lang`, so a screen reader voices it in its own language: before this, an
418
+ English panel on an Arabic page was read aloud with an Arabic voice.
419
+
420
+ ### Detection
421
+
422
+ Detection is direction-agnostic: every rule matches on the *shape* of a value,
423
+ not its script. Three Arabic-specific cases are worth calling out, because the
424
+ first is the one most people expect and the other two are the ones that
425
+ actually bite:
426
+
427
+ - **Different digit scripts.** Arabic-Indic `٠١٢` on one side, Latin `012` on
428
+ the other — the classic symptom of `Intl`/`toLocaleString` resolving to a
429
+ different locale on the server than in the browser. Reported as
430
+ [`locale-format`](#cause-locale-format) at 92% confidence.
431
+ - **Same digit script, different formatting.** An Arabic-first app renders
432
+ Arabic-Indic digits on *both* sides, so there is no script difference to key
433
+ off — only a grouping separator (`١٬٤٠٠` vs `١٤٠٠`), a decimal separator
434
+ (`١٢٣٤٫٥٦` vs `١٢٣٤.٥٦`), a field order, or a time. These are folded to Latin
435
+ before the numeric/date shape tests run, so they are classified exactly like
436
+ their English equivalents instead of falling through to `unknown`. Persian /
437
+ Extended Arabic-Indic digits (`۰۱۲`) are handled the same way.
438
+ - **Invisible bidi marks.** `Intl` wraps numbers and date fields in
439
+ bidirectional control characters (LRM, RLM, ALM, isolates) in RTL locales,
440
+ and *which* ones it emits differs between ICU versions — so Node and the
441
+ browser routinely format the same date into strings that are visually
442
+ identical and byte-different. This is the hardest hydration mismatch to debug
443
+ by eye, since the console diff looks like the same text twice. It is detected
444
+ and named explicitly.
445
+
446
+ Both directions are covered by the test suite as a matched pair
447
+ (`test/i18n.test.tsx`), plus overlay directionality in `test/direction.test.ts`,
448
+ so English and Arabic behaviour cannot drift apart.
355
449
 
356
450
  ---
357
451
 
@@ -414,11 +508,19 @@ interface HydrationInspectorHandle {
414
508
  }
415
509
  ```
416
510
 
511
+ `Provider` owns the inspector's lifetime: it must actually be mounted, and
512
+ unmounting it tears the inspector down (overlay removed, `console.error`
513
+ handed back untouched). Mounting it again restarts detection cleanly rather
514
+ than stacking a second overlay or a second console patch — which is what makes
515
+ it safe under `<React.StrictMode>`, where React deliberately runs every mount
516
+ effect setup → cleanup → setup.
517
+
417
518
  ### `OverlayOptions`
418
519
 
419
520
  ```ts
420
521
  interface OverlayOptions {
421
522
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; // default 'bottom-right'
523
+ locale?: 'auto' | 'en' | 'ar'; // default 'auto' — follows <html lang>
422
524
  }
423
525
  ```
424
526
 
@@ -467,9 +569,11 @@ interface HydrationReport {
467
569
  cause: {
468
570
  category: HydrationCauseCategory; // one of the categories below, or "unknown"
469
571
  confidence: number; // 0–1
470
- explanation: string;
471
- suggestion: string;
572
+ explanation: string; // always English
573
+ suggestion: string; // always English
472
574
  docsUrl?: string;
575
+ messageId?: string; // which message produced the text, e.g. "attribute-mismatch.class"
576
+ params?: Record<string, string | string[]>; // the values in it, e.g. { added: ['wide'], removed: [] }
473
577
  };
474
578
  raw?: { reactMessage?: string }; // the original React console message, if any
475
579
  }
@@ -508,6 +612,14 @@ Server and client rendered different random-looking values (UUID, token, React
508
612
 
509
613
  Values are dates/times that differ by a small delta — the clock or timezone
510
614
  moved between server and client render.
615
+
616
+ Matched on **shape**, not on whether `Date.parse` happens to accept the value:
617
+ ISO dates, `D/M/YYYY`-style dates, clock times, month names with a number, and
618
+ 10–13 digit epoch timestamps. That distinction matters because `Date.parse` is
619
+ extremely permissive — it reads `100` as the year 100 and `server-0` as the
620
+ year 2000 — so an ordinary price, count, or id would otherwise be diagnosed as
621
+ a clock drift.
622
+
511
623
  **Fix:** render time after mount, or pass one server timestamp down and pin
512
624
  the timezone when formatting.
513
625
  **Reference:** [React — different client/server content](https://react.dev/reference/react-dom/client/hydrateRoot#handling-different-client-and-server-content).
@@ -518,11 +630,21 @@ the timezone when formatting.
518
630
 
519
631
  <img src="docs/screenshots/cause-locale-format.png" alt="locale-format report" width="420">
520
632
 
521
- Same underlying value, different formatting: **Arabic-Indic ٠١٢ vs Latin 012**,
522
- decimal/thousand separators (`1,234.56` vs `1.234,56`), or date field order
523
- (MM/DD vs DD/MM).
633
+ Same underlying value, different formatting. Covers:
634
+
635
+ - **Different digit scripts** — Arabic-Indic `٠١٢` vs Latin `012`, or Persian
636
+ `۰۱۲` vs Latin.
637
+ - **Different separators or field order** — `1,234.56` vs `1.234,56`,
638
+ `١٬٤٠٠` vs `١٤٠٠`, MM/DD vs DD/MM. Detected in Arabic-Indic and Persian
639
+ digits as well as Latin, so an app that renders the same digit script on both
640
+ sides is still diagnosed.
641
+ - **Invisible bidirectional marks** — values that are identical on screen but
642
+ differ by LRM/RLM/ALM or isolate characters, which `Intl` adds around numbers
643
+ and dates in RTL locales and which different ICU versions (Node vs the
644
+ browser) emit differently.
645
+
524
646
  **Fix:** pass an explicit `locale` and timezone to `Intl` on both sides, or
525
- format after mount.
647
+ format after mount. See also [RTL and Arabic support](#rtl-and-arabic-support).
526
648
  **Reference:** [MDN `Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat).
527
649
 
528
650
  <a id="cause-third-party-dom-mutation"></a>
@@ -612,6 +734,13 @@ drive the visual change with a CSS media query instead of a JS class toggle.
612
734
  A mismatch was detected but didn't match any of the rules above. The report
613
735
  still shows the exact server vs. client values and node path so you can
614
736
  diagnose it directly.
737
+
738
+ React also logs a bare "hydration failed" message that names no node at all.
739
+ When the DOM diff or another React message has already located the mismatch,
740
+ that message adds nothing and is not shown. When it is the *only* signal —
741
+ typically because the snapshot script is missing — it is reported once, with an
742
+ explanation that says React did not name the node and a fix that points at the
743
+ snapshot script, rather than asking you to inspect values that do not exist.
615
744
  **Fix:** compare the two values — the cause is usually one of the categories
616
745
  above. If you find a reliable signal for it, add a custom rule via the
617
746
  `classify` option (see [Contributing](#contributing)).
@@ -628,11 +757,24 @@ above. If you find a reliable signal for it, add a custom rule via the
628
757
  check, written so bundlers can statically fold it away. In a production
629
758
  build, `<HydrationInspector>` becomes a plain pass-through and
630
759
  `createHydrationInspector()` returns no-op functions.
760
+ - Production is **opt-out**, not opt-in: the internal dev check treats an
761
+ explicit `NODE_ENV === 'production'` as production and everything else,
762
+ including a bundle where `process` was never defined at all, as development.
763
+ That matters because Vite, Rollup and esbuild substitute
764
+ `process.env.NODE_ENV` without shimming `process` itself — requiring
765
+ `process` to exist would silently disable the inspector for all of them. It
766
+ cannot leak dev code into a production bundle, because the entry-point gates
767
+ above have already been folded away by then.
631
768
  - This is enforced, not just claimed: the project's CI pipeline runs a size
632
769
  budget (`npm run size`) against a real production bundle built with webpack
633
770
  and terser — the same toolchain Next.js and CRA use for production — and
634
771
  fails the build if the tree-shaken output for any entry point isn't reduced
635
- to a near-empty stub.
772
+ to a near-empty stub. The budgets sit just above today's output, so even a
773
+ small leak fails CI. Verified independently against Rollup, which is what
774
+ Vite uses for production builds: **99 B** minified + gzipped under webpack,
775
+ and **156 B** minified (not gzipped) under Rollup — versus ~59 KB minified
776
+ for the same entry built for development. The Arabic translations exist only
777
+ in that development build.
636
778
  - `<HydrationSnapshotScript>` also renders `null` outside development, so no
637
779
  snapshot script is emitted into your production HTML.
638
780
 
@@ -662,6 +804,16 @@ above. If you find a reliable signal for it, add a custom rule via the
662
804
  - **Bounded, not continuous.** Detection re-checks the DOM a handful of times
663
805
  in the ~1.5 seconds after hydration and then stops — there is no standing
664
806
  `MutationObserver` watching your app for the rest of its lifetime.
807
+ - **Bounded in cost, too.** A burst of React warnings landing in one frame
808
+ triggers one diff pass, not one per warning. The captured server markup is
809
+ parsed once per root and reused across passes rather than re-parsed each
810
+ time. Retained warning text is capped, so an app erroring in a render loop
811
+ cannot grow the per-pass cost without limit, and reports are capped by
812
+ `maxReports` (default 25).
813
+ - **Symmetrical teardown.** Unmounting the inspector restores `console.error`
814
+ to exactly the function it replaced, removes the overlay, and clears every
815
+ pending timer — so a remount (including React Strict Mode's deliberate
816
+ double-mount in dev) leaves one overlay and one console patch, not two.
665
817
 
666
818
  ---
667
819
 
@@ -673,11 +825,30 @@ Remix, or a custom SSR setup. Use `createHydrationInspector` where you own
673
825
  The core engine (`why-hydration`) is framework-agnostic.
674
826
 
675
827
  **Nothing shows up, but I know there's a mismatch.** Confirm `NODE_ENV` isn't
676
- `production`, that `<HydrationInspector>` (or its `Provider`) actually wraps
677
- the part of the tree that mismatches, and that the snapshot script is present
678
- in `<head>` and runs before your app's hydration script. Without the snapshot
679
- script, the tool still reports mismatches it can parse from React's own
680
- console warning, but loses the precise DOM-level diff.
828
+ `production`, that `<HydrationInspector>` (or its `Provider`) is actually
829
+ **mounted** and wraps the part of the tree that mismatches, and that the
830
+ snapshot script is present in `<head>` and runs before your app's hydration
831
+ script. Without the snapshot script, the tool still reports mismatches it can
832
+ parse from React's own console warning, but loses the precise DOM-level diff.
833
+ If you passed `roots`, check the console for a
834
+ `[why-hydration] Skipping root:` warning — a root with no captured server HTML
835
+ can never produce a DOM-level report, and it says so rather than failing
836
+ silently.
837
+
838
+ **Does it work under `<React.StrictMode>`?** Yes. Strict Mode runs every mount
839
+ effect setup → cleanup → setup in development, which tears the inspector down
840
+ and rebuilds it. You get one overlay, one console patch, and each mismatch
841
+ reported once — the warnings React logged during the first pass are replayed
842
+ to the rebuilt inspector rather than lost.
843
+
844
+ **I loaded the page in a background tab and got nothing.** Fixed — browsers
845
+ suspend `requestAnimationFrame` on hidden pages, so scheduling races a frame
846
+ against a 50 ms timer and no longer depends on the page being painted.
847
+
848
+ **The component name in a report was a tag like `<p>`.** Fixed. React's
849
+ component stacks list host elements first (`at p`, `at span`), and those were
850
+ being taken as the component. The nearest component you wrote is used instead,
851
+ and when there is none the report names no component rather than a tag.
681
852
 
682
853
  **The "Learn more →" link 404s.** The links point at this README on GitHub
683
854
  (`github.com/razan-aboushi/why-hydration#cause-…`). If you've forked the
@@ -700,8 +871,24 @@ production. It's dev-only, runs a bounded diff across roughly 1.5 seconds
700
871
  right after hydration, then stops.
701
872
 
702
873
  **Does it work with `<html dir="rtl">`?** Yes — see
703
- [RTL and Arabic support](#rtl-and-arabic-support). The overlay stays
704
- left-to-right on purpose; this is not a bug.
874
+ [RTL and Arabic support](#rtl-and-arabic-support). The panel's language follows
875
+ `<html lang>`, not `dir`: an Arabic page gets the Arabic right-to-left panel,
876
+ and a right-to-left page in another language gets the English left-to-right
877
+ one. Detection covers Arabic-script formatting mismatches on both sides, not
878
+ just Arabic-vs-Latin.
879
+
880
+ **I want the English panel on my Arabic site (or the reverse).** Pass
881
+ `overlay={{ locale: 'en' }}` (or `'ar'`). The console and `onReport` are English
882
+ either way.
883
+
884
+ **My Arabic app reports a mismatch between two values that look the same.** They
885
+ differ by invisible bidirectional control characters — `Intl` adds LRM/RLM/isolate
886
+ marks around numbers and dates in RTL locales, and Node's ICU and the browser's
887
+ ICU do not always agree on which. The report names this explicitly under
888
+ [`locale-format`](#cause-locale-format), and the overlay draws each invisible
889
+ mark as a small labelled badge (`RLM`, `LRM`, …) in the value cell, so you can
890
+ see which side has it. Format the value in one place and pass the string down,
891
+ or add `suppressHydrationWarning` if the marks are harmless.
705
892
 
706
893
  ---
707
894