@inoo-ch/payload-image-optimizer 1.0.0 → 1.1.0
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 +18 -1
- package/dist/components/RegenerationButton.js +220 -16
- package/dist/components/RegenerationButton.js.map +1 -1
- package/dist/defaults.js +5 -6
- package/dist/defaults.js.map +1 -1
- package/dist/hooks/afterChange.js +38 -13
- package/dist/hooks/afterChange.js.map +1 -1
- package/dist/hooks/beforeChange.js +21 -4
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/tasks/convertFormats.js +4 -1
- package/dist/tasks/convertFormats.js.map +1 -1
- package/dist/tasks/regenerateDocument.js +43 -17
- package/dist/tasks/regenerateDocument.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ imageOptimizer({
|
|
|
90
90
|
| Option | Type | Default | Description |
|
|
91
91
|
|---|---|---|---|
|
|
92
92
|
| `collections` | `Record<string, true \| CollectionConfig>` | *required* | Collections to optimize. Use `true` for defaults or an object for overrides. |
|
|
93
|
-
| `formats` | `FormatQuality[]` | `[{ format: 'webp', quality: 80 }
|
|
93
|
+
| `formats` | `FormatQuality[]` | `[{ format: 'webp', quality: 80 }]` | Output formats and quality (1-100). |
|
|
94
94
|
| `maxDimensions` | `{ width: number, height: number }` | `{ width: 2560, height: 2560 }` | Maximum image dimensions. Images are resized to fit within these bounds. |
|
|
95
95
|
| `generateThumbHash` | `boolean` | `true` | Generate ThumbHash blur placeholders for instant image previews. |
|
|
96
96
|
| `stripMetadata` | `boolean` | `true` | Remove EXIF and other metadata from images. |
|
|
@@ -212,6 +212,23 @@ GET /api/image-optimizer/regenerate?collection=media
|
|
|
212
212
|
|
|
213
213
|
Both endpoints require an authenticated user.
|
|
214
214
|
|
|
215
|
+
## AI Agent Integration
|
|
216
|
+
|
|
217
|
+
Full technical documentation for AI coding agents is available in [`AGENT_DOCS.md`](./AGENT_DOCS.md). It covers all configuration options, field schemas, endpoints, client utilities, background jobs, and context flags in a single reference file.
|
|
218
|
+
|
|
219
|
+
### Prompt for AI Agents
|
|
220
|
+
|
|
221
|
+
Copy-paste this instruction to your AI coding agent to have it autonomously integrate the plugin:
|
|
222
|
+
|
|
223
|
+
> Install `@inoo-ch/payload-image-optimizer` and integrate it into this Payload CMS project. Read `node_modules/@inoo-ch/payload-image-optimizer/AGENT_DOCS.md` for the full API reference. Based on the project's existing upload collections and frontend, decide on your own:
|
|
224
|
+
>
|
|
225
|
+
> 1. Which upload collections should be optimized and with what settings
|
|
226
|
+
> 2. Whether to use `replaceOriginal` or keep originals alongside variants
|
|
227
|
+
> 3. Where to add `<ImageBox>` or `getImageOptimizerProps()` in the frontend for ThumbHash blur placeholders and focal point support
|
|
228
|
+
> 4. Whether any existing image rendering code should use the optimized variants
|
|
229
|
+
>
|
|
230
|
+
> Use the zero-config default (`collections: { <slug>: true }`) unless the project has specific requirements that call for custom settings.
|
|
231
|
+
|
|
215
232
|
## Contributing
|
|
216
233
|
|
|
217
234
|
This plugin is open source and we welcome community involvement:
|
|
@@ -1,19 +1,48 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
const STALL_THRESHOLD = 5;
|
|
4
5
|
export const RegenerationButton = ()=>{
|
|
5
6
|
const [isRunning, setIsRunning] = useState(false);
|
|
6
7
|
const [progress, setProgress] = useState(null);
|
|
7
8
|
const [queued, setQueued] = useState(null);
|
|
8
9
|
const [force, setForce] = useState(false);
|
|
9
10
|
const [error, setError] = useState(null);
|
|
11
|
+
const [stalled, setStalled] = useState(false);
|
|
10
12
|
const [collectionSlug, setCollectionSlug] = useState(null);
|
|
13
|
+
const [stats, setStats] = useState(null);
|
|
11
14
|
const intervalRef = useRef(null);
|
|
15
|
+
const stallRef = useRef({
|
|
16
|
+
lastProcessed: 0,
|
|
17
|
+
stallCount: 0
|
|
18
|
+
});
|
|
19
|
+
const prevIsRunningRef = useRef(false);
|
|
12
20
|
// Extract collection slug from URL after mount to avoid hydration mismatch
|
|
13
21
|
useEffect(()=>{
|
|
14
22
|
const slug = window.location.pathname.split('/collections/')[1]?.split('/')[0] ?? null;
|
|
15
23
|
setCollectionSlug(slug);
|
|
16
24
|
}, []);
|
|
25
|
+
// Fetch optimization stats (independent of regeneration)
|
|
26
|
+
const fetchStats = useCallback(async ()=>{
|
|
27
|
+
if (!collectionSlug) return;
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`/api/image-optimizer/regenerate?collection=${collectionSlug}`);
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
setStats(data);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// ignore stats fetch errors
|
|
36
|
+
}
|
|
37
|
+
}, [
|
|
38
|
+
collectionSlug
|
|
39
|
+
]);
|
|
40
|
+
const stopPolling = useCallback(()=>{
|
|
41
|
+
if (intervalRef.current) {
|
|
42
|
+
clearInterval(intervalRef.current);
|
|
43
|
+
intervalRef.current = null;
|
|
44
|
+
}
|
|
45
|
+
}, []);
|
|
17
46
|
const pollProgress = useCallback(async ()=>{
|
|
18
47
|
if (!collectionSlug) return;
|
|
19
48
|
try {
|
|
@@ -24,24 +53,85 @@ export const RegenerationButton = ()=>{
|
|
|
24
53
|
// Stop polling when no more pending
|
|
25
54
|
if (data.pending <= 0) {
|
|
26
55
|
setIsRunning(false);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
56
|
+
stopPolling();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Stall detection
|
|
60
|
+
const processed = data.complete + data.errored;
|
|
61
|
+
if (processed === stallRef.current.lastProcessed) {
|
|
62
|
+
stallRef.current.stallCount += 1;
|
|
63
|
+
} else {
|
|
64
|
+
stallRef.current.stallCount = 0;
|
|
65
|
+
stallRef.current.lastProcessed = processed;
|
|
66
|
+
}
|
|
67
|
+
if (stallRef.current.stallCount >= STALL_THRESHOLD) {
|
|
68
|
+
stopPolling();
|
|
69
|
+
setIsRunning(false);
|
|
70
|
+
setStalled(true);
|
|
31
71
|
}
|
|
32
72
|
}
|
|
33
73
|
} catch {
|
|
34
74
|
// ignore polling errors
|
|
35
75
|
}
|
|
36
76
|
}, [
|
|
37
|
-
collectionSlug
|
|
77
|
+
collectionSlug,
|
|
78
|
+
stopPolling
|
|
79
|
+
]);
|
|
80
|
+
// On mount (once collectionSlug is known), check if there's an ongoing job and resume polling
|
|
81
|
+
useEffect(()=>{
|
|
82
|
+
if (!collectionSlug) return;
|
|
83
|
+
let cancelled = false;
|
|
84
|
+
const checkOngoing = async ()=>{
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(`/api/image-optimizer/regenerate?collection=${collectionSlug}`);
|
|
87
|
+
if (!res.ok || cancelled) return;
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
// Always store stats on mount
|
|
90
|
+
setStats(data);
|
|
91
|
+
if (data.pending > 0) {
|
|
92
|
+
setProgress(data);
|
|
93
|
+
setIsRunning(true);
|
|
94
|
+
setStalled(false);
|
|
95
|
+
setQueued(null);
|
|
96
|
+
stallRef.current = {
|
|
97
|
+
lastProcessed: data.complete + data.errored,
|
|
98
|
+
stallCount: 0
|
|
99
|
+
};
|
|
100
|
+
intervalRef.current = setInterval(pollProgress, 2000);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
checkOngoing();
|
|
107
|
+
return ()=>{
|
|
108
|
+
cancelled = true;
|
|
109
|
+
};
|
|
110
|
+
}, [
|
|
111
|
+
collectionSlug,
|
|
112
|
+
pollProgress
|
|
113
|
+
]);
|
|
114
|
+
// Refresh stats when regeneration finishes (isRunning transitions from true to false)
|
|
115
|
+
useEffect(()=>{
|
|
116
|
+
if (prevIsRunningRef.current && !isRunning) {
|
|
117
|
+
fetchStats();
|
|
118
|
+
}
|
|
119
|
+
prevIsRunningRef.current = isRunning;
|
|
120
|
+
}, [
|
|
121
|
+
isRunning,
|
|
122
|
+
fetchStats
|
|
38
123
|
]);
|
|
39
124
|
const handleRegenerate = async ()=>{
|
|
40
125
|
if (!collectionSlug) return;
|
|
41
126
|
setError(null);
|
|
127
|
+
setStalled(false);
|
|
42
128
|
setIsRunning(true);
|
|
43
129
|
setQueued(null);
|
|
44
130
|
setProgress(null);
|
|
131
|
+
stallRef.current = {
|
|
132
|
+
lastProcessed: 0,
|
|
133
|
+
stallCount: 0
|
|
134
|
+
};
|
|
45
135
|
try {
|
|
46
136
|
const res = await fetch('/api/image-optimizer/regenerate', {
|
|
47
137
|
method: 'POST',
|
|
@@ -78,6 +168,10 @@ export const RegenerationButton = ()=>{
|
|
|
78
168
|
}, []);
|
|
79
169
|
if (!collectionSlug) return null;
|
|
80
170
|
const progressPercent = progress && progress.total > 0 ? Math.round(progress.complete / progress.total * 100) : 0;
|
|
171
|
+
const showProgressBar = isRunning && progress || stalled && progress;
|
|
172
|
+
// Stats computations
|
|
173
|
+
const statsPercent = stats && stats.total > 0 ? Math.round(stats.complete / stats.total * 100) : 0;
|
|
174
|
+
const allOptimized = stats && stats.total > 0 && stats.complete === stats.total;
|
|
81
175
|
return /*#__PURE__*/ _jsxs("div", {
|
|
82
176
|
style: {
|
|
83
177
|
padding: '16px 24px',
|
|
@@ -127,14 +221,27 @@ export const RegenerationButton = ()=>{
|
|
|
127
221
|
},
|
|
128
222
|
children: error
|
|
129
223
|
}),
|
|
130
|
-
queued === 0 && !isRunning && /*#__PURE__*/ _jsx("span", {
|
|
224
|
+
queued === 0 && !isRunning && !stalled && /*#__PURE__*/ _jsx("span", {
|
|
131
225
|
style: {
|
|
132
226
|
color: '#10b981',
|
|
133
227
|
fontSize: '13px'
|
|
134
228
|
},
|
|
135
229
|
children: "All images already optimized."
|
|
136
230
|
}),
|
|
137
|
-
|
|
231
|
+
stalled && progress && /*#__PURE__*/ _jsxs("span", {
|
|
232
|
+
style: {
|
|
233
|
+
color: '#f59e0b',
|
|
234
|
+
fontSize: '13px'
|
|
235
|
+
},
|
|
236
|
+
children: [
|
|
237
|
+
"Process stalled. ",
|
|
238
|
+
progress.pending,
|
|
239
|
+
" image",
|
|
240
|
+
progress.pending !== 1 ? 's' : '',
|
|
241
|
+
" failed to process."
|
|
242
|
+
]
|
|
243
|
+
}),
|
|
244
|
+
showProgressBar && /*#__PURE__*/ _jsxs("div", {
|
|
138
245
|
style: {
|
|
139
246
|
flex: 1,
|
|
140
247
|
minWidth: '200px'
|
|
@@ -184,7 +291,7 @@ export const RegenerationButton = ()=>{
|
|
|
184
291
|
style: {
|
|
185
292
|
height: '100%',
|
|
186
293
|
width: `${progressPercent}%`,
|
|
187
|
-
backgroundColor: '#10b981',
|
|
294
|
+
backgroundColor: stalled ? '#f59e0b' : '#10b981',
|
|
188
295
|
borderRadius: '3px',
|
|
189
296
|
transition: 'width 0.3s ease'
|
|
190
297
|
}
|
|
@@ -192,17 +299,114 @@ export const RegenerationButton = ()=>{
|
|
|
192
299
|
})
|
|
193
300
|
]
|
|
194
301
|
}),
|
|
195
|
-
!isRunning && progress && progress.complete > 0 && queued !== 0 && /*#__PURE__*/ _jsxs("span", {
|
|
302
|
+
!isRunning && !stalled && progress && progress.complete > 0 && queued !== 0 && /*#__PURE__*/ _jsxs("span", {
|
|
196
303
|
style: {
|
|
197
|
-
color: '#10b981',
|
|
198
304
|
fontSize: '13px'
|
|
199
305
|
},
|
|
200
306
|
children: [
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
307
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
308
|
+
style: {
|
|
309
|
+
color: '#10b981'
|
|
310
|
+
},
|
|
311
|
+
children: [
|
|
312
|
+
"Done! ",
|
|
313
|
+
progress.complete,
|
|
314
|
+
"/",
|
|
315
|
+
progress.total,
|
|
316
|
+
" optimized."
|
|
317
|
+
]
|
|
318
|
+
}),
|
|
319
|
+
progress.errored > 0 && /*#__PURE__*/ _jsxs("span", {
|
|
320
|
+
style: {
|
|
321
|
+
color: '#ef4444'
|
|
322
|
+
},
|
|
323
|
+
children: [
|
|
324
|
+
' ',
|
|
325
|
+
progress.errored,
|
|
326
|
+
" failed."
|
|
327
|
+
]
|
|
328
|
+
})
|
|
329
|
+
]
|
|
330
|
+
}),
|
|
331
|
+
!isRunning && stats && stats.total > 0 && /*#__PURE__*/ _jsxs("div", {
|
|
332
|
+
style: {
|
|
333
|
+
marginLeft: 'auto',
|
|
334
|
+
display: 'flex',
|
|
335
|
+
flexDirection: 'column',
|
|
336
|
+
alignItems: 'flex-end',
|
|
337
|
+
gap: '4px',
|
|
338
|
+
minWidth: '180px'
|
|
339
|
+
},
|
|
340
|
+
children: [
|
|
341
|
+
/*#__PURE__*/ _jsx("div", {
|
|
342
|
+
style: {
|
|
343
|
+
display: 'flex',
|
|
344
|
+
alignItems: 'center',
|
|
345
|
+
gap: '8px',
|
|
346
|
+
fontSize: '13px'
|
|
347
|
+
},
|
|
348
|
+
children: allOptimized ? /*#__PURE__*/ _jsxs("span", {
|
|
349
|
+
style: {
|
|
350
|
+
color: '#10b981'
|
|
351
|
+
},
|
|
352
|
+
children: [
|
|
353
|
+
"✓ All ",
|
|
354
|
+
stats.total,
|
|
355
|
+
" images optimized"
|
|
356
|
+
]
|
|
357
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
358
|
+
children: [
|
|
359
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
360
|
+
style: {
|
|
361
|
+
color: '#6b7280'
|
|
362
|
+
},
|
|
363
|
+
children: [
|
|
364
|
+
stats.complete,
|
|
365
|
+
"/",
|
|
366
|
+
stats.total,
|
|
367
|
+
" optimized"
|
|
368
|
+
]
|
|
369
|
+
}),
|
|
370
|
+
stats.errored > 0 && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
371
|
+
children: [
|
|
372
|
+
/*#__PURE__*/ _jsx("span", {
|
|
373
|
+
style: {
|
|
374
|
+
color: '#d1d5db'
|
|
375
|
+
},
|
|
376
|
+
children: "·"
|
|
377
|
+
}),
|
|
378
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
379
|
+
style: {
|
|
380
|
+
color: '#ef4444'
|
|
381
|
+
},
|
|
382
|
+
children: [
|
|
383
|
+
stats.errored,
|
|
384
|
+
" errors"
|
|
385
|
+
]
|
|
386
|
+
})
|
|
387
|
+
]
|
|
388
|
+
})
|
|
389
|
+
]
|
|
390
|
+
})
|
|
391
|
+
}),
|
|
392
|
+
!allOptimized && /*#__PURE__*/ _jsx("div", {
|
|
393
|
+
style: {
|
|
394
|
+
width: '100%',
|
|
395
|
+
height: '3px',
|
|
396
|
+
backgroundColor: '#e5e7eb',
|
|
397
|
+
borderRadius: '2px',
|
|
398
|
+
overflow: 'hidden'
|
|
399
|
+
},
|
|
400
|
+
children: /*#__PURE__*/ _jsx("div", {
|
|
401
|
+
style: {
|
|
402
|
+
height: '100%',
|
|
403
|
+
width: `${statsPercent}%`,
|
|
404
|
+
backgroundColor: stats.errored > 0 ? '#f59e0b' : '#10b981',
|
|
405
|
+
borderRadius: '2px',
|
|
406
|
+
transition: 'width 0.3s ease'
|
|
407
|
+
}
|
|
408
|
+
})
|
|
409
|
+
})
|
|
206
410
|
]
|
|
207
411
|
})
|
|
208
412
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/RegenerationButton.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState, useEffect, useCallback, useRef } from 'react'\n\ntype RegenerationProgress = {\n total: number\n complete: number\n errored: number\n pending: number\n}\n\nexport const RegenerationButton: React.FC = () => {\n const [isRunning, setIsRunning] = useState(false)\n const [progress, setProgress] = useState<RegenerationProgress | null>(null)\n const [queued, setQueued] = useState<number | null>(null)\n const [force, setForce] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [collectionSlug, setCollectionSlug] = useState<string | null>(null)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n\n // Extract collection slug from URL after mount to avoid hydration mismatch\n useEffect(() => {\n const slug = window.location.pathname.split('/collections/')[1]?.split('/')[0] ?? null\n setCollectionSlug(slug)\n }, [])\n\n const pollProgress = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data = await res.json()\n setProgress(data)\n // Stop polling when no more pending\n if (data.pending <= 0) {\n setIsRunning(false)\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }\n }\n } catch {\n // ignore polling errors\n }\n }, [collectionSlug])\n\n const handleRegenerate = async () => {\n if (!collectionSlug) return\n setError(null)\n setIsRunning(true)\n setQueued(null)\n setProgress(null)\n\n try {\n const res = await fetch('/api/image-optimizer/regenerate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ collectionSlug, force }),\n })\n\n if (!res.ok) {\n const data = await res.json()\n throw new Error(data.error || 'Failed to start regeneration')\n }\n\n const data = await res.json()\n setQueued(data.queued)\n\n if (data.queued === 0) {\n setIsRunning(false)\n return\n }\n\n // Start polling\n intervalRef.current = setInterval(pollProgress, 2000)\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err))\n setIsRunning(false)\n }\n }\n\n // Cleanup interval on unmount\n useEffect(() => {\n return () => {\n if (intervalRef.current) clearInterval(intervalRef.current)\n }\n }, [])\n\n if (!collectionSlug) return null\n\n const progressPercent =\n progress && progress.total > 0\n ? Math.round((progress.complete / progress.total) * 100)\n : 0\n\n return (\n <div\n style={{\n padding: '16px 24px',\n borderBottom: '1px solid #e5e7eb',\n display: 'flex',\n alignItems: 'center',\n gap: '16px',\n flexWrap: 'wrap',\n }}\n >\n <button\n onClick={handleRegenerate}\n disabled={isRunning}\n style={{\n backgroundColor: isRunning ? '#9ca3af' : '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '8px 16px',\n fontSize: '14px',\n fontWeight: 500,\n cursor: isRunning ? 'not-allowed' : 'pointer',\n }}\n >\n {isRunning ? 'Regenerating...' : 'Regenerate Images'}\n </button>\n\n <label\n style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' }}\n >\n <input\n type=\"checkbox\"\n checked={force}\n onChange={(e) => setForce(e.target.checked)}\n disabled={isRunning}\n />\n Force re-process all\n </label>\n\n {error && (\n <span style={{ color: '#ef4444', fontSize: '13px' }}>{error}</span>\n )}\n\n {queued === 0 && !isRunning && (\n <span style={{ color: '#10b981', fontSize: '13px' }}>\n All images already optimized.\n </span>\n )}\n\n {isRunning && progress && (\n <div style={{ flex: 1, minWidth: '200px' }}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: '12px',\n marginBottom: '4px',\n }}\n >\n <span>\n {progress.complete} / {progress.total} complete\n </span>\n {progress.errored > 0 && (\n <span style={{ color: '#ef4444' }}>{progress.errored} errors</span>\n )}\n <span>{progressPercent}%</span>\n </div>\n <div\n style={{\n height: '6px',\n backgroundColor: '#e5e7eb',\n borderRadius: '3px',\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${progressPercent}%`,\n backgroundColor: '#10b981',\n borderRadius: '3px',\n transition: 'width 0.3s ease',\n }}\n />\n </div>\n </div>\n )}\n\n {!isRunning && progress && progress.complete > 0 && queued !== 0 && (\n <span style={{ color: '#10b981', fontSize: '13px' }}>\n Done! {progress.complete}/{progress.total} optimized.\n </span>\n )}\n </div>\n )\n}\n"],"names":["React","useState","useEffect","useCallback","useRef","RegenerationButton","isRunning","setIsRunning","progress","setProgress","queued","setQueued","force","setForce","error","setError","collectionSlug","setCollectionSlug","intervalRef","slug","window","location","pathname","split","pollProgress","res","fetch","ok","data","json","pending","current","clearInterval","handleRegenerate","method","headers","body","JSON","stringify","Error","setInterval","err","message","String","progressPercent","total","Math","round","complete","div","style","padding","borderBottom","display","alignItems","gap","flexWrap","button","onClick","disabled","backgroundColor","color","border","borderRadius","fontSize","fontWeight","cursor","label","input","type","checked","onChange","e","target","span","flex","minWidth","justifyContent","marginBottom","errored","height","overflow","width","transition"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/components/RegenerationButton.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState, useEffect, useCallback, useRef } from 'react'\n\ntype RegenerationProgress = {\n total: number\n complete: number\n errored: number\n pending: number\n}\n\nconst STALL_THRESHOLD = 5\n\nexport const RegenerationButton: React.FC = () => {\n const [isRunning, setIsRunning] = useState(false)\n const [progress, setProgress] = useState<RegenerationProgress | null>(null)\n const [queued, setQueued] = useState<number | null>(null)\n const [force, setForce] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [stalled, setStalled] = useState(false)\n const [collectionSlug, setCollectionSlug] = useState<string | null>(null)\n const [stats, setStats] = useState<RegenerationProgress | null>(null)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const stallRef = useRef({ lastProcessed: 0, stallCount: 0 })\n const prevIsRunningRef = useRef(false)\n\n // Extract collection slug from URL after mount to avoid hydration mismatch\n useEffect(() => {\n const slug = window.location.pathname.split('/collections/')[1]?.split('/')[0] ?? null\n setCollectionSlug(slug)\n }, [])\n\n // Fetch optimization stats (independent of regeneration)\n const fetchStats = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data: RegenerationProgress = await res.json()\n setStats(data)\n }\n } catch {\n // ignore stats fetch errors\n }\n }, [collectionSlug])\n\n const stopPolling = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }, [])\n\n const pollProgress = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data: RegenerationProgress = await res.json()\n setProgress(data)\n\n // Stop polling when no more pending\n if (data.pending <= 0) {\n setIsRunning(false)\n stopPolling()\n return\n }\n\n // Stall detection\n const processed = data.complete + data.errored\n if (processed === stallRef.current.lastProcessed) {\n stallRef.current.stallCount += 1\n } else {\n stallRef.current.stallCount = 0\n stallRef.current.lastProcessed = processed\n }\n\n if (stallRef.current.stallCount >= STALL_THRESHOLD) {\n stopPolling()\n setIsRunning(false)\n setStalled(true)\n }\n }\n } catch {\n // ignore polling errors\n }\n }, [collectionSlug, stopPolling])\n\n // On mount (once collectionSlug is known), check if there's an ongoing job and resume polling\n useEffect(() => {\n if (!collectionSlug) return\n let cancelled = false\n const checkOngoing = async () => {\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (!res.ok || cancelled) return\n const data: RegenerationProgress = await res.json()\n // Always store stats on mount\n setStats(data)\n if (data.pending > 0) {\n setProgress(data)\n setIsRunning(true)\n setStalled(false)\n setQueued(null)\n stallRef.current = { lastProcessed: data.complete + data.errored, stallCount: 0 }\n intervalRef.current = setInterval(pollProgress, 2000)\n }\n } catch {\n // ignore\n }\n }\n checkOngoing()\n return () => {\n cancelled = true\n }\n }, [collectionSlug, pollProgress])\n\n // Refresh stats when regeneration finishes (isRunning transitions from true to false)\n useEffect(() => {\n if (prevIsRunningRef.current && !isRunning) {\n fetchStats()\n }\n prevIsRunningRef.current = isRunning\n }, [isRunning, fetchStats])\n\n const handleRegenerate = async () => {\n if (!collectionSlug) return\n setError(null)\n setStalled(false)\n setIsRunning(true)\n setQueued(null)\n setProgress(null)\n stallRef.current = { lastProcessed: 0, stallCount: 0 }\n\n try {\n const res = await fetch('/api/image-optimizer/regenerate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ collectionSlug, force }),\n })\n\n if (!res.ok) {\n const data = await res.json()\n throw new Error(data.error || 'Failed to start regeneration')\n }\n\n const data = await res.json()\n setQueued(data.queued)\n\n if (data.queued === 0) {\n setIsRunning(false)\n return\n }\n\n // Start polling\n intervalRef.current = setInterval(pollProgress, 2000)\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err))\n setIsRunning(false)\n }\n }\n\n // Cleanup interval on unmount\n useEffect(() => {\n return () => {\n if (intervalRef.current) clearInterval(intervalRef.current)\n }\n }, [])\n\n if (!collectionSlug) return null\n\n const progressPercent =\n progress && progress.total > 0\n ? Math.round((progress.complete / progress.total) * 100)\n : 0\n\n const showProgressBar = (isRunning && progress) || (stalled && progress)\n\n // Stats computations\n const statsPercent =\n stats && stats.total > 0\n ? Math.round((stats.complete / stats.total) * 100)\n : 0\n const allOptimized = stats && stats.total > 0 && stats.complete === stats.total\n\n return (\n <div\n style={{\n padding: '16px 24px',\n borderBottom: '1px solid #e5e7eb',\n display: 'flex',\n alignItems: 'center',\n gap: '16px',\n flexWrap: 'wrap',\n }}\n >\n <button\n onClick={handleRegenerate}\n disabled={isRunning}\n style={{\n backgroundColor: isRunning ? '#9ca3af' : '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '8px 16px',\n fontSize: '14px',\n fontWeight: 500,\n cursor: isRunning ? 'not-allowed' : 'pointer',\n }}\n >\n {isRunning ? 'Regenerating...' : 'Regenerate Images'}\n </button>\n\n <label\n style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' }}\n >\n <input\n type=\"checkbox\"\n checked={force}\n onChange={(e) => setForce(e.target.checked)}\n disabled={isRunning}\n />\n Force re-process all\n </label>\n\n {error && (\n <span style={{ color: '#ef4444', fontSize: '13px' }}>{error}</span>\n )}\n\n {queued === 0 && !isRunning && !stalled && (\n <span style={{ color: '#10b981', fontSize: '13px' }}>\n All images already optimized.\n </span>\n )}\n\n {stalled && progress && (\n <span style={{ color: '#f59e0b', fontSize: '13px' }}>\n Process stalled. {progress.pending} image{progress.pending !== 1 ? 's' : ''} failed to process.\n </span>\n )}\n\n {showProgressBar && (\n <div style={{ flex: 1, minWidth: '200px' }}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: '12px',\n marginBottom: '4px',\n }}\n >\n <span>\n {progress.complete} / {progress.total} complete\n </span>\n {progress.errored > 0 && (\n <span style={{ color: '#ef4444' }}>{progress.errored} errors</span>\n )}\n <span>{progressPercent}%</span>\n </div>\n <div\n style={{\n height: '6px',\n backgroundColor: '#e5e7eb',\n borderRadius: '3px',\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${progressPercent}%`,\n backgroundColor: stalled ? '#f59e0b' : '#10b981',\n borderRadius: '3px',\n transition: 'width 0.3s ease',\n }}\n />\n </div>\n </div>\n )}\n\n {!isRunning && !stalled && progress && progress.complete > 0 && queued !== 0 && (\n <span style={{ fontSize: '13px' }}>\n <span style={{ color: '#10b981' }}>\n Done! {progress.complete}/{progress.total} optimized.\n </span>\n {progress.errored > 0 && (\n <span style={{ color: '#ef4444' }}>\n {' '}{progress.errored} failed.\n </span>\n )}\n </span>\n )}\n\n {/* Persistent optimization stats — always visible when not actively regenerating */}\n {!isRunning && stats && stats.total > 0 && (\n <div\n style={{\n marginLeft: 'auto',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-end',\n gap: '4px',\n minWidth: '180px',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>\n {allOptimized ? (\n <span style={{ color: '#10b981' }}>\n ✓ All {stats.total} images optimized\n </span>\n ) : (\n <>\n <span style={{ color: '#6b7280' }}>\n {stats.complete}/{stats.total} optimized\n </span>\n {stats.errored > 0 && (\n <>\n <span style={{ color: '#d1d5db' }}>·</span>\n <span style={{ color: '#ef4444' }}>{stats.errored} errors</span>\n </>\n )}\n </>\n )}\n </div>\n {!allOptimized && (\n <div\n style={{\n width: '100%',\n height: '3px',\n backgroundColor: '#e5e7eb',\n borderRadius: '2px',\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${statsPercent}%`,\n backgroundColor: stats.errored > 0 ? '#f59e0b' : '#10b981',\n borderRadius: '2px',\n transition: 'width 0.3s ease',\n }}\n />\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":["React","useState","useEffect","useCallback","useRef","STALL_THRESHOLD","RegenerationButton","isRunning","setIsRunning","progress","setProgress","queued","setQueued","force","setForce","error","setError","stalled","setStalled","collectionSlug","setCollectionSlug","stats","setStats","intervalRef","stallRef","lastProcessed","stallCount","prevIsRunningRef","slug","window","location","pathname","split","fetchStats","res","fetch","ok","data","json","stopPolling","current","clearInterval","pollProgress","pending","processed","complete","errored","cancelled","checkOngoing","setInterval","handleRegenerate","method","headers","body","JSON","stringify","Error","err","message","String","progressPercent","total","Math","round","showProgressBar","statsPercent","allOptimized","div","style","padding","borderBottom","display","alignItems","gap","flexWrap","button","onClick","disabled","backgroundColor","color","border","borderRadius","fontSize","fontWeight","cursor","label","input","type","checked","onChange","e","target","span","flex","minWidth","justifyContent","marginBottom","height","overflow","width","transition","marginLeft","flexDirection"],"mappings":"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"}
|
package/dist/defaults.js
CHANGED
|
@@ -5,10 +5,6 @@ export const resolveConfig = (config)=>({
|
|
|
5
5
|
{
|
|
6
6
|
format: 'webp',
|
|
7
7
|
quality: 80
|
|
8
|
-
},
|
|
9
|
-
{
|
|
10
|
-
format: 'avif',
|
|
11
|
-
quality: 65
|
|
12
8
|
}
|
|
13
9
|
],
|
|
14
10
|
generateThumbHash: config.generateThumbHash ?? true,
|
|
@@ -16,6 +12,7 @@ export const resolveConfig = (config)=>({
|
|
|
16
12
|
width: 2560,
|
|
17
13
|
height: 2560
|
|
18
14
|
},
|
|
15
|
+
replaceOriginal: config.replaceOriginal ?? true,
|
|
19
16
|
stripMetadata: config.stripMetadata ?? true
|
|
20
17
|
});
|
|
21
18
|
export const resolveCollectionConfig = (resolvedConfig, collectionSlug)=>{
|
|
@@ -23,12 +20,14 @@ export const resolveCollectionConfig = (resolvedConfig, collectionSlug)=>{
|
|
|
23
20
|
if (!collectionValue || collectionValue === true) {
|
|
24
21
|
return {
|
|
25
22
|
formats: resolvedConfig.formats,
|
|
26
|
-
maxDimensions: resolvedConfig.maxDimensions
|
|
23
|
+
maxDimensions: resolvedConfig.maxDimensions,
|
|
24
|
+
replaceOriginal: resolvedConfig.replaceOriginal
|
|
27
25
|
};
|
|
28
26
|
}
|
|
29
27
|
return {
|
|
30
28
|
formats: collectionValue.formats ?? resolvedConfig.formats,
|
|
31
|
-
maxDimensions: collectionValue.maxDimensions ?? resolvedConfig.maxDimensions
|
|
29
|
+
maxDimensions: collectionValue.maxDimensions ?? resolvedConfig.maxDimensions,
|
|
30
|
+
replaceOriginal: collectionValue.replaceOriginal ?? resolvedConfig.replaceOriginal
|
|
32
31
|
};
|
|
33
32
|
};
|
|
34
33
|
|
package/dist/defaults.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nimport type { ImageOptimizerConfig, ResolvedCollectionOptimizerConfig, ResolvedImageOptimizerConfig } from './types.js'\n\nexport const resolveConfig = (config: ImageOptimizerConfig): ResolvedImageOptimizerConfig => ({\n collections: config.collections,\n disabled: config.disabled ?? false,\n formats: config.formats ?? [\n { format: 'webp', quality: 80 },\n
|
|
1
|
+
{"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nimport type { ImageOptimizerConfig, ResolvedCollectionOptimizerConfig, ResolvedImageOptimizerConfig } from './types.js'\n\nexport const resolveConfig = (config: ImageOptimizerConfig): ResolvedImageOptimizerConfig => ({\n collections: config.collections,\n disabled: config.disabled ?? false,\n formats: config.formats ?? [\n { format: 'webp', quality: 80 },\n ],\n generateThumbHash: config.generateThumbHash ?? true,\n maxDimensions: config.maxDimensions ?? { width: 2560, height: 2560 },\n replaceOriginal: config.replaceOriginal ?? true,\n stripMetadata: config.stripMetadata ?? true,\n})\n\nexport const resolveCollectionConfig = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): ResolvedCollectionOptimizerConfig => {\n const collectionValue = resolvedConfig.collections[collectionSlug as CollectionSlug]\n\n if (!collectionValue || collectionValue === true) {\n return {\n formats: resolvedConfig.formats,\n maxDimensions: resolvedConfig.maxDimensions,\n replaceOriginal: resolvedConfig.replaceOriginal,\n }\n }\n\n return {\n formats: collectionValue.formats ?? resolvedConfig.formats,\n maxDimensions: collectionValue.maxDimensions ?? resolvedConfig.maxDimensions,\n replaceOriginal: collectionValue.replaceOriginal ?? resolvedConfig.replaceOriginal,\n }\n}\n"],"names":["resolveConfig","config","collections","disabled","formats","format","quality","generateThumbHash","maxDimensions","width","height","replaceOriginal","stripMetadata","resolveCollectionConfig","resolvedConfig","collectionSlug","collectionValue"],"mappings":"AAIA,OAAO,MAAMA,gBAAgB,CAACC,SAAgE,CAAA;QAC5FC,aAAaD,OAAOC,WAAW;QAC/BC,UAAUF,OAAOE,QAAQ,IAAI;QAC7BC,SAASH,OAAOG,OAAO,IAAI;YACzB;gBAAEC,QAAQ;gBAAQC,SAAS;YAAG;SAC/B;QACDC,mBAAmBN,OAAOM,iBAAiB,IAAI;QAC/CC,eAAeP,OAAOO,aAAa,IAAI;YAAEC,OAAO;YAAMC,QAAQ;QAAK;QACnEC,iBAAiBV,OAAOU,eAAe,IAAI;QAC3CC,eAAeX,OAAOW,aAAa,IAAI;IACzC,CAAA,EAAE;AAEF,OAAO,MAAMC,0BAA0B,CACrCC,gBACAC;IAEA,MAAMC,kBAAkBF,eAAeZ,WAAW,CAACa,eAAiC;IAEpF,IAAI,CAACC,mBAAmBA,oBAAoB,MAAM;QAChD,OAAO;YACLZ,SAASU,eAAeV,OAAO;YAC/BI,eAAeM,eAAeN,aAAa;YAC3CG,iBAAiBG,eAAeH,eAAe;QACjD;IACF;IAEA,OAAO;QACLP,SAASY,gBAAgBZ,OAAO,IAAIU,eAAeV,OAAO;QAC1DI,eAAeQ,gBAAgBR,aAAa,IAAIM,eAAeN,aAAa;QAC5EG,iBAAiBK,gBAAgBL,eAAe,IAAIG,eAAeH,eAAe;IACpF;AACF,EAAC"}
|
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { resolveCollectionConfig } from '../defaults.js';
|
|
3
4
|
export const createAfterChangeHook = (resolvedConfig, collectionSlug)=>{
|
|
4
5
|
return async ({ context, doc, req })=>{
|
|
5
6
|
if (context?.imageOptimizer_skip) return doc;
|
|
6
7
|
if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return doc;
|
|
7
|
-
|
|
8
|
+
const collectionConfig = req.payload.collections[collectionSlug].config;
|
|
9
|
+
let staticDir = typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : '';
|
|
10
|
+
if (staticDir && !path.isAbsolute(staticDir)) {
|
|
11
|
+
staticDir = path.resolve(process.cwd(), staticDir);
|
|
12
|
+
}
|
|
13
|
+
const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug);
|
|
14
|
+
// Overwrite the file on disk with the processed (stripped/resized/converted) buffer
|
|
8
15
|
// Payload 3.0 writes the original buffer to disk; we replace it here
|
|
9
16
|
const processedBuffer = context.imageOptimizer_processedBuffer;
|
|
10
|
-
if (processedBuffer && doc.filename) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
if (processedBuffer && doc.filename && staticDir) {
|
|
18
|
+
const safeFilename = path.basename(doc.filename);
|
|
19
|
+
const filePath = path.join(staticDir, safeFilename);
|
|
20
|
+
await fs.writeFile(filePath, processedBuffer);
|
|
21
|
+
// If replaceOriginal changed the filename, clean up the old file Payload wrote
|
|
22
|
+
const originalFilename = context.imageOptimizer_originalFilename;
|
|
23
|
+
if (originalFilename && originalFilename !== safeFilename) {
|
|
24
|
+
const oldFilePath = path.join(staticDir, path.basename(originalFilename));
|
|
25
|
+
await fs.unlink(oldFilePath).catch(()=>{
|
|
26
|
+
// Old file may not exist if Payload used the new filename
|
|
27
|
+
});
|
|
21
28
|
}
|
|
22
29
|
}
|
|
23
|
-
//
|
|
30
|
+
// When replaceOriginal is on and only one format is configured, the main file
|
|
31
|
+
// is already converted — skip the async job and mark complete immediately.
|
|
32
|
+
if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length <= 1) {
|
|
33
|
+
await req.payload.update({
|
|
34
|
+
collection: collectionSlug,
|
|
35
|
+
id: doc.id,
|
|
36
|
+
data: {
|
|
37
|
+
imageOptimizer: {
|
|
38
|
+
status: 'complete',
|
|
39
|
+
variants: []
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
context: {
|
|
43
|
+
imageOptimizer_skip: true
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return doc;
|
|
47
|
+
}
|
|
48
|
+
// Queue async format conversion job for remaining variants
|
|
24
49
|
await req.payload.jobs.queue({
|
|
25
50
|
task: 'imageOptimizer_convertFormats',
|
|
26
51
|
input: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/afterChange.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\nimport type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\n\nexport const createAfterChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionAfterChangeHook => {\n return async ({ context, doc, req }) => {\n if (context?.imageOptimizer_skip) return doc\n\n if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return doc\n\n // Overwrite the file on disk with the processed (stripped/resized) buffer\n // Payload 3.0 writes the original buffer to disk; we replace it here\n const processedBuffer = context.imageOptimizer_processedBuffer as Buffer | undefined\n if (processedBuffer && doc.filename) {\n const
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/afterChange.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\nimport type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\n\nexport const createAfterChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionAfterChangeHook => {\n return async ({ context, doc, req }) => {\n if (context?.imageOptimizer_skip) return doc\n\n if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return doc\n\n const collectionConfig = req.payload.collections[collectionSlug as keyof typeof req.payload.collections].config\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n\n if (staticDir && !path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)\n\n // Overwrite the file on disk with the processed (stripped/resized/converted) buffer\n // Payload 3.0 writes the original buffer to disk; we replace it here\n const processedBuffer = context.imageOptimizer_processedBuffer as Buffer | undefined\n if (processedBuffer && doc.filename && staticDir) {\n const safeFilename = path.basename(doc.filename as string)\n const filePath = path.join(staticDir, safeFilename)\n await fs.writeFile(filePath, processedBuffer)\n\n // If replaceOriginal changed the filename, clean up the old file Payload wrote\n const originalFilename = context.imageOptimizer_originalFilename as string | undefined\n if (originalFilename && originalFilename !== safeFilename) {\n const oldFilePath = path.join(staticDir, path.basename(originalFilename))\n await fs.unlink(oldFilePath).catch(() => {\n // Old file may not exist if Payload used the new filename\n })\n }\n }\n\n // When replaceOriginal is on and only one format is configured, the main file\n // is already converted — skip the async job and mark complete immediately.\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length <= 1) {\n await req.payload.update({\n collection: collectionSlug,\n id: doc.id,\n data: {\n imageOptimizer: {\n status: 'complete',\n variants: [],\n },\n },\n context: { imageOptimizer_skip: true },\n })\n return doc\n }\n\n // Queue async format conversion job for remaining variants\n await req.payload.jobs.queue({\n task: 'imageOptimizer_convertFormats',\n input: {\n collectionSlug,\n docId: String(doc.id),\n },\n })\n\n req.payload.jobs.run().catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Image optimizer job runner failed')\n })\n\n return doc\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","createAfterChangeHook","resolvedConfig","collectionSlug","context","doc","req","imageOptimizer_skip","file","data","mimetype","startsWith","collectionConfig","payload","collections","config","staticDir","upload","isAbsolute","resolve","process","cwd","perCollectionConfig","processedBuffer","imageOptimizer_processedBuffer","filename","safeFilename","basename","filePath","join","writeFile","originalFilename","imageOptimizer_originalFilename","oldFilePath","unlink","catch","replaceOriginal","formats","length","update","collection","id","imageOptimizer","status","variants","jobs","queue","task","input","docId","String","run","err","logger","error"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,UAAU,OAAM;AAIvB,SAASC,uBAAuB,QAAQ,iBAAgB;AAExD,OAAO,MAAMC,wBAAwB,CACnCC,gBACAC;IAEA,OAAO,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QACjC,IAAIF,SAASG,qBAAqB,OAAOF;QAEzC,IAAI,CAACC,IAAIE,IAAI,IAAI,CAACF,IAAIE,IAAI,CAACC,IAAI,IAAI,CAACH,IAAIE,IAAI,CAACE,QAAQ,EAAEC,WAAW,WAAW,OAAON;QAEpF,MAAMO,mBAAmBN,IAAIO,OAAO,CAACC,WAAW,CAACX,eAAuD,CAACY,MAAM;QAC/G,IAAIC,YACF,OAAOJ,iBAAiBK,MAAM,KAAK,WAAWL,iBAAiBK,MAAM,CAACD,SAAS,IAAI,KAAK;QAE1F,IAAIA,aAAa,CAACjB,KAAKmB,UAAU,CAACF,YAAY;YAC5CA,YAAYjB,KAAKoB,OAAO,CAACC,QAAQC,GAAG,IAAIL;QAC1C;QAEA,MAAMM,sBAAsBtB,wBAAwBE,gBAAgBC;QAEpE,oFAAoF;QACpF,qEAAqE;QACrE,MAAMoB,kBAAkBnB,QAAQoB,8BAA8B;QAC9D,IAAID,mBAAmBlB,IAAIoB,QAAQ,IAAIT,WAAW;YAChD,MAAMU,eAAe3B,KAAK4B,QAAQ,CAACtB,IAAIoB,QAAQ;YAC/C,MAAMG,WAAW7B,KAAK8B,IAAI,CAACb,WAAWU;YACtC,MAAM5B,GAAGgC,SAAS,CAACF,UAAUL;YAE7B,+EAA+E;YAC/E,MAAMQ,mBAAmB3B,QAAQ4B,+BAA+B;YAChE,IAAID,oBAAoBA,qBAAqBL,cAAc;gBACzD,MAAMO,cAAclC,KAAK8B,IAAI,CAACb,WAAWjB,KAAK4B,QAAQ,CAACI;gBACvD,MAAMjC,GAAGoC,MAAM,CAACD,aAAaE,KAAK,CAAC;gBACjC,0DAA0D;gBAC5D;YACF;QACF;QAEA,8EAA8E;QAC9E,2EAA2E;QAC3E,IAAIb,oBAAoBc,eAAe,IAAId,oBAAoBe,OAAO,CAACC,MAAM,IAAI,GAAG;YAClF,MAAMhC,IAAIO,OAAO,CAAC0B,MAAM,CAAC;gBACvBC,YAAYrC;gBACZsC,IAAIpC,IAAIoC,EAAE;gBACVhC,MAAM;oBACJiC,gBAAgB;wBACdC,QAAQ;wBACRC,UAAU,EAAE;oBACd;gBACF;gBACAxC,SAAS;oBAAEG,qBAAqB;gBAAK;YACvC;YACA,OAAOF;QACT;QAEA,2DAA2D;QAC3D,MAAMC,IAAIO,OAAO,CAACgC,IAAI,CAACC,KAAK,CAAC;YAC3BC,MAAM;YACNC,OAAO;gBACL7C;gBACA8C,OAAOC,OAAO7C,IAAIoC,EAAE;YACtB;QACF;QAEAnC,IAAIO,OAAO,CAACgC,IAAI,CAACM,GAAG,GAAGhB,KAAK,CAAC,CAACiB;YAC5B9C,IAAIO,OAAO,CAACwC,MAAM,CAACC,KAAK,CAAC;gBAAEF;YAAI,GAAG;QACpC;QAEA,OAAO/C;IACT;AACF,EAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { resolveCollectionConfig } from '../defaults.js';
|
|
2
|
-
import { generateThumbHash, stripAndResize } from '../processing/index.js';
|
|
3
|
+
import { convertFormat, generateThumbHash, stripAndResize } from '../processing/index.js';
|
|
3
4
|
export const createBeforeChangeHook = (resolvedConfig, collectionSlug)=>{
|
|
4
5
|
return async ({ context, data, req })=>{
|
|
5
6
|
if (context?.imageOptimizer_skip) return data;
|
|
@@ -8,17 +9,33 @@ export const createBeforeChangeHook = (resolvedConfig, collectionSlug)=>{
|
|
|
8
9
|
const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug);
|
|
9
10
|
// Process in memory: strip EXIF, resize, generate blur
|
|
10
11
|
const processed = await stripAndResize(req.file.data, perCollectionConfig.maxDimensions, resolvedConfig.stripMetadata);
|
|
12
|
+
let finalBuffer = processed.buffer;
|
|
13
|
+
let finalSize = processed.size;
|
|
14
|
+
if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {
|
|
15
|
+
// Convert to primary format (first in the formats array)
|
|
16
|
+
const primaryFormat = perCollectionConfig.formats[0];
|
|
17
|
+
const converted = await convertFormat(processed.buffer, primaryFormat.format, primaryFormat.quality);
|
|
18
|
+
finalBuffer = converted.buffer;
|
|
19
|
+
finalSize = converted.size;
|
|
20
|
+
// Update filename and mimeType so Payload stores the correct metadata
|
|
21
|
+
const originalFilename = data.filename || req.file.name || '';
|
|
22
|
+
const newFilename = `${path.parse(originalFilename).name}.${primaryFormat.format}`;
|
|
23
|
+
context.imageOptimizer_originalFilename = originalFilename;
|
|
24
|
+
data.filename = newFilename;
|
|
25
|
+
data.mimeType = converted.mimeType;
|
|
26
|
+
data.filesize = finalSize;
|
|
27
|
+
}
|
|
11
28
|
data.imageOptimizer = {
|
|
12
29
|
originalSize,
|
|
13
|
-
optimizedSize:
|
|
30
|
+
optimizedSize: finalSize,
|
|
14
31
|
status: 'pending'
|
|
15
32
|
};
|
|
16
33
|
if (resolvedConfig.generateThumbHash) {
|
|
17
|
-
data.imageOptimizer.thumbHash = await generateThumbHash(
|
|
34
|
+
data.imageOptimizer.thumbHash = await generateThumbHash(finalBuffer);
|
|
18
35
|
}
|
|
19
36
|
// Store processed buffer in context for afterChange to write to disk
|
|
20
37
|
// (Payload 3.0 does not use modified req.file.data for the disk write)
|
|
21
|
-
context.imageOptimizer_processedBuffer =
|
|
38
|
+
context.imageOptimizer_processedBuffer = finalBuffer;
|
|
22
39
|
return data;
|
|
23
40
|
};
|
|
24
41
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/beforeChange.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { generateThumbHash, stripAndResize } from '../processing/index.js'\n\nexport const createBeforeChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionBeforeChangeHook => {\n return async ({ context, data, req }) => {\n if (context?.imageOptimizer_skip) return data\n\n if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return data\n\n const originalSize = req.file.data.length\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)\n\n // Process in memory: strip EXIF, resize, generate blur\n const processed = await stripAndResize(\n req.file.data,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n data.imageOptimizer = {\n originalSize,\n optimizedSize:
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/beforeChange.ts"],"sourcesContent":["import path from 'path'\nimport type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { convertFormat, generateThumbHash, stripAndResize } from '../processing/index.js'\n\nexport const createBeforeChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionBeforeChangeHook => {\n return async ({ context, data, req }) => {\n if (context?.imageOptimizer_skip) return data\n\n if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return data\n\n const originalSize = req.file.data.length\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)\n\n // Process in memory: strip EXIF, resize, generate blur\n const processed = await stripAndResize(\n req.file.data,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n let finalBuffer = processed.buffer\n let finalSize = processed.size\n\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {\n // Convert to primary format (first in the formats array)\n const primaryFormat = perCollectionConfig.formats[0]\n const converted = await convertFormat(processed.buffer, primaryFormat.format, primaryFormat.quality)\n\n finalBuffer = converted.buffer\n finalSize = converted.size\n\n // Update filename and mimeType so Payload stores the correct metadata\n const originalFilename = data.filename || req.file.name || ''\n const newFilename = `${path.parse(originalFilename).name}.${primaryFormat.format}`\n context.imageOptimizer_originalFilename = originalFilename\n data.filename = newFilename\n data.mimeType = converted.mimeType\n data.filesize = finalSize\n }\n\n data.imageOptimizer = {\n originalSize,\n optimizedSize: finalSize,\n status: 'pending',\n }\n\n if (resolvedConfig.generateThumbHash) {\n data.imageOptimizer.thumbHash = await generateThumbHash(finalBuffer)\n }\n\n // Store processed buffer in context for afterChange to write to disk\n // (Payload 3.0 does not use modified req.file.data for the disk write)\n context.imageOptimizer_processedBuffer = finalBuffer\n\n return data\n }\n}\n"],"names":["path","resolveCollectionConfig","convertFormat","generateThumbHash","stripAndResize","createBeforeChangeHook","resolvedConfig","collectionSlug","context","data","req","imageOptimizer_skip","file","mimetype","startsWith","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","finalBuffer","buffer","finalSize","size","replaceOriginal","formats","primaryFormat","converted","format","quality","originalFilename","filename","name","newFilename","parse","imageOptimizer_originalFilename","mimeType","filesize","imageOptimizer","optimizedSize","status","thumbHash","imageOptimizer_processedBuffer"],"mappings":"AAAA,OAAOA,UAAU,OAAM;AAIvB,SAASC,uBAAuB,QAAQ,iBAAgB;AACxD,SAASC,aAAa,EAAEC,iBAAiB,EAAEC,cAAc,QAAQ,yBAAwB;AAEzF,OAAO,MAAMC,yBAAyB,CACpCC,gBACAC;IAEA,OAAO,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;QAClC,IAAIF,SAASG,qBAAqB,OAAOF;QAEzC,IAAI,CAACC,IAAIE,IAAI,IAAI,CAACF,IAAIE,IAAI,CAACH,IAAI,IAAI,CAACC,IAAIE,IAAI,CAACC,QAAQ,EAAEC,WAAW,WAAW,OAAOL;QAEpF,MAAMM,eAAeL,IAAIE,IAAI,CAACH,IAAI,CAACO,MAAM;QAEzC,MAAMC,sBAAsBhB,wBAAwBK,gBAAgBC;QAEpE,uDAAuD;QACvD,MAAMW,YAAY,MAAMd,eACtBM,IAAIE,IAAI,CAACH,IAAI,EACbQ,oBAAoBE,aAAa,EACjCb,eAAec,aAAa;QAG9B,IAAIC,cAAcH,UAAUI,MAAM;QAClC,IAAIC,YAAYL,UAAUM,IAAI;QAE9B,IAAIP,oBAAoBQ,eAAe,IAAIR,oBAAoBS,OAAO,CAACV,MAAM,GAAG,GAAG;YACjF,yDAAyD;YACzD,MAAMW,gBAAgBV,oBAAoBS,OAAO,CAAC,EAAE;YACpD,MAAME,YAAY,MAAM1B,cAAcgB,UAAUI,MAAM,EAAEK,cAAcE,MAAM,EAAEF,cAAcG,OAAO;YAEnGT,cAAcO,UAAUN,MAAM;YAC9BC,YAAYK,UAAUJ,IAAI;YAE1B,sEAAsE;YACtE,MAAMO,mBAAmBtB,KAAKuB,QAAQ,IAAItB,IAAIE,IAAI,CAACqB,IAAI,IAAI;YAC3D,MAAMC,cAAc,GAAGlC,KAAKmC,KAAK,CAACJ,kBAAkBE,IAAI,CAAC,CAAC,EAAEN,cAAcE,MAAM,EAAE;YAClFrB,QAAQ4B,+BAA+B,GAAGL;YAC1CtB,KAAKuB,QAAQ,GAAGE;YAChBzB,KAAK4B,QAAQ,GAAGT,UAAUS,QAAQ;YAClC5B,KAAK6B,QAAQ,GAAGf;QAClB;QAEAd,KAAK8B,cAAc,GAAG;YACpBxB;YACAyB,eAAejB;YACfkB,QAAQ;QACV;QAEA,IAAInC,eAAeH,iBAAiB,EAAE;YACpCM,KAAK8B,cAAc,CAACG,SAAS,GAAG,MAAMvC,kBAAkBkB;QAC1D;QAEA,qEAAqE;QACrE,uEAAuE;QACvEb,QAAQmC,8BAA8B,GAAGtB;QAEzC,OAAOZ;IACT;AACF,EAAC"}
|
|
@@ -23,7 +23,10 @@ export const createConvertFormatsHandler = (resolvedConfig)=>{
|
|
|
23
23
|
const fileBuffer = await fs.readFile(filePath);
|
|
24
24
|
const variants = [];
|
|
25
25
|
const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug);
|
|
26
|
-
|
|
26
|
+
// When replaceOriginal is on, the main file is already in the primary format —
|
|
27
|
+
// skip it and only generate variants for the remaining formats.
|
|
28
|
+
const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0 ? perCollectionConfig.formats.slice(1) : perCollectionConfig.formats;
|
|
29
|
+
for (const format of formatsToGenerate){
|
|
27
30
|
const result = await convertFormat(fileBuffer, format.format, format.quality);
|
|
28
31
|
const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`;
|
|
29
32
|
await fs.writeFile(path.join(staticDir, variantFilename), result.buffer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tasks/convertFormats.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { convertFormat } from '../processing/index.js'\n\nexport const createConvertFormatsHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n const fileBuffer = await fs.readFile(filePath)\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n for
|
|
1
|
+
{"version":3,"sources":["../../src/tasks/convertFormats.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { convertFormat } from '../processing/index.js'\n\nexport const createConvertFormatsHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n const fileBuffer = await fs.readFile(filePath)\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // When replaceOriginal is on, the main file is already in the primary format —\n // skip it and only generate variants for the remaining formats.\n const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0\n ? perCollectionConfig.formats.slice(1)\n : perCollectionConfig.formats\n\n for (const format of formatsToGenerate) {\n const result = await convertFormat(fileBuffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'complete',\n variants,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { variantsGenerated: variants.length } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","convertFormat","createConvertFormatsHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","variants","perCollectionConfig","formatsToGenerate","replaceOriginal","formats","length","slice","format","result","quality","variantFilename","parse","name","writeFile","buffer","push","filesize","size","width","height","mimeType","url","update","data","imageOptimizer","status","context","imageOptimizer_skip","output","variantsGenerated","err","errorMessage","message","String","error","updateErr","logger"],"mappings":"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"}
|
|
@@ -46,18 +46,37 @@ export const createRegenerateDocumentHandler = (resolvedConfig)=>{
|
|
|
46
46
|
const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug);
|
|
47
47
|
// Step 1: Strip metadata + resize
|
|
48
48
|
const processed = await stripAndResize(fileBuffer, perCollectionConfig.maxDimensions, resolvedConfig.stripMetadata);
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
let mainBuffer = processed.buffer;
|
|
50
|
+
let mainSize = processed.size;
|
|
51
|
+
let newFilename = safeFilename;
|
|
52
|
+
let newMimeType;
|
|
53
|
+
// Step 1b: If replaceOriginal, convert main file to primary format
|
|
54
|
+
if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {
|
|
55
|
+
const primaryFormat = perCollectionConfig.formats[0];
|
|
56
|
+
const converted = await convertFormat(processed.buffer, primaryFormat.format, primaryFormat.quality);
|
|
57
|
+
mainBuffer = converted.buffer;
|
|
58
|
+
mainSize = converted.size;
|
|
59
|
+
newFilename = `${path.parse(safeFilename).name}.${primaryFormat.format}`;
|
|
60
|
+
newMimeType = converted.mimeType;
|
|
61
|
+
}
|
|
62
|
+
// Write optimized file to disk
|
|
63
|
+
const newFilePath = path.join(staticDir, newFilename);
|
|
64
|
+
await fs.writeFile(newFilePath, mainBuffer);
|
|
65
|
+
// Clean up old file if filename changed
|
|
66
|
+
if (newFilename !== safeFilename) {
|
|
67
|
+
await fs.unlink(filePath).catch(()=>{});
|
|
68
|
+
}
|
|
51
69
|
// Step 2: Generate ThumbHash
|
|
52
70
|
let thumbHash;
|
|
53
71
|
if (resolvedConfig.generateThumbHash) {
|
|
54
|
-
thumbHash = await generateThumbHash(
|
|
72
|
+
thumbHash = await generateThumbHash(mainBuffer);
|
|
55
73
|
}
|
|
56
|
-
// Step 3: Convert to
|
|
74
|
+
// Step 3: Convert to configured formats (skip primary when replaceOriginal)
|
|
75
|
+
const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0 ? perCollectionConfig.formats.slice(1) : perCollectionConfig.formats;
|
|
57
76
|
const variants = [];
|
|
58
|
-
for (const format of
|
|
59
|
-
const result = await convertFormat(
|
|
60
|
-
const variantFilename = `${path.parse(
|
|
77
|
+
for (const format of formatsToGenerate){
|
|
78
|
+
const result = await convertFormat(mainBuffer, format.format, format.quality);
|
|
79
|
+
const variantFilename = `${path.parse(newFilename).name}-optimized.${format.format}`;
|
|
61
80
|
await fs.writeFile(path.join(staticDir, variantFilename), result.buffer);
|
|
62
81
|
variants.push({
|
|
63
82
|
format: format.format,
|
|
@@ -70,19 +89,26 @@ export const createRegenerateDocumentHandler = (resolvedConfig)=>{
|
|
|
70
89
|
});
|
|
71
90
|
}
|
|
72
91
|
// Step 4: Update the document with all optimization data
|
|
92
|
+
const updateData = {
|
|
93
|
+
imageOptimizer: {
|
|
94
|
+
originalSize,
|
|
95
|
+
optimizedSize: mainSize,
|
|
96
|
+
status: 'complete',
|
|
97
|
+
thumbHash,
|
|
98
|
+
variants,
|
|
99
|
+
error: null
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// Update filename, mimeType, and filesize when replaceOriginal changed them
|
|
103
|
+
if (newFilename !== safeFilename) {
|
|
104
|
+
updateData.filename = newFilename;
|
|
105
|
+
updateData.filesize = mainSize;
|
|
106
|
+
updateData.mimeType = newMimeType;
|
|
107
|
+
}
|
|
73
108
|
await req.payload.update({
|
|
74
109
|
collection: input.collectionSlug,
|
|
75
110
|
id: input.docId,
|
|
76
|
-
data:
|
|
77
|
-
imageOptimizer: {
|
|
78
|
-
originalSize,
|
|
79
|
-
optimizedSize: processed.size,
|
|
80
|
-
status: 'complete',
|
|
81
|
-
thumbHash,
|
|
82
|
-
variants,
|
|
83
|
-
error: null
|
|
84
|
-
}
|
|
85
|
-
},
|
|
111
|
+
data: updateData,
|
|
86
112
|
context: {
|
|
87
113
|
imageOptimizer_skip: true
|
|
88
114
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tasks/regenerateDocument.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js'\n\nexport const createRegenerateDocumentHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n // Skip non-image documents\n if (!doc.mimeType || !doc.mimeType.startsWith('image/')) {\n return { output: { status: 'skipped', reason: 'not-image' } }\n }\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n\n let fileBuffer: Buffer\n try {\n fileBuffer = await fs.readFile(filePath)\n } catch {\n // If file not on disk, try fetching from URL\n if (doc.url) {\n const url = doc.url.startsWith('http')\n ? doc.url\n : `${process.env.NEXT_PUBLIC_SERVER_URL || ''}${doc.url}`\n const response = await fetch(url)\n fileBuffer = Buffer.from(await response.arrayBuffer())\n } else {\n throw new Error(`File not found: ${filePath}`)\n }\n }\n\n const originalSize = fileBuffer.length\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // Step 1: Strip metadata + resize\n const processed = await stripAndResize(\n fileBuffer,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n // Write optimized file back to disk\n await fs.writeFile(filePath, processed.buffer)\n\n // Step 2: Generate ThumbHash\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(processed.buffer)\n }\n\n // Step 3: Convert to all configured formats\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n for (const format of perCollectionConfig.formats) {\n const result = await convertFormat(processed.buffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n // Step 4: Update the document with all optimization data\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n originalSize,\n optimizedSize: processed.size,\n status: 'complete',\n thumbHash,\n variants,\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { status: 'complete' } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer regeneration',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","stripAndResize","generateThumbHash","convertFormat","createRegenerateDocumentHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","mimeType","startsWith","output","status","reason","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","url","env","NEXT_PUBLIC_SERVER_URL","response","fetch","Buffer","from","arrayBuffer","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","writeFile","buffer","thumbHash","variants","format","formats","result","quality","variantFilename","parse","name","push","filesize","size","width","height","update","data","imageOptimizer","optimizedSize","error","context","imageOptimizer_skip","err","errorMessage","message","String","updateErr","logger"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/tasks/regenerateDocument.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js'\n\nexport const createRegenerateDocumentHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n // Skip non-image documents\n if (!doc.mimeType || !doc.mimeType.startsWith('image/')) {\n return { output: { status: 'skipped', reason: 'not-image' } }\n }\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n\n let fileBuffer: Buffer\n try {\n fileBuffer = await fs.readFile(filePath)\n } catch {\n // If file not on disk, try fetching from URL\n if (doc.url) {\n const url = doc.url.startsWith('http')\n ? doc.url\n : `${process.env.NEXT_PUBLIC_SERVER_URL || ''}${doc.url}`\n const response = await fetch(url)\n fileBuffer = Buffer.from(await response.arrayBuffer())\n } else {\n throw new Error(`File not found: ${filePath}`)\n }\n }\n\n const originalSize = fileBuffer.length\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // Step 1: Strip metadata + resize\n const processed = await stripAndResize(\n fileBuffer,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n let mainBuffer = processed.buffer\n let mainSize = processed.size\n let newFilename = safeFilename\n let newMimeType: string | undefined\n\n // Step 1b: If replaceOriginal, convert main file to primary format\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {\n const primaryFormat = perCollectionConfig.formats[0]\n const converted = await convertFormat(processed.buffer, primaryFormat.format, primaryFormat.quality)\n mainBuffer = converted.buffer\n mainSize = converted.size\n newFilename = `${path.parse(safeFilename).name}.${primaryFormat.format}`\n newMimeType = converted.mimeType\n }\n\n // Write optimized file to disk\n const newFilePath = path.join(staticDir, newFilename)\n await fs.writeFile(newFilePath, mainBuffer)\n\n // Clean up old file if filename changed\n if (newFilename !== safeFilename) {\n await fs.unlink(filePath).catch(() => {})\n }\n\n // Step 2: Generate ThumbHash\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(mainBuffer)\n }\n\n // Step 3: Convert to configured formats (skip primary when replaceOriginal)\n const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0\n ? perCollectionConfig.formats.slice(1)\n : perCollectionConfig.formats\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n for (const format of formatsToGenerate) {\n const result = await convertFormat(mainBuffer, format.format, format.quality)\n const variantFilename = `${path.parse(newFilename).name}-optimized.${format.format}`\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n // Step 4: Update the document with all optimization data\n const updateData: Record<string, any> = {\n imageOptimizer: {\n originalSize,\n optimizedSize: mainSize,\n status: 'complete',\n thumbHash,\n variants,\n error: null,\n },\n }\n\n // Update filename, mimeType, and filesize when replaceOriginal changed them\n if (newFilename !== safeFilename) {\n updateData.filename = newFilename\n updateData.filesize = mainSize\n updateData.mimeType = newMimeType\n }\n\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: updateData,\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { status: 'complete' } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer regeneration',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","stripAndResize","generateThumbHash","convertFormat","createRegenerateDocumentHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","mimeType","startsWith","output","status","reason","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","url","env","NEXT_PUBLIC_SERVER_URL","response","fetch","Buffer","from","arrayBuffer","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","mainBuffer","buffer","mainSize","size","newFilename","newMimeType","replaceOriginal","formats","primaryFormat","converted","format","quality","parse","name","newFilePath","writeFile","unlink","catch","thumbHash","formatsToGenerate","slice","variants","result","variantFilename","push","filesize","width","height","updateData","imageOptimizer","optimizedSize","error","update","data","context","imageOptimizer_skip","err","errorMessage","message","String","updateErr","logger"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type CollectionOptimizerConfig = {
|
|
|
10
10
|
width: number;
|
|
11
11
|
height: number;
|
|
12
12
|
};
|
|
13
|
+
replaceOriginal?: boolean;
|
|
13
14
|
};
|
|
14
15
|
export type ImageOptimizerConfig = {
|
|
15
16
|
collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>;
|
|
@@ -20,6 +21,7 @@ export type ImageOptimizerConfig = {
|
|
|
20
21
|
width: number;
|
|
21
22
|
height: number;
|
|
22
23
|
};
|
|
24
|
+
replaceOriginal?: boolean;
|
|
23
25
|
stripMetadata?: boolean;
|
|
24
26
|
};
|
|
25
27
|
export type ResolvedCollectionOptimizerConfig = {
|
|
@@ -28,8 +30,10 @@ export type ResolvedCollectionOptimizerConfig = {
|
|
|
28
30
|
width: number;
|
|
29
31
|
height: number;
|
|
30
32
|
};
|
|
33
|
+
replaceOriginal: boolean;
|
|
31
34
|
};
|
|
32
35
|
export type ResolvedImageOptimizerConfig = Required<Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>> & {
|
|
33
36
|
collections: ImageOptimizerConfig['collections'];
|
|
34
37
|
disabled: boolean;
|
|
38
|
+
replaceOriginal: boolean;
|
|
35
39
|
};
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nexport type ImageFormat = 'webp' | 'avif'\n\nexport type FormatQuality = {\n format: ImageFormat\n quality: number // 1-100\n}\n\nexport type CollectionOptimizerConfig = {\n formats?: FormatQuality[]\n maxDimensions?: { width: number; height: number }\n}\n\nexport type ImageOptimizerConfig = {\n collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>\n disabled?: boolean\n formats?: FormatQuality[]\n generateThumbHash?: boolean\n maxDimensions?: { width: number; height: number }\n stripMetadata?: boolean\n}\n\nexport type ResolvedCollectionOptimizerConfig = {\n formats: FormatQuality[]\n maxDimensions: { width: number; height: number }\n}\n\nexport type ResolvedImageOptimizerConfig = Required<\n Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>\n> & {\n collections: ImageOptimizerConfig['collections']\n disabled: boolean\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nexport type ImageFormat = 'webp' | 'avif'\n\nexport type FormatQuality = {\n format: ImageFormat\n quality: number // 1-100\n}\n\nexport type CollectionOptimizerConfig = {\n formats?: FormatQuality[]\n maxDimensions?: { width: number; height: number }\n replaceOriginal?: boolean\n}\n\nexport type ImageOptimizerConfig = {\n collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>\n disabled?: boolean\n formats?: FormatQuality[]\n generateThumbHash?: boolean\n maxDimensions?: { width: number; height: number }\n replaceOriginal?: boolean\n stripMetadata?: boolean\n}\n\nexport type ResolvedCollectionOptimizerConfig = {\n formats: FormatQuality[]\n maxDimensions: { width: number; height: number }\n replaceOriginal: boolean\n}\n\nexport type ResolvedImageOptimizerConfig = Required<\n Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>\n> & {\n collections: ImageOptimizerConfig['collections']\n disabled: boolean\n replaceOriginal: boolean\n}\n"],"names":[],"mappings":"AA+BA,WAMC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inoo-ch/payload-image-optimizer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Payload CMS plugin for automatic image optimization — WebP/AVIF conversion, resize, EXIF strip, ThumbHash placeholders, and bulk regeneration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|