@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/client.js CHANGED
@@ -1,5 +1,531 @@
1
- import { i as captureRegime, t as CaptureTooLargeError } from "./serialize-Bq6yJnNV.js";
2
- import { n as regimeFor, t as deriveBreakpoints } from "./breakpoints-CBNlh7lQ.js";
1
+ import { a as captureRegime, c as formatDiagnostic, l as makeDiagnostic, n as regimeFor, r as CaptureTooLargeError, s as diagnosticsHeader, t as deriveBreakpoints } from "./breakpoints-CMoFUUOv.js";
2
+ //#region ../../node_modules/.pnpm/devalue@5.8.1/node_modules/devalue/src/base64.js
3
+ /** @type {(array_buffer: ArrayBuffer) => string} */
4
+ function encode_native(array_buffer) {
5
+ return new Uint8Array(array_buffer).toBase64();
6
+ }
7
+ /** @type {(base64: string) => ArrayBuffer} */
8
+ function decode_native(base64) {
9
+ return Uint8Array.fromBase64(base64).buffer;
10
+ }
11
+ /** @type {(array_buffer: ArrayBuffer) => string} */
12
+ function encode_buffer(array_buffer) {
13
+ return Buffer.from(array_buffer).toString("base64");
14
+ }
15
+ /** @type {(base64: string) => ArrayBuffer} */
16
+ function decode_buffer(base64) {
17
+ return Uint8Array.from(Buffer.from(base64, "base64")).buffer;
18
+ }
19
+ /** @type {(array_buffer: ArrayBuffer) => string} */
20
+ function encode_legacy(array_buffer) {
21
+ const array = new Uint8Array(array_buffer);
22
+ let binary = "";
23
+ const chunk_size = 32768;
24
+ for (let i = 0; i < array.length; i += chunk_size) {
25
+ const chunk = array.subarray(i, i + chunk_size);
26
+ binary += String.fromCharCode.apply(null, chunk);
27
+ }
28
+ return btoa(binary);
29
+ }
30
+ /** @type {(base64: string) => ArrayBuffer} */
31
+ function decode_legacy(base64) {
32
+ const binary_string = atob(base64);
33
+ const len = binary_string.length;
34
+ const array = new Uint8Array(len);
35
+ for (let i = 0; i < len; i++) array[i] = binary_string.charCodeAt(i);
36
+ return array.buffer;
37
+ }
38
+ const native = typeof Uint8Array.fromBase64 === "function";
39
+ const buffer = typeof process === "object" && process.versions?.node !== void 0;
40
+ const encode64 = native ? encode_native : buffer ? encode_buffer : encode_legacy;
41
+ const decode64 = native ? decode_native : buffer ? decode_buffer : decode_legacy;
42
+ //#endregion
43
+ //#region ../../node_modules/.pnpm/devalue@5.8.1/node_modules/devalue/src/constants.js
44
+ const MAX_ARRAY_LEN = 2 ** 32 - 1;
45
+ const MAX_ARRAY_INDEX = MAX_ARRAY_LEN - 1;
46
+ //#endregion
47
+ //#region ../../node_modules/.pnpm/devalue@5.8.1/node_modules/devalue/src/utils.js
48
+ var DevalueError = class extends Error {
49
+ /**
50
+ * @param {string} message
51
+ * @param {string[]} keys
52
+ * @param {any} [value] - The value that failed to be serialized
53
+ * @param {any} [root] - The root value being serialized
54
+ */
55
+ constructor(message, keys, value, root) {
56
+ super(message);
57
+ this.name = "DevalueError";
58
+ this.path = keys.join("");
59
+ this.value = value;
60
+ this.root = root;
61
+ }
62
+ };
63
+ /** @param {any} thing */
64
+ function is_primitive(thing) {
65
+ return thing === null || typeof thing !== "object" && typeof thing !== "function";
66
+ }
67
+ const object_proto_names = /* @__PURE__ */ Object.getOwnPropertyNames(Object.prototype).sort().join("\0");
68
+ /** @param {any} thing */
69
+ function is_plain_object(thing) {
70
+ const proto = Object.getPrototypeOf(thing);
71
+ return proto === Object.prototype || proto === null || Object.getPrototypeOf(proto) === null || Object.getOwnPropertyNames(proto).sort().join("\0") === object_proto_names;
72
+ }
73
+ /** @param {any} thing */
74
+ function get_type(thing) {
75
+ return Object.prototype.toString.call(thing).slice(8, -1);
76
+ }
77
+ /** @param {string} char */
78
+ function get_escaped_char(char) {
79
+ switch (char) {
80
+ case "\"": return "\\\"";
81
+ case "<": return "\\u003C";
82
+ case "\\": return "\\\\";
83
+ case "\n": return "\\n";
84
+ case "\r": return "\\r";
85
+ case " ": return "\\t";
86
+ case "\b": return "\\b";
87
+ case "\f": return "\\f";
88
+ case "\u2028": return "\\u2028";
89
+ case "\u2029": return "\\u2029";
90
+ default: return char < " " ? `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}` : "";
91
+ }
92
+ }
93
+ /** @param {string} str */
94
+ function stringify_string(str) {
95
+ let result = "";
96
+ let last_pos = 0;
97
+ const len = str.length;
98
+ for (let i = 0; i < len; i += 1) {
99
+ const char = str[i];
100
+ const replacement = get_escaped_char(char);
101
+ if (replacement) {
102
+ result += str.slice(last_pos, i) + replacement;
103
+ last_pos = i + 1;
104
+ }
105
+ }
106
+ return `"${last_pos === 0 ? str : result + str.slice(last_pos)}"`;
107
+ }
108
+ /** @param {Record<string | symbol, any>} object */
109
+ function enumerable_symbols(object) {
110
+ return Object.getOwnPropertySymbols(object).filter((symbol) => Object.getOwnPropertyDescriptor(object, symbol).enumerable);
111
+ }
112
+ const is_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
113
+ /** @param {string} key */
114
+ function stringify_key(key) {
115
+ return is_identifier.test(key) ? "." + key : "[" + JSON.stringify(key) + "]";
116
+ }
117
+ /** @param {number} n */
118
+ function is_valid_array_index(n) {
119
+ if (!Number.isInteger(n)) return false;
120
+ if (n < 0) return false;
121
+ if (n > MAX_ARRAY_INDEX) return false;
122
+ return true;
123
+ }
124
+ /** @param {number} n */
125
+ function is_valid_array_len(n) {
126
+ if (!Number.isInteger(n)) return false;
127
+ if (n < 0) return false;
128
+ if (n > MAX_ARRAY_LEN) return false;
129
+ return true;
130
+ }
131
+ /** @param {string} s */
132
+ function is_valid_array_index_string(s) {
133
+ if (s.length === 0) return false;
134
+ if (s.length > 1 && s.charCodeAt(0) === 48) return false;
135
+ for (let i = 0; i < s.length; i++) {
136
+ const c = s.charCodeAt(i);
137
+ if (c < 48 || c > 57) return false;
138
+ }
139
+ return is_valid_array_index(+s);
140
+ }
141
+ /**
142
+ * Finds the populated indices of an array.
143
+ * @param {unknown[]} array
144
+ */
145
+ function valid_array_indices(array) {
146
+ const keys = Object.keys(array);
147
+ for (var i = keys.length - 1; i >= 0; i--) if (is_valid_array_index_string(keys[i])) break;
148
+ keys.length = i + 1;
149
+ return keys;
150
+ }
151
+ //#endregion
152
+ //#region ../../node_modules/.pnpm/devalue@5.8.1/node_modules/devalue/src/parse.js
153
+ /**
154
+ * Revive a value serialized with `devalue.stringify`
155
+ * @param {string} serialized
156
+ * @param {Record<string, (value: any) => any>} [revivers]
157
+ */
158
+ function parse(serialized, revivers) {
159
+ return unflatten(JSON.parse(serialized), revivers);
160
+ }
161
+ /**
162
+ * Revive a value flattened with `devalue.stringify`
163
+ * @param {number | any[]} parsed
164
+ * @param {Record<string, (value: any) => any>} [revivers]
165
+ */
166
+ function unflatten(parsed, revivers) {
167
+ if (typeof parsed === "number") return hydrate(parsed, true);
168
+ if (!Array.isArray(parsed) || parsed.length === 0) throw new Error("Invalid input");
169
+ const values = parsed;
170
+ const hydrated = Array(values.length);
171
+ /**
172
+ * A set of values currently being hydrated with custom revivers,
173
+ * used to detect invalid cyclical dependencies
174
+ * @type {Set<number> | null}
175
+ */
176
+ let hydrating = null;
177
+ /**
178
+ * @param {number} index
179
+ * @returns {any}
180
+ */
181
+ function hydrate(index, standalone = false) {
182
+ if (index === -1) return void 0;
183
+ if (index === -3) return NaN;
184
+ if (index === -4) return Infinity;
185
+ if (index === -5) return -Infinity;
186
+ if (index === -6) return -0;
187
+ if (standalone || typeof index !== "number") throw new Error(`Invalid input`);
188
+ if (index in hydrated) return hydrated[index];
189
+ const value = values[index];
190
+ if (!value || typeof value !== "object") hydrated[index] = value;
191
+ else if (Array.isArray(value)) if (typeof value[0] === "string") {
192
+ const type = value[0];
193
+ const reviver = revivers && Object.hasOwn(revivers, type) ? revivers[type] : void 0;
194
+ if (reviver) {
195
+ let i = value[1];
196
+ if (typeof i !== "number") i = values.push(value[1]) - 1;
197
+ hydrating ??= /* @__PURE__ */ new Set();
198
+ if (hydrating.has(i)) throw new Error("Invalid circular reference");
199
+ hydrating.add(i);
200
+ hydrated[index] = reviver(hydrate(i));
201
+ hydrating.delete(i);
202
+ return hydrated[index];
203
+ }
204
+ switch (type) {
205
+ case "Date":
206
+ hydrated[index] = new Date(value[1]);
207
+ break;
208
+ case "Set":
209
+ const set = /* @__PURE__ */ new Set();
210
+ hydrated[index] = set;
211
+ for (let i = 1; i < value.length; i += 1) set.add(hydrate(value[i]));
212
+ break;
213
+ case "Map":
214
+ const map = /* @__PURE__ */ new Map();
215
+ hydrated[index] = map;
216
+ for (let i = 1; i < value.length; i += 2) map.set(hydrate(value[i]), hydrate(value[i + 1]));
217
+ break;
218
+ case "RegExp":
219
+ hydrated[index] = new RegExp(value[1], value[2]);
220
+ break;
221
+ case "Object": {
222
+ const wrapped_index = value[1];
223
+ if (typeof values[wrapped_index] === "object" && values[wrapped_index][0] !== "BigInt") throw new Error("Invalid input");
224
+ hydrated[index] = Object(hydrate(wrapped_index));
225
+ break;
226
+ }
227
+ case "BigInt":
228
+ hydrated[index] = BigInt(value[1]);
229
+ break;
230
+ case "null":
231
+ const obj = Object.create(null);
232
+ hydrated[index] = obj;
233
+ for (let i = 1; i < value.length; i += 2) {
234
+ if (value[i] === "__proto__") throw new Error("Cannot parse an object with a `__proto__` property");
235
+ obj[value[i]] = hydrate(value[i + 1]);
236
+ }
237
+ break;
238
+ case "Int8Array":
239
+ case "Uint8Array":
240
+ case "Uint8ClampedArray":
241
+ case "Int16Array":
242
+ case "Uint16Array":
243
+ case "Float16Array":
244
+ case "Int32Array":
245
+ case "Uint32Array":
246
+ case "Float32Array":
247
+ case "Float64Array":
248
+ case "BigInt64Array":
249
+ case "BigUint64Array":
250
+ case "DataView": {
251
+ if (values[value[1]][0] !== "ArrayBuffer") throw new Error("Invalid data");
252
+ const TypedArrayConstructor = globalThis[type];
253
+ const buffer = hydrate(value[1]);
254
+ hydrated[index] = value[2] !== void 0 ? new TypedArrayConstructor(buffer, value[2], value[3]) : new TypedArrayConstructor(buffer);
255
+ break;
256
+ }
257
+ case "ArrayBuffer": {
258
+ const base64 = value[1];
259
+ if (typeof base64 !== "string") throw new Error("Invalid ArrayBuffer encoding");
260
+ hydrated[index] = decode64(base64);
261
+ break;
262
+ }
263
+ case "Temporal.Duration":
264
+ case "Temporal.Instant":
265
+ case "Temporal.PlainDate":
266
+ case "Temporal.PlainTime":
267
+ case "Temporal.PlainDateTime":
268
+ case "Temporal.PlainMonthDay":
269
+ case "Temporal.PlainYearMonth":
270
+ case "Temporal.ZonedDateTime": {
271
+ const temporalName = type.slice(9);
272
+ hydrated[index] = Temporal[temporalName].from(value[1]);
273
+ break;
274
+ }
275
+ case "URL":
276
+ hydrated[index] = new URL(value[1]);
277
+ break;
278
+ case "URLSearchParams":
279
+ hydrated[index] = new URLSearchParams(value[1]);
280
+ break;
281
+ default: throw new Error(`Unknown type ${type}`);
282
+ }
283
+ } else if (value[0] === -7) {
284
+ const len = value[1];
285
+ if (!is_valid_array_len(len)) throw new Error("Invalid input");
286
+ /** @type {any[]} */
287
+ const array = [];
288
+ hydrated[index] = array;
289
+ array[MAX_ARRAY_INDEX] = void 0;
290
+ delete array[MAX_ARRAY_INDEX];
291
+ for (let i = 2; i < value.length; i += 2) {
292
+ const idx = value[i];
293
+ if (!is_valid_array_index(idx) || idx >= len) throw new Error("Invalid input");
294
+ array[idx] = hydrate(value[i + 1]);
295
+ }
296
+ array.length = len;
297
+ } else {
298
+ const array = new Array(value.length);
299
+ hydrated[index] = array;
300
+ for (let i = 0; i < value.length; i += 1) {
301
+ const n = value[i];
302
+ if (n === -2) continue;
303
+ array[i] = hydrate(n);
304
+ }
305
+ }
306
+ else {
307
+ /** @type {Record<string, any>} */
308
+ const object = {};
309
+ hydrated[index] = object;
310
+ for (const key of Object.keys(value)) {
311
+ if (key === "__proto__") throw new Error("Cannot parse an object with a `__proto__` property");
312
+ const n = value[key];
313
+ object[key] = hydrate(n);
314
+ }
315
+ }
316
+ return hydrated[index];
317
+ }
318
+ return hydrate(0);
319
+ }
320
+ //#endregion
321
+ //#region ../../node_modules/.pnpm/devalue@5.8.1/node_modules/devalue/src/stringify.js
322
+ /**
323
+ * Turn a value into a JSON string that can be parsed with `devalue.parse`
324
+ * @param {any} value
325
+ * @param {Record<string, (value: any) => any>} [reducers]
326
+ */
327
+ function stringify(value, reducers) {
328
+ const stringified = run(false, value, reducers);
329
+ return typeof stringified === "string" ? stringified : `[${stringified.join(",")}]`;
330
+ }
331
+ /**
332
+ * @param {boolean} async
333
+ * @param {any} value
334
+ * @param {Record<string, (value: any) => any>} [reducers]
335
+ */
336
+ function run(async, value, reducers) {
337
+ /** @type {any[]} */
338
+ const stringified = [];
339
+ /** @type {Map<any, number>} */
340
+ const indexes = /* @__PURE__ */ new Map();
341
+ /** @type {Array<{ key: string, fn: (value: any) => any }>} */
342
+ const custom = [];
343
+ if (reducers) for (const key of Object.getOwnPropertyNames(reducers)) custom.push({
344
+ key,
345
+ fn: reducers[key]
346
+ });
347
+ /** @type {string[]} */
348
+ const keys = [];
349
+ let p = 0;
350
+ /**
351
+ * @param {any} thing
352
+ * @param {number} [index]
353
+ */
354
+ function flatten(thing, index) {
355
+ if (thing === void 0) return -1;
356
+ if (Number.isNaN(thing)) return -3;
357
+ if (thing === Infinity) return -4;
358
+ if (thing === -Infinity) return -5;
359
+ if (thing === 0 && 1 / thing < 0) return -6;
360
+ if (indexes.has(thing)) return indexes.get(thing);
361
+ index ??= p++;
362
+ indexes.set(thing, index);
363
+ for (const { key, fn } of custom) {
364
+ const value = fn(thing);
365
+ if (value) {
366
+ stringified[index] = `["${key}",${flatten(value)}]`;
367
+ return index;
368
+ }
369
+ }
370
+ if (typeof thing === "function") throw new DevalueError(`Cannot stringify a function`, keys, thing, value);
371
+ else if (typeof thing === "symbol") throw new DevalueError(`Cannot stringify a Symbol primitive`, keys, thing, value);
372
+ /** @type {string | Promise<any>} */
373
+ let str = "";
374
+ if (is_primitive(thing)) str = stringify_primitive(thing);
375
+ else if (typeof thing.then === "function") {
376
+ if (!async) throw new DevalueError(`Cannot stringify a Promise or thenable — use stringifyAsync instead`, keys, thing, value);
377
+ str = Promise.resolve(thing).then((value) => {
378
+ const i = flatten(value, index);
379
+ if (i < 0) stringified[index] = i;
380
+ });
381
+ } else {
382
+ const type = get_type(thing);
383
+ switch (type) {
384
+ case "Number":
385
+ case "String":
386
+ case "Boolean":
387
+ case "BigInt":
388
+ str = `["Object",${flatten(thing.valueOf())}]`;
389
+ break;
390
+ case "Date":
391
+ str = `["Date","${!isNaN(thing.getDate()) ? thing.toISOString() : ""}"]`;
392
+ break;
393
+ case "URL":
394
+ str = `["URL",${stringify_string(thing.toString())}]`;
395
+ break;
396
+ case "URLSearchParams":
397
+ str = `["URLSearchParams",${stringify_string(thing.toString())}]`;
398
+ break;
399
+ case "RegExp":
400
+ const { source, flags } = thing;
401
+ str = flags ? `["RegExp",${stringify_string(source)},"${flags}"]` : `["RegExp",${stringify_string(source)}]`;
402
+ break;
403
+ case "Array": {
404
+ let mostly_dense = false;
405
+ str = "[";
406
+ for (let i = 0; i < thing.length; i += 1) {
407
+ if (i > 0) str += ",";
408
+ if (Object.hasOwn(thing, i)) {
409
+ keys.push(`[${i}]`);
410
+ str += flatten(thing[i]);
411
+ keys.pop();
412
+ } else if (mostly_dense) str += -2;
413
+ else {
414
+ const populated_keys = valid_array_indices(thing);
415
+ const population = populated_keys.length;
416
+ const d = String(thing.length).length;
417
+ if ((thing.length - population) * 3 > 4 + d + population * (d + 1)) {
418
+ str = "[-7," + thing.length;
419
+ for (let j = 0; j < populated_keys.length; j++) {
420
+ const key = populated_keys[j];
421
+ keys.push(`[${key}]`);
422
+ str += "," + key + "," + flatten(thing[key]);
423
+ keys.pop();
424
+ }
425
+ break;
426
+ } else {
427
+ mostly_dense = true;
428
+ str += -2;
429
+ }
430
+ }
431
+ }
432
+ str += "]";
433
+ break;
434
+ }
435
+ case "Set":
436
+ str = "[\"Set\"";
437
+ for (const value of thing) str += `,${flatten(value)}`;
438
+ str += "]";
439
+ break;
440
+ case "Map":
441
+ str = "[\"Map\"";
442
+ for (const [key, value] of thing) {
443
+ keys.push(`.get(${is_primitive(key) ? stringify_primitive(key) : "..."})`);
444
+ str += `,${flatten(key)},${flatten(value)}`;
445
+ keys.pop();
446
+ }
447
+ str += "]";
448
+ break;
449
+ case "Int8Array":
450
+ case "Uint8Array":
451
+ case "Uint8ClampedArray":
452
+ case "Int16Array":
453
+ case "Uint16Array":
454
+ case "Float16Array":
455
+ case "Int32Array":
456
+ case "Uint32Array":
457
+ case "Float32Array":
458
+ case "Float64Array":
459
+ case "BigInt64Array":
460
+ case "BigUint64Array":
461
+ case "DataView": {
462
+ /** @type {import("./types.js").TypedArray} */
463
+ const typedArray = thing;
464
+ str = "[\"" + type + "\"," + flatten(typedArray.buffer);
465
+ if (typedArray.byteLength !== typedArray.buffer.byteLength) str += `,${typedArray.byteOffset},${typedArray.length}`;
466
+ str += "]";
467
+ break;
468
+ }
469
+ case "ArrayBuffer":
470
+ str = `["ArrayBuffer","${encode64(thing)}"]`;
471
+ break;
472
+ case "Temporal.Duration":
473
+ case "Temporal.Instant":
474
+ case "Temporal.PlainDate":
475
+ case "Temporal.PlainTime":
476
+ case "Temporal.PlainDateTime":
477
+ case "Temporal.PlainMonthDay":
478
+ case "Temporal.PlainYearMonth":
479
+ case "Temporal.ZonedDateTime":
480
+ str = `["${type}",${stringify_string(thing.toString())}]`;
481
+ break;
482
+ default:
483
+ if (!is_plain_object(thing)) throw new DevalueError(`Cannot stringify arbitrary non-POJOs`, keys, thing, value);
484
+ if (enumerable_symbols(thing).length > 0) throw new DevalueError(`Cannot stringify POJOs with symbolic keys`, keys, thing, value);
485
+ if (Object.getPrototypeOf(thing) === null) {
486
+ str = "[\"null\"";
487
+ for (const key of Object.keys(thing)) {
488
+ if (key === "__proto__") throw new DevalueError(`Cannot stringify objects with __proto__ keys`, keys, thing, value);
489
+ keys.push(stringify_key(key));
490
+ str += `,${stringify_string(key)},${flatten(thing[key])}`;
491
+ keys.pop();
492
+ }
493
+ str += "]";
494
+ } else {
495
+ str = "{";
496
+ let started = false;
497
+ for (const key of Object.keys(thing)) {
498
+ if (key === "__proto__") throw new DevalueError(`Cannot stringify objects with __proto__ keys`, keys, thing, value);
499
+ if (started) str += ",";
500
+ started = true;
501
+ keys.push(stringify_key(key));
502
+ str += `${stringify_string(key)}:${flatten(thing[key])}`;
503
+ keys.pop();
504
+ }
505
+ str += "}";
506
+ }
507
+ }
508
+ }
509
+ stringified[index] = str;
510
+ return index;
511
+ }
512
+ const index = flatten(value);
513
+ if (index < 0) return `${index}`;
514
+ return stringified;
515
+ }
516
+ /**
517
+ * @param {any} thing
518
+ * @returns {string}
519
+ */
520
+ function stringify_primitive(thing) {
521
+ const type = typeof thing;
522
+ if (type === "string") return stringify_string(thing);
523
+ if (thing === void 0) return (-1).toString();
524
+ if (thing === 0 && 1 / thing < 0) return (-6).toString();
525
+ if (type === "bigint") return `["BigInt","${thing}"]`;
526
+ return String(thing);
527
+ }
528
+ //#endregion
3
529
  //#region src/flash.ts
