@agent-scope/render 1.17.0 → 1.17.2
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/README.md +640 -0
- package/package.json +4 -3
package/README.md
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# `@agent-scope/render`
|
|
2
|
+
|
|
3
|
+
Dual-path render engine for React components. Routes simple (flexbox-only) components through a pure Node.js Satori pipeline, and complex components through a warm Playwright `BrowserPool`. A `RenderMatrix` generates the full Cartesian product of prop axes; `SpriteSheetGenerator` composites the results into a single annotated PNG.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @agent-scope/render
|
|
9
|
+
# or
|
|
10
|
+
bun add @agent-scope/render
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependencies: `react`, `react-dom`.
|
|
14
|
+
|
|
15
|
+
## What it does / when to use
|
|
16
|
+
|
|
17
|
+
| Use case | Choose |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Flexbox-only component, screenshot needed fast (~8 ms) | `SatoriRenderer` |
|
|
20
|
+
| CSS grid, absolute positioning, animations, real browser CSS | `BrowserPool` |
|
|
21
|
+
| Render all prop combinations in a grid | `RenderMatrix` |
|
|
22
|
+
| Composite results into a single labelled PNG | `SpriteSheetGenerator` |
|
|
23
|
+
| Layout context stress-testing | `contextAxis` + `RenderMatrix` |
|
|
24
|
+
| Edge-case prop stress-testing | `stressAxis` + `RenderMatrix` |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
complexityClass?
|
|
32
|
+
│
|
|
33
|
+
┌────────────────┴────────────────┐
|
|
34
|
+
"simple" "complex"
|
|
35
|
+
│ │
|
|
36
|
+
SatoriRenderer BrowserPool
|
|
37
|
+
React → Satori SVG Playwright pages
|
|
38
|
+
→ resvg-js PNG (inject-don't-navigate)
|
|
39
|
+
│ │
|
|
40
|
+
└─────────────┬────────────────────┘
|
|
41
|
+
│
|
|
42
|
+
RenderResult
|
|
43
|
+
│
|
|
44
|
+
┌─────────────┴───────────────┐
|
|
45
|
+
RenderMatrix SpriteSheetGenerator
|
|
46
|
+
(Cartesian product) (PNG composite)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Satori path
|
|
50
|
+
|
|
51
|
+
`SatoriRenderer` runs entirely in Node.js with no browser overhead:
|
|
52
|
+
|
|
53
|
+
1. Load font (Inter `.woff` by default; custom TTF/OTF/WOFF supported — **not** woff2, opentype.js limitation)
|
|
54
|
+
2. Wrap element with mock providers (`ThemeContext`, `LocaleContext`, generic fallbacks)
|
|
55
|
+
3. Call `satori(element, { width, height, fonts })` → SVG string
|
|
56
|
+
4. Call `new Resvg(svg).render().asPng()` → PNG `Buffer`
|
|
57
|
+
|
|
58
|
+
Target: **~8 ms** per render at 375×812.
|
|
59
|
+
|
|
60
|
+
### BrowserPool path
|
|
61
|
+
|
|
62
|
+
`BrowserPool` maintains N browser instances × M pages per browser. The "inject-don't-navigate" pattern keeps pages warm:
|
|
63
|
+
|
|
64
|
+
1. `init()` launches Chromium instances and pre-loads each page **once** with a skeleton HTML containing `window.__renderComponent`, `window.__componentRegistry`, and `window.__registerComponent`
|
|
65
|
+
2. `render(name, props)` calls `page.evaluate(() => window.__renderComponent(name, props))` — no `page.goto()` between renders
|
|
66
|
+
3. Waits for `window.__renderReady === true`
|
|
67
|
+
4. Screenshots the component bounding box (not the full viewport)
|
|
68
|
+
5. Optionally extracts DOM tree, computed styles, console output, and accessibility info
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## API Reference
|
|
73
|
+
|
|
74
|
+
### `SatoriRenderer`
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { SatoriRenderer } from '@agent-scope/render';
|
|
78
|
+
|
|
79
|
+
const renderer = new SatoriRenderer(config?: RendererConfig);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**`RendererConfig`**
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface RendererConfig {
|
|
86
|
+
/** Path to a TTF/OTF/WOFF font file. NOT woff2 — opentype.js limitation.
|
|
87
|
+
* Defaults to bundled Inter woff (@fontsource/inter). */
|
|
88
|
+
fontPath?: string;
|
|
89
|
+
/** Font family name. Defaults to "Inter". */
|
|
90
|
+
fontFamily?: string;
|
|
91
|
+
/** Default viewport. Defaults to { width: 375, height: 812 }. */
|
|
92
|
+
defaultViewport?: ViewportOptions;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Methods**
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Pre-load and cache the font (call before the first render for consistent timing).
|
|
100
|
+
renderer.preloadFont(): Promise<void>
|
|
101
|
+
|
|
102
|
+
// Render a React element to PNG.
|
|
103
|
+
renderer.render(
|
|
104
|
+
element: React.ReactElement,
|
|
105
|
+
options?: SatoriRenderOptions,
|
|
106
|
+
descriptor?: Pick<ComponentDescriptor, 'requiredContexts'>,
|
|
107
|
+
): Promise<RenderResult>
|
|
108
|
+
|
|
109
|
+
// Shorthand: render at explicit viewport size.
|
|
110
|
+
renderer.renderAt(
|
|
111
|
+
element: React.ReactElement,
|
|
112
|
+
width: number,
|
|
113
|
+
height: number,
|
|
114
|
+
descriptor?: Pick<ComponentDescriptor, 'requiredContexts'>,
|
|
115
|
+
): Promise<RenderResult>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**`SatoriRenderOptions`**
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
interface SatoriRenderOptions {
|
|
122
|
+
viewport?: { width: number; height: number };
|
|
123
|
+
container?: Partial<ContainerOptions>;
|
|
124
|
+
capture?: Partial<CaptureOptions>;
|
|
125
|
+
environment?: EnvironmentOptions;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface ContainerOptions {
|
|
129
|
+
type: 'centered' | 'flex-row' | 'flex-col' | 'none';
|
|
130
|
+
padding: number; // pixels, applied to all sides
|
|
131
|
+
background?: string; // CSS colour string
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface CaptureOptions {
|
|
135
|
+
screenshot: boolean; // default: true
|
|
136
|
+
styles: boolean; // default: true
|
|
137
|
+
timing: boolean; // default: true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface EnvironmentOptions {
|
|
141
|
+
viewport?: ViewportOptions;
|
|
142
|
+
theme?: string; // passed to mock ThemeContext
|
|
143
|
+
locale?: string; // e.g. "en-US"
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Example**
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import React from 'react';
|
|
151
|
+
import { SatoriRenderer } from '@agent-scope/render';
|
|
152
|
+
|
|
153
|
+
const renderer = new SatoriRenderer();
|
|
154
|
+
await renderer.preloadFont();
|
|
155
|
+
|
|
156
|
+
const result = await renderer.render(
|
|
157
|
+
<button style={{ padding: 16, background: '#0070f3', color: '#fff' }}>
|
|
158
|
+
Click me
|
|
159
|
+
</button>,
|
|
160
|
+
{ viewport: { width: 375, height: 200 } },
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
console.log(result.width, result.height, result.renderTimeMs);
|
|
164
|
+
// → 375 200 7.4
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### `BrowserPool`
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { BrowserPool } from '@agent-scope/render';
|
|
173
|
+
|
|
174
|
+
const pool = new BrowserPool(config?: BrowserPoolConfig);
|
|
175
|
+
await pool.init();
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**`BrowserPoolConfig`**
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
interface BrowserPoolConfig {
|
|
182
|
+
/**
|
|
183
|
+
* Named size preset. Mutually exclusive with `size`.
|
|
184
|
+
* Defaults to "local".
|
|
185
|
+
*
|
|
186
|
+
* | Preset | browsers | pagesPerBrowser | max concurrency |
|
|
187
|
+
* |--------------|----------|-----------------|-----------------|
|
|
188
|
+
* | local | 1 | 5 | 5 |
|
|
189
|
+
* | ci-standard | 3 | 15 | 45 |
|
|
190
|
+
* | ci-large | 6 | 20 | 120 |
|
|
191
|
+
*/
|
|
192
|
+
preset?: 'local' | 'ci-standard' | 'ci-large';
|
|
193
|
+
/** Custom size config — overrides preset when provided. */
|
|
194
|
+
size?: { browsers: number; pagesPerBrowser: number };
|
|
195
|
+
viewportWidth?: number; // default: 1440
|
|
196
|
+
viewportHeight?: number; // default: 900
|
|
197
|
+
/** Max ms to wait for a free slot before acquire() rejects. Default: 30_000. */
|
|
198
|
+
acquireTimeoutMs?: number;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Methods**
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Launch browsers and pre-load skeleton HTML. Must be called before render().
|
|
206
|
+
pool.init(): Promise<void>
|
|
207
|
+
|
|
208
|
+
// Render a registered component by name.
|
|
209
|
+
pool.render(
|
|
210
|
+
componentName: string,
|
|
211
|
+
props?: Record<string, unknown>,
|
|
212
|
+
options?: RenderOptions,
|
|
213
|
+
): Promise<RenderResult>
|
|
214
|
+
|
|
215
|
+
// Acquire a free page slot manually (advanced — prefer render()).
|
|
216
|
+
pool.acquire(): Promise<PageSlot>
|
|
217
|
+
|
|
218
|
+
// Return a slot to the pool (wakes the next queued waiter).
|
|
219
|
+
pool.release(slot: PageSlot): void
|
|
220
|
+
|
|
221
|
+
// Gracefully drain in-flight renders then close all browsers.
|
|
222
|
+
pool.close(): Promise<void>
|
|
223
|
+
|
|
224
|
+
// Introspection
|
|
225
|
+
pool.totalSlots: number
|
|
226
|
+
pool.freeSlots: number
|
|
227
|
+
pool.activeSlots: number
|
|
228
|
+
pool.queueDepth: number
|
|
229
|
+
pool.isInitialized: boolean
|
|
230
|
+
pool.isClosed: boolean
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**`RenderOptions`**
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
interface RenderOptions {
|
|
237
|
+
captureDom?: boolean; // capture DOM tree. Default: false
|
|
238
|
+
captureStyles?: boolean; // capture computed styles. Default: true
|
|
239
|
+
captureConsole?: boolean; // capture console output. Default: false
|
|
240
|
+
captureA11y?: boolean; // capture accessibility snapshot. Default: false
|
|
241
|
+
viewportWidth?: number; // override pool-level viewport
|
|
242
|
+
viewportHeight?: number;
|
|
243
|
+
timeoutMs?: number; // wait for window.__renderReady. Default: 10_000
|
|
244
|
+
screenshotPadding?: number; // px around bounding box crop. Default: 24
|
|
245
|
+
minScreenshotWidth?: number; // Default: 320
|
|
246
|
+
minScreenshotHeight?: number;// Default: 200
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Example**
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { BrowserPool } from '@agent-scope/render';
|
|
254
|
+
|
|
255
|
+
const pool = new BrowserPool({ preset: 'ci-standard' });
|
|
256
|
+
await pool.init();
|
|
257
|
+
|
|
258
|
+
const result = await pool.render('Button', { label: 'Click me' }, {
|
|
259
|
+
captureDom: true,
|
|
260
|
+
captureConsole: true,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// result.dom.elementCount, result.console.errors, etc.
|
|
264
|
+
await pool.close();
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Pool exhaustion & queuing** (from `browser-pool.test.ts`)
|
|
268
|
+
|
|
269
|
+
When all slots are busy, `acquire()` queues callers FIFO and resumes them on `release()`. With `acquireTimeoutMs: 50` and one slot:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
const slot = await pool.acquire();
|
|
273
|
+
// Second acquire times out after 50 ms when no slot is released:
|
|
274
|
+
await pool.acquire(); // throws: "BrowserPool.acquire() timed out after 50ms"
|
|
275
|
+
pool.release(slot);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### `RenderMatrix`
|
|
281
|
+
|
|
282
|
+
Generates the Cartesian product of all axis value combinations and renders each cell.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { RenderMatrix } from '@agent-scope/render';
|
|
286
|
+
|
|
287
|
+
const matrix = new RenderMatrix(
|
|
288
|
+
renderer, // anything implementing MatrixRenderer
|
|
289
|
+
axes, // MatrixAxis[]
|
|
290
|
+
options?, // { complexityClass?, concurrency? }
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const result: MatrixResult = await matrix.render();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**`MatrixAxis`**
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
interface MatrixAxis<T = unknown> {
|
|
300
|
+
name: string; // axis name, used as prop key
|
|
301
|
+
values: T[]; // values along this axis
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**`MatrixRenderer` interface**
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
interface MatrixRenderer {
|
|
309
|
+
renderCell(
|
|
310
|
+
props: Record<string, unknown>,
|
|
311
|
+
complexityClass: ComplexityClass,
|
|
312
|
+
): Promise<RenderResult>;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Both `SatoriRenderer` and `BrowserPool` can be adapted to this interface.
|
|
317
|
+
|
|
318
|
+
**`MatrixResult`**
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
interface MatrixResult {
|
|
322
|
+
cells: MatrixCell[]; // flat list, row-major order
|
|
323
|
+
axes: MatrixAxis[];
|
|
324
|
+
axisLabels: string[][]; // [axisIndex][valueIndex] → display label
|
|
325
|
+
stats: MatrixStats;
|
|
326
|
+
rows: number; // last axis cardinality
|
|
327
|
+
cols: number; // product of remaining axes
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
interface MatrixCell {
|
|
331
|
+
props: Record<string, unknown>; // axis name → value for this cell
|
|
332
|
+
result: RenderResult;
|
|
333
|
+
index: number; // flat row-major index
|
|
334
|
+
axisIndices: number[]; // per-axis indices
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface MatrixStats {
|
|
338
|
+
totalCells: number;
|
|
339
|
+
totalRenderTimeMs: number;
|
|
340
|
+
avgRenderTimeMs: number;
|
|
341
|
+
minRenderTimeMs: number;
|
|
342
|
+
maxRenderTimeMs: number;
|
|
343
|
+
wallClockTimeMs: number;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Cell ordering** (from `matrix.test.ts`)
|
|
348
|
+
|
|
349
|
+
First axis iterates slowest (row-major). For axes `[['A','B'], [1,2]]`:
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
index 0 → { x: 'A', y: 1 }
|
|
353
|
+
index 1 → { x: 'A', y: 2 }
|
|
354
|
+
index 2 → { x: 'B', y: 1 }
|
|
355
|
+
index 3 → { x: 'B', y: 2 }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Example: 3×3×2 = 18 cells**
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
const matrix = new RenderMatrix(renderer, [
|
|
362
|
+
{ name: 'variant', values: ['primary', 'secondary', 'ghost'] },
|
|
363
|
+
{ name: 'size', values: ['sm', 'md', 'lg'] },
|
|
364
|
+
{ name: 'disabled', values: [false, true] },
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
const result = await matrix.render();
|
|
368
|
+
// result.cells.length === 18
|
|
369
|
+
// result.stats.avgRenderTimeMs — average per-cell render time
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Grid dimensions**
|
|
373
|
+
|
|
374
|
+
```
|
|
375
|
+
rows = last axis cardinality
|
|
376
|
+
cols = totalCells / rows
|
|
377
|
+
|
|
378
|
+
// Single-axis: [{ name: 'v', values: ['a','b','c'] }]
|
|
379
|
+
// → rows: 3, cols: 1
|
|
380
|
+
|
|
381
|
+
// Two axes: [2 values, 3 values]
|
|
382
|
+
// → rows: 3, cols: 2, cells: 6
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**`cartesianProduct` utility**
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { cartesianProduct } from '@agent-scope/render';
|
|
389
|
+
|
|
390
|
+
cartesianProduct([[1,2], ['a','b']]);
|
|
391
|
+
// → [[1,'a'], [1,'b'], [2,'a'], [2,'b']]
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
### `SpriteSheetGenerator`
|
|
397
|
+
|
|
398
|
+
Composites a `MatrixResult` into a single labelled PNG.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { SpriteSheetGenerator } from '@agent-scope/render';
|
|
402
|
+
|
|
403
|
+
const gen = new SpriteSheetGenerator(options?: SpriteSheetOptions);
|
|
404
|
+
const { png, coordinates, width, height } = await gen.generate(matrixResult);
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**`SpriteSheetOptions`**
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
interface SpriteSheetOptions {
|
|
411
|
+
cellPadding?: number; // px around each cell. Default: 8
|
|
412
|
+
borderWidth?: number; // border thickness. Default: 1
|
|
413
|
+
background?: string; // CSS hex. Default: "#f5f5f5"
|
|
414
|
+
borderColor?: string; // Default: "#cccccc"
|
|
415
|
+
labelHeight?: number; // top label row height. Default: 32
|
|
416
|
+
labelWidth?: number; // left label column width. Default: 80
|
|
417
|
+
labelDpi?: number; // label text DPI. Default: 72
|
|
418
|
+
labelBackground?: string; // Default: "#e8e8e8"
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**`SpriteSheetResult`**
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
interface SpriteSheetResult {
|
|
426
|
+
png: Buffer; // full composite PNG
|
|
427
|
+
coordinates: CellCoordinateMap; // CellBounds[] indexed by flat cell index
|
|
428
|
+
width: number; // total sprite sheet width in pixels
|
|
429
|
+
height: number; // total sprite sheet height in pixels
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
interface CellBounds {
|
|
433
|
+
x: number; // left edge in sprite sheet
|
|
434
|
+
y: number; // top edge in sprite sheet
|
|
435
|
+
width: number; // content width (excludes padding)
|
|
436
|
+
height: number; // content height (excludes padding)
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Layout rules** (from `sprite-sheet.test.ts`)
|
|
441
|
+
|
|
442
|
+
- Cells in the same **column** share the same `x` coordinate
|
|
443
|
+
- Cells in the same **row** share the same `y` coordinate
|
|
444
|
+
- Row `y` values increase monotonically: `coords[0].y < coords[1].y < coords[2].y`
|
|
445
|
+
- Larger `cellPadding` → larger total sprite sheet dimensions
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
const gen = new SpriteSheetGenerator({ cellPadding: 12, background: '#fff' });
|
|
449
|
+
const { png, coordinates } = await gen.generate(matrixResult);
|
|
450
|
+
|
|
451
|
+
// Access cell 3's position:
|
|
452
|
+
const { x, y, width, height } = coordinates[3];
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### Composition Contexts
|
|
458
|
+
|
|
459
|
+
Ten built-in layout environments for use as `RenderMatrix` axes.
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { contextAxis, COMPOSITION_CONTEXTS, getContext } from '@agent-scope/render';
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Available context IDs**
|
|
466
|
+
|
|
467
|
+
| ID | Description |
|
|
468
|
+
|---|---|
|
|
469
|
+
| `centered` | Centered in viewport (flex, align/justify center) |
|
|
470
|
+
| `flex-row` | Flex row with sibling placeholders |
|
|
471
|
+
| `flex-col` | Flex column with sibling placeholders |
|
|
472
|
+
| `grid` | CSS grid cell (3-column auto-fill) |
|
|
473
|
+
| `sidebar` | Narrow 240 px sidebar |
|
|
474
|
+
| `scroll` | Scrollable container, fixed 300 px height |
|
|
475
|
+
| `full-width` | Stretches to full viewport width |
|
|
476
|
+
| `constrained` | Centered, max-width 1024 px |
|
|
477
|
+
| `rtl` | `dir="rtl"`, `lang="ar"` — bidi layout testing |
|
|
478
|
+
| `nested-flex` | Three levels of nested flex containers |
|
|
479
|
+
|
|
480
|
+
**Usage**
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import { RenderMatrix, contextAxis } from '@agent-scope/render';
|
|
484
|
+
|
|
485
|
+
// All 10 contexts × 2 variants = 20 cells
|
|
486
|
+
const matrix = new RenderMatrix(renderer, [
|
|
487
|
+
contextAxis(['centered', 'flex-row', 'sidebar', 'rtl']),
|
|
488
|
+
{ name: 'variant', values: ['primary', 'secondary'] },
|
|
489
|
+
]);
|
|
490
|
+
|
|
491
|
+
// Or use all 10:
|
|
492
|
+
const allContextMatrix = new RenderMatrix(renderer, [
|
|
493
|
+
contextAxis(),
|
|
494
|
+
{ name: 'size', values: ['sm', 'md', 'lg'] },
|
|
495
|
+
]);
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**Helper functions**
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
getContext('rtl') // → CompositionContext
|
|
502
|
+
getContexts(['rtl', 'centered']) // → CompositionContext[]
|
|
503
|
+
contextAxis(['centered', 'rtl', 'grid']) // → MatrixAxis<CompositionContext>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
### Content Stress Presets
|
|
509
|
+
|
|
510
|
+
Seven categories of edge-case prop values for `RenderMatrix` axes.
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
import { stressAxis, getStressPreset, ALL_STRESS_PRESETS } from '@agent-scope/render';
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Available categories**
|
|
517
|
+
|
|
518
|
+
| ID | Values | What it exercises |
|
|
519
|
+
|---|---|---|
|
|
520
|
+
| `text.short` | `""`, `" "`, `"A"`, `"OK"`, `"Hello"`, `"Submit"`, `"Cancel"` | Empty, whitespace, single-char |
|
|
521
|
+
| `text.long` | 80–200 char strings, long words, 100-char runs | Overflow, text wrapping |
|
|
522
|
+
| `text.unicode` | Japanese, Chinese, Korean, diacritics, emoji, zero-width space, RTL override | CJK, emoji, bidi |
|
|
523
|
+
| `text.rtl` | Arabic and Hebrew strings, mixed Arabic+LTR | Right-to-left layout |
|
|
524
|
+
| `number.edge` | `0`, `-0`, `1`, `-1`, `NaN`, `Infinity`, `-Infinity`, `999999`, `MAX_SAFE_INTEGER` | Numeric extremes |
|
|
525
|
+
| `list.count` | `[]`, 1-item, 3-item, 10-item, 100-item, 1000-item arrays | Empty/huge lists |
|
|
526
|
+
| `image.states` | `null`, `""`, broken URL, 1×1 data URI, 200×200, 4000×4000 | Image error states |
|
|
527
|
+
|
|
528
|
+
**Usage**
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
import { RenderMatrix, stressAxis } from '@agent-scope/render';
|
|
532
|
+
|
|
533
|
+
// 7 short-text values × 2 disabled states = 14 cells
|
|
534
|
+
const matrix = new RenderMatrix(renderer, [
|
|
535
|
+
stressAxis('text.short'),
|
|
536
|
+
{ name: 'disabled', values: [false, true] },
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
// Direct preset access:
|
|
540
|
+
const preset = getStressPreset('number.edge');
|
|
541
|
+
// preset.values → [0, -0, 1, -1, NaN, Infinity, ...]
|
|
542
|
+
// preset.valueLabels → ['zero', 'neg-zero', 'one', ...]
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
### `RenderResult`
|
|
548
|
+
|
|
549
|
+
Shared return type for both render paths.
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
interface RenderResult {
|
|
553
|
+
screenshot: Buffer; // PNG as a Buffer
|
|
554
|
+
width: number; // pixel width of the rendered component
|
|
555
|
+
height: number; // pixel height
|
|
556
|
+
renderTimeMs: number; // wall-clock render time in ms
|
|
557
|
+
computedStyles: Record<string, Record<string, string>>;
|
|
558
|
+
// BrowserPool only (when captureXxx options enabled):
|
|
559
|
+
dom?: { tree: DOMNode; elementCount: number; boundingBox: BoundingBox };
|
|
560
|
+
console?: { errors: string[]; warnings: string[]; logs: string[] };
|
|
561
|
+
accessibility?: { role: string; name: string; violations: string[] };
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
The `computedStyles` key for the BrowserPool path is `"[data-reactscope-root] > *"` and captures: `display`, `flexDirection`, `flexWrap`, `alignItems`, `justifyContent`, `gridTemplateColumns`, `position`, `width`, `height`, `color`, `backgroundColor`, `fontSize`, `fontFamily`, `fontWeight`, `borderRadius`, `boxShadow`, `opacity`, `transform`, and more.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
### Structured error handling
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
import { safeRender, RenderError, detectHeuristicFlags } from '@agent-scope/render';
|
|
573
|
+
// or from the sub-path:
|
|
574
|
+
import { safeRender } from '@agent-scope/render/errors';
|
|
575
|
+
|
|
576
|
+
const outcome = await safeRender(
|
|
577
|
+
() => pool.render('Button', props),
|
|
578
|
+
{ props, sourceLocation: { file: 'Button.tsx', line: 12, column: 0 } },
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
if (outcome.crashed) {
|
|
582
|
+
console.error(outcome.error.heuristicFlags);
|
|
583
|
+
// e.g. ['MISSING_PROVIDER', 'TYPE_MISMATCH']
|
|
584
|
+
} else {
|
|
585
|
+
const png = outcome.result.screenshot;
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**`HeuristicFlag` values**: `MISSING_PROVIDER`, `UNDEFINED_PROP`, `TYPE_MISMATCH`, `NETWORK_REQUIRED`, `ASYNC_NOT_SUSPENDED`, `HOOK_CALL_VIOLATION`, `ELEMENT_TYPE_INVALID`, `HYDRATION_MISMATCH`, `CIRCULAR_DEPENDENCY`, `UNKNOWN_ERROR`.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Real test payloads
|
|
594
|
+
|
|
595
|
+
### Matrix: 2×2 produces 4 cells with correct prop sets (from `matrix.test.ts`)
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
const matrix = new RenderMatrix(renderer, [
|
|
599
|
+
{ name: 'variant', values: ['primary', 'secondary'] },
|
|
600
|
+
{ name: 'size', values: ['sm', 'md'] },
|
|
601
|
+
]);
|
|
602
|
+
const result = await matrix.render();
|
|
603
|
+
|
|
604
|
+
// result.cells.map(c => c.props):
|
|
605
|
+
// [{ variant: 'primary', size: 'sm' }, { variant: 'primary', size: 'md' },
|
|
606
|
+
// { variant: 'secondary', size: 'sm' }, { variant: 'secondary', size: 'md' }]
|
|
607
|
+
|
|
608
|
+
// result.stats:
|
|
609
|
+
// { totalCells: 4, avgRenderTimeMs: 12.5, minRenderTimeMs: 5, maxRenderTimeMs: 20 }
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### BrowserPool: clipped screenshot dimensions match bounding box (from `browser-pool.test.ts`)
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
// Component bounding box: 40×40, viewport: 1440×900
|
|
616
|
+
// result.width === 40, result.height === 40 (not 1440×900)
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### SpriteSheet: coordinate alignment (from `sprite-sheet.test.ts`)
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
const matrix = makeMockMatrixResult(3, 2); // 3 rows, 2 cols
|
|
623
|
+
const { coordinates } = await gen.generate(matrix);
|
|
624
|
+
|
|
625
|
+
// Col 0: cells 0,1,2 share x:
|
|
626
|
+
coordinates[0].x === coordinates[1].x === coordinates[2].x
|
|
627
|
+
|
|
628
|
+
// Row 0: cells 0 and 3 share y:
|
|
629
|
+
coordinates[0].y === coordinates[3].y
|
|
630
|
+
|
|
631
|
+
// y increases down rows:
|
|
632
|
+
coordinates[0].y < coordinates[1].y < coordinates[2].y
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Used by
|
|
638
|
+
|
|
639
|
+
- `@agent-scope/cli` — invokes `BrowserPool` and `SatoriRenderer` to render component screenshots during `scope render` runs
|
|
640
|
+
- `@agent-scope/manifest` — provides `ComplexityClass` to route renders between paths
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-scope/render",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"description": "Render paths for React components: Satori (simple/flexbox) and Playwright BrowserPool (complex)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"module": "./dist/index.js",
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
32
|
"files": [
|
|
33
|
-
"dist"
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md"
|
|
34
35
|
],
|
|
35
36
|
"scripts": {
|
|
36
37
|
"build": "tsup",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@fontsource/inter": "^5.2.8",
|
|
44
45
|
"@resvg/resvg-js": "^2.6.2",
|
|
45
|
-
"@agent-scope/manifest": "1.17.
|
|
46
|
+
"@agent-scope/manifest": "1.17.2",
|
|
46
47
|
"playwright": "^1.50.0",
|
|
47
48
|
"react": "^18.3.1",
|
|
48
49
|
"react-dom": "^18.3.1",
|