@effing/satori 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -118,7 +118,7 @@ const png = await pool.renderToPng(
118
118
  await pool.destroy();
119
119
  ```
120
120
 
121
- **Peer dependencies:** The pool and serde sub-paths require `react` to be installed. It is listed as an optional peer dependency so the main `@effing/satori` entry works without it.
121
+ **Peer dependencies:** The pool and elements sub-paths require `react` to be installed. It is listed as an optional peer dependency so the main `@effing/satori` entry works without it.
122
122
 
123
123
  ### Vite Plugin (SSR)
124
124
 
@@ -212,7 +212,7 @@ Vite plugin that bundles the satori worker into the SSR output. Required for pro
212
212
  function satoriPoolPlugin(): Plugin;
213
213
  ```
214
214
 
215
- ### `@effing/satori/serde`
215
+ ### `@effing/satori/elements`
216
216
 
217
217
  React element serialization for cross-thread communication. Used internally by the pool, but available for custom worker setups.
218
218
 
@@ -1,4 +1,4 @@
1
- // src/serde/index.ts
1
+ // src/elements/index.ts
2
2
  import React from "react";
3
3
  function ensureSingleElement(node) {
4
4
  if (Array.isArray(node)) {
@@ -107,4 +107,4 @@ export {
107
107
  serializeElement,
108
108
  deserializeElement
109
109
  };
110
- //# sourceMappingURL=chunk-W5PCQTH3.js.map
110
+ //# sourceMappingURL=chunk-RYGCKNPA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/elements/index.ts"],"sourcesContent":["import React, { type ReactElement, type ReactNode } from \"react\";\n\n/**\n * Wrap a root-level array (e.g. from a top-level Fragment) in a\n * layout-transparent div so satori always receives a single element.\n */\nexport function ensureSingleElement(node: ReactNode): ReactNode {\n if (Array.isArray(node)) {\n return React.createElement(\n \"div\",\n { style: { display: \"contents\" } },\n ...node,\n );\n }\n return node;\n}\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)) {\n return node.flatMap((child) => {\n const result = expandElement(child);\n return Array.isArray(result) ? result : [result];\n });\n }\n\n if (!React.isValidElement(node)) return node;\n\n const element = node as ReactElement;\n\n if (element.type === React.Fragment) {\n const children = (element.props as Record<string, unknown>)\n .children as ReactNode;\n if (children == null) return null;\n return expandElement(children);\n }\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 if (typeof element.type !== \"string\") {\n throw new Error(\n `Cannot serialize element with type \"${String(element.type)}\". 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":"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","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  ensureSingleElement,
4
4
  expandElement,
5
5
  serializeElement
6
- } from "../chunk-W5PCQTH3.js";
6
+ } from "../chunk-RYGCKNPA.js";
7
7
  export {
8
8
  deserializeElement,
9
9
  ensureSingleElement,
@@ -2,7 +2,7 @@ import {
2
2
  ensureSingleElement,
3
3
  expandElement,
4
4
  serializeElement
5
- } from "../chunk-W5PCQTH3.js";
5
+ } from "../chunk-RYGCKNPA.js";
6
6
 
7
7
  // src/pool/index.ts
8
8
  import os from "os";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pool/index.ts","../../src/pool/worker-pool.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\";\n\nimport type { EmojiStyle } from \"../emoji.ts\";\nimport {\n ensureSingleElement,\n expandElement,\n serializeElement,\n} from \"../serde/index.ts\";\nimport { WorkerPool } from \"./worker-pool.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 /** Absolute path to a bundled worker file (set automatically by the Vite plugin) */\n workerFile?: string;\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 | ArrayBuffer;\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 | ArrayBuffer;\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 =\n options?.workerFile ??\n path.resolve(\n path.dirname(fileURLToPath(import.meta.url)),\n \"../worker/index.js\",\n );\n\n const pool = new WorkerPool({\n 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(\n ensureSingleElement(expandElement(element as ReactNode)),\n );\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 )) as Uint8Array;\n return Buffer.from(result);\n },\n\n async renderToSvg(element, opts) {\n const serialized = serializeElement(\n ensureSingleElement(expandElement(element as ReactNode)),\n );\n return (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: \"renderToSvg\" },\n )) as string;\n },\n\n async rasterizeSvgToPng(svg, opts) {\n const result = (await pool.run(\n { svg, options: opts },\n { name: \"rasterizeSvgToPng\" },\n )) as Uint8Array;\n return Buffer.from(result);\n },\n\n async destroy() {\n await pool.destroy();\n },\n };\n}\n","import { Worker, type WorkerOptions } from \"node:worker_threads\";\nimport { pathToFileURL } from \"node:url\";\n\ninterface Task {\n id: number;\n name: string;\n args: unknown[];\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n}\n\ninterface WorkerEntry {\n worker: Worker;\n busy: boolean;\n}\n\nexport interface WorkerPoolOptions {\n workerFile: string;\n minThreads?: number;\n maxThreads?: number;\n}\n\n// CJS bootstrap that dynamically imports the ESM worker module, then\n// wires up parentPort message handling for { id, name, args } calls.\nconst BOOTSTRAP = `\n\"use strict\";\nconst { parentPort, workerData } = require(\"node:worker_threads\");\n\nconst modPromise = import(workerData.workerUrl)\n .catch((err) => {\n parentPort.postMessage({\n id: -1,\n error: { name: \"InitError\", message: err.message, stack: err.stack },\n });\n process.exit(1);\n });\n\nparentPort.on(\"message\", async (msg) => {\n const { id, name, args } = msg;\n try {\n const mod = await modPromise;\n const result = await mod[name](...args);\n parentPort.postMessage({ id, result });\n } catch (err) {\n parentPort.postMessage({\n id,\n error: {\n name: err?.name ?? \"Error\",\n message: err?.message ?? String(err),\n stack: err?.stack,\n code: err?.code,\n },\n });\n }\n});\n`.trim();\n\nexport class WorkerPool {\n private readonly workerUrl: string;\n private readonly workerOpts: WorkerOptions;\n private readonly minThreads: number;\n private readonly maxThreads: number;\n private readonly workers: WorkerEntry[] = [];\n private readonly queue: Task[] = [];\n private nextId = 0;\n private destroyed = false;\n\n constructor(options: WorkerPoolOptions) {\n this.workerUrl = pathToFileURL(options.workerFile).href;\n this.minThreads = options.minThreads ?? 1;\n this.maxThreads = options.maxThreads ?? 1;\n this.workerOpts = {\n eval: true,\n workerData: { workerUrl: this.workerUrl },\n };\n\n // Pre-spawn minimum workers\n for (let i = 0; i < this.minThreads; i++) {\n this.workers.push(this.spawnWorker());\n }\n }\n\n run(args: unknown, options: { name: string }): Promise<unknown> {\n if (this.destroyed) {\n return Promise.reject(new Error(\"WorkerPool has been destroyed\"));\n }\n\n return new Promise<unknown>((resolve, reject) => {\n const task: Task = {\n id: this.nextId++,\n name: options.name,\n args: [args],\n resolve,\n reject,\n };\n this.dispatch(task);\n });\n }\n\n async destroy(): Promise<void> {\n this.destroyed = true;\n\n // Reject all queued tasks\n for (const task of this.queue) {\n task.reject(new Error(\"WorkerPool has been destroyed\"));\n }\n this.queue.length = 0;\n\n // Terminate all workers\n await Promise.all(this.workers.map((entry) => entry.worker.terminate()));\n this.workers.length = 0;\n }\n\n private dispatch(task: Task): void {\n // Find an idle worker\n const idle = this.workers.find((w) => !w.busy);\n if (idle) {\n this.sendToWorker(idle, task);\n return;\n }\n\n // Spawn a new worker if under the max\n if (this.workers.length < this.maxThreads) {\n const entry = this.spawnWorker();\n this.workers.push(entry);\n this.sendToWorker(entry, task);\n return;\n }\n\n // All workers busy, queue the task\n this.queue.push(task);\n }\n\n private sendToWorker(entry: WorkerEntry, task: Task): void {\n entry.busy = true;\n\n const onMessage = (msg: {\n id: number;\n result?: unknown;\n error?: { name: string; message: string; stack?: string; code?: string };\n }) => {\n if (msg.id !== task.id) return;\n entry.worker.off(\"message\", onMessage);\n entry.worker.off(\"error\", onError);\n entry.busy = false;\n\n if (msg.error) {\n const err = new Error(msg.error.message);\n err.name = msg.error.name;\n if (msg.error.stack) err.stack = msg.error.stack;\n if (msg.error.code)\n (err as NodeJS.ErrnoException).code = msg.error.code;\n task.reject(err);\n } else {\n task.resolve(msg.result);\n }\n\n this.processQueue();\n };\n\n const onError = (err: Error) => {\n entry.worker.off(\"message\", onMessage);\n entry.worker.off(\"error\", onError);\n entry.busy = false;\n task.reject(err);\n this.processQueue();\n };\n\n entry.worker.on(\"message\", onMessage);\n entry.worker.on(\"error\", onError);\n\n entry.worker.postMessage({ id: task.id, name: task.name, args: task.args });\n }\n\n private processQueue(): void {\n if (this.queue.length === 0) return;\n const idle = this.workers.find((w) => !w.busy);\n if (idle) {\n const task = this.queue.shift()!;\n this.sendToWorker(idle, task);\n }\n }\n\n private spawnWorker(): WorkerEntry {\n const worker = new Worker(BOOTSTRAP, this.workerOpts);\n const entry: WorkerEntry = { worker, busy: false };\n\n worker.on(\"exit\", (code) => {\n const idx = this.workers.indexOf(entry);\n if (idx !== -1) this.workers.splice(idx, 1);\n\n // Auto-restart on unexpected exit (not from destroy)\n if (!this.destroyed && code !== 0) {\n this.workers.push(this.spawnWorker());\n }\n });\n\n // Handle init errors (id === -1 from bootstrap)\n worker.on(\n \"message\",\n (msg: {\n id: number;\n error?: { name: string; message: string; stack?: string };\n }) => {\n if (msg.id === -1 && msg.error) {\n const err = new Error(msg.error.message);\n err.name = msg.error.name;\n if (msg.error.stack) err.stack = msg.error.stack;\n // Worker will exit(1) after this, triggering auto-restart\n }\n },\n );\n\n return entry;\n }\n}\n"],"mappings":";;;;;;;AAAA,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,OAAO,UAAU;;;ACFjB,SAAS,cAAkC;AAC3C,SAAS,qBAAqB;AAuB9B,IAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+BhB,KAAK;AAEA,IAAM,aAAN,MAAiB;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAyB,CAAC;AAAA,EAC1B,QAAgB,CAAC;AAAA,EAC1B,SAAS;AAAA,EACT,YAAY;AAAA,EAEpB,YAAY,SAA4B;AACtC,SAAK,YAAY,cAAc,QAAQ,UAAU,EAAE;AACnD,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,aAAa;AAAA,MAChB,MAAM;AAAA,MACN,YAAY,EAAE,WAAW,KAAK,UAAU;AAAA,IAC1C;AAGA,aAAS,IAAI,GAAG,IAAI,KAAK,YAAY,KAAK;AACxC,WAAK,QAAQ,KAAK,KAAK,YAAY,CAAC;AAAA,IACtC;AAAA,EACF;AAAA,EAEA,IAAI,MAAe,SAA6C;AAC9D,QAAI,KAAK,WAAW;AAClB,aAAO,QAAQ,OAAO,IAAI,MAAM,+BAA+B,CAAC;AAAA,IAClE;AAEA,WAAO,IAAI,QAAiB,CAAC,SAAS,WAAW;AAC/C,YAAM,OAAa;AAAA,QACjB,IAAI,KAAK;AAAA,QACT,MAAM,QAAQ;AAAA,QACd,MAAM,CAAC,IAAI;AAAA,QACX;AAAA,QACA;AAAA,MACF;AACA,WAAK,SAAS,IAAI;AAAA,IACpB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,UAAyB;AAC7B,SAAK,YAAY;AAGjB,eAAW,QAAQ,KAAK,OAAO;AAC7B,WAAK,OAAO,IAAI,MAAM,+BAA+B,CAAC;AAAA,IACxD;AACA,SAAK,MAAM,SAAS;AAGpB,UAAM,QAAQ,IAAI,KAAK,QAAQ,IAAI,CAAC,UAAU,MAAM,OAAO,UAAU,CAAC,CAAC;AACvE,SAAK,QAAQ,SAAS;AAAA,EACxB;AAAA,EAEQ,SAAS,MAAkB;AAEjC,UAAM,OAAO,KAAK,QAAQ,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI;AAC7C,QAAI,MAAM;AACR,WAAK,aAAa,MAAM,IAAI;AAC5B;AAAA,IACF;AAGA,QAAI,KAAK,QAAQ,SAAS,KAAK,YAAY;AACzC,YAAM,QAAQ,KAAK,YAAY;AAC/B,WAAK,QAAQ,KAAK,KAAK;AACvB,WAAK,aAAa,OAAO,IAAI;AAC7B;AAAA,IACF;AAGA,SAAK,MAAM,KAAK,IAAI;AAAA,EACtB;AAAA,EAEQ,aAAa,OAAoB,MAAkB;AACzD,UAAM,OAAO;AAEb,UAAM,YAAY,CAAC,QAIb;AACJ,UAAI,IAAI,OAAO,KAAK,GAAI;AACxB,YAAM,OAAO,IAAI,WAAW,SAAS;AACrC,YAAM,OAAO,IAAI,SAAS,OAAO;AACjC,YAAM,OAAO;AAEb,UAAI,IAAI,OAAO;AACb,cAAM,MAAM,IAAI,MAAM,IAAI,MAAM,OAAO;AACvC,YAAI,OAAO,IAAI,MAAM;AACrB,YAAI,IAAI,MAAM,MAAO,KAAI,QAAQ,IAAI,MAAM;AAC3C,YAAI,IAAI,MAAM;AACZ,UAAC,IAA8B,OAAO,IAAI,MAAM;AAClD,aAAK,OAAO,GAAG;AAAA,MACjB,OAAO;AACL,aAAK,QAAQ,IAAI,MAAM;AAAA,MACzB;AAEA,WAAK,aAAa;AAAA,IACpB;AAEA,UAAM,UAAU,CAAC,QAAe;AAC9B,YAAM,OAAO,IAAI,WAAW,SAAS;AACrC,YAAM,OAAO,IAAI,SAAS,OAAO;AACjC,YAAM,OAAO;AACb,WAAK,OAAO,GAAG;AACf,WAAK,aAAa;AAAA,IACpB;AAEA,UAAM,OAAO,GAAG,WAAW,SAAS;AACpC,UAAM,OAAO,GAAG,SAAS,OAAO;AAEhC,UAAM,OAAO,YAAY,EAAE,IAAI,KAAK,IAAI,MAAM,KAAK,MAAM,MAAM,KAAK,KAAK,CAAC;AAAA,EAC5E;AAAA,EAEQ,eAAqB;AAC3B,QAAI,KAAK,MAAM,WAAW,EAAG;AAC7B,UAAM,OAAO,KAAK,QAAQ,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI;AAC7C,QAAI,MAAM;AACR,YAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,WAAK,aAAa,MAAM,IAAI;AAAA,IAC9B;AAAA,EACF;AAAA,EAEQ,cAA2B;AACjC,UAAM,SAAS,IAAI,OAAO,WAAW,KAAK,UAAU;AACpD,UAAM,QAAqB,EAAE,QAAQ,MAAM,MAAM;AAEjD,WAAO,GAAG,QAAQ,CAAC,SAAS;AAC1B,YAAM,MAAM,KAAK,QAAQ,QAAQ,KAAK;AACtC,UAAI,QAAQ,GAAI,MAAK,QAAQ,OAAO,KAAK,CAAC;AAG1C,UAAI,CAAC,KAAK,aAAa,SAAS,GAAG;AACjC,aAAK,QAAQ,KAAK,KAAK,YAAY,CAAC;AAAA,MACtC;AAAA,IACF,CAAC;AAGD,WAAO;AAAA,MACL;AAAA,MACA,CAAC,QAGK;AACJ,YAAI,IAAI,OAAO,MAAM,IAAI,OAAO;AAC9B,gBAAM,MAAM,IAAI,MAAM,IAAI,MAAM,OAAO;AACvC,cAAI,OAAO,IAAI,MAAM;AACrB,cAAI,IAAI,MAAM,MAAO,KAAI,QAAQ,IAAI,MAAM;AAAA,QAE7C;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;ADlIO,SAAS,iBAAiB,SAAyC;AACxE,QAAM,aACJ,SAAS,cACT,KAAK;AAAA,IACH,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAAA,IAC3C;AAAA,EACF;AAEF,QAAM,OAAO,IAAI,WAAW;AAAA,IAC1B;AAAA,IACA,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;AAAA,QACjB,oBAAoB,cAAc,OAAoB,CAAC;AAAA,MACzD;AACA,YAAM,SAAU,MAAM,KAAK;AAAA,QACzB;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;AAAA,QACjB,oBAAoB,cAAc,OAAoB,CAAC;AAAA,MACzD;AACA,aAAQ,MAAM,KAAK;AAAA,QACjB;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,SAAU,MAAM,KAAK;AAAA,QACzB,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":[]}
