UNPKG

nuxt-og-image

Version:

Enlightened OG Image generation for Nuxt.

782 lines (769 loc) 31.2 kB
import * as fs from 'node:fs'; import { existsSync } from 'node:fs'; import { readFile, writeFile } from 'node:fs/promises'; import { useNuxt, addTemplate, loadNuxtModuleInstance, createResolver, resolvePath, tryResolveModule, defineNuxtModule, useLogger, addImports, hasNuxtModule, addServerPlugin, addServerHandler, addComponentsDir, addComponent, addPlugin } from '@nuxt/kit'; import { assertSiteConfig, installNuxtSiteConfig } from 'nuxt-site-config-kit'; import { provider, env, isCI, isDevelopment } from 'std-env'; import { hash } from 'ohash'; import { relative, dirname, basename, join } from 'pathe'; import { defu } from 'defu'; import { readPackageJSON } from 'pkg-types'; import { createStorage } from 'unstorage'; import fsDriver from 'unstorage/drivers/fs'; import { withoutLeadingSlash } from 'ufo'; import { ensureDependencyInstalled } from 'nypm'; import { relative as relative$1 } from 'node:path'; import { onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit'; import { createHash } from 'node:crypto'; import { Launcher } from 'chrome-launcher'; import { $fetch } from 'ofetch'; const autodetectableProviders = { azure_static: "azure", cloudflare_pages: "cloudflare-pages", netlify: "netlify", stormkit: "stormkit", vercel: "vercel", cleavr: "cleavr", stackblitz: "stackblitz" }; const autodetectableStaticProviders = { netlify: "netlify-static", vercel: "vercel-static" }; const NodeRuntime = { // node-server runtime "chromium": "on-demand", // this gets changed build start "css-inline": "node", "resvg": "node", "satori": "node", "sharp": "node" // will be disabled if they're missing the dependency }; const cloudflare = { "chromium": false, "css-inline": false, "resvg": "wasm", "satori": "node", "sharp": false, "wasm": { esmImport: true, lazy: true } }; const awsLambda = { "chromium": false, "css-inline": "node", "resvg": "node", "satori": "node", "sharp": false // 0.33.x has issues }; const WebContainer = { "chromium": false, "css-inline": "wasm-fs", "resvg": "wasm-fs", "satori": "wasm-fs", "sharp": false }; const RuntimeCompatibility = { "nitro-dev": NodeRuntime, "nitro-prerender": NodeRuntime, "node-server": NodeRuntime, "stackblitz": WebContainer, "codesandbox": WebContainer, "aws-lambda": awsLambda, "netlify": awsLambda, "netlify-edge": { "chromium": false, "css-inline": "wasm", "resvg": "wasm", "satori": "node", "sharp": false, "wasm": { rollup: { targetEnv: "auto-inline", sync: ["@resvg/resvg-wasm/index_bg.wasm"] } } }, "firebase": awsLambda, "vercel": awsLambda, "vercel-edge": { "chromium": false, "css-inline": false, // size constraint (2mb is max) "resvg": "wasm", "satori": "node", "sharp": false, "wasm": { // lowers workers kb size esmImport: true, lazy: true } }, "cloudflare-pages": cloudflare, "cloudflare": cloudflare, "cloudflare-module": cloudflare }; function detectTarget(options = {}) { return options?.static ? autodetectableStaticProviders[provider] : autodetectableProviders[provider]; } function resolveNitroPreset(nitroConfig) { if (provider === "stackblitz" || provider === "codesandbox") return provider; const nuxt = useNuxt(); if (nuxt.options.dev) return "nitro-dev"; if (nuxt.options._generate) return "nitro-prerender"; let preset; if (nitroConfig && nitroConfig?.preset) preset = nitroConfig.preset; if (!preset) preset = env.NITRO_PRESET || detectTarget() || "node-server"; return preset.replace("_", "-"); } function getPresetNitroPresetCompatibility(target) { let compatibility = RuntimeCompatibility[target]; if (!compatibility) compatibility = RuntimeCompatibility["nitro-dev"]; return compatibility; } function applyNitroPresetCompatibility(nitroConfig, options) { const target = resolveNitroPreset(nitroConfig); const compatibility = getPresetNitroPresetCompatibility(target); const { resolve } = options; const satoriEnabled = typeof options.compatibility?.satori !== "undefined" ? !!options.compatibility.satori : !!compatibility.satori; const chromiumEnabled = typeof options.compatibility?.chromium !== "undefined" ? !!options.compatibility.chromium : !!compatibility.chromium; nitroConfig.alias["#nuxt-og-image/renderers/satori"] = satoriEnabled ? resolve("./runtime/nitro/og-image/satori/renderer") : "unenv/runtime/mock/empty"; nitroConfig.alias["#nuxt-og-image/renderers/chromium"] = chromiumEnabled ? resolve("./runtime/nitro/og-image/chromium/renderer") : "unenv/runtime/mock/empty"; const resolvedCompatibility = {}; function applyBinding(key) { let binding = options.compatibility?.[key]; if (typeof binding === "undefined") binding = compatibility[key]; if (key === "chromium" && binding === "node") binding = "playwright"; resolvedCompatibility[key] = binding; return { [`#nuxt-og-image/bindings/${key}`]: binding === false ? "unenv/runtime/mock/empty" : resolve(`./runtime/nitro/og-image/bindings/${key}/${binding}`) }; } nitroConfig.alias = defu( applyBinding("chromium"), applyBinding("satori"), applyBinding("resvg"), applyBinding("sharp"), applyBinding("css-inline"), nitroConfig.alias || {} ); if (Object.values(compatibility).includes("wasm")) { nitroConfig.experimental = nitroConfig.experimental || {}; nitroConfig.experimental.wasm = true; } nitroConfig.rollupConfig = nitroConfig.rollupConfig || {}; nitroConfig.wasm = defu(compatibility.wasm, nitroConfig.wasm); nitroConfig.virtual["#nuxt-og-image/compatibility"] = () => `export default ${JSON.stringify(resolvedCompatibility)}`; addTemplate({ filename: "nuxt-og-image/compatibility.mjs", getContents() { return `export default ${JSON.stringify(resolvedCompatibility)}`; }, options: { mode: "server" } }); return resolvedCompatibility; } function ensureDependencies(dep, nuxt = useNuxt()) { return Promise.all(dep.map((d) => { return ensureDependencyInstalled(d, { cwd: nuxt.options.rootDir, dev: true }); })); } async function getNuxtModuleOptions(module, nuxt = useNuxt()) { const moduleMeta = (typeof module === "string" ? { name: module } : await module.getMeta?.()) || {}; const { nuxtModule } = await loadNuxtModuleInstance(module, nuxt); let moduleEntry; for (const m of nuxt.options.modules) { if (Array.isArray(m) && m.length >= 2) { const _module = m[0]; const _moduleEntryName = typeof _module === "string" ? _module : (await _module.getMeta?.())?.name || ""; if (_moduleEntryName === moduleMeta.name) moduleEntry = m; } } let inlineOptions = {}; if (moduleEntry) inlineOptions = moduleEntry[1]; if (nuxtModule.getOptions) return nuxtModule.getOptions(inlineOptions, nuxt); return inlineOptions; } function extendTypes(module, template) { const nuxt = useNuxt(); const { resolve } = createResolver(import.meta.url); addTemplate({ filename: `module/${module}.d.ts`, getContents: async () => { const typesPath = relative(resolve(nuxt.options.rootDir, nuxt.options.buildDir, "module"), resolve("runtime/types")); const s = await template({ typesPath }); return `// Generated by ${module} ${s} export {} `; } }); nuxt.hooks.hook("prepare:types", ({ references }) => { references.push({ path: resolve(nuxt.options.buildDir, `module/${module}.d.ts`) }); }); nuxt.hooks.hook("nitro:config", (config) => { config.typescript = config.typescript || {}; config.typescript.tsConfig = config.typescript.tsConfig || {}; config.typescript.tsConfig.include = config.typescript.tsConfig.include || []; config.typescript.tsConfig.include.push(`./module/${module}.d.ts`); }); } function isNuxtGenerate(nuxt = useNuxt()) { return nuxt.options._generate || nuxt.options.nitro.static || nuxt.options.nitro.preset === "static"; } const DEVTOOLS_UI_ROUTE = "/__nuxt-og-image"; const DEVTOOLS_UI_LOCAL_PORT = 3030; function setupDevToolsUI(options, resolve, nuxt = useNuxt()) { const clientPath = resolve("./client"); const isProductionBuild = existsSync(clientPath); if (isProductionBuild) { nuxt.hook("vite:serverCreated", async (server) => { const sirv = await import('sirv').then((r) => r.default || r); server.middlewares.use( DEVTOOLS_UI_ROUTE, sirv(clientPath, { dev: true, single: true }) ); }); } else { nuxt.hook("vite:extendConfig", (config) => { config.server = config.server || {}; config.server.proxy = config.server.proxy || {}; config.server.proxy[DEVTOOLS_UI_ROUTE] = { target: `http://localhost:${DEVTOOLS_UI_LOCAL_PORT}${DEVTOOLS_UI_ROUTE}`, changeOrigin: true, followRedirects: true, rewrite: (path) => path.replace(DEVTOOLS_UI_ROUTE, "") }; }); } onDevToolsInitialized(async () => { const rpc = extendServerRpc("nuxt-og-image", {}); nuxt.hook("builder:watch", (e, path) => { path = relative$1(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path)); if ((e === "change" || e.includes("link")) && path.startsWith("pages")) { rpc.broadcast.refreshRouteData(path).catch(() => { }); } if (options.componentDirs.some((dir) => path.includes(dir))) { if (e === "change") { rpc.broadcast.refresh().catch(() => { }); } else { rpc.broadcast.refreshGlobalData().catch(() => { }); } } }); }); nuxt.hook("devtools:customTabs", (tabs) => { tabs.push({ // unique identifier name: "nuxt-og-image", // title to display in the tab title: "OG Image", // any icon from Iconify, or a URL to an image icon: "carbon:image-search", // iframe view view: { type: "iframe", src: DEVTOOLS_UI_ROUTE } }); }); } function setupDevHandler(options, resolve, nuxt = useNuxt()) { nuxt.hooks.hook("nitro:config", async (nitroConfig) => { applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.dev, resolve }); }); } function setupGenerateHandler(options, resolve, nuxt = useNuxt()) { nuxt.hooks.hook("nitro:config", async (nitroConfig) => { applyNitroPresetCompatibility(nitroConfig, { compatibility: { "chromium": false, "satori": false, "css-inline": false, "resvg": false, "sharp": false }, resolve }); assertSiteConfig("nuxt-og-image", { url: "OG Image tags are required to be absolute URLs." }, { throwError: true }); }); } function setupPrerenderHandler(options, resolve, nuxt = useNuxt()) { nuxt.hooks.hook("nitro:init", async (nitro) => { nitro.hooks.hook("prerender:config", async (nitroConfig) => { applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.prerender, resolve }); nitroConfig.wasm = nitroConfig.wasm || {}; nitroConfig.wasm.esmImport = false; const prerenderingPages = (nuxt.options.nitro.prerender?.routes || []).some((r) => r && (!r.includes(".") || r.includes("*"))); prerenderingPages && assertSiteConfig("nuxt-og-image", { url: "OG Image tags are required to be absolute URLs." }, { throwError: false }); }); }); } async function setupBuildHandler(config, resolve, nuxt = useNuxt()) { nuxt.options.nitro.storage = nuxt.options.nitro.storage || {}; if (typeof config.runtimeCacheStorage === "object") nuxt.options.nitro.storage["og-image"] = config.runtimeCacheStorage; nuxt.hooks.hook("nitro:config", async (nitroConfig) => { applyNitroPresetCompatibility(nitroConfig, { compatibility: config.compatibility?.runtime, resolve }); nitroConfig.alias.electron = "unenv/runtime/mock/proxy-cjs"; nitroConfig.alias.bufferutil = "unenv/runtime/mock/proxy-cjs"; nitroConfig.alias["utf-8-validate"] = "unenv/runtime/mock/proxy-cjs"; nitroConfig.alias.queue = "unenv/runtime/mock/proxy-cjs"; }); nuxt.hooks.hook("nitro:init", async (nitro) => { nitro.hooks.hook("compiled", async (_nitro) => { const target = resolveNitroPreset(_nitro.options); const compatibility = getPresetNitroPresetCompatibility(target); if (compatibility.wasm?.esmImport !== true) return; const configuredEntry = nitro.options.rollupConfig?.output.entryFileNames; let serverEntry = resolve(_nitro.options.output.serverDir, typeof configuredEntry === "string" ? configuredEntry : "index.mjs"); const isCloudflarePagesOrModule = target === "cloudflare-pages" || target === "cloudflare-module"; if (isCloudflarePagesOrModule) { serverEntry = [ resolve(dirname(serverEntry), "./chunks/wasm.mjs"), resolve(dirname(serverEntry), "./chunks/_/wasm.mjs") ].filter(existsSync)[0] || serverEntry; } const contents = await readFile(serverEntry, "utf-8"); const resvgHash = sha1(await readFile(await resolvePath("@resvg/resvg-wasm/index_bg.wasm"))); const yogaHash = sha1(await readFile(await resolvePath("yoga-wasm-web/dist/yoga.wasm"))); const cssInlineHash = sha1(await readFile(await resolvePath("@css-inline/css-inline-wasm/index_bg.wasm"))); const postfix = target === "vercel-edge" ? "?module" : ""; const path = isCloudflarePagesOrModule ? `../wasm/` : `./wasm/`; await writeFile(serverEntry, contents.replaceAll('"@resvg/resvg-wasm/index_bg.wasm?module"', `"${path}index_bg-${resvgHash}.wasm${postfix}"`).replaceAll('"@css-inline/css-inline-wasm/index_bg.wasm?module"', `"${path}index_bg-${cssInlineHash}.wasm${postfix}"`).replaceAll('"yoga-wasm-web/dist/yoga.wasm?module"', `"${path}yoga-${yogaHash}.wasm${postfix}"`), { encoding: "utf-8" }); }); }); } function sha1(source) { return createHash("sha1").update(source).digest("hex").slice(0, 16); } const isUndefinedOrTruthy = (v) => typeof v === "undefined" || v !== false; function checkLocalChrome() { if (isCI) return false; let hasChromeLocally = false; try { hasChromeLocally = !!Launcher.getFirstInstallation(); } catch { } return hasChromeLocally; } async function checkPlaywrightDependency() { return !!await tryResolveModule("playwright"); } async function downloadFont(font, storage, mirror) { const { name, weight } = font; const key = `${name}-${weight}.ttf.base64`; if (await storage.hasItem(key)) return true; const host = typeof mirror === "undefined" ? "fonts.googleapis.com" : mirror === true ? "fonts.font.im" : mirror; const css = await $fetch(`https://${host}/css2?family=${name}:wght@${weight}`, { timeout: 10 * 1e3, // 10 second timeout headers: { // Make sure it returns TTF. "User-Agent": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1" } }).catch(() => { return false; }); if (!css) return false; const ttfResource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/); if (ttfResource?.[1]) { const buf = await $fetch(ttfResource[1], { baseURL: host, responseType: "arrayBuffer" }); const base64Font = Buffer.from(buf).toString("base64"); await storage.setItem(key, base64Font); return true; } return false; } function normaliseFontInput(fonts) { return fonts.map((f) => { if (typeof f === "string") { const [name, weight] = f.split(":"); return { cacheKey: f, name, weight: weight || 400, style: "normal", path: void 0 }; } return { cacheKey: f.key || `${f.name}:${f.weight}`, style: "normal", weight: 400, ...f }; }); } const module = defineNuxtModule({ meta: { name: "nuxt-og-image", compatibility: { nuxt: "^3.10.3", bridge: false }, configKey: "ogImage" }, defaults() { return { enabled: true, defaults: { emojis: "noto", renderer: "satori", component: "NuxtSeo", extension: "png", width: 1200, height: 600, // default is to cache the image for 3 day (72 hours) cacheMaxAgeSeconds: 60 * 60 * 24 * 3 }, componentDirs: ["OgImage", "OgImageTemplate"], fonts: [], runtimeCacheStorage: true, debug: isDevelopment }; }, async setup(config, nuxt) { const { resolve } = createResolver(import.meta.url); const { name, version } = await readPackageJSON(resolve("../package.json")); const logger = useLogger(name); logger.level = config.debug || nuxt.options.debug ? 4 : 3; if (config.enabled === false) { logger.debug("The module is disabled, skipping setup."); ["defineOgImage", "defineOgImageComponent", "defineOgImageScreenshot"].forEach((name2) => { addImports({ name: name2, from: resolve(`./runtime/nuxt/composables/mock`) }); }); return; } if (config.enabled && !nuxt.options.ssr) { logger.warn("Nuxt OG Image is enabled but SSR is disabled.\n\nYou should enable SSR (`ssr: true`) or disable the module (`ogImage: { enabled: false }`)."); return; } nuxt.options.build.transpile.push(resolve("./runtime")); const preset = resolveNitroPreset(nuxt.options.nitro); const targetCompatibility = getPresetNitroPresetCompatibility(preset); const hasSharpDependency = !!await tryResolveModule("sharp"); const userConfiguredExtension = config.defaults.extension; const hasConfiguredJpegs = userConfiguredExtension && ["jpeg", "jpg"].includes(userConfiguredExtension); if (hasConfiguredJpegs && config.defaults.renderer !== "chromium") { if (hasSharpDependency && !targetCompatibility.sharp) { logger.warn(`Rendering JPEGs requires sharp which does not work with ${preset}. Images will be rendered as PNG at runtime.`); config.compatibility = defu(config.compatibility, { runtime: { sharp: false } }); } else if (!hasSharpDependency) { logger.warn("You have enabled `JPEG` images. These require the `sharp` dependency which is missing, installing it for you."); await ensureDependencies(["sharp"]); logger.warn("Support for `sharp` is limited so check the compatibility guide."); } } else if (!hasSharpDependency) { config.compatibility = defu(config.compatibility, { runtime: { sharp: false }, dev: { sharp: false }, prerender: { sharp: false } }); } const hasChromeLocally = checkLocalChrome(); const hasPlaywrightDependency = await checkPlaywrightDependency(); const chromeCompatibilityFlags = { prerender: config.compatibility?.prerender?.chromium, dev: config.compatibility?.dev?.chromium, runtime: config.compatibility?.runtime?.chromium }; const chromiumBinding = { dev: null, prerender: null, runtime: null }; if (nuxt.options.dev) { if (isUndefinedOrTruthy(chromeCompatibilityFlags.dev)) chromiumBinding.dev = hasChromeLocally ? "chrome-launcher" : hasPlaywrightDependency ? "playwright" : "on-demand"; } else { if (isUndefinedOrTruthy(chromeCompatibilityFlags.prerender)) chromiumBinding.prerender = hasChromeLocally ? "chrome-launcher" : hasPlaywrightDependency ? "playwright" : "on-demand"; if (isUndefinedOrTruthy(chromeCompatibilityFlags.runtime)) chromiumBinding.runtime = hasPlaywrightDependency ? "playwright" : null; } config.compatibility = defu(config.compatibility, { runtime: { chromium: chromiumBinding.runtime }, dev: { chromium: chromiumBinding.dev }, prerender: { chromium: chromiumBinding.prerender } }); await import('@resvg/resvg-js').catch(() => { logger.warn("ReSVG is missing dependencies for environment. Falling back to WASM version, this may slow down PNG rendering."); config.compatibility = defu(config.compatibility, { dev: { resvg: "wasm-fs" }, prerender: { resvg: "wasm-fs" } }); if (targetCompatibility.resvg === "node") { config.compatibility = defu(config.compatibility, { runtime: { resvg: "wasm" } }); } }); await installNuxtSiteConfig(); if (hasNuxtModule("@nuxt/content")) addServerPlugin(resolve("./runtime/nitro/plugins/nuxt-content")); if (!config.fonts.length) { config.fonts = [ { name: "Inter", weight: 400, path: resolve("./runtime/assets/Inter-400.ttf.base64"), absolutePath: true }, { name: "Inter", weight: 700, path: resolve("./runtime/assets/Inter-700.ttf.base64"), absolutePath: true } ]; } const serverFontsDir = resolve(nuxt.options.buildDir, "cache", `nuxt-og-image@${version}`, "_fonts"); const fontStorage = createStorage({ driver: fsDriver({ base: serverFontsDir }) }); config.fonts = (await Promise.all(normaliseFontInput(config.fonts).map(async (f) => { if (!f.key && !f.path) { if (preset === "stackblitz") { logger.warn(`The ${f.name}:${f.weight} font was skipped because remote fonts are not available in StackBlitz, please use a local font.`); return false; } if (await downloadFont(f, fontStorage, config.googleFontMirror)) { f.key = `nuxt-og-image:fonts:${f.name}-${f.weight}.ttf.base64`; } else { logger.warn(`Failed to download font ${f.name}:${f.weight}. You may be offline or behind a firewall blocking Google. Consider setting \`googleFontMirror: true\`.`); return false; } } else if (f.path) { const extension = basename(f.path.replace(".base64", "")).split(".").pop(); if (!["woff", "ttf", "otf"].includes(extension)) { logger.warn(`The ${f.name}:${f.weight} font was skipped because the file extension ${extension} is not supported. Only woff, ttf and otf are supported.`); return false; } if (!f.absolutePath) f.path = join(nuxt.options.rootDir, nuxt.options.dir.public, withoutLeadingSlash(f.path)); if (!existsSync(f.path)) { logger.warn(`The ${f.name}:${f.weight} font was skipped because the file does not exist at path ${f.path}.`); return false; } const fontData = await readFile(f.path, f.path.endsWith(".base64") ? "utf-8" : "base64"); f.key = `nuxt-og-image:fonts:${f.name}-${f.weight}.${extension}.base64`; await fontStorage.setItem(`${f.name}-${f.weight}.${extension}.base64`, fontData); delete f.path; delete f.absolutePath; } return f; }))).filter(Boolean); nuxt.options.nitro.serverAssets = nuxt.options.nitro.serverAssets || []; nuxt.options.nitro.serverAssets.push({ baseName: "nuxt-og-image:fonts", dir: serverFontsDir }); nuxt.options.experimental.componentIslands = true; addServerHandler({ lazy: true, route: "/__og-image__/font/**", handler: resolve("./runtime/nitro/routes/font") }); if (config.debug || nuxt.options.dev) { addServerHandler({ lazy: true, route: "/__og-image__/debug.json", handler: resolve("./runtime/nitro/routes/debug.json") }); } addServerHandler({ lazy: true, route: "/__og-image__/image/**", handler: resolve("./runtime/nitro/routes/image") }); nuxt.options.optimization.treeShake.composables.client["nuxt-og-image"] = []; ["defineOgImage", "defineOgImageComponent", "defineOgImageScreenshot"].forEach((name2) => { addImports({ name: name2, from: resolve(`./runtime/nuxt/composables/${name2}`) }); nuxt.options.optimization.treeShake.composables.client["nuxt-og-image"].push(name2); }); await addComponentsDir({ path: resolve("./runtime/nuxt/components/Templates/Community"), island: true, watch: true }); [ // new "OgImage", "OgImageScreenshot" ].forEach((name2) => { addComponent({ name: name2, filePath: resolve(`./runtime/nuxt/components/OgImage/${name2}`), ...config.componentOptions }); }); addPlugin({ mode: "server", src: resolve("./runtime/nuxt/plugins/route-rule-og-image.server") }); addPlugin({ mode: "server", src: resolve("./runtime/nuxt/plugins/og-image-canonical-urls.server") }); const ogImageComponentCtx = { components: [] }; nuxt.hook("components:extend", (components) => { ogImageComponentCtx.components = []; components.forEach((component) => { let valid = false; config.componentDirs.forEach((dir) => { if (component.pascalName.startsWith(dir) || component.kebabName.startsWith(dir) || component.shortPath.includes(`/${dir}/`)) valid = true; }); if (component.filePath.includes(resolve("./runtime/nuxt/components/Templates"))) valid = true; if (valid && fs.existsSync(component.filePath)) { component.island = true; component.mode = "server"; let category = "app"; if (component.filePath.includes(resolve("./runtime/nuxt/components/Templates/Community"))) category = "community"; const componentFile = fs.readFileSync(component.filePath, "utf-8"); const credits = componentFile.split("\n").find((line) => line.startsWith(" * @credits"))?.replace("* @credits", "").trim(); ogImageComponentCtx.components.push({ // purge cache when component changes hash: hash(componentFile), pascalName: component.pascalName, kebabName: component.kebabName, path: nuxt.options.dev ? component.filePath : void 0, category, credits }); } }); nuxt.hooks.hook("nuxt-og-image:components", ogImageComponentCtx); }); addTemplate({ filename: "nuxt-og-image/components.mjs", getContents() { return `export const componentNames = ${JSON.stringify(ogImageComponentCtx.components)}`; }, options: { mode: "server" } }); nuxt.options.nitro.virtual = nuxt.options.nitro.virtual || {}; nuxt.options.nitro.virtual["#nuxt-og-image/component-names.mjs"] = () => { return `export const componentNames = ${JSON.stringify(ogImageComponentCtx.components)}`; }; let unoCssConfig = {}; nuxt.hook("tailwindcss:config", (tailwindConfig) => { unoCssConfig = defu(tailwindConfig.theme?.extend, { ...tailwindConfig.theme, extend: void 0 }); }); nuxt.hook("unocss:config", (_unoCssConfig) => { unoCssConfig = { ..._unoCssConfig.theme }; }); nuxt.options.nitro.virtual["#nuxt-og-image/unocss-config.mjs"] = () => { return `export const theme = ${JSON.stringify(unoCssConfig)}`; }; extendTypes("nuxt-og-image", ({ typesPath }) => { const componentImports = ogImageComponentCtx.components.map((component) => { const relativeComponentPath = relative(resolve(nuxt.options.rootDir, nuxt.options.buildDir, "module"), component.path); const name2 = config.componentDirs.sort((a, b) => b.length - a.length).reduce((name3, dir) => { return name3.replace(new RegExp(`^${dir}`), ""); }, component.pascalName); return ` '${name2}': typeof import('${relativeComponentPath}')['default']`; }).join("\n"); return ` declare module 'nitropack' { interface NitroRouteRules { ogImage?: false | import('${typesPath}').OgImageOptions & Record<string, any> } interface NitroRouteConfig { ogImage?: false | import('${typesPath}').OgImageOptions & Record<string, any> } interface NitroRuntimeHooks { 'nuxt-og-image:context': (ctx: import('${typesPath}').OgImageRenderEventContext) => void | Promise<void> 'nuxt-og-image:satori:vnodes': (vnodes: import('${typesPath}').VNode, ctx: import('${typesPath}').OgImageRenderEventContext) => void | Promise<void> } } declare module '#nuxt-og-image/components' { export interface OgImageComponents { ${componentImports} } } declare module '#nuxt-og-image/unocss-config' { export type theme = any } `; }); const cacheEnabled = typeof config.runtimeCacheStorage !== "undefined" && config.runtimeCacheStorage !== false; const runtimeCacheStorage = typeof config.runtimeCacheStorage === "boolean" ? "default" : config.runtimeCacheStorage.driver; let baseCacheKey = runtimeCacheStorage === "default" ? `/cache/nuxt-og-image@${version}` : `/nuxt-og-image/${version}`; if (!cacheEnabled) baseCacheKey = false; if (!nuxt.options.dev && config.runtimeCacheStorage && typeof config.runtimeCacheStorage === "object") { nuxt.options.nitro.storage = nuxt.options.nitro.storage || {}; nuxt.options.nitro.storage["nuxt-og-image"] = config.runtimeCacheStorage; } nuxt.hooks.hook("modules:done", async () => { const normalisedFonts = normaliseFontInput(config.fonts); if (!isNuxtGenerate() && nuxt.options.build) { nuxt.options.nitro = nuxt.options.nitro || {}; nuxt.options.nitro.prerender = nuxt.options.nitro.prerender || {}; nuxt.options.nitro.prerender.routes = nuxt.options.nitro.prerender.routes || []; } const hasColorModeModule = hasNuxtModule("@nuxtjs/color-mode"); const colorModeOptions = hasColorModeModule ? await getNuxtModuleOptions("@nuxtjs/color-mode") : {}; let colorPreference = colorModeOptions.preference; if (!colorPreference || colorPreference === "system") colorPreference = colorModeOptions.fallback; if (!colorPreference || colorPreference === "system") colorPreference = "light"; const runtimeConfig = { version, // binding options satoriOptions: config.satoriOptions || {}, resvgOptions: config.resvgOptions || {}, sharpOptions: config.sharpOptions || {}, defaults: config.defaults, debug: config.debug, // avoid adding credentials baseCacheKey, // convert the fonts to uniform type to fix ts issue fonts: normalisedFonts, hasNuxtIcon: hasNuxtModule("nuxt-icon"), colorPreference, // @ts-expect-error runtime type isNuxtContentDocumentDriven: !!nuxt.options.content?.documentDriven }; nuxt.hooks.callHook("nuxt-og-image:runtime-config", runtimeConfig); nuxt.options.runtimeConfig["nuxt-og-image"] = runtimeConfig; }); if (nuxt.options.dev) { setupDevHandler(config, resolve); setupDevToolsUI(config, resolve); } else if (isNuxtGenerate()) { setupGenerateHandler(config, resolve); } else if (nuxt.options.build) { await setupBuildHandler(config, resolve); } if (nuxt.options.build) addServerPlugin(resolve("./runtime/nitro/plugins/prerender")); setupPrerenderHandler(config, resolve); } }); export { module as default };