@inoo-ch/payload-image-optimizer 1.3.1 → 1.3.3
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/dist/components/RegenerationButton.js +32 -20
- package/dist/components/RegenerationButton.js.map +1 -1
- package/dist/hooks/afterChange.js +10 -3
- package/dist/hooks/afterChange.js.map +1 -1
- package/dist/hooks/beforeChange.js +1 -0
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/tasks/convertFormats.js +6 -2
- package/dist/tasks/convertFormats.js.map +1 -1
- package/package.json +59 -31
- package/src/components/RegenerationButton.tsx +22 -9
- package/src/hooks/afterChange.ts +8 -1
- package/src/hooks/beforeChange.ts +1 -0
- package/src/tasks/convertFormats.ts +4 -0
|
@@ -167,7 +167,7 @@ export const RegenerationButton = ()=>{
|
|
|
167
167
|
};
|
|
168
168
|
}, []);
|
|
169
169
|
if (!collectionSlug) return null;
|
|
170
|
-
const progressPercent = progress && progress.total > 0 ? Math.round(progress.complete / progress.total * 100) : 0;
|
|
170
|
+
const progressPercent = progress && progress.total > 0 ? Math.round((progress.complete + progress.errored) / progress.total * 100) : 0;
|
|
171
171
|
const showProgressBar = isRunning && progress || stalled && progress;
|
|
172
172
|
// Stats computations
|
|
173
173
|
const statsPercent = stats && stats.total > 0 ? Math.round(stats.complete / stats.total * 100) : 0;
|
|
@@ -234,11 +234,13 @@ export const RegenerationButton = ()=>{
|
|
|
234
234
|
fontSize: '13px'
|
|
235
235
|
},
|
|
236
236
|
children: [
|
|
237
|
-
"
|
|
238
|
-
progress.pending,
|
|
237
|
+
"Processing finished with issues. ",
|
|
238
|
+
progress.errored + progress.pending,
|
|
239
239
|
" image",
|
|
240
|
-
progress.pending !== 1 ? 's' : '',
|
|
241
|
-
" failed
|
|
240
|
+
progress.errored + progress.pending !== 1 ? 's' : '',
|
|
241
|
+
" failed",
|
|
242
|
+
progress.pending > 0 ? ` (${progress.pending} stuck)` : '',
|
|
243
|
+
". Re-run to retry."
|
|
242
244
|
]
|
|
243
245
|
}),
|
|
244
246
|
showProgressBar && /*#__PURE__*/ _jsxs("div", {
|
|
@@ -280,33 +282,43 @@ export const RegenerationButton = ()=>{
|
|
|
280
282
|
})
|
|
281
283
|
]
|
|
282
284
|
}),
|
|
283
|
-
/*#__PURE__*/
|
|
285
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
284
286
|
style: {
|
|
285
287
|
height: '6px',
|
|
286
288
|
backgroundColor: '#e5e7eb',
|
|
287
289
|
borderRadius: '3px',
|
|
288
|
-
overflow: 'hidden'
|
|
290
|
+
overflow: 'hidden',
|
|
291
|
+
display: 'flex'
|
|
289
292
|
},
|
|
290
|
-
children:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
293
|
+
children: [
|
|
294
|
+
/*#__PURE__*/ _jsx("div", {
|
|
295
|
+
style: {
|
|
296
|
+
height: '100%',
|
|
297
|
+
width: `${progress.total > 0 ? Math.round(progress.complete / progress.total * 100) : 0}%`,
|
|
298
|
+
backgroundColor: '#10b981',
|
|
299
|
+
transition: 'width 0.3s ease'
|
|
300
|
+
}
|
|
301
|
+
}),
|
|
302
|
+
progress.errored > 0 && /*#__PURE__*/ _jsx("div", {
|
|
303
|
+
style: {
|
|
304
|
+
height: '100%',
|
|
305
|
+
width: `${progress.total > 0 ? Math.round(progress.errored / progress.total * 100) : 0}%`,
|
|
306
|
+
backgroundColor: '#ef4444',
|
|
307
|
+
transition: 'width 0.3s ease'
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
]
|
|
299
311
|
})
|
|
300
312
|
]
|
|
301
313
|
}),
|
|
302
|
-
!isRunning &&
|
|
314
|
+
!isRunning && progress && progress.complete > 0 && queued !== 0 && /*#__PURE__*/ _jsxs("span", {
|
|
303
315
|
style: {
|
|
304
316
|
fontSize: '13px'
|
|
305
317
|
},
|
|
306
318
|
children: [
|
|
307
319
|
/*#__PURE__*/ _jsxs("span", {
|
|
308
320
|
style: {
|
|
309
|
-
color: '#10b981'
|
|
321
|
+
color: progress.errored > 0 || stalled ? '#f59e0b' : '#10b981'
|
|
310
322
|
},
|
|
311
323
|
children: [
|
|
312
324
|
"Done! ",
|
|
@@ -316,13 +328,13 @@ export const RegenerationButton = ()=>{
|
|
|
316
328
|
" optimized."
|
|
317
329
|
]
|
|
318
330
|
}),
|
|
319
|
-
progress.errored > 0 && /*#__PURE__*/ _jsxs("span", {
|
|
331
|
+
(progress.errored > 0 || stalled && progress.pending > 0) && /*#__PURE__*/ _jsxs("span", {
|
|
320
332
|
style: {
|
|
321
333
|
color: '#ef4444'
|
|
322
334
|
},
|
|
323
335
|
children: [
|
|
324
336
|
' ',
|
|
325
|
-
progress.errored,
|
|
337
|
+
progress.errored + (stalled ? progress.pending : 0),
|
|
326
338
|
" failed."
|
|
327
339
|
]
|
|
328
340
|
})
|
|
@@ -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\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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"}
|
|
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.errored) / 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 Processing finished with issues. {progress.errored + progress.pending} image\n {progress.errored + progress.pending !== 1 ? 's' : ''} failed\n {progress.pending > 0 ? ` (${progress.pending} stuck)` : ''}.\n Re-run to retry.\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 display: 'flex',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${progress.total > 0 ? Math.round((progress.complete / progress.total) * 100) : 0}%`,\n backgroundColor: '#10b981',\n transition: 'width 0.3s ease',\n }}\n />\n {progress.errored > 0 && (\n <div\n style={{\n height: '100%',\n width: `${progress.total > 0 ? Math.round((progress.errored / progress.total) * 100) : 0}%`,\n backgroundColor: '#ef4444',\n transition: 'width 0.3s ease',\n }}\n />\n )}\n </div>\n </div>\n )}\n\n {!isRunning && progress && progress.complete > 0 && queued !== 0 && (\n <span style={{ fontSize: '13px' }}>\n <span style={{ color: progress.errored > 0 || stalled ? '#f59e0b' : '#10b981' }}>\n Done! {progress.complete}/{progress.total} optimized.\n </span>\n {(progress.errored > 0 || (stalled && progress.pending > 0)) && (\n <span style={{ color: '#ef4444' }}>\n {' '}{progress.errored + (stalled ? progress.pending : 0)} 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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"}
|
|
@@ -6,7 +6,10 @@ import { isCloudStorage } from '../utilities/storage.js';
|
|
|
6
6
|
export const createAfterChangeHook = (resolvedConfig, collectionSlug)=>{
|
|
7
7
|
return async ({ context, doc, req })=>{
|
|
8
8
|
if (context?.imageOptimizer_skip) return doc;
|
|
9
|
-
|
|
9
|
+
// Use context flag from beforeChange instead of checking req.file.data directly.
|
|
10
|
+
// Cloud storage adapters may consume req.file.data in their own afterChange hook
|
|
11
|
+
// before ours runs, which would cause this guard to bail out and leave status as 'pending'.
|
|
12
|
+
if (!context?.imageOptimizer_hasUpload) return doc;
|
|
10
13
|
const collectionConfig = req.payload.collections[collectionSlug].config;
|
|
11
14
|
const cloudStorage = isCloudStorage(collectionConfig);
|
|
12
15
|
// When using local storage, overwrite the file on disk with the processed buffer.
|
|
@@ -39,8 +42,10 @@ export const createAfterChangeHook = (resolvedConfig, collectionSlug)=>{
|
|
|
39
42
|
id: doc.id,
|
|
40
43
|
data: {
|
|
41
44
|
imageOptimizer: {
|
|
45
|
+
...doc.imageOptimizer,
|
|
42
46
|
status: 'complete',
|
|
43
|
-
variants: []
|
|
47
|
+
variants: [],
|
|
48
|
+
error: null
|
|
44
49
|
}
|
|
45
50
|
},
|
|
46
51
|
context: {
|
|
@@ -58,8 +63,10 @@ export const createAfterChangeHook = (resolvedConfig, collectionSlug)=>{
|
|
|
58
63
|
id: doc.id,
|
|
59
64
|
data: {
|
|
60
65
|
imageOptimizer: {
|
|
66
|
+
...doc.imageOptimizer,
|
|
61
67
|
status: 'complete',
|
|
62
|
-
variants: []
|
|
68
|
+
variants: [],
|
|
69
|
+
error: null
|
|
63
70
|
}
|
|
64
71
|
},
|
|
65
72
|
context: {
|
|
@@ -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'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { isCloudStorage } from '../utilities/storage.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
|
|
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'\nimport { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { isCloudStorage } from '../utilities/storage.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 // Use context flag from beforeChange instead of checking req.file.data directly.\n // Cloud storage adapters may consume req.file.data in their own afterChange hook\n // before ours runs, which would cause this guard to bail out and leave status as 'pending'.\n if (!context?.imageOptimizer_hasUpload) return doc\n\n const collectionConfig = req.payload.collections[collectionSlug as keyof typeof req.payload.collections].config\n const cloudStorage = isCloudStorage(collectionConfig)\n\n // When using local storage, overwrite the file on disk with the processed buffer.\n // Payload's uploadFiles step writes the original buffer; we replace it here.\n // When using cloud storage, skip — the cloud adapter's afterChange hook already\n // uploads the correct buffer from req.file.data (set in our beforeChange hook).\n if (!cloudStorage) {\n const staticDir = resolveStaticDir(collectionConfig)\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\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)\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 ...doc.imageOptimizer,\n status: 'complete',\n variants: [],\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n return doc\n }\n\n // With cloud storage, variant files cannot be written — skip the async job\n // and mark complete. CDN-level image optimization (e.g. Next.js Image) can\n // serve alternative formats on the fly.\n if (cloudStorage) {\n await req.payload.update({\n collection: collectionSlug,\n id: doc.id,\n data: {\n imageOptimizer: {\n ...doc.imageOptimizer,\n status: 'complete',\n variants: [],\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n return doc\n }\n\n // Queue async format conversion job for remaining variants (local storage only)\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","resolveStaticDir","isCloudStorage","createAfterChangeHook","resolvedConfig","collectionSlug","context","doc","req","imageOptimizer_skip","imageOptimizer_hasUpload","collectionConfig","payload","collections","config","cloudStorage","staticDir","processedBuffer","imageOptimizer_processedBuffer","filename","safeFilename","basename","filePath","join","writeFile","originalFilename","imageOptimizer_originalFilename","oldFilePath","unlink","catch","perCollectionConfig","replaceOriginal","formats","length","update","collection","id","data","imageOptimizer","status","variants","error","jobs","queue","task","input","docId","String","run","err","logger"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,UAAU,OAAM;AAIvB,SAASC,uBAAuB,QAAQ,iBAAgB;AACxD,SAASC,gBAAgB,QAAQ,mCAAkC;AACnE,SAASC,cAAc,QAAQ,0BAAyB;AAExD,OAAO,MAAMC,wBAAwB,CACnCC,gBACAC;IAEA,OAAO,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QACjC,IAAIF,SAASG,qBAAqB,OAAOF;QAEzC,iFAAiF;QACjF,iFAAiF;QACjF,4FAA4F;QAC5F,IAAI,CAACD,SAASI,0BAA0B,OAAOH;QAE/C,MAAMI,mBAAmBH,IAAII,OAAO,CAACC,WAAW,CAACR,eAAuD,CAACS,MAAM;QAC/G,MAAMC,eAAeb,eAAeS;QAEpC,kFAAkF;QAClF,6EAA6E;QAC7E,gFAAgF;QAChF,gFAAgF;QAChF,IAAI,CAACI,cAAc;YACjB,MAAMC,YAAYf,iBAAiBU;YACnC,MAAMM,kBAAkBX,QAAQY,8BAA8B;YAC9D,IAAID,mBAAmBV,IAAIY,QAAQ,IAAIH,WAAW;gBAChD,MAAMI,eAAerB,KAAKsB,QAAQ,CAACd,IAAIY,QAAQ;gBAC/C,MAAMG,WAAWvB,KAAKwB,IAAI,CAACP,WAAWI;gBACtC,MAAMtB,GAAG0B,SAAS,CAACF,UAAUL;gBAE7B,+EAA+E;gBAC/E,MAAMQ,mBAAmBnB,QAAQoB,+BAA+B;gBAChE,IAAID,oBAAoBA,qBAAqBL,cAAc;oBACzD,MAAMO,cAAc5B,KAAKwB,IAAI,CAACP,WAAWjB,KAAKsB,QAAQ,CAACI;oBACvD,MAAM3B,GAAG8B,MAAM,CAACD,aAAaE,KAAK,CAAC;oBACjC,0DAA0D;oBAC5D;gBACF;YACF;QACF;QAEA,MAAMC,sBAAsB9B,wBAAwBI,gBAAgBC;QAEpE,8EAA8E;QAC9E,2EAA2E;QAC3E,IAAIyB,oBAAoBC,eAAe,IAAID,oBAAoBE,OAAO,CAACC,MAAM,IAAI,GAAG;YAClF,MAAMzB,IAAII,OAAO,CAACsB,MAAM,CAAC;gBACvBC,YAAY9B;gBACZ+B,IAAI7B,IAAI6B,EAAE;gBACVC,MAAM;oBACJC,gBAAgB;wBACd,GAAG/B,IAAI+B,cAAc;wBACrBC,QAAQ;wBACRC,UAAU,EAAE;wBACZC,OAAO;oBACT;gBACF;gBACAnC,SAAS;oBAAEG,qBAAqB;gBAAK;YACvC;YACA,OAAOF;QACT;QAEA,2EAA2E;QAC3E,2EAA2E;QAC3E,wCAAwC;QACxC,IAAIQ,cAAc;YAChB,MAAMP,IAAII,OAAO,CAACsB,MAAM,CAAC;gBACvBC,YAAY9B;gBACZ+B,IAAI7B,IAAI6B,EAAE;gBACVC,MAAM;oBACJC,gBAAgB;wBACd,GAAG/B,IAAI+B,cAAc;wBACrBC,QAAQ;wBACRC,UAAU,EAAE;wBACZC,OAAO;oBACT;gBACF;gBACAnC,SAAS;oBAAEG,qBAAqB;gBAAK;YACvC;YACA,OAAOF;QACT;QAEA,gFAAgF;QAChF,MAAMC,IAAII,OAAO,CAAC8B,IAAI,CAACC,KAAK,CAAC;YAC3BC,MAAM;YACNC,OAAO;gBACLxC;gBACAyC,OAAOC,OAAOxC,IAAI6B,EAAE;YACtB;QACF;QAEA5B,IAAII,OAAO,CAAC8B,IAAI,CAACM,GAAG,GAAGnB,KAAK,CAAC,CAACoB;YAC5BzC,IAAII,OAAO,CAACsC,MAAM,CAACT,KAAK,CAAC;gBAAEQ;YAAI,GAAG;QACpC;QAEA,OAAO1C;IACT;AACF,EAAC"}
|
|
@@ -1 +1 @@
|
|
|
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 // Write processed buffer back to req.file so cloud storage adapters\n // (which read req.file in their afterChange hook) upload the optimized version.\n // Payload's own uploadFiles step does NOT re-read req.file.data for its local\n // disk write, so we also store the buffer in context for our afterChange hook\n // to overwrite the local file when local storage is enabled.\n req.file.data = finalBuffer\n req.file.size = finalSize\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {\n req.file.name = data.filename\n req.file.mimetype = data.mimeType\n }\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,oEAAoE;QACpE,gFAAgF;QAChF,8EAA8E;QAC9E,8EAA8E;QAC9E,6DAA6D;QAC7DX,IAAIE,IAAI,CAACH,IAAI,GAAGY;QAChBX,IAAIE,IAAI,CAACY,IAAI,GAAGD;QAChB,IAAIN,oBAAoBQ,eAAe,IAAIR,oBAAoBS,OAAO,CAACV,MAAM,GAAG,GAAG;YACjFN,IAAIE,IAAI,CAACqB,IAAI,GAAGxB,KAAKuB,QAAQ;YAC7BtB,IAAIE,IAAI,CAACC,QAAQ,GAAGJ,KAAK4B,QAAQ;QACnC;QACA7B,QAAQmC,8BAA8B,GAAGtB;
|
|
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 // Write processed buffer back to req.file so cloud storage adapters\n // (which read req.file in their afterChange hook) upload the optimized version.\n // Payload's own uploadFiles step does NOT re-read req.file.data for its local\n // disk write, so we also store the buffer in context for our afterChange hook\n // to overwrite the local file when local storage is enabled.\n req.file.data = finalBuffer\n req.file.size = finalSize\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {\n req.file.name = data.filename\n req.file.mimetype = data.mimeType\n }\n context.imageOptimizer_processedBuffer = finalBuffer\n context.imageOptimizer_hasUpload = true\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","imageOptimizer_hasUpload"],"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,oEAAoE;QACpE,gFAAgF;QAChF,8EAA8E;QAC9E,8EAA8E;QAC9E,6DAA6D;QAC7DX,IAAIE,IAAI,CAACH,IAAI,GAAGY;QAChBX,IAAIE,IAAI,CAACY,IAAI,GAAGD;QAChB,IAAIN,oBAAoBQ,eAAe,IAAIR,oBAAoBS,OAAO,CAACV,MAAM,GAAG,GAAG;YACjFN,IAAIE,IAAI,CAACqB,IAAI,GAAGxB,KAAKuB,QAAQ;YAC7BtB,IAAIE,IAAI,CAACC,QAAQ,GAAGJ,KAAK4B,QAAQ;QACnC;QACA7B,QAAQmC,8BAA8B,GAAGtB;QACzCb,QAAQoC,wBAAwB,GAAG;QAEnC,OAAOnC;IACT;AACF,EAAC"}
|
|
@@ -21,8 +21,10 @@ export const createConvertFormatsHandler = (resolvedConfig)=>{
|
|
|
21
21
|
id: input.docId,
|
|
22
22
|
data: {
|
|
23
23
|
imageOptimizer: {
|
|
24
|
+
...doc.imageOptimizer,
|
|
24
25
|
status: 'complete',
|
|
25
|
-
variants: []
|
|
26
|
+
variants: [],
|
|
27
|
+
error: null
|
|
26
28
|
}
|
|
27
29
|
},
|
|
28
30
|
context: {
|
|
@@ -65,8 +67,10 @@ export const createConvertFormatsHandler = (resolvedConfig)=>{
|
|
|
65
67
|
id: input.docId,
|
|
66
68
|
data: {
|
|
67
69
|
imageOptimizer: {
|
|
70
|
+
...doc.imageOptimizer,
|
|
68
71
|
status: 'complete',
|
|
69
|
-
variants
|
|
72
|
+
variants,
|
|
73
|
+
error: null
|
|
70
74
|
}
|
|
71
75
|
},
|
|
72
76
|
context: {
|
|
@@ -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'\nimport { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { fetchFileBuffer, isCloudStorage } from '../utilities/storage.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 const cloudStorage = isCloudStorage(collectionConfig)\n\n // Cloud storage: variant files cannot be uploaded without direct adapter access.\n // Mark as complete — CDN-level image optimization handles format conversion.\n if (cloudStorage) {\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 return { output: { variantsGenerated: 0 } }\n }\n\n const staticDir = resolveStaticDir(collectionConfig)\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n\n const fileBuffer = await fetchFileBuffer(doc, collectionConfig)\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 const safeFilename = path.basename(doc.filename)\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","resolveStaticDir","fetchFileBuffer","isCloudStorage","createConvertFormatsHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","collectionConfig","collections","config","cloudStorage","update","data","imageOptimizer","status","variants","context","imageOptimizer_skip","output","variantsGenerated","staticDir","Error","fileBuffer","perCollectionConfig","formatsToGenerate","replaceOriginal","formats","length","slice","safeFilename","basename","filename","format","result","quality","variantFilename","parse","name","writeFile","join","buffer","push","filesize","size","width","height","mimeType","url","err","errorMessage","message","String","
|
|
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'\nimport { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { fetchFileBuffer, isCloudStorage } from '../utilities/storage.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 const cloudStorage = isCloudStorage(collectionConfig)\n\n // Cloud storage: variant files cannot be uploaded without direct adapter access.\n // Mark as complete — CDN-level image optimization handles format conversion.\n if (cloudStorage) {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n ...doc.imageOptimizer,\n status: 'complete',\n variants: [],\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n return { output: { variantsGenerated: 0 } }\n }\n\n const staticDir = resolveStaticDir(collectionConfig)\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n\n const fileBuffer = await fetchFileBuffer(doc, collectionConfig)\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 const safeFilename = path.basename(doc.filename)\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 ...doc.imageOptimizer,\n status: 'complete',\n variants,\n error: null,\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","resolveStaticDir","fetchFileBuffer","isCloudStorage","createConvertFormatsHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","collectionConfig","collections","config","cloudStorage","update","data","imageOptimizer","status","variants","error","context","imageOptimizer_skip","output","variantsGenerated","staticDir","Error","fileBuffer","perCollectionConfig","formatsToGenerate","replaceOriginal","formats","length","slice","safeFilename","basename","filename","format","result","quality","variantFilename","parse","name","writeFile","join","buffer","push","filesize","size","width","height","mimeType","url","err","errorMessage","message","String","updateErr","logger"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inoo-ch/payload-image-optimizer",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
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": [
|
|
@@ -24,28 +24,46 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
|
26
26
|
".": {
|
|
27
|
-
"import": "./
|
|
28
|
-
"types": "./
|
|
29
|
-
"default": "./
|
|
27
|
+
"import": "./src/index.ts",
|
|
28
|
+
"types": "./src/index.ts",
|
|
29
|
+
"default": "./src/index.ts"
|
|
30
30
|
},
|
|
31
31
|
"./client": {
|
|
32
|
-
"import": "./
|
|
33
|
-
"types": "./
|
|
34
|
-
"default": "./
|
|
32
|
+
"import": "./src/exports/client.ts",
|
|
33
|
+
"types": "./src/exports/client.ts",
|
|
34
|
+
"default": "./src/exports/client.ts"
|
|
35
35
|
},
|
|
36
36
|
"./rsc": {
|
|
37
|
-
"import": "./
|
|
38
|
-
"types": "./
|
|
39
|
-
"default": "./
|
|
37
|
+
"import": "./src/exports/rsc.ts",
|
|
38
|
+
"types": "./src/exports/rsc.ts",
|
|
39
|
+
"default": "./src/exports/rsc.ts"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
|
-
"main": "./
|
|
43
|
-
"types": "./
|
|
42
|
+
"main": "./src/index.ts",
|
|
43
|
+
"types": "./src/index.ts",
|
|
44
44
|
"files": [
|
|
45
45
|
"dist",
|
|
46
46
|
"src",
|
|
47
47
|
"AGENT_DOCS.md"
|
|
48
48
|
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
51
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
52
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
53
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
54
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
55
|
+
"dev": "next dev dev --turbo",
|
|
56
|
+
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
57
|
+
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
58
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
59
|
+
"generate:importmap": "pnpm dev:generate-importmap",
|
|
60
|
+
"generate:types": "pnpm dev:generate-types",
|
|
61
|
+
"lint": "eslint",
|
|
62
|
+
"lint:fix": "eslint ./src --fix",
|
|
63
|
+
"test": "pnpm test:int && pnpm test:e2e",
|
|
64
|
+
"test:e2e": "playwright test",
|
|
65
|
+
"test:int": "vitest"
|
|
66
|
+
},
|
|
49
67
|
"devDependencies": {
|
|
50
68
|
"@eslint/eslintrc": "^3.2.0",
|
|
51
69
|
"@payloadcms/db-mongodb": "3.79.0",
|
|
@@ -91,26 +109,36 @@
|
|
|
91
109
|
"node": "^18.20.2 || >=20.9.0",
|
|
92
110
|
"pnpm": "^9 || ^10"
|
|
93
111
|
},
|
|
112
|
+
"publishConfig": {
|
|
113
|
+
"exports": {
|
|
114
|
+
".": {
|
|
115
|
+
"import": "./dist/index.js",
|
|
116
|
+
"types": "./dist/index.d.ts",
|
|
117
|
+
"default": "./dist/index.js"
|
|
118
|
+
},
|
|
119
|
+
"./client": {
|
|
120
|
+
"import": "./dist/exports/client.js",
|
|
121
|
+
"types": "./dist/exports/client.d.ts",
|
|
122
|
+
"default": "./dist/exports/client.js"
|
|
123
|
+
},
|
|
124
|
+
"./rsc": {
|
|
125
|
+
"import": "./dist/exports/rsc.js",
|
|
126
|
+
"types": "./dist/exports/rsc.d.ts",
|
|
127
|
+
"default": "./dist/exports/rsc.js"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"main": "./dist/index.js",
|
|
131
|
+
"types": "./dist/index.d.ts"
|
|
132
|
+
},
|
|
133
|
+
"pnpm": {
|
|
134
|
+
"onlyBuiltDependencies": [
|
|
135
|
+
"sharp",
|
|
136
|
+
"esbuild",
|
|
137
|
+
"unrs-resolver"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
94
140
|
"registry": "https://registry.npmjs.org/",
|
|
95
141
|
"dependencies": {
|
|
96
142
|
"thumbhash": "^0.1.1"
|
|
97
|
-
},
|
|
98
|
-
"scripts": {
|
|
99
|
-
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
100
|
-
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
101
|
-
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
102
|
-
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
103
|
-
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
104
|
-
"dev": "next dev dev --turbo",
|
|
105
|
-
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
106
|
-
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
107
|
-
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
108
|
-
"generate:importmap": "pnpm dev:generate-importmap",
|
|
109
|
-
"generate:types": "pnpm dev:generate-types",
|
|
110
|
-
"lint": "eslint",
|
|
111
|
-
"lint:fix": "eslint ./src --fix",
|
|
112
|
-
"test": "pnpm test:int && pnpm test:e2e",
|
|
113
|
-
"test:e2e": "playwright test",
|
|
114
|
-
"test:int": "vitest"
|
|
115
143
|
}
|
|
116
|
-
}
|
|
144
|
+
}
|
|
@@ -177,7 +177,7 @@ export const RegenerationButton: React.FC = () => {
|
|
|
177
177
|
|
|
178
178
|
const progressPercent =
|
|
179
179
|
progress && progress.total > 0
|
|
180
|
-
? Math.round((progress.complete / progress.total) * 100)
|
|
180
|
+
? Math.round(((progress.complete + progress.errored) / progress.total) * 100)
|
|
181
181
|
: 0
|
|
182
182
|
|
|
183
183
|
const showProgressBar = (isRunning && progress) || (stalled && progress)
|
|
@@ -241,7 +241,10 @@ export const RegenerationButton: React.FC = () => {
|
|
|
241
241
|
|
|
242
242
|
{stalled && progress && (
|
|
243
243
|
<span style={{ color: '#f59e0b', fontSize: '13px' }}>
|
|
244
|
-
|
|
244
|
+
Processing finished with issues. {progress.errored + progress.pending} image
|
|
245
|
+
{progress.errored + progress.pending !== 1 ? 's' : ''} failed
|
|
246
|
+
{progress.pending > 0 ? ` (${progress.pending} stuck)` : ''}.
|
|
247
|
+
Re-run to retry.
|
|
245
248
|
</span>
|
|
246
249
|
)}
|
|
247
250
|
|
|
@@ -269,29 +272,39 @@ export const RegenerationButton: React.FC = () => {
|
|
|
269
272
|
backgroundColor: '#e5e7eb',
|
|
270
273
|
borderRadius: '3px',
|
|
271
274
|
overflow: 'hidden',
|
|
275
|
+
display: 'flex',
|
|
272
276
|
}}
|
|
273
277
|
>
|
|
274
278
|
<div
|
|
275
279
|
style={{
|
|
276
280
|
height: '100%',
|
|
277
|
-
width: `${
|
|
278
|
-
backgroundColor:
|
|
279
|
-
borderRadius: '3px',
|
|
281
|
+
width: `${progress.total > 0 ? Math.round((progress.complete / progress.total) * 100) : 0}%`,
|
|
282
|
+
backgroundColor: '#10b981',
|
|
280
283
|
transition: 'width 0.3s ease',
|
|
281
284
|
}}
|
|
282
285
|
/>
|
|
286
|
+
{progress.errored > 0 && (
|
|
287
|
+
<div
|
|
288
|
+
style={{
|
|
289
|
+
height: '100%',
|
|
290
|
+
width: `${progress.total > 0 ? Math.round((progress.errored / progress.total) * 100) : 0}%`,
|
|
291
|
+
backgroundColor: '#ef4444',
|
|
292
|
+
transition: 'width 0.3s ease',
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
)}
|
|
283
296
|
</div>
|
|
284
297
|
</div>
|
|
285
298
|
)}
|
|
286
299
|
|
|
287
|
-
{!isRunning &&
|
|
300
|
+
{!isRunning && progress && progress.complete > 0 && queued !== 0 && (
|
|
288
301
|
<span style={{ fontSize: '13px' }}>
|
|
289
|
-
<span style={{ color: '#10b981' }}>
|
|
302
|
+
<span style={{ color: progress.errored > 0 || stalled ? '#f59e0b' : '#10b981' }}>
|
|
290
303
|
Done! {progress.complete}/{progress.total} optimized.
|
|
291
304
|
</span>
|
|
292
|
-
{progress.errored > 0 && (
|
|
305
|
+
{(progress.errored > 0 || (stalled && progress.pending > 0)) && (
|
|
293
306
|
<span style={{ color: '#ef4444' }}>
|
|
294
|
-
{' '}{progress.errored} failed.
|
|
307
|
+
{' '}{progress.errored + (stalled ? progress.pending : 0)} failed.
|
|
295
308
|
</span>
|
|
296
309
|
)}
|
|
297
310
|
</span>
|
package/src/hooks/afterChange.ts
CHANGED
|
@@ -14,7 +14,10 @@ export const createAfterChangeHook = (
|
|
|
14
14
|
return async ({ context, doc, req }) => {
|
|
15
15
|
if (context?.imageOptimizer_skip) return doc
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Use context flag from beforeChange instead of checking req.file.data directly.
|
|
18
|
+
// Cloud storage adapters may consume req.file.data in their own afterChange hook
|
|
19
|
+
// before ours runs, which would cause this guard to bail out and leave status as 'pending'.
|
|
20
|
+
if (!context?.imageOptimizer_hasUpload) return doc
|
|
18
21
|
|
|
19
22
|
const collectionConfig = req.payload.collections[collectionSlug as keyof typeof req.payload.collections].config
|
|
20
23
|
const cloudStorage = isCloudStorage(collectionConfig)
|
|
@@ -52,8 +55,10 @@ export const createAfterChangeHook = (
|
|
|
52
55
|
id: doc.id,
|
|
53
56
|
data: {
|
|
54
57
|
imageOptimizer: {
|
|
58
|
+
...doc.imageOptimizer,
|
|
55
59
|
status: 'complete',
|
|
56
60
|
variants: [],
|
|
61
|
+
error: null,
|
|
57
62
|
},
|
|
58
63
|
},
|
|
59
64
|
context: { imageOptimizer_skip: true },
|
|
@@ -70,8 +75,10 @@ export const createAfterChangeHook = (
|
|
|
70
75
|
id: doc.id,
|
|
71
76
|
data: {
|
|
72
77
|
imageOptimizer: {
|
|
78
|
+
...doc.imageOptimizer,
|
|
73
79
|
status: 'complete',
|
|
74
80
|
variants: [],
|
|
81
|
+
error: null,
|
|
75
82
|
},
|
|
76
83
|
},
|
|
77
84
|
context: { imageOptimizer_skip: true },
|
|
@@ -28,8 +28,10 @@ export const createConvertFormatsHandler = (resolvedConfig: ResolvedImageOptimiz
|
|
|
28
28
|
id: input.docId,
|
|
29
29
|
data: {
|
|
30
30
|
imageOptimizer: {
|
|
31
|
+
...doc.imageOptimizer,
|
|
31
32
|
status: 'complete',
|
|
32
33
|
variants: [],
|
|
34
|
+
error: null,
|
|
33
35
|
},
|
|
34
36
|
},
|
|
35
37
|
context: { imageOptimizer_skip: true },
|
|
@@ -86,8 +88,10 @@ export const createConvertFormatsHandler = (resolvedConfig: ResolvedImageOptimiz
|
|
|
86
88
|
id: input.docId,
|
|
87
89
|
data: {
|
|
88
90
|
imageOptimizer: {
|
|
91
|
+
...doc.imageOptimizer,
|
|
89
92
|
status: 'complete',
|
|
90
93
|
variants,
|
|
94
|
+
error: null,
|
|
91
95
|
},
|
|
92
96
|
},
|
|
93
97
|
context: { imageOptimizer_skip: true },
|