1
+ {"version":3,"sources":["../../src/pool/index.ts","../../src/pool/worker-pool.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\";\n\nimport type { EmojiStyle } from \"../emoji.ts\";\nimport {\n ensureSingleElement,\n expandElement,\n serializeElement,\n} from \"../elements/index.ts\";\nimport { WorkerPool } from \"./worker-pool.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 /** Absolute path to a bundled worker file (set automatically by the Vite plugin) */\n workerFile?: string;\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 | ArrayBuffer;\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 | ArrayBuffer;\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 =\n options?.workerFile ??\n path.resolve(\n path.dirname(fileURLToPath(import.meta.url)),\n \"../worker/index.js\",\n );\n\n const pool = new WorkerPool({\n 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(\n ensureSingleElement(expandElement(element as ReactNode)),\n );\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 )) as Uint8Array;\n return Buffer.from(result);\n },\n\n async renderToSvg(element, opts) {\n const serialized = serializeElement(\n ensureSingleElement(expandElement(element as ReactNode)),\n );\n return (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: \"renderToSvg\" },\n )) as string;\n },\n\n async rasterizeSvgToPng(svg, opts) {\n const result = (await pool.run(\n { svg, options: opts },\n { name: \"rasterizeSvgToPng\" },\n )) as Uint8Array;\n return Buffer.from(result);\n },\n\n async destroy() {\n await pool.destroy();\n },\n };\n}\n","import { Worker, type WorkerOptions } from \"node:worker_threads\";\nimport { pathToFileURL } from \"node:url\";\n\ninterface Task {\n id: number;\n name: string;\n args: unknown[];\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n}\n\ninterface WorkerEntry {\n worker: Worker;\n busy: boolean;\n}\n\nexport interface WorkerPoolOptions {\n workerFile: string;\n minThreads?: number;\n maxThreads?: number;\n}\n\n// CJS bootstrap that dynamically imports the ESM worker module, then\n// wires up parentPort message handling for { id, name, args } calls.\nconst BOOTSTRAP = `\n\"use strict\";\nconst { parentPort, workerData } = require(\"node:worker_threads\");\n\nconst modPromise = import(workerData.workerUrl)\n .catch((err) => {\n parentPort.postMessage({\n id: -1,\n error: { name: \"InitError\", message: err.message, stack: err.stack },\n });\n process.exit(1);\n });\n\nparentPort.on(\"message\", async (msg) => {\n const { id, name, args } = msg;\n try {\n const mod = await modPromise;\n const result = await mod[name](...args);\n parentPort.postMessage({ id, result });\n } catch (err) {\n parentPort.postMessage({\n id,\n error: {\n name: err?.name ?? \"Error\",\n message: err?.message ?? String(err),\n stack: err?.stack,\n code: err?.code,\n },\n });\n }\n});\n`.trim();\n\nexport class WorkerPool {\n private readonly workerUrl: string;\n private readonly workerOpts: WorkerOptions;\n private readonly minThreads: number;\n private readonly maxThreads: number;\n private readonly workers: WorkerEntry[] = [];\n private readonly queue: Task[] = [];\n private nextId = 0;\n private destroyed = false;\n\n constructor(options: WorkerPoolOptions) {\n this.workerUrl = pathToFileURL(options.workerFile).href;\n this.minThreads = options.minThreads ?? 1;\n this.maxThreads = options.maxThreads ?? 1;\n this.workerOpts = {\n eval: true,\n workerData: { workerUrl: this.workerUrl },\n };\n\n // Pre-spawn minimum workers\n for (let i = 0; i < this.minThreads; i++) {\n this.workers.push(this.spawnWorker());\n }\n }\n\n run(args: unknown, options: { name: string }): Promise<unknown> {\n if (this.destroyed) {\n return Promise.reject(new Error(\"WorkerPool has been destroyed\"));\n }\n\n return new Promise<unknown>((resolve, reject) => {\n const task: Task = {\n id: this.nextId++,\n name: options.name,\n args: [args],\n resolve,\n reject,\n };\n this.dispatch(task);\n });\n }\n\n async destroy(): Promise<void> {\n this.destroyed = true;\n\n // Reject all queued tasks\n for (const task of this.queue) {\n task.reject(new Error(\"WorkerPool has been destroyed\"));\n }\n this.queue.length = 0;\n\n // Terminate all workers\n await Promise.all(this.workers.map((entry) => entry.worker.terminate()));\n this.workers.length = 0;\n }\n\n private dispatch(task: Task): void {\n // Find an idle worker\n const idle = this.workers.find((w) => !w.busy);\n if (idle) {\n this.sendToWorker(idle, task);\n return;\n }\n\n // Spawn a new worker if under the max\n if (this.workers.length < this.maxThreads) {\n const entry = this.spawnWorker();\n this.workers.push(entry);\n this.sendToWorker(entry, task);\n return;\n }\n\n // All workers busy, queue the task\n this.queue.push(task);\n }\n\n private sendToWorker(entry: WorkerEntry, task: Task): void {\n entry.busy = true;\n\n const onMessage = (msg: {\n id: number;\n result?: unknown;\n error?: { name: string; message: string; stack?: string; code?: string };\n }) => {\n if (msg.id !== task.id) return;\n entry.worker.off(\"message\", onMessage);\n entry.worker.off(\"error\", onError);\n entry.busy = false;\n\n if (msg.error) {\n const err = new Error(msg.error.message);\n err.name = msg.error.name;\n if (msg.error.stack) err.stack = msg.error.stack;\n if (msg.error.code)\n (err as NodeJS.ErrnoException).code = msg.error.code;\n task.reject(err);\n } else {\n task.resolve(msg.result);\n }\n\n this.processQueue();\n };\n\n const onError = (err: Error) => {\n entry.worker.off(\"message\", onMessage);\n entry.worker.off(\"error\", onError);\n entry.busy = false;\n task.reject(err);\n this.processQueue();\n };\n\n entry.worker.on(\"message\", onMessage);\n entry.worker.on(\"error\", onError);\n\n entry.worker.postMessage({ id: task.id, name: task.name, args: task.args });\n }\n\n private processQueue(): void {\n if (this.queue.length === 0) return;\n const idle = this.workers.find((w) => !w.busy);\n if (idle) {\n const task = this.queue.shift()!;\n this.sendToWorker(idle, task);\n }\n }\n\n private spawnWorker(): WorkerEntry {\n const worker = new Worker(BOOTSTRAP, this.workerOpts);\n const entry: WorkerEntry = { worker, busy: false };\n\n worker.on(\"exit\", (code) => {\n const idx = this.workers.indexOf(entry);\n if (idx !== -1) this.workers.splice(idx, 1);\n\n // Auto-restart on unexpected exit (not from destroy)\n if (!this.destroyed && code !== 0) {\n this.workers.push(this.spawnWorker());\n }\n });\n\n // Handle init errors (id === -1 from bootstrap)\n worker.on(\n \"message\",\n (msg: {\n id: number;\n error?: { name: string; message: string; stack?: string };\n }) => {\n if (msg.id === -1 && msg.error) {\n const err = new Error(msg.error.message);\n err.name = msg.error.name;\n if (msg.error.stack) err.stack = msg.error.stack;\n // Worker will exit(1) after this, triggering auto-restart\n }\n },\n );\n\n return entry;\n }\n}\n"],"mappings":"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","names":[]}
@@ -53,7 +53,7 @@ function makeLoadAdditionalAsset(emoji) {
53
53
  };
