nuxt-og-image 7.0.0 → 7.0.2
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/dist/chunks/tw4.mjs +48 -41
- package/dist/chunks/uno.mjs +2 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/server/og-image/cache/publish.d.ts +4 -1
- package/dist/runtime/server/og-image/cache/publish.js +2 -2
- package/dist/runtime/server/og-image/context.d.ts +0 -1
- package/dist/runtime/server/og-image/context.js +10 -25
- package/dist/runtime/server/og-image/core/plugins/imageSrc.js +6 -3
- package/dist/runtime/server/og-image/core/transforms/emojis/fetch.js +4 -4
- package/dist/runtime/server/og-image/devtools.d.ts +1 -1
- package/dist/runtime/server/og-image/devtools.js +3 -1
- package/dist/runtime/server/plugins/prerender.js +4 -12
- package/dist/runtime/server/routes/resolve.js +6 -4
- package/dist/runtime/server/util/cache.d.ts +3 -3
- package/dist/runtime/server/util/cache.js +4 -19
- package/dist/runtime/server/util/eventHandlers.js +10 -5
- package/dist/runtime/server/util/purge.d.ts +14 -0
- package/dist/runtime/server/util/purge.js +15 -0
- package/dist/runtime/server/util/ssrf.d.ts +12 -5
- package/dist/runtime/server/util/ssrf.js +26 -9
- package/dist/runtime/server/util/storageKey.d.ts +14 -0
- package/dist/runtime/server/util/storageKey.js +4 -0
- package/dist/runtime/shared/urlEncoding.d.ts +5 -0
- package/dist/runtime/shared/urlEncoding.js +4 -1
- package/dist/shared/{nuxt-og-image.DR_SxdDv.mjs → nuxt-og-image.CSPRgfu6.mjs} +22 -17
- package/package.json +1 -1
package/dist/chunks/tw4.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
import { resolveModulePath } from 'exsolve';
|
|
4
4
|
import { dirname, join } from 'pathe';
|
|
5
|
-
import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.
|
|
5
|
+
import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.CSPRgfu6.mjs';
|
|
6
6
|
import 'node:fs';
|
|
7
7
|
import 'node:module';
|
|
8
8
|
import '@nuxt/kit';
|
|
@@ -225,7 +225,7 @@ async function parseCssOutput(rawCss, vars, extractedAccum) {
|
|
|
225
225
|
if (!extractedAccum.rootVars.has(n))
|
|
226
226
|
extractedAccum.rootVars.set(n, v);
|
|
227
227
|
}
|
|
228
|
-
extractedAccum.qualifiedRules
|
|
228
|
+
extractedAccum.qualifiedRules = fresh.qualifiedRules;
|
|
229
229
|
for (const [n, v] of fresh.universalVars) {
|
|
230
230
|
if (!extractedAccum.universalVars.has(n))
|
|
231
231
|
extractedAccum.universalVars.set(n, v);
|
|
@@ -254,7 +254,16 @@ const LINEAR_GRADIENT_DIRECTIONS = {
|
|
|
254
254
|
"bg-gradient-to-b": "to bottom",
|
|
255
255
|
"bg-gradient-to-bl": "to bottom left",
|
|
256
256
|
"bg-gradient-to-l": "to left",
|
|
257
|
-
"bg-gradient-to-tl": "to top left"
|
|
257
|
+
"bg-gradient-to-tl": "to top left",
|
|
258
|
+
// Tailwind v4 names
|
|
259
|
+
"bg-linear-to-t": "to top",
|
|
260
|
+
"bg-linear-to-tr": "to top right",
|
|
261
|
+
"bg-linear-to-r": "to right",
|
|
262
|
+
"bg-linear-to-br": "to bottom right",
|
|
263
|
+
"bg-linear-to-b": "to bottom",
|
|
264
|
+
"bg-linear-to-bl": "to bottom left",
|
|
265
|
+
"bg-linear-to-l": "to left",
|
|
266
|
+
"bg-linear-to-tl": "to top left"
|
|
258
267
|
};
|
|
259
268
|
const RADIAL_GRADIENT_SHAPES = {
|
|
260
269
|
"bg-radial": "circle at center",
|
|
@@ -268,32 +277,31 @@ const RADIAL_GRADIENT_SHAPES = {
|
|
|
268
277
|
"bg-radial-at-tl": "circle at top left",
|
|
269
278
|
"bg-radial-at-c": "circle at center"
|
|
270
279
|
};
|
|
271
|
-
async function
|
|
280
|
+
async function toGradientColor(value, vars) {
|
|
281
|
+
const resolved = await resolveVarsDeep(value, vars);
|
|
282
|
+
if (resolved.includes("var("))
|
|
283
|
+
return null;
|
|
284
|
+
const simplified = await simplifyCss(`.x{color:${resolved}}`);
|
|
285
|
+
return simplified.match(RE_CSS_COLOR_VALUE)?.[1]?.trim() ?? resolved;
|
|
286
|
+
}
|
|
287
|
+
async function resolveGradientColor(cls, vars, classVars) {
|
|
272
288
|
const colorName = cls.replace(RE_GRADIENT_STOP_PREFIX, "");
|
|
273
289
|
const shadeMatch = colorName.match(RE_COLOR_SHADE);
|
|
274
290
|
if (shadeMatch?.[1] && shadeMatch?.[2]) {
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
if (!resolved.includes("var(")) {
|
|
280
|
-
const simplified = await simplifyCss(`.x{color:${resolved}}`);
|
|
281
|
-
return simplified.match(RE_CSS_COLOR_VALUE)?.[1]?.trim() ?? resolved;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
291
|
+
const value2 = vars.get(`--color-${shadeMatch[1]}-${shadeMatch[2]}`);
|
|
292
|
+
const color2 = value2 ? await toGradientColor(value2, vars) : null;
|
|
293
|
+
if (color2)
|
|
294
|
+
return color2;
|
|
284
295
|
}
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return null;
|
|
296
|
+
const value = vars.get(`--color-${colorName}`);
|
|
297
|
+
const color = value ? await toGradientColor(value, vars) : null;
|
|
298
|
+
if (color)
|
|
299
|
+
return color;
|
|
300
|
+
const stop = cls.match(RE_GRADIENT_STOP_PREFIX)?.[1];
|
|
301
|
+
const own = stop ? classVars?.get(`--tw-gradient-${stop}`) : void 0;
|
|
302
|
+
return own ? toGradientColor(own, vars) : null;
|
|
295
303
|
}
|
|
296
|
-
async function buildGradient(classes, vars) {
|
|
304
|
+
async function buildGradient(classes, vars, perClassVars) {
|
|
297
305
|
const linearDir = classes.find((c) => LINEAR_GRADIENT_DIRECTIONS[c]);
|
|
298
306
|
const radialShape = classes.find((c) => RADIAL_GRADIENT_SHAPES[c]);
|
|
299
307
|
const fromClass = classes.find((c) => c.startsWith("from-"));
|
|
@@ -303,9 +311,9 @@ async function buildGradient(classes, vars) {
|
|
|
303
311
|
return null;
|
|
304
312
|
if (!fromClass && !toClass)
|
|
305
313
|
return null;
|
|
306
|
-
const fromColor = fromClass ? await resolveGradientColor(fromClass, vars) : null;
|
|
307
|
-
const viaColor = viaClass ? await resolveGradientColor(viaClass, vars) : null;
|
|
308
|
-
const toColor = toClass ? await resolveGradientColor(toClass, vars) : null;
|
|
314
|
+
const fromColor = fromClass ? await resolveGradientColor(fromClass, vars, perClassVars?.get(fromClass)) : null;
|
|
315
|
+
const viaColor = viaClass ? await resolveGradientColor(viaClass, vars, perClassVars?.get(viaClass)) : null;
|
|
316
|
+
const toColor = toClass ? await resolveGradientColor(toClass, vars, perClassVars?.get(toClass)) : null;
|
|
309
317
|
if (!fromColor && !toColor)
|
|
310
318
|
return null;
|
|
311
319
|
const stops = [fromColor, viaColor, toColor].filter(Boolean).join(", ");
|
|
@@ -348,21 +356,20 @@ async function resolveClassesToStyles(classes, options, context, theme) {
|
|
|
348
356
|
const outputCss = compiler.build(uncached);
|
|
349
357
|
const { classes: parsedClasses, perClassVars } = await parseCssOutput(outputCss, vars, cachedExtractedVars || void 0);
|
|
350
358
|
const resolveVarsForClass = theme ? buildThemeVars(vars, cachedExtractedVars, theme, options.nuxtUiColors) : vars;
|
|
351
|
-
for (const
|
|
352
|
-
const classVars = perClassVars.get(className);
|
|
353
|
-
const mergedVars = classVars ? new Map([...resolveVarsForClass, ...classVars]) : resolveVarsForClass;
|
|
354
|
-
const styles = await postProcessStyles(rawStyles, mergedVars, void 0, context);
|
|
359
|
+
for (const className of uncached) {
|
|
355
360
|
const cacheKey = cacheKeyFor(className);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
361
|
+
const rawStyles = parsedClasses.get(className);
|
|
362
|
+
if (!rawStyles) {
|
|
363
|
+
resolvedStyleCache.set(cacheKey, null);
|
|
364
|
+
continue;
|
|
360
365
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
+
if (LINEAR_GRADIENT_DIRECTIONS[className] || RADIAL_GRADIENT_SHAPES[className]) {
|
|
367
|
+
resolvedStyleCache.set(cacheKey, {});
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const classVars = perClassVars.get(className);
|
|
371
|
+
const mergedVars = classVars ? new Map([...resolveVarsForClass, ...classVars]) : resolveVarsForClass;
|
|
372
|
+
resolvedStyleCache.set(cacheKey, await postProcessStyles(rawStyles, mergedVars, void 0, context));
|
|
366
373
|
}
|
|
367
374
|
}
|
|
368
375
|
const result = {};
|
|
@@ -372,7 +379,7 @@ async function resolveClassesToStyles(classes, options, context, theme) {
|
|
|
372
379
|
result[cls] = resolved;
|
|
373
380
|
}
|
|
374
381
|
const gradientVars = theme ? buildThemeVars(vars, cachedExtractedVars, theme, options.nuxtUiColors) : vars;
|
|
375
|
-
const gradient = await buildGradient(classes, gradientVars);
|
|
382
|
+
const gradient = await buildGradient(classes, gradientVars, cachedExtractedVars?.perClassVars);
|
|
376
383
|
if (gradient) {
|
|
377
384
|
result[gradient.gradientClass] = { "background-image": gradient.value };
|
|
378
385
|
for (const colorClass of gradient.colorClasses) {
|
package/dist/chunks/uno.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
3
|
import { logger } from '../../dist/runtime/logger.js';
|
|
4
|
-
import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.
|
|
5
|
-
export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.
|
|
4
|
+
import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.CSPRgfu6.mjs';
|
|
5
|
+
export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.CSPRgfu6.mjs';
|
|
6
6
|
import 'node:fs';
|
|
7
7
|
import 'node:module';
|
|
8
8
|
import 'node:url';
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import 'defu';
|
|
|
7
7
|
import 'fnv1a-64';
|
|
8
8
|
import 'nuxtseo-shared/kit';
|
|
9
9
|
import 'pathe';
|
|
10
|
-
export { n as default } from './shared/nuxt-og-image.
|
|
10
|
+
export { n as default } from './shared/nuxt-og-image.CSPRgfu6.mjs';
|
|
11
11
|
import '../dist/runtime/logger.js';
|
|
12
12
|
import '../dist/runtime/shared/publish.js';
|
|
13
13
|
import 'node:crypto';
|
|
@@ -26,7 +26,10 @@ interface Unavailable {
|
|
|
26
26
|
_tag: 'Unavailable';
|
|
27
27
|
reason: unknown;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Normalize signed image paths without loading a renderer during page SSR.
|
|
31
|
+
* The key is a full SHA-256 hex digest, so it has one fixed format.
|
|
32
|
+
*/
|
|
30
33
|
export declare function getPublishKey(url: string, defaults: Record<string, unknown>, cacheVersion: string): string;
|
|
31
34
|
/** A page may reference the bucket only while its manifest and object both exist. */
|
|
32
35
|
export declare function getPublishedImage(input: PublishInput): Promise<Published | Unavailable | {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { parseURL } from "ufo";
|
|
2
2
|
import { parsePublishConfig } from "../../../shared/publish.js";
|
|
3
|
-
import {
|
|
3
|
+
import { digestCanonical, extractEncodedSegment } from "../../../shared/urlEncoding.js";
|
|
4
4
|
import { withTimeout } from "../../util/withTimeout.js";
|
|
5
5
|
export function getPublishKey(url, defaults, cacheVersion) {
|
|
6
6
|
const { pathname: path, host, protocol } = parseURL(url);
|
|
7
7
|
const extension = path.split(".").pop();
|
|
8
8
|
const segment = decodeURIComponent(extractEncodedSegment(path, extension)).replace(/,s_[^,]+$/, "");
|
|
9
|
-
return
|
|
9
|
+
return digestCanonical([{ segment, defaults, host, protocol }, cacheVersion]);
|
|
10
10
|
}
|
|
11
11
|
export async function getPublishedImage(input) {
|
|
12
12
|
return withTimeout((async () => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { H3Error, H3Event } from '#nuxtseo/h3';
|
|
2
2
|
import type { OgImageRenderEventContext } from '../../types.js';
|
|
3
|
-
export declare function resolvePathCacheKey(e: H3Event, path: string, resolvedOptions?: Record<string, any>): string;
|
|
4
3
|
export declare function resolveContext(e: H3Event): Promise<H3Error | OgImageRenderEventContext>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defu } from "defu";
|
|
2
|
-
import { parseURL,
|
|
3
|
-
import { normalizeKey } from "unstorage";
|
|
2
|
+
import { parseURL, withoutTrailingSlash, withQuery } from "ufo";
|
|
4
3
|
import { createError } from "#nuxtseo/h3";
|
|
5
4
|
import { useNitroApp } from "#nuxtseo/nitro";
|
|
6
5
|
import { prerenderOptionsCache } from "#og-image-cache";
|
|
@@ -8,33 +7,18 @@ import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
|
8
7
|
import { createSitePathResolver } from "#site-config/server/composables/utils";
|
|
9
8
|
import { logger } from "../../logger.js";
|
|
10
9
|
import { decodeOgImageParams, extractEncodedSegment, sanitizeProps, separateProps, verifyOgImageSignature } from "../../shared.js";
|
|
11
|
-
import {
|
|
10
|
+
import { digestCanonical } from "../../shared/urlEncoding.js";
|
|
12
11
|
import { createNitroRouteRuleMatcher } from "../util/kit.js";
|
|
13
12
|
import { normaliseOptions } from "../util/options.js";
|
|
14
13
|
import { getEventQuery } from "../util/query.js";
|
|
15
14
|
import { parseSameOriginPath } from "../util/ssrf.js";
|
|
15
|
+
import { toStorageKey } from "../util/storageKey.js";
|
|
16
16
|
import { createTimings, TIMING_CTX_KEY } from "../util/timings.js";
|
|
17
17
|
import { withTimeout } from "../util/withTimeout.js";
|
|
18
18
|
import { useOgImageRuntimeConfig } from "../utils.js";
|
|
19
19
|
import { getBrowserRenderer, getSatoriRenderer, getTakumiRenderer } from "./instances.js";
|
|
20
20
|
const RE_HASH_MODE = /^o_([a-z0-9]+)$/i;
|
|
21
21
|
const RE_SIGNATURE_SUFFIX = /,s_([^,]+)$/;
|
|
22
|
-
export function resolvePathCacheKey(e, path, resolvedOptions) {
|
|
23
|
-
const siteConfig = getSiteConfig(e, {
|
|
24
|
-
resolveRefs: true
|
|
25
|
-
});
|
|
26
|
-
const basePath = withoutTrailingSlash(withoutLeadingSlash(normalizeKey(path)));
|
|
27
|
-
const hashParts = [
|
|
28
|
-
basePath,
|
|
29
|
-
import.meta.prerender ? "" : siteConfig.url
|
|
30
|
-
];
|
|
31
|
-
if (resolvedOptions)
|
|
32
|
-
hashParts.push(hashKey(resolvedOptions));
|
|
33
|
-
return [
|
|
34
|
-
!basePath || basePath === "/" ? "index" : basePath,
|
|
35
|
-
hashKey(hashParts)
|
|
36
|
-
].join(":");
|
|
37
|
-
}
|
|
38
22
|
export async function resolveContext(e) {
|
|
39
23
|
const runtimeConfig = useOgImageRuntimeConfig(e);
|
|
40
24
|
const resolvePathWithBase = createSitePathResolver(e, {
|
|
@@ -73,7 +57,7 @@ export async function resolveContext(e) {
|
|
|
73
57
|
if (hashMatch) {
|
|
74
58
|
const optionsHash = hashMatch[1];
|
|
75
59
|
if (import.meta.prerender && prerenderOptionsCache) {
|
|
76
|
-
const cached = await prerenderOptionsCache.getItem(
|
|
60
|
+
const cached = await prerenderOptionsCache.getItem(toStorageKey("prerender", optionsHash));
|
|
77
61
|
if (cached && typeof cached === "object") {
|
|
78
62
|
urlOptions = cached;
|
|
79
63
|
} else {
|
|
@@ -121,13 +105,14 @@ export async function resolveContext(e) {
|
|
|
121
105
|
queryParams = separateProps(queryParams);
|
|
122
106
|
}
|
|
123
107
|
const rawPath = urlOptions._path ?? "/";
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
108
|
+
const parsedPath = parseSameOriginPath(typeof rawPath === "string" ? withoutTrailingSlash(rawPath) : rawPath);
|
|
109
|
+
if (parsedPath._tag === "Err") {
|
|
126
110
|
return createError({
|
|
127
111
|
statusCode: 400,
|
|
128
|
-
statusMessage:
|
|
112
|
+
statusMessage: `[Nuxt OG Image] The _path option must be a same-origin path. ${parsedPath.reason}`
|
|
129
113
|
});
|
|
130
114
|
}
|
|
115
|
+
const basePath = parsedPath.value;
|
|
131
116
|
const componentHash = urlOptions._componentHash || "";
|
|
132
117
|
delete urlOptions._path;
|
|
133
118
|
delete urlOptions._hash;
|
|
@@ -174,8 +159,8 @@ export async function resolveContext(e) {
|
|
|
174
159
|
normalised.options.props = filtered;
|
|
175
160
|
}
|
|
176
161
|
const rendererType = normalised.renderer;
|
|
177
|
-
const
|
|
178
|
-
const key =
|
|
162
|
+
const identity = normalised.options.cacheKey ? ["cacheKey", String(normalised.options.cacheKey)] : hashMatch ? ["hash", hashMatch[1]] : ["page", basePathWithQuery, import.meta.prerender ? "" : getSiteConfig(e, { resolveRefs: true }).url, normalised.options];
|
|
163
|
+
const key = digestCanonical([...identity, componentHash]);
|
|
179
164
|
let renderer;
|
|
180
165
|
switch (rendererType) {
|
|
181
166
|
case "satori":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { withBase, withoutLeadingSlash } from "ufo";
|
|
1
|
+
import { parseURL, withBase, withoutLeadingSlash } from "ufo";
|
|
2
2
|
import { useStorage } from "#nuxtseo/nitro";
|
|
3
3
|
import { toBase64Image } from "../../../../shared.js";
|
|
4
4
|
import { decodeHtml } from "../../../util/encoding.js";
|
|
@@ -6,15 +6,18 @@ import { fetchLocalAsset } from "../../../util/fetchLocalAsset.js";
|
|
|
6
6
|
import { getFetchTimeout } from "../../../util/fetchTimeout.js";
|
|
7
7
|
import { logger } from "../../../util/logger.js";
|
|
8
8
|
import { getOgImageOrigin } from "../../../util/origin.js";
|
|
9
|
-
import { fetchWithRedirectValidation, isBlockedUrl } from "../../../util/ssrf.js";
|
|
9
|
+
import { fetchWithRedirectValidation, isBlockedUrl, parseSameOriginPath } from "../../../util/ssrf.js";
|
|
10
10
|
import { getImageDimensions } from "../../utils/image-detector.js";
|
|
11
11
|
import { defineTransformer } from "../plugins.js";
|
|
12
12
|
const RE_URL_LEADING = /^url\(['"]?/;
|
|
13
13
|
const RE_URL_TRAILING = /['"]?\)$/;
|
|
14
14
|
const SUBREQUEST_HEADERS = { "x-nuxt-og-image": "1" };
|
|
15
15
|
async function resolveLocalFilePathImage(publicStoragePath, src) {
|
|
16
|
+
const path = parseSameOriginPath(parseURL(src).pathname);
|
|
17
|
+
if (path._tag === "Err")
|
|
18
|
+
return;
|
|
16
19
|
const normalizedSrc = withoutLeadingSlash(
|
|
17
|
-
|
|
20
|
+
decodeURIComponent(path.value).replace("_nuxt/@fs/", "").replace("_nuxt/", "").replace("./", "")
|
|
18
21
|
).replaceAll("/", ":");
|
|
19
22
|
const key = `${publicStoragePath}:${normalizedSrc}`;
|
|
20
23
|
if (await useStorage().hasItem(key))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { $fetch } from "ofetch";
|
|
2
2
|
import { emojiCache } from "#og-image-cache";
|
|
3
3
|
import { getFetchTimeout } from "../../../../util/fetchTimeout.js";
|
|
4
|
+
import { toStorageKey } from "../../../../util/storageKey.js";
|
|
4
5
|
import { getEmojiCodePoint, getEmojiIconNames } from "./emoji-utils.js";
|
|
5
6
|
export async function getEmojiSvg(ctx, emojiChar) {
|
|
6
7
|
const emojiSet = ctx.options.emojis;
|
|
@@ -8,7 +9,7 @@ export async function getEmojiSvg(ctx, emojiChar) {
|
|
|
8
9
|
const possibleNames = getEmojiIconNames(codePoint, emojiSet);
|
|
9
10
|
let svg = null;
|
|
10
11
|
for (const iconName of possibleNames) {
|
|
11
|
-
const key =
|
|
12
|
+
const key = toStorageKey("emoji", [emojiSet, iconName]);
|
|
12
13
|
if (await emojiCache.hasItem(key)) {
|
|
13
14
|
svg = await emojiCache.getItem(key);
|
|
14
15
|
if (svg)
|
|
@@ -24,15 +25,14 @@ export async function getEmojiSvg(ctx, emojiChar) {
|
|
|
24
25
|
if (deadline.aborted)
|
|
25
26
|
break;
|
|
26
27
|
try {
|
|
27
|
-
svg = await $fetch(`https://api.iconify.design/${emojiSet}/${iconName}.svg`, {
|
|
28
|
+
svg = await $fetch(`https://api.iconify.design/${encodeURIComponent(emojiSet)}/${encodeURIComponent(iconName)}.svg`, {
|
|
28
29
|
responseType: "text",
|
|
29
30
|
retry: 0,
|
|
30
31
|
signal: deadline,
|
|
31
32
|
timeout
|
|
32
33
|
});
|
|
33
34
|
if (svg && svg !== "404") {
|
|
34
|
-
|
|
35
|
-
await emojiCache.setItem(key, svg);
|
|
35
|
+
await emojiCache.setItem(toStorageKey("emoji", [emojiSet, iconName]), svg);
|
|
36
36
|
break;
|
|
37
37
|
}
|
|
38
38
|
} catch {
|
|
@@ -7,4 +7,4 @@ export interface DevToolsExtractPayload {
|
|
|
7
7
|
images: DevToolsMetaDataExtraction[];
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
-
export declare function fetchPathHtmlAndExtractOptions(e: H3Event, path: string,
|
|
10
|
+
export declare function fetchPathHtmlAndExtractOptions(e: H3Event, path: string, key: string): Promise<H3Error | DevToolsExtractPayload>;
|
|
@@ -3,6 +3,7 @@ import { createError } from "#nuxtseo/h3";
|
|
|
3
3
|
import { htmlPayloadCache } from "#og-image-cache";
|
|
4
4
|
import { extractSocialPreviewTags } from "../../pure.js";
|
|
5
5
|
import { logger } from "../util/logger.js";
|
|
6
|
+
import { toStorageKey } from "../util/storageKey.js";
|
|
6
7
|
const PAYLOAD_REGEX = /<script.+id="nuxt-og-image-options"[^>]*>(.+?)<\/script>/;
|
|
7
8
|
function extractOptionsFromHtml(html) {
|
|
8
9
|
const match = String(html).match(PAYLOAD_REGEX);
|
|
@@ -36,7 +37,8 @@ async function doFetchWithErrorHandling(fetch, path) {
|
|
|
36
37
|
}
|
|
37
38
|
return [res._data || await res.text(), null];
|
|
38
39
|
}
|
|
39
|
-
export async function fetchPathHtmlAndExtractOptions(e, path,
|
|
40
|
+
export async function fetchPathHtmlAndExtractOptions(e, path, key) {
|
|
41
|
+
const cacheKey = toStorageKey("devtools", key);
|
|
40
42
|
const cachedHtmlPayload = await htmlPayloadCache.getItem(cacheKey);
|
|
41
43
|
if (!import.meta.dev && cachedHtmlPayload && cachedHtmlPayload.expiresAt > Date.now())
|
|
42
44
|
return cachedHtmlPayload.value;
|
|
@@ -4,12 +4,11 @@ import { appendResponseHeader } from "#nuxtseo/h3";
|
|
|
4
4
|
import { defineNitroPlugin, useRuntimeConfig, useStorage } from "#nuxtseo/nitro";
|
|
5
5
|
import { prerenderOptionsCache } from "#og-image-cache";
|
|
6
6
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
7
|
-
import { createSitePathResolver } from "#site-config/server/composables/utils";
|
|
8
7
|
import { logger } from "../../logger.js";
|
|
9
8
|
import { isInternalRoute } from "../../shared.js";
|
|
10
9
|
import { getPublishKey, publishImage } from "../og-image/cache/publish.js";
|
|
11
|
-
import { resolvePathCacheKey } from "../og-image/context.js";
|
|
12
10
|
import { createNitroRouteRuleMatcher } from "../util/kit.js";
|
|
11
|
+
import { toStorageKey } from "../util/storageKey.js";
|
|
13
12
|
import { useOgImageRuntimeConfig } from "../utils.js";
|
|
14
13
|
const PAYLOAD_REGEX = /<script.+id="nuxt-og-image-options"[^>]*>(.+?)<\/script>/;
|
|
15
14
|
const RE_SCRIPT_OPTIONS = /<script id="nuxt-og-image-options" type="application\/json">[\s\S]*?<\/script>/;
|
|
@@ -46,16 +45,10 @@ export default defineNitroPlugin(async (nitro) => {
|
|
|
46
45
|
return;
|
|
47
46
|
const parsed = parse(_payload);
|
|
48
47
|
const payloads = parsed.map((opt) => [opt.key || "og", opt]);
|
|
49
|
-
const resolvePathWithBase = createSitePathResolver(ctx.event, {
|
|
50
|
-
absolute: false,
|
|
51
|
-
withBase: true
|
|
52
|
-
});
|
|
53
|
-
const key = resolvePathCacheKey(ctx.event, resolvePathWithBase(path));
|
|
54
|
-
await prerenderOptionsCache.setItem(key, payloads);
|
|
55
48
|
const pagePath = withoutBase(path, useRuntimeConfig().app.baseURL);
|
|
56
49
|
for (const [_ogKey, opt] of payloads) {
|
|
57
50
|
if (opt._hash) {
|
|
58
|
-
await prerenderOptionsCache.setItem(
|
|
51
|
+
await prerenderOptionsCache.setItem(toStorageKey("prerender", opt._hash), { ...opt, _path: pagePath });
|
|
59
52
|
}
|
|
60
53
|
}
|
|
61
54
|
const prerenderPaths = ctx.event.context._ogImagePrerenderPaths;
|
|
@@ -69,7 +62,7 @@ export default defineNitroPlugin(async (nitro) => {
|
|
|
69
62
|
const extension = prerenderPath.split(".").pop();
|
|
70
63
|
if (!prerenderPath.includes("/_og/s/") || extension !== "png" && extension !== "jpeg" && extension !== "jpg" && extension !== "webp")
|
|
71
64
|
continue;
|
|
72
|
-
const opt = payloads.find(([
|
|
65
|
+
const opt = payloads.find(([key]) => key === ogKey)?.[1];
|
|
73
66
|
if (!opt)
|
|
74
67
|
continue;
|
|
75
68
|
const result = await publishImage({
|
|
@@ -84,9 +77,8 @@ export default defineNitroPlugin(async (nitro) => {
|
|
|
84
77
|
render: async () => {
|
|
85
78
|
if (opt.component === "PageScreenshot") {
|
|
86
79
|
opt._prerenderHtml = `<!DOCTYPE html><html ${(html.htmlAttrs || []).join(" ")}><head>${html.head.join("")}</head><body ${(html.bodyAttrs || []).join(" ")}>${[...html.bodyPrepend || [], ...html.body || [], ...html.bodyAppend].join("")}</body></html>`;
|
|
87
|
-
await prerenderOptionsCache.setItem(key, payloads);
|
|
88
80
|
if (opt._hash)
|
|
89
|
-
await prerenderOptionsCache.setItem(
|
|
81
|
+
await prerenderOptionsCache.setItem(toStorageKey("prerender", opt._hash), { ...opt, _path: pagePath });
|
|
90
82
|
}
|
|
91
83
|
const response = await nitro.localFetch(opt._query ? withQuery(prerenderPath, { _query: opt._query }) : prerenderPath);
|
|
92
84
|
if (!response.ok || !response.headers.get("content-type")?.startsWith("image/"))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseURL,
|
|
1
|
+
import { parseURL, withQuery } from "ufo";
|
|
2
2
|
import { createError, defineEventHandler, getRequestHost, sendRedirect } from "#nuxtseo/h3";
|
|
3
3
|
import { fetchWithEvent } from "#nuxtseo/nitro";
|
|
4
4
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
@@ -78,13 +78,15 @@ export default defineEventHandler(async (event) => {
|
|
|
78
78
|
statusMessage: `[Nuxt OG Image] Target path exceeds ${MAX_RESOLVE_PATH_LENGTH} characters.`
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
const parsedPath = parseSameOriginPath(targetPath);
|
|
82
|
+
if (targetPath.includes("://") || parsedPath._tag === "Err") {
|
|
82
83
|
throw createError({
|
|
83
84
|
statusCode: 400,
|
|
84
85
|
statusMessage: "[Nuxt OG Image] Target path must be a same-origin path."
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
+
const pagePath = parsedPath.value;
|
|
89
|
+
if (isInternalRoute(pagePath)) {
|
|
88
90
|
throw createError({
|
|
89
91
|
statusCode: 400,
|
|
90
92
|
statusMessage: "[Nuxt OG Image] Cannot resolve og:image for internal route."
|
|
@@ -98,7 +100,7 @@ export default defineEventHandler(async (event) => {
|
|
|
98
100
|
if (!k.startsWith("_og_"))
|
|
99
101
|
forwardQuery[k] = v;
|
|
100
102
|
}
|
|
101
|
-
const fetchPath = withQuery(
|
|
103
|
+
const fetchPath = withQuery(pagePath, forwardQuery);
|
|
102
104
|
const html = await fetchWithEvent(event, fetchPath, {
|
|
103
105
|
headers: { accept: "text/html" },
|
|
104
106
|
responseType: "text"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { H3Error } from '#nuxtseo/h3';
|
|
2
1
|
import type { OgImageRenderEventContext } from '../../types.js';
|
|
3
2
|
export declare function useOgImageBufferCache(ctx: OgImageRenderEventContext, options: {
|
|
4
3
|
baseCacheKey: string | false;
|
|
5
4
|
cacheMaxAgeSeconds?: number;
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/** The caller has checked the purge token. Drop the stored entry and render again. */
|
|
6
|
+
purge?: boolean;
|
|
7
|
+
}): Promise<void | {
|
|
8
8
|
cachedItem: false | BufferSource;
|
|
9
9
|
enabled: boolean;
|
|
10
10
|
expiresAt: number;
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { fnv1a64Base36 } from "fnv1a-64";
|
|
2
2
|
import { withTrailingSlash } from "ufo";
|
|
3
3
|
import { prefixStorage } from "unstorage";
|
|
4
|
-
import {
|
|
4
|
+
import { handleCacheHeaders, setHeader, setHeaders } from "#nuxtseo/h3";
|
|
5
5
|
import { useStorage } from "#nuxtseo/nitro";
|
|
6
6
|
import { logger } from "../../logger.js";
|
|
7
|
-
import {
|
|
8
|
-
function safeCompare(a, b) {
|
|
9
|
-
if (a.length !== b.length)
|
|
10
|
-
return false;
|
|
11
|
-
let mismatch = 0;
|
|
12
|
-
for (let i = 0; i < a.length; i++)
|
|
13
|
-
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
14
|
-
return mismatch === 0;
|
|
15
|
-
}
|
|
7
|
+
import { toStorageKey } from "./storageKey.js";
|
|
16
8
|
function warnCacheBackendUnreachable(ctx, baseCacheKey, e) {
|
|
17
9
|
if (ctx._nitro._ogImageCacheBackendWarned)
|
|
18
10
|
return;
|
|
@@ -24,7 +16,7 @@ export async function useOgImageBufferCache(ctx, options) {
|
|
|
24
16
|
const intentionallyEnabled = options.baseCacheKey !== false && !import.meta.dev && !import.meta.prerender && maxAge > 0;
|
|
25
17
|
let enabled = intentionallyEnabled;
|
|
26
18
|
const cache = options.baseCacheKey === false ? void 0 : prefixStorage(useStorage(), withTrailingSlash(options.baseCacheKey));
|
|
27
|
-
const key = ctx.key;
|
|
19
|
+
const key = toStorageKey("image", ctx.key);
|
|
28
20
|
let cachedItem = false;
|
|
29
21
|
let cacheExpiresAt = 0;
|
|
30
22
|
if (enabled && cache) {
|
|
@@ -40,14 +32,7 @@ export async function useOgImageBufferCache(ctx, options) {
|
|
|
40
32
|
}) : null;
|
|
41
33
|
if (entry) {
|
|
42
34
|
const { value, expiresAt, headers } = entry;
|
|
43
|
-
|
|
44
|
-
if (typeof purgeValue !== "undefined") {
|
|
45
|
-
if (options.secret && !safeCompare(String(purgeValue), options.secret)) {
|
|
46
|
-
return createError({
|
|
47
|
-
statusCode: 403,
|
|
48
|
-
statusMessage: "[Nuxt OG Image] Invalid purge token. Provide the signing secret as ?purge=<secret>."
|
|
49
|
-
});
|
|
50
|
-
}
|
|
35
|
+
if (options.purge) {
|
|
51
36
|
await cache.removeItem(key).catch((err) => {
|
|
52
37
|
void err;
|
|
53
38
|
});
|
|
@@ -11,6 +11,7 @@ import { html } from "../og-image/templates/html.js";
|
|
|
11
11
|
import { useOgImageRuntimeConfig } from "../utils.js";
|
|
12
12
|
import { useOgImageBufferCache } from "./cache.js";
|
|
13
13
|
import { warnPublishUnavailable } from "./publishWarnings.js";
|
|
14
|
+
import { parsePurgeRequest } from "./purge.js";
|
|
14
15
|
import { getEventQuery } from "./query.js";
|
|
15
16
|
export async function imageEventHandler(e) {
|
|
16
17
|
const reqStart = performance.now();
|
|
@@ -64,6 +65,14 @@ async function renderOgImage(e, ctx) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
}
|
|
68
|
+
const purgeRequest = import.meta.dev || import.meta.prerender ? { _tag: "None" } : parsePurgeRequest(getEventQuery(e).purge, security?.secret);
|
|
69
|
+
if (purgeRequest._tag === "Denied") {
|
|
70
|
+
return createError({
|
|
71
|
+
statusCode: 403,
|
|
72
|
+
statusMessage: "[Nuxt OG Image] Invalid purge token. Provide the signing secret as ?purge=<secret>."
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const purge = purgeRequest._tag === "Authorized";
|
|
67
76
|
if ((import.meta.dev || debug) && isDevToolsContextRequest) {
|
|
68
77
|
setHeader(e, "Content-Type", "application/json");
|
|
69
78
|
const [extract, rendererDebug] = await Promise.all([
|
|
@@ -122,7 +131,6 @@ async function renderOgImage(e, ctx) {
|
|
|
122
131
|
timings.record("cache-hit", 0);
|
|
123
132
|
return buildCachedImage;
|
|
124
133
|
}
|
|
125
|
-
const purge = typeof getEventQuery(e).purge !== "undefined";
|
|
126
134
|
let publication = publish && !import.meta.dev && !import.meta.prerender ? (() => {
|
|
127
135
|
const key = getPublishKey(new URL(e.path, getSiteConfig(e).url).href, config.defaults, publish.cacheVersion);
|
|
128
136
|
return {
|
|
@@ -158,14 +166,11 @@ async function renderOgImage(e, ctx) {
|
|
|
158
166
|
const cacheApi = await useOgImageBufferCache(ctx, {
|
|
159
167
|
cacheMaxAgeSeconds: ctx.options.cacheMaxAgeSeconds,
|
|
160
168
|
baseCacheKey,
|
|
161
|
-
|
|
169
|
+
purge
|
|
162
170
|
}).finally(endCacheLookup);
|
|
163
171
|
if (typeof cacheApi === "undefined") {
|
|
164
172
|
return;
|
|
165
173
|
}
|
|
166
|
-
if (cacheApi instanceof H3Error) {
|
|
167
|
-
return cacheApi;
|
|
168
|
-
}
|
|
169
174
|
if (publication && purge)
|
|
170
175
|
publication.cache.forget(publication.cacheKey);
|
|
171
176
|
let image = cacheApi.cachedItem;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** What a request asks of the runtime cache through its `purge` query value. */
|
|
2
|
+
export type PurgeRequest = {
|
|
3
|
+
_tag: 'None';
|
|
4
|
+
} | {
|
|
5
|
+
_tag: 'Authorized';
|
|
6
|
+
} | {
|
|
7
|
+
_tag: 'Denied';
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Parse the `purge` query value. A purge needs the signing secret as its
|
|
11
|
+
* value, the same secret that signs image URLs. Without a secret, no purge
|
|
12
|
+
* can pass.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parsePurgeRequest(value: unknown, secret: string | false | undefined): PurgeRequest;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function safeCompare(a, b) {
|
|
2
|
+
if (a.length !== b.length)
|
|
3
|
+
return false;
|
|
4
|
+
let mismatch = 0;
|
|
5
|
+
for (let i = 0; i < a.length; i++)
|
|
6
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
7
|
+
return mismatch === 0;
|
|
8
|
+
}
|
|
9
|
+
export function parsePurgeRequest(value, secret) {
|
|
10
|
+
if (value === void 0)
|
|
11
|
+
return { _tag: "None" };
|
|
12
|
+
if (typeof secret !== "string" || !secret || typeof value !== "string")
|
|
13
|
+
return { _tag: "Denied" };
|
|
14
|
+
return safeCompare(value, secret) ? { _tag: "Authorized" } : { _tag: "Denied" };
|
|
15
|
+
}
|
|
@@ -21,17 +21,24 @@ declare const sameOriginPathBrand: unique symbol;
|
|
|
21
21
|
export type SameOriginPath = string & {
|
|
22
22
|
readonly [sameOriginPathBrand]: true;
|
|
23
23
|
};
|
|
24
|
+
export type SameOriginPathResult = {
|
|
25
|
+
_tag: 'Ok';
|
|
26
|
+
value: SameOriginPath;
|
|
27
|
+
} | {
|
|
28
|
+
_tag: 'Err';
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
24
31
|
/**
|
|
25
32
|
* Parse an untrusted path (for example the `_path` URL option) into a
|
|
26
|
-
* same-origin path.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
33
|
+
* normalized same-origin path. The input is decoded once and must then be a
|
|
34
|
+
* plain absolute path: one leading `/`, no `.` or `..` segments, no
|
|
35
|
+
* encoded separators, no `?` or `#`, and no control characters. The value
|
|
36
|
+
* is the percent-encoded pathname, so it resolves the same way everywhere.
|
|
30
37
|
*
|
|
31
38
|
* GHSA-h5qf-97cw-2h86: an absolute `_path` navigated the browser renderer to
|
|
32
39
|
* an internal host and reflected the response into the image.
|
|
33
40
|
*/
|
|
34
|
-
export declare function parseSameOriginPath(input: unknown):
|
|
41
|
+
export declare function parseSameOriginPath(input: unknown): SameOriginPathResult;
|
|
35
42
|
/**
|
|
36
43
|
* Resolve `path` against `origin` and return the href, but only when the result
|
|
37
44
|
* stays on that origin. Returns null for a missing or unparseable origin, and for
|
|
@@ -153,28 +153,45 @@ export function isBlockedAddress(address) {
|
|
|
153
153
|
return isBlockedUrl(`http://${address.includes(":") ? `[${address}]` : address}/`);
|
|
154
154
|
}
|
|
155
155
|
const PROBE_ORIGIN = "http://og-image.invalid";
|
|
156
|
-
|
|
156
|
+
const RE_PERCENT_ESCAPE = /%[\da-f]{2}/i;
|
|
157
|
+
const RE_ENCODED_SEPARATOR = /%(?:2f|5c)/i;
|
|
158
|
+
function hasReservedPathChar(input) {
|
|
157
159
|
for (let i = 0; i < input.length; i++) {
|
|
158
160
|
const code = input.charCodeAt(i);
|
|
159
|
-
if (code <= 31 || code === 127 || code === 92)
|
|
161
|
+
if (code <= 31 || code === 127 || code === 92 || code === 63 || code === 35)
|
|
160
162
|
return true;
|
|
161
163
|
}
|
|
162
164
|
return false;
|
|
163
165
|
}
|
|
166
|
+
function err(reason) {
|
|
167
|
+
return { _tag: "Err", reason };
|
|
168
|
+
}
|
|
164
169
|
export function parseSameOriginPath(input) {
|
|
165
170
|
if (typeof input !== "string" || input[0] !== "/" || input[1] === "/")
|
|
166
|
-
return
|
|
167
|
-
if (
|
|
168
|
-
return
|
|
171
|
+
return err("The path must start with a single /.");
|
|
172
|
+
if (RE_ENCODED_SEPARATOR.test(input))
|
|
173
|
+
return err("The path must not encode a / or \\.");
|
|
174
|
+
let decoded;
|
|
175
|
+
try {
|
|
176
|
+
decoded = decodeURIComponent(input);
|
|
177
|
+
} catch {
|
|
178
|
+
return err("The path has an invalid percent-encoding.");
|
|
179
|
+
}
|
|
180
|
+
if (RE_PERCENT_ESCAPE.test(decoded))
|
|
181
|
+
return err("The path must be percent-encoded once.");
|
|
182
|
+
if (hasReservedPathChar(decoded))
|
|
183
|
+
return err("The path must not contain ?, #, \\ or control characters.");
|
|
184
|
+
if (decoded.split("/").some((segment) => segment === "." || segment === ".."))
|
|
185
|
+
return err("The path must not contain . or .. segments.");
|
|
169
186
|
let resolved;
|
|
170
187
|
try {
|
|
171
188
|
resolved = new URL(input, PROBE_ORIGIN);
|
|
172
189
|
} catch {
|
|
173
|
-
return
|
|
190
|
+
return err("The path is not a valid URL path.");
|
|
174
191
|
}
|
|
175
|
-
if (resolved.origin !== PROBE_ORIGIN)
|
|
176
|
-
return
|
|
177
|
-
return
|
|
192
|
+
if (resolved.origin !== PROBE_ORIGIN || resolved.search || resolved.hash)
|
|
193
|
+
return err("The path must stay on the same origin.");
|
|
194
|
+
return { _tag: "Ok", value: resolved.pathname };
|
|
178
195
|
}
|
|
179
196
|
export function resolveSameOriginUrl(path, origin) {
|
|
180
197
|
if (!origin)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare const storageKeyBrand: unique symbol;
|
|
2
|
+
/**
|
|
3
|
+
* A storage key made of a fixed namespace and a SHA-256 hex digest. Only
|
|
4
|
+
* `toStorageKey` creates one, so every key has the same short, fixed format
|
|
5
|
+
* whatever the identity it stands for.
|
|
6
|
+
*/
|
|
7
|
+
export type StorageKey = string & {
|
|
8
|
+
readonly [storageKeyBrand]: true;
|
|
9
|
+
};
|
|
10
|
+
/** Each namespace keeps one kind of entry apart from the others. */
|
|
11
|
+
export type StorageKeyNamespace = 'image' | 'devtools' | 'emoji' | 'prerender';
|
|
12
|
+
/** Derive the storage key for an identity. The same identity always gives the same key. */
|
|
13
|
+
export declare function toStorageKey(namespace: StorageKeyNamespace, identity: unknown): StorageKey;
|
|
14
|
+
export {};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 hex digest (64 chars of [0-9a-f]) of a canonical, key-sorted
|
|
3
|
+
* serialization. Equal structures produce equal digests in every runtime.
|
|
4
|
+
*/
|
|
5
|
+
export declare function digestCanonical(value: unknown): string;
|
|
1
6
|
/**
|
|
2
7
|
* Generate a deterministic hash from options object
|
|
3
8
|
* Excludes _path so images with same options can be cached across pages,
|
|
@@ -112,11 +112,14 @@ function base64UrlToHex(input) {
|
|
|
112
112
|
}
|
|
113
113
|
return hex;
|
|
114
114
|
}
|
|
115
|
+
export function digestCanonical(value) {
|
|
116
|
+
return base64UrlToHex(digest(canonicalJson(value)));
|
|
117
|
+
}
|
|
115
118
|
export function hashOgImageOptions(options, componentHash, version) {
|
|
116
119
|
const { _path, _hash, ...rest } = options;
|
|
117
120
|
const hashableOptions = rest.component === "PageScreenshot" ? { ...rest, _path } : rest;
|
|
118
121
|
const hashInput = componentHash || version ? [hashableOptions, componentHash || "", version || ""] : hashableOptions;
|
|
119
|
-
return
|
|
122
|
+
return digestCanonical(hashInput).slice(0, 16);
|
|
120
123
|
}
|
|
121
124
|
export function encodeOgImageParams(options, defaults) {
|
|
122
125
|
const parts = [];
|
|
@@ -671,7 +671,7 @@ const RE_CSS_DECL = /([\w-]+)\s*:\s*([^\s;][^;]*)(?:;|$)/g;
|
|
|
671
671
|
const RE_CSS_TOGGLE = /(--[\w-]+)\s*:\s+;/g;
|
|
672
672
|
const RE_CSS_CLASS_RULE = /\.((?:\\[0-9a-f]{1,6}\s?|\\[^0-9a-f]|[^\\\s{])+)\s*\{([^}]+)\}/gi;
|
|
673
673
|
const RE_ROOT_HOST_BLOCK = /:(?:root|host)[^\s{]*\s*\{([^}]+)\}/g;
|
|
674
|
-
const RE_UNIVERSAL_BLOCK =
|
|
674
|
+
const RE_UNIVERSAL_BLOCK = /(?:^|[{};])\s*\*[^{};]*\{([^{}]+)\}/g;
|
|
675
675
|
const RE_PROPERTY_AT_RULE = /@property\s+(--[\w-]+)\s*\{([^}]+)\}/g;
|
|
676
676
|
const RE_INITIAL_VALUE = /initial-value\s*:\s*([^\s;][^;]*);?/;
|
|
677
677
|
const RE_CSS_ESCAPE_HEX = /\\([0-9a-f]{1,6})\s?/gi;
|
|
@@ -1112,15 +1112,24 @@ async function extractVarsFromCss(css) {
|
|
|
1112
1112
|
}
|
|
1113
1113
|
return { rootVars, qualifiedRules, universalVars, propertyInitials, perClassVars };
|
|
1114
1114
|
}
|
|
1115
|
+
const MAX_VAR_SUBSTITUTIONS = 100;
|
|
1115
1116
|
function resolveCssVars(css, vars) {
|
|
1116
1117
|
let result = css;
|
|
1117
|
-
let
|
|
1118
|
-
|
|
1119
|
-
const
|
|
1120
|
-
if (
|
|
1118
|
+
let cursor = result.length;
|
|
1119
|
+
for (let substitutions = 0; substitutions < MAX_VAR_SUBSTITUTIONS; ) {
|
|
1120
|
+
const idx = result.lastIndexOf("var(", cursor);
|
|
1121
|
+
if (idx === -1)
|
|
1121
1122
|
break;
|
|
1122
|
-
|
|
1123
|
-
|
|
1123
|
+
const resolved = substituteVar(result, idx, vars);
|
|
1124
|
+
if (resolved === void 0) {
|
|
1125
|
+
if (idx === 0)
|
|
1126
|
+
break;
|
|
1127
|
+
cursor = idx - 1;
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
result = result.substring(0, idx) + resolved.value + result.substring(resolved.end);
|
|
1131
|
+
cursor = idx + resolved.value.length;
|
|
1132
|
+
substitutions++;
|
|
1124
1133
|
}
|
|
1125
1134
|
return result;
|
|
1126
1135
|
}
|
|
@@ -1136,23 +1145,19 @@ function findClosingParen(str, startIdx) {
|
|
|
1136
1145
|
}
|
|
1137
1146
|
return -1;
|
|
1138
1147
|
}
|
|
1139
|
-
function
|
|
1140
|
-
const idx = css.lastIndexOf("var(");
|
|
1141
|
-
if (idx === -1)
|
|
1142
|
-
return css;
|
|
1148
|
+
function substituteVar(css, idx, vars) {
|
|
1143
1149
|
const closeIdx = findClosingParen(css, idx + 4);
|
|
1144
1150
|
if (closeIdx === -1)
|
|
1145
|
-
return
|
|
1151
|
+
return void 0;
|
|
1146
1152
|
const content = css.substring(idx + 4, closeIdx);
|
|
1147
|
-
if (content.includes("var("))
|
|
1148
|
-
return css;
|
|
1149
1153
|
const comma = content.indexOf(",");
|
|
1150
1154
|
const name = (comma >= 0 ? content.substring(0, comma) : content).trim();
|
|
1151
1155
|
const fallback = comma >= 0 ? content.substring(comma + 1).trim() : void 0;
|
|
1152
|
-
const
|
|
1156
|
+
const value = vars.get(name);
|
|
1157
|
+
const resolved = value !== void 0 && value.trim() !== "initial" ? value : fallback;
|
|
1153
1158
|
if (resolved === void 0)
|
|
1154
|
-
return
|
|
1155
|
-
return
|
|
1159
|
+
return void 0;
|
|
1160
|
+
return { value: resolved, end: closeIdx + 1 };
|
|
1156
1161
|
}
|
|
1157
1162
|
function decodeCssClassName(selector) {
|
|
1158
1163
|
let className = selector.startsWith(".") ? selector.slice(1) : selector;
|