nuxt-og-image 6.7.5 → 6.7.7

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.
Files changed (76) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/chunks/decode.mjs +1087 -0
  3. package/dist/chunks/tw4.mjs +2 -2
  4. package/dist/chunks/uno.mjs +19 -5
  5. package/dist/module.d.ts +1 -1
  6. package/dist/module.json +1 -1
  7. package/dist/module.mjs +2 -2
  8. package/dist/runtime/app/utils/plugins.js +4 -10
  9. package/dist/runtime/server/og-image/bindings/browser/chrome-launcher.d.ts +1 -1
  10. package/dist/runtime/server/og-image/bindings/browser/cloudflare.d.ts +1 -1
  11. package/dist/runtime/server/og-image/bindings/browser/on-demand.d.ts +1 -1
  12. package/dist/runtime/server/og-image/bindings/browser/playwright.d.ts +1 -1
  13. package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.d.ts +1 -1
  14. package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.js +1 -1
  15. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.d.ts +1 -1
  16. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +6 -7
  17. package/dist/runtime/server/og-image/bindings/font-assets/node.d.ts +2 -2
  18. package/dist/runtime/server/og-image/bindings/font-assets/node.js +2 -3
  19. package/dist/runtime/server/og-image/bindings/resvg/node-dev.js +18 -64
  20. package/dist/runtime/server/og-image/bindings/takumi/node-dev.js +26 -83
  21. package/dist/runtime/server/og-image/bindings/worker-queue.d.ts +34 -0
  22. package/dist/runtime/server/og-image/bindings/worker-queue.js +150 -0
  23. package/dist/runtime/server/og-image/browser/renderer.js +1 -1
  24. package/dist/runtime/server/og-image/browser/screenshot.js +2 -1
  25. package/dist/runtime/server/og-image/context.d.ts +1 -1
  26. package/dist/runtime/server/og-image/context.js +2 -3
  27. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +1 -1
  28. package/dist/runtime/server/og-image/core/transforms/emojis/local.d.ts +2 -17
  29. package/dist/runtime/server/og-image/core/transforms/emojis/local.js +1 -1
  30. package/dist/runtime/server/og-image/devtools.d.ts +1 -1
  31. package/dist/runtime/server/og-image/devtools.js +1 -1
  32. package/dist/runtime/server/og-image/font-source.d.ts +3 -12
  33. package/dist/runtime/server/og-image/font-source.js +1 -4
  34. package/dist/runtime/server/og-image/fonts.d.ts +1 -8
  35. package/dist/runtime/server/og-image/fonts.js +1 -1
  36. package/dist/runtime/server/og-image/takumi/renderer.js +2 -2
  37. package/dist/runtime/server/og-image/templates/html.js +1 -1
  38. package/dist/runtime/server/plugins/prerender.d.ts +1 -1
  39. package/dist/runtime/server/plugins/prerender.js +2 -2
  40. package/dist/runtime/server/routes/__zero-runtime/image.d.ts +1 -1
  41. package/dist/runtime/server/routes/__zero-runtime/image.js +1 -1
  42. package/dist/runtime/server/routes/debug.json.d.ts +1 -10
  43. package/dist/runtime/server/routes/debug.json.js +1 -1
  44. package/dist/runtime/server/routes/image.d.ts +1 -1
  45. package/dist/runtime/server/routes/image.js +1 -1
  46. package/dist/runtime/server/routes/resolve.d.ts +1 -1
  47. package/dist/runtime/server/routes/resolve.js +3 -2
  48. package/dist/runtime/server/util/cache.d.ts +1 -6
  49. package/dist/runtime/server/util/cache.js +2 -2
  50. package/dist/runtime/server/util/cloudflare.d.ts +1 -1
  51. package/dist/runtime/server/util/cloudflareAssets.d.ts +1 -1
  52. package/dist/runtime/server/util/cloudflareAssets.js +1 -1
  53. package/dist/runtime/server/util/eventHandlers.d.ts +1 -1
  54. package/dist/runtime/server/util/eventHandlers.js +1 -1
  55. package/dist/runtime/server/util/fetchLocalAsset.d.ts +2 -2
  56. package/dist/runtime/server/util/fetchLocalAsset.js +2 -1
  57. package/dist/runtime/server/util/kit.d.ts +7 -3
  58. package/dist/runtime/server/util/kit.js +4 -18
  59. package/dist/runtime/server/util/options.js +1 -1
  60. package/dist/runtime/server/util/query.d.ts +1 -1
  61. package/dist/runtime/server/utils.d.ts +1 -1
  62. package/dist/runtime/server/utils.js +1 -1
  63. package/dist/runtime/types.d.ts +12 -10
  64. package/dist/shared/{nuxt-og-image.D3mV8OQ9.mjs → nuxt-og-image.BMHzg1Vz.mjs} +334 -155
  65. package/package.json +49 -39
  66. package/types/virtual.d.ts +15 -0
  67. package/dist/chunks/tw4.cjs +0 -493
  68. package/dist/chunks/uno.cjs +0 -234
  69. package/dist/cli.cjs +0 -1345
  70. package/dist/cli.d.cts +0 -1
  71. package/dist/content.cjs +0 -21
  72. package/dist/content.d.cts +0 -16
  73. package/dist/module.cjs +0 -39
  74. package/dist/module.d.cts +0 -219
  75. package/dist/shared/nuxt-og-image.CMYbz66P.cjs +0 -193
  76. package/dist/shared/nuxt-og-image.Cctpuv4O.cjs +0 -5867
