nuxt-og-image 6.6.0 → 6.7.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.
Files changed (35) hide show
  1. package/README.md +2 -0
  2. package/dist/chunks/tw4.cjs +7 -7
  3. package/dist/chunks/tw4.mjs +7 -7
  4. package/dist/chunks/uno.cjs +6 -6
  5. package/dist/chunks/uno.mjs +6 -6
  6. package/dist/cli.cjs +12 -5
  7. package/dist/cli.mjs +10 -3
  8. package/dist/module.cjs +5 -5
  9. package/dist/module.d.cts +5 -3
  10. package/dist/module.d.mts +5 -3
  11. package/dist/module.d.ts +5 -3
  12. package/dist/module.json +1 -1
  13. package/dist/module.mjs +5 -5
  14. package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.d.ts +1 -1
  15. package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.js +6 -1
  16. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +13 -6
  17. package/dist/runtime/server/og-image/bindings/font-assets/external-url.d.ts +36 -0
  18. package/dist/runtime/server/og-image/bindings/font-assets/external-url.js +47 -0
  19. package/dist/runtime/server/og-image/bindings/font-assets/node.d.ts +1 -1
  20. package/dist/runtime/server/og-image/bindings/font-assets/node.js +14 -7
  21. package/dist/runtime/server/og-image/bindings/takumi/node-dev.js +7 -1
  22. package/dist/runtime/server/og-image/fonts.d.ts +4 -0
  23. package/dist/runtime/server/og-image/fonts.js +21 -5
  24. package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
  25. package/dist/runtime/server/og-image/takumi/renderer.js +61 -19
  26. package/dist/runtime/server/util/ssrf.d.ts +10 -0
  27. package/dist/runtime/server/util/ssrf.js +9 -1
  28. package/dist/runtime/types.d.ts +13 -0
  29. package/dist/shared/{nuxt-og-image.DdbTs-xp.mjs → nuxt-og-image.CPSxZhVO.mjs} +122 -496
  30. package/dist/shared/nuxt-og-image.DM30kcDz.mjs +660 -0
  31. package/dist/shared/nuxt-og-image.RI9DgjgV.cjs +689 -0
  32. package/dist/shared/{nuxt-og-image.CfTPCtaS.cjs → nuxt-og-image.yDJBWXBt.cjs} +181 -555
  33. package/package.json +43 -39
  34. package/dist/shared/nuxt-og-image.CJa2KCie.mjs +0 -189
  35. package/dist/shared/nuxt-og-image.CMYbz66P.cjs +0 -193
@@ -1,23 +1,21 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
1
2
  import * as fs from 'node:fs';
2
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, rmSync } from 'node:fs';
3
4
  import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
4
5
  import { pathToFileURL } from 'node:url';
5
- import { resolvePath, useNuxt, addTemplate, updateTemplates, addTypeTemplate, defineNuxtModule, createResolver, addImports, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, addVitePlugin, addBuildPlugin, addServerHandler, addPlugin, addComponentsDir, addServerPlugin } from '@nuxt/kit';
6
+ import { useNuxt, resolvePath, updateTemplates, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, addImports, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, addVitePlugin, addBuildPlugin, addServerHandler, addPlugin, addComponentsDir, addServerPlugin } from '@nuxt/kit';
6
7
  import { defu } from 'defu';
7
8
  import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
8
9
  import { hash } from 'ohash';
9
- import { basename, join, dirname, relative, isAbsolute, resolve } from 'pathe';
10
+ import { join, dirname, relative, isAbsolute, resolve, basename } from 'pathe';
10
11
  import { readPackageJSON } from 'pkg-types';
11
- import { isCI, isAgent, hasTTY } from 'std-env';
12
- import { createHash } from 'node:crypto';
12
+ import { isCI, isAgent } from 'std-env';
13
13
  import { parseAndWalk } from 'oxc-walker';
14
- import { resolveNitroPreset, getNuxtModuleOptions, isNuxtGenerate } from 'nuxtseo-shared/kit';
15
- import { logger } from '../../dist/runtime/logger.js';
16
- import { Launcher } from 'chrome-launcher';
17
- import { detectPackageManager, addDependency } from 'nypm';
14
+ import { c as applyNitroPresetCompatibility, r as resolveOgImagePreset, R as RE_LEGACY_SUFFIX, g as getPresetNitroPresetCompatibility, d as getRecommendedBinding, e as getMissingDependencies, f as RE_RENDERER_SUFFIX, h as RE_WHITESPACE, p as promptFontsMigration, i as getRendererFromFilename, v as validateProviderSetup, j as getInstalledProviders, T as TAKUMI_CORE_INSTALL_SPEC, k as canPromptInteractively, P as PROVIDER_DEPENDENCIES, l as getRecommendedBindingFromPreset, n as getMissingDependencyInstallSpecs, o as ensureProviderDependencies, q as getProviderDependencyInstallSpecs, s as hasResolvableDependency, t as getRegisteredBaseNames, u as ensureDependencies, w as checkLocalChrome, x as promptForRendererSelection, y as isUndefinedOrTruthy } from './nuxt-og-image.DM30kcDz.mjs';
18
15
  import { setupDevToolsUI as setupDevToolsUI$1, setupDevToolsRpc } from 'nuxtseo-shared/devtools';
19
16
  import { createStorage } from 'unstorage';
20
17
  import fsDriver from 'unstorage/drivers/fs-lite';
18
+ import { logger } from '../../dist/runtime/logger.js';
21
19
  import { resolveColorMix, GRADIENT_COLOR_SPACE_RE, splitCssDeclarations } from '../../dist/runtime/server/og-image/utils/css.js';
22
20
  import MagicString from 'magic-string';
23
21
  import { stripLiteral } from 'strip-literal';
@@ -28,281 +26,11 @@ import { ofetch } from 'ofetch';
28
26
  import { parse as parse$1, walkSync, ELEMENT_NODE as ELEMENT_NODE$2, renderSync } from 'ultrahtml';
29
27
  import { RE_MATCH_EMOJIS, getEmojiCodePoint, getEmojiIconNames } from '../../dist/runtime/server/og-image/core/transforms/emojis/emoji-utils.js';
