@hueest/xray 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{serialize-Bq6yJnNV.js → breakpoints-CMoFUUOv.js} +426 -104
- package/dist/client.d.ts +38 -3
- package/dist/client.js +716 -27
- package/dist/core.d.ts +8 -3
- package/dist/core.js +12 -6
- package/dist/css-escape-N7bOusGW.js +52 -0
- package/dist/hud.js +46 -0
- package/dist/index.d.ts +231 -5
- package/dist/index.js +753 -71
- package/dist/{plate-DboxMKg-.d.ts → plate-Bv6W-GkA.d.ts} +142 -0
- package/dist/plate-DoE1HEXp.js +152 -0
- package/dist/react.core-BzMG_cDy.d.ts +79 -0
- package/dist/react.core-IyFy2b_8.js +267 -0
- package/dist/react.d.ts +2 -8
- package/dist/react.dev.d.ts +2 -9
- package/dist/react.dev.js +153 -23
- package/dist/react.js +24 -12
- package/dist/serialize-B--oY2bV.d.ts +143 -0
- package/package.json +7 -7
- package/virtual.d.ts +6 -0
- package/dist/breakpoints-CBNlh7lQ.js +0 -47
- package/dist/plate-BuzRMPx4.js +0 -94
- package/dist/react.core-CqnDjfAJ.js +0 -59
- package/dist/react.core-fgWG_svU.d.ts +0 -21
- package/dist/serialize.d.ts +0 -88
- package/dist/serialize.js +0 -2
package/dist/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as PlateNode, c as emptyPlate, i as PlateFile, n as MergedPlate, o as RenderNode, r as Plate, s as ViewCapture, t as LeafKind } from "./plate-
|
|
2
|
-
import {
|
|
1
|
+
import { a as PlateNode, c as emptyPlate, i as PlateFile, n as MergedPlate, o as RenderNode, r as Plate, s as ViewCapture, t as LeafKind } from "./plate-Bv6W-GkA.js";
|
|
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";
|
|
3
3
|
|
|
4
4
|
//#region src/chunk.d.ts
|
|
5
5
|
/**
|
|
@@ -39,6 +39,11 @@ declare function addCapture(file: PlateFile | null, name: string, capture: ViewC
|
|
|
39
39
|
* Capture a live DOM subtree straight to a renderable Plate — `capture` followed
|
|
40
40
|
* by `serializePlate` in one call. `roots` are the top-level elements to capture
|
|
41
41
|
* (a component's rendered roots). Pass the result to a `<Skeleton plate>`.
|
|
42
|
+
*
|
|
43
|
+
* `options.walker` is the public programmable capture walker (ADR 0018, Q2 —
|
|
44
|
+
* yes, public): the same `XrayCaptureWalker` the `<Skeleton captureWalker>` prop
|
|
45
|
+
* threads through in dev, available here for driving capture directly. It is
|
|
46
|
+
* capture-only and never persisted into the returned Plate.
|
|
42
47
|
*/
|
|
43
48
|
declare function captureElement(roots: readonly Element[], options: CaptureOptions): Plate;
|
|
44
49
|
/**
|
|
@@ -52,4 +57,4 @@ declare function captureElement(roots: readonly Element[], options: CaptureOptio
|
|
|
52
57
|
*/
|
|
53
58
|
declare function renderPlateHtml(plate: Plate): string;
|
|
54
59
|
//#endregion
|
|
55
|
-
export { type CaptureOptions, type CaptureRegimeOptions, CaptureTooLargeError, type LeafKind, type MergedPlate, type Plate, type PlateFile, type PlateNode, type ViewCapture, addCapture, capture, captureElement, captureRegime, collectRefs, emptyPlate, mergeViews, renderPlateHtml, serializePlate };
|
|
60
|
+
export { type CaptureOptions, type CaptureRegimeOptions, CaptureTooLargeError, type CaptureWalkerContext, type LeafKind, type MergedPlate, type Plate, type PlateFile, type PlateNode, type ViewCapture, type WalkerResult, type XrayCaptureWalker, addCapture, capture, captureElement, captureRegime, collectRefs, defineXrayCaptureWalker, emptyPlate, mergeViews, renderPlateHtml, serializePlate };
|
package/dist/core.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as ROOT_ATTR,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { a as ROOT_ATTR, c as SPEC_GATE, d as emptyPlate, i as NODE_CLASS, o as ROOT_CLASS, r as LEAF_CLASS, t as BASE_CSS } from "./plate-DoE1HEXp.js";
|
|
2
|
+
import { a as captureRegime, i as capture, n as regimeFor, o as defineXrayCaptureWalker, r as CaptureTooLargeError, u as classify } from "./breakpoints-CMoFUUOv.js";
|
|
3
|
+
import { t as escapeStyleText } from "./css-escape-N7bOusGW.js";
|
|
4
4
|
//#region src/chunk.ts
|
|
5
5
|
/**
|
|
6
6
|
* Serialize a merged plate's RenderNode tree into the shipped `Plate` (ADR
|
|
@@ -190,7 +190,8 @@ function remapRules(rules, idMap) {
|
|
|
190
190
|
function addCapture(file, name, capture, breakpoints) {
|
|
191
191
|
const allBreakpoints = [...new Set([...file?.breakpoints ?? [], ...breakpoints])].toSorted((a, b) => a - b);
|
|
192
192
|
const spanKey = (width) => JSON.stringify(regimeFor(width, allBreakpoints));
|
|
193
|
-
const
|
|
193
|
+
const { diagnostics: _diagnostics, ...persisted } = capture;
|
|
194
|
+
const views = [...file?.views ?? [], persisted];
|
|
194
195
|
const byView = /* @__PURE__ */ new Map();
|
|
195
196
|
for (const view of views) byView.set(spanKey(view.width), view);
|
|
196
197
|
return {
|
|
@@ -206,6 +207,11 @@ function addCapture(file, name, capture, breakpoints) {
|
|
|
206
207
|
* Capture a live DOM subtree straight to a renderable Plate — `capture` followed
|
|
207
208
|
* by `serializePlate` in one call. `roots` are the top-level elements to capture
|
|
208
209
|
* (a component's rendered roots). Pass the result to a `<Skeleton plate>`.
|
|
210
|
+
*
|
|
211
|
+
* `options.walker` is the public programmable capture walker (ADR 0018, Q2 —
|
|
212
|
+
* yes, public): the same `XrayCaptureWalker` the `<Skeleton captureWalker>` prop
|
|
213
|
+
* threads through in dev, available here for driving capture directly. It is
|
|
214
|
+
* capture-only and never persisted into the returned Plate.
|
|
209
215
|
*/
|
|
210
216
|
function captureElement(roots, options) {
|
|
211
217
|
return serializePlate(capture(roots, options));
|
|
@@ -224,7 +230,7 @@ function renderPlateHtml(plate) {
|
|
|
224
230
|
if (!chunks) return "";
|
|
225
231
|
if (chunks.length !== 1 || typeof chunks[0] !== "string") throw new Error("xray: renderPlateHtml renders stitch-free plates only — use a framework adapter for stitched plates");
|
|
226
232
|
const className = plate.rootCls ? `${ROOT_CLASS} ${plate.rootCls}` : ROOT_CLASS;
|
|
227
|
-
return `<div ${ROOT_ATTR}="${plate.name}" class="${className}" aria-hidden="true" aria-busy="true"><style>${BASE_CSS}\n${plate.css}</style>${chunks[0]}</div>`;
|
|
233
|
+
return `<div ${ROOT_ATTR}="${plate.name}" class="${className}" aria-hidden="true" aria-busy="true"><style>${BASE_CSS}\n${escapeStyleText(plate.css)}</style>${chunks[0]}</div>`;
|
|
228
234
|
}
|
|
229
235
|
//#endregion
|
|
230
|
-
export { CaptureTooLargeError, addCapture, capture, captureElement, captureRegime, collectRefs, emptyPlate, mergeViews, renderPlateHtml, serializePlate };
|
|
236
|
+
export { CaptureTooLargeError, addCapture, capture, captureElement, captureRegime, collectRefs, defineXrayCaptureWalker, emptyPlate, mergeViews, renderPlateHtml, serializePlate };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/css-escape.ts
|
|
2
|
+
/**
|
|
3
|
+
* Dependency-free raw-text safety for the one place a Plate's CSS string is
|
|
4
|
+
* interpolated into `<style>…</style>` markup.
|
|
5
|
+
*
|
|
6
|
+
* A plate's `css` is normally produced by the capture path (`filterLayoutDecls`
|
|
7
|
+
* + `classify`), which only ever emits well-formed `selector { prop: value }`
|
|
8
|
+
* rules — never a raw-text terminator. But a committed `plates/<name>.json` is
|
|
9
|
+
* UNTRUSTED at render time (it can be hand-edited or arrive over git, see the
|
|
10
|
+
* capture-input-validation plan's decisions), and a corrupt/malicious `css`
|
|
11
|
+
* could carry a `</style>` or an HTML comment delimiter. `<style>` is a
|
|
12
|
+
* raw-text element: the parser ends the element at the first `</style` it sees
|
|
13
|
+
* and resumes parsing the rest as ordinary HTML, so an unescaped terminator in
|
|
14
|
+
* the CSS body turns the tail of the plate into live markup.
|
|
15
|
+
*
|
|
16
|
+
* The React text-child path (`<style>{css}</style>`) is already safe — React
|
|
17
|
+
* escapes the child — but the two STRING paths interpolate `css` directly into
|
|
18
|
+
* a markup string (`renderPlateHtml` builds an HTML string; `react.core.tsx`
|
|
19
|
+
* feeds one to `dangerouslySetInnerHTML`). Those must run the body through this
|
|
20
|
+
* helper first.
|
|
21
|
+
*
|
|
22
|
+
* This lives in its own dependency-free module ON PURPOSE: it is imported by
|
|
23
|
+
* render/browser paths, so it must NOT pull in the Node-only validation code
|
|
24
|
+
* (valibot). It neutralizes only the sequences that can escape a raw-text
|
|
25
|
+
* `<style>` context — a `<` that begins `</style`, and the HTML comment / CDATA
|
|
26
|
+
* delimiters the tokenizer treats specially inside raw text — by inserting a
|
|
27
|
+
* CSS-comment break (`/**/`) that the CSS parser ignores. It does not, and is
|
|
28
|
+
* not meant to, sanitize CSS semantically; POST-time validation
|
|
29
|
+
* (`validate.ts`) is what rejects junk declarations.
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Make a CSS body safe to interpolate into a `<style>…</style>` STRING. Breaks
|
|
35
|
+
* up the only sequences the HTML tokenizer acts on inside a raw-text `<style>`
|
|
36
|
+
* element, so none can terminate the element early or open a comment/CDATA run:
|
|
37
|
+
*
|
|
38
|
+
* - `</style` (the end-tag open, case-insensitive) — inserts the break between
|
|
39
|
+
* `<` and `/style` so the parser never matches the end tag.
|
|
40
|
+
* - `<!--`, `-->`, `<![CDATA[` — the comment/CDATA delimiters; split so neither
|
|
41
|
+
* a comment nor a CDATA section can open.
|
|
42
|
+
*
|
|
43
|
+
* The inserted `/**/` is an empty CSS comment: inert to the CSS parser (it
|
|
44
|
+
* cannot appear mid-token in a value we emit, and a stray one only ends a
|
|
45
|
+
* token early), so a well-formed plate is rendered byte-for-byte equivalently
|
|
46
|
+
* while a corrupt one can no longer break out of the style element.
|
|
47
|
+
*/
|
|
48
|
+
function escapeStyleText(css) {
|
|
49
|
+
return css.replace(/<(\/style)/gi, "</**/$1").replace(/<!--/g, "<!/**/--").replace(/-->/g, "--/**/>").replace(/<!\[CDATA\[/g, "<![/**/CDATA[");
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { escapeStyleText as t };
|
package/dist/hud.js
CHANGED
|
@@ -52,6 +52,7 @@ function installXrayHud() {
|
|
|
52
52
|
.bands { text-align: right }
|
|
53
53
|
.band.covered { color: #4ade80 }
|
|
54
54
|
.band.missing { color: #52525b }
|
|
55
|
+
.fixture { color: #fbbf24; margin-left: 6px }
|
|
55
56
|
.toggle { display: flex; gap: 6px; align-items: center; cursor: pointer; user-select: none }
|
|
56
57
|
.toggle input { accent-color: #4ade80; margin: 0 }
|
|
57
58
|
.action {
|
|
@@ -70,6 +71,8 @@ function installXrayHud() {
|
|
|
70
71
|
let extensionPresent = false;
|
|
71
72
|
let sweeping = false;
|
|
72
73
|
let sweepStatus = "";
|
|
74
|
+
let recording = false;
|
|
75
|
+
let recordStatus = "";
|
|
73
76
|
const render = () => {
|
|
74
77
|
panel.textContent = "";
|
|
75
78
|
const coverage = globalThis.__XRAY__?.coverage?.get();
|
|
@@ -82,6 +85,26 @@ function installXrayHud() {
|
|
|
82
85
|
if (capture) panel.append(toggleRow(capture, "Capture"));
|
|
83
86
|
const lightbox = globalThis.__XRAY__?.lightbox;
|
|
84
87
|
if (lightbox) panel.append(toggleRow(lightbox, "Light Box"));
|
|
88
|
+
const replay = globalThis.__XRAY__?.replay;
|
|
89
|
+
if (replay) panel.append(toggleRow(replay, "Replay"));
|
|
90
|
+
const fixtures = globalThis.__XRAY__?.fixtures;
|
|
91
|
+
if (fixtures) {
|
|
92
|
+
const button = document.createElement("button");
|
|
93
|
+
button.className = "action";
|
|
94
|
+
button.type = "button";
|
|
95
|
+
button.disabled = recording;
|
|
96
|
+
button.textContent = recording ? "Recording fixture…" : "Record fixture";
|
|
97
|
+
button.addEventListener("click", () => {
|
|
98
|
+
recordFixtures(fixtures);
|
|
99
|
+
});
|
|
100
|
+
panel.append(button);
|
|
101
|
+
if (recordStatus) {
|
|
102
|
+
const status = document.createElement("div");
|
|
103
|
+
status.className = "status";
|
|
104
|
+
status.textContent = recordStatus;
|
|
105
|
+
panel.append(status);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
85
108
|
if (globalThis.__XRAY__?.captureAllViews) if (extensionPresent) {
|
|
86
109
|
const button = document.createElement("button");
|
|
87
110
|
button.className = "action";
|
|
@@ -109,6 +132,12 @@ function installXrayHud() {
|
|
|
109
132
|
row.className = "plate";
|
|
110
133
|
const label = document.createElement("span");
|
|
111
134
|
label.textContent = name;
|
|
135
|
+
if (globalThis.__XRAY__?.fixtures?.has(name)) {
|
|
136
|
+
const tag = document.createElement("span");
|
|
137
|
+
tag.className = "fixture";
|
|
138
|
+
tag.textContent = "◆ fixture";
|
|
139
|
+
label.append(tag);
|
|
140
|
+
}
|
|
112
141
|
const bands = document.createElement("span");
|
|
113
142
|
bands.className = "bands";
|
|
114
143
|
for (const band of bandsOf(plateCoverage)) {
|
|
@@ -136,6 +165,21 @@ function installXrayHud() {
|
|
|
136
165
|
render();
|
|
137
166
|
}
|
|
138
167
|
}
|
|
168
|
+
async function recordFixtures(fixtures) {
|
|
169
|
+
if (recording || !fixtures) return;
|
|
170
|
+
recording = true;
|
|
171
|
+
recordStatus = "";
|
|
172
|
+
render();
|
|
173
|
+
try {
|
|
174
|
+
await fixtures.recordAll();
|
|
175
|
+
recordStatus = "Fixture recorded.";
|
|
176
|
+
} catch (error) {
|
|
177
|
+
recordStatus = `Failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
178
|
+
} finally {
|
|
179
|
+
recording = false;
|
|
180
|
+
render();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
139
183
|
const onHudMessage = (event) => {
|
|
140
184
|
if (event.source !== window) return;
|
|
141
185
|
if (isExtensionHello(event.data) && !extensionPresent) {
|
|
@@ -149,6 +193,7 @@ function installXrayHud() {
|
|
|
149
193
|
kind: "whois"
|
|
150
194
|
}, "*");
|
|
151
195
|
const unsubscribeCoverage = globalThis.__XRAY__?.coverage?.subscribe(render);
|
|
196
|
+
const unsubscribeFixtures = globalThis.__XRAY__?.fixtures?.subscribe(render);
|
|
152
197
|
render();
|
|
153
198
|
const mount = () => {
|
|
154
199
|
if (document.body) document.body.append(host);
|
|
@@ -158,6 +203,7 @@ function installXrayHud() {
|
|
|
158
203
|
return { dispose() {
|
|
159
204
|
window.removeEventListener("message", onHudMessage);
|
|
160
205
|
unsubscribeCoverage?.();
|
|
206
|
+
unsubscribeFixtures?.();
|
|
161
207
|
host.remove();
|
|
162
208
|
} };
|
|
163
209
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import { Plugin } from "vite";
|
|
2
2
|
|
|
3
|
+
//#region src/diagnostics.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Dev-only capture diagnostics: a small, serializable record of why a Plate may
|
|
6
|
+
* be incomplete or lower-fidelity, plus the one shared formatter both the
|
|
7
|
+
* browser console (`client.ts`) and the Vite server output (`index.ts`) print.
|
|
8
|
+
*
|
|
9
|
+
* Motivation: the serializer silently swallows a handful of capture omissions
|
|
10
|
+
* (an unreadable cross-origin stylesheet, skipped pseudo-elements, a collapsed
|
|
11
|
+
* list run, an over-the-limit subtree). Early adopters asking "why does this
|
|
12
|
+
* skeleton look wrong?" had to read the serializer source to find out. These
|
|
13
|
+
* diagnostics surface those omissions as data the capture process returns, so a
|
|
14
|
+
* caller can log them — without the core performing any console side effects.
|
|
15
|
+
*
|
|
16
|
+
* Strictly DEV-ONLY (see the plan's decisions and ADR 0008): this type is NOT a
|
|
17
|
+
* public package export and MUST NEVER reach the persisted Plate JSON, the
|
|
18
|
+
* `MergedPlate`, or the production adapter. It rides on the TRANSIENT
|
|
19
|
+
* `ViewCapture.diagnostics` field only, which `captureRegime` populates and the
|
|
20
|
+
* dev client strips before posting (so committed `plates/<name>.json` stays
|
|
21
|
+
* byte-identical to a capture that produced no diagnostics).
|
|
22
|
+
*
|
|
23
|
+
* Centralized on purpose: a follow-up plan (capture-input-validation) reuses
|
|
24
|
+
* this same messaging for invalid posted/committed plates, so the code table
|
|
25
|
+
* and formatter are kept reusable. Browser and server share `formatDiagnostic`
|
|
26
|
+
* so their warning text cannot drift.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* One aggregated capture omission. Serializable on purpose: NO DOM nodes, NO
|
|
32
|
+
* CSS bodies, NO captured text — only a stable `code`, a human message, an
|
|
33
|
+
* optional aggregate `count`, and an optional short `detail` (e.g. a plate name
|
|
34
|
+
* or a node count, never user content). `count` aggregates occurrences: there
|
|
35
|
+
* is at most one record per code per capture, never one per skipped selector or
|
|
36
|
+
* node.
|
|
37
|
+
*/
|
|
38
|
+
interface CaptureDiagnostic {
|
|
39
|
+
code: DiagnosticCode;
|
|
40
|
+
message: string;
|
|
41
|
+
count?: number;
|
|
42
|
+
detail?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The closed set of capture-omission codes. Each maps to one silent site in the
|
|
46
|
+
* serializer (or an existing oversized-capture warning folded into the same
|
|
47
|
+
* vocabulary). Kept as a const union so the message table is exhaustive.
|
|
48
|
+
*/
|
|
49
|
+
type DiagnosticCode = 'unreadable-stylesheet' | 'unreadable-import' | 'skipped-dynamic-pseudo' | 'skipped-pseudo-element' | 'unsupported-rule' | 'dropped-tag' | 'pruned-run' | 'too-large' | 'invalid-plate-file';
|
|
50
|
+
//#endregion
|
|
3
51
|
//#region src/plate.d.ts
|
|
4
52
|
type LeafKind = 'text' | 'media' | 'box';
|
|
5
53
|
/**
|
|
@@ -110,6 +158,17 @@ interface ViewCapture {
|
|
|
110
158
|
max?: number;
|
|
111
159
|
tree: PlateNode;
|
|
112
160
|
rules: PlateRule[];
|
|
161
|
+
/**
|
|
162
|
+
* TRANSIENT, dev-only capture diagnostics (why this view may be lower
|
|
163
|
+
* fidelity). Populated by `captureRegime` in the browser; the dev client logs
|
|
164
|
+
* it and posts it (so the Vite server can print the same text), then it is
|
|
165
|
+
* STRIPPED at the merge layer (`addCapture`) before persisting. It MUST NEVER
|
|
166
|
+
* be persisted: it is not part of the committed `plates/<name>.json` shape,
|
|
167
|
+
* the `isViewCapture` validator does not require it, and `addCapture` drops it
|
|
168
|
+
* so neither it nor `mergeViews` ever writes it. Absent in production (the
|
|
169
|
+
* prod adapter ships no capture). See diagnostics.ts and ADR 0008.
|
|
170
|
+
*/
|
|
171
|
+
diagnostics?: CaptureDiagnostic[];
|
|
113
172
|
}
|
|
114
173
|
/** The committed `plates/<name>.json`: per-view captures, merged at load. */
|
|
115
174
|
interface PlateFile {
|
|
@@ -151,6 +210,68 @@ interface Span {
|
|
|
151
210
|
min?: number;
|
|
152
211
|
max?: number;
|
|
153
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Dev-only Fixture store (ADR 0015): stored render-INPUT `data` per Plate,
|
|
215
|
+
* recorded to and replayed from a `plates/<name>.fixture.json` sidecar. Entirely
|
|
216
|
+
* dev-only — devalue and this store never reach the production bundle.
|
|
217
|
+
*
|
|
218
|
+
* Two cooperating halves, both flowing through the render-prop `data` seam (ADR
|
|
219
|
+
* 0014):
|
|
220
|
+
* - The dev adapter (`react.dev.tsx`) calls `register(name, data)` at
|
|
221
|
+
* `loading=false` with the live `data` it is about to render, so the latest
|
|
222
|
+
* recordable input for each mounted Plate is always in scope for Record.
|
|
223
|
+
* - `record(name)` serializes that registered live `data` with devalue and POSTs
|
|
224
|
+
* it to the sidecar (the HUD button and the `first-ready`/`always` modes call
|
|
225
|
+
* it). `has(name)` reports whether a Fixture exists (HUD coverage marker), and
|
|
226
|
+
* `read(name)` returns the parsed Fixture `data` for Replay to substitute in
|
|
227
|
+
* front of the render prop.
|
|
228
|
+
*
|
|
229
|
+
* The store holds DECODED values (the dev client owns the devalue parse, since it
|
|
230
|
+
* has the live runtime); only the on-disk sidecar and the wire carry the opaque
|
|
231
|
+
* devalue string.
|
|
232
|
+
*/
|
|
233
|
+
interface FixtureStore {
|
|
234
|
+
/**
|
|
235
|
+
* Record the latest live `data` the dev adapter is about to render for `name`,
|
|
236
|
+
* so Record always has the current recordable input in scope. A `register` with
|
|
237
|
+
* the same value is cheap; the adapter calls it on every ready render. Returns a
|
|
238
|
+
* disposer the adapter runs on unmount so an unmounted Skeleton's `data` never
|
|
239
|
+
* lingers in the store: an unmounted instance must never be recorded or shadow a
|
|
240
|
+
* still-mounted one (ADR 0015). With several Skeletons of the same plate name
|
|
241
|
+
* mounted, the live value is the most-recently-registered still-mounted instance.
|
|
242
|
+
*/
|
|
243
|
+
register: (name: string, data: unknown) => () => void;
|
|
244
|
+
/** True once a recorded Fixture exists for `name` (drives the HUD coverage marker). */
|
|
245
|
+
has: (name: string) => boolean;
|
|
246
|
+
/** The parsed Fixture `data` for `name`, or `undefined` when none is recorded. */
|
|
247
|
+
read: (name: string) => {
|
|
248
|
+
data: unknown;
|
|
249
|
+
} | undefined;
|
|
250
|
+
/**
|
|
251
|
+
* Serialize the registered live `data` for `name` with devalue and POST it to
|
|
252
|
+
* the sidecar. Returns a promise that resolves once the write is
|
|
253
|
+
* server-confirmed (so the HUD can surface a failed Record). Throws loudly when
|
|
254
|
+
* the live `data` is unrecordable (devalue's contract) or no live `data` was
|
|
255
|
+
* registered (a plain-children `<Skeleton>` has none).
|
|
256
|
+
*/
|
|
257
|
+
record: (name: string) => Promise<void>;
|
|
258
|
+
/**
|
|
259
|
+
* Record every Skeleton that currently has live `data` registered (the HUD's
|
|
260
|
+
* single "Record fixture" button — one click captures the inputs of all mounted
|
|
261
|
+
* render-prop Skeletons). Resolves once all writes are server-confirmed; a
|
|
262
|
+
* rejected write surfaces to the HUD.
|
|
263
|
+
*/
|
|
264
|
+
recordAll: () => Promise<void>;
|
|
265
|
+
/**
|
|
266
|
+
* Seed the store from the on-disk sidecars on boot: a `{ <name>: devalueString }`
|
|
267
|
+
* map (the opaque strings the server serves). The client decodes each with
|
|
268
|
+
* devalue. Existing entries are not clobbered, so a Fixture recorded this
|
|
269
|
+
* session survives a re-seed.
|
|
270
|
+
*/
|
|
271
|
+
seed: (encoded: Record<string, string>) => void;
|
|
272
|
+
/** React subscription so the HUD re-renders when a Fixture is recorded or seeded. */
|
|
273
|
+
subscribe: (onChange: () => void) => () => void;
|
|
274
|
+
}
|
|
154
275
|
/** A plate's detected width thresholds and the spans actually captured, from disk. */
|
|
155
276
|
interface PlateCoverage {
|
|
156
277
|
breakpoints: number[];
|
|
@@ -175,6 +296,8 @@ interface CoverageStore {
|
|
|
175
296
|
interface CaptureHook {
|
|
176
297
|
captured: (name: string, el: Element, opts?: {
|
|
177
298
|
delay?: number;
|
|
299
|
+
walker?: unknown;
|
|
300
|
+
mode?: 'auto' | 'manual';
|
|
178
301
|
}) => (() => void) | undefined;
|
|
179
302
|
/** Light Box toggle: force every `<Skeleton>` to its skeleton, read by `<Skeleton>`. */
|
|
180
303
|
lightbox?: ToggleStore;
|
|
@@ -188,6 +311,25 @@ interface CaptureHook {
|
|
|
188
311
|
captureAllViews?: () => Promise<void>;
|
|
189
312
|
/** Dev-only per-plate breakpoints + captured spans, from disk; fed by the bootstrap. */
|
|
190
313
|
coverage?: CoverageStore;
|
|
314
|
+
/**
|
|
315
|
+
* Replay toggle (ADR 0015): when active, the dev adapter substitutes a recorded
|
|
316
|
+
* Fixture's `data` for the live `data` at `loading=false`. The boolean is the
|
|
317
|
+
* coarse on/off the HUD owns per tab; the per-mode policy (`prefer` vs
|
|
318
|
+
* `missing-only`) is carried alongside in `replayMode`.
|
|
319
|
+
*/
|
|
320
|
+
replay?: ToggleStore;
|
|
321
|
+
/** Dev-only Replay policy: how a Fixture substitutes for live `data` (ADR 0015). */
|
|
322
|
+
replayMode?: 'prefer' | 'missing-only';
|
|
323
|
+
/**
|
|
324
|
+
* Dev-only Record policy (ADR 0015): when the dev adapter records the live
|
|
325
|
+
* `data` it is about to render. `'manual'` (the default) records nothing
|
|
326
|
+
* automatically — only the HUD button does; `'first-ready'` records once when a
|
|
327
|
+
* Skeleton goes ready and has no Fixture yet; `'always'` records on every ready
|
|
328
|
+
* render.
|
|
329
|
+
*/
|
|
330
|
+
recordMode?: 'manual' | 'first-ready' | 'always';
|
|
331
|
+
/** Dev-only Fixture record/replay store (ADR 0015); never present in production. */
|
|
332
|
+
fixtures?: FixtureStore;
|
|
191
333
|
}
|
|
192
334
|
declare global {
|
|
193
335
|
var __XRAY__: CaptureHook | undefined;
|
|
@@ -217,6 +359,35 @@ declare function collectRefs(tree: RenderNode | null): string[];
|
|
|
217
359
|
/** Fold a fresh capture into the plate file: union breakpoints, one capture per view. */
|
|
218
360
|
declare function addCapture(file: PlateFile | null, name: string, capture: ViewCapture, breakpoints: readonly number[]): PlateFile;
|
|
219
361
|
//#endregion
|
|
362
|
+
//#region src/name.d.ts
|
|
363
|
+
/**
|
|
364
|
+
* Plate-name validation, shared by the Vite plugin (`index.ts`) and the
|
|
365
|
+
* Node-only POST validator (`validate.ts`).
|
|
366
|
+
*
|
|
367
|
+
* Lives in its own dependency-free module so `validate.ts` can reuse it without
|
|
368
|
+
* importing the plugin entry (which would be a cycle), and so the regex stays
|
|
369
|
+
* the single source of truth for what a plate name may be — the same rule the
|
|
370
|
+
* loader, the file watcher, the coverage reader, the POST endpoint, and the
|
|
371
|
+
* ref-graph emitter all gate on.
|
|
372
|
+
*
|
|
373
|
+
* @module
|
|
374
|
+
*/
|
|
375
|
+
/**
|
|
376
|
+
* Names come from import specifiers and POST bodies — keep them inside the plates dir.
|
|
377
|
+
*
|
|
378
|
+
* Each path segment is `\w+(-\w+)*` — word chars with single interior hyphens,
|
|
379
|
+
* never doubled and never at a segment edge — and segments are joined by `/`.
|
|
380
|
+
* The class-prefix encoder (`classify.ts`) renders `/` as `--` to keep nested
|
|
381
|
+
* names readable, and that prefix is the isolation boundary across stitched
|
|
382
|
+
* plates (ADR 0007), so the encoding must be injective. Banning a literal `--`
|
|
383
|
+
* and any edge hyphen guarantees no `-` ever abuts a `/`, so every `--` in the
|
|
384
|
+
* prefix came from exactly one `/` — making distinct names always map to
|
|
385
|
+
* distinct prefixes (else e.g. `a/-b` and `a-/b` would both encode to `a---b`).
|
|
386
|
+
*
|
|
387
|
+
* @internal
|
|
388
|
+
*/
|
|
389
|
+
declare function isValidPlateName(name: string): boolean;
|
|
390
|
+
//#endregion
|
|
220
391
|
//#region src/index.d.ts
|
|
221
392
|
/**
|
|
222
393
|
* Options for `xrayVitePlugin()`.
|
|
@@ -243,6 +414,29 @@ interface XrayOptions {
|
|
|
243
414
|
settleCap?: number;
|
|
244
415
|
/** Refuse captures larger than this many nodes (Skeleton too high in the tree). Default: `4000`. */
|
|
245
416
|
maxNodes?: number;
|
|
417
|
+
/**
|
|
418
|
+
* Dev-only Fixtures (ADR 0015): record a `<Skeleton>`'s render-input `data` to a
|
|
419
|
+
* `plates/<name>.fixture.json` sidecar and replay it in place of live data so
|
|
420
|
+
* capture is deterministic. Boot state only — the HUD is per-tab authority, just
|
|
421
|
+
* like `capture`. Entirely dev-only; nothing here ships in production.
|
|
422
|
+
*/
|
|
423
|
+
fixtures?: {
|
|
424
|
+
/**
|
|
425
|
+
* When to record the live `data`: `'manual'` (HUD button only — the default,
|
|
426
|
+
* so live app data is never silently committed), `'first-ready'` (record once
|
|
427
|
+
* the first time a Skeleton goes ready with no Fixture yet), or `'always'`
|
|
428
|
+
* (record on every ready render). Default: `'manual'`.
|
|
429
|
+
*/
|
|
430
|
+
record?: 'manual' | 'first-ready' | 'always';
|
|
431
|
+
/**
|
|
432
|
+
* Whether to replay a recorded Fixture in place of live `data`: `false` (off,
|
|
433
|
+
* the default), `'prefer'` (always use the Fixture when one exists), or
|
|
434
|
+
* `'missing-only'` (use the Fixture only when live `data` is nullish —
|
|
435
|
+
* `missing ≡ nullish`, so an empty array/object is present and stays
|
|
436
|
+
* capturable). Default: `false`.
|
|
437
|
+
*/
|
|
438
|
+
replay?: false | 'prefer' | 'missing-only';
|
|
439
|
+
};
|
|
246
440
|
}
|
|
247
441
|
/**
|
|
248
442
|
* Create the Vite plugins that resolve committed Plates and, during dev,
|
|
@@ -265,7 +459,10 @@ interface XrayOptions {
|
|
|
265
459
|
declare function xrayVitePlugin(options?: XrayOptions): Plugin[];
|
|
266
460
|
interface CaptureRequest {
|
|
267
461
|
method?: string;
|
|
462
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
268
463
|
on(event: 'data' | 'end', listener: (chunk?: Buffer | string) => void): unknown;
|
|
464
|
+
/** Stop buffering the request body once the byte cap is exceeded. */
|
|
465
|
+
destroy?: () => void;
|
|
269
466
|
}
|
|
270
467
|
interface CaptureResponse {
|
|
271
468
|
statusCode: number;
|
|
@@ -274,8 +471,21 @@ interface CaptureResponse {
|
|
|
274
471
|
interface PlateBroadcaster {
|
|
275
472
|
send(payload: unknown): void;
|
|
276
473
|
}
|
|
474
|
+
/** The subset of Vite's logger we use; it preserves the formatted string verbatim. */
|
|
475
|
+
interface ServerLogger {
|
|
476
|
+
warn(message: string): void;
|
|
477
|
+
}
|
|
277
478
|
interface CaptureServer {
|
|
278
479
|
ws: PlateBroadcaster;
|
|
480
|
+
/**
|
|
481
|
+
* Vite's logger (`server.config.logger`). When present its `warn` prints the
|
|
482
|
+
* diagnostic text to the server output; otherwise we fall back to
|
|
483
|
+
* `console.warn`. Either way the SAME shared formatter (diagnostics.ts) is
|
|
484
|
+
* used, so server output matches the browser console exactly.
|
|
485
|
+
*/
|
|
486
|
+
config?: {
|
|
487
|
+
logger?: ServerLogger;
|
|
488
|
+
};
|
|
279
489
|
}
|
|
280
490
|
/**
|
|
281
491
|
* Per-plate breakpoints + captured spans, read from the committed plate files.
|
|
@@ -290,17 +500,33 @@ declare function readCoverage(dir: string): Record<string, {
|
|
|
290
500
|
}[];
|
|
291
501
|
}>;
|
|
292
502
|
/**
|
|
293
|
-
*
|
|
503
|
+
* All recorded Fixtures as `{ <name>: <devalueString> }`, read from the
|
|
504
|
+
* `<name>.fixture.json` sidecars (ADR 0015). The map values are the OPAQUE
|
|
505
|
+
* devalue strings exactly as written — the node side never parses them; the
|
|
506
|
+
* browser dev client owns the devalue decode. The `.fixture.json` suffix keeps
|
|
507
|
+
* these distinct from Plate files (`isValidPlateName` rejects the dotted name, so
|
|
508
|
+
* `readCoverage` never picks them up — Fixtures never leak into Plate coverage).
|
|
294
509
|
*
|
|
295
510
|
* @internal
|
|
296
511
|
*/
|
|
297
|
-
declare function
|
|
512
|
+
declare function readFixtures(dir: string): Record<string, string>;
|
|
298
513
|
/**
|
|
299
|
-
*
|
|
514
|
+
* Write a recorded Fixture sidecar (ADR 0015). Reuses the capture endpoint's
|
|
515
|
+
* front-door checks (method, content type, declared/streamed size cap) and the
|
|
516
|
+
* `isValidPlateName` gate, but treats the devalue `data` payload as OPAQUE TEXT:
|
|
517
|
+
* it is validated only as a string and written verbatim, never eval'd or parsed
|
|
518
|
+
* server-side. The sidecar lives NEXT TO the Plate but is never merged into it,
|
|
519
|
+
* so the persisted Plate stays purely structural.
|
|
300
520
|
*
|
|
301
521
|
* @internal
|
|
302
522
|
*/
|
|
303
|
-
declare function
|
|
523
|
+
declare function handleFixturePost(req: CaptureRequest, res: CaptureResponse, fixturePath: (name: string) => string): void;
|
|
524
|
+
/**
|
|
525
|
+
* Fold a posted capture into its plate file and hot-swap the plate in place.
|
|
526
|
+
*
|
|
527
|
+
* @internal
|
|
528
|
+
*/
|
|
529
|
+
declare function handleCapturePost(req: CaptureRequest, res: CaptureResponse, server: CaptureServer, platePath: (name: string) => string): void;
|
|
304
530
|
/**
|
|
305
531
|
* Render a plate as the virtual module's source. Each stitch (`ref` node) becomes
|
|
306
532
|
* a real import of that child's virtual module, wired into a `refs` map on the
|
|
@@ -314,4 +540,4 @@ declare function isValidPlateName(name: string): boolean;
|
|
|
314
540
|
*/
|
|
315
541
|
declare function renderPlateModule(merged: MergedPlate): string;
|
|
316
542
|
//#endregion
|
|
317
|
-
export { type LeafKind, type Plate, type PlateFile, type PlateNode, type PlateRule, type ViewCapture, XrayOptions, addCapture, collectRefs, handleCapturePost, isValidPlateName, mergeViews, readCoverage, renderPlateModule, xrayVitePlugin };
|
|
543
|
+
export { type LeafKind, type Plate, type PlateFile, type PlateNode, type PlateRule, type ViewCapture, XrayOptions, addCapture, collectRefs, handleCapturePost, handleFixturePost, isValidPlateName, mergeViews, readCoverage, readFixtures, renderPlateModule, xrayVitePlugin };
|