@ethercorps/sveltekit-og 4.2.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.
- package/README.md +27 -193
- package/dist/fonts.d.ts +5 -13
- package/dist/fonts.js +31 -24
- package/dist/helpers/create.d.ts +2 -2
- package/dist/helpers/create.js +27 -27
- package/dist/helpers/defaults.d.ts +4 -4
- package/dist/helpers/defaults.js +18 -17
- package/dist/helpers/emoji.js +30 -15
- package/dist/helpers/error-handler.d.ts +33 -0
- package/dist/helpers/error-handler.js +71 -0
- package/dist/helpers/logger.d.ts +15 -0
- package/dist/helpers/logger.js +39 -0
- package/dist/helpers/toJSX.d.ts +2 -2
- package/dist/helpers/toJSX.js +10 -5
- package/dist/helpers/utils.d.ts +1 -0
- package/dist/helpers/utils.js +9 -0
- package/dist/image-response.d.ts +2 -2
- package/dist/image-response.js +31 -13
- package/dist/plugin.js +6 -6
- package/dist/providers/instances.d.ts +1 -1
- package/dist/providers/instances.js +35 -8
- package/dist/providers/resvg/edge.d.ts +1 -1
- package/dist/providers/resvg/edge.js +8 -5
- package/dist/providers/resvg/node.d.ts +1 -1
- package/dist/providers/resvg/node.js +3 -3
- package/dist/providers/satori/node.d.ts +1 -1
- package/dist/providers/satori/node.js +4 -4
- package/dist/types.d.ts +10 -10
- package/package.json +50 -18
- package/dist/providers/resvg/resvg.wasm +0 -0
package/README.md
CHANGED
|
@@ -6,8 +6,24 @@
|
|
|
6
6
|
|
|
7
7
|
Dynamically generate Open Graph images from an HTML+CSS template or Svelte component using fast and efficient conversion from HTML > SVG > PNG. Based on [Satori](https://github.com/vercel/satori#documentation). No headless browser required.
|
|
8
8
|
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [SvelteKit Open Graph Image Generation](#sveltekit-open-graph-image-generation)
|
|
12
|
+
- [Table of Contents](#table-of-contents)
|
|
13
|
+
- [Docs](#docs)
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Usage](#usage)
|
|
16
|
+
- [Examples](#examples)
|
|
17
|
+
- [Contributing](#contributing)
|
|
18
|
+
- [Changelog](#changelog)
|
|
19
|
+
- [License](#license)
|
|
20
|
+
- [Acknowledgements](#acknowledgements)
|
|
21
|
+
- [Authors](#authors)
|
|
22
|
+
- [Contributors](#contributors)
|
|
23
|
+
|
|
9
24
|
## Docs
|
|
10
|
-
|
|
25
|
+
|
|
26
|
+
For more detailed information and advanced usage, please refer to the [official documentation](https://sveltekit-og.dev).
|
|
11
27
|
|
|
12
28
|
## Installation
|
|
13
29
|
|
|
@@ -17,210 +33,28 @@ pnpm install @ethercorps/sveltekit-og
|
|
|
17
33
|
|
|
18
34
|
## Usage
|
|
19
35
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- Add vite plugin
|
|
23
|
-
|
|
24
|
-
```typescript title="vite.cofig.js"
|
|
25
|
-
import { sveltekit } from '@sveltejs/kit/vite';
|
|
26
|
-
import { sveltekitOG } from '@ethercorps/sveltekit-og/plugin';
|
|
27
|
-
const config = {
|
|
28
|
-
plugins: [sveltekit(), sveltekitOG()]
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export default config;
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### Rollup (will be deprecated in v5)
|
|
35
|
-
- Add `rollupWasm` to `build.rollupOptions.plugins` in `vite.cofig.js` file.
|
|
36
|
-
- For more information, check [docs](https://sveltekit-og.dev/docs/getting-started)
|
|
37
|
-
|
|
38
|
-
```ts title="vite.cofig.js"
|
|
39
|
-
import { sveltekit } from '@sveltejs/kit/vite';
|
|
40
|
-
import { defineConfig } from 'vitest/config';
|
|
41
|
-
import { rollupWasm } from '@ethercorps/sveltekit-og/plugin';
|
|
42
|
-
|
|
43
|
-
export default defineConfig({
|
|
44
|
-
plugins: [sveltekit()],
|
|
45
|
-
build: {
|
|
46
|
-
rollupOptions: {
|
|
47
|
-
plugins: [rollupWasm()],
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
- For node adapter update config with `rollupWasm`
|
|
54
|
-
- Check node runtime [docs](https://sveltekit-og.dev/docs/runtime/node)
|
|
55
|
-
|
|
56
|
-
```ts title="vite.cofig.js"
|
|
57
|
-
import { sveltekit } from '@sveltejs/kit/vite';
|
|
58
|
-
import { defineConfig } from 'vitest/config';
|
|
59
|
-
import { rollupWasm } from '@ethercorps/sveltekit-og/plugin';
|
|
60
|
-
|
|
61
|
-
export default defineConfig({
|
|
62
|
-
plugins: [sveltekit()],
|
|
63
|
-
build: {
|
|
64
|
-
rollupOptions: {
|
|
65
|
-
plugins: [
|
|
66
|
-
rollupWasm({ esmImport: false })
|
|
67
|
-
],
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
- Create a file at `/src/routes/og/+server.ts`. Alternatively, you can use JavaScript by removing the types from this example.
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// src/routes/og/+server.ts
|
|
77
|
-
import { ImageResponse } from '@ethercorps/sveltekit-og';
|
|
78
|
-
import { RequestHandler } from './$types';
|
|
79
|
-
|
|
80
|
-
const template = `
|
|
81
|
-
<div tw="bg-gray-50 flex w-full h-full items-center justify-center">
|
|
82
|
-
<div tw="flex flex-col md:flex-row w-full py-12 px-4 md:items-center justify-between p-8">
|
|
83
|
-
<h2 tw="flex flex-col text-3xl sm:text-4xl font-bold tracking-tight text-gray-900 text-left">
|
|
84
|
-
<span>Ready to dive in?</span>
|
|
85
|
-
<span tw="text-indigo-600">Start your free trial today.</span>
|
|
86
|
-
</h2>
|
|
87
|
-
<div tw="mt-8 flex md:mt-0">
|
|
88
|
-
<div tw="flex rounded-md shadow">
|
|
89
|
-
<a href="#" tw="flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-5 py-3 text-base font-medium text-white">Get started</a>
|
|
90
|
-
</div>
|
|
91
|
-
<div tw="ml-3 flex rounded-md shadow">
|
|
92
|
-
<a href="#" tw="flex items-center justify-center rounded-md border border-transparent bg-white px-5 py-3 text-base font-medium text-indigo-600">Learn more</a>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
`;
|
|
98
|
-
|
|
99
|
-
const fontFile = await fetch('https://og-playground.vercel.app/inter-latin-ext-400-normal.woff');
|
|
100
|
-
const fontData: ArrayBuffer = await fontFile.arrayBuffer();
|
|
101
|
-
|
|
102
|
-
export const GET: RequestHandler = async () => {
|
|
103
|
-
return await new ImageResponse(template, {
|
|
104
|
-
height: 630,
|
|
105
|
-
width: 1200,
|
|
106
|
-
fonts: [
|
|
107
|
-
{
|
|
108
|
-
name: 'Inter Latin',
|
|
109
|
-
data: fontData,
|
|
110
|
-
weight: 400
|
|
111
|
-
}
|
|
112
|
-
]
|
|
113
|
-
});
|
|
114
|
-
};
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Then run `npm dev` and visit `localhost:5173/og` to view your generated PNG. Remember that hot module reloading does not work with server routes, so if you change your HTML or CSS, hard refresh the route to see changes.
|
|
118
|
-
|
|
119
|
-
## Example Output
|
|
120
|
-
|
|
121
|
-

|
|
122
|
-
|
|
123
|
-
## Headers
|
|
124
|
-
|
|
125
|
-
When run in development, image headers contain `cache-control: no-cache, no-store`. In production, image headers contain `'cache-control': 'public, immutable, no-transform, max-age=31536000'`, which caches the image for 1 year. In both cases, the `'content-type': 'image/png'` is used.
|
|
126
|
-
|
|
127
|
-
## Styling
|
|
128
|
-
|
|
129
|
-
Notice that our example uses TailwindCSS classes (e.g. `tw="bg-gray-50"`). Alternatively, your HTML can contain style attributes using any of [the subset of CSS supported by Satori](https://github.com/vercel/satori#css).
|
|
130
|
-
|
|
131
|
-
Satori supports only a subset of HTML and CSS. For full details, see [Satori’s documentation](https://github.com/vercel/satori#documentation). Notably, Satori only supports flex-based layouts.
|
|
132
|
-
|
|
133
|
-
## Fonts
|
|
134
|
-
|
|
135
|
-
Satori supports `ttf`, `otf`, and `woff` font formats; `woff2` is not supported. To maximize the font parsing speed, `ttf` or `otf` are recommended over `woff`.
|
|
136
|
-
|
|
137
|
-
By default, `@ethercorps/sveltekit-og` includes only 'Noto Sans' font. If you need to use other fonts, you can specify them as shown in the example. Notably, you can also import a font file that is stored locally within your project and are not required to use fetch.
|
|
36
|
+
For detailed usage instructions, please see the [Getting Started](https://sveltekit-og.dev/docs/getting-started) section of our documentation.
|
|
138
37
|
|
|
139
38
|
## Examples
|
|
140
39
|
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
## API Reference
|
|
145
|
-
|
|
146
|
-
The package exposes an `ImageResponse` constructors, with the following options available:
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
import {ImageResponse} from '@ethercorps/sveltekit-og'
|
|
150
|
-
import {SvelteComponent} from "svelte";
|
|
151
|
-
|
|
152
|
-
ImageResponse(
|
|
153
|
-
element : string | Component,
|
|
154
|
-
options : {
|
|
155
|
-
width ? : number = 1200
|
|
156
|
-
height ? : number = 630,
|
|
157
|
-
backgroundColor ? : string = "#fff"
|
|
158
|
-
fonts ? : {
|
|
159
|
-
name: string,
|
|
160
|
-
data: ArrayBuffer,
|
|
161
|
-
weight: number,
|
|
162
|
-
style: 'normal' | 'italic'
|
|
163
|
-
}[]
|
|
164
|
-
debug ? : boolean = false
|
|
165
|
-
// Options that will be passed to the HTTP response
|
|
166
|
-
status ? : number = 200
|
|
167
|
-
statusText ? : string
|
|
168
|
-
headers ? : Record<string, string>
|
|
169
|
-
},
|
|
170
|
-
// Component props if components.
|
|
171
|
-
ComponentProps<Component>
|
|
172
|
-
)
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Changelog
|
|
176
|
-
|
|
177
|
-
### v4.0.0 (Breaking Changes)
|
|
178
|
-
|
|
179
|
-
> Just install @ethercorps/sveltekit-og
|
|
180
|
-
|
|
181
|
-
> Support for NodeJS, Deno, Cloudflare Pages, Cloudflare Workers, Vercel and Netlify.
|
|
182
|
-
|
|
183
|
-
> No support for Bun tried and failed.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
### v3.0.0 (Breaking Changes)
|
|
187
|
-
|
|
188
|
-
> Just install @ethercorps/sveltekit-og
|
|
189
|
-
> No wasm as of now, only support for nodejs based runtime.
|
|
40
|
+
- **ImageResponse**: [_source_](/src/routes/+server.ts) · [_demo_](https://vercel.sveltekit-og.dev)
|
|
41
|
+
- **Component Rendering**: [_source_](/src/routes/sc/+server.ts) · [_demo_](https://vercel.sveltekit-og.dev/sc)
|
|
190
42
|
|
|
191
|
-
|
|
43
|
+
## Contributing
|
|
192
44
|
|
|
193
|
-
|
|
45
|
+
Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) to get started.
|
|
194
46
|
|
|
195
|
-
|
|
196
|
-
npm i @resvg/resvg-js
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
```
|
|
200
|
-
npm i satori
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
> From now on their will be no issues related to build, and soon this library going to have its own documentation.
|
|
204
|
-
|
|
205
|
-
### v1.2.2 Update (Breaking Change)
|
|
206
|
-
|
|
207
|
-
- We don't provide access to satori from `@ethercorps/sveltekit-og`.
|
|
208
|
-
|
|
209
|
-
### v1.0.0 Update (Breaking Changes)
|
|
47
|
+
## Changelog
|
|
210
48
|
|
|
211
|
-
|
|
212
|
-
Now you can use `{ toReactElement }` with `"@ethercorps/sveltekit-og"` like:
|
|
49
|
+
All notable changes to this project are documented in the [changelog](CHANGELOG.md).
|
|
213
50
|
|
|
214
|
-
|
|
215
|
-
- Removed `@resvg/resvg-wasm` with `@resvg/resvg-js` because of internal errors.
|
|
216
|
-
- Removed `satori-html` because now we have `toReactElement` out of the box with svelte compiler.
|
|
217
|
-
> If you find a problem related to undefined a please check [_vite.config.js_](/vite.config.ts) and add ` define: { _a: 'undefined' } in config.`
|
|
51
|
+
## License
|
|
218
52
|
|
|
219
|
-
|
|
53
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
220
54
|
|
|
221
55
|
## Acknowledgements
|
|
222
56
|
|
|
223
|
-
This project
|
|
57
|
+
This project would not be possible without the following projects:
|
|
224
58
|
|
|
225
59
|
- [Satori & @vercel/og](https://github.com/vercel/satori)
|
|
226
60
|
- [Noto by Google Fonts](https://fonts.google.com/noto)
|
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,32 +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
|
-
/** * A helper class to load Custom fonts, typically from local files.
|
|
21
|
-
* The input must be a function provided by the user (e.g., using $app/server/read).
|
|
22
|
-
*/
|
|
23
19
|
export declare class CustomFont extends BaseFont {
|
|
20
|
+
protected input: MayBePromise<Buffer | ArrayBuffer> | (() => MayBePromise<Buffer | ArrayBuffer>);
|
|
24
21
|
private promise?;
|
|
25
|
-
/**
|
|
26
|
-
* Creates an instance of CustomFont.
|
|
27
|
-
* @param name The name of the font (for CSS font-family).
|
|
28
|
-
* @param input Font data as ArrayBuffer or a function that resolves to ArrayBuffer (user must provide the loading logic).
|
|
29
|
-
*/
|
|
30
22
|
constructor(name: string, input: MayBePromise<Buffer | ArrayBuffer> | (() => MayBePromise<Buffer | ArrayBuffer>), options?: BaseFontOptions);
|
|
31
|
-
/** A promise which resolves to font data as `ArrayBuffer` (Lazy load) */
|
|
23
|
+
/** A promise which resolves to font data as `ArrayBuffer` (Lazy load and CACHED) */
|
|
32
24
|
get data(): Promise<Buffer | ArrayBuffer>;
|
|
33
25
|
}
|
|
34
26
|
/** Loads Google font ArrayBuffer with caching. */
|
|
35
|
-
export declare const loadGoogleFont: (family: string, { text, weight, style, display }?: {
|
|
27
|
+
export declare const loadGoogleFont: (family: string, { text, weight, style, display, }?: {
|
|
36
28
|
text?: string;
|
|
37
29
|
weight?: string | number;
|
|
38
30
|
style?: FontStyle;
|
package/dist/fonts.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { FONT_CACHE_MAP } from
|
|
1
|
+
import { FONT_CACHE_MAP } from "./helpers/cache.js";
|
|
2
|
+
import { logger } from "./helpers/logger.js";
|
|
2
3
|
/** * Base font class defining the structure required by Satori.
|
|
3
4
|
* All font types inherit from this class.
|
|
4
5
|
*/
|
|
5
6
|
export class BaseFont {
|
|
6
|
-
input;
|
|
7
7
|
name;
|
|
8
8
|
style;
|
|
9
9
|
weight;
|
|
10
|
-
constructor(name,
|
|
11
|
-
this.input = input;
|
|
10
|
+
constructor(name, { weight = 400, style = "normal" } = {}) {
|
|
12
11
|
this.name = name;
|
|
13
12
|
this.style = style;
|
|
14
13
|
this.weight = weight;
|
|
@@ -17,43 +16,47 @@ export class BaseFont {
|
|
|
17
16
|
* Overridden by CustomFont and GoogleFont for lazy loading.
|
|
18
17
|
*/
|
|
19
18
|
get data() {
|
|
20
|
-
|
|
19
|
+
throw new Error("The 'data' getter must be implemented by subclasses of BaseFont.");
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
|
-
/** * A helper class to load Custom fonts, typically from local files.
|
|
24
|
-
* The input must be a function provided by the user (e.g., using $app/server/read).
|
|
25
|
-
*/
|
|
26
22
|
export class CustomFont extends BaseFont {
|
|
23
|
+
input;
|
|
27
24
|
promise;
|
|
28
|
-
/**
|
|
29
|
-
* Creates an instance of CustomFont.
|
|
30
|
-
* @param name The name of the font (for CSS font-family).
|
|
31
|
-
* @param input Font data as ArrayBuffer or a function that resolves to ArrayBuffer (user must provide the loading logic).
|
|
32
|
-
*/
|
|
33
25
|
constructor(name, input, options) {
|
|
34
|
-
super(name,
|
|
26
|
+
super(name, options);
|
|
27
|
+
this.input = input;
|
|
35
28
|
}
|
|
36
|
-
/** A promise which resolves to font data as `ArrayBuffer` (Lazy load) */
|
|
29
|
+
/** A promise which resolves to font data as `ArrayBuffer` (Lazy load and CACHED) */
|
|
37
30
|
get data() {
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
31
|
+
const cacheKey = `${this.name}-${this.weight}-${this.style}`;
|
|
32
|
+
const cachedData = FONT_CACHE_MAP.get(cacheKey);
|
|
33
|
+
if (cachedData) {
|
|
34
|
+
return Promise.resolve(cachedData);
|
|
35
|
+
}
|
|
36
|
+
const fallback = async () => {
|
|
37
|
+
const buffer = typeof this.input === "function" ? this.input() : this.input;
|
|
38
|
+
const resolvedBuffer = await buffer;
|
|
39
|
+
FONT_CACHE_MAP.set(cacheKey, resolvedBuffer);
|
|
40
|
+
return resolvedBuffer;
|
|
41
|
+
};
|
|
41
42
|
this.promise = this.promise?.then(null, fallback) ?? fallback();
|
|
42
43
|
return this.promise;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
/** Constructs Google font css url */
|
|
46
|
-
const constructGoogleFontCssUrl = (family, { text, weight = 400, style =
|
|
47
|
+
const constructGoogleFontCssUrl = (family, { text, weight = 400, style = "normal", display, } = {}) => {
|
|
47
48
|
// Logic to build the URL (e.g., https://fonts.googleapis.com/css2?family=...wght@...)
|
|
48
49
|
const params = {
|
|
49
|
-
family: `${family.replaceAll(
|
|
50
|
+
family: `${family.replaceAll(" ", "+")}:${style === "italic" ? "ital," : ""}wght@${style === "italic" ? "1," : ""}${weight}`,
|
|
50
51
|
};
|
|
51
52
|
if (text)
|
|
52
53
|
params.text = encodeURIComponent(text);
|
|
53
|
-
return `https://fonts.googleapis.com/css2?${Object.keys(params)
|
|
54
|
+
return `https://fonts.googleapis.com/css2?${Object.keys(params)
|
|
55
|
+
.map((key) => `${key}=${params[key]}`)
|
|
56
|
+
.join("&")}`;
|
|
54
57
|
};
|
|
55
58
|
/** Loads Google font ArrayBuffer with caching. */
|
|
56
|
-
export const loadGoogleFont = async (family, { text, weight = 400, style =
|
|
59
|
+
export const loadGoogleFont = async (family, { text, weight = 400, style = "normal", display, } = {}) => {
|
|
57
60
|
const cssUrl = constructGoogleFontCssUrl(family, { text, weight, display, style });
|
|
58
61
|
const fromMap = FONT_CACHE_MAP.get(cssUrl);
|
|
59
62
|
if (fromMap) {
|
|
@@ -61,22 +64,26 @@ export const loadGoogleFont = async (family, { text, weight = 400, style = 'norm
|
|
|
61
64
|
}
|
|
62
65
|
const cssResponse = await fetch(cssUrl);
|
|
63
66
|
if (!cssResponse.ok) {
|
|
67
|
+
logger.error(`Failed to fetch Google Font CSS for ${family}. Status: ${cssResponse.status}`);
|
|
64
68
|
throw new Error(`Failed to fetch Google Font CSS for ${family}. Status: ${cssResponse.status}`);
|
|
65
69
|
}
|
|
66
70
|
const css = await cssResponse.text();
|
|
67
71
|
// 3. Extract the font file URL (the actual TTF/OTF file)
|
|
68
72
|
const fontUrl = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)?.[1];
|
|
69
73
|
if (!fontUrl) {
|
|
74
|
+
logger.error(`Could not find a compatible truetype font source in the CSS for ${family}.`);
|
|
70
75
|
throw new Error(`Could not find a compatible truetype font source in the CSS for ${family}.`);
|
|
71
76
|
}
|
|
72
77
|
// 4. Fetch the font buffer
|
|
73
78
|
const fontResponse = await fetch(fontUrl);
|
|
74
79
|
if (!fontResponse.ok) {
|
|
80
|
+
logger.error(`Failed to fetch font file from URL. Status: ${fontResponse.status}`);
|
|
75
81
|
throw new Error(`Failed to fetch font file from URL. Status: ${fontResponse.status}`);
|
|
76
82
|
}
|
|
77
83
|
const buffer = await fontResponse.arrayBuffer();
|
|
78
84
|
// 5. CACHE AND RETURN: Store the resolved ArrayBuffer in the Map, keyed by the CSS URL.
|
|
79
85
|
FONT_CACHE_MAP.set(cssUrl, buffer);
|
|
86
|
+
logger.debug(`Loaded Google Font: ${family}`);
|
|
80
87
|
return buffer;
|
|
81
88
|
};
|
|
82
89
|
export class GoogleFont extends BaseFont {
|
|
@@ -84,7 +91,7 @@ export class GoogleFont extends BaseFont {
|
|
|
84
91
|
text;
|
|
85
92
|
promise;
|
|
86
93
|
constructor(family, options = {}) {
|
|
87
|
-
super(options.name || family,
|
|
94
|
+
super(options.name || family, options);
|
|
88
95
|
this.family = family;
|
|
89
96
|
this.text = options.text;
|
|
90
97
|
}
|
|
@@ -108,5 +115,5 @@ export async function resolveFonts(fontClasses) {
|
|
|
108
115
|
style: fontClass.style,
|
|
109
116
|
};
|
|
110
117
|
}));
|
|
111
|
-
return resolvedFonts.filter(font => font !== null);
|
|
118
|
+
return resolvedFonts.filter((font) => font !== null);
|
|
112
119
|
}
|
package/dist/helpers/create.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Component } from
|
|
2
|
-
import type { ComponentOptions, ImageOptions } from
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { ComponentOptions, ImageOptions } from "../types.js";
|
|
3
3
|
export declare function createSvg(element: string | Component, imageOptions: ImageOptions, componentOptions?: ComponentOptions): Promise<string>;
|
|
4
4
|
export declare function createPng(element: string | Component, imageOptions: ImageOptions, componentOptions?: ComponentOptions): Promise<Uint8Array<ArrayBufferLike>>;
|
package/dist/helpers/create.js
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
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 { logger } from "./logger.js";
|
|
6
|
+
import { handleAsyncAll, handleAsync, ErrorCodes } from "./error-handler.js";
|
|
5
7
|
export async function createSvg(element, imageOptions, componentOptions) {
|
|
6
|
-
const [satori, vnodes] = await
|
|
8
|
+
const [satori, vnodes] = await handleAsyncAll([() => useSatori(), () => Promise.resolve(createVNode(element, componentOptions))], ErrorCodes.SATORI_RENDER_FAILED, "Failed to initialize Satori or create VNode");
|
|
7
9
|
const satoriOptions = structuredClone(imageOptions);
|
|
8
|
-
if (!Object.hasOwn(satoriOptions,
|
|
9
|
-
satoriOptions[
|
|
10
|
+
if (!Object.hasOwn(satoriOptions, "fonts")) {
|
|
11
|
+
satoriOptions["fonts"] = await handleAsync(() => default_fonts(), ErrorCodes.FONT_LOAD_FAILED, "Failed to load default fonts for Satori");
|
|
10
12
|
}
|
|
11
|
-
satoriOptions[
|
|
12
|
-
emoji: imageOptions.emoji
|
|
13
|
+
satoriOptions["loadAdditionalAsset"] = loadDynamicAsset({
|
|
14
|
+
emoji: imageOptions.emoji,
|
|
13
15
|
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return satori(vnodes, satoriOptions);
|
|
16
|
+
logger.debug("Generating SVG with Satori");
|
|
17
|
+
logger.info("VNode provided to satori:", JSON.stringify(vnodes, null, 2), "\n");
|
|
18
|
+
logger.info("Options provided to satori:", imageOptions);
|
|
19
|
+
return handleAsync(() => satori(vnodes, satoriOptions), ErrorCodes.SATORI_RENDER_FAILED, "Failed to render SVG with Satori");
|
|
19
20
|
}
|
|
20
21
|
export async function createPng(element, imageOptions, componentOptions) {
|
|
21
|
-
const svg = await createSvg(element, imageOptions, componentOptions);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
const resvg_instance = await useResvg();
|
|
22
|
+
const svg = await handleAsync(() => createSvg(element, imageOptions, componentOptions), ErrorCodes.SATORI_RENDER_FAILED, "Failed to create SVG for PNG rendering");
|
|
23
|
+
logger.debug("SVG generated by satori for ReSVG: \n", svg, "\n");
|
|
24
|
+
const resvg_instance = await handleAsync(() => useResvg(), ErrorCodes.RESVG_INIT_FAILED, "Failed to initialize ReSVG");
|
|
26
25
|
const resvg_options = {
|
|
27
26
|
fitTo: {
|
|
28
|
-
mode:
|
|
27
|
+
mode: "width",
|
|
29
28
|
value: imageOptions.width || DEFAULT_WIDTH,
|
|
30
|
-
}
|
|
29
|
+
},
|
|
31
30
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
logger.debug("Rendering PNG with ReSVG");
|
|
32
|
+
logger.info("Options provided to ReSVG:", resvg_options, "\n");
|
|
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,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,38 @@
|
|
|
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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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");
|
|
10
11
|
return [
|
|
11
12
|
{
|
|
12
13
|
data: noto_sans_regular_font,
|
|
13
|
-
name:
|
|
14
|
+
name: "Inter",
|
|
14
15
|
weight: 400,
|
|
15
|
-
style:
|
|
16
|
+
style: "normal",
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
19
|
data: noto_sans_bold_font,
|
|
19
|
-
name:
|
|
20
|
+
name: "Inter",
|
|
20
21
|
weight: 700,
|
|
21
|
-
style:
|
|
22
|
-
}
|
|
22
|
+
style: "normal",
|
|
23
|
+
},
|
|
23
24
|
];
|
|
24
25
|
}
|
|
25
|
-
export const DEFAULT_FORMAT =
|
|
26
|
+
export const DEFAULT_FORMAT = "png";
|
|
26
27
|
export const DEFAULT_WIDTH = 1200;
|
|
27
28
|
export const DEFAULT_HEIGHT = 630;
|
|
28
|
-
export const DEFAULT_EMOJI_PROVIDER =
|
|
29
|
+
export const DEFAULT_EMOJI_PROVIDER = "twemoji";
|
|
29
30
|
export const DEFAULT_STATUS_CODE = 200;
|
|
30
|
-
export const DEFAULT_STATUS_TEXT =
|
|
31
|
+
export const DEFAULT_STATUS_TEXT = "Success";
|
|
31
32
|
export const DEFAULT_OPTIONS = {
|
|
32
33
|
height: DEFAULT_HEIGHT,
|
|
33
34
|
width: DEFAULT_WIDTH,
|
|
34
35
|
debug: false,
|
|
35
36
|
format: DEFAULT_FORMAT,
|
|
36
|
-
emoji:
|
|
37
|
+
emoji: "twemoji",
|
|
37
38
|
};
|
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,27 +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
|
const fn = 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
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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set the debug flag for the current request context
|
|
3
|
+
* Call this once at the beginning of your request handler
|
|
4
|
+
*/
|
|
5
|
+
export declare function setDebug(enabled: boolean): void;
|
|
6
|
+
/**
|
|
7
|
+
* Get the current debug flag from the request context
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDebugEnabled(): boolean;
|
|
10
|
+
export declare const logger: {
|
|
11
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
12
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
13
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
14
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const PREFIX = '[SvelteKit-OG]';
|
|
3
|
+
// Store for request-scoped debug flag
|
|
4
|
+
const debugStorage = new AsyncLocalStorage();
|
|
5
|
+
/**
|
|
6
|
+
* Set the debug flag for the current request context
|
|
7
|
+
* Call this once at the beginning of your request handler
|
|
8
|
+
*/
|
|
9
|
+
export function setDebug(enabled) {
|
|
10
|
+
debugStorage.enterWith(enabled);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get the current debug flag from the request context
|
|
14
|
+
*/
|
|
15
|
+
export function isDebugEnabled() {
|
|
16
|
+
return debugStorage.getStore() ?? false;
|
|
17
|
+
}
|
|
18
|
+
export const logger = {
|
|
19
|
+
debug: (message, ...args) => {
|
|
20
|
+
if (isDebugEnabled()) {
|
|
21
|
+
console.log(`${PREFIX} 🔍 ${message}`, ...args);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
info: (message, ...args) => {
|
|
25
|
+
if (isDebugEnabled()) {
|
|
26
|
+
console.info(`${PREFIX} ℹ️ ${message}`, ...args);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
warn: (message, ...args) => {
|
|
30
|
+
if (isDebugEnabled()) {
|
|
31
|
+
console.warn(`${PREFIX} ⚠️ ${message}`, ...args);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
error: (message, ...args) => {
|
|
35
|
+
if (isDebugEnabled()) {
|
|
36
|
+
console.error(`${PREFIX} ❌ ${message}`, ...args);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
package/dist/helpers/toJSX.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { Component } from
|
|
2
|
-
import type { ComponentOptions, VNode } from
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { ComponentOptions, VNode } from "../types.js";
|
|
3
3
|
export declare function createVNode(element: string | Component, componentOptions?: ComponentOptions): VNode;
|
package/dist/helpers/toJSX.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { render } from
|
|
2
|
-
import { html } from
|
|
1
|
+
import { render } from "svelte/server";
|
|
2
|
+
import { html } from "satori-html";
|
|
3
|
+
import { handleSync, ErrorCodes } from "./error-handler.js";
|
|
3
4
|
function svelteComponentToHTML(component, props = {}) {
|
|
4
|
-
|
|
5
|
-
|
|
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 ===
|
|
11
|
+
return handleSync(() => typeof element === "string"
|
|
12
|
+
? html(element.replaceAll("\n", "").trim())
|
|
13
|
+
: svelteComponentToHTML(element, componentOptions?.props), ErrorCodes.VNODE_CREATION_FAILED, "Failed to create VNode");
|
|
9
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
|
+
};
|
package/dist/image-response.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Component, ComponentProps } from
|
|
2
|
-
import type { ImageResponseOptions } from
|
|
1
|
+
import type { Component, ComponentProps } from "svelte";
|
|
2
|
+
import type { ImageResponseOptions } from "./types.js";
|
|
3
3
|
export declare class ImageResponse<T extends string | Component<any>> extends Response {
|
|
4
4
|
constructor(element: T, options?: ImageResponseOptions, props?: T extends Component<any> ? ComponentProps<T> : never);
|
|
5
5
|
}
|
package/dist/image-response.js
CHANGED
|
@@ -1,26 +1,44 @@
|
|
|
1
|
-
import { DEFAULT_OPTIONS, DEFAULT_STATUS_CODE, DEFAULT_STATUS_TEXT } from
|
|
2
|
-
import { createPng, createSvg } from
|
|
1
|
+
import { DEFAULT_OPTIONS, DEFAULT_STATUS_CODE, DEFAULT_STATUS_TEXT } from "./helpers/defaults.js";
|
|
2
|
+
import { createPng, createSvg } from "./helpers/create.js";
|
|
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";
|
|
3
6
|
export class ImageResponse extends Response {
|
|
4
7
|
constructor(element, options, props) {
|
|
5
8
|
const extended_options = Object.assign({ ...DEFAULT_OPTIONS }, options);
|
|
6
|
-
|
|
9
|
+
setDebug(extended_options.debug ?? false);
|
|
10
|
+
logger.debug("Debug mode", isDebugEnabled());
|
|
11
|
+
const create_image_function = extended_options.format === "png" ? createPng : createSvg;
|
|
7
12
|
const body = new ReadableStream({
|
|
8
13
|
async start(controller) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
|
30
|
+
},
|
|
13
31
|
});
|
|
14
32
|
super(body, {
|
|
15
33
|
headers: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
?
|
|
19
|
-
:
|
|
20
|
-
...extended_options.headers
|
|
34
|
+
"Content-Type": `image/${extended_options.format}${extended_options.format === "svg" ? "+xml" : ""}`,
|
|
35
|
+
"Cache-Control": extended_options.debug
|
|
36
|
+
? "no-cache, no-store"
|
|
37
|
+
: "public, immutable, no-transform, max-age=31536000",
|
|
38
|
+
...extended_options.headers,
|
|
21
39
|
},
|
|
22
40
|
status: extended_options.status || DEFAULT_STATUS_CODE,
|
|
23
|
-
statusText: extended_options.statusText || DEFAULT_STATUS_TEXT
|
|
41
|
+
statusText: extended_options.statusText || DEFAULT_STATUS_TEXT,
|
|
24
42
|
});
|
|
25
43
|
}
|
|
26
44
|
}
|
package/dist/plugin.js
CHANGED
|
@@ -3,20 +3,20 @@ export function rollupWasm(options) {
|
|
|
3
3
|
return unwasm({
|
|
4
4
|
esmImport: true,
|
|
5
5
|
lazy: true,
|
|
6
|
-
...options
|
|
6
|
+
...options,
|
|
7
7
|
});
|
|
8
8
|
}
|
|
9
9
|
export function sveltekitOG(options) {
|
|
10
10
|
return {
|
|
11
|
-
name:
|
|
11
|
+
name: "vite-plugin-sveltekit-og",
|
|
12
12
|
config() {
|
|
13
13
|
return {
|
|
14
14
|
build: {
|
|
15
15
|
rollupOptions: {
|
|
16
|
-
plugins: [rollupWasm(options)]
|
|
17
|
-
}
|
|
18
|
-
}
|
|
16
|
+
plugins: [rollupWasm(options)],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
19
|
};
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
21
|
};
|
|
22
22
|
}
|
|
@@ -1,17 +1,44 @@
|
|
|
1
|
-
import { isEdgeLight, isWorkerd } from
|
|
1
|
+
import { isEdgeLight, isWorkerd } from "std-env";
|
|
2
|
+
import { logger } from "../helpers/logger.js";
|
|
3
|
+
import { handleAsync, ErrorCodes } from "../helpers/error-handler.js";
|
|
2
4
|
// we keep instances alive to avoid re-importing them on every request, maybe not needed but
|
|
3
5
|
// also helps with type inference
|
|
4
6
|
// Code from vue-og-images
|
|
5
|
-
const resvgInstance = {
|
|
6
|
-
|
|
7
|
+
const resvgInstance = {
|
|
8
|
+
instance: undefined,
|
|
9
|
+
};
|
|
10
|
+
const satoriInstance = {
|
|
11
|
+
instance: undefined,
|
|
12
|
+
};
|
|
7
13
|
export async function useResvg() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
if (resvgInstance.instance) {
|
|
15
|
+
return resvgInstance.instance.Resvg;
|
|
16
|
+
}
|
|
17
|
+
logger.debug("Initializing Resvg WASM");
|
|
18
|
+
const isWorkerLikeRuntime = isEdgeLight || isWorkerd;
|
|
19
|
+
logger.info(`Detected runtime: ${isWorkerLikeRuntime ? "Edge Light or Workerd" : "Node.js"}`);
|
|
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");
|
|
11
31
|
return resvgInstance.instance.Resvg;
|
|
12
32
|
}
|
|
13
33
|
export async function useSatori() {
|
|
14
|
-
|
|
15
|
-
|
|
34
|
+
if (satoriInstance.instance) {
|
|
35
|
+
return satoriInstance.instance.satori;
|
|
36
|
+
}
|
|
37
|
+
logger.debug("Initializing Satori WASM");
|
|
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");
|
|
16
43
|
return satoriInstance.instance.satori;
|
|
17
44
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { Resvg as _Resvg, initWasm } from
|
|
1
|
+
import { Resvg as _Resvg, initWasm } from "@resvg/resvg-wasm";
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
initWasmPromise: initWasm(
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import("@resvg/resvg-wasm/index_bg.wasm?module").then((r) => r.default || r)
|
|
9
|
+
),
|
|
10
|
+
Resvg: _Resvg,
|
|
11
|
+
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { Resvg as _Resvg, initWasm } from
|
|
1
|
+
import { Resvg as _Resvg, initWasm } from "@resvg/resvg-wasm";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Fetch will be called only once whenever you load this file.
|
|
5
5
|
* In vercel serverless functions, fetch will run on cold start.
|
|
6
6
|
* In Node.js (Stateful e.g. Linux servers), Fetch will run once when you start your server.
|
|
7
7
|
* */
|
|
8
|
-
const resvgWasm = fetch(
|
|
8
|
+
const resvgWasm = fetch("https://unpkg.com/@resvg/resvg-wasm/index_bg.wasm");
|
|
9
9
|
|
|
10
10
|
export default {
|
|
11
11
|
initWasmPromise: initWasm(resvgWasm),
|
|
12
12
|
Resvg: _Resvg,
|
|
13
|
-
}
|
|
13
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { SatoriOptions } from
|
|
2
|
-
import type { EmojiType } from
|
|
3
|
-
export type Font = SatoriOptions[
|
|
1
|
+
import type { SatoriOptions } from "satori";
|
|
2
|
+
import type { EmojiType } from "./helpers/emoji.js";
|
|
3
|
+
export type Font = SatoriOptions["fonts"][number];
|
|
4
4
|
export type Fonts = Font[];
|
|
5
|
-
export type FontStyle = Font[
|
|
6
|
-
export type FontWeight = Font[
|
|
5
|
+
export type FontStyle = Font["style"];
|
|
6
|
+
export type FontWeight = Font["weight"];
|
|
7
7
|
export type FinalFontOptions = NonNullable<Fonts>;
|
|
8
8
|
export type ImageOptions = {
|
|
9
9
|
/**
|
|
@@ -30,7 +30,7 @@ export type ImageOptions = {
|
|
|
30
30
|
* Tailwind config
|
|
31
31
|
* @default provided by satori
|
|
32
32
|
* */
|
|
33
|
-
tailwindConfig?: SatoriOptions[
|
|
33
|
+
tailwindConfig?: SatoriOptions["tailwindConfig"];
|
|
34
34
|
/**
|
|
35
35
|
* Debug operations
|
|
36
36
|
* @default false
|
|
@@ -40,7 +40,7 @@ export type ImageOptions = {
|
|
|
40
40
|
* Image format
|
|
41
41
|
* @default png
|
|
42
42
|
* */
|
|
43
|
-
format?:
|
|
43
|
+
format?: "svg" | "png";
|
|
44
44
|
};
|
|
45
45
|
export type ResponseImageOptions = {
|
|
46
46
|
/**
|
|
@@ -70,7 +70,7 @@ export type ImageResponseOptions = ImageOptions & ResponseImageOptions;
|
|
|
70
70
|
* Svelte Component props to render the component which dynamic content
|
|
71
71
|
* */
|
|
72
72
|
export type ComponentOptions = {
|
|
73
|
-
props?: Record<string,
|
|
73
|
+
props?: Record<string, unknown>;
|
|
74
74
|
};
|
|
75
75
|
/**
|
|
76
76
|
* React virtual node, supported by satori as input (alternative to JSX input).
|
|
@@ -78,9 +78,9 @@ export type ComponentOptions = {
|
|
|
78
78
|
export interface VNode {
|
|
79
79
|
type: string;
|
|
80
80
|
props: {
|
|
81
|
-
style?: Record<string,
|
|
81
|
+
style?: Record<string, unknown>;
|
|
82
82
|
children?: string | VNode | VNode[];
|
|
83
|
-
[prop: string]:
|
|
83
|
+
[prop: string]: unknown;
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
/** utils types */
|
package/package.json
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethercorps/sveltekit-og",
|
|
3
|
-
"version": "4.2
|
|
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",
|
|
7
7
|
"funding": "https://github.com/sponsors/ethercorps",
|
|
8
8
|
"author": "Shivam Meena <https://github.com/theetherGit>",
|
|
9
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
|
+
],
|
|
10
36
|
"exports": {
|
|
11
37
|
".": {
|
|
12
38
|
"types": "./dist/index.d.ts",
|
|
@@ -28,26 +54,33 @@
|
|
|
28
54
|
"!dist/**/*.spec.*"
|
|
29
55
|
],
|
|
30
56
|
"devDependencies": {
|
|
31
|
-
"@
|
|
32
|
-
"@
|
|
33
|
-
"@sveltejs/
|
|
57
|
+
"@eslint/compat": "^2.0.3",
|
|
58
|
+
"@eslint/js": "^9.39.3",
|
|
59
|
+
"@sveltejs/adapter-vercel": "^5.10.3",
|
|
60
|
+
"@sveltejs/kit": "^2.53.4",
|
|
61
|
+
"@sveltejs/package": "^2.5.7",
|
|
34
62
|
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
|
35
|
-
"@types/node": "^24.
|
|
63
|
+
"@types/node": "^24.11.0",
|
|
36
64
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
|
37
65
|
"@typescript-eslint/parser": "^5.62.0",
|
|
38
66
|
"css-tree": "^2.3.1",
|
|
39
67
|
"eslint": "^8.57.1",
|
|
40
|
-
"eslint-config-prettier": "^8.10.
|
|
68
|
+
"eslint-config-prettier": "^8.10.2",
|
|
69
|
+
"eslint-plugin-oxlint": "^1.55.0",
|
|
41
70
|
"eslint-plugin-svelte": "^2.46.1",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
71
|
+
"globals": "^16.5.0",
|
|
72
|
+
"oxlint": "^1.55.0",
|
|
73
|
+
"prettier": "^3.8.1",
|
|
74
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
75
|
+
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
44
76
|
"publint": "^0.1.16",
|
|
45
77
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
46
|
-
"svelte": "^5.
|
|
47
|
-
"svelte-check": "^4.
|
|
78
|
+
"svelte": "^5.53.10",
|
|
79
|
+
"svelte-check": "^4.4.5",
|
|
48
80
|
"tslib": "^2.8.1",
|
|
49
81
|
"typescript": "^5.9.3",
|
|
50
|
-
"
|
|
82
|
+
"typescript-eslint": "^8.57.0",
|
|
83
|
+
"vite": "^5.4.21",
|
|
51
84
|
"vitest": "^1.6.1"
|
|
52
85
|
},
|
|
53
86
|
"main": "./dist/index.js",
|
|
@@ -56,13 +89,10 @@
|
|
|
56
89
|
"type": "module",
|
|
57
90
|
"dependencies": {
|
|
58
91
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
59
|
-
"
|
|
60
|
-
"@takumi-rs/image-response": "^0.55.0",
|
|
61
|
-
"@takumi-rs/wasm": "^0.55.0",
|
|
62
|
-
"satori": "^0.10.14",
|
|
92
|
+
"satori": "^0.25.0",
|
|
63
93
|
"satori-html": "0.3.2",
|
|
64
|
-
"std-env": "^3.
|
|
65
|
-
"unwasm": "^0.5.
|
|
94
|
+
"std-env": "^3.10.0",
|
|
95
|
+
"unwasm": "^0.5.3"
|
|
66
96
|
},
|
|
67
97
|
"peerDependencies": {
|
|
68
98
|
"@sveltejs/kit": ">=2.0.0"
|
|
@@ -70,12 +100,14 @@
|
|
|
70
100
|
"scripts": {
|
|
71
101
|
"dev": "vite dev",
|
|
72
102
|
"build": "vite build && npm run package",
|
|
103
|
+
"build:examples": "pnpm -F \"./examples/**\" --parallel --color build",
|
|
104
|
+
"check:examples": "pnpm install -r",
|
|
73
105
|
"preview": "vite preview",
|
|
74
106
|
"package": "svelte-kit sync && svelte-package && publint",
|
|
75
107
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
76
108
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
77
109
|
"test": "vitest",
|
|
78
|
-
"lint": "
|
|
110
|
+
"lint": "oxlint . && eslint .",
|
|
79
111
|
"format": "prettier --plugin-search-dir . --write .",
|
|
80
112
|
"publishBeta": "npm publish --tag beta"
|
|
81
113
|
}
|
|
Binary file
|