@ethercorps/sveltekit-og 4.3.0-next.1 → 4.3.0-next.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.
@@ -3,14 +3,12 @@ import { default_fonts, DEFAULT_WIDTH } from "../helpers/defaults.js";
3
3
  import { useResvg, useSatori } from "../providers/instances.js";
4
4
  import { createVNode } from "./toJSX.js";
5
5
  import { logger } from "./logger.js";
6
+ import { handleAsyncAll, handleAsync, ErrorCodes } from "./error-handler.js";
6
7
  export async function createSvg(element, imageOptions, componentOptions) {
7
- const [satori, vnodes] = await Promise.all([
8
- useSatori(),
9
- createVNode(element, componentOptions),
10
- ]);
8
+ const [satori, vnodes] = await handleAsyncAll([() => useSatori(), () => Promise.resolve(createVNode(element, componentOptions))], ErrorCodes.SATORI_RENDER_FAILED, "Failed to initialize Satori or create VNode");
11
9
  const satoriOptions = structuredClone(imageOptions);
12
10
  if (!Object.hasOwn(satoriOptions, "fonts")) {
13
- satoriOptions["fonts"] = await default_fonts();
11
+ satoriOptions["fonts"] = await handleAsync(() => default_fonts(), ErrorCodes.FONT_LOAD_FAILED, "Failed to load default fonts for Satori");
14
12
  }
15
13
  satoriOptions["loadAdditionalAsset"] = loadDynamicAsset({
16
14
  emoji: imageOptions.emoji,
@@ -18,12 +16,12 @@ export async function createSvg(element, imageOptions, componentOptions) {
18
16
  logger.debug("Generating SVG with Satori");
19
17
  logger.info("VNode provided to satori:", JSON.stringify(vnodes, null, 2), "\n");
20
18
  logger.info("Options provided to satori:", imageOptions);
21
- return satori(vnodes, satoriOptions);
19
+ return handleAsync(() => satori(vnodes, satoriOptions), ErrorCodes.SATORI_RENDER_FAILED, "Failed to render SVG with Satori");
22
20
  }
23
21
  export async function createPng(element, imageOptions, componentOptions) {
24
- const svg = await createSvg(element, imageOptions, componentOptions);
22
+ const svg = await handleAsync(() => createSvg(element, imageOptions, componentOptions), ErrorCodes.SATORI_RENDER_FAILED, "Failed to create SVG for PNG rendering");
25
23
  logger.debug("SVG generated by satori for ReSVG: \n", svg, "\n");
26
- const resvg_instance = await useResvg();
24
+ const resvg_instance = await handleAsync(() => useResvg(), ErrorCodes.RESVG_INIT_FAILED, "Failed to initialize ReSVG");
27
25
  const resvg_options = {
28
26
  fitTo: {
29
27
  mode: "width",
@@ -32,7 +30,9 @@ export async function createPng(element, imageOptions, componentOptions) {
32
30
  };
33
31
  logger.debug("Rendering PNG with ReSVG");
34
32
  logger.info("Options provided to ReSVG:", resvg_options, "\n");
35
- const resvg = new resvg_instance(svg, resvg_options);
36
- const png_data = resvg.render();
37
- return png_data.asPng();
33
+ return handleAsync(async () => {
34
+ const resvg = new resvg_instance(svg, resvg_options);
35
+ const png_data = resvg.render();
36
+ return png_data.asPng();
37
+ }, ErrorCodes.RESVG_RENDER_FAILED, "Failed to render PNG with ReSVG");
38
38
  }
@@ -1,16 +1,13 @@
1
+ import { handleAsyncAll, validateResponse, ErrorCodes } from "./error-handler.js";
1
2
  export async function default_fonts() {
2
- const [noto_sans_regular_font_resp, noto_sans_bold_font_reps] = await Promise.all([
3
- fetch("https://cdn-sveltekit-og.ethercorps.io/NotoSans-Regular.ttf"),
4
- fetch("https://cdn-sveltekit-og.ethercorps.io/NotoSans-Bold.ttf"),
5
- ]);
6
- if (!(noto_sans_bold_font_reps.ok || noto_sans_bold_font_reps.ok)) {
7
- console.error("Not able to load default fonts");
8
- throw new Error("Not able to load default fonts");
9
- }
10
- const [noto_sans_regular_font, noto_sans_bold_font] = await Promise.all([
11
- noto_sans_regular_font_resp.arrayBuffer(),
12
- noto_sans_bold_font_reps.arrayBuffer(),
13
- ]);
3
+ const [noto_sans_regular_font_resp, noto_sans_bold_font_reps] = await handleAsyncAll([
4
+ () => fetch("https://cdn-sveltekit-og.ethercorps.io/NotoSans-Regular.ttf"),
5
+ () => fetch("https://cdn-sveltekit-og.ethercorps.io/NotoSans-Bold.ttf"),
6
+ ], ErrorCodes.FONT_LOAD_FAILED, "Failed to fetch default fonts");
7
+ const [noto_sans_regular_font, noto_sans_bold_font] = await handleAsyncAll([
8
+ () => validateResponse(noto_sans_regular_font_resp, ErrorCodes.FONT_LOAD_FAILED, "Failed to validate regular font response"),
9
+ () => validateResponse(noto_sans_bold_font_reps, ErrorCodes.FONT_LOAD_FAILED, "Failed to validate bold font response"),
10
+ ], ErrorCodes.FONT_LOAD_FAILED, "Failed to process font responses");
14
11
  return [
15
12
  {
16
13
  data: noto_sans_regular_font,
@@ -1,4 +1,5 @@
1
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;
@@ -37,24 +38,28 @@ const emoji_apis = {
37
38
  code.toLowerCase() +
38
39
  "_flat.svg",
39
40
  };
40
- function loadEmoji(code, type) {
41
- if (!type || !emoji_apis[type]) {
42
- type = DEFAULT_EMOJI_PROVIDER;
43
- }
44
- const api = emoji_apis[type];
45
- if (typeof api === "function") {
46
- return fetch(api(code));
47
- }
48
- return fetch(`${api}${code.toUpperCase()}.svg`);
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}`);
49
52
  }
50
53
  export const loadDynamicAsset = ({ emoji }) => {
51
54
  const fn = async (code, text) => {
52
55
  if (code === "emoji") {
53
- const iconCode = getIconCode(text);
54
- const emojiResponse = await loadEmoji(iconCode, emoji);
55
- const svgText = await emojiResponse.text();
56
- const base64Data = btoa(svgText);
57
- return `data:image/svg+xml;base64,` + base64Data;
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}`);
58
63
  }
59
64
  };
60
65
  return async (...args) => {
@@ -0,0 +1,33 @@
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
+ export declare const ErrorCodes: {
7
+ readonly FONT_LOAD_FAILED: "FONT_LOAD_FAILED";
8
+ readonly VNODE_CREATION_FAILED: "VNODE_CREATION_FAILED";
9
+ readonly SATORI_RENDER_FAILED: "SATORI_RENDER_FAILED";
10
+ readonly RESVG_INIT_FAILED: "RESVG_INIT_FAILED";
11
+ readonly RESVG_RENDER_FAILED: "RESVG_RENDER_FAILED";
12
+ readonly SATORI_INIT_FAILED: "SATORI_INIT_FAILED";
13
+ readonly EMOJI_LOAD_FAILED: "EMOJI_LOAD_FAILED";
14
+ readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
15
+ };
16
+ /**
17
+ * Wraps an async operation with error handling and logging
18
+ */
19
+ export declare function handleAsync<T = unknown>(operation: () => Promise<T>, errorCode: string, errorMessage: string): Promise<T>;
20
+ /**
21
+ * Wraps a sync operation with error handling and logging
22
+ */
23
+ export declare function handleSync<T>(operation: () => T, errorCode: string, errorMessage: string): T;
24
+ /**
25
+ * Wraps multiple async operations with error handling
26
+ */
27
+ export declare function handleAsyncAll<T extends readonly unknown[]>(operations: {
28
+ readonly [K in keyof T]: () => Promise<T[K]>;
29
+ }, errorCode: string, errorMessage: string): Promise<T>;
30
+ /**
31
+ * Validates a response and throws if not ok
32
+ */
33
+ export declare function validateResponse(response: Response, errorCode: string, errorMessage: string): Promise<ArrayBuffer>;
@@ -0,0 +1,71 @@
1
+ import { logger } from "./logger.js";
2
+ export class ImageResponseError extends Error {
3
+ code;
4
+ originalError;
5
+ constructor(message, code, originalError) {
6
+ super(message);
7
+ this.code = code;
8
+ this.originalError = originalError;
9
+ this.name = "ImageResponseError";
10
+ }
11
+ }
12
+ export const ErrorCodes = {
13
+ FONT_LOAD_FAILED: "FONT_LOAD_FAILED",
14
+ VNODE_CREATION_FAILED: "VNODE_CREATION_FAILED",
15
+ SATORI_RENDER_FAILED: "SATORI_RENDER_FAILED",
16
+ RESVG_INIT_FAILED: "RESVG_INIT_FAILED",
17
+ RESVG_RENDER_FAILED: "RESVG_RENDER_FAILED",
18
+ SATORI_INIT_FAILED: "SATORI_INIT_FAILED",
19
+ EMOJI_LOAD_FAILED: "EMOJI_LOAD_FAILED",
20
+ UNKNOWN_ERROR: "UNKNOWN_ERROR",
21
+ };
22
+ /**
23
+ * Wraps an async operation with error handling and logging
24
+ */
25
+ export async function handleAsync(operation, errorCode, errorMessage) {
26
+ try {
27
+ return await operation();
28
+ }
29
+ catch (error) {
30
+ const err = error instanceof Error ? error : new Error(String(error));
31
+ logger.error(`${errorMessage}:`, err.message);
32
+ throw new ImageResponseError(errorMessage, errorCode, err);
33
+ }
34
+ }
35
+ /**
36
+ * Wraps a sync operation with error handling and logging
37
+ */
38
+ export function handleSync(operation, errorCode, errorMessage) {
39
+ try {
40
+ return operation();
41
+ }
42
+ catch (error) {
43
+ const err = error instanceof Error ? error : new Error(String(error));
44
+ logger.error(`${errorMessage}:`, err.message);
45
+ throw new ImageResponseError(errorMessage, errorCode, err);
46
+ }
47
+ }
48
+ /**
49
+ * Wraps multiple async operations with error handling
50
+ */
51
+ export async function handleAsyncAll(operations, errorCode, errorMessage) {
52
+ try {
53
+ return (await Promise.all(operations.map((op) => op())));
54
+ }
55
+ catch (error) {
56
+ const err = error instanceof Error ? error : new Error(String(error));
57
+ logger.error(`${errorMessage}:`, err.message);
58
+ throw new ImageResponseError(errorMessage, errorCode, err);
59
+ }
60
+ }
61
+ /**
62
+ * Validates a response and throws if not ok
63
+ */
64
+ export async function validateResponse(response, errorCode, errorMessage) {
65
+ if (!response.ok) {
66
+ logger.error(`${errorMessage}: HTTP ${response.status} ${response.statusText}`);
67
+ throw new ImageResponseError(`${errorMessage} (HTTP ${response.status})`, errorCode);
68
+ }
69
+ const buffer = await response.arrayBuffer();
70
+ return buffer;
71
+ }
@@ -1,11 +1,14 @@
1
1
  import { render } from "svelte/server";
2
2
  import { html } from "satori-html";
3
+ import { handleSync, ErrorCodes } from "./error-handler.js";
3
4
  function svelteComponentToHTML(component, props = {}) {
4
- const { body, head } = render(component, { props });
5
- return html(body + head);
5
+ return handleSync(() => {
6
+ const { body, head } = render(component, { props });
7
+ return html(body + head);
8
+ }, ErrorCodes.VNODE_CREATION_FAILED, "Failed to render Svelte component to HTML");
6
9
  }
7
10
  export function createVNode(element, componentOptions) {
8
- return typeof element === "string"
11
+ return handleSync(() => typeof element === "string"
9
12
  ? html(element.replaceAll("\n", "").trim())
10
- : svelteComponentToHTML(element, componentOptions?.props);
13
+ : svelteComponentToHTML(element, componentOptions?.props), ErrorCodes.VNODE_CREATION_FAILED, "Failed to create VNode");
11
14
  }
@@ -0,0 +1 @@
1
+ export declare const formatBytes: (bytes: number, decimals?: number) => string;
@@ -0,0 +1,9 @@
1
+ const sizeFormats = ['Bytes', 'KB', 'MB', 'GB'];
2
+ const kbSize = 1024;
3
+ export const formatBytes = (bytes, decimals = 2) => {
4
+ if (bytes === 0)
5
+ return '0 Bytes';
6
+ const decimalPoint = decimals < 0 ? 0 : decimals;
7
+ const sizeIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(kbSize)), 3);
8
+ return parseFloat((bytes / Math.pow(kbSize, sizeIndex)).toFixed(decimalPoint)) + ' ' + sizeFormats[sizeIndex];
9
+ };
@@ -1,6 +1,8 @@
1
1
  import { DEFAULT_OPTIONS, DEFAULT_STATUS_CODE, DEFAULT_STATUS_TEXT } from "./helpers/defaults.js";
2
2
  import { createPng, createSvg } from "./helpers/create.js";
3
3
  import { isDebugEnabled, logger, setDebug } from "./helpers/logger.js";
4
+ import { handleAsync, ImageResponseError, ErrorCodes } from "./helpers/error-handler.js";
5
+ import { formatBytes } from "./helpers/utils.js";
4
6
  export class ImageResponse extends Response {
5
7
  constructor(element, options, props) {
6
8
  const extended_options = Object.assign({ ...DEFAULT_OPTIONS }, options);
@@ -9,11 +11,22 @@ export class ImageResponse extends Response {
9
11
  const create_image_function = extended_options.format === "png" ? createPng : createSvg;
10
12
  const body = new ReadableStream({
11
13
  async start(controller) {
12
- const buffer = await create_image_function(element, extended_options, {
13
- props,
14
- });
15
- controller.enqueue(buffer);
16
- controller.close();
14
+ try {
15
+ const buffer = (await handleAsync(() => create_image_function(element, extended_options, {
16
+ props,
17
+ }), ErrorCodes.UNKNOWN_ERROR, `Failed to generate ${extended_options.format?.toUpperCase()}`));
18
+ logger.debug(buffer.length.toLocaleString());
19
+ logger.info(`Generated ${extended_options.format.toUpperCase()}: ${formatBytes(buffer.length)}`);
20
+ controller.enqueue(buffer);
21
+ controller.close();
22
+ }
23
+ catch (error) {
24
+ const err = error instanceof ImageResponseError
25
+ ? error
26
+ : new ImageResponseError(error instanceof Error ? error.message : String(error), ErrorCodes.UNKNOWN_ERROR, error instanceof Error ? error : new Error(String(error)));
27
+ logger.error("Failed to create image response:", err.message);
28
+ controller.error(err);
29
+ }
17
30
  },
18
31
  });
19
32
  super(body, {
@@ -1,5 +1,6 @@
1
1
  import { isEdgeLight, isWorkerd } from "std-env";
2
2
  import { logger } from "../helpers/logger.js";
3
+ import { handleAsync, ErrorCodes } from "../helpers/error-handler.js";
3
4
  // we keep instances alive to avoid re-importing them on every request, maybe not needed but
4
5
  // also helps with type inference
5
6
  // Code from vue-og-images
@@ -16,9 +17,17 @@ export async function useResvg() {
16
17
  logger.debug("Initializing Resvg WASM");
17
18
  const isWorkerLikeRuntime = isEdgeLight || isWorkerd;
18
19
  logger.info(`Detected runtime: ${isWorkerLikeRuntime ? "Edge Light or Workerd" : "Node.js"}`);
19
- const moduleImport = isWorkerLikeRuntime ? import(`./resvg/edge.js`) : import("./resvg/node.js");
20
- resvgInstance.instance = await moduleImport.then((m) => m.default);
21
- await resvgInstance.instance.initWasmPromise;
20
+ const moduleImport = await handleAsync(async () => {
21
+ if (isWorkerLikeRuntime) {
22
+ return import("./resvg/edge.js");
23
+ }
24
+ return import("./resvg/node.js");
25
+ }, ErrorCodes.RESVG_INIT_FAILED, "Failed to import ReSVG module");
26
+ resvgInstance.instance = await handleAsync(async () => {
27
+ const mod = await moduleImport;
28
+ return mod.default;
29
+ }, ErrorCodes.RESVG_INIT_FAILED, "Failed to load ReSVG default export");
30
+ await handleAsync(async () => resvgInstance.instance.initWasmPromise, ErrorCodes.RESVG_INIT_FAILED, "Failed to initialize ReSVG WASM");
22
31
  return resvgInstance.instance.Resvg;
23
32
  }
24
33
  export async function useSatori() {
@@ -26,7 +35,10 @@ export async function useSatori() {
26
35
  return satoriInstance.instance.satori;
27
36
  }
28
37
  logger.debug("Initializing Satori WASM");
29
- satoriInstance.instance = await import(`./satori/node.js`).then((m) => m.default);
30
- await satoriInstance.instance.initWasmPromise;
38
+ satoriInstance.instance = await handleAsync(async () => {
39
+ const mod = await import("./satori/node.js");
40
+ return mod.default;
41
+ }, ErrorCodes.SATORI_INIT_FAILED, "Failed to load Satori module");
42
+ await handleAsync(async () => satoriInstance.instance.initWasmPromise, ErrorCodes.SATORI_INIT_FAILED, "Failed to initialize Satori WASM");
31
43
  return satoriInstance.instance.satori;
32
44
  }
@@ -4,6 +4,7 @@ export default {
4
4
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5
5
  // @ts-ignore
6
6
  initWasmPromise: initWasm(
7
+ // @ts-ignore
7
8
  import("@resvg/resvg-wasm/index_bg.wasm?module").then((r) => r.default || r)
8
9
  ),
9
10
  Resvg: _Resvg,
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SatoriOptions } from "satori/wasm";
1
+ import type { SatoriOptions } from "satori";
2
2
  import type { EmojiType } from "./helpers/emoji.js";
3
3
  export type Font = SatoriOptions["fonts"][number];
4
4
  export type Fonts = Font[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethercorps/sveltekit-og",
3
- "version": "4.3.0-next.1",
3
+ "version": "4.3.0-next.2",
4
4
  "license": "MIT",
5
5
  "homepage": "https://sveltekit-og.dev",
6
6
  "repository": "github:ethercorps/sveltekit-og",
@@ -97,14 +97,11 @@
97
97
  "peerDependencies": {
98
98
  "@sveltejs/kit": ">=2.0.0"
99
99
  },
100
- "engines": {
101
- "node": ">=22.16.0"
102
- },
103
100
  "scripts": {
104
101
  "dev": "vite dev",
105
102
  "build": "vite build && npm run package",
106
103
  "build:examples": "pnpm -F \"./examples/**\" --parallel --color build",
107
- "check:examples": "pnpm -F \"./examples/**\" --parallel --color i",
104
+ "check:examples": "pnpm install -r",
108
105
  "preview": "vite preview",
109
106
  "package": "svelte-kit sync && svelte-package && publint",
110
107
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",