@agent-scope/render 1.0.1
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/index.cjs +1555 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +925 -0
- package/dist/index.d.ts +925 -0
- package/dist/index.js +1523 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1523 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { existsSync, readdirSync } from 'fs';
|
|
3
|
+
import { readFile } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { Resvg } from '@resvg/resvg-js';
|
|
7
|
+
import satori from 'satori';
|
|
8
|
+
import sharp from 'sharp';
|
|
9
|
+
|
|
10
|
+
// src/browser-pool.ts
|
|
11
|
+
var CHROMIUM_ARGS = [
|
|
12
|
+
"--headless=new",
|
|
13
|
+
"--disable-gpu",
|
|
14
|
+
"--disable-dev-shm-usage",
|
|
15
|
+
"--no-sandbox",
|
|
16
|
+
"--no-zygote",
|
|
17
|
+
"--disable-extensions",
|
|
18
|
+
"--disable-background-networking",
|
|
19
|
+
"--disable-sync",
|
|
20
|
+
"--hide-scrollbars",
|
|
21
|
+
"--mute-audio",
|
|
22
|
+
"--font-render-hinting=none"
|
|
23
|
+
];
|
|
24
|
+
var PRESET_SIZES = {
|
|
25
|
+
local: { browsers: 1, pagesPerBrowser: 5 },
|
|
26
|
+
"ci-standard": { browsers: 3, pagesPerBrowser: 15 },
|
|
27
|
+
"ci-large": { browsers: 6, pagesPerBrowser: 20 }
|
|
28
|
+
};
|
|
29
|
+
function buildSkeletonHtml(viewportWidth, _viewportHeight) {
|
|
30
|
+
return `<!DOCTYPE html>
|
|
31
|
+
<html lang="en">
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="UTF-8" />
|
|
34
|
+
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
35
|
+
<style>
|
|
36
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
37
|
+
body { margin: 0; padding: 0; font-family: system-ui, sans-serif; }
|
|
38
|
+
#scope-root { display: inline-block; }
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<div id="scope-root" data-reactscope-root></div>
|
|
43
|
+
<script>
|
|
44
|
+
// Component registry \u2014 populated lazily by inject calls
|
|
45
|
+
window.__componentRegistry = {};
|
|
46
|
+
|
|
47
|
+
// Render state
|
|
48
|
+
window.__renderReady = false;
|
|
49
|
+
window.__renderError = null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Swap in a component by name.
|
|
53
|
+
* @param {string} name - component name key in __componentRegistry
|
|
54
|
+
* @param {object} props - serialised props object
|
|
55
|
+
*/
|
|
56
|
+
window.__renderComponent = function(name, props) {
|
|
57
|
+
window.__renderReady = false;
|
|
58
|
+
window.__renderError = null;
|
|
59
|
+
|
|
60
|
+
var root = document.getElementById("scope-root");
|
|
61
|
+
if (!root) {
|
|
62
|
+
window.__renderError = "scope-root element not found";
|
|
63
|
+
window.__renderReady = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var factory = window.__componentRegistry[name];
|
|
68
|
+
if (!factory) {
|
|
69
|
+
// Fallback: render a placeholder div so screenshots still work
|
|
70
|
+
root.innerHTML = "<div data-placeholder=\\"" + name + "\\" style=\\"padding:8px;border:1px dashed #ccc\\">" + name + "</div>";
|
|
71
|
+
window.__renderReady = true;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
factory(root, props || {});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
window.__renderError = err instanceof Error ? err.message : String(err);
|
|
79
|
+
root.innerHTML = "<div data-render-error style=\\"color:red\\">" + window.__renderError + "</div>";
|
|
80
|
+
window.__renderReady = true;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Register a component factory.
|
|
86
|
+
* @param {string} name
|
|
87
|
+
* @param {function(HTMLElement, object): void} factory
|
|
88
|
+
*/
|
|
89
|
+
window.__registerComponent = function(name, factory) {
|
|
90
|
+
window.__componentRegistry[name] = factory;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Signal that the skeleton is ready (not a component render yet)
|
|
94
|
+
window.__skeletonReady = true;
|
|
95
|
+
</script>
|
|
96
|
+
</body>
|
|
97
|
+
</html>`;
|
|
98
|
+
}
|
|
99
|
+
async function extractDomTree(page, selector) {
|
|
100
|
+
const result = await page.evaluate((sel) => {
|
|
101
|
+
let count = 0;
|
|
102
|
+
function walk(node) {
|
|
103
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
104
|
+
const text = node.textContent?.trim();
|
|
105
|
+
return { tag: "#text", attrs: {}, text: text ?? "", children: [] };
|
|
106
|
+
}
|
|
107
|
+
const el = node;
|
|
108
|
+
count++;
|
|
109
|
+
const attrs = {};
|
|
110
|
+
for (const attr of Array.from(el.attributes)) {
|
|
111
|
+
attrs[attr.name] = attr.value;
|
|
112
|
+
}
|
|
113
|
+
const children = Array.from(el.childNodes).filter(
|
|
114
|
+
(n) => n.nodeType === Node.ELEMENT_NODE || n.nodeType === Node.TEXT_NODE && (n.textContent?.trim() ?? "").length > 0
|
|
115
|
+
).map(walk);
|
|
116
|
+
return { tag: el.tagName.toLowerCase(), attrs, children };
|
|
117
|
+
}
|
|
118
|
+
const root = document.querySelector(sel);
|
|
119
|
+
if (!root) {
|
|
120
|
+
return { tree: { tag: "div", attrs: {}, children: [] }, elementCount: 0 };
|
|
121
|
+
}
|
|
122
|
+
const tree = walk(root);
|
|
123
|
+
return { tree, elementCount: count };
|
|
124
|
+
}, selector);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
var BrowserPool = class {
|
|
128
|
+
config;
|
|
129
|
+
sizeConfig;
|
|
130
|
+
browsers = [];
|
|
131
|
+
slots = [];
|
|
132
|
+
waiters = [];
|
|
133
|
+
closed = false;
|
|
134
|
+
initialized = false;
|
|
135
|
+
constructor(config = {}) {
|
|
136
|
+
const viewportWidth = config.viewportWidth ?? 1440;
|
|
137
|
+
const viewportHeight = config.viewportHeight ?? 900;
|
|
138
|
+
const acquireTimeoutMs = config.acquireTimeoutMs ?? 3e4;
|
|
139
|
+
const preset = config.preset ?? "local";
|
|
140
|
+
this.config = {
|
|
141
|
+
preset,
|
|
142
|
+
size: config.size ?? { browsers: 1, pagesPerBrowser: 5 },
|
|
143
|
+
viewportWidth,
|
|
144
|
+
viewportHeight,
|
|
145
|
+
acquireTimeoutMs
|
|
146
|
+
};
|
|
147
|
+
if (config.size) {
|
|
148
|
+
this.sizeConfig = config.size;
|
|
149
|
+
} else {
|
|
150
|
+
this.sizeConfig = PRESET_SIZES[preset] ?? PRESET_SIZES.local ?? { browsers: 1, pagesPerBrowser: 5 };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// -------------------------------------------------------------------------
|
|
154
|
+
// Initialisation
|
|
155
|
+
// -------------------------------------------------------------------------
|
|
156
|
+
/**
|
|
157
|
+
* Launch all browser instances and pre-load every page with the skeleton HTML.
|
|
158
|
+
* Must be called before `render()`.
|
|
159
|
+
*/
|
|
160
|
+
async init() {
|
|
161
|
+
if (this.initialized) return;
|
|
162
|
+
this.initialized = true;
|
|
163
|
+
const { browsers: browserCount, pagesPerBrowser } = this.sizeConfig;
|
|
164
|
+
const skeleton = buildSkeletonHtml(this.config.viewportWidth, this.config.viewportHeight);
|
|
165
|
+
for (let b = 0; b < browserCount; b++) {
|
|
166
|
+
const browser = await chromium.launch({ args: CHROMIUM_ARGS });
|
|
167
|
+
this.browsers.push(browser);
|
|
168
|
+
for (let p = 0; p < pagesPerBrowser; p++) {
|
|
169
|
+
const page = await browser.newPage({
|
|
170
|
+
viewport: { width: this.config.viewportWidth, height: this.config.viewportHeight }
|
|
171
|
+
});
|
|
172
|
+
await page.setContent(skeleton, { waitUntil: "load" });
|
|
173
|
+
const index = this.slots.length;
|
|
174
|
+
this.slots.push({ page, inUse: false, index });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// -------------------------------------------------------------------------
|
|
179
|
+
// Slot management
|
|
180
|
+
// -------------------------------------------------------------------------
|
|
181
|
+
/**
|
|
182
|
+
* Acquire a free page slot. Blocks until one is available or
|
|
183
|
+
* `acquireTimeoutMs` is exceeded.
|
|
184
|
+
*/
|
|
185
|
+
async acquire() {
|
|
186
|
+
if (this.closed) throw new Error("BrowserPool is closed");
|
|
187
|
+
const free = this.slots.find((s) => !s.inUse);
|
|
188
|
+
if (free) {
|
|
189
|
+
free.inUse = true;
|
|
190
|
+
return free;
|
|
191
|
+
}
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
const idx = this.waiters.indexOf(wakeUp);
|
|
195
|
+
if (idx !== -1) this.waiters.splice(idx, 1);
|
|
196
|
+
reject(
|
|
197
|
+
new Error(`BrowserPool.acquire() timed out after ${this.config.acquireTimeoutMs}ms`)
|
|
198
|
+
);
|
|
199
|
+
}, this.config.acquireTimeoutMs);
|
|
200
|
+
const wakeUp = (slot) => {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
resolve(slot);
|
|
203
|
+
};
|
|
204
|
+
this.waiters.push(wakeUp);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Return a page slot to the pool. Wakes the next queued waiter, if any.
|
|
209
|
+
*/
|
|
210
|
+
release(slot) {
|
|
211
|
+
if (!slot.inUse) return;
|
|
212
|
+
slot.inUse = false;
|
|
213
|
+
const next = this.waiters.shift();
|
|
214
|
+
if (next) {
|
|
215
|
+
slot.inUse = true;
|
|
216
|
+
next(slot);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// -------------------------------------------------------------------------
|
|
220
|
+
// Render
|
|
221
|
+
// -------------------------------------------------------------------------
|
|
222
|
+
/**
|
|
223
|
+
* Render a component by name with the given props and return a `RenderResult`.
|
|
224
|
+
*
|
|
225
|
+
* The inject-don't-navigate pattern is used:
|
|
226
|
+
* 1. Acquire a warm page slot.
|
|
227
|
+
* 2. Call `window.__renderComponent(name, props)` — no navigation.
|
|
228
|
+
* 3. Wait for `window.__renderReady`.
|
|
229
|
+
* 4. Clip screenshot to the component bounding box.
|
|
230
|
+
* 5. Optionally capture DOM, styles, console output, and a11y info.
|
|
231
|
+
* 6. Release the slot.
|
|
232
|
+
*/
|
|
233
|
+
async render(componentName, props = {}, options = {}) {
|
|
234
|
+
if (!this.initialized) {
|
|
235
|
+
throw new Error("BrowserPool.init() must be called before render()");
|
|
236
|
+
}
|
|
237
|
+
if (this.closed) {
|
|
238
|
+
throw new Error("BrowserPool is closed");
|
|
239
|
+
}
|
|
240
|
+
const captureStyles = options.captureStyles ?? true;
|
|
241
|
+
const captureConsole = options.captureConsole ?? false;
|
|
242
|
+
const captureDom = options.captureDom ?? false;
|
|
243
|
+
const captureA11y = options.captureA11y ?? false;
|
|
244
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
245
|
+
const consoleErrors = [];
|
|
246
|
+
const consoleWarnings = [];
|
|
247
|
+
const consoleLogs = [];
|
|
248
|
+
const slot = await this.acquire();
|
|
249
|
+
const { page } = slot;
|
|
250
|
+
const consoleHandler = captureConsole ? (msg) => {
|
|
251
|
+
const text = msg.text();
|
|
252
|
+
if (msg.type() === "error") consoleErrors.push(text);
|
|
253
|
+
else if (msg.type() === "warning") consoleWarnings.push(text);
|
|
254
|
+
else consoleLogs.push(text);
|
|
255
|
+
} : null;
|
|
256
|
+
if (consoleHandler) page.on("console", consoleHandler);
|
|
257
|
+
const startTime = Date.now();
|
|
258
|
+
try {
|
|
259
|
+
await page.evaluate(
|
|
260
|
+
({ name, componentProps }) => {
|
|
261
|
+
window.__renderComponent?.(name, componentProps);
|
|
262
|
+
},
|
|
263
|
+
{ name: componentName, componentProps: props }
|
|
264
|
+
);
|
|
265
|
+
await page.waitForFunction(
|
|
266
|
+
() => {
|
|
267
|
+
return window.__renderReady === true;
|
|
268
|
+
},
|
|
269
|
+
{ timeout: timeoutMs }
|
|
270
|
+
);
|
|
271
|
+
const renderError = await page.evaluate(() => {
|
|
272
|
+
return window.__renderError ?? null;
|
|
273
|
+
});
|
|
274
|
+
if (renderError) {
|
|
275
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
276
|
+
}
|
|
277
|
+
const renderTimeMs = Date.now() - startTime;
|
|
278
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
279
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
280
|
+
if (!boundingBox || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
281
|
+
throw new Error(`Component "${componentName}" rendered with zero bounding box`);
|
|
282
|
+
}
|
|
283
|
+
const screenshot = await page.screenshot({
|
|
284
|
+
clip: {
|
|
285
|
+
x: boundingBox.x,
|
|
286
|
+
y: boundingBox.y,
|
|
287
|
+
width: boundingBox.width,
|
|
288
|
+
height: boundingBox.height
|
|
289
|
+
},
|
|
290
|
+
type: "png"
|
|
291
|
+
});
|
|
292
|
+
const computedStyles = {};
|
|
293
|
+
if (captureStyles) {
|
|
294
|
+
const styles = await page.evaluate((sel) => {
|
|
295
|
+
const el = document.querySelector(sel);
|
|
296
|
+
if (!el) return {};
|
|
297
|
+
const computed = window.getComputedStyle(el);
|
|
298
|
+
const snapshot = {};
|
|
299
|
+
const PROPS = [
|
|
300
|
+
"display",
|
|
301
|
+
"flexDirection",
|
|
302
|
+
"flexWrap",
|
|
303
|
+
"alignItems",
|
|
304
|
+
"justifyContent",
|
|
305
|
+
"gridTemplateColumns",
|
|
306
|
+
"gridTemplateRows",
|
|
307
|
+
"position",
|
|
308
|
+
"width",
|
|
309
|
+
"height",
|
|
310
|
+
"margin",
|
|
311
|
+
"padding",
|
|
312
|
+
"color",
|
|
313
|
+
"backgroundColor",
|
|
314
|
+
"fontSize",
|
|
315
|
+
"fontFamily",
|
|
316
|
+
"fontWeight",
|
|
317
|
+
"lineHeight",
|
|
318
|
+
"borderRadius",
|
|
319
|
+
"boxShadow",
|
|
320
|
+
"opacity",
|
|
321
|
+
"transform",
|
|
322
|
+
"animation",
|
|
323
|
+
"transition",
|
|
324
|
+
"overflow"
|
|
325
|
+
];
|
|
326
|
+
for (const prop of PROPS) {
|
|
327
|
+
snapshot[prop] = computed.getPropertyValue(prop);
|
|
328
|
+
}
|
|
329
|
+
return snapshot;
|
|
330
|
+
}, "[data-reactscope-root] > *");
|
|
331
|
+
computedStyles["[data-reactscope-root] > *"] = styles;
|
|
332
|
+
}
|
|
333
|
+
let dom;
|
|
334
|
+
if (captureDom) {
|
|
335
|
+
const { tree, elementCount } = await extractDomTree(page, "[data-reactscope-root]");
|
|
336
|
+
dom = {
|
|
337
|
+
tree,
|
|
338
|
+
elementCount,
|
|
339
|
+
boundingBox: {
|
|
340
|
+
x: boundingBox.x,
|
|
341
|
+
y: boundingBox.y,
|
|
342
|
+
width: boundingBox.width,
|
|
343
|
+
height: boundingBox.height
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
let accessibility;
|
|
348
|
+
if (captureA11y) {
|
|
349
|
+
const violations = [];
|
|
350
|
+
const a11yInfo = await page.evaluate((sel) => {
|
|
351
|
+
const el = document.querySelector(sel);
|
|
352
|
+
if (!el) return { role: "generic", name: "" };
|
|
353
|
+
return {
|
|
354
|
+
role: el.getAttribute("role") ?? el.tagName.toLowerCase() ?? "generic",
|
|
355
|
+
name: el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 100) ?? ""
|
|
356
|
+
};
|
|
357
|
+
}, "[data-reactscope-root]");
|
|
358
|
+
const imgViolations = await page.evaluate((sel) => {
|
|
359
|
+
const container = document.querySelector(sel);
|
|
360
|
+
if (!container) return [];
|
|
361
|
+
const imgs = container.querySelectorAll("img");
|
|
362
|
+
const issues = [];
|
|
363
|
+
imgs.forEach((img) => {
|
|
364
|
+
if (!img.alt) issues.push("Image missing accessible name");
|
|
365
|
+
});
|
|
366
|
+
return issues;
|
|
367
|
+
}, "[data-reactscope-root]");
|
|
368
|
+
violations.push(...imgViolations);
|
|
369
|
+
accessibility = {
|
|
370
|
+
role: a11yInfo.role,
|
|
371
|
+
name: a11yInfo.name,
|
|
372
|
+
violations
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const consoleOutput = captureConsole ? { errors: [...consoleErrors], warnings: [...consoleWarnings], logs: [...consoleLogs] } : void 0;
|
|
376
|
+
return {
|
|
377
|
+
screenshot,
|
|
378
|
+
width: boundingBox.width,
|
|
379
|
+
height: boundingBox.height,
|
|
380
|
+
renderTimeMs,
|
|
381
|
+
computedStyles,
|
|
382
|
+
dom,
|
|
383
|
+
console: consoleOutput,
|
|
384
|
+
accessibility
|
|
385
|
+
};
|
|
386
|
+
} finally {
|
|
387
|
+
if (consoleHandler) page.off("console", consoleHandler);
|
|
388
|
+
this.release(slot);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// -------------------------------------------------------------------------
|
|
392
|
+
// Shutdown
|
|
393
|
+
// -------------------------------------------------------------------------
|
|
394
|
+
/**
|
|
395
|
+
* Gracefully shut down the pool.
|
|
396
|
+
*
|
|
397
|
+
* Waits for all in-use slots to be released, then closes every browser.
|
|
398
|
+
* Subsequent `acquire()` or `render()` calls will throw.
|
|
399
|
+
*/
|
|
400
|
+
async close() {
|
|
401
|
+
if (this.closed) return;
|
|
402
|
+
this.closed = true;
|
|
403
|
+
const drainPromises = this.slots.filter((s) => s.inUse).map(
|
|
404
|
+
(s) => new Promise((resolve) => {
|
|
405
|
+
const poll = setInterval(() => {
|
|
406
|
+
if (!s.inUse) {
|
|
407
|
+
clearInterval(poll);
|
|
408
|
+
resolve();
|
|
409
|
+
}
|
|
410
|
+
}, 50);
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
await Promise.all(drainPromises);
|
|
414
|
+
this.waiters = [];
|
|
415
|
+
await Promise.all(this.browsers.map((b) => b.close()));
|
|
416
|
+
this.browsers = [];
|
|
417
|
+
this.slots = [];
|
|
418
|
+
}
|
|
419
|
+
// -------------------------------------------------------------------------
|
|
420
|
+
// Introspection helpers
|
|
421
|
+
// -------------------------------------------------------------------------
|
|
422
|
+
/** Total number of page slots in the pool. */
|
|
423
|
+
get totalSlots() {
|
|
424
|
+
return this.slots.length;
|
|
425
|
+
}
|
|
426
|
+
/** Number of currently available (free) slots. */
|
|
427
|
+
get freeSlots() {
|
|
428
|
+
return this.slots.filter((s) => !s.inUse).length;
|
|
429
|
+
}
|
|
430
|
+
/** Number of currently in-use slots. */
|
|
431
|
+
get activeSlots() {
|
|
432
|
+
return this.slots.filter((s) => s.inUse).length;
|
|
433
|
+
}
|
|
434
|
+
/** Number of callers waiting for a free slot. */
|
|
435
|
+
get queueDepth() {
|
|
436
|
+
return this.waiters.length;
|
|
437
|
+
}
|
|
438
|
+
/** Whether the pool has been initialised. */
|
|
439
|
+
get isInitialized() {
|
|
440
|
+
return this.initialized;
|
|
441
|
+
}
|
|
442
|
+
/** Whether the pool is closed. */
|
|
443
|
+
get isClosed() {
|
|
444
|
+
return this.closed;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// src/contexts.ts
|
|
449
|
+
var COMPOSITION_CONTEXTS = {
|
|
450
|
+
centered: {
|
|
451
|
+
id: "centered",
|
|
452
|
+
label: "Centered",
|
|
453
|
+
description: "Component centered both horizontally and vertically inside the viewport.",
|
|
454
|
+
containerStyle: {
|
|
455
|
+
display: "flex",
|
|
456
|
+
alignItems: "center",
|
|
457
|
+
justifyContent: "center",
|
|
458
|
+
width: "100%",
|
|
459
|
+
height: "100%"
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
"flex-row": {
|
|
463
|
+
id: "flex-row",
|
|
464
|
+
label: "Flex Row",
|
|
465
|
+
description: "Component placed inside a flex row with placeholder siblings on either side.",
|
|
466
|
+
containerStyle: {
|
|
467
|
+
display: "flex",
|
|
468
|
+
flexDirection: "row",
|
|
469
|
+
alignItems: "flex-start",
|
|
470
|
+
gap: "8px",
|
|
471
|
+
width: "100%"
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
"flex-col": {
|
|
475
|
+
id: "flex-col",
|
|
476
|
+
label: "Flex Col",
|
|
477
|
+
description: "Component placed inside a flex column with placeholder siblings.",
|
|
478
|
+
containerStyle: {
|
|
479
|
+
display: "flex",
|
|
480
|
+
flexDirection: "column",
|
|
481
|
+
alignItems: "flex-start",
|
|
482
|
+
gap: "8px",
|
|
483
|
+
width: "100%"
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
grid: {
|
|
487
|
+
id: "grid",
|
|
488
|
+
label: "Grid",
|
|
489
|
+
description: "Component placed inside a CSS grid cell (3-column auto-fill).",
|
|
490
|
+
containerStyle: {
|
|
491
|
+
display: "grid",
|
|
492
|
+
gridTemplateColumns: "repeat(3, 1fr)",
|
|
493
|
+
gap: "8px",
|
|
494
|
+
width: "100%"
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
sidebar: {
|
|
498
|
+
id: "sidebar",
|
|
499
|
+
label: "Sidebar",
|
|
500
|
+
description: "Narrow 240 px sidebar layout \u2014 tests constrained-width rendering.",
|
|
501
|
+
containerStyle: {
|
|
502
|
+
display: "flex",
|
|
503
|
+
flexDirection: "column",
|
|
504
|
+
width: "240px",
|
|
505
|
+
minHeight: "100%",
|
|
506
|
+
padding: "8px",
|
|
507
|
+
overflow: "hidden"
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
scroll: {
|
|
511
|
+
id: "scroll",
|
|
512
|
+
label: "Scroll",
|
|
513
|
+
description: "Component inside a vertically scrollable container with fixed height.",
|
|
514
|
+
containerStyle: {
|
|
515
|
+
display: "block",
|
|
516
|
+
overflowY: "auto",
|
|
517
|
+
height: "300px",
|
|
518
|
+
width: "100%"
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
"full-width": {
|
|
522
|
+
id: "full-width",
|
|
523
|
+
label: "Full Width",
|
|
524
|
+
description: "Component stretched to full viewport width with no padding.",
|
|
525
|
+
containerStyle: {
|
|
526
|
+
display: "block",
|
|
527
|
+
width: "100%"
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
constrained: {
|
|
531
|
+
id: "constrained",
|
|
532
|
+
label: "Constrained",
|
|
533
|
+
description: "Component inside a centered max-width container (1024 px).",
|
|
534
|
+
containerStyle: {
|
|
535
|
+
display: "block",
|
|
536
|
+
maxWidth: "1024px",
|
|
537
|
+
width: "100%",
|
|
538
|
+
marginLeft: "auto",
|
|
539
|
+
marginRight: "auto",
|
|
540
|
+
padding: "0 16px"
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
rtl: {
|
|
544
|
+
id: "rtl",
|
|
545
|
+
label: "RTL",
|
|
546
|
+
description: "Right-to-left text direction \u2014 tests bidi layout behaviour.",
|
|
547
|
+
containerStyle: {
|
|
548
|
+
direction: "rtl",
|
|
549
|
+
display: "flex",
|
|
550
|
+
flexDirection: "row",
|
|
551
|
+
width: "100%"
|
|
552
|
+
},
|
|
553
|
+
containerAttrs: {
|
|
554
|
+
dir: "rtl",
|
|
555
|
+
lang: "ar"
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
"nested-flex": {
|
|
559
|
+
id: "nested-flex",
|
|
560
|
+
label: "Nested Flex",
|
|
561
|
+
description: "Component inside three levels of nested flex containers.",
|
|
562
|
+
containerStyle: {
|
|
563
|
+
display: "flex",
|
|
564
|
+
flexDirection: "row",
|
|
565
|
+
width: "100%"
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
var ALL_CONTEXTS = Object.values(COMPOSITION_CONTEXTS);
|
|
570
|
+
var ALL_CONTEXT_IDS = Object.keys(
|
|
571
|
+
COMPOSITION_CONTEXTS
|
|
572
|
+
);
|
|
573
|
+
function getContext(id) {
|
|
574
|
+
const ctx = COMPOSITION_CONTEXTS[id];
|
|
575
|
+
if (!ctx) throw new Error(`Unknown composition context: "${id}"`);
|
|
576
|
+
return ctx;
|
|
577
|
+
}
|
|
578
|
+
function getContexts(ids) {
|
|
579
|
+
return ids.map((id) => getContext(id));
|
|
580
|
+
}
|
|
581
|
+
function contextAxis(ids = ALL_CONTEXT_IDS) {
|
|
582
|
+
return {
|
|
583
|
+
name: "context",
|
|
584
|
+
values: ids.map((id) => getContext(id))
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/errors.ts
|
|
589
|
+
var RenderError = class _RenderError extends Error {
|
|
590
|
+
componentStack;
|
|
591
|
+
sourceLocation;
|
|
592
|
+
propsAtCrash;
|
|
593
|
+
errorMessage;
|
|
594
|
+
errorType;
|
|
595
|
+
heuristicFlags;
|
|
596
|
+
constructor(fields) {
|
|
597
|
+
super(fields.errorMessage);
|
|
598
|
+
this.name = "RenderError";
|
|
599
|
+
this.componentStack = fields.componentStack;
|
|
600
|
+
this.sourceLocation = fields.sourceLocation;
|
|
601
|
+
this.propsAtCrash = fields.propsAtCrash;
|
|
602
|
+
this.errorMessage = fields.errorMessage;
|
|
603
|
+
this.errorType = fields.errorType;
|
|
604
|
+
this.heuristicFlags = fields.heuristicFlags;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Construct a `RenderError` from an arbitrary caught value.
|
|
608
|
+
*
|
|
609
|
+
* Extracts `componentStack` from React error boundaries when present,
|
|
610
|
+
* and runs heuristic analysis on the error message.
|
|
611
|
+
*/
|
|
612
|
+
static from(caught, context = {}) {
|
|
613
|
+
const err = caught instanceof Error ? caught : new Error(String(caught));
|
|
614
|
+
const errorMessage = err.message;
|
|
615
|
+
const errorType = err.constructor.name !== "Error" ? err.constructor.name : "Error";
|
|
616
|
+
const componentStack = err.componentStack ?? err.stack ?? "";
|
|
617
|
+
const sourceLocation = context.sourceLocation ?? {
|
|
618
|
+
file: "<unknown>",
|
|
619
|
+
line: 0,
|
|
620
|
+
column: 0
|
|
621
|
+
};
|
|
622
|
+
const propsAtCrash = context.props ?? {};
|
|
623
|
+
const heuristicFlags = detectHeuristicFlags(errorMessage, componentStack);
|
|
624
|
+
return new _RenderError({
|
|
625
|
+
componentStack,
|
|
626
|
+
sourceLocation,
|
|
627
|
+
propsAtCrash,
|
|
628
|
+
errorMessage,
|
|
629
|
+
errorType,
|
|
630
|
+
heuristicFlags
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
/** Serialise to a plain object (useful for JSON output). */
|
|
634
|
+
toJSON() {
|
|
635
|
+
return {
|
|
636
|
+
name: this.name,
|
|
637
|
+
componentStack: this.componentStack,
|
|
638
|
+
sourceLocation: this.sourceLocation,
|
|
639
|
+
propsAtCrash: this.propsAtCrash,
|
|
640
|
+
errorMessage: this.errorMessage,
|
|
641
|
+
errorType: this.errorType,
|
|
642
|
+
heuristicFlags: this.heuristicFlags
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
function detectHeuristicFlags(errorMessage, componentStack) {
|
|
647
|
+
const flags = /* @__PURE__ */ new Set();
|
|
648
|
+
const msg = errorMessage.toLowerCase();
|
|
649
|
+
const stack = componentStack.toLowerCase();
|
|
650
|
+
const combined = msg + " " + stack;
|
|
651
|
+
if (combined.includes("must be used within") || combined.includes("no provider") || combined.includes("provider") && combined.includes("context") || combined.includes("context") && combined.includes("undefined") || combined.includes("provider") && combined.includes("undefined")) {
|
|
652
|
+
flags.add("MISSING_PROVIDER");
|
|
653
|
+
}
|
|
654
|
+
if (combined.includes("cannot read propert") || combined.includes("undefined is not an object") || combined.includes("undefined") && (combined.includes("prop") || combined.includes("null"))) {
|
|
655
|
+
flags.add("UNDEFINED_PROP");
|
|
656
|
+
}
|
|
657
|
+
if (msg.includes("typeerror") || combined.includes("is not a function") || combined.includes("is not iterable") || combined.includes("cannot set propert")) {
|
|
658
|
+
flags.add("TYPE_MISMATCH");
|
|
659
|
+
}
|
|
660
|
+
if (combined.includes("fetch") || combined.includes("network") || combined.includes("http") || combined.includes("xhr")) {
|
|
661
|
+
flags.add("NETWORK_REQUIRED");
|
|
662
|
+
}
|
|
663
|
+
if (combined.includes("suspend") || combined.includes("promise")) {
|
|
664
|
+
flags.add("ASYNC_NOT_SUSPENDED");
|
|
665
|
+
}
|
|
666
|
+
if (combined.includes("invalid hook call") || combined.includes("hooks can only be called") || combined.includes("rendered more hooks") || combined.includes("rendered fewer hooks")) {
|
|
667
|
+
flags.add("HOOK_CALL_VIOLATION");
|
|
668
|
+
}
|
|
669
|
+
if (combined.includes("element type is invalid") || combined.includes("react.createelement") || combined.includes("expected a string") && combined.includes("class/function")) {
|
|
670
|
+
flags.add("ELEMENT_TYPE_INVALID");
|
|
671
|
+
}
|
|
672
|
+
if (combined.includes("hydrat") || combined.includes("did not match") || combined.includes("server rendered html")) {
|
|
673
|
+
flags.add("HYDRATION_MISMATCH");
|
|
674
|
+
}
|
|
675
|
+
if (combined.includes("circular") || combined.includes("maximum call stack") || combined.includes("stack overflow")) {
|
|
676
|
+
flags.add("CIRCULAR_DEPENDENCY");
|
|
677
|
+
}
|
|
678
|
+
if (flags.size === 0) {
|
|
679
|
+
flags.add("UNKNOWN_ERROR");
|
|
680
|
+
}
|
|
681
|
+
return Array.from(flags);
|
|
682
|
+
}
|
|
683
|
+
async function safeRender(renderFn, context = {}) {
|
|
684
|
+
const start = performance.now();
|
|
685
|
+
try {
|
|
686
|
+
const result = await renderFn();
|
|
687
|
+
return { crashed: false, result };
|
|
688
|
+
} catch (err) {
|
|
689
|
+
const renderTimeMs = performance.now() - start;
|
|
690
|
+
const renderError = RenderError.from(err, context);
|
|
691
|
+
return { crashed: true, error: renderError, renderTimeMs };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
var fontCache = /* @__PURE__ */ new Map();
|
|
695
|
+
var NO_FONT_SENTINEL = "__none__";
|
|
696
|
+
var DEFAULT_FAMILY = "Inter";
|
|
697
|
+
function resolveBundledFontPath() {
|
|
698
|
+
const fontFile = join("@fontsource", "inter", "files", "inter-latin-400-normal.woff");
|
|
699
|
+
const roots = [
|
|
700
|
+
join("node_modules"),
|
|
701
|
+
join(__dirname, "..", "node_modules"),
|
|
702
|
+
join(__dirname, "..", "..", "node_modules"),
|
|
703
|
+
join(__dirname, "..", "..", "..", "node_modules")
|
|
704
|
+
];
|
|
705
|
+
for (const root of roots) {
|
|
706
|
+
const candidate = join(root, fontFile);
|
|
707
|
+
if (existsSync(candidate)) return candidate;
|
|
708
|
+
}
|
|
709
|
+
const bunCacheRoot = join("node_modules", ".bun");
|
|
710
|
+
if (existsSync(bunCacheRoot)) {
|
|
711
|
+
try {
|
|
712
|
+
const entries = readdirSync(bunCacheRoot);
|
|
713
|
+
for (const entry of entries) {
|
|
714
|
+
if (entry.startsWith("@fontsource+inter@")) {
|
|
715
|
+
const candidate = join(bunCacheRoot, entry, "node_modules", fontFile);
|
|
716
|
+
if (existsSync(candidate)) return candidate;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
async function loadFont(fontPath, fontFamily = DEFAULT_FAMILY) {
|
|
725
|
+
const resolvedPath = fontPath ?? resolveBundledFontPath() ?? NO_FONT_SENTINEL;
|
|
726
|
+
const cached = fontCache.get(resolvedPath);
|
|
727
|
+
if (cached !== void 0) return cached;
|
|
728
|
+
if (resolvedPath === NO_FONT_SENTINEL) {
|
|
729
|
+
const empty = [];
|
|
730
|
+
fontCache.set(NO_FONT_SENTINEL, empty);
|
|
731
|
+
return empty;
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
const buffer = await readFile(resolvedPath);
|
|
735
|
+
const arrayBuffer = buffer.buffer.slice(
|
|
736
|
+
buffer.byteOffset,
|
|
737
|
+
buffer.byteOffset + buffer.byteLength
|
|
738
|
+
);
|
|
739
|
+
const entries = [
|
|
740
|
+
{ name: fontFamily, data: arrayBuffer, weight: 400, style: "normal" },
|
|
741
|
+
{ name: fontFamily, data: arrayBuffer, weight: 700, style: "normal" }
|
|
742
|
+
];
|
|
743
|
+
fontCache.set(resolvedPath, entries);
|
|
744
|
+
return entries;
|
|
745
|
+
} catch {
|
|
746
|
+
const empty = [];
|
|
747
|
+
fontCache.set(resolvedPath, empty);
|
|
748
|
+
return empty;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function clearFontCache() {
|
|
752
|
+
fontCache.clear();
|
|
753
|
+
}
|
|
754
|
+
function fontCacheSize() {
|
|
755
|
+
return fontCache.size;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/matrix.ts
|
|
759
|
+
function cartesianProduct(axes) {
|
|
760
|
+
if (axes.length === 0) return [[]];
|
|
761
|
+
const result = [];
|
|
762
|
+
function recurse(axisIndex, current) {
|
|
763
|
+
if (axisIndex === axes.length) {
|
|
764
|
+
result.push([...current]);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const axis = axes[axisIndex];
|
|
768
|
+
if (!axis) return;
|
|
769
|
+
for (const value of axis) {
|
|
770
|
+
current.push(value);
|
|
771
|
+
recurse(axisIndex + 1, current);
|
|
772
|
+
current.pop();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
recurse(0, []);
|
|
776
|
+
return result;
|
|
777
|
+
}
|
|
778
|
+
function labelForValue(value) {
|
|
779
|
+
if (value === null) return "null";
|
|
780
|
+
if (value === void 0) return "undefined";
|
|
781
|
+
if (typeof value === "string") return value;
|
|
782
|
+
if (typeof value === "number") {
|
|
783
|
+
if (Number.isNaN(value)) return "NaN";
|
|
784
|
+
if (!Number.isFinite(value)) return value > 0 ? "+Infinity" : "-Infinity";
|
|
785
|
+
return String(value);
|
|
786
|
+
}
|
|
787
|
+
if (typeof value === "boolean") return String(value);
|
|
788
|
+
return JSON.stringify(value);
|
|
789
|
+
}
|
|
790
|
+
var RenderMatrix = class {
|
|
791
|
+
renderer;
|
|
792
|
+
axes;
|
|
793
|
+
complexityClass;
|
|
794
|
+
/** Maximum number of cells to render in parallel. @default 8 */
|
|
795
|
+
concurrency;
|
|
796
|
+
constructor(renderer, axes, options = {}) {
|
|
797
|
+
if (axes.length === 0) {
|
|
798
|
+
throw new Error("RenderMatrix requires at least one axis");
|
|
799
|
+
}
|
|
800
|
+
for (const axis of axes) {
|
|
801
|
+
if (axis.values.length === 0) {
|
|
802
|
+
throw new Error(`Axis "${axis.name}" must have at least one value`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
this.renderer = renderer;
|
|
806
|
+
this.axes = axes;
|
|
807
|
+
this.complexityClass = options.complexityClass ?? "simple";
|
|
808
|
+
this.concurrency = options.concurrency ?? 8;
|
|
809
|
+
}
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
// Grid dimensions
|
|
812
|
+
// ---------------------------------------------------------------------------
|
|
813
|
+
/** Total number of cells (product of all axis cardinalities). */
|
|
814
|
+
get cellCount() {
|
|
815
|
+
return this.axes.reduce((acc, axis) => acc * axis.values.length, 1);
|
|
816
|
+
}
|
|
817
|
+
/** Axis labels indexed [axisIndex][valueIndex]. */
|
|
818
|
+
get axisLabels() {
|
|
819
|
+
return this.axes.map((axis) => axis.values.map((v) => labelForValue(v)));
|
|
820
|
+
}
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
// Main render
|
|
823
|
+
// ---------------------------------------------------------------------------
|
|
824
|
+
/**
|
|
825
|
+
* Render all cells of the matrix.
|
|
826
|
+
*
|
|
827
|
+
* Cells are dispatched with bounded parallelism (up to `concurrency` at once)
|
|
828
|
+
* and returned in row-major order (first axis iterates slowest).
|
|
829
|
+
*/
|
|
830
|
+
async render() {
|
|
831
|
+
const wallStart = performance.now();
|
|
832
|
+
const axisValueArrays = this.axes.map((a) => a.values);
|
|
833
|
+
const combinations = cartesianProduct(axisValueArrays);
|
|
834
|
+
const cells = new Array(combinations.length);
|
|
835
|
+
await this._renderWithConcurrency(combinations, cells);
|
|
836
|
+
const wallEnd = performance.now();
|
|
837
|
+
const stats = this._computeStats(cells, wallEnd - wallStart);
|
|
838
|
+
const lastAxis = this.axes[this.axes.length - 1];
|
|
839
|
+
const rows = lastAxis !== void 0 ? lastAxis.values.length : 1;
|
|
840
|
+
const cols = this.cellCount / rows;
|
|
841
|
+
return {
|
|
842
|
+
cells,
|
|
843
|
+
axes: this.axes,
|
|
844
|
+
axisLabels: this.axisLabels,
|
|
845
|
+
stats,
|
|
846
|
+
rows,
|
|
847
|
+
cols
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
// ---------------------------------------------------------------------------
|
|
851
|
+
// Private helpers
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
async _renderWithConcurrency(combinations, cells) {
|
|
854
|
+
let nextIndex = 0;
|
|
855
|
+
const worker = async () => {
|
|
856
|
+
while (nextIndex < combinations.length) {
|
|
857
|
+
const i = nextIndex++;
|
|
858
|
+
const combo = combinations[i];
|
|
859
|
+
if (!combo) continue;
|
|
860
|
+
const props = {};
|
|
861
|
+
for (let axisIdx = 0; axisIdx < this.axes.length; axisIdx++) {
|
|
862
|
+
const axis = this.axes[axisIdx];
|
|
863
|
+
if (axis) {
|
|
864
|
+
props[axis.name] = combo[axisIdx];
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const axisIndices = [];
|
|
868
|
+
for (let axisIdx = 0; axisIdx < this.axes.length; axisIdx++) {
|
|
869
|
+
const axis = this.axes[axisIdx];
|
|
870
|
+
const val = combo[axisIdx];
|
|
871
|
+
axisIndices.push(axis ? axis.values.indexOf(val) : -1);
|
|
872
|
+
}
|
|
873
|
+
const result = await this.renderer.renderCell(props, this.complexityClass);
|
|
874
|
+
cells[i] = { props, result, index: i, axisIndices };
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
const workerCount = Math.min(this.concurrency, combinations.length);
|
|
878
|
+
const workers = [];
|
|
879
|
+
for (let w = 0; w < workerCount; w++) {
|
|
880
|
+
workers.push(worker());
|
|
881
|
+
}
|
|
882
|
+
await Promise.all(workers);
|
|
883
|
+
}
|
|
884
|
+
_computeStats(cells, wallClockTimeMs) {
|
|
885
|
+
const totalCells = cells.length;
|
|
886
|
+
if (totalCells === 0) {
|
|
887
|
+
return {
|
|
888
|
+
totalCells: 0,
|
|
889
|
+
totalRenderTimeMs: 0,
|
|
890
|
+
avgRenderTimeMs: 0,
|
|
891
|
+
minRenderTimeMs: 0,
|
|
892
|
+
maxRenderTimeMs: 0,
|
|
893
|
+
wallClockTimeMs
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
let total = 0;
|
|
897
|
+
let min = Infinity;
|
|
898
|
+
let max = -Infinity;
|
|
899
|
+
for (const cell of cells) {
|
|
900
|
+
const t = cell.result.renderTimeMs;
|
|
901
|
+
total += t;
|
|
902
|
+
if (t < min) min = t;
|
|
903
|
+
if (t > max) max = t;
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
totalCells,
|
|
907
|
+
totalRenderTimeMs: total,
|
|
908
|
+
avgRenderTimeMs: total / totalCells,
|
|
909
|
+
minRenderTimeMs: min === Infinity ? 0 : min,
|
|
910
|
+
maxRenderTimeMs: max === -Infinity ? 0 : max,
|
|
911
|
+
wallClockTimeMs
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
var MOCK_CONTEXT_VALUE = Object.freeze({});
|
|
916
|
+
var KNOWN_PROVIDERS = {
|
|
917
|
+
ThemeContext: (children, theme = "light") => React.createElement(
|
|
918
|
+
React.createContext({ theme }).Provider,
|
|
919
|
+
{ value: { theme } },
|
|
920
|
+
children
|
|
921
|
+
),
|
|
922
|
+
LocaleContext: (children, _theme, locale = "en-US") => React.createElement(
|
|
923
|
+
React.createContext({ locale }).Provider,
|
|
924
|
+
{ value: { locale } },
|
|
925
|
+
children
|
|
926
|
+
)
|
|
927
|
+
};
|
|
928
|
+
function wrapWithProviders(element, requiredContexts, options = {}) {
|
|
929
|
+
if (requiredContexts.length === 0) return element;
|
|
930
|
+
return [...requiredContexts].reverse().reduce((child, ctxName) => {
|
|
931
|
+
const factory = KNOWN_PROVIDERS[ctxName];
|
|
932
|
+
if (factory) {
|
|
933
|
+
return factory(child, options.theme, options.locale);
|
|
934
|
+
}
|
|
935
|
+
const GenericCtx = React.createContext(MOCK_CONTEXT_VALUE);
|
|
936
|
+
return React.createElement(GenericCtx.Provider, { value: MOCK_CONTEXT_VALUE }, child);
|
|
937
|
+
}, element);
|
|
938
|
+
}
|
|
939
|
+
function extractInlineStyles(element, path = "root") {
|
|
940
|
+
const result = {};
|
|
941
|
+
if (!React.isValidElement(element)) return result;
|
|
942
|
+
const props = element.props;
|
|
943
|
+
if (props.style && typeof props.style === "object" && props.style !== null) {
|
|
944
|
+
const style = props.style;
|
|
945
|
+
const stringified = {};
|
|
946
|
+
for (const [k, v] of Object.entries(style)) {
|
|
947
|
+
if (v !== void 0 && v !== null) {
|
|
948
|
+
stringified[k] = String(v);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (Object.keys(stringified).length > 0) {
|
|
952
|
+
result[path] = stringified;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const children = props.children;
|
|
956
|
+
if (children === void 0 || children === null) return result;
|
|
957
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
958
|
+
childArray.forEach((child, idx) => {
|
|
959
|
+
const childPath = `${path}.${idx}`;
|
|
960
|
+
const childStyles = extractInlineStyles(child, childPath);
|
|
961
|
+
Object.assign(result, childStyles);
|
|
962
|
+
});
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/satori.ts
|
|
967
|
+
var DEFAULT_VIEWPORT = { width: 375, height: 812 };
|
|
968
|
+
var DEFAULT_CONTAINER = {
|
|
969
|
+
type: "centered",
|
|
970
|
+
padding: 16,
|
|
971
|
+
background: "#ffffff"
|
|
972
|
+
};
|
|
973
|
+
var DEFAULT_CAPTURE = {
|
|
974
|
+
screenshot: true,
|
|
975
|
+
styles: true,
|
|
976
|
+
timing: true
|
|
977
|
+
};
|
|
978
|
+
function buildContainerStyle(viewport, container) {
|
|
979
|
+
const base = {
|
|
980
|
+
width: viewport.width,
|
|
981
|
+
height: viewport.height,
|
|
982
|
+
padding: container.padding,
|
|
983
|
+
backgroundColor: container.background ?? "#ffffff",
|
|
984
|
+
display: "flex",
|
|
985
|
+
overflow: "hidden"
|
|
986
|
+
};
|
|
987
|
+
const typeStyles = {
|
|
988
|
+
centered: {
|
|
989
|
+
alignItems: "center",
|
|
990
|
+
justifyContent: "center",
|
|
991
|
+
flexDirection: "column"
|
|
992
|
+
},
|
|
993
|
+
"flex-row": {
|
|
994
|
+
alignItems: "flex-start",
|
|
995
|
+
justifyContent: "flex-start",
|
|
996
|
+
flexDirection: "row"
|
|
997
|
+
},
|
|
998
|
+
"flex-col": {
|
|
999
|
+
alignItems: "flex-start",
|
|
1000
|
+
justifyContent: "flex-start",
|
|
1001
|
+
flexDirection: "column"
|
|
1002
|
+
},
|
|
1003
|
+
none: {}
|
|
1004
|
+
};
|
|
1005
|
+
return { ...base, ...typeStyles[container.type] ?? {} };
|
|
1006
|
+
}
|
|
1007
|
+
var SatoriRenderer = class {
|
|
1008
|
+
config;
|
|
1009
|
+
fontsPromise = null;
|
|
1010
|
+
constructor(config = {}) {
|
|
1011
|
+
this.config = {
|
|
1012
|
+
fontPath: config.fontPath ?? "",
|
|
1013
|
+
fontFamily: config.fontFamily ?? "Inter",
|
|
1014
|
+
defaultViewport: config.defaultViewport ?? DEFAULT_VIEWPORT
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
// ---------------------------------------------------------------------------
|
|
1018
|
+
// Font loading (lazy, cached)
|
|
1019
|
+
// ---------------------------------------------------------------------------
|
|
1020
|
+
/**
|
|
1021
|
+
* Pre-load and cache the configured font. Calling this before `render()`
|
|
1022
|
+
* ensures the first render isn't slowed by font I/O.
|
|
1023
|
+
*/
|
|
1024
|
+
async preloadFont() {
|
|
1025
|
+
await this._getFonts();
|
|
1026
|
+
}
|
|
1027
|
+
_getFonts() {
|
|
1028
|
+
if (!this.fontsPromise) {
|
|
1029
|
+
const fontPath = this.config.fontPath || void 0;
|
|
1030
|
+
this.fontsPromise = loadFont(fontPath, this.config.fontFamily);
|
|
1031
|
+
}
|
|
1032
|
+
return this.fontsPromise;
|
|
1033
|
+
}
|
|
1034
|
+
// ---------------------------------------------------------------------------
|
|
1035
|
+
// Core render
|
|
1036
|
+
// ---------------------------------------------------------------------------
|
|
1037
|
+
/**
|
|
1038
|
+
* Render a React element tree to PNG via Satori (SVG) → resvg-js (PNG).
|
|
1039
|
+
*
|
|
1040
|
+
* @param element - The React element to render (pre-built, not a component class).
|
|
1041
|
+
* @param options - Viewport, container, capture, and environment overrides.
|
|
1042
|
+
* @param descriptor - Optional ComponentDescriptor for provider wrapping.
|
|
1043
|
+
* When provided, `requiredContexts` are used to inject
|
|
1044
|
+
* mock providers around the element.
|
|
1045
|
+
*/
|
|
1046
|
+
async render(element, options = {}, descriptor) {
|
|
1047
|
+
const startMs = performance.now();
|
|
1048
|
+
const viewport = {
|
|
1049
|
+
...this.config.defaultViewport,
|
|
1050
|
+
...options.environment?.viewport,
|
|
1051
|
+
...options.viewport
|
|
1052
|
+
};
|
|
1053
|
+
const container = {
|
|
1054
|
+
...DEFAULT_CONTAINER,
|
|
1055
|
+
...options.container
|
|
1056
|
+
};
|
|
1057
|
+
const capture = {
|
|
1058
|
+
...DEFAULT_CAPTURE,
|
|
1059
|
+
...options.capture
|
|
1060
|
+
};
|
|
1061
|
+
const requiredContexts = descriptor?.requiredContexts ?? [];
|
|
1062
|
+
const wrappedElement = wrapWithProviders(element, requiredContexts, {
|
|
1063
|
+
theme: options.environment?.theme,
|
|
1064
|
+
locale: options.environment?.locale
|
|
1065
|
+
});
|
|
1066
|
+
const computedStyles = capture.styles ? extractInlineStyles(wrappedElement) : {};
|
|
1067
|
+
const containerStyle = buildContainerStyle(viewport, container);
|
|
1068
|
+
const rootElement = React.createElement("div", { style: containerStyle }, wrappedElement);
|
|
1069
|
+
const fonts = await this._getFonts();
|
|
1070
|
+
const svg = await satori(rootElement, {
|
|
1071
|
+
width: viewport.width,
|
|
1072
|
+
height: viewport.height,
|
|
1073
|
+
fonts: fonts.map((f) => ({
|
|
1074
|
+
name: f.name,
|
|
1075
|
+
data: f.data,
|
|
1076
|
+
weight: f.weight,
|
|
1077
|
+
style: f.style
|
|
1078
|
+
}))
|
|
1079
|
+
});
|
|
1080
|
+
let screenshot = Buffer.alloc(0);
|
|
1081
|
+
if (capture.screenshot) {
|
|
1082
|
+
const resvg = new Resvg(svg, {
|
|
1083
|
+
fitTo: { mode: "width", value: viewport.width }
|
|
1084
|
+
});
|
|
1085
|
+
const rendered = resvg.render();
|
|
1086
|
+
screenshot = Buffer.from(rendered.asPng());
|
|
1087
|
+
}
|
|
1088
|
+
const renderTimeMs = capture.timing ? performance.now() - startMs : 0;
|
|
1089
|
+
return {
|
|
1090
|
+
screenshot,
|
|
1091
|
+
width: viewport.width,
|
|
1092
|
+
height: viewport.height,
|
|
1093
|
+
renderTimeMs,
|
|
1094
|
+
computedStyles
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
// ---------------------------------------------------------------------------
|
|
1098
|
+
// Convenience: render with explicit viewport
|
|
1099
|
+
// ---------------------------------------------------------------------------
|
|
1100
|
+
/**
|
|
1101
|
+
* Render at a specific viewport size. Shorthand for `render(el, { viewport })`.
|
|
1102
|
+
*/
|
|
1103
|
+
async renderAt(element, width, height, descriptor) {
|
|
1104
|
+
return this.render(element, { viewport: { width, height } }, descriptor);
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
function resolveOptions(opts) {
|
|
1108
|
+
return {
|
|
1109
|
+
cellPadding: opts.cellPadding ?? 8,
|
|
1110
|
+
borderWidth: opts.borderWidth ?? 1,
|
|
1111
|
+
background: opts.background ?? "#f5f5f5",
|
|
1112
|
+
borderColor: opts.borderColor ?? "#cccccc",
|
|
1113
|
+
labelHeight: opts.labelHeight ?? 32,
|
|
1114
|
+
labelWidth: opts.labelWidth ?? 80,
|
|
1115
|
+
labelDpi: opts.labelDpi ?? 72,
|
|
1116
|
+
labelBackground: opts.labelBackground ?? "#e8e8e8"
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function hexToRgb(hex) {
|
|
1120
|
+
const cleaned = hex.replace(/^#/, "");
|
|
1121
|
+
const full = cleaned.length === 3 ? cleaned.split("").map((c) => c + c).join("") : cleaned;
|
|
1122
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
1123
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
1124
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
1125
|
+
return {
|
|
1126
|
+
r: Number.isNaN(r) ? 0 : r,
|
|
1127
|
+
g: Number.isNaN(g) ? 0 : g,
|
|
1128
|
+
b: Number.isNaN(b) ? 0 : b,
|
|
1129
|
+
alpha: 1
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
async function makeRect(width, height, color) {
|
|
1133
|
+
const bg = hexToRgb(color);
|
|
1134
|
+
return sharp({
|
|
1135
|
+
create: { width: Math.max(1, width), height: Math.max(1, height), channels: 4, background: bg }
|
|
1136
|
+
}).png().toBuffer();
|
|
1137
|
+
}
|
|
1138
|
+
async function makeLabelPng(text, width, height, dpi, bgColor) {
|
|
1139
|
+
const w = Math.max(1, width);
|
|
1140
|
+
const h = Math.max(1, height);
|
|
1141
|
+
const bg = await makeRect(w, h, bgColor);
|
|
1142
|
+
const maxChars = Math.max(1, Math.floor(w / 7));
|
|
1143
|
+
const displayText = text.length > maxChars ? `${text.slice(0, maxChars - 1)}\u2026` : text;
|
|
1144
|
+
let textBuf = null;
|
|
1145
|
+
try {
|
|
1146
|
+
textBuf = await sharp({
|
|
1147
|
+
text: {
|
|
1148
|
+
text: displayText,
|
|
1149
|
+
font: "Sans",
|
|
1150
|
+
width: w - 4,
|
|
1151
|
+
height: h - 4,
|
|
1152
|
+
dpi,
|
|
1153
|
+
rgba: true,
|
|
1154
|
+
align: "centre"
|
|
1155
|
+
}
|
|
1156
|
+
}).png().toBuffer();
|
|
1157
|
+
} catch {
|
|
1158
|
+
return bg;
|
|
1159
|
+
}
|
|
1160
|
+
const textMeta = await sharp(textBuf).metadata();
|
|
1161
|
+
const textW = textMeta.width ?? 0;
|
|
1162
|
+
const textH = textMeta.height ?? 0;
|
|
1163
|
+
const left = Math.max(0, Math.floor((w - textW) / 2));
|
|
1164
|
+
const top = Math.max(0, Math.floor((h - textH) / 2));
|
|
1165
|
+
return sharp(bg).composite([{ input: textBuf, top, left, blend: "over" }]).png().toBuffer();
|
|
1166
|
+
}
|
|
1167
|
+
var SpriteSheetGenerator = class {
|
|
1168
|
+
opts;
|
|
1169
|
+
constructor(options = {}) {
|
|
1170
|
+
this.opts = resolveOptions(options);
|
|
1171
|
+
}
|
|
1172
|
+
// ---------------------------------------------------------------------------
|
|
1173
|
+
// Public: generate
|
|
1174
|
+
// ---------------------------------------------------------------------------
|
|
1175
|
+
/**
|
|
1176
|
+
* Generate a sprite sheet from a `MatrixResult`.
|
|
1177
|
+
*
|
|
1178
|
+
* Grid layout:
|
|
1179
|
+
* - Rows = last axis cardinality
|
|
1180
|
+
* - Cols = product of all other axis cardinalities (or 1 if single axis)
|
|
1181
|
+
*
|
|
1182
|
+
* @param matrixResult - The result of `RenderMatrix.render()`.
|
|
1183
|
+
*/
|
|
1184
|
+
async generate(matrixResult) {
|
|
1185
|
+
const { cells, rows, cols, axisLabels } = matrixResult;
|
|
1186
|
+
if (cells.length === 0) {
|
|
1187
|
+
throw new Error("Cannot generate sprite sheet from empty MatrixResult");
|
|
1188
|
+
}
|
|
1189
|
+
const opts = this.opts;
|
|
1190
|
+
const firstCell = cells[0];
|
|
1191
|
+
if (!firstCell) {
|
|
1192
|
+
throw new Error("Cannot generate sprite sheet from empty MatrixResult");
|
|
1193
|
+
}
|
|
1194
|
+
const cellContentWidth = firstCell.result.width;
|
|
1195
|
+
const cellContentHeight = firstCell.result.height;
|
|
1196
|
+
const slotWidth = cellContentWidth + opts.cellPadding * 2 + opts.borderWidth;
|
|
1197
|
+
const slotHeight = cellContentHeight + opts.cellPadding * 2 + opts.borderWidth;
|
|
1198
|
+
const gridWidth = cols * slotWidth + opts.borderWidth;
|
|
1199
|
+
const gridHeight = rows * slotHeight + opts.borderWidth;
|
|
1200
|
+
const totalWidth = opts.labelWidth + gridWidth;
|
|
1201
|
+
const totalHeight = opts.labelHeight + gridHeight;
|
|
1202
|
+
const compositeOps = [];
|
|
1203
|
+
const topLabelBg = await makeRect(totalWidth, opts.labelHeight, opts.labelBackground);
|
|
1204
|
+
compositeOps.push({ input: topLabelBg, top: 0, left: 0 });
|
|
1205
|
+
const leftLabelBg = await makeRect(opts.labelWidth, totalHeight, opts.labelBackground);
|
|
1206
|
+
compositeOps.push({ input: leftLabelBg, top: 0, left: 0 });
|
|
1207
|
+
for (let c = 0; c <= cols; c++) {
|
|
1208
|
+
const x = opts.labelWidth + c * slotWidth;
|
|
1209
|
+
const borderLine = await makeRect(opts.borderWidth, gridHeight, opts.borderColor);
|
|
1210
|
+
compositeOps.push({ input: borderLine, top: opts.labelHeight, left: x });
|
|
1211
|
+
}
|
|
1212
|
+
for (let r = 0; r <= rows; r++) {
|
|
1213
|
+
const y = opts.labelHeight + r * slotHeight;
|
|
1214
|
+
const borderLine = await makeRect(gridWidth, opts.borderWidth, opts.borderColor);
|
|
1215
|
+
compositeOps.push({ input: borderLine, top: y, left: opts.labelWidth });
|
|
1216
|
+
}
|
|
1217
|
+
const vSep = await makeRect(opts.borderWidth, totalHeight, opts.borderColor);
|
|
1218
|
+
compositeOps.push({
|
|
1219
|
+
input: vSep,
|
|
1220
|
+
top: 0,
|
|
1221
|
+
left: Math.max(0, opts.labelWidth - opts.borderWidth)
|
|
1222
|
+
});
|
|
1223
|
+
const hSep = await makeRect(totalWidth, opts.borderWidth, opts.borderColor);
|
|
1224
|
+
compositeOps.push({
|
|
1225
|
+
input: hSep,
|
|
1226
|
+
top: Math.max(0, opts.labelHeight - opts.borderWidth),
|
|
1227
|
+
left: 0
|
|
1228
|
+
});
|
|
1229
|
+
const axisCount = matrixResult.axes.length;
|
|
1230
|
+
const colLabels = [];
|
|
1231
|
+
for (let c = 0; c < cols; c++) {
|
|
1232
|
+
if (axisCount === 1) {
|
|
1233
|
+
colLabels.push(axisLabels[0]?.[c] ?? String(c));
|
|
1234
|
+
} else {
|
|
1235
|
+
const cellAtCol = cells[c * rows];
|
|
1236
|
+
if (cellAtCol) {
|
|
1237
|
+
const parts = [];
|
|
1238
|
+
for (let ai = 0; ai < axisCount - 1; ai++) {
|
|
1239
|
+
const axis = matrixResult.axes[ai];
|
|
1240
|
+
if (axis) {
|
|
1241
|
+
parts.push(`${axis.name}=${axisLabels[ai]?.[cellAtCol.axisIndices[ai] ?? 0] ?? "?"}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
colLabels.push(parts.join(", "));
|
|
1245
|
+
} else {
|
|
1246
|
+
colLabels.push(String(c));
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
for (let c = 0; c < cols; c++) {
|
|
1251
|
+
const labelBuf = await makeLabelPng(
|
|
1252
|
+
colLabels[c] ?? String(c),
|
|
1253
|
+
slotWidth,
|
|
1254
|
+
opts.labelHeight,
|
|
1255
|
+
opts.labelDpi,
|
|
1256
|
+
opts.labelBackground
|
|
1257
|
+
);
|
|
1258
|
+
compositeOps.push({
|
|
1259
|
+
input: labelBuf,
|
|
1260
|
+
top: 0,
|
|
1261
|
+
left: opts.labelWidth + c * slotWidth
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
const lastAxisLabels = axisLabels[matrixResult.axes.length - 1] ?? [];
|
|
1265
|
+
for (let r = 0; r < rows; r++) {
|
|
1266
|
+
const labelBuf = await makeLabelPng(
|
|
1267
|
+
lastAxisLabels[r] ?? String(r),
|
|
1268
|
+
opts.labelWidth,
|
|
1269
|
+
slotHeight,
|
|
1270
|
+
opts.labelDpi,
|
|
1271
|
+
opts.labelBackground
|
|
1272
|
+
);
|
|
1273
|
+
compositeOps.push({
|
|
1274
|
+
input: labelBuf,
|
|
1275
|
+
top: opts.labelHeight + r * slotHeight,
|
|
1276
|
+
left: 0
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
const coordinateMap = new Array(cells.length);
|
|
1280
|
+
for (let i = 0; i < cells.length; i++) {
|
|
1281
|
+
const cell = cells[i];
|
|
1282
|
+
if (!cell) continue;
|
|
1283
|
+
const col = Math.floor(i / rows);
|
|
1284
|
+
const row = i % rows;
|
|
1285
|
+
const slotLeft = opts.labelWidth + col * slotWidth + opts.borderWidth;
|
|
1286
|
+
const slotTop = opts.labelHeight + row * slotHeight + opts.borderWidth;
|
|
1287
|
+
const contentLeft = slotLeft + opts.cellPadding;
|
|
1288
|
+
const contentTop = slotTop + opts.cellPadding;
|
|
1289
|
+
coordinateMap[i] = {
|
|
1290
|
+
x: contentLeft,
|
|
1291
|
+
y: contentTop,
|
|
1292
|
+
width: cellContentWidth,
|
|
1293
|
+
height: cellContentHeight
|
|
1294
|
+
};
|
|
1295
|
+
if (cell.result.screenshot.length > 0) {
|
|
1296
|
+
let cellPng;
|
|
1297
|
+
try {
|
|
1298
|
+
cellPng = await sharp(cell.result.screenshot).resize(cellContentWidth, cellContentHeight, { fit: "fill" }).png().toBuffer();
|
|
1299
|
+
} catch {
|
|
1300
|
+
cellPng = await makeRect(cellContentWidth, cellContentHeight, "#ffffff");
|
|
1301
|
+
}
|
|
1302
|
+
compositeOps.push({
|
|
1303
|
+
input: cellPng,
|
|
1304
|
+
top: contentTop,
|
|
1305
|
+
left: contentLeft
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
const bg = hexToRgb(opts.background);
|
|
1310
|
+
const png = await sharp({
|
|
1311
|
+
create: {
|
|
1312
|
+
width: totalWidth,
|
|
1313
|
+
height: totalHeight,
|
|
1314
|
+
channels: 4,
|
|
1315
|
+
background: bg
|
|
1316
|
+
}
|
|
1317
|
+
}).composite(compositeOps).png().toBuffer();
|
|
1318
|
+
return {
|
|
1319
|
+
png,
|
|
1320
|
+
coordinates: coordinateMap,
|
|
1321
|
+
width: totalWidth,
|
|
1322
|
+
height: totalHeight
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
// src/stress.ts
|
|
1328
|
+
var SHORT_TEXT_VALUES = ["", " ", "A", "OK", "Hello", "Submit", "Cancel"];
|
|
1329
|
+
var SHORT_TEXT_LABELS = [
|
|
1330
|
+
"empty",
|
|
1331
|
+
"space",
|
|
1332
|
+
"single-char",
|
|
1333
|
+
"2-char",
|
|
1334
|
+
"5-char",
|
|
1335
|
+
"6-char",
|
|
1336
|
+
"6-char-2"
|
|
1337
|
+
];
|
|
1338
|
+
var LONG_TEXT_VALUES = [
|
|
1339
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.",
|
|
1340
|
+
"The quick brown fox jumps over the lazy dog. ".repeat(3).trimEnd(),
|
|
1341
|
+
"A very long label that contains many words and will likely overflow any container that does not handle text overflow properly in some manner or another.",
|
|
1342
|
+
"supercalifragilisticexpialidocious-pneumonoultramicroscopicsilicovolcanoconiosis-antidisestablishmentarianism",
|
|
1343
|
+
"Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
1344
|
+
];
|
|
1345
|
+
var LONG_TEXT_LABELS = ["80-chars", "3x-sentence", "200-chars", "long-words", "100-a-chars"];
|
|
1346
|
+
var UNICODE_TEXT_VALUES = [
|
|
1347
|
+
"\u65E5\u672C\u8A9E\u30C6\u30AD\u30B9\u30C8",
|
|
1348
|
+
"\u4E2D\u6587\u5185\u5BB9",
|
|
1349
|
+
"\uD55C\uAD6D\uC5B4 \uD14D\uC2A4\uD2B8",
|
|
1350
|
+
"H\xE9llo W\xF6rld \u2014 caf\xE9 na\xEFve r\xE9sum\xE9",
|
|
1351
|
+
"\xD1o\xF1o se\xF1or",
|
|
1352
|
+
"\u{1F44B} Hello \u{1F30D}",
|
|
1353
|
+
"\u{1F3F3}\uFE0F\u200D\u{1F308} \u{1F389} \u{1F4A1} \u{1F680} \u2705",
|
|
1354
|
+
"a\u200Bb\u200Bc",
|
|
1355
|
+
// Zero-width space
|
|
1356
|
+
"\u202Eflipped text\u202C"
|
|
1357
|
+
// Right-to-left override
|
|
1358
|
+
];
|
|
1359
|
+
var UNICODE_TEXT_LABELS = [
|
|
1360
|
+
"japanese",
|
|
1361
|
+
"chinese",
|
|
1362
|
+
"korean",
|
|
1363
|
+
"diacritics",
|
|
1364
|
+
"tilde-n",
|
|
1365
|
+
"emoji-basic",
|
|
1366
|
+
"emoji-complex",
|
|
1367
|
+
"zero-width",
|
|
1368
|
+
"rtl-override"
|
|
1369
|
+
];
|
|
1370
|
+
var RTL_TEXT_VALUES = [
|
|
1371
|
+
"\u0645\u0631\u062D\u0628\u0627 \u0628\u0627\u0644\u0639\u0627\u0644\u0645",
|
|
1372
|
+
// Arabic: Hello World
|
|
1373
|
+
"\u0627\u0644\u0646\u0635 \u0627\u0644\u0639\u0631\u0628\u064A \u0627\u0644\u0637\u0648\u064A\u0644 \u062C\u062F\u0627\u064B \u0627\u0644\u0630\u064A \u064A\u0645\u062A\u062F \u0639\u0628\u0631 \u0623\u0633\u0637\u0631 \u0645\u062A\u0639\u062F\u062F\u0629",
|
|
1374
|
+
// Arabic long
|
|
1375
|
+
"\u05E9\u05DC\u05D5\u05DD \u05E2\u05D5\u05DC\u05DD",
|
|
1376
|
+
// Hebrew: Hello World
|
|
1377
|
+
"\u05D6\u05D4\u05D5 \u05D8\u05E7\u05E1\u05D8 \u05E2\u05D1\u05E8\u05D9 \u05D0\u05E8\u05D5\u05DA \u05E9\u05DE\u05EA\u05E4\u05E8\u05E1 \u05E2\u05DC \u05E4\u05E0\u05D9 \u05DB\u05DE\u05D4 \u05E9\u05D5\u05E8\u05D5\u05EA",
|
|
1378
|
+
// Hebrew long
|
|
1379
|
+
"\u0645\u0631\u062D\u0628\u0627\u064B \u200F hello"
|
|
1380
|
+
// Arabic mixed with LTR
|
|
1381
|
+
];
|
|
1382
|
+
var RTL_TEXT_LABELS = [
|
|
1383
|
+
"arabic-short",
|
|
1384
|
+
"arabic-long",
|
|
1385
|
+
"hebrew-short",
|
|
1386
|
+
"hebrew-long",
|
|
1387
|
+
"arabic-mixed"
|
|
1388
|
+
];
|
|
1389
|
+
var NUMBER_EDGE_VALUES = [
|
|
1390
|
+
0,
|
|
1391
|
+
-0,
|
|
1392
|
+
1,
|
|
1393
|
+
-1,
|
|
1394
|
+
NaN,
|
|
1395
|
+
Infinity,
|
|
1396
|
+
-Infinity,
|
|
1397
|
+
999999,
|
|
1398
|
+
Number.MAX_SAFE_INTEGER,
|
|
1399
|
+
Number.MIN_SAFE_INTEGER,
|
|
1400
|
+
Number.EPSILON
|
|
1401
|
+
];
|
|
1402
|
+
var NUMBER_EDGE_LABELS = [
|
|
1403
|
+
"zero",
|
|
1404
|
+
"neg-zero",
|
|
1405
|
+
"one",
|
|
1406
|
+
"neg-one",
|
|
1407
|
+
"NaN",
|
|
1408
|
+
"+Infinity",
|
|
1409
|
+
"-Infinity",
|
|
1410
|
+
"999999",
|
|
1411
|
+
"MAX_SAFE_INT",
|
|
1412
|
+
"MIN_SAFE_INT",
|
|
1413
|
+
"EPSILON"
|
|
1414
|
+
];
|
|
1415
|
+
function makeList(count) {
|
|
1416
|
+
if (count === 0) return [];
|
|
1417
|
+
return Array.from({ length: count }, (_, i) => `Item ${i + 1}`);
|
|
1418
|
+
}
|
|
1419
|
+
var LIST_COUNT_VALUES = [
|
|
1420
|
+
makeList(0),
|
|
1421
|
+
makeList(1),
|
|
1422
|
+
makeList(3),
|
|
1423
|
+
makeList(10),
|
|
1424
|
+
makeList(100),
|
|
1425
|
+
makeList(1e3)
|
|
1426
|
+
];
|
|
1427
|
+
var LIST_COUNT_LABELS = [
|
|
1428
|
+
"empty-list",
|
|
1429
|
+
"1-item",
|
|
1430
|
+
"3-items",
|
|
1431
|
+
"10-items",
|
|
1432
|
+
"100-items",
|
|
1433
|
+
"1000-items"
|
|
1434
|
+
];
|
|
1435
|
+
var IMAGE_STATE_VALUES = [
|
|
1436
|
+
null,
|
|
1437
|
+
"",
|
|
1438
|
+
"https://broken.invalid/image.png",
|
|
1439
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg==",
|
|
1440
|
+
// 1×1 transparent PNG
|
|
1441
|
+
"https://picsum.photos/200/200",
|
|
1442
|
+
"https://picsum.photos/4000/4000"
|
|
1443
|
+
];
|
|
1444
|
+
var IMAGE_STATE_LABELS = [
|
|
1445
|
+
"null",
|
|
1446
|
+
"empty-string",
|
|
1447
|
+
"broken-url",
|
|
1448
|
+
"1x1-data-uri",
|
|
1449
|
+
"200x200",
|
|
1450
|
+
"4000x4000"
|
|
1451
|
+
];
|
|
1452
|
+
var STRESS_PRESETS = {
|
|
1453
|
+
"text.short": {
|
|
1454
|
+
id: "text.short",
|
|
1455
|
+
label: "Short Text",
|
|
1456
|
+
description: "Empty, whitespace-only, single-char, and short-word string values.",
|
|
1457
|
+
values: SHORT_TEXT_VALUES,
|
|
1458
|
+
valueLabels: SHORT_TEXT_LABELS
|
|
1459
|
+
},
|
|
1460
|
+
"text.long": {
|
|
1461
|
+
id: "text.long",
|
|
1462
|
+
label: "Long Text",
|
|
1463
|
+
description: "200+ character strings designed to overflow containers.",
|
|
1464
|
+
values: LONG_TEXT_VALUES,
|
|
1465
|
+
valueLabels: LONG_TEXT_LABELS
|
|
1466
|
+
},
|
|
1467
|
+
"text.unicode": {
|
|
1468
|
+
id: "text.unicode",
|
|
1469
|
+
label: "Unicode Text",
|
|
1470
|
+
description: "CJK ideographs, diacritics, emoji, and zero-width characters.",
|
|
1471
|
+
values: UNICODE_TEXT_VALUES,
|
|
1472
|
+
valueLabels: UNICODE_TEXT_LABELS
|
|
1473
|
+
},
|
|
1474
|
+
"text.rtl": {
|
|
1475
|
+
id: "text.rtl",
|
|
1476
|
+
label: "RTL Text",
|
|
1477
|
+
description: "Arabic and Hebrew strings for bidi text rendering.",
|
|
1478
|
+
values: RTL_TEXT_VALUES,
|
|
1479
|
+
valueLabels: RTL_TEXT_LABELS
|
|
1480
|
+
},
|
|
1481
|
+
"number.edge": {
|
|
1482
|
+
id: "number.edge",
|
|
1483
|
+
label: "Edge Numbers",
|
|
1484
|
+
description: "0, -1, NaN, \xB1Infinity, MAX_SAFE_INTEGER, and other numeric extremes.",
|
|
1485
|
+
values: NUMBER_EDGE_VALUES,
|
|
1486
|
+
valueLabels: NUMBER_EDGE_LABELS
|
|
1487
|
+
},
|
|
1488
|
+
"list.count": {
|
|
1489
|
+
id: "list.count",
|
|
1490
|
+
label: "List Count",
|
|
1491
|
+
description: "Arrays of 0, 1, 3, 10, 100, and 1000 items.",
|
|
1492
|
+
values: LIST_COUNT_VALUES,
|
|
1493
|
+
valueLabels: LIST_COUNT_LABELS
|
|
1494
|
+
},
|
|
1495
|
+
"image.states": {
|
|
1496
|
+
id: "image.states",
|
|
1497
|
+
label: "Image States",
|
|
1498
|
+
description: "null, empty string, broken URL, 1\xD71 data URI, and large image URLs.",
|
|
1499
|
+
values: IMAGE_STATE_VALUES,
|
|
1500
|
+
valueLabels: IMAGE_STATE_LABELS
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
function getStressPreset(id) {
|
|
1504
|
+
const preset = STRESS_PRESETS[id];
|
|
1505
|
+
if (!preset) throw new Error(`Unknown stress preset category: "${id}"`);
|
|
1506
|
+
return preset;
|
|
1507
|
+
}
|
|
1508
|
+
function getStressPresets(ids) {
|
|
1509
|
+
return ids.map((id) => getStressPreset(id));
|
|
1510
|
+
}
|
|
1511
|
+
var ALL_STRESS_PRESETS = Object.values(STRESS_PRESETS);
|
|
1512
|
+
var ALL_STRESS_IDS = Object.keys(STRESS_PRESETS);
|
|
1513
|
+
function stressAxis(id) {
|
|
1514
|
+
const preset = getStressPreset(id);
|
|
1515
|
+
return {
|
|
1516
|
+
name: preset.label,
|
|
1517
|
+
values: preset.values
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
export { ALL_CONTEXTS, ALL_CONTEXT_IDS, ALL_STRESS_IDS, ALL_STRESS_PRESETS, BrowserPool, COMPOSITION_CONTEXTS, RenderError, RenderMatrix, SatoriRenderer, SpriteSheetGenerator, cartesianProduct, clearFontCache, contextAxis, detectHeuristicFlags, extractInlineStyles, fontCacheSize, getContext, getContexts, getStressPreset, getStressPresets, labelForValue, loadFont, safeRender, stressAxis, wrapWithProviders };
|
|
1522
|
+
//# sourceMappingURL=index.js.map
|
|
1523
|
+
//# sourceMappingURL=index.js.map
|