nuxt-og-image 6.7.6 → 6.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +6 -0
- package/dist/chunks/decode.mjs +1087 -0
- package/dist/chunks/tw4.mjs +1 -1
- package/dist/chunks/uno.mjs +2 -2
- package/dist/module.d.ts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +5 -6
- 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 +2 -3
- package/dist/runtime/server/og-image/browser/screenshot.js +2 -1
- package/dist/runtime/server/og-image/font-source.d.ts +3 -12
- package/dist/runtime/server/og-image/font-source.js +1 -4
- package/dist/runtime/server/og-image/fonts.d.ts +0 -7
- package/dist/runtime/server/og-image/fonts.js +1 -1
- package/dist/runtime/server/og-image/takumi/renderer.js +2 -2
- package/dist/runtime/server/routes/resolve.js +2 -1
- package/dist/runtime/server/util/fetchLocalAsset.d.ts +1 -1
- package/dist/runtime/server/util/fetchLocalAsset.js +2 -1
- package/dist/runtime/types.d.ts +1 -1
- package/dist/shared/{nuxt-og-image.BIbfn5xQ.mjs → nuxt-og-image.BMHzg1Vz.mjs} +227 -76
- package/package.json +21 -22
- package/dist/chunks/tw4.cjs +0 -493
- package/dist/chunks/uno.cjs +0 -249
- package/dist/cli.cjs +0 -1345
- package/dist/cli.d.cts +0 -1
- package/dist/content.cjs +0 -21
- package/dist/content.d.cts +0 -16
- package/dist/module.cjs +0 -39
- package/dist/module.d.cts +0 -219
- package/dist/shared/nuxt-og-image.CMYbz66P.cjs +0 -193
- package/dist/shared/nuxt-og-image.CdpC3njh.cjs +0 -5898
package/dist/cli.cjs
DELETED
|
@@ -1,1345 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const node_crypto = require('node:crypto');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const node_url = require('node:url');
|
|
7
|
-
const p = require('@clack/prompts');
|
|
8
|
-
const kit = require('@nuxt/kit');
|
|
9
|
-
const nypm = require('nypm');
|
|
10
|
-
const oxcWalker = require('oxc-walker');
|
|
11
|
-
const pathe = require('pathe');
|
|
12
|
-
const ultrahtml = require('ultrahtml');
|
|
13
|
-
const fonts = require('./shared/nuxt-og-image.CMYbz66P.cjs');
|
|
14
|
-
require('magicast');
|
|
15
|
-
require('magicast/helpers');
|
|
16
|
-
require('../dist/runtime/logger.js');
|
|
17
|
-
|
|
18
|
-
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
19
|
-
function _interopNamespaceCompat(e) {
|
|
20
|
-
if (e && typeof e === 'object' && 'default' in e) return e;
|
|
21
|
-
const n = Object.create(null);
|
|
22
|
-
if (e) {
|
|
23
|
-
for (const k in e) {
|
|
24
|
-
n[k] = e[k];
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
n.default = e;
|
|
28
|
-
return n;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const p__namespace = /*#__PURE__*/_interopNamespaceCompat(p);
|
|
32
|
-
|
|
33
|
-
const __dirname$1 = pathe.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
34
|
-
const communityDir = pathe.resolve(__dirname$1, "runtime/app/components/Templates/Community");
|
|
35
|
-
const RE_RENDERER_SUFFIX = /\.(satori|browser|takumi)\.vue$/;
|
|
36
|
-
const RE_ANY_RENDERER_SUFFIX = /\.(?:satori|browser|takumi|chromium)\.vue$/;
|
|
37
|
-
const RE_CHROMIUM_SUFFIX = /\.chromium\.vue$/;
|
|
38
|
-
const RE_EXCLUDE_NODE_MODULES = /node_modules/;
|
|
39
|
-
const RE_EXCLUDE_NUXT = /\.nuxt/;
|
|
40
|
-
const RE_EXCLUDE_OUTPUT = /\.output/;
|
|
41
|
-
const RE_EXCLUDE_DATA = /\.data/;
|
|
42
|
-
const RE_EXCLUDE_DIST = /dist/;
|
|
43
|
-
const RE_VUE_OR_SCRIPT = /\.(?:vue|ts|tsx|js|jsx)$/;
|
|
44
|
-
const RE_PASCAL_CASE_STRICT = /^[A-Z]\w*$/;
|
|
45
|
-
const RE_PASCAL_CASE_START = /^[A-Z]/;
|
|
46
|
-
const RE_SCRIPT_SETUP = /<script\s+setup[^>]*>\r?\n?/;
|
|
47
|
-
const RE_PATH_SEPARATOR = /[\\/]|\.\./;
|
|
48
|
-
const DEPRECATED_COMPOSABLE_NAMES = /* @__PURE__ */ new Set([
|
|
49
|
-
"defineOgImageComponent",
|
|
50
|
-
"defineOgImageStatic",
|
|
51
|
-
"defineOgImageDynamic",
|
|
52
|
-
"defineOgImageCached",
|
|
53
|
-
"defineOgImageWithoutCache"
|
|
54
|
-
]);
|
|
55
|
-
const ALL_OG_IMAGE_COMPOSABLES = /* @__PURE__ */ new Set([
|
|
56
|
-
"defineOgImage",
|
|
57
|
-
...DEPRECATED_COMPOSABLE_NAMES
|
|
58
|
-
]);
|
|
59
|
-
function collectScriptReplacements(code, filename) {
|
|
60
|
-
const replacements = [];
|
|
61
|
-
oxcWalker.parseAndWalk(code, filename, (node) => {
|
|
62
|
-
if (node.type === "ImportDeclaration") {
|
|
63
|
-
const source = node.source;
|
|
64
|
-
const sourceValue = source?.value;
|
|
65
|
-
if (sourceValue === "#nuxt-og-image-utils") {
|
|
66
|
-
replacements.push({ start: source.start + 1, end: source.end - 1, text: "#og-image/shared" });
|
|
67
|
-
} else if (sourceValue === "#og-image/shared") {
|
|
68
|
-
const specifiers = node.specifiers || [];
|
|
69
|
-
const hasRuntimeConfig = specifiers.some((s) => {
|
|
70
|
-
const imported = s.imported || s.local;
|
|
71
|
-
return imported?.name === "useOgImageRuntimeConfig";
|
|
72
|
-
});
|
|
73
|
-
if (hasRuntimeConfig) {
|
|
74
|
-
replacements.push({ start: source.start + 1, end: source.end - 1, text: "#og-image/app/utils" });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (node.type !== "CallExpression")
|
|
80
|
-
return;
|
|
81
|
-
const callee = node.callee;
|
|
82
|
-
const calleeName = callee?.name;
|
|
83
|
-
if (!calleeName || !ALL_OG_IMAGE_COMPOSABLES.has(calleeName))
|
|
84
|
-
return;
|
|
85
|
-
const args2 = node.arguments || [];
|
|
86
|
-
if (args2.length === 1 && args2[0]?.type === "ObjectExpression") {
|
|
87
|
-
const objArg = args2[0];
|
|
88
|
-
const properties = (objArg.properties || []).filter(
|
|
89
|
-
(p2) => p2.type === "ObjectProperty" || p2.type === "Property"
|
|
90
|
-
);
|
|
91
|
-
const findProp = (name) => properties.find(
|
|
92
|
-
(p2) => p2.key?.name === name
|
|
93
|
-
);
|
|
94
|
-
const urlProp = findProp("url");
|
|
95
|
-
const componentProp = findProp("component");
|
|
96
|
-
const rendererProp = findProp("renderer");
|
|
97
|
-
const propsProp = findProp("props");
|
|
98
|
-
if (urlProp) {
|
|
99
|
-
const urlValue = code.slice(urlProp.value.start, urlProp.value.end);
|
|
100
|
-
const ogPropMap = {
|
|
101
|
-
width: "ogImageWidth",
|
|
102
|
-
height: "ogImageHeight",
|
|
103
|
-
alt: "ogImageAlt",
|
|
104
|
-
type: "ogImageType"
|
|
105
|
-
};
|
|
106
|
-
const seoMetaProps = [`ogImage: ${urlValue}`];
|
|
107
|
-
for (const prop of properties) {
|
|
108
|
-
if (prop === urlProp)
|
|
109
|
-
continue;
|
|
110
|
-
const keyName = prop.key?.name;
|
|
111
|
-
const mappedName = ogPropMap[keyName];
|
|
112
|
-
if (mappedName) {
|
|
113
|
-
seoMetaProps.push(`${mappedName}: ${code.slice(prop.value.start, prop.value.end)}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const text = `useSeoMeta({ ${seoMetaProps.join(", ")} })`;
|
|
117
|
-
replacements.push({ start: node.start, end: node.end, text });
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (!componentProp && rendererProp) {
|
|
121
|
-
const rendererValue = code.slice(rendererProp.value.start, rendererProp.value.end).trim();
|
|
122
|
-
if (rendererValue === "'chromium'" || rendererValue === '"chromium"') {
|
|
123
|
-
const otherProps = properties.filter((p2) => p2 !== rendererProp);
|
|
124
|
-
const text = otherProps.length > 0 ? `defineOgImageScreenshot({ ${otherProps.map((p2) => code.slice(p2.start, p2.end)).join(", ")} })` : `defineOgImageScreenshot()`;
|
|
125
|
-
replacements.push({ start: node.start, end: node.end, text });
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (componentProp || rendererProp) {
|
|
130
|
-
const componentName = componentProp ? code.slice(componentProp.value.start, componentProp.value.end) : "'NuxtSeo'";
|
|
131
|
-
const propsValue = propsProp ? code.slice(propsProp.value.start, propsProp.value.end) : "{}";
|
|
132
|
-
const otherProps = properties.filter(
|
|
133
|
-
(p2) => p2 !== componentProp && p2 !== rendererProp && p2 !== propsProp
|
|
134
|
-
);
|
|
135
|
-
const text = otherProps.length > 0 ? `defineOgImage(${componentName}, ${propsValue}, { ${otherProps.map((p2) => code.slice(p2.start, p2.end)).join(", ")} })` : `defineOgImage(${componentName}, ${propsValue})`;
|
|
136
|
-
replacements.push({ start: node.start, end: node.end, text });
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (DEPRECATED_COMPOSABLE_NAMES.has(calleeName)) {
|
|
141
|
-
replacements.push({ start: callee.start, end: callee.end, text: "defineOgImage" });
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
return replacements;
|
|
145
|
-
}
|
|
146
|
-
function parseSfcBlocks(code) {
|
|
147
|
-
const scripts = [];
|
|
148
|
-
let template = null;
|
|
149
|
-
const ast = ultrahtml.parse(code);
|
|
150
|
-
ultrahtml.walkSync(ast, (node) => {
|
|
151
|
-
if (node.type !== ultrahtml.ELEMENT_NODE || !("name" in node))
|
|
152
|
-
return;
|
|
153
|
-
const el = node;
|
|
154
|
-
if (el.name === "script" && el.loc) {
|
|
155
|
-
const start = el.loc[0].end;
|
|
156
|
-
const end = el.loc[1].start;
|
|
157
|
-
scripts.push({ content: code.slice(start, end), offset: start });
|
|
158
|
-
} else if (el.name === "template" && el.loc) {
|
|
159
|
-
const start = el.loc[0].end;
|
|
160
|
-
const end = el.loc[1].start;
|
|
161
|
-
template = { content: code.slice(start, end), offset: start };
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
return { scripts, template };
|
|
165
|
-
}
|
|
166
|
-
function applyReplacements(code, replacements) {
|
|
167
|
-
const sorted = replacements.toSorted((a, b) => b.start - a.start);
|
|
168
|
-
for (const r of sorted) {
|
|
169
|
-
code = code.slice(0, r.start) + r.text + code.slice(r.end);
|
|
170
|
-
}
|
|
171
|
-
return code;
|
|
172
|
-
}
|
|
173
|
-
const defaultComponentDirs = ["OgImage", "OgImageCommunity", "og-image", "OgImageTemplate"];
|
|
174
|
-
const RENDERERS = [
|
|
175
|
-
{
|
|
176
|
-
name: "takumi",
|
|
177
|
-
label: "Takumi (Recommended)",
|
|
178
|
-
description: "2-10x faster, comprehensive CSS support"
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
name: "satori",
|
|
182
|
-
label: "Satori",
|
|
183
|
-
description: "SVG-based renderer - works without extra dependencies"
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
name: "browser",
|
|
187
|
-
label: "Browser",
|
|
188
|
-
description: "Full CSS via screenshots - prerender only"
|
|
189
|
-
}
|
|
190
|
-
];
|
|
191
|
-
const EDGE_PRESETS = ["cloudflare", "cloudflare-pages", "cloudflare-module", "vercel-edge", "netlify-edge"];
|
|
192
|
-
function getRendererDeps(renderer, isEdge) {
|
|
193
|
-
switch (renderer) {
|
|
194
|
-
case "satori":
|
|
195
|
-
return isEdge ? ["satori", "@resvg/resvg-js", "@resvg/resvg-wasm"] : ["satori", "@resvg/resvg-js"];
|
|
196
|
-
case "takumi":
|
|
197
|
-
return isEdge ? ["@takumi-rs/core", "@takumi-rs/wasm"] : ["@takumi-rs/core"];
|
|
198
|
-
case "browser":
|
|
199
|
-
return isEdge ? [] : ["playwright-core"];
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
function getBaseName(filename) {
|
|
203
|
-
return filename.replace(RE_RENDERER_SUFFIX, "");
|
|
204
|
-
}
|
|
205
|
-
function hasRendererSuffix(filename) {
|
|
206
|
-
return RE_ANY_RENDERER_SUFFIX.test(filename);
|
|
207
|
-
}
|
|
208
|
-
function hasChromiumSuffix(filename) {
|
|
209
|
-
return RE_CHROMIUM_SUFFIX.test(filename);
|
|
210
|
-
}
|
|
211
|
-
function listTemplates() {
|
|
212
|
-
const templates = [...new Set(
|
|
213
|
-
fs.readdirSync(communityDir).filter((f) => f.endsWith(".vue")).map(getBaseName)
|
|
214
|
-
)];
|
|
215
|
-
p__namespace.intro("Community Templates");
|
|
216
|
-
p__namespace.note(templates.map((t) => `\u2022 ${t}`).join("\n"), "Available");
|
|
217
|
-
p__namespace.log.info("Usage: npx nuxt-og-image eject <template-name>");
|
|
218
|
-
p__namespace.outro("");
|
|
219
|
-
}
|
|
220
|
-
function findTemplateFile(name) {
|
|
221
|
-
const files = fs.readdirSync(communityDir).filter((f) => f.endsWith(".vue"));
|
|
222
|
-
const match = files.find((f) => getBaseName(f) === name);
|
|
223
|
-
return match || null;
|
|
224
|
-
}
|
|
225
|
-
function ejectTemplate(name, targetDir) {
|
|
226
|
-
const templateFile = findTemplateFile(name);
|
|
227
|
-
if (!templateFile) {
|
|
228
|
-
p__namespace.log.error(`Template "${name}" not found.`);
|
|
229
|
-
listTemplates();
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
const templatePath = pathe.join(communityDir, templateFile);
|
|
233
|
-
const outputDir = pathe.resolve(targetDir, "components", "OgImage");
|
|
234
|
-
if (!fs.existsSync(outputDir))
|
|
235
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
236
|
-
const outputPath = pathe.join(outputDir, templateFile);
|
|
237
|
-
if (fs.existsSync(outputPath)) {
|
|
238
|
-
p__namespace.log.error(`File already exists: ${outputPath}`);
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
241
|
-
const content = fs.readFileSync(templatePath, "utf-8");
|
|
242
|
-
fs.writeFileSync(outputPath, content, "utf-8");
|
|
243
|
-
p__namespace.log.success(`Ejected "${name}" to ${outputPath}`);
|
|
244
|
-
}
|
|
245
|
-
function findOgImageComponents(dir) {
|
|
246
|
-
const components = [];
|
|
247
|
-
for (const componentDir of defaultComponentDirs) {
|
|
248
|
-
const ogImageDir = pathe.join(dir, "components", componentDir);
|
|
249
|
-
if (fs.existsSync(ogImageDir)) {
|
|
250
|
-
const files = fs.readdirSync(ogImageDir, { withFileTypes: true });
|
|
251
|
-
for (const file of files) {
|
|
252
|
-
if (file.isFile() && file.name.endsWith(".vue"))
|
|
253
|
-
components.push(pathe.join(ogImageDir, file.name));
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return components;
|
|
258
|
-
}
|
|
259
|
-
function globFiles(dir, pattern, exclude = []) {
|
|
260
|
-
const results = [];
|
|
261
|
-
function walk(currentDir) {
|
|
262
|
-
if (!fs.existsSync(currentDir))
|
|
263
|
-
return;
|
|
264
|
-
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
265
|
-
for (const entry of entries) {
|
|
266
|
-
const fullPath = pathe.join(currentDir, entry.name);
|
|
267
|
-
if (exclude.some((re) => re.test(fullPath)))
|
|
268
|
-
continue;
|
|
269
|
-
if (entry.isDirectory()) {
|
|
270
|
-
walk(fullPath);
|
|
271
|
-
} else if (entry.isFile() && pattern.test(entry.name)) {
|
|
272
|
-
results.push(fullPath);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
walk(dir);
|
|
277
|
-
return results;
|
|
278
|
-
}
|
|
279
|
-
const COMMUNITY_TEMPLATES = ["NuxtSeo", "Brutalist", "SimpleBlog"];
|
|
280
|
-
const RE_COMMUNITY_TEMPLATE = /defineOgImage\w*\s*\(\s*['"](\w+)['"]/g;
|
|
281
|
-
function detectCommunityTemplateUsage(rootDir) {
|
|
282
|
-
const excludePatterns = [RE_EXCLUDE_NODE_MODULES, RE_EXCLUDE_NUXT, RE_EXCLUDE_OUTPUT, RE_EXCLUDE_DATA, RE_EXCLUDE_DIST];
|
|
283
|
-
const files = globFiles(rootDir, RE_VUE_OR_SCRIPT, excludePatterns);
|
|
284
|
-
const used = /* @__PURE__ */ new Set();
|
|
285
|
-
for (const file of files) {
|
|
286
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
287
|
-
RE_COMMUNITY_TEMPLATE.lastIndex = 0;
|
|
288
|
-
for (let m = RE_COMMUNITY_TEMPLATE.exec(content); m !== null; m = RE_COMMUNITY_TEMPLATE.exec(content)) {
|
|
289
|
-
const baseName = m[1]?.split(".")[0];
|
|
290
|
-
if (baseName && COMMUNITY_TEMPLATES.includes(baseName)) {
|
|
291
|
-
used.add(baseName);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return [...used];
|
|
296
|
-
}
|
|
297
|
-
async function removeDeprecatedConfigKeys(rootDir, keys) {
|
|
298
|
-
const configPaths = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
|
|
299
|
-
let configPath;
|
|
300
|
-
for (const cp of configPaths) {
|
|
301
|
-
const fullPath = pathe.join(rootDir, cp);
|
|
302
|
-
if (fs.existsSync(fullPath)) {
|
|
303
|
-
configPath = fullPath;
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if (!configPath)
|
|
308
|
-
return { removed: [], failed: keys.map((k) => k.key) };
|
|
309
|
-
const { loadFile, writeFile } = await import('magicast');
|
|
310
|
-
const mod = await loadFile(configPath);
|
|
311
|
-
const config = mod.exports.default;
|
|
312
|
-
const ogImageConfig = config?.ogImage;
|
|
313
|
-
if (!ogImageConfig)
|
|
314
|
-
return { removed: [], failed: keys.map((k) => k.key) };
|
|
315
|
-
const removed = [];
|
|
316
|
-
const failed = [];
|
|
317
|
-
for (const { key } of keys) {
|
|
318
|
-
if (key.includes(".")) {
|
|
319
|
-
const parts = key.split(".");
|
|
320
|
-
let obj = ogImageConfig;
|
|
321
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
322
|
-
obj = obj?.[parts[i]];
|
|
323
|
-
}
|
|
324
|
-
if (obj && parts.at(-1) in obj) {
|
|
325
|
-
delete obj[parts.at(-1)];
|
|
326
|
-
if (Object.keys(obj).length === 0) {
|
|
327
|
-
delete ogImageConfig[parts[0]];
|
|
328
|
-
}
|
|
329
|
-
removed.push(key);
|
|
330
|
-
} else {
|
|
331
|
-
failed.push(key);
|
|
332
|
-
}
|
|
333
|
-
} else if (key in ogImageConfig) {
|
|
334
|
-
delete ogImageConfig[key];
|
|
335
|
-
removed.push(key);
|
|
336
|
-
} else {
|
|
337
|
-
failed.push(key);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
if (Object.keys(ogImageConfig).length === 0)
|
|
341
|
-
delete config.ogImage;
|
|
342
|
-
await writeFile(mod, configPath);
|
|
343
|
-
return { removed, failed };
|
|
344
|
-
}
|
|
345
|
-
const DEPRECATED_CONFIG_KEYS = {
|
|
346
|
-
fonts: "@nuxt/fonts module (migrated automatically)",
|
|
347
|
-
strictNuxtContentPaths: "Removed (no effect in Content v3)",
|
|
348
|
-
playground: "Removed (use Nuxt DevTools)",
|
|
349
|
-
host: "site.url or NUXT_SITE_URL",
|
|
350
|
-
siteUrl: "site.url or NUXT_SITE_URL",
|
|
351
|
-
runtimeBrowser: "compatibility.runtime.browser",
|
|
352
|
-
runtimeSatori: "compatibility.runtime.satori",
|
|
353
|
-
cacheTtl: "cacheMaxAgeSeconds",
|
|
354
|
-
cache: "cacheMaxAgeSeconds",
|
|
355
|
-
cacheQueryParams: "Removed",
|
|
356
|
-
cacheKey: "Removed",
|
|
357
|
-
static: "zeroRuntime",
|
|
358
|
-
componentOptions: "Removed (use defineOgImage())"
|
|
359
|
-
};
|
|
360
|
-
async function checkNuxtConfig(rootDir) {
|
|
361
|
-
let hasNuxtFontsInPkg = false;
|
|
362
|
-
const pkgPath = pathe.join(rootDir, "package.json");
|
|
363
|
-
if (fs.existsSync(pkgPath)) {
|
|
364
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
365
|
-
hasNuxtFontsInPkg = !!(pkg.dependencies?.["@nuxt/fonts"] || pkg.devDependencies?.["@nuxt/fonts"]);
|
|
366
|
-
}
|
|
367
|
-
const config = await kit.loadNuxtConfig({ cwd: rootDir }).catch((error) => {
|
|
368
|
-
p__namespace.log.warn(`Could not inspect Nuxt config: ${error.message}`);
|
|
369
|
-
return null;
|
|
370
|
-
});
|
|
371
|
-
if (!config)
|
|
372
|
-
return { hasDeprecatedFonts: false, hasNuxtFonts: hasNuxtFontsInPkg, hasDefaultsComponent: false, defaultComponentName: null, nitroPreset: null, deprecatedConfigKeys: [] };
|
|
373
|
-
const ogImageConfig = config.ogImage;
|
|
374
|
-
const hasDeprecatedFonts = !!ogImageConfig?.fonts;
|
|
375
|
-
const hasDefaultsComponent = !!ogImageConfig?.defaults?.component;
|
|
376
|
-
const defaultComponentName = hasDefaultsComponent ? String(ogImageConfig.defaults.component) : null;
|
|
377
|
-
const modules = config.modules || [];
|
|
378
|
-
const hasNuxtFontsInConfig = modules.some(
|
|
379
|
-
(m) => typeof m === "string" && m === "@nuxt/fonts" || Array.isArray(m) && m[0] === "@nuxt/fonts"
|
|
380
|
-
);
|
|
381
|
-
const nitroPreset = config.nitro?.preset || null;
|
|
382
|
-
const deprecatedConfigKeys = [];
|
|
383
|
-
if (ogImageConfig) {
|
|
384
|
-
for (const [key, replacement] of Object.entries(DEPRECATED_CONFIG_KEYS)) {
|
|
385
|
-
if (key in ogImageConfig) {
|
|
386
|
-
deprecatedConfigKeys.push({ key, replacement });
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if (ogImageConfig.defaults?.renderer) {
|
|
390
|
-
deprecatedConfigKeys.push({ key: "defaults.renderer", replacement: "Removed (renderer determined by component filename suffix)" });
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return {
|
|
394
|
-
hasDeprecatedFonts,
|
|
395
|
-
hasNuxtFonts: hasNuxtFontsInPkg || hasNuxtFontsInConfig,
|
|
396
|
-
hasDefaultsComponent,
|
|
397
|
-
defaultComponentName,
|
|
398
|
-
nitroPreset,
|
|
399
|
-
deprecatedConfigKeys
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
async function checkMigrationNeeded(rootDir) {
|
|
403
|
-
const result = {
|
|
404
|
-
needsComponentRename: false,
|
|
405
|
-
needsFontsMigration: false,
|
|
406
|
-
needsNuxtFonts: false,
|
|
407
|
-
needsDefaultsComponentMigration: false,
|
|
408
|
-
defaultComponentName: null,
|
|
409
|
-
componentsToRename: [],
|
|
410
|
-
deprecatedConfigKeys: [],
|
|
411
|
-
usedCommunityTemplates: []
|
|
412
|
-
};
|
|
413
|
-
const dirs = [rootDir];
|
|
414
|
-
if (fs.existsSync(pathe.join(rootDir, "app")))
|
|
415
|
-
dirs.push(pathe.join(rootDir, "app"));
|
|
416
|
-
const layersDir = pathe.join(rootDir, "layers");
|
|
417
|
-
if (fs.existsSync(layersDir)) {
|
|
418
|
-
const layerDirs = fs.readdirSync(layersDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => pathe.join(layersDir, d.name));
|
|
419
|
-
dirs.push(...layerDirs);
|
|
420
|
-
}
|
|
421
|
-
for (const dir of dirs) {
|
|
422
|
-
const components = findOgImageComponents(dir);
|
|
423
|
-
for (const filepath of components) {
|
|
424
|
-
const filename = pathe.basename(filepath);
|
|
425
|
-
if (hasChromiumSuffix(filename)) {
|
|
426
|
-
result.componentsToRename.push({ from: filepath, to: filepath.replace(".chromium.vue", ".browser.vue") });
|
|
427
|
-
} else if (!hasRendererSuffix(filename)) {
|
|
428
|
-
result.componentsToRename.push({ from: filepath, to: "" });
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
result.needsComponentRename = result.componentsToRename.length > 0;
|
|
433
|
-
const configCheck = await checkNuxtConfig(rootDir);
|
|
434
|
-
result.needsFontsMigration = configCheck.hasDeprecatedFonts;
|
|
435
|
-
result.needsNuxtFonts = !configCheck.hasNuxtFonts;
|
|
436
|
-
result.needsDefaultsComponentMigration = configCheck.hasDefaultsComponent;
|
|
437
|
-
result.defaultComponentName = configCheck.defaultComponentName;
|
|
438
|
-
result.deprecatedConfigKeys = configCheck.deprecatedConfigKeys.filter((k) => k.key !== "fonts");
|
|
439
|
-
result.usedCommunityTemplates = detectCommunityTemplateUsage(rootDir);
|
|
440
|
-
return result;
|
|
441
|
-
}
|
|
442
|
-
function attrsToProps(attributes) {
|
|
443
|
-
const props = [];
|
|
444
|
-
for (const [key, value] of Object.entries(attributes)) {
|
|
445
|
-
if (key.startsWith(":")) {
|
|
446
|
-
props.push(`${key.slice(1)}: ${value}`);
|
|
447
|
-
} else if (value === "") {
|
|
448
|
-
props.push(`${key}: true`);
|
|
449
|
-
} else {
|
|
450
|
-
props.push(`${key}: '${value}'`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
return props.length ? `{ ${props.join(", ")} }` : "";
|
|
454
|
-
}
|
|
455
|
-
function collectTemplateReplacements(templateHtml) {
|
|
456
|
-
const replacements = [];
|
|
457
|
-
const ast = ultrahtml.parse(templateHtml);
|
|
458
|
-
ultrahtml.walkSync(ast, (node) => {
|
|
459
|
-
if (node.type !== ultrahtml.ELEMENT_NODE || !("name" in node))
|
|
460
|
-
return;
|
|
461
|
-
const el = node;
|
|
462
|
-
if (el.name !== "OgImage" && el.name !== "OgImageScreenshot")
|
|
463
|
-
return;
|
|
464
|
-
const isScreenshot = el.name === "OgImageScreenshot";
|
|
465
|
-
const composable = isScreenshot ? "defineOgImageScreenshot" : "defineOgImage";
|
|
466
|
-
const propsStr = attrsToProps(el.attributes);
|
|
467
|
-
const start = el.loc[0].start;
|
|
468
|
-
const end = el.loc[1].end;
|
|
469
|
-
const text = propsStr ? `<!-- Migrated: use ${composable}(${propsStr}) in <script setup> -->` : `<!-- Migrated: use ${composable}() in <script setup> -->`;
|
|
470
|
-
replacements.push({ start, end, text });
|
|
471
|
-
});
|
|
472
|
-
return replacements;
|
|
473
|
-
}
|
|
474
|
-
function migrateDefineOgImageApi(dryRun) {
|
|
475
|
-
const cwd = process.cwd();
|
|
476
|
-
const excludePatterns = [RE_EXCLUDE_NODE_MODULES, RE_EXCLUDE_NUXT, RE_EXCLUDE_OUTPUT, RE_EXCLUDE_DATA, RE_EXCLUDE_DIST];
|
|
477
|
-
const files = globFiles(cwd, RE_VUE_OR_SCRIPT, excludePatterns);
|
|
478
|
-
const changes = [];
|
|
479
|
-
for (const file of files) {
|
|
480
|
-
let content = fs.readFileSync(file, "utf-8");
|
|
481
|
-
if (!content.includes("defineOgImage") && !content.includes("OgImage") && !content.includes("#nuxt-og-image-utils") && !content.includes("useOgImageRuntimeConfig")) {
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
let modified = false;
|
|
485
|
-
let changeCount = 0;
|
|
486
|
-
if (file.endsWith(".vue")) {
|
|
487
|
-
const { scripts, template } = parseSfcBlocks(content);
|
|
488
|
-
if (template && content.includes("OgImage")) {
|
|
489
|
-
const replacements = collectTemplateReplacements(template.content);
|
|
490
|
-
if (replacements.length > 0) {
|
|
491
|
-
const adjusted = replacements.map((r) => ({
|
|
492
|
-
start: r.start + template.offset,
|
|
493
|
-
end: r.end + template.offset,
|
|
494
|
-
text: r.text
|
|
495
|
-
}));
|
|
496
|
-
content = applyReplacements(content, adjusted);
|
|
497
|
-
modified = true;
|
|
498
|
-
changeCount += replacements.length;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
const allReplacements = [];
|
|
502
|
-
for (const script of scripts) {
|
|
503
|
-
const replacements = collectScriptReplacements(script.content, file);
|
|
504
|
-
for (const r of replacements) {
|
|
505
|
-
allReplacements.push({ start: r.start + script.offset, end: r.end + script.offset, text: r.text });
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
if (allReplacements.length > 0) {
|
|
509
|
-
content = applyReplacements(content, allReplacements);
|
|
510
|
-
modified = true;
|
|
511
|
-
changeCount += allReplacements.length;
|
|
512
|
-
}
|
|
513
|
-
} else {
|
|
514
|
-
const replacements = collectScriptReplacements(content, file);
|
|
515
|
-
if (replacements.length > 0) {
|
|
516
|
-
content = applyReplacements(content, replacements);
|
|
517
|
-
modified = true;
|
|
518
|
-
changeCount += replacements.length;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
if (modified) {
|
|
522
|
-
changes.push({ file, count: changeCount });
|
|
523
|
-
if (!dryRun) {
|
|
524
|
-
fs.writeFileSync(file, content, "utf-8");
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
return { changes };
|
|
529
|
-
}
|
|
530
|
-
function migrateV6Components(componentsToRename, defaultRenderer, dryRun) {
|
|
531
|
-
for (const item of componentsToRename) {
|
|
532
|
-
if (item.to)
|
|
533
|
-
continue;
|
|
534
|
-
const filename = pathe.basename(item.from);
|
|
535
|
-
const newName = filename.replace(".vue", `.${defaultRenderer}.vue`);
|
|
536
|
-
item.to = pathe.join(pathe.dirname(item.from), newName);
|
|
537
|
-
}
|
|
538
|
-
if (dryRun) {
|
|
539
|
-
console.log("\nWould rename:");
|
|
540
|
-
for (const { from, to } of componentsToRename) {
|
|
541
|
-
console.log(` ${pathe.basename(from)} \u2192 ${pathe.basename(to)}`);
|
|
542
|
-
}
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
for (const { from, to } of componentsToRename) {
|
|
546
|
-
fs.renameSync(from, to);
|
|
547
|
-
console.log(`\u2713 Renamed ${pathe.basename(from)}`);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
async function detectDeploymentTarget(rootDir) {
|
|
551
|
-
const config = await kit.loadNuxtConfig({ cwd: rootDir }).catch((error) => {
|
|
552
|
-
p__namespace.log.warn(`Could not detect deployment target: ${error.message}`);
|
|
553
|
-
return null;
|
|
554
|
-
});
|
|
555
|
-
return config?.nitro?.preset || null;
|
|
556
|
-
}
|
|
557
|
-
async function installRendererDeps(renderers, isEdge) {
|
|
558
|
-
const cwd = process.cwd();
|
|
559
|
-
const pm = await nypm.detectPackageManager(cwd);
|
|
560
|
-
const pmName = pm?.name || "npm";
|
|
561
|
-
const allDeps = [];
|
|
562
|
-
for (const renderer of renderers) {
|
|
563
|
-
allDeps.push(...getRendererDeps(renderer, isEdge));
|
|
564
|
-
}
|
|
565
|
-
const uniqueDeps = [...new Set(allDeps)];
|
|
566
|
-
if (uniqueDeps.length === 0)
|
|
567
|
-
return;
|
|
568
|
-
const spinner = p__namespace.spinner();
|
|
569
|
-
spinner.start(`Installing dependencies with ${pmName}...`);
|
|
570
|
-
for (const dep of uniqueDeps) {
|
|
571
|
-
await nypm.addDependency(dep, { cwd, dev: false }).catch(() => {
|
|
572
|
-
spinner.stop(`Failed to install ${dep}`);
|
|
573
|
-
p__namespace.log.warn(`Run manually: ${pmName} add ${dep}`);
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
spinner.stop("Dependencies installed");
|
|
577
|
-
}
|
|
578
|
-
async function installNuxtFonts() {
|
|
579
|
-
const cwd = process.cwd();
|
|
580
|
-
const spinner = p__namespace.spinner();
|
|
581
|
-
spinner.start("Adding @nuxt/fonts module...");
|
|
582
|
-
const { exec } = await import('tinyexec');
|
|
583
|
-
try {
|
|
584
|
-
await exec("npx", ["nuxi", "module", "add", "@nuxt/fonts"], { nodeOptions: { cwd } });
|
|
585
|
-
spinner.stop("@nuxt/fonts module added");
|
|
586
|
-
} catch {
|
|
587
|
-
spinner.stop("Failed to add @nuxt/fonts");
|
|
588
|
-
p__namespace.log.warn("Run manually: npx nuxi module add @nuxt/fonts");
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
async function runMigrate(args2) {
|
|
592
|
-
const dryRun = args2.includes("--dry-run") || args2.includes("-d");
|
|
593
|
-
const skipConfirm = args2.includes("--yes") || args2.includes("-y");
|
|
594
|
-
const rendererIdx = args2.indexOf("--renderer");
|
|
595
|
-
const cliRenderer = rendererIdx !== -1 ? args2[rendererIdx + 1] : null;
|
|
596
|
-
if (cliRenderer && !["satori", "browser", "takumi"].includes(cliRenderer)) {
|
|
597
|
-
console.error(`Invalid renderer: ${cliRenderer}. Must be satori, browser, or takumi.`);
|
|
598
|
-
process.exit(1);
|
|
599
|
-
}
|
|
600
|
-
p__namespace.intro("nuxt-og-image v6 Migration");
|
|
601
|
-
const cwd = process.cwd();
|
|
602
|
-
const migrationCheck = await checkMigrationNeeded(cwd);
|
|
603
|
-
const noComponentWork = !migrationCheck.needsComponentRename;
|
|
604
|
-
const noFontsWork = !migrationCheck.needsFontsMigration && !migrationCheck.needsNuxtFonts;
|
|
605
|
-
const noDefaultsWork = !migrationCheck.needsDefaultsComponentMigration;
|
|
606
|
-
const noConfigWork = migrationCheck.deprecatedConfigKeys.length === 0;
|
|
607
|
-
const noCommunityWork = migrationCheck.usedCommunityTemplates.length === 0;
|
|
608
|
-
if (noComponentWork && noFontsWork && noDefaultsWork && noConfigWork && noCommunityWork) {
|
|
609
|
-
console.log("\u2713 All OG Image components already have renderer suffixes.");
|
|
610
|
-
p__namespace.outro("Done");
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
const tasks = [];
|
|
614
|
-
if (migrationCheck.needsComponentRename) {
|
|
615
|
-
tasks.push(`Rename ${migrationCheck.componentsToRename.length} component(s) to include renderer suffix`);
|
|
616
|
-
}
|
|
617
|
-
if (migrationCheck.needsDefaultsComponentMigration) {
|
|
618
|
-
tasks.push(`Remove deprecated defaults.component: '${migrationCheck.defaultComponentName}'`);
|
|
619
|
-
}
|
|
620
|
-
if (migrationCheck.deprecatedConfigKeys.length > 0) {
|
|
621
|
-
tasks.push(`Remove ${migrationCheck.deprecatedConfigKeys.length} deprecated config option(s): ${migrationCheck.deprecatedConfigKeys.map((k) => k.key).join(", ")}`);
|
|
622
|
-
}
|
|
623
|
-
if (migrationCheck.needsFontsMigration) {
|
|
624
|
-
tasks.push("Migrate ogImage.fonts to @nuxt/fonts config");
|
|
625
|
-
}
|
|
626
|
-
if (migrationCheck.needsNuxtFonts) {
|
|
627
|
-
tasks.push("Install @nuxt/fonts module");
|
|
628
|
-
}
|
|
629
|
-
if (migrationCheck.usedCommunityTemplates.length > 0) {
|
|
630
|
-
tasks.push(`Eject community templates: ${migrationCheck.usedCommunityTemplates.join(", ")}`);
|
|
631
|
-
}
|
|
632
|
-
tasks.push("Migrate deprecated composables (defineOgImageStatic, etc.) to defineOgImage()");
|
|
633
|
-
tasks.push("Migrate <OgImage> and <OgImageScreenshot> components to composables");
|
|
634
|
-
tasks.push("Migrate defineOgImage({ url }) to useSeoMeta({ ogImage })");
|
|
635
|
-
tasks.push("Update defineOgImage() calls to new API");
|
|
636
|
-
p__namespace.note(tasks.map((t) => `\u2022 ${t}`).join("\n"), "Migration tasks");
|
|
637
|
-
if (dryRun) {
|
|
638
|
-
p__namespace.log.warn("[Dry run mode - no changes will be made]");
|
|
639
|
-
}
|
|
640
|
-
let selectedRenderers;
|
|
641
|
-
if (skipConfirm) {
|
|
642
|
-
selectedRenderers = cliRenderer ? [cliRenderer] : ["takumi"];
|
|
643
|
-
} else {
|
|
644
|
-
const confirmed = await p__namespace.confirm({
|
|
645
|
-
message: "This will modify files. Continue?",
|
|
646
|
-
initialValue: false
|
|
647
|
-
});
|
|
648
|
-
if (p__namespace.isCancel(confirmed) || !confirmed) {
|
|
649
|
-
p__namespace.cancel("Migration cancelled");
|
|
650
|
-
process.exit(0);
|
|
651
|
-
}
|
|
652
|
-
const rendererSelection = await p__namespace.multiselect({
|
|
653
|
-
message: "Which renderers do you want to use?",
|
|
654
|
-
options: RENDERERS.map((r) => ({
|
|
655
|
-
value: r.name,
|
|
656
|
-
label: r.label,
|
|
657
|
-
hint: r.description
|
|
658
|
-
})),
|
|
659
|
-
initialValues: ["takumi"],
|
|
660
|
-
required: true
|
|
661
|
-
});
|
|
662
|
-
if (p__namespace.isCancel(rendererSelection)) {
|
|
663
|
-
p__namespace.cancel("Migration cancelled");
|
|
664
|
-
process.exit(0);
|
|
665
|
-
}
|
|
666
|
-
selectedRenderers = rendererSelection;
|
|
667
|
-
if (!dryRun) {
|
|
668
|
-
const installDeps = await p__namespace.confirm({
|
|
669
|
-
message: "Install renderer dependencies?",
|
|
670
|
-
initialValue: true
|
|
671
|
-
});
|
|
672
|
-
if (!p__namespace.isCancel(installDeps) && installDeps) {
|
|
673
|
-
const detectedPreset = await detectDeploymentTarget(cwd);
|
|
674
|
-
let isEdge = detectedPreset ? EDGE_PRESETS.includes(detectedPreset) : false;
|
|
675
|
-
if (detectedPreset) {
|
|
676
|
-
p__namespace.log.info(`Detected deployment target: ${detectedPreset}`);
|
|
677
|
-
} else {
|
|
678
|
-
const targetSelection = await p__namespace.select({
|
|
679
|
-
message: "What is your deployment target?",
|
|
680
|
-
options: [
|
|
681
|
-
{ value: "node", label: "Node.js", hint: "node-server, vercel, netlify, aws-lambda, etc." },
|
|
682
|
-
{ value: "edge", label: "Edge/Workers", hint: "cloudflare, vercel-edge, netlify-edge" }
|
|
683
|
-
],
|
|
684
|
-
initialValue: "node"
|
|
685
|
-
});
|
|
686
|
-
if (!p__namespace.isCancel(targetSelection)) {
|
|
687
|
-
isEdge = targetSelection === "edge";
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
if (isEdge && selectedRenderers.includes("browser")) {
|
|
691
|
-
p__namespace.log.warn("Browser renderer is not supported on edge runtimes - skipping its dependencies");
|
|
692
|
-
}
|
|
693
|
-
await installRendererDeps(selectedRenderers, isEdge);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
if (migrationCheck.needsComponentRename) {
|
|
698
|
-
console.log("\nRenaming components...");
|
|
699
|
-
migrateV6Components(migrationCheck.componentsToRename, selectedRenderers[0] || "takumi", dryRun);
|
|
700
|
-
}
|
|
701
|
-
console.log("\nMigrating defineOgImage calls...");
|
|
702
|
-
const apiChanges = migrateDefineOgImageApi(dryRun);
|
|
703
|
-
if (apiChanges.changes.length > 0) {
|
|
704
|
-
for (const { file, count } of apiChanges.changes) {
|
|
705
|
-
const relPath = pathe.relative(cwd, file);
|
|
706
|
-
console.log(` ${relPath} (${count} change${count > 1 ? "s" : ""})`);
|
|
707
|
-
}
|
|
708
|
-
} else {
|
|
709
|
-
console.log(" No API changes needed");
|
|
710
|
-
}
|
|
711
|
-
if (migrationCheck.needsDefaultsComponentMigration) {
|
|
712
|
-
if (dryRun) {
|
|
713
|
-
p__namespace.log.info(`Would remove defaults.component: '${migrationCheck.defaultComponentName}'`);
|
|
714
|
-
} else {
|
|
715
|
-
const result = await fonts.migrateDefaultsComponent(cwd).catch((err) => {
|
|
716
|
-
p__namespace.log.warn(`Failed to migrate defaults.component: ${err.message}`);
|
|
717
|
-
return { migrated: false, componentName: null, message: err.message };
|
|
718
|
-
});
|
|
719
|
-
if (result.migrated) {
|
|
720
|
-
p__namespace.log.success(`Removed defaults.component: '${result.componentName}'`);
|
|
721
|
-
p__namespace.log.info(` To use '${result.componentName}' as default, rename it to OgImage/Default.{renderer}.vue`);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
if (migrationCheck.needsFontsMigration) {
|
|
726
|
-
if (dryRun) {
|
|
727
|
-
p__namespace.log.info("Would migrate ogImage.fonts to @nuxt/fonts config");
|
|
728
|
-
} else {
|
|
729
|
-
const result = await fonts.migrateFontsConfig(cwd).catch((err) => {
|
|
730
|
-
p__namespace.log.warn(`Failed to migrate fonts config: ${err.message}`);
|
|
731
|
-
return { migrated: false, message: err.message };
|
|
732
|
-
});
|
|
733
|
-
if (result.migrated) {
|
|
734
|
-
p__namespace.log.success(result.message);
|
|
735
|
-
} else {
|
|
736
|
-
p__namespace.log.warn(`Fonts migration: ${result.message}`);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
if (migrationCheck.deprecatedConfigKeys.length > 0) {
|
|
741
|
-
if (dryRun) {
|
|
742
|
-
p__namespace.log.info("Would remove deprecated config options:");
|
|
743
|
-
for (const { key, replacement } of migrationCheck.deprecatedConfigKeys) {
|
|
744
|
-
p__namespace.log.info(` \u2022 ogImage.${key} \u2192 ${replacement}`);
|
|
745
|
-
}
|
|
746
|
-
} else {
|
|
747
|
-
const result = await removeDeprecatedConfigKeys(cwd, migrationCheck.deprecatedConfigKeys).catch((err) => {
|
|
748
|
-
p__namespace.log.warn(`Failed to remove deprecated config: ${err.message}`);
|
|
749
|
-
return { removed: [], failed: migrationCheck.deprecatedConfigKeys.map((k) => k.key) };
|
|
750
|
-
});
|
|
751
|
-
if (result.removed.length > 0) {
|
|
752
|
-
p__namespace.log.success(`Removed deprecated config: ${result.removed.map((k) => `ogImage.${k}`).join(", ")}`);
|
|
753
|
-
}
|
|
754
|
-
for (const key of result.failed) {
|
|
755
|
-
const replacement = migrationCheck.deprecatedConfigKeys.find((k) => k.key === key)?.replacement;
|
|
756
|
-
if (replacement) {
|
|
757
|
-
p__namespace.log.warn(` Could not remove ogImage.${key} \u2014 manually replace with: ${replacement}`);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
if (migrationCheck.usedCommunityTemplates.length > 0) {
|
|
763
|
-
p__namespace.log.warn("Community templates detected that must be ejected for production:");
|
|
764
|
-
for (const name of migrationCheck.usedCommunityTemplates) {
|
|
765
|
-
p__namespace.log.warn(` \u2022 ${name}`);
|
|
766
|
-
}
|
|
767
|
-
if (!dryRun) {
|
|
768
|
-
const shouldEject = skipConfirm || await p__namespace.confirm({
|
|
769
|
-
message: "Eject community templates now?",
|
|
770
|
-
initialValue: true
|
|
771
|
-
}).then((v) => !p__namespace.isCancel(v) && v);
|
|
772
|
-
if (shouldEject) {
|
|
773
|
-
const targetDir = fs.existsSync(pathe.join(cwd, "app")) ? pathe.join(cwd, "app") : cwd;
|
|
774
|
-
for (const name of migrationCheck.usedCommunityTemplates) {
|
|
775
|
-
ejectTemplate(name, targetDir);
|
|
776
|
-
}
|
|
777
|
-
} else {
|
|
778
|
-
p__namespace.log.info("Run manually before building for production:");
|
|
779
|
-
for (const name of migrationCheck.usedCommunityTemplates) {
|
|
780
|
-
p__namespace.log.info(` npx nuxt-og-image eject ${name}`);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
if (migrationCheck.needsNuxtFonts && !dryRun) {
|
|
786
|
-
if (skipConfirm) {
|
|
787
|
-
await installNuxtFonts();
|
|
788
|
-
} else {
|
|
789
|
-
const addFonts = await p__namespace.confirm({
|
|
790
|
-
message: "@nuxt/fonts is recommended for font management. Install it?",
|
|
791
|
-
initialValue: true
|
|
792
|
-
});
|
|
793
|
-
if (!p__namespace.isCancel(addFonts) && addFonts) {
|
|
794
|
-
await installNuxtFonts();
|
|
795
|
-
} else {
|
|
796
|
-
p__namespace.log.info("Skipped @nuxt/fonts installation. Add manually: npx nuxi module add @nuxt/fonts");
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
if (dryRun) {
|
|
801
|
-
console.log("\n[Dry run - no changes made]");
|
|
802
|
-
console.log("Run without --dry-run to apply changes.");
|
|
803
|
-
} else {
|
|
804
|
-
const spinner = p__namespace.spinner();
|
|
805
|
-
spinner.start("Running nuxt prepare to update types...");
|
|
806
|
-
const { exec } = await import('tinyexec');
|
|
807
|
-
try {
|
|
808
|
-
await exec("npx", ["nuxi", "prepare"], { nodeOptions: { cwd } });
|
|
809
|
-
spinner.stop("Types updated");
|
|
810
|
-
} catch {
|
|
811
|
-
spinner.stop("Failed to run nuxt prepare");
|
|
812
|
-
p__namespace.log.warn("Run manually: npx nuxt prepare");
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
p__namespace.outro(dryRun ? "Dry run complete" : "Migration complete!");
|
|
816
|
-
}
|
|
817
|
-
async function runSwitch(args2) {
|
|
818
|
-
const dryRun = args2.includes("--dry-run") || args2.includes("-d");
|
|
819
|
-
const skipConfirm = args2.includes("--yes") || args2.includes("-y");
|
|
820
|
-
const fromIdx = args2.indexOf("--from");
|
|
821
|
-
const toIdx = args2.indexOf("--to");
|
|
822
|
-
const cliFrom = fromIdx !== -1 ? args2[fromIdx + 1] : null;
|
|
823
|
-
const cliTo = toIdx !== -1 ? args2[toIdx + 1] : null;
|
|
824
|
-
const validRenderers = RENDERERS.map((r) => r.name);
|
|
825
|
-
if (cliFrom && !validRenderers.includes(cliFrom)) {
|
|
826
|
-
p__namespace.log.error(`Invalid --from renderer: ${cliFrom}. Must be ${validRenderers.join(", ")}`);
|
|
827
|
-
process.exit(1);
|
|
828
|
-
}
|
|
829
|
-
if (cliTo && !validRenderers.includes(cliTo)) {
|
|
830
|
-
p__namespace.log.error(`Invalid --to renderer: ${cliTo}. Must be ${validRenderers.join(", ")}`);
|
|
831
|
-
process.exit(1);
|
|
832
|
-
}
|
|
833
|
-
p__namespace.intro("Switch OG Image renderer");
|
|
834
|
-
const cwd = process.cwd();
|
|
835
|
-
const dirs = [cwd];
|
|
836
|
-
if (fs.existsSync(pathe.join(cwd, "app")))
|
|
837
|
-
dirs.push(pathe.join(cwd, "app"));
|
|
838
|
-
const layersDir = pathe.join(cwd, "layers");
|
|
839
|
-
if (fs.existsSync(layersDir)) {
|
|
840
|
-
const layerDirs = fs.readdirSync(layersDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => pathe.join(layersDir, d.name));
|
|
841
|
-
dirs.push(...layerDirs);
|
|
842
|
-
}
|
|
843
|
-
const allComponents = [];
|
|
844
|
-
for (const dir of dirs) {
|
|
845
|
-
const components = findOgImageComponents(dir);
|
|
846
|
-
for (const filepath of components) {
|
|
847
|
-
const match = pathe.basename(filepath).match(RE_RENDERER_SUFFIX);
|
|
848
|
-
if (match) {
|
|
849
|
-
allComponents.push({ path: filepath, renderer: match[1] });
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
if (allComponents.length === 0) {
|
|
854
|
-
p__namespace.log.warn("No OG image components with renderer suffixes found.");
|
|
855
|
-
p__namespace.outro("Nothing to do");
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
const currentRenderers = [...new Set(allComponents.map((c) => c.renderer))];
|
|
859
|
-
p__namespace.log.info(`Found ${allComponents.length} component(s) using: ${currentRenderers.join(", ")}`);
|
|
860
|
-
let fromRenderer;
|
|
861
|
-
if (cliFrom) {
|
|
862
|
-
fromRenderer = cliFrom;
|
|
863
|
-
} else if (skipConfirm) {
|
|
864
|
-
if (currentRenderers.length === 1) {
|
|
865
|
-
fromRenderer = currentRenderers[0];
|
|
866
|
-
} else {
|
|
867
|
-
p__namespace.log.error("Multiple renderers detected. Use --from to specify which to migrate.");
|
|
868
|
-
process.exit(1);
|
|
869
|
-
}
|
|
870
|
-
} else {
|
|
871
|
-
const fromSelection = await p__namespace.select({
|
|
872
|
-
message: "Which renderer do you want to migrate from?",
|
|
873
|
-
options: currentRenderers.map((r) => ({
|
|
874
|
-
value: r,
|
|
875
|
-
label: RENDERERS.find((rd) => rd.name === r)?.label || r
|
|
876
|
-
})),
|
|
877
|
-
initialValue: currentRenderers[0]
|
|
878
|
-
});
|
|
879
|
-
if (p__namespace.isCancel(fromSelection)) {
|
|
880
|
-
p__namespace.cancel("Cancelled");
|
|
881
|
-
process.exit(0);
|
|
882
|
-
}
|
|
883
|
-
fromRenderer = fromSelection;
|
|
884
|
-
}
|
|
885
|
-
let toRenderer;
|
|
886
|
-
if (cliTo) {
|
|
887
|
-
toRenderer = cliTo;
|
|
888
|
-
} else if (skipConfirm) {
|
|
889
|
-
toRenderer = fromRenderer === "takumi" ? "satori" : "takumi";
|
|
890
|
-
} else {
|
|
891
|
-
const targetOptions = RENDERERS.filter((r) => r.name !== fromRenderer);
|
|
892
|
-
const toSelection = await p__namespace.select({
|
|
893
|
-
message: "Which renderer do you want to switch to?",
|
|
894
|
-
options: targetOptions.map((r) => ({
|
|
895
|
-
value: r.name,
|
|
896
|
-
label: r.label,
|
|
897
|
-
hint: r.description
|
|
898
|
-
})),
|
|
899
|
-
initialValue: targetOptions[0]?.name
|
|
900
|
-
});
|
|
901
|
-
if (p__namespace.isCancel(toSelection)) {
|
|
902
|
-
p__namespace.cancel("Cancelled");
|
|
903
|
-
process.exit(0);
|
|
904
|
-
}
|
|
905
|
-
toRenderer = toSelection;
|
|
906
|
-
}
|
|
907
|
-
if (fromRenderer === toRenderer) {
|
|
908
|
-
p__namespace.log.warn(`Source and target renderer are the same (${fromRenderer}).`);
|
|
909
|
-
p__namespace.outro("Nothing to do");
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
const toMigrate = allComponents.filter((c) => c.renderer === fromRenderer);
|
|
913
|
-
if (toMigrate.length === 0) {
|
|
914
|
-
p__namespace.log.warn(`No components using ${fromRenderer} renderer found.`);
|
|
915
|
-
p__namespace.outro("Nothing to do");
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
p__namespace.note(
|
|
919
|
-
toMigrate.map((c) => `${pathe.relative(cwd, c.path)} \u2192 ${pathe.basename(c.path).replace(`.${fromRenderer}.vue`, `.${toRenderer}.vue`)}`).join("\n"),
|
|
920
|
-
`Renaming ${toMigrate.length} component(s): ${fromRenderer} \u2192 ${toRenderer}`
|
|
921
|
-
);
|
|
922
|
-
if (dryRun) {
|
|
923
|
-
p__namespace.log.warn("[Dry run mode, no changes will be made]");
|
|
924
|
-
p__namespace.outro("Dry run complete");
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
if (!skipConfirm) {
|
|
928
|
-
const confirmed = await p__namespace.confirm({
|
|
929
|
-
message: `Rename ${toMigrate.length} component(s) from ${fromRenderer} to ${toRenderer}?`,
|
|
930
|
-
initialValue: true
|
|
931
|
-
});
|
|
932
|
-
if (p__namespace.isCancel(confirmed) || !confirmed) {
|
|
933
|
-
p__namespace.cancel("Cancelled");
|
|
934
|
-
process.exit(0);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
const conflicts = [];
|
|
938
|
-
for (const component of toMigrate) {
|
|
939
|
-
const newPath = component.path.replace(`.${fromRenderer}.vue`, `.${toRenderer}.vue`);
|
|
940
|
-
if (fs.existsSync(newPath))
|
|
941
|
-
conflicts.push(`${pathe.basename(component.path)} \u2192 ${pathe.basename(newPath)} (already exists)`);
|
|
942
|
-
}
|
|
943
|
-
if (conflicts.length > 0) {
|
|
944
|
-
p__namespace.log.error("Conflicts detected, aborting:");
|
|
945
|
-
for (const c of conflicts) p__namespace.log.error(` ${c}`);
|
|
946
|
-
process.exit(1);
|
|
947
|
-
}
|
|
948
|
-
for (const component of toMigrate) {
|
|
949
|
-
const newPath = component.path.replace(`.${fromRenderer}.vue`, `.${toRenderer}.vue`);
|
|
950
|
-
fs.renameSync(component.path, newPath);
|
|
951
|
-
p__namespace.log.success(`${pathe.basename(component.path)} \u2192 ${pathe.basename(newPath)}`);
|
|
952
|
-
}
|
|
953
|
-
const hasPkg = fs.existsSync(pathe.join(cwd, "package.json"));
|
|
954
|
-
if (hasPkg) {
|
|
955
|
-
const noFromComponentsRemaining = allComponents.filter((c) => c.renderer === fromRenderer).length === toMigrate.length;
|
|
956
|
-
const detectedPreset = await detectDeploymentTarget(cwd);
|
|
957
|
-
const isEdge = detectedPreset ? EDGE_PRESETS.includes(detectedPreset) : false;
|
|
958
|
-
if (detectedPreset)
|
|
959
|
-
p__namespace.log.info(`Detected deployment target: ${detectedPreset}`);
|
|
960
|
-
const manageDeps = skipConfirm || await p__namespace.confirm({
|
|
961
|
-
message: `Manage dependencies? (install ${toRenderer}, ${noFromComponentsRemaining ? `remove ${fromRenderer}` : "keep existing"})`,
|
|
962
|
-
initialValue: true
|
|
963
|
-
}).then((v) => !p__namespace.isCancel(v) && v);
|
|
964
|
-
if (manageDeps) {
|
|
965
|
-
if (noFromComponentsRemaining) {
|
|
966
|
-
const oldDeps = [.../* @__PURE__ */ new Set([...getRendererDeps(fromRenderer, false), ...getRendererDeps(fromRenderer, true)])];
|
|
967
|
-
const newDeps = /* @__PURE__ */ new Set([...getRendererDeps(toRenderer, false), ...getRendererDeps(toRenderer, true)]);
|
|
968
|
-
const toRemove = oldDeps.filter((d) => !newDeps.has(d));
|
|
969
|
-
if (toRemove.length > 0) {
|
|
970
|
-
const pm = await nypm.detectPackageManager(cwd);
|
|
971
|
-
const spinner2 = p__namespace.spinner();
|
|
972
|
-
spinner2.start(`Removing ${fromRenderer} dependencies...`);
|
|
973
|
-
for (const dep of toRemove) {
|
|
974
|
-
await nypm.removeDependency(dep, { cwd }).catch(() => p__namespace.log.warn(`Could not remove ${dep}, remove manually: ${pm?.name || "npm"} remove ${dep}`));
|
|
975
|
-
}
|
|
976
|
-
spinner2.stop(`Removed ${fromRenderer} dependencies`);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
await installRendererDeps([toRenderer], isEdge);
|
|
980
|
-
}
|
|
981
|
-
const spinner = p__namespace.spinner();
|
|
982
|
-
spinner.start("Running nuxt prepare to update types...");
|
|
983
|
-
const { exec } = await import('tinyexec');
|
|
984
|
-
try {
|
|
985
|
-
await exec("npx", ["nuxi", "prepare"], { nodeOptions: { cwd } });
|
|
986
|
-
spinner.stop("Types updated");
|
|
987
|
-
} catch {
|
|
988
|
-
spinner.stop("Failed to run nuxt prepare");
|
|
989
|
-
p__namespace.log.warn("Run manually: npx nuxt prepare");
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
p__namespace.outro("Renderer switch complete!");
|
|
993
|
-
}
|
|
994
|
-
function readPackageJson(cwd) {
|
|
995
|
-
const pkgPath = pathe.join(cwd, "package.json");
|
|
996
|
-
if (!fs.existsSync(pkgPath))
|
|
997
|
-
return null;
|
|
998
|
-
try {
|
|
999
|
-
return JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
1000
|
-
} catch {
|
|
1001
|
-
return null;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
function inferRenderer(cwd) {
|
|
1005
|
-
const dirs = [cwd];
|
|
1006
|
-
if (fs.existsSync(pathe.join(cwd, "app")))
|
|
1007
|
-
dirs.push(pathe.join(cwd, "app"));
|
|
1008
|
-
for (const dir of dirs) {
|
|
1009
|
-
const components = findOgImageComponents(dir);
|
|
1010
|
-
for (const filepath of components) {
|
|
1011
|
-
const match = pathe.basename(filepath).match(RE_RENDERER_SUFFIX);
|
|
1012
|
-
if (match)
|
|
1013
|
-
return match[1];
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
const pkg = readPackageJson(cwd);
|
|
1017
|
-
if (pkg) {
|
|
1018
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1019
|
-
if (allDeps["@takumi-rs/core"] || allDeps["@takumi-rs/wasm"])
|
|
1020
|
-
return "takumi";
|
|
1021
|
-
if (allDeps.satori)
|
|
1022
|
-
return "satori";
|
|
1023
|
-
}
|
|
1024
|
-
return null;
|
|
1025
|
-
}
|
|
1026
|
-
function detectCssFramework(cwd) {
|
|
1027
|
-
const pkg = readPackageJson(cwd);
|
|
1028
|
-
if (!pkg)
|
|
1029
|
-
return "none";
|
|
1030
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1031
|
-
if (allDeps["@unocss/nuxt"] || allDeps.unocss)
|
|
1032
|
-
return "unocss";
|
|
1033
|
-
if (allDeps["@nuxtjs/tailwindcss"] || allDeps.tailwindcss || allDeps["@nuxt/ui"])
|
|
1034
|
-
return "tailwind";
|
|
1035
|
-
return "none";
|
|
1036
|
-
}
|
|
1037
|
-
const SCRIPT_BLOCK = `<script setup lang="ts">
|
|
1038
|
-
const { title = 'My Page', description = '' } = defineProps<{
|
|
1039
|
-
title?: string
|
|
1040
|
-
description?: string
|
|
1041
|
-
}>()
|
|
1042
|
-
<\/script>`;
|
|
1043
|
-
function getStarterTemplate(renderer, css) {
|
|
1044
|
-
if (renderer === "satori") {
|
|
1045
|
-
return `${SCRIPT_BLOCK}
|
|
1046
|
-
|
|
1047
|
-
<template>
|
|
1048
|
-
<div :style="{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%', padding: '60px', backgroundColor: 'white', color: '#171717' }">
|
|
1049
|
-
<h1 :style="{ fontSize: '72px', fontWeight: 'bold', margin: 0, lineHeight: 1.1, textAlign: 'center' }">
|
|
1050
|
-
{{ title }}
|
|
1051
|
-
</h1>
|
|
1052
|
-
<p v-if="description" :style="{ fontSize: '32px', opacity: 0.6, marginTop: '16px', textAlign: 'center', maxWidth: '900px' }">
|
|
1053
|
-
{{ description }}
|
|
1054
|
-
</p>
|
|
1055
|
-
</div>
|
|
1056
|
-
</template>
|
|
1057
|
-
`;
|
|
1058
|
-
}
|
|
1059
|
-
if (css === "tailwind" || css === "unocss") {
|
|
1060
|
-
return `${SCRIPT_BLOCK}
|
|
1061
|
-
|
|
1062
|
-
<template>
|
|
1063
|
-
<div class="w-full h-full flex flex-col justify-center items-center p-[60px] bg-white text-neutral-900 dark:bg-neutral-900 dark:text-white">
|
|
1064
|
-
<h1 class="text-[72px] font-bold m-0 leading-tight text-center" style="text-wrap: balance;">
|
|
1065
|
-
{{ title }}
|
|
1066
|
-
</h1>
|
|
1067
|
-
<p v-if="description" class="text-[32px] opacity-60 mt-4 text-center max-w-[900px]">
|
|
1068
|
-
{{ description }}
|
|
1069
|
-
</p>
|
|
1070
|
-
</div>
|
|
1071
|
-
</template>
|
|
1072
|
-
`;
|
|
1073
|
-
}
|
|
1074
|
-
return `${SCRIPT_BLOCK}
|
|
1075
|
-
|
|
1076
|
-
<template>
|
|
1077
|
-
<div class="container">
|
|
1078
|
-
<h1>{{ title }}</h1>
|
|
1079
|
-
<p v-if="description">
|
|
1080
|
-
{{ description }}
|
|
1081
|
-
</p>
|
|
1082
|
-
</div>
|
|
1083
|
-
</template>
|
|
1084
|
-
|
|
1085
|
-
<style scoped>
|
|
1086
|
-
.container {
|
|
1087
|
-
width: 100%;
|
|
1088
|
-
height: 100%;
|
|
1089
|
-
display: flex;
|
|
1090
|
-
flex-direction: column;
|
|
1091
|
-
justify-content: center;
|
|
1092
|
-
align-items: center;
|
|
1093
|
-
padding: 60px;
|
|
1094
|
-
background: white;
|
|
1095
|
-
color: #171717;
|
|
1096
|
-
}
|
|
1097
|
-
h1 {
|
|
1098
|
-
font-size: 72px;
|
|
1099
|
-
font-weight: bold;
|
|
1100
|
-
margin: 0;
|
|
1101
|
-
line-height: 1.1;
|
|
1102
|
-
text-align: center;
|
|
1103
|
-
text-wrap: balance;
|
|
1104
|
-
}
|
|
1105
|
-
p {
|
|
1106
|
-
font-size: 32px;
|
|
1107
|
-
opacity: 0.6;
|
|
1108
|
-
margin-top: 16px;
|
|
1109
|
-
text-align: center;
|
|
1110
|
-
max-width: 900px;
|
|
1111
|
-
}
|
|
1112
|
-
</style>
|
|
1113
|
-
`;
|
|
1114
|
-
}
|
|
1115
|
-
function insertDefineOgImage(filePath, componentName) {
|
|
1116
|
-
if (!fs.existsSync(filePath))
|
|
1117
|
-
return false;
|
|
1118
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
1119
|
-
if (content.includes("defineOgImage"))
|
|
1120
|
-
return false;
|
|
1121
|
-
const call = `defineOgImage('${componentName}', { title: 'Hello' })`;
|
|
1122
|
-
const scriptSetupMatch = content.match(RE_SCRIPT_SETUP);
|
|
1123
|
-
if (scriptSetupMatch) {
|
|
1124
|
-
const insertPos = scriptSetupMatch.index + scriptSetupMatch[0].length;
|
|
1125
|
-
const updated2 = `${content.slice(0, insertPos)}${call}
|
|
1126
|
-
${content.slice(insertPos)}`;
|
|
1127
|
-
fs.writeFileSync(filePath, updated2, "utf-8");
|
|
1128
|
-
return true;
|
|
1129
|
-
}
|
|
1130
|
-
const updated = `<script setup lang="ts">
|
|
1131
|
-
${call}
|
|
1132
|
-
<\/script>
|
|
1133
|
-
|
|
1134
|
-
${content}`;
|
|
1135
|
-
fs.writeFileSync(filePath, updated, "utf-8");
|
|
1136
|
-
return true;
|
|
1137
|
-
}
|
|
1138
|
-
async function runCreate(name, args2) {
|
|
1139
|
-
const cwd = process.cwd();
|
|
1140
|
-
const rendererIdx = args2.indexOf("--renderer");
|
|
1141
|
-
const cliRenderer = rendererIdx !== -1 ? args2[rendererIdx + 1] : null;
|
|
1142
|
-
if (cliRenderer && !RENDERERS.some((r) => r.name === cliRenderer)) {
|
|
1143
|
-
p__namespace.log.error(`Invalid renderer: ${cliRenderer}. Must be ${RENDERERS.map((r) => r.name).join(", ")}`);
|
|
1144
|
-
process.exit(1);
|
|
1145
|
-
}
|
|
1146
|
-
const pathIdx = args2.indexOf("--path");
|
|
1147
|
-
const cliPath = pathIdx !== -1 ? args2[pathIdx + 1] : null;
|
|
1148
|
-
p__namespace.intro("Create OG Image Component");
|
|
1149
|
-
const validateName = (v) => {
|
|
1150
|
-
const trimmed = v?.trim();
|
|
1151
|
-
if (!trimmed)
|
|
1152
|
-
return "Name is required";
|
|
1153
|
-
if (RE_PATH_SEPARATOR.test(trimmed))
|
|
1154
|
-
return 'Name must not contain path separators or ".."';
|
|
1155
|
-
if (!RE_PASCAL_CASE_STRICT.test(trimmed))
|
|
1156
|
-
return "Must be PascalCase (e.g. MyOgImage)";
|
|
1157
|
-
};
|
|
1158
|
-
let componentName = name;
|
|
1159
|
-
if (!componentName) {
|
|
1160
|
-
const input = await p__namespace.text({
|
|
1161
|
-
message: "Component name:",
|
|
1162
|
-
placeholder: "MyOgImage",
|
|
1163
|
-
validate: validateName
|
|
1164
|
-
});
|
|
1165
|
-
if (p__namespace.isCancel(input)) {
|
|
1166
|
-
p__namespace.cancel("Cancelled");
|
|
1167
|
-
process.exit(0);
|
|
1168
|
-
}
|
|
1169
|
-
componentName = input.trim();
|
|
1170
|
-
} else {
|
|
1171
|
-
componentName = componentName.trim();
|
|
1172
|
-
if (!RE_PASCAL_CASE_START.test(componentName))
|
|
1173
|
-
componentName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
|
|
1174
|
-
const err = validateName(componentName);
|
|
1175
|
-
if (err) {
|
|
1176
|
-
p__namespace.log.error(err);
|
|
1177
|
-
process.exit(1);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
let renderer;
|
|
1181
|
-
if (cliRenderer) {
|
|
1182
|
-
renderer = cliRenderer;
|
|
1183
|
-
} else {
|
|
1184
|
-
const inferred = inferRenderer(cwd);
|
|
1185
|
-
if (inferred) {
|
|
1186
|
-
renderer = inferred;
|
|
1187
|
-
p__namespace.log.info(`Inferred renderer: ${renderer}`);
|
|
1188
|
-
} else {
|
|
1189
|
-
const selection = await p__namespace.select({
|
|
1190
|
-
message: "Which renderer?",
|
|
1191
|
-
options: RENDERERS.map((r) => ({
|
|
1192
|
-
value: r.name,
|
|
1193
|
-
label: r.label,
|
|
1194
|
-
hint: r.description
|
|
1195
|
-
})),
|
|
1196
|
-
initialValue: "takumi"
|
|
1197
|
-
});
|
|
1198
|
-
if (p__namespace.isCancel(selection)) {
|
|
1199
|
-
p__namespace.cancel("Cancelled");
|
|
1200
|
-
process.exit(0);
|
|
1201
|
-
}
|
|
1202
|
-
renderer = selection;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
const css = detectCssFramework(cwd);
|
|
1206
|
-
if (css !== "none")
|
|
1207
|
-
p__namespace.log.info(`Detected CSS framework: ${css}`);
|
|
1208
|
-
const baseDir = fs.existsSync(pathe.join(cwd, "app")) ? pathe.join(cwd, "app") : cwd;
|
|
1209
|
-
const outputDir = cliPath ? pathe.resolve(cwd, cliPath) : pathe.resolve(baseDir, "components", "OgImage");
|
|
1210
|
-
if (!outputDir.startsWith(cwd)) {
|
|
1211
|
-
p__namespace.log.error("Output path must be within the project directory");
|
|
1212
|
-
process.exit(1);
|
|
1213
|
-
}
|
|
1214
|
-
if (!fs.existsSync(outputDir))
|
|
1215
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1216
|
-
const filename = `${componentName}.${renderer}.vue`;
|
|
1217
|
-
const outputPath = pathe.join(outputDir, filename);
|
|
1218
|
-
if (fs.existsSync(outputPath)) {
|
|
1219
|
-
p__namespace.log.error(`File already exists: ${pathe.relative(cwd, outputPath)}`);
|
|
1220
|
-
process.exit(1);
|
|
1221
|
-
}
|
|
1222
|
-
const template = getStarterTemplate(renderer, css);
|
|
1223
|
-
fs.writeFileSync(outputPath, template, "utf-8");
|
|
1224
|
-
p__namespace.log.success(`Created ${pathe.relative(cwd, outputPath)}`);
|
|
1225
|
-
const pagesDir = fs.existsSync(pathe.join(baseDir, "pages")) ? pathe.join(baseDir, "pages") : null;
|
|
1226
|
-
if (pagesDir) {
|
|
1227
|
-
const pageInput = await p__namespace.text({
|
|
1228
|
-
message: "Add defineOgImage to a page? (relative path from pages/, leave empty to skip)",
|
|
1229
|
-
placeholder: "index.vue"
|
|
1230
|
-
});
|
|
1231
|
-
if (!p__namespace.isCancel(pageInput) && pageInput && pageInput.trim()) {
|
|
1232
|
-
let pagePath = pageInput.trim();
|
|
1233
|
-
if (!pagePath.endsWith(".vue"))
|
|
1234
|
-
pagePath += ".vue";
|
|
1235
|
-
const fullPagePath = pathe.resolve(pagesDir, pagePath);
|
|
1236
|
-
if (!fullPagePath.startsWith(pagesDir)) {
|
|
1237
|
-
p__namespace.log.warn("Page path must be within the pages directory");
|
|
1238
|
-
} else if (insertDefineOgImage(fullPagePath, componentName)) {
|
|
1239
|
-
p__namespace.log.success(`Added defineOgImage('${componentName}') to ${pathe.relative(cwd, fullPagePath)}`);
|
|
1240
|
-
} else if (!fs.existsSync(fullPagePath)) {
|
|
1241
|
-
p__namespace.log.warn(`Page not found: ${pathe.relative(cwd, fullPagePath)}`);
|
|
1242
|
-
} else {
|
|
1243
|
-
p__namespace.log.info("Page already has defineOgImage, skipped");
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
p__namespace.log.info(`Usage: defineOgImage('${componentName}', { title: 'Hello' })`);
|
|
1248
|
-
p__namespace.outro("");
|
|
1249
|
-
}
|
|
1250
|
-
async function runEnable(renderer, args2) {
|
|
1251
|
-
const def = RENDERERS.find((r) => r.name === renderer);
|
|
1252
|
-
if (!def) {
|
|
1253
|
-
p__namespace.log.error(`Unknown renderer: ${renderer}`);
|
|
1254
|
-
p__namespace.log.info(`Available: ${RENDERERS.map((r) => r.name).join(", ")}`);
|
|
1255
|
-
process.exit(1);
|
|
1256
|
-
}
|
|
1257
|
-
p__namespace.intro(`Enable ${def.label} renderer`);
|
|
1258
|
-
const cwd = process.cwd();
|
|
1259
|
-
let isEdge = args2.includes("--edge");
|
|
1260
|
-
if (!isEdge) {
|
|
1261
|
-
const detectedPreset = await detectDeploymentTarget(cwd);
|
|
1262
|
-
if (detectedPreset) {
|
|
1263
|
-
isEdge = EDGE_PRESETS.includes(detectedPreset);
|
|
1264
|
-
if (isEdge)
|
|
1265
|
-
p__namespace.log.info(`Detected edge deployment target: ${detectedPreset}`);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
if (isEdge && renderer === "browser") {
|
|
1269
|
-
p__namespace.log.error("Browser renderer is not supported on edge runtimes");
|
|
1270
|
-
process.exit(1);
|
|
1271
|
-
}
|
|
1272
|
-
await installRendererDeps([renderer], isEdge);
|
|
1273
|
-
p__namespace.outro("Done");
|
|
1274
|
-
}
|
|
1275
|
-
function generateSecret() {
|
|
1276
|
-
const secret = node_crypto.randomBytes(32).toString("hex");
|
|
1277
|
-
p__namespace.intro("nuxt-og-image generate-secret");
|
|
1278
|
-
p__namespace.log.step(`Secret: ${secret}`);
|
|
1279
|
-
p__namespace.log.message("");
|
|
1280
|
-
p__namespace.log.message("Set the environment variable:");
|
|
1281
|
-
p__namespace.log.message(` NUXT_OG_IMAGE_SECRET=${secret}`);
|
|
1282
|
-
p__namespace.log.message("");
|
|
1283
|
-
p__namespace.log.message("The secret is automatically picked up via runtime config.");
|
|
1284
|
-
p__namespace.outro("");
|
|
1285
|
-
}
|
|
1286
|
-
function showHelp() {
|
|
1287
|
-
p__namespace.intro("nuxt-og-image CLI");
|
|
1288
|
-
p__namespace.note([
|
|
1289
|
-
"create [name] Scaffold a new OG image component",
|
|
1290
|
-
" Options: --renderer <renderer>, --path <dir>",
|
|
1291
|
-
"list List available community templates",
|
|
1292
|
-
"eject <name> Eject a community template to your project",
|
|
1293
|
-
"switch Switch components between renderers",
|
|
1294
|
-
" Options: --from <renderer>, --to <renderer>, --dry-run, --yes",
|
|
1295
|
-
"enable <renderer> Install dependencies for a renderer (satori, browser, takumi)",
|
|
1296
|
-
" Options: --edge (install wasm versions for edge runtimes)",
|
|
1297
|
-
"migrate v6 Migrate to v6 (component suffixes + new API)",
|
|
1298
|
-
" Options: --dry-run, --yes, --renderer <renderer>",
|
|
1299
|
-
"generate-secret Generate a signing secret for URL tamper protection"
|
|
1300
|
-
].join("\n"), "Commands");
|
|
1301
|
-
p__namespace.outro("");
|
|
1302
|
-
}
|
|
1303
|
-
const args = process.argv.slice(2);
|
|
1304
|
-
const command = args[0];
|
|
1305
|
-
const hasHelp = args.includes("--help") || args.includes("-h");
|
|
1306
|
-
if (hasHelp || !command) {
|
|
1307
|
-
showHelp();
|
|
1308
|
-
} else if (command === "create") {
|
|
1309
|
-
runCreate(args[1], args.slice(1));
|
|
1310
|
-
} else if (command === "eject") {
|
|
1311
|
-
const templateName = args[1];
|
|
1312
|
-
if (!templateName) {
|
|
1313
|
-
p__namespace.log.error("Please specify a template name.");
|
|
1314
|
-
listTemplates();
|
|
1315
|
-
process.exit(1);
|
|
1316
|
-
}
|
|
1317
|
-
const cwd = process.cwd();
|
|
1318
|
-
const targetDir = fs.existsSync(pathe.join(cwd, "app")) ? pathe.join(cwd, "app") : cwd;
|
|
1319
|
-
ejectTemplate(templateName, targetDir);
|
|
1320
|
-
} else if (command === "list") {
|
|
1321
|
-
listTemplates();
|
|
1322
|
-
} else if (command === "migrate") {
|
|
1323
|
-
const version = args[1];
|
|
1324
|
-
if (version !== "v6") {
|
|
1325
|
-
p__namespace.log.error("Usage: npx nuxt-og-image migrate v6 [--dry-run] [--yes] [--renderer <satori|browser|takumi>]");
|
|
1326
|
-
process.exit(1);
|
|
1327
|
-
}
|
|
1328
|
-
runMigrate(args);
|
|
1329
|
-
} else if (command === "switch") {
|
|
1330
|
-
runSwitch(args.slice(1));
|
|
1331
|
-
} else if (command === "generate-secret") {
|
|
1332
|
-
generateSecret();
|
|
1333
|
-
} else if (command === "enable") {
|
|
1334
|
-
const renderer = args[1];
|
|
1335
|
-
if (!renderer) {
|
|
1336
|
-
p__namespace.log.error("Usage: npx nuxt-og-image enable <renderer> [--edge]");
|
|
1337
|
-
p__namespace.log.info(`Available: ${RENDERERS.map((r) => r.name).join(", ")}`);
|
|
1338
|
-
process.exit(1);
|
|
1339
|
-
}
|
|
1340
|
-
runEnable(renderer, args);
|
|
1341
|
-
} else {
|
|
1342
|
-
p__namespace.log.error(`Unknown command: ${command}`);
|
|
1343
|
-
showHelp();
|
|
1344
|
-
process.exit(1);
|
|
1345
|
-
}
|