@takumi-rs/image-response 0.29.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 ADDED
@@ -0,0 +1,73 @@
1
+ # @takumi-rs/image-response
2
+
3
+ A universal `ImageResponse` implementation for Takumi in Next.js and other environments.
4
+
5
+ Checkout the migration guide [From Next.js ImageResponse](https://takumi.kane.tw/docs/migration/migrate-from-image-response) for more details.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @takumi-rs/image-response @takumi-rs/core @takumi-rs/helpers
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```tsx
16
+ import ImageResponse from "@takumi-rs/image-response";
17
+
18
+ export function GET(request: Request) {
19
+ return new ImageResponse(<OgImage />, {
20
+ width: 1200,
21
+ height: 630,
22
+ format: "webp",
23
+ headers: {
24
+ "Cache-Control": "public, immutable, max-age=31536000",
25
+ },
26
+ });
27
+ }
28
+ ```
29
+
30
+ ### Fonts
31
+
32
+ Takumi comes with full axis [Geist](https://vercel.com/font) and Geist Mono by default.
33
+
34
+ We have global fonts cache to avoid loading the same fonts multiple times.
35
+
36
+ If your environment supports top-level await, you can load the fonts in global scope and reuse the fonts array.
37
+
38
+ ```tsx
39
+ const fonts = [
40
+ {
41
+ name: "Inter",
42
+ data: await fetch("/fonts/Inter-Regular.ttf").then((res) => res.arrayBuffer()),
43
+ style: "normal",
44
+ weight: 400,
45
+ },
46
+ ];
47
+
48
+ new ImageResponse(<OgImage />, { fonts });
49
+ ```
50
+
51
+ If your environment doesn't support top-level await, or just want the fonts to get garbage collected after initialization, you can load the fonts like this.
52
+
53
+ ```tsx
54
+ let isFontsLoaded = false;
55
+
56
+ export function GET(request: Request) {
57
+ const fonts = [];
58
+
59
+ if (!isFontsLoaded) {
60
+ isFontsLoaded = true;
61
+ fonts = [
62
+ {
63
+ name: "Inter",
64
+ data: await fetch("/fonts/Inter-Regular.ttf").then((res) => res.arrayBuffer()),
65
+ style: "normal",
66
+ weight: 400,
67
+ },
68
+ ];
69
+ }
70
+
71
+ return new ImageResponse(<OgImage />, { fonts });
72
+ }
73
+ ```
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@takumi-rs/image-response",
3
+ "type": "module",
4
+ "version": "0.29.2",
5
+ "author": {
6
+ "email": "me@kane.tw",
7
+ "name": "Kane Wang",
8
+ "url": "https://kane.tw"
9
+ },
10
+ "repository": {
11
+ "url": "git+https://github.com/kane50613/takumi.git"
12
+ },
13
+ "peerDependencies": {
14
+ "@takumi-rs/core": "^0.29.2",
15
+ "@takumi-rs/helpers": "^0.29.2"
16
+ },
17
+ "devDependencies": {
18
+ "@takumi-rs/core": "workspace:*",
19
+ "@takumi-rs/helpers": "workspace:*",
20
+ "@types/bun": "catalog:",
21
+ "react": "^19.1.1",
22
+ "react-dom": "^19.1.1",
23
+ "tsup": "^8.5.0",
24
+ "typescript": "catalog:"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup-node src/index.ts --minify --dts --format esm,cjs --no-splitting"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js",
33
+ "require": "./dist/index.cjs",
34
+ "default": "./dist/index.js"
35
+ }
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { Renderer, type RenderOptions } from "@takumi-rs/core";
2
+ import { fromJsx } from "@takumi-rs/helpers/jsx";
3
+ import type { HeadersInit } from "bun";
4
+ import type { ReactNode } from "react";
5
+
6
+ const renderer = new Renderer();
7
+
8
+ const fontLoadMarker = new WeakSet<Font>();
9
+
10
+ export type Font = Parameters<typeof renderer.loadFontAsync>[0];
11
+
12
+ export type ImageResponseOptions = RenderOptions &
13
+ ResponseInit & {
14
+ headers?: HeadersInit;
15
+ signal?: AbortSignal;
16
+ fonts?: Font[];
17
+ };
18
+
19
+ const defaultOptions: ImageResponseOptions = {
20
+ width: 1200,
21
+ height: 630,
22
+ format: "webp",
23
+ };
24
+
25
+ export function loadFonts(fonts: Font[]) {
26
+ const fontsToLoad = fonts.filter((font) => !fontLoadMarker.has(font));
27
+
28
+ for (const font of fontsToLoad) {
29
+ fontLoadMarker.add(font);
30
+ }
31
+
32
+ return renderer.loadFontsAsync(fontsToLoad);
33
+ }
34
+
35
+ function createStream(component: ReactNode, options?: ImageResponseOptions) {
36
+ return new ReadableStream({
37
+ async start(controller) {
38
+ try {
39
+ if (options?.fonts) await loadFonts(options.fonts);
40
+
41
+ const node = await fromJsx(component);
42
+ const image = await renderer.renderAsync(
43
+ node,
44
+ options ?? defaultOptions,
45
+ options?.signal,
46
+ );
47
+
48
+ controller.enqueue(image);
49
+ controller.close();
50
+ } catch (error) {
51
+ controller.error(error);
52
+ }
53
+ },
54
+ });
55
+ }
56
+
57
+ export default class ImageResponse extends Response {
58
+ constructor(component: ReactNode, options?: ImageResponseOptions) {
59
+ const stream = createStream(component, options);
60
+ const headers = new Headers(options?.headers);
61
+
62
+ headers.set("Content-Type", "image/webp");
63
+
64
+ if (!headers.get("cache-control")) {
65
+ headers.set("cache-control", "public, max-age=0, must-revalidate");
66
+ }
67
+
68
+ super(stream, {
69
+ status: options?.status,
70
+ statusText: options?.statusText,
71
+ headers,
72
+ });
73
+ }
74
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import ImageResponse from "../src";
3
+
4
+ describe("ImageResponse", () => {
5
+ test("should not crash", async () => {
6
+ const response = new ImageResponse(<div>Hello</div>);
7
+
8
+ expect(response.status).toBe(200);
9
+ expect(response.headers.get("content-type")).toBe("image/webp");
10
+
11
+ expect(await response.arrayBuffer()).toBeDefined();
12
+ });
13
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }