@hueest/xray 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +28 -15
- package/dist/client.js +220 -78
- package/dist/core.d.ts +16 -27
- package/dist/core.js +33 -210
- package/dist/index.d.ts +80 -57
- package/dist/index.js +235 -596
- package/dist/{plate-Bv6W-GkA.d.ts → plate-BIr62u7l.d.ts} +55 -30
- package/dist/{plate-DoE1HEXp.js → plate-BRR6d8Se.js} +8 -4
- package/dist/project-DORoPAo7.js +10224 -0
- package/dist/{react.core-BzMG_cDy.d.ts → react.core-BUj8ziwh.d.ts} +3 -3
- package/dist/{react.core-IyFy2b_8.js → react.core-DMIhXHZF.js} +6 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.dev.d.ts +1 -1
- package/dist/react.dev.js +2 -2
- package/dist/react.js +1 -1
- package/dist/serialize-Cs7hUxmK.d.ts +107 -0
- package/package.json +2 -1
- package/dist/breakpoints-CMoFUUOv.js +0 -1393
- package/dist/serialize-B--oY2bV.d.ts +0 -143
package/dist/client.d.ts
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as CaptureDiagnostic, o as StoredPlate } from "./plate-BIr62u7l.js";
|
|
2
2
|
|
|
3
3
|
//#region src/client.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The dev capture client (M2). Runs in the browser during dev only: installs
|
|
6
|
-
* the `__XRAY__` hook `<Skeleton>` calls when content becomes ready,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* the `__XRAY__` hook `<Skeleton>` calls when content becomes ready, settles,
|
|
7
|
+
* measures the subtree into a per-Plate `PlateIR`, projects it to the structured
|
|
8
|
+
* `StoredPlate` in the browser (the ADR 0022 in-browser Serialize), and hands the
|
|
9
|
+
* envelope to the sink (the plugin's POST endpoint). Re-captures when the viewport
|
|
9
10
|
* crosses into another view while a site is still mounted.
|
|
11
|
+
*
|
|
12
|
+
* Two paths produce a Plate. BROWSE-TIME is a RECORDING SESSION (the Capture toggle):
|
|
13
|
+
* while on, each capture accumulates a View into a name-keyed `PlateIR` buffer
|
|
14
|
+
* (deduped per breakpoint band) and live-previews it in-memory; toggling off writes
|
|
15
|
+
* each accumulated Plate to the sink ONCE. The capture-all SWEEP threads ONE `PlateIR`
|
|
16
|
+
* through every emulated width (one View each) and posts the projected StoredPlate once.
|
|
10
17
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The `/__xray` POST envelope (ADR 0022 cutover). `plate` is the finished,
|
|
20
|
+
* in-browser-projected `StoredPlate` written to disk as-is; `diagnostics` is a
|
|
21
|
+
* SIBLING dev-only field the server logs and drops — it NEVER reaches the stored
|
|
22
|
+
* artifact (ADR 0008).
|
|
23
|
+
*/
|
|
24
|
+
interface CaptureEnvelope {
|
|
25
|
+
plate: StoredPlate;
|
|
26
|
+
diagnostics?: CaptureDiagnostic[];
|
|
17
27
|
}
|
|
18
28
|
interface ClientOptions {
|
|
19
29
|
/** Settle delay (ms) once the subtree goes quiet. Default 200. */
|
|
@@ -29,9 +39,12 @@ interface ClientOptions {
|
|
|
29
39
|
*/
|
|
30
40
|
maxNodes?: number;
|
|
31
41
|
/**
|
|
32
|
-
* Initial state of the capture toggle: whether
|
|
33
|
-
* A per-tab override (sessionStorage / `?xray-capture`) wins over it.
|
|
34
|
-
* Defaults to `
|
|
42
|
+
* Initial state of the capture toggle: whether the recording session is active
|
|
43
|
+
* at boot. A per-tab override (sessionStorage / `?xray-capture`) wins over it.
|
|
44
|
+
* Defaults to `false` — recording is explicit opt-in (turn it on, browse/resize to
|
|
45
|
+
* accumulate Views, turn it off to write), so normal browsing never accumulates and
|
|
46
|
+
* an HMR/close can't drop an unsaved recording. The plugin passes the same
|
|
47
|
+
* off-by-default policy; a direct caller can pass `true` to capture on boot.
|
|
35
48
|
*/
|
|
36
49
|
capture?: boolean;
|
|
37
50
|
/**
|
|
@@ -41,7 +54,7 @@ interface ClientOptions {
|
|
|
41
54
|
* does not advance to the next emulated width until the current writes have
|
|
42
55
|
* been accepted by the dev server (backpressure — capture-all-backpressure plan).
|
|
43
56
|
*/
|
|
44
|
-
sink: (
|
|
57
|
+
sink: (envelope: CaptureEnvelope) => void | Promise<void>;
|
|
45
58
|
/**
|
|
46
59
|
* Re-pull the on-disk per-plate coverage and feed it into the coverage store.
|
|
47
60
|
* The capture-all sweep calls this after each pass so the next `sweepWidths()`
|
|
@@ -74,4 +87,4 @@ interface ClientOptions {
|
|
|
74
87
|
}
|
|
75
88
|
declare function installXrayClient(options: ClientOptions): () => void;
|
|
76
89
|
//#endregion
|
|
77
|
-
export {
|
|
90
|
+
export { CaptureEnvelope, ClientOptions, installXrayClient };
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as serializePlate, d as formatDiagnostic, f as makeDiagnostic, l as runPlatePasses, n as deriveBreakpoints, o as CaptureTooLargeError, p as emit, r as regimeFor, s as captureView, t as serializePlateIR, u as diagnosticsHeader } from "./project-DORoPAo7.js";
|
|
2
2
|
//#region ../../node_modules/.pnpm/devalue@5.8.1/node_modules/devalue/src/base64.js
|
|
3
3
|
/** @type {(array_buffer: ArrayBuffer) => string} */
|
|
4
4
|
function encode_native(array_buffer) {
|
|
@@ -528,14 +528,18 @@ function stringify_primitive(thing) {
|
|
|
528
528
|
//#endregion
|
|
529
529
|
//#region src/flash.ts
|
|
530
530
|
/**
|
|
531
|
-
* Dev-only radiographic capture effect — pure delight. When a capture lands
|
|
532
|
-
* briefly "expose" the captured element: a
|
|
533
|
-
* x-ray IS a negative — `backdrop-filter: invert`)
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
531
|
+
* Dev-only radiographic capture effect — pure delight. When a capture lands (and
|
|
532
|
+
* actually CHANGES the stored result) we briefly "expose" the captured element: a
|
|
533
|
+
* negative blink (an x-ray IS a negative — `backdrop-filter: invert`) that enters
|
|
534
|
+
* SHARPLY and fades a touch slower, framed by a soft shadow ring around the box so
|
|
535
|
+
* you can see WHICH area was captured. Short and quiet — it's feedback that a
|
|
536
|
+
* resize/navigation (or the capture-all sweep) recorded something, not a light show.
|
|
537
|
+
* Honors `prefers-reduced-motion` (then just a shadow pulse, no invert). A transient
|
|
538
|
+
* `position: fixed`, `pointer-events: none` overlay just under the HUD; it never
|
|
539
|
+
* touches the capture pipeline or the captured DOM.
|
|
537
540
|
*/
|
|
538
541
|
const STYLE_ID = "xr-flash-style";
|
|
542
|
+
const DURATION_MS = 300;
|
|
539
543
|
function ensureStyle() {
|
|
540
544
|
if (document.getElementById(STYLE_ID)) return;
|
|
541
545
|
const style = document.createElement("style");
|
|
@@ -543,31 +547,22 @@ function ensureStyle() {
|
|
|
543
547
|
style.textContent = `
|
|
544
548
|
.xr-flash {
|
|
545
549
|
position: fixed; pointer-events: none; z-index: 2147483646;
|
|
546
|
-
|
|
547
|
-
animation: xr-expose
|
|
548
|
-
}
|
|
549
|
-
.xr-flash::after {
|
|
550
|
-
content: ''; position: absolute; left: 0; right: 0; height: 18%;
|
|
551
|
-
background: linear-gradient(180deg, transparent, rgba(186, 230, 253, 0.9), transparent);
|
|
552
|
-
filter: blur(1px);
|
|
553
|
-
animation: xr-scan 480ms ease-in-out forwards;
|
|
550
|
+
border-radius: 4px;
|
|
551
|
+
animation: xr-expose ${DURATION_MS}ms ease-out forwards;
|
|
554
552
|
}
|
|
555
553
|
@keyframes xr-expose {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
from { transform: translateY(-100%) }
|
|
562
|
-
to { transform: translateY(560%) }
|
|
554
|
+
/* Sharp in: full invert + ring by ~12% (~36ms). */
|
|
555
|
+
0% { backdrop-filter: invert(0); box-shadow: 0 0 0 1px rgba(56, 189, 248, 0), 0 0 0 0 rgba(56, 189, 248, 0) }
|
|
556
|
+
12% { backdrop-filter: invert(1); box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.85), 0 0 14px 1px rgba(56, 189, 248, 0.5) }
|
|
557
|
+
/* Slower fade out (ease-out decelerates the remaining ~88%). */
|
|
558
|
+
100% { backdrop-filter: invert(0); box-shadow: 0 0 0 2px rgba(56, 189, 248, 0), 0 0 14px 1px rgba(56, 189, 248, 0) }
|
|
563
559
|
}
|
|
564
560
|
@keyframes xr-expose-soft {
|
|
565
|
-
0%
|
|
566
|
-
100% {
|
|
561
|
+
0% { box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.7) }
|
|
562
|
+
100% { box-shadow: 0 0 0 2px rgba(56, 189, 248, 0) }
|
|
567
563
|
}
|
|
568
564
|
@media (prefers-reduced-motion: reduce) {
|
|
569
|
-
.xr-flash { animation: xr-expose-soft
|
|
570
|
-
.xr-flash::after { display: none }
|
|
565
|
+
.xr-flash { animation: xr-expose-soft ${DURATION_MS}ms ease-out forwards }
|
|
571
566
|
}
|
|
572
567
|
`;
|
|
573
568
|
(document.head ?? document.documentElement).append(style);
|
|
@@ -604,7 +599,7 @@ function flashCapture(el) {
|
|
|
604
599
|
flash.style.height = `${box.height}px`;
|
|
605
600
|
flash.addEventListener("animationend", () => flash.remove(), { once: true });
|
|
606
601
|
(document.body ?? document.documentElement).append(flash);
|
|
607
|
-
setTimeout(() => flash.remove(),
|
|
602
|
+
setTimeout(() => flash.remove(), 500);
|
|
608
603
|
}
|
|
609
604
|
//#endregion
|
|
610
605
|
//#region src/client.ts
|
|
@@ -639,6 +634,7 @@ function installXrayClient(options) {
|
|
|
639
634
|
const sites = /* @__PURE__ */ new Set();
|
|
640
635
|
const matchMediaQueries = /* @__PURE__ */ new Set();
|
|
641
636
|
let captureGeneration = 0;
|
|
637
|
+
let sweeping = false;
|
|
642
638
|
const livePlates = /* @__PURE__ */ new Map();
|
|
643
639
|
const plateSerialized = /* @__PURE__ */ new Map();
|
|
644
640
|
const plateListeners = /* @__PURE__ */ new Map();
|
|
@@ -661,10 +657,11 @@ function installXrayClient(options) {
|
|
|
661
657
|
};
|
|
662
658
|
const updatePlate = (name, plate) => {
|
|
663
659
|
const json = JSON.stringify(plate);
|
|
664
|
-
if (plateSerialized.get(name) === json) return;
|
|
660
|
+
if (plateSerialized.get(name) === json) return false;
|
|
665
661
|
plateSerialized.set(name, json);
|
|
666
662
|
livePlates.set(name, plate);
|
|
667
663
|
for (const cb of plateListeners.get(name) ?? []) cb();
|
|
664
|
+
return true;
|
|
668
665
|
};
|
|
669
666
|
const nativeMatchMedia = win.matchMedia.bind(win);
|
|
670
667
|
win.matchMedia = (query) => {
|
|
@@ -672,7 +669,7 @@ function installXrayClient(options) {
|
|
|
672
669
|
return nativeMatchMedia(query);
|
|
673
670
|
};
|
|
674
671
|
const lightbox = createToggleStore(win, LIGHTBOX_KEY, LIGHTBOX_PARAM, false);
|
|
675
|
-
const captureEnabled = createToggleStore(win, CAPTURE_KEY, CAPTURE_PARAM, options.capture ??
|
|
672
|
+
const captureEnabled = createToggleStore(win, CAPTURE_KEY, CAPTURE_PARAM, options.capture ?? false);
|
|
676
673
|
const replayEnabled = createToggleStore(win, REPLAY_KEY, REPLAY_PARAM, options.replay === "prefer" || options.replay === "missing-only");
|
|
677
674
|
const replayMode = options.replay === "missing-only" ? "missing-only" : "prefer";
|
|
678
675
|
const recordMode = options.record ?? "manual";
|
|
@@ -683,36 +680,36 @@ function installXrayClient(options) {
|
|
|
683
680
|
}
|
|
684
681
|
return true;
|
|
685
682
|
};
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
683
|
+
/**
|
|
684
|
+
* Measure one View into a LOCAL, fresh 1-View `PlateIR` (no shared bundle) and
|
|
685
|
+
* return it only if the generation guard still holds AFTER the settle — the
|
|
686
|
+
* heart of the ADR 0022 generation-guard preservation (plan §4). Because
|
|
687
|
+
* `captureView(roots, plate)` MUTATES the passed bundle in place
|
|
688
|
+
* (`plate.views.push`), a stale capture appended to a SHARED session PlateIR
|
|
689
|
+
* would corrupt the whole Plate's projection (one width's geometry under
|
|
690
|
+
* another's island/gate), and post-hoc rollback is impossible. So we capture
|
|
691
|
+
* into a local bundle FIRST, re-check the guard, and only the caller splices the
|
|
692
|
+
* survivor into the session bundle. Returns the local View on success, or
|
|
693
|
+
* `undefined` when the guard moved / the capture was refused / the site was torn
|
|
694
|
+
* down — the caller then DISCARDS it (never appends).
|
|
695
|
+
*
|
|
696
|
+
* `generation` is the token snapshotted by the caller BEFORE the settle; a
|
|
697
|
+
* width-change/disposal/uninstall mid-settle moves `captureGeneration` and this
|
|
698
|
+
* returns undefined. A `CaptureTooLargeError` marks the site and warns. Any
|
|
699
|
+
* other failure warns and returns undefined.
|
|
700
|
+
*/
|
|
701
|
+
const captureLocalView = async (site, generation) => {
|
|
689
702
|
await settle(site.el, site.delay, settleCap, site.mode);
|
|
690
703
|
if (generation !== captureGeneration || site.disposed || site.tooLarge || !site.el.isConnected || !isPrimaryForName(site)) return;
|
|
691
704
|
const roots = rootsFor(site);
|
|
692
|
-
if (roots.length === 0) return;
|
|
693
|
-
let
|
|
694
|
-
let key;
|
|
705
|
+
if (roots.length === 0) return void 0;
|
|
706
|
+
let local;
|
|
695
707
|
try {
|
|
696
|
-
|
|
708
|
+
local = captureView(roots, {
|
|
697
709
|
maxNodes,
|
|
698
710
|
walker: site.walker,
|
|
699
711
|
captureRootIsBoundary: site.mode === "manual"
|
|
700
712
|
});
|
|
701
|
-
reportDiagnostics(site.name, capture.diagnostics);
|
|
702
|
-
const breakpoints = deriveBreakpoints(capture.rules, [...matchMediaQueries]);
|
|
703
|
-
const span = regimeFor(capture.width, breakpoints);
|
|
704
|
-
payload = {
|
|
705
|
-
name: site.name,
|
|
706
|
-
capture: {
|
|
707
|
-
...capture,
|
|
708
|
-
...span
|
|
709
|
-
},
|
|
710
|
-
breakpoints
|
|
711
|
-
};
|
|
712
|
-
key = JSON.stringify(span);
|
|
713
|
-
const body = JSON.stringify(payload);
|
|
714
|
-
if (site.sent.get(key) === body) return;
|
|
715
|
-
site.sent.set(key, body);
|
|
716
713
|
} catch (error) {
|
|
717
714
|
if (error instanceof CaptureTooLargeError) {
|
|
718
715
|
site.tooLarge = true;
|
|
@@ -722,56 +719,173 @@ function installXrayClient(options) {
|
|
|
722
719
|
console.warn("[xray] capture failed for", site.name, error);
|
|
723
720
|
return;
|
|
724
721
|
}
|
|
725
|
-
|
|
726
|
-
|
|
722
|
+
const view = local.views[0];
|
|
723
|
+
if (!view) return void 0;
|
|
724
|
+
reportDiagnostics(site.name, view.diagnostics);
|
|
725
|
+
if (generation !== captureGeneration || site.disposed) return void 0;
|
|
726
|
+
return view;
|
|
727
|
+
};
|
|
728
|
+
const recording = /* @__PURE__ */ new Map();
|
|
729
|
+
const recordingBreakpoints = (buf) => [...new Set([...buf.views.flatMap((v) => v.bpSeed ?? []), ...deriveBreakpoints([], [...matchMediaQueries])])].toSorted((a, b) => a - b);
|
|
730
|
+
const projectRecording = (buf) => {
|
|
731
|
+
if (buf.views.length === 0) return void 0;
|
|
732
|
+
const breakpoints = recordingBreakpoints(buf);
|
|
733
|
+
const perBand = /* @__PURE__ */ new Map();
|
|
734
|
+
for (const view of buf.views) perBand.set(regimeFor(view.width, breakpoints).min ?? 0, view);
|
|
735
|
+
const views = [...perBand.values()].toSorted((a, b) => a.width - b.width);
|
|
736
|
+
return serializePlateIR({
|
|
737
|
+
name: buf.name,
|
|
738
|
+
views,
|
|
739
|
+
breakpoints
|
|
740
|
+
});
|
|
741
|
+
};
|
|
742
|
+
const previewRecording = (buf) => {
|
|
743
|
+
const stored = projectRecording(buf);
|
|
744
|
+
return stored ? updatePlate(buf.name, serializePlate(stored)) : false;
|
|
745
|
+
};
|
|
746
|
+
const finalizeRecording = async () => {
|
|
747
|
+
const entries = [...recording];
|
|
748
|
+
recording.clear();
|
|
749
|
+
await Promise.all(entries.map(async ([name, buf]) => {
|
|
750
|
+
const stored = projectRecording(buf);
|
|
751
|
+
if (!stored) return;
|
|
752
|
+
const diagnostics = buf.views.flatMap((v) => v.diagnostics ?? []);
|
|
753
|
+
const envelope = {
|
|
754
|
+
plate: stored,
|
|
755
|
+
...diagnostics.length > 0 ? { diagnostics } : {}
|
|
756
|
+
};
|
|
757
|
+
try {
|
|
758
|
+
await options.sink(envelope);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (!recording.has(name)) recording.set(name, buf);
|
|
761
|
+
console.warn(`[xray] recording write failed for "${name}" — kept in memory; retried when you next start a recording`, error);
|
|
762
|
+
}
|
|
763
|
+
}));
|
|
764
|
+
};
|
|
765
|
+
const runCapture = async (site, force) => {
|
|
766
|
+
if (!force && (!captureEnabled.get() || sweeping) || site.tooLarge || !isPrimaryForName(site)) return;
|
|
767
|
+
const view = await captureLocalView(site, captureGeneration);
|
|
768
|
+
if (!view) return;
|
|
769
|
+
if (sweeping && force) {
|
|
770
|
+
(site.sessionPlate ??= {
|
|
771
|
+
name: site.name,
|
|
772
|
+
views: [],
|
|
773
|
+
breakpoints: []
|
|
774
|
+
}).views.push(view);
|
|
727
775
|
return;
|
|
728
776
|
}
|
|
729
|
-
|
|
777
|
+
if (!captureEnabled.get()) return;
|
|
778
|
+
view.bpSeed = deriveBreakpoints(emit(view.bundle).rules, []);
|
|
779
|
+
runPlatePasses({
|
|
780
|
+
name: site.name,
|
|
781
|
+
views: [view],
|
|
782
|
+
breakpoints: []
|
|
783
|
+
});
|
|
784
|
+
let buf = recording.get(site.name);
|
|
785
|
+
if (!buf) {
|
|
786
|
+
buf = {
|
|
787
|
+
name: site.name,
|
|
788
|
+
views: [],
|
|
789
|
+
breakpoints: []
|
|
790
|
+
};
|
|
791
|
+
recording.set(site.name, buf);
|
|
792
|
+
}
|
|
793
|
+
buf.views = buf.views.filter((v) => v.width !== view.width);
|
|
794
|
+
buf.views.push(view);
|
|
795
|
+
if (previewRecording(buf)) flashCapture(site.el);
|
|
730
796
|
};
|
|
731
|
-
|
|
797
|
+
/**
|
|
798
|
+
* Project a per-site `PlateIR` to the structured `StoredPlate` IN THE BROWSER
|
|
799
|
+
* (the ADR 0022 Serialize) and hand the envelope to the sink. Derives the
|
|
800
|
+
* breakpoint union ONCE over every View's rules + the recorded matchMedia
|
|
801
|
+
* queries (relocating the old per-View derivation), runs the reduction passes
|
|
802
|
+
* once over the whole bundle, then `serializePlateIR`. Dedupes per-PLATE by the
|
|
803
|
+
* serialized StoredPlate string. ALWAYS catches so neither mode leaks an
|
|
804
|
+
* unhandled rejection; on failure it clears the dedupe so a retry re-sends and,
|
|
805
|
+
* ONLY when `awaitSink`, rethrows so capture-all still fails loudly (pieces
|
|
806
|
+
* 3/8). The radiographic flash runs only AFTER a successful send.
|
|
807
|
+
*/
|
|
808
|
+
const sendPlate = async (site, plate, awaitSink) => {
|
|
809
|
+
if (plate.views.length === 0) return;
|
|
810
|
+
plate.name = site.name;
|
|
811
|
+
plate.breakpoints = deriveBreakpoints(plate.views.flatMap((v) => emit(v.bundle).rules), [...matchMediaQueries]);
|
|
812
|
+
runPlatePasses(plate);
|
|
813
|
+
const stored = serializePlateIR(plate);
|
|
814
|
+
const diagnostics = plate.views.flatMap((v) => v.diagnostics ?? []);
|
|
815
|
+
const envelope = {
|
|
816
|
+
plate: stored,
|
|
817
|
+
...diagnostics.length > 0 ? { diagnostics } : {}
|
|
818
|
+
};
|
|
819
|
+
const body = JSON.stringify(envelope);
|
|
820
|
+
if (site.lastSent === body) return;
|
|
821
|
+
site.lastSent = body;
|
|
732
822
|
try {
|
|
733
|
-
await options.sink(
|
|
823
|
+
await options.sink(envelope);
|
|
734
824
|
} catch (error) {
|
|
735
|
-
site.
|
|
825
|
+
site.lastSent = void 0;
|
|
736
826
|
if (awaitSink) throw error;
|
|
737
827
|
console.warn("[xray] capture send failed for", site.name, error);
|
|
738
828
|
return;
|
|
739
829
|
}
|
|
740
830
|
flashCapture(site.el);
|
|
741
831
|
};
|
|
742
|
-
|
|
832
|
+
/**
|
|
833
|
+
* Finalize a site's accumulated sweep `PlateIR` and POST it ONCE (awaited, the
|
|
834
|
+
* backpressure point). Clears `site.sessionPlate` first so any straggler capture
|
|
835
|
+
* for the site falls back to the browse path rather than mutating the bundle
|
|
836
|
+
* mid-flight. A session that captured no surviving View posts nothing.
|
|
837
|
+
*/
|
|
838
|
+
const finalizeSessionPlate = async (site) => {
|
|
839
|
+
const session = site.sessionPlate;
|
|
840
|
+
site.sessionPlate = void 0;
|
|
841
|
+
if (!session || session.views.length === 0) return;
|
|
842
|
+
await sendPlate(site, session, true);
|
|
843
|
+
};
|
|
844
|
+
const scheduleCapture = (site, force = false) => {
|
|
743
845
|
if (site.inflight) {
|
|
744
846
|
site.dirty = true;
|
|
745
847
|
site.dirtyForce = site.dirtyForce || force;
|
|
746
|
-
site.dirtyAwaitSink = site.dirtyAwaitSink || awaitSink;
|
|
747
848
|
return site.inflight;
|
|
748
849
|
}
|
|
749
|
-
const loop = async (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
850
|
+
const loop = async (initialForce) => {
|
|
851
|
+
let wantForce = initialForce;
|
|
852
|
+
for (;;) {
|
|
853
|
+
await runCapture(site, wantForce);
|
|
854
|
+
if (!site.dirty) {
|
|
855
|
+
site.inflight = void 0;
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
site.dirty = false;
|
|
859
|
+
wantForce = site.dirtyForce;
|
|
860
|
+
site.dirtyForce = false;
|
|
861
|
+
}
|
|
758
862
|
};
|
|
759
|
-
const promise = loop(force
|
|
760
|
-
site.inflight = void 0;
|
|
761
|
-
});
|
|
863
|
+
const promise = loop(force);
|
|
762
864
|
site.inflight = promise;
|
|
763
865
|
return promise;
|
|
764
866
|
};
|
|
765
867
|
let resizeTimer;
|
|
766
868
|
const onResize = () => {
|
|
869
|
+
if (sweeping) {
|
|
870
|
+
clearTimeout(resizeTimer);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
767
873
|
clearTimeout(resizeTimer);
|
|
768
874
|
resizeTimer = setTimeout(() => {
|
|
875
|
+
if (sweeping) return;
|
|
769
876
|
for (const site of sites) scheduleCapture(site);
|
|
770
877
|
}, RESIZE_DEBOUNCE);
|
|
771
878
|
};
|
|
772
879
|
win.addEventListener("resize", onResize);
|
|
880
|
+
let recordingOn = captureEnabled.get();
|
|
773
881
|
const unsubscribeCapture = captureEnabled.subscribe(() => {
|
|
774
|
-
|
|
882
|
+
const on = captureEnabled.get();
|
|
883
|
+
if (on === recordingOn) return;
|
|
884
|
+
recordingOn = on;
|
|
885
|
+
if (on) {
|
|
886
|
+
finalizeRecording();
|
|
887
|
+
for (const site of sites) scheduleCapture(site);
|
|
888
|
+
} else finalizeRecording();
|
|
775
889
|
});
|
|
776
890
|
const extAcks = /* @__PURE__ */ new Map();
|
|
777
891
|
let nextExtReqId = 1;
|
|
@@ -869,7 +983,7 @@ function installXrayClient(options) {
|
|
|
869
983
|
};
|
|
870
984
|
const sweepWidths = () => {
|
|
871
985
|
const set = new Set(deriveBreakpoints([], [...matchMediaQueries]));
|
|
872
|
-
for (const site of sites) for (const
|
|
986
|
+
for (const site of sites) for (const view of site.sessionPlate?.views ?? []) for (const bp of deriveBreakpoints(emit(view.bundle).rules, [...matchMediaQueries])) set.add(bp);
|
|
873
987
|
const breakpoints = [...set].toSorted((a, b) => a - b);
|
|
874
988
|
const min = breakpoints[0];
|
|
875
989
|
const first = min === void 0 ? 390 : Math.max(1, Math.min(390, min - 1));
|
|
@@ -878,6 +992,12 @@ function installXrayClient(options) {
|
|
|
878
992
|
const MAX_SWEEP_PASSES = 8;
|
|
879
993
|
const captureAllViews = async () => {
|
|
880
994
|
const visited = /* @__PURE__ */ new Set();
|
|
995
|
+
sweeping = true;
|
|
996
|
+
for (const site of sites) site.sessionPlate = {
|
|
997
|
+
name: site.name,
|
|
998
|
+
views: [],
|
|
999
|
+
breakpoints: []
|
|
1000
|
+
};
|
|
881
1001
|
try {
|
|
882
1002
|
for (let pass = 0;; pass++) {
|
|
883
1003
|
const pending = sweepWidths().filter((width) => !visited.has(width));
|
|
@@ -891,11 +1011,15 @@ function installXrayClient(options) {
|
|
|
891
1011
|
height: 2400,
|
|
892
1012
|
mobile: width < 600
|
|
893
1013
|
});
|
|
894
|
-
await Promise.all([...sites].map((site) => scheduleCapture(site, true
|
|
1014
|
+
await Promise.all([...sites].map((site) => scheduleCapture(site, true)));
|
|
895
1015
|
}
|
|
896
1016
|
await options.refreshCoverage?.();
|
|
897
1017
|
}
|
|
1018
|
+
sweeping = false;
|
|
1019
|
+
await Promise.all([...sites].map((site) => finalizeSessionPlate(site)));
|
|
898
1020
|
} finally {
|
|
1021
|
+
sweeping = false;
|
|
1022
|
+
for (const site of sites) site.sessionPlate = void 0;
|
|
899
1023
|
await extCommand("reset").catch(() => void 0);
|
|
900
1024
|
}
|
|
901
1025
|
};
|
|
@@ -917,12 +1041,10 @@ function installXrayClient(options) {
|
|
|
917
1041
|
delay: opts?.delay ?? defaultDelay,
|
|
918
1042
|
mode: opts?.mode ?? "auto",
|
|
919
1043
|
walker: opts?.walker,
|
|
920
|
-
sent: /* @__PURE__ */ new Map(),
|
|
921
1044
|
disposed: false,
|
|
922
1045
|
tooLarge: false,
|
|
923
1046
|
dirty: false,
|
|
924
|
-
dirtyForce: false
|
|
925
|
-
dirtyAwaitSink: false
|
|
1047
|
+
dirtyForce: false
|
|
926
1048
|
};
|
|
927
1049
|
sites.add(site);
|
|
928
1050
|
scheduleCapture(site);
|
|
@@ -1010,9 +1132,29 @@ function createToggleStore(win, key, param, initial) {
|
|
|
1010
1132
|
* made single-shot capture non-deterministic in the M1 harness; this is the
|
|
1011
1133
|
* cure. Hard-capped so a perpetually-animating subtree still captures.
|
|
1012
1134
|
*/
|
|
1135
|
+
/**
|
|
1136
|
+
* Force lazy images in the subtree to load and await their decode, so the walk
|
|
1137
|
+
* measures their real boxes. A `loading="lazy"` image below the fold never enters
|
|
1138
|
+
* the viewport during capture, so its IntersectionObserver never fires — flip it
|
|
1139
|
+
* to eager to trigger the load. Capped: a slow or broken image resolves the race
|
|
1140
|
+
* via the timeout instead of blocking the capture.
|
|
1141
|
+
*/
|
|
1142
|
+
function awaitImages(root, cap) {
|
|
1143
|
+
const imgs = Array.from(root.querySelectorAll("img"));
|
|
1144
|
+
if (imgs.length === 0) return Promise.resolve();
|
|
1145
|
+
const decoded = imgs.map((img) => {
|
|
1146
|
+
if (img.loading === "lazy") img.loading = "eager";
|
|
1147
|
+
if (typeof img.decode !== "function") return Promise.resolve();
|
|
1148
|
+
return img.decode().then(() => void 0, () => void 0);
|
|
1149
|
+
});
|
|
1150
|
+
return Promise.race([Promise.all(decoded).then(() => void 0), new Promise((resolve) => {
|
|
1151
|
+
setTimeout(resolve, cap);
|
|
1152
|
+
})]);
|
|
1153
|
+
}
|
|
1013
1154
|
function settle(el, delay, cap, mode = "auto") {
|
|
1014
1155
|
const doc = el.ownerDocument;
|
|
1015
|
-
|
|
1156
|
+
const fonts = doc.fonts ? doc.fonts.ready : Promise.resolve();
|
|
1157
|
+
return Promise.all([fonts, awaitImages(el, cap)]).then(() => new Promise((resolve) => {
|
|
1016
1158
|
let timer;
|
|
1017
1159
|
let capTimer;
|
|
1018
1160
|
const observer = new MutationObserver(bump);
|
package/dist/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as
|
|
1
|
+
import { a as RenderNode, c as emptyPlate, i as PlateNode, n as MergedPlate, r as Plate, s as ViewCapture, t as LeafKind } from "./plate-BIr62u7l.js";
|
|
2
|
+
import { a as XrayCaptureWalker, i as WalkerDecision, n as CaptureTooLargeError, o as defineXrayCaptureWalker, r as CaptureWalkerContext, t as CaptureOptions } from "./serialize-Cs7hUxmK.js";
|
|
3
3
|
|
|
4
4
|
//#region src/chunk.d.ts
|
|
5
5
|
/**
|
|
@@ -9,36 +9,25 @@ import { a as WalkerResult, c as captureRegime, i as CaptureWalkerContext, l as
|
|
|
9
9
|
* chunks and renders the fallback.
|
|
10
10
|
*/
|
|
11
11
|
declare function serializePlate(merged: MergedPlate): Plate;
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/merge.d.ts
|
|
14
12
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* genuinely-different DOM trees (mobile stacked vs desktop two-column) without
|
|
23
|
-
* stable keys proved unreliable: every heuristic mis-paired some nodes and
|
|
24
|
-
* remapped one view's rules onto another's elements, corrupting both. A lone
|
|
25
|
-
* capture is pixel-faithful; keeping each view lone preserves that. The cost
|
|
26
|
-
* is a larger plate (roughly the sum of the views) — bounded by the plate
|
|
27
|
-
* size cap — in exchange for correctness.
|
|
28
|
-
*
|
|
29
|
-
* Pure data-in/data-out — runs in node (plugin load) and in tests.
|
|
13
|
+
* Distinct plate names referenced by the tree's stitches, in first-seen order.
|
|
14
|
+
* Walks a `RenderNode` tree (the stored, post-classify shape), so it drives the
|
|
15
|
+
* gen-side stitch import graph straight off a `StoredPlate.tree`
|
|
16
|
+
* (`renderPlateModule`, index.ts). Relocated here from the now-deleted `merge.ts`
|
|
17
|
+
* in the ADR 0022 cutover — it is the one node-side primitive that survived the
|
|
18
|
+
* `mergeViews`/`addCapture` removal (the multi-View merge/gate now lives in the
|
|
19
|
+
* in-browser `serializePlateIR`, project.ts).
|
|
30
20
|
*/
|
|
31
|
-
declare function mergeViews(file: PlateFile): MergedPlate;
|
|
32
|
-
/** Distinct plate names referenced by the tree's stitches, in first-seen order. */
|
|
33
21
|
declare function collectRefs(tree: RenderNode | null): string[];
|
|
34
|
-
/** Fold a fresh capture into the plate file: union breakpoints, one capture per view. */
|
|
35
|
-
declare function addCapture(file: PlateFile | null, name: string, capture: ViewCapture, breakpoints: readonly number[]): PlateFile;
|
|
36
22
|
//#endregion
|
|
37
23
|
//#region src/core.d.ts
|
|
38
24
|
/**
|
|
39
|
-
* Capture a live DOM subtree straight to a renderable Plate
|
|
40
|
-
*
|
|
41
|
-
* (
|
|
25
|
+
* Capture a live DOM subtree straight to a renderable Plate. Runs the same
|
|
26
|
+
* uniform pipeline the dev client does for a single View — measure (`captureView`),
|
|
27
|
+
* reduce (`runPlatePasses`), project (`serializePlateIR` → the structured
|
|
28
|
+
* `StoredPlate`), then lower to chunks (`serializePlate`) — in one call. `roots`
|
|
29
|
+
* are the top-level elements to capture (a component's rendered roots). Pass the
|
|
30
|
+
* result to a `<Skeleton plate>`.
|
|
42
31
|
*
|
|
43
32
|
* `options.walker` is the public programmable capture walker (ADR 0018, Q2 —
|
|
44
33
|
* yes, public): the same `XrayCaptureWalker` the `<Skeleton captureWalker>` prop
|
|
@@ -57,4 +46,4 @@ declare function captureElement(roots: readonly Element[], options: CaptureOptio
|
|
|
57
46
|
*/
|
|
58
47
|
declare function renderPlateHtml(plate: Plate): string;
|
|
59
48
|
//#endregion
|
|
60
|
-
export { type CaptureOptions,
|
|
49
|
+
export { type CaptureOptions, CaptureTooLargeError, type CaptureWalkerContext, type LeafKind, type MergedPlate, type Plate, type PlateNode, type ViewCapture, type WalkerDecision, type XrayCaptureWalker, captureElement, collectRefs, defineXrayCaptureWalker, emptyPlate, renderPlateHtml, serializePlate };
|