@cibule/image-cf 0.1.1

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,120 @@
1
+ # @cibule/image-cf
2
+
3
+ Cloudflare Image Resizing driver for `@cibule/image` — transforms images via the `cf.image` API with CDN cache purge support.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @cibule/image-cf @cibule/image
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { CfImageTransformer } from '@cibule/image-cf';
15
+
16
+ const transformer = new CfImageTransformer({
17
+ sourceBaseUrl: 'https://cdn.example.com',
18
+ zoneId: 'your-zone-id', // required for cache purge
19
+ apiToken: 'your-api-token', // required for cache purge
20
+ defaultFormat: 'avif', // default: 'avif'
21
+ });
22
+
23
+ // Transform an image via CF Image Resizing
24
+ const result = await transformer.transform('photos/hero.jpg', {
25
+ width: 800,
26
+ quality: 80, // default: 80
27
+ format: 'webp', // overrides defaultFormat
28
+ fit: 'cover', // default: 'cover'
29
+ });
30
+
31
+ // result.body — ReadableStream<Uint8Array>
32
+ // result.contentType — 'image/webp'
33
+ // result.width — 800
34
+ // result.height — 0 (not available from CF Image Resizing)
35
+ // result.size — byte size from content-length header
36
+
37
+ // Purge CDN cache — zone-wide by default
38
+ const purgeResult = await transformer.purgeCache();
39
+ // purgeResult.purgedCount — 1 (zone-wide operation), 0 if no credentials
40
+
41
+ // For selective purge, configure purgeUrls:
42
+ const selectiveTransformer = new CfImageTransformer({
43
+ sourceBaseUrl: 'https://cdn.example.com',
44
+ zoneId: 'your-zone-id',
45
+ apiToken: 'your-api-token',
46
+ purgeUrls: ['https://cdn.example.com/photos/hero.jpg'],
47
+ });
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ | Option | Type | Default | Description |
53
+ | --------------- | ------------- | ------------------ | -------------------------------------- |
54
+ | `sourceBaseUrl` | `string` | _(required)_ | Base URL for source images |
55
+ | `zoneId` | `string` | `undefined` | CF zone ID (required for purge) |
56
+ | `apiToken` | `string` | `undefined` | CF API token (required for purge) |
57
+ | `defaultFormat` | `ImageFormat` | `'avif'` | Output format when not specified |
58
+ | `fetcher` | `Fetcher` | `globalThis.fetch` | Custom fetch implementation |
59
+ | `purgeUrls` | `string[]` | `undefined` | Selective URL purge (avoids zone-wide) |
60
+
61
+ ## Supported Formats
62
+
63
+ - `avif` — best compression, modern browsers
64
+ - `webp` — good compression, wide support
65
+ - `jpeg` — universal compatibility
66
+ - `png` — lossless, transparency support
67
+
68
+ ## How It Works
69
+
70
+ The transformer appends `cf.image` options to the fetch request, leveraging Cloudflare's Image Resizing service to resize and convert images at the edge. No local processing or caching is needed — Cloudflare handles everything.
71
+
72
+ ```typescript
73
+ // Under the hood, the fetch call looks like:
74
+ fetch(sourceUrl, {
75
+ cf: {
76
+ image: { width: 800, quality: 80, format: 'webp', fit: 'cover' },
77
+ },
78
+ });
79
+ ```
80
+
81
+ ## API
82
+
83
+ ### `CfImageTransformer`
84
+
85
+ Extends `ImageTransformer` from `@cibule/image`.
86
+
87
+ #### `transform(key: string, options: TransformOptions): Promise<TransformResult>`
88
+
89
+ Fetches the source image with `cf.image` options to resize and convert it at the edge. Returns a `TransformResult` with the transformed image body and metadata.
90
+
91
+ #### `purgeCache(): Promise<PurgeCacheResult>`
92
+
93
+ Purges the Cloudflare CDN cache. When `purgeUrls` is configured, uses the CF API `files` parameter for selective URL purge. When not configured, falls back to `purge_everything: true` which purges **all** cached assets for the zone (HTML, CSS, JS, images — not just transformed images). Returns `{ purgedCount: 0 }` if `zoneId` or `apiToken` is not configured.
94
+
95
+ ### Custom Fetcher
96
+
97
+ You can provide a custom fetcher for testing or proxying:
98
+
99
+ ```typescript
100
+ import type { Fetcher } from '@cibule/image-cf';
101
+
102
+ const customFetcher: Fetcher = async (url, init) => {
103
+ // Custom logic
104
+ return globalThis.fetch(url, init as RequestInit);
105
+ };
106
+
107
+ const transformer = new CfImageTransformer({
108
+ sourceBaseUrl: 'https://cdn.example.com',
109
+ fetcher: customFetcher,
110
+ });
111
+ ```
112
+
113
+ ## Requirements
114
+
115
+ - Cloudflare Workers environment (uses `cf.image` API)
116
+ - For Node.js, use `@cibule/image-sharp` instead
117
+
118
+ ## License
119
+
120
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CfImageTransformer: () => CfImageTransformer
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // ../image/src/lib/constants.ts
28
+ var DEFAULT_QUALITY = 80;
29
+ var DEFAULT_MAX_SOURCE_SIZE = 60 * 1024 * 1024;
30
+
31
+ // ../image/src/lib/content-type-map.ts
32
+ var FORMAT_TO_CONTENT_TYPE = {
33
+ avif: "image/avif",
34
+ webp: "image/webp",
35
+ jpeg: "image/jpeg",
36
+ png: "image/png"
37
+ };
38
+ function getContentType(format) {
39
+ return FORMAT_TO_CONTENT_TYPE[format];
40
+ }
41
+
42
+ // ../image/src/lib/image-transformer.ts
43
+ var ImageTransformer = class {
44
+ };
45
+
46
+ // src/lib/cf-image-transformer.ts
47
+ var CF_PURGE_API_BASE = "https://api.cloudflare.com/client/v4/zones";
48
+ var CfImageTransformer = class extends ImageTransformer {
49
+ constructor(config) {
50
+ super();
51
+ this.config = config;
52
+ this.fetcher = config.fetcher ?? ((url, init) => globalThis.fetch(url, init));
53
+ this.sourceBaseUrl = config.sourceBaseUrl.endsWith("/") ? config.sourceBaseUrl : `${config.sourceBaseUrl}/`;
54
+ }
55
+ fetcher;
56
+ sourceBaseUrl;
57
+ async transform(key, options) {
58
+ const format = options.format ?? this.config.defaultFormat ?? "avif";
59
+ const quality = options.quality ?? DEFAULT_QUALITY;
60
+ const fit = options.fit ?? "cover";
61
+ const sourceUrl = `${this.sourceBaseUrl}${key}`;
62
+ const response = await this.fetcher(sourceUrl, {
63
+ cf: { image: { width: options.width, quality, format, fit } }
64
+ });
65
+ if (!response.ok) {
66
+ throw new Error(
67
+ `CF Image Resizing failed for "${key}": ${String(response.status)} ${response.statusText}`
68
+ );
69
+ }
70
+ const body = response.body;
71
+ if (!body) {
72
+ throw new Error(`CF Image Resizing returned empty body for "${key}"`);
73
+ }
74
+ const size = Number(response.headers.get("content-length")) || 0;
75
+ return {
76
+ body,
77
+ contentType: getContentType(format),
78
+ width: options.width,
79
+ height: 0,
80
+ size
81
+ };
82
+ }
83
+ async purgeCache() {
84
+ if (!this.config.zoneId || !this.config.apiToken) {
85
+ return { purgedCount: 0 };
86
+ }
87
+ const url = `${CF_PURGE_API_BASE}/${this.config.zoneId}/purge_cache`;
88
+ const urlCount = this.config.purgeUrls?.length ?? 0;
89
+ const purgeBody = urlCount > 0 ? { files: this.config.purgeUrls } : { purge_everything: true };
90
+ const response = await this.fetcher(url, {
91
+ method: "POST",
92
+ headers: {
93
+ Authorization: `Bearer ${this.config.apiToken}`,
94
+ "Content-Type": "application/json"
95
+ },
96
+ body: JSON.stringify(purgeBody)
97
+ });
98
+ if (!response.ok) {
99
+ throw new Error(`CF cache purge failed: ${String(response.status)} ${response.statusText}`);
100
+ }
101
+ return { purgedCount: urlCount > 0 ? urlCount : 1 };
102
+ }
103
+ };
104
+ // Annotate the CommonJS export names for ESM import in node:
105
+ 0 && (module.exports = {
106
+ CfImageTransformer
107
+ });
108
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../../image/src/lib/constants.ts", "../../image/src/lib/content-type-map.ts", "../../image/src/lib/image-transformer.ts", "../src/lib/cf-image-transformer.ts"],
4
+ "sourcesContent": ["export { CfImageTransformer } from './lib/cf-image-transformer';\nexport type {\n CfFetchInit,\n CfFetchResponse,\n CfImageOptions,\n CfImageTransformerConfig,\n Fetcher,\n} from './lib/cf-image-transformer-config';\n", "export const ALLOWED_WIDTHS = [384, 640, 750, 828, 1080, 1200, 1920] as const;\n\nexport const ALLOWED_PLACEHOLDER_WIDTHS = [20, 30, 50, 80] as const;\n\nexport const DEFAULT_QUALITY = 80;\n\nexport const DEFAULT_MAX_SOURCE_SIZE = 60 * 1024 * 1024;\n", "import type { ImageFormat } from './transform-options';\n\nconst FORMAT_TO_CONTENT_TYPE: Record<ImageFormat, string> = {\n avif: 'image/avif',\n webp: 'image/webp',\n jpeg: 'image/jpeg',\n png: 'image/png',\n};\n\nexport function getContentType(format: ImageFormat): string {\n return FORMAT_TO_CONTENT_TYPE[format];\n}\n", "import type { PurgeCacheResult, TransformOptions, TransformResult } from './transform-options';\n\nexport abstract class ImageTransformer {\n abstract transform(key: string, options: TransformOptions): Promise<TransformResult>;\n abstract purgeCache(): Promise<PurgeCacheResult>;\n}\n", "import type { PurgeCacheResult, TransformOptions, TransformResult } from '@cibule/image';\nimport { DEFAULT_QUALITY, getContentType, ImageTransformer } from '@cibule/image';\n\nimport type { CfImageTransformerConfig, Fetcher } from './cf-image-transformer-config';\n\nconst CF_PURGE_API_BASE = 'https://api.cloudflare.com/client/v4/zones';\n\nexport class CfImageTransformer extends ImageTransformer {\n private readonly fetcher: Fetcher;\n private readonly sourceBaseUrl: string;\n\n public constructor(private readonly config: CfImageTransformerConfig) {\n super();\n this.fetcher = config.fetcher ?? ((url, init) => globalThis.fetch(url, init as RequestInit));\n this.sourceBaseUrl = config.sourceBaseUrl.endsWith('/')\n ? config.sourceBaseUrl\n : `${config.sourceBaseUrl}/`;\n }\n\n public async transform(key: string, options: TransformOptions): Promise<TransformResult> {\n const format = options.format ?? this.config.defaultFormat ?? 'avif';\n const quality = options.quality ?? DEFAULT_QUALITY;\n const fit = options.fit ?? 'cover';\n const sourceUrl = `${this.sourceBaseUrl}${key}`;\n\n const response = await this.fetcher(sourceUrl, {\n cf: { image: { width: options.width, quality, format, fit } },\n });\n\n if (!response.ok) {\n throw new Error(\n `CF Image Resizing failed for \"${key}\": ${String(response.status)} ${response.statusText}`,\n );\n }\n\n const body = response.body;\n if (!body) {\n throw new Error(`CF Image Resizing returned empty body for \"${key}\"`);\n }\n\n const size = Number(response.headers.get('content-length')) || 0;\n\n return {\n body,\n contentType: getContentType(format),\n width: options.width,\n height: 0,\n size,\n };\n }\n\n public async purgeCache(): Promise<PurgeCacheResult> {\n if (!this.config.zoneId || !this.config.apiToken) {\n return { purgedCount: 0 };\n }\n\n const url = `${CF_PURGE_API_BASE}/${this.config.zoneId}/purge_cache`;\n const urlCount = this.config.purgeUrls?.length ?? 0;\n const purgeBody = urlCount > 0 ? { files: this.config.purgeUrls } : { purge_everything: true };\n\n const response = await this.fetcher(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.apiToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(purgeBody),\n });\n\n if (!response.ok) {\n throw new Error(`CF cache purge failed: ${String(response.status)} ${response.statusText}`);\n }\n\n return { purgedCount: urlCount > 0 ? urlCount : 1 };\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIO,IAAM,kBAAkB;AAExB,IAAM,0BAA0B,KAAK,OAAO;;;ACJnD,IAAM,yBAAsD;AAAA,EAC1D,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AACP;AAEO,SAAS,eAAe,QAA6B;AAC1D,SAAO,uBAAuB,MAAM;AACtC;;;ACTO,IAAe,mBAAf,MAAgC;AAGvC;;;ACAA,IAAM,oBAAoB;AAEnB,IAAM,qBAAN,cAAiC,iBAAiB;AAAA,EAIhD,YAA6B,QAAkC;AACpE,UAAM;AAD4B;AAElC,SAAK,UAAU,OAAO,YAAY,CAAC,KAAK,SAAS,WAAW,MAAM,KAAK,IAAmB;AAC1F,SAAK,gBAAgB,OAAO,cAAc,SAAS,GAAG,IAClD,OAAO,gBACP,GAAG,OAAO,aAAa;AAAA,EAC7B;AAAA,EATiB;AAAA,EACA;AAAA,EAUjB,MAAa,UAAU,KAAa,SAAqD;AACvF,UAAM,SAAS,QAAQ,UAAU,KAAK,OAAO,iBAAiB;AAC9D,UAAM,UAAU,QAAQ,WAAW;AACnC,UAAM,MAAM,QAAQ,OAAO;AAC3B,UAAM,YAAY,GAAG,KAAK,aAAa,GAAG,GAAG;AAE7C,UAAM,WAAW,MAAM,KAAK,QAAQ,WAAW;AAAA,MAC7C,IAAI,EAAE,OAAO,EAAE,OAAO,QAAQ,OAAO,SAAS,QAAQ,IAAI,EAAE;AAAA,IAC9D,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,iCAAiC,GAAG,MAAM,OAAO,SAAS,MAAM,CAAC,IAAI,SAAS,UAAU;AAAA,MAC1F;AAAA,IACF;AAEA,UAAM,OAAO,SAAS;AACtB,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,8CAA8C,GAAG,GAAG;AAAA,IACtE;AAEA,UAAM,OAAO,OAAO,SAAS,QAAQ,IAAI,gBAAgB,CAAC,KAAK;AAE/D,WAAO;AAAA,MACL;AAAA,MACA,aAAa,eAAe,MAAM;AAAA,MAClC,OAAO,QAAQ;AAAA,MACf,QAAQ;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAa,aAAwC;AACnD,QAAI,CAAC,KAAK,OAAO,UAAU,CAAC,KAAK,OAAO,UAAU;AAChD,aAAO,EAAE,aAAa,EAAE;AAAA,IAC1B;AAEA,UAAM,MAAM,GAAG,iBAAiB,IAAI,KAAK,OAAO,MAAM;AACtD,UAAM,WAAW,KAAK,OAAO,WAAW,UAAU;AAClD,UAAM,YAAY,WAAW,IAAI,EAAE,OAAO,KAAK,OAAO,UAAU,IAAI,EAAE,kBAAkB,KAAK;AAE7F,UAAM,WAAW,MAAM,KAAK,QAAQ,KAAK;AAAA,MACvC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,KAAK,OAAO,QAAQ;AAAA,QAC7C,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,SAAS;AAAA,IAChC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,0BAA0B,OAAO,SAAS,MAAM,CAAC,IAAI,SAAS,UAAU,EAAE;AAAA,IAC5F;AAEA,WAAO,EAAE,aAAa,WAAW,IAAI,WAAW,EAAE;AAAA,EACpD;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,3 @@
1
+ export { CfImageTransformer } from './lib/cf-image-transformer';
2
+ export type { CfFetchInit, CfFetchResponse, CfImageOptions, CfImageTransformerConfig, Fetcher, } from './lib/cf-image-transformer-config';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,YAAY,EACV,WAAW,EACX,eAAe,EACf,cAAc,EACd,wBAAwB,EACxB,OAAO,GACR,MAAM,mCAAmC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ // ../image/src/lib/constants.ts
2
+ var DEFAULT_QUALITY = 80;
3
+ var DEFAULT_MAX_SOURCE_SIZE = 60 * 1024 * 1024;
4
+
5
+ // ../image/src/lib/content-type-map.ts
6
+ var FORMAT_TO_CONTENT_TYPE = {
7
+ avif: "image/avif",
8
+ webp: "image/webp",
9
+ jpeg: "image/jpeg",
10
+ png: "image/png"
11
+ };
12
+ function getContentType(format) {
13
+ return FORMAT_TO_CONTENT_TYPE[format];
14
+ }
15
+
16
+ // ../image/src/lib/image-transformer.ts
17
+ var ImageTransformer = class {
18
+ };
19
+
20
+ // src/lib/cf-image-transformer.ts
21
+ var CF_PURGE_API_BASE = "https://api.cloudflare.com/client/v4/zones";
22
+ var CfImageTransformer = class extends ImageTransformer {
23
+ constructor(config) {
24
+ super();
25
+ this.config = config;
26
+ this.fetcher = config.fetcher ?? ((url, init) => globalThis.fetch(url, init));
27
+ this.sourceBaseUrl = config.sourceBaseUrl.endsWith("/") ? config.sourceBaseUrl : `${config.sourceBaseUrl}/`;
28
+ }
29
+ fetcher;
30
+ sourceBaseUrl;
31
+ async transform(key, options) {
32
+ const format = options.format ?? this.config.defaultFormat ?? "avif";
33
+ const quality = options.quality ?? DEFAULT_QUALITY;
34
+ const fit = options.fit ?? "cover";
35
+ const sourceUrl = `${this.sourceBaseUrl}${key}`;
36
+ const response = await this.fetcher(sourceUrl, {
37
+ cf: { image: { width: options.width, quality, format, fit } }
38
+ });
39
+ if (!response.ok) {
40
+ throw new Error(
41
+ `CF Image Resizing failed for "${key}": ${String(response.status)} ${response.statusText}`
42
+ );
43
+ }
44
+ const body = response.body;
45
+ if (!body) {
46
+ throw new Error(`CF Image Resizing returned empty body for "${key}"`);
47
+ }
48
+ const size = Number(response.headers.get("content-length")) || 0;
49
+ return {
50
+ body,
51
+ contentType: getContentType(format),
52
+ width: options.width,
53
+ height: 0,
54
+ size
55
+ };
56
+ }
57
+ async purgeCache() {
58
+ if (!this.config.zoneId || !this.config.apiToken) {
59
+ return { purgedCount: 0 };
60
+ }
61
+ const url = `${CF_PURGE_API_BASE}/${this.config.zoneId}/purge_cache`;
62
+ const urlCount = this.config.purgeUrls?.length ?? 0;
63
+ const purgeBody = urlCount > 0 ? { files: this.config.purgeUrls } : { purge_everything: true };
64
+ const response = await this.fetcher(url, {
65
+ method: "POST",
66
+ headers: {
67
+ Authorization: `Bearer ${this.config.apiToken}`,
68
+ "Content-Type": "application/json"
69
+ },
70
+ body: JSON.stringify(purgeBody)
71
+ });
72
+ if (!response.ok) {
73
+ throw new Error(`CF cache purge failed: ${String(response.status)} ${response.statusText}`);
74
+ }
75
+ return { purgedCount: urlCount > 0 ? urlCount : 1 };
76
+ }
77
+ };
78
+ export {
79
+ CfImageTransformer
80
+ };
81
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../image/src/lib/constants.ts", "../../image/src/lib/content-type-map.ts", "../../image/src/lib/image-transformer.ts", "../src/lib/cf-image-transformer.ts"],
4
+ "sourcesContent": ["export const ALLOWED_WIDTHS = [384, 640, 750, 828, 1080, 1200, 1920] as const;\n\nexport const ALLOWED_PLACEHOLDER_WIDTHS = [20, 30, 50, 80] as const;\n\nexport const DEFAULT_QUALITY = 80;\n\nexport const DEFAULT_MAX_SOURCE_SIZE = 60 * 1024 * 1024;\n", "import type { ImageFormat } from './transform-options';\n\nconst FORMAT_TO_CONTENT_TYPE: Record<ImageFormat, string> = {\n avif: 'image/avif',\n webp: 'image/webp',\n jpeg: 'image/jpeg',\n png: 'image/png',\n};\n\nexport function getContentType(format: ImageFormat): string {\n return FORMAT_TO_CONTENT_TYPE[format];\n}\n", "import type { PurgeCacheResult, TransformOptions, TransformResult } from './transform-options';\n\nexport abstract class ImageTransformer {\n abstract transform(key: string, options: TransformOptions): Promise<TransformResult>;\n abstract purgeCache(): Promise<PurgeCacheResult>;\n}\n", "import type { PurgeCacheResult, TransformOptions, TransformResult } from '@cibule/image';\nimport { DEFAULT_QUALITY, getContentType, ImageTransformer } from '@cibule/image';\n\nimport type { CfImageTransformerConfig, Fetcher } from './cf-image-transformer-config';\n\nconst CF_PURGE_API_BASE = 'https://api.cloudflare.com/client/v4/zones';\n\nexport class CfImageTransformer extends ImageTransformer {\n private readonly fetcher: Fetcher;\n private readonly sourceBaseUrl: string;\n\n public constructor(private readonly config: CfImageTransformerConfig) {\n super();\n this.fetcher = config.fetcher ?? ((url, init) => globalThis.fetch(url, init as RequestInit));\n this.sourceBaseUrl = config.sourceBaseUrl.endsWith('/')\n ? config.sourceBaseUrl\n : `${config.sourceBaseUrl}/`;\n }\n\n public async transform(key: string, options: TransformOptions): Promise<TransformResult> {\n const format = options.format ?? this.config.defaultFormat ?? 'avif';\n const quality = options.quality ?? DEFAULT_QUALITY;\n const fit = options.fit ?? 'cover';\n const sourceUrl = `${this.sourceBaseUrl}${key}`;\n\n const response = await this.fetcher(sourceUrl, {\n cf: { image: { width: options.width, quality, format, fit } },\n });\n\n if (!response.ok) {\n throw new Error(\n `CF Image Resizing failed for \"${key}\": ${String(response.status)} ${response.statusText}`,\n );\n }\n\n const body = response.body;\n if (!body) {\n throw new Error(`CF Image Resizing returned empty body for \"${key}\"`);\n }\n\n const size = Number(response.headers.get('content-length')) || 0;\n\n return {\n body,\n contentType: getContentType(format),\n width: options.width,\n height: 0,\n size,\n };\n }\n\n public async purgeCache(): Promise<PurgeCacheResult> {\n if (!this.config.zoneId || !this.config.apiToken) {\n return { purgedCount: 0 };\n }\n\n const url = `${CF_PURGE_API_BASE}/${this.config.zoneId}/purge_cache`;\n const urlCount = this.config.purgeUrls?.length ?? 0;\n const purgeBody = urlCount > 0 ? { files: this.config.purgeUrls } : { purge_everything: true };\n\n const response = await this.fetcher(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.apiToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(purgeBody),\n });\n\n if (!response.ok) {\n throw new Error(`CF cache purge failed: ${String(response.status)} ${response.statusText}`);\n }\n\n return { purgedCount: urlCount > 0 ? urlCount : 1 };\n }\n}\n"],
5
+ "mappings": ";AAIO,IAAM,kBAAkB;AAExB,IAAM,0BAA0B,KAAK,OAAO;;;ACJnD,IAAM,yBAAsD;AAAA,EAC1D,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AACP;AAEO,SAAS,eAAe,QAA6B;AAC1D,SAAO,uBAAuB,MAAM;AACtC;;;ACTO,IAAe,mBAAf,MAAgC;AAGvC;;;ACAA,IAAM,oBAAoB;AAEnB,IAAM,qBAAN,cAAiC,iBAAiB;AAAA,EAIhD,YAA6B,QAAkC;AACpE,UAAM;AAD4B;AAElC,SAAK,UAAU,OAAO,YAAY,CAAC,KAAK,SAAS,WAAW,MAAM,KAAK,IAAmB;AAC1F,SAAK,gBAAgB,OAAO,cAAc,SAAS,GAAG,IAClD,OAAO,gBACP,GAAG,OAAO,aAAa;AAAA,EAC7B;AAAA,EATiB;AAAA,EACA;AAAA,EAUjB,MAAa,UAAU,KAAa,SAAqD;AACvF,UAAM,SAAS,QAAQ,UAAU,KAAK,OAAO,iBAAiB;AAC9D,UAAM,UAAU,QAAQ,WAAW;AACnC,UAAM,MAAM,QAAQ,OAAO;AAC3B,UAAM,YAAY,GAAG,KAAK,aAAa,GAAG,GAAG;AAE7C,UAAM,WAAW,MAAM,KAAK,QAAQ,WAAW;AAAA,MAC7C,IAAI,EAAE,OAAO,EAAE,OAAO,QAAQ,OAAO,SAAS,QAAQ,IAAI,EAAE;AAAA,IAC9D,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,iCAAiC,GAAG,MAAM,OAAO,SAAS,MAAM,CAAC,IAAI,SAAS,UAAU;AAAA,MAC1F;AAAA,IACF;AAEA,UAAM,OAAO,SAAS;AACtB,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,8CAA8C,GAAG,GAAG;AAAA,IACtE;AAEA,UAAM,OAAO,OAAO,SAAS,QAAQ,IAAI,gBAAgB,CAAC,KAAK;AAE/D,WAAO;AAAA,MACL;AAAA,MACA,aAAa,eAAe,MAAM;AAAA,MAClC,OAAO,QAAQ;AAAA,MACf,QAAQ;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAa,aAAwC;AACnD,QAAI,CAAC,KAAK,OAAO,UAAU,CAAC,KAAK,OAAO,UAAU;AAChD,aAAO,EAAE,aAAa,EAAE;AAAA,IAC1B;AAEA,UAAM,MAAM,GAAG,iBAAiB,IAAI,KAAK,OAAO,MAAM;AACtD,UAAM,WAAW,KAAK,OAAO,WAAW,UAAU;AAClD,UAAM,YAAY,WAAW,IAAI,EAAE,OAAO,KAAK,OAAO,UAAU,IAAI,EAAE,kBAAkB,KAAK;AAE7F,UAAM,WAAW,MAAM,KAAK,QAAQ,KAAK;AAAA,MACvC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,KAAK,OAAO,QAAQ;AAAA,QAC7C,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,SAAS;AAAA,IAChC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,0BAA0B,OAAO,SAAS,MAAM,CAAC,IAAI,SAAS,UAAU,EAAE;AAAA,IAC5F;AAEA,WAAO,EAAE,aAAa,WAAW,IAAI,WAAW,EAAE;AAAA,EACpD;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,34 @@
1
+ import type { ImageFit, ImageFormat } from '@cibule/image';
2
+ export interface CfImageOptions {
3
+ readonly width: number;
4
+ readonly quality: number;
5
+ readonly format: ImageFormat;
6
+ readonly fit: ImageFit;
7
+ }
8
+ export interface CfFetchInit {
9
+ readonly method?: string;
10
+ readonly headers?: Record<string, string>;
11
+ readonly body?: string;
12
+ readonly cf?: {
13
+ readonly image: CfImageOptions;
14
+ };
15
+ }
16
+ export interface CfFetchResponse {
17
+ readonly ok: boolean;
18
+ readonly status: number;
19
+ readonly statusText: string;
20
+ readonly body: ReadableStream<Uint8Array> | null;
21
+ readonly headers: {
22
+ get(name: string): string | null;
23
+ };
24
+ }
25
+ export type Fetcher = (url: string, init?: CfFetchInit) => Promise<CfFetchResponse>;
26
+ export interface CfImageTransformerConfig {
27
+ readonly sourceBaseUrl: string;
28
+ readonly zoneId?: string;
29
+ readonly apiToken?: string;
30
+ readonly defaultFormat?: ImageFormat;
31
+ readonly fetcher?: Fetcher;
32
+ readonly purgeUrls?: readonly string[];
33
+ }
34
+ //# sourceMappingURL=cf-image-transformer-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cf-image-transformer-config.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/cf-image-transformer-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;KAAE,CAAC;CAClD;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAEpF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC"}
@@ -0,0 +1,12 @@
1
+ import type { PurgeCacheResult, TransformOptions, TransformResult } from '@cibule/image';
2
+ import { ImageTransformer } from '@cibule/image';
3
+ import type { CfImageTransformerConfig } from './cf-image-transformer-config';
4
+ export declare class CfImageTransformer extends ImageTransformer {
5
+ private readonly config;
6
+ private readonly fetcher;
7
+ private readonly sourceBaseUrl;
8
+ constructor(config: CfImageTransformerConfig);
9
+ transform(key: string, options: TransformOptions): Promise<TransformResult>;
10
+ purgeCache(): Promise<PurgeCacheResult>;
11
+ }
12
+ //# sourceMappingURL=cf-image-transformer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cf-image-transformer.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/cf-image-transformer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACzF,OAAO,EAAmC,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAElF,OAAO,KAAK,EAAE,wBAAwB,EAAW,MAAM,+BAA+B,CAAC;AAIvF,qBAAa,kBAAmB,SAAQ,gBAAgB;IAInC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAH1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAEH,MAAM,EAAE,wBAAwB;IAQvD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAgC3E,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;CAwBrD"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@cibule/image-cf",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://gitlab.com/LadaBr/cibule",
28
+ "directory": "packages/image-cf"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@cibule/image": "workspace:*"
35
+ }
36
+ }