4
530
  /**
5
531
  * Dev-only radiographic capture effect — pure delight. When a capture lands we
@@ -88,6 +614,23 @@ function isExtensionAck(value) {
88
614
  const DEFAULT_SETTLE_CAP = 5e3;
89
615
  const DEFAULT_MAX_NODES = 4e3;
90
616
  const RESIZE_DEBOUNCE = 300;
617
+ /**
618
+ * The capture roots for a site, the ONE place the auto/manual root models
619
+ * diverge (ADR 0014, finding #6).
620
+ *
621
+ * - AUTO: `site.el` is the `display:contents` wrapper, a layout-neutral box, so
622
+ * the content is its CHILDREN — the synthetic root (id 0) stands in for the
623
+ * wrapper and the children become the plate's top level (today's behavior).
624
+ * `<STYLE>` children are dropped (a stitched-in scoped sheet, never content).
625
+ * - MANUAL: `site.el` is the consumer's OWN styled element, so the element
626
+ * ITSELF is the root. Capturing its children would drop its box, classes,
627
+ * padding, border, background, and radius — the exact bug this fixes — so the
628
+ * element is the single root and its own box is serialized.
629
+ */
630
+ function rootsFor(site) {
631
+ if (site.mode === "manual") return [site.el];
632
+ return Array.from(site.el.children).filter((child) => child.tagName !== "STYLE");
633
+ }
91
634
  function installXrayClient(options) {
92
635
  const win = window;
93
636
  const defaultDelay = options.delay ?? 200;
@@ -95,6 +638,7 @@ function installXrayClient(options) {
95
638
  const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
96
639
  const sites = /* @__PURE__ */ new Set();
97
640
  const matchMediaQueries = /* @__PURE__ */ new Set();
641
+ let captureGeneration = 0;
98
642
  const livePlates = /* @__PURE__ */ new Map();
99
643
  const plateSerialized = /* @__PURE__ */ new Map();
100
644
  const plateListeners = /* @__PURE__ */ new Map();
@@ -129,6 +673,9 @@ function installXrayClient(options) {
129
673
  };
130
674
  const lightbox = createToggleStore(win, LIGHTBOX_KEY, LIGHTBOX_PARAM, false);
131
675
  const captureEnabled = createToggleStore(win, CAPTURE_KEY, CAPTURE_PARAM, options.capture ?? true);
676
+ const replayEnabled = createToggleStore(win, REPLAY_KEY, REPLAY_PARAM, options.replay === "prefer" || options.replay === "missing-only");
677
+ const replayMode = options.replay === "missing-only" ? "missing-only" : "prefer";
678
+ const recordMode = options.record ?? "manual";
132
679
  const isPrimaryForName = (site) => {
133
680
  for (const other of sites) {
134
681
  if (other === site || other.disposed || other.name !== site.name || !other.el.isConnected) continue;
@@ -136,17 +683,25 @@ function installXrayClient(options) {
136
683
  }
137
684
  return true;
138
685
  };
139
- const captureSite = async (site, force = false) => {
686
+ const runCapture = async (site, force, awaitSink) => {
140
687
  if (!force && !captureEnabled.get() || site.tooLarge || !isPrimaryForName(site)) return;
141
- await settle(site.el, site.delay, settleCap);
142
- if (site.disposed || site.tooLarge || !site.el.isConnected || !isPrimaryForName(site)) return;
143
- const roots = Array.from(site.el.children).filter((child) => child.tagName !== "STYLE");
688
+ const generation = captureGeneration;
689
+ await settle(site.el, site.delay, settleCap, site.mode);
690
+ if (generation !== captureGeneration || site.disposed || site.tooLarge || !site.el.isConnected || !isPrimaryForName(site)) return;
691
+ const roots = rootsFor(site);
144
692
  if (roots.length === 0) return;
693
+ let payload;
694
+ let key;
145
695
  try {
146
- const capture = captureRegime(roots, { maxNodes });
696
+ const capture = captureRegime(roots, {
697
+ maxNodes,
698
+ walker: site.walker,
699
+ captureRootIsBoundary: site.mode === "manual"
700
+ });
701
+ reportDiagnostics(site.name, capture.diagnostics);
147
702
  const breakpoints = deriveBreakpoints(capture.rules, [...matchMediaQueries]);
148
703
  const span = regimeFor(capture.width, breakpoints);
149
- const payload = {
704
+ payload = {
150
705
  name: site.name,
151
706
  capture: {
152
707
  ...capture,
@@ -154,31 +709,69 @@ function installXrayClient(options) {
154
709
  },
155
710
  breakpoints
156
711
  };
157
- const key = JSON.stringify(span);
712
+ key = JSON.stringify(span);
158
713
  const body = JSON.stringify(payload);
159
714
  if (site.sent.get(key) === body) return;
160
715
  site.sent.set(key, body);
161
- options.sink(payload);
162
- flashCapture(site.el);
163
716
  } catch (error) {
164
717
  if (error instanceof CaptureTooLargeError) {
165
718
  site.tooLarge = true;
166
- console.warn(`[xray] skipping capture of "${site.name}": ${error.nodeCount} nodes (max ${maxNodes}).`, "A skeleton this big usually means the <Skeleton> sits too high in the tree.");
719
+ reportDiagnostics(site.name, [makeDiagnostic(error.code, { detail: `${error.nodeCount} nodes (max ${maxNodes})` })]);
167
720
  return;
168
721
  }
169
722
  console.warn("[xray] capture failed for", site.name, error);
723
+ return;
724
+ }
725
+ if (generation !== captureGeneration || site.disposed) {
726
+ site.sent.delete(key);
727
+ return;
728
+ }
729
+ await sendCapture(site, payload, key, awaitSink);
730
+ };
731
+ const sendCapture = async (site, payload, key, awaitSink) => {
732
+ try {
733
+ await options.sink(payload);
734
+ } catch (error) {
735
+ site.sent.delete(key);
736
+ if (awaitSink) throw error;
737
+ console.warn("[xray] capture send failed for", site.name, error);
738
+ return;
739
+ }
740
+ flashCapture(site.el);
741
+ };
742
+ const scheduleCapture = (site, force = false, awaitSink = false) => {
743
+ if (site.inflight) {
744
+ site.dirty = true;
745
+ site.dirtyForce = site.dirtyForce || force;
746
+ site.dirtyAwaitSink = site.dirtyAwaitSink || awaitSink;
747
+ return site.inflight;
170
748
  }
749
+ const loop = async (f, a) => {
750
+ await runCapture(site, f, a);
751
+ if (!site.dirty) return;
752
+ site.dirty = false;
753
+ const nextForce = site.dirtyForce;
754
+ const nextAwaitSink = site.dirtyAwaitSink;
755
+ site.dirtyForce = false;
756
+ site.dirtyAwaitSink = false;
757
+ await loop(nextForce, nextAwaitSink);
758
+ };
759
+ const promise = loop(force, awaitSink).finally(() => {
760
+ site.inflight = void 0;
761
+ });
762
+ site.inflight = promise;
763
+ return promise;
171
764
  };
172
765
  let resizeTimer;
173
766
  const onResize = () => {
174
767
  clearTimeout(resizeTimer);
175
768
  resizeTimer = setTimeout(() => {
176
- for (const site of sites) captureSite(site);
769
+ for (const site of sites) scheduleCapture(site);
177
770
  }, RESIZE_DEBOUNCE);
178
771
  };
179
772
  win.addEventListener("resize", onResize);
180
773
  const unsubscribeCapture = captureEnabled.subscribe(() => {
181
- if (captureEnabled.get()) for (const site of sites) captureSite(site);
774
+ if (captureEnabled.get()) for (const site of sites) scheduleCapture(site);
182
775
  });
183
776
  const extAcks = /* @__PURE__ */ new Map();
184
777
  let nextExtReqId = 1;
@@ -222,6 +815,58 @@ function installXrayClient(options) {
222
815
  return () => coverageListeners.delete(cb);
223
816
  }
224
817
  };
818
+ const liveData = /* @__PURE__ */ new Map();
819
+ const recorded = /* @__PURE__ */ new Map();
820
+ const fixtureListeners = /* @__PURE__ */ new Set();
821
+ const notifyFixtures = () => {
822
+ for (const cb of fixtureListeners) cb();
823
+ };
824
+ const fixtures = {
825
+ register: (name, data) => {
826
+ const entry = { data };
827
+ const stack = liveData.get(name);
828
+ if (stack) stack.push(entry);
829
+ else liveData.set(name, [entry]);
830
+ return () => {
831
+ const current = liveData.get(name);
832
+ if (!current) return;
833
+ const i = current.indexOf(entry);
834
+ if (i !== -1) current.splice(i, 1);
835
+ if (current.length === 0) liveData.delete(name);
836
+ };
837
+ },
838
+ has: (name) => recorded.has(name),
839
+ read: (name) => recorded.get(name),
840
+ record: async (name) => {
841
+ const live = liveData.get(name)?.at(-1);
842
+ if (!live) throw new Error(`[xray] cannot record fixture for "${name}": no render-prop \`data\` is in scope. Only render-prop <Skeleton data={…}> sites participate in fixtures.`);
843
+ const encoded = stringify(live.data);
844
+ await options.recordFixtureSink?.({
845
+ name,
846
+ data: encoded
847
+ });
848
+ recorded.set(name, { data: parse(encoded) });
849
+ notifyFixtures();
850
+ },
851
+ recordAll: async () => {
852
+ await Promise.all([...liveData.keys()].map((name) => fixtures.record(name)));
853
+ },
854
+ seed: (encoded) => {
855
+ let changed = false;
856
+ for (const [name, str] of Object.entries(encoded)) {
857
+ if (recorded.has(name)) continue;
858
+ try {
859
+ recorded.set(name, { data: parse(str) });
860
+ changed = true;
861
+ } catch {}
862
+ }
863
+ if (changed) notifyFixtures();
864
+ },
865
+ subscribe: (cb) => {
866
+ fixtureListeners.add(cb);
867
+ return () => fixtureListeners.delete(cb);
868
+ }
869
+ };
225
870
  const sweepWidths = () => {
226
871
  const set = new Set(deriveBreakpoints([], [...matchMediaQueries]));
227
872
  for (const site of sites) for (const bp of coverageMap.get(site.name)?.breakpoints ?? []) set.add(bp);
@@ -230,16 +875,25 @@ function installXrayClient(options) {
230
875
  const first = min === void 0 ? 390 : Math.max(1, Math.min(390, min - 1));
231
876
  return [...new Set([first, ...breakpoints])].toSorted((a, b) => a - b);
232
877
  };
878
+ const MAX_SWEEP_PASSES = 8;
233
879
  const captureAllViews = async () => {
234
- const widths = sweepWidths();
880
+ const visited = /* @__PURE__ */ new Set();
235
881
  try {
236
- for (const width of widths) {
237
- await extCommand("emulate", {
238
- width,
239
- height: 2400,
240
- mobile: width < 600
241
- });
242
- await Promise.all([...sites].map((site) => captureSite(site, true)));
882
+ for (let pass = 0;; pass++) {
883
+ const pending = sweepWidths().filter((width) => !visited.has(width));
884
+ if (pending.length === 0) break;
885
+ if (pass >= MAX_SWEEP_PASSES) throw new Error(`xray capture-all did not converge after ${MAX_SWEEP_PASSES} passes (still discovering widths: ${pending.join(", ")})`);
886
+ for (const width of pending) {
887
+ visited.add(width);
888
+ captureGeneration++;
889
+ await extCommand("emulate", {
890
+ width,
891
+ height: 2400,
892
+ mobile: width < 600
893
+ });
894
+ await Promise.all([...sites].map((site) => scheduleCapture(site, true, true)));
895
+ }
896
+ await options.refreshCoverage?.();
243
897
  }
244
898
  } finally {
245
899
  await extCommand("reset").catch(() => void 0);
@@ -252,21 +906,31 @@ function installXrayClient(options) {
252
906
  updatePlate,
253
907
  captureAllViews,
254
908
  coverage,
909
+ replay: replayEnabled,
910
+ replayMode,
911
+ recordMode,
912
+ fixtures,
255
913
  captured: (name, el, opts) => {
256
914
  const site = {
257
915
  name,
258
916
  el,
259
917
  delay: opts?.delay ?? defaultDelay,
918
+ mode: opts?.mode ?? "auto",
919
+ walker: opts?.walker,
260
920
  sent: /* @__PURE__ */ new Map(),
261
921
  disposed: false,
262
- tooLarge: false
922
+ tooLarge: false,
923
+ dirty: false,
924
+ dirtyForce: false,
925
+ dirtyAwaitSink: false
263
926
  };
264
927
  sites.add(site);
265
- captureSite(site);
928
+ scheduleCapture(site);
266
929
  return () => {
267
930
  site.disposed = true;
268
931
  sites.delete(site);
269
- for (const other of sites) if (other.name === site.name) captureSite(other);
932
+ captureGeneration++;
933
+ for (const other of sites) if (other.name === site.name) scheduleCapture(other);
270
934
  };
271
935
  }
272
936
  };
@@ -277,13 +941,37 @@ function installXrayClient(options) {
277
941
  clearTimeout(resizeTimer);
278
942
  unsubscribeCapture();
279
943
  globalThis.__XRAY__ = void 0;
944
+ captureGeneration++;
945
+ for (const site of sites) site.disposed = true;
280
946
  sites.clear();
281
947
  };
282
948
  }
949
+ /**
950
+ * Surface a capture's dev-only diagnostics as ONE grouped `console.warn` keyed
951
+ * by plate name: a header line, then one formatted line per diagnostic via the
952
+ * shared formatter (diagnostics.ts) — the same text the Vite server prints, so
953
+ * the two never drift. `console.group` collapses the lines under the header;
954
+ * we fall back to a single multi-line warn where it's unavailable. No-op when
955
+ * there are no diagnostics, so a clean capture is silent.
956
+ */
957
+ function reportDiagnostics(name, diagnostics) {
958
+ if (!diagnostics || diagnostics.length === 0) return;
959
+ const header = diagnosticsHeader(name);
960
+ const lines = diagnostics.map((d) => formatDiagnostic(d));
961
+ if (typeof console.group === "function" && typeof console.groupEnd === "function") {
962
+ console.group(header);
963
+ for (const line of lines) console.warn(line);
964
+ console.groupEnd();
965
+ return;
966
+ }
967
+ console.warn([header, ...lines].join("\n"));
968
+ }
283
969
  const LIGHTBOX_KEY = "xray:lightbox";
284
970
  const LIGHTBOX_PARAM = "xray-lightbox";
285
971
  const CAPTURE_KEY = "xray:capture";
286
972
  const CAPTURE_PARAM = "xray-capture";
973
+ const REPLAY_KEY = "xray:replay";
974
+ const REPLAY_PARAM = "xray-replay";
287
975
  /**
288
976
  * A sticky boolean toggle: per-tab in sessionStorage, seedable to `true` via
289
977
  * `?<param>` in the URL, flipped from the HUD or the console
@@ -322,7 +1010,7 @@ function createToggleStore(win, key, param, initial) {
322
1010
  * made single-shot capture non-deterministic in the M1 harness; this is the
323
1011
  * cure. Hard-capped so a perpetually-animating subtree still captures.
324
1012
  */
325
- function settle(el, delay, cap) {
1013
+ function settle(el, delay, cap, mode = "auto") {
326
1014
  const doc = el.ownerDocument;
327
1015
  return (doc.fonts ? doc.fonts.ready : Promise.resolve()).then(() => new Promise((resolve) => {
328
1016
  let timer;
@@ -345,7 +1033,8 @@ function settle(el, delay, cap) {
345
1033
  childList: true,
346
1034
  characterData: true
347
1035
  });
348
- if (resizeObserver) for (const child of Array.from(el.children)) resizeObserver.observe(child);
1036
+ if (resizeObserver) if (mode === "manual") resizeObserver.observe(el);
1037
+ else for (const child of Array.from(el.children)) resizeObserver.observe(child);
349
1038
  timer = setTimeout(done, delay);
350
1039
  capTimer = setTimeout(done, cap);
351
1040
  }));