@ethercorps/sveltekit-og 4.3.1-next.2 â 4.4.0-next.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 +35 -193
- package/dist/client/assets/NotoSans-Bold.ttf +0 -0
- package/dist/client/assets/NotoSans-Regular.ttf +0 -0
- package/dist/client/component.d.ts +19 -0
- package/dist/client/component.js +32 -0
- package/dist/client/create.d.ts +7 -0
- package/dist/client/create.js +8 -0
- package/dist/client/engines/satori.d.ts +4 -0
- package/dist/client/engines/satori.js +81 -0
- package/dist/client/engines/takumi.d.ts +7 -0
- package/dist/client/engines/takumi.js +9 -0
- package/dist/client/fonts.d.ts +2 -0
- package/dist/client/fonts.js +36 -0
- package/dist/client/image-response.d.ts +13 -0
- package/dist/client/image-response.js +88 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +6 -0
- package/dist/client/render.d.ts +7 -0
- package/dist/client/render.js +37 -0
- package/dist/client/types.d.ts +14 -0
- package/dist/client/types.js +1 -0
- package/dist/fonts.d.ts +4 -4
- package/dist/fonts.js +17 -13
- package/dist/helpers/create.d.ts +7 -4
- package/dist/helpers/create.js +39 -28
- package/dist/helpers/defaults.d.ts +4 -4
- package/dist/helpers/defaults.js +28 -18
- package/dist/helpers/emoji.d.ts +1 -1
- package/dist/helpers/emoji.js +31 -21
- package/dist/helpers/error-handler.d.ts +38 -0
- package/dist/helpers/error-handler.js +76 -0
- package/dist/helpers/logger.d.ts +7 -0
- package/dist/helpers/logger.js +21 -0
- package/dist/helpers/response.d.ts +30 -0
- package/dist/helpers/response.js +52 -0
- package/dist/helpers/to-html.d.ts +10 -0
- package/dist/helpers/to-html.js +10 -0
- package/dist/helpers/toJSX.d.ts +3 -3
- package/dist/helpers/toJSX.js +9 -7
- package/dist/helpers/utils.d.ts +2 -0
- package/dist/helpers/utils.js +25 -0
- package/dist/image-response.d.ts +2 -2
- package/dist/image-response.js +13 -21
- package/dist/plugin.js +12 -5
- package/dist/providers/instances.d.ts +3 -3
- package/dist/providers/instances.js +43 -15
- package/dist/providers/resvg/edge.d.ts +1 -1
- package/dist/providers/resvg/edge.js +10 -6
- package/dist/providers/resvg/node.d.ts +1 -1
- package/dist/providers/resvg/node.js +18 -9
- package/dist/providers/satori/edge.d.ts +6 -0
- package/dist/providers/satori/edge.js +12 -0
- package/dist/providers/satori/node.d.ts +1 -1
- package/dist/providers/satori/node.js +4 -4
- package/dist/takumi/fonts.d.ts +16 -0
- package/dist/takumi/fonts.js +15 -0
- package/dist/takumi/image-response.d.ts +6 -0
- package/dist/takumi/image-response.js +27 -0
- package/dist/takumi/index.d.ts +5 -0
- package/dist/takumi/index.js +4 -0
- package/dist/takumi/render.d.ts +4 -0
- package/dist/takumi/render.js +23 -0
- package/dist/takumi/renderer.d.ts +7 -0
- package/dist/takumi/renderer.js +39 -0
- package/dist/takumi/types.d.ts +64 -0
- package/dist/takumi/types.js +1 -0
- package/dist/types.d.ts +10 -14
- package/package.json +76 -21
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ImageResponseOptions } from "../types.js";
|
|
2
|
+
import type { TakumiImageResponseOptions } from "../takumi/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Options for the client `ImageResponse` / `createImage`. The `engine` field
|
|
5
|
+
* selects the renderer and narrows the rest of the options to that engine's
|
|
6
|
+
* shape (fonts, formats and emoji differ between the two).
|
|
7
|
+
*
|
|
8
|
+
* @default engine "takumi"
|
|
9
|
+
*/
|
|
10
|
+
export type ClientImageResponseOptions = ({
|
|
11
|
+
engine?: "takumi";
|
|
12
|
+
} & TakumiImageResponseOptions) | ({
|
|
13
|
+
engine: "satori";
|
|
14
|
+
} & ImageResponseOptions);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/fonts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FinalFontOptions, FontStyle, FontWeight, MayBePromise } from
|
|
1
|
+
import type { FinalFontOptions, FontStyle, FontWeight, MayBePromise } from "./types.js";
|
|
2
2
|
interface BaseFontOptions {
|
|
3
3
|
weight?: FontWeight;
|
|
4
4
|
style?: FontStyle;
|
|
@@ -7,24 +7,24 @@ interface BaseFontOptions {
|
|
|
7
7
|
* All font types inherit from this class.
|
|
8
8
|
*/
|
|
9
9
|
export declare class BaseFont {
|
|
10
|
-
protected input: any;
|
|
11
10
|
name: string;
|
|
12
11
|
style: FontStyle;
|
|
13
12
|
weight: FontWeight;
|
|
14
|
-
constructor(name: string,
|
|
13
|
+
constructor(name: string, { weight, style }?: BaseFontOptions);
|
|
15
14
|
/** * Abstract getter that returns the promised ArrayBuffer.
|
|
16
15
|
* Overridden by CustomFont and GoogleFont for lazy loading.
|
|
17
16
|
*/
|
|
18
17
|
get data(): MayBePromise<Buffer | ArrayBuffer>;
|
|
19
18
|
}
|
|
20
19
|
export declare class CustomFont extends BaseFont {
|
|
20
|
+
protected input: MayBePromise<Buffer | ArrayBuffer> | (() => MayBePromise<Buffer | ArrayBuffer>);
|
|
21
21
|
private promise?;
|
|
22
22
|
constructor(name: string, input: MayBePromise<Buffer | ArrayBuffer> | (() => MayBePromise<Buffer | ArrayBuffer>), options?: BaseFontOptions);
|
|
23
23
|
/** A promise which resolves to font data as `ArrayBuffer` (Lazy load and CACHED) */
|
|
24
24
|
get data(): Promise<Buffer | ArrayBuffer>;
|
|
25
25
|
}
|
|
26
26
|
/** Loads Google font ArrayBuffer with caching. */
|
|
27
|
-
export declare const loadGoogleFont: (family: string, { text, weight, style, display }?: {
|
|
27
|
+
export declare const loadGoogleFont: (family: string, { text, weight, style, display, }?: {
|
|
28
28
|
text?: string;
|
|
29
29
|
weight?: string | number;
|
|
30
30
|
style?: FontStyle;
|
package/dist/fonts.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { FONT_CACHE_MAP } from
|
|
1
|
+
import { FONT_CACHE_MAP } from "./helpers/cache.js";
|
|
2
2
|
/** * Base font class defining the structure required by Satori.
|
|
3
3
|
* All font types inherit from this class.
|
|
4
4
|
*/
|
|
5
5
|
export class BaseFont {
|
|
6
|
-
input;
|
|
7
6
|
name;
|
|
8
7
|
style;
|
|
9
8
|
weight;
|
|
10
|
-
constructor(name,
|
|
11
|
-
this.input = input;
|
|
9
|
+
constructor(name, { weight = 400, style = "normal" } = {}) {
|
|
12
10
|
this.name = name;
|
|
13
11
|
this.style = style;
|
|
14
12
|
this.weight = weight;
|
|
@@ -17,13 +15,15 @@ export class BaseFont {
|
|
|
17
15
|
* Overridden by CustomFont and GoogleFont for lazy loading.
|
|
18
16
|
*/
|
|
19
17
|
get data() {
|
|
20
|
-
|
|
18
|
+
throw new Error("The 'data' getter must be implemented by subclasses of BaseFont.");
|
|
21
19
|
}
|
|
22
20
|
}
|
|
23
21
|
export class CustomFont extends BaseFont {
|
|
22
|
+
input;
|
|
24
23
|
promise;
|
|
25
24
|
constructor(name, input, options) {
|
|
26
|
-
super(name,
|
|
25
|
+
super(name, options);
|
|
26
|
+
this.input = input;
|
|
27
27
|
}
|
|
28
28
|
/** A promise which resolves to font data as `ArrayBuffer` (Lazy load and CACHED) */
|
|
29
29
|
get data() {
|
|
@@ -33,7 +33,7 @@ export class CustomFont extends BaseFont {
|
|
|
33
33
|
return Promise.resolve(cachedData);
|
|
34
34
|
}
|
|
35
35
|
const fallback = async () => {
|
|
36
|
-
const buffer = typeof this.input ===
|
|
36
|
+
const buffer = typeof this.input === "function" ? this.input() : this.input;
|
|
37
37
|
const resolvedBuffer = await buffer;
|
|
38
38
|
FONT_CACHE_MAP.set(cacheKey, resolvedBuffer);
|
|
39
39
|
return resolvedBuffer;
|
|
@@ -43,17 +43,21 @@ export class CustomFont extends BaseFont {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
/** Constructs Google font css url */
|
|
46
|
-
const constructGoogleFontCssUrl = (family, { text, weight = 400, style =
|
|
46
|
+
const constructGoogleFontCssUrl = (family, { text, weight = 400, style = "normal", display, } = {}) => {
|
|
47
47
|
// Logic to build the URL (e.g., https://fonts.googleapis.com/css2?family=...wght@...)
|
|
48
48
|
const params = {
|
|
49
|
-
family: `${family.replaceAll(
|
|
49
|
+
family: `${family.replaceAll(" ", "+")}:${style === "italic" ? "ital," : ""}wght@${style === "italic" ? "1," : ""}${weight}`,
|
|
50
50
|
};
|
|
51
51
|
if (text)
|
|
52
52
|
params.text = encodeURIComponent(text);
|
|
53
|
-
|
|
53
|
+
if (display)
|
|
54
|
+
params.display = display;
|
|
55
|
+
return `https://fonts.googleapis.com/css2?${Object.entries(params)
|
|
56
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
57
|
+
.join("&")}`;
|
|
54
58
|
};
|
|
55
59
|
/** Loads Google font ArrayBuffer with caching. */
|
|
56
|
-
export const loadGoogleFont = async (family, { text, weight = 400, style =
|
|
60
|
+
export const loadGoogleFont = async (family, { text, weight = 400, style = "normal", display, } = {}) => {
|
|
57
61
|
const cssUrl = constructGoogleFontCssUrl(family, { text, weight, display, style });
|
|
58
62
|
const fromMap = FONT_CACHE_MAP.get(cssUrl);
|
|
59
63
|
if (fromMap) {
|
|
@@ -84,7 +88,7 @@ export class GoogleFont extends BaseFont {
|
|
|
84
88
|
text;
|
|
85
89
|
promise;
|
|
86
90
|
constructor(family, options = {}) {
|
|
87
|
-
super(options.name || family,
|
|
91
|
+
super(options.name || family, options);
|
|
88
92
|
this.family = family;
|
|
89
93
|
this.text = options.text;
|
|
90
94
|
}
|
|
@@ -108,5 +112,5 @@ export async function resolveFonts(fontClasses) {
|
|
|
108
112
|
style: fontClass.style,
|
|
109
113
|
};
|
|
110
114
|
}));
|
|
111
|
-
return resolvedFonts.filter(font => font !== null);
|
|
115
|
+
return resolvedFonts.filter((font) => font !== null);
|
|
112
116
|
}
|
package/dist/helpers/create.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { Component } from
|
|
2
|
-
import type { ComponentOptions, ImageOptions } from
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { ComponentOptions, ImageOptions } from "../types.js";
|
|
3
|
+
/** Single entry for the Satori and ReSVG engine */
|
|
4
|
+
export declare function createImage(element: string | Component<any>, imageOptions: ImageOptions, componentOptions?: ComponentOptions): Promise<Uint8Array | string>;
|
|
5
|
+
/** Create an SVG string from a Svelte component or HTML string using Satori */
|
|
6
|
+
export declare function createSvg(element: string | Component<any>, imageOptions: ImageOptions, componentOptions?: ComponentOptions): Promise<string>;
|
|
7
|
+
export declare function createPng(element: string | Component<any>, imageOptions: ImageOptions, componentOptions?: ComponentOptions): Promise<Uint8Array<ArrayBufferLike>>;
|
package/dist/helpers/create.js
CHANGED
|
@@ -1,38 +1,49 @@
|
|
|
1
|
-
import { loadDynamicAsset } from
|
|
2
|
-
import { default_fonts, DEFAULT_WIDTH } from
|
|
3
|
-
import { useResvg, useSatori } from
|
|
4
|
-
import { createVNode } from
|
|
1
|
+
import { loadDynamicAsset } from "./emoji.js";
|
|
2
|
+
import { default_fonts, DEFAULT_WIDTH } from "../helpers/defaults.js";
|
|
3
|
+
import { useResvg, useSatori } from "../providers/instances.js";
|
|
4
|
+
import { createVNode } from "./toJSX.js";
|
|
5
|
+
import { createLogger } from "./logger.js";
|
|
6
|
+
import { handleAsync, ErrorCodes } from "./error-handler.js";
|
|
7
|
+
/** Single entry for the Satori and ReSVG engine */
|
|
8
|
+
export function createImage(element, imageOptions, componentOptions) {
|
|
9
|
+
return imageOptions.format === "svg"
|
|
10
|
+
? createSvg(element, imageOptions, componentOptions)
|
|
11
|
+
: createPng(element, imageOptions, componentOptions);
|
|
12
|
+
}
|
|
13
|
+
/** Create an SVG string from a Svelte component or HTML string using Satori */
|
|
5
14
|
export async function createSvg(element, imageOptions, componentOptions) {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
const log = createLogger(imageOptions.debug ?? false);
|
|
16
|
+
const vnodes = createVNode(element, componentOptions);
|
|
17
|
+
const satori = await useSatori(imageOptions.debug);
|
|
18
|
+
const satoriOptions = { ...imageOptions };
|
|
19
|
+
if (!satoriOptions.fonts) {
|
|
20
|
+
satoriOptions.fonts = await handleAsync(() => default_fonts(), ErrorCodes.FONT_LOAD_FAILED, "Failed to load default fonts for Satori");
|
|
10
21
|
}
|
|
11
|
-
satoriOptions
|
|
12
|
-
emoji: imageOptions.emoji
|
|
22
|
+
satoriOptions.loadAdditionalAsset = loadDynamicAsset({
|
|
23
|
+
emoji: imageOptions.emoji,
|
|
13
24
|
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return satori(vnodes, satoriOptions);
|
|
25
|
+
log.debug("Generating SVG with Satori");
|
|
26
|
+
log.info("VNode provided to satori:", JSON.stringify(vnodes, null, 2), "\n");
|
|
27
|
+
log.info("Options provided to satori:", imageOptions);
|
|
28
|
+
return handleAsync(() => satori(vnodes, satoriOptions), ErrorCodes.SATORI_RENDER_FAILED, "Failed to render SVG with Satori");
|
|
19
29
|
}
|
|
30
|
+
/* Create a PNG image from a Svelte component or HTML string using Satori and ReSVG */
|
|
20
31
|
export async function createPng(element, imageOptions, componentOptions) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const resvg_instance = await useResvg();
|
|
32
|
+
const log = createLogger(imageOptions.debug ?? false);
|
|
33
|
+
const svg = await handleAsync(() => createSvg(element, imageOptions, componentOptions), ErrorCodes.SATORI_RENDER_FAILED, "Failed to create SVG for PNG rendering");
|
|
34
|
+
log.debug("SVG generated by satori for ReSVG: \n", svg, "\n");
|
|
35
|
+
const resvg_instance = await handleAsync(() => useResvg(imageOptions.debug), ErrorCodes.RESVG_INIT_FAILED, "Failed to initialize ReSVG");
|
|
26
36
|
const resvg_options = {
|
|
27
37
|
fitTo: {
|
|
28
|
-
mode:
|
|
38
|
+
mode: "width",
|
|
29
39
|
value: imageOptions.width || DEFAULT_WIDTH,
|
|
30
|
-
}
|
|
40
|
+
},
|
|
31
41
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
log.debug("Rendering PNG with ReSVG");
|
|
43
|
+
log.info("Options provided to ReSVG:", resvg_options, "\n");
|
|
44
|
+
return handleAsync(async () => {
|
|
45
|
+
const resvg = new resvg_instance(svg, resvg_options);
|
|
46
|
+
const png_data = resvg.render();
|
|
47
|
+
return png_data.asPng();
|
|
48
|
+
}, ErrorCodes.RESVG_RENDER_FAILED, "Failed to render PNG with ReSVG");
|
|
38
49
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { SatoriOptions } from
|
|
2
|
-
import type { ImageOptions } from
|
|
3
|
-
import type { EmojiType } from
|
|
4
|
-
export declare function default_fonts(): Promise<SatoriOptions[
|
|
1
|
+
import type { SatoriOptions } from "satori";
|
|
2
|
+
import type { ImageOptions } from "../types.js";
|
|
3
|
+
import type { EmojiType } from "./emoji.js";
|
|
4
|
+
export declare function default_fonts(): Promise<SatoriOptions["fonts"]>;
|
|
5
5
|
export declare const DEFAULT_FORMAT = "png";
|
|
6
6
|
export declare const DEFAULT_WIDTH = 1200;
|
|
7
7
|
export declare const DEFAULT_HEIGHT = 630;
|
package/dist/helpers/defaults.js
CHANGED
|
@@ -1,37 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
throw
|
|
8
|
-
}
|
|
9
|
-
|
|
1
|
+
import { handleAsyncAll, validateResponse, ErrorCodes } from "./error-handler.js";
|
|
2
|
+
// fetched once per process; a rejected fetch clears the cache so the next request retries
|
|
3
|
+
let defaultFontsPromise;
|
|
4
|
+
export function default_fonts() {
|
|
5
|
+
defaultFontsPromise ??= loadDefaultFonts().catch((error) => {
|
|
6
|
+
defaultFontsPromise = undefined;
|
|
7
|
+
throw error;
|
|
8
|
+
});
|
|
9
|
+
return defaultFontsPromise;
|
|
10
|
+
}
|
|
11
|
+
async function loadDefaultFonts() {
|
|
12
|
+
const [noto_sans_regular_font_resp, noto_sans_bold_font_reps] = await handleAsyncAll([
|
|
13
|
+
() => fetch("https://cdn-sveltekit-og.ethercorps.io/NotoSans-Regular.ttf"),
|
|
14
|
+
() => fetch("https://cdn-sveltekit-og.ethercorps.io/NotoSans-Bold.ttf"),
|
|
15
|
+
], ErrorCodes.FONT_LOAD_FAILED, "Failed to fetch default fonts");
|
|
16
|
+
const [noto_sans_regular_font, noto_sans_bold_font] = await handleAsyncAll([
|
|
17
|
+
() => validateResponse(noto_sans_regular_font_resp, ErrorCodes.FONT_LOAD_FAILED, "Failed to validate regular font response"),
|
|
18
|
+
() => validateResponse(noto_sans_bold_font_reps, ErrorCodes.FONT_LOAD_FAILED, "Failed to validate bold font response"),
|
|
19
|
+
], ErrorCodes.FONT_LOAD_FAILED, "Failed to process font responses");
|
|
10
20
|
return [
|
|
11
21
|
{
|
|
12
22
|
data: noto_sans_regular_font,
|
|
13
|
-
name:
|
|
23
|
+
name: "Inter",
|
|
14
24
|
weight: 400,
|
|
15
|
-
style:
|
|
25
|
+
style: "normal",
|
|
16
26
|
},
|
|
17
27
|
{
|
|
18
28
|
data: noto_sans_bold_font,
|
|
19
|
-
name:
|
|
29
|
+
name: "Inter",
|
|
20
30
|
weight: 700,
|
|
21
|
-
style:
|
|
22
|
-
}
|
|
31
|
+
style: "normal",
|
|
32
|
+
},
|
|
23
33
|
];
|
|
24
34
|
}
|
|
25
|
-
export const DEFAULT_FORMAT =
|
|
35
|
+
export const DEFAULT_FORMAT = "png";
|
|
26
36
|
export const DEFAULT_WIDTH = 1200;
|
|
27
37
|
export const DEFAULT_HEIGHT = 630;
|
|
28
|
-
export const DEFAULT_EMOJI_PROVIDER =
|
|
38
|
+
export const DEFAULT_EMOJI_PROVIDER = "twemoji";
|
|
29
39
|
export const DEFAULT_STATUS_CODE = 200;
|
|
30
|
-
export const DEFAULT_STATUS_TEXT =
|
|
40
|
+
export const DEFAULT_STATUS_TEXT = "Success";
|
|
31
41
|
export const DEFAULT_OPTIONS = {
|
|
32
42
|
height: DEFAULT_HEIGHT,
|
|
33
43
|
width: DEFAULT_WIDTH,
|
|
34
44
|
debug: false,
|
|
35
45
|
format: DEFAULT_FORMAT,
|
|
36
|
-
emoji:
|
|
46
|
+
emoji: "twemoji",
|
|
37
47
|
};
|
package/dist/helpers/emoji.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ declare const emoji_apis: {
|
|
|
8
8
|
};
|
|
9
9
|
export declare const loadDynamicAsset: ({ emoji }: {
|
|
10
10
|
emoji: EmojiType;
|
|
11
|
-
}) => (
|
|
11
|
+
}) => (code: string, text: string) => Promise<string | undefined>;
|
|
12
12
|
export type EmojiType = keyof typeof emoji_apis;
|
|
13
13
|
export {};
|
package/dist/helpers/emoji.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { DEFAULT_EMOJI_PROVIDER } from
|
|
1
|
+
import { DEFAULT_EMOJI_PROVIDER } from "../helpers/defaults.js";
|
|
2
|
+
import { handleAsync, ErrorCodes } from "./error-handler.js";
|
|
2
3
|
// Code stolen from @vercel/og and https://github.com/fineshopdesign/cf-wasm
|
|
3
4
|
const U200D = String.fromCharCode(8205);
|
|
4
5
|
const UFE0Fg = /\uFE0F/g;
|
|
@@ -11,7 +12,7 @@ function toCodePoint(unicodeSurrogates) {
|
|
|
11
12
|
while (i < unicodeSurrogates.length) {
|
|
12
13
|
c = unicodeSurrogates.charCodeAt(i++);
|
|
13
14
|
if (p) {
|
|
14
|
-
r.push((65536 + (p - 55296 << 10) + (c - 56320)).toString(16));
|
|
15
|
+
r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16));
|
|
15
16
|
p = 0;
|
|
16
17
|
}
|
|
17
18
|
else if (55296 <= c && c <= 56319) {
|
|
@@ -24,32 +25,41 @@ function toCodePoint(unicodeSurrogates) {
|
|
|
24
25
|
return r.join("-");
|
|
25
26
|
}
|
|
26
27
|
const emoji_apis = {
|
|
27
|
-
twemoji: (code) => "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/" +
|
|
28
|
+
twemoji: (code) => "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/" +
|
|
29
|
+
code.toLowerCase() +
|
|
30
|
+
".svg",
|
|
28
31
|
openmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/",
|
|
29
32
|
blobmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/",
|
|
30
33
|
noto: "https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/",
|
|
31
|
-
fluent: (code) => "https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/" +
|
|
32
|
-
|
|
34
|
+
fluent: (code) => "https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/" +
|
|
35
|
+
code.toLowerCase() +
|
|
36
|
+
"_color.svg",
|
|
37
|
+
fluentFlat: (code) => "https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/" +
|
|
38
|
+
code.toLowerCase() +
|
|
39
|
+
"_flat.svg",
|
|
33
40
|
};
|
|
34
|
-
function loadEmoji(code, type) {
|
|
35
|
-
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
async function loadEmoji(code, type) {
|
|
42
|
+
return handleAsync(async () => {
|
|
43
|
+
if (!type || !emoji_apis[type]) {
|
|
44
|
+
type = DEFAULT_EMOJI_PROVIDER;
|
|
45
|
+
}
|
|
46
|
+
const api = emoji_apis[type];
|
|
47
|
+
if (typeof api === "function") {
|
|
48
|
+
return fetch(api(code));
|
|
49
|
+
}
|
|
50
|
+
return fetch(`${api}${code.toUpperCase()}.svg`);
|
|
51
|
+
}, ErrorCodes.EMOJI_LOAD_FAILED, `Failed to load emoji for code: ${code}`);
|
|
43
52
|
}
|
|
44
53
|
export const loadDynamicAsset = ({ emoji }) => {
|
|
45
|
-
|
|
54
|
+
return async (code, text) => {
|
|
46
55
|
if (code === "emoji") {
|
|
47
|
-
return
|
|
56
|
+
return handleAsync(async () => {
|
|
57
|
+
const iconCode = getIconCode(text);
|
|
58
|
+
const emojiResponse = await loadEmoji(iconCode, emoji);
|
|
59
|
+
const svgText = await emojiResponse.text();
|
|
60
|
+
const base64Data = btoa(svgText);
|
|
61
|
+
return `data:image/svg+xml;base64,` + base64Data;
|
|
62
|
+
}, ErrorCodes.EMOJI_LOAD_FAILED, `Failed to process emoji: ${text}`);
|
|
48
63
|
}
|
|
49
64
|
};
|
|
50
|
-
return async (...args) => {
|
|
51
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
52
|
-
// @ts-ignore
|
|
53
|
-
return await fn(...args);
|
|
54
|
-
};
|
|
55
65
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare class ImageResponseError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
originalError?: Error | undefined;
|
|
4
|
+
constructor(message: string, code: string, originalError?: Error | undefined);
|
|
5
|
+
}
|
|
6
|
+
/** Coerce anything thrown into an ImageResponseError, keeping existing ones as-is. */
|
|
7
|
+
export declare function toImageResponseError(error: unknown): ImageResponseError;
|
|
8
|
+
export declare const ErrorCodes: {
|
|
9
|
+
readonly FONT_LOAD_FAILED: "FONT_LOAD_FAILED";
|
|
10
|
+
readonly VNODE_CREATION_FAILED: "VNODE_CREATION_FAILED";
|
|
11
|
+
readonly SATORI_RENDER_FAILED: "SATORI_RENDER_FAILED";
|
|
12
|
+
readonly RESVG_INIT_FAILED: "RESVG_INIT_FAILED";
|
|
13
|
+
readonly RESVG_RENDER_FAILED: "RESVG_RENDER_FAILED";
|
|
14
|
+
readonly SATORI_INIT_FAILED: "SATORI_INIT_FAILED";
|
|
15
|
+
readonly EMOJI_LOAD_FAILED: "EMOJI_LOAD_FAILED";
|
|
16
|
+
readonly TAKUMI_INIT_FAILED: "TAKUMI_INIT_FAILED";
|
|
17
|
+
readonly TAKUMI_RENDER_FAILED: "TAKUMI_RENDER_FAILED";
|
|
18
|
+
readonly COMPONENT_IN_WORKER: "COMPONENT_IN_WORKER";
|
|
19
|
+
readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Wraps an async operation with error handling and logging
|
|
23
|
+
*/
|
|
24
|
+
export declare function handleAsync<T = unknown>(operation: () => Promise<T>, errorCode: string, errorMessage: string): Promise<T>;
|
|
25
|
+
/**
|
|
26
|
+
* Wraps a sync operation with error handling and logging
|
|
27
|
+
*/
|
|
28
|
+
export declare function handleSync<T>(operation: () => T, errorCode: string, errorMessage: string): T;
|
|
29
|
+
/**
|
|
30
|
+
* Wraps multiple async operations with error handling
|
|
31
|
+
*/
|
|
32
|
+
export declare function handleAsyncAll<T extends readonly unknown[]>(operations: {
|
|
33
|
+
readonly [K in keyof T]: () => Promise<T[K]>;
|
|
34
|
+
}, errorCode: string, errorMessage: string): Promise<T>;
|
|
35
|
+
/**
|
|
36
|
+
* Validates a response and throws if not ok
|
|
37
|
+
*/
|
|
38
|
+
export declare function validateResponse(response: Response, errorCode: string, errorMessage: string): Promise<ArrayBuffer>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export class ImageResponseError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
originalError;
|
|
4
|
+
constructor(message, code, originalError) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.originalError = originalError;
|
|
8
|
+
this.name = "ImageResponseError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Coerce anything thrown into an ImageResponseError, keeping existing ones as-is. */
|
|
12
|
+
export function toImageResponseError(error) {
|
|
13
|
+
if (error instanceof ImageResponseError)
|
|
14
|
+
return error;
|
|
15
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
16
|
+
return new ImageResponseError(err.message, ErrorCodes.UNKNOWN_ERROR, err);
|
|
17
|
+
}
|
|
18
|
+
export const ErrorCodes = {
|
|
19
|
+
FONT_LOAD_FAILED: "FONT_LOAD_FAILED",
|
|
20
|
+
VNODE_CREATION_FAILED: "VNODE_CREATION_FAILED",
|
|
21
|
+
SATORI_RENDER_FAILED: "SATORI_RENDER_FAILED",
|
|
22
|
+
RESVG_INIT_FAILED: "RESVG_INIT_FAILED",
|
|
23
|
+
RESVG_RENDER_FAILED: "RESVG_RENDER_FAILED",
|
|
24
|
+
SATORI_INIT_FAILED: "SATORI_INIT_FAILED",
|
|
25
|
+
EMOJI_LOAD_FAILED: "EMOJI_LOAD_FAILED",
|
|
26
|
+
TAKUMI_INIT_FAILED: "TAKUMI_INIT_FAILED",
|
|
27
|
+
TAKUMI_RENDER_FAILED: "TAKUMI_RENDER_FAILED",
|
|
28
|
+
COMPONENT_IN_WORKER: "COMPONENT_IN_WORKER",
|
|
29
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Wraps an async operation with error handling and logging
|
|
33
|
+
*/
|
|
34
|
+
export async function handleAsync(operation, errorCode, errorMessage) {
|
|
35
|
+
try {
|
|
36
|
+
return await operation();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
40
|
+
throw new ImageResponseError(errorMessage, errorCode, err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wraps a sync operation with error handling and logging
|
|
45
|
+
*/
|
|
46
|
+
export function handleSync(operation, errorCode, errorMessage) {
|
|
47
|
+
try {
|
|
48
|
+
return operation();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
52
|
+
throw new ImageResponseError(errorMessage, errorCode, err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Wraps multiple async operations with error handling
|
|
57
|
+
*/
|
|
58
|
+
export async function handleAsyncAll(operations, errorCode, errorMessage) {
|
|
59
|
+
try {
|
|
60
|
+
return (await Promise.all(operations.map((op) => op())));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
64
|
+
throw new ImageResponseError(errorMessage, errorCode, err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Validates a response and throws if not ok
|
|
69
|
+
*/
|
|
70
|
+
export async function validateResponse(response, errorCode, errorMessage) {
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new ImageResponseError(`${errorMessage} (HTTP ${response.status})`, errorCode);
|
|
73
|
+
}
|
|
74
|
+
const buffer = await response.arrayBuffer();
|
|
75
|
+
return buffer;
|
|
76
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Logger = ReturnType<typeof createLogger>;
|
|
2
|
+
export declare function createLogger(debug: boolean): {
|
|
3
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
4
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
5
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
6
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const PREFIX = "[SvelteKit-OG]";
|
|
2
|
+
export function createLogger(debug) {
|
|
3
|
+
return {
|
|
4
|
+
debug: (message, ...args) => {
|
|
5
|
+
if (debug)
|
|
6
|
+
console.log(`${PREFIX} đ ${message}`, ...args);
|
|
7
|
+
},
|
|
8
|
+
info: (message, ...args) => {
|
|
9
|
+
if (debug)
|
|
10
|
+
console.info(`${PREFIX} âšī¸ ${message}`, ...args);
|
|
11
|
+
},
|
|
12
|
+
warn: (message, ...args) => {
|
|
13
|
+
if (debug)
|
|
14
|
+
console.warn(`${PREFIX} â ī¸ ${message}`, ...args);
|
|
15
|
+
},
|
|
16
|
+
error: (message, ...args) => {
|
|
17
|
+
if (debug)
|
|
18
|
+
console.error(`${PREFIX} â ${message}`, ...args);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** format â content type for every format either engine can emit; satori uses the
|
|
2
|
+
* png/svg subset, takumi the whole map. */
|
|
3
|
+
export declare const CONTENT_TYPES: {
|
|
4
|
+
readonly png: "image/png";
|
|
5
|
+
readonly jpeg: "image/jpeg";
|
|
6
|
+
readonly webp: "image/webp";
|
|
7
|
+
readonly ico: "image/x-icon";
|
|
8
|
+
readonly raw: "application/octet-stream";
|
|
9
|
+
readonly svg: "image/svg+xml";
|
|
10
|
+
};
|
|
11
|
+
type ImageProducer = () => Promise<Uint8Array | string>;
|
|
12
|
+
interface BuildOptions {
|
|
13
|
+
/** uppercased format, used in logs e.g. "PNG" */
|
|
14
|
+
label: string;
|
|
15
|
+
contentType: string;
|
|
16
|
+
debug: boolean;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
status?: number;
|
|
19
|
+
statusText?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Shared body + response init for both engines' ImageResponse. Streams the image
|
|
23
|
+
* out as bytes (a Response stream can't take a raw string), with consistent
|
|
24
|
+
* cache headers and error handling.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildImageResponse(produce: ImageProducer, opts: BuildOptions): {
|
|
27
|
+
body: ReadableStream;
|
|
28
|
+
init: ResponseInit;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createLogger } from "./logger.js";
|
|
2
|
+
import { handleAsync, toImageResponseError, ErrorCodes } from "./error-handler.js";
|
|
3
|
+
import { DEFAULT_STATUS_CODE, DEFAULT_STATUS_TEXT } from "./defaults.js";
|
|
4
|
+
import { formatBytes } from "./utils.js";
|
|
5
|
+
/** format â content type for every format either engine can emit; satori uses the
|
|
6
|
+
* png/svg subset, takumi the whole map. */
|
|
7
|
+
export const CONTENT_TYPES = {
|
|
8
|
+
png: "image/png",
|
|
9
|
+
jpeg: "image/jpeg",
|
|
10
|
+
webp: "image/webp",
|
|
11
|
+
ico: "image/x-icon",
|
|
12
|
+
raw: "application/octet-stream",
|
|
13
|
+
svg: "image/svg+xml",
|
|
14
|
+
};
|
|
15
|
+
// TextEncoder is stateless; one shared instance instead of one per svg response
|
|
16
|
+
const textEncoder = new TextEncoder();
|
|
17
|
+
/**
|
|
18
|
+
* Shared body + response init for both engines' ImageResponse. Streams the image
|
|
19
|
+
* out as bytes (a Response stream can't take a raw string), with consistent
|
|
20
|
+
* cache headers and error handling.
|
|
21
|
+
*/
|
|
22
|
+
export function buildImageResponse(produce, opts) {
|
|
23
|
+
const log = createLogger(opts.debug);
|
|
24
|
+
const body = new ReadableStream({
|
|
25
|
+
async start(controller) {
|
|
26
|
+
try {
|
|
27
|
+
const out = await handleAsync(produce, ErrorCodes.UNKNOWN_ERROR, `Failed to generate ${opts.label}`);
|
|
28
|
+
const bytes = typeof out === "string" ? textEncoder.encode(out) : out;
|
|
29
|
+
log.info(`Generated ${opts.label}: ${formatBytes(bytes.byteLength)}`);
|
|
30
|
+
controller.enqueue(bytes);
|
|
31
|
+
controller.close();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const err = toImageResponseError(error);
|
|
35
|
+
log.error("Failed to create image response:", err.message);
|
|
36
|
+
controller.error(err);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const init = {
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": opts.contentType,
|
|
43
|
+
"Cache-Control": opts.debug
|
|
44
|
+
? "no-cache, no-store"
|
|
45
|
+
: "public, immutable, no-transform, max-age=31536000",
|
|
46
|
+
...opts.headers,
|
|
47
|
+
},
|
|
48
|
+
status: opts.status || DEFAULT_STATUS_CODE,
|
|
49
|
+
statusText: opts.statusText || DEFAULT_STATUS_TEXT,
|
|
50
|
+
};
|
|
51
|
+
return { body, init };
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
/**
|
|
3
|
+
* Render a Svelte component to its SSR html parts. Shared by both engines â satori
|
|
4
|
+
* feeds body+head to satori-html, takumi passes the html string straight in. Use
|
|
5
|
+
* `<svelte:options css="injected" />` to get component styles into `head`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function renderComponentToHtml(component: Component<any>, props?: Record<string, unknown>): {
|
|
8
|
+
head: string;
|
|
9
|
+
body: string;
|
|
10
|
+
};
|