@hueest/xray 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +64 -16
- package/dist/client.js +903 -72
- package/dist/core.d.ts +21 -27
- package/dist/core.js +39 -210
- package/dist/css-escape-N7bOusGW.js +52 -0
- package/dist/hud.js +46 -0
- package/dist/index.d.ts +297 -48
- package/dist/index.js +674 -353
- package/dist/plate-BIr62u7l.d.ts +363 -0
- package/dist/plate-BRR6d8Se.js +156 -0
- package/dist/project-DORoPAo7.js +10224 -0
- package/dist/react.core-BUj8ziwh.d.ts +79 -0
- package/dist/react.core-DMIhXHZF.js +272 -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-Cs7hUxmK.d.ts +107 -0
- package/package.json +8 -7
- package/virtual.d.ts +6 -0
- package/dist/breakpoints-CBNlh7lQ.js +0 -47
- package/dist/plate-BuzRMPx4.js +0 -94
- package/dist/plate-DboxMKg-.d.ts +0 -196
- package/dist/react.core-CqnDjfAJ.js +0 -59
- package/dist/react.core-fgWG_svU.d.ts +0 -21
- package/dist/serialize-Bq6yJnNV.js +0 -1071
- package/dist/serialize.d.ts +0 -88
- package/dist/serialize.js +0 -2
package/dist/client.js
CHANGED
|
@@ -1,15 +1,545 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { a as serializePlate, d as formatDiagnostic, f as makeDiagnostic, l as runPlatePasses, n as deriveBreakpoints, o as CaptureTooLargeError, p as emit, r as regimeFor, s as captureView, t as serializePlateIR, u as diagnosticsHeader } from "./project-DORoPAo7.js";
|
|
2
|
+
//#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
|
-
* Dev-only radiographic capture effect — pure delight. When a capture lands
|
|
6
|
-
* briefly "expose" the captured element: a
|
|
7
|
-
* x-ray IS a negative — `backdrop-filter: invert`)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
540
|
*/
|
|
12
541
|
const STYLE_ID = "xr-flash-style";
|
|
542
|
+
const DURATION_MS = 300;
|
|
13
543
|
function ensureStyle() {
|
|
14
544
|
if (document.getElementById(STYLE_ID)) return;
|
|
15
545
|
const style = document.createElement("style");
|
|
@@ -17,31 +547,22 @@ function ensureStyle() {
|
|
|
17
547
|
style.textContent = `
|
|
18
548
|
.xr-flash {
|
|
19
549
|
position: fixed; pointer-events: none; z-index: 2147483646;
|
|
20
|
-
|
|
21
|
-
animation: xr-expose
|
|
22
|
-
}
|
|
23
|
-
.xr-flash::after {
|
|
24
|
-
content: ''; position: absolute; left: 0; right: 0; height: 18%;
|
|
25
|
-
background: linear-gradient(180deg, transparent, rgba(186, 230, 253, 0.9), transparent);
|
|
26
|
-
filter: blur(1px);
|
|
27
|
-
animation: xr-scan 480ms ease-in-out forwards;
|
|
550
|
+
border-radius: 4px;
|
|
551
|
+
animation: xr-expose ${DURATION_MS}ms ease-out forwards;
|
|
28
552
|
}
|
|
29
553
|
@keyframes xr-expose {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
from { transform: translateY(-100%) }
|
|
36
|
-
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) }
|
|
37
559
|
}
|
|
38
560
|
@keyframes xr-expose-soft {
|
|
39
|
-
0%
|
|
40
|
-
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) }
|
|
41
563
|
}
|
|
42
564
|
@media (prefers-reduced-motion: reduce) {
|
|
43
|
-
.xr-flash { animation: xr-expose-soft
|
|
44
|
-
.xr-flash::after { display: none }
|
|
565
|
+
.xr-flash { animation: xr-expose-soft ${DURATION_MS}ms ease-out forwards }
|
|
45
566
|
}
|
|
46
567
|
`;
|
|
47
568
|
(document.head ?? document.documentElement).append(style);
|
|
@@ -78,7 +599,7 @@ function flashCapture(el) {
|
|
|
78
599
|
flash.style.height = `${box.height}px`;
|
|
79
600
|
flash.addEventListener("animationend", () => flash.remove(), { once: true });
|
|
80
601
|
(document.body ?? document.documentElement).append(flash);
|
|
81
|
-
setTimeout(() => flash.remove(),
|
|
602
|
+
setTimeout(() => flash.remove(), 500);
|
|
82
603
|
}
|
|
83
604
|
//#endregion
|
|
84
605
|
//#region src/client.ts
|
|
@@ -88,6 +609,23 @@ function isExtensionAck(value) {
|
|
|
88
609
|
const DEFAULT_SETTLE_CAP = 5e3;
|
|
89
610
|
const DEFAULT_MAX_NODES = 4e3;
|
|
90
611
|
const RESIZE_DEBOUNCE = 300;
|
|
612
|
+
/**
|
|
613
|
+
* The capture roots for a site, the ONE place the auto/manual root models
|
|
614
|
+
* diverge (ADR 0014, finding #6).
|
|
615
|
+
*
|
|
616
|
+
* - AUTO: `site.el` is the `display:contents` wrapper, a layout-neutral box, so
|
|
617
|
+
* the content is its CHILDREN — the synthetic root (id 0) stands in for the
|
|
618
|
+
* wrapper and the children become the plate's top level (today's behavior).
|
|
619
|
+
* `<STYLE>` children are dropped (a stitched-in scoped sheet, never content).
|
|
620
|
+
* - MANUAL: `site.el` is the consumer's OWN styled element, so the element
|
|
621
|
+
* ITSELF is the root. Capturing its children would drop its box, classes,
|
|
622
|
+
* padding, border, background, and radius — the exact bug this fixes — so the
|
|
623
|
+
* element is the single root and its own box is serialized.
|
|
624
|
+
*/
|
|
625
|
+
function rootsFor(site) {
|
|
626
|
+
if (site.mode === "manual") return [site.el];
|
|
627
|
+
return Array.from(site.el.children).filter((child) => child.tagName !== "STYLE");
|
|
628
|
+
}
|
|
91
629
|
function installXrayClient(options) {
|
|
92
630
|
const win = window;
|
|
93
631
|
const defaultDelay = options.delay ?? 200;
|
|
@@ -95,6 +633,8 @@ function installXrayClient(options) {
|
|
|
95
633
|
const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
96
634
|
const sites = /* @__PURE__ */ new Set();
|
|
97
635
|
const matchMediaQueries = /* @__PURE__ */ new Set();
|
|
636
|
+
let captureGeneration = 0;
|
|
637
|
+
let sweeping = false;
|
|
98
638
|
const livePlates = /* @__PURE__ */ new Map();
|
|
99
639
|
const plateSerialized = /* @__PURE__ */ new Map();
|
|
100
640
|
const plateListeners = /* @__PURE__ */ new Map();
|
|
@@ -117,10 +657,11 @@ function installXrayClient(options) {
|
|
|
117
657
|
};
|
|
118
658
|
const updatePlate = (name, plate) => {
|
|
119
659
|
const json = JSON.stringify(plate);
|
|
120
|
-
if (plateSerialized.get(name) === json) return;
|
|
660
|
+
if (plateSerialized.get(name) === json) return false;
|
|
121
661
|
plateSerialized.set(name, json);
|
|
122
662
|
livePlates.set(name, plate);
|
|
123
663
|
for (const cb of plateListeners.get(name) ?? []) cb();
|
|
664
|
+
return true;
|
|
124
665
|
};
|
|
125
666
|
const nativeMatchMedia = win.matchMedia.bind(win);
|
|
126
667
|
win.matchMedia = (query) => {
|
|
@@ -128,7 +669,10 @@ function installXrayClient(options) {
|
|
|
128
669
|
return nativeMatchMedia(query);
|
|
129
670
|
};
|
|
130
671
|
const lightbox = createToggleStore(win, LIGHTBOX_KEY, LIGHTBOX_PARAM, false);
|
|
131
|
-
const captureEnabled = createToggleStore(win, CAPTURE_KEY, CAPTURE_PARAM, options.capture ??
|
|
672
|
+
const captureEnabled = createToggleStore(win, CAPTURE_KEY, CAPTURE_PARAM, options.capture ?? false);
|
|
673
|
+
const replayEnabled = createToggleStore(win, REPLAY_KEY, REPLAY_PARAM, options.replay === "prefer" || options.replay === "missing-only");
|
|
674
|
+
const replayMode = options.replay === "missing-only" ? "missing-only" : "prefer";
|
|
675
|
+
const recordMode = options.record ?? "manual";
|
|
132
676
|
const isPrimaryForName = (site) => {
|
|
133
677
|
for (const other of sites) {
|
|
134
678
|
if (other === site || other.disposed || other.name !== site.name || !other.el.isConnected) continue;
|
|
@@ -136,49 +680,212 @@ function installXrayClient(options) {
|
|
|
136
680
|
}
|
|
137
681
|
return true;
|
|
138
682
|
};
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
683
|
+
/**
|
|
684
|
+
* Measure one View into a LOCAL, fresh 1-View `PlateIR` (no shared bundle) and
|
|
685
|
+
* return it only if the generation guard still holds AFTER the settle — the
|
|
686
|
+
* heart of the ADR 0022 generation-guard preservation (plan §4). Because
|
|
687
|
+
* `captureView(roots, plate)` MUTATES the passed bundle in place
|
|
688
|
+
* (`plate.views.push`), a stale capture appended to a SHARED session PlateIR
|
|
689
|
+
* would corrupt the whole Plate's projection (one width's geometry under
|
|
690
|
+
* another's island/gate), and post-hoc rollback is impossible. So we capture
|
|
691
|
+
* into a local bundle FIRST, re-check the guard, and only the caller splices the
|
|
692
|
+
* survivor into the session bundle. Returns the local View on success, or
|
|
693
|
+
* `undefined` when the guard moved / the capture was refused / the site was torn
|
|
694
|
+
* down — the caller then DISCARDS it (never appends).
|
|
695
|
+
*
|
|
696
|
+
* `generation` is the token snapshotted by the caller BEFORE the settle; a
|
|
697
|
+
* width-change/disposal/uninstall mid-settle moves `captureGeneration` and this
|
|
698
|
+
* returns undefined. A `CaptureTooLargeError` marks the site and warns. Any
|
|
699
|
+
* other failure warns and returns undefined.
|
|
700
|
+
*/
|
|
701
|
+
const captureLocalView = async (site, generation) => {
|
|
702
|
+
await settle(site.el, site.delay, settleCap, site.mode);
|
|
703
|
+
if (generation !== captureGeneration || site.disposed || site.tooLarge || !site.el.isConnected || !isPrimaryForName(site)) return;
|
|
704
|
+
const roots = rootsFor(site);
|
|
705
|
+
if (roots.length === 0) return void 0;
|
|
706
|
+
let local;
|
|
145
707
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
capture: {
|
|
152
|
-
...capture,
|
|
153
|
-
...span
|
|
154
|
-
},
|
|
155
|
-
breakpoints
|
|
156
|
-
};
|
|
157
|
-
const key = JSON.stringify(span);
|
|
158
|
-
const body = JSON.stringify(payload);
|
|
159
|
-
if (site.sent.get(key) === body) return;
|
|
160
|
-
site.sent.set(key, body);
|
|
161
|
-
options.sink(payload);
|
|
162
|
-
flashCapture(site.el);
|
|
708
|
+
local = captureView(roots, {
|
|
709
|
+
maxNodes,
|
|
710
|
+
walker: site.walker,
|
|
711
|
+
captureRootIsBoundary: site.mode === "manual"
|
|
712
|
+
});
|
|
163
713
|
} catch (error) {
|
|
164
714
|
if (error instanceof CaptureTooLargeError) {
|
|
165
715
|
site.tooLarge = true;
|
|
166
|
-
|
|
716
|
+
reportDiagnostics(site.name, [makeDiagnostic(error.code, { detail: `${error.nodeCount} nodes (max ${maxNodes})` })]);
|
|
167
717
|
return;
|
|
168
718
|
}
|
|
169
719
|
console.warn("[xray] capture failed for", site.name, error);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const view = local.views[0];
|
|
723
|
+
if (!view) return void 0;
|
|
724
|
+
reportDiagnostics(site.name, view.diagnostics);
|
|
725
|
+
if (generation !== captureGeneration || site.disposed) return void 0;
|
|
726
|
+
return view;
|
|
727
|
+
};
|
|
728
|
+
const recording = /* @__PURE__ */ new Map();
|
|
729
|
+
const recordingBreakpoints = (buf) => [...new Set([...buf.views.flatMap((v) => v.bpSeed ?? []), ...deriveBreakpoints([], [...matchMediaQueries])])].toSorted((a, b) => a - b);
|
|
730
|
+
const projectRecording = (buf) => {
|
|
731
|
+
if (buf.views.length === 0) return void 0;
|
|
732
|
+
const breakpoints = recordingBreakpoints(buf);
|
|
733
|
+
const perBand = /* @__PURE__ */ new Map();
|
|
734
|
+
for (const view of buf.views) perBand.set(regimeFor(view.width, breakpoints).min ?? 0, view);
|
|
735
|
+
const views = [...perBand.values()].toSorted((a, b) => a.width - b.width);
|
|
736
|
+
return serializePlateIR({
|
|
737
|
+
name: buf.name,
|
|
738
|
+
views,
|
|
739
|
+
breakpoints
|
|
740
|
+
});
|
|
741
|
+
};
|
|
742
|
+
const previewRecording = (buf) => {
|
|
743
|
+
const stored = projectRecording(buf);
|
|
744
|
+
return stored ? updatePlate(buf.name, serializePlate(stored)) : false;
|
|
745
|
+
};
|
|
746
|
+
const finalizeRecording = async () => {
|
|
747
|
+
const entries = [...recording];
|
|
748
|
+
recording.clear();
|
|
749
|
+
await Promise.all(entries.map(async ([name, buf]) => {
|
|
750
|
+
const stored = projectRecording(buf);
|
|
751
|
+
if (!stored) return;
|
|
752
|
+
const diagnostics = buf.views.flatMap((v) => v.diagnostics ?? []);
|
|
753
|
+
const envelope = {
|
|
754
|
+
plate: stored,
|
|
755
|
+
...diagnostics.length > 0 ? { diagnostics } : {}
|
|
756
|
+
};
|
|
757
|
+
try {
|
|
758
|
+
await options.sink(envelope);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (!recording.has(name)) recording.set(name, buf);
|
|
761
|
+
console.warn(`[xray] recording write failed for "${name}" — kept in memory; retried when you next start a recording`, error);
|
|
762
|
+
}
|
|
763
|
+
}));
|
|
764
|
+
};
|
|
765
|
+
const runCapture = async (site, force) => {
|
|
766
|
+
if (!force && (!captureEnabled.get() || sweeping) || site.tooLarge || !isPrimaryForName(site)) return;
|
|
767
|
+
const view = await captureLocalView(site, captureGeneration);
|
|
768
|
+
if (!view) return;
|
|
769
|
+
if (sweeping && force) {
|
|
770
|
+
(site.sessionPlate ??= {
|
|
771
|
+
name: site.name,
|
|
772
|
+
views: [],
|
|
773
|
+
breakpoints: []
|
|
774
|
+
}).views.push(view);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (!captureEnabled.get()) return;
|
|
778
|
+
view.bpSeed = deriveBreakpoints(emit(view.bundle).rules, []);
|
|
779
|
+
runPlatePasses({
|
|
780
|
+
name: site.name,
|
|
781
|
+
views: [view],
|
|
782
|
+
breakpoints: []
|
|
783
|
+
});
|
|
784
|
+
let buf = recording.get(site.name);
|
|
785
|
+
if (!buf) {
|
|
786
|
+
buf = {
|
|
787
|
+
name: site.name,
|
|
788
|
+
views: [],
|
|
789
|
+
breakpoints: []
|
|
790
|
+
};
|
|
791
|
+
recording.set(site.name, buf);
|
|
792
|
+
}
|
|
793
|
+
buf.views = buf.views.filter((v) => v.width !== view.width);
|
|
794
|
+
buf.views.push(view);
|
|
795
|
+
if (previewRecording(buf)) flashCapture(site.el);
|
|
796
|
+
};
|
|
797
|
+
/**
|
|
798
|
+
* Project a per-site `PlateIR` to the structured `StoredPlate` IN THE BROWSER
|
|
799
|
+
* (the ADR 0022 Serialize) and hand the envelope to the sink. Derives the
|
|
800
|
+
* breakpoint union ONCE over every View's rules + the recorded matchMedia
|
|
801
|
+
* queries (relocating the old per-View derivation), runs the reduction passes
|
|
802
|
+
* once over the whole bundle, then `serializePlateIR`. Dedupes per-PLATE by the
|
|
803
|
+
* serialized StoredPlate string. ALWAYS catches so neither mode leaks an
|
|
804
|
+
* unhandled rejection; on failure it clears the dedupe so a retry re-sends and,
|
|
805
|
+
* ONLY when `awaitSink`, rethrows so capture-all still fails loudly (pieces
|
|
806
|
+
* 3/8). The radiographic flash runs only AFTER a successful send.
|
|
807
|
+
*/
|
|
808
|
+
const sendPlate = async (site, plate, awaitSink) => {
|
|
809
|
+
if (plate.views.length === 0) return;
|
|
810
|
+
plate.name = site.name;
|
|
811
|
+
plate.breakpoints = deriveBreakpoints(plate.views.flatMap((v) => emit(v.bundle).rules), [...matchMediaQueries]);
|
|
812
|
+
runPlatePasses(plate);
|
|
813
|
+
const stored = serializePlateIR(plate);
|
|
814
|
+
const diagnostics = plate.views.flatMap((v) => v.diagnostics ?? []);
|
|
815
|
+
const envelope = {
|
|
816
|
+
plate: stored,
|
|
817
|
+
...diagnostics.length > 0 ? { diagnostics } : {}
|
|
818
|
+
};
|
|
819
|
+
const body = JSON.stringify(envelope);
|
|
820
|
+
if (site.lastSent === body) return;
|
|
821
|
+
site.lastSent = body;
|
|
822
|
+
try {
|
|
823
|
+
await options.sink(envelope);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
site.lastSent = void 0;
|
|
826
|
+
if (awaitSink) throw error;
|
|
827
|
+
console.warn("[xray] capture send failed for", site.name, error);
|
|
828
|
+
return;
|
|
170
829
|
}
|
|
830
|
+
flashCapture(site.el);
|
|
831
|
+
};
|
|
832
|
+
/**
|
|
833
|
+
* Finalize a site's accumulated sweep `PlateIR` and POST it ONCE (awaited, the
|
|
834
|
+
* backpressure point). Clears `site.sessionPlate` first so any straggler capture
|
|
835
|
+
* for the site falls back to the browse path rather than mutating the bundle
|
|
836
|
+
* mid-flight. A session that captured no surviving View posts nothing.
|
|
837
|
+
*/
|
|
838
|
+
const finalizeSessionPlate = async (site) => {
|
|
839
|
+
const session = site.sessionPlate;
|
|
840
|
+
site.sessionPlate = void 0;
|
|
841
|
+
if (!session || session.views.length === 0) return;
|
|
842
|
+
await sendPlate(site, session, true);
|
|
843
|
+
};
|
|
844
|
+
const scheduleCapture = (site, force = false) => {
|
|
845
|
+
if (site.inflight) {
|
|
846
|
+
site.dirty = true;
|
|
847
|
+
site.dirtyForce = site.dirtyForce || force;
|
|
848
|
+
return site.inflight;
|
|
849
|
+
}
|
|
850
|
+
const loop = async (initialForce) => {
|
|
851
|
+
let wantForce = initialForce;
|
|
852
|
+
for (;;) {
|
|
853
|
+
await runCapture(site, wantForce);
|
|
854
|
+
if (!site.dirty) {
|
|
855
|
+
site.inflight = void 0;
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
site.dirty = false;
|
|
859
|
+
wantForce = site.dirtyForce;
|
|
860
|
+
site.dirtyForce = false;
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
const promise = loop(force);
|
|
864
|
+
site.inflight = promise;
|
|
865
|
+
return promise;
|
|
171
866
|
};
|
|
172
867
|
let resizeTimer;
|
|
173
868
|
const onResize = () => {
|
|
869
|
+
if (sweeping) {
|
|
870
|
+
clearTimeout(resizeTimer);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
174
873
|
clearTimeout(resizeTimer);
|
|
175
874
|
resizeTimer = setTimeout(() => {
|
|
176
|
-
|
|
875
|
+
if (sweeping) return;
|
|
876
|
+
for (const site of sites) scheduleCapture(site);
|
|
177
877
|
}, RESIZE_DEBOUNCE);
|
|
178
878
|
};
|
|
179
879
|
win.addEventListener("resize", onResize);
|
|
880
|
+
let recordingOn = captureEnabled.get();
|
|
180
881
|
const unsubscribeCapture = captureEnabled.subscribe(() => {
|
|
181
|
-
|
|
882
|
+
const on = captureEnabled.get();
|
|
883
|
+
if (on === recordingOn) return;
|
|
884
|
+
recordingOn = on;
|
|
885
|
+
if (on) {
|
|
886
|
+
finalizeRecording();
|
|
887
|
+
for (const site of sites) scheduleCapture(site);
|
|
888
|
+
} else finalizeRecording();
|
|
182
889
|
});
|
|
183
890
|
const extAcks = /* @__PURE__ */ new Map();
|
|
184
891
|
let nextExtReqId = 1;
|
|
@@ -222,26 +929,97 @@ function installXrayClient(options) {
|
|
|
222
929
|
return () => coverageListeners.delete(cb);
|
|
223
930
|
}
|
|
224
931
|
};
|
|
932
|
+
const liveData = /* @__PURE__ */ new Map();
|
|
933
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
934
|
+
const fixtureListeners = /* @__PURE__ */ new Set();
|
|
935
|
+
const notifyFixtures = () => {
|
|
936
|
+
for (const cb of fixtureListeners) cb();
|
|
937
|
+
};
|
|
938
|
+
const fixtures = {
|
|
939
|
+
register: (name, data) => {
|
|
940
|
+
const entry = { data };
|
|
941
|
+
const stack = liveData.get(name);
|
|
942
|
+
if (stack) stack.push(entry);
|
|
943
|
+
else liveData.set(name, [entry]);
|
|
944
|
+
return () => {
|
|
945
|
+
const current = liveData.get(name);
|
|
946
|
+
if (!current) return;
|
|
947
|
+
const i = current.indexOf(entry);
|
|
948
|
+
if (i !== -1) current.splice(i, 1);
|
|
949
|
+
if (current.length === 0) liveData.delete(name);
|
|
950
|
+
};
|
|
951
|
+
},
|
|
952
|
+
has: (name) => recorded.has(name),
|
|
953
|
+
read: (name) => recorded.get(name),
|
|
954
|
+
record: async (name) => {
|
|
955
|
+
const live = liveData.get(name)?.at(-1);
|
|
956
|
+
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.`);
|
|
957
|
+
const encoded = stringify(live.data);
|
|
958
|
+
await options.recordFixtureSink?.({
|
|
959
|
+
name,
|
|
960
|
+
data: encoded
|
|
961
|
+
});
|
|
962
|
+
recorded.set(name, { data: parse(encoded) });
|
|
963
|
+
notifyFixtures();
|
|
964
|
+
},
|
|
965
|
+
recordAll: async () => {
|
|
966
|
+
await Promise.all([...liveData.keys()].map((name) => fixtures.record(name)));
|
|
967
|
+
},
|
|
968
|
+
seed: (encoded) => {
|
|
969
|
+
let changed = false;
|
|
970
|
+
for (const [name, str] of Object.entries(encoded)) {
|
|
971
|
+
if (recorded.has(name)) continue;
|
|
972
|
+
try {
|
|
973
|
+
recorded.set(name, { data: parse(str) });
|
|
974
|
+
changed = true;
|
|
975
|
+
} catch {}
|
|
976
|
+
}
|
|
977
|
+
if (changed) notifyFixtures();
|
|
978
|
+
},
|
|
979
|
+
subscribe: (cb) => {
|
|
980
|
+
fixtureListeners.add(cb);
|
|
981
|
+
return () => fixtureListeners.delete(cb);
|
|
982
|
+
}
|
|
983
|
+
};
|
|
225
984
|
const sweepWidths = () => {
|
|
226
985
|
const set = new Set(deriveBreakpoints([], [...matchMediaQueries]));
|
|
227
|
-
for (const site of sites) for (const
|
|
986
|
+
for (const site of sites) for (const view of site.sessionPlate?.views ?? []) for (const bp of deriveBreakpoints(emit(view.bundle).rules, [...matchMediaQueries])) set.add(bp);
|
|
228
987
|
const breakpoints = [...set].toSorted((a, b) => a - b);
|
|
229
988
|
const min = breakpoints[0];
|
|
230
989
|
const first = min === void 0 ? 390 : Math.max(1, Math.min(390, min - 1));
|
|
231
990
|
return [...new Set([first, ...breakpoints])].toSorted((a, b) => a - b);
|
|
232
991
|
};
|
|
992
|
+
const MAX_SWEEP_PASSES = 8;
|
|
233
993
|
const captureAllViews = async () => {
|
|
234
|
-
const
|
|
994
|
+
const visited = /* @__PURE__ */ new Set();
|
|
995
|
+
sweeping = true;
|
|
996
|
+
for (const site of sites) site.sessionPlate = {
|
|
997
|
+
name: site.name,
|
|
998
|
+
views: [],
|
|
999
|
+
breakpoints: []
|
|
1000
|
+
};
|
|
235
1001
|
try {
|
|
236
|
-
for (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
1002
|
+
for (let pass = 0;; pass++) {
|
|
1003
|
+
const pending = sweepWidths().filter((width) => !visited.has(width));
|
|
1004
|
+
if (pending.length === 0) break;
|
|
1005
|
+
if (pass >= MAX_SWEEP_PASSES) throw new Error(`xray capture-all did not converge after ${MAX_SWEEP_PASSES} passes (still discovering widths: ${pending.join(", ")})`);
|
|
1006
|
+
for (const width of pending) {
|
|
1007
|
+
visited.add(width);
|
|
1008
|
+
captureGeneration++;
|
|
1009
|
+
await extCommand("emulate", {
|
|
1010
|
+
width,
|
|
1011
|
+
height: 2400,
|
|
1012
|
+
mobile: width < 600
|
|
1013
|
+
});
|
|
1014
|
+
await Promise.all([...sites].map((site) => scheduleCapture(site, true)));
|
|
1015
|
+
}
|
|
1016
|
+
await options.refreshCoverage?.();
|
|
243
1017
|
}
|
|
1018
|
+
sweeping = false;
|
|
1019
|
+
await Promise.all([...sites].map((site) => finalizeSessionPlate(site)));
|
|
244
1020
|
} finally {
|
|
1021
|
+
sweeping = false;
|
|
1022
|
+
for (const site of sites) site.sessionPlate = void 0;
|
|
245
1023
|
await extCommand("reset").catch(() => void 0);
|
|
246
1024
|
}
|
|
247
1025
|
};
|
|
@@ -252,21 +1030,29 @@ function installXrayClient(options) {
|
|
|
252
1030
|
updatePlate,
|
|
253
1031
|
captureAllViews,
|
|
254
1032
|
coverage,
|
|
1033
|
+
replay: replayEnabled,
|
|
1034
|
+
replayMode,
|
|
1035
|
+
recordMode,
|
|
1036
|
+
fixtures,
|
|
255
1037
|
captured: (name, el, opts) => {
|
|
256
1038
|
const site = {
|
|
257
1039
|
name,
|
|
258
1040
|
el,
|
|
259
1041
|
delay: opts?.delay ?? defaultDelay,
|
|
260
|
-
|
|
1042
|
+
mode: opts?.mode ?? "auto",
|
|
1043
|
+
walker: opts?.walker,
|
|
261
1044
|
disposed: false,
|
|
262
|
-
tooLarge: false
|
|
1045
|
+
tooLarge: false,
|
|
1046
|
+
dirty: false,
|
|
1047
|
+
dirtyForce: false
|
|
263
1048
|
};
|
|
264
1049
|
sites.add(site);
|
|
265
|
-
|
|
1050
|
+
scheduleCapture(site);
|
|
266
1051
|
return () => {
|
|
267
1052
|
site.disposed = true;
|
|
268
1053
|
sites.delete(site);
|
|
269
|
-
|
|
1054
|
+
captureGeneration++;
|
|
1055
|
+
for (const other of sites) if (other.name === site.name) scheduleCapture(other);
|
|
270
1056
|
};
|
|
271
1057
|
}
|
|
272
1058
|
};
|
|
@@ -277,13 +1063,37 @@ function installXrayClient(options) {
|
|
|
277
1063
|
clearTimeout(resizeTimer);
|
|
278
1064
|
unsubscribeCapture();
|
|
279
1065
|
globalThis.__XRAY__ = void 0;
|
|
1066
|
+
captureGeneration++;
|
|
1067
|
+
for (const site of sites) site.disposed = true;
|
|
280
1068
|
sites.clear();
|
|
281
1069
|
};
|
|
282
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Surface a capture's dev-only diagnostics as ONE grouped `console.warn` keyed
|
|
1073
|
+
* by plate name: a header line, then one formatted line per diagnostic via the
|
|
1074
|
+
* shared formatter (diagnostics.ts) — the same text the Vite server prints, so
|
|
1075
|
+
* the two never drift. `console.group` collapses the lines under the header;
|
|
1076
|
+
* we fall back to a single multi-line warn where it's unavailable. No-op when
|
|
1077
|
+
* there are no diagnostics, so a clean capture is silent.
|
|
1078
|
+
*/
|
|
1079
|
+
function reportDiagnostics(name, diagnostics) {
|
|
1080
|
+
if (!diagnostics || diagnostics.length === 0) return;
|
|
1081
|
+
const header = diagnosticsHeader(name);
|
|
1082
|
+
const lines = diagnostics.map((d) => formatDiagnostic(d));
|
|
1083
|
+
if (typeof console.group === "function" && typeof console.groupEnd === "function") {
|
|
1084
|
+
console.group(header);
|
|
1085
|
+
for (const line of lines) console.warn(line);
|
|
1086
|
+
console.groupEnd();
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
console.warn([header, ...lines].join("\n"));
|
|
1090
|
+
}
|
|
283
1091
|
const LIGHTBOX_KEY = "xray:lightbox";
|
|
284
1092
|
const LIGHTBOX_PARAM = "xray-lightbox";
|
|
285
1093
|
const CAPTURE_KEY = "xray:capture";
|
|
286
1094
|
const CAPTURE_PARAM = "xray-capture";
|
|
1095
|
+
const REPLAY_KEY = "xray:replay";
|
|
1096
|
+
const REPLAY_PARAM = "xray-replay";
|
|
287
1097
|
/**
|
|
288
1098
|
* A sticky boolean toggle: per-tab in sessionStorage, seedable to `true` via
|
|
289
1099
|
* `?<param>` in the URL, flipped from the HUD or the console
|
|
@@ -322,9 +1132,29 @@ function createToggleStore(win, key, param, initial) {
|
|
|
322
1132
|
* made single-shot capture non-deterministic in the M1 harness; this is the
|
|
323
1133
|
* cure. Hard-capped so a perpetually-animating subtree still captures.
|
|
324
1134
|
*/
|
|
325
|
-
|
|
1135
|
+
/**
|
|
1136
|
+
* Force lazy images in the subtree to load and await their decode, so the walk
|
|
1137
|
+
* measures their real boxes. A `loading="lazy"` image below the fold never enters
|
|
1138
|
+
* the viewport during capture, so its IntersectionObserver never fires — flip it
|
|
1139
|
+
* to eager to trigger the load. Capped: a slow or broken image resolves the race
|
|
1140
|
+
* via the timeout instead of blocking the capture.
|
|
1141
|
+
*/
|
|
1142
|
+
function awaitImages(root, cap) {
|
|
1143
|
+
const imgs = Array.from(root.querySelectorAll("img"));
|
|
1144
|
+
if (imgs.length === 0) return Promise.resolve();
|
|
1145
|
+
const decoded = imgs.map((img) => {
|
|
1146
|
+
if (img.loading === "lazy") img.loading = "eager";
|
|
1147
|
+
if (typeof img.decode !== "function") return Promise.resolve();
|
|
1148
|
+
return img.decode().then(() => void 0, () => void 0);
|
|
1149
|
+
});
|
|
1150
|
+
return Promise.race([Promise.all(decoded).then(() => void 0), new Promise((resolve) => {
|
|
1151
|
+
setTimeout(resolve, cap);
|
|
1152
|
+
})]);
|
|
1153
|
+
}
|
|
1154
|
+
function settle(el, delay, cap, mode = "auto") {
|
|
326
1155
|
const doc = el.ownerDocument;
|
|
327
|
-
|
|
1156
|
+
const fonts = doc.fonts ? doc.fonts.ready : Promise.resolve();
|
|
1157
|
+
return Promise.all([fonts, awaitImages(el, cap)]).then(() => new Promise((resolve) => {
|
|
328
1158
|
let timer;
|
|
329
1159
|
let capTimer;
|
|
330
1160
|
const observer = new MutationObserver(bump);
|
|
@@ -345,7 +1175,8 @@ function settle(el, delay, cap) {
|
|
|
345
1175
|
childList: true,
|
|
346
1176
|
characterData: true
|
|
347
1177
|
});
|
|
348
|
-
if (resizeObserver)
|
|
1178
|
+
if (resizeObserver) if (mode === "manual") resizeObserver.observe(el);
|
|
1179
|
+
else for (const child of Array.from(el.children)) resizeObserver.observe(child);
|
|
349
1180
|
timer = setTimeout(done, delay);
|
|
350
1181
|
capTimer = setTimeout(done, cap);
|
|
351
1182
|
}));
|