@@ -2,17 +2,17 @@ import { readFile } from 'node:fs/promises';
2
2
  import { pathToFileURL } from 'node:url';
3
3
  import { resolveModulePath } from 'exsolve';
4
4
  import { dirname, join } from 'pathe';
5
- import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, i as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, l as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.D3mV8OQ9.mjs';
5
+ import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.BMHzg1Vz.mjs';
6
6
  import 'node:crypto';
7
7
  import 'node:fs';
8
8
  import '@nuxt/kit';
9
9
  import 'defu';
10
10
  import 'fnv1a-64';
11
11
  import 'nuxt-site-config/kit';
12
+ import 'nuxtseo-shared/kit';
12
13
  import 'pkg-types';
13
14
  import 'std-env';
14
15
  import 'oxc-walker';
15
- import 'nuxtseo-shared/kit';
16
16
  import '../../dist/runtime/logger.js';
17
17
  import 'chrome-launcher';
18
18
  import 'nypm';
@@ -1,18 +1,19 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { defu } from 'defu';
3
3
  import { logger } from '../../dist/runtime/logger.js';
4
- import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.D3mV8OQ9.mjs';
4
+ import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.BMHzg1Vz.mjs';
5
+ export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.BMHzg1Vz.mjs';
5
6
  import 'node:crypto';
6
7
  import 'node:fs';
7
8
  import 'node:url';
8
9
  import '@nuxt/kit';
9
10
  import 'fnv1a-64';
10
11
  import 'nuxt-site-config/kit';
12
+ import 'nuxtseo-shared/kit';
11
13
  import 'pathe';
12
14
  import 'pkg-types';
13
15
  import 'std-env';
14
16
  import 'oxc-walker';
15
- import 'nuxtseo-shared/kit';
16
17
  import 'chrome-launcher';
17
18
  import 'nypm';
18
19
  import 'nuxtseo-shared/devtools';
