@ethercorps/sveltekit-og 4.3.0-next.5 → 4.3.0-next.7
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/helpers/error-handler.d.ts +2 -0
- package/dist/helpers/error-handler.js +2 -0
- package/dist/helpers/to-html.d.ts +13 -0
- package/dist/helpers/to-html.js +13 -0
- package/dist/helpers/toJSX.js +2 -2
- package/dist/helpers/utils.d.ts +1 -0
- package/dist/helpers/utils.js +19 -3
- package/dist/providers/instances.js +11 -4
- package/dist/providers/resvg/edge.js +1 -8
- package/dist/providers/satori/edge.d.ts +6 -0
- package/dist/providers/satori/edge.js +16 -0
- package/dist/providers/satori/yoga.wasm +0 -0
- package/dist/providers/takumi/edge.d.ts +6 -0
- package/dist/providers/takumi/edge.js +16 -0
- package/dist/providers/takumi/node.d.ts +6 -0
- package/dist/providers/takumi/node.js +10 -0
- package/dist/providers/takumi/takumi.wasm +0 -0
- package/dist/takumi/fonts.d.ts +26 -0
- package/dist/takumi/fonts.js +18 -0
- package/dist/takumi/image-response.d.ts +9 -0
- package/dist/takumi/image-response.js +62 -0
- package/dist/takumi/index.d.ts +5 -0
- package/dist/takumi/index.js +4 -0
- package/dist/takumi/render.d.ts +7 -0
- package/dist/takumi/render.js +36 -0
- package/dist/takumi/renderer.d.ts +10 -0
- package/dist/takumi/renderer.js +39 -0
- package/dist/takumi/types.d.ts +64 -0
- package/dist/takumi/types.js +1 -0
- package/package.json +130 -113
|
@@ -11,6 +11,8 @@ export declare const ErrorCodes: {
|
|
|
11
11
|
readonly RESVG_RENDER_FAILED: "RESVG_RENDER_FAILED";
|
|
12
12
|
readonly SATORI_INIT_FAILED: "SATORI_INIT_FAILED";
|
|
13
13
|
readonly EMOJI_LOAD_FAILED: "EMOJI_LOAD_FAILED";
|
|
14
|
+
readonly TAKUMI_INIT_FAILED: "TAKUMI_INIT_FAILED";
|
|
15
|
+
readonly TAKUMI_RENDER_FAILED: "TAKUMI_RENDER_FAILED";
|
|
14
16
|
readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
|
|
15
17
|
};
|
|
16
18
|
/**
|
|
@@ -16,6 +16,8 @@ export const ErrorCodes = {
|
|
|
16
16
|
RESVG_RENDER_FAILED: "RESVG_RENDER_FAILED",
|
|
17
17
|
SATORI_INIT_FAILED: "SATORI_INIT_FAILED",
|
|
18
18
|
EMOJI_LOAD_FAILED: "EMOJI_LOAD_FAILED",
|
|
19
|
+
TAKUMI_INIT_FAILED: "TAKUMI_INIT_FAILED",
|
|
20
|
+
TAKUMI_RENDER_FAILED: "TAKUMI_RENDER_FAILED",
|
|
19
21
|
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
|
20
22
|
};
|
|
21
23
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
/**
|
|
3
|
+
* Renders a Svelte component to its server-side HTML parts.
|
|
4
|
+
*
|
|
5
|
+
* Shared by both rendering engines: the satori path feeds `body + head` into
|
|
6
|
+
* `satori-html`, while the takumi path passes an HTML string to its renderer.
|
|
7
|
+
* Components that need their styles inlined should use
|
|
8
|
+
* `<svelte:options css="injected" />` so the CSS lands in `head`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderComponentToHtml(component: Component<any>, props?: Record<string, unknown>): {
|
|
11
|
+
head: string;
|
|
12
|
+
body: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { render } from "svelte/server";
|
|
2
|
+
import { handleSync, ErrorCodes } from "./error-handler.js";
|
|
3
|
+
/**
|
|
4
|
+
* Renders a Svelte component to its server-side HTML parts.
|
|
5
|
+
*
|
|
6
|
+
* Shared by both rendering engines: the satori path feeds `body + head` into
|
|
7
|
+
* `satori-html`, while the takumi path passes an HTML string to its renderer.
|
|
8
|
+
* Components that need their styles inlined should use
|
|
9
|
+
* `<svelte:options css="injected" />` so the CSS lands in `head`.
|
|
10
|
+
*/
|
|
11
|
+
export function renderComponentToHtml(component, props = {}) {
|
|
12
|
+
return handleSync(() => render(component, { props }), ErrorCodes.VNODE_CREATION_FAILED, "Failed to render Svelte component to HTML");
|
|
13
|
+
}
|
package/dist/helpers/toJSX.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { render } from "svelte/server";
|
|
2
1
|
import { html } from "satori-html";
|
|
3
2
|
import { handleSync, ErrorCodes } from "./error-handler.js";
|
|
3
|
+
import { renderComponentToHtml } from "./to-html.js";
|
|
4
4
|
function svelteComponentToHTML(component, props = {}) {
|
|
5
5
|
return handleSync(() => {
|
|
6
|
-
const { body, head } =
|
|
6
|
+
const { body, head } = renderComponentToHtml(component, props);
|
|
7
7
|
return html(body + head);
|
|
8
8
|
}, ErrorCodes.VNODE_CREATION_FAILED, "Failed to render Svelte component to HTML");
|
|
9
9
|
}
|
package/dist/helpers/utils.d.ts
CHANGED
package/dist/helpers/utils.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
const sizeFormats = [
|
|
1
|
+
const sizeFormats = ["Bytes", "KB", "MB", "GB"];
|
|
2
2
|
const kbSize = 1024;
|
|
3
3
|
export const formatBytes = (bytes, decimals = 2) => {
|
|
4
4
|
if (bytes === 0)
|
|
5
|
-
return
|
|
5
|
+
return "0 Bytes";
|
|
6
6
|
const decimalPoint = decimals < 0 ? 0 : decimals;
|
|
7
7
|
const sizeIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(kbSize)), 3);
|
|
8
|
-
return parseFloat((bytes / Math.pow(kbSize, sizeIndex)).toFixed(decimalPoint)) +
|
|
8
|
+
return (parseFloat((bytes / Math.pow(kbSize, sizeIndex)).toFixed(decimalPoint)) +
|
|
9
|
+
" " +
|
|
10
|
+
sizeFormats[sizeIndex]);
|
|
9
11
|
};
|
|
12
|
+
export async function importWasm(input) {
|
|
13
|
+
// may be a nested await for some reason
|
|
14
|
+
const _input = await input;
|
|
15
|
+
const _module = _input.default || _input;
|
|
16
|
+
// this is from rollup/wasm, it does some magic we need to recover from
|
|
17
|
+
if (typeof _module === "function") {
|
|
18
|
+
// empty input is to avoid instantiating the wasm module
|
|
19
|
+
// this will just compile it
|
|
20
|
+
const fnRes = await _module();
|
|
21
|
+
const _instance = fnRes.instance || fnRes;
|
|
22
|
+
return _instance.exports || _instance || _module;
|
|
23
|
+
}
|
|
24
|
+
return _module;
|
|
25
|
+
}
|
|
@@ -15,13 +15,15 @@ export async function useResvg(debug = false) {
|
|
|
15
15
|
if (resvgInstance.instance) {
|
|
16
16
|
return resvgInstance.instance.Resvg;
|
|
17
17
|
}
|
|
18
|
-
log.debug("Initializing
|
|
18
|
+
log.debug("Initializing ReSVG WASM");
|
|
19
19
|
const isWorkerLikeRuntime = isEdgeLight || isWorkerd;
|
|
20
20
|
log.info(`Detected runtime: ${isWorkerLikeRuntime ? "Edge Light or Workerd" : "Node.js"}`);
|
|
21
21
|
// Keep both dynamic imports as direct expressions so the bundler (unwasm/Rollup)
|
|
22
22
|
// can statically analyse them and emit the `?module` wasm chunk correctly.
|
|
23
23
|
// Burying these inside nested async callbacks breaks wasm bundling on Cloudflare.
|
|
24
|
-
const moduleImport = isWorkerLikeRuntime
|
|
24
|
+
const moduleImport = isWorkerLikeRuntime
|
|
25
|
+
? import("./resvg/edge.js")
|
|
26
|
+
: import("./resvg/node.js");
|
|
25
27
|
resvgInstance.instance = await handleAsync(() => moduleImport.then((m) => m.default), ErrorCodes.RESVG_INIT_FAILED, "Failed to import ReSVG module");
|
|
26
28
|
await handleAsync(() => resvgInstance.instance.initWasmPromise, ErrorCodes.RESVG_INIT_FAILED, "Failed to initialize ReSVG WASM");
|
|
27
29
|
return resvgInstance.instance.Resvg;
|
|
@@ -31,8 +33,13 @@ export async function useSatori(debug = false) {
|
|
|
31
33
|
if (satoriInstance.instance) {
|
|
32
34
|
return satoriInstance.instance.satori;
|
|
33
35
|
}
|
|
34
|
-
log.debug("Initializing Satori
|
|
35
|
-
|
|
36
|
+
log.debug("Initializing Satori");
|
|
37
|
+
const isWorkerLikeRuntime = isEdgeLight || isWorkerd;
|
|
38
|
+
log.info(`Detected runtime: ${isWorkerLikeRuntime ? "Edge Light or Workerd" : "Node.js"}`);
|
|
39
|
+
const moduleImport = isWorkerLikeRuntime
|
|
40
|
+
? import("./satori/edge.js")
|
|
41
|
+
: import("./satori/node.js");
|
|
42
|
+
satoriInstance.instance = await handleAsync(() => moduleImport.then((m) => m.default), ErrorCodes.SATORI_INIT_FAILED, "Failed to import Satori module");
|
|
36
43
|
await handleAsync(() => satoriInstance.instance.initWasmPromise, ErrorCodes.SATORI_INIT_FAILED, "Failed to initialize Satori WASM");
|
|
37
44
|
return satoriInstance.instance.satori;
|
|
38
45
|
}
|
|
@@ -3,13 +3,6 @@ import { Resvg as _Resvg, initWasm } from "@resvg/resvg-wasm";
|
|
|
3
3
|
export default {
|
|
4
4
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
5
5
|
// @ts-ignore
|
|
6
|
-
initWasmPromise: initWasm(
|
|
7
|
-
// @ts-ignore
|
|
8
|
-
// Vendored wasm: a relative import resolves inside this package so the
|
|
9
|
-
// consumer's bundler emits a real CompiledWasm module. A bare
|
|
10
|
-
// "@resvg/resvg-wasm/...?module" specifier falls back to runtime byte
|
|
11
|
-
// compilation, which Cloudflare Workers block.
|
|
12
|
-
import("./resvg.wasm?module").then((r) => r.default || r)
|
|
13
|
-
),
|
|
6
|
+
initWasmPromise: initWasm(import("./resvg.wasm?module").then((r) => r.default || r)),
|
|
14
7
|
Resvg: _Resvg,
|
|
15
8
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import _satori, { init } from "satori/standalone";
|
|
2
|
+
|
|
3
|
+
// On worker-like runtimes (Cloudflare Workers, Edge) the default `satori` entry
|
|
4
|
+
// fails: it bundles yoga's layout wasm as base64 and compiles it at runtime,
|
|
5
|
+
// which Workers block ("Wasm code generation disallowed by embedder").
|
|
6
|
+
//
|
|
7
|
+
// `satori/standalone` exposes `init()` so we can hand it a pre-compiled
|
|
8
|
+
// WebAssembly.Module instead. Importing the vendored yoga.wasm with `?module`
|
|
9
|
+
// makes the consumer bundler emit a real CompiledWasm module, so no runtime
|
|
10
|
+
// byte compilation happens. Mirrors providers/resvg/edge.js.
|
|
11
|
+
export default {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
initWasmPromise: init(import("./yoga.wasm?module").then((r) => r.default || r)),
|
|
15
|
+
satori: _satori,
|
|
16
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { init, Renderer } from "takumi-js/wasm";
|
|
2
|
+
|
|
3
|
+
// Worker-like runtimes (Cloudflare Workers, Vercel Edge) can't compile WASM at
|
|
4
|
+
// runtime, so we hand the wasm-bindgen `init` a pre-compiled module. Importing
|
|
5
|
+
// the vendored takumi.wasm with `?module` makes the consumer bundler emit a real
|
|
6
|
+
// CompiledWasm module — mirrors providers/resvg/edge.js and providers/satori/edge.js.
|
|
7
|
+
export default {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
initWasmPromise: init({
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
module_or_path: import("./takumi.wasm?module").then((r) => r.default || r),
|
|
14
|
+
}),
|
|
15
|
+
Renderer,
|
|
16
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Renderer } from "takumi-js/node";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native Node.js backend (@takumi-rs/core). The renderer is a native addon, so
|
|
5
|
+
* there is no WASM to initialize — construction is synchronous.
|
|
6
|
+
* */
|
|
7
|
+
export default {
|
|
8
|
+
initWasmPromise: Promise.resolve(),
|
|
9
|
+
Renderer,
|
|
10
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FontDetails } from "takumi-js/node";
|
|
2
|
+
import { BaseFont } from "../fonts.js";
|
|
3
|
+
import type { MayBePromise } from "../types.js";
|
|
4
|
+
/** Font bytes Takumi accepts for registration. */
|
|
5
|
+
type ByteBuf = Uint8Array | ArrayBuffer | Buffer;
|
|
6
|
+
/**
|
|
7
|
+
* A Takumi-native font descriptor. `data` may be the bytes directly or a lazy
|
|
8
|
+
* loader returning them — matching `takumi-js`'s own font option shape.
|
|
9
|
+
*/
|
|
10
|
+
export interface TakumiFontDescriptor {
|
|
11
|
+
name?: string;
|
|
12
|
+
data: ByteBuf | (() => MayBePromise<ByteBuf>);
|
|
13
|
+
weight?: number;
|
|
14
|
+
style?: FontDetails["style"];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Accepted font inputs on the Takumi path: this library's `GoogleFont` /
|
|
18
|
+
* `CustomFont` helpers (any `BaseFont`) or a raw Takumi descriptor.
|
|
19
|
+
*/
|
|
20
|
+
export type TakumiFontInput = BaseFont | TakumiFontDescriptor;
|
|
21
|
+
/**
|
|
22
|
+
* Resolves mixed font inputs into Takumi `FontDetails` ready for
|
|
23
|
+
* `renderer.registerFont`. Loaders run in parallel.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveTakumiFonts(fonts: TakumiFontInput[]): Promise<FontDetails[]>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseFont } from "../fonts.js";
|
|
2
|
+
import { handleAsync, ErrorCodes } from "../helpers/error-handler.js";
|
|
3
|
+
async function normalizeFont(font) {
|
|
4
|
+
if (font instanceof BaseFont) {
|
|
5
|
+
// GoogleFont/CustomFont: the `data` getter lazily loads (and caches) bytes.
|
|
6
|
+
const data = (await font.data);
|
|
7
|
+
return { name: font.name, data, weight: font.weight, style: font.style };
|
|
8
|
+
}
|
|
9
|
+
const data = typeof font.data === "function" ? await font.data() : await font.data;
|
|
10
|
+
return { name: font.name, data, weight: font.weight, style: font.style };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolves mixed font inputs into Takumi `FontDetails` ready for
|
|
14
|
+
* `renderer.registerFont`. Loaders run in parallel.
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveTakumiFonts(fonts) {
|
|
17
|
+
return handleAsync(() => Promise.all(fonts.map(normalizeFont)), ErrorCodes.FONT_LOAD_FAILED, "Failed to resolve fonts for Takumi");
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Component, ComponentProps } from "svelte";
|
|
2
|
+
import type { TakumiImageResponseOptions } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generates an Open Graph image with the Takumi engine and returns it as a
|
|
5
|
+
* `Response`. Accepts an HTML string or a Svelte component (with props).
|
|
6
|
+
*/
|
|
7
|
+
export declare class ImageResponse<T extends string | Component<any>> extends Response {
|
|
8
|
+
constructor(element: T, options?: TakumiImageResponseOptions, props?: T extends Component<any> ? ComponentProps<T> : never);
|
|
9
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createTakumiImage } from "./render.js";
|
|
2
|
+
import { createLogger } from "../helpers/logger.js";
|
|
3
|
+
import { handleAsync, ImageResponseError, ErrorCodes } from "../helpers/error-handler.js";
|
|
4
|
+
import { formatBytes } from "../helpers/utils.js";
|
|
5
|
+
const DEFAULT_OPTIONS = {
|
|
6
|
+
width: 1200,
|
|
7
|
+
height: 630,
|
|
8
|
+
format: "png",
|
|
9
|
+
emoji: "twemoji",
|
|
10
|
+
debug: false,
|
|
11
|
+
};
|
|
12
|
+
const CONTENT_TYPES = {
|
|
13
|
+
png: "image/png",
|
|
14
|
+
jpeg: "image/jpeg",
|
|
15
|
+
webp: "image/webp",
|
|
16
|
+
ico: "image/x-icon",
|
|
17
|
+
raw: "application/octet-stream",
|
|
18
|
+
svg: "image/svg+xml",
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Generates an Open Graph image with the Takumi engine and returns it as a
|
|
22
|
+
* `Response`. Accepts an HTML string or a Svelte component (with props).
|
|
23
|
+
*/
|
|
24
|
+
export class ImageResponse extends Response {
|
|
25
|
+
constructor(element, options, props) {
|
|
26
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
27
|
+
const log = createLogger(opts.debug);
|
|
28
|
+
log.debug("Takumi ImageResponse created");
|
|
29
|
+
const format = opts.format;
|
|
30
|
+
const body = new ReadableStream({
|
|
31
|
+
async start(controller) {
|
|
32
|
+
try {
|
|
33
|
+
const output = await handleAsync(() => createTakumiImage(element, opts, props), ErrorCodes.UNKNOWN_ERROR, `Failed to generate ${format.toUpperCase()}`);
|
|
34
|
+
// renderSvg returns a string; raster formats return bytes. A
|
|
35
|
+
// Response body stream must emit Uint8Array chunks.
|
|
36
|
+
const bytes = typeof output === "string" ? new TextEncoder().encode(output) : output;
|
|
37
|
+
log.info(`Generated ${format.toUpperCase()}: ${formatBytes(bytes.byteLength)}`);
|
|
38
|
+
controller.enqueue(bytes);
|
|
39
|
+
controller.close();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const err = error instanceof ImageResponseError
|
|
43
|
+
? error
|
|
44
|
+
: new ImageResponseError(error instanceof Error ? error.message : String(error), ErrorCodes.UNKNOWN_ERROR, error instanceof Error ? error : new Error(String(error)));
|
|
45
|
+
log.error("Failed to create Takumi image response:", err.message);
|
|
46
|
+
controller.error(err);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
super(body, {
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": CONTENT_TYPES[format],
|
|
53
|
+
"Cache-Control": opts.debug
|
|
54
|
+
? "no-cache, no-store"
|
|
55
|
+
: "public, immutable, no-transform, max-age=31536000",
|
|
56
|
+
...opts.headers,
|
|
57
|
+
},
|
|
58
|
+
status: opts.status || 200,
|
|
59
|
+
statusText: opts.statusText || "Success",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ImageResponse } from "./image-response.js";
|
|
2
|
+
export type { TakumiImageResponseOptions, TakumiImageOptions, TakumiResponseOptions, TakumiFormat, } from "./types.js";
|
|
3
|
+
export type { TakumiFontInput, TakumiFontDescriptor } from "./fonts.js";
|
|
4
|
+
export { resolveTakumiFonts } from "./fonts.js";
|
|
5
|
+
export { GoogleFont, CustomFont, loadGoogleFont } from "../fonts.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { TakumiImageOptions } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Renders an HTML string or Svelte component to image bytes (or an SVG string)
|
|
5
|
+
* using a cached, font-registered Takumi renderer for the current runtime.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createTakumiImage(element: string | Component, options: TakumiImageOptions, props?: Record<string, unknown>): Promise<Uint8Array | string>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { render as takumiRender, renderSvg } from "takumi-js";
|
|
2
|
+
import { renderComponentToHtml } from "../helpers/to-html.js";
|
|
3
|
+
import { useTakumiRenderer, registerTakumiFonts } from "./renderer.js";
|
|
4
|
+
import { resolveTakumiFonts } from "./fonts.js";
|
|
5
|
+
import { createLogger } from "../helpers/logger.js";
|
|
6
|
+
import { handleAsync, handleSync, ErrorCodes } from "../helpers/error-handler.js";
|
|
7
|
+
function elementToHtml(element, props) {
|
|
8
|
+
if (typeof element === "string")
|
|
9
|
+
return element.replaceAll("\n", "").trim();
|
|
10
|
+
// Takumi reads inline `style`/`<style>`; `head` carries CSS injected via
|
|
11
|
+
// `<svelte:options css="injected" />`, so it goes first.
|
|
12
|
+
const { head, body } = renderComponentToHtml(element, props);
|
|
13
|
+
return head + body;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Renders an HTML string or Svelte component to image bytes (or an SVG string)
|
|
17
|
+
* using a cached, font-registered Takumi renderer for the current runtime.
|
|
18
|
+
*/
|
|
19
|
+
export async function createTakumiImage(element, options, props) {
|
|
20
|
+
const log = createLogger(options.debug ?? false);
|
|
21
|
+
const html = handleSync(() => elementToHtml(element, props), ErrorCodes.VNODE_CREATION_FAILED, "Failed to create HTML for Takumi");
|
|
22
|
+
const renderer = await useTakumiRenderer(options.debug);
|
|
23
|
+
if (options.fonts?.length) {
|
|
24
|
+
const fonts = await resolveTakumiFonts(options.fonts);
|
|
25
|
+
await registerTakumiFonts(renderer, fonts);
|
|
26
|
+
}
|
|
27
|
+
const { width, height, format = "png", quality, stylesheets, emoji } = options;
|
|
28
|
+
// `renderer` is typed as the native Renderer; on edge it's the wasm Renderer,
|
|
29
|
+
// which is structurally compatible for takumi-js's managed render.
|
|
30
|
+
const shared = { renderer: renderer, width, height, stylesheets, emoji };
|
|
31
|
+
log.debug(`Rendering ${format.toUpperCase()} with Takumi`);
|
|
32
|
+
if (format === "svg") {
|
|
33
|
+
return handleAsync(() => renderSvg(html, shared), ErrorCodes.TAKUMI_RENDER_FAILED, "Failed to render SVG with Takumi");
|
|
34
|
+
}
|
|
35
|
+
return handleAsync(() => takumiRender(html, { ...shared, format, quality }), ErrorCodes.TAKUMI_RENDER_FAILED, "Failed to render image with Takumi");
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Renderer as NodeRenderer, FontDetails } from "takumi-js/node";
|
|
2
|
+
/** Either backend's Renderer; both expose `render`/`renderSvg`/`registerFont`. */
|
|
3
|
+
export type TakumiRenderer = NodeRenderer;
|
|
4
|
+
/** Lazily creates and caches the Takumi renderer for the current runtime. */
|
|
5
|
+
export declare function useTakumiRenderer(debug?: boolean): Promise<TakumiRenderer>;
|
|
6
|
+
/**
|
|
7
|
+
* Registers each font on the renderer once. Keyed by name/weight/style so the
|
|
8
|
+
* same face isn't re-registered across requests.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerTakumiFonts(renderer: TakumiRenderer, fonts: FontDetails[]): Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { isEdgeLight, isWorkerd } from "std-env";
|
|
2
|
+
import { createLogger } from "../helpers/logger.js";
|
|
3
|
+
import { handleAsync, ErrorCodes } from "../helpers/error-handler.js";
|
|
4
|
+
// Keep one renderer alive across requests (mirrors satori/resvg instance reuse
|
|
5
|
+
// and Takumi's own global renderer). Fonts registered on it accumulate, so we
|
|
6
|
+
// track which have already been registered to avoid duplicate work.
|
|
7
|
+
let rendererPromise;
|
|
8
|
+
const registeredFontKeys = new Set();
|
|
9
|
+
async function initRenderer(debug) {
|
|
10
|
+
const log = createLogger(debug);
|
|
11
|
+
const isWorkerLikeRuntime = isEdgeLight || isWorkerd;
|
|
12
|
+
log.info(`Detected runtime: ${isWorkerLikeRuntime ? "Edge Light or Workerd" : "Node.js"}`);
|
|
13
|
+
// Keep the imports as direct ternary expressions so the bundler can statically
|
|
14
|
+
// emit the wasm chunk for the edge build (see providers/takumi/edge.js).
|
|
15
|
+
const moduleImport = isWorkerLikeRuntime
|
|
16
|
+
? import("../providers/takumi/edge.js")
|
|
17
|
+
: import("../providers/takumi/node.js");
|
|
18
|
+
const provider = (await handleAsync(() => moduleImport.then((m) => m.default), ErrorCodes.TAKUMI_INIT_FAILED, "Failed to import Takumi renderer module"));
|
|
19
|
+
await handleAsync(() => provider.initWasmPromise, ErrorCodes.TAKUMI_INIT_FAILED, "Failed to initialize Takumi WASM");
|
|
20
|
+
return new provider.Renderer();
|
|
21
|
+
}
|
|
22
|
+
/** Lazily creates and caches the Takumi renderer for the current runtime. */
|
|
23
|
+
export async function useTakumiRenderer(debug = false) {
|
|
24
|
+
rendererPromise ??= initRenderer(debug);
|
|
25
|
+
return rendererPromise;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Registers each font on the renderer once. Keyed by name/weight/style so the
|
|
29
|
+
* same face isn't re-registered across requests.
|
|
30
|
+
*/
|
|
31
|
+
export async function registerTakumiFonts(renderer, fonts) {
|
|
32
|
+
for (const font of fonts) {
|
|
33
|
+
const key = `${font.name ?? "unnamed"}-${font.weight ?? "auto"}-${font.style ?? "normal"}`;
|
|
34
|
+
if (registeredFontKeys.has(key))
|
|
35
|
+
continue;
|
|
36
|
+
await handleAsync(() => renderer.registerFont(font), ErrorCodes.FONT_LOAD_FAILED, `Failed to register Takumi font: ${key}`);
|
|
37
|
+
registeredFontKeys.add(key);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { EmojiType } from "takumi-js/helpers/emoji";
|
|
2
|
+
import type { OutputFormat } from "takumi-js/node";
|
|
3
|
+
import type { TakumiFontInput } from "./fonts.js";
|
|
4
|
+
/** Static output formats Takumi can encode, plus vector `svg`. */
|
|
5
|
+
export type TakumiFormat = OutputFormat | "svg";
|
|
6
|
+
export type TakumiImageOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* Width of the image.
|
|
9
|
+
* @default 1200
|
|
10
|
+
* */
|
|
11
|
+
width?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Height of the image.
|
|
14
|
+
* @default 630
|
|
15
|
+
* */
|
|
16
|
+
height?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Output format.
|
|
19
|
+
* @default png
|
|
20
|
+
* */
|
|
21
|
+
format?: TakumiFormat;
|
|
22
|
+
/**
|
|
23
|
+
* Quality for lossy formats (jpeg, lossy webp), 0-100.
|
|
24
|
+
* */
|
|
25
|
+
quality?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Fonts to register. Accepts this library's `GoogleFont`/`CustomFont`
|
|
28
|
+
* helpers or raw Takumi font descriptors. If omitted, Takumi's built-in
|
|
29
|
+
* sans-serif is used.
|
|
30
|
+
* */
|
|
31
|
+
fonts?: TakumiFontInput[];
|
|
32
|
+
/**
|
|
33
|
+
* Extra CSS stylesheets applied before rendering.
|
|
34
|
+
* */
|
|
35
|
+
stylesheets?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Emoji provider, or `"from-font"` to source emoji glyphs from loaded fonts.
|
|
38
|
+
* @default twemoji
|
|
39
|
+
* */
|
|
40
|
+
emoji?: EmojiType | "from-font";
|
|
41
|
+
/**
|
|
42
|
+
* Enable debug logging.
|
|
43
|
+
* @default false
|
|
44
|
+
* */
|
|
45
|
+
debug?: boolean;
|
|
46
|
+
};
|
|
47
|
+
export type TakumiResponseOptions = {
|
|
48
|
+
/**
|
|
49
|
+
* Response status code.
|
|
50
|
+
* @default 200
|
|
51
|
+
* */
|
|
52
|
+
status?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Response status text.
|
|
55
|
+
* @default Success
|
|
56
|
+
* */
|
|
57
|
+
statusText?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Response headers.
|
|
60
|
+
* */
|
|
61
|
+
headers?: Record<string, string>;
|
|
62
|
+
};
|
|
63
|
+
/** Options for the Takumi `ImageResponse`. */
|
|
64
|
+
export type TakumiImageResponseOptions = TakumiImageOptions & TakumiResponseOptions;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,114 +1,131 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
2
|
+
"name": "@ethercorps/sveltekit-og",
|
|
3
|
+
"version": "4.3.0-next.7",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"homepage": "https://sveltekit-og.dev",
|
|
6
|
+
"repository": "github:ethercorps/sveltekit-og",
|
|
7
|
+
"funding": "https://github.com/sponsors/ethercorps",
|
|
8
|
+
"author": "Shivam Meena <https://github.com/theetherGit>",
|
|
9
|
+
"description": "Dynamically generate Open Graph images from an HTML, CSS template or Svelte component using fast and efficient conversion from HTML > SVG > PNG",
|
|
10
|
+
"contributors": [
|
|
11
|
+
{
|
|
12
|
+
"name": "Shivam Meena",
|
|
13
|
+
"github": "https://github.com/theetherGit"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "Jason",
|
|
17
|
+
"github": "https://github.com/jasongitmail"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "Mihkel Martin Kasterpalu",
|
|
21
|
+
"github": "https://github.com/MihkelMK"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "Luke Parke",
|
|
25
|
+
"github": "https://github.com/LukasParke"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "Willow (GHOST)",
|
|
29
|
+
"github": "https://github.com/ghostdevv"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "Minseo Lee",
|
|
33
|
+
"github": "https://github.com/quiple"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "vite dev",
|
|
38
|
+
"build": "vite build && npm run package",
|
|
39
|
+
"build:examples": "pnpm -F \"./examples/**\" --parallel --color build",
|
|
40
|
+
"check:examples": "pnpm install -r",
|
|
41
|
+
"preview": "vite preview",
|
|
42
|
+
"package": "svelte-kit sync && svelte-package && publint",
|
|
43
|
+
"prepublishOnly": "pnpm run package",
|
|
44
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
45
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
46
|
+
"test": "vitest",
|
|
47
|
+
"bench": "vitest bench --run",
|
|
48
|
+
"bench:save": "vitest bench --run --outputJson benchmarks/baseline.json",
|
|
49
|
+
"bench:compare": "vitest bench --run --compare benchmarks/baseline.json",
|
|
50
|
+
"bench:snapshot": "node scripts/bench-snapshot.mjs",
|
|
51
|
+
"lint": "oxlint . && eslint .",
|
|
52
|
+
"format": "prettier --plugin-search-dir . --write .",
|
|
53
|
+
"publishBeta": "npm publish --tag beta"
|
|
54
|
+
},
|
|
55
|
+
"exports": {
|
|
56
|
+
".": {
|
|
57
|
+
"types": "./dist/index.d.ts",
|
|
58
|
+
"svelte": "./dist/index.js",
|
|
59
|
+
"import": "./dist/index.js"
|
|
60
|
+
},
|
|
61
|
+
"./fonts": {
|
|
62
|
+
"types": "./dist/fonts.d.ts",
|
|
63
|
+
"import": "./dist/fonts.js"
|
|
64
|
+
},
|
|
65
|
+
"./takumi": {
|
|
66
|
+
"types": "./dist/takumi/index.d.ts",
|
|
67
|
+
"svelte": "./dist/takumi/index.js",
|
|
68
|
+
"import": "./dist/takumi/index.js"
|
|
69
|
+
},
|
|
70
|
+
"./plugin": {
|
|
71
|
+
"types": "./dist/plugin.d.ts",
|
|
72
|
+
"import": "./dist/plugin.js"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"files": [
|
|
76
|
+
"dist",
|
|
77
|
+
"!dist/**/*.test.*",
|
|
78
|
+
"!dist/**/*.spec.*"
|
|
79
|
+
],
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@eslint/compat": "^2.0.3",
|
|
82
|
+
"@eslint/js": "^9.39.3",
|
|
83
|
+
"@sveltejs/adapter-vercel": "^5.10.3",
|
|
84
|
+
"@sveltejs/kit": "^2.53.4",
|
|
85
|
+
"@sveltejs/package": "^2.5.7",
|
|
86
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
|
87
|
+
"@types/node": "^24.11.0",
|
|
88
|
+
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
|
89
|
+
"@typescript-eslint/parser": "^5.62.0",
|
|
90
|
+
"css-tree": "^2.3.1",
|
|
91
|
+
"eslint": "^8.57.1",
|
|
92
|
+
"eslint-config-prettier": "^8.10.2",
|
|
93
|
+
"eslint-plugin-oxlint": "^1.55.0",
|
|
94
|
+
"eslint-plugin-svelte": "^2.46.1",
|
|
95
|
+
"globals": "^16.5.0",
|
|
96
|
+
"oxlint": "^1.55.0",
|
|
97
|
+
"prettier": "^3.8.1",
|
|
98
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
99
|
+
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
100
|
+
"publint": "^0.1.16",
|
|
101
|
+
"rollup-plugin-visualizer": "^5.14.0",
|
|
102
|
+
"svelte": "^5.53.10",
|
|
103
|
+
"svelte-check": "^4.4.5",
|
|
104
|
+
"tslib": "^2.8.1",
|
|
105
|
+
"typescript": "^5.9.3",
|
|
106
|
+
"typescript-eslint": "^8.57.0",
|
|
107
|
+
"vite": "^5.4.21",
|
|
108
|
+
"takumi-js": "2.0.0-beta.14",
|
|
109
|
+
"vitest": "^1.6.1"
|
|
110
|
+
},
|
|
111
|
+
"main": "./dist/index.js",
|
|
112
|
+
"svelte": "./dist/index.js",
|
|
113
|
+
"types": "./dist/index.d.ts",
|
|
114
|
+
"type": "module",
|
|
115
|
+
"dependencies": {
|
|
116
|
+
"@resvg/resvg-wasm": "^2.6.2",
|
|
117
|
+
"satori": "^0.25.0",
|
|
118
|
+
"satori-html": "0.3.2",
|
|
119
|
+
"std-env": "^4.1.0",
|
|
120
|
+
"unwasm": "^0.5.3"
|
|
121
|
+
},
|
|
122
|
+
"peerDependencies": {
|
|
123
|
+
"@sveltejs/kit": ">=2.0.0",
|
|
124
|
+
"takumi-js": "^2.0.0-beta.14"
|
|
125
|
+
},
|
|
126
|
+
"peerDependenciesMeta": {
|
|
127
|
+
"takumi-js": {
|
|
128
|
+
"optional": true
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|