30
28
  import { resolveModulePath } from 'exsolve';
31
- import { p as promptFontsMigration } from './nuxt-og-image.CJa2KCie.mjs';
32
-
33
- const isUndefinedOrTruthy = (v) => typeof v === "undefined" || v !== false;
34
- function checkLocalChrome() {
35
- if (isCI)
36
- return false;
37
- let hasChromeLocally = false;
38
- try {
39
- hasChromeLocally = !!Launcher.getFirstInstallation();
40
- } catch {
41
- }
42
- return hasChromeLocally;
43
- }
44
- async function hasResolvableDependency(dep) {
45
- return await resolvePath(dep, { fallbackToOriginal: true }).catch(() => null).then((r) => r && r !== dep);
46
- }
47
- const VALID_RENDERER_SUFFIXES = ["satori", "browser", "takumi"];
48
- const RE_RENDERER_SUFFIX_CI = /\.?(satori|browser|takumi)$/i;
49
- const RE_RENDERER_SUFFIX = /(Satori|Browser|Takumi)$/;
50
- const RE_LEGACY_SUFFIX = /-legacy$/;
51
- const RE_WHITESPACE = /\s+/;
52
- function getRendererFromFilename(filepath) {
53
- const filename = basename(filepath).replace(".vue", "");
54
- for (const suffix of VALID_RENDERER_SUFFIXES) {
55
- if (filename.endsWith(`.${suffix}`))
56
- return suffix;
57
- }
58
- return null;
59
- }
60
- const OGIMAGE_PREFIXES = [
61
- { prefix: "OgImageCommunity", overlapWord: "Community" },
62
- { prefix: "OgImageTemplate", overlapWord: "Template" },
63
- { prefix: "OgImage", overlapWord: "Image" }
64
- ];
65
- function getRegisteredBaseNames(registeredPascalName) {
66
- const stripped = registeredPascalName.replace(RE_RENDERER_SUFFIX_CI, "").replace(RE_RENDERER_SUFFIX, "");
67
- const names = [];
68
- for (const { prefix, overlapWord } of OGIMAGE_PREFIXES) {
69
- if (!stripped.startsWith(prefix))
70
- continue;
71
- const withoutPrefix = stripped.slice(prefix.length);
72
- if (withoutPrefix) {
73
- names.push(withoutPrefix);
74
- if (withoutPrefix !== overlapWord)
75
- names.push(overlapWord + withoutPrefix);
76
- } else {
77
- names.push(overlapWord);
78
- }
79
- break;
80
- }
81
- if (names.length === 0)
82
- names.push(stripped);
83
- return names;
84
- }
85
-
86
- const NodeRuntime = {
87
- // node-server runtime
88
- browser: "on-demand",
89
- // this gets changed build start
90
- resvg: "node",
91
- satori: "node",
92
- takumi: "node",
93
- sharp: "node",
94
- // will be disabled if they're missing the dependency
95
- emoji: "local"
96
- // can bundle icons, no size constraints
97
- };
98
- const NodeDevRuntime = {
99
- ...NodeRuntime,
100
- resvg: "node-dev",
101
- // use worker to prevent crashes from killing process
102
- takumi: "node-dev"
103
- // use worker to prevent crashes from killing process
104
- };
105
- const NodePrerenderRuntime = {
106
- ...NodeRuntime,
107
- resvg: "node-dev",
108
- // use worker to prevent crashes from killing prerender process
109
- takumi: "node-dev"
110
- // use worker to prevent crashes from killing prerender process
111
- };
112
- const cloudflare = {
113
- browser: "cloudflare",
114
- resvg: "wasm",
115
- satori: "wasm",
116
- takumi: "wasm",
117
- sharp: false,
118
- emoji: "fetch",
119
- // edge size limits - use API instead of bundling 24MB icons
120
- wasm: {
121
- esmImport: true
122
- // Do NOT set lazy here — Nitro's cloudflare preset uses lazy: false because
123
- // CF Workers require WASM modules to be imported eagerly via top-level await
124
- }
125
- };
126
- const awsLambda = {
127
- browser: false,
128
- resvg: "node",
129
- satori: "node",
130
- takumi: "node",
131
- sharp: false,
132
- // 0.33.x has issues
133
- emoji: "local"
134
- // serverless has larger size limits
135
- };
136
- const WebContainer = {
137
- browser: false,
138
- resvg: "wasm-fs",
139
- satori: "wasm-fs",
140
- takumi: "wasm",
141
- sharp: false,
142
- emoji: "fetch"
143
- // webcontainer has size constraints
144
- };
145
- const RuntimeCompatibility = {
146
- "nitro-dev": NodeDevRuntime,
147
- "nitro-prerender": NodePrerenderRuntime,
148
- "node-server": NodeRuntime,
149
- "node-cluster": NodeRuntime,
150
- "node-listener": NodeRuntime,
151
- "bun": NodeRuntime,
152
- "deno-server": NodeRuntime,
153
- "deno-deploy": NodeRuntime,
154
- // node-server extending presets
155
- "alwaysdata": NodeRuntime,
156
- "cleavr": NodeRuntime,
157
- "digital-ocean": NodeRuntime,
158
- "edgio": NodeRuntime,
159
- "flight-control": NodeRuntime,
160
- "heroku": NodeRuntime,
161
- "iis-handler": NodeRuntime,
162
- "iis-node": NodeRuntime,
163
- "koyeb": NodeRuntime,
164
- "platform-sh": NodeRuntime,
165
- "render-com": NodeRuntime,
166
- "zeabur": NodeRuntime,
167
- "zerops": NodeRuntime,
168
- "stackblitz": WebContainer,
169
- "codesandbox": WebContainer,
170
- "aws-amplify": awsLambda,
171
- "aws-lambda": awsLambda,
172
- "azure": awsLambda,
173
- "azure-functions": awsLambda,
174
- "azure-swa": awsLambda,
175
- "netlify": awsLambda,
176
- "netlify-edge": {
177
- browser: false,
178
- resvg: "wasm",
179
- satori: "node",
180
- takumi: "wasm",
181
- sharp: false,
182
- emoji: "fetch",
183
- // edge size limits
184
- wasm: {
185
- rollup: {
186
- targetEnv: "auto-inline",
187
- sync: ["@resvg/resvg-wasm/index_bg.wasm"]
188
- }
189
- }
190
- },
191
- "firebase": awsLambda,
192
- "firebase-app-hosting": awsLambda,
193
- "genezio": awsLambda,
194
- "stormkit": awsLambda,
195
- "vercel": awsLambda,
196
- "vercel-edge": {
197
- browser: false,
198
- resvg: "wasm",
199
- satori: "wasm",
200
- takumi: "wasm",
201
- sharp: false,
202
- emoji: "fetch",
203
- // edge size limits - bundling 24MB icons not viable
204
- wasm: {
205
- // lowers workers kb size
206
- esmImport: true,
207
- lazy: true
208
- }
209
- },
210
- "cloudflare-pages": cloudflare,
211
- "cloudflare-pages-static": cloudflare,
212
- "cloudflare": cloudflare,
213
- "cloudflare-module": cloudflare,
214
- "cloudflare-durable": cloudflare
215
- };
216
- function resolveOgImagePreset(nitroConfig) {
217
- const nuxt = useNuxt();
218
- if (nuxt.options.dev)
219
- return "nitro-dev";
220
- if (nuxt.options.nitro.static)
221
- return "nitro-prerender";
222
- return resolveNitroPreset(nitroConfig);
223
- }
224
- function getPresetNitroPresetCompatibility(target) {
225
- const normalizedTarget = target.replace(RE_LEGACY_SUFFIX, "");
226
- let compatibility = RuntimeCompatibility[normalizedTarget];
227
- if (!compatibility) {
228
- logger.warn(`Unknown Nitro preset "${target}" \u2014 falling back to node-server compatibility. Set ogImage.compatibility.runtime to override.`);
229
- compatibility = NodeRuntime;
230
- }
231
- return compatibility;
232
- }
233
- async function applyNitroPresetCompatibility(nitroConfig, options) {
234
- const target = resolveOgImagePreset(nitroConfig);
235
- const compatibility = getPresetNitroPresetCompatibility(target);
236
- const { resolve, detectedRenderers } = options;
237
- const satoriEnabled = detectedRenderers.has("satori");
238
- const browserEnabled = detectedRenderers.has("browser");
239
- const takumiEnabled = detectedRenderers.has("takumi");
240
- for (const renderer of detectedRenderers) {
241
- if (!compatibility[renderer]) {
242
- logger.warn(`Renderer "${renderer}" detected but not supported on "${target}" preset. OG images using .${renderer}.vue components may fail.`);
243
- }
244
- }
245
- const emptyMock = await resolve.resolvePath("./runtime/mock/empty");
246
- nitroConfig.alias["#og-image/renderers/satori"] = satoriEnabled ? await resolve.resolvePath("./runtime/server/og-image/satori/renderer") : emptyMock;
247
- nitroConfig.alias["#og-image/renderers/browser"] = browserEnabled ? await resolve.resolvePath("./runtime/server/og-image/browser/renderer") : emptyMock;
248
- nitroConfig.alias["#og-image/renderers/takumi"] = takumiEnabled ? await resolve.resolvePath("./runtime/server/og-image/takumi/renderer") : emptyMock;
249
- const resolvedCompatibility = {};
250
- async function applyBinding(key) {
251
- let binding = options.compatibility?.[key];
252
- if (typeof binding === "undefined")
253
- binding = compatibility[key];
254
- const isRendererBinding = key === "browser" || key === "satori" || key === "takumi";
255
- if (isRendererBinding && !detectedRenderers.has(key)) {
256
- binding = false;
257
- }
258
- resolvedCompatibility[key] = binding;
259
- if (!binding && isRendererBinding) {
260
- nitroConfig.alias[`#og-image/renderers/${key}`] = emptyMock;
261
- }
262
- return {
263
- [`#og-image/bindings/${key}`]: !binding ? emptyMock : await resolve.resolvePath(`./runtime/server/og-image/bindings/${key}/${binding}`)
264
- };
265
- }
266
- const satoriPkgDir = await resolve.resolvePath("satori/package.json").then((p) => p.replace("/package.json", ""));
267
- const yogaWasmPath = `${satoriPkgDir}/yoga.wasm`;
268
- nitroConfig.alias = defu(
269
- { "#og-image/yoga-wasm": `${yogaWasmPath}?module` },
270
- await applyBinding("browser"),
271
- await applyBinding("satori"),
272
- await applyBinding("takumi"),
273
- await applyBinding("resvg"),
274
- await applyBinding("sharp"),
275
- nitroConfig.alias || {}
276
- );
277
- if (Object.values(resolvedCompatibility).includes("wasm")) {
278
- nitroConfig.experimental = nitroConfig.experimental || {};
279
- nitroConfig.experimental.wasm = true;
280
- }
281
- nitroConfig.rollupConfig = nitroConfig.rollupConfig || {};
282
- nitroConfig.wasm = defu(compatibility.wasm, nitroConfig.wasm);
283
- nitroConfig.virtual["#og-image/compatibility"] = () => `export default ${JSON.stringify(resolvedCompatibility)}`;
284
- addTemplate({
285
- filename: "nuxt-og-image/compatibility.mjs",
286
- getContents() {
287
- return `export default ${JSON.stringify(resolvedCompatibility)}`;
288
- },
289
- options: { mode: "server" }
290
- });
291
- return resolvedCompatibility;
292
- }
293
- async function ensureDependencies(dep, nuxt = useNuxt()) {
294
- const { ensureDependencyInstalled } = await import('nypm');
295
- return Promise.all(dep.map((d) => {
296
- return ensureDependencyInstalled(d, {
297
- cwd: nuxt.options.rootDir,
298
- dev: true
299
- });
300
- }));
301
- }
29
+ import { getNuxtModuleOptions, isNuxtGenerate } from 'nuxtseo-shared/kit';
302
30
 
