nuxt-og-image 6.6.0 → 6.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/chunks/tw4.cjs +7 -7
- package/dist/chunks/tw4.mjs +7 -7
- package/dist/chunks/uno.cjs +6 -6
- package/dist/chunks/uno.mjs +6 -6
- package/dist/cli.cjs +12 -5
- package/dist/cli.mjs +10 -3
- package/dist/module.cjs +5 -5
- package/dist/module.d.cts +5 -3
- package/dist/module.d.mts +5 -3
- package/dist/module.d.ts +5 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -5
- package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.d.ts +1 -1
- package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.js +6 -1
- package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +13 -6
- package/dist/runtime/server/og-image/bindings/font-assets/external-url.d.ts +36 -0
- package/dist/runtime/server/og-image/bindings/font-assets/external-url.js +47 -0
- package/dist/runtime/server/og-image/bindings/font-assets/node.d.ts +1 -1
- package/dist/runtime/server/og-image/bindings/font-assets/node.js +14 -7
- package/dist/runtime/server/og-image/bindings/takumi/node-dev.d.ts +2 -1
- package/dist/runtime/server/og-image/bindings/takumi/node-dev.js +10 -8
- package/dist/runtime/server/og-image/bindings/takumi/node.d.ts +2 -1
- package/dist/runtime/server/og-image/bindings/takumi/node.js +2 -1
- package/dist/runtime/server/og-image/bindings/takumi/resource-urls.d.ts +2 -0
- package/dist/runtime/server/og-image/bindings/takumi/resource-urls.js +91 -0
- package/dist/runtime/server/og-image/bindings/takumi/wasm.d.ts +2 -1
- package/dist/runtime/server/og-image/bindings/takumi/wasm.js +2 -1
- package/dist/runtime/server/og-image/fonts.d.ts +4 -0
- package/dist/runtime/server/og-image/fonts.js +21 -5
- package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
- package/dist/runtime/server/og-image/takumi/renderer.js +61 -19
- package/dist/runtime/server/util/ssrf.d.ts +10 -0
- package/dist/runtime/server/util/ssrf.js +9 -1
- package/dist/runtime/types.d.ts +13 -0
- package/dist/shared/{nuxt-og-image.CfTPCtaS.cjs → nuxt-og-image.BCK8Adm4.cjs} +181 -555
- package/dist/shared/nuxt-og-image.DQkJQHaN.cjs +689 -0
- package/dist/shared/nuxt-og-image.LgUGeaz-.mjs +660 -0
- package/dist/shared/{nuxt-og-image.DdbTs-xp.mjs → nuxt-og-image.Pfj5JkJd.mjs} +122 -496
- package/package.json +46 -42
- package/dist/shared/nuxt-og-image.CJa2KCie.mjs +0 -189
- package/dist/shared/nuxt-og-image.CMYbz66P.cjs +0 -193
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { loadFile, writeFile } from 'magicast';
|
|
3
|
+
import { addNuxtModule } from 'magicast/helpers';
|
|
4
|
+
import { join, basename } from 'pathe';
|
|
5
|
+
import { logger } from '../../dist/runtime/logger.js';
|
|
6
|
+
import { detectPackageManager, addDependency } from 'nypm';
|
|
7
|
+
import { isCI, isAgent, hasTTY } from 'std-env';
|
|
8
|
+
import { resolvePath, useNuxt, addTemplate } from '@nuxt/kit';
|
|
9
|
+
import { defu } from 'defu';
|
|
10
|
+
import { resolveNitroPreset } from 'nuxtseo-shared/kit';
|
|
11
|
+
import { Launcher } from 'chrome-launcher';
|
|
12
|
+
|
|
13
|
+
function parseFontString(font) {
|
|
14
|
+
const parts = font.split(":");
|
|
15
|
+
if (parts.length === 3) {
|
|
16
|
+
return {
|
|
17
|
+
name: parts[0] || font,
|
|
18
|
+
style: parts[1] === "ital" ? "italic" : "normal",
|
|
19
|
+
weight: Number.parseInt(parts[2] || "") || 400
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
name: parts[0] || font,
|
|
24
|
+
weight: Number.parseInt(parts[1] || "") || 400,
|
|
25
|
+
style: "normal"
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function groupFontsByFamily(fonts) {
|
|
29
|
+
const families = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const font of fonts) {
|
|
31
|
+
const parsed = parseFontString(font);
|
|
32
|
+
const existing = families.get(parsed.name);
|
|
33
|
+
if (existing) {
|
|
34
|
+
existing.weights.add(parsed.weight);
|
|
35
|
+
existing.styles.add(parsed.style);
|
|
36
|
+
} else {
|
|
37
|
+
families.set(parsed.name, {
|
|
38
|
+
weights: /* @__PURE__ */ new Set([parsed.weight]),
|
|
39
|
+
styles: /* @__PURE__ */ new Set([parsed.style])
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return Array.from(families.entries(), ([name, { weights, styles }]) => ({
|
|
44
|
+
name,
|
|
45
|
+
weights: [...weights].toSorted((a, b) => a - b),
|
|
46
|
+
styles: [...styles]
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
async function migrateFontsConfig(rootDir) {
|
|
50
|
+
const configPaths = [
|
|
51
|
+
"nuxt.config.ts",
|
|
52
|
+
"nuxt.config.js",
|
|
53
|
+
"nuxt.config.mjs"
|
|
54
|
+
];
|
|
55
|
+
let configPath;
|
|
56
|
+
for (const p of configPaths) {
|
|
57
|
+
const fullPath = join(rootDir, p);
|
|
58
|
+
if (existsSync(fullPath)) {
|
|
59
|
+
configPath = fullPath;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!configPath) {
|
|
64
|
+
return { migrated: false, message: "No nuxt.config found" };
|
|
65
|
+
}
|
|
66
|
+
const mod = await loadFile(configPath);
|
|
67
|
+
const config = mod.exports.default;
|
|
68
|
+
if (!config?.ogImage?.fonts) {
|
|
69
|
+
return { migrated: false, message: "No ogImage.fonts config found" };
|
|
70
|
+
}
|
|
71
|
+
const oldFonts = config.ogImage.fonts;
|
|
72
|
+
if (!Array.isArray(oldFonts) || oldFonts.length === 0) {
|
|
73
|
+
return { migrated: false, message: "ogImage.fonts is empty or invalid" };
|
|
74
|
+
}
|
|
75
|
+
const groupedFonts = groupFontsByFamily(oldFonts);
|
|
76
|
+
if (!config.fonts) {
|
|
77
|
+
config.fonts = {};
|
|
78
|
+
}
|
|
79
|
+
if (!config.fonts.families) {
|
|
80
|
+
config.fonts.families = [];
|
|
81
|
+
}
|
|
82
|
+
const existingFamilies = config.fonts.families;
|
|
83
|
+
for (const font of groupedFonts) {
|
|
84
|
+
const existing = existingFamilies.find((f) => f.name === font.name);
|
|
85
|
+
if (existing) {
|
|
86
|
+
const existingWeights = new Set(existing.weights || []);
|
|
87
|
+
for (const w of font.weights) {
|
|
88
|
+
existingWeights.add(w);
|
|
89
|
+
}
|
|
90
|
+
existing.weights = [...existingWeights].toSorted((a, b) => a - b);
|
|
91
|
+
if (font.styles.includes("italic")) {
|
|
92
|
+
const existingStyles = new Set(existing.styles || ["normal"]);
|
|
93
|
+
existingStyles.add("italic");
|
|
94
|
+
existing.styles = [...existingStyles];
|
|
95
|
+
}
|
|
96
|
+
existing.global = true;
|
|
97
|
+
} else {
|
|
98
|
+
const family = {
|
|
99
|
+
name: font.name,
|
|
100
|
+
weights: font.weights,
|
|
101
|
+
global: true
|
|
102
|
+
};
|
|
103
|
+
if (font.styles.includes("italic")) {
|
|
104
|
+
family.styles = font.styles;
|
|
105
|
+
}
|
|
106
|
+
existingFamilies.push(family);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
addNuxtModule(mod, "@nuxt/fonts");
|
|
110
|
+
delete config.ogImage.fonts;
|
|
111
|
+
if (Object.keys(config.ogImage).length === 0) {
|
|
112
|
+
delete config.ogImage;
|
|
113
|
+
}
|
|
114
|
+
await writeFile(mod, configPath);
|
|
115
|
+
const familyNames = groupedFonts.map((f) => f.name).join(", ");
|
|
116
|
+
return {
|
|
117
|
+
migrated: true,
|
|
118
|
+
message: `Migrated fonts (${familyNames}) from ogImage.fonts to @nuxt/fonts`
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function migrateDefaultsComponent(rootDir) {
|
|
122
|
+
const configPaths = [
|
|
123
|
+
"nuxt.config.ts",
|
|
124
|
+
"nuxt.config.js",
|
|
125
|
+
"nuxt.config.mjs"
|
|
126
|
+
];
|
|
127
|
+
let configPath;
|
|
128
|
+
for (const cp of configPaths) {
|
|
129
|
+
const fullPath = join(rootDir, cp);
|
|
130
|
+
if (existsSync(fullPath)) {
|
|
131
|
+
configPath = fullPath;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!configPath)
|
|
136
|
+
return { migrated: false, componentName: null, message: "No nuxt.config found" };
|
|
137
|
+
const mod = await loadFile(configPath);
|
|
138
|
+
const config = mod.exports.default;
|
|
139
|
+
if (!config?.ogImage?.defaults?.component)
|
|
140
|
+
return { migrated: false, componentName: null, message: "No defaults.component found" };
|
|
141
|
+
const componentName = String(config.ogImage.defaults.component);
|
|
142
|
+
delete config.ogImage.defaults.component;
|
|
143
|
+
if (config.ogImage.defaults && Object.keys(config.ogImage.defaults).length === 0)
|
|
144
|
+
delete config.ogImage.defaults;
|
|
145
|
+
if (config.ogImage && Object.keys(config.ogImage).length === 0)
|
|
146
|
+
delete config.ogImage;
|
|
147
|
+
await writeFile(mod, configPath);
|
|
148
|
+
return { migrated: true, componentName, message: `Removed defaults.component: '${componentName}'` };
|
|
149
|
+
}
|
|
150
|
+
async function promptFontsMigration(rootDir) {
|
|
151
|
+
logger.info("");
|
|
152
|
+
logger.info("Detected deprecated ogImage.fonts configuration.");
|
|
153
|
+
logger.info("");
|
|
154
|
+
logger.info("Inter fonts now work out of the box without any configuration.");
|
|
155
|
+
logger.info("For custom fonts, use @nuxt/fonts:");
|
|
156
|
+
logger.info("");
|
|
157
|
+
logger.info("Before:");
|
|
158
|
+
logger.info(" ogImage: {");
|
|
159
|
+
logger.info(" fonts: ['Inter:400', 'Inter:700']");
|
|
160
|
+
logger.info(" }");
|
|
161
|
+
logger.info("");
|
|
162
|
+
logger.info("After (Inter only): Remove the config entirely \u2014 it works by default.");
|
|
163
|
+
logger.info("");
|
|
164
|
+
logger.info("After (custom fonts):");
|
|
165
|
+
logger.info(" modules: ['@nuxt/fonts', 'nuxt-og-image'],");
|
|
166
|
+
logger.info(" fonts: {");
|
|
167
|
+
logger.info(" families: [");
|
|
168
|
+
logger.info(" { name: 'Roboto', weights: [400, 700], global: true }");
|
|
169
|
+
logger.info(" ]");
|
|
170
|
+
logger.info(" }");
|
|
171
|
+
logger.info("");
|
|
172
|
+
const migrate = await logger.prompt("Automatically migrate nuxt.config?", {
|
|
173
|
+
type: "confirm",
|
|
174
|
+
initial: true
|
|
175
|
+
});
|
|
176
|
+
if (migrate) {
|
|
177
|
+
const result = await migrateFontsConfig(rootDir).catch((err) => {
|
|
178
|
+
logger.error(`Migration failed: ${err.message}`);
|
|
179
|
+
return { migrated: false, message: err.message };
|
|
180
|
+
});
|
|
181
|
+
if (result.migrated) {
|
|
182
|
+
logger.success(result.message);
|
|
183
|
+
logger.info("");
|
|
184
|
+
logger.info("Please install @nuxt/fonts:");
|
|
185
|
+
logger.info(" npm add @nuxt/fonts");
|
|
186
|
+
} else {
|
|
187
|
+
logger.warn(`Could not migrate: ${result.message}`);
|
|
188
|
+
logger.info("Please migrate manually.");
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
logger.info("Skipping automatic migration. Please migrate manually.");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const isUndefinedOrTruthy = (v) => typeof v === "undefined" || v !== false;
|
|
196
|
+
function checkLocalChrome() {
|
|
197
|
+
if (isCI)
|
|
198
|
+
return false;
|
|
199
|
+
let hasChromeLocally = false;
|
|
200
|
+
try {
|
|
201
|
+
hasChromeLocally = !!Launcher.getFirstInstallation();
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
return hasChromeLocally;
|
|
205
|
+
}
|
|
206
|
+
async function hasResolvableDependency(dep) {
|
|
207
|
+
return await resolvePath(dep, { fallbackToOriginal: true }).catch(() => null).then((r) => r && r !== dep);
|
|
208
|
+
}
|
|
209
|
+
const VALID_RENDERER_SUFFIXES = ["satori", "browser", "takumi"];
|
|
210
|
+
const RE_RENDERER_SUFFIX_CI = /\.?(satori|browser|takumi)$/i;
|
|
211
|
+
const RE_RENDERER_SUFFIX = /(Satori|Browser|Takumi)$/;
|
|
212
|
+
const RE_LEGACY_SUFFIX = /-legacy$/;
|
|
213
|
+
const RE_WHITESPACE = /\s+/;
|
|
214
|
+
function getRendererFromFilename(filepath) {
|
|
215
|
+
const filename = basename(filepath).replace(".vue", "");
|
|
216
|
+
for (const suffix of VALID_RENDERER_SUFFIXES) {
|
|
217
|
+
if (filename.endsWith(`.${suffix}`))
|
|
218
|
+
return suffix;
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const OGIMAGE_PREFIXES = [
|
|
223
|
+
{ prefix: "OgImageCommunity", overlapWord: "Community" },
|
|
224
|
+
{ prefix: "OgImageTemplate", overlapWord: "Template" },
|
|
225
|
+
{ prefix: "OgImage", overlapWord: "Image" }
|
|
226
|
+
];
|
|
227
|
+
function getRegisteredBaseNames(registeredPascalName) {
|
|
228
|
+
const stripped = registeredPascalName.replace(RE_RENDERER_SUFFIX_CI, "").replace(RE_RENDERER_SUFFIX, "");
|
|
229
|
+
const names = [];
|
|
230
|
+
for (const { prefix, overlapWord } of OGIMAGE_PREFIXES) {
|
|
231
|
+
if (!stripped.startsWith(prefix))
|
|
232
|
+
continue;
|
|
233
|
+
const withoutPrefix = stripped.slice(prefix.length);
|
|
234
|
+
if (withoutPrefix) {
|
|
235
|
+
names.push(withoutPrefix);
|
|
236
|
+
if (withoutPrefix !== overlapWord)
|
|
237
|
+
names.push(overlapWord + withoutPrefix);
|
|
238
|
+
} else {
|
|
239
|
+
names.push(overlapWord);
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (names.length === 0)
|
|
244
|
+
names.push(stripped);
|
|
245
|
+
return names;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const NodeRuntime = {
|
|
249
|
+
// node-server runtime
|
|
250
|
+
browser: "on-demand",
|
|
251
|
+
// this gets changed build start
|
|
252
|
+
resvg: "node",
|
|
253
|
+
satori: "node",
|
|
254
|
+
takumi: "node",
|
|
255
|
+
sharp: "node",
|
|
256
|
+
// will be disabled if they're missing the dependency
|
|
257
|
+
emoji: "local"
|
|
258
|
+
// can bundle icons, no size constraints
|
|
259
|
+
};
|
|
260
|
+
const NodeDevRuntime = {
|
|
261
|
+
...NodeRuntime,
|
|
262
|
+
resvg: "node-dev",
|
|
263
|
+
// use worker to prevent crashes from killing process
|
|
264
|
+
takumi: "node-dev"
|
|
265
|
+
// use worker to prevent crashes from killing process
|
|
266
|
+
};
|
|
267
|
+
const NodePrerenderRuntime = {
|
|
268
|
+
...NodeRuntime,
|
|
269
|
+
resvg: "node-dev",
|
|
270
|
+
// use worker to prevent crashes from killing prerender process
|
|
271
|
+
takumi: "node-dev"
|
|
272
|
+
// use worker to prevent crashes from killing prerender process
|
|
273
|
+
};
|
|
274
|
+
const cloudflare = {
|
|
275
|
+
browser: "cloudflare",
|
|
276
|
+
resvg: "wasm",
|
|
277
|
+
satori: "wasm",
|
|
278
|
+
takumi: "wasm",
|
|
279
|
+
sharp: false,
|
|
280
|
+
emoji: "fetch",
|
|
281
|
+
// edge size limits - use API instead of bundling 24MB icons
|
|
282
|
+
wasm: {
|
|
283
|
+
esmImport: true
|
|
284
|
+
// Do NOT set lazy here — Nitro's cloudflare preset uses lazy: false because
|
|
285
|
+
// CF Workers require WASM modules to be imported eagerly via top-level await
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const awsLambda = {
|
|
289
|
+
browser: false,
|
|
290
|
+
resvg: "node",
|
|
291
|
+
satori: "node",
|
|
292
|
+
takumi: "node",
|
|
293
|
+
sharp: false,
|
|
294
|
+
// 0.33.x has issues
|
|
295
|
+
emoji: "local"
|
|
296
|
+
// serverless has larger size limits
|
|
297
|
+
};
|
|
298
|
+
const WebContainer = {
|
|
299
|
+
browser: false,
|
|
300
|
+
resvg: "wasm-fs",
|
|
301
|
+
satori: "wasm-fs",
|
|
302
|
+
takumi: "wasm",
|
|
303
|
+
sharp: false,
|
|
304
|
+
emoji: "fetch"
|
|
305
|
+
// webcontainer has size constraints
|
|
306
|
+
};
|
|
307
|
+
const RuntimeCompatibility = {
|
|
308
|
+
"nitro-dev": NodeDevRuntime,
|
|
309
|
+
"nitro-prerender": NodePrerenderRuntime,
|
|
310
|
+
"node-server": NodeRuntime,
|
|
311
|
+
"node-cluster": NodeRuntime,
|
|
312
|
+
"node-listener": NodeRuntime,
|
|
313
|
+
"bun": NodeRuntime,
|
|
314
|
+
"deno-server": NodeRuntime,
|
|
315
|
+
"deno-deploy": NodeRuntime,
|
|
316
|
+
// node-server extending presets
|
|
317
|
+
"alwaysdata": NodeRuntime,
|
|
318
|
+
"cleavr": NodeRuntime,
|
|
319
|
+
"digital-ocean": NodeRuntime,
|
|
320
|
+
"edgio": NodeRuntime,
|
|
321
|
+
"flight-control": NodeRuntime,
|
|
322
|
+
"heroku": NodeRuntime,
|
|
323
|
+
"iis-handler": NodeRuntime,
|
|
324
|
+
"iis-node": NodeRuntime,
|
|
325
|
+
"koyeb": NodeRuntime,
|
|
326
|
+
"platform-sh": NodeRuntime,
|
|
327
|
+
"render-com": NodeRuntime,
|
|
328
|
+
"zeabur": NodeRuntime,
|
|
329
|
+
"zerops": NodeRuntime,
|
|
330
|
+
"stackblitz": WebContainer,
|
|
331
|
+
"codesandbox": WebContainer,
|
|
332
|
+
"aws-amplify": awsLambda,
|
|
333
|
+
"aws-lambda": awsLambda,
|
|
334
|
+
"azure": awsLambda,
|
|
335
|
+
"azure-functions": awsLambda,
|
|
336
|
+
"azure-swa": awsLambda,
|
|
337
|
+
"netlify": awsLambda,
|
|
338
|
+
"netlify-edge": {
|
|
339
|
+
browser: false,
|
|
340
|
+
resvg: "wasm",
|
|
341
|
+
satori: "node",
|
|
342
|
+
takumi: "wasm",
|
|
343
|
+
sharp: false,
|
|
344
|
+
emoji: "fetch",
|
|
345
|
+
// edge size limits
|
|
346
|
+
wasm: {
|
|
347
|
+
rollup: {
|
|
348
|
+
targetEnv: "auto-inline",
|
|
349
|
+
sync: ["@resvg/resvg-wasm/index_bg.wasm"]
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
"firebase": awsLambda,
|
|
354
|
+
"firebase-app-hosting": awsLambda,
|
|
355
|
+
"genezio": awsLambda,
|
|
356
|
+
"stormkit": awsLambda,
|
|
357
|
+
"vercel": awsLambda,
|
|
358
|
+
"vercel-edge": {
|
|
359
|
+
browser: false,
|
|
360
|
+
resvg: "wasm",
|
|
361
|
+
satori: "wasm",
|
|
362
|
+
takumi: "wasm",
|
|
363
|
+
sharp: false,
|
|
364
|
+
emoji: "fetch",
|
|
365
|
+
// edge size limits - bundling 24MB icons not viable
|
|
366
|
+
wasm: {
|
|
367
|
+
// lowers workers kb size
|
|
368
|
+
esmImport: true,
|
|
369
|
+
lazy: true
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
"cloudflare-pages": cloudflare,
|
|
373
|
+
"cloudflare-pages-static": cloudflare,
|
|
374
|
+
"cloudflare": cloudflare,
|
|
375
|
+
"cloudflare-module": cloudflare,
|
|
376
|
+
"cloudflare-durable": cloudflare
|
|
377
|
+
};
|
|
378
|
+
function resolveOgImagePreset(nitroConfig) {
|
|
379
|
+
const nuxt = useNuxt();
|
|
380
|
+
if (nuxt.options.dev)
|
|
381
|
+
return "nitro-dev";
|
|
382
|
+
if (nuxt.options.nitro.static)
|
|
383
|
+
return "nitro-prerender";
|
|
384
|
+
return resolveNitroPreset(nitroConfig);
|
|
385
|
+
}
|
|
386
|
+
function getPresetNitroPresetCompatibility(target) {
|
|
387
|
+
const normalizedTarget = target.replace(RE_LEGACY_SUFFIX, "");
|
|
388
|
+
let compatibility = RuntimeCompatibility[normalizedTarget];
|
|
389
|
+
if (!compatibility) {
|
|
390
|
+
logger.warn(`Unknown Nitro preset "${target}" \u2014 falling back to node-server compatibility. Set ogImage.compatibility.runtime to override.`);
|
|
391
|
+
compatibility = NodeRuntime;
|
|
392
|
+
}
|
|
393
|
+
return compatibility;
|
|
394
|
+
}
|
|
395
|
+
async function applyNitroPresetCompatibility(nitroConfig, options) {
|
|
396
|
+
const target = resolveOgImagePreset(nitroConfig);
|
|
397
|
+
const compatibility = getPresetNitroPresetCompatibility(target);
|
|
398
|
+
const { resolve, detectedRenderers } = options;
|
|
399
|
+
const satoriEnabled = detectedRenderers.has("satori");
|
|
400
|
+
const browserEnabled = detectedRenderers.has("browser");
|
|
401
|
+
const takumiEnabled = detectedRenderers.has("takumi");
|
|
402
|
+
for (const renderer of detectedRenderers) {
|
|
403
|
+
if (!compatibility[renderer]) {
|
|
404
|
+
logger.warn(`Renderer "${renderer}" detected but not supported on "${target}" preset. OG images using .${renderer}.vue components may fail.`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const emptyMock = await resolve.resolvePath("./runtime/mock/empty");
|
|
408
|
+
nitroConfig.alias["#og-image/renderers/satori"] = satoriEnabled ? await resolve.resolvePath("./runtime/server/og-image/satori/renderer") : emptyMock;
|
|
409
|
+
nitroConfig.alias["#og-image/renderers/browser"] = browserEnabled ? await resolve.resolvePath("./runtime/server/og-image/browser/renderer") : emptyMock;
|
|
410
|
+
nitroConfig.alias["#og-image/renderers/takumi"] = takumiEnabled ? await resolve.resolvePath("./runtime/server/og-image/takumi/renderer") : emptyMock;
|
|
411
|
+
const resolvedCompatibility = { ...options.metadata || {} };
|
|
412
|
+
async function applyBinding(key) {
|
|
413
|
+
let binding = options.compatibility?.[key];
|
|
414
|
+
if (typeof binding === "undefined")
|
|
415
|
+
binding = compatibility[key];
|
|
416
|
+
const isRendererBinding = key === "browser" || key === "satori" || key === "takumi";
|
|
417
|
+
if (isRendererBinding && !detectedRenderers.has(key)) {
|
|
418
|
+
binding = false;
|
|
419
|
+
}
|
|
420
|
+
resolvedCompatibility[key] = binding;
|
|
421
|
+
if (!binding && isRendererBinding) {
|
|
422
|
+
nitroConfig.alias[`#og-image/renderers/${key}`] = emptyMock;
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
[`#og-image/bindings/${key}`]: !binding ? emptyMock : await resolve.resolvePath(`./runtime/server/og-image/bindings/${key}/${binding}`)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const satoriPkgDir = await resolve.resolvePath("satori/package.json").then((p) => p.replace("/package.json", ""));
|
|
429
|
+
const yogaWasmPath = `${satoriPkgDir}/yoga.wasm`;
|
|
430
|
+
nitroConfig.alias = defu(
|
|
431
|
+
{ "#og-image/yoga-wasm": `${yogaWasmPath}?module` },
|
|
432
|
+
await applyBinding("browser"),
|
|
433
|
+
await applyBinding("satori"),
|
|
434
|
+
await applyBinding("takumi"),
|
|
435
|
+
await applyBinding("resvg"),
|
|
436
|
+
await applyBinding("sharp"),
|
|
437
|
+
nitroConfig.alias || {}
|
|
438
|
+
);
|
|
439
|
+
if (Object.values(resolvedCompatibility).includes("wasm")) {
|
|
440
|
+
nitroConfig.experimental = nitroConfig.experimental || {};
|
|
441
|
+
nitroConfig.experimental.wasm = true;
|
|
442
|
+
}
|
|
443
|
+
nitroConfig.rollupConfig = nitroConfig.rollupConfig || {};
|
|
444
|
+
nitroConfig.wasm = defu(compatibility.wasm, nitroConfig.wasm);
|
|
445
|
+
nitroConfig.virtual["#og-image/compatibility"] = () => `export default ${JSON.stringify(resolvedCompatibility)}`;
|
|
446
|
+
addTemplate({
|
|
447
|
+
filename: "nuxt-og-image/compatibility.mjs",
|
|
448
|
+
getContents() {
|
|
449
|
+
return `export default ${JSON.stringify(resolvedCompatibility)}`;
|
|
450
|
+
},
|
|
451
|
+
options: { mode: "server" }
|
|
452
|
+
});
|
|
453
|
+
return resolvedCompatibility;
|
|
454
|
+
}
|
|
455
|
+
async function ensureDependencies(dep, nuxt = useNuxt()) {
|
|
456
|
+
const { ensureDependencyInstalled } = await import('nypm');
|
|
457
|
+
return Promise.all(dep.map((d) => {
|
|
458
|
+
return ensureDependencyInstalled(d, {
|
|
459
|
+
cwd: nuxt.options.rootDir,
|
|
460
|
+
dev: true
|
|
461
|
+
});
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const TAKUMI_INSTALL_TAG = "rc";
|
|
466
|
+
const TAKUMI_CORE_PACKAGE = "@takumi-rs/core";
|
|
467
|
+
const TAKUMI_WASM_PACKAGE = "@takumi-rs/wasm";
|
|
468
|
+
const TAKUMI_CORE_INSTALL_SPEC = `${TAKUMI_CORE_PACKAGE}@${TAKUMI_INSTALL_TAG}`;
|
|
469
|
+
const TAKUMI_WASM_INSTALL_SPEC = `${TAKUMI_WASM_PACKAGE}@${TAKUMI_INSTALL_TAG}`;
|
|
470
|
+
const PROVIDER_DEPENDENCIES = [
|
|
471
|
+
{
|
|
472
|
+
name: "satori",
|
|
473
|
+
description: "SVG-based renderer using Satori",
|
|
474
|
+
bindings: {
|
|
475
|
+
"node": [
|
|
476
|
+
{ name: "satori", description: "HTML to SVG renderer" },
|
|
477
|
+
{ name: "@resvg/resvg-js", description: "SVG to PNG converter (native)" }
|
|
478
|
+
],
|
|
479
|
+
"wasm": [
|
|
480
|
+
{ name: "satori", description: "HTML to SVG renderer" },
|
|
481
|
+
{ name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
|
|
482
|
+
],
|
|
483
|
+
"wasm-fs": [
|
|
484
|
+
{ name: "satori", description: "HTML to SVG renderer" },
|
|
485
|
+
{ name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
|
|
486
|
+
]
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: "takumi",
|
|
491
|
+
description: "Rust-based high-performance renderer (recommended)",
|
|
492
|
+
bindings: {
|
|
493
|
+
"node": [
|
|
494
|
+
{ name: TAKUMI_CORE_PACKAGE, installSpec: TAKUMI_CORE_INSTALL_SPEC, description: "Native Takumi renderer" }
|
|
495
|
+
],
|
|
496
|
+
"wasm": [
|
|
497
|
+
{ name: TAKUMI_WASM_PACKAGE, installSpec: TAKUMI_WASM_INSTALL_SPEC, description: "WASM Takumi renderer" }
|
|
498
|
+
],
|
|
499
|
+
"wasm-fs": [
|
|
500
|
+
{ name: TAKUMI_WASM_PACKAGE, installSpec: TAKUMI_WASM_INSTALL_SPEC, description: "WASM Takumi renderer" }
|
|
501
|
+
]
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "browser",
|
|
506
|
+
description: "Browser-based screenshot renderer",
|
|
507
|
+
bindings: {
|
|
508
|
+
node: [
|
|
509
|
+
{ name: "playwright-core", description: "Headless browser automation" }
|
|
510
|
+
]
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
];
|
|
514
|
+
async function getInstalledProviders() {
|
|
515
|
+
const installed = [];
|
|
516
|
+
for (const provider of PROVIDER_DEPENDENCIES) {
|
|
517
|
+
if (provider.bindings.node) {
|
|
518
|
+
const allNodeInstalled = await Promise.all(
|
|
519
|
+
provider.bindings.node.map((dep) => hasResolvableDependency(dep.name))
|
|
520
|
+
);
|
|
521
|
+
if (allNodeInstalled.every(Boolean)) {
|
|
522
|
+
installed.push({ provider: provider.name, binding: "node" });
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (provider.bindings.wasm) {
|
|
527
|
+
const allWasmInstalled = await Promise.all(
|
|
528
|
+
provider.bindings.wasm.map((dep) => hasResolvableDependency(dep.name))
|
|
529
|
+
);
|
|
530
|
+
if (allWasmInstalled.every(Boolean)) {
|
|
531
|
+
installed.push({ provider: provider.name, binding: "wasm" });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return installed;
|
|
536
|
+
}
|
|
537
|
+
async function getMissingDependencies(provider, binding = "node") {
|
|
538
|
+
const missing = await getMissingProviderDependencyDefinitions(provider, binding);
|
|
539
|
+
return missing.map((d) => d.name);
|
|
540
|
+
}
|
|
541
|
+
async function getMissingProviderDependencyDefinitions(provider, binding = "node") {
|
|
542
|
+
const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
|
|
543
|
+
if (!providerDef)
|
|
544
|
+
return [];
|
|
545
|
+
const deps = providerDef.bindings[binding] || providerDef.bindings.node || [];
|
|
546
|
+
const missing = [];
|
|
547
|
+
for (const dep of deps) {
|
|
548
|
+
if (!await hasResolvableDependency(dep.name))
|
|
549
|
+
missing.push(dep);
|
|
550
|
+
}
|
|
551
|
+
return missing;
|
|
552
|
+
}
|
|
553
|
+
function getDependencyDefinitions(provider, binding = "node") {
|
|
554
|
+
const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
|
|
555
|
+
if (!providerDef)
|
|
556
|
+
return [];
|
|
557
|
+
return providerDef.bindings[binding] || providerDef.bindings.node || [];
|
|
558
|
+
}
|
|
559
|
+
function getDependencyInstallSpec(dep) {
|
|
560
|
+
return dep.installSpec || dep.name;
|
|
561
|
+
}
|
|
562
|
+
async function getMissingDependencyInstallSpecs(provider, binding = "node") {
|
|
563
|
+
const missing = await getMissingProviderDependencyDefinitions(provider, binding);
|
|
564
|
+
return missing.map(getDependencyInstallSpec);
|
|
565
|
+
}
|
|
566
|
+
function getProviderDependencyInstallSpecs(provider, binding = "node") {
|
|
567
|
+
return getDependencyDefinitions(provider, binding).map(getDependencyInstallSpec);
|
|
568
|
+
}
|
|
569
|
+
function getRecommendedBindingFromPreset(provider) {
|
|
570
|
+
const preset = resolveOgImagePreset();
|
|
571
|
+
const compatibility = getPresetNitroPresetCompatibility(preset);
|
|
572
|
+
return getRecommendedBinding(provider, compatibility);
|
|
573
|
+
}
|
|
574
|
+
function getRecommendedBinding(provider, compatibility) {
|
|
575
|
+
if (provider === "satori") {
|
|
576
|
+
const resvgBinding = compatibility.resvg;
|
|
577
|
+
if (resvgBinding === "wasm-fs")
|
|
578
|
+
return "wasm-fs";
|
|
579
|
+
if (resvgBinding === "wasm")
|
|
580
|
+
return "wasm";
|
|
581
|
+
return "node";
|
|
582
|
+
}
|
|
583
|
+
if (provider === "takumi") {
|
|
584
|
+
const takumiBinding = compatibility.takumi;
|
|
585
|
+
if (takumiBinding === "wasm")
|
|
586
|
+
return "wasm";
|
|
587
|
+
return "node";
|
|
588
|
+
}
|
|
589
|
+
if (provider === "browser") {
|
|
590
|
+
return "node";
|
|
591
|
+
}
|
|
592
|
+
return "node";
|
|
593
|
+
}
|
|
594
|
+
async function ensureProviderDependencies(provider, binding, nuxt) {
|
|
595
|
+
const missingDeps = await getMissingProviderDependencyDefinitions(provider, binding);
|
|
596
|
+
if (missingDeps.length === 0)
|
|
597
|
+
return { success: true, installed: [] };
|
|
598
|
+
const missingInstallSpecs = missingDeps.map(getDependencyInstallSpec);
|
|
599
|
+
const pm = await detectPackageManager(nuxt.options.rootDir);
|
|
600
|
+
const pmName = pm?.name || "npm";
|
|
601
|
+
logger.info(`Installing ${provider} dependencies: ${missingInstallSpecs.join(", ")}`);
|
|
602
|
+
const installed = [];
|
|
603
|
+
for (const dep of missingDeps) {
|
|
604
|
+
const installSpec = getDependencyInstallSpec(dep);
|
|
605
|
+
const success = await addDependency(installSpec, {
|
|
606
|
+
cwd: nuxt.options.rootDir,
|
|
607
|
+
dev: false
|
|
608
|
+
}).then(() => {
|
|
609
|
+
installed.push(installSpec);
|
|
610
|
+
return true;
|
|
611
|
+
}).catch(() => {
|
|
612
|
+
logger.error(`Failed to install ${installSpec}. Run manually: ${pmName} add ${installSpec}`);
|
|
613
|
+
return false;
|
|
614
|
+
});
|
|
615
|
+
if (!success)
|
|
616
|
+
return { success: false, installed };
|
|
617
|
+
}
|
|
618
|
+
return { success: true, installed };
|
|
619
|
+
}
|
|
620
|
+
function canPromptInteractively(env = {
|
|
621
|
+
hasTTY,
|
|
622
|
+
hasStdinTTY: Boolean(process.stdin?.isTTY),
|
|
623
|
+
isAgent,
|
|
624
|
+
isCI
|
|
625
|
+
}) {
|
|
626
|
+
return env.hasTTY && env.hasStdinTTY && !env.isAgent && !env.isCI;
|
|
627
|
+
}
|
|
628
|
+
async function promptForRendererSelection() {
|
|
629
|
+
logger.info("Welcome to Nuxt OG Image! No renderer dependencies detected.");
|
|
630
|
+
const renderer = await logger.prompt("Which renderer would you like to use?", {
|
|
631
|
+
type: "select",
|
|
632
|
+
options: PROVIDER_DEPENDENCIES.map((p) => p.name),
|
|
633
|
+
initial: "takumi"
|
|
634
|
+
});
|
|
635
|
+
return renderer || "takumi";
|
|
636
|
+
}
|
|
637
|
+
async function validateProviderSetup(renderer, compatibility) {
|
|
638
|
+
const issues = [];
|
|
639
|
+
const binding = getRecommendedBinding(renderer, compatibility);
|
|
640
|
+
const missing = await getMissingDependencyInstallSpecs(renderer, binding);
|
|
641
|
+
if (missing.length > 0) {
|
|
642
|
+
issues.push(`Missing ${renderer} dependencies: ${missing.join(", ")}`);
|
|
643
|
+
}
|
|
644
|
+
if (renderer === "satori") {
|
|
645
|
+
const hasResvgNode = await hasResolvableDependency("@resvg/resvg-js");
|
|
646
|
+
const hasResvgWasm = await hasResolvableDependency("@resvg/resvg-wasm");
|
|
647
|
+
if (!hasResvgNode && !hasResvgWasm) {
|
|
648
|
+
issues.push("Satori requires either @resvg/resvg-js (node) or @resvg/resvg-wasm to render PNGs");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (renderer === "browser") {
|
|
652
|
+
const hasPlaywright = await hasResolvableDependency("playwright-core");
|
|
653
|
+
if (!hasPlaywright && binding === "node") {
|
|
654
|
+
issues.push("Browser renderer requires playwright-core");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return { valid: issues.length === 0, issues };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export { PROVIDER_DEPENDENCIES as P, RE_LEGACY_SUFFIX as R, TAKUMI_CORE_INSTALL_SPEC as T, migrateFontsConfig as a, TAKUMI_WASM_INSTALL_SPEC as b, applyNitroPresetCompatibility as c, getRecommendedBinding as d, getMissingDependencies as e, RE_RENDERER_SUFFIX as f, getPresetNitroPresetCompatibility as g, RE_WHITESPACE as h, getRendererFromFilename as i, getInstalledProviders as j, canPromptInteractively as k, getRecommendedBindingFromPreset as l, migrateDefaultsComponent as m, getMissingDependencyInstallSpecs as n, ensureProviderDependencies as o, promptFontsMigration as p, getProviderDependencyInstallSpecs as q, resolveOgImagePreset as r, hasResolvableDependency as s, getRegisteredBaseNames as t, ensureDependencies as u, validateProviderSetup as v, checkLocalChrome as w, promptForRendererSelection as x, isUndefinedOrTruthy as y };
|