@@ -40,6 +41,8 @@ const RE_HEIGHT_ATTR = /\bheight=['"](\d+)['"]/;
40
41
  const RE_SVG_BODY = /<svg[^>]*>([\s\S]*)<\/svg>/;
41
42
  let unoConfig = null;
42
43
  let rootDir = null;
44
+ let unocssCorePath;
45
+ let unocssConfigPath;
43
46
  let generator = null;
44
47
  let pendingGenerator = null;
45
48
  let cachedExtracted = null;
@@ -58,6 +61,15 @@ function clearUnoCache() {
58
61
  pendingGenerator = null;
59
62
  cachedExtracted = null;
60
63
  }
64
+ function setUnoModulePaths(options) {
65
+ const nextCorePath = options?.unocssCorePath;
66
+ const nextConfigPath = options?.unocssConfigPath;
67
+ if (nextCorePath === unocssCorePath && nextConfigPath === unocssConfigPath)
68
+ return;
69
+ unocssCorePath = nextCorePath;
70
+ unocssConfigPath = nextConfigPath;
71
+ clearUnoCache();
72
+ }
61
73
  async function getGenerator() {
62
74
  if (generator)
63
75
  return generator;
@@ -66,17 +78,18 @@ async function getGenerator() {
66
78
  pendingGenerator = (async () => {
67
79
  let loadedConfig;
68
80
  try {
69
- const { loadConfig } = await import('@unocss/config');
81
+ const { loadConfig } = await importOptionalPeer("@unocss/config", unocssConfigPath, "UnoCSS class extraction with OG images");
70
82
  const result = await loadConfig(rootDir || process.cwd());
71
83
  loadedConfig = result.config;
72
84
  } catch (e) {
73
- logger.warn("[og-image UnoCSS] Failed to load uno.config.ts:", e.message);
85
+ if (!unoConfig)
86
+ logger.warn("[og-image UnoCSS] Failed to load uno.config.ts:", e.message);
74
87
  }
75
88
  const finalConfig = loadedConfig ? defu(unoConfig || {}, loadedConfig) : unoConfig;
76
89
  if (!finalConfig) {
77
90
  throw new Error("UnoCSS config not found. Ensure uno.config.ts exists or @unocss/nuxt is loaded.");
78
91
  }
79
- const { createGenerator } = await import('@unocss/core');
92
+ const { createGenerator } = await importOptionalPeer("@unocss/core", unocssCorePath, "UnoCSS class extraction with OG images");
80
93
  generator = await createGenerator(finalConfig);
81
94
  return generator;
82
95
  })();
@@ -114,6 +127,7 @@ function resolveUserVars(extracted, rootAttrs) {
114
127
  return resolveExtractedVars(extracted, rootAttrs);
115
128
  }
116
129
  function createUnoProvider(options) {
130
+ setUnoModulePaths(options);
117
131
  return {
118
132
  name: "unocss",
119
133
  async resolveClassesToStyles(classes, context, rootAttrs) {
package/dist/module.d.ts CHANGED
@@ -215,5 +215,5 @@ interface ModuleHooks {
215
215
  }
216
216
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
217
217
 
218
- export = _default;
218
+ export { _default as default };
219
219
  export type { ModuleHooks, ModuleOptions };
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.16.0"
5
5
  },
6
6
  "configKey": "ogImage",
7
- "version": "6.7.5",
7
+ "version": "6.7.7",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -6,11 +6,11 @@ import '@nuxt/kit';
6
6
  import 'defu';
7
7
  import 'fnv1a-64';
8
8
  import 'nuxt-site-config/kit';
9
+ import 'nuxtseo-shared/kit';
9
10
  import 'pathe';
10
11
  import 'pkg-types';
11
12
  import 'std-env';
12
- export { m as default } from './shared/nuxt-og-image.D3mV8OQ9.mjs';
13
- import 'nuxtseo-shared/kit';
13
+ export { n as default } from './shared/nuxt-og-image.BMHzg1Vz.mjs';
14
14
  import '../dist/runtime/logger.js';
15
15
  import 'oxc-walker';
16
16
  import 'chrome-launcher';
@@ -1,7 +1,7 @@
1
1
  import { TemplateParamsPlugin } from "@unhead/vue/plugins";
2
2
  import { defu } from "defu";
3
- import { createRouter as createRadixRouter, toRouteMatcher } from "radix3";
4
- import { parseURL, withoutBase } from "ufo";
3
+ import { createNitroRouteRuleMatcher } from "nuxtseo-shared/server";
4
+ import { parseURL } from "ufo";
5
5
  import { toValue } from "vue";
6
6
  import { defineNuxtPlugin, useRequestEvent } from "#app";
7
7
  import { withSiteUrl } from "#site-config/app/composables";
@@ -60,14 +60,8 @@ export const routeRuleOgImage = defineNuxtPlugin((nuxtApp) => {
60
60
  const path = parseURL(e?.path || "").pathname;
61
61
  if (isInternalRoute(path))
62
62
  return;
63
- const _routeRulesMatcher = toRouteMatcher(
64
- createRadixRouter({ routes: ssrContext?.runtimeConfig?.nitro?.routeRules })
65
- );
66
- const matchedRules = _routeRulesMatcher.matchAll(
67
- withoutBase(path.split("?")?.[0] || "", ssrContext?.runtimeConfig?.app.baseURL || "")
68
- ).reverse();
69
- const combinedRules = defu({}, ...matchedRules);
70
- let routeRules = combinedRules?.ogImage;
63
+ const matchRouteRules = createNitroRouteRuleMatcher(ssrContext?.runtimeConfig || {});
64
+ let routeRules = matchRouteRules(path).ogImage;
71
65
  if (typeof routeRules === "undefined")
72
66
  return;
73
67
  if (routeRules === false) {
@@ -1,3 +1,3 @@
1
- import type { H3Event } from 'h3';
2
1
  import type { Browser } from 'playwright-core';
2
+ import type { H3Event } from '#nuxtseo/h3';
3
3
  export declare function createBrowser(_event?: H3Event): Promise<Browser | void>;
@@ -1,4 +1,4 @@
1
- import type { H3Event } from 'h3';
1
+ import type { H3Event } from '#nuxtseo/h3';
2
2
  interface Browser {
3
3
  connected: boolean;
4
4
  newPage: () => Promise<any>;
@@ -1,3 +1,3 @@
1
- import type { H3Event } from 'h3';
2
1
  import type { Browser } from 'playwright-core';
2
+ import type { H3Event } from '#nuxtseo/h3';
3
3
  export declare function createBrowser(_event?: H3Event): Promise<Browser | void>;
@@ -1,3 +1,3 @@
1
- import type { H3Event } from 'h3';
2
1
  import type { Browser } from 'playwright-core';
2
+ import type { H3Event } from '#nuxtseo/h3';
3
3
  export declare function createBrowser(_event?: H3Event): Promise<Browser | void>;
@@ -1,3 +1,3 @@
1
- import type { H3Event } from 'h3';
1
+ import type { H3Event } from '#nuxtseo/h3';
2
2
  import type { FontConfig } from '../../../../types.js';
3
3
  export declare function resolve(event: H3Event, font: FontConfig): Promise<Buffer<ArrayBufferLike>>;
@@ -1,5 +1,5 @@
1
- import { useRuntimeConfig } from "nitropack/runtime";
2
1
  import { withBase } from "ufo";
2
+ import { useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { getSiteConfig } from "#site-config/server/composables";
4
4
  import { getCloudflareAssets } from "../../../util/cloudflareAssets.js";
5
5
  import { fetchLocalAsset } from "../../../util/fetchLocalAsset.js";
@@ -1,3 +1,3 @@
1
- import type { H3Event } from 'h3';
1
+ import type { H3Event } from '#nuxtseo/h3';
2
2
  import type { FontConfig } from '../../../../types.js';
3
3
  export declare function resolve(event: H3Event, font: FontConfig): Promise<Buffer>;
@@ -1,9 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { getRequestURL } from "h3";
3
- import { useRuntimeConfig } from "nitropack/runtime";
4
2
  import { join } from "pathe";
5
3
  import { withBase } from "ufo";
6
- import { buildDir, rootDir } from "#og-image-virtual/build-dir.mjs";
4
+ import { getRequestURL } from "#nuxtseo/h3";
5
+ import { fetchWithEvent, useRuntimeConfig } from "#nuxtseo/nitro";
6
+ import { buildDir, rootDir, staticFontCacheDir } from "#og-image-virtual/build-dir.mjs";
7
7
  import { getSiteConfig } from "#site-config/server/composables";
8
8
  import { getFetchTimeout } from "../../../util/fetchTimeout.js";
9
9
  import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
@@ -43,7 +43,7 @@ export async function resolve(event, font) {
43
43
  if (import.meta.prerender) {
44
44
  if (path.startsWith("/_og-static-fonts/")) {
45
45
  const filename2 = path.slice("/_og-static-fonts/".length);
46
- const cached = await readOptionalFile(join(buildDir, "cache", "og-image", "fonts-ttf", filename2)) || await readOptionalFile(join(rootDir, ".output", "public", "_og-static-fonts", filename2));
46
+ const cached = await readOptionalFile(join(staticFontCacheDir, filename2)) || await readOptionalFile(join(rootDir, ".output", "public", "_og-static-fonts", filename2));
47
47
  if (cached?.length)
48
48
  return cached;
49
49
  }
@@ -67,7 +67,7 @@ export async function resolve(event, font) {
67
67
  }
68
68
  if (path.startsWith("/_og-static-fonts/")) {
69
69
  const filename = path.slice("/_og-static-fonts/".length);
70
- const cached = await readOptionalFile(join(buildDir, "cache", "og-image", "fonts-ttf", filename));
70
+ const cached = await readOptionalFile(join(staticFontCacheDir, filename));
71
71
  if (cached?.length)
72
72
  return cached;
73
73
  }
@@ -99,8 +99,7 @@ export async function resolve(event, font) {
99
99
  }
100
100
  }
101
101
  const fullPath = withBase(path, app.baseURL);
102
- const fetchArrayBuffer = event.$fetch;
103
- const arrayBuffer = await fetchArrayBuffer(fullPath, {
102
+ const arrayBuffer = await fetchWithEvent(event, fullPath, {
104
103
  responseType: "arrayBuffer",
105
104
  timeout
106
105
  });
@@ -1,3 +1,3 @@
1
- import type { H3Event } from 'h3';
1
+ import type { H3Event } from '#nuxtseo/h3';
2
2
  import type { FontConfig } from '../../../../types.js';
3
- export declare function resolve(event: H3Event, font: FontConfig): Promise<Buffer<ArrayBufferLike>>;
3
+ export declare function resolve(event: H3Event, font: FontConfig): Promise<Buffer<any>>;
@@ -1,5 +1,5 @@
1
- import { useRuntimeConfig } from "nitropack/runtime";
2
1
  import { withBase } from "ufo";
2
+ import { fetchWithEvent, useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { getNitroOrigin, getSiteConfig } from "#site-config/server/composables";
4
4
  import { getFetchTimeout } from "../../../util/fetchTimeout.js";
5
5
  import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
@@ -21,8 +21,7 @@ export async function resolve(event, font) {
21
21
  if (ab) {
22
22
  return Buffer.from(ab);
23
23
  }
24
- const fetchArrayBuffer = event.$fetch;
25
- const arrayBuffer = await fetchArrayBuffer(fullPath, {
24
+ const arrayBuffer = await fetchWithEvent(event, fullPath, {
26
25
  responseType: "arrayBuffer",
27
26
  timeout
28
27
  });
@@ -1,4 +1,5 @@
1
1
  import { Worker } from "node:worker_threads";
2
+ import { createWorkerQueue, parseWorkerTimeout } from "../worker-queue.js";
2
3
  const workerCode = `
3
4
  const { createRequire } = require('node:module')
4
5
  const _require = createRequire(process.cwd() + '/')
@@ -12,78 +13,31 @@ parentPort.on('message', ({ id, svg, options }) => {
12
13
  // Always slice to create a standard ArrayBuffer \u2014 native addon buffers
13
14
  // use external memory that can't be transferred via postMessage
14
15
  const ab = png.buffer.slice(png.byteOffset, png.byteOffset + png.byteLength)
15
- parentPort.postMessage({ id, png: ab }, [ab])
16
+ parentPort.postMessage({ _tag: 'response', id, payload: { _tag: 'success', png: ab } }, [ab])
16
17
  } catch (err) {
17
- parentPort.postMessage({ id, error: err?.message || String(err) })
18
+ parentPort.postMessage({ _tag: 'response', id, payload: { _tag: 'failure', error: err?.message || String(err) } })
18
19
  }
19
20
  })
21
+ parentPort.postMessage({ _tag: 'ready' })
20
22
  `;
21
- let worker = null;
22
- let requestId = 0;
23
- const pending = /* @__PURE__ */ new Map();
24
- function killWorker() {
25
- if (!worker)
26
- return;
27
- worker.terminate();
28
- worker = null;
29
- for (const [id, p] of pending) {
30
- clearTimeout(p.timer);
31
- pending.delete(id);
32
- p.reject(new Error("Resvg worker terminated"));
33
- }
23
+ const workerTimeout = parseWorkerTimeout(process.env.NUXT_OG_IMAGE_WORKER_TIMEOUT);
24
+ const workerQueue = createWorkerQueue({
25
+ createWorker: () => new Worker(workerCode, { eval: true }),
26
+ executionTimeout: workerTimeout,
27
+ startupTimeout: workerTimeout,
28
+ label: "resvg"
29
+ });
30
+ function renderPng(svg, options) {
31
+ return workerQueue.enqueue(({ id }) => ({ id, svg, options })).then((result) => {
32
+ if (result._tag === "failure")
33
+ throw new Error(result.error);
34
+ return Buffer.from(result.png);
35
+ });
34
36
  }
35
37
  const signalKey = Symbol.for("og-image:resvg-worker-cleanup");
36
38
  if (!globalThis[signalKey]) {
37
39
  globalThis[signalKey] = true;
38
- process.on("exit", killWorker);
39
- }
40
- function createWorker() {
41
- const w = new Worker(workerCode, { eval: true });
42
- w.on("message", ({ id, png, error }) => {
43
- const p = pending.get(id);
44
- if (p) {
45
- clearTimeout(p.timer);
46
- pending.delete(id);
47
- if (error)
48
- p.reject(new Error(error));
49
- else
50
- p.resolve(Buffer.from(png));
51
- }
52
- });
53
- w.on("error", (err) => {
54
- for (const [id, p] of pending) {
55
- clearTimeout(p.timer);
56
- pending.delete(id);
57
- p.reject(err);
58
- }
59
- worker = null;
60
- });
61
- w.on("exit", (code) => {
62
- if (code !== 0) {
63
- for (const [id, p] of pending) {
64
- clearTimeout(p.timer);
65
- pending.delete(id);
66
- p.reject(new Error(`Resvg worker exited with code ${code}`));
67
- }
68
- }
69
- worker = null;
70
- });
71
- w.unref();
72
- return w;
73
- }
74
- function renderPng(svg, options) {
75
- return new Promise((resolve, reject) => {
76
- if (!worker)
77
- worker = createWorker();
78
- const id = ++requestId;
79
- const timer = setTimeout(() => {
80
- pending.delete(id);
81
- reject(new Error("resvg worker timed out \u2014 killing worker"));
82
- killWorker();
83
- }, 3e4);
84
- pending.set(id, { resolve, reject, timer });
85
- worker.postMessage({ id, svg, options });
86
- });
40
+ process.on("exit", workerQueue.shutdown);
87
41
  }
88
42
  class ResvgWorkerProxy {
89
43
  svg;
@@ -1,4 +1,5 @@
1
1
  import { Worker } from "node:worker_threads";
2
+ import { createWorkerQueue, parseWorkerTimeout } from "../worker-queue.js";
2
3
  import { extractResourceUrls as extractTakumiResourceUrls } from "./resource-urls.js";
3
4
  const workerCode = `
4
5
  const { createRequire } = require('node:module')
@@ -30,85 +31,24 @@ parentPort.on('message', async ({ id, type, newFonts, nodes, options }) => {
30
31
  // Always slice to create a standard ArrayBuffer \u2014 native addon buffers
31
32
  // use external memory that can't be transferred via postMessage
32
33
  const ab = image.buffer.slice(image.byteOffset, image.byteOffset + image.byteLength)
33
- parentPort.postMessage({ id, image: ab, fontWarnings }, [ab])
34
+ parentPort.postMessage({ _tag: 'response', id, payload: { _tag: 'success', image: ab, fontWarnings } }, [ab])
34
35
  } catch (err) {
35
- parentPort.postMessage({ id, error: err?.message || String(err) })
36
+ parentPort.postMessage({ _tag: 'response', id, payload: { _tag: 'failure', error: err?.message || String(err) } })
36
37
  }
37
38
  })
39
+ parentPort.postMessage({ _tag: 'ready' })
38
40
  `;
39
- let worker = null;
40
- let workerGeneration = 0;
41
- let requestId = 0;
42
- const pending = /* @__PURE__ */ new Map();
43
- function killWorker() {
44
- if (!worker)
45
- return;
46
- worker.terminate();
47
- worker = null;
48
- for (const [id, p] of pending) {
49
- clearTimeout(p.timer);
50
- pending.delete(id);
51
- p.reject(new Error("Takumi worker terminated"));
52
- }
53
- }
41
+ const workerTimeout = parseWorkerTimeout(process.env.NUXT_OG_IMAGE_WORKER_TIMEOUT);
42
+ const workerQueue = createWorkerQueue({
43
+ createWorker: () => new Worker(workerCode, { eval: true }),
44
+ executionTimeout: workerTimeout,
45
+ startupTimeout: workerTimeout,
46
+ label: "takumi"
47
+ });
54
48
  const signalKey = Symbol.for("og-image:takumi-worker-cleanup");
55
49
  if (!globalThis[signalKey]) {
56
50
  globalThis[signalKey] = true;
57
- process.on("exit", killWorker);
58
- }
59
- function createWorker() {
60
- workerGeneration++;
61
- const w = new Worker(workerCode, { eval: true });
62
- w.on("message", ({ id, image, urls, error, fontWarnings }) => {
63
- const p = pending.get(id);
64
- if (p) {
65
- clearTimeout(p.timer);
66
- pending.delete(id);
67
- if (error)
68
- p.reject(new Error(error));
69
- else if (urls !== void 0)
70
- p.resolve(urls);
71
- else
72
- p.resolve({ image: Buffer.from(image), fontWarnings });
73
- }
74
- });
75
- w.on("error", (err) => {
76
- for (const [id, p] of pending) {
77
- clearTimeout(p.timer);
78
- pending.delete(id);
79
- p.reject(err);
80
- }
81
- worker = null;
82
- });
83
- w.on("exit", (code) => {
84
- if (code !== 0) {
85
- for (const [id, p] of pending) {
86
- clearTimeout(p.timer);
87
- pending.delete(id);
88
- p.reject(new Error(`Takumi worker exited with code ${code}`));
89
- }
90
- }
91
- worker = null;
92
- });
93
- w.unref();
94
- return w;
95
- }
96
- function ensureWorker() {
97
- if (!worker)
98
- worker = createWorker();
99
- }
100
- function postToWorker(msg, timeoutMs = 3e4) {
101
- return new Promise((resolve, reject) => {
102
- ensureWorker();
103
- const id = ++requestId;
104
- const timer = setTimeout(() => {
105
- pending.delete(id);
106
- reject(new Error("takumi worker timed out \u2014 killing worker"));
107
- killWorker();
108
- }, timeoutMs);
109
- pending.set(id, { resolve, reject, timer });
110
- worker.postMessage({ id, ...msg });
111
- });
51
+ process.on("exit", workerQueue.shutdown);
112
52
  }
113
53
  function extractResourceUrls(nodes) {
114
54
  return Promise.resolve(extractTakumiResourceUrls(nodes));
@@ -130,21 +70,24 @@ class RendererWorkerProxy {
130
70
  this.loadFont(font);
131
71
  }
132
72
  render(nodes, options) {
133
- ensureWorker();
134
- let fontsToSend;
135
- if (this.syncedGeneration !== workerGeneration) {
136
- fontsToSend = [...this.allFonts];
137
- this.pendingFonts = [];
138
- } else {
139
- fontsToSend = this.pendingFonts.splice(0);
140
- }
141
- this.syncedGeneration = workerGeneration;
142
- return postToWorker({ type: "render", newFonts: fontsToSend, nodes, options }).then((result) => {
73
+ return workerQueue.enqueue(({ id, generation }) => {
74
+ let fontsToSend;
75
+ if (this.syncedGeneration !== generation) {
76
+ fontsToSend = [...this.allFonts];
77
+ this.pendingFonts = [];
78
+ } else {
79
+ fontsToSend = this.pendingFonts.splice(0);
80
+ }
81
+ this.syncedGeneration = generation;
82
+ return { id, type: "render", newFonts: fontsToSend, nodes, options };
83
+ }).then((result) => {
84
+ if (result._tag === "failure")
85
+ throw new Error(result.error);
143
86
  if (result.fontWarnings?.length) {
144
87
  for (const w of result.fontWarnings)
145
88
  console.warn(`[nuxt-og-image] Failed to load font "${w.name}" (weight: ${w.weight}) into takumi renderer: ${w.error}`);
146
89
  }
147
- return result.image;
90
+ return Buffer.from(result.image);
148
91
  });
149
92
  }
150
93
  }
@@ -0,0 +1,34 @@
1
+ interface WorkerHandle {
2
+ on: {
3
+ (event: 'message', listener: (message: unknown) => void): WorkerHandle;
4
+ (event: 'error', listener: (error: Error) => void): WorkerHandle;
5
+ (event: 'exit', listener: (code: number) => void): WorkerHandle;
6
+ };
7
+ postMessage: (message: unknown) => void;
8
+ removeAllListeners: () => WorkerHandle;
9
+ terminate: () => Promise<number>;
10
+ unref: () => void;
11
+ }
12
+ interface QueueOptions {
13
+ createWorker: () => WorkerHandle;
14
+ executionTimeout: number;
15
+ startupTimeout: number;
16
+ label: string;
17
+ maxAttempts?: number;
18
+ }
19
+ interface Job<TMessage, TResponse> {
20
+ id: number;
21
+ buildMessage: (context: {
22
+ id: number;
23
+ generation: number;
24
+ }) => TMessage;
25
+ resolve: (response: TResponse) => void;
26
+ reject: (error: Error) => void;
27
+ attempts: number;
28
+ }
29
+ export declare function parseWorkerTimeout(value: string | undefined, fallback?: number): number;
30
+ export declare function createWorkerQueue<TMessage, TResponse>(options: QueueOptions): {
31
+ enqueue: (buildMessage: Job<TMessage, TResponse>["buildMessage"]) => Promise<TResponse>;
32
+ shutdown: () => void;
33
+ };
34
+ export {};