@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 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: PngFromSatoriOptions,
132
+ options: SatoriOptions,
103
133
  ): Promise<Buffer>;
104
134
  ```
105
135
 
106
- **Options:**
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":[]}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Emoji style options for rendering
3
+ */
4
+ type EmojiStyle = "twemoji" | "openmoji" | "blobmoji" | "noto" | "fluent" | "fluentFlat";
5
+
6
+ export type { EmojiStyle as E };
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
- * Emoji style options for rendering
14
+ * Options for satori rendering functions
14
15
  */
15
- type EmojiStyle = "twemoji" | "openmoji" | "blobmoji" | "noto" | "fluent" | "fluentFlat";
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], { width, height, fonts, emoji }: PngFromSatoriOptions): Promise<Buffer>;
60
+ declare function pngFromSatori(template: Parameters<typeof satori>[0], options: SatoriOptions): Promise<Buffer>;
45
61
 
46
- export { type EmojiStyle, type FontData, type PngFromSatoriOptions, pngFromSatori };
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
- async function pngFromSatori(template, { width, height, fonts, emoji = "twemoji" }) {
46
- const overlaySvg = await satori(template, {
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: async (code, segment) => {
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
- const resvg = new Resvg(overlaySvg, { font: { loadSystemFonts: false } });
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\nconst emojiApis: Record<EmojiStyle, string | ((code: string) => string)> = {\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\nfunction getEmojiCode(char: string): string {\n return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, \"\") : char);\n}\n\nfunction 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\nasync function loadEmoji(type: EmojiStyle, code: string): 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\n/**\n * Options for pngFromSatori\n */\nexport type PngFromSatoriOptions = {\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 * 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 { width, height, fonts, emoji = \"twemoji\" }: PngFromSatoriOptions,\n): Promise<Buffer> {\n const overlaySvg = await satori(template, {\n width,\n height,\n fonts,\n loadAdditionalAsset: 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 const resvg = new Resvg(overlaySvg, { font: { loadSystemFonts: false } });\n return resvg.render().asPng();\n}\n"],"mappings":";AAAA,SAAS,aAAa;AACtB,OAAO,YAAY;AAuBnB,IAAM,YAAqE;AAAA,EACzE,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;AAEf,SAAS,aAAa,MAAsB;AAC1C,SAAO,YAAY,KAAK,QAAQ,KAAK,IAAI,IAAI,KAAK,QAAQ,QAAQ,EAAE,IAAI,IAAI;AAC9E;AAEA,SAAS,YAAY,mBAAmC;AACtD,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,eAAe,UAAU,MAAkB,MAA+B;AACxE,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;AA+BA,eAAsB,cACpB,UACA,EAAE,OAAO,QAAQ,OAAO,QAAQ,UAAU,GACzB;AACjB,QAAM,aAAa,MAAM,OAAO,UAAU;AAAA,IACxC;AAAA,IACA;AAAA,IACA;AAAA,IACA,qBAAqB,OAAO,MAAc,YAAoB;AAC5D,UAAI,SAAS,SAAS;AACpB,eACE,+BACA,KAAK,MAAM,UAAU,OAAO,aAAa,OAAO,CAAC,CAAC;AAAA,MAEtD;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACD,QAAM,QAAQ,IAAI,MAAM,YAAY,EAAE,MAAM,EAAE,iBAAiB,MAAM,EAAE,CAAC;AACxE,SAAO,MAAM,OAAO,EAAE,MAAM;AAC9B;","names":[]}
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,11 @@
1
+ import {
2
+ deserializeElement,
3
+ expandElement,
4
+ serializeElement
5
+ } from "../chunk-H5M6ZFOA.js";
6
+ export {
7
+ deserializeElement,
8
+ expandElement,
9
+ serializeElement
10
+ };
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}