54
54
  }
55
55
 
56
- // src/serde/index.ts
56
+ // src/elements/index.ts
57
57
  import React from "react";
58
58
  function ensureSingleElement(node) {
59
59
  if (Array.isArray(node)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effing/satori",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
4
4
  "description": "Render JSX to PNG using Satori with emoji support",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,9 +8,9 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "import": "./dist/index.js"
10
10
  },
11
- "./serde": {
12
- "types": "./dist/serde/index.d.ts",
13
- "import": "./dist/serde/index.js"
11
+ "./elements": {
12
+ "types": "./dist/elements/index.d.ts",
13
+ "import": "./dist/elements/index.js"
14
14
  },
15
15
  "./pool": {
16
16
  "types": "./dist/pool/index.d.ts",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/serde/index.ts"],"sourcesContent":["import React, { type ReactElement, type ReactNode } from \"react\";\n\n/**\n * Wrap a root-level array (e.g. from a top-level Fragment) in a\n * layout-transparent div so satori always receives a single element.\n */\nexport function ensureSingleElement(node: ReactNode): ReactNode {\n if (Array.isArray(node)) {\n return React.createElement(\n \"div\",\n { style: { display: \"contents\" } },\n ...node,\n );\n }\n return node;\n}\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)) {\n return node.flatMap((child) => {\n const result = expandElement(child);\n return Array.isArray(result) ? result : [result];\n });\n }\n\n if (!React.isValidElement(node)) return node;\n\n const element = node as ReactElement;\n\n if (element.type === React.Fragment) {\n const children = (element.props as Record<string, unknown>)\n .children as ReactNode;\n if (children == null) return null;\n return expandElement(children);\n }\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 if (typeof element.type !== \"string\") {\n throw new Error(\n `Cannot serialize element with type \"${String(element.type)}\". 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":"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","names":[]}
File without changes
File without changes