nuxt-og-image 6.9.0 → 6.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/chunks/tw4.mjs +2 -3
- package/dist/chunks/uno.mjs +3 -4
- package/dist/cli.mjs +98 -15
- package/dist/module.d.mts +1 -1
- package/dist/module.d.ts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -3
- package/dist/runtime/app/composables/defineOgImageScreenshot.js +4 -1
- package/dist/runtime/app/composables/zero-runtime.d.ts +9 -0
- package/dist/runtime/app/composables/zero-runtime.js +6 -0
- package/dist/runtime/app/utils.js +3 -0
- package/dist/runtime/server/og-image/context.js +0 -3
- package/dist/runtime/server/og-image/takumi/nodes.d.ts +1 -1
- package/dist/runtime/server/og-image/takumi/nodes.js +20 -45
- package/dist/runtime/server/plugins/auto-eject.d.ts +2 -0
- package/dist/runtime/server/plugins/auto-eject.js +36 -0
- package/dist/runtime/server/plugins/prerender.js +4 -3
- package/dist/runtime/server/util/auto-eject.d.ts +1 -3
- package/dist/runtime/server/util/auto-eject.js +1 -4
- package/dist/runtime/server/util/options.js +5 -15
- package/dist/runtime/shared/urlEncoding.d.ts +9 -3
- package/dist/runtime/shared/urlEncoding.js +27 -10
- package/dist/runtime/types.d.ts +2 -1
- package/dist/shared/{nuxt-og-image.CTD0ZPNd.mjs → nuxt-og-image.DWcVJOVy.mjs} +239 -117
- package/package.json +19 -18
- package/skills/nuxt-og-image/SKILL.md +134 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate a deterministic hash from options object
|
|
3
|
-
* Excludes _path so images with same options can be cached across pages
|
|
3
|
+
* Excludes _path so images with same options can be cached across pages,
|
|
4
|
+
* except for PageScreenshot, which renders the page itself.
|
|
4
5
|
* Optionally includes componentHash and version for cache busting
|
|
6
|
+
*
|
|
7
|
+
* Uses the first 64 bits of a SHA-256 digest over a canonical (key-sorted)
|
|
8
|
+
* serialization. The previous 32-bit rolling hash collided on large sites,
|
|
9
|
+
* silently overwriting other pages' prerendered images.
|
|
5
10
|
*/
|
|
6
11
|
export declare function hashOgImageOptions(options: Record<string, any>, componentHash?: string, version?: string): string;
|
|
7
12
|
/**
|
|
@@ -25,7 +30,8 @@ export interface BuildOgImageUrlResult {
|
|
|
25
30
|
/**
|
|
26
31
|
* Build full OG image URL
|
|
27
32
|
*
|
|
28
|
-
*
|
|
33
|
+
* Static URLs fall back to hash mode when encoded params exceed MAX_PATH_LENGTH
|
|
34
|
+
* or hold a character outside [A-Za-z0-9_.~-]:
|
|
29
35
|
* - Returns short path: /_og/s/o_<hash>.png
|
|
30
36
|
* - Returns hash in result for cache storage
|
|
31
37
|
*
|
|
@@ -34,7 +40,7 @@ export interface BuildOgImageUrlResult {
|
|
|
34
40
|
* @param isStatic - Whether this is a static/prerendered image
|
|
35
41
|
* @param defaults - Optional defaults to skip from URL (keeps URLs shorter)
|
|
36
42
|
* @example buildOgImageUrl({ width: 1200, props: { title: 'Hello' } }, 'png', true)
|
|
37
|
-
* // Returns: { url: "/_og/s/
|
|
43
|
+
* // Returns: { url: "/_og/s/o_<hash>.png", hash: "<hash>" }
|
|
38
44
|
*/
|
|
39
45
|
export declare function buildOgImageUrl(options: Record<string, any>, extension?: string, isStatic?: boolean, defaults?: Record<string, any>, secret?: string): BuildOgImageUrlResult;
|
|
40
46
|
/**
|
|
@@ -19,6 +19,7 @@ const RE_COMMA_PARAM_SEPARATOR = /,(?=\w+_)/;
|
|
|
19
19
|
const RE_SIGNATURE_SUFFIX = /,s_[^,]+$/;
|
|
20
20
|
const RE_NON_ASCII = /[^\u0000-\u007F]/;
|
|
21
21
|
const RE_WINDOWS_RESERVED_FILENAME_CHARACTERS = /[<>:"/\\|?*\u0000-\u001F]/;
|
|
22
|
+
const RE_NOT_UNRESERVED = /[^\w.~-]/;
|
|
22
23
|
const PARAM_ALIASES = {
|
|
23
24
|
w: "width",
|
|
24
25
|
h: "height",
|
|
@@ -85,19 +86,35 @@ function b64Decode(str) {
|
|
|
85
86
|
}
|
|
86
87
|
return Buffer.from(padded, "base64").toString("utf8");
|
|
87
88
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
function canonicalJson(value) {
|
|
90
|
+
if (value === null || typeof value !== "object")
|
|
91
|
+
return JSON.stringify(value) ?? "null";
|
|
92
|
+
if (Array.isArray(value))
|
|
93
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
94
|
+
const record = value;
|
|
95
|
+
const entries = Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`);
|
|
96
|
+
return `{${entries.join(",")}}`;
|
|
97
|
+
}
|
|
98
|
+
const B64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
99
|
+
function base64UrlToHex(input) {
|
|
100
|
+
let hex = "";
|
|
101
|
+
let buffer = 0;
|
|
102
|
+
let bits = 0;
|
|
103
|
+
for (const char of input) {
|
|
104
|
+
buffer = buffer << 6 | B64URL_ALPHABET.indexOf(char);
|
|
105
|
+
bits += 6;
|
|
106
|
+
if (bits >= 8) {
|
|
107
|
+
bits -= 8;
|
|
108
|
+
hex += (buffer >> bits & 255).toString(16).padStart(2, "0");
|
|
109
|
+
}
|
|
94
110
|
}
|
|
95
|
-
return
|
|
111
|
+
return hex;
|
|
96
112
|
}
|
|
97
113
|
export function hashOgImageOptions(options, componentHash, version) {
|
|
98
|
-
const { _path, _hash, ...
|
|
114
|
+
const { _path, _hash, ...rest } = options;
|
|
115
|
+
const hashableOptions = rest.component === "PageScreenshot" ? { ...rest, _path } : rest;
|
|
99
116
|
const hashInput = componentHash || version ? [hashableOptions, componentHash || "", version || ""] : hashableOptions;
|
|
100
|
-
return
|
|
117
|
+
return base64UrlToHex(digest(canonicalJson(hashInput))).slice(0, 16);
|
|
101
118
|
}
|
|
102
119
|
export function encodeOgImageParams(options, defaults) {
|
|
103
120
|
const parts = [];
|
|
@@ -233,7 +250,7 @@ export function decodeOgImageParams(encoded) {
|
|
|
233
250
|
export function buildOgImageUrl(options, extension = "png", isStatic = false, defaults, secret) {
|
|
234
251
|
const encoded = encodeOgImageParams(options, defaults);
|
|
235
252
|
const prefix = isStatic ? "/_og/s" : "/_og/d";
|
|
236
|
-
if (isStatic && (encoded.length > MAX_PATH_LENGTH ||
|
|
253
|
+
if (isStatic && (encoded.length > MAX_PATH_LENGTH || RE_NOT_UNRESERVED.test(encoded))) {
|
|
237
254
|
const hash = hashOgImageOptions(options);
|
|
238
255
|
return {
|
|
239
256
|
url: `${prefix}/o_${hash}.${extension}`,
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface OgImageIconsData {
|
|
|
31
31
|
height: number;
|
|
32
32
|
}
|
|
33
33
|
export interface OgImageRuntimeConfig {
|
|
34
|
+
browserEnabled: boolean;
|
|
34
35
|
version: string;
|
|
35
36
|
satoriOptions: SatoriOptions;
|
|
36
37
|
resvgOptions: ResvgRenderOptions;
|
|
@@ -77,7 +78,7 @@ export interface OgImageRuntimeConfig {
|
|
|
77
78
|
}
|
|
78
79
|
export type RendererType = 'satori' | 'browser' | 'takumi';
|
|
79
80
|
export type BrowserProvider = 'playwright' | 'chrome-launcher' | 'on-demand' | 'cloudflare';
|
|
80
|
-
export type BrowserConfig =
|
|
81
|
+
export type BrowserConfig = boolean | {
|
|
81
82
|
provider: BrowserProvider;
|
|
82
83
|
/** Required when provider is 'cloudflare' - the wrangler binding name */
|
|
83
84
|
binding?: string;
|
|
@@ -3,26 +3,25 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, rmSync } from 'node:fs';
|
|
4
4
|
import { readFile, mkdir, writeFile, readdir } from 'node:fs/promises';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
-
import { resolvePath, useNuxt, getNuxtVersion, addTemplate, updateTemplates, directoryToURL, tryResolveModule, addTypeTemplate, defineNuxtModule, createResolver, addImports, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, addVitePlugin,
|
|
6
|
+
import { resolvePath, useNuxt, getNuxtVersion, addTemplate, updateTemplates, directoryToURL, tryResolveModule, addTypeTemplate, defineNuxtModule, createResolver, addImports, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, addVitePlugin, addServerHandler, addServerImports, addPlugin, addComponentsDir, addServerPlugin } from '@nuxt/kit';
|
|
7
7
|
import { defu } from 'defu';
|
|
8
8
|
import { fnv1a64Base36 } from 'fnv1a-64';
|
|
9
|
-
import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
|
|
9
|
+
import { useSiteConfig, installNuxtSiteConfig } from 'nuxt-site-config/kit';
|
|
10
10
|
import { resolveNitroPreset, renderNitroTypeAugmentations, setupNitroRuntimeCompatibility, getNuxtModuleOptions, isNuxtGenerate } from 'nuxtseo-shared/kit';
|
|
11
11
|
import { join, basename, dirname, relative, isAbsolute, resolve } from 'pathe';
|
|
12
12
|
import { readPackageJSON } from 'pkg-types';
|
|
13
|
-
import { isCI, isAgent, hasTTY } from 'std-env';
|
|
14
13
|
import MagicString from 'magic-string';
|
|
15
14
|
import { parseAndWalk } from 'oxc-walker';
|
|
16
15
|
import { resolveModulePath } from 'exsolve';
|
|
17
16
|
import { logger } from '../../dist/runtime/logger.js';
|
|
18
17
|
import { Launcher } from 'chrome-launcher';
|
|
18
|
+
import { isCI, isAgent, hasTTY } from 'std-env';
|
|
19
19
|
import { detectPackageManager, addDependency } from 'nypm';
|
|
20
20
|
import { setupDevToolsUI as setupDevToolsUI$1, setupDevToolsRpc } from 'nuxtseo-shared/devtools';
|
|
21
21
|
import { createStorage } from 'unstorage';
|
|
22
22
|
import fsDriver from 'unstorage/drivers/fs-lite';
|
|
23
23
|
import { resolveColorMix, GRADIENT_COLOR_SPACE_RE, splitCssDeclarations } from '../../dist/runtime/server/og-image/utils/css.js';
|
|
24
24
|
import { stripLiteral } from 'strip-literal';
|
|
25
|
-
import { parseURL, parseQuery } from 'ufo';
|
|
26
25
|
import { createUnplugin } from 'unplugin';
|
|
27
26
|
import { parse } from '@vue/compiler-sfc';
|
|
28
27
|
import { ofetch } from 'ofetch';
|
|
@@ -132,6 +131,7 @@ function isSupportedSatoriVersion(version) {
|
|
|
132
131
|
const VALID_RENDERER_SUFFIXES = ["satori", "browser", "takumi"];
|
|
133
132
|
const RE_RENDERER_SUFFIX_CI = /\.?(satori|browser|takumi)$/i;
|
|
134
133
|
const RE_RENDERER_SUFFIX = /(Satori|Browser|Takumi)$/;
|
|
134
|
+
const RE_OG_IMAGE_PREFIX = /^OgImage/;
|
|
135
135
|
const RE_LEGACY_SUFFIX = /-legacy$/;
|
|
136
136
|
const RE_WHITESPACE = /\s+/;
|
|
137
137
|
function getRendererFromFilename(filepath) {
|
|
@@ -167,6 +167,28 @@ function getRegisteredBaseNames(registeredPascalName) {
|
|
|
167
167
|
names.push(stripped);
|
|
168
168
|
return names;
|
|
169
169
|
}
|
|
170
|
+
function matchesComponentName(registeredPascalName, inputName) {
|
|
171
|
+
const baseNames = getRegisteredBaseNames(registeredPascalName);
|
|
172
|
+
const { baseName } = parseComponentName(inputName);
|
|
173
|
+
const strippedBaseName = baseName.replace(RE_OG_IMAGE_PREFIX, "");
|
|
174
|
+
return baseNames.some(
|
|
175
|
+
(cBase) => cBase === baseName || cBase === strippedBaseName || cBase === `OgImage${baseName}` || cBase === `OgImage${strippedBaseName}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
function parseComponentName(name) {
|
|
179
|
+
for (const suffix of VALID_RENDERER_SUFFIXES) {
|
|
180
|
+
if (name.endsWith(`.${suffix}`)) {
|
|
181
|
+
return { baseName: name.slice(0, -(suffix.length + 1)), renderer: suffix };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const suffix of VALID_RENDERER_SUFFIXES) {
|
|
185
|
+
const pascalSuffix = suffix.charAt(0).toUpperCase() + suffix.slice(1);
|
|
186
|
+
if (name.endsWith(pascalSuffix)) {
|
|
187
|
+
return { baseName: name.slice(0, -pascalSuffix.length), renderer: suffix };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { baseName: name, renderer: null };
|
|
191
|
+
}
|
|
170
192
|
|
|
171
193
|
const NodeRuntime = {
|
|
172
194
|
// node-server runtime
|
|
@@ -446,6 +468,12 @@ async function setupBuildHandler(config, resolve, getDetectedRenderers, getCompa
|
|
|
446
468
|
metadata: getCompatibilityMeta()
|
|
447
469
|
});
|
|
448
470
|
nuxt.hooks.hook("nitro:init", async (nitro) => {
|
|
471
|
+
nitro.hooks.hook("prerender:config", (prerenderConfig) => {
|
|
472
|
+
if (prerenderConfig.virtual?.["playwright-core"]) {
|
|
473
|
+
const { "playwright-core": _, ...virtual } = prerenderConfig.virtual;
|
|
474
|
+
prerenderConfig.virtual = virtual;
|
|
475
|
+
}
|
|
476
|
+
});
|
|
449
477
|
await applyNitroPresetCompatibility(nitro.options, {
|
|
450
478
|
compatibility: config.compatibility?.runtime,
|
|
451
479
|
resolve,
|
|
@@ -651,6 +679,25 @@ const PROVIDER_DEPENDENCIES = [
|
|
|
651
679
|
}
|
|
652
680
|
}
|
|
653
681
|
];
|
|
682
|
+
function resolveAutoDetectedProvider(input) {
|
|
683
|
+
if (input.hasUserComponents)
|
|
684
|
+
return { preferred: null, noRenderer: false };
|
|
685
|
+
const preferred = input.installedProviders.find((p) => p === "takumi") ?? input.installedProviders[0] ?? null;
|
|
686
|
+
if (preferred)
|
|
687
|
+
return { preferred, noRenderer: false };
|
|
688
|
+
return { preferred: null, noRenderer: true };
|
|
689
|
+
}
|
|
690
|
+
function getMissingRendererMessage(renderer, installSpecs) {
|
|
691
|
+
return `The ${renderer} renderer is missing dependencies, so templates that use it cannot render. Install them with: npx nypm add ${installSpecs.join(" ")}`;
|
|
692
|
+
}
|
|
693
|
+
function resolveMissingRendererAction(input) {
|
|
694
|
+
if (!input.dev)
|
|
695
|
+
return { _tag: "CheckUsage" };
|
|
696
|
+
if (input.interactive)
|
|
697
|
+
return { _tag: "AskToInstall" };
|
|
698
|
+
return { _tag: "Report", message: getMissingRendererMessage(input.renderer, input.installSpecs) };
|
|
699
|
+
}
|
|
700
|
+
const NO_RENDERER_MESSAGE = `No OG image renderer is installed, so OG images are off. To add one, run: npx nuxt-og-image enable takumi`;
|
|
654
701
|
async function getInstalledProviders() {
|
|
655
702
|
const installed = [];
|
|
656
703
|
for (const provider of PROVIDER_DEPENDENCIES) {
|
|
@@ -765,15 +812,6 @@ function canPromptInteractively(env = {
|
|
|
765
812
|
}) {
|
|
766
813
|
return env.hasTTY && env.hasStdinTTY && !env.isAgent && !env.isCI;
|
|
767
814
|
}
|
|
768
|
-
async function promptForRendererSelection() {
|
|
769
|
-
logger.info("Welcome to Nuxt OG Image! No renderer dependencies detected.");
|
|
770
|
-
const renderer = await logger.prompt("Which renderer would you like to use?", {
|
|
771
|
-
type: "select",
|
|
772
|
-
options: PROVIDER_DEPENDENCIES.map((p) => p.name),
|
|
773
|
-
initial: "takumi"
|
|
774
|
-
});
|
|
775
|
-
return renderer || "takumi";
|
|
776
|
-
}
|
|
777
815
|
async function validateProviderSetup(renderer, compatibility) {
|
|
778
816
|
const issues = [];
|
|
779
817
|
const binding = getRecommendedBinding(renderer, compatibility);
|
|
@@ -2774,6 +2812,11 @@ async function importOptionalPeer(id, resolvedPath, usage) {
|
|
|
2774
2812
|
}
|
|
2775
2813
|
|
|
2776
2814
|
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2815
|
+
function getMissingSiteUrlWarning(input) {
|
|
2816
|
+
if (input.siteUrl || input.prerenderedImageCount === 0)
|
|
2817
|
+
return null;
|
|
2818
|
+
return `Prerendered ${input.prerenderedImageCount} OG image${input.prerenderedImageCount > 1 ? "s" : ""} without a site URL, so each og:image is a relative path such as \`/_og/s/...\`. Social crawlers need an absolute URL. Set \`site.url\` in nuxt.config.ts or the \`NUXT_SITE_URL\` environment variable.`;
|
|
2819
|
+
}
|
|
2777
2820
|
function setupPrerenderHandler(options, resolve, getDetectedRenderers, getCompatibilityMeta = () => ({}), nuxt = useNuxt()) {
|
|
2778
2821
|
nuxt.hooks.hook("nitro:init", async (nitro) => {
|
|
2779
2822
|
nitro.hooks.hook("prerender:config", async (nitroConfig) => {
|
|
@@ -2787,6 +2830,16 @@ function setupPrerenderHandler(options, resolve, getDetectedRenderers, getCompat
|
|
|
2787
2830
|
nitroConfig.prerender.ignore.push("/_og/r/");
|
|
2788
2831
|
}
|
|
2789
2832
|
});
|
|
2833
|
+
let prerenderedImageCount = 0;
|
|
2834
|
+
nitro.hooks.hook("prerender:route", (route) => {
|
|
2835
|
+
if (!route.error && route.route.includes("/_og/s/"))
|
|
2836
|
+
prerenderedImageCount++;
|
|
2837
|
+
});
|
|
2838
|
+
nitro.hooks.hook("prerender:done", () => {
|
|
2839
|
+
const warning = getMissingSiteUrlWarning({ siteUrl: useSiteConfig(nuxt).url, prerenderedImageCount });
|
|
2840
|
+
if (warning)
|
|
2841
|
+
logger.warn(warning);
|
|
2842
|
+
});
|
|
2790
2843
|
const orphanedOgHashes = [];
|
|
2791
2844
|
nitro.hooks.hook("prerender:generate", (route) => {
|
|
2792
2845
|
if (!route.error || route.error.statusCode !== 404)
|
|
@@ -2874,68 +2927,138 @@ function resolveSigningSecret(configSecret, envSecret, generate) {
|
|
|
2874
2927
|
return { secret: generate(), hasExplicit: false, optOut: false, generated: true };
|
|
2875
2928
|
}
|
|
2876
2929
|
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
if (!
|
|
2883
|
-
return
|
|
2884
|
-
|
|
2885
|
-
const
|
|
2886
|
-
|
|
2887
|
-
|
|
2930
|
+
const RE_NAMED_CALL = /\b(defineOgImage(?:Component)?)\s*\(\s*(['"`])([\w.-]+)\2/g;
|
|
2931
|
+
const RE_SCREENSHOT_CALL = /\bdefineOgImageScreenshot\s*\(/g;
|
|
2932
|
+
const RE_ANY_CALL = /\bdefineOgImage(?:Component)?\s*\(/g;
|
|
2933
|
+
const RE_VUE_OR_SCRIPT = /\.(?:vue|[cm]?[jt]sx?)$/;
|
|
2934
|
+
function findOgImageUsageErrors(code, ctx) {
|
|
2935
|
+
if (!code.includes("defineOgImage"))
|
|
2936
|
+
return [];
|
|
2937
|
+
const stripped = stripLiteral(code);
|
|
2938
|
+
const errors = [];
|
|
2939
|
+
for (const match of code.matchAll(RE_NAMED_CALL)) {
|
|
2940
|
+
if (!stripped.startsWith(match[1], match.index))
|
|
2941
|
+
continue;
|
|
2942
|
+
const name = match[3];
|
|
2943
|
+
const { renderer } = parseComponentName(name);
|
|
2944
|
+
const matches = ctx.components.filter(
|
|
2945
|
+
(c) => matchesComponentName(c.pascalName, name) && (!renderer || c.renderer === renderer)
|
|
2946
|
+
);
|
|
2947
|
+
if (matches.length > 0 && matches.every((c) => c.category === "community"))
|
|
2948
|
+
errors.push(`\`${match[1]}('${name}')\` uses a community template that is not ejected. Production builds do not include community templates. Run: npx nuxt-og-image eject ${name}`);
|
|
2949
|
+
}
|
|
2950
|
+
if (!ctx.browserEnabled && RE_SCREENSHOT_CALL.test(stripped))
|
|
2951
|
+
errors.push("`defineOgImageScreenshot()` needs the browser renderer. Set ogImage.browser to true or a browser provider in nuxt.config.ts, or remove the call.");
|
|
2952
|
+
RE_SCREENSHOT_CALL.lastIndex = 0;
|
|
2953
|
+
return errors;
|
|
2954
|
+
}
|
|
2955
|
+
function collectOgImageReferences(code) {
|
|
2956
|
+
const refs = { names: [], hasDynamicName: false, usesDefault: false };
|
|
2957
|
+
if (!code.includes("defineOgImage"))
|
|
2958
|
+
return refs;
|
|
2959
|
+
const stripped = stripLiteral(code);
|
|
2960
|
+
const literalCalls = /* @__PURE__ */ new Set();
|
|
2961
|
+
for (const match of code.matchAll(RE_NAMED_CALL)) {
|
|
2962
|
+
if (!stripped.startsWith(match[1], match.index))
|
|
2963
|
+
continue;
|
|
2964
|
+
literalCalls.add(match.index);
|
|
2965
|
+
refs.names.push(match[3]);
|
|
2888
2966
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2967
|
+
for (const match of stripped.matchAll(RE_ANY_CALL)) {
|
|
2968
|
+
if (!literalCalls.has(match.index))
|
|
2969
|
+
refs.hasDynamicName = true;
|
|
2891
2970
|
}
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2971
|
+
return refs;
|
|
2972
|
+
}
|
|
2973
|
+
function collectRouteRuleReferences(routeRules) {
|
|
2974
|
+
const refs = { names: [], hasDynamicName: false, usesDefault: false };
|
|
2975
|
+
for (const rule of Object.values(routeRules || {})) {
|
|
2976
|
+
const ogImage = rule?.ogImage;
|
|
2977
|
+
if (!ogImage || typeof ogImage !== "object")
|
|
2978
|
+
continue;
|
|
2979
|
+
const component = ogImage.component;
|
|
2980
|
+
if (typeof component === "string")
|
|
2981
|
+
refs.names.push(component);
|
|
2982
|
+
else
|
|
2983
|
+
refs.usesDefault = true;
|
|
2984
|
+
}
|
|
2985
|
+
return refs;
|
|
2986
|
+
}
|
|
2987
|
+
function installHint(missing) {
|
|
2988
|
+
return `npx nypm add ${missing.join(" ")}`;
|
|
2989
|
+
}
|
|
2990
|
+
function checkMissingRendererUsage(input) {
|
|
2991
|
+
const errors = [];
|
|
2992
|
+
const warnings = [];
|
|
2993
|
+
const appComponents = input.components.filter((c) => c.category !== "community");
|
|
2994
|
+
const isMissing = (c) => !!input.missingRenderers[c.renderer]?.length;
|
|
2995
|
+
if (!appComponents.some(isMissing))
|
|
2996
|
+
return { errors, warnings };
|
|
2997
|
+
const used = /* @__PURE__ */ new Set();
|
|
2998
|
+
for (const name of input.references.names) {
|
|
2999
|
+
const { renderer } = parseComponentName(name);
|
|
3000
|
+
const matches = appComponents.filter((c) => matchesComponentName(c.pascalName, name) && (!renderer || c.renderer === renderer));
|
|
3001
|
+
for (const c of matches)
|
|
3002
|
+
used.add(c);
|
|
3003
|
+
const broken = matches.find(isMissing);
|
|
3004
|
+
if (broken && matches.every(isMissing))
|
|
3005
|
+
errors.push(`\`defineOgImage('${name}')\` uses ${broken.path || broken.pascalName}, but the ${broken.renderer} renderer is not installed. Run: ${installHint(input.missingRenderers[broken.renderer])}`);
|
|
3006
|
+
}
|
|
3007
|
+
const fallback = appComponents[0];
|
|
3008
|
+
if (input.references.usesDefault && fallback) {
|
|
3009
|
+
used.add(fallback);
|
|
3010
|
+
if (isMissing(fallback))
|
|
3011
|
+
errors.push(`A route rule renders the default template ${fallback.path || fallback.pascalName}, but the ${fallback.renderer} renderer is not installed. Run: ${installHint(input.missingRenderers[fallback.renderer])}`);
|
|
3012
|
+
}
|
|
3013
|
+
const unused = appComponents.filter((c) => isMissing(c) && !used.has(c));
|
|
3014
|
+
if (unused.length > 0) {
|
|
3015
|
+
const lines = unused.map((c) => ` ${c.path || c.pascalName}: ${installHint(input.missingRenderers[c.renderer])}`);
|
|
3016
|
+
const why = input.references.hasDynamicName ? "The build found a component name built at runtime, so it cannot tell whether they render. If one does, that image returns 500." : "No page or route rule uses them, so the build continues.";
|
|
3017
|
+
warnings.push(`These OG image templates need a renderer that is not installed:
|
|
3018
|
+
${lines.join("\n")}
|
|
3019
|
+
${why}`);
|
|
2895
3020
|
}
|
|
2896
|
-
return
|
|
3021
|
+
return { errors: [...new Set(errors)], warnings };
|
|
2897
3022
|
}
|
|
2898
|
-
const
|
|
2899
|
-
const
|
|
2900
|
-
const
|
|
2901
|
-
const COMPOSABLE_CODE_MARKER = "defineOgImage";
|
|
2902
|
-
const TreeShakeComposablesPlugin = createUnplugin(() => {
|
|
2903
|
-
const composableNames = [
|
|
2904
|
-
"defineOgImage",
|
|
2905
|
-
"defineOgImageComponent",
|
|
2906
|
-
"defineOgImageScreenshot"
|
|
2907
|
-
];
|
|
2908
|
-
const regexp = `(^\\s*)(${composableNames.join("|")})(?=\\((?!\\) \\{))`;
|
|
2909
|
-
const COMPOSABLE_RE = new RegExp(regexp, "m");
|
|
2910
|
-
const COMPOSABLE_RE_GLOBAL = new RegExp(regexp, "gm");
|
|
3023
|
+
const OgImageUsageCheckPlugin = createUnplugin((options) => {
|
|
3024
|
+
const errors = [];
|
|
3025
|
+
const references = { names: [], hasDynamicName: false, usesDefault: false };
|
|
2911
3026
|
return {
|
|
2912
|
-
name: "nuxt-og-image:
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3027
|
+
name: "nuxt-og-image:usage-check",
|
|
3028
|
+
buildStart() {
|
|
3029
|
+
errors.length = 0;
|
|
3030
|
+
references.names.length = 0;
|
|
3031
|
+
references.hasDynamicName = false;
|
|
3032
|
+
references.usesDefault = false;
|
|
3033
|
+
},
|
|
3034
|
+
transform(code, id) {
|
|
3035
|
+
const [path] = id.split("?");
|
|
3036
|
+
if (!path || !RE_VUE_OR_SCRIPT.test(path) || path.includes("/node_modules/") || options.ignoreDirs.some((dir) => path.startsWith(dir)))
|
|
3037
|
+
return;
|
|
3038
|
+
for (const error of findOgImageUsageErrors(code, options.getContext()))
|
|
3039
|
+
errors.push(`${path}: ${error}`);
|
|
3040
|
+
const found = collectOgImageReferences(code);
|
|
3041
|
+
references.names.push(...found.names);
|
|
3042
|
+
references.hasDynamicName ||= found.hasDynamicName;
|
|
3043
|
+
},
|
|
3044
|
+
buildEnd() {
|
|
3045
|
+
const ctx = options.getContext();
|
|
3046
|
+
const routeRules = ctx.routeRuleReferences;
|
|
3047
|
+
const renderers = checkMissingRendererUsage({
|
|
3048
|
+
components: ctx.components,
|
|
3049
|
+
missingRenderers: ctx.missingRenderers || {},
|
|
3050
|
+
references: {
|
|
3051
|
+
names: [...references.names, ...routeRules?.names || []],
|
|
3052
|
+
hasDynamicName: references.hasDynamicName,
|
|
3053
|
+
usesDefault: !!routeRules?.usesDefault
|
|
2933
3054
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3055
|
+
});
|
|
3056
|
+
errors.push(...renderers.errors);
|
|
3057
|
+
for (const warning of renderers.warnings)
|
|
3058
|
+
options.warn(warning);
|
|
3059
|
+
if (errors.length > 0)
|
|
3060
|
+
throw new Error(`[nuxt-og-image] OG images in this build cannot render:
|
|
3061
|
+
${[...new Set(errors)].map((e) => ` ${e}`).join("\n")}`);
|
|
2939
3062
|
}
|
|
2940
3063
|
};
|
|
2941
3064
|
});
|
|
@@ -3536,7 +3659,7 @@ function buildEmojiSvg(emoji, icons, emojiSet) {
|
|
|
3536
3659
|
const body = wrapDefsElements(iconData.body || "");
|
|
3537
3660
|
const width = iconData.width || icons.width || 128;
|
|
3538
3661
|
const height = iconData.height || icons.height || 128;
|
|
3539
|
-
let svg = `<
|
|
3662
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="1em" height="1em">${body}</svg>`;
|
|
3540
3663
|
svg = makeIdsUnique(svg);
|
|
3541
3664
|
return svg;
|
|
3542
3665
|
}
|
|
@@ -3561,7 +3684,7 @@ async function fetchEmojiSvg(emoji, emojiSet) {
|
|
|
3561
3684
|
const viewBox = viewBoxMatch ? viewBoxMatch[1] : "0 0 128 128";
|
|
3562
3685
|
const bodyMatch = svg.match(RE_SVG_BODY);
|
|
3563
3686
|
const body = bodyMatch ? bodyMatch[1] : "";
|
|
3564
|
-
let result = `<
|
|
3687
|
+
let result = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" width="1em" height="1em">${body}</svg>`;
|
|
3565
3688
|
result = makeIdsUnique(result);
|
|
3566
3689
|
emojiFetchCache.set(cacheKey, result);
|
|
3567
3690
|
if (emojiFetchCache.size > 200)
|
|
@@ -4532,21 +4655,8 @@ async function onInstall(nuxt) {
|
|
|
4532
4655
|
}
|
|
4533
4656
|
if (nuxt.options._prepare)
|
|
4534
4657
|
return;
|
|
4535
|
-
if (isCI) {
|
|
4536
|
-
|
|
4537
|
-
if (installedProviders2.length === 0) {
|
|
4538
|
-
throw new Error(
|
|
4539
|
-
`[nuxt-og-image] No OG image provider dependencies found. Install a provider before running in CI:
|
|
4540
|
-
npm add ${TAKUMI_CORE_PACKAGE} # for takumi
|
|
4541
|
-
npm add @resvg/resvg-js satori yoga-wasm-web # for satori
|
|
4542
|
-
npm add playwright-core # for browser
|
|
4543
|
-
See: https://nuxtseo.com/og-image/getting-started`
|
|
4544
|
-
);
|
|
4545
|
-
}
|
|
4546
|
-
return;
|
|
4547
|
-
}
|
|
4548
|
-
if (!canPromptInteractively()) {
|
|
4549
|
-
logger.info("Skipping interactive onboarding (non-interactive environment). Defaulting to the takumi renderer.");
|
|
4658
|
+
if (isCI || !canPromptInteractively()) {
|
|
4659
|
+
logger.info("Skipping interactive onboarding (non-interactive environment).");
|
|
4550
4660
|
return;
|
|
4551
4661
|
}
|
|
4552
4662
|
logger.info("");
|
|
@@ -5443,10 +5553,21 @@ ${familyInfo}`);
|
|
|
5443
5553
|
takumi: false
|
|
5444
5554
|
}
|
|
5445
5555
|
});
|
|
5446
|
-
if (!nuxt.options.dev)
|
|
5447
|
-
addBuildPlugin(TreeShakeComposablesPlugin, { server: true, client: true });
|
|
5556
|
+
if (!nuxt.options.dev)
|
|
5448
5557
|
setRuntimeAlias("#og-image-cache", resolve("./runtime/server/og-image/cache/mock"));
|
|
5449
|
-
|
|
5558
|
+
}
|
|
5559
|
+
const missingRenderers = {};
|
|
5560
|
+
if (!nuxt.options.dev && !nuxt.options._prepare) {
|
|
5561
|
+
addVitePlugin(OgImageUsageCheckPlugin.vite({
|
|
5562
|
+
getContext: () => ({
|
|
5563
|
+
components: ogImageComponentCtx.components,
|
|
5564
|
+
browserEnabled: !!config.browser,
|
|
5565
|
+
missingRenderers,
|
|
5566
|
+
routeRuleReferences: collectRouteRuleReferences(nuxt.options.routeRules)
|
|
5567
|
+
}),
|
|
5568
|
+
warn: (message) => logger.warn(message),
|
|
5569
|
+
ignoreDirs: [resolve("./runtime"), nuxt.options.buildDir]
|
|
5570
|
+
}), { server: false });
|
|
5450
5571
|
}
|
|
5451
5572
|
const basePath = config.zeroRuntime ? "./runtime/server/routes/__zero-runtime" : "./runtime/server/routes";
|
|
5452
5573
|
let publicDirAbs = nuxt.options.dir.public;
|
|
@@ -5600,7 +5721,8 @@ ${familyInfo}`);
|
|
|
5600
5721
|
}
|
|
5601
5722
|
addImports({
|
|
5602
5723
|
name,
|
|
5603
|
-
|
|
5724
|
+
// zeroRuntime production builds get wrappers that only run during prerender
|
|
5725
|
+
from: config.zeroRuntime && !nuxt.options.dev ? resolve("./runtime/app/composables/zero-runtime") : resolve(`./runtime/app/composables/${name}`)
|
|
5604
5726
|
});
|
|
5605
5727
|
});
|
|
5606
5728
|
addImports({
|
|
@@ -5657,19 +5779,18 @@ ${familyInfo}`);
|
|
|
5657
5779
|
}
|
|
5658
5780
|
}
|
|
5659
5781
|
}
|
|
5782
|
+
if (config.browser)
|
|
5783
|
+
ogImageComponentCtx.detectedRenderers.add("browser");
|
|
5660
5784
|
if (!nuxt.options._prepare && !hasUserComponents) {
|
|
5661
|
-
const
|
|
5662
|
-
|
|
5785
|
+
const { preferred, noRenderer } = resolveAutoDetectedProvider({
|
|
5786
|
+
hasUserComponents,
|
|
5787
|
+
installedProviders: (await getInstalledProviders()).map((p) => p.provider)
|
|
5788
|
+
});
|
|
5663
5789
|
if (preferred) {
|
|
5664
|
-
ogImageComponentCtx.detectedRenderers.add(preferred
|
|
5665
|
-
logger.debug(`Using ${preferred
|
|
5666
|
-
} else if (
|
|
5667
|
-
|
|
5668
|
-
ogImageComponentCtx.detectedRenderers.add(renderer);
|
|
5669
|
-
logger.debug(`Using ${renderer} renderer`);
|
|
5670
|
-
} else {
|
|
5671
|
-
ogImageComponentCtx.detectedRenderers.add("takumi");
|
|
5672
|
-
logger.warn(`No OG image renderer dependency detected. Defaulting to \`takumi\` (non-interactive environment). Install \`${TAKUMI_CORE_PACKAGE}\`, or add a renderer component (e.g. components/OgImage/Default.satori.vue) to choose explicitly.`);
|
|
5790
|
+
ogImageComponentCtx.detectedRenderers.add(preferred);
|
|
5791
|
+
logger.debug(`Using ${preferred} renderer`);
|
|
5792
|
+
} else if (noRenderer) {
|
|
5793
|
+
logger.warn(NO_RENDERER_MESSAGE);
|
|
5673
5794
|
}
|
|
5674
5795
|
}
|
|
5675
5796
|
const availableRenderers = /* @__PURE__ */ new Set();
|
|
@@ -5678,25 +5799,22 @@ ${familyInfo}`);
|
|
|
5678
5799
|
for (const renderer of ogImageComponentCtx.detectedRenderers) {
|
|
5679
5800
|
const binding = getRecommendedBinding(renderer, targetCompatibility);
|
|
5680
5801
|
const missing = await getMissingDependencies(renderer, binding);
|
|
5681
|
-
if (missing.length === 0) {
|
|
5802
|
+
if (missing.length === 0 || renderer === "browser") {
|
|
5682
5803
|
availableRenderers.add(renderer);
|
|
5683
|
-
} else
|
|
5804
|
+
} else {
|
|
5684
5805
|
const installSpecs = await getMissingDependencyInstallSpecs(renderer, binding);
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
throw new Error(`[nuxt-og-image] Failed to install ${renderer} dependencies: ${missing.join(", ")}. Install manually: npx nypm add ${installSpecs.join(" ")}`);
|
|
5806
|
+
const action = resolveMissingRendererAction({ renderer, installSpecs, dev: nuxt.options.dev, interactive: canPromptInteractively() });
|
|
5807
|
+
if (action._tag === "CheckUsage") {
|
|
5808
|
+
missingRenderers[renderer] = installSpecs;
|
|
5809
|
+
} else if (action._tag === "Report") {
|
|
5810
|
+
logger.error(action.message);
|
|
5691
5811
|
} else {
|
|
5692
|
-
logger.
|
|
5812
|
+
const accepted = await logger.prompt(`The ${renderer} renderer needs ${installSpecs.join(", ")}. Install now?`, { type: "confirm", initial: true });
|
|
5813
|
+
if (accepted === true && (await ensureProviderDependencies(renderer, binding, nuxt)).success)
|
|
5814
|
+
availableRenderers.add(renderer);
|
|
5815
|
+
else
|
|
5816
|
+
logger.error(getMissingRendererMessage(renderer, installSpecs));
|
|
5693
5817
|
}
|
|
5694
|
-
} else if (isAgent) {
|
|
5695
|
-
const installSpecs = await getMissingDependencyInstallSpecs(renderer, binding);
|
|
5696
|
-
throw new Error(`[nuxt-og-image] ${renderer} renderer missing dependencies: ${missing.join(", ")}. Install with: npx nypm add ${installSpecs.join(" ")}`);
|
|
5697
|
-
} else {
|
|
5698
|
-
const installSpecs = await getMissingDependencyInstallSpecs(renderer, binding);
|
|
5699
|
-
logger.error(`${renderer} renderer missing dependencies: ${missing.join(", ")}. Install with: npx nypm add ${installSpecs.join(" ")}`);
|
|
5700
5818
|
}
|
|
5701
5819
|
if (renderer === "satori" && availableRenderers.has(renderer) && binding !== "node") {
|
|
5702
5820
|
logger.warn("ReSVG native binding not available. Falling back to WASM version, this may slow down PNG rendering.");
|
|
@@ -6075,6 +6193,7 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
|
|
|
6075
6193
|
// @ts-expect-error runtime type
|
|
6076
6194
|
isNuxtContentDocumentDriven: !!nuxt.options.content?.documentDriven,
|
|
6077
6195
|
cssFramework: cssFramework || "none",
|
|
6196
|
+
browserEnabled: !!config.browser,
|
|
6078
6197
|
// Browser renderer config for cloudflare binding access
|
|
6079
6198
|
browser: typeof config.browser === "object" ? {
|
|
6080
6199
|
provider: config.browser.provider,
|
|
@@ -6109,6 +6228,7 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
|
|
|
6109
6228
|
"nuxt-og-image": {
|
|
6110
6229
|
defaults: runtimeConfig.defaults,
|
|
6111
6230
|
includeTwitter: runtimeConfig.includeTwitter,
|
|
6231
|
+
browserEnabled: runtimeConfig.browserEnabled,
|
|
6112
6232
|
hasServerRuntime: !nuxt.options._generate && !nuxt.options.nitro?.static
|
|
6113
6233
|
}
|
|
6114
6234
|
};
|
|
@@ -6123,7 +6243,7 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
|
|
|
6123
6243
|
}
|
|
6124
6244
|
};
|
|
6125
6245
|
});
|
|
6126
|
-
const getDetectedRenderers = () => ogImageComponentCtx.detectedRenderers;
|
|
6246
|
+
const getDetectedRenderers = () => new Set([...ogImageComponentCtx.detectedRenderers].filter((r) => !missingRenderers[r]));
|
|
6127
6247
|
const getCompatibilityMeta = () => runtimeCompatibilityMeta;
|
|
6128
6248
|
if (nuxt.options.dev) {
|
|
6129
6249
|
await setupDevHandler(config, resolver, getDetectedRenderers, getCompatibilityMeta);
|
|
@@ -6167,6 +6287,8 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
|
|
|
6167
6287
|
setRuntimeAlias("#og-image/types", resolve("./runtime/types"));
|
|
6168
6288
|
if (nuxt.options.build)
|
|
6169
6289
|
addServerPlugin(resolve("./runtime/server/plugins/prerender"));
|
|
6290
|
+
if (nuxt.options.dev)
|
|
6291
|
+
addServerPlugin(resolve("./runtime/server/plugins/auto-eject"));
|
|
6170
6292
|
setupPrerenderHandler(config, resolver, getDetectedRenderers, getCompatibilityMeta);
|
|
6171
6293
|
if (nuxt.options.dev && hasWarnings()) {
|
|
6172
6294
|
nuxt.hook("ready", () => {
|