nuxt-og-image 6.7.2 → 6.7.4

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 (36) hide show
  1. package/dist/chunks/tw4.cjs +7 -7
  2. package/dist/chunks/tw4.mjs +7 -7
  3. package/dist/chunks/uno.cjs +6 -6
  4. package/dist/chunks/uno.mjs +6 -6
  5. package/dist/cli.cjs +5 -12
  6. package/dist/cli.mjs +3 -10
  7. package/dist/devtools/lib/og-image/shared/urlEncoding.ts +5 -4
  8. package/dist/module.cjs +5 -5
  9. package/dist/module.json +1 -1
  10. package/dist/module.mjs +5 -5
  11. package/dist/runtime/app/plugins/__zero-runtime/og-image-canonical-urls.server.js +4 -6
  12. package/dist/runtime/app/plugins/__zero-runtime/route-rule-og-image.server.js +4 -6
  13. package/dist/runtime/app/plugins/og-image-canonical-urls.server.d.ts +1 -2
  14. package/dist/runtime/app/plugins/og-image-canonical-urls.server.js +1 -7
  15. package/dist/runtime/app/plugins/route-rule-og-image.server.d.ts +1 -2
  16. package/dist/runtime/app/plugins/route-rule-og-image.server.js +1 -7
  17. package/dist/runtime/app/utils/plugins.d.ts +2 -3
  18. package/dist/runtime/app/utils/plugins.js +5 -5
  19. package/dist/runtime/server/og-image/context.js +6 -5
  20. package/dist/runtime/server/routes/resolve.js +3 -2
  21. package/dist/runtime/server/util/cache.js +5 -4
  22. package/dist/runtime/server/util/kit.js +2 -2
  23. package/dist/runtime/server/util/query.d.ts +7 -0
  24. package/dist/runtime/server/util/query.js +4 -0
  25. package/dist/runtime/shared/hash.d.ts +10 -0
  26. package/dist/runtime/shared/hash.js +5 -0
  27. package/dist/runtime/shared/urlEncoding.d.ts +3 -2
  28. package/dist/runtime/shared/urlEncoding.js +9 -9
  29. package/dist/shared/{nuxt-og-image.BCK8Adm4.cjs → nuxt-og-image.BRBtdkUp.cjs} +570 -81
  30. package/dist/shared/{nuxt-og-image.Pfj5JkJd.mjs → nuxt-og-image.Bv8VSGTj.mjs} +499 -10
  31. package/dist/shared/nuxt-og-image.CJa2KCie.mjs +189 -0
  32. package/dist/shared/nuxt-og-image.CMYbz66P.cjs +193 -0
  33. package/package.json +46 -44
  34. package/types/virtual.d.ts +9 -0
  35. package/dist/shared/nuxt-og-image.DQkJQHaN.cjs +0 -689
  36. package/dist/shared/nuxt-og-image.LgUGeaz-.mjs +0 -660
@@ -3,19 +3,21 @@ import * as fs from 'node:fs';
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, rmSync } from 'node:fs';
4
4
  import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
5
5
  import { pathToFileURL } from 'node:url';
6
- import { useNuxt, resolvePath, updateTemplates, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, addImports, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, addVitePlugin, addBuildPlugin, addServerHandler, addPlugin, addComponentsDir, addServerPlugin } from '@nuxt/kit';
6
+ import { resolvePath, useNuxt, addTemplate, updateTemplates, addTypeTemplate, defineNuxtModule, createResolver, addImports, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, addVitePlugin, addBuildPlugin, addServerHandler, addPlugin, addComponentsDir, getNuxtVersion, addServerPlugin } from '@nuxt/kit';
7
7
  import { defu } from 'defu';
8
+ import { fnv1a64Base36 } from 'fnv1a-64';
8
9
  import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
9
- import { hash } from 'ohash';
10
- import { join, dirname, relative, isAbsolute, resolve, basename } from 'pathe';
10
+ import { basename, join, dirname, relative, isAbsolute, resolve } from 'pathe';
11
11
  import { readPackageJSON } from 'pkg-types';
12
- import { isCI, isAgent } from 'std-env';
12
+ import { isCI, isAgent, hasTTY } from 'std-env';
13
13
  import { parseAndWalk } from 'oxc-walker';
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.LgUGeaz-.mjs';
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';
15
18
  import { setupDevToolsUI as setupDevToolsUI$1, setupDevToolsRpc } from 'nuxtseo-shared/devtools';
16
19
  import { createStorage } from 'unstorage';
17
20
  import fsDriver from 'unstorage/drivers/fs-lite';
18
- import { logger } from '../../dist/runtime/logger.js';
19
21
  import { resolveColorMix, GRADIENT_COLOR_SPACE_RE, splitCssDeclarations } from '../../dist/runtime/server/og-image/utils/css.js';
