@hueest/xray 0.2.0 → 0.4.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 +34 -18
- package/dist/client.js +236 -82
- package/dist/core.d.ts +15 -27
- package/dist/core.js +34 -210
- package/dist/index.d.ts +98 -59
- package/dist/index.js +240 -601
- package/dist/{plate-DoE1HEXp.js → plate-BRR6d8Se.js} +8 -4
- package/dist/project-BZosujs9.js +10250 -0
- package/dist/{react.core-IyFy2b_8.js → react.core-DMIhXHZF.js} +6 -1
- package/dist/{react.core-BzMG_cDy.d.ts → react.core-NhWc9qan.d.ts} +13 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.dev.d.ts +1 -1
- package/dist/react.dev.js +34 -9
- package/dist/react.js +1 -1
- package/dist/{plate-Bv6W-GkA.d.ts → serialize-BgdGt34A.d.ts} +193 -30
- 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 { h as StoredPlate, o as CollectLimits, v as CaptureDiagnostic } from "./serialize-BgdGt34A.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. */
|
|
@@ -24,14 +34,20 @@ interface ClientOptions {
|
|
|
24
34
|
*/
|
|
25
35
|
settleCap?: number;
|
|
26
36
|
/**
|
|
27
|
-
*
|
|
28
|
-
* the Skeleton sits too high in the component tree.
|
|
37
|
+
* Collect limits (ADR 0022): refuse a capture whose measured tree exceeds any
|
|
38
|
+
* axis — that big means the Skeleton sits too high in the component tree.
|
|
39
|
+
* `maxNodes` caps total surviving nodes (default 4096), `maxBreadth` the widest
|
|
40
|
+
* level (default 512), `maxDepth` the nesting depth (default 32). When `maxNodes`
|
|
41
|
+
* is omitted it derives as `round(maxBreadth × maxDepth / 4)`.
|
|
29
42
|
*/
|
|
30
|
-
|
|
43
|
+
collect?: CollectLimits;
|
|
31
44
|
/**
|
|
32
|
-
* Initial state of the capture toggle: whether
|
|
33
|
-
* A per-tab override (sessionStorage / `?xray-capture`) wins over it.
|
|
34
|
-
* Defaults to `
|
|
45
|
+
* Initial state of the capture toggle: whether the recording session is active
|
|
46
|
+
* at boot. A per-tab override (sessionStorage / `?xray-capture`) wins over it.
|
|
47
|
+
* Defaults to `false` — recording is explicit opt-in (turn it on, browse/resize to
|
|
48
|
+
* accumulate Views, turn it off to write), so normal browsing never accumulates and
|
|
49
|
+
* an HMR/close can't drop an unsaved recording. The plugin passes the same
|
|
50
|
+
* off-by-default policy; a direct caller can pass `true` to capture on boot.
|
|
35
51
|
*/
|
|
36
52
|
capture?: boolean;
|
|
37
53
|
/**
|
|
@@ -41,7 +57,7 @@ interface ClientOptions {
|
|
|
41
57
|
* does not advance to the next emulated width until the current writes have
|
|
42
58
|
* been accepted by the dev server (backpressure — capture-all-backpressure plan).
|
|
43
59
|
*/
|
|
44
|
-
sink: (
|
|
60
|
+
sink: (envelope: CaptureEnvelope) => void | Promise<void>;
|
|
45
61
|
/**
|
|
46
62
|
* Re-pull the on-disk per-plate coverage and feed it into the coverage store.
|
|
47
63
|
* The capture-all sweep calls this after each pass so the next `sweepWidths()`
|
|
@@ -74,4 +90,4 @@ interface ClientOptions {
|
|
|
74
90
|
}
|
|
75
91
|
declare function installXrayClient(options: ClientOptions): () => void;
|
|
76
92
|
//#endregion
|
|
77
|
-
export {
|
|
93
|
+
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-BZosujs9.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
|
|
@@ -612,7 +607,9 @@ function isExtensionAck(value) {
|
|
|
612
607
|
return typeof value === "object" && value !== null && "source" in value && value.source === "xray-ext" && "kind" in value && value.kind === "ack" && "id" in value && typeof value.id === "number" && (!("ok" in value) || typeof value.ok === "boolean") && (!("error" in value) || typeof value.error === "string");
|
|
613
608
|
}
|
|
614
609
|
const DEFAULT_SETTLE_CAP = 5e3;
|
|
615
|
-
const
|
|
610
|
+
const DEFAULT_MAX_DEPTH = 32;
|
|
611
|
+
const DEFAULT_MAX_BREADTH = 512;
|
|
612
|
+
const deriveMaxNodes = (maxBreadth, maxDepth) => Math.round(maxBreadth * maxDepth / 4);
|
|
616
613
|
const RESIZE_DEBOUNCE = 300;
|
|
617
614
|
/**
|
|
618
615
|
* The capture roots for a site, the ONE place the auto/manual root models
|
|
@@ -635,10 +632,20 @@ function installXrayClient(options) {
|
|
|
635
632
|
const win = window;
|
|
636
633
|
const defaultDelay = options.delay ?? 200;
|
|
637
634
|
const settleCap = options.settleCap ?? DEFAULT_SETTLE_CAP;
|
|
638
|
-
const
|
|
635
|
+
const baseCollect = options.collect ?? {};
|
|
636
|
+
const resolveCollect = (site) => {
|
|
637
|
+
const maxDepth = site?.maxDepth ?? baseCollect.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
638
|
+
const maxBreadth = site?.maxBreadth ?? baseCollect.maxBreadth ?? DEFAULT_MAX_BREADTH;
|
|
639
|
+
return {
|
|
640
|
+
maxNodes: site?.maxNodes ?? baseCollect.maxNodes ?? deriveMaxNodes(maxBreadth, maxDepth),
|
|
641
|
+
maxBreadth,
|
|
642
|
+
maxDepth
|
|
643
|
+
};
|
|
644
|
+
};
|
|
639
645
|
const sites = /* @__PURE__ */ new Set();
|
|
640
646
|
const matchMediaQueries = /* @__PURE__ */ new Set();
|
|
641
647
|
let captureGeneration = 0;
|
|
648
|
+
let sweeping = false;
|
|
642
649
|
const livePlates = /* @__PURE__ */ new Map();
|
|
643
650
|
const plateSerialized = /* @__PURE__ */ new Map();
|
|
644
651
|
const plateListeners = /* @__PURE__ */ new Map();
|
|
@@ -661,10 +668,11 @@ function installXrayClient(options) {
|
|
|
661
668
|
};
|
|
662
669
|
const updatePlate = (name, plate) => {
|
|
663
670
|
const json = JSON.stringify(plate);
|
|
664
|
-
if (plateSerialized.get(name) === json) return;
|
|
671
|
+
if (plateSerialized.get(name) === json) return false;
|
|
665
672
|
plateSerialized.set(name, json);
|
|
666
673
|
livePlates.set(name, plate);
|
|
667
674
|
for (const cb of plateListeners.get(name) ?? []) cb();
|
|
675
|
+
return true;
|
|
668
676
|
};
|
|
669
677
|
const nativeMatchMedia = win.matchMedia.bind(win);
|
|
670
678
|
win.matchMedia = (query) => {
|
|
@@ -672,7 +680,7 @@ function installXrayClient(options) {
|
|
|
672
680
|
return nativeMatchMedia(query);
|
|
673
681
|
};
|
|
674
682
|
const lightbox = createToggleStore(win, LIGHTBOX_KEY, LIGHTBOX_PARAM, false);
|
|
675
|
-
const captureEnabled = createToggleStore(win, CAPTURE_KEY, CAPTURE_PARAM, options.capture ??
|
|
683
|
+
const captureEnabled = createToggleStore(win, CAPTURE_KEY, CAPTURE_PARAM, options.capture ?? false);
|
|
676
684
|
const replayEnabled = createToggleStore(win, REPLAY_KEY, REPLAY_PARAM, options.replay === "prefer" || options.replay === "missing-only");
|
|
677
685
|
const replayMode = options.replay === "missing-only" ? "missing-only" : "prefer";
|
|
678
686
|
const recordMode = options.record ?? "manual";
|
|
@@ -683,95 +691,212 @@ function installXrayClient(options) {
|
|
|
683
691
|
}
|
|
684
692
|
return true;
|
|
685
693
|
};
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
694
|
+
/**
|
|
695
|
+
* Measure one View into a LOCAL, fresh 1-View `PlateIR` (no shared bundle) and
|
|
696
|
+
* return it only if the generation guard still holds AFTER the settle — the
|
|
697
|
+
* heart of the ADR 0022 generation-guard preservation (plan §4). Because
|
|
698
|
+
* `captureView(roots, plate)` MUTATES the passed bundle in place
|
|
699
|
+
* (`plate.views.push`), a stale capture appended to a SHARED session PlateIR
|
|
700
|
+
* would corrupt the whole Plate's projection (one width's geometry under
|
|
701
|
+
* another's island/gate), and post-hoc rollback is impossible. So we capture
|
|
702
|
+
* into a local bundle FIRST, re-check the guard, and only the caller splices the
|
|
703
|
+
* survivor into the session bundle. Returns the local View on success, or
|
|
704
|
+
* `undefined` when the guard moved / the capture was refused / the site was torn
|
|
705
|
+
* down — the caller then DISCARDS it (never appends).
|
|
706
|
+
*
|
|
707
|
+
* `generation` is the token snapshotted by the caller BEFORE the settle; a
|
|
708
|
+
* width-change/disposal/uninstall mid-settle moves `captureGeneration` and this
|
|
709
|
+
* returns undefined. A `CaptureTooLargeError` marks the site and warns. Any
|
|
710
|
+
* other failure warns and returns undefined.
|
|
711
|
+
*/
|
|
712
|
+
const captureLocalView = async (site, generation) => {
|
|
689
713
|
await settle(site.el, site.delay, settleCap, site.mode);
|
|
690
714
|
if (generation !== captureGeneration || site.disposed || site.tooLarge || !site.el.isConnected || !isPrimaryForName(site)) return;
|
|
691
715
|
const roots = rootsFor(site);
|
|
692
|
-
if (roots.length === 0) return;
|
|
693
|
-
let
|
|
694
|
-
let key;
|
|
716
|
+
if (roots.length === 0) return void 0;
|
|
717
|
+
let local;
|
|
695
718
|
try {
|
|
696
|
-
|
|
697
|
-
|
|
719
|
+
local = captureView(roots, {
|
|
720
|
+
collect: resolveCollect(site.collect),
|
|
698
721
|
walker: site.walker,
|
|
699
722
|
captureRootIsBoundary: site.mode === "manual"
|
|
700
723
|
});
|
|
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
724
|
} catch (error) {
|
|
717
725
|
if (error instanceof CaptureTooLargeError) {
|
|
718
726
|
site.tooLarge = true;
|
|
719
|
-
reportDiagnostics(site.name, [makeDiagnostic(error.code, { detail: `${error.
|
|
727
|
+
reportDiagnostics(site.name, [makeDiagnostic(error.code, { detail: `${error.axis} ${error.size[error.axis]} over the ${error.axis} limit ${error.max} (nodes ${error.size.nodes}, breadth ${error.size.breadth}, depth ${error.size.depth})` })]);
|
|
720
728
|
return;
|
|
721
729
|
}
|
|
722
730
|
console.warn("[xray] capture failed for", site.name, error);
|
|
723
731
|
return;
|
|
724
732
|
}
|
|
725
|
-
|
|
726
|
-
|
|
733
|
+
const view = local.views[0];
|
|
734
|
+
if (!view) return void 0;
|
|
735
|
+
reportDiagnostics(site.name, view.diagnostics);
|
|
736
|
+
if (generation !== captureGeneration || site.disposed) return void 0;
|
|
737
|
+
return view;
|
|
738
|
+
};
|
|
739
|
+
const recording = /* @__PURE__ */ new Map();
|
|
740
|
+
const recordingBreakpoints = (buf) => [...new Set([...buf.views.flatMap((v) => v.bpSeed ?? []), ...deriveBreakpoints([], [...matchMediaQueries])])].toSorted((a, b) => a - b);
|
|
741
|
+
const projectRecording = (buf) => {
|
|
742
|
+
if (buf.views.length === 0) return void 0;
|
|
743
|
+
const breakpoints = recordingBreakpoints(buf);
|
|
744
|
+
const perBand = /* @__PURE__ */ new Map();
|
|
745
|
+
for (const view of buf.views) perBand.set(regimeFor(view.width, breakpoints).min ?? 0, view);
|
|
746
|
+
const views = [...perBand.values()].toSorted((a, b) => a.width - b.width);
|
|
747
|
+
return serializePlateIR({
|
|
748
|
+
name: buf.name,
|
|
749
|
+
views,
|
|
750
|
+
breakpoints
|
|
751
|
+
});
|
|
752
|
+
};
|
|
753
|
+
const previewRecording = (buf) => {
|
|
754
|
+
const stored = projectRecording(buf);
|
|
755
|
+
return stored ? updatePlate(buf.name, serializePlate(stored)) : false;
|
|
756
|
+
};
|
|
757
|
+
const finalizeRecording = async () => {
|
|
758
|
+
const entries = [...recording];
|
|
759
|
+
recording.clear();
|
|
760
|
+
await Promise.all(entries.map(async ([name, buf]) => {
|
|
761
|
+
const stored = projectRecording(buf);
|
|
762
|
+
if (!stored) return;
|
|
763
|
+
const diagnostics = buf.views.flatMap((v) => v.diagnostics ?? []);
|
|
764
|
+
const envelope = {
|
|
765
|
+
plate: stored,
|
|
766
|
+
...diagnostics.length > 0 ? { diagnostics } : {}
|
|
767
|
+
};
|
|
768
|
+
try {
|
|
769
|
+
await options.sink(envelope);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (!recording.has(name)) recording.set(name, buf);
|
|
772
|
+
console.warn(`[xray] recording write failed for "${name}" — kept in memory; retried when you next start a recording`, error);
|
|
773
|
+
}
|
|
774
|
+
}));
|
|
775
|
+
};
|
|
776
|
+
const runCapture = async (site, force) => {
|
|
777
|
+
if (!force && (!captureEnabled.get() || sweeping) || site.tooLarge || !isPrimaryForName(site)) return;
|
|
778
|
+
const view = await captureLocalView(site, captureGeneration);
|
|
779
|
+
if (!view) return;
|
|
780
|
+
if (sweeping && force) {
|
|
781
|
+
(site.sessionPlate ??= {
|
|
782
|
+
name: site.name,
|
|
783
|
+
views: [],
|
|
784
|
+
breakpoints: []
|
|
785
|
+
}).views.push(view);
|
|
727
786
|
return;
|
|
728
787
|
}
|
|
729
|
-
|
|
788
|
+
if (!captureEnabled.get()) return;
|
|
789
|
+
view.bpSeed = deriveBreakpoints(emit(view.bundle).rules, []);
|
|
790
|
+
runPlatePasses({
|
|
791
|
+
name: site.name,
|
|
792
|
+
views: [view],
|
|
793
|
+
breakpoints: []
|
|
794
|
+
});
|
|
795
|
+
let buf = recording.get(site.name);
|
|
796
|
+
if (!buf) {
|
|
797
|
+
buf = {
|
|
798
|
+
name: site.name,
|
|
799
|
+
views: [],
|
|
800
|
+
breakpoints: []
|
|
801
|
+
};
|
|
802
|
+
recording.set(site.name, buf);
|
|
803
|
+
}
|
|
804
|
+
buf.views = buf.views.filter((v) => v.width !== view.width);
|
|
805
|
+
buf.views.push(view);
|
|
806
|
+
if (previewRecording(buf)) flashCapture(site.el);
|
|
730
807
|
};
|
|
731
|
-
|
|
808
|
+
/**
|
|
809
|
+
* Project a per-site `PlateIR` to the structured `StoredPlate` IN THE BROWSER
|
|
810
|
+
* (the ADR 0022 Serialize) and hand the envelope to the sink. Derives the
|
|
811
|
+
* breakpoint union ONCE over every View's rules + the recorded matchMedia
|
|
812
|
+
* queries (relocating the old per-View derivation), runs the reduction passes
|
|
813
|
+
* once over the whole bundle, then `serializePlateIR`. Dedupes per-PLATE by the
|
|
814
|
+
* serialized StoredPlate string. ALWAYS catches so neither mode leaks an
|
|
815
|
+
* unhandled rejection; on failure it clears the dedupe so a retry re-sends and,
|
|
816
|
+
* ONLY when `awaitSink`, rethrows so capture-all still fails loudly (pieces
|
|
817
|
+
* 3/8). The radiographic flash runs only AFTER a successful send.
|
|
818
|
+
*/
|
|
819
|
+
const sendPlate = async (site, plate, awaitSink) => {
|
|
820
|
+
if (plate.views.length === 0) return;
|
|
821
|
+
plate.name = site.name;
|
|
822
|
+
plate.breakpoints = deriveBreakpoints(plate.views.flatMap((v) => emit(v.bundle).rules), [...matchMediaQueries]);
|
|
823
|
+
runPlatePasses(plate);
|
|
824
|
+
const stored = serializePlateIR(plate);
|
|
825
|
+
const diagnostics = plate.views.flatMap((v) => v.diagnostics ?? []);
|
|
826
|
+
const envelope = {
|
|
827
|
+
plate: stored,
|
|
828
|
+
...diagnostics.length > 0 ? { diagnostics } : {}
|
|
829
|
+
};
|
|
830
|
+
const body = JSON.stringify(envelope);
|
|
831
|
+
if (site.lastSent === body) return;
|
|
832
|
+
site.lastSent = body;
|
|
732
833
|
try {
|
|
733
|
-
await options.sink(
|
|
834
|
+
await options.sink(envelope);
|
|
734
835
|
} catch (error) {
|
|
735
|
-
site.
|
|
836
|
+
site.lastSent = void 0;
|
|
736
837
|
if (awaitSink) throw error;
|
|
737
838
|
console.warn("[xray] capture send failed for", site.name, error);
|
|
738
839
|
return;
|
|
739
840
|
}
|
|
740
841
|
flashCapture(site.el);
|
|
741
842
|
};
|
|
742
|
-
|
|
843
|
+
/**
|
|
844
|
+
* Finalize a site's accumulated sweep `PlateIR` and POST it ONCE (awaited, the
|
|
845
|
+
* backpressure point). Clears `site.sessionPlate` first so any straggler capture
|
|
846
|
+
* for the site falls back to the browse path rather than mutating the bundle
|
|
847
|
+
* mid-flight. A session that captured no surviving View posts nothing.
|
|
848
|
+
*/
|
|
849
|
+
const finalizeSessionPlate = async (site) => {
|
|
850
|
+
const session = site.sessionPlate;
|
|
851
|
+
site.sessionPlate = void 0;
|
|
852
|
+
if (!session || session.views.length === 0) return;
|
|
853
|
+
await sendPlate(site, session, true);
|
|
854
|
+
};
|
|
855
|
+
const scheduleCapture = (site, force = false) => {
|
|
743
856
|
if (site.inflight) {
|
|
744
857
|
site.dirty = true;
|
|
745
858
|
site.dirtyForce = site.dirtyForce || force;
|
|
746
|
-
site.dirtyAwaitSink = site.dirtyAwaitSink || awaitSink;
|
|
747
859
|
return site.inflight;
|
|
748
860
|
}
|
|
749
|
-
const loop = async (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
861
|
+
const loop = async (initialForce) => {
|
|
862
|
+
let wantForce = initialForce;
|
|
863
|
+
for (;;) {
|
|
864
|
+
await runCapture(site, wantForce);
|
|
865
|
+
if (!site.dirty) {
|
|
866
|
+
site.inflight = void 0;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
site.dirty = false;
|
|
870
|
+
wantForce = site.dirtyForce;
|
|
871
|
+
site.dirtyForce = false;
|
|
872
|
+
}
|
|
758
873
|
};
|
|
759
|
-
const promise = loop(force
|
|
760
|
-
site.inflight = void 0;
|
|
761
|
-
});
|
|
874
|
+
const promise = loop(force);
|
|
762
875
|
site.inflight = promise;
|
|
763
876
|
return promise;
|
|
764
877
|
};
|
|
765
878
|
let resizeTimer;
|
|
766
879
|
const onResize = () => {
|
|
880
|
+
if (sweeping) {
|
|
881
|
+
clearTimeout(resizeTimer);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
767
884
|
clearTimeout(resizeTimer);
|
|
768
885
|
resizeTimer = setTimeout(() => {
|
|
886
|
+
if (sweeping) return;
|
|
769
887
|
for (const site of sites) scheduleCapture(site);
|
|
770
888
|
}, RESIZE_DEBOUNCE);
|
|
771
889
|
};
|
|
772
890
|
win.addEventListener("resize", onResize);
|
|
891
|
+
let recordingOn = captureEnabled.get();
|
|
773
892
|
const unsubscribeCapture = captureEnabled.subscribe(() => {
|
|
774
|
-
|
|
893
|
+
const on = captureEnabled.get();
|
|
894
|
+
if (on === recordingOn) return;
|
|
895
|
+
recordingOn = on;
|
|
896
|
+
if (on) {
|
|
897
|
+
finalizeRecording();
|
|
898
|
+
for (const site of sites) scheduleCapture(site);
|
|
899
|
+
} else finalizeRecording();
|
|
775
900
|
});
|
|
776
901
|
const extAcks = /* @__PURE__ */ new Map();
|
|
777
902
|
let nextExtReqId = 1;
|
|
@@ -869,7 +994,7 @@ function installXrayClient(options) {
|
|
|
869
994
|
};
|
|
870
995
|
const sweepWidths = () => {
|
|
871
996
|
const set = new Set(deriveBreakpoints([], [...matchMediaQueries]));
|
|
872
|
-
for (const site of sites) for (const
|
|
997
|
+
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
998
|
const breakpoints = [...set].toSorted((a, b) => a - b);
|
|
874
999
|
const min = breakpoints[0];
|
|
875
1000
|
const first = min === void 0 ? 390 : Math.max(1, Math.min(390, min - 1));
|
|
@@ -878,6 +1003,12 @@ function installXrayClient(options) {
|
|
|
878
1003
|
const MAX_SWEEP_PASSES = 8;
|
|
879
1004
|
const captureAllViews = async () => {
|
|
880
1005
|
const visited = /* @__PURE__ */ new Set();
|
|
1006
|
+
sweeping = true;
|
|
1007
|
+
for (const site of sites) site.sessionPlate = {
|
|
1008
|
+
name: site.name,
|
|
1009
|
+
views: [],
|
|
1010
|
+
breakpoints: []
|
|
1011
|
+
};
|
|
881
1012
|
try {
|
|
882
1013
|
for (let pass = 0;; pass++) {
|
|
883
1014
|
const pending = sweepWidths().filter((width) => !visited.has(width));
|
|
@@ -891,11 +1022,15 @@ function installXrayClient(options) {
|
|
|
891
1022
|
height: 2400,
|
|
892
1023
|
mobile: width < 600
|
|
893
1024
|
});
|
|
894
|
-
await Promise.all([...sites].map((site) => scheduleCapture(site, true
|
|
1025
|
+
await Promise.all([...sites].map((site) => scheduleCapture(site, true)));
|
|
895
1026
|
}
|
|
896
1027
|
await options.refreshCoverage?.();
|
|
897
1028
|
}
|
|
1029
|
+
sweeping = false;
|
|
1030
|
+
await Promise.all([...sites].map((site) => finalizeSessionPlate(site)));
|
|
898
1031
|
} finally {
|
|
1032
|
+
sweeping = false;
|
|
1033
|
+
for (const site of sites) site.sessionPlate = void 0;
|
|
899
1034
|
await extCommand("reset").catch(() => void 0);
|
|
900
1035
|
}
|
|
901
1036
|
};
|
|
@@ -917,12 +1052,11 @@ function installXrayClient(options) {
|
|
|
917
1052
|
delay: opts?.delay ?? defaultDelay,
|
|
918
1053
|
mode: opts?.mode ?? "auto",
|
|
919
1054
|
walker: opts?.walker,
|
|
920
|
-
|
|
1055
|
+
collect: opts?.collect,
|
|
921
1056
|
disposed: false,
|
|
922
1057
|
tooLarge: false,
|
|
923
1058
|
dirty: false,
|
|
924
|
-
dirtyForce: false
|
|
925
|
-
dirtyAwaitSink: false
|
|
1059
|
+
dirtyForce: false
|
|
926
1060
|
};
|
|
927
1061
|
sites.add(site);
|
|
928
1062
|
scheduleCapture(site);
|
|
@@ -1010,9 +1144,29 @@ function createToggleStore(win, key, param, initial) {
|
|
|
1010
1144
|
* made single-shot capture non-deterministic in the M1 harness; this is the
|
|
1011
1145
|
* cure. Hard-capped so a perpetually-animating subtree still captures.
|
|
1012
1146
|
*/
|
|
1147
|
+
/**
|
|
1148
|
+
* Force lazy images in the subtree to load and await their decode, so the walk
|
|
1149
|
+
* measures their real boxes. A `loading="lazy"` image below the fold never enters
|
|
1150
|
+
* the viewport during capture, so its IntersectionObserver never fires — flip it
|
|
1151
|
+
* to eager to trigger the load. Capped: a slow or broken image resolves the race
|
|
1152
|
+
* via the timeout instead of blocking the capture.
|
|
1153
|
+
*/
|
|
1154
|
+
function awaitImages(root, cap) {
|
|
1155
|
+
const imgs = Array.from(root.querySelectorAll("img"));
|
|
1156
|
+
if (imgs.length === 0) return Promise.resolve();
|
|
1157
|
+
const decoded = imgs.map((img) => {
|
|
1158
|
+
if (img.loading === "lazy") img.loading = "eager";
|
|
1159
|
+
if (typeof img.decode !== "function") return Promise.resolve();
|
|
1160
|
+
return img.decode().then(() => void 0, () => void 0);
|
|
1161
|
+
});
|
|
1162
|
+
return Promise.race([Promise.all(decoded).then(() => void 0), new Promise((resolve) => {
|
|
1163
|
+
setTimeout(resolve, cap);
|
|
1164
|
+
})]);
|
|
1165
|
+
}
|
|
1013
1166
|
function settle(el, delay, cap, mode = "auto") {
|
|
1014
1167
|
const doc = el.ownerDocument;
|
|
1015
|
-
|
|
1168
|
+
const fonts = doc.fonts ? doc.fonts.ready : Promise.resolve();
|
|
1169
|
+
return Promise.all([fonts, awaitImages(el, cap)]).then(() => new Promise((resolve) => {
|
|
1016
1170
|
let timer;
|
|
1017
1171
|
let capTimer;
|
|
1018
1172
|
const observer = new MutationObserver(bump);
|
package/dist/core.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as WalkerResult, c as captureRegime, i as CaptureWalkerContext, l as defineXrayCaptureWalker, n as CaptureRegimeOptions, o as XrayCaptureWalker, r as CaptureTooLargeError, s as capture, t as CaptureOptions } from "./serialize-B--oY2bV.js";
|
|
1
|
+
import { _ as emptyPlate, a as CollectAxis, c as XrayCaptureWalker, d as MergedPlate, f as Plate, g as ViewCapture, i as CaptureWalkerContext, l as defineXrayCaptureWalker, m as RenderNode, n as CaptureSize, o as CollectLimits, p as PlateNode, r as CaptureTooLargeError, s as WalkerDecision, t as CaptureOptions, u as LeafKind } from "./serialize-BgdGt34A.js";
|
|
3
2
|
|
|
4
3
|
//#region src/chunk.d.ts
|
|
5
4
|
/**
|
|
@@ -9,36 +8,25 @@ import { a as WalkerResult, c as captureRegime, i as CaptureWalkerContext, l as
|
|
|
9
8
|
* chunks and renders the fallback.
|
|
10
9
|
*/
|
|
11
10
|
declare function serializePlate(merged: MergedPlate): Plate;
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/merge.d.ts
|
|
14
11
|
/**
|
|
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.
|
|
12
|
+
* Distinct plate names referenced by the tree's stitches, in first-seen order.
|
|
13
|
+
* Walks a `RenderNode` tree (the stored, post-classify shape), so it drives the
|
|
14
|
+
* gen-side stitch import graph straight off a `StoredPlate.tree`
|
|
15
|
+
* (`renderPlateModule`, index.ts). Relocated here from the now-deleted `merge.ts`
|
|
16
|
+
* in the ADR 0022 cutover — it is the one node-side primitive that survived the
|
|
17
|
+
* `mergeViews`/`addCapture` removal (the multi-View merge/gate now lives in the
|
|
18
|
+
* in-browser `serializePlateIR`, project.ts).
|
|
30
19
|
*/
|
|
31
|
-
declare function mergeViews(file: PlateFile): MergedPlate;
|
|
32
|
-
/** Distinct plate names referenced by the tree's stitches, in first-seen order. */
|
|
33
20
|
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
21
|
//#endregion
|
|
37
22
|
//#region src/core.d.ts
|
|
38
23
|
/**
|
|
39
|
-
* Capture a live DOM subtree straight to a renderable Plate
|
|
40
|
-
*
|
|
41
|
-
* (
|
|
24
|
+
* Capture a live DOM subtree straight to a renderable Plate. Runs the same
|
|
25
|
+
* uniform pipeline the dev client does for a single View — measure (`captureView`),
|
|
26
|
+
* reduce (`runPlatePasses`), project (`serializePlateIR` → the structured
|
|
27
|
+
* `StoredPlate`), then lower to chunks (`serializePlate`) — in one call. `roots`
|
|
28
|
+
* are the top-level elements to capture (a component's rendered roots). Pass the
|
|
29
|
+
* result to a `<Skeleton plate>`.
|
|
42
30
|
*
|
|
43
31
|
* `options.walker` is the public programmable capture walker (ADR 0018, Q2 —
|
|
44
32
|
* yes, public): the same `XrayCaptureWalker` the `<Skeleton captureWalker>` prop
|
|
@@ -57,4 +45,4 @@ declare function captureElement(roots: readonly Element[], options: CaptureOptio
|
|
|
57
45
|
*/
|
|
58
46
|
declare function renderPlateHtml(plate: Plate): string;
|
|
59
47
|
//#endregion
|
|
60
|
-
export { type CaptureOptions, type
|
|
48
|
+
export { type CaptureOptions, type CaptureSize, CaptureTooLargeError, type CaptureWalkerContext, type CollectAxis, type CollectLimits, type LeafKind, type MergedPlate, type Plate, type PlateNode, type ViewCapture, type WalkerDecision, type XrayCaptureWalker, captureElement, collectRefs, defineXrayCaptureWalker, emptyPlate, renderPlateHtml, serializePlate };
|