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