303
31
  const RE_REFLECT_HAS_MINIFIED = /Reflect\.has\(([\w$]+),([\w$]+)\)\?Reflect\.get\(\1,\2,([\w$]+)\):Reflect\.get\(([\w$]+),\2,\3\)/g;
304
32
  const RE_WASM_IMPORT = /import\("(\.\/wasm\/[^"]+\.wasm)"\)/g;
305
- async function setupBuildHandler(config, resolve, getDetectedRenderers, nuxt = useNuxt()) {
33
+ async function setupBuildHandler(config, resolve, getDetectedRenderers, getCompatibilityMeta = () => ({}), nuxt = useNuxt()) {
306
34
  nuxt.options.nitro.storage = nuxt.options.nitro.storage || {};
307
35
  if (typeof config.runtimeCacheStorage === "object")
308
36
  nuxt.options.nitro.storage["nuxt-og-image"] = config.runtimeCacheStorage;
@@ -322,7 +50,7 @@ async function setupBuildHandler(config, resolve, getDetectedRenderers, nuxt = u
322
50
  });
323
51
  nuxt.hooks.hook("nitro:init", async (nitro) => {
324
52
  const renderers = getDetectedRenderers();
325
- await applyNitroPresetCompatibility(nitro.options, { compatibility: config.compatibility?.runtime, resolve, detectedRenderers: renderers });
53
+ await applyNitroPresetCompatibility(nitro.options, { compatibility: config.compatibility?.runtime, resolve, detectedRenderers: renderers, metadata: getCompatibilityMeta() });
326
54
  const target = resolveOgImagePreset(nitro.options);
327
55
  const normalizedTarget = target.replace(RE_LEGACY_SUFFIX, "");
328
56
  const isEdgePreset = ["cloudflare", "cloudflare-pages", "cloudflare-pages-static", "cloudflare-module", "cloudflare-durable", "vercel-edge", "netlify-edge"].includes(normalizedTarget);
@@ -455,182 +183,7 @@ function patchWebAssemblyInstantiate(code) {
455
183
  return code;
456
184
  }
457
185
 
458
- const PROVIDER_DEPENDENCIES = [
459
- {
460
- name: "satori",
461
- description: "SVG-based renderer using Satori",
462
- bindings: {
463
- "node": [
464
- { name: "satori", description: "HTML to SVG renderer" },
465
- { name: "@resvg/resvg-js", description: "SVG to PNG converter (native)" }
466
- ],
467
- "wasm": [
468
- { name: "satori", description: "HTML to SVG renderer" },
469
- { name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
470
- ],
471
- "wasm-fs": [
472
- { name: "satori", description: "HTML to SVG renderer" },
473
- { name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
474
- ]
475
- }
476
- },
477
- {
478
- name: "takumi",
479
- description: "Rust-based high-performance renderer (recommended)",
480
- bindings: {
481
- "node": [
482
- { name: "@takumi-rs/core", description: "Native Takumi renderer" }
483
- ],
484
- "wasm": [
485
- { name: "@takumi-rs/wasm", description: "WASM Takumi renderer" }
486
- ],
487
- "wasm-fs": [
488
- { name: "@takumi-rs/wasm", description: "WASM Takumi renderer" }
489
- ]
490
- }
491
- },
492
- {
493
- name: "browser",
494
- description: "Browser-based screenshot renderer",
495
- bindings: {
496
- node: [
497
- { name: "playwright-core", description: "Headless browser automation" }
498
- ]
499
- }
500
- }
501
- ];
502
- async function getInstalledProviders() {
503
- const installed = [];
504
- for (const provider of PROVIDER_DEPENDENCIES) {
505
- if (provider.bindings.node) {
506
- const allNodeInstalled = await Promise.all(
507
- provider.bindings.node.map((dep) => hasResolvableDependency(dep.name))
508
- );
509
- if (allNodeInstalled.every(Boolean)) {
510
- installed.push({ provider: provider.name, binding: "node" });
511
- continue;
512
- }
513
- }
514
- if (provider.bindings.wasm) {
515
- const allWasmInstalled = await Promise.all(
516
- provider.bindings.wasm.map((dep) => hasResolvableDependency(dep.name))
517
- );
518
- if (allWasmInstalled.every(Boolean)) {
519
- installed.push({ provider: provider.name, binding: "wasm" });
520
- }
521
- }
522
- }
523
- return installed;
524
- }
525
- async function getMissingDependencies(provider, binding = "node") {
526
- const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
527
- if (!providerDef)
528
- return [];
529
- const deps = providerDef.bindings[binding] || providerDef.bindings.node || [];
530
- const missing = [];
531
- for (const dep of deps) {
532
- if (!await hasResolvableDependency(dep.name))
533
- missing.push(dep.name);
534
- }
535
- return missing;
536
- }
537
- function getProviderDependencies(provider, binding = "node") {
538
- const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
539
- if (!providerDef)
540
- return [];
541
- const deps = providerDef.bindings[binding] || providerDef.bindings.node || [];
542
- return deps.map((d) => d.name);
543
- }
544
- function getRecommendedBindingFromPreset(provider) {
545
- const preset = resolveOgImagePreset();
546
- const compatibility = getPresetNitroPresetCompatibility(preset);
547
- return getRecommendedBinding(provider, compatibility);
548
- }
549
- function getRecommendedBinding(provider, compatibility) {
550
- if (provider === "satori") {
551
- const resvgBinding = compatibility.resvg;
552
- if (resvgBinding === "wasm-fs")
553
- return "wasm-fs";
554
- if (resvgBinding === "wasm")
555
- return "wasm";
556
- return "node";
557
- }
558
- if (provider === "takumi") {
559
- const takumiBinding = compatibility.takumi;
560
- if (takumiBinding === "wasm")
561
- return "wasm";
562
- return "node";
563
- }
564
- if (provider === "browser") {
565
- return "node";
566
- }
567
- return "node";
568
- }
569
- async function ensureProviderDependencies(provider, binding, nuxt) {
570
- const missing = await getMissingDependencies(provider, binding);
571
- if (missing.length === 0)
572
- return { success: true, installed: [] };
573
- const pm = await detectPackageManager(nuxt.options.rootDir);
574
- const pmName = pm?.name || "npm";
575
- logger.info(`Installing ${provider} dependencies: ${missing.join(", ")}`);
576
- const installed = [];
577
- for (const pkg of missing) {
578
- const success = await addDependency(pkg, {
579
- cwd: nuxt.options.rootDir,
580
- dev: false
581
- }).then(() => {
582
- installed.push(pkg);
583
- return true;
584
- }).catch(() => {
585
- logger.error(`Failed to install ${pkg}. Run manually: ${pmName} add ${pkg}`);
586
- return false;
587
- });
588
- if (!success)
589
- return { success: false, installed };
590
- }
591
- return { success: true, installed };
592
- }
593
- function canPromptInteractively(env = {
594
- hasTTY,
595
- hasStdinTTY: Boolean(process.stdin?.isTTY),
596
- isAgent,
597
- isCI
598
- }) {
599
- return env.hasTTY && env.hasStdinTTY && !env.isAgent && !env.isCI;
600
- }
601
- async function promptForRendererSelection() {
602
- logger.info("Welcome to Nuxt OG Image! No renderer dependencies detected.");
603
- const renderer = await logger.prompt("Which renderer would you like to use?", {
604
- type: "select",
605
- options: PROVIDER_DEPENDENCIES.map((p) => p.name),
606
- initial: "takumi"
607
- });
608
- return renderer || "takumi";
609
- }
610
- async function validateProviderSetup(renderer, compatibility) {
611
- const issues = [];
612
- const binding = getRecommendedBinding(renderer, compatibility);
613
- const missing = await getMissingDependencies(renderer, binding);
614
- if (missing.length > 0) {
615
- issues.push(`Missing ${renderer} dependencies: ${missing.join(", ")}`);
616
- }
617
- if (renderer === "satori") {
618
- const hasResvgNode = await hasResolvableDependency("@resvg/resvg-js");
619
- const hasResvgWasm = await hasResolvableDependency("@resvg/resvg-wasm");
620
- if (!hasResvgNode && !hasResvgWasm) {
621
- issues.push("Satori requires either @resvg/resvg-js (node) or @resvg/resvg-wasm to render PNGs");
622
- }
623
- }
624
- if (renderer === "browser") {
625
- const hasPlaywright = await hasResolvableDependency("playwright-core");
626
- if (!hasPlaywright && binding === "node") {
627
- issues.push("Browser renderer requires playwright-core");
628
- }
629
- }
630
- return { valid: issues.length === 0, issues };
631
- }
632
-
633
- function setupDevHandler(options, resolve, getDetectedRenderers, nuxt = useNuxt()) {
186
+ function setupDevHandler(options, resolve, getDetectedRenderers, getCompatibilityMeta = () => ({}), nuxt = useNuxt()) {
634
187
  nuxt.hooks.hook("nitro:init", async (nitro) => {
635
188
  const detectedRenderers = new Set(getDetectedRenderers());
636
189
  const targetCompatibility = getPresetNitroPresetCompatibility(resolveOgImagePreset(nitro.options));
@@ -642,7 +195,7 @@ function setupDevHandler(options, resolve, getDetectedRenderers, nuxt = useNuxt(
642
195
  detectedRenderers.add(renderer);
643
196
  }
644
197
  }
645
- await applyNitroPresetCompatibility(nitro.options, { compatibility: options.compatibility?.dev, resolve, detectedRenderers });
198
+ await applyNitroPresetCompatibility(nitro.options, { compatibility: options.compatibility?.dev, resolve, detectedRenderers, metadata: getCompatibilityMeta() });
646
199
  });
647
200
  }
648
201
 
@@ -2446,10 +1999,10 @@ function setupGenerateHandler(_options, resolve, _getDetectedRenderers, nuxt = u
2446
1999
  }
2447
2000
 
2448
2001
  const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
2449
- function setupPrerenderHandler(options, resolve, getDetectedRenderers, nuxt = useNuxt()) {
2002
+ function setupPrerenderHandler(options, resolve, getDetectedRenderers, getCompatibilityMeta = () => ({}), nuxt = useNuxt()) {
2450
2003
  nuxt.hooks.hook("nitro:init", async (nitro) => {
2451
2004
  nitro.hooks.hook("prerender:config", async (nitroConfig) => {
2452
- await applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.prerender, resolve, detectedRenderers: getDetectedRenderers() });
2005
+ await applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.prerender, resolve, detectedRenderers: getDetectedRenderers(), metadata: getCompatibilityMeta() });
2453
2006
  nitroConfig.wasm = nitroConfig.wasm || {};
2454
2007
  nitroConfig.wasm.esmImport = false;
2455
2008
  nitroConfig.prerender = nitroConfig.prerender || {};
@@ -2537,6 +2090,15 @@ function extractPropNamesFromVue(code) {
2537
2090
  }
2538
2091
  }
2539
2092
 
2093
+ function resolveSigningSecret(configSecret, envSecret, generate) {
2094
+ if (configSecret === false)
2095
+ return { secret: "", hasExplicit: false, optOut: true, generated: false };
2096
+ const explicit = configSecret || "" || (envSecret || "");
2097
+ if (explicit)
2098
+ return { secret: explicit, hasExplicit: true, optOut: false, generated: false };
2099
+ return { secret: generate(), hasExplicit: false, optOut: false, generated: true };
2100
+ }
2101
+
2540
2102
  function isVue(id, opts = {}) {
2541
2103
  const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href));
2542
2104
  if (id.endsWith(".vue") && !search) {
@@ -4197,7 +3759,11 @@ async function onInstall(nuxt) {
4197
3759
  const installedProviders2 = await getInstalledProviders();
4198
3760
  if (installedProviders2.length === 0) {
4199
3761
  throw new Error(
4200
- "[nuxt-og-image] No OG image provider dependencies found. Install a provider before running in CI:\n npm add @resvg/resvg-js satori yoga-wasm-web # for satori\n npm add playwright-core # for browser\nSee: https://nuxtseo.com/og-image/getting-started"
3762
+ `[nuxt-og-image] No OG image provider dependencies found. Install a provider before running in CI:
3763
+ npm add ${TAKUMI_CORE_INSTALL_SPEC} # for takumi
3764
+ npm add @resvg/resvg-js satori yoga-wasm-web # for satori
3765
+ npm add playwright-core # for browser
3766
+ See: https://nuxtseo.com/og-image/getting-started`
4201
3767
  );
4202
3768
  }
4203
3769
  return;
@@ -4269,10 +3835,10 @@ async function onInstall(nuxt) {
4269
3835
  state.selectedBinding = hasNodeBinding ? "node" : "wasm";
4270
3836
  }
4271
3837
  }
4272
- const missing = await getMissingDependencies(state.selectedRenderer, state.selectedBinding);
4273
- if (missing.length > 0) {
3838
+ const missingInstallSpecs = await getMissingDependencyInstallSpecs(state.selectedRenderer, state.selectedBinding);
3839
+ if (missingInstallSpecs.length > 0) {
4274
3840
  logger.info(`Required dependencies for ${state.selectedRenderer} (${state.selectedBinding}):`);
4275
- for (const pkg of missing) {
3841
+ for (const pkg of missingInstallSpecs) {
4276
3842
  logger.info(` - ${pkg}`);
4277
3843
  }
4278
3844
  const installDeps = await logger.prompt("Install missing dependencies?", {
@@ -4289,11 +3855,11 @@ async function onInstall(nuxt) {
4289
3855
  logger.success(`Installed: ${result.installed.join(", ")}`);
4290
3856
  } else {
4291
3857
  logger.error("Failed to install some dependencies. Install manually:");
4292
- const allDeps = getProviderDependencies(state.selectedRenderer, state.selectedBinding);
3858
+ const allDeps = getProviderDependencyInstallSpecs(state.selectedRenderer, state.selectedBinding);
4293
3859
  logger.info(` npm add ${allDeps.join(" ")}`);
4294
3860
  }
4295
3861
  } else {
4296
- const allDeps = getProviderDependencies(state.selectedRenderer, state.selectedBinding);
3862
+ const allDeps = getProviderDependencyInstallSpecs(state.selectedRenderer, state.selectedBinding);
4297
3863
  logger.warn("Dependencies required. Install manually:");
4298
3864
  logger.info(` npm add ${allDeps.join(" ")}`);
4299
3865
  }
@@ -4528,8 +4094,8 @@ declare module '#og-image-virtual/build-dir.mjs' {
4528
4094
  }
4529
4095
 
4530
4096
  declare module '#og-image/compatibility' {
4531
- import type { RuntimeCompatibilitySchema } from '${typesPath}'
4532
- const compatibility: Partial<Omit<RuntimeCompatibilitySchema, 'wasm'>>
4097
+ import type { RuntimeCompatibilityPayload } from '${typesPath}'
4098
+ const compatibility: RuntimeCompatibilityPayload
4533
4099
  export default compatibility
4534
4100
  }
4535
4101
  declare module '#og-image-virtual/tw4-theme.mjs' {
@@ -4645,6 +4211,44 @@ const RE_INLINE_CONFIG = /const\s+inlineConfig\s*=\s*(\{[\s\S]*?\n\})/;
4645
4211
  const RE_PASCAL_TO_KEBAB = /([a-z])([A-Z])/g;
4646
4212
  const RE_TILDE_PREFIX = /^~\//;
4647
4213
  const IS_MODULE_DEVELOPMENT = import.meta.filename.endsWith(".ts");
4214
+ async function findPackageVersionFromEntry(pkg, resolver) {
4215
+ const entry = await resolver.resolvePath(pkg, { fallbackToOriginal: false }).catch(() => null);
4216
+ if (!entry || entry === pkg)
4217
+ return;
4218
+ let dir = dirname(entry);
4219
+ while (dir && dir !== dirname(dir)) {
4220
+ const pkgPath = join(dir, "package.json");
4221
+ if (existsSync(pkgPath)) {
4222
+ const pkgJson = await readPackageJSON(pkgPath).catch(() => null);
4223
+ if (pkgJson?.name === pkg)
4224
+ return pkgJson.version;
4225
+ }
4226
+ dir = dirname(dir);
4227
+ }
4228
+ }
4229
+ function normalizeTakumiVersion(version) {
4230
+ const major = version ? Number.parseInt(version.split(".")[0], 10) : Number.NaN;
4231
+ if (major >= 2)
4232
+ return 2;
4233
+ if (major === 1)
4234
+ return 1;
4235
+ }
4236
+ async function detectTakumiVersion(resolver, binding) {
4237
+ const packages = binding === "wasm" ? ["@takumi-rs/wasm", "@takumi-rs/core"] : ["@takumi-rs/core", "@takumi-rs/wasm"];
4238
+ const versions = (await Promise.all(packages.map(async (pkg) => {
4239
+ const version = await findPackageVersionFromEntry(pkg, resolver);
4240
+ const major = normalizeTakumiVersion(version);
4241
+ return version && major ? { pkg, version, major } : void 0;
4242
+ }))).filter(Boolean);
4243
+ const detectedMajors = new Set(versions.map(({ major }) => major));
4244
+ if (detectedMajors.size > 1) {
4245
+ const preferred = versions[0];
4246
+ logger.warn(
4247
+ `Mismatched @takumi-rs package major versions detected (${versions.map(({ pkg, version }) => `${pkg}@${version}`).join(", ")}). Using ${preferred.pkg}@${preferred.version} for Takumi v${preferred.major} compatibility.`
4248
+ );
4249
+ }
4250
+ return versions[0]?.major;
4251
+ }
4648
4252
  function isProviderEnabledForEnv(provider, nuxt, config) {
4649
4253
  return nuxt.options.dev && config.compatibility?.dev?.[provider] !== false || !nuxt.options.dev && (config.compatibility?.runtime?.[provider] !== false || config.compatibility?.prerender?.[provider] !== false);
4650
4254
  }
@@ -4724,23 +4328,30 @@ const module$1 = defineNuxtModule({
4724
4328
  if (config.debug && !nuxt.options.dev) {
4725
4329
  logger.warn("`ogImage.debug` is enabled in production. This exposes the `/_og/debug.json` endpoint and should not be enabled in production. Disable it before deploying.");
4726
4330
  }
4727
- const hasSecret = !!(config.security?.secret || process.env.NUXT_OG_IMAGE_SECRET);
4728
- if (config.security?.strict && !hasSecret) {
4331
+ const signing = resolveSigningSecret(
4332
+ config.security?.secret,
4333
+ process.env.NUXT_OG_IMAGE_SECRET,
4334
+ () => randomBytes(32).toString("base64url")
4335
+ );
4336
+ const resolvedSecret = signing.secret;
4337
+ if (config.security?.strict && !signing.hasExplicit) {
4729
4338
  throw new Error("[nuxt-og-image] `security.strict` requires a signing secret. Generate one with: npx nuxt-og-image generate-secret");
4730
4339
  }
4731
- if (nuxt.options.dev && !config.zeroRuntime && !hasSecret) {
4732
- logger.warn([
4733
- "OG image URLs are not signed. Anyone can craft arbitrary image generation requests.",
4734
- "",
4735
- "Set a signing secret via env variable:",
4736
- " NUXT_OG_IMAGE_SECRET=<secret>",
4737
- "",
4738
- " Generate one with: npx nuxt-og-image generate-secret",
4739
- "",
4740
- "Or enable zero-runtime mode to disable dynamic generation entirely:",
4741
- " ogImage: { zeroRuntime: true }"
4742
- ].join("\n"));
4743
- }
4340
+ nuxt.hook("nitro:init", (nitro) => {
4341
+ const hasServerRuntime = !nitro.options.static && !nuxt.options._generate;
4342
+ if (!nuxt.options.dev || config.zeroRuntime || !hasServerRuntime)
4343
+ return;
4344
+ if (signing.optOut) {
4345
+ logger.warn("OG image URL signing is disabled (`security.secret: false`). Anyone can craft arbitrary image generation requests.");
4346
+ } else if (signing.generated) {
4347
+ logger.warn([
4348
+ "OG image URLs are signed with an auto-generated secret that changes every build.",
4349
+ "This is fine for single-instance deploys; for rolling or multi-instance deploys set a stable secret:",
4350
+ " NUXT_OG_IMAGE_SECRET=<secret>",
4351
+ " Generate one with: npx nuxt-og-image generate-secret"
4352
+ ].join("\n"));
4353
+ }
4354
+ });
4744
4355
  const ogImageConfig = config;
4745
4356
  for (const key of Object.keys(REMOVED_CONFIG)) {
4746
4357
  if (key !== "chromium-node" && key !== "browser-node" && key in ogImageConfig && ogImageConfig[key] !== void 0) {
@@ -4780,6 +4391,7 @@ Or pin to a specific version in package.json:
4780
4391
  nuxt.options.alias["#og-image-cache"] = resolve("./runtime/server/og-image/cache/lru");
4781
4392
  const preset = resolveOgImagePreset(nuxt.options.nitro);
4782
4393
  const targetCompatibility = getPresetNitroPresetCompatibility(preset);
4394
+ const runtimeCompatibilityMeta = {};
4783
4395
  const normalizedPreset = preset.replace(RE_LEGACY_SUFFIX, "");
4784
4396
  const isCloudflareWorkers = ["cloudflare", "cloudflare-module", "cloudflare-durable"].includes(normalizedPreset);
4785
4397
  if (isCloudflareWorkers) {
@@ -5240,7 +4852,7 @@ ${familyInfo}`);
5240
4852
  logger.debug(`Using ${renderer} renderer`);
5241
4853
  } else {
5242
4854
  ogImageComponentCtx.detectedRenderers.add("takumi");
5243
- logger.warn("No OG image renderer dependency detected. Defaulting to `takumi` (non-interactive environment). Install `@takumi-rs/core`, or add a renderer component (e.g. components/OgImage/Default.satori.vue) to choose explicitly.");
4855
+ logger.warn(`No OG image renderer dependency detected. Defaulting to \`takumi\` (non-interactive environment). Install \`${TAKUMI_CORE_INSTALL_SPEC}\`, or add a renderer component (e.g. components/OgImage/Default.satori.vue) to choose explicitly.`);
5244
4856
  }
5245
4857
  }
5246
4858
  const availableRenderers = /* @__PURE__ */ new Set();
@@ -5252,19 +4864,22 @@ ${familyInfo}`);
5252
4864
  if (missing.length === 0) {
5253
4865
  availableRenderers.add(renderer);
5254
4866
  } else if (nuxt.options.dev && !nuxt.options._prepare) {
5255
- logger.warn(`${renderer} renderer requires: ${missing.join(", ")}`);
4867
+ const installSpecs = await getMissingDependencyInstallSpecs(renderer, binding);
4868
+ logger.warn(`${renderer} renderer requires: ${installSpecs.join(", ")}`);
5256
4869
  const { success } = await ensureProviderDependencies(renderer, binding, nuxt);
5257
4870
  if (success) {
5258
4871
  availableRenderers.add(renderer);
5259
4872
  } else if (isAgent) {
5260
- throw new Error(`[nuxt-og-image] Failed to install ${renderer} dependencies: ${missing.join(", ")}. Install manually: npx nypm add ${missing.join(" ")}`);
4873
+ throw new Error(`[nuxt-og-image] Failed to install ${renderer} dependencies: ${missing.join(", ")}. Install manually: npx nypm add ${installSpecs.join(" ")}`);
5261
4874
  } else {
5262
4875
  logger.error(`Failed to install ${renderer} dependencies. Templates using this renderer won't work.`);
5263
4876
  }
5264
4877
  } else if (isAgent) {
5265
- throw new Error(`[nuxt-og-image] ${renderer} renderer missing dependencies: ${missing.join(", ")}. Install with: npx nypm add ${missing.join(" ")}`);
4878
+ const installSpecs = await getMissingDependencyInstallSpecs(renderer, binding);
4879
+ throw new Error(`[nuxt-og-image] ${renderer} renderer missing dependencies: ${missing.join(", ")}. Install with: npx nypm add ${installSpecs.join(" ")}`);
5266
4880
  } else {
5267
- logger.error(`${renderer} renderer missing dependencies: ${missing.join(", ")}. Install with: npx nypm add ${missing.join(" ")}`);
4881
+ const installSpecs = await getMissingDependencyInstallSpecs(renderer, binding);
4882
+ logger.error(`${renderer} renderer missing dependencies: ${missing.join(", ")}. Install with: npx nypm add ${installSpecs.join(" ")}`);
5268
4883
  }
5269
4884
  if (renderer === "satori" && availableRenderers.has(renderer) && binding !== "node") {
5270
4885
  logger.warn("ReSVG native binding not available. Falling back to WASM version, this may slow down PNG rendering.");
@@ -5284,6 +4899,14 @@ ${familyInfo}`);
5284
4899
  availableRenderers.add(renderer);
5285
4900
  }
5286
4901
  }
4902
+ runtimeCompatibilityMeta.takumiVersion = await detectTakumiVersion(
4903
+ resolver,
4904
+ getRecommendedBinding("takumi", targetCompatibility)
4905
+ );
4906
+ if (ogImageComponentCtx.detectedRenderers.has("takumi") && !runtimeCompatibilityMeta.takumiVersion) {
4907
+ logger.warn("Unable to detect @takumi-rs version. Falling back to Takumi v1 renderer compatibility.");
4908
+ runtimeCompatibilityMeta.takumiVersion = 1;
4909
+ }
5287
4910
  if (nuxt.options.dev) {
5288
4911
  const communityRenderers = /* @__PURE__ */ new Set();
5289
4912
  for (const renderer of ["satori", "takumi", "browser"]) {
@@ -5383,9 +5006,11 @@ ${familyInfo}`);
5383
5006
  for (const renderer of ogImageComponentCtx.detectedRenderers) {
5384
5007
  if (!availableRenderers.has(renderer)) {
5385
5008
  const binding = getRecommendedBinding(renderer, targetCompatibility);
5386
- getMissingDependencies(renderer, binding).then((missing) => {
5387
- if (missing.length > 0)
5388
- logger.warn(`New ${renderer} component detected but dependencies missing: ${missing.join(", ")}. Install with: npx nypm add ${missing.join(" ")}`);
5009
+ getMissingDependencies(renderer, binding).then(async (missing) => {
5010
+ if (missing.length > 0) {
5011
+ const installSpecs = await getMissingDependencyInstallSpecs(renderer, binding);
5012
+ logger.warn(`New ${renderer} component detected but dependencies missing: ${missing.join(", ")}. Install with: npx nypm add ${installSpecs.join(" ")}`);
5013
+ }
5389
5014
  });
5390
5015
  }
5391
5016
  }
@@ -5623,7 +5248,7 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5623
5248
  imageFetchTimeout: config.security?.imageFetchTimeout ?? 3e3,
5624
5249
  maxQueryParamSize: config.security?.maxQueryParamSize ?? (config.security?.strict ? 2048 : null),
5625
5250
  restrictRuntimeImagesToOrigin: config.security?.restrictRuntimeImagesToOrigin === true || config.security?.strict && config.security?.restrictRuntimeImagesToOrigin == null ? [] : config.security?.restrictRuntimeImagesToOrigin || false,
5626
- secret: config.security?.secret || process.env.NUXT_OG_IMAGE_SECRET || ""
5251
+ secret: resolvedSecret
5627
5252
  }
5628
5253
  };
5629
5254
  if (nuxt.options.dev) {
@@ -5637,7 +5262,7 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5637
5262
  const existingOgImageCfg = nuxt.options.runtimeConfig.ogImage;
5638
5263
  nuxt.options.runtimeConfig.ogImage = {
5639
5264
  ...existingOgImageCfg && typeof existingOgImageCfg === "object" ? existingOgImageCfg : {},
5640
- secret: existingOgImageCfg?.secret || config.security?.secret || process.env.NUXT_OG_IMAGE_SECRET || ""
5265
+ secret: existingOgImageCfg?.secret || (signing.hasExplicit ? signing.secret : "")
5641
5266
  };
5642
5267
  nuxt.options.runtimeConfig.public = {
5643
5268
  ...nuxt.options.runtimeConfig.public,
@@ -5658,8 +5283,9 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5658
5283
  };
5659
5284
  });
5660
5285
  const getDetectedRenderers = () => ogImageComponentCtx.detectedRenderers;
5286
+ const getCompatibilityMeta = () => runtimeCompatibilityMeta;
5661
5287
  if (nuxt.options.dev) {
5662
- setupDevHandler(config, resolver, getDetectedRenderers);
5288
+ setupDevHandler(config, resolver, getDetectedRenderers, getCompatibilityMeta);
5663
5289
  setupDevToolsUI(config, resolve, nuxt, cssFramework || "none");
5664
5290
  const useNitro = new Promise((resolveNitro) => {
5665
5291
  nuxt.hooks.hook("nitro:init", resolveNitro);
@@ -5693,11 +5319,11 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5693
5319
  } else if (isNuxtGenerate()) {
5694
5320
  setupGenerateHandler(config, resolver);
5695
5321
  } else if (nuxt.options.build) {
5696
- await setupBuildHandler(config, resolver, getDetectedRenderers);
5322
+ await setupBuildHandler(config, resolver, getDetectedRenderers, getCompatibilityMeta);
5697
5323
  }
5698
5324
  if (nuxt.options.build)
5699
5325
  addServerPlugin(resolve("./runtime/server/plugins/prerender"));
5700
- setupPrerenderHandler(config, resolver, getDetectedRenderers);
5326
+ setupPrerenderHandler(config, resolver, getDetectedRenderers, getCompatibilityMeta);
5701
5327
  if (nuxt.options.dev && hasWarnings()) {
5702
5328
  nuxt.hook("ready", () => {
5703
5329
  emitWarnings();