@hueest/xray 0.1.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.
@@ -0,0 +1,317 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/plate.d.ts
4
+ type LeafKind = 'text' | 'media' | 'box';
5
+ /**
6
+ * The shipped structural capture of a component's rendered DOM — the data a
7
+ * Skeleton renders from (see CONTEXT.md). Self-contained: pre-serialized chunks
8
+ * plus one scoped CSS string; depends on nothing in the consumer's build (ADR
9
+ * 0003). Built from the RenderNode tree at plugin load (see chunk.ts), so the
10
+ * adapter injects strings instead of walking a tree on the latency-critical
11
+ * first paint — and no per-framework serializer ships to the client (ADR 0008).
12
+ */
13
+ interface Plate {
14
+ v: number;
15
+ /** Capture name; comes from the virtual-module specifier, never typed at a call site. */
16
+ name: string;
17
+ /**
18
+ * The root's children, pre-serialized (ADR 0008): a stitch-free run is one
19
+ * HTML `string`; a stitch is `{ r: name }`; a stitch-bearing element is
20
+ * `{ c: className, k: chunk[] }`. Null until first capture.
21
+ */
22
+ chunks: Chunk[] | null;
23
+ /** The root node's own content classes (its inherited font context), if any. */
24
+ rootCls?: string;
25
+ /** Layout-relevant CSS (incl. `@media`) scoped under the plate root, paint stripped. */
26
+ css: string;
27
+ /**
28
+ * Child plates named by the chunks' `{ r }` stitches, resolved through the
29
+ * import graph (ADR 0006). Populated by the loader's generated imports, never
30
+ * serialized into the plates JSON; absent in production when there are no
31
+ * stitches. In dev the live store takes precedence, so this need not be present.
32
+ */
33
+ refs?: Record<string, Plate>;
34
+ }
35
+ /**
36
+ * One serialized child of a plate root (or of a stitch-bearing node): a
37
+ * stitch-free HTML run (`string`), a stitch to a child plate (`{ r: name }`),
38
+ * or a stitch-bearing element kept real so its inner stitches can mount
39
+ * (`{ c: className, k: chunk[] }`). The framework-neutral render contract: an
40
+ * adapter injects the strings and mounts a child plate at each stitch (ADR 0008).
41
+ */
42
+ type Chunk = string | {
43
+ r: string;
44
+ } | {
45
+ c: string;
46
+ k: Chunk[];
47
+ };
48
+ /**
49
+ * A plate after view-merge + classify but before chunk serialization: the
50
+ * build-time RenderNode tree plus its scoped CSS. Produced by `mergeViews` and
51
+ * `capture`, then serialized into the shipped `Plate` (chunks) at load
52
+ * (`serializePlate`). Never shipped to the client.
53
+ */
54
+ interface MergedPlate {
55
+ v: number;
56
+ name: string;
57
+ tree: RenderNode | null;
58
+ css: string;
59
+ }
60
+ interface PlateNode {
61
+ /** Plate-local id; rules reference it pre-classify. Stripped from the shipped tree. */
62
+ id: number;
63
+ /** Present on paint leaves; absent on layout containers. */
64
+ leaf?: LeafKind;
65
+ /**
66
+ * A stitch: render the named child plate's skeleton here instead of own
67
+ * structure (ADR 0006). Mutually exclusive with `leaf`/`kids`. Resolved
68
+ * against `Plate.refs` (prod) or the live store (dev).
69
+ */
70
+ ref?: string;
71
+ kids?: PlateNode[];
72
+ }
73
+ /**
74
+ * A node in the shipped tree, after classify (ADR 0007). Carries the
75
+ * content-addressed style classes it needs instead of an id — the CSS targets
76
+ * those classes, not a per-node `data-xr` value, so node scoping (the old
77
+ * `nodeKey`) is gone. `cls` is a space-joined class-token list.
78
+ */
79
+ interface RenderNode {
80
+ leaf?: LeafKind;
81
+ ref?: string;
82
+ cls?: string;
83
+ kids?: RenderNode[];
84
+ }
85
+ /**
86
+ * A captured style rule in id form — selectors are generated at render time,
87
+ * so view merging can remap node ids without string surgery. `ids: []`
88
+ * targets the plate root.
89
+ */
90
+ interface PlateRule {
91
+ ids: number[];
92
+ decls: string[];
93
+ /** Media-condition stack, outermost first; conditions AND together. */
94
+ media?: string[];
95
+ /** `@container`-condition stack, outermost first; conditions AND together. Absent when none. */
96
+ container?: string[];
97
+ layered?: boolean;
98
+ spec: number;
99
+ order: number;
100
+ }
101
+ /**
102
+ * One capture of a component within one viewport view — `[min, max)` in px,
103
+ * either side open-ended when undefined. What the dev client ships to the
104
+ * plugin, and what the plate file accumulates (ADR 0004).
105
+ */
106
+ interface ViewCapture {
107
+ /** Viewport width at capture time. */
108
+ width: number;
109
+ min?: number;
110
+ max?: number;
111
+ tree: PlateNode;
112
+ rules: PlateRule[];
113
+ }
114
+ /** The committed `plates/<name>.json`: per-view captures, merged at load. */
115
+ interface PlateFile {
116
+ v: number;
117
+ name: string;
118
+ /** Width thresholds (view boundaries) derived so far. */
119
+ breakpoints: number[];
120
+ views: ViewCapture[];
121
+ }
122
+ /**
123
+ * A dev-only sticky boolean toggle (get/set/subscribe), persisted per tab.
124
+ * Used for both "light box" (render every `<Skeleton>` regardless of readiness)
125
+ * and "capture" (whether browsing re-captures plates).
126
+ */
127
+ interface ToggleStore {
128
+ get: () => boolean;
129
+ set: (value: boolean) => void;
130
+ subscribe: (onChange: () => void) => () => void;
131
+ }
132
+ /**
133
+ * Dev-only store of the freshest plate per name, updated in place when a
134
+ * capture lands. `<Skeleton>` reads through it so a new plate re-renders the
135
+ * site rather than reloading the module — which would remount the capture
136
+ * boundary and re-fire the capture, a feedback loop.
137
+ */
138
+ interface PlateStore {
139
+ get: (name: string) => Plate | undefined;
140
+ subscribe: (name: string, onChange: () => void) => () => void;
141
+ /**
142
+ * Register a plate imported via the ref graph if the store has none yet — so a
143
+ * stitch resolves even before its child is re-captured, and survives a parent
144
+ * hot-swap (whose pushed plate carries no resolved `refs`). Never overwrites a
145
+ * live capture.
146
+ */
147
+ seed: (name: string, plate: Plate) => void;
148
+ }
149
+ /** A view interval `[min, max)`; an open end is undefined (ADR 0004). */
150
+ interface Span {
151
+ min?: number;
152
+ max?: number;
153
+ }
154
+ /** A plate's detected width thresholds and the spans actually captured, from disk. */
155
+ interface PlateCoverage {
156
+ breakpoints: number[];
157
+ spans: Span[];
158
+ }
159
+ /**
160
+ * Dev-only per-plate coverage, sourced from the committed plate files (not the
161
+ * session): the breakpoints detected so far and which view spans are captured.
162
+ * The HUD renders every band — covered ones lit, the rest dim — and the
163
+ * capture-all sweep derives its target widths from the breakpoints.
164
+ */
165
+ interface CoverageStore {
166
+ get: () => ReadonlyMap<string, PlateCoverage>;
167
+ set: (name: string, coverage: PlateCoverage) => void;
168
+ subscribe: (onChange: () => void) => () => void;
169
+ }
170
+ /**
171
+ * Dev-only capture hook, installed by the dev client before the app boots.
172
+ * Absent in production builds, so `<Skeleton>` carries zero capture weight.
173
+ * `captured` returns a dispose callback (stop observing this site).
174
+ */
175
+ interface CaptureHook {
176
+ captured: (name: string, el: Element, opts?: {
177
+ delay?: number;
178
+ }) => (() => void) | undefined;
179
+ /** Light Box toggle: force every `<Skeleton>` to its skeleton, read by `<Skeleton>`. */
180
+ lightbox?: ToggleStore;
181
+ /** Capture toggle: whether browsing re-captures plates, flipped from the HUD. */
182
+ capture?: ToggleStore;
183
+ /** Read side for `<Skeleton>`. */
184
+ plates?: PlateStore;
185
+ /** Write side for the dev client's HMR-event handler. */
186
+ updatePlate?: (name: string, plate: Plate) => void;
187
+ /** Dev-only: run the HUD-triggered capture-all-views sweep via the extension (ADR 0010). */
188
+ captureAllViews?: () => Promise<void>;
189
+ /** Dev-only per-plate breakpoints + captured spans, from disk; fed by the bootstrap. */
190
+ coverage?: CoverageStore;
191
+ }
192
+ declare global {
193
+ var __XRAY__: CaptureHook | undefined;
194
+ }
195
+ //#endregion
196
+ //#region src/merge.d.ts
197
+ /**
198
+ * Turn the per-view captures in a plate file into the one plate a Skeleton
199
+ * renders.
200
+ *
201
+ * Each view is kept whole — its own subtree, its own ids, its own rules —
202
+ * and shown only across the viewport range it owns, via `@media display:none`
203
+ * gates. We do NOT merge the views' trees into one. An earlier design (ADR
204
+ * 0004) did, sharing common structure and forking the rest, but aligning two
205
+ * genuinely-different DOM trees (mobile stacked vs desktop two-column) without
206
+ * stable keys proved unreliable: every heuristic mis-paired some nodes and
207
+ * remapped one view's rules onto another's elements, corrupting both. A lone
208
+ * capture is pixel-faithful; keeping each view lone preserves that. The cost
209
+ * is a larger plate (roughly the sum of the views) — bounded by the plate
210
+ * size cap — in exchange for correctness.
211
+ *
212
+ * Pure data-in/data-out — runs in node (plugin load) and in tests.
213
+ */
214
+ declare function mergeViews(file: PlateFile): MergedPlate;
215
+ /** Distinct plate names referenced by the tree's stitches, in first-seen order. */
216
+ declare function collectRefs(tree: RenderNode | null): string[];
217
+ /** Fold a fresh capture into the plate file: union breakpoints, one capture per view. */
218
+ declare function addCapture(file: PlateFile | null, name: string, capture: ViewCapture, breakpoints: readonly number[]): PlateFile;
219
+ //#endregion
220
+ //#region src/index.d.ts
221
+ /**
222
+ * Options for `xrayVitePlugin()`.
223
+ *
224
+ * Fixed conventions are intentionally not configurable: Plate names come from
225
+ * `virtual:xray/plates/<name>`, files live under `platesDir`, and responsive
226
+ * breakpoints are derived from captured CSS plus the `matchMedia` queries the
227
+ * page evaluates.
228
+ */
229
+ interface XrayOptions {
230
+ /** Directory where Plate JSON files are written and read, relative to the Vite root. Default: `plates`. */
231
+ platesDir?: string;
232
+ /** Mount the dev HUD for coverage, Capture, Light Box, and capture-all controls. Default: `false`. */
233
+ hud?: boolean;
234
+ /**
235
+ * Initial state of the browse-time Capture toggle in dev. Keep this off for
236
+ * normal browsing and let the HUD, capture-all flow, or `?xray-capture` turn
237
+ * Capture on per tab. Default: `false`.
238
+ */
239
+ capture?: boolean;
240
+ /** Settle delay in milliseconds before capturing a ready Skeleton; override one boundary with the `delay` prop. Default: `200`. */
241
+ delay?: number;
242
+ /** Upper bound in milliseconds on waiting for a busy subtree to go quiet before capturing anyway. Default: `5000`. */
243
+ settleCap?: number;
244
+ /** Refuse captures larger than this many nodes (Skeleton too high in the tree). Default: `4000`. */
245
+ maxNodes?: number;
246
+ }
247
+ /**
248
+ * Create the Vite plugins that resolve committed Plates and, during dev,
249
+ * capture new Plates from rendered `<Skeleton>` boundaries.
250
+ *
251
+ * Typical setup:
252
+ *
253
+ * ```ts
254
+ * import { xrayVitePlugin } from '@hueest/xray'
255
+ *
256
+ * export default {
257
+ * plugins: [xrayVitePlugin({ hud: true })],
258
+ * }
259
+ * ```
260
+ *
261
+ * Returns two plugins:
262
+ * - `xray:data` resolves `virtual:xray/plates/<name>` from committed Plate files in dev and build.
263
+ * - `xray:dev` runs only during `vite serve`; it injects the capture client, receives Captures, writes Plate files, and hot-updates the page.
264
+ */
265
+ declare function xrayVitePlugin(options?: XrayOptions): Plugin[];
266
+ interface CaptureRequest {
267
+ method?: string;
268
+ on(event: 'data' | 'end', listener: (chunk?: Buffer | string) => void): unknown;
269
+ }
270
+ interface CaptureResponse {
271
+ statusCode: number;
272
+ end(chunk?: string): unknown;
273
+ }
274
+ interface PlateBroadcaster {
275
+ send(payload: unknown): void;
276
+ }
277
+ interface CaptureServer {
278
+ ws: PlateBroadcaster;
279
+ }
280
+ /**
281
+ * Per-plate breakpoints + captured spans, read from the committed plate files.
282
+ *
283
+ * @internal
284
+ */
285
+ declare function readCoverage(dir: string): Record<string, {
286
+ breakpoints: number[];
287
+ spans: {
288
+ min?: number;
289
+ max?: number;
290
+ }[];
291
+ }>;
292
+ /**
293
+ * Fold a posted capture into its plate file and hot-swap the plate in place.
294
+ *
295
+ * @internal
296
+ */
297
+ declare function handleCapturePost(req: CaptureRequest, res: CaptureResponse, server: CaptureServer, platePath: (name: string) => string): void;
298
+ /**
299
+ * Names come from import specifiers and POST bodies — keep them inside the plates dir.
300
+ *
301
+ * @internal
302
+ */
303
+ declare function isValidPlateName(name: string): boolean;
304
+ /**
305
+ * Render a plate as the virtual module's source. Each stitch (`ref` node) becomes
306
+ * a real import of that child's virtual module, wired into a `refs` map on the
307
+ * default export — so the bundler builds the actual reference graph and chunks
308
+ * the transitive closure a page uses, with no runtime registry (ADR 0006). A
309
+ * stitchless plate is just its JSON. Self-references are dropped (a module can't
310
+ * import itself); an unknown child resolves to its own empty plate, never a
311
+ * build break.
312
+ *
313
+ * @internal
314
+ */
315
+ declare function renderPlateModule(merged: MergedPlate): string;
316
+ //#endregion
317
+ export { type LeafKind, type Plate, type PlateFile, type PlateNode, type PlateRule, type ViewCapture, XrayOptions, addCapture, collectRefs, handleCapturePost, isValidPlateName, mergeViews, readCoverage, renderPlateModule, xrayVitePlugin };