@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 +120 -0
- package/dist/index.cjs +108 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/cf-image-transformer-config.d.ts +34 -0
- package/dist/lib/cf-image-transformer-config.d.ts.map +1 -0
- package/dist/lib/cf-image-transformer.d.ts +12 -0
- package/dist/lib/cf-image-transformer.d.ts.map +1 -0
- package/package.json +36 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|