@effing/satori 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -3
- package/dist/chunk-H5M6ZFOA.js +84 -0
- package/dist/chunk-H5M6ZFOA.js.map +1 -0
- package/dist/emoji-CtRDUFb8.d.ts +6 -0
- package/dist/index.d.ts +24 -8
- package/dist/index.js +25 -10
- package/dist/index.js.map +1 -1
- package/dist/pool/index.d.ts +67 -0
- package/dist/pool/index.js +64 -0
- package/dist/pool/index.js.map +1 -0
- package/dist/serde/index.d.ts +18 -0
- package/dist/serde/index.js +11 -0
- package/dist/serde/index.js.map +1 -0
- package/dist/worker/index.js +20004 -0
- package/package.json +30 -5
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> Part of the [**Effing**](../../README.md) family — programmatic video creation with TypeScript.
|
|
6
6
|
|
|
7
|
-
A thin wrapper around [Satori](https://github.com/vercel/satori) that renders JSX to PNG buffers. Includes built-in emoji support with multiple emoji styles.
|
|
7
|
+
A thin wrapper around [Satori](https://github.com/vercel/satori) that renders JSX to PNG buffers. Includes built-in emoji support with multiple emoji styles, standalone SVG/rasterization functions, and an optional worker pool for parallelized rendering.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -12,6 +12,12 @@ A thin wrapper around [Satori](https://github.com/vercel/satori) that renders JS
|
|
|
12
12
|
npm install @effing/satori
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
For worker pool support (optional):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @effing/satori react tinypool
|
|
19
|
+
```
|
|
20
|
+
|
|
15
21
|
## Quick Start
|
|
16
22
|
|
|
17
23
|
```typescript
|
|
@@ -62,6 +68,8 @@ JSX → Satori → SVG → Resvg → PNG Buffer
|
|
|
62
68
|
1. **Satori** converts JSX with CSS-like styles to SVG
|
|
63
69
|
2. **Resvg** renders the SVG to a PNG buffer
|
|
64
70
|
|
|
71
|
+
The pipeline is also available as standalone functions (`svgFromSatori` and `rasterizeSvg`) for cases where you need the intermediate SVG or want to rasterize SVGs from other sources.
|
|
72
|
+
|
|
65
73
|
### Font Loading
|
|
66
74
|
|
|
67
75
|
Satori requires font data to render text. You must provide fonts as ArrayBuffers:
|
|
@@ -90,6 +98,28 @@ The package automatically loads emoji SVGs from CDNs. Supported styles:
|
|
|
90
98
|
| `fluent` | Microsoft Fluent Emoji (color) |
|
|
91
99
|
| `fluentFlat` | Microsoft Fluent Emoji (flat) |
|
|
92
100
|
|
|
101
|
+
### Worker Pool
|
|
102
|
+
|
|
103
|
+
When rendering many frames (e.g. for animations), you can parallelize rendering across CPU cores using the worker pool. This can provide up to 4x speedups depending on render complexity.
|
|
104
|
+
|
|
105
|
+
The pool handles React element serialization automatically — you pass JSX in and get PNG/SVG buffers out, just like the single-threaded API.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { createSatoriPool } from "@effing/satori/pool";
|
|
109
|
+
|
|
110
|
+
const pool = createSatoriPool({ maxThreads: 4 });
|
|
111
|
+
|
|
112
|
+
const png = await pool.renderToPng(
|
|
113
|
+
<div style={{ fontSize: 48 }}>Hello from a worker!</div>,
|
|
114
|
+
{ width: 800, height: 600, fonts }
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Clean up when done
|
|
118
|
+
await pool.destroy();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Peer dependencies:** The pool and serde sub-paths require `react` and `tinypool` to be installed. They are listed as optional peer dependencies so the main `@effing/satori` entry works without them.
|
|
122
|
+
|
|
93
123
|
## API Overview
|
|
94
124
|
|
|
95
125
|
### `pngFromSatori(template, options)`
|
|
@@ -99,11 +129,30 @@ Render a JSX template to a PNG buffer.
|
|
|
99
129
|
```typescript
|
|
100
130
|
function pngFromSatori(
|
|
101
131
|
template: React.ReactNode,
|
|
102
|
-
options:
|
|
132
|
+
options: SatoriOptions,
|
|
103
133
|
): Promise<Buffer>;
|
|
104
134
|
```
|
|
105
135
|
|
|
106
|
-
|
|
136
|
+
### `svgFromSatori(template, options)`
|
|
137
|
+
|
|
138
|
+
Render a JSX template to an SVG string.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
function svgFromSatori(
|
|
142
|
+
template: React.ReactNode,
|
|
143
|
+
options: SatoriOptions,
|
|
144
|
+
): Promise<string>;
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `rasterizeSvg(svg)`
|
|
148
|
+
|
|
149
|
+
Rasterize an SVG string to a PNG buffer using Resvg.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
function rasterizeSvg(svg: string): Buffer;
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Options
|
|
107
156
|
|
|
108
157
|
| Option | Type | Required | Description |
|
|
109
158
|
| -------- | ------------ | -------- | ---------------------------------- |
|
|
@@ -112,6 +161,38 @@ function pngFromSatori(
|
|
|
112
161
|
| `fonts` | `FontData[]` | Yes | Font data for text rendering |
|
|
113
162
|
| `emoji` | `EmojiStyle` | No | Emoji style (default: `"twemoji"`) |
|
|
114
163
|
|
|
164
|
+
### `@effing/satori/pool`
|
|
165
|
+
|
|
166
|
+
#### `createSatoriPool(options?)`
|
|
167
|
+
|
|
168
|
+
Create a worker pool for parallelized rendering.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
function createSatoriPool(options?: SatoriPoolOptions): SatoriPool;
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Pool options:**
|
|
175
|
+
|
|
176
|
+
| Option | Type | Default | Description |
|
|
177
|
+
| ------------ | -------- | ------------------ | ---------------------- |
|
|
178
|
+
| `minThreads` | `number` | `1` | Minimum worker threads |
|
|
179
|
+
| `maxThreads` | `number` | `os.cpus().length` | Maximum worker threads |
|
|
180
|
+
|
|
181
|
+
**`SatoriPool` methods:**
|
|
182
|
+
|
|
183
|
+
- `renderToPng(element, options)` — Render JSX to PNG buffer
|
|
184
|
+
- `renderToSvg(element, options)` — Render JSX to SVG string
|
|
185
|
+
- `rasterizeSvgToPng(svg, options?)` — Rasterize SVG to PNG buffer
|
|
186
|
+
- `destroy()` — Shut down the pool
|
|
187
|
+
|
|
188
|
+
### `@effing/satori/serde`
|
|
189
|
+
|
|
190
|
+
React element serialization for cross-thread communication. Used internally by the pool, but available for custom worker setups.
|
|
191
|
+
|
|
192
|
+
- `expandElement(node)` — Recursively flatten function components to intrinsic elements
|
|
193
|
+
- `serializeElement(node)` — Make a React element tree structured-clone-safe
|
|
194
|
+
- `deserializeElement(data)` — Reconstruct React elements from serialized data
|
|
195
|
+
|
|
115
196
|
### Types
|
|
116
197
|
|
|
117
198
|
```typescript
|
|
@@ -165,6 +246,46 @@ async function* generateFrames() {
|
|
|
165
246
|
const stream = annieStream(generateFrames());
|
|
166
247
|
```
|
|
167
248
|
|
|
249
|
+
### Pool-Based Frame Generation
|
|
250
|
+
|
|
251
|
+
For animation workloads with many frames, the worker pool provides significant speedups:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { createSatoriPool } from "@effing/satori/pool";
|
|
255
|
+
import { annieStream } from "@effing/annie";
|
|
256
|
+
|
|
257
|
+
const pool = createSatoriPool();
|
|
258
|
+
|
|
259
|
+
async function* generateFrames() {
|
|
260
|
+
const frames = Array.from({ length: 90 }, (_, i) => i / 89);
|
|
261
|
+
|
|
262
|
+
const results = await Promise.all(
|
|
263
|
+
frames.map((progress) =>
|
|
264
|
+
pool.renderToPng(
|
|
265
|
+
<div style={{
|
|
266
|
+
width: 1080,
|
|
267
|
+
height: 1920,
|
|
268
|
+
display: "flex",
|
|
269
|
+
alignItems: "center",
|
|
270
|
+
justifyContent: "center",
|
|
271
|
+
transform: `scale(${1 + 0.3 * progress})`,
|
|
272
|
+
fontSize: 72,
|
|
273
|
+
color: "white",
|
|
274
|
+
}}>
|
|
275
|
+
✨ Animated! ✨
|
|
276
|
+
</div>,
|
|
277
|
+
{ width: 1080, height: 1920, fonts }
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
for (const frame of results) yield frame;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const stream = annieStream(generateFrames());
|
|
286
|
+
await pool.destroy();
|
|
287
|
+
```
|
|
288
|
+
|
|
168
289
|
### Multiple Font Weights
|
|
169
290
|
|
|
170
291
|
```typescript
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/serde/index.ts
|
|
2
|
+
import React from "react";
|
|
3
|
+
var ELEMENT_MARKER = "__react_element__";
|
|
4
|
+
function expandElement(node) {
|
|
5
|
+
if (node == null || typeof node === "boolean") return node;
|
|
6
|
+
if (typeof node === "string" || typeof node === "number") return node;
|
|
7
|
+
if (Array.isArray(node)) return node.map(expandElement);
|
|
8
|
+
if (!React.isValidElement(node)) return node;
|
|
9
|
+
const element = node;
|
|
10
|
+
if (typeof element.type === "function") {
|
|
11
|
+
const result = element.type(
|
|
12
|
+
element.props
|
|
13
|
+
);
|
|
14
|
+
return expandElement(result);
|
|
15
|
+
}
|
|
16
|
+
const props = Object.assign({}, element.props);
|
|
17
|
+
if (props.children != null) {
|
|
18
|
+
props.children = expandElement(props.children);
|
|
19
|
+
}
|
|
20
|
+
return React.createElement(element.type, {
|
|
21
|
+
...props,
|
|
22
|
+
key: element.key
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function serializeElement(node) {
|
|
26
|
+
if (node == null || typeof node === "boolean") return node;
|
|
27
|
+
if (typeof node === "string" || typeof node === "number") return node;
|
|
28
|
+
if (Array.isArray(node)) return node.map(serializeElement);
|
|
29
|
+
if (!React.isValidElement(node)) return node;
|
|
30
|
+
const element = node;
|
|
31
|
+
if (typeof element.type === "function") {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Cannot serialize function component "${element.type.name || "anonymous"}". Call expandElement first.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const props = {};
|
|
37
|
+
for (const [key, value] of Object.entries(
|
|
38
|
+
element.props
|
|
39
|
+
)) {
|
|
40
|
+
if (key === "children") {
|
|
41
|
+
props.children = serializeElement(value);
|
|
42
|
+
} else if (value instanceof Buffer) {
|
|
43
|
+
props[key] = { __buffer__: true, data: Array.from(value) };
|
|
44
|
+
} else {
|
|
45
|
+
props[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
[ELEMENT_MARKER]: true,
|
|
50
|
+
type: element.type,
|
|
51
|
+
key: element.key,
|
|
52
|
+
props
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function deserializeElement(data) {
|
|
56
|
+
if (data == null || typeof data === "boolean") return data;
|
|
57
|
+
if (typeof data === "string" || typeof data === "number") return data;
|
|
58
|
+
if (Array.isArray(data)) return data.map(deserializeElement);
|
|
59
|
+
if (typeof data === "object" && data !== null && ELEMENT_MARKER in data) {
|
|
60
|
+
const serialized = data;
|
|
61
|
+
const props = {};
|
|
62
|
+
for (const [key, value] of Object.entries(serialized.props)) {
|
|
63
|
+
if (key === "children") {
|
|
64
|
+
props.children = deserializeElement(value);
|
|
65
|
+
} else if (typeof value === "object" && value !== null && "__buffer__" in value) {
|
|
66
|
+
props[key] = Buffer.from(value.data);
|
|
67
|
+
} else {
|
|
68
|
+
props[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return React.createElement(serialized.type, {
|
|
72
|
+
...props,
|
|
73
|
+
key: serialized.key
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
expandElement,
|
|
81
|
+
serializeElement,
|
|
82
|
+
deserializeElement
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=chunk-H5M6ZFOA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/serde/index.ts"],"sourcesContent":["import React, { type ReactElement, type ReactNode } from \"react\";\n\nconst ELEMENT_MARKER = \"__react_element__\";\n\ntype SerializedElement = {\n [ELEMENT_MARKER]: true;\n type: string;\n key: string | null;\n props: Record<string, unknown>;\n};\n\n/**\n * Recursively expand all function components in a React element tree\n * until only intrinsic elements (div, svg, path, img, etc.) remain.\n */\nexport function expandElement(node: ReactNode): ReactNode {\n if (node == null || typeof node === \"boolean\") return node;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node)) return node.map(expandElement);\n\n if (!React.isValidElement(node)) return node;\n\n const element = node as ReactElement;\n\n if (typeof element.type === \"function\") {\n const result = (element.type as (props: unknown) => ReactNode)(\n element.props,\n );\n return expandElement(result);\n }\n\n // Intrinsic element — recursively expand children\n const props = Object.assign({}, element.props) as Record<string, unknown>;\n if (props.children != null) {\n props.children = expandElement(props.children as ReactNode);\n }\n return React.createElement(element.type as string, {\n ...props,\n key: element.key,\n });\n}\n\n/**\n * Serialize an expanded React element tree to a structured-clone-safe format.\n * All function components must already be expanded to intrinsic elements.\n */\nexport function serializeElement(node: ReactNode): unknown {\n if (node == null || typeof node === \"boolean\") return node;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node)) return node.map(serializeElement);\n\n if (!React.isValidElement(node)) return node;\n\n const element = node as ReactElement;\n\n if (typeof element.type === \"function\") {\n throw new Error(\n `Cannot serialize function component \"${element.type.name || \"anonymous\"}\". ` +\n \"Call expandElement first.\",\n );\n }\n\n const props: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(\n element.props as Record<string, unknown>,\n )) {\n if (key === \"children\") {\n props.children = serializeElement(value as ReactNode);\n } else if (value instanceof Buffer) {\n props[key] = { __buffer__: true, data: Array.from(value) };\n } else {\n props[key] = value;\n }\n }\n\n return {\n [ELEMENT_MARKER]: true,\n type: element.type as string,\n key: element.key,\n props,\n } satisfies SerializedElement;\n}\n\n/**\n * Deserialize a serialized element tree back into React elements.\n */\nexport function deserializeElement(data: unknown): ReactNode {\n if (data == null || typeof data === \"boolean\") return data as ReactNode;\n if (typeof data === \"string\" || typeof data === \"number\") return data;\n if (Array.isArray(data)) return data.map(deserializeElement);\n\n if (typeof data === \"object\" && data !== null && ELEMENT_MARKER in data) {\n const serialized = data as SerializedElement;\n const props: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(serialized.props)) {\n if (key === \"children\") {\n props.children = deserializeElement(value);\n } else if (\n typeof value === \"object\" &&\n value !== null &&\n \"__buffer__\" in value\n ) {\n props[key] = Buffer.from((value as unknown as { data: number[] }).data);\n } else {\n props[key] = value;\n }\n }\n return React.createElement(serialized.type, {\n ...props,\n key: serialized.key,\n });\n }\n\n return data as ReactNode;\n}\n"],"mappings":";AAAA,OAAO,WAAkD;AAEzD,IAAM,iBAAiB;AAahB,SAAS,cAAc,MAA4B;AACxD,MAAI,QAAQ,QAAQ,OAAO,SAAS,UAAW,QAAO;AACtD,MAAI,OAAO,SAAS,YAAY,OAAO,SAAS,SAAU,QAAO;AACjE,MAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,IAAI,aAAa;AAEtD,MAAI,CAAC,MAAM,eAAe,IAAI,EAAG,QAAO;AAExC,QAAM,UAAU;AAEhB,MAAI,OAAO,QAAQ,SAAS,YAAY;AACtC,UAAM,SAAU,QAAQ;AAAA,MACtB,QAAQ;AAAA,IACV;AACA,WAAO,cAAc,MAAM;AAAA,EAC7B;AAGA,QAAM,QAAQ,OAAO,OAAO,CAAC,GAAG,QAAQ,KAAK;AAC7C,MAAI,MAAM,YAAY,MAAM;AAC1B,UAAM,WAAW,cAAc,MAAM,QAAqB;AAAA,EAC5D;AACA,SAAO,MAAM,cAAc,QAAQ,MAAgB;AAAA,IACjD,GAAG;AAAA,IACH,KAAK,QAAQ;AAAA,EACf,CAAC;AACH;AAMO,SAAS,iBAAiB,MAA0B;AACzD,MAAI,QAAQ,QAAQ,OAAO,SAAS,UAAW,QAAO;AACtD,MAAI,OAAO,SAAS,YAAY,OAAO,SAAS,SAAU,QAAO;AACjE,MAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,IAAI,gBAAgB;AAEzD,MAAI,CAAC,MAAM,eAAe,IAAI,EAAG,QAAO;AAExC,QAAM,UAAU;AAEhB,MAAI,OAAO,QAAQ,SAAS,YAAY;AACtC,UAAM,IAAI;AAAA,MACR,wCAAwC,QAAQ,KAAK,QAAQ,WAAW;AAAA,IAE1E;AAAA,EACF;AAEA,QAAM,QAAiC,CAAC;AACxC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO;AAAA,IAChC,QAAQ;AAAA,EACV,GAAG;AACD,QAAI,QAAQ,YAAY;AACtB,YAAM,WAAW,iBAAiB,KAAkB;AAAA,IACtD,WAAW,iBAAiB,QAAQ;AAClC,YAAM,GAAG,IAAI,EAAE,YAAY,MAAM,MAAM,MAAM,KAAK,KAAK,EAAE;AAAA,IAC3D,OAAO;AACL,YAAM,GAAG,IAAI;AAAA,IACf;AAAA,EACF;AAEA,SAAO;AAAA,IACL,CAAC,cAAc,GAAG;AAAA,IAClB,MAAM,QAAQ;AAAA,IACd,KAAK,QAAQ;AAAA,IACb;AAAA,EACF;AACF;AAKO,SAAS,mBAAmB,MAA0B;AAC3D,MAAI,QAAQ,QAAQ,OAAO,SAAS,UAAW,QAAO;AACtD,MAAI,OAAO,SAAS,YAAY,OAAO,SAAS,SAAU,QAAO;AACjE,MAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,IAAI,kBAAkB;AAE3D,MAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,kBAAkB,MAAM;AACvE,UAAM,aAAa;AACnB,UAAM,QAAiC,CAAC;AACxC,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,WAAW,KAAK,GAAG;AAC3D,UAAI,QAAQ,YAAY;AACtB,cAAM,WAAW,mBAAmB,KAAK;AAAA,MAC3C,WACE,OAAO,UAAU,YACjB,UAAU,QACV,gBAAgB,OAChB;AACA,cAAM,GAAG,IAAI,OAAO,KAAM,MAAwC,IAAI;AAAA,MACxE,OAAO;AACL,cAAM,GAAG,IAAI;AAAA,MACf;AAAA,IACF;AACA,WAAO,MAAM,cAAc,WAAW,MAAM;AAAA,MAC1C,GAAG;AAAA,MACH,KAAK,WAAW;AAAA,IAClB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import satori from 'satori';
|
|
2
|
+
import { E as EmojiStyle } from './emoji-CtRDUFb8.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Font data for satori rendering
|
|
@@ -10,13 +11,9 @@ type FontData = {
|
|
|
10
11
|
style: "normal" | "italic";
|
|
11
12
|
};
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* Options for satori rendering functions
|
|
14
15
|
*/
|
|
15
|
-
type
|
|
16
|
-
/**
|
|
17
|
-
* Options for pngFromSatori
|
|
18
|
-
*/
|
|
19
|
-
type PngFromSatoriOptions = {
|
|
16
|
+
type SatoriOptions = {
|
|
20
17
|
/** Frame width in pixels */
|
|
21
18
|
width: number;
|
|
22
19
|
/** Frame height in pixels */
|
|
@@ -26,6 +23,25 @@ type PngFromSatoriOptions = {
|
|
|
26
23
|
/** Emoji style to use (default: "twemoji") */
|
|
27
24
|
emoji?: EmojiStyle;
|
|
28
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* @deprecated Use `SatoriOptions` instead
|
|
28
|
+
*/
|
|
29
|
+
type PngFromSatoriOptions = SatoriOptions;
|
|
30
|
+
/**
|
|
31
|
+
* Render a React/JSX template to an SVG string using Satori
|
|
32
|
+
*
|
|
33
|
+
* @param template React element to render
|
|
34
|
+
* @param options Rendering options
|
|
35
|
+
* @returns SVG markup as a string
|
|
36
|
+
*/
|
|
37
|
+
declare function svgFromSatori(template: Parameters<typeof satori>[0], { width, height, fonts, emoji }: SatoriOptions): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Rasterize an SVG string to a PNG buffer using Resvg
|
|
40
|
+
*
|
|
41
|
+
* @param svg SVG markup string
|
|
42
|
+
* @returns PNG image as a Buffer
|
|
43
|
+
*/
|
|
44
|
+
declare function rasterizeSvg(svg: string): Buffer;
|
|
29
45
|
/**
|
|
30
46
|
* Render a React/JSX template to a PNG buffer using Satori
|
|
31
47
|
*
|
|
@@ -41,6 +57,6 @@ type PngFromSatoriOptions = {
|
|
|
41
57
|
* );
|
|
42
58
|
* ```
|
|
43
59
|
*/
|
|
44
|
-
declare function pngFromSatori(template: Parameters<typeof satori>[0],
|
|
60
|
+
declare function pngFromSatori(template: Parameters<typeof satori>[0], options: SatoriOptions): Promise<Buffer>;
|
|
45
61
|
|
|
46
|
-
export {
|
|
62
|
+
export { EmojiStyle, type FontData, type PngFromSatoriOptions, type SatoriOptions, pngFromSatori, rasterizeSvg, svgFromSatori };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Resvg } from "@resvg/resvg-js";
|
|
3
3
|
import satori from "satori";
|
|
4
|
+
|
|
5
|
+
// src/emoji.ts
|
|
4
6
|
var emojiApis = {
|
|
5
7
|
twemoji: (code) => `https://cdnjs.cloudflare.com/ajax/libs/twemoji/16.0.1/svg/${code.toLowerCase()}.svg`,
|
|
6
8
|
openmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/",
|
|
@@ -42,22 +44,35 @@ async function loadEmoji(type, code) {
|
|
|
42
44
|
(r) => r.text()
|
|
43
45
|
);
|
|
44
46
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
function makeLoadAdditionalAsset(emoji) {
|
|
48
|
+
return async (code, segment) => {
|
|
49
|
+
if (code === "emoji") {
|
|
50
|
+
return "data:image/svg+xml;base64," + btoa(await loadEmoji(emoji, getEmojiCode(segment)));
|
|
51
|
+
}
|
|
52
|
+
return segment;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/index.ts
|
|
57
|
+
async function svgFromSatori(template, { width, height, fonts, emoji = "twemoji" }) {
|
|
58
|
+
return satori(template, {
|
|
47
59
|
width,
|
|
48
60
|
height,
|
|
49
61
|
fonts,
|
|
50
|
-
loadAdditionalAsset:
|
|
51
|
-
if (code === "emoji") {
|
|
52
|
-
return "data:image/svg+xml;base64," + btoa(await loadEmoji(emoji, getEmojiCode(segment)));
|
|
53
|
-
}
|
|
54
|
-
return segment;
|
|
55
|
-
}
|
|
62
|
+
loadAdditionalAsset: makeLoadAdditionalAsset(emoji)
|
|
56
63
|
});
|
|
57
|
-
|
|
64
|
+
}
|
|
65
|
+
function rasterizeSvg(svg) {
|
|
66
|
+
const resvg = new Resvg(svg, { font: { loadSystemFonts: false } });
|
|
58
67
|
return resvg.render().asPng();
|
|
59
68
|
}
|
|
69
|
+
async function pngFromSatori(template, options) {
|
|
70
|
+
const svg = await svgFromSatori(template, options);
|
|
71
|
+
return rasterizeSvg(svg);
|
|
72
|
+
}
|
|
60
73
|
export {
|
|
61
|
-
pngFromSatori
|
|
74
|
+
pngFromSatori,
|
|
75
|
+
rasterizeSvg,
|
|
76
|
+
svgFromSatori
|
|
62
77
|
};
|
|
63
78
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Resvg } from \"@resvg/resvg-js\";\nimport satori from \"satori\";\n\n/**\n * Font data for satori rendering\n */\nexport type FontData = {\n name: string;\n data: Buffer | ArrayBuffer;\n weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;\n style: \"normal\" | \"italic\";\n};\n\n/**\n * Emoji style options for rendering\n */\nexport type EmojiStyle =\n | \"twemoji\"\n | \"openmoji\"\n | \"blobmoji\"\n | \"noto\"\n | \"fluent\"\n | \"fluentFlat\";\n\
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/emoji.ts"],"sourcesContent":["import { Resvg } from \"@resvg/resvg-js\";\nimport satori from \"satori\";\n\nimport { makeLoadAdditionalAsset } from \"./emoji.ts\";\nimport type { EmojiStyle } from \"./emoji.ts\";\n\nexport type { EmojiStyle } from \"./emoji.ts\";\n\n/**\n * Font data for satori rendering\n */\nexport type FontData = {\n name: string;\n data: Buffer | ArrayBuffer;\n weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;\n style: \"normal\" | \"italic\";\n};\n\n/**\n * Options for satori rendering functions\n */\nexport type SatoriOptions = {\n /** Frame width in pixels */\n width: number;\n /** Frame height in pixels */\n height: number;\n /** Font data for text rendering */\n fonts: FontData[];\n /** Emoji style to use (default: \"twemoji\") */\n emoji?: EmojiStyle;\n};\n\n/**\n * @deprecated Use `SatoriOptions` instead\n */\nexport type PngFromSatoriOptions = SatoriOptions;\n\n/**\n * Render a React/JSX template to an SVG string using Satori\n *\n * @param template React element to render\n * @param options Rendering options\n * @returns SVG markup as a string\n */\nexport async function svgFromSatori(\n template: Parameters<typeof satori>[0],\n { width, height, fonts, emoji = \"twemoji\" }: SatoriOptions,\n): Promise<string> {\n return satori(template, {\n width,\n height,\n fonts,\n loadAdditionalAsset: makeLoadAdditionalAsset(emoji),\n });\n}\n\n/**\n * Rasterize an SVG string to a PNG buffer using Resvg\n *\n * @param svg SVG markup string\n * @returns PNG image as a Buffer\n */\nexport function rasterizeSvg(svg: string): Buffer {\n const resvg = new Resvg(svg, { font: { loadSystemFonts: false } });\n return resvg.render().asPng();\n}\n\n/**\n * Render a React/JSX template to a PNG buffer using Satori\n *\n * @param template React element to render\n * @param options Rendering options\n * @returns PNG image as a Buffer\n *\n * @example\n * ```tsx\n * const png = await pngFromSatori(\n * <div style={{ fontSize: 48, color: \"white\" }}>Hello World</div>,\n * { width: 1080, height: 1080, fonts: [myFont] }\n * );\n * ```\n */\nexport async function pngFromSatori(\n template: Parameters<typeof satori>[0],\n options: SatoriOptions,\n): Promise<Buffer> {\n const svg = await svgFromSatori(template, options);\n return rasterizeSvg(svg);\n}\n","/**\n * Emoji style options for rendering\n */\nexport type EmojiStyle =\n | \"twemoji\"\n | \"openmoji\"\n | \"blobmoji\"\n | \"noto\"\n | \"fluent\"\n | \"fluentFlat\";\n\nexport const emojiApis: Record<\n EmojiStyle,\n string | ((code: string) => string)\n> = {\n twemoji: (code: string) =>\n `https://cdnjs.cloudflare.com/ajax/libs/twemoji/16.0.1/svg/${code.toLowerCase()}.svg`,\n openmoji: \"https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/\",\n blobmoji: \"https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/\",\n noto: \"https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/\",\n fluent: (code: string) =>\n `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_color.svg`,\n fluentFlat: (code: string) =>\n `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_flat.svg`,\n};\n\nconst U200D = String.fromCharCode(8205);\nconst UFE0Fg = /\\uFE0F/g;\n\nexport function getEmojiCode(char: string): string {\n return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, \"\") : char);\n}\n\nexport function toCodePoint(unicodeSurrogates: string): string {\n const r: string[] = [];\n let c = 0,\n p = 0,\n i = 0;\n\n while (i < unicodeSurrogates.length) {\n c = unicodeSurrogates.charCodeAt(i++);\n if (p) {\n r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16));\n p = 0;\n } else if (55296 <= c && c <= 56319) {\n p = c;\n } else {\n r.push(c.toString(16));\n }\n }\n return r.join(\"-\");\n}\n\nconst emojiCache: Record<string, Promise<string>> = {};\n\nexport async function loadEmoji(\n type: EmojiStyle,\n code: string,\n): Promise<string> {\n const key = type + \":\" + code;\n if (key in emojiCache) return emojiCache[key];\n\n const api = emojiApis[type];\n if (typeof api === \"function\") {\n return (emojiCache[key] = fetch(api(code)).then((r) => r.text()));\n }\n return (emojiCache[key] = fetch(`${api}${code.toUpperCase()}.svg`).then((r) =>\n r.text(),\n ));\n}\n\nexport function makeLoadAdditionalAsset(emoji: EmojiStyle) {\n return async (code: string, segment: string) => {\n if (code === \"emoji\") {\n return (\n \"data:image/svg+xml;base64,\" +\n btoa(await loadEmoji(emoji, getEmojiCode(segment)))\n );\n }\n return segment;\n };\n}\n"],"mappings":";AAAA,SAAS,aAAa;AACtB,OAAO,YAAY;;;ACUZ,IAAM,YAGT;AAAA,EACF,SAAS,CAAC,SACR,6DAA6D,KAAK,YAAY,CAAC;AAAA,EACjF,UAAU;AAAA,EACV,UAAU;AAAA,EACV,MAAM;AAAA,EACN,QAAQ,CAAC,SACP,qEAAqE,KAAK,YAAY,CAAC;AAAA,EACzF,YAAY,CAAC,SACX,qEAAqE,KAAK,YAAY,CAAC;AAC3F;AAEA,IAAM,QAAQ,OAAO,aAAa,IAAI;AACtC,IAAM,SAAS;AAER,SAAS,aAAa,MAAsB;AACjD,SAAO,YAAY,KAAK,QAAQ,KAAK,IAAI,IAAI,KAAK,QAAQ,QAAQ,EAAE,IAAI,IAAI;AAC9E;AAEO,SAAS,YAAY,mBAAmC;AAC7D,QAAM,IAAc,CAAC;AACrB,MAAI,IAAI,GACN,IAAI,GACJ,IAAI;AAEN,SAAO,IAAI,kBAAkB,QAAQ;AACnC,QAAI,kBAAkB,WAAW,GAAG;AACpC,QAAI,GAAG;AACL,QAAE,MAAM,SAAU,IAAI,SAAU,OAAO,IAAI,QAAQ,SAAS,EAAE,CAAC;AAC/D,UAAI;AAAA,IACN,WAAW,SAAS,KAAK,KAAK,OAAO;AACnC,UAAI;AAAA,IACN,OAAO;AACL,QAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAAA,IACvB;AAAA,EACF;AACA,SAAO,EAAE,KAAK,GAAG;AACnB;AAEA,IAAM,aAA8C,CAAC;AAErD,eAAsB,UACpB,MACA,MACiB;AACjB,QAAM,MAAM,OAAO,MAAM;AACzB,MAAI,OAAO,WAAY,QAAO,WAAW,GAAG;AAE5C,QAAM,MAAM,UAAU,IAAI;AAC1B,MAAI,OAAO,QAAQ,YAAY;AAC7B,WAAQ,WAAW,GAAG,IAAI,MAAM,IAAI,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC;AAAA,EACjE;AACA,SAAQ,WAAW,GAAG,IAAI,MAAM,GAAG,GAAG,GAAG,KAAK,YAAY,CAAC,MAAM,EAAE;AAAA,IAAK,CAAC,MACvE,EAAE,KAAK;AAAA,EACT;AACF;AAEO,SAAS,wBAAwB,OAAmB;AACzD,SAAO,OAAO,MAAc,YAAoB;AAC9C,QAAI,SAAS,SAAS;AACpB,aACE,+BACA,KAAK,MAAM,UAAU,OAAO,aAAa,OAAO,CAAC,CAAC;AAAA,IAEtD;AACA,WAAO;AAAA,EACT;AACF;;;ADrCA,eAAsB,cACpB,UACA,EAAE,OAAO,QAAQ,OAAO,QAAQ,UAAU,GACzB;AACjB,SAAO,OAAO,UAAU;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA,qBAAqB,wBAAwB,KAAK;AAAA,EACpD,CAAC;AACH;AAQO,SAAS,aAAa,KAAqB;AAChD,QAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,MAAM,EAAE,iBAAiB,MAAM,EAAE,CAAC;AACjE,SAAO,MAAM,OAAO,EAAE,MAAM;AAC9B;AAiBA,eAAsB,cACpB,UACA,SACiB;AACjB,QAAM,MAAM,MAAM,cAAc,UAAU,OAAO;AACjD,SAAO,aAAa,GAAG;AACzB;","names":[]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import satori from 'satori';
|
|
2
|
+
import { E as EmojiStyle } from '../emoji-CtRDUFb8.js';
|
|
3
|
+
|
|
4
|
+
type SatoriPoolOptions = {
|
|
5
|
+
/** Minimum number of worker threads (default: 1) */
|
|
6
|
+
minThreads?: number;
|
|
7
|
+
/** Maximum number of worker threads (default: os.cpus().length) */
|
|
8
|
+
maxThreads?: number;
|
|
9
|
+
};
|
|
10
|
+
type SatoriPool = {
|
|
11
|
+
/** Render a serialized React element to PNG via the worker pool */
|
|
12
|
+
renderToPng(element: Parameters<typeof satori>[0], options: {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
fonts: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
data: Buffer;
|
|
18
|
+
weight: number;
|
|
19
|
+
style: string;
|
|
20
|
+
}>;
|
|
21
|
+
emoji?: EmojiStyle;
|
|
22
|
+
}): Promise<Buffer>;
|
|
23
|
+
/** Render a serialized React element to SVG via the worker pool */
|
|
24
|
+
renderToSvg(element: Parameters<typeof satori>[0], options: {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
fonts: Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
data: Buffer;
|
|
30
|
+
weight: number;
|
|
31
|
+
style: string;
|
|
32
|
+
}>;
|
|
33
|
+
emoji?: EmojiStyle;
|
|
34
|
+
}): Promise<string>;
|
|
35
|
+
/** Rasterize an SVG string to PNG via the worker pool */
|
|
36
|
+
rasterizeSvgToPng(svg: string, options?: {
|
|
37
|
+
fitTo?: {
|
|
38
|
+
mode: "original";
|
|
39
|
+
} | {
|
|
40
|
+
mode: "width";
|
|
41
|
+
value: number;
|
|
42
|
+
} | {
|
|
43
|
+
mode: "height";
|
|
44
|
+
value: number;
|
|
45
|
+
} | {
|
|
46
|
+
mode: "zoom";
|
|
47
|
+
value: number;
|
|
48
|
+
};
|
|
49
|
+
crop?: {
|
|
50
|
+
left: number;
|
|
51
|
+
top: number;
|
|
52
|
+
right?: number;
|
|
53
|
+
bottom?: number;
|
|
54
|
+
};
|
|
55
|
+
}): Promise<Buffer>;
|
|
56
|
+
/** Shut down the worker pool */
|
|
57
|
+
destroy(): Promise<void>;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Create a worker pool for parallelized satori rendering.
|
|
61
|
+
*
|
|
62
|
+
* @param options Pool configuration
|
|
63
|
+
* @returns A `SatoriPool` with `renderToPng`, `renderToSvg`, `rasterizeSvgToPng`, and `destroy`
|
|
64
|
+
*/
|
|
65
|
+
declare function createSatoriPool(options?: SatoriPoolOptions): SatoriPool;
|
|
66
|
+
|
|
67
|
+
export { type SatoriPool, type SatoriPoolOptions, createSatoriPool };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expandElement,
|
|
3
|
+
serializeElement
|
|
4
|
+
} from "../chunk-H5M6ZFOA.js";
|
|
5
|
+
|
|
6
|
+
// src/pool/index.ts
|
|
7
|
+
import os from "os";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import Tinypool from "tinypool";
|
|
11
|
+
function createSatoriPool(options) {
|
|
12
|
+
const workerFile = path.resolve(
|
|
13
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
14
|
+
"../worker/index.js"
|
|
15
|
+
);
|
|
16
|
+
const pool = new Tinypool({
|
|
17
|
+
filename: workerFile,
|
|
18
|
+
minThreads: options?.minThreads ?? 1,
|
|
19
|
+
maxThreads: options?.maxThreads ?? os.cpus().length
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
async renderToPng(element, opts) {
|
|
23
|
+
const serialized = serializeElement(expandElement(element));
|
|
24
|
+
const result = await pool.run(
|
|
25
|
+
{
|
|
26
|
+
element: serialized,
|
|
27
|
+
width: opts.width,
|
|
28
|
+
height: opts.height,
|
|
29
|
+
fonts: opts.fonts,
|
|
30
|
+
emoji: opts.emoji
|
|
31
|
+
},
|
|
32
|
+
{ name: "renderToPng" }
|
|
33
|
+
);
|
|
34
|
+
return Buffer.from(result);
|
|
35
|
+
},
|
|
36
|
+
async renderToSvg(element, opts) {
|
|
37
|
+
const serialized = serializeElement(expandElement(element));
|
|
38
|
+
return pool.run(
|
|
39
|
+
{
|
|
40
|
+
element: serialized,
|
|
41
|
+
width: opts.width,
|
|
42
|
+
height: opts.height,
|
|
43
|
+
fonts: opts.fonts,
|
|
44
|
+
emoji: opts.emoji
|
|
45
|
+
},
|
|
46
|
+
{ name: "renderToSvg" }
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
async rasterizeSvgToPng(svg, opts) {
|
|
50
|
+
const result = await pool.run(
|
|
51
|
+
{ svg, options: opts },
|
|
52
|
+
{ name: "rasterizeSvgToPng" }
|
|
53
|
+
);
|
|
54
|
+
return Buffer.from(result);
|
|
55
|
+
},
|
|
56
|
+
async destroy() {
|
|
57
|
+
await pool.destroy();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
createSatoriPool
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/pool/index.ts"],"sourcesContent":["import os from \"os\";\nimport { fileURLToPath } from \"url\";\nimport path from \"path\";\n\nimport type { ReactNode } from \"react\";\nimport type satori from \"satori\";\nimport Tinypool from \"tinypool\";\n\nimport type { EmojiStyle } from \"../emoji.ts\";\nimport { expandElement, serializeElement } from \"../serde/index.ts\";\n\nexport type SatoriPoolOptions = {\n /** Minimum number of worker threads (default: 1) */\n minThreads?: number;\n /** Maximum number of worker threads (default: os.cpus().length) */\n maxThreads?: number;\n};\n\nexport type SatoriPool = {\n /** Render a serialized React element to PNG via the worker pool */\n renderToPng(\n element: Parameters<typeof satori>[0],\n options: {\n width: number;\n height: number;\n fonts: Array<{\n name: string;\n data: Buffer;\n weight: number;\n style: string;\n }>;\n emoji?: EmojiStyle;\n },\n ): Promise<Buffer>;\n\n /** Render a serialized React element to SVG via the worker pool */\n renderToSvg(\n element: Parameters<typeof satori>[0],\n options: {\n width: number;\n height: number;\n fonts: Array<{\n name: string;\n data: Buffer;\n weight: number;\n style: string;\n }>;\n emoji?: EmojiStyle;\n },\n ): Promise<string>;\n\n /** Rasterize an SVG string to PNG via the worker pool */\n rasterizeSvgToPng(\n svg: string,\n options?: {\n fitTo?:\n | { mode: \"original\" }\n | { mode: \"width\"; value: number }\n | { mode: \"height\"; value: number }\n | { mode: \"zoom\"; value: number };\n crop?: {\n left: number;\n top: number;\n right?: number;\n bottom?: number;\n };\n },\n ): Promise<Buffer>;\n\n /** Shut down the worker pool */\n destroy(): Promise<void>;\n};\n\n/**\n * Create a worker pool for parallelized satori rendering.\n *\n * @param options Pool configuration\n * @returns A `SatoriPool` with `renderToPng`, `renderToSvg`, `rasterizeSvgToPng`, and `destroy`\n */\nexport function createSatoriPool(options?: SatoriPoolOptions): SatoriPool {\n const workerFile = path.resolve(\n path.dirname(fileURLToPath(import.meta.url)),\n \"../worker/index.js\",\n );\n\n const pool = new Tinypool({\n filename: workerFile,\n minThreads: options?.minThreads ?? 1,\n maxThreads: options?.maxThreads ?? os.cpus().length,\n });\n\n return {\n async renderToPng(element, opts) {\n const serialized = serializeElement(expandElement(element as ReactNode));\n const result = await pool.run(\n {\n element: serialized,\n width: opts.width,\n height: opts.height,\n fonts: opts.fonts,\n emoji: opts.emoji,\n },\n { name: \"renderToPng\" },\n );\n return Buffer.from(result);\n },\n\n async renderToSvg(element, opts) {\n const serialized = serializeElement(expandElement(element as ReactNode));\n return pool.run(\n {\n element: serialized,\n width: opts.width,\n height: opts.height,\n fonts: opts.fonts,\n emoji: opts.emoji,\n },\n { name: \"renderToSvg\" },\n );\n },\n\n async rasterizeSvgToPng(svg, opts) {\n const result = await pool.run(\n { svg, options: opts },\n { name: \"rasterizeSvgToPng\" },\n );\n return Buffer.from(result);\n },\n\n async destroy() {\n await pool.destroy();\n },\n };\n}\n"],"mappings":";;;;;;AAAA,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AAIjB,OAAO,cAAc;AAyEd,SAAS,iBAAiB,SAAyC;AACxE,QAAM,aAAa,KAAK;AAAA,IACtB,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAAA,IAC3C;AAAA,EACF;AAEA,QAAM,OAAO,IAAI,SAAS;AAAA,IACxB,UAAU;AAAA,IACV,YAAY,SAAS,cAAc;AAAA,IACnC,YAAY,SAAS,cAAc,GAAG,KAAK,EAAE;AAAA,EAC/C,CAAC;AAED,SAAO;AAAA,IACL,MAAM,YAAY,SAAS,MAAM;AAC/B,YAAM,aAAa,iBAAiB,cAAc,OAAoB,CAAC;AACvE,YAAM,SAAS,MAAM,KAAK;AAAA,QACxB;AAAA,UACE,SAAS;AAAA,UACT,OAAO,KAAK;AAAA,UACZ,QAAQ,KAAK;AAAA,UACb,OAAO,KAAK;AAAA,UACZ,OAAO,KAAK;AAAA,QACd;AAAA,QACA,EAAE,MAAM,cAAc;AAAA,MACxB;AACA,aAAO,OAAO,KAAK,MAAM;AAAA,IAC3B;AAAA,IAEA,MAAM,YAAY,SAAS,MAAM;AAC/B,YAAM,aAAa,iBAAiB,cAAc,OAAoB,CAAC;AACvE,aAAO,KAAK;AAAA,QACV;AAAA,UACE,SAAS;AAAA,UACT,OAAO,KAAK;AAAA,UACZ,QAAQ,KAAK;AAAA,UACb,OAAO,KAAK;AAAA,UACZ,OAAO,KAAK;AAAA,QACd;AAAA,QACA,EAAE,MAAM,cAAc;AAAA,MACxB;AAAA,IACF;AAAA,IAEA,MAAM,kBAAkB,KAAK,MAAM;AACjC,YAAM,SAAS,MAAM,KAAK;AAAA,QACxB,EAAE,KAAK,SAAS,KAAK;AAAA,QACrB,EAAE,MAAM,oBAAoB;AAAA,MAC9B;AACA,aAAO,OAAO,KAAK,MAAM;AAAA,IAC3B;AAAA,IAEA,MAAM,UAAU;AACd,YAAM,KAAK,QAAQ;AAAA,IACrB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recursively expand all function components in a React element tree
|
|
5
|
+
* until only intrinsic elements (div, svg, path, img, etc.) remain.
|
|
6
|
+
*/
|
|
7
|
+
declare function expandElement(node: ReactNode): ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Serialize an expanded React element tree to a structured-clone-safe format.
|
|
10
|
+
* All function components must already be expanded to intrinsic elements.
|
|
11
|
+
*/
|
|
12
|
+
declare function serializeElement(node: ReactNode): unknown;
|
|
13
|
+
/**
|
|
14
|
+
* Deserialize a serialized element tree back into React elements.
|
|
15
|
+
*/
|
|
16
|
+
declare function deserializeElement(data: unknown): ReactNode;
|
|
17
|
+
|
|
18
|
+
export { deserializeElement, expandElement, serializeElement };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|