20
22
  import MagicString from 'magic-string';
21
23
  import { stripLiteral } from 'strip-literal';
@@ -26,7 +28,277 @@ import { ofetch } from 'ofetch';
26
28
  import { parse as parse$1, walkSync, ELEMENT_NODE as ELEMENT_NODE$2, renderSync } from 'ultrahtml';
27
29
  import { RE_MATCH_EMOJIS, getEmojiCodePoint, getEmojiIconNames } from '../../dist/runtime/server/og-image/core/transforms/emojis/emoji-utils.js';
28
30
  import { resolveModulePath } from 'exsolve';
29
- import { getNuxtModuleOptions, isNuxtGenerate } from 'nuxtseo-shared/kit';
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 = { ...options.metadata || {} };
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
+ }
30
302
 
31
303
  const RE_REFLECT_HAS_MINIFIED = /Reflect\.has\(([\w$]+),([\w$]+)\)\?Reflect\.get\(\1,\2,([\w$]+)\):Reflect\.get\(([\w$]+),\2,\3\)/g;
32
304
  const RE_WASM_IMPORT = /import\("(\.\/wasm\/[^"]+\.wasm)"\)/g;
@@ -183,6 +455,198 @@ function patchWebAssemblyInstantiate(code) {
183
455
  return code;
184
456
  }
185
457
 
