@createcms/core 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 +169 -0
- package/dist/ab-edge/index.cjs +214 -0
- package/dist/ab-edge/index.d.cts +121 -0
- package/dist/ab-edge/index.d.ts +121 -0
- package/dist/ab-edge/index.js +205 -0
- package/dist/bin/createcms.js +3082 -0
- package/dist/db.cjs +496 -0
- package/dist/db.d.cts +128 -0
- package/dist/db.d.ts +128 -0
- package/dist/db.js +488 -0
- package/dist/index.cjs +13789 -0
- package/dist/index.d.cts +10277 -0
- package/dist/index.d.ts +10277 -0
- package/dist/index.js +13737 -0
- package/dist/nanoid.cjs +50 -0
- package/dist/nanoid.d.cts +29 -0
- package/dist/nanoid.d.ts +29 -0
- package/dist/nanoid.js +47 -0
- package/dist/next/index.cjs +60 -0
- package/dist/next/index.d.cts +141 -0
- package/dist/next/index.d.ts +141 -0
- package/dist/next/index.js +58 -0
- package/dist/next/middleware.cjs +113 -0
- package/dist/next/middleware.d.cts +77 -0
- package/dist/next/middleware.d.ts +77 -0
- package/dist/next/middleware.js +111 -0
- package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
- package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.js +343 -0
- package/dist/plugins/ab-test/client.cjs +686 -0
- package/dist/plugins/ab-test/client.d.cts +233 -0
- package/dist/plugins/ab-test/client.d.ts +233 -0
- package/dist/plugins/ab-test/client.js +684 -0
- package/dist/plugins/ab-test/index.cjs +3400 -0
- package/dist/plugins/ab-test/index.d.cts +1131 -0
- package/dist/plugins/ab-test/index.d.ts +1131 -0
- package/dist/plugins/ab-test/index.js +3367 -0
- package/dist/plugins/client.cjs +20 -0
- package/dist/plugins/client.d.cts +3 -0
- package/dist/plugins/client.d.ts +3 -0
- package/dist/plugins/client.js +3 -0
- package/dist/plugins/consent/client.cjs +315 -0
- package/dist/plugins/consent/client.d.cts +145 -0
- package/dist/plugins/consent/client.d.ts +145 -0
- package/dist/plugins/consent/client.js +313 -0
- package/dist/plugins/consent/index.cjs +267 -0
- package/dist/plugins/consent/index.d.cts +618 -0
- package/dist/plugins/consent/index.d.ts +618 -0
- package/dist/plugins/consent/index.js +258 -0
- package/dist/plugins/i18n/index.cjs +2177 -0
- package/dist/plugins/i18n/index.d.cts +562 -0
- package/dist/plugins/i18n/index.d.ts +562 -0
- package/dist/plugins/i18n/index.js +2150 -0
- package/dist/plugins/media-optimize/index.cjs +315 -0
- package/dist/plugins/media-optimize/index.d.cts +144 -0
- package/dist/plugins/media-optimize/index.d.ts +144 -0
- package/dist/plugins/media-optimize/index.js +311 -0
- package/dist/plugins/multi-tenant/index.cjs +210 -0
- package/dist/plugins/multi-tenant/index.d.cts +431 -0
- package/dist/plugins/multi-tenant/index.d.ts +431 -0
- package/dist/plugins/multi-tenant/index.js +207 -0
- package/dist/plugins/server.cjs +24 -0
- package/dist/plugins/server.d.cts +3 -0
- package/dist/plugins/server.d.ts +3 -0
- package/dist/plugins/server.js +3 -0
- package/dist/react/blocks.cjs +233 -0
- package/dist/react/blocks.d.cts +320 -0
- package/dist/react/blocks.d.ts +320 -0
- package/dist/react/blocks.js +226 -0
- package/dist/react/index.cjs +901 -0
- package/dist/react/index.d.cts +992 -0
- package/dist/react/index.d.ts +992 -0
- package/dist/react/index.js +872 -0
- package/dist/react/tracking.cjs +243 -0
- package/dist/react/tracking.d.cts +364 -0
- package/dist/react/tracking.d.ts +364 -0
- package/dist/react/tracking.js +216 -0
- package/dist/react/variant.cjs +59 -0
- package/dist/react/variant.d.cts +26 -0
- package/dist/react/variant.d.ts +26 -0
- package/dist/react/variant.js +57 -0
- package/package.json +303 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
function isImageFile(file) {
|
|
6
|
+
return file.type.startsWith('image/');
|
|
7
|
+
}
|
|
8
|
+
function loadImage(file) {
|
|
9
|
+
return new Promise((resolve, reject)=>{
|
|
10
|
+
const img = new Image();
|
|
11
|
+
const url = URL.createObjectURL(file);
|
|
12
|
+
img.onload = ()=>{
|
|
13
|
+
URL.revokeObjectURL(url);
|
|
14
|
+
resolve(img);
|
|
15
|
+
};
|
|
16
|
+
img.onerror = ()=>{
|
|
17
|
+
URL.revokeObjectURL(url);
|
|
18
|
+
reject(new Error(`Failed to load image: ${file.name}`));
|
|
19
|
+
};
|
|
20
|
+
img.src = url;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function calculateDimensions(width, height, maxSize) {
|
|
24
|
+
if (width <= maxSize && height <= maxSize) {
|
|
25
|
+
return {
|
|
26
|
+
width,
|
|
27
|
+
height
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const ratio = width / height;
|
|
31
|
+
if (width > height) {
|
|
32
|
+
return {
|
|
33
|
+
width: maxSize,
|
|
34
|
+
height: Math.round(maxSize / ratio)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
width: Math.round(maxSize * ratio),
|
|
39
|
+
height: maxSize
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function drawToCanvas(img, width, height) {
|
|
43
|
+
const canvas = document.createElement('canvas');
|
|
44
|
+
canvas.width = width;
|
|
45
|
+
canvas.height = height;
|
|
46
|
+
const ctx = canvas.getContext('2d');
|
|
47
|
+
if (!ctx) throw new Error('Failed to get canvas 2d context');
|
|
48
|
+
ctx.imageSmoothingEnabled = true;
|
|
49
|
+
ctx.imageSmoothingQuality = 'high';
|
|
50
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
51
|
+
return canvas;
|
|
52
|
+
}
|
|
53
|
+
function canvasToBlobAsync(canvas, mimeType, quality) {
|
|
54
|
+
return new Promise((resolve, reject)=>{
|
|
55
|
+
canvas.toBlob((blob)=>blob ? resolve(blob) : reject(new Error(`canvas.toBlob failed for ${mimeType}`)), mimeType, quality);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
let _nativeWebpSupported = null;
|
|
59
|
+
async function supportsNativeWebp() {
|
|
60
|
+
if (_nativeWebpSupported !== null) return _nativeWebpSupported;
|
|
61
|
+
const c = document.createElement('canvas');
|
|
62
|
+
c.width = 1;
|
|
63
|
+
c.height = 1;
|
|
64
|
+
const blob = await new Promise((r)=>c.toBlob((b)=>r(b), 'image/webp', 0.5));
|
|
65
|
+
_nativeWebpSupported = blob?.type === 'image/webp';
|
|
66
|
+
return _nativeWebpSupported;
|
|
67
|
+
}
|
|
68
|
+
async function encodeWebp(canvas, quality) {
|
|
69
|
+
if (await supportsNativeWebp()) {
|
|
70
|
+
return canvasToBlobAsync(canvas, 'image/webp', quality);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const { encode } = await import(/* webpackIgnore: true */ '@jsquash/webp');
|
|
74
|
+
const ctx = canvas.getContext('2d');
|
|
75
|
+
if (!ctx) throw new Error('Failed to get canvas 2d context');
|
|
76
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
77
|
+
const buffer = await encode(imageData, {
|
|
78
|
+
quality: quality * 100
|
|
79
|
+
});
|
|
80
|
+
return new Blob([
|
|
81
|
+
buffer
|
|
82
|
+
], {
|
|
83
|
+
type: 'image/webp'
|
|
84
|
+
});
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error('WebP encoding not supported natively and @jsquash/webp is not installed. ' + 'Install it as a dependency: npm install @jsquash/webp');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function inferMimeFromName(name) {
|
|
90
|
+
const ext = name.split('.').pop()?.toLowerCase();
|
|
91
|
+
const map = {
|
|
92
|
+
png: 'image/png',
|
|
93
|
+
jpg: 'image/jpeg',
|
|
94
|
+
jpeg: 'image/jpeg',
|
|
95
|
+
gif: 'image/gif',
|
|
96
|
+
webp: 'image/webp',
|
|
97
|
+
svg: 'image/svg+xml',
|
|
98
|
+
bmp: 'image/bmp',
|
|
99
|
+
avif: 'image/avif'
|
|
100
|
+
};
|
|
101
|
+
return ext && map[ext] || 'image/jpeg';
|
|
102
|
+
}
|
|
103
|
+
function replaceExtension(filename, newExt) {
|
|
104
|
+
const dot = filename.lastIndexOf('.');
|
|
105
|
+
const base = dot > 0 ? filename.slice(0, dot) : filename;
|
|
106
|
+
return `${base}.${newExt}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Optimize an image file on the client before uploading.
|
|
110
|
+
*
|
|
111
|
+
* - **Resize**: Downscale if either dimension exceeds `config.resize.maxSize`
|
|
112
|
+
* - **Compress**: Reduce quality via `config.compress.quality` (1-100)
|
|
113
|
+
* - **Convert**: Convert to WebP when `config.convert.format` is set
|
|
114
|
+
* - **storeOriginal**: When `convert.storeOriginal` is true, also returns an
|
|
115
|
+
* `originalVariant` -- same resize + compress but kept in the original format
|
|
116
|
+
*
|
|
117
|
+
* Non-image files are returned unchanged with `optimized: false`.
|
|
118
|
+
*/ async function optimizeImage(file, config) {
|
|
119
|
+
if (!isImageFile(file)) {
|
|
120
|
+
return {
|
|
121
|
+
file,
|
|
122
|
+
optimized: false
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const hasResize = !!config.resize?.maxSize;
|
|
126
|
+
const hasCompress = config.compress?.quality != null;
|
|
127
|
+
const hasConvert = config.convert?.format === 'webp';
|
|
128
|
+
if (!hasResize && !hasCompress && !hasConvert) {
|
|
129
|
+
return {
|
|
130
|
+
file,
|
|
131
|
+
optimized: false
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const img = await loadImage(file);
|
|
135
|
+
const maxSize = config.resize?.maxSize ?? Math.max(img.naturalWidth, img.naturalHeight);
|
|
136
|
+
const quality = config.compress?.quality != null ? config.compress.quality / 100 : 0.8;
|
|
137
|
+
const { width, height } = calculateDimensions(img.naturalWidth, img.naturalHeight, maxSize);
|
|
138
|
+
const canvas = drawToCanvas(img, width, height);
|
|
139
|
+
let originalVariant;
|
|
140
|
+
const originalMime = file.type || inferMimeFromName(file.name);
|
|
141
|
+
if (hasConvert && config.convert.storeOriginal) {
|
|
142
|
+
const originalBlob = await canvasToBlobAsync(canvas, originalMime, quality);
|
|
143
|
+
originalVariant = new File([
|
|
144
|
+
originalBlob
|
|
145
|
+
], file.name, {
|
|
146
|
+
type: originalMime,
|
|
147
|
+
lastModified: Date.now()
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
let primaryBlob;
|
|
151
|
+
let primaryName;
|
|
152
|
+
let primaryType;
|
|
153
|
+
if (hasConvert) {
|
|
154
|
+
primaryBlob = await encodeWebp(canvas, quality);
|
|
155
|
+
primaryName = replaceExtension(file.name, 'webp');
|
|
156
|
+
primaryType = 'image/webp';
|
|
157
|
+
} else {
|
|
158
|
+
primaryBlob = await canvasToBlobAsync(canvas, originalMime, quality);
|
|
159
|
+
primaryName = file.name;
|
|
160
|
+
primaryType = originalMime;
|
|
161
|
+
}
|
|
162
|
+
const primaryFile = new File([
|
|
163
|
+
primaryBlob
|
|
164
|
+
], primaryName, {
|
|
165
|
+
type: primaryType,
|
|
166
|
+
lastModified: Date.now()
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
file: primaryFile,
|
|
170
|
+
originalVariant,
|
|
171
|
+
optimized: true
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const INITIAL_STATE = {
|
|
176
|
+
results: null,
|
|
177
|
+
isOptimizing: false,
|
|
178
|
+
error: null
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* React hook that optimizes image files on the client.
|
|
182
|
+
*
|
|
183
|
+
* Accepts a single `File` or `File[]` and an `OptimizationConfig`.
|
|
184
|
+
* Runs `optimizeImage` automatically whenever the input reference changes.
|
|
185
|
+
* Non-image files pass through with `optimized: false`.
|
|
186
|
+
*
|
|
187
|
+
* ```tsx
|
|
188
|
+
* const { results, isOptimizing, error } = cmsClient.optimize.useOptimize(file, {
|
|
189
|
+
* compress: { quality: 80 },
|
|
190
|
+
* resize: { maxSize: 1200 },
|
|
191
|
+
* });
|
|
192
|
+
* ```
|
|
193
|
+
*/ function useOptimize(input, config) {
|
|
194
|
+
const [state, setState] = react.useState(INITIAL_STATE);
|
|
195
|
+
const abortRef = react.useRef(false);
|
|
196
|
+
const runIdRef = react.useRef(0);
|
|
197
|
+
const files = react.useMemo(()=>Array.isArray(input) ? input : [
|
|
198
|
+
input
|
|
199
|
+
], // Stabilize: same File instances → same array
|
|
200
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
201
|
+
[
|
|
202
|
+
Array.isArray(input) ? input.length : input
|
|
203
|
+
]);
|
|
204
|
+
const configKey = JSON.stringify(config);
|
|
205
|
+
const configRef = react.useRef(config);
|
|
206
|
+
configRef.current = config;
|
|
207
|
+
react.useEffect(()=>{
|
|
208
|
+
if (files.length === 0) {
|
|
209
|
+
setState((prev)=>prev.results?.length === 0 && !prev.isOptimizing ? prev : {
|
|
210
|
+
results: [],
|
|
211
|
+
isOptimizing: false,
|
|
212
|
+
error: null
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
abortRef.current = false;
|
|
217
|
+
const currentRun = ++runIdRef.current;
|
|
218
|
+
setState({
|
|
219
|
+
results: null,
|
|
220
|
+
isOptimizing: true,
|
|
221
|
+
error: null
|
|
222
|
+
});
|
|
223
|
+
Promise.all(files.map((file)=>optimizeImage(file, configRef.current))).then((results)=>{
|
|
224
|
+
if (abortRef.current || runIdRef.current !== currentRun) return;
|
|
225
|
+
setState({
|
|
226
|
+
results,
|
|
227
|
+
isOptimizing: false,
|
|
228
|
+
error: null
|
|
229
|
+
});
|
|
230
|
+
}).catch((err)=>{
|
|
231
|
+
if (abortRef.current || runIdRef.current !== currentRun) return;
|
|
232
|
+
setState({
|
|
233
|
+
results: null,
|
|
234
|
+
isOptimizing: false,
|
|
235
|
+
error: err instanceof Error ? err.message : 'Optimization failed'
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
return ()=>{
|
|
239
|
+
abortRef.current = true;
|
|
240
|
+
};
|
|
241
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
242
|
+
}, [
|
|
243
|
+
files,
|
|
244
|
+
configKey
|
|
245
|
+
]);
|
|
246
|
+
return state;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const PLUGIN_ID = 'media-optimize';
|
|
250
|
+
const $ERROR_CODES = {
|
|
251
|
+
OPTIMIZATION_FAILED: {
|
|
252
|
+
status: 422,
|
|
253
|
+
message: 'Image optimization failed'
|
|
254
|
+
},
|
|
255
|
+
WEBP_NOT_SUPPORTED: {
|
|
256
|
+
status: 422,
|
|
257
|
+
message: 'WebP encoding is not supported in this browser and @jsquash/webp is not installed'
|
|
258
|
+
},
|
|
259
|
+
CANVAS_CONTEXT_FAILED: {
|
|
260
|
+
status: 500,
|
|
261
|
+
message: 'Failed to acquire canvas 2D context for image processing'
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Client plugin that adds image optimization under its own namespace.
|
|
266
|
+
*
|
|
267
|
+
* Exposes `cmsClient.optimize.useOptimize(file, config)` for client-side
|
|
268
|
+
* image optimization. Optimized files can then be passed to
|
|
269
|
+
* `cmsClient.media.useUploadAssets().upload(files)`.
|
|
270
|
+
*
|
|
271
|
+
* ```ts
|
|
272
|
+
* import { mediaOptimizeClient } from '@createcms/core/plugins/media-optimize';
|
|
273
|
+
*
|
|
274
|
+
* const client = createCMSClient<typeof cms>({
|
|
275
|
+
* baseURL: '/api/cms',
|
|
276
|
+
* plugins: [
|
|
277
|
+
* mediaOptimizeClient({
|
|
278
|
+
* compress: { quality: 90 },
|
|
279
|
+
* resize: { maxSize: 2000 },
|
|
280
|
+
* convert: { format: 'webp', storeOriginal: true },
|
|
281
|
+
* }),
|
|
282
|
+
* ],
|
|
283
|
+
* });
|
|
284
|
+
*
|
|
285
|
+
* // In a component:
|
|
286
|
+
* const { results, isOptimizing } = client.optimize.useOptimize(file, config);
|
|
287
|
+
* ```
|
|
288
|
+
*/ function mediaOptimizeClient(config) {
|
|
289
|
+
return {
|
|
290
|
+
id: PLUGIN_ID,
|
|
291
|
+
$ERROR_CODES,
|
|
292
|
+
async init (_$fetch, _$store) {
|
|
293
|
+
return {
|
|
294
|
+
context: {
|
|
295
|
+
[`${PLUGIN_ID}:config`]: config
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
getActions: ()=>({
|
|
300
|
+
optimize: {
|
|
301
|
+
useOptimize: (input, overrideConfig)=>useOptimize(input, overrideConfig ?? config)
|
|
302
|
+
}
|
|
303
|
+
}),
|
|
304
|
+
atomListeners: [
|
|
305
|
+
{
|
|
306
|
+
matcher: (path)=>path.startsWith('/media/createSignedUpload'),
|
|
307
|
+
signal: '$mediaSignal'
|
|
308
|
+
}
|
|
309
|
+
]
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
exports.mediaOptimizeClient = mediaOptimizeClient;
|
|
314
|
+
exports.optimizeImage = optimizeImage;
|
|
315
|
+
exports.useOptimize = useOptimize;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { WritableAtom } from 'nanostores';
|
|
2
|
+
|
|
3
|
+
type CompressOptions = {
|
|
4
|
+
/** JPEG/PNG quality (1-100). @default 80 */
|
|
5
|
+
quality?: number;
|
|
6
|
+
};
|
|
7
|
+
type ResizeOptions = {
|
|
8
|
+
/** Maximum width/height in pixels. @default 2000 */
|
|
9
|
+
maxSize?: number;
|
|
10
|
+
};
|
|
11
|
+
type ConvertOptions = {
|
|
12
|
+
/** Only WebP conversion is supported. */
|
|
13
|
+
format: 'webp';
|
|
14
|
+
/**
|
|
15
|
+
* When true, also store a copy in the original format (resized + compressed
|
|
16
|
+
* but not converted). Useful for email clients that don't support WebP.
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
storeOriginal?: boolean;
|
|
20
|
+
};
|
|
21
|
+
type OptimizationConfig = {
|
|
22
|
+
/** Image compression options. Omit to disable. */
|
|
23
|
+
compress?: CompressOptions;
|
|
24
|
+
/** Resize options. Omit to disable. */
|
|
25
|
+
resize?: ResizeOptions;
|
|
26
|
+
/** Format conversion options. Omit to disable. */
|
|
27
|
+
convert?: ConvertOptions;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface OptimizeResult {
|
|
31
|
+
file: File;
|
|
32
|
+
originalVariant?: File;
|
|
33
|
+
optimized: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Optimize an image file on the client before uploading.
|
|
37
|
+
*
|
|
38
|
+
* - **Resize**: Downscale if either dimension exceeds `config.resize.maxSize`
|
|
39
|
+
* - **Compress**: Reduce quality via `config.compress.quality` (1-100)
|
|
40
|
+
* - **Convert**: Convert to WebP when `config.convert.format` is set
|
|
41
|
+
* - **storeOriginal**: When `convert.storeOriginal` is true, also returns an
|
|
42
|
+
* `originalVariant` -- same resize + compress but kept in the original format
|
|
43
|
+
*
|
|
44
|
+
* Non-image files are returned unchanged with `optimized: false`.
|
|
45
|
+
*/
|
|
46
|
+
declare function optimizeImage(file: File, config: OptimizationConfig): Promise<OptimizeResult>;
|
|
47
|
+
|
|
48
|
+
type OptimizeState = {
|
|
49
|
+
results: OptimizeResult[] | null;
|
|
50
|
+
isOptimizing: boolean;
|
|
51
|
+
error: string | null;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* React hook that optimizes image files on the client.
|
|
55
|
+
*
|
|
56
|
+
* Accepts a single `File` or `File[]` and an `OptimizationConfig`.
|
|
57
|
+
* Runs `optimizeImage` automatically whenever the input reference changes.
|
|
58
|
+
* Non-image files pass through with `optimized: false`.
|
|
59
|
+
*
|
|
60
|
+
* ```tsx
|
|
61
|
+
* const { results, isOptimizing, error } = cmsClient.optimize.useOptimize(file, {
|
|
62
|
+
* compress: { quality: 80 },
|
|
63
|
+
* resize: { maxSize: 1200 },
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function useOptimize(input: File | File[], config: OptimizationConfig): OptimizeState;
|
|
68
|
+
|
|
69
|
+
type CMSFetch = (path: string, options?: {
|
|
70
|
+
method?: string;
|
|
71
|
+
body?: unknown;
|
|
72
|
+
query?: unknown;
|
|
73
|
+
/**
|
|
74
|
+
* Forwarded to the underlying `fetch` (better-call → @better-fetch → native
|
|
75
|
+
* `fetch`). Set for fire-and-forget analytics beacons (the A/B event ingest)
|
|
76
|
+
* so the POST is NOT cancelled when the page unloads/navigates mid-request.
|
|
77
|
+
*/
|
|
78
|
+
keepalive?: boolean;
|
|
79
|
+
}) => Promise<unknown>;
|
|
80
|
+
interface CMSClientStore {
|
|
81
|
+
notify: (signal: string) => void;
|
|
82
|
+
listen: (signal: string, listener: () => void) => void;
|
|
83
|
+
atoms: Record<string, WritableAtom<unknown>>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Client plugin that adds image optimization under its own namespace.
|
|
88
|
+
*
|
|
89
|
+
* Exposes `cmsClient.optimize.useOptimize(file, config)` for client-side
|
|
90
|
+
* image optimization. Optimized files can then be passed to
|
|
91
|
+
* `cmsClient.media.useUploadAssets().upload(files)`.
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* import { mediaOptimizeClient } from '@createcms/core/plugins/media-optimize';
|
|
95
|
+
*
|
|
96
|
+
* const client = createCMSClient<typeof cms>({
|
|
97
|
+
* baseURL: '/api/cms',
|
|
98
|
+
* plugins: [
|
|
99
|
+
* mediaOptimizeClient({
|
|
100
|
+
* compress: { quality: 90 },
|
|
101
|
+
* resize: { maxSize: 2000 },
|
|
102
|
+
* convert: { format: 'webp', storeOriginal: true },
|
|
103
|
+
* }),
|
|
104
|
+
* ],
|
|
105
|
+
* });
|
|
106
|
+
*
|
|
107
|
+
* // In a component:
|
|
108
|
+
* const { results, isOptimizing } = client.optimize.useOptimize(file, config);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare function mediaOptimizeClient(config: OptimizationConfig): {
|
|
112
|
+
id: "media-optimize";
|
|
113
|
+
$ERROR_CODES: {
|
|
114
|
+
readonly OPTIMIZATION_FAILED: {
|
|
115
|
+
readonly status: 422;
|
|
116
|
+
readonly message: "Image optimization failed";
|
|
117
|
+
};
|
|
118
|
+
readonly WEBP_NOT_SUPPORTED: {
|
|
119
|
+
readonly status: 422;
|
|
120
|
+
readonly message: "WebP encoding is not supported in this browser and @jsquash/webp is not installed";
|
|
121
|
+
};
|
|
122
|
+
readonly CANVAS_CONTEXT_FAILED: {
|
|
123
|
+
readonly status: 500;
|
|
124
|
+
readonly message: "Failed to acquire canvas 2D context for image processing";
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
init(_$fetch: CMSFetch, _$store: CMSClientStore): Promise<{
|
|
128
|
+
context: {
|
|
129
|
+
[x: string]: OptimizationConfig;
|
|
130
|
+
};
|
|
131
|
+
}>;
|
|
132
|
+
getActions: () => {
|
|
133
|
+
optimize: {
|
|
134
|
+
useOptimize: (input: File | File[], overrideConfig?: OptimizationConfig) => OptimizeState;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
atomListeners: {
|
|
138
|
+
matcher: (path: string) => boolean;
|
|
139
|
+
signal: "$mediaSignal";
|
|
140
|
+
}[];
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export { mediaOptimizeClient, optimizeImage, useOptimize };
|
|
144
|
+
export type { OptimizeResult, OptimizeState };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { WritableAtom } from 'nanostores';
|
|
2
|
+
|
|
3
|
+
type CompressOptions = {
|
|
4
|
+
/** JPEG/PNG quality (1-100). @default 80 */
|
|
5
|
+
quality?: number;
|
|
6
|
+
};
|
|
7
|
+
type ResizeOptions = {
|
|
8
|
+
/** Maximum width/height in pixels. @default 2000 */
|
|
9
|
+
maxSize?: number;
|
|
10
|
+
};
|
|
11
|
+
type ConvertOptions = {
|
|
12
|
+
/** Only WebP conversion is supported. */
|
|
13
|
+
format: 'webp';
|
|
14
|
+
/**
|
|
15
|
+
* When true, also store a copy in the original format (resized + compressed
|
|
16
|
+
* but not converted). Useful for email clients that don't support WebP.
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
storeOriginal?: boolean;
|
|
20
|
+
};
|
|
21
|
+
type OptimizationConfig = {
|
|
22
|
+
/** Image compression options. Omit to disable. */
|
|
23
|
+
compress?: CompressOptions;
|
|
24
|
+
/** Resize options. Omit to disable. */
|
|
25
|
+
resize?: ResizeOptions;
|
|
26
|
+
/** Format conversion options. Omit to disable. */
|
|
27
|
+
convert?: ConvertOptions;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface OptimizeResult {
|
|
31
|
+
file: File;
|
|
32
|
+
originalVariant?: File;
|
|
33
|
+
optimized: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Optimize an image file on the client before uploading.
|
|
37
|
+
*
|
|
38
|
+
* - **Resize**: Downscale if either dimension exceeds `config.resize.maxSize`
|
|
39
|
+
* - **Compress**: Reduce quality via `config.compress.quality` (1-100)
|
|
40
|
+
* - **Convert**: Convert to WebP when `config.convert.format` is set
|
|
41
|
+
* - **storeOriginal**: When `convert.storeOriginal` is true, also returns an
|
|
42
|
+
* `originalVariant` -- same resize + compress but kept in the original format
|
|
43
|
+
*
|
|
44
|
+
* Non-image files are returned unchanged with `optimized: false`.
|
|
45
|
+
*/
|
|
46
|
+
declare function optimizeImage(file: File, config: OptimizationConfig): Promise<OptimizeResult>;
|
|
47
|
+
|
|
48
|
+
type OptimizeState = {
|
|
49
|
+
results: OptimizeResult[] | null;
|
|
50
|
+
isOptimizing: boolean;
|
|
51
|
+
error: string | null;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* React hook that optimizes image files on the client.
|
|
55
|
+
*
|
|
56
|
+
* Accepts a single `File` or `File[]` and an `OptimizationConfig`.
|
|
57
|
+
* Runs `optimizeImage` automatically whenever the input reference changes.
|
|
58
|
+
* Non-image files pass through with `optimized: false`.
|
|
59
|
+
*
|
|
60
|
+
* ```tsx
|
|
61
|
+
* const { results, isOptimizing, error } = cmsClient.optimize.useOptimize(file, {
|
|
62
|
+
* compress: { quality: 80 },
|
|
63
|
+
* resize: { maxSize: 1200 },
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function useOptimize(input: File | File[], config: OptimizationConfig): OptimizeState;
|
|
68
|
+
|
|
69
|
+
type CMSFetch = (path: string, options?: {
|
|
70
|
+
method?: string;
|
|
71
|
+
body?: unknown;
|
|
72
|
+
query?: unknown;
|
|
73
|
+
/**
|
|
74
|
+
* Forwarded to the underlying `fetch` (better-call → @better-fetch → native
|
|
75
|
+
* `fetch`). Set for fire-and-forget analytics beacons (the A/B event ingest)
|
|
76
|
+
* so the POST is NOT cancelled when the page unloads/navigates mid-request.
|
|
77
|
+
*/
|
|
78
|
+
keepalive?: boolean;
|
|
79
|
+
}) => Promise<unknown>;
|
|
80
|
+
interface CMSClientStore {
|
|
81
|
+
notify: (signal: string) => void;
|
|
82
|
+
listen: (signal: string, listener: () => void) => void;
|
|
83
|
+
atoms: Record<string, WritableAtom<unknown>>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Client plugin that adds image optimization under its own namespace.
|
|
88
|
+
*
|
|
89
|
+
* Exposes `cmsClient.optimize.useOptimize(file, config)` for client-side
|
|
90
|
+
* image optimization. Optimized files can then be passed to
|
|
91
|
+
* `cmsClient.media.useUploadAssets().upload(files)`.
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* import { mediaOptimizeClient } from '@createcms/core/plugins/media-optimize';
|
|
95
|
+
*
|
|
96
|
+
* const client = createCMSClient<typeof cms>({
|
|
97
|
+
* baseURL: '/api/cms',
|
|
98
|
+
* plugins: [
|
|
99
|
+
* mediaOptimizeClient({
|
|
100
|
+
* compress: { quality: 90 },
|
|
101
|
+
* resize: { maxSize: 2000 },
|
|
102
|
+
* convert: { format: 'webp', storeOriginal: true },
|
|
103
|
+
* }),
|
|
104
|
+
* ],
|
|
105
|
+
* });
|
|
106
|
+
*
|
|
107
|
+
* // In a component:
|
|
108
|
+
* const { results, isOptimizing } = client.optimize.useOptimize(file, config);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare function mediaOptimizeClient(config: OptimizationConfig): {
|
|
112
|
+
id: "media-optimize";
|
|
113
|
+
$ERROR_CODES: {
|
|
114
|
+
readonly OPTIMIZATION_FAILED: {
|
|
115
|
+
readonly status: 422;
|
|
116
|
+
readonly message: "Image optimization failed";
|
|
117
|
+
};
|
|
118
|
+
readonly WEBP_NOT_SUPPORTED: {
|
|
119
|
+
readonly status: 422;
|
|
120
|
+
readonly message: "WebP encoding is not supported in this browser and @jsquash/webp is not installed";
|
|
121
|
+
};
|
|
122
|
+
readonly CANVAS_CONTEXT_FAILED: {
|
|
123
|
+
readonly status: 500;
|
|
124
|
+
readonly message: "Failed to acquire canvas 2D context for image processing";
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
init(_$fetch: CMSFetch, _$store: CMSClientStore): Promise<{
|
|
128
|
+
context: {
|
|
129
|
+
[x: string]: OptimizationConfig;
|
|
130
|
+
};
|
|
131
|
+
}>;
|
|
132
|
+
getActions: () => {
|
|
133
|
+
optimize: {
|
|
134
|
+
useOptimize: (input: File | File[], overrideConfig?: OptimizationConfig) => OptimizeState;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
atomListeners: {
|
|
138
|
+
matcher: (path: string) => boolean;
|
|
139
|
+
signal: "$mediaSignal";
|
|
140
|
+
}[];
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export { mediaOptimizeClient, optimizeImage, useOptimize };
|
|
144
|
+
export type { OptimizeResult, OptimizeState };
|