why-hydration 0.1.4 → 0.1.5
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 +58 -1
- package/README.md +158 -27
- package/dist/{chunk-3KULWJ7A.js → chunk-3I6F4K4L.js} +120 -47
- package/dist/chunk-3I6F4K4L.js.map +1 -0
- package/dist/{chunk-WQLUD25W.js → chunk-AS5DZZHI.js} +138 -53
- package/dist/chunk-AS5DZZHI.js.map +1 -0
- package/dist/{chunk-DSO337ME.cjs → chunk-AT6A77X3.cjs} +139 -54
- package/dist/chunk-AT6A77X3.cjs.map +1 -0
- package/dist/{chunk-OGQEPU7G.cjs → chunk-DXPTZB3Z.cjs} +44 -2
- package/dist/chunk-DXPTZB3Z.cjs.map +1 -0
- package/dist/{chunk-5CBGYE7A.cjs → chunk-OFVFNPE6.cjs} +129 -56
- package/dist/chunk-OFVFNPE6.cjs.map +1 -0
- package/dist/{chunk-FV3PEJQE.js → chunk-XIM33ZGB.js} +44 -2
- package/dist/chunk-XIM33ZGB.js.map +1 -0
- package/dist/index.cjs +24 -24
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +2 -2
- package/dist/next/index.cjs +5 -5
- package/dist/next/index.js +3 -3
- package/dist/next/script.cjs +2 -2
- package/dist/next/script.js +1 -1
- package/dist/react.cjs +5 -5
- package/dist/react.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-3KULWJ7A.js.map +0 -1
- package/dist/chunk-5CBGYE7A.cjs.map +0 -1
- package/dist/chunk-DSO337ME.cjs.map +0 -1
- package/dist/chunk-FV3PEJQE.js.map +0 -1
- package/dist/chunk-OGQEPU7G.cjs.map +0 -1
- package/dist/chunk-WQLUD25W.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7b16e20: Fix the inspector lifecycle, bound detection cost, and close detection gaps.
|
|
8
|
+
|
|
9
|
+
**Lifecycle**
|
|
10
|
+
|
|
11
|
+
- `createHydrationInspector()` is no longer dead after a React Strict Mode
|
|
12
|
+
remount. Its `<Provider>` only ever stopped the controller, so Strict Mode's
|
|
13
|
+
setup → cleanup → setup left the documented Vite/CRA/Remix integration
|
|
14
|
+
silently doing nothing for the rest of the session.
|
|
15
|
+
- The dev check no longer requires a `process` global. Vite, Rollup and esbuild
|
|
16
|
+
substitute `process.env.NODE_ENV` without defining `process` itself, which
|
|
17
|
+
compiled the old `typeof process !== 'undefined'` guard down to `false` and
|
|
18
|
+
disabled the inspector entirely in those bundlers.
|
|
19
|
+
- Scheduling races an animation frame against a timer, so a page that is hidden
|
|
20
|
+
at load — where the browser suspends `requestAnimationFrame` outright — is
|
|
21
|
+
still inspected.
|
|
22
|
+
- The console capture hands `console.error` back correctly when the same
|
|
23
|
+
listener subscribes twice, and its message cap now drops past-cap messages
|
|
24
|
+
instead of recording-but-forwarding them (which broke dedup exactly when the
|
|
25
|
+
cap was meant to engage).
|
|
26
|
+
|
|
27
|
+
**Cost**
|
|
28
|
+
|
|
29
|
+
- A burst of React warnings arriving in one frame triggers one diff pass rather
|
|
30
|
+
than one per warning.
|
|
31
|
+
- Captured server markup is parsed once per root and reused across the settling
|
|
32
|
+
window instead of being re-parsed on every pass.
|
|
33
|
+
- Retained warning text is bounded, so an app erroring in a render loop cannot
|
|
34
|
+
grow the per-pass cost without limit.
|
|
35
|
+
|
|
36
|
+
**Detection**
|
|
37
|
+
|
|
38
|
+
- `date-time` no longer fires on ordinary values. `Date.parse` accepts almost
|
|
39
|
+
anything — `100` is the year 100, `server-0` is the year 2000 — so prices,
|
|
40
|
+
counts and ids were being diagnosed as a clock or timezone drift. A value must
|
|
41
|
+
now look like a date by shape.
|
|
42
|
+
- Arabic formatting mismatches are detected when both sides use the same digit
|
|
43
|
+
script: grouping/decimal separators, field order and times in Arabic-Indic and
|
|
44
|
+
Persian digits previously fell through to `unknown`.
|
|
45
|
+
- Values differing only by invisible bidirectional control marks (LRM/RLM/ALM,
|
|
46
|
+
isolates) are detected and named. Different ICU versions emit different marks
|
|
47
|
+
for the same `Intl` call, so Node and the browser routinely produce strings
|
|
48
|
+
that look identical and are not.
|
|
49
|
+
- Report values are no longer truncated mid surrogate pair.
|
|
50
|
+
|
|
51
|
+
**Overlay**
|
|
52
|
+
|
|
53
|
+
- A panel re-mounted after being dismissed no longer reports a stale count or
|
|
54
|
+
promise scrolling it cannot do.
|
|
55
|
+
|
|
56
|
+
Note for anyone consuming `report.cause.category` in an `onReport` sink: the
|
|
57
|
+
`date-time` and `locale-format` fixes change which category some mismatches
|
|
58
|
+
resolve to.
|
|
59
|
+
|
|
3
60
|
## 0.1.4
|
|
4
61
|
|
|
5
62
|
- **License: restored MIT.** Re-added `LICENSE` (MIT) and set
|
|
@@ -93,7 +150,7 @@ Router, React 18 & 19). **0.1.0 is broken in Next.js — use 0.1.1 or later.**
|
|
|
93
150
|
the browser-normalized live DOM (`#hex` → `rgb()`, spacing) no longer produce
|
|
94
151
|
a false `unknown` mismatch.
|
|
95
152
|
- **`typesVersions`** added so subpath types resolve under `moduleResolution:
|
|
96
|
-
|
|
153
|
+
"node"` (fixes `next build` type errors in apps using classic resolution).
|
|
97
154
|
|
|
98
155
|
Verified live: locale-format, non-deterministic-value, date-time, and
|
|
99
156
|
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
|
[](https://www.npmjs.com/package/why-hydration)
|
|
4
4
|
[](https://www.npmjs.com/package/why-hydration)
|
|
5
|
-
[](https://github.com/razan-aboushi/why-hydration/actions/workflows/ci.yml)
|
|
6
|
+
[](#production-behavior)
|
|
7
|
+
[](#install)
|
|
6
8
|
[](LICENSE)
|
|
7
9
|
|
|
8
10
|
📦 **npm:** https://www.npmjs.com/package/why-hydration · 🐙 **GitHub:** https://github.com/razan-aboushi/why-hydration · 💼 **Author:** [Razan Aboushi](https://www.linkedin.com/in/razan-aboushi/)
|
|
@@ -288,6 +290,14 @@ React/Next.js's own internal markup markers.
|
|
|
288
290
|
misattribute every following sibling. This is a bounded check, not a
|
|
289
291
|
standing `MutationObserver`, so legitimate DOM changes from your app's own
|
|
290
292
|
state updates after this window are never mistaken for a hydration issue.
|
|
293
|
+
|
|
294
|
+
The passes are also bounded in cost. React logs its warnings in bursts, so
|
|
295
|
+
every signal that lands in the same frame is coalesced into **one** diff
|
|
296
|
+
rather than one diff each, and the captured server markup — re-materialising
|
|
297
|
+
it is a full HTML parse of your server render — is parsed **once per root**
|
|
298
|
+
and reused across every pass. Scheduling races an animation frame against a
|
|
299
|
+
50 ms timer, so a page that is hidden at load (where the browser suspends
|
|
300
|
+
`requestAnimationFrame` entirely) still gets inspected.
|
|
291
301
|
4. **Classify.** Each divergence is passed through an ordered list of rules
|
|
292
302
|
(see [Cause categories](#cause-categories)); the first rule whose
|
|
293
303
|
confidence clears the threshold wins, otherwise the mismatch is reported as
|
|
@@ -321,6 +331,27 @@ production):
|
|
|
321
331
|
|
|
322
332
|
A page with no mismatches renders **nothing** — no overlay, no console output.
|
|
323
333
|
|
|
334
|
+
### Controlling the overlay
|
|
335
|
+
|
|
336
|
+
| Action | Effect |
|
|
337
|
+
| ------ | ------ |
|
|
338
|
+
| **Dismiss** button, or **Esc** | Removes the panel for the rest of the page load. |
|
|
339
|
+
| **✕** on the hint bar | Closes just the "scroll to see all" hint; the panel stays. |
|
|
340
|
+
| `overlay={false}` | Never mounts it at all — `onReport` and the console output still work. |
|
|
341
|
+
| `overlay={{ position }}` | `bottom-right` (default), `bottom-left`, `top-right`, `top-left`. |
|
|
342
|
+
|
|
343
|
+
The overlay is a *view* over the collected reports, not the collector itself:
|
|
344
|
+
dismissing it does not stop detection, and `onReport` keeps firing. If a
|
|
345
|
+
mismatch is found after you dismissed it — a late signal, or a second hydration
|
|
346
|
+
error — the panel returns showing that mismatch, starting from a clean count
|
|
347
|
+
rather than resuming a stale one.
|
|
348
|
+
|
|
349
|
+
It renders in an isolated Shadow DOM, is never part of your app's tree, and is
|
|
350
|
+
excluded from its own diff, so it can never be mistaken for a mismatch.
|
|
351
|
+
Mismatched values are rendered as **text**, and "Learn more" links are
|
|
352
|
+
restricted to `http(s)` URLs, so nothing in a mismatched value can inject markup
|
|
353
|
+
or script into the panel.
|
|
354
|
+
|
|
324
355
|
### In a real app
|
|
325
356
|
|
|
326
357
|
Captured from a production Next.js app: every mismatch on the page collected
|
|
@@ -337,21 +368,54 @@ there are more than fit:
|
|
|
337
368
|
|
|
338
369
|
## RTL and Arabic support
|
|
339
370
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
371
|
+
An Arabic app gets the same diagnosis quality as an English one. That covers
|
|
372
|
+
both how the overlay renders and what the engine can actually detect.
|
|
373
|
+
|
|
374
|
+
### The overlay
|
|
375
|
+
|
|
376
|
+
- The overlay's own layout **always renders left-to-right**, on any page. Its
|
|
377
|
+
content — file paths, DOM selectors, code values, category names — is
|
|
378
|
+
English, so keeping it LTR keeps it readable regardless of the host page's
|
|
379
|
+
direction.
|
|
380
|
+
- This is automatic and needs no configuration. The overlay lives in an
|
|
381
|
+
isolated Shadow DOM, sets `direction: ltr` on both `:host` and the panel, and
|
|
382
|
+
carries a `dir="ltr"` attribute as well — belt and braces, because the CSS
|
|
383
|
+
`all` shorthand deliberately excludes `direction` (per spec), so
|
|
384
|
+
`:host { all: initial }` alone would still let `direction: rtl` leak in and
|
|
385
|
+
flip the server/client diff columns.
|
|
386
|
+
- Values are rendered as **text**, so Arabic, Hebrew and mixed bidi content
|
|
387
|
+
display intact inside the LTR panel without reordering the surrounding
|
|
388
|
+
layout.
|
|
389
|
+
|
|
390
|
+
### Detection
|
|
391
|
+
|
|
392
|
+
Detection is direction-agnostic: every rule matches on the *shape* of a value,
|
|
393
|
+
not its script. Three Arabic-specific cases are worth calling out, because the
|
|
394
|
+
first is the one most people expect and the other two are the ones that
|
|
395
|
+
actually bite:
|
|
396
|
+
|
|
397
|
+
- **Different digit scripts.** Arabic-Indic `٠١٢` on one side, Latin `012` on
|
|
398
|
+
the other — the classic symptom of `Intl`/`toLocaleString` resolving to a
|
|
399
|
+
different locale on the server than in the browser. Reported as
|
|
400
|
+
[`locale-format`](#cause-locale-format) at 92% confidence.
|
|
401
|
+
- **Same digit script, different formatting.** An Arabic-first app renders
|
|
402
|
+
Arabic-Indic digits on *both* sides, so there is no script difference to key
|
|
403
|
+
off — only a grouping separator (`١٬٤٠٠` vs `١٤٠٠`), a decimal separator
|
|
404
|
+
(`١٢٣٤٫٥٦` vs `١٢٣٤.٥٦`), a field order, or a time. These are folded to Latin
|
|
405
|
+
before the numeric/date shape tests run, so they are classified exactly like
|
|
406
|
+
their English equivalents instead of falling through to `unknown`. Persian /
|
|
407
|
+
Extended Arabic-Indic digits (`۰۱۲`) are handled the same way.
|
|
408
|
+
- **Invisible bidi marks.** `Intl` wraps numbers and date fields in
|
|
409
|
+
bidirectional control characters (LRM, RLM, ALM, isolates) in RTL locales,
|
|
410
|
+
and *which* ones it emits differs between ICU versions — so Node and the
|
|
411
|
+
browser routinely format the same date into strings that are visually
|
|
412
|
+
identical and byte-different. This is the hardest hydration mismatch to debug
|
|
413
|
+
by eye, since the console diff looks like the same text twice. It is detected
|
|
414
|
+
and named explicitly.
|
|
415
|
+
|
|
416
|
+
Both directions are covered by the test suite as a matched pair
|
|
417
|
+
(`test/i18n.test.tsx`), plus overlay directionality in `test/direction.test.ts`,
|
|
418
|
+
so English and Arabic behaviour cannot drift apart.
|
|
355
419
|
|
|
356
420
|
---
|
|
357
421
|
|
|
@@ -414,6 +478,13 @@ interface HydrationInspectorHandle {
|
|
|
414
478
|
}
|
|
415
479
|
```
|
|
416
480
|
|
|
481
|
+
`Provider` owns the inspector's lifetime: it must actually be mounted, and
|
|
482
|
+
unmounting it tears the inspector down (overlay removed, `console.error`
|
|
483
|
+
handed back untouched). Mounting it again restarts detection cleanly rather
|
|
484
|
+
than stacking a second overlay or a second console patch — which is what makes
|
|
485
|
+
it safe under `<React.StrictMode>`, where React deliberately runs every mount
|
|
486
|
+
effect setup → cleanup → setup.
|
|
487
|
+
|
|
417
488
|
### `OverlayOptions`
|
|
418
489
|
|
|
419
490
|
```ts
|
|
@@ -508,6 +579,14 @@ Server and client rendered different random-looking values (UUID, token, React
|
|
|
508
579
|
|
|
509
580
|
Values are dates/times that differ by a small delta — the clock or timezone
|
|
510
581
|
moved between server and client render.
|
|
582
|
+
|
|
583
|
+
Matched on **shape**, not on whether `Date.parse` happens to accept the value:
|
|
584
|
+
ISO dates, `D/M/YYYY`-style dates, clock times, month names with a number, and
|
|
585
|
+
10–13 digit epoch timestamps. That distinction matters because `Date.parse` is
|
|
586
|
+
extremely permissive — it reads `100` as the year 100 and `server-0` as the
|
|
587
|
+
year 2000 — so an ordinary price, count, or id would otherwise be diagnosed as
|
|
588
|
+
a clock drift.
|
|
589
|
+
|
|
511
590
|
**Fix:** render time after mount, or pass one server timestamp down and pin
|
|
512
591
|
the timezone when formatting.
|
|
513
592
|
**Reference:** [React — different client/server content](https://react.dev/reference/react-dom/client/hydrateRoot#handling-different-client-and-server-content).
|
|
@@ -518,11 +597,21 @@ the timezone when formatting.
|
|
|
518
597
|
|
|
519
598
|
<img src="docs/screenshots/cause-locale-format.png" alt="locale-format report" width="420">
|
|
520
599
|
|
|
521
|
-
Same underlying value, different formatting:
|
|
522
|
-
|
|
523
|
-
|
|
600
|
+
Same underlying value, different formatting. Covers:
|
|
601
|
+
|
|
602
|
+
- **Different digit scripts** — Arabic-Indic `٠١٢` vs Latin `012`, or Persian
|
|
603
|
+
`۰۱۲` vs Latin.
|
|
604
|
+
- **Different separators or field order** — `1,234.56` vs `1.234,56`,
|
|
605
|
+
`١٬٤٠٠` vs `١٤٠٠`, MM/DD vs DD/MM. Detected in Arabic-Indic and Persian
|
|
606
|
+
digits as well as Latin, so an app that renders the same digit script on both
|
|
607
|
+
sides is still diagnosed.
|
|
608
|
+
- **Invisible bidirectional marks** — values that are identical on screen but
|
|
609
|
+
differ by LRM/RLM/ALM or isolate characters, which `Intl` adds around numbers
|
|
610
|
+
and dates in RTL locales and which different ICU versions (Node vs the
|
|
611
|
+
browser) emit differently.
|
|
612
|
+
|
|
524
613
|
**Fix:** pass an explicit `locale` and timezone to `Intl` on both sides, or
|
|
525
|
-
format after mount.
|
|
614
|
+
format after mount. See also [RTL and Arabic support](#rtl-and-arabic-support).
|
|
526
615
|
**Reference:** [MDN `Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat).
|
|
527
616
|
|
|
528
617
|
<a id="cause-third-party-dom-mutation"></a>
|
|
@@ -628,11 +717,21 @@ above. If you find a reliable signal for it, add a custom rule via the
|
|
|
628
717
|
check, written so bundlers can statically fold it away. In a production
|
|
629
718
|
build, `<HydrationInspector>` becomes a plain pass-through and
|
|
630
719
|
`createHydrationInspector()` returns no-op functions.
|
|
720
|
+
- Production is **opt-out**, not opt-in: the internal dev check treats an
|
|
721
|
+
explicit `NODE_ENV === 'production'` as production and everything else,
|
|
722
|
+
including a bundle where `process` was never defined at all, as development.
|
|
723
|
+
That matters because Vite, Rollup and esbuild substitute
|
|
724
|
+
`process.env.NODE_ENV` without shimming `process` itself — requiring
|
|
725
|
+
`process` to exist would silently disable the inspector for all of them. It
|
|
726
|
+
cannot leak dev code into a production bundle, because the entry-point gates
|
|
727
|
+
above have already been folded away by then.
|
|
631
728
|
- This is enforced, not just claimed: the project's CI pipeline runs a size
|
|
632
729
|
budget (`npm run size`) against a real production bundle built with webpack
|
|
633
730
|
and terser — the same toolchain Next.js and CRA use for production — and
|
|
634
731
|
fails the build if the tree-shaken output for any entry point isn't reduced
|
|
635
|
-
to a near-empty stub.
|
|
732
|
+
to a near-empty stub. Verified independently against Rollup, which is what
|
|
733
|
+
Vite uses for production builds: **99 B** under webpack, **166 B** under
|
|
734
|
+
Rollup, versus ~40 KB for the same entry built for development.
|
|
636
735
|
- `<HydrationSnapshotScript>` also renders `null` outside development, so no
|
|
637
736
|
snapshot script is emitted into your production HTML.
|
|
638
737
|
|
|
@@ -662,6 +761,16 @@ above. If you find a reliable signal for it, add a custom rule via the
|
|
|
662
761
|
- **Bounded, not continuous.** Detection re-checks the DOM a handful of times
|
|
663
762
|
in the ~1.5 seconds after hydration and then stops — there is no standing
|
|
664
763
|
`MutationObserver` watching your app for the rest of its lifetime.
|
|
764
|
+
- **Bounded in cost, too.** A burst of React warnings landing in one frame
|
|
765
|
+
triggers one diff pass, not one per warning. The captured server markup is
|
|
766
|
+
parsed once per root and reused across passes rather than re-parsed each
|
|
767
|
+
time. Retained warning text is capped, so an app erroring in a render loop
|
|
768
|
+
cannot grow the per-pass cost without limit, and reports are capped by
|
|
769
|
+
`maxReports` (default 25).
|
|
770
|
+
- **Symmetrical teardown.** Unmounting the inspector restores `console.error`
|
|
771
|
+
to exactly the function it replaced, removes the overlay, and clears every
|
|
772
|
+
pending timer — so a remount (including React Strict Mode's deliberate
|
|
773
|
+
double-mount in dev) leaves one overlay and one console patch, not two.
|
|
665
774
|
|
|
666
775
|
---
|
|
667
776
|
|
|
@@ -673,11 +782,25 @@ Remix, or a custom SSR setup. Use `createHydrationInspector` where you own
|
|
|
673
782
|
The core engine (`why-hydration`) is framework-agnostic.
|
|
674
783
|
|
|
675
784
|
**Nothing shows up, but I know there's a mismatch.** Confirm `NODE_ENV` isn't
|
|
676
|
-
`production`, that `<HydrationInspector>` (or its `Provider`) actually
|
|
677
|
-
the part of the tree that mismatches, and that the
|
|
678
|
-
in `<head>` and runs before your app's hydration
|
|
679
|
-
script, the tool still reports mismatches it can
|
|
680
|
-
console warning, but loses the precise DOM-level diff.
|
|
785
|
+
`production`, that `<HydrationInspector>` (or its `Provider`) is actually
|
|
786
|
+
**mounted** and wraps the part of the tree that mismatches, and that the
|
|
787
|
+
snapshot script is present in `<head>` and runs before your app's hydration
|
|
788
|
+
script. Without the snapshot script, the tool still reports mismatches it can
|
|
789
|
+
parse from React's own console warning, but loses the precise DOM-level diff.
|
|
790
|
+
If you passed `roots`, check the console for a
|
|
791
|
+
`[why-hydration] Skipping root:` warning — a root with no captured server HTML
|
|
792
|
+
can never produce a DOM-level report, and it says so rather than failing
|
|
793
|
+
silently.
|
|
794
|
+
|
|
795
|
+
**Does it work under `<React.StrictMode>`?** Yes. Strict Mode runs every mount
|
|
796
|
+
effect setup → cleanup → setup in development, which tears the inspector down
|
|
797
|
+
and rebuilds it. You get one overlay, one console patch, and each mismatch
|
|
798
|
+
reported once — the warnings React logged during the first pass are replayed
|
|
799
|
+
to the rebuilt inspector rather than lost.
|
|
800
|
+
|
|
801
|
+
**I loaded the page in a background tab and got nothing.** Fixed — browsers
|
|
802
|
+
suspend `requestAnimationFrame` on hidden pages, so scheduling races a frame
|
|
803
|
+
against a 50 ms timer and no longer depends on the page being painted.
|
|
681
804
|
|
|
682
805
|
**The "Learn more →" link 404s.** The links point at this README on GitHub
|
|
683
806
|
(`github.com/razan-aboushi/why-hydration#cause-…`). If you've forked the
|
|
@@ -701,7 +824,15 @@ right after hydration, then stops.
|
|
|
701
824
|
|
|
702
825
|
**Does it work with `<html dir="rtl">`?** Yes — see
|
|
703
826
|
[RTL and Arabic support](#rtl-and-arabic-support). The overlay stays
|
|
704
|
-
left-to-right on purpose; this is not a bug.
|
|
827
|
+
left-to-right on purpose; this is not a bug. Detection covers Arabic-script
|
|
828
|
+
formatting mismatches on both sides, not just Arabic-vs-Latin.
|
|
829
|
+
|
|
830
|
+
**My Arabic app shows two identical-looking values as a mismatch.** They differ
|
|
831
|
+
by invisible bidirectional control characters — `Intl` adds LRM/RLM/isolate
|
|
832
|
+
marks around numbers and dates in RTL locales, and Node's ICU and the browser's
|
|
833
|
+
ICU do not always agree on which. The report names this explicitly under
|
|
834
|
+
[`locale-format`](#cause-locale-format). Format the value in one place and pass
|
|
835
|
+
the string down, or add `suppressHydrationWarning` if the marks are harmless.
|
|
705
836
|
|
|
706
837
|
---
|
|
707
838
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { isDev, ReportCollector, inspectRoot, reportFromMessage, formatConsoleArgs, isHydrationMessage } from './chunk-
|
|
2
|
-
import { readSnapshot, DEFAULT_SNAPSHOT_SELECTORS } from './chunk-
|
|
1
|
+
import { isDev, ReportCollector, inspectRoot, reportFromMessage, formatConsoleArgs, isHydrationMessage } from './chunk-AS5DZZHI.js';
|
|
2
|
+
import { getServerHtmlForRoot, readSnapshot, DEFAULT_SNAPSHOT_SELECTORS } from './chunk-XIM33ZGB.js';
|
|
3
3
|
import * as React2 from 'react';
|
|
4
4
|
|
|
5
5
|
// src/react/console.ts
|
|
6
6
|
function installConsoleInterceptor(onMessage) {
|
|
7
7
|
if (typeof console === "undefined") return () => {
|
|
8
8
|
};
|
|
9
|
-
const original = console.error
|
|
9
|
+
const original = console.error;
|
|
10
10
|
const patched = (...args) => {
|
|
11
11
|
try {
|
|
12
12
|
const message = formatConsoleArgs(args);
|
|
@@ -15,7 +15,7 @@ function installConsoleInterceptor(onMessage) {
|
|
|
15
15
|
}
|
|
16
16
|
} catch {
|
|
17
17
|
}
|
|
18
|
-
original(
|
|
18
|
+
original.apply(console, args);
|
|
19
19
|
};
|
|
20
20
|
console.error = patched;
|
|
21
21
|
return () => {
|
|
@@ -52,6 +52,32 @@ function createConsoleReporter() {
|
|
|
52
52
|
return sink;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// src/react/capture.ts
|
|
56
|
+
var MAX_CAPTURED = 50;
|
|
57
|
+
var captured = /* @__PURE__ */ new Set();
|
|
58
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
59
|
+
var uninstall = null;
|
|
60
|
+
function startCapture() {
|
|
61
|
+
if (uninstall || typeof window === "undefined") return;
|
|
62
|
+
uninstall = installConsoleInterceptor((message) => {
|
|
63
|
+
if (captured.has(message) || captured.size >= MAX_CAPTURED) return;
|
|
64
|
+
captured.add(message);
|
|
65
|
+
for (const listener of [...listeners]) listener(message);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function subscribeCapture(listener) {
|
|
69
|
+
startCapture();
|
|
70
|
+
listeners.add(listener);
|
|
71
|
+
for (const message of [...captured]) listener(message);
|
|
72
|
+
return () => {
|
|
73
|
+
if (!listeners.delete(listener)) return;
|
|
74
|
+
if (listeners.size === 0 && uninstall) {
|
|
75
|
+
uninstall();
|
|
76
|
+
uninstall = null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
// src/react/overlay.ts
|
|
56
82
|
var CONTAINER_ID = "why-hydration-overlay";
|
|
57
83
|
var CATEGORY_LABELS = {
|
|
@@ -262,6 +288,7 @@ function createOverlay(options = {}) {
|
|
|
262
288
|
host.id = CONTAINER_ID;
|
|
263
289
|
host.setAttribute("data-why-hydration", "overlay");
|
|
264
290
|
host.setAttribute("aria-hidden", "false");
|
|
291
|
+
host.setAttribute("dir", "ltr");
|
|
265
292
|
const shadow = host.attachShadow({ mode: "open" });
|
|
266
293
|
const style = document.createElement("style");
|
|
267
294
|
style.textContent = STYLES;
|
|
@@ -313,6 +340,8 @@ function createOverlay(options = {}) {
|
|
|
313
340
|
countEl = null;
|
|
314
341
|
hintEl = null;
|
|
315
342
|
hintTextEl = null;
|
|
343
|
+
count = 0;
|
|
344
|
+
hintDismissed = false;
|
|
316
345
|
}
|
|
317
346
|
function push(report) {
|
|
318
347
|
ensureMounted();
|
|
@@ -381,6 +410,7 @@ function resolveReactSource(node) {
|
|
|
381
410
|
}
|
|
382
411
|
|
|
383
412
|
// src/react/controller.ts
|
|
413
|
+
var INSPECT_FALLBACK_MS = 50;
|
|
384
414
|
function buildIgnore(ignore) {
|
|
385
415
|
if (!ignore || ignore.length === 0) return void 0;
|
|
386
416
|
return (divergence) => {
|
|
@@ -404,11 +434,14 @@ var InspectorController = class {
|
|
|
404
434
|
this.started = false;
|
|
405
435
|
this.pendingContext = {};
|
|
406
436
|
this.messages = /* @__PURE__ */ new Set();
|
|
437
|
+
this.warnedRoots = /* @__PURE__ */ new Set();
|
|
407
438
|
this.settlingTimers = [];
|
|
439
|
+
this.inspectScheduled = false;
|
|
440
|
+
this.inspectTimer = null;
|
|
408
441
|
this.onRecoverableError = (error, info) => {
|
|
409
442
|
if (!isDev) return;
|
|
410
443
|
const message = error instanceof Error ? error.message : String(error);
|
|
411
|
-
this.
|
|
444
|
+
this.rememberMessage(message);
|
|
412
445
|
this.mergeContext({
|
|
413
446
|
componentStack: info?.componentStack,
|
|
414
447
|
component: firstComponentFromStack(info?.componentStack),
|
|
@@ -420,30 +453,31 @@ var InspectorController = class {
|
|
|
420
453
|
this.collector = new ReportCollector({
|
|
421
454
|
maxReports: options.maxReports,
|
|
422
455
|
extra: options.classify,
|
|
423
|
-
ignore: buildIgnore(options
|
|
456
|
+
ignore: buildIgnore(options?.ignore)
|
|
424
457
|
});
|
|
425
458
|
}
|
|
426
459
|
start() {
|
|
427
460
|
if (this.started || !isDev || typeof window === "undefined") return;
|
|
428
461
|
this.started = true;
|
|
429
|
-
this.pendingContext = {};
|
|
430
|
-
this.messages.clear();
|
|
431
462
|
if (this.options.onReport) {
|
|
432
|
-
this.collector.addSink(this.options.onReport);
|
|
463
|
+
this.cleanups.push(this.collector.addSink(this.options.onReport));
|
|
433
464
|
}
|
|
434
|
-
this.collector.addSink(createConsoleReporter());
|
|
465
|
+
this.cleanups.push(this.collector.addSink(createConsoleReporter()));
|
|
435
466
|
if (this.options.overlay !== false) {
|
|
436
467
|
const overlayOpts = typeof this.options.overlay === "object" ? this.options.overlay : {};
|
|
437
468
|
const handle = createOverlay(overlayOpts);
|
|
438
|
-
this.
|
|
439
|
-
|
|
469
|
+
this.cleanups.push(
|
|
470
|
+
this.collector.addSink(handle.push, { replay: true }),
|
|
471
|
+
() => handle.destroy()
|
|
472
|
+
);
|
|
440
473
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
474
|
+
this.cleanups.push(
|
|
475
|
+
subscribeCapture((message) => {
|
|
476
|
+
this.rememberMessage(message);
|
|
477
|
+
this.mergeContext({ reactMessage: message });
|
|
478
|
+
this.scheduleInspect();
|
|
479
|
+
})
|
|
480
|
+
);
|
|
447
481
|
this.scheduleSettlingInspections();
|
|
448
482
|
}
|
|
449
483
|
// React applies client values to mismatched subtrees via a client re-render a
|
|
@@ -458,18 +492,31 @@ var InspectorController = class {
|
|
|
458
492
|
this.settlingTimers.push(timer);
|
|
459
493
|
}
|
|
460
494
|
}
|
|
495
|
+
rememberMessage(message) {
|
|
496
|
+
if (this.messages.size >= MAX_CAPTURED && !this.messages.has(message)) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
this.messages.add(message);
|
|
500
|
+
}
|
|
461
501
|
mergeContext(ctx) {
|
|
462
502
|
this.pendingContext = {
|
|
463
|
-
componentStack:
|
|
464
|
-
component:
|
|
465
|
-
location:
|
|
466
|
-
reactMessage:
|
|
503
|
+
componentStack: ctx.componentStack ?? this.pendingContext.componentStack,
|
|
504
|
+
component: ctx.component ?? this.pendingContext.component,
|
|
505
|
+
location: ctx.location ?? this.pendingContext.location,
|
|
506
|
+
reactMessage: ctx.reactMessage ?? this.pendingContext.reactMessage
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
domContext() {
|
|
510
|
+
return {
|
|
511
|
+
componentStack: this.pendingContext.componentStack,
|
|
512
|
+
component: this.pendingContext.component,
|
|
513
|
+
location: this.pendingContext.location
|
|
467
514
|
};
|
|
468
515
|
}
|
|
469
516
|
stop() {
|
|
470
517
|
for (const timer of this.settlingTimers.splice(0)) clearTimeout(timer);
|
|
518
|
+
this.clearScheduledInspect();
|
|
471
519
|
for (const cleanup of this.cleanups.splice(0)) cleanup();
|
|
472
|
-
this.messages.clear();
|
|
473
520
|
this.started = false;
|
|
474
521
|
}
|
|
475
522
|
getReports() {
|
|
@@ -483,16 +530,31 @@ var InspectorController = class {
|
|
|
483
530
|
if (!this.started || typeof document === "undefined" || this.collector.isFull) {
|
|
484
531
|
return;
|
|
485
532
|
}
|
|
486
|
-
const
|
|
533
|
+
const configured = this.options.roots;
|
|
534
|
+
const selectors = configured ?? snapshotSelectors();
|
|
535
|
+
const context = this.domContext();
|
|
487
536
|
const seen = /* @__PURE__ */ new Set();
|
|
488
537
|
for (const selector of selectors) {
|
|
489
|
-
|
|
538
|
+
let root = null;
|
|
539
|
+
try {
|
|
540
|
+
root = document.querySelector(selector);
|
|
541
|
+
} catch {
|
|
542
|
+
this.warnRoot(selector, `"${selector}" is not a valid CSS selector.`);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
490
545
|
if (!root || seen.has(root)) continue;
|
|
491
546
|
seen.add(root);
|
|
547
|
+
if (configured && getServerHtmlForRoot(root) == null) {
|
|
548
|
+
this.warnRoot(
|
|
549
|
+
selector,
|
|
550
|
+
`no server HTML was captured for "${selector}". Add it to the \`selectors\` prop of <HydrationSnapshotScript> so the server markup for that root is snapshotted.`
|
|
551
|
+
);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
492
554
|
inspectRoot(
|
|
493
555
|
root,
|
|
494
556
|
this.collector,
|
|
495
|
-
|
|
557
|
+
context,
|
|
496
558
|
(divergence) => resolveReactSource(divergence.element ?? null)
|
|
497
559
|
);
|
|
498
560
|
}
|
|
@@ -500,12 +562,27 @@ var InspectorController = class {
|
|
|
500
562
|
reportFromMessage(message, this.collector, this.pendingContext);
|
|
501
563
|
}
|
|
502
564
|
}
|
|
565
|
+
warnRoot(selector, detail) {
|
|
566
|
+
if (this.warnedRoots.has(selector)) return;
|
|
567
|
+
this.warnedRoots.add(selector);
|
|
568
|
+
console.warn(`[why-hydration] Skipping root: ${detail}`);
|
|
569
|
+
}
|
|
503
570
|
scheduleInspect() {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
571
|
+
if (this.inspectScheduled) return;
|
|
572
|
+
this.inspectScheduled = true;
|
|
573
|
+
const run = () => {
|
|
574
|
+
if (!this.inspectScheduled) return;
|
|
575
|
+
this.clearScheduledInspect();
|
|
576
|
+
this.inspectAllRoots();
|
|
577
|
+
};
|
|
578
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
|
|
579
|
+
this.inspectTimer = setTimeout(run, INSPECT_FALLBACK_MS);
|
|
580
|
+
}
|
|
581
|
+
clearScheduledInspect() {
|
|
582
|
+
this.inspectScheduled = false;
|
|
583
|
+
if (this.inspectTimer != null) {
|
|
584
|
+
clearTimeout(this.inspectTimer);
|
|
585
|
+
this.inspectTimer = null;
|
|
509
586
|
}
|
|
510
587
|
}
|
|
511
588
|
};
|
|
@@ -521,20 +598,13 @@ function firstComponentFromStack(stack) {
|
|
|
521
598
|
}
|
|
522
599
|
function InspectorImpl(props) {
|
|
523
600
|
const { children, overlay, onReport, ignore, classify, maxReports } = props;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
overlay,
|
|
528
|
-
onReport,
|
|
529
|
-
ignore,
|
|
530
|
-
classify,
|
|
531
|
-
maxReports
|
|
532
|
-
});
|
|
533
|
-
controllerRef.current.start();
|
|
534
|
-
}
|
|
601
|
+
startCapture();
|
|
602
|
+
const optionsRef = React2.useRef({});
|
|
603
|
+
optionsRef.current = { overlay, onReport, ignore, classify, maxReports };
|
|
535
604
|
React2.useEffect(() => {
|
|
536
|
-
const controller =
|
|
537
|
-
|
|
605
|
+
const controller = new InspectorController(optionsRef.current);
|
|
606
|
+
controller.start();
|
|
607
|
+
return () => controller.stop();
|
|
538
608
|
}, []);
|
|
539
609
|
return React2.createElement(React2.Fragment, null, children);
|
|
540
610
|
}
|
|
@@ -555,7 +625,10 @@ function createHydrationInspector(options = {}) {
|
|
|
555
625
|
const controller = new InspectorController(options);
|
|
556
626
|
controller.start();
|
|
557
627
|
function Provider(props) {
|
|
558
|
-
React2.useEffect(() =>
|
|
628
|
+
React2.useEffect(() => {
|
|
629
|
+
controller.start();
|
|
630
|
+
return () => controller.stop();
|
|
631
|
+
}, []);
|
|
559
632
|
return React2.createElement(React2.Fragment, null, props.children);
|
|
560
633
|
}
|
|
561
634
|
return {
|
|
@@ -565,5 +638,5 @@ function createHydrationInspector(options = {}) {
|
|
|
565
638
|
}
|
|
566
639
|
|
|
567
640
|
export { HydrationInspector, createHydrationInspector };
|
|
568
|
-
//# sourceMappingURL=chunk-
|
|
569
|
-
//# sourceMappingURL=chunk-
|
|
641
|
+
//# sourceMappingURL=chunk-3I6F4K4L.js.map
|
|
642
|
+
//# sourceMappingURL=chunk-3I6F4K4L.js.map
|