nuxt-og-image 6.10.3 → 7.0.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.
Files changed (103) hide show
  1. package/README.md +10 -14
  2. package/dist/chunks/tw4.mjs +6 -6
  3. package/dist/chunks/uno.mjs +7 -7
  4. package/dist/cli.mjs +4 -4
  5. package/dist/devtools/components/og-image/RendererSelectModal.vue +1 -1
  6. package/dist/devtools/lib/og-image/fonts.ts +38 -0
  7. package/dist/devtools/lib/og-image/types.ts +7 -0
  8. package/dist/devtools/pages/og-image/index.vue +19 -14
  9. package/dist/module.d.mts +14 -10
  10. package/dist/module.d.ts +14 -10
  11. package/dist/module.json +2 -2
  12. package/dist/module.mjs +6 -6
  13. package/dist/runtime/app/client-utils.js +7 -4
  14. package/dist/runtime/app/components/Templates/Community/SaaS.satori.vue +1 -1
  15. package/dist/runtime/app/components/Templates/Community/SimpleBlog.satori.vue +1 -1
  16. package/dist/runtime/app/disabled/components.d.ts +1 -0
  17. package/dist/runtime/app/disabled/components.js +0 -0
  18. package/dist/runtime/app/disabled/composables/defineOgImage.d.ts +1 -0
  19. package/dist/runtime/app/disabled/composables/defineOgImage.js +1 -0
  20. package/dist/runtime/app/disabled/composables/defineOgImageComponent.d.ts +1 -0
  21. package/dist/runtime/app/disabled/composables/defineOgImageComponent.js +1 -0
  22. package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.d.ts +1 -0
  23. package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.js +1 -0
  24. package/dist/runtime/app/disabled/composables/mock.d.ts +1 -0
  25. package/dist/runtime/app/disabled/composables/mock.js +1 -0
  26. package/dist/runtime/app/disabled/index.d.ts +3 -0
  27. package/dist/runtime/app/disabled/index.js +3 -0
  28. package/dist/runtime/app/index.d.ts +4 -0
  29. package/dist/runtime/app/index.js +4 -0
  30. package/dist/runtime/app/utils/plugins.js +1 -1
  31. package/dist/runtime/app/utils.js +2 -2
  32. package/dist/runtime/server/index.d.ts +1 -0
  33. package/dist/runtime/server/index.js +1 -0
  34. package/dist/runtime/server/middleware/signing.d.ts +2 -0
  35. package/dist/runtime/server/middleware/signing.js +19 -0
  36. package/dist/runtime/server/nitro.d.ts +3 -3
  37. package/dist/runtime/server/nitro.js +4 -4
  38. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +8 -49
  39. package/dist/runtime/server/og-image/bindings/font-assets/node.js +3 -2
  40. package/dist/runtime/server/og-image/bindings/satori/node.d.ts +1 -2
  41. package/dist/runtime/server/og-image/bindings/satori/wasm-fs.d.ts +1 -2
  42. package/dist/runtime/server/og-image/bindings/satori/wasm-fs.js +1 -1
  43. package/dist/runtime/server/og-image/bindings/satori/wasm.d.ts +1 -2
  44. package/dist/runtime/server/og-image/bindings/satori/wasm.js +1 -1
  45. package/dist/runtime/server/og-image/browser/screenshot.js +5 -3
  46. package/dist/runtime/server/og-image/cache/publish.d.ts +43 -0
  47. package/dist/runtime/server/og-image/cache/publish.js +91 -0
  48. package/dist/runtime/server/og-image/cache/published.d.ts +17 -0
  49. package/dist/runtime/server/og-image/cache/published.js +49 -0
  50. package/dist/runtime/server/og-image/context.js +1 -1
  51. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -5
  52. package/dist/runtime/server/og-image/core/transforms/emojis/index.d.ts +3 -2
  53. package/dist/runtime/server/og-image/core/vnodes.js +19 -1
  54. package/dist/runtime/server/og-image/font-source.d.ts +1 -0
  55. package/dist/runtime/server/og-image/font-source.js +1 -0
  56. package/dist/runtime/server/og-image/fonts.d.ts +1 -1
  57. package/dist/runtime/server/og-image/fonts.js +1 -1
  58. package/dist/runtime/server/og-image/satori/filters.d.ts +1 -0
  59. package/dist/runtime/server/og-image/satori/filters.js +14 -0
  60. package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
  61. package/dist/runtime/server/og-image/satori/plugins/classes.js +0 -7
  62. package/dist/runtime/server/og-image/satori/renderer.js +2 -1
  63. package/dist/runtime/server/og-image/satori/vnodes.js +1 -4
  64. package/dist/runtime/server/og-image/takumi/renderer.js +11 -47
  65. package/dist/runtime/server/og-image/utils/css.d.ts +0 -5
  66. package/dist/runtime/server/og-image/utils/css.js +0 -5
  67. package/dist/runtime/server/plugins/auto-eject-nitro3.d.ts +2 -0
  68. package/dist/runtime/server/plugins/auto-eject-nitro3.js +9 -0
  69. package/dist/runtime/server/plugins/auto-eject.js +3 -31
  70. package/dist/runtime/server/plugins/prerender.js +68 -2
  71. package/dist/runtime/server/plugins/publish.d.ts +2 -0
  72. package/dist/runtime/server/plugins/publish.js +44 -0
  73. package/dist/runtime/server/routes/debug.json.d.ts +10 -1
  74. package/dist/runtime/server/routes/debug.json.js +4 -4
  75. package/dist/runtime/server/routes/static-font.d.ts +2 -0
  76. package/dist/runtime/server/routes/static-font.js +22 -0
  77. package/dist/runtime/server/signing-secret.d.ts +8 -0
  78. package/dist/runtime/server/signing-secret.js +75 -0
  79. package/dist/runtime/server/util/auto-eject-lifecycle.d.ts +13 -0
  80. package/dist/runtime/server/util/auto-eject-lifecycle.js +31 -0
  81. package/dist/runtime/server/util/cache.d.ts +1 -0
  82. package/dist/runtime/server/util/cache.js +12 -6
  83. package/dist/runtime/server/util/cloudflare.d.ts +2 -2
  84. package/dist/runtime/server/util/eventHandlers.js +69 -3
  85. package/dist/runtime/server/util/fetchLocalAsset.js +2 -2
  86. package/dist/runtime/server/util/origin.d.ts +3 -0
  87. package/dist/runtime/server/util/origin.js +4 -0
  88. package/dist/runtime/server/util/publishWarnings.d.ts +6 -0
  89. package/dist/runtime/server/util/publishWarnings.js +6 -0
  90. package/dist/runtime/server/utils.d.ts +3 -3
  91. package/dist/runtime/server/utils.js +5 -10
  92. package/dist/runtime/shared/publish.d.ts +13 -0
  93. package/dist/runtime/shared/publish.js +17 -0
  94. package/dist/runtime/shared/urlEncoding.js +9 -4
  95. package/dist/runtime/types.d.ts +11 -16
  96. package/dist/shared/{nuxt-og-image.Bcegsloy.mjs → nuxt-og-image.DR_SxdDv.mjs} +1224 -1109
  97. package/package.json +60 -62
  98. package/skills/nuxt-og-image/SKILL.md +31 -14
  99. package/types/virtual.d.ts +8 -0
  100. package/dist/runtime/server/og-image/satori/plugins/emojis.d.ts +0 -2
  101. package/dist/runtime/server/og-image/satori/plugins/emojis.js +0 -112
  102. package/dist/runtime/server/og-image/satori/plugins/flex.d.ts +0 -2
  103. package/dist/runtime/server/og-image/satori/plugins/flex.js +0 -48
@@ -1,26 +1,25 @@
1
- import { createHash, randomBytes } from 'node:crypto';
2
1
  import * as fs from 'node:fs';
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, rmSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, rmSync } from 'node:fs';
4
3
  import { readFile, mkdir, writeFile, readdir } from 'node:fs/promises';
5
- import { fileURLToPath, pathToFileURL } from 'node:url';
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';
4
+ import { findPackageJSON } from 'node:module';
5
+ import { pathToFileURL, fileURLToPath } from 'node:url';
6
+ import { resolvePath, useNuxt, getNuxtVersion, addTemplate, updateTemplates, directoryToURL, tryResolveModule, addTypeTemplate, defineNuxtModule, createResolver, addImports, hasNuxtModule, hasNuxtModuleCompatibility, getNuxtModuleVersion, addVitePlugin, addServerHandler, addServerImports, addPlugin, getLayerDirectories, addComponentsDir, addServerPlugin, getNitroVersion } from '@nuxt/kit';
7
7
  import { defu } from 'defu';
8
8
  import { fnv1a64Base36 } from 'fnv1a-64';
9
- import { useSiteConfig, installNuxtSiteConfig } from 'nuxt-site-config/kit';
10
- import { resolveNitroPreset, renderNitroTypeAugmentations, setupNitroRuntimeCompatibility, getNuxtModuleOptions, isNuxtGenerate } from 'nuxtseo-shared/kit';
9
+ import { resolveNitroPreset, renderNitroTypeAugmentations, setupRuntimeAliases, setupNitroRuntimeCompatibility, getNuxtModuleOptions, isNuxtGenerate } from 'nuxtseo-shared/kit';
11
10
  import { join, basename, dirname, relative, isAbsolute, resolve } from 'pathe';
12
- import { readPackageJSON } from 'pkg-types';
11
+ import { createHash } from 'node:crypto';
13
12
  import MagicString from 'magic-string';
14
13
  import { parseAndWalk } from 'oxc-walker';
15
14
  import { resolveModulePath } from 'exsolve';
16
15
  import { logger } from '../../dist/runtime/logger.js';
17
- import { Launcher } from 'chrome-launcher';
18
16
  import { isCI, isAgent, hasTTY } from 'std-env';
17
+ import { resolveColorMix, GRADIENT_COLOR_SPACE_RE, splitCssDeclarations } from '../../dist/runtime/server/og-image/utils/css.js';
19
18
  import { detectPackageManager, addDependency } from 'nypm';
20
19
  import { setupDevToolsUI as setupDevToolsUI$1, setupDevToolsRpc } from 'nuxtseo-shared/devtools';
21
20
  import { createStorage } from 'unstorage';
22
21
  import fsDriver from 'unstorage/drivers/fs-lite';
23
- import { resolveColorMix, GRADIENT_COLOR_SPACE_RE, splitCssDeclarations } from '../../dist/runtime/server/og-image/utils/css.js';
22
+ import { useSiteConfig } from 'nuxt-site-config/kit';
24
23
  import { stripLiteral } from 'strip-literal';
25
24
  import { createUnplugin } from 'unplugin';
26
25
  import { parse } from '@vue/compiler-sfc';
@@ -28,6 +27,7 @@ import { ofetch } from 'ofetch';
28
27
  import { parse as parse$1, walkSync, ELEMENT_NODE as ELEMENT_NODE$2, renderSync } from 'ultrahtml';
29
28
  import { RE_MATCH_EMOJIS, getEmojiCodePoint, getEmojiIconNames } from '../../dist/runtime/server/og-image/core/transforms/emojis/emoji-utils.js';
30
29
  import { p as promptFontsMigration } from './nuxt-og-image.CJa2KCie.mjs';
30
+ import { parsePublishConfig, hasPublishMount } from '../../dist/runtime/shared/publish.js';
31
31
 
32
32
  const valueTypes = { i: 127, p: 127, j: 126, f: 125, d: 124 };
33
33
  function encodeLength(value) {
@@ -101,11 +101,43 @@ async function prepareHarfBuzzCallbacks(adapterPath, outputDir) {
101
101
  `export default { ${entries.map(({ signature }) => signature).join(", ")} }`
102
102
  ].join("\n");
103
103
  }
104
+ function externalizeSatoriHarfBuzz(code, id, specifier = "harfbuzzjs") {
105
+ const blocks = [];
106
+ const loaders = [];
107
+ parseAndWalk(code, id, {
108
+ enter(node) {
109
+ if (node.type === "BlockStatement")
110
+ blocks.push(node);
111
+ if (node.type !== "AssignmentExpression" || node.operator !== "=" || node.left.type !== "MemberExpression" || node.left.computed || node.left.property.type !== "Identifier" || node.left.property.name !== "exports" || node.right.type !== "NewExpression" || node.right.callee.type !== "Identifier" || node.right.callee.name !== "Promise") {
112
+ return;
113
+ }
114
+ const block = blocks.at(-1);
115
+ if (block)
116
+ loaders.push({ ...block, target: code.slice(node.left.start, node.left.end) });
117
+ },
118
+ leave(node) {
119
+ if (node.type === "BlockStatement")
120
+ blocks.pop();
121
+ }
122
+ });
123
+ if (loaders.length !== 1)
124
+ throw new Error(`Cannot replace the Satori HarfBuzz loader in ${id}: found ${loaders.length} loaders.`);
125
+ const loader = loaders[0];
126
+ return `import _ogImageHarfBuzz from ${JSON.stringify(specifier)};
127
+ ${code.slice(0, loader.start)}{${loader.target} = _ogImageHarfBuzz;}${code.slice(loader.end)}`;
128
+ }
129
+
130
+ async function readPackageManifest(path) {
131
+ return JSON.parse(await readFile(path, "utf8"));
132
+ }
104
133
 
105
134
  const isUndefinedOrTruthy = (v) => typeof v === "undefined" || v !== false;
106
- function checkLocalChrome() {
135
+ async function checkLocalChrome() {
107
136
  if (isCI)
108
137
  return false;
138
+ if (!await hasResolvableDependency("chrome-launcher"))
139
+ return false;
140
+ const { Launcher } = await import('chrome-launcher');
109
141
  let hasChromeLocally = false;
110
142
  try {
111
143
  hasChromeLocally = !!Launcher.getFirstInstallation();
@@ -118,7 +150,7 @@ async function hasResolvableDependency(dep) {
118
150
  return null;
119
151
  }).then((r) => r && r !== dep);
120
152
  }
121
- const MIN_SATORI_VERSION = "0.33.5";
153
+ const MIN_SATORI_VERSION = "0.47.1";
122
154
  function isSupportedSatoriVersion(version) {
123
155
  const current = (version || "").split(".").map((part) => Number.parseInt(part, 10) || 0);
124
156
  const minimum = MIN_SATORI_VERSION.split(".").map(Number);
@@ -358,7 +390,7 @@ async function applyNitroPresetCompatibility(nitroConfig, options) {
358
390
  nitroConfig.alias["#og-image/renderers/satori"] = satoriEnabled ? await resolve.resolvePath("./runtime/server/og-image/satori/renderer") : emptyMock;
359
391
  nitroConfig.alias["#og-image/renderers/browser"] = browserEnabled ? await resolve.resolvePath("./runtime/server/og-image/browser/renderer") : emptyMock;
360
392
  nitroConfig.alias["#og-image/renderers/takumi"] = takumiEnabled ? await resolve.resolvePath("./runtime/server/og-image/takumi/renderer") : emptyMock;
361
- const resolvedCompatibility = { ...options.metadata || {} };
393
+ const resolvedCompatibility = {};
362
394
  async function applyBinding(key) {
363
395
  let binding = options.compatibility?.[key];
364
396
  if (typeof binding === "undefined")
@@ -375,10 +407,7 @@ async function applyNitroPresetCompatibility(nitroConfig, options) {
375
407
  [`#og-image/bindings/${key}`]: !binding ? emptyMock : await resolve.resolvePath(`./runtime/server/og-image/bindings/${key}/${binding}`)
376
408
  };
377
409
  }
378
- const satoriPkgDir = await resolve.resolvePath("satori/package.json").then((p) => p.replace("/package.json", ""));
379
- const yogaWasmPath = `${satoriPkgDir}/yoga.wasm`;
380
410
  nitroConfig.alias = defu(
381
- { "#og-image/yoga-wasm": `${yogaWasmPath}?module` },
382
411
  await applyBinding("browser"),
383
412
  await applyBinding("satori"),
384
413
  await applyBinding("takumi"),
@@ -387,9 +416,11 @@ async function applyNitroPresetCompatibility(nitroConfig, options) {
387
416
  nitroConfig.alias || {}
388
417
  );
389
418
  if (resolvedCompatibility.satori) {
390
- const satoriPackage = await readPackageJSON(`${satoriPkgDir}/package.json`);
419
+ const satoriPkgDir = await resolve.resolvePath("satori/package.json").then((p) => p.replace("/package.json", ""));
420
+ nitroConfig.alias["#og-image/layout-wasm"] = `${satoriPkgDir}/layout.wasm?module`;
421
+ const satoriPackage = await readPackageManifest(`${satoriPkgDir}/package.json`);
391
422
  if (!isSupportedSatoriVersion(satoriPackage.version))
392
- throw new Error(`[Nuxt OG Image] Satori ${satoriPackage.version} is not supported. Satori ${MIN_SATORI_VERSION} or later is required (GHSA-wx4j-mvgx-mqwp). Run: npm install satori@latest`);
423
+ throw new Error(`[Nuxt OG Image] Satori ${satoriPackage.version} is not supported. Satori ${MIN_SATORI_VERSION} or later is required. Run: npm install satori@latest`);
393
424
  const harfbuzzWasmPath = resolveModulePath("harfbuzzjs/hb.wasm", { from: `${satoriPkgDir}/package.json` });
394
425
  if (resolvedCompatibility.satori === "wasm" || target.replace(RE_LEGACY_SUFFIX, "") === "netlify-edge") {
395
426
  const harfbuzzDir = harfbuzzWasmPath.slice(0, -"hb.wasm".length);
@@ -444,7 +475,7 @@ async function ensureDependencies(dep, nuxt = useNuxt()) {
444
475
 
445
476
  const RE_REFLECT_HAS_MINIFIED = /Reflect\.has\(([\w$]+),([\w$]+)\)\?Reflect\.get\(\1,\2,([\w$]+)\):Reflect\.get\(([\w$]+),\2,\3\)/g;
446
477
  const RE_WASM_IMPORT = /import\("(\.\/wasm\/[^"]+\.wasm)"\)/g;
447
- async function setupBuildHandler(config, resolve, getDetectedRenderers, getCompatibilityMeta = () => ({}), nuxt = useNuxt()) {
478
+ async function setupBuildHandler(config, resolve, getDetectedRenderers, nuxt = useNuxt()) {
448
479
  nuxt.options.nitro.storage = nuxt.options.nitro.storage || {};
449
480
  if (typeof config.runtimeCacheStorage === "object")
450
481
  nuxt.options.nitro.storage["nuxt-og-image"] = config.runtimeCacheStorage;
@@ -464,8 +495,7 @@ async function setupBuildHandler(config, resolve, getDetectedRenderers, getCompa
464
495
  await applyNitroPresetCompatibility(nitroConfig, {
465
496
  compatibility: config.compatibility?.runtime,
466
497
  resolve,
467
- detectedRenderers: getDetectedRenderers(),
468
- metadata: getCompatibilityMeta()
498
+ detectedRenderers: getDetectedRenderers()
469
499
  });
470
500
  nuxt.hooks.hook("nitro:init", async (nitro) => {
471
501
  nitro.hooks.hook("prerender:config", (prerenderConfig) => {
@@ -477,14 +507,17 @@ async function setupBuildHandler(config, resolve, getDetectedRenderers, getCompa
477
507
  await applyNitroPresetCompatibility(nitro.options, {
478
508
  compatibility: config.compatibility?.runtime,
479
509
  resolve,
480
- detectedRenderers: getDetectedRenderers(),
481
- metadata: getCompatibilityMeta()
510
+ detectedRenderers: getDetectedRenderers()
482
511
  });
483
512
  const rollupConfig = nitro.options.rollupConfig ||= { output: {} };
484
513
  rollupConfig.plugins = [rollupConfig.plugins, {
485
514
  name: "og-image-harfbuzz-worker",
486
515
  transform(code, id) {
487
- if (!nitro.options.alias["#og-image/harfbuzz-factory"] || !id.endsWith("/harfbuzzjs/hb.js"))
516
+ if (!nitro.options.alias["#og-image/harfbuzz-factory"])
517
+ return;
518
+ if (/\/satori\/dist\/(?:standalone|index)\.js$/.test(id))
519
+ return { code: externalizeSatoriHarfBuzz(code, id), map: null };
520
+ if (!id.endsWith("/harfbuzzjs/hb.js"))
488
521
  return;
489
522
  const transformed = new MagicString(code);
490
523
  parseAndWalk(code, id, {
@@ -534,9 +567,9 @@ async function setupBuildHandler(config, resolve, getDetectedRenderers, getCompa
534
567
  }
535
568
  }
536
569
  }
537
- const [resvgHash, yogaHash, takumiHash] = await Promise.all([
570
+ const [resvgHash, layoutHash, takumiHash] = await Promise.all([
538
571
  resolveFilePathSha1("@resvg/resvg-wasm/index_bg.wasm"),
539
- resolveFilePathSha1("satori/yoga.wasm"),
572
+ resolveFilePathSha1("satori/layout.wasm"),
540
573
  resolveFilePathSha1("@takumi-rs/wasm/takumi_wasm_bg.wasm")
541
574
  ]);
542
575
  for (const entry of wasmEntries) {
@@ -555,7 +588,7 @@ async function setupBuildHandler(config, resolve, getDetectedRenderers, getCompa
555
588
  if (compatibility.wasm?.esmImport) {
556
589
  const postfix = normalizedTarget === "vercel-edge" ? "?module" : "";
557
590
  const wasmPath = isCloudflarePagesOrModule ? `../wasm/` : `./wasm/`;
558
- contents = contents.replaceAll('"@resvg/resvg-wasm/index_bg.wasm?module"', `"${wasmPath}index_bg-${resvgHash}.wasm${postfix}"`).replaceAll('"satori/yoga.wasm?module"', `"${wasmPath}yoga-${yogaHash}.wasm${postfix}"`).replaceAll('"@takumi-rs/wasm/takumi_wasm_bg.wasm?module"', `"${wasmPath}takumi_wasm_bg-${takumiHash}.wasm${postfix}"`).replaceAll('"yoga-wasm-web/dist/yoga.wasm?module"', `"${wasmPath}yoga-${yogaHash}.wasm${postfix}"`);
591
+ contents = contents.replaceAll('"@resvg/resvg-wasm/index_bg.wasm?module"', `"${wasmPath}index_bg-${resvgHash}.wasm${postfix}"`).replaceAll('"satori/layout.wasm?module"', `"${wasmPath}layout-${layoutHash}.wasm${postfix}"`).replaceAll('"@takumi-rs/wasm/takumi_wasm_bg.wasm?module"', `"${wasmPath}takumi_wasm_bg-${takumiHash}.wasm${postfix}"`);
559
592
  if (postfix) {
560
593
  contents = contents.replaceAll(RE_WASM_IMPORT, `import("$1${postfix}")`);
561
594
  }
@@ -633,754 +666,325 @@ function patchWebAssemblyInstantiate(code) {
633
666
  return code;
634
667
  }
635
668
 
636
- const TAKUMI_CORE_PACKAGE = "@takumi-rs/core";
637
- const TAKUMI_WASM_PACKAGE = "@takumi-rs/wasm";
638
- const PROVIDER_DEPENDENCIES = [
639
- {
640
- name: "satori",
641
- description: "SVG-based renderer using Satori",
642
- bindings: {
643
- "node": [
644
- { name: "satori", description: "HTML to SVG renderer" },
645
- { name: "@resvg/resvg-js", description: "SVG to PNG converter (native)" }
646
- ],
647
- "wasm": [
648
- { name: "satori", description: "HTML to SVG renderer" },
649
- { name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
650
- ],
651
- "wasm-fs": [
652
- { name: "satori", description: "HTML to SVG renderer" },
653
- { name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
654
- ]
655
- }
656
- },
657
- {
658
- name: "takumi",
659
- description: "Rust-based high-performance renderer (recommended)",
660
- bindings: {
661
- "node": [
662
- { name: TAKUMI_CORE_PACKAGE, description: "Native Takumi renderer" }
663
- ],
664
- "wasm": [
665
- { name: TAKUMI_WASM_PACKAGE, description: "WASM Takumi renderer" }
666
- ],
667
- "wasm-fs": [
668
- { name: TAKUMI_WASM_PACKAGE, description: "WASM Takumi renderer" }
669
- ]
670
- }
671
- },
672
- {
673
- name: "browser",
674
- description: "Browser-based screenshot renderer",
675
- bindings: {
676
- node: [
677
- { name: "playwright-core", description: "Headless browser automation" }
678
- ]
679
- }
669
+ const _warnedVars = /* @__PURE__ */ new Set();
670
+ const RE_CSS_DECL = /([\w-]+)\s*:\s*([^\s;][^;]*)(?:;|$)/g;
671
+ const RE_CSS_TOGGLE = /(--[\w-]+)\s*:\s+;/g;
672
+ const RE_CSS_CLASS_RULE = /\.((?:\\[0-9a-f]{1,6}\s?|\\[^0-9a-f]|[^\\\s{])+)\s*\{([^}]+)\}/gi;
673
+ const RE_ROOT_HOST_BLOCK = /:(?:root|host)[^\s{]*\s*\{([^}]+)\}/g;
674
+ const RE_UNIVERSAL_BLOCK = /\*[^{]*\{([^}]+)\}/g;
675
+ const RE_PROPERTY_AT_RULE = /@property\s+(--[\w-]+)\s*\{([^}]+)\}/g;
676
+ const RE_INITIAL_VALUE = /initial-value\s*:\s*([^\s;][^;]*);?/;
677
+ const RE_CSS_ESCAPE_HEX = /\\([0-9a-f]{1,6})\s?/gi;
678
+ const RE_CSS_ESCAPE_CHAR = /\\(.)/g;
679
+ const RE_CSS_ESCAPE_HEX_STRIP = /\\[0-9a-f]{1,6}\s?/gi;
680
+ const RE_CSS_ESCAPE_DOT = /\\./g;
681
+ const RE_COMBINATOR_OR_SPACE = /[\s>+~]/;
682
+ const RE_PSEUDO_CLASS = /:[a-z]/i;
683
+ const RE_CAMEL_CASE = /-([a-z])/g;
684
+ const RE_ONLY_DIGITS = /^\d+$/;
685
+ const RE_IMPORTANT = /\s*!important\s*$/;
686
+ const RE_QUOTES = /^['"]|['"]$/g;
687
+ const RE_WIDTH_VALUE = /width:([^;}]+)/;
688
+ const RE_CALC_VAR_MULTIPLY = /calc\(var\((--[\w-]+)\)\s*\*\s*([\d.]+)\)/;
689
+ const RE_NUMERIC_WITH_UNIT = /([\d.]+)(rem|px|em|%)/;
690
+ const RE_CSS_KEYWORD = /^(?:initial|inherit|unset|revert|revert-layer)$/;
691
+ const RE_UNRESOLVED_VAR = /var\((--[\w-]+)/g;
692
+ const RE_INVALID_CALC = /calc\(\s*[*/]/;
693
+ const RE_COLOR_PROP = /color|fill|stroke|outline|border|background|caret|accent/;
694
+ let transform;
695
+ let Features;
696
+ const LEGACY_TARGETS = {
697
+ chrome: 60 << 16,
698
+ safari: 10 << 16
699
+ };
700
+ async function loadLightningCss() {
701
+ if (!transform) {
702
+ const lcss = await import('lightningcss');
703
+ transform = lcss.transform;
704
+ Features = lcss.Features;
680
705
  }
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(" ")}`;
706
+ return { transform, Features };
692
707
  }
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) };
708
+ async function transformWithLightningCss(css, options = {}) {
709
+ const { transform: transform2 } = await loadLightningCss();
710
+ const {
711
+ minify = true,
712
+ filename = "input.css",
713
+ targets = LEGACY_TARGETS,
714
+ include
715
+ } = options;
716
+ const transformOptions = {
717
+ filename,
718
+ code: Buffer.from(css),
719
+ minify,
720
+ targets,
721
+ drafts: { customMedia: true }
722
+ };
723
+ if (include !== void 0)
724
+ transformOptions.include = include;
725
+ return transform2(transformOptions);
699
726
  }
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`;
701
- async function getInstalledProviders() {
702
- const installed = [];
703
- for (const provider of PROVIDER_DEPENDENCIES) {
704
- if (provider.bindings.node) {
705
- const allNodeInstalled = await Promise.all(
706
- provider.bindings.node.map((dep) => hasResolvableDependency(dep.name))
707
- );
708
- if (allNodeInstalled.every(Boolean)) {
709
- installed.push({ provider: provider.name, binding: "node" });
710
- continue;
711
- }
727
+ function extractDeclarations(body, onDeclaration) {
728
+ const seen = /* @__PURE__ */ new Set();
729
+ for (const match of body.matchAll(RE_CSS_DECL)) {
730
+ if (match[1] && match[2] && !seen.has(match[1])) {
731
+ seen.add(match[1]);
732
+ onDeclaration(match[1], match[2].trim());
712
733
  }
713
- if (provider.bindings.wasm) {
714
- const allWasmInstalled = await Promise.all(
715
- provider.bindings.wasm.map((dep) => hasResolvableDependency(dep.name))
716
- );
717
- if (allWasmInstalled.every(Boolean)) {
718
- installed.push({ provider: provider.name, binding: "wasm" });
719
- }
734
+ }
735
+ for (const match of body.matchAll(RE_CSS_TOGGLE)) {
736
+ if (match[1] && !seen.has(match[1])) {
737
+ seen.add(match[1]);
738
+ onDeclaration(match[1], " ");
720
739
  }
721
740
  }
722
- return installed;
723
741
  }
724
- async function getMissingDependencies(provider, binding = "node") {
725
- const missing = await getMissingProviderDependencyDefinitions(provider, binding);
726
- return missing.map((d) => d.name);
727
- }
728
- async function getMissingProviderDependencyDefinitions(provider, binding = "node") {
729
- const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
730
- if (!providerDef)
731
- return [];
732
- const deps = providerDef.bindings[binding] || providerDef.bindings.node || [];
733
- const missing = [];
734
- for (const dep of deps) {
735
- if (!await hasResolvableDependency(dep.name))
736
- missing.push(dep);
742
+ function walkBlockRules(css, selectorRe, onBlock) {
743
+ for (const match of css.matchAll(selectorRe)) {
744
+ if (match[1])
745
+ onBlock(match[1]);
737
746
  }
738
- return missing;
739
- }
740
- function getDependencyDefinitions(provider, binding = "node") {
741
- const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
742
- if (!providerDef)
743
- return [];
744
- return providerDef.bindings[binding] || providerDef.bindings.node || [];
745
747
  }
746
- function getDependencyInstallSpec(dep) {
747
- return dep.installSpec || dep.name;
748
+ function walkClassRules(css, onRule) {
749
+ for (const match of css.matchAll(RE_CSS_CLASS_RULE)) {
750
+ const rawSelector = match[1];
751
+ if (isSimpleClassSelector(rawSelector))
752
+ onRule(decodeCssClassName(rawSelector), match[2]);
753
+ }
748
754
  }
749
- async function getMissingDependencyInstallSpecs(provider, binding = "node") {
750
- const missing = await getMissingProviderDependencyDefinitions(provider, binding);
751
- return missing.map(getDependencyInstallSpec);
755
+ async function simplifyCss(css) {
756
+ const result = await transformWithLightningCss(css, { minify: true });
757
+ return result.code.toString();
752
758
  }
753
- function getProviderDependencyInstallSpecs(provider, binding = "node") {
754
- return getDependencyDefinitions(provider, binding).map(getDependencyInstallSpec);
759
+ function extractCssVars(css) {
760
+ const vars = /* @__PURE__ */ new Map();
761
+ walkBlockRules(css, RE_ROOT_HOST_BLOCK, (body) => {
762
+ extractDeclarations(body, (prop, value) => {
763
+ if (prop.startsWith("--"))
764
+ vars.set(prop, value);
765
+ });
766
+ });
767
+ return vars;
755
768
  }
756
- function getRecommendedBindingFromPreset(provider) {
757
- const preset = resolveOgImagePreset();
758
- const compatibility = getPresetNitroPresetCompatibility(preset);
759
- return getRecommendedBinding(provider, compatibility);
769
+ function extractUniversalVars(css) {
770
+ const vars = /* @__PURE__ */ new Map();
771
+ walkBlockRules(css, RE_UNIVERSAL_BLOCK, (body) => {
772
+ extractDeclarations(body, (prop, value) => {
773
+ if (prop.startsWith("--"))
774
+ vars.set(prop, value);
775
+ });
776
+ });
777
+ return vars;
760
778
  }
761
- function getRecommendedBinding(provider, compatibility) {
762
- if (provider === "satori") {
763
- const resvgBinding = compatibility.resvg;
764
- if (resvgBinding === "wasm-fs")
765
- return "wasm-fs";
766
- if (resvgBinding === "wasm")
767
- return "wasm";
768
- return "node";
769
- }
770
- if (provider === "takumi") {
771
- const takumiBinding = compatibility.takumi;
772
- if (takumiBinding === "wasm")
773
- return "wasm";
774
- return "node";
775
- }
776
- if (provider === "browser") {
777
- return "node";
779
+ function extractPropertyInitialValues(css) {
780
+ const vars = /* @__PURE__ */ new Map();
781
+ for (const match of css.matchAll(RE_PROPERTY_AT_RULE)) {
782
+ const initialMatch = match[2].match(RE_INITIAL_VALUE);
783
+ if (initialMatch?.[1])
784
+ vars.set(match[1], initialMatch[1].trim());
778
785
  }
779
- return "node";
786
+ return vars;
780
787
  }
781
- async function ensureProviderDependencies(provider, binding, nuxt) {
782
- const missingDeps = await getMissingProviderDependencyDefinitions(provider, binding);
783
- if (missingDeps.length === 0)
784
- return { success: true, installed: [] };
785
- const missingInstallSpecs = missingDeps.map(getDependencyInstallSpec);
786
- const pm = await detectPackageManager(nuxt.options.rootDir);
787
- const pmName = pm?.name || "npm";
788
- logger.info(`Installing ${provider} dependencies: ${missingInstallSpecs.join(", ")}`);
789
- const installed = [];
790
- for (const dep of missingDeps) {
791
- const installSpec = getDependencyInstallSpec(dep);
792
- const success = await addDependency(installSpec, {
793
- cwd: nuxt.options.rootDir,
794
- dev: false
795
- }).then(() => {
796
- installed.push(installSpec);
797
- return true;
798
- }).catch(() => {
799
- logger.error(`Failed to install ${installSpec}. Run manually: ${pmName} add ${installSpec}`);
800
- return false;
788
+ function extractPerClassVars(css) {
789
+ const result = /* @__PURE__ */ new Map();
790
+ walkClassRules(css, (className, body) => {
791
+ const classVars = /* @__PURE__ */ new Map();
792
+ extractDeclarations(body, (prop, value) => {
793
+ if (prop.startsWith("--"))
794
+ classVars.set(prop, value);
801
795
  });
802
- if (!success)
803
- return { success: false, installed };
796
+ if (classVars.size > 0)
797
+ result.set(className, classVars);
798
+ });
799
+ return result;
800
+ }
801
+ function serializeTokens(tokens) {
802
+ const parts = [];
803
+ for (const tov of tokens) {
804
+ switch (tov.type) {
805
+ case "token":
806
+ parts.push(serializeToken(tov.value));
807
+ break;
808
+ case "color":
809
+ parts.push(serializeColor(tov.value));
810
+ break;
811
+ case "unresolved-color":
812
+ parts.push(serializeUnresolvedColor(tov.value));
813
+ break;
814
+ case "var":
815
+ parts.push(`var(${tov.value.name.ident}${tov.value.fallback ? `, ${serializeTokens(tov.value.fallback)}` : ""})`);
816
+ break;
817
+ case "env":
818
+ parts.push(`env(${serializeEnvName(tov.value.name)}${tov.value.fallback ? `, ${serializeTokens(tov.value.fallback)}` : ""})`);
819
+ break;
820
+ case "function":
821
+ parts.push(`${tov.value.name}(${serializeTokens(tov.value.arguments)})`);
822
+ break;
823
+ case "length":
824
+ parts.push(`${tov.value.value}${tov.value.unit}`);
825
+ break;
826
+ case "angle":
827
+ parts.push(`${tov.value.value}${tov.value.type}`);
828
+ break;
829
+ case "time":
830
+ parts.push(`${tov.value.value}${tov.value.type === "seconds" ? "s" : "ms"}`);
831
+ break;
832
+ case "resolution":
833
+ parts.push(`${tov.value.value}${tov.value.type}`);
834
+ break;
835
+ case "percentage":
836
+ parts.push(`${tov.value * 100}%`);
837
+ break;
838
+ case "dashed-ident":
839
+ parts.push(tov.value);
840
+ break;
841
+ case "url":
842
+ parts.push(`url(${tov.value.url})`);
843
+ break;
844
+ case "animation-name":
845
+ parts.push(typeof tov.value === "string" ? tov.value : tov.value.value || "");
846
+ break;
847
+ }
804
848
  }
805
- return { success: true, installed };
849
+ return parts.join("");
806
850
  }
807
- function canPromptInteractively(env = {
808
- hasTTY,
809
- hasStdinTTY: Boolean(process.stdin?.isTTY),
810
- isAgent,
811
- isCI
812
- }) {
813
- return env.hasTTY && env.hasStdinTTY && !env.isAgent && !env.isCI;
851
+ function serializeToken(token) {
852
+ switch (token.type) {
853
+ case "ident":
854
+ return token.value;
855
+ case "number":
856
+ return String(token.value);
857
+ case "percentage":
858
+ return `${token.value * 100}%`;
859
+ case "dimension":
860
+ return `${token.value}${token.unit}`;
861
+ case "string":
862
+ return `"${token.value}"`;
863
+ case "hash":
864
+ case "id-hash":
865
+ return `#${token.value}`;
866
+ case "white-space":
867
+ return " ";
868
+ case "delim":
869
+ return token.value;
870
+ case "comma":
871
+ return ", ";
872
+ case "colon":
873
+ return ":";
874
+ case "semicolon":
875
+ return ";";
876
+ case "unquoted-url":
877
+ return token.value;
878
+ case "at-keyword":
879
+ return `@${token.value}`;
880
+ default:
881
+ return "";
882
+ }
814
883
  }
815
- async function validateProviderSetup(renderer, compatibility) {
816
- const issues = [];
817
- const binding = getRecommendedBinding(renderer, compatibility);
818
- const missing = await getMissingDependencyInstallSpecs(renderer, binding);
819
- if (missing.length > 0) {
820
- issues.push(`Missing ${renderer} dependencies: ${missing.join(", ")}`);
884
+ function serializeColor(color) {
885
+ if (typeof color === "string")
886
+ return color;
887
+ const a = color.alpha !== void 0 && color.alpha !== 1 ? ` / ${color.alpha}` : "";
888
+ switch (color.type) {
889
+ case "currentcolor":
890
+ return "currentColor";
891
+ case "rgb":
892
+ return color.alpha !== 1 ? `rgba(${color.r}, ${color.g}, ${color.b}, ${color.alpha})` : `rgb(${color.r}, ${color.g}, ${color.b})`;
893
+ case "hsl":
894
+ return `hsl(${color.h} ${color.s}% ${color.l}%${a})`;
895
+ case "hwb":
896
+ return `hwb(${color.h} ${color.w}% ${color.b}%${a})`;
897
+ case "lab":
898
+ return `lab(${color.l} ${color.a} ${color.b}${a})`;
899
+ case "lch":
900
+ return `lch(${color.l} ${color.c} ${color.h}${a})`;
901
+ case "oklab":
902
+ return `oklab(${color.l} ${color.a} ${color.b}${a})`;
903
+ case "oklch":
904
+ return `oklch(${color.l} ${color.c} ${color.h}${a})`;
905
+ case "light-dark":
906
+ return `light-dark(${serializeColor(color.light)}, ${serializeColor(color.dark)})`;
907
+ default:
908
+ if ("x" in color)
909
+ return `color(${color.type} ${color.x} ${color.y} ${color.z}${a})`;
910
+ if ("r" in color)
911
+ return `color(${color.type} ${color.r} ${color.g} ${color.b}${a})`;
912
+ return "";
821
913
  }
822
- if (renderer === "satori") {
823
- const hasResvgNode = await hasResolvableDependency("@resvg/resvg-js");
824
- const hasResvgWasm = await hasResolvableDependency("@resvg/resvg-wasm");
825
- if (!hasResvgNode && !hasResvgWasm) {
826
- issues.push("Satori requires either @resvg/resvg-js (node) or @resvg/resvg-wasm to render PNGs");
914
+ }
915
+ function serializeUnresolvedColor(color) {
916
+ const alpha = serializeTokens(color.alpha);
917
+ switch (color.type) {
918
+ case "rgb":
919
+ return `rgb(${color.r}, ${color.g}, ${color.b}, ${alpha})`;
920
+ case "hsl":
921
+ return `hsl(${color.h} ${color.s}% ${color.l}% / ${alpha})`;
922
+ case "light-dark":
923
+ return `light-dark(${serializeTokens(color.light)}, ${serializeTokens(color.dark)})`;
924
+ default:
925
+ return "";
926
+ }
927
+ }
928
+ function serializeEnvName(name) {
929
+ if (name.type === "ua")
930
+ return name.value;
931
+ return name.ident || "";
932
+ }
933
+ function resolveExtractedVars(extracted, rootAttrs) {
934
+ const vars = new Map(extracted.rootVars);
935
+ const sorted = extracted.qualifiedRules.toSorted((a, b) => a.reqs.length - b.reqs.length);
936
+ for (const rule of sorted) {
937
+ const matches = rule.reqs.every((req) => {
938
+ const actual = rootAttrs[req.name];
939
+ return req.negated ? actual !== req.value : actual === req.value;
940
+ });
941
+ if (matches) {
942
+ for (const [name, value] of rule.vars)
943
+ vars.set(name, value);
827
944
  }
828
945
  }
829
- if (renderer === "browser") {
830
- const hasPlaywright = await hasResolvableDependency("playwright-core");
831
- if (!hasPlaywright && binding === "node") {
832
- issues.push("Browser renderer requires playwright-core");
946
+ return vars;
947
+ }
948
+ function selectorsContain(selectors, predicate) {
949
+ for (const selector of selectors) {
950
+ for (const component of selector) {
951
+ if (predicate(component))
952
+ return true;
833
953
  }
834
954
  }
835
- return { valid: issues.length === 0, issues };
955
+ return false;
836
956
  }
837
-
838
- async function setupDevHandler(options, resolve, getDetectedRenderers, getCompatibilityMeta = () => ({}), nuxt = useNuxt()) {
839
- const applyCompatibility = async (nitroConfig) => {
840
- const detectedRenderers = new Set(getDetectedRenderers());
841
- const targetCompatibility = getPresetNitroPresetCompatibility(resolveOgImagePreset(nitroConfig));
842
- for (const renderer of ["satori", "takumi", "browser"]) {
843
- if (!detectedRenderers.has(renderer)) {
844
- const binding = getRecommendedBinding(renderer, targetCompatibility);
845
- const missing = await getMissingDependencies(renderer, binding);
846
- if (missing.length === 0)
847
- detectedRenderers.add(renderer);
957
+ function isRootOrHost(selectors) {
958
+ return selectorsContain(selectors, (c) => c.type === "pseudo-class" && (c.kind === "root" || c.kind === "host"));
959
+ }
960
+ function isBareThemeClass(selectors) {
961
+ return selectorsContain(selectors, (c) => c.type === "class" && (c.name === "dark" || c.name === "light"));
962
+ }
963
+ function isUniversal(selectors) {
964
+ return selectorsContain(selectors, (c) => c.type === "universal");
965
+ }
966
+ function extractSingleSelectorReqs(components) {
967
+ const reqs = [];
968
+ for (const component of components) {
969
+ if (component.type === "class") {
970
+ if (component.name === "dark" || component.name === "light")
971
+ reqs.push({ name: "data-theme", value: component.name, negated: false });
972
+ }
973
+ if (component.type === "attribute" && component.operation) {
974
+ const name = component.name;
975
+ const value = component.operation.value;
976
+ if (name && value)
977
+ reqs.push({ name, value, negated: false });
978
+ }
979
+ if (component.type === "pseudo-class" && component.kind === "not") {
980
+ for (const innerSelector of component.selectors || []) {
981
+ const innerReqs = extractSingleSelectorReqs(innerSelector);
982
+ for (const req of innerReqs)
983
+ reqs.push({ ...req, negated: !req.negated });
848
984
  }
849
985
  }
850
- await applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.dev, resolve, detectedRenderers, metadata: getCompatibilityMeta() });
851
- };
852
- await applyCompatibility(nuxt.options.nitro);
853
- nuxt.hooks.hook("nitro:init", (nitro) => applyCompatibility(nitro.options));
854
- }
855
-
856
- const RE_TILDE_SLASH = /^~\//;
857
- const RE_WORD_CHARS_ONLY = /^\w+$/;
858
- const RE_SCRIPT_SETUP = /<script\s+setup[^>]*>\n?/;
859
- const SCRIPT_BLOCK = `<script setup lang="ts">
860
- const { title = 'My Page', description = '' } = defineProps<{
861
- title?: string
862
- description?: string
863
- }>()
864
- <\/script>`;
865
- function getStarterTemplate(renderer, css) {
866
- if (renderer === "satori") {
867
- return `${SCRIPT_BLOCK}
868
-
869
- <template>
870
- <div :style="{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%', padding: '60px', backgroundColor: 'white', color: '#171717' }">
871
- <h1 :style="{ fontSize: '72px', fontWeight: 'bold', margin: 0, lineHeight: 1.1, textAlign: 'center' }">
872
- {{ title }}
873
- </h1>
874
- <p v-if="description" :style="{ fontSize: '32px', opacity: 0.6, marginTop: '16px', textAlign: 'center', maxWidth: '900px' }">
875
- {{ description }}
876
- </p>
877
- </div>
878
- </template>
879
- `;
880
- }
881
- if (css === "tailwind" || css === "unocss") {
882
- return `${SCRIPT_BLOCK}
883
-
884
- <template>
885
- <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">
886
- <h1 class="text-[72px] font-bold m-0 leading-tight text-center" style="text-wrap: balance;">
887
- {{ title }}
888
- </h1>
889
- <p v-if="description" class="text-[32px] opacity-60 mt-4 text-center max-w-[900px]">
890
- {{ description }}
891
- </p>
892
- </div>
893
- </template>
894
- `;
895
- }
896
- return `${SCRIPT_BLOCK}
897
-
898
- <template>
899
- <div class="container">
900
- <h1>{{ title }}</h1>
901
- <p v-if="description">
902
- {{ description }}
903
- </p>
904
- </div>
905
- </template>
906
-
907
- <style scoped>
908
- .container {
909
- width: 100%;
910
- height: 100%;
911
- display: flex;
912
- flex-direction: column;
913
- justify-content: center;
914
- align-items: center;
915
- padding: 60px;
916
- background: white;
917
- color: #171717;
918
- }
919
- h1 {
920
- font-size: 72px;
921
- font-weight: bold;
922
- margin: 0;
923
- line-height: 1.1;
924
- text-align: center;
925
- text-wrap: balance;
926
- }
927
- p {
928
- font-size: 32px;
929
- opacity: 0.6;
930
- margin-top: 16px;
931
- text-align: center;
932
- max-width: 900px;
933
- }
934
- </style>
935
- `;
936
- }
937
- function setupDevToolsUI(options, resolve$1, nuxt = useNuxt(), cssFramework = "none") {
938
- setupDevToolsUI$1({
939
- route: "/__nuxt-og-image",
940
- name: "nuxt-og-image",
941
- title: "OG Image",
942
- icon: "carbon:image-search"
943
- }, resolve$1, nuxt);
944
- const communityTemplatesDir = resolve$1("./runtime/app/components/Templates/Community");
945
- const useNitro = new Promise((resolve2) => {
946
- nuxt.hooks.hook("nitro:init", resolve2);
947
- });
948
- async function insertDefineOgImage(componentName, pageFile) {
949
- const resolved = pageFile ? resolve(pageFile) : "";
950
- if (!resolved || !resolved.startsWith(nuxt.options.srcDir) || !existsSync(resolved))
951
- return false;
952
- const content = await readFile(resolved, "utf-8");
953
- if (content.includes("defineOgImage"))
954
- return false;
955
- const call = `defineOgImage('${componentName}')`;
956
- const scriptSetupMatch = content.match(RE_SCRIPT_SETUP);
957
- if (scriptSetupMatch) {
958
- const insertPos = scriptSetupMatch.index + scriptSetupMatch[0].length;
959
- await writeFile(resolved, `${content.slice(0, insertPos)}${call}
960
- ${content.slice(insertPos)}`, "utf-8");
961
- } else {
962
- await writeFile(resolved, `<script setup lang="ts">
963
- ${call}
964
- <\/script>
965
-
966
- ${content}`, "utf-8");
967
- }
968
- return true;
969
- }
970
- setupDevToolsRpc("nuxt-og-image", {
971
- async ejectCommunityTemplate(path) {
972
- const [dirName, componentName] = path.split("/");
973
- const nameWithoutExt = componentName?.replace(".vue", "") || "";
974
- let dotNotationName;
975
- if (nameWithoutExt.includes(".")) {
976
- dotNotationName = `${nameWithoutExt}.vue`;
977
- } else {
978
- const rendererMatch = nameWithoutExt.match(RE_RENDERER_SUFFIX);
979
- const renderer = rendererMatch?.[1]?.toLowerCase() || "satori";
980
- const baseName = nameWithoutExt.replace(RE_RENDERER_SUFFIX, "");
981
- dotNotationName = `${baseName}.${renderer}.vue`;
982
- }
983
- const dir = join(nuxt.options.srcDir, "components", dirName || "");
984
- if (!existsSync(dir)) {
985
- mkdirSync(dir, { recursive: true });
986
- }
987
- const newPath = join(dir, dotNotationName);
988
- const templatePath = join(communityTemplatesDir, dotNotationName);
989
- const template = (await readFile(templatePath, "utf-8")).replace("{{ title }}", `{{ title }} - Ejected!`);
990
- await writeFile(newPath, template, { encoding: "utf-8" });
991
- await updateTemplates({ filter: (t) => t.filename.includes("nuxt-og-image/components.mjs") });
992
- const nitro = await useNitro;
993
- await nitro.hooks.callHook("rollup:reload");
994
- return newPath;
995
- },
996
- async createComponent({ name, renderer, pageFile }) {
997
- const validRenderers = ["satori", "browser", "takumi"];
998
- if (!validRenderers.includes(renderer))
999
- throw new Error(`Invalid renderer: ${renderer}`);
1000
- if (!RE_WORD_CHARS_ONLY.test(name))
1001
- throw new Error(`Invalid component name: ${name}`);
1002
- const baseDir = existsSync(join(nuxt.options.srcDir, "app")) ? join(nuxt.options.srcDir, "app") : nuxt.options.srcDir;
1003
- const outputDir = join(baseDir, "components", "OgImage");
1004
- if (!existsSync(outputDir))
1005
- mkdirSync(outputDir, { recursive: true });
1006
- const filename = `${name}.${renderer}.vue`;
1007
- const outputPath = join(outputDir, filename);
1008
- if (existsSync(outputPath))
1009
- throw new Error(`File already exists: ${relative(nuxt.options.rootDir, outputPath)}`);
1010
- const template = getStarterTemplate(renderer, cssFramework);
1011
- await writeFile(outputPath, template, { encoding: "utf-8" });
1012
- await insertDefineOgImage(name, pageFile);
1013
- await updateTemplates({ filter: (t) => t.filename.includes("nuxt-og-image/components.mjs") });
1014
- const nitro = await useNitro;
1015
- await nitro.hooks.callHook("rollup:reload");
1016
- return outputPath;
1017
- },
1018
- async addOgImageToPage(componentName, pageFile) {
1019
- if (!RE_WORD_CHARS_ONLY.test(componentName))
1020
- throw new Error(`Invalid component name: ${componentName}`);
1021
- return insertDefineOgImage(componentName, pageFile);
1022
- }
1023
- }, nuxt).then((rpc) => {
1024
- const safeBroadcast = (fn) => {
1025
- try {
1026
- const result = fn();
1027
- if (result && typeof result.then === "function") {
1028
- result.catch((err) => {
1029
- void err;
1030
- });
1031
- }
1032
- } catch {
1033
- }
1034
- };
1035
- let cssRefreshTimer;
1036
- nuxt.hook("builder:watch", (e, watchPath) => {
1037
- if (!e || !watchPath)
1038
- return;
1039
- const normalizedPath = relative(nuxt.options.srcDir, isAbsolute(watchPath) ? watchPath : resolve(nuxt.options.srcDir, watchPath));
1040
- const absolutePath = isAbsolute(watchPath) ? watchPath : join(nuxt.options.rootDir, watchPath);
1041
- const isCssChange = absolutePath.endsWith(".css") && nuxt.options.css.some((entry) => {
1042
- const src = typeof entry === "string" ? entry : entry?.src;
1043
- return src && absolutePath.endsWith(src.replace(RE_TILDE_SLASH, ""));
1044
- });
1045
- if (isCssChange || normalizedPath.includes("uno.config")) {
1046
- clearTimeout(cssRefreshTimer);
1047
- cssRefreshTimer = setTimeout(() => {
1048
- safeBroadcast(() => rpc.broadcast.refresh.asEvent());
1049
- }, 200);
1050
- return;
1051
- }
1052
- if ((e === "change" || e.includes("link")) && (normalizedPath.startsWith("pages") || normalizedPath.startsWith("content"))) {
1053
- safeBroadcast(() => rpc.broadcast.refreshRouteData.asEvent(normalizedPath));
1054
- }
1055
- if (options.componentDirs.some((dir) => normalizedPath.includes(dir))) {
1056
- if (e === "change")
1057
- safeBroadcast(() => rpc.broadcast.refresh.asEvent());
1058
- else
1059
- safeBroadcast(() => rpc.broadcast.refreshGlobalData.asEvent());
1060
- }
1061
- });
1062
- });
1063
- }
1064
-
1065
- const _warnedVars = /* @__PURE__ */ new Set();
1066
- const RE_CSS_DECL = /([\w-]+)\s*:\s*([^\s;][^;]*)(?:;|$)/g;
1067
- const RE_CSS_TOGGLE = /(--[\w-]+)\s*:\s+;/g;
1068
- const RE_CSS_CLASS_RULE = /\.((?:\\[0-9a-f]{1,6}\s?|\\[^0-9a-f]|[^\\\s{])+)\s*\{([^}]+)\}/gi;
1069
- const RE_ROOT_HOST_BLOCK = /:(?:root|host)[^\s{]*\s*\{([^}]+)\}/g;
1070
- const RE_UNIVERSAL_BLOCK = /\*[^{]*\{([^}]+)\}/g;
1071
- const RE_PROPERTY_AT_RULE = /@property\s+(--[\w-]+)\s*\{([^}]+)\}/g;
1072
- const RE_INITIAL_VALUE = /initial-value\s*:\s*([^\s;][^;]*);?/;
1073
- const RE_CSS_ESCAPE_HEX = /\\([0-9a-f]{1,6})\s?/gi;
1074
- const RE_CSS_ESCAPE_CHAR = /\\(.)/g;
1075
- const RE_CSS_ESCAPE_HEX_STRIP = /\\[0-9a-f]{1,6}\s?/gi;
1076
- const RE_CSS_ESCAPE_DOT = /\\./g;
1077
- const RE_COMBINATOR_OR_SPACE = /[\s>+~]/;
1078
- const RE_PSEUDO_CLASS = /:[a-z]/i;
1079
- const RE_CAMEL_CASE = /-([a-z])/g;
1080
- const RE_ONLY_DIGITS = /^\d+$/;
1081
- const RE_IMPORTANT = /\s*!important\s*$/;
1082
- const RE_QUOTES = /^['"]|['"]$/g;
1083
- const RE_WIDTH_VALUE = /width:([^;}]+)/;
1084
- const RE_CALC_VAR_MULTIPLY = /calc\(var\((--[\w-]+)\)\s*\*\s*([\d.]+)\)/;
1085
- const RE_NUMERIC_WITH_UNIT = /([\d.]+)(rem|px|em|%)/;
1086
- const RE_CSS_KEYWORD = /^(?:initial|inherit|unset|revert|revert-layer)$/;
1087
- const RE_UNRESOLVED_VAR = /var\((--[\w-]+)/g;
1088
- const RE_INVALID_CALC = /calc\(\s*[*/]/;
1089
- const RE_COLOR_PROP = /color|fill|stroke|outline|border|background|caret|accent/;
1090
- let transform;
1091
- let Features;
1092
- const LEGACY_TARGETS = {
1093
- chrome: 60 << 16,
1094
- safari: 10 << 16
1095
- };
1096
- async function loadLightningCss() {
1097
- if (!transform) {
1098
- const lcss = await import('lightningcss');
1099
- transform = lcss.transform;
1100
- Features = lcss.Features;
1101
- }
1102
- return { transform, Features };
1103
- }
1104
- async function transformWithLightningCss(css, options = {}) {
1105
- const { transform: transform2 } = await loadLightningCss();
1106
- const {
1107
- minify = true,
1108
- filename = "input.css",
1109
- targets = LEGACY_TARGETS,
1110
- include
1111
- } = options;
1112
- const transformOptions = {
1113
- filename,
1114
- code: Buffer.from(css),
1115
- minify,
1116
- targets,
1117
- drafts: { customMedia: true }
1118
- };
1119
- if (include !== void 0)
1120
- transformOptions.include = include;
1121
- return transform2(transformOptions);
1122
- }
1123
- function extractDeclarations(body, onDeclaration) {
1124
- const seen = /* @__PURE__ */ new Set();
1125
- for (const match of body.matchAll(RE_CSS_DECL)) {
1126
- if (match[1] && match[2] && !seen.has(match[1])) {
1127
- seen.add(match[1]);
1128
- onDeclaration(match[1], match[2].trim());
1129
- }
1130
- }
1131
- for (const match of body.matchAll(RE_CSS_TOGGLE)) {
1132
- if (match[1] && !seen.has(match[1])) {
1133
- seen.add(match[1]);
1134
- onDeclaration(match[1], " ");
1135
- }
1136
- }
1137
- }
1138
- function walkBlockRules(css, selectorRe, onBlock) {
1139
- for (const match of css.matchAll(selectorRe)) {
1140
- if (match[1])
1141
- onBlock(match[1]);
1142
- }
1143
- }
1144
- function walkClassRules(css, onRule) {
1145
- for (const match of css.matchAll(RE_CSS_CLASS_RULE)) {
1146
- const rawSelector = match[1];
1147
- if (isSimpleClassSelector(rawSelector))
1148
- onRule(decodeCssClassName(rawSelector), match[2]);
1149
- }
1150
- }
1151
- async function simplifyCss(css) {
1152
- const result = await transformWithLightningCss(css, { minify: true });
1153
- return result.code.toString();
1154
- }
1155
- function extractCssVars(css) {
1156
- const vars = /* @__PURE__ */ new Map();
1157
- walkBlockRules(css, RE_ROOT_HOST_BLOCK, (body) => {
1158
- extractDeclarations(body, (prop, value) => {
1159
- if (prop.startsWith("--"))
1160
- vars.set(prop, value);
1161
- });
1162
- });
1163
- return vars;
1164
- }
1165
- function extractUniversalVars(css) {
1166
- const vars = /* @__PURE__ */ new Map();
1167
- walkBlockRules(css, RE_UNIVERSAL_BLOCK, (body) => {
1168
- extractDeclarations(body, (prop, value) => {
1169
- if (prop.startsWith("--"))
1170
- vars.set(prop, value);
1171
- });
1172
- });
1173
- return vars;
1174
- }
1175
- function extractPropertyInitialValues(css) {
1176
- const vars = /* @__PURE__ */ new Map();
1177
- for (const match of css.matchAll(RE_PROPERTY_AT_RULE)) {
1178
- const initialMatch = match[2].match(RE_INITIAL_VALUE);
1179
- if (initialMatch?.[1])
1180
- vars.set(match[1], initialMatch[1].trim());
1181
- }
1182
- return vars;
1183
- }
1184
- function extractPerClassVars(css) {
1185
- const result = /* @__PURE__ */ new Map();
1186
- walkClassRules(css, (className, body) => {
1187
- const classVars = /* @__PURE__ */ new Map();
1188
- extractDeclarations(body, (prop, value) => {
1189
- if (prop.startsWith("--"))
1190
- classVars.set(prop, value);
1191
- });
1192
- if (classVars.size > 0)
1193
- result.set(className, classVars);
1194
- });
1195
- return result;
1196
- }
1197
- function serializeTokens(tokens) {
1198
- const parts = [];
1199
- for (const tov of tokens) {
1200
- switch (tov.type) {
1201
- case "token":
1202
- parts.push(serializeToken(tov.value));
1203
- break;
1204
- case "color":
1205
- parts.push(serializeColor(tov.value));
1206
- break;
1207
- case "unresolved-color":
1208
- parts.push(serializeUnresolvedColor(tov.value));
1209
- break;
1210
- case "var":
1211
- parts.push(`var(${tov.value.name.ident}${tov.value.fallback ? `, ${serializeTokens(tov.value.fallback)}` : ""})`);
1212
- break;
1213
- case "env":
1214
- parts.push(`env(${serializeEnvName(tov.value.name)}${tov.value.fallback ? `, ${serializeTokens(tov.value.fallback)}` : ""})`);
1215
- break;
1216
- case "function":
1217
- parts.push(`${tov.value.name}(${serializeTokens(tov.value.arguments)})`);
1218
- break;
1219
- case "length":
1220
- parts.push(`${tov.value.value}${tov.value.unit}`);
1221
- break;
1222
- case "angle":
1223
- parts.push(`${tov.value.value}${tov.value.type}`);
1224
- break;
1225
- case "time":
1226
- parts.push(`${tov.value.value}${tov.value.type === "seconds" ? "s" : "ms"}`);
1227
- break;
1228
- case "resolution":
1229
- parts.push(`${tov.value.value}${tov.value.type}`);
1230
- break;
1231
- case "percentage":
1232
- parts.push(`${tov.value * 100}%`);
1233
- break;
1234
- case "dashed-ident":
1235
- parts.push(tov.value);
1236
- break;
1237
- case "url":
1238
- parts.push(`url(${tov.value.url})`);
1239
- break;
1240
- case "animation-name":
1241
- parts.push(typeof tov.value === "string" ? tov.value : tov.value.value || "");
1242
- break;
1243
- }
1244
- }
1245
- return parts.join("");
1246
- }
1247
- function serializeToken(token) {
1248
- switch (token.type) {
1249
- case "ident":
1250
- return token.value;
1251
- case "number":
1252
- return String(token.value);
1253
- case "percentage":
1254
- return `${token.value * 100}%`;
1255
- case "dimension":
1256
- return `${token.value}${token.unit}`;
1257
- case "string":
1258
- return `"${token.value}"`;
1259
- case "hash":
1260
- case "id-hash":
1261
- return `#${token.value}`;
1262
- case "white-space":
1263
- return " ";
1264
- case "delim":
1265
- return token.value;
1266
- case "comma":
1267
- return ", ";
1268
- case "colon":
1269
- return ":";
1270
- case "semicolon":
1271
- return ";";
1272
- case "unquoted-url":
1273
- return token.value;
1274
- case "at-keyword":
1275
- return `@${token.value}`;
1276
- default:
1277
- return "";
1278
- }
1279
- }
1280
- function serializeColor(color) {
1281
- if (typeof color === "string")
1282
- return color;
1283
- const a = color.alpha !== void 0 && color.alpha !== 1 ? ` / ${color.alpha}` : "";
1284
- switch (color.type) {
1285
- case "currentcolor":
1286
- return "currentColor";
1287
- case "rgb":
1288
- return color.alpha !== 1 ? `rgba(${color.r}, ${color.g}, ${color.b}, ${color.alpha})` : `rgb(${color.r}, ${color.g}, ${color.b})`;
1289
- case "hsl":
1290
- return `hsl(${color.h} ${color.s}% ${color.l}%${a})`;
1291
- case "hwb":
1292
- return `hwb(${color.h} ${color.w}% ${color.b}%${a})`;
1293
- case "lab":
1294
- return `lab(${color.l} ${color.a} ${color.b}${a})`;
1295
- case "lch":
1296
- return `lch(${color.l} ${color.c} ${color.h}${a})`;
1297
- case "oklab":
1298
- return `oklab(${color.l} ${color.a} ${color.b}${a})`;
1299
- case "oklch":
1300
- return `oklch(${color.l} ${color.c} ${color.h}${a})`;
1301
- case "light-dark":
1302
- return `light-dark(${serializeColor(color.light)}, ${serializeColor(color.dark)})`;
1303
- default:
1304
- if ("x" in color)
1305
- return `color(${color.type} ${color.x} ${color.y} ${color.z}${a})`;
1306
- if ("r" in color)
1307
- return `color(${color.type} ${color.r} ${color.g} ${color.b}${a})`;
1308
- return "";
1309
- }
1310
- }
1311
- function serializeUnresolvedColor(color) {
1312
- const alpha = serializeTokens(color.alpha);
1313
- switch (color.type) {
1314
- case "rgb":
1315
- return `rgb(${color.r}, ${color.g}, ${color.b}, ${alpha})`;
1316
- case "hsl":
1317
- return `hsl(${color.h} ${color.s}% ${color.l}% / ${alpha})`;
1318
- case "light-dark":
1319
- return `light-dark(${serializeTokens(color.light)}, ${serializeTokens(color.dark)})`;
1320
- default:
1321
- return "";
1322
- }
1323
- }
1324
- function serializeEnvName(name) {
1325
- if (name.type === "ua")
1326
- return name.value;
1327
- return name.ident || "";
1328
- }
1329
- function resolveExtractedVars(extracted, rootAttrs) {
1330
- const vars = new Map(extracted.rootVars);
1331
- const sorted = extracted.qualifiedRules.toSorted((a, b) => a.reqs.length - b.reqs.length);
1332
- for (const rule of sorted) {
1333
- const matches = rule.reqs.every((req) => {
1334
- const actual = rootAttrs[req.name];
1335
- return req.negated ? actual !== req.value : actual === req.value;
1336
- });
1337
- if (matches) {
1338
- for (const [name, value] of rule.vars)
1339
- vars.set(name, value);
1340
- }
1341
- }
1342
- return vars;
1343
- }
1344
- function selectorsContain(selectors, predicate) {
1345
- for (const selector of selectors) {
1346
- for (const component of selector) {
1347
- if (predicate(component))
1348
- return true;
1349
- }
1350
- }
1351
- return false;
1352
- }
1353
- function isRootOrHost(selectors) {
1354
- return selectorsContain(selectors, (c) => c.type === "pseudo-class" && (c.kind === "root" || c.kind === "host"));
1355
- }
1356
- function isBareThemeClass(selectors) {
1357
- return selectorsContain(selectors, (c) => c.type === "class" && (c.name === "dark" || c.name === "light"));
1358
- }
1359
- function isUniversal(selectors) {
1360
- return selectorsContain(selectors, (c) => c.type === "universal");
1361
- }
1362
- function extractSingleSelectorReqs(components) {
1363
- const reqs = [];
1364
- for (const component of components) {
1365
- if (component.type === "class") {
1366
- if (component.name === "dark" || component.name === "light")
1367
- reqs.push({ name: "data-theme", value: component.name, negated: false });
1368
- }
1369
- if (component.type === "attribute" && component.operation) {
1370
- const name = component.name;
1371
- const value = component.operation.value;
1372
- if (name && value)
1373
- reqs.push({ name, value, negated: false });
1374
- }
1375
- if (component.type === "pseudo-class" && component.kind === "not") {
1376
- for (const innerSelector of component.selectors || []) {
1377
- const innerReqs = extractSingleSelectorReqs(innerSelector);
1378
- for (const req of innerReqs)
1379
- reqs.push({ ...req, negated: !req.negated });
1380
- }
1381
- }
1382
- }
1383
- return reqs;
986
+ }
987
+ return reqs;
1384
988
  }
1385
989
  function extractSelectorAttrReqsList(selectors) {
1386
990
  return selectors.map((selector) => extractSingleSelectorReqs(selector));
@@ -1616,140 +1220,548 @@ const RE_FONTAINE_FALLBACK = / fallback$/i;
1616
1220
  function isFontaineFallback(name) {
1617
1221
  return RE_FONTAINE_FALLBACK.test(name);
1618
1222
  }
1619
- function extractCustomFontFamilies(cssValue) {
1620
- return cssValue.replace(RE_IMPORTANT, "").split(",").map((p) => p.trim().replace(RE_QUOTES, "")).filter((name) => name && !GENERIC_FONT_FAMILIES.has(name.toLowerCase()) && !RE_ONLY_DIGITS.test(name) && !isSystemFont(name) && !isFontaineFallback(name));
1223
+ function extractCustomFontFamilies(cssValue) {
1224
+ return cssValue.replace(RE_IMPORTANT, "").split(",").map((p) => p.trim().replace(RE_QUOTES, "")).filter((name) => name && !GENERIC_FONT_FAMILIES.has(name.toLowerCase()) && !RE_ONLY_DIGITS.test(name) && !isSystemFont(name) && !isFontaineFallback(name));
1225
+ }
1226
+ async function evaluateCalc(value) {
1227
+ if (!value.includes("calc("))
1228
+ return value;
1229
+ const result = await simplifyCss(`.x{width:${value}}`);
1230
+ return result.match(RE_WIDTH_VALUE)?.[1]?.trim() ?? value;
1231
+ }
1232
+ async function resolveVarsDeep(value, vars, depth = 0) {
1233
+ if (depth > 10 || !value.includes("var("))
1234
+ return evaluateCalc(value);
1235
+ const calcMatch = value.match(RE_CALC_VAR_MULTIPLY);
1236
+ if (calcMatch?.[1] && calcMatch?.[2]) {
1237
+ const varValue = vars.get(calcMatch[1]);
1238
+ if (varValue) {
1239
+ const numMatch = varValue.match(RE_NUMERIC_WITH_UNIT);
1240
+ if (numMatch?.[1] && numMatch?.[2]) {
1241
+ const computed = Number.parseFloat(numMatch[1]) * Number.parseFloat(calcMatch[2]);
1242
+ return `${computed}${numMatch[2]}`;
1243
+ }
1244
+ }
1245
+ }
1246
+ const result = resolveCssVars(value, vars);
1247
+ return result.includes("var(") && depth < 10 ? resolveVarsDeep(result, vars, depth + 1) : evaluateCalc(result);
1248
+ }
1249
+ async function downlevelColor(prop, value) {
1250
+ if (!value || value.includes("var("))
1251
+ return value;
1252
+ try {
1253
+ const result = await transformWithLightningCss(`.x{${prop}:${value}}`, { minify: true });
1254
+ const output = result.code.toString();
1255
+ const match = output.match(new RegExp(`[{;]${prop}:(.*?)[;}]`));
1256
+ return match?.[1]?.trim() ?? value;
1257
+ } catch {
1258
+ return value;
1259
+ }
1260
+ }
1261
+ function convertLogicalProperties(styles) {
1262
+ const mapping = [
1263
+ ["inset-inline", ["left", "right"]],
1264
+ ["inset-block", ["top", "bottom"]],
1265
+ ["padding-inline", ["padding-left", "padding-right"]],
1266
+ ["padding-block", ["padding-top", "padding-bottom"]],
1267
+ ["margin-inline", ["margin-left", "margin-right"]],
1268
+ ["margin-block", ["margin-top", "margin-bottom"]]
1269
+ ];
1270
+ for (const [logical, physicals] of mapping) {
1271
+ if (styles[logical]) {
1272
+ const v = styles[logical];
1273
+ for (const phys of physicals) styles[phys] ??= v;
1274
+ delete styles[logical];
1275
+ }
1276
+ }
1277
+ }
1278
+ function stripGradientColorSpace(value) {
1279
+ return value.replace(GRADIENT_COLOR_SPACE_RE, "");
1280
+ }
1281
+ async function postProcessStyles(rawStyles, vars, resolveVar, context) {
1282
+ const styles = {};
1283
+ const resolve = resolveVarsDeep;
1284
+ for (const [prop, rawValue] of Object.entries(rawStyles)) {
1285
+ let value = await resolve(rawValue, vars);
1286
+ if (value.includes("var(") || RE_CSS_KEYWORD.test(value)) {
1287
+ if (value.includes("var(")) {
1288
+ const unresolvedVars = Array.from(value.matchAll(RE_UNRESOLVED_VAR), (m) => m[1]);
1289
+ for (const name of unresolvedVars) {
1290
+ const key = `${context || ""}:${name}`;
1291
+ if (!_warnedVars.has(key)) {
1292
+ _warnedVars.add(key);
1293
+ logger.warn(`[og-image]${context ? ` ${context}:` : ""} CSS variable ${name} not found, style will be skipped`);
1294
+ }
1295
+ }
1296
+ }
1297
+ continue;
1298
+ }
1299
+ if (RE_INVALID_CALC.test(value))
1300
+ continue;
1301
+ if (value.includes(",")) {
1302
+ const cleaned = value.split(",").map((s) => s.trim()).filter(Boolean).join(", ");
1303
+ if (!cleaned)
1304
+ continue;
1305
+ value = cleaned;
1306
+ }
1307
+ if (prop === "font-family") {
1308
+ const custom = extractCustomFontFamilies(value);
1309
+ if (custom.length) {
1310
+ const name = custom[0];
1311
+ value = name.includes(" ") ? `'${name}'` : name;
1312
+ }
1313
+ }
1314
+ if (RE_COLOR_PROP.test(prop)) {
1315
+ value = await downlevelColor(prop, value);
1316
+ if (value.includes("color-mix("))
1317
+ value = resolveColorMix(value);
1318
+ }
1319
+ if (prop === "background-image" && value.includes("-gradient("))
1320
+ value = stripGradientColorSpace(value);
1321
+ styles[prop] = value;
1322
+ }
1323
+ convertLogicalProperties(styles);
1324
+ return Object.keys(styles).length ? styles : null;
1325
+ }
1326
+ function walkTemplateAst(nodes, visitor) {
1327
+ for (const node of nodes) {
1328
+ visitor(node);
1329
+ if ("children" in node && Array.isArray(node.children))
1330
+ walkTemplateAst(node.children, visitor);
1331
+ }
1332
+ }
1333
+
1334
+ const TAKUMI_CORE_PACKAGE = "@takumi-rs/core";
1335
+ const TAKUMI_WASM_PACKAGE = "@takumi-rs/wasm";
1336
+ const PROVIDER_DEPENDENCIES = [
1337
+ {
1338
+ name: "satori",
1339
+ description: "SVG-based renderer using Satori",
1340
+ bindings: {
1341
+ "node": [
1342
+ { name: "satori", description: "HTML to SVG renderer" },
1343
+ { name: "@resvg/resvg-js", description: "SVG to PNG converter (native)" }
1344
+ ],
1345
+ "wasm": [
1346
+ { name: "satori", description: "HTML to SVG renderer" },
1347
+ { name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
1348
+ ],
1349
+ "wasm-fs": [
1350
+ { name: "satori", description: "HTML to SVG renderer" },
1351
+ { name: "@resvg/resvg-wasm", description: "SVG to PNG converter (WASM)" }
1352
+ ]
1353
+ }
1354
+ },
1355
+ {
1356
+ name: "takumi",
1357
+ description: "Rust-based high-performance renderer (recommended)",
1358
+ bindings: {
1359
+ "node": [
1360
+ { name: TAKUMI_CORE_PACKAGE, description: "Native Takumi renderer" }
1361
+ ],
1362
+ "wasm": [
1363
+ { name: TAKUMI_WASM_PACKAGE, description: "WASM Takumi renderer" }
1364
+ ],
1365
+ "wasm-fs": [
1366
+ { name: TAKUMI_WASM_PACKAGE, description: "WASM Takumi renderer" }
1367
+ ]
1368
+ }
1369
+ },
1370
+ {
1371
+ name: "browser",
1372
+ description: "Browser-based screenshot renderer",
1373
+ bindings: {
1374
+ node: [
1375
+ { name: "playwright-core", description: "Headless browser automation" }
1376
+ ]
1377
+ }
1378
+ }
1379
+ ];
1380
+ function resolveAutoDetectedProvider(input) {
1381
+ if (input.hasUserComponents)
1382
+ return { preferred: null, noRenderer: false };
1383
+ const preferred = input.installedProviders.find((p) => p === "takumi") ?? input.installedProviders[0] ?? null;
1384
+ if (preferred)
1385
+ return { preferred, noRenderer: false };
1386
+ return { preferred: null, noRenderer: true };
1387
+ }
1388
+ function getMissingRendererMessage(renderer, installSpecs) {
1389
+ return `The ${renderer} renderer is missing dependencies, so templates that use it cannot render. Install them with: npx nypm add ${installSpecs.join(" ")}`;
1390
+ }
1391
+ function resolveMissingRendererAction(input) {
1392
+ if (!input.dev)
1393
+ return { _tag: "CheckUsage" };
1394
+ if (input.interactive)
1395
+ return { _tag: "AskToInstall" };
1396
+ return { _tag: "Report", message: getMissingRendererMessage(input.renderer, input.installSpecs) };
1397
+ }
1398
+ 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`;
1399
+ async function getInstalledProviders() {
1400
+ const installed = [];
1401
+ for (const provider of PROVIDER_DEPENDENCIES) {
1402
+ if (provider.bindings.node) {
1403
+ const allNodeInstalled = await Promise.all(
1404
+ provider.bindings.node.map((dep) => hasResolvableDependency(dep.name))
1405
+ );
1406
+ if (allNodeInstalled.every(Boolean)) {
1407
+ installed.push({ provider: provider.name, binding: "node" });
1408
+ continue;
1409
+ }
1410
+ }
1411
+ if (provider.bindings.wasm) {
1412
+ const allWasmInstalled = await Promise.all(
1413
+ provider.bindings.wasm.map((dep) => hasResolvableDependency(dep.name))
1414
+ );
1415
+ if (allWasmInstalled.every(Boolean)) {
1416
+ installed.push({ provider: provider.name, binding: "wasm" });
1417
+ }
1418
+ }
1419
+ }
1420
+ return installed;
1421
+ }
1422
+ async function getMissingDependencies(provider, binding = "node") {
1423
+ const missing = await getMissingProviderDependencyDefinitions(provider, binding);
1424
+ return missing.map((d) => d.name);
1425
+ }
1426
+ async function getMissingProviderDependencyDefinitions(provider, binding = "node") {
1427
+ const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
1428
+ if (!providerDef)
1429
+ return [];
1430
+ const deps = providerDef.bindings[binding] || providerDef.bindings.node || [];
1431
+ const missing = [];
1432
+ for (const dep of deps) {
1433
+ if (!await hasResolvableDependency(dep.name))
1434
+ missing.push(dep);
1435
+ }
1436
+ return missing;
1437
+ }
1438
+ function getDependencyDefinitions(provider, binding = "node") {
1439
+ const providerDef = PROVIDER_DEPENDENCIES.find((p) => p.name === provider);
1440
+ if (!providerDef)
1441
+ return [];
1442
+ return providerDef.bindings[binding] || providerDef.bindings.node || [];
1443
+ }
1444
+ function getDependencyInstallSpec(dep) {
1445
+ return dep.installSpec || dep.name;
1446
+ }
1447
+ async function getMissingDependencyInstallSpecs(provider, binding = "node") {
1448
+ const missing = await getMissingProviderDependencyDefinitions(provider, binding);
1449
+ return missing.map(getDependencyInstallSpec);
1450
+ }
1451
+ function getProviderDependencyInstallSpecs(provider, binding = "node") {
1452
+ return getDependencyDefinitions(provider, binding).map(getDependencyInstallSpec);
1621
1453
  }
1622
- async function evaluateCalc(value) {
1623
- if (!value.includes("calc("))
1624
- return value;
1625
- const result = await simplifyCss(`.x{width:${value}}`);
1626
- return result.match(RE_WIDTH_VALUE)?.[1]?.trim() ?? value;
1454
+ function getRecommendedBindingFromPreset(provider) {
1455
+ const preset = resolveOgImagePreset();
1456
+ const compatibility = getPresetNitroPresetCompatibility(preset);
1457
+ return getRecommendedBinding(provider, compatibility);
1627
1458
  }
1628
- async function resolveVarsDeep(value, vars, depth = 0) {
1629
- if (depth > 10 || !value.includes("var("))
1630
- return evaluateCalc(value);
1631
- const calcMatch = value.match(RE_CALC_VAR_MULTIPLY);
1632
- if (calcMatch?.[1] && calcMatch?.[2]) {
1633
- const varValue = vars.get(calcMatch[1]);
1634
- if (varValue) {
1635
- const numMatch = varValue.match(RE_NUMERIC_WITH_UNIT);
1636
- if (numMatch?.[1] && numMatch?.[2]) {
1637
- const computed = Number.parseFloat(numMatch[1]) * Number.parseFloat(calcMatch[2]);
1638
- return `${computed}${numMatch[2]}`;
1639
- }
1640
- }
1459
+ function getRecommendedBinding(provider, compatibility) {
1460
+ if (provider === "satori") {
1461
+ const resvgBinding = compatibility.resvg;
1462
+ if (resvgBinding === "wasm-fs")
1463
+ return "wasm-fs";
1464
+ if (resvgBinding === "wasm")
1465
+ return "wasm";
1466
+ return "node";
1641
1467
  }
1642
- const result = resolveCssVars(value, vars);
1643
- return result.includes("var(") && depth < 10 ? resolveVarsDeep(result, vars, depth + 1) : evaluateCalc(result);
1468
+ if (provider === "takumi") {
1469
+ const takumiBinding = compatibility.takumi;
1470
+ if (takumiBinding === "wasm")
1471
+ return "wasm";
1472
+ return "node";
1473
+ }
1474
+ if (provider === "browser") {
1475
+ return "node";
1476
+ }
1477
+ return "node";
1644
1478
  }
1645
- async function downlevelColor(prop, value) {
1646
- if (!value || value.includes("var("))
1647
- return value;
1648
- try {
1649
- const result = await transformWithLightningCss(`.x{${prop}:${value}}`, { minify: true });
1650
- const output = result.code.toString();
1651
- const match = output.match(new RegExp(`[{;]${prop}:(.*?)[;}]`));
1652
- return match?.[1]?.trim() ?? value;
1653
- } catch {
1654
- return value;
1479
+ async function ensureProviderDependencies(provider, binding, nuxt) {
1480
+ const missingDeps = await getMissingProviderDependencyDefinitions(provider, binding);
1481
+ if (missingDeps.length === 0)
1482
+ return { success: true, installed: [] };
1483
+ const missingInstallSpecs = missingDeps.map(getDependencyInstallSpec);
1484
+ const pm = await detectPackageManager(nuxt.options.rootDir);
1485
+ const pmName = pm?.name || "npm";
1486
+ logger.info(`Installing ${provider} dependencies: ${missingInstallSpecs.join(", ")}`);
1487
+ const installed = [];
1488
+ for (const dep of missingDeps) {
1489
+ const installSpec = getDependencyInstallSpec(dep);
1490
+ const success = await addDependency(installSpec, {
1491
+ cwd: nuxt.options.rootDir,
1492
+ dev: false
1493
+ }).then(() => {
1494
+ installed.push(installSpec);
1495
+ return true;
1496
+ }).catch(() => {
1497
+ logger.error(`Failed to install ${installSpec}. Run manually: ${pmName} add ${installSpec}`);
1498
+ return false;
1499
+ });
1500
+ if (!success)
1501
+ return { success: false, installed };
1655
1502
  }
1503
+ return { success: true, installed };
1656
1504
  }
1657
- function convertIndividualTransforms(styles) {
1658
- const parts = [];
1659
- if (styles.translate) {
1660
- const vals = styles.translate.trim().split(RE_WHITESPACE);
1661
- parts.push(vals.length === 2 ? `translate(${vals[0]}, ${vals[1]})` : `translateX(${vals[0]})`);
1662
- delete styles.translate;
1505
+ function canPromptInteractively(env = {
1506
+ hasTTY,
1507
+ hasStdinTTY: Boolean(process.stdin?.isTTY),
1508
+ isAgent,
1509
+ isCI
1510
+ }) {
1511
+ return env.hasTTY && env.hasStdinTTY && !env.isAgent && !env.isCI;
1512
+ }
1513
+ async function validateProviderSetup(renderer, compatibility) {
1514
+ const issues = [];
1515
+ const binding = getRecommendedBinding(renderer, compatibility);
1516
+ const missing = await getMissingDependencyInstallSpecs(renderer, binding);
1517
+ if (missing.length > 0) {
1518
+ issues.push(`Missing ${renderer} dependencies: ${missing.join(", ")}`);
1519
+ }
1520
+ if (renderer === "satori") {
1521
+ const hasResvgNode = await hasResolvableDependency("@resvg/resvg-js");
1522
+ const hasResvgWasm = await hasResolvableDependency("@resvg/resvg-wasm");
1523
+ if (!hasResvgNode && !hasResvgWasm) {
1524
+ issues.push("Satori requires either @resvg/resvg-js (node) or @resvg/resvg-wasm to render PNGs");
1525
+ }
1663
1526
  }
1664
- if (styles.rotate) {
1665
- parts.push(`rotate(${styles.rotate})`);
1666
- delete styles.rotate;
1527
+ if (renderer === "browser") {
1528
+ const hasPlaywright = await hasResolvableDependency("playwright-core");
1529
+ if (!hasPlaywright && binding === "node") {
1530
+ issues.push("Browser renderer requires playwright-core");
1531
+ }
1667
1532
  }
1668
- if (styles.scale) {
1669
- const vals = styles.scale.trim().split(RE_WHITESPACE).filter(Boolean);
1670
- parts.push(`scale(${vals.join(", ")})`);
1671
- delete styles.scale;
1533
+ return { valid: issues.length === 0, issues };
1534
+ }
1535
+
1536
+ async function setupDevHandler(options, resolve, getDetectedRenderers, nuxt = useNuxt()) {
1537
+ const applyCompatibility = async (nitroConfig) => {
1538
+ const detectedRenderers = new Set(getDetectedRenderers());
1539
+ const targetCompatibility = getPresetNitroPresetCompatibility(resolveOgImagePreset(nitroConfig));
1540
+ for (const renderer of ["satori", "takumi", "browser"]) {
1541
+ if (!detectedRenderers.has(renderer)) {
1542
+ const binding = getRecommendedBinding(renderer, targetCompatibility);
1543
+ const missing = await getMissingDependencies(renderer, binding);
1544
+ if (missing.length === 0)
1545
+ detectedRenderers.add(renderer);
1546
+ }
1547
+ }
1548
+ await applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.dev, resolve, detectedRenderers });
1549
+ };
1550
+ await applyCompatibility(nuxt.options.nitro);
1551
+ nuxt.hooks.hook("nitro:init", (nitro) => applyCompatibility(nitro.options));
1552
+ }
1553
+
1554
+ const RE_TILDE_SLASH = /^~\//;
1555
+ const RE_WORD_CHARS_ONLY = /^\w+$/;
1556
+ const RE_SCRIPT_SETUP = /<script\s+setup[^>]*>\n?/;
1557
+ const SCRIPT_BLOCK = `<script setup lang="ts">
1558
+ const { title = 'My Page', description = '' } = defineProps<{
1559
+ title?: string
1560
+ description?: string
1561
+ }>()
1562
+ <\/script>`;
1563
+ function getStarterTemplate(renderer, css) {
1564
+ if (renderer === "satori") {
1565
+ return `${SCRIPT_BLOCK}
1566
+
1567
+ <template>
1568
+ <div :style="{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%', padding: '60px', backgroundColor: 'white', color: '#171717' }">
1569
+ <h1 :style="{ fontSize: '72px', fontWeight: 'bold', margin: 0, lineHeight: 1.1, textAlign: 'center' }">
1570
+ {{ title }}
1571
+ </h1>
1572
+ <p v-if="description" :style="{ fontSize: '32px', opacity: 0.6, marginTop: '16px', textAlign: 'center', maxWidth: '900px' }">
1573
+ {{ description }}
1574
+ </p>
1575
+ </div>
1576
+ </template>
1577
+ `;
1672
1578
  }
1673
- if (parts.length > 0) {
1674
- styles.transform = styles.transform ? `${parts.join(" ")} ${styles.transform}` : parts.join(" ");
1579
+ if (css === "tailwind" || css === "unocss") {
1580
+ return `${SCRIPT_BLOCK}
1581
+
1582
+ <template>
1583
+ <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">
1584
+ <h1 class="text-[72px] font-bold m-0 leading-tight text-center" style="text-wrap: balance;">
1585
+ {{ title }}
1586
+ </h1>
1587
+ <p v-if="description" class="text-[32px] opacity-60 mt-4 text-center max-w-[900px]">
1588
+ {{ description }}
1589
+ </p>
1590
+ </div>
1591
+ </template>
1592
+ `;
1675
1593
  }
1594
+ return `${SCRIPT_BLOCK}
1595
+
1596
+ <template>
1597
+ <div class="container">
1598
+ <h1>{{ title }}</h1>
1599
+ <p v-if="description">
1600
+ {{ description }}
1601
+ </p>
1602
+ </div>
1603
+ </template>
1604
+
1605
+ <style scoped>
1606
+ .container {
1607
+ width: 100%;
1608
+ height: 100%;
1609
+ display: flex;
1610
+ flex-direction: column;
1611
+ justify-content: center;
1612
+ align-items: center;
1613
+ padding: 60px;
1614
+ background: white;
1615
+ color: #171717;
1676
1616
  }
1677
- function convertLogicalProperties(styles) {
1678
- const mapping = [
1679
- ["inset", ["top", "right", "bottom", "left"]],
1680
- ["inset-inline", ["left", "right"]],
1681
- ["inset-block", ["top", "bottom"]],
1682
- ["padding-inline", ["padding-left", "padding-right"]],
1683
- ["padding-block", ["padding-top", "padding-bottom"]],
1684
- ["margin-inline", ["margin-left", "margin-right"]],
1685
- ["margin-block", ["margin-top", "margin-bottom"]]
1686
- ];
1687
- for (const [logical, physicals] of mapping) {
1688
- if (styles[logical]) {
1689
- const v = styles[logical];
1690
- for (const phys of physicals) styles[phys] ??= v;
1691
- delete styles[logical];
1617
+ h1 {
1618
+ font-size: 72px;
1619
+ font-weight: bold;
1620
+ margin: 0;
1621
+ line-height: 1.1;
1622
+ text-align: center;
1623
+ text-wrap: balance;
1624
+ }
1625
+ p {
1626
+ font-size: 32px;
1627
+ opacity: 0.6;
1628
+ margin-top: 16px;
1629
+ text-align: center;
1630
+ max-width: 900px;
1631
+ }
1632
+ </style>
1633
+ `;
1634
+ }
1635
+ function setupDevToolsUI(options, resolve$1, nuxt = useNuxt(), cssFramework = "none") {
1636
+ setupDevToolsUI$1({
1637
+ route: "/__nuxt-og-image",
1638
+ name: "nuxt-og-image",
1639
+ title: "OG Image",
1640
+ icon: "carbon:image-search"
1641
+ }, resolve$1, nuxt);
1642
+ const communityTemplatesDir = resolve$1("./runtime/app/components/Templates/Community");
1643
+ const useNitro = new Promise((resolve2) => {
1644
+ nuxt.hooks.hook("nitro:init", resolve2);
1645
+ });
1646
+ async function insertDefineOgImage(componentName, pageFile) {
1647
+ const resolved = pageFile ? resolve(pageFile) : "";
1648
+ if (!resolved || !resolved.startsWith(nuxt.options.srcDir) || !existsSync(resolved))
1649
+ return false;
1650
+ const content = await readFile(resolved, "utf-8");
1651
+ if (content.includes("defineOgImage"))
1652
+ return false;
1653
+ const call = `defineOgImage('${componentName}')`;
1654
+ const scriptSetupMatch = content.match(RE_SCRIPT_SETUP);
1655
+ if (scriptSetupMatch) {
1656
+ const insertPos = scriptSetupMatch.index + scriptSetupMatch[0].length;
1657
+ await writeFile(resolved, `${content.slice(0, insertPos)}${call}
1658
+ ${content.slice(insertPos)}`, "utf-8");
1659
+ } else {
1660
+ await writeFile(resolved, `<script setup lang="ts">
1661
+ ${call}
1662
+ <\/script>
1663
+
1664
+ ${content}`, "utf-8");
1665
+ }
1666
+ return true;
1667
+ }
1668
+ setupDevToolsRpc("nuxt-og-image", {
1669
+ async ejectCommunityTemplate(path) {
1670
+ const [dirName, componentName] = path.split("/");
1671
+ const nameWithoutExt = componentName?.replace(".vue", "") || "";
1672
+ let dotNotationName;
1673
+ if (nameWithoutExt.includes(".")) {
1674
+ dotNotationName = `${nameWithoutExt}.vue`;
1675
+ } else {
1676
+ const rendererMatch = nameWithoutExt.match(RE_RENDERER_SUFFIX);
1677
+ const renderer = rendererMatch?.[1]?.toLowerCase() || "satori";
1678
+ const baseName = nameWithoutExt.replace(RE_RENDERER_SUFFIX, "");
1679
+ dotNotationName = `${baseName}.${renderer}.vue`;
1680
+ }
1681
+ const dir = join(nuxt.options.srcDir, "components", dirName || "");
1682
+ if (!existsSync(dir)) {
1683
+ mkdirSync(dir, { recursive: true });
1684
+ }
1685
+ const newPath = join(dir, dotNotationName);
1686
+ const templatePath = join(communityTemplatesDir, dotNotationName);
1687
+ const template = (await readFile(templatePath, "utf-8")).replace("{{ title }}", `{{ title }} - Ejected!`);
1688
+ await writeFile(newPath, template, { encoding: "utf-8" });
1689
+ await updateTemplates({ filter: (t) => t.filename.includes("nuxt-og-image/components.mjs") });
1690
+ const nitro = await useNitro;
1691
+ await nitro.hooks.callHook("rollup:reload");
1692
+ return newPath;
1693
+ },
1694
+ async createComponent({ name, renderer, pageFile }) {
1695
+ const validRenderers = ["satori", "browser", "takumi"];
1696
+ if (!validRenderers.includes(renderer))
1697
+ throw new Error(`Invalid renderer: ${renderer}`);
1698
+ if (!RE_WORD_CHARS_ONLY.test(name))
1699
+ throw new Error(`Invalid component name: ${name}`);
1700
+ const baseDir = existsSync(join(nuxt.options.srcDir, "app")) ? join(nuxt.options.srcDir, "app") : nuxt.options.srcDir;
1701
+ const outputDir = join(baseDir, "components", "OgImage");
1702
+ if (!existsSync(outputDir))
1703
+ mkdirSync(outputDir, { recursive: true });
1704
+ const filename = `${name}.${renderer}.vue`;
1705
+ const outputPath = join(outputDir, filename);
1706
+ if (existsSync(outputPath))
1707
+ throw new Error(`File already exists: ${relative(nuxt.options.rootDir, outputPath)}`);
1708
+ const template = getStarterTemplate(renderer, cssFramework);
1709
+ await writeFile(outputPath, template, { encoding: "utf-8" });
1710
+ await insertDefineOgImage(name, pageFile);
1711
+ await updateTemplates({ filter: (t) => t.filename.includes("nuxt-og-image/components.mjs") });
1712
+ const nitro = await useNitro;
1713
+ await nitro.hooks.callHook("rollup:reload");
1714
+ return outputPath;
1715
+ },
1716
+ async addOgImageToPage(componentName, pageFile) {
1717
+ if (!RE_WORD_CHARS_ONLY.test(componentName))
1718
+ throw new Error(`Invalid component name: ${componentName}`);
1719
+ return insertDefineOgImage(componentName, pageFile);
1692
1720
  }
1693
- }
1694
- }
1695
- function stripGradientColorSpace(value) {
1696
- return value.replace(GRADIENT_COLOR_SPACE_RE, "");
1697
- }
1698
- async function postProcessStyles(rawStyles, vars, resolveVar, context) {
1699
- const styles = {};
1700
- const resolve = resolveVarsDeep;
1701
- for (const [prop, rawValue] of Object.entries(rawStyles)) {
1702
- let value = await resolve(rawValue, vars);
1703
- if (value.includes("var(") || RE_CSS_KEYWORD.test(value)) {
1704
- if (value.includes("var(")) {
1705
- const unresolvedVars = Array.from(value.matchAll(RE_UNRESOLVED_VAR), (m) => m[1]);
1706
- for (const name of unresolvedVars) {
1707
- const key = `${context || ""}:${name}`;
1708
- if (!_warnedVars.has(key)) {
1709
- _warnedVars.add(key);
1710
- logger.warn(`[og-image]${context ? ` ${context}:` : ""} CSS variable ${name} not found, style will be skipped`);
1711
- }
1721
+ }, nuxt).then((rpc) => {
1722
+ if (!rpc)
1723
+ return;
1724
+ const safeBroadcast = (fn) => {
1725
+ try {
1726
+ const result = fn();
1727
+ if (result && typeof result.then === "function") {
1728
+ result.catch((err) => {
1729
+ void err;
1730
+ });
1712
1731
  }
1732
+ } catch {
1713
1733
  }
1714
- continue;
1715
- }
1716
- if (RE_INVALID_CALC.test(value))
1717
- continue;
1718
- if (value.includes(",")) {
1719
- const cleaned = value.split(",").map((s) => s.trim()).filter(Boolean).join(", ");
1720
- if (!cleaned)
1721
- continue;
1722
- value = cleaned;
1723
- }
1724
- if (prop === "font-family") {
1725
- const custom = extractCustomFontFamilies(value);
1726
- if (custom.length) {
1727
- const name = custom[0];
1728
- value = name.includes(" ") ? `'${name}'` : name;
1734
+ };
1735
+ let cssRefreshTimer;
1736
+ nuxt.hook("builder:watch", (e, watchPath) => {
1737
+ if (!e || !watchPath)
1738
+ return;
1739
+ const normalizedPath = relative(nuxt.options.srcDir, isAbsolute(watchPath) ? watchPath : resolve(nuxt.options.srcDir, watchPath));
1740
+ const absolutePath = isAbsolute(watchPath) ? watchPath : join(nuxt.options.rootDir, watchPath);
1741
+ const isCssChange = absolutePath.endsWith(".css") && nuxt.options.css.some((entry) => {
1742
+ const src = typeof entry === "string" ? entry : entry?.src;
1743
+ return src && absolutePath.endsWith(src.replace(RE_TILDE_SLASH, ""));
1744
+ });
1745
+ if (isCssChange || normalizedPath.includes("uno.config")) {
1746
+ clearTimeout(cssRefreshTimer);
1747
+ cssRefreshTimer = setTimeout(() => {
1748
+ safeBroadcast(() => rpc.broadcast.refresh.asEvent());
1749
+ }, 200);
1750
+ return;
1729
1751
  }
1730
- }
1731
- if (RE_COLOR_PROP.test(prop)) {
1732
- value = await downlevelColor(prop, value);
1733
- if (value.includes("color-mix("))
1734
- value = resolveColorMix(value);
1735
- }
1736
- if (prop === "background-image" && value.includes("-gradient("))
1737
- value = stripGradientColorSpace(value);
1738
- styles[prop] = value;
1739
- }
1740
- convertIndividualTransforms(styles);
1741
- convertLogicalProperties(styles);
1742
- return Object.keys(styles).length ? styles : null;
1743
- }
1744
- function walkTemplateAst(nodes, visitor) {
1745
- for (const node of nodes) {
1746
- visitor(node);
1747
- if ("children" in node && Array.isArray(node.children))
1748
- walkTemplateAst(node.children, visitor);
1749
- }
1752
+ if ((e === "change" || e.includes("link")) && (normalizedPath.startsWith("pages") || normalizedPath.startsWith("content"))) {
1753
+ safeBroadcast(() => rpc.broadcast.refreshRouteData.asEvent(normalizedPath));
1754
+ }
1755
+ if (options.componentDirs.some((dir) => normalizedPath.includes(dir))) {
1756
+ if (e === "change")
1757
+ safeBroadcast(() => rpc.broadcast.refresh.asEvent());
1758
+ else
1759
+ safeBroadcast(() => rpc.broadcast.refreshGlobalData.asEvent());
1760
+ }
1761
+ });
1762
+ });
1750
1763
  }
1751
1764
 
1752
- const RE_FONT_FACE_WITH_SUBSET = /(?:\/\*\s*([a-z-]+)\s*\*\/\s*)?(@font-face\s*\{[^}]+\})/g;
1753
1765
  async function extractFontFaces(css) {
1754
1766
  const { transform } = await loadLightningCss();
1755
1767
  const fonts = [];
@@ -1852,23 +1864,32 @@ async function extractFontFacesSimple(css) {
1852
1864
  };
1853
1865
  }).filter((f) => f.src);
1854
1866
  }
1855
- async function extractFontFacesWithSubsets(css) {
1856
- const fontFaceRe = RE_FONT_FACE_WITH_SUBSET;
1857
- const results = [];
1858
- for (const match of css.matchAll(fontFaceRe)) {
1859
- const subset = match[1];
1860
- const fonts = await extractFontFacesSimple(match[2]);
1861
- for (const font of fonts)
1862
- results.push({ ...font, subset });
1863
- }
1864
- return results;
1867
+ function fontFacesFromResolved(family, faces) {
1868
+ return faces.flatMap((face) => {
1869
+ const source = face.src.find((s) => "url" in s);
1870
+ if (!source)
1871
+ return [];
1872
+ const range = typeof face.weight === "string" ? face.weight.split(/\s+/).map(Number) : Array.isArray(face.weight) ? face.weight : [face.weight ?? 400];
1873
+ const [min = 400, max = min] = range;
1874
+ const weightRange = max !== min ? [min, max] : void 0;
1875
+ return [{
1876
+ family,
1877
+ src: source.url,
1878
+ weight: weightRange ? min <= 400 && max >= 400 ? 400 : min : min,
1879
+ style: face.style || "normal",
1880
+ unicodeRange: face.unicodeRange?.join(","),
1881
+ isWoff2: source.url.endsWith(".woff2"),
1882
+ subset: face.meta?.subset,
1883
+ weightRange
1884
+ }];
1885
+ });
1865
1886
  }
1866
1887
 
1867
1888
  const fontFace = {
1868
1889
  __proto__: null,
1869
1890
  extractFontFaces: extractFontFaces,
1870
1891
  extractFontFacesSimple: extractFontFacesSimple,
1871
- extractFontFacesWithSubsets: extractFontFacesWithSubsets
1892
+ fontFacesFromResolved: fontFacesFromResolved
1872
1893
  };
1873
1894
 
1874
1895
  const RE_FONT_FACE_BLOCK = /@font-face\s*\{[^}]+\}/g;
@@ -1880,6 +1901,17 @@ const STATIC_FONTS_PREFIX = "/_og-static-fonts";
1880
1901
  function getStaticFontCacheDir(buildDir) {
1881
1902
  return join(buildDir, "cache", "og-image", "static-fonts");
1882
1903
  }
1904
+ async function cacheResolvedFontFiles(files, dir) {
1905
+ const paths = {};
1906
+ for (const [url, readFont] of files) {
1907
+ const data = await readFont();
1908
+ await mkdir(dir, { recursive: true });
1909
+ const path = join(dir, createHash("sha256").update(url).update(data).digest("hex"));
1910
+ await writeFile(path, data);
1911
+ paths[url] = path;
1912
+ }
1913
+ return paths;
1914
+ }
1883
1915
  function fontKey(f) {
1884
1916
  return `${f.family}-${f.weight}-${f.style}-${f.unicodeRange || "default"}`;
1885
1917
  }
@@ -1922,6 +1954,15 @@ function resolveFontFamilies(familyClasses, familyNames, fontVars, knownFamilies
1922
1954
  }
1923
1955
  return [...families];
1924
1956
  }
1957
+ function resolveAppCssPath(cssPath, srcDir, rootDir = srcDir) {
1958
+ if (cssPath.startsWith("~~/") || cssPath.startsWith("@@/"))
1959
+ return join(rootDir, cssPath.slice(3));
1960
+ if (cssPath.startsWith("~/") || cssPath.startsWith("@/"))
1961
+ return join(srcDir, cssPath.slice(2));
1962
+ if (cssPath.startsWith("/"))
1963
+ return cssPath;
1964
+ return join(srcDir, cssPath);
1965
+ }
1925
1966
  async function parseAppCssFontFaces(nuxt) {
1926
1967
  const cssEntries = nuxt.options.css || [];
1927
1968
  if (cssEntries.length === 0)
@@ -1933,7 +1974,7 @@ async function parseAppCssFontFaces(nuxt) {
1933
1974
  const cssPath = typeof entry === "string" ? entry : entry.src;
1934
1975
  if (!cssPath)
1935
1976
  continue;
1936
- const resolved = cssPath.startsWith("~/") ? join(nuxt.options.srcDir, cssPath.slice(2)) : cssPath.startsWith("/") ? cssPath : join(nuxt.options.srcDir, cssPath);
1977
+ const resolved = resolveAppCssPath(cssPath, nuxt.options.srcDir, nuxt.options.rootDir);
1937
1978
  if (!existsSync(resolved))
1938
1979
  continue;
1939
1980
  const content = fs.readFileSync(resolved, "utf-8");
@@ -2061,22 +2102,17 @@ function parseConfiguredLocalFonts(nuxt) {
2061
2102
  );
2062
2103
  return results;
2063
2104
  }
2064
- async function parseFontsFromTemplate(nuxt, options) {
2105
+ async function getResolvedNuxtFonts(nuxt, options) {
2065
2106
  const weightsKey = options.requiredWeights?.toSorted((a, b) => a - b).join(",") || "";
2066
2107
  const sourceKey = [...options.fontState.sourceMap.entries()].toSorted(([a], [b]) => a.localeCompare(b)).flat().join(",");
2067
2108
  const fallbackKey = [...options.fontState.fallbackMap.entries()].toSorted(([a], [b]) => a.localeCompare(b)).flat().join(",");
2068
- const cacheKey = `${sourceKey}:${fallbackKey}:${weightsKey}`;
2109
+ const resolvedFamilies = [...options.fontState.resolvedFaces?.keys() || []].filter((f) => options.fontState.isOgFamily?.(f) ?? true);
2110
+ const cacheKey = `${sourceKey}:${fallbackKey}:${weightsKey}:${getResolvedFontStateKey(options.fontState)}`;
2069
2111
  const cache = nuxt._ogImageParsedFontsCache ||= /* @__PURE__ */ new Map();
2070
2112
  const cached = cache.get(cacheKey);
2071
2113
  if (cached)
2072
2114
  return cached;
2073
- const templates = nuxt.options.build.templates;
2074
- const nuxtFontsTemplate = templates.find((t) => t.filename?.endsWith("nuxt-fonts-global.css"));
2075
- if (!nuxtFontsTemplate?.getContents) {
2076
- return [];
2077
- }
2078
- const contents = await nuxtFontsTemplate.getContents({});
2079
- const allFonts = await extractFontFacesWithSubsets(contents);
2115
+ const allFonts = resolvedFamilies.flatMap((family) => fontFacesFromResolved(family, options.fontState.resolvedFaces.get(family)));
2080
2116
  const fontMap = /* @__PURE__ */ new Map();
2081
2117
  for (const font of allFonts) {
2082
2118
  const key = fontKey(font);
@@ -2098,10 +2134,10 @@ async function parseFontsFromTemplate(nuxt, options) {
2098
2134
  continue;
2099
2135
  const [, family, weightStr, style] = match;
2100
2136
  const weight = Number(weightStr);
2101
- const templates2 = expandedFonts.filter((f) => f.family === family && f.style === style);
2102
- if (templates2.length === 0)
2137
+ const templates = expandedFonts.filter((f) => f.family === family && f.style === style);
2138
+ if (templates.length === 0)
2103
2139
  continue;
2104
- for (const template of templates2)
2140
+ for (const template of templates)
2105
2141
  expandedFonts.push({ ...template, weight });
2106
2142
  existingKeys.add(key);
2107
2143
  }
@@ -2115,10 +2151,10 @@ async function parseFontsFromTemplate(nuxt, options) {
2115
2151
  const expandKey = `${varFont.family}-${w}-${varFont.style}`;
2116
2152
  if (existingKeys.has(expandKey))
2117
2153
  continue;
2118
- const templates2 = expandedFonts.filter(
2154
+ const templates = expandedFonts.filter(
2119
2155
  (f) => f.family === varFont.family && f.style === varFont.style
2120
2156
  );
2121
- for (const template of templates2)
2157
+ for (const template of templates)
2122
2158
  expandedFonts.push({ ...template, weight: w });
2123
2159
  existingKeys.add(expandKey);
2124
2160
  }
@@ -2143,6 +2179,19 @@ async function parseFontsFromTemplate(nuxt, options) {
2143
2179
  cache.set(cacheKey, result);
2144
2180
  return result;
2145
2181
  }
2182
+ function getResolvedFontStateKey(fontState) {
2183
+ return JSON.stringify(
2184
+ [...fontState.resolvedFaces || []].filter(([family]) => fontState.isOgFamily?.(family) ?? true).flatMap(([family, faces]) => fontFacesFromResolved(family, faces)).map((font) => [
2185
+ font.family,
2186
+ font.src,
2187
+ font.weight,
2188
+ font.style,
2189
+ font.unicodeRange || "",
2190
+ font.subset || "",
2191
+ font.weightRange?.join(",") || ""
2192
+ ]).toSorted((a, b) => a.join("\0").localeCompare(b.join("\0")))
2193
+ );
2194
+ }
2146
2195
  async function downloadFontFile(url, destPath) {
2147
2196
  if (existsSync(destPath))
2148
2197
  return true;
@@ -2198,6 +2247,44 @@ function copyStaticFontsToOutput(options) {
2198
2247
  logger.debug(`Copied ${count} static fonts to output`);
2199
2248
  }
2200
2249
 
2250
+ const RE_FONT_FAMILY_DECLARATION = /font-family\s*:\s*([^;}]+)/g;
2251
+ function fontFamiliesFromCssEntries(css, srcDir, rootDir = srcDir) {
2252
+ const families = /* @__PURE__ */ new Set();
2253
+ for (const entry of css || []) {
2254
+ const cssPath = typeof entry === "string" ? entry : entry?.src;
2255
+ if (!cssPath)
2256
+ continue;
2257
+ const resolved = resolveAppCssPath(cssPath, srcDir, rootDir);
2258
+ let content;
2259
+ try {
2260
+ content = readFileSync(resolved, "utf-8");
2261
+ } catch {
2262
+ continue;
2263
+ }
2264
+ for (const match of content.matchAll(RE_FONT_FAMILY_DECLARATION)) {
2265
+ for (const family of extractCustomFontFamilies(match[1]))
2266
+ families.add(family);
2267
+ }
2268
+ }
2269
+ return [...families];
2270
+ }
2271
+ function warnWhenFontsUnreported(options) {
2272
+ const { nuxt, logger, fontState, loadCssMetadata, expectedFamilies } = options;
2273
+ let warned = false;
2274
+ nuxt.hook("nitro:build:before", async () => {
2275
+ if (warned || (fontState.resolvedFaces?.size ?? 0) > 0)
2276
+ return;
2277
+ if (nuxt.options.dev)
2278
+ return;
2279
+ await loadCssMetadata?.();
2280
+ const configuredFamilies = nuxt.options.fonts?.families || [];
2281
+ if (configuredFamilies.length === 0 && expectedFamilies().length === 0)
2282
+ return;
2283
+ warned = true;
2284
+ logger.warn("@nuxt/fonts did not report any fonts, so OG images use the bundled Inter font.");
2285
+ });
2286
+ }
2287
+
2201
2288
  const RE_NON_ALPHANUMERIC = /[^a-z0-9]/gi;
2202
2289
  function getFontlessContext(nuxt) {
2203
2290
  return nuxt._ogImageFontless;
@@ -2206,6 +2293,11 @@ function getNuxtFontsFamilyConfig(nuxt, family) {
2206
2293
  const families = nuxt.options.fonts?.families;
2207
2294
  return families?.find((f) => typeof f?.name === "string" && f.name.toLowerCase() === family.toLowerCase());
2208
2295
  }
2296
+ function isGlyphSubsetFamily(fonts, family) {
2297
+ const config = fonts?.families?.find((f) => typeof f.name === "string" && f.name.toLowerCase() === family.toLowerCase());
2298
+ const glyphs = config?.glyphs || fonts?.defaults?.glyphs;
2299
+ return !!glyphs?.length;
2300
+ }
2209
2301
  function isConfiguredLocalFontFamily(nuxt, family) {
2210
2302
  const config = getNuxtFontsFamilyConfig(nuxt, family);
2211
2303
  return !!config && config.global === true && (config.provider === "local" || typeof config.src === "string");
@@ -2224,6 +2316,14 @@ function getFamilyRequirements(fontRequirements, family) {
2224
2316
  styles: [...new Set(matchingComponents.flatMap((component) => component.styles))]
2225
2317
  };
2226
2318
  }
2319
+ function getResolvedWeights(fonts, family) {
2320
+ const faces = fonts.filter((font) => font.family === family);
2321
+ const weights = new Set(faces.flatMap((font) => font.weightRange ? [400, 700].filter((weight) => font.weightRange[0] <= weight && weight <= font.weightRange[1]) : [font.weight]));
2322
+ return {
2323
+ weights: [...weights].toSorted((a, b) => a - b),
2324
+ styles: [...new Set(faces.map((font) => font.style))]
2325
+ };
2326
+ }
2227
2327
  async function initFontless(options) {
2228
2328
  if (getFontlessContext(options.nuxt))
2229
2329
  return;
@@ -2289,15 +2389,6 @@ async function initFontless(options) {
2289
2389
  options.logger?.debug(`fontless initialized with formats: ['woff', 'ttf'], subsets: ${JSON.stringify(options.fontSubsets || ["latin"])}, priority: ${JSON.stringify(nuxtFontsConfig?.priority || ["google", "bunny", "fontsource"])}`);
2290
2390
  options.nuxt._ogImageFontless = { resolver, providerNames: priority };
2291
2391
  }
2292
- function persistFontUrlMapping(options) {
2293
- if (!options.fontContext?.renderedFontURLs.size)
2294
- return;
2295
- const cacheDir = join(options.buildDir, "cache", "og-image");
2296
- fs.mkdirSync(cacheDir, { recursive: true });
2297
- const mapping = Object.fromEntries(options.fontContext.renderedFontURLs);
2298
- fs.writeFileSync(join(cacheDir, "font-urls.json"), JSON.stringify(mapping));
2299
- options.logger.debug(`Persisted ${options.fontContext.renderedFontURLs.size} font URLs for prerender`);
2300
- }
2301
2392
  function resolveWeightsFromFontEntry(fontWeight, requestedWeights) {
2302
2393
  if (typeof fontWeight === "number")
2303
2394
  return requestedWeights.includes(fontWeight) ? [fontWeight] : [];
@@ -2404,20 +2495,20 @@ async function downloadStaticFonts(options) {
2404
2495
  ];
2405
2496
  if (configuredFamily && configuredFamily.global !== true) {
2406
2497
  lines.push(
2407
- ` "${family}" is declared in fonts.families, but it is not global so @nuxt/fonts did not emit it in nuxt-fonts-global.css.`,
2498
+ ` "${family}" is in fonts.families, but it is not global and no site CSS uses it, so @nuxt/fonts did not resolve it.`,
2408
2499
  ` Set global: true, e.g. fonts: { families: [{ name: '${family}', provider: 'local', weights: [400, 700], global: true }] }.`
2409
2500
  );
2410
2501
  } else if (configuredFamily) {
2411
2502
  lines.push(
2412
- ` "${family}" is declared with global: true, but @nuxt/fonts still did not emit @font-face for it.`,
2503
+ ` "${family}" has global: true, but @nuxt/fonts did not resolve it.`,
2413
2504
  ` Check that the configured provider/src, weights, styles, and file names match the available font files.`
2414
2505
  );
2415
2506
  if (local)
2416
2507
  lines.push(` Found ${local.matches.length} matching file(s) under public/fonts/ (e.g. ${local.matches.slice(0, 2).join(", ")}).`);
2417
2508
  } else if (local) {
2418
2509
  lines.push(
2419
- ` Found ${local.matches.length} matching file(s) under public/fonts/ (e.g. ${local.matches.slice(0, 2).join(", ")}) but @nuxt/fonts did not emit @font-face for "${family}".`,
2420
- ` Tailwind v4 @theme variables are not scanned by @nuxt/fonts, and OG images only read globally emitted font faces.`,
2510
+ ` Found ${local.matches.length} matching file(s) under public/fonts/ (e.g. ${local.matches.slice(0, 2).join(", ")}), but @nuxt/fonts did not resolve "${family}".`,
2511
+ ` @nuxt/fonts does not scan Tailwind v4 @theme variables. OG images only use fonts that @nuxt/fonts resolves.`,
2421
2512
  ` Declare it explicitly with global: true, e.g. fonts: { families: [{ name: '${family}', provider: 'local', weights: [400, 700], global: true }] }.`
2422
2513
  );
2423
2514
  } else {
@@ -2458,9 +2549,8 @@ async function resolveAndDownloadFamily(options) {
2458
2549
  if (!resolution?.fonts?.length)
2459
2550
  return results;
2460
2551
  for (const font of resolution.fonts) {
2461
- const srcs = Array.isArray(font.src) ? font.src : [font.src];
2462
- for (const src of srcs) {
2463
- if (typeof src !== "object" || !("url" in src))
2552
+ for (const src of font.src) {
2553
+ if (!("url" in src))
2464
2554
  continue;
2465
2555
  const url = src.originalURL || src.url;
2466
2556
  const format = src.format || (url.endsWith(".woff") ? "woff" : url.endsWith(".ttf") ? "truetype" : void 0);
@@ -2509,30 +2599,6 @@ function hasOpenTypeTable(data, expectedTag) {
2509
2599
  }
2510
2600
  return false;
2511
2601
  }
2512
- function getNuxtFontOriginalSource(fontSrc, context) {
2513
- if (!context)
2514
- return;
2515
- const prefix = `${context.assetsBaseURL.replace(/\/$/, "")}/`;
2516
- if (!fontSrc.startsWith(prefix))
2517
- return;
2518
- return context.renderedFontURLs.get(fontSrc.slice(prefix.length));
2519
- }
2520
- async function readNuxtFontAsset(nuxt, source) {
2521
- if (source.startsWith("/")) {
2522
- const configuredPublicDir = nuxt.options.dir.public || "public";
2523
- const publicDir = isAbsolute(configuredPublicDir) ? configuredPublicDir : join(nuxt.options.srcDir, configuredPublicDir);
2524
- const path = join(publicDir, source.slice(1).split(/[?#]/, 1)[0]);
2525
- return fs.promises.readFile(path).then((data) => ({ _tag: "Ok", data })).catch((error) => ({
2526
- _tag: "Err",
2527
- reason: error.code === "ENOENT" ? `local source not found: ${path}` : error.message
2528
- }));
2529
- }
2530
- if (source.startsWith("file:")) {
2531
- const path = fileURLToPath(source);
2532
- return fs.promises.readFile(path).then((data) => ({ _tag: "Ok", data })).catch((error) => ({ _tag: "Err", reason: error.message }));
2533
- }
2534
- return fetch(source).then(async (response) => response.ok ? { _tag: "Ok", data: new Uint8Array(await response.arrayBuffer()) } : { _tag: "Err", reason: `source returned HTTP ${response.status}` }).catch((error) => ({ _tag: "Err", reason: error.message }));
2535
- }
2536
2602
  async function convertNuxtWoff2Sources(options) {
2537
2603
  const staticFontDir = getStaticFontCacheDir(options.nuxt.options.buildDir);
2538
2604
  fs.mkdirSync(staticFontDir, { recursive: true });
@@ -2541,23 +2607,16 @@ async function convertNuxtWoff2Sources(options) {
2541
2607
  if (!font.weightRange && !fontsBySource.has(font.src))
2542
2608
  fontsBySource.set(font.src, font);
2543
2609
  }
2544
- const mappedSources = [];
2610
+ let decoder;
2545
2611
  for (const [fontSrc, font] of fontsBySource) {
2546
- const originalSource = getNuxtFontOriginalSource(fontSrc, options.context) || (isConfiguredLocalFontFamily(options.nuxt, font.family) ? fontSrc : void 0);
2547
- if (originalSource)
2548
- mappedSources.push({ font, fontSrc, originalSource });
2549
- else
2550
- options.logger.debug(`Could not map Nuxt Fonts asset to its original source: ${fontSrc}`);
2551
- }
2552
- if (mappedSources.length === 0)
2553
- return;
2554
- const { woff2Decode } = await import('../chunks/decode.mjs');
2555
- for (const { fontSrc, font, originalSource } of mappedSources) {
2556
- const asset = await readNuxtFontAsset(options.nuxt, originalSource);
2612
+ const served = await options.context?.readFont(fontSrc).then((data) => data && { _tag: "Ok", data }).catch((error) => ({ _tag: "Err", reason: error.message }));
2613
+ const asset = served || { _tag: "Err", reason: "not served by Nuxt Fonts" };
2557
2614
  if (asset._tag === "Err") {
2558
2615
  options.logger.debug(`Could not read Nuxt Fonts asset ${fontSrc}: ${asset.reason}`);
2559
2616
  continue;
2560
2617
  }
2618
+ decoder ||= await import('../chunks/decode.mjs');
2619
+ const { woff2Decode } = decoder;
2561
2620
  const decoded = await Promise.resolve().then(() => woff2Decode(asset.data)).catch((error) => {
2562
2621
  options.logger.warn(`Failed to decode Nuxt Fonts asset ${fontSrc}: ${error.message}`);
2563
2622
  return null;
@@ -2578,11 +2637,12 @@ async function convertNuxtWoff2Sources(options) {
2578
2637
  }
2579
2638
  async function prepareWoff2Fonts(options) {
2580
2639
  const { nuxt, logger, fontRequirements, fontState, nuxtFontsContext, fontSubsets, warnOnMissingStaticFonts = true } = options;
2581
- const parsedFonts = await parseFontsFromTemplate(nuxt, { fontState });
2640
+ const parsedFonts = await getResolvedNuxtFonts(nuxt, { fontState });
2582
2641
  const requirementsByFamily = /* @__PURE__ */ new Map();
2583
2642
  for (const font of parsedFonts) {
2584
- if (!requirementsByFamily.has(font.family))
2585
- requirementsByFamily.set(font.family, getFamilyRequirements(fontRequirements, font.family));
2643
+ if (!requirementsByFamily.has(font.family)) {
2644
+ requirementsByFamily.set(font.family, fontRequirements.scanned === false ? getResolvedWeights(parsedFonts, font.family) : getFamilyRequirements(fontRequirements, font.family));
2645
+ }
2586
2646
  }
2587
2647
  const hasNonWoff2 = new Set(
2588
2648
  parsedFonts.filter((f) => !f.src.endsWith(".woff2")).map((f) => fontKey(f))
@@ -2645,7 +2705,7 @@ async function prepareWoff2Fonts(options) {
2645
2705
  nuxt,
2646
2706
  logger,
2647
2707
  fontSubsets,
2648
- warnOnMissingStaticFonts
2708
+ warnOnMissingStaticFonts: false
2649
2709
  }).catch((error) => {
2650
2710
  logger.debug("fontless resolution failed:", error);
2651
2711
  return [];
@@ -2654,14 +2714,23 @@ async function prepareWoff2Fonts(options) {
2654
2714
  const key = `${font.family}-${font.weight}-${font.style}`;
2655
2715
  fontState.fallbackMap.set(key, `${STATIC_FONTS_PREFIX}/${font.filename}`);
2656
2716
  }
2717
+ if (warnOnMissingStaticFonts) {
2718
+ const usableFamilies = /* @__PURE__ */ new Set([
2719
+ ...parsedFonts.filter((font) => !font.src.endsWith(".woff2") || fontState.sourceMap.has(font.src)).map((font) => font.family),
2720
+ ...downloaded.map((font) => font.family)
2721
+ ]);
2722
+ for (const family of new Set(families.map((f) => f.family))) {
2723
+ if (!usableFamilies.has(family))
2724
+ logger.warn(`Satori cannot use the Nuxt Fonts files for "${family}", and no provider has static files for it. Satori renders "${family}" text with the bundled Inter font. Use the Takumi renderer for WOFF2 and variable fonts.`);
2725
+ }
2726
+ }
2657
2727
  if (fontState.fallbackMap.size > 0)
2658
2728
  logger.debug(`Resolved ${fontState.fallbackMap.size} static font fallbacks via fontless`);
2659
2729
  else if (warnOnMissingStaticFonts && fontState.sourceMap.size === 0)
2660
2730
  logger.warn(`No static fonts available for Satori. Falling back to bundled Inter font. Consider using the Takumi renderer for variable font support.`);
2661
2731
  }
2662
2732
  async function resolveMissingFontFamilies(options) {
2663
- const { missingFamilies, weights, styles, nuxt, logger, fontSubsets } = options;
2664
- const families = missingFamilies.map((family) => ({ family, weights, styles }));
2733
+ const { families, nuxt, logger, fontSubsets } = options;
2665
2734
  const downloaded = await downloadStaticFonts({ families, nuxt, logger, fontSubsets });
2666
2735
  const results = downloaded.map((f) => ({
2667
2736
  family: f.family,
@@ -2671,13 +2740,13 @@ async function resolveMissingFontFamilies(options) {
2671
2740
  satoriSrc: `${STATIC_FONTS_PREFIX}/${f.filename}`
2672
2741
  }));
2673
2742
  if (results.length > 0)
2674
- logger.debug(`Resolved ${results.length} font files via fontless for: ${missingFamilies.join(", ")}`);
2743
+ logger.debug(`Resolved ${results.length} font files via fontless for: ${families.map((f) => f.family).join(", ")}`);
2675
2744
  return results;
2676
2745
  }
2677
2746
  async function resolveOgImageFonts(options) {
2678
2747
  const { nuxt, hasNuxtFonts, hasSatoriRenderer, hasTakumiRenderer, fontState, fontSubsets, fontRequirements, tw4FontVars, logger, ogFontsDir } = options;
2679
2748
  const staticInterFonts = getStaticInterFonts(ogFontsDir);
2680
- const allFonts = hasNuxtFonts ? await parseFontsFromTemplate(nuxt, { fontState, requiredWeights: fontRequirements.weights }) : [];
2749
+ const allFonts = hasNuxtFonts ? await getResolvedNuxtFonts(nuxt, { fontState, requiredWeights: fontRequirements.weights }) : [];
2681
2750
  if (hasNuxtFonts) {
2682
2751
  const configuredLocalFonts = parseConfiguredLocalFonts(nuxt);
2683
2752
  if (configuredLocalFonts.length > 0) {
@@ -2706,6 +2775,11 @@ async function resolveOgImageFonts(options) {
2706
2775
  logger.debug(`Parsed ${appCssFonts.length} fonts from app CSS @font-face declarations`);
2707
2776
  }
2708
2777
  if ((hasSatoriRenderer || hasTakumiRenderer) && hasNuxtFonts) {
2778
+ const nuxtFontsOptions = nuxt.options.fonts;
2779
+ const glyphSubsetFamilies = [...new Set(allFonts.map((f) => f.family))].filter((family) => isGlyphSubsetFamily(nuxtFontsOptions, family));
2780
+ const glyphSubsetFaces = allFonts.filter((f) => glyphSubsetFamilies.includes(f.family));
2781
+ if (glyphSubsetFaces.length > 0)
2782
+ allFonts.splice(0, allFonts.length, ...allFonts.filter((f) => !glyphSubsetFaces.includes(f)));
2709
2783
  const coveredFamilies = new Set(allFonts.map((f) => f.family));
2710
2784
  let missingFamilies = [];
2711
2785
  const isFontaineFallback = (name) => / fallback$/i.test(name);
@@ -2716,11 +2790,18 @@ async function resolveOgImageFonts(options) {
2716
2790
  if (defaultVar)
2717
2791
  missingFamilies = extractCustomFontFamilies(defaultVar).filter((f) => !coveredFamilies.has(f) && !isFontaineFallback(f));
2718
2792
  }
2793
+ missingFamilies = [.../* @__PURE__ */ new Set([...missingFamilies, ...glyphSubsetFamilies])];
2719
2794
  if (missingFamilies.length > 0) {
2795
+ const familyRequirements = missingFamilies.map((family) => {
2796
+ if (fontRequirements.scanned === false) {
2797
+ const resolved = getResolvedWeights(glyphSubsetFaces, family);
2798
+ if (resolved.weights.length > 0 || resolved.styles.length > 0)
2799
+ return { family, ...resolved };
2800
+ }
2801
+ return { family, weights: fontRequirements.weights, styles: fontRequirements.styles };
2802
+ });
2720
2803
  const additionalFonts = await resolveMissingFontFamilies({
2721
- missingFamilies,
2722
- weights: fontRequirements.weights,
2723
- styles: fontRequirements.styles,
2804
+ families: familyRequirements,
2724
2805
  nuxt,
2725
2806
  logger,
2726
2807
  fontSubsets: effectiveSubsets
@@ -2730,11 +2811,17 @@ async function resolveOgImageFonts(options) {
2730
2811
  });
2731
2812
  allFonts.push(...additionalFonts);
2732
2813
  }
2814
+ const resolvedFamilies = new Set(allFonts.map((f) => f.family));
2815
+ const unresolvedSubsetFaces = glyphSubsetFaces.filter((f) => !resolvedFamilies.has(f.family));
2816
+ if (unresolvedSubsetFaces.length > 0) {
2817
+ logger.warn(`Could not resolve ${[...new Set(unresolvedSubsetFaces.map((f) => f.family))].join(", ")} without \`glyphs\` subsetting. OG images may miss characters outside the configured glyphs.`);
2818
+ allFonts.push(...unresolvedSubsetFaces);
2819
+ }
2733
2820
  }
2734
2821
  const nuxtFontFamilies = new Set(
2735
- hasNuxtFonts ? (await parseFontsFromTemplate(nuxt, { fontState, requiredWeights: fontRequirements.weights })).map((f) => f.family) : []
2822
+ hasNuxtFonts ? (await getResolvedNuxtFonts(nuxt, { fontState, requiredWeights: fontRequirements.weights })).map((f) => f.family) : []
2736
2823
  );
2737
- const fonts = !fontRequirements.hasDynamicBindings ? allFonts.filter(
2824
+ const fonts = !(fontRequirements.hasDynamicBindings || fontRequirements.scanned === false) ? allFonts.filter(
2738
2825
  (f) => nuxtFontFamilies.has(f.family) ? fontRequirements.styles.includes(f.style) : matchesFontRequirements(f, fontRequirements)
2739
2826
  ) : allFonts;
2740
2827
  const fontsByFamily = /* @__PURE__ */ new Map();
@@ -2817,10 +2904,10 @@ function getMissingSiteUrlWarning(input) {
2817
2904
  return null;
2818
2905
  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
2906
  }
2820
- function setupPrerenderHandler(options, resolve, getDetectedRenderers, getCompatibilityMeta = () => ({}), nuxt = useNuxt()) {
2907
+ function setupPrerenderHandler(options, resolve, getDetectedRenderers, nuxt = useNuxt()) {
2821
2908
  nuxt.hooks.hook("nitro:init", async (nitro) => {
2822
2909
  nitro.hooks.hook("prerender:config", async (nitroConfig) => {
2823
- await applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.prerender, resolve, detectedRenderers: getDetectedRenderers(), metadata: getCompatibilityMeta() });
2910
+ await applyNitroPresetCompatibility(nitroConfig, { compatibility: options.compatibility?.prerender, resolve, detectedRenderers: getDetectedRenderers() });
2824
2911
  nitroConfig.wasm = nitroConfig.wasm || {};
2825
2912
  nitroConfig.wasm.esmImport = false;
2826
2913
  nitroConfig.prerender = nitroConfig.prerender || {};
@@ -2854,7 +2941,7 @@ function setupPrerenderHandler(options, resolve, getDetectedRenderers, getCompat
2854
2941
  nitro.hooks.hook("prerender:done", async () => {
2855
2942
  if (orphanedOgHashes.length > 0) {
2856
2943
  logger.warn(
2857
- `Skipped ${orphanedOgHashes.length} orphaned OG image hash URL${orphanedOgHashes.length > 1 ? "s" : ""} during prerender. These URLs were crawled from HTML but their source page was not prerendered, so the hash cache entry was never written. If your pages are prerendered but OG images are generated at runtime, enable \`security.strict\` with a \`security.secret\` to switch to signed dynamic URLs.`
2944
+ `Skipped ${orphanedOgHashes.length} orphaned OG image hash URL${orphanedOgHashes.length > 1 ? "s" : ""} during prerender. These URLs were crawled from HTML but their source page was not prerendered, so the hash cache entry was never written. If your pages are prerendered but OG images are generated at runtime, enable \`security.strict\` with \`NUXT_APP_SECRET\` to switch to signed dynamic URLs.`
2858
2945
  );
2859
2946
  for (const route of orphanedOgHashes)
2860
2947
  logger.info(` ${route}`);
@@ -2918,13 +3005,38 @@ function extractPropNamesFromVue(code) {
2918
3005
  }
2919
3006
  }
2920
3007
 
2921
- function resolveSigningSecret(configSecret, envSecret, generate) {
2922
- if (configSecret === false)
2923
- return { secret: "", hasExplicit: false, optOut: true, generated: false };
2924
- const explicit = configSecret || "" || (envSecret || "");
2925
- if (explicit)
2926
- return { secret: explicit, hasExplicit: true, optOut: false, generated: false };
2927
- return { secret: generate(), hasExplicit: false, optOut: false, generated: true };
3008
+ const DEFAULT_CACHE_MOUNT = "cache/nuxt-og-image";
3009
+ async function resolveRuntimeCacheDriver(storage, nitro, rootDir) {
3010
+ if (storage?.driver !== "lru-cache" && storage?.driver !== "lruCache")
3011
+ return;
3012
+ if (nitro.alias?.["lru-cache"] || await tryResolveModule("lru-cache", directoryToURL(rootDir)))
3013
+ return;
3014
+ const library = await tryResolveModule("lru-cache", import.meta.url);
3015
+ if (!library)
3016
+ throw new Error('[nuxt-og-image] Cannot resolve the runtime cache dependency "lru-cache".');
3017
+ nitro.alias ||= {};
3018
+ nitro.alias["lru-cache"] = library;
3019
+ }
3020
+ function findStorageMount(nitro, prefix) {
3021
+ return Object.entries(nitro.storage || {}).map(([key, storage]) => ({ mount: key.replaceAll(":", "/").replace(/^\/+|\/+$/g, ""), storage })).filter(({ mount }) => prefix === mount || prefix.startsWith(`${mount}/`)).sort((a, b) => b.mount.length - a.mount.length)[0]?.storage;
3022
+ }
3023
+ function configureRuntimeCacheStorage(config, nitro) {
3024
+ if (typeof config.runtimeCacheStorage === "object")
3025
+ return config.runtimeCacheStorage;
3026
+ if (typeof config.runtimeCacheStorage === "string")
3027
+ return findStorageMount(nitro, `${config.runtimeCacheStorage.replaceAll(":", "/").replace(/^\/+|\/+$/g, "")}/nuxt-og-image`);
3028
+ if (config.runtimeCacheStorage !== true)
3029
+ return;
3030
+ const configuredStorage = findStorageMount(nitro, DEFAULT_CACHE_MOUNT);
3031
+ if (configuredStorage)
3032
+ return configuredStorage;
3033
+ nitro.storage ||= {};
3034
+ nitro.storage[DEFAULT_CACHE_MOUNT] = {
3035
+ driver: "lru-cache",
3036
+ max: 0,
3037
+ maxSize: 64 * 1024 * 1024
3038
+ };
3039
+ return nitro.storage[DEFAULT_CACHE_MOUNT];
2928
3040
  }
2929
3041
 
2930
3042
  const RE_NAMED_CALL = /\b(defineOgImage(?:Component)?)\s*\(\s*(['"`])([\w.-]+)\2/g;
@@ -3522,9 +3634,6 @@ async function transformVueTemplate(code, options) {
3522
3634
  if (collector.dynamicStyleProps) {
3523
3635
  Object.assign(styleProps, collector.dynamicStyleProps);
3524
3636
  }
3525
- if (styleProps.display === "flex" && !styleProps["flex-direction"]) {
3526
- styleProps["flex-direction"] = "row";
3527
- }
3528
3637
  const styleStr = Object.entries(styleProps).map(([prop, value]) => `${prop}: ${escapeAttrValue(value)}`).join("; ");
3529
3638
  const hasResolvedStyles = Object.keys(styleProps).length > 0;
3530
3639
  const hasUnresolved = unresolvedClasses.length > 0;
@@ -3711,16 +3820,12 @@ function getMimeType(ext) {
3711
3820
  return mimeTypes[ext] || "application/octet-stream";
3712
3821
  }
3713
3822
  const UNSUPPORTED_CLASS_PATTERNS = [
3714
- /^z-/,
3715
- // z-index not supported by Satori
3716
3823
  /^ring(-|$)/,
3717
3824
  // Ring utilities - not supported
3718
3825
  /^divide(-|$)/,
3719
3826
  // Divide utilities - not supported
3720
3827
  /^space-(x|y)(-|$)/,
3721
3828
  // Space utilities - use gap instead
3722
- /^backdrop-/,
3723
- // Backdrop utilities - not supported
3724
3829
  // Note: filter utilities (blur, brightness, etc.) ARE supported by Satori
3725
3830
  // Note: skew transforms ARE supported by Satori
3726
3831
  /^transition(-|$)/,
@@ -4984,10 +5089,15 @@ declare module '#og-image-virtual/component-names.mjs' {
4984
5089
  export const componentNames: OgImageComponent[]
4985
5090
  }
4986
5091
 
5092
+ declare module '#og-image-virtual/signing.mjs' {
5093
+ export const rendersImages: boolean
5094
+ }
5095
+
4987
5096
  declare module '#og-image-virtual/build-dir.mjs' {
4988
5097
  export const buildDir: string
4989
5098
  export const rootDir: string
4990
5099
  export const staticFontCacheDir: string
5100
+ export const resolvedFontPaths: Record<string, string>
4991
5101
  }
4992
5102
 
4993
5103
  declare module '#og-image/compatibility' {
@@ -5076,10 +5186,7 @@ declare module '#og-image-cache' {
5076
5186
  nitroInterfaces: {
5077
5187
  NitroApp: `_ogImageCacheBackendWarned?: boolean
5078
5188
  _ogImageIconsData?: OgImageIconsData`,
5079
- NitroRuntimeConfig: `'nuxt-og-image': OgImageRuntimeConfig
5080
- ogImage: {
5081
- secret: string
5082
- }`
5189
+ NitroRuntimeConfig: `'nuxt-og-image': OgImageRuntimeConfig`
5083
5190
  },
5084
5191
  routeRules: "ogImage?: false | OgImageOptions & Record<string, any>",
5085
5192
  routeConfig: "ogImage?: false | OgImageOptions & Record<string, any>",
@@ -5088,6 +5195,7 @@ ogImage: {
5088
5195
  eventContext: `_nitro?: {
5089
5196
  routeRules?: import('${nitroCompatibility.nitroTypesModule}').NitroRouteRules
5090
5197
  }
5198
+ _ogImageSigningSecret?: string
5091
5199
  _ogImagePrerenderPaths?: Map<string, string>`
5092
5200
  });
5093
5201
  return `/// <reference path="./og-image-virtual.d.ts" />
@@ -5106,46 +5214,6 @@ const RE_INLINE_CONFIG = /const\s+inlineConfig\s*=\s*(\{[\s\S]*?\n\})/;
5106
5214
  const RE_PASCAL_TO_KEBAB = /([a-z])([A-Z])/g;
5107
5215
  const RE_TILDE_PREFIX = /^~\//;
5108
5216
  const IS_MODULE_DEVELOPMENT = import.meta.filename.endsWith(".ts");
5109
- async function findPackageVersionFromEntry(pkg, resolver) {
5110
- const entry = await resolver.resolvePath(pkg, { fallbackToOriginal: false }).catch(() => {
5111
- return null;
5112
- });
5113
- if (!entry || entry === pkg)
5114
- return;
5115
- let dir = dirname(entry);
5116
- while (dir && dir !== dirname(dir)) {
5117
- const pkgPath = join(dir, "package.json");
5118
- if (existsSync(pkgPath)) {
5119
- const pkgJson = await readPackageJSON(pkgPath);
5120
- if (pkgJson?.name === pkg)
5121
- return pkgJson.version;
5122
- }
5123
- dir = dirname(dir);
5124
- }
5125
- }
5126
- function normalizeTakumiVersion(version) {
5127
- const major = version ? Number.parseInt(version.split(".")[0], 10) : Number.NaN;
5128
- if (major >= 2)
5129
- return 2;
5130
- if (major === 1)
5131
- return 1;
5132
- }
5133
- async function detectTakumiVersion(resolver, binding) {
5134
- const packages = binding === "wasm" ? ["@takumi-rs/wasm", "@takumi-rs/core"] : ["@takumi-rs/core", "@takumi-rs/wasm"];
5135
- const versions = (await Promise.all(packages.map(async (pkg) => {
5136
- const version = await findPackageVersionFromEntry(pkg, resolver);
5137
- const major = normalizeTakumiVersion(version);
5138
- return version && major ? { pkg, version, major } : void 0;
5139
- }))).filter(Boolean);
5140
- const detectedMajors = new Set(versions.map(({ major }) => major));
5141
- if (detectedMajors.size > 1) {
5142
- const preferred = versions[0];
5143
- logger.warn(
5144
- `Mismatched @takumi-rs package major versions detected (${versions.map(({ pkg, version }) => `${pkg}@${version}`).join(", ")}). Using ${preferred.pkg}@${preferred.version} for Takumi v${preferred.major} compatibility.`
5145
- );
5146
- }
5147
- return versions[0]?.major;
5148
- }
5149
5217
  function isProviderEnabledForEnv(provider, nuxt, config) {
5150
5218
  return nuxt.options.dev && config.compatibility?.dev?.[provider] !== false || !nuxt.options.dev && (config.compatibility?.runtime?.[provider] !== false || config.compatibility?.prerender?.[provider] !== false);
5151
5219
  }
@@ -5154,7 +5222,7 @@ const module$1 = defineNuxtModule({
5154
5222
  meta: {
5155
5223
  name: "nuxt-og-image",
5156
5224
  compatibility: {
5157
- nuxt: ">=3.16.0"
5225
+ nuxt: "^4.6.1 || ^5.0.0-0"
5158
5226
  },
5159
5227
  configKey: "ogImage"
5160
5228
  },
@@ -5166,8 +5234,10 @@ const module$1 = defineNuxtModule({
5166
5234
  "@nuxt/fonts": {
5167
5235
  optional: true
5168
5236
  },
5169
- "nuxt-site-config": {
5170
- version: ">=3.2"
5237
+ // Keep package-relative resolution until the minimum Nuxt version includes this fix:
5238
+ // https://github.com/nuxt/nuxt/pull/36542
5239
+ [fileURLToPath(import.meta.resolve("nuxt-site-config"))]: {
5240
+ version: ">=5.0.0"
5171
5241
  }
5172
5242
  },
5173
5243
  defaults() {
@@ -5204,17 +5274,28 @@ const module$1 = defineNuxtModule({
5204
5274
  resolve,
5205
5275
  resolvePath: async (path, opts) => fixSharedPath(await _resolver.resolvePath(path, opts))
5206
5276
  };
5207
- const { version } = await readPackageJSON(resolve("../package.json"));
5208
- const userAppPkgJson = await readPackageJSON(nuxt.options.rootDir).catch(() => ({ dependencies: {}, devDependencies: {} }));
5277
+ const packageManifest = findPackageJSON("./", import.meta.url);
5278
+ if (!packageManifest)
5279
+ throw new Error("Could not resolve the Nuxt OG Image package manifest.");
5280
+ const { version } = await readPackageManifest(packageManifest);
5281
+ const userAppPkgJson = await readPackageManifest(join(nuxt.options.rootDir, "package.json")).catch(() => ({ dependencies: {}, devDependencies: {} }));
5209
5282
  logger.level = config.debug || nuxt.options.debug ? 4 : 3;
5283
+ nuxt.options.build.transpile.push(resolve("./runtime/app"));
5210
5284
  if (config.enabled === false) {
5211
5285
  logger.info("The module is disabled, skipping setup.");
5286
+ setupRuntimeAliases({ namespace: "#og-image", app: resolve("./runtime/app/disabled") }, nuxt);
5287
+ nuxt.options.alias["#og-image/components"] = resolve("./runtime/app/disabled/components");
5212
5288
  ["defineOgImage", "defineOgImageComponent", "defineOgImageScreenshot"].forEach((name) => {
5213
- addImports({ name, from: resolve(`./runtime/app/composables/mock`) });
5289
+ addImports({ name, from: "#og-image/app" });
5214
5290
  });
5215
5291
  return;
5216
5292
  }
5217
5293
  const nitroCompatibility = setupNitroRuntimeCompatibility(nuxt);
5294
+ if (nitroCompatibility._tag === "nitro-v2") {
5295
+ nuxt.options.nitro.externals ||= {};
5296
+ nuxt.options.nitro.externals.inline ||= [];
5297
+ nuxt.options.nitro.externals.inline.push(resolve("./runtime"));
5298
+ }
5218
5299
  if (nuxt.options.dev && nitroCompatibility._tag === "nitro-v2")
5219
5300
  nuxt.options.nitro.noExternals ??= false;
5220
5301
  const setRuntimeAlias = (id, path) => {
@@ -5226,6 +5307,12 @@ const module$1 = defineNuxtModule({
5226
5307
  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 }`).");
5227
5308
  return;
5228
5309
  }
5310
+ if (config.publish) {
5311
+ const parsed = parsePublishConfig(config.publish);
5312
+ if (parsed._tag === "Err")
5313
+ throw new Error(`[nuxt-og-image] ${parsed.reason}`);
5314
+ config.publish = parsed.value;
5315
+ }
5229
5316
  if (config.cacheMaxAgeSeconds != null) {
5230
5317
  config.defaults = config.defaults || {};
5231
5318
  config.defaults.cacheMaxAgeSeconds = config.cacheMaxAgeSeconds;
@@ -5233,36 +5320,9 @@ const module$1 = defineNuxtModule({
5233
5320
  if (config.debug && !nuxt.options.dev) {
5234
5321
  logger.warn("`ogImage.debug` is enabled in production. This exposes the `/_og/debug.json` endpoint and should not be enabled in production. Disable it before deploying.");
5235
5322
  }
5236
- const signing = resolveSigningSecret(
5237
- config.security?.secret,
5238
- process.env.NUXT_OG_IMAGE_SECRET,
5239
- () => randomBytes(32).toString("base64url")
5240
- );
5241
- const resolvedSecret = signing.secret;
5242
- if (config.security?.strict && !signing.hasExplicit) {
5243
- throw new Error("[nuxt-og-image] `security.strict` requires a signing secret. Generate one with: npx nuxt-og-image generate-secret");
5323
+ if (config.security?.strict && config.security.secret === false) {
5324
+ throw new Error("[nuxt-og-image] Strict mode requires URL signing. Remove security.secret: false and set NUXT_APP_SECRET.");
5244
5325
  }
5245
- nuxt.hook("nitro:init", (nitro) => {
5246
- const hasServerRuntime = !nitro.options.static && !nuxt.options._generate;
5247
- if (!nuxt.options.dev || config.zeroRuntime || !hasServerRuntime)
5248
- return;
5249
- if (signing.optOut) {
5250
- logger.warn([
5251
- "OG image URL signing is disabled (`security.secret: false`).",
5252
- "Attackers can craft image requests and poison the cache so your real OG image URLs serve attacker-controlled content (content spoofing).",
5253
- "Signing will be required in v7 whenever runtime image generation is enabled. Leave signing on, or set an explicit secret:",
5254
- " NUXT_OG_IMAGE_SECRET=<secret>",
5255
- " Generate one with: npx nuxt-og-image generate-secret"
5256
- ].join("\n"));
5257
- } else if (signing.generated) {
5258
- logger.warn([
5259
- "OG image URLs are signed with an auto-generated secret that changes every build.",
5260
- "This is fine for single-instance deploys; for rolling or multi-instance deploys set a stable secret:",
5261
- " NUXT_OG_IMAGE_SECRET=<secret>",
5262
- " Generate one with: npx nuxt-og-image generate-secret"
5263
- ].join("\n"));
5264
- }
5265
- });
5266
5326
  const ogImageConfig = config;
5267
5327
  for (const key of Object.keys(REMOVED_CONFIG)) {
5268
5328
  if (key !== "chromium-node" && key !== "browser-node" && key in ogImageConfig && ogImageConfig[key] !== void 0) {
@@ -5274,17 +5334,17 @@ const module$1 = defineNuxtModule({
5274
5334
  addConfigWarning("browser-node");
5275
5335
  }
5276
5336
  let hasNuxtFonts = hasNuxtModule("@nuxt/fonts");
5277
- if (hasNuxtFonts && !await hasNuxtModuleCompatibility("@nuxt/fonts", ">=0.13.0")) {
5337
+ if (hasNuxtFonts && !await hasNuxtModuleCompatibility("@nuxt/fonts", ">=1.0.0")) {
5278
5338
  const version2 = await getNuxtModuleVersion("@nuxt/fonts");
5279
5339
  logger.error(
5280
- `@nuxt/fonts ${version2} is not supported. Version 0.13.0+ is required for OG image font extraction.
5340
+ `@nuxt/fonts ${version2} is not supported. Version 1.0.0+ is required for OG image font extraction.
5281
5341
  Falling back to bundled Inter font.
5282
5342
 
5283
5343
  To fix, update @nuxt/fonts:
5284
5344
  pnpm add @nuxt/fonts@latest
5285
5345
 
5286
5346
  Or pin to a specific version in package.json:
5287
- "@nuxt/fonts": "^0.13.0"`
5347
+ "@nuxt/fonts": "^1.0.0"`
5288
5348
  );
5289
5349
  hasNuxtFonts = false;
5290
5350
  }
@@ -5302,7 +5362,6 @@ Or pin to a specific version in package.json:
5302
5362
  const preset = resolveOgImagePreset(nuxt.options.nitro);
5303
5363
  const targetCompatibility = getPresetNitroPresetCompatibility(preset);
5304
5364
  setRuntimeAlias("#og-image/bindings/fetch", resolve(`./runtime/server/og-image/bindings/fetch/${targetCompatibility.fetch ?? "node"}`));
5305
- const runtimeCompatibilityMeta = {};
5306
5365
  const normalizedPreset = preset.replace(RE_LEGACY_SUFFIX, "");
5307
5366
  const isCloudflareWorkers = ["cloudflare", "cloudflare-module", "cloudflare-durable"].includes(normalizedPreset);
5308
5367
  if (isCloudflareWorkers) {
@@ -5490,7 +5549,8 @@ Or pin to a specific version in package.json:
5490
5549
  const ogImageTransformedFiles = /* @__PURE__ */ new Set();
5491
5550
  const fontState = {
5492
5551
  sourceMap: /* @__PURE__ */ new Map(),
5493
- fallbackMap: /* @__PURE__ */ new Map()
5552
+ fallbackMap: /* @__PURE__ */ new Map(),
5553
+ resolvedFaces: /* @__PURE__ */ new Map()
5494
5554
  };
5495
5555
  const ogImageComponentCtx = { components: [], detectedRenderers: /* @__PURE__ */ new Set() };
5496
5556
  nuxt.hook("modules:done", () => {
@@ -5518,7 +5578,7 @@ Or pin to a specific version in package.json:
5518
5578
  const p = loadCssMetadata().then(async () => {
5519
5579
  let knownFamilies;
5520
5580
  if (hasNuxtFonts) {
5521
- const resolvedFonts = await parseFontsFromTemplate(nuxt, { fontState });
5581
+ const resolvedFonts = await getResolvedNuxtFonts(nuxt, { fontState });
5522
5582
  knownFamilies = buildFontFamilyCanonicalMap(resolvedFonts.map((f) => f.family));
5523
5583
  }
5524
5584
  const families = resolveFontFamilies([...reqs.familyClasses], [...reqs.familyNames], cssMetadata.fontVars, knownFamilies);
@@ -5533,6 +5593,7 @@ Or pin to a specific version in package.json:
5533
5593
  category: component.category
5534
5594
  };
5535
5595
  recomputeFontRequirements();
5596
+ refreshDevFonts();
5536
5597
  const mapPath = join(nuxt.options.buildDir, "cache", "og-image", "component-font-map.json");
5537
5598
  await mkdir(join(nuxt.options.buildDir, "cache", "og-image"), { recursive: true });
5538
5599
  await writeFile(mapPath, JSON.stringify(fontRequirementsState.componentMap));
@@ -5612,7 +5673,7 @@ ${familyInfo}`);
5612
5673
  }
5613
5674
  }
5614
5675
  if (isProviderEnabledForEnv("browser", nuxt, config)) {
5615
- const hasChromeLocally = checkLocalChrome();
5676
+ const hasChromeLocally = await checkLocalChrome();
5616
5677
  const hasPlaywrightDependency = await hasResolvableDependency("playwright");
5617
5678
  const browserCompatibilityFlags = {
5618
5679
  prerender: config.compatibility?.prerender?.browser,
@@ -5654,7 +5715,6 @@ ${familyInfo}`);
5654
5715
  prerender: { browser: browserBinding.prerender }
5655
5716
  });
5656
5717
  }
5657
- await installNuxtSiteConfig();
5658
5718
  nuxt.options.experimental.componentIslands ||= true;
5659
5719
  if (config.debug || nuxt.options.dev) {
5660
5720
  addServerHandler({
@@ -5663,6 +5723,13 @@ ${familyInfo}`);
5663
5723
  handler: resolve("./runtime/server/routes/debug.json")
5664
5724
  });
5665
5725
  }
5726
+ if (nuxt.options.dev) {
5727
+ addServerHandler({
5728
+ lazy: true,
5729
+ route: "/_og-static-fonts/**",
5730
+ handler: resolve("./runtime/server/routes/static-font")
5731
+ });
5732
+ }
5666
5733
  addServerHandler({
5667
5734
  lazy: true,
5668
5735
  route: "/_og/d/**",
@@ -5749,10 +5816,10 @@ ${familyInfo}`);
5749
5816
  }
5750
5817
  }
5751
5818
  }
5752
- for (const layer of nuxt.options._layers || []) {
5753
- registerOgComponentDir(join(layer.cwd, "components"));
5754
- if (layer.config?.srcDir && layer.config.srcDir !== layer.cwd)
5755
- registerOgComponentDir(join(layer.config.srcDir, "components"));
5819
+ for (const layer of getLayerDirectories(nuxt)) {
5820
+ registerOgComponentDir(join(layer.root, "components"));
5821
+ if (layer.app !== layer.root)
5822
+ registerOgComponentDir(join(layer.app, "components"));
5756
5823
  }
5757
5824
  nuxt.hook("components:dirs", (dirs) => {
5758
5825
  for (const dirConfig of dirs) {
@@ -5799,11 +5866,14 @@ ${familyInfo}`);
5799
5866
  for (const renderer of ogImageComponentCtx.detectedRenderers) {
5800
5867
  const binding = getRecommendedBinding(renderer, targetCompatibility);
5801
5868
  const missing = await getMissingDependencies(renderer, binding);
5802
- if (missing.length === 0 || renderer === "browser") {
5869
+ const hasOtherBrowserLauncher = renderer === "browser" && (typeof config.browser === "object" && config.browser.provider === "cloudflare" || await hasResolvableDependency("playwright"));
5870
+ if (missing.length === 0 || hasOtherBrowserLauncher) {
5803
5871
  availableRenderers.add(renderer);
5804
5872
  } else {
5805
5873
  const installSpecs = await getMissingDependencyInstallSpecs(renderer, binding);
5806
5874
  const action = resolveMissingRendererAction({ renderer, installSpecs, dev: nuxt.options.dev, interactive: canPromptInteractively() });
5875
+ if (action._tag === "CheckUsage" && renderer === "browser" && config.browser)
5876
+ throw new Error(`[nuxt-og-image] ${getMissingRendererMessage(renderer, installSpecs)}`);
5807
5877
  if (action._tag === "CheckUsage") {
5808
5878
  missingRenderers[renderer] = installSpecs;
5809
5879
  } else if (action._tag === "Report") {
@@ -5834,14 +5904,6 @@ ${familyInfo}`);
5834
5904
  availableRenderers.add(renderer);
5835
5905
  }
5836
5906
  }
5837
- runtimeCompatibilityMeta.takumiVersion = await detectTakumiVersion(
5838
- resolver,
5839
- getRecommendedBinding("takumi", targetCompatibility)
5840
- );
5841
- if (ogImageComponentCtx.detectedRenderers.has("takumi") && !runtimeCompatibilityMeta.takumiVersion) {
5842
- logger.warn("Unable to detect @takumi-rs version. Falling back to Takumi v1 renderer compatibility.");
5843
- runtimeCompatibilityMeta.takumiVersion = 1;
5844
- }
5845
5907
  if (nuxt.options.dev) {
5846
5908
  const communityRenderers = /* @__PURE__ */ new Set();
5847
5909
  for (const renderer of ["satori", "takumi", "browser"]) {
@@ -5974,25 +6036,20 @@ Run: npx nuxt-og-image migrate v6`;
5974
6036
  },
5975
6037
  options: { mode: "server" }
5976
6038
  });
6039
+ addTemplate({
6040
+ filename: "nuxt-og-image/client-config.mjs",
6041
+ getContents() {
6042
+ const config2 = nuxt.options.runtimeConfig.public["nuxt-og-image"];
6043
+ return `export const hasServerRuntime = ${config2.hasServerRuntime === true}`;
6044
+ }
6045
+ });
5977
6046
  nuxt.options.nitro.virtual = nuxt.options.nitro.virtual || {};
5978
6047
  nuxt.options.nitro.virtual["#og-image-virtual/component-names.mjs"] = () => {
5979
6048
  return `export const componentNames = ${JSON.stringify(ogImageComponentCtx.components)}`;
5980
6049
  };
5981
6050
  nuxt.options.nitro.virtual["#og-image/island-hash"] = () => {
5982
6051
  const islandHashFile = join(nuxt.options.appDir, "island-hash.js");
5983
- const [nuxtMajor = 0, nuxtMinor = 0] = getNuxtVersion(nuxt).split(".").map((v) => Number.parseInt(v));
5984
- if (nuxtMajor > 4 || nuxtMajor === 4 && nuxtMinor >= 5)
5985
- return `export { getIslandHash } from '${islandHashFile}'`;
5986
- if (existsSync(islandHashFile)) {
5987
- return `import { computeIslandHash } from '${islandHashFile}'
5988
- export function getIslandHash({ name, props, context, source }) {
5989
- return computeIslandHash(name, props ?? {}, context ?? {}, source)
5990
- }`;
5991
- }
5992
- return `import { hash } from 'ohash'
5993
- export function getIslandHash({ name, props, context, source }) {
5994
- return hash([name, props ?? {}, context ?? {}, source]).replace(/[-_]/g, '')
5995
- }`;
6052
+ return `export { getIslandHash } from '${islandHashFile}'`;
5996
6053
  };
5997
6054
  nuxt.options.nitro.publicAssets ||= [];
5998
6055
  const staticFontCacheDir = getStaticFontCacheDir(nuxt.options.buildDir);
@@ -6000,11 +6057,13 @@ export function getIslandHash({ name, props, context, source }) {
6000
6057
  nuxt.options.nitro.publicAssets.push({
6001
6058
  dir: staticFontCacheDir,
6002
6059
  baseURL: "/_og-static-fonts",
6060
+ fallthrough: nuxt.options.dev,
6003
6061
  maxAge: 60 * 60 * 24 * 365
6004
6062
  });
6005
6063
  nuxt.options.nitro.publicAssets.push({
6006
6064
  dir: resolve("./runtime/public/_og-fonts"),
6007
6065
  baseURL: "/_og-static-fonts",
6066
+ fallthrough: nuxt.options.dev,
6008
6067
  maxAge: 60 * 60 * 24 * 365
6009
6068
  });
6010
6069
  nuxt.options.nitro.virtual["#og-image-virtual/public-assets.mjs"] = async () => {
@@ -6029,13 +6088,36 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
6029
6088
  };
6030
6089
  const hasSatoriRenderer = () => ogImageComponentCtx.detectedRenderers.has("satori");
6031
6090
  const hasTakumiRenderer = () => ogImageComponentCtx.detectedRenderers.has("takumi");
6032
- let fontContext = null;
6091
+ const nuxtFontFiles = /* @__PURE__ */ new Map();
6092
+ const fontContext = hasNuxtFonts ? { readFont: async (url) => nuxtFontFiles.get(url)?.() } : null;
6033
6093
  let fontProcessingDone = false;
6094
+ let builtFontKey;
6095
+ const currentFontKey = () => JSON.stringify([
6096
+ getResolvedFontStateKey(fontState),
6097
+ fontRequirementsState.weights,
6098
+ fontRequirementsState.styles
6099
+ ]);
6100
+ let reloadNitro;
6101
+ function refreshDevFonts() {
6102
+ if (!reloadNitro || builtFontKey === void 0 || builtFontKey === currentFontKey())
6103
+ return;
6104
+ reloadNitro().catch((error) => {
6105
+ logger.warn(`Could not rebuild the OG image font list: ${error.message}`);
6106
+ });
6107
+ }
6108
+ if (nuxt.options.dev) {
6109
+ nuxt.hook("nitro:init", (nitro) => {
6110
+ let pending;
6111
+ reloadNitro = () => pending ||= Promise.resolve().then(async () => {
6112
+ pending = void 0;
6113
+ fontProcessingDone = false;
6114
+ await nitro.hooks.callHook("rollup:reload");
6115
+ });
6116
+ });
6117
+ }
6034
6118
  nuxt.options.nitro.virtual["#og-image/fonts"] = async () => {
6119
+ builtFontKey = currentFontKey();
6035
6120
  await loadCssMetadata();
6036
- if (hasNuxtFonts && fontContext) {
6037
- persistFontUrlMapping({ fontContext, buildDir: nuxt.options.buildDir, logger });
6038
- }
6039
6121
  if (!fontProcessingDone && hasSatoriRenderer() && hasNuxtFonts) {
6040
6122
  if (pendingFontRequirements.length > 0)
6041
6123
  await Promise.all(pendingFontRequirements);
@@ -6065,7 +6147,7 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
6065
6147
  return `export default ${JSON.stringify(fonts)}`;
6066
6148
  };
6067
6149
  nuxt.options.nitro.virtual["#og-image/fonts-available"] = async () => {
6068
- const fonts = hasNuxtFonts ? await parseFontsFromTemplate(nuxt, { fontState, requiredWeights: fontRequirementsState.weights }) : [];
6150
+ const fonts = hasNuxtFonts ? await getResolvedNuxtFonts(nuxt, { fontState, requiredWeights: fontRequirementsState.weights }) : [];
6069
6151
  return `export default ${JSON.stringify(fonts)}`;
6070
6152
  };
6071
6153
  nuxt.options.nitro.virtual["#og-image/font-requirements"] = async () => {
@@ -6075,7 +6157,8 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
6075
6157
  weights: fontRequirementsState.weights,
6076
6158
  styles: fontRequirementsState.styles,
6077
6159
  families: fontRequirementsState.families,
6078
- hasDynamicBindings: fontRequirementsState.hasDynamicBindings
6160
+ // Unanalysed components (webpack, rspack) may use any font, like dynamic bindings
6161
+ hasDynamicBindings: fontRequirementsState.hasDynamicBindings || !fontRequirementsState.scanned
6079
6162
  })}
6080
6163
  export const hasNuxtFonts = ${JSON.stringify(hasNuxtFonts)}`;
6081
6164
  if (nuxt.options.dev) {
@@ -6098,17 +6181,34 @@ export function getComponentFontMap() { return _staticMap }`;
6098
6181
  export const tw4Breakpoints = ${JSON.stringify(cssMetadata.breakpoints)}
6099
6182
  export const tw4Colors = ${JSON.stringify(cssMetadata.colors)}`;
6100
6183
  };
6101
- nuxt.options.nitro.virtual["#og-image-virtual/build-dir.mjs"] = () => {
6184
+ nuxt.options.nitro.virtual["#og-image-virtual/build-dir.mjs"] = async () => {
6185
+ const resolvedFontPaths = await cacheResolvedFontFiles(nuxtFontFiles, join(nuxt.options.buildDir, "cache", "og-image", "resolved-fonts"));
6102
6186
  return `export const buildDir = ${JSON.stringify(nuxt.options.buildDir)}
6103
6187
  export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}
6188
+ export const resolvedFontPaths = ${JSON.stringify(resolvedFontPaths)}
6104
6189
  export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.options.buildDir))}`;
6105
6190
  };
6106
6191
  if (hasNuxtFonts) {
6107
- nuxt.hook("fonts:public-asset-context", (ctx) => {
6108
- fontContext = ctx;
6192
+ nuxt.hook("fonts:resolved", (font) => {
6193
+ fontState.resolvedFaces.set(font.fontFamily, font.fonts);
6194
+ for (const file of font.files)
6195
+ nuxtFontFiles.set(file.url, file.readFont);
6196
+ refreshDevFonts();
6197
+ });
6198
+ warnWhenFontsUnreported({
6199
+ nuxt,
6200
+ logger,
6201
+ fontState,
6202
+ loadCssMetadata,
6203
+ expectedFamilies: () => [
6204
+ ...fontRequirementsState.families,
6205
+ ...Object.values(cssMetadata.fontVars).flatMap((value) => extractCustomFontFamilies(value)),
6206
+ ...fontFamiliesFromCssEntries(nuxt.options.css, nuxt.options.srcDir, nuxt.options.rootDir)
6207
+ ]
6109
6208
  });
6209
+ const globalFamilies = new Set((nuxt.options.fonts?.families || []).filter((f) => f.global).map((f) => f.name.toLowerCase()));
6210
+ fontState.isOgFamily = (family) => globalFamilies.has(family.toLowerCase()) || !fontRequirementsState.scanned || fontRequirementsState.families.some((f) => f.toLowerCase() === family.toLowerCase());
6110
6211
  nuxt.hook("vite:compiled", async () => {
6111
- persistFontUrlMapping({ fontContext, buildDir: nuxt.options.buildDir, logger });
6112
6212
  if (fontProcessingDone || !hasSatoriRenderer())
6113
6213
  return;
6114
6214
  if (pendingFontRequirements.length === 0)
@@ -6165,6 +6265,22 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6165
6265
  }
6166
6266
  if (!cacheEnabled)
6167
6267
  baseCacheKey = false;
6268
+ let hasUnboundedRuntimeCache = false;
6269
+ nuxt.hooks.hook("nitro:config", async (nitroConfig) => {
6270
+ const storage = configureRuntimeCacheStorage(config, nitroConfig);
6271
+ await resolveRuntimeCacheDriver(storage, nitroConfig, nuxt.options.rootDir);
6272
+ hasUnboundedRuntimeCache = storage?.driver === "memory";
6273
+ });
6274
+ nuxt.hooks.hook("nitro:init", (nitro) => {
6275
+ const hasServerRuntime = !nitro.options.static && !nuxt.options._generate;
6276
+ const publishStorage = hasServerRuntime && !config.zeroRuntime ? nitro.options.storage : { ...nitro.options.storage, ...nitro.options.devStorage };
6277
+ if (!nuxt.options.dev && config.publish && !hasPublishMount(config.publish.storage, publishStorage || {})) {
6278
+ logger.warn(`Publish storage mount "${config.publish.storage}" is not configured. Set nitro.storage or mount it in a Nitro plugin.`);
6279
+ }
6280
+ if (!nuxt.options.dev && hasServerRuntime && !config.zeroRuntime && Number(config.defaults?.cacheMaxAgeSeconds) > 0 && hasUnboundedRuntimeCache) {
6281
+ logger.warn("The runtime OG image cache uses memory storage without a size limit. Use lru-cache with maxSize or an expiring driver. See https://nuxtseo.com/docs/og-image/guides/runtime-cache");
6282
+ }
6283
+ });
6168
6284
  const buildCachePath = typeof config.buildCache === "object" && config.buildCache.base ? config.buildCache.base : "node_modules/.cache/nuxt-seo/og-image";
6169
6285
  const buildCacheDir = config.buildCache ? join(nuxt.options.rootDir, buildCachePath) : void 0;
6170
6286
  nuxt.hooks.hook("modules:done", async () => {
@@ -6189,10 +6305,9 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6189
6305
  // avoid adding credentials
6190
6306
  baseCacheKey,
6191
6307
  buildCacheDir,
6308
+ publish: config.publish ? { ...config.publish, cacheVersion } : void 0,
6192
6309
  hasNuxtIcon: hasNuxtModule("nuxt-icon") || hasNuxtModule("@nuxt/icon"),
6193
6310
  colorPreference,
6194
- // @ts-expect-error runtime type
6195
- isNuxtContentDocumentDriven: !!nuxt.options.content?.documentDriven,
6196
6311
  cssFramework: cssFramework || "none",
6197
6312
  browserEnabled: !!config.browser,
6198
6313
  // Browser renderer config for cloudflare binding access
@@ -6208,7 +6323,7 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6208
6323
  imageFetchTimeout: config.security?.imageFetchTimeout ?? 3e3,
6209
6324
  maxQueryParamSize: config.security?.maxQueryParamSize ?? (config.security?.strict ? 2048 : null),
6210
6325
  restrictRuntimeImagesToOrigin: config.security?.restrictRuntimeImagesToOrigin === true || config.security?.strict && config.security?.restrictRuntimeImagesToOrigin == null ? [] : config.security?.restrictRuntimeImagesToOrigin || false,
6211
- secret: resolvedSecret
6326
+ secret: config.security?.secret === false ? false : ""
6212
6327
  }
6213
6328
  };
6214
6329
  if (nuxt.options.dev) {
@@ -6219,11 +6334,6 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6219
6334
  }
6220
6335
  nuxt.hooks.callHook("nuxt-og-image:runtime-config", runtimeConfig);
6221
6336
  nuxt.options.runtimeConfig["nuxt-og-image"] = runtimeConfig;
6222
- const existingOgImageCfg = nuxt.options.runtimeConfig.ogImage;
6223
- nuxt.options.runtimeConfig.ogImage = {
6224
- ...existingOgImageCfg && typeof existingOgImageCfg === "object" ? existingOgImageCfg : {},
6225
- secret: existingOgImageCfg?.secret || (signing.hasExplicit ? signing.secret : "")
6226
- };
6227
6337
  nuxt.options.runtimeConfig.public = {
6228
6338
  ...nuxt.options.runtimeConfig.public,
6229
6339
  "nuxt-og-image": {
@@ -6245,9 +6355,8 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6245
6355
  };
6246
6356
  });
6247
6357
  const getDetectedRenderers = () => new Set([...ogImageComponentCtx.detectedRenderers].filter((r) => !missingRenderers[r]));
6248
- const getCompatibilityMeta = () => runtimeCompatibilityMeta;
6249
6358
  if (nuxt.options.dev) {
6250
- await setupDevHandler(config, resolver, getDetectedRenderers, getCompatibilityMeta);
6359
+ await setupDevHandler(config, resolver, getDetectedRenderers);
6251
6360
  setupDevToolsUI(config, resolve, nuxt, cssFramework || "none");
6252
6361
  const useNitro = new Promise((resolveNitro) => {
6253
6362
  nuxt.hooks.hook("nitro:init", resolveNitro);
@@ -6281,16 +6390,22 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6281
6390
  } else if (isNuxtGenerate()) {
6282
6391
  setupGenerateHandler(config, resolver);
6283
6392
  } else if (nuxt.options.build) {
6284
- await setupBuildHandler(config, resolver, getDetectedRenderers, getCompatibilityMeta);
6393
+ await setupBuildHandler(config, resolver, getDetectedRenderers);
6285
6394
  }
6286
- setRuntimeAlias("#og-image/app", resolve("./runtime/app"));
6395
+ setupRuntimeAliases({ namespace: "#og-image", app: resolve("./runtime/app"), server: resolve("./runtime/server") }, nuxt);
6287
6396
  setRuntimeAlias("#og-image/shared", resolve("./runtime/shared"));
6288
6397
  setRuntimeAlias("#og-image/types", resolve("./runtime/types"));
6398
+ if (!config.zeroRuntime) {
6399
+ nuxt.options.nitro.virtual["#og-image-virtual/signing.mjs"] = () => `export const rendersImages = ${getDetectedRenderers().size > 0}`;
6400
+ addServerHandler({ middleware: true, handler: resolve("./runtime/server/middleware/signing") });
6401
+ }
6289
6402
  if (nuxt.options.build)
6290
6403
  addServerPlugin(resolve("./runtime/server/plugins/prerender"));
6404
+ if (nuxt.options.build && config.publish && !config.zeroRuntime)
6405
+ addServerPlugin(resolve("./runtime/server/plugins/publish"));
6291
6406
  if (nuxt.options.dev)
6292
- addServerPlugin(resolve("./runtime/server/plugins/auto-eject"));
6293
- setupPrerenderHandler(config, resolver, getDetectedRenderers, getCompatibilityMeta);
6407
+ addServerPlugin(resolve(getNitroVersion(nuxt) === 3 ? "./runtime/server/plugins/auto-eject-nitro3" : "./runtime/server/plugins/auto-eject"));
6408
+ setupPrerenderHandler(config, resolver, getDetectedRenderers);
6294
6409
  if (nuxt.options.dev && hasWarnings()) {
6295
6410
  nuxt.hook("ready", () => {
6296
6411
  emitWarnings();