@effing/satori 0.15.0 → 0.15.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
@@ -120,23 +120,6 @@ await pool.destroy();
120
120
 
121
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
- ### Vite Plugin (SSR)
124
-
125
- **The `@effing/satori/vite` plugin is required when using the worker pool in production SSR builds.** Without it, the worker file path breaks after Vite bundles the pool code, because `import.meta.url` points at the build output directory instead of `node_modules`.
126
-
127
- The plugin bundles the worker into a self-contained file in the SSR output and rewrites `createSatoriPool()` calls to point at it.
128
-
129
- ```typescript
130
- // vite.config.ts
131
- import { satoriPoolPlugin } from "@effing/satori/vite";
132
-
133
- export default defineConfig({
134
- plugins: [satoriPoolPlugin()],
135
- });
136
- ```
137
-
138
- In dev mode the plugin is inert — `import.meta.url` still resolves into `node_modules` correctly, so no rewriting is needed.
139
-
140
123
  ## API Overview
141
124
 
142
125
  ### `pngFromSatori(template, options)`
@@ -190,10 +173,11 @@ function createSatoriPool(options?: SatoriPoolOptions): SatoriPool;
190
173
 
191
174
  **Pool options:**
192
175
 
193
- | Option | Type | Default | Description |
194
- | ------------ | -------- | ------------------ | ---------------------- |
195
- | `minThreads` | `number` | `1` | Minimum worker threads |
196
- | `maxThreads` | `number` | `os.cpus().length` | Maximum worker threads |
176
+ | Option | Type | Default | Description |
177
+ | ------------ | -------- | ------------------ | ------------------------------------------ |
178
+ | `minThreads` | `number` | `1` | Minimum worker threads |
179
+ | `maxThreads` | `number` | `os.cpus().length` | Maximum worker threads |
180
+ | `workerFile` | `string` | auto-resolved | Absolute path to a pre-bundled worker file |
197
181
 
198
182
  **`SatoriPool` methods:**
199
183
 
@@ -202,16 +186,6 @@ function createSatoriPool(options?: SatoriPoolOptions): SatoriPool;
202
186
  - `rasterizeSvgToPng(svg, options?)` — Rasterize SVG to PNG buffer
203
187
  - `destroy()` — Shut down the pool
204
188
 
205
- ### `@effing/satori/vite`
206
-
207
- #### `satoriPoolPlugin()`
208
-
209
- Vite plugin that bundles the satori worker into the SSR output. Required for production SSR builds using the worker pool.
210
-
211
- ```typescript
212
- function satoriPoolPlugin(): Plugin;
213
- ```
214
-
215
189
  ### `@effing/satori/elements`
216
190
 
217
191
  React element serialization for cross-thread communication. Used internally by the pool, but available for custom worker setups.
@@ -6,7 +6,7 @@ type SatoriPoolOptions = {
6
6
  minThreads?: number;
7
7
  /** Maximum number of worker threads (default: os.cpus().length) */
8
8
  maxThreads?: number;
9
- /** Absolute path to a bundled worker file (set automatically by the Vite plugin) */
9
+ /** Absolute path to a bundled worker file */
10
10
  workerFile?: string;
11
11
  };
12
12
  type SatoriPool = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pool/index.ts","../../src/pool/worker-pool.ts"],"sourcesContent":["import os from \"os\";\nimport { fileURLToPath } from \"url\";\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 let resolvedWorkerUrl: string;\n try {\n resolvedWorkerUrl = import.meta.resolve(\"@effing/satori/worker\");\n } catch {\n // Vite's SSR module runner doesn't support import.meta.resolve;\n // fall back to relative resolution (works in dev where import.meta.url\n // still points at the source file inside node_modules).\n resolvedWorkerUrl = new URL(\"../worker/index.js\", import.meta.url).href;\n }\n const workerFile = options?.workerFile ?? fileURLToPath(resolvedWorkerUrl);\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":[]}
1
+ {"version":3,"sources":["../../src/pool/index.ts","../../src/pool/worker-pool.ts"],"sourcesContent":["import os from \"os\";\nimport { fileURLToPath } from \"url\";\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 */\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 let resolvedWorkerUrl: string;\n try {\n resolvedWorkerUrl = import.meta.resolve(\"@effing/satori/worker\");\n } catch {\n // Vite's SSR module runner doesn't support import.meta.resolve;\n // fall back to relative resolution (works in dev where import.meta.url\n // still points at the source file inside node_modules).\n resolvedWorkerUrl = new URL(\"../worker/index.js\", import.meta.url).href;\n }\n const workerFile = options?.workerFile ?? fileURLToPath(resolvedWorkerUrl);\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effing/satori",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Render JSX to PNG using Satori with emoji support",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,9 +23,6 @@
23
23
  "files": [
24
24
  "dist"
25
25
  ],
26
- "dependencies": {
27
- "satori": ">=0.10.0 <1.0.0"
28
- },
29
26
  "devDependencies": {
30
27
  "@types/react": "^19.0.0",
31
28
  "tsup": "^8.0.0",
@@ -34,7 +31,8 @@
34
31
  },
35
32
  "peerDependencies": {
36
33
  "@resvg/resvg-js": "^2.6.2",
37
- "react": "^18.0.0 || ^19.0.0"
34
+ "react": "^18.0.0 || ^19.0.0",
35
+ "satori": ">=0.10.0 <1.0.0"
38
36
  },
39
37
  "peerDependenciesMeta": {
40
38
  "react": {