nuxt-og-image 6.10.2 → 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.
- package/README.md +10 -14
- package/dist/chunks/tw4.mjs +6 -6
- package/dist/chunks/uno.mjs +7 -7
- package/dist/cli.mjs +4 -4
- package/dist/devtools/components/og-image/RendererSelectModal.vue +1 -1
- package/dist/devtools/lib/og-image/fonts.ts +38 -0
- package/dist/devtools/lib/og-image/types.ts +7 -0
- package/dist/devtools/pages/og-image/index.vue +19 -14
- package/dist/module.d.mts +14 -10
- package/dist/module.d.ts +14 -10
- package/dist/module.json +2 -2
- package/dist/module.mjs +6 -6
- package/dist/runtime/app/client-utils.js +7 -4
- package/dist/runtime/app/components/Templates/Community/SaaS.satori.vue +1 -1
- package/dist/runtime/app/components/Templates/Community/SimpleBlog.satori.vue +1 -1
- package/dist/runtime/app/disabled/components.d.ts +1 -0
- package/dist/runtime/app/disabled/components.js +0 -0
- package/dist/runtime/app/disabled/composables/defineOgImage.d.ts +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImage.js +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImageComponent.d.ts +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImageComponent.js +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.d.ts +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.js +1 -0
- package/dist/runtime/app/disabled/composables/mock.d.ts +1 -0
- package/dist/runtime/app/disabled/composables/mock.js +1 -0
- package/dist/runtime/app/disabled/index.d.ts +3 -0
- package/dist/runtime/app/disabled/index.js +3 -0
- package/dist/runtime/app/index.d.ts +4 -0
- package/dist/runtime/app/index.js +4 -0
- package/dist/runtime/app/utils/plugins.js +1 -1
- package/dist/runtime/app/utils.js +2 -2
- package/dist/runtime/server/index.d.ts +1 -0
- package/dist/runtime/server/index.js +1 -0
- package/dist/runtime/server/middleware/signing.d.ts +2 -0
- package/dist/runtime/server/middleware/signing.js +19 -0
- package/dist/runtime/server/nitro.d.ts +3 -3
- package/dist/runtime/server/nitro.js +4 -4
- package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +8 -49
- package/dist/runtime/server/og-image/bindings/font-assets/node.js +3 -2
- package/dist/runtime/server/og-image/bindings/satori/node.d.ts +1 -2
- package/dist/runtime/server/og-image/bindings/satori/wasm-fs.d.ts +1 -2
- package/dist/runtime/server/og-image/bindings/satori/wasm-fs.js +1 -1
- package/dist/runtime/server/og-image/bindings/satori/wasm.d.ts +1 -2
- package/dist/runtime/server/og-image/bindings/satori/wasm.js +1 -1
- package/dist/runtime/server/og-image/browser/screenshot.js +5 -3
- package/dist/runtime/server/og-image/cache/publish.d.ts +43 -0
- package/dist/runtime/server/og-image/cache/publish.js +91 -0
- package/dist/runtime/server/og-image/cache/published.d.ts +17 -0
- package/dist/runtime/server/og-image/cache/published.js +49 -0
- package/dist/runtime/server/og-image/context.js +1 -1
- package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -5
- package/dist/runtime/server/og-image/core/transforms/emojis/index.d.ts +3 -2
- package/dist/runtime/server/og-image/core/vnodes.js +19 -1
- package/dist/runtime/server/og-image/font-source.d.ts +1 -0
- package/dist/runtime/server/og-image/font-source.js +1 -0
- package/dist/runtime/server/og-image/fonts.d.ts +1 -1
- package/dist/runtime/server/og-image/fonts.js +1 -1
- package/dist/runtime/server/og-image/satori/filters.d.ts +1 -0
- package/dist/runtime/server/og-image/satori/filters.js +14 -0
- package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
- package/dist/runtime/server/og-image/satori/plugins/classes.js +0 -7
- package/dist/runtime/server/og-image/satori/renderer.js +2 -1
- package/dist/runtime/server/og-image/satori/vnodes.js +1 -4
- package/dist/runtime/server/og-image/takumi/renderer.js +11 -47
- package/dist/runtime/server/og-image/utils/css.d.ts +0 -5
- package/dist/runtime/server/og-image/utils/css.js +0 -5
- package/dist/runtime/server/plugins/auto-eject-nitro3.d.ts +2 -0
- package/dist/runtime/server/plugins/auto-eject-nitro3.js +9 -0
- package/dist/runtime/server/plugins/auto-eject.js +3 -31
- package/dist/runtime/server/plugins/prerender.js +68 -2
- package/dist/runtime/server/plugins/publish.d.ts +2 -0
- package/dist/runtime/server/plugins/publish.js +44 -0
- package/dist/runtime/server/routes/debug.json.d.ts +10 -1
- package/dist/runtime/server/routes/debug.json.js +4 -4
- package/dist/runtime/server/routes/resolve.js +2 -2
- package/dist/runtime/server/routes/static-font.d.ts +2 -0
- package/dist/runtime/server/routes/static-font.js +22 -0
- package/dist/runtime/server/signing-secret.d.ts +8 -0
- package/dist/runtime/server/signing-secret.js +75 -0
- package/dist/runtime/server/util/auto-eject-lifecycle.d.ts +13 -0
- package/dist/runtime/server/util/auto-eject-lifecycle.js +31 -0
- package/dist/runtime/server/util/cache.d.ts +1 -0
- package/dist/runtime/server/util/cache.js +12 -6
- package/dist/runtime/server/util/cloudflare.d.ts +2 -2
- package/dist/runtime/server/util/eventHandlers.js +69 -3
- package/dist/runtime/server/util/fetchLocalAsset.js +2 -2
- package/dist/runtime/server/util/origin.d.ts +3 -0
- package/dist/runtime/server/util/origin.js +4 -0
- package/dist/runtime/server/util/publishWarnings.d.ts +6 -0
- package/dist/runtime/server/util/publishWarnings.js +6 -0
- package/dist/runtime/server/utils.d.ts +3 -3
- package/dist/runtime/server/utils.js +5 -10
- package/dist/runtime/shared/publish.d.ts +13 -0
- package/dist/runtime/shared/publish.js +17 -0
- package/dist/runtime/shared/urlEncoding.js +9 -4
- package/dist/runtime/types.d.ts +11 -16
- package/dist/shared/{nuxt-og-image.Bcegsloy.mjs → nuxt-og-image.DR_SxdDv.mjs} +1224 -1109
- package/package.json +60 -62
- package/skills/nuxt-og-image/SKILL.md +31 -14
- package/types/virtual.d.ts +8 -0
- package/dist/runtime/server/og-image/satori/plugins/emojis.d.ts +0 -2
- package/dist/runtime/server/og-image/satori/plugins/emojis.js +0 -112
- package/dist/runtime/server/og-image/satori/plugins/flex.d.ts +0 -2
- 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,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, rmSync } from 'node:fs';
|
|
4
3
|
import { readFile, mkdir, writeFile, readdir } from 'node:fs/promises';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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.
|
|
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 = {
|
|
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
|
|
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
|
|
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,
|
|
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"]
|
|
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,
|
|
570
|
+
const [resvgHash, layoutHash, takumiHash] = await Promise.all([
|
|
538
571
|
resolveFilePathSha1("@resvg/resvg-wasm/index_bg.wasm"),
|
|
539
|
-
resolveFilePathSha1("satori/
|
|
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/
|
|
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
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
);
|
|
717
|
-
|
|
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
|
-
|
|
725
|
-
const
|
|
726
|
-
|
|
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
|
|
747
|
-
|
|
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
|
|
750
|
-
const
|
|
751
|
-
return
|
|
755
|
+
async function simplifyCss(css) {
|
|
756
|
+
const result = await transformWithLightningCss(css, { minify: true });
|
|
757
|
+
return result.code.toString();
|
|
752
758
|
}
|
|
753
|
-
function
|
|
754
|
-
|
|
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
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
|
786
|
+
return vars;
|
|
780
787
|
}
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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 (
|
|
803
|
-
|
|
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
|
|
849
|
+
return parts.join("");
|
|
806
850
|
}
|
|
807
|
-
function
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
|
955
|
+
return false;
|
|
836
956
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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
|
-
|
|
1629
|
-
if (
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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
|
-
|
|
1643
|
-
|
|
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
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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 (
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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 (
|
|
1674
|
-
|
|
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
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
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
|
-
|
|
1732
|
-
|
|
1733
|
-
if (
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2102
|
-
if (
|
|
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
|
|
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
|
|
2154
|
+
const templates = expandedFonts.filter(
|
|
2119
2155
|
(f) => f.family === varFont.family && f.style === varFont.style
|
|
2120
2156
|
);
|
|
2121
|
-
for (const template of
|
|
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
|
|
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}"
|
|
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
|
|
2420
|
-
`
|
|
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
|
|
2462
|
-
|
|
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
|
-
|
|
2610
|
+
let decoder;
|
|
2545
2611
|
for (const [fontSrc, font] of fontsBySource) {
|
|
2546
|
-
const
|
|
2547
|
-
|
|
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
|
|
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 {
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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()
|
|
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
|
|
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
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
if (
|
|
2926
|
-
return
|
|
2927
|
-
|
|
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: "
|
|
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
|
-
|
|
5170
|
-
|
|
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
|
|
5208
|
-
|
|
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:
|
|
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
|
-
|
|
5237
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
5753
|
-
registerOgComponentDir(join(layer.
|
|
5754
|
-
if (layer.
|
|
5755
|
-
registerOgComponentDir(join(layer.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
6108
|
-
|
|
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:
|
|
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
|
|
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
|
|
6393
|
+
await setupBuildHandler(config, resolver, getDetectedRenderers);
|
|
6285
6394
|
}
|
|
6286
|
-
|
|
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
|
|
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();
|