458
+ const TAKUMI_CORE_PACKAGE = "@takumi-rs/core";
459
+ const TAKUMI_WASM_PACKAGE = "@takumi-rs/wasm";
460
+ const PROVIDER_DEPENDENCIES = [
461
+ {
462
+ name: "satori",
463
+ description: "SVG-based renderer using Satori",
464
+ bindings: {
465
+ "node": [
466
+ { name: "satori", description: "HTML to SVG renderer" },
467
+ { name: "@resvg/resvg-js", description: "SVG to PNG converter (native)" }
468
+ ],
469
+ "wasm": [
470
+ { name: "satori", description: "HTML to SVG renderer" },
471
+ { name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
472
+ ],
473
+ "wasm-fs": [
474
+ { name: "satori", description: "HTML to SVG renderer" },
475
+ { name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
476
+ ]
477
+ }
478
+ },
479
+ {
480
+ name: "takumi",
481
+ description: "Rust-based high-performance renderer (recommended)",
482
+ bindings: {
483
+ "node": [
484
+ { name: TAKUMI_CORE_PACKAGE, description: "Native Takumi renderer" }
485
+ ],
486
+ "wasm": [
487
+ { name: TAKUMI_WASM_PACKAGE, description: "WASM Takumi renderer" }
488
+ ],
489
+ "wasm-fs": [
490
+ { name: TAKUMI_WASM_PACKAGE, description: "WASM Takumi renderer" }
491
+ ]
492
+ }
493
+ },
494
+ {
495
+ name: "browser",
496
+ description: "Browser-based screenshot renderer",
497
+ bindings: {
498
+ node: [
499
+ { name: "playwright-core", description: "Headless browser automation" }
500
+ ]
501
+ }
502
+ }
503
+ ];
504
+ async function getInstalledProviders() {
505
+ const installed = [];
506
+ for (const provider of PROVIDER_DEPENDENCIES) {
507
+ if (provider.bindings.node) {
508
+ const allNodeInstalled = await Promise.all(
509
+ provider.bindings.node.map((dep) => hasResolvableDependency(dep.name))
510
+ );
511
+ if (allNodeInstalled.every(Boolean)) {
512
+ installed.push({ provider: provider.name, binding: "node" });
513
+ continue;
514
+ }
515
+ }
516
+ if (provider.bindings.wasm) {
517
+ const allWasmInstalled = await Promise.all(
518
+ provider.bindings.wasm.map((dep) => hasResolvableDependency(dep.name))
519
+ );
520
+ if (allWasmInstalled.every(Boolean)) {
521
+ installed.push({ provider: provider.name, binding: "wasm" });
522
+ }
523
+ }
524
+ }
525
+ return installed;
526
+ }
527
+ async function getMissingDependencies(provider, binding = "node") {
528
+ const missing = await getMissingProviderDependencyDefinitions(provider, binding);
529
+ return missing.map((d) => d.name);
530
+ }
531
+ async function getMissingProviderDependencyDefinitions(provider, binding = "node") {
532
+ const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
533
+ if (!providerDef)
534
+ return [];
535
+ const deps = providerDef.bindings[binding] || providerDef.bindings.node || [];
536
+ const missing = [];
537
+ for (const dep of deps) {
538
+ if (!await hasResolvableDependency(dep.name))
539
+ missing.push(dep);
540
+ }
541
+ return missing;
542
+ }
543
+ function getDependencyDefinitions(provider, binding = "node") {
544
+ const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
545
+ if (!providerDef)
546
+ return [];
547
+ return providerDef.bindings[binding] || providerDef.bindings.node || [];
548
+ }
549
+ function getDependencyInstallSpec(dep) {
550
+ return dep.installSpec || dep.name;
551
+ }
552
+ async function getMissingDependencyInstallSpecs(provider, binding = "node") {
553
+ const missing = await getMissingProviderDependencyDefinitions(provider, binding);
554
+ return missing.map(getDependencyInstallSpec);
555
+ }
556
+ function getProviderDependencyInstallSpecs(provider, binding = "node") {
557
+ return getDependencyDefinitions(provider, binding).map(getDependencyInstallSpec);
558
+ }
559
+ function getRecommendedBindingFromPreset(provider) {
560
+ const preset = resolveOgImagePreset();
561
+ const compatibility = getPresetNitroPresetCompatibility(preset);
562
+ return getRecommendedBinding(provider, compatibility);
563
+ }
564
+ function getRecommendedBinding(provider, compatibility) {
565
+ if (provider === "satori") {
566
+ const resvgBinding = compatibility.resvg;
567
+ if (resvgBinding === "wasm-fs")
568
+ return "wasm-fs";
569
+ if (resvgBinding === "wasm")
570
+ return "wasm";
571
+ return "node";
572
+ }
573
+ if (provider === "takumi") {
574
+ const takumiBinding = compatibility.takumi;
575
+ if (takumiBinding === "wasm")
576
+ return "wasm";
577
+ return "node";
578
+ }
579
+ if (provider === "browser") {
580
+ return "node";
581
+ }
582
+ return "node";
583
+ }
584
+ async function ensureProviderDependencies(provider, binding, nuxt) {
585
+ const missingDeps = await getMissingProviderDependencyDefinitions(provider, binding);
586
+ if (missingDeps.length === 0)
587
+ return { success: true, installed: [] };
588
+ const missingInstallSpecs = missingDeps.map(getDependencyInstallSpec);
589
+ const pm = await detectPackageManager(nuxt.options.rootDir);
590
+ const pmName = pm?.name || "npm";
591
+ logger.info(`Installing ${provider} dependencies: ${missingInstallSpecs.join(", ")}`);
592
+ const installed = [];
593
+ for (const dep of missingDeps) {
594
+ const installSpec = getDependencyInstallSpec(dep);
595
+ const success = await addDependency(installSpec, {
596
+ cwd: nuxt.options.rootDir,
597
+ dev: false
598
+ }).then(() => {
599
+ installed.push(installSpec);
600
+ return true;
601
+ }).catch(() => {
602
+ logger.error(`Failed to install ${installSpec}. Run manually: ${pmName} add ${installSpec}`);
603
+ return false;
604
+ });
605
+ if (!success)
606
+ return { success: false, installed };
607
+ }
608
+ return { success: true, installed };
609
+ }
610
+ function canPromptInteractively(env = {
611
+ hasTTY,
612
+ hasStdinTTY: Boolean(process.stdin?.isTTY),
613
+ isAgent,
614
+ isCI
615
+ }) {
616
+ return env.hasTTY && env.hasStdinTTY && !env.isAgent && !env.isCI;
617
+ }
618
+ async function promptForRendererSelection() {
619
+ logger.info("Welcome to Nuxt OG Image! No renderer dependencies detected.");
620
+ const renderer = await logger.prompt("Which renderer would you like to use?", {
621
+ type: "select",
622
+ options: PROVIDER_DEPENDENCIES.map((p) => p.name),
623
+ initial: "takumi"
624
+ });
625
+ return renderer || "takumi";
626
+ }
627
+ async function validateProviderSetup(renderer, compatibility) {
628
+ const issues = [];
629
+ const binding = getRecommendedBinding(renderer, compatibility);
630
+ const missing = await getMissingDependencyInstallSpecs(renderer, binding);
631
+ if (missing.length > 0) {
632
+ issues.push(`Missing ${renderer} dependencies: ${missing.join(", ")}`);
633
+ }
634
+ if (renderer === "satori") {
635
+ const hasResvgNode = await hasResolvableDependency("@resvg/resvg-js");
636
+ const hasResvgWasm = await hasResolvableDependency("@resvg/resvg-wasm");
637
+ if (!hasResvgNode && !hasResvgWasm) {
638
+ issues.push("Satori requires either @resvg/resvg-js (node) or @resvg/resvg-wasm to render PNGs");
639
+ }
640
+ }
641
+ if (renderer === "browser") {
642
+ const hasPlaywright = await hasResolvableDependency("playwright-core");
643
+ if (!hasPlaywright && binding === "node") {
644
+ issues.push("Browser renderer requires playwright-core");
645
+ }
646
+ }
647
+ return { valid: issues.length === 0, issues };
648
+ }
649
+
186
650
  function setupDevHandler(options, resolve, getDetectedRenderers, getCompatibilityMeta = () => ({}), nuxt = useNuxt()) {
187
651
  nuxt.hooks.hook("nitro:init", async (nitro) => {
188
652
  const detectedRenderers = new Set(getDetectedRenderers());
@@ -3760,7 +4224,7 @@ async function onInstall(nuxt) {
3760
4224
  if (installedProviders2.length === 0) {
3761
4225
  throw new Error(
3762
4226
  `[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
4227
+ npm add ${TAKUMI_CORE_PACKAGE} # for takumi
3764
4228
  npm add @resvg/resvg-js satori yoga-wasm-web # for satori
3765
4229
  npm add playwright-core # for browser
3766
4230
  See: https://nuxtseo.com/og-image/getting-started`
@@ -4074,6 +4538,15 @@ declare module '#og-image/font-requirements' {
4074
4538
  export const hasNuxtFonts: boolean
4075
4539
  }
4076
4540
 
4541
+ declare module '#og-image/island-hash' {
4542
+ export function getIslandHash(input: {
4543
+ name: string
4544
+ props?: Record<string, any> | string | null
4545
+ context?: Record<string, any>
4546
+ source?: string
4547
+ }): string
4548
+ }
4549
+
4077
4550
  declare module '#og-image-virtual/unocss-config.mjs' {
4078
4551
  export const theme: Record<string, any>
4079
4552
  }
@@ -4852,7 +5325,7 @@ ${familyInfo}`);
4852
5325
  logger.debug(`Using ${renderer} renderer`);
4853
5326
  } else {
4854
5327
  ogImageComponentCtx.detectedRenderers.add("takumi");
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.`);
5328
+ logger.warn(`No OG image renderer dependency detected. Defaulting to \`takumi\` (non-interactive environment). Install \`${TAKUMI_CORE_PACKAGE}\`, or add a renderer component (e.g. components/OgImage/Default.satori.vue) to choose explicitly.`);
4856
5329
  }
4857
5330
  }
4858
5331
  const availableRenderers = /* @__PURE__ */ new Set();
@@ -4958,7 +5431,7 @@ ${familyInfo}`);
4958
5431
  const credits = componentFile.split("\n").find((line) => line.startsWith(" * @credits"))?.replace("* @credits", "").trim();
4959
5432
  const propNames = extractPropNamesFromVue(componentFile);
4960
5433
  ogImageComponentCtx.components.push({
4961
- hash: hash(componentFile).replaceAll("_", "-"),
5434
+ hash: fnv1a64Base36(componentFile),
4962
5435
  pascalName: component.pascalName,
4963
5436
  kebabName: component.kebabName,
4964
5437
  path: component.filePath,
@@ -5043,6 +5516,22 @@ Run: npx nuxt-og-image migrate v6`;
5043
5516
  nuxt.options.nitro.virtual["#og-image-virtual/component-names.mjs"] = () => {
5044
5517
  return `export const componentNames = ${JSON.stringify(ogImageComponentCtx.components)}`;
5045
5518
  };
5519
+ nuxt.options.nitro.virtual["#og-image/island-hash"] = () => {
5520
+ const islandHashFile = join(nuxt.options.appDir, "island-hash.js");
5521
+ const [nuxtMajor = 0, nuxtMinor = 0] = getNuxtVersion(nuxt).split(".").map((v) => Number.parseInt(v));
5522
+ if (nuxtMajor > 4 || nuxtMajor === 4 && nuxtMinor >= 5)
5523
+ return `export { getIslandHash } from '${islandHashFile}'`;
5524
+ if (existsSync(islandHashFile)) {
5525
+ return `import { computeIslandHash } from '${islandHashFile}'
5526
+ export function getIslandHash({ name, props, context, source }) {
5527
+ return computeIslandHash(name, props ?? {}, context ?? {}, source)
5528
+ }`;
5529
+ }
5530
+ return `import { hash } from 'ohash'
5531
+ export function getIslandHash({ name, props, context, source }) {
5532
+ return hash([name, props ?? {}, context ?? {}, source]).replace(/[-_]/g, '')
5533
+ }`;
5534
+ };
5046
5535
  nuxt.options.nitro.publicAssets ||= [];
5047
5536
  const staticFontsTtfDir = join(nuxt.options.buildDir, "cache", "og-image", "fonts-ttf");
5048
5537
  nuxt.options.nitro.publicAssets.push({