@inoo-ch/payload-image-optimizer 1.8.1 → 1.9.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.
Files changed (36) hide show
  1. package/dist/components/RegenerationButton.js +85 -21
  2. package/dist/components/RegenerationButton.js.map +1 -1
  3. package/dist/defaults.js +14 -2
  4. package/dist/defaults.js.map +1 -1
  5. package/dist/endpoints/regenerate.d.ts +1 -0
  6. package/dist/endpoints/regenerate.js +77 -1
  7. package/dist/endpoints/regenerate.js.map +1 -1
  8. package/dist/hooks/beforeChange.js +15 -18
  9. package/dist/hooks/beforeChange.js.map +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.js +32 -5
  12. package/dist/index.js.map +1 -1
  13. package/dist/tasks/regenerateDocument.js +27 -4
  14. package/dist/tasks/regenerateDocument.js.map +1 -1
  15. package/dist/types.d.ts +49 -2
  16. package/dist/types.js.map +1 -1
  17. package/dist/utilities/filenameStrategies.d.ts +25 -0
  18. package/dist/utilities/filenameStrategies.js +46 -0
  19. package/dist/utilities/filenameStrategies.js.map +1 -0
  20. package/dist/utilities/stripDiacritics.d.ts +9 -0
  21. package/dist/utilities/stripDiacritics.js +10 -0
  22. package/dist/utilities/stripDiacritics.js.map +1 -0
  23. package/dist/utilities/toKebabCase.d.ts +10 -0
  24. package/dist/utilities/toKebabCase.js +11 -0
  25. package/dist/utilities/toKebabCase.js.map +1 -0
  26. package/package.json +1 -1
  27. package/src/components/RegenerationButton.tsx +92 -24
  28. package/src/defaults.ts +15 -1
  29. package/src/endpoints/regenerate.ts +68 -0
  30. package/src/hooks/beforeChange.ts +15 -18
  31. package/src/index.ts +27 -6
  32. package/src/tasks/regenerateDocument.ts +24 -4
  33. package/src/types.ts +51 -2
  34. package/src/utilities/filenameStrategies.ts +61 -0
  35. package/src/utilities/stripDiacritics.ts +10 -0
  36. package/src/utilities/toKebabCase.ts +16 -0
@@ -16,6 +16,7 @@ export const RegenerationButton = ()=>{
16
16
  const [force, setForce] = useState(false);
17
17
  const [error, setError] = useState(null);
18
18
  const [stalled, setStalled] = useState(false);
19
+ const [cancelled, setCancelled] = useState(false);
19
20
  const [collectionSlug, setCollectionSlug] = useState(null);
20
21
  const [stats, setStats] = useState(null);
21
22
  const [confirming, setConfirming] = useState(false);
@@ -24,6 +25,9 @@ export const RegenerationButton = ()=>{
24
25
  lastProcessed: 0,
25
26
  stallCount: 0
26
27
  });
28
+ // Snapshot of complete+errored at the moment regeneration starts,
29
+ // so we can compute batch-relative progress for selective regeneration.
30
+ const baselineRef = useRef(null);
27
31
  const prevIsRunningRef = useRef(false);
28
32
  // Extract collection slug from URL after mount to avoid hydration mismatch
29
33
  useEffect(()=>{
@@ -65,6 +69,15 @@ export const RegenerationButton = ()=>{
65
69
  if (res.ok) {
66
70
  const data = await res.json();
67
71
  setProgress(data);
72
+ // Stop polling if server reports cancellation
73
+ if (data.cancelled) {
74
+ setCancelled(true);
75
+ setIsRunning(false);
76
+ setStalled(false);
77
+ stopPolling();
78
+ sessionStorage.removeItem(SESSION_KEY);
79
+ return;
80
+ }
68
81
  // Stop polling when no more pending
69
82
  if (data.pending <= 0) {
70
83
  setIsRunning(false);
@@ -158,12 +171,35 @@ export const RegenerationButton = ()=>{
158
171
  const handleCancel = ()=>{
159
172
  setConfirming(false);
160
173
  };
174
+ const handleStop = async ()=>{
175
+ if (!collectionSlug) return;
176
+ try {
177
+ await fetch('/api/image-optimizer/regenerate', {
178
+ method: 'DELETE',
179
+ headers: {
180
+ 'Content-Type': 'application/json'
181
+ },
182
+ body: JSON.stringify({
183
+ collectionSlug
184
+ })
185
+ });
186
+ setCancelled(true);
187
+ setIsRunning(false);
188
+ setStalled(false);
189
+ stopPolling();
190
+ sessionStorage.removeItem(SESSION_KEY);
191
+ fetchStats();
192
+ } catch {
193
+ // ignore cancel errors
194
+ }
195
+ };
161
196
  // Phase 2: Actually start regeneration (after user confirms)
162
197
  const handleConfirm = async ()=>{
163
198
  if (!collectionSlug) return;
164
199
  setConfirming(false);
165
200
  setError(null);
166
201
  setStalled(false);
202
+ setCancelled(false);
167
203
  setIsRunning(true);
168
204
  setQueued(null);
169
205
  setProgress(null);
@@ -171,6 +207,8 @@ export const RegenerationButton = ()=>{
171
207
  lastProcessed: 0,
172
208
  stallCount: 0
173
209
  };
210
+ // Capture current complete+errored as baseline before new jobs run
211
+ baselineRef.current = stats ? stats.complete + stats.errored : 0;
174
212
  try {
175
213
  const requestBody = {
176
214
  collectionSlug,
@@ -212,7 +250,13 @@ export const RegenerationButton = ()=>{
212
250
  stopPolling
213
251
  ]);
214
252
  if (!collectionSlug) return null;
215
- const progressPercent = progress && progress.total > 0 ? Math.round((progress.complete + progress.errored) / progress.total * 100) : 0;
253
+ // When a batch is running, compute progress relative to the queued count
254
+ // (not the total collection) so selective regeneration shows e.g. 1/2, not 1/167.
255
+ const batchTotal = queued ?? progress?.total ?? 0;
256
+ const batchProcessed = progress ? progress.complete + progress.errored - (baselineRef.current ?? 0) : 0;
257
+ const batchComplete = progress ? progress.complete - Math.max((baselineRef.current ?? 0) - progress.errored, 0) : 0;
258
+ const batchErrored = progress ? Math.max(batchProcessed - Math.max(batchComplete, 0), 0) : 0;
259
+ const progressPercent = batchTotal > 0 ? Math.min(Math.round(batchProcessed / batchTotal * 100), 100) : 0;
216
260
  const showProgressBar = isRunning && progress || stalled && progress;
217
261
  // Stats computations
218
262
  const statsPercent = stats && stats.total > 0 ? Math.round(stats.complete / stats.total * 100) : 0;
@@ -227,20 +271,33 @@ export const RegenerationButton = ()=>{
227
271
  flexWrap: 'wrap'
228
272
  },
229
273
  children: [
230
- !confirming && /*#__PURE__*/ _jsx("button", {
274
+ !confirming && !isRunning && /*#__PURE__*/ _jsx("button", {
231
275
  onClick: handlePreflight,
232
- disabled: isRunning,
233
276
  style: {
234
- backgroundColor: isRunning ? '#9ca3af' : '#4f46e5',
277
+ backgroundColor: '#4f46e5',
278
+ color: '#fff',
279
+ border: 'none',
280
+ borderRadius: '6px',
281
+ padding: '8px 16px',
282
+ fontSize: '14px',
283
+ fontWeight: 500,
284
+ cursor: 'pointer'
285
+ },
286
+ children: hasSelection ? `Regenerate ${selectionCount} Selected` : 'Regenerate All Images'
287
+ }),
288
+ !confirming && isRunning && /*#__PURE__*/ _jsx("button", {
289
+ onClick: handleStop,
290
+ style: {
291
+ backgroundColor: '#ef4444',
235
292
  color: '#fff',
236
293
  border: 'none',
237
294
  borderRadius: '6px',
238
295
  padding: '8px 16px',
239
296
  fontSize: '14px',
240
297
  fontWeight: 500,
241
- cursor: isRunning ? 'not-allowed' : 'pointer'
298
+ cursor: 'pointer'
242
299
  },
243
- children: isRunning ? 'Processing images...' : hasSelection ? `Regenerate ${selectionCount} Selected` : 'Regenerate All Images'
300
+ children: "Stop Processing"
244
301
  }),
245
302
  confirming && stats && /*#__PURE__*/ _jsxs("div", {
246
303
  style: {
@@ -323,13 +380,20 @@ export const RegenerationButton = ()=>{
323
380
  " for processing"
324
381
  ]
325
382
  }),
326
- queued === 0 && !isRunning && !stalled && !confirming && /*#__PURE__*/ _jsx("span", {
383
+ queued === 0 && !isRunning && !stalled && !confirming && !cancelled && /*#__PURE__*/ _jsx("span", {
327
384
  style: {
328
385
  color: '#10b981',
329
386
  fontSize: '13px'
330
387
  },
331
388
  children: "All images already optimized."
332
389
  }),
390
+ cancelled && !isRunning && !confirming && /*#__PURE__*/ _jsx("span", {
391
+ style: {
392
+ color: '#f59e0b',
393
+ fontSize: '13px'
394
+ },
395
+ children: "Processing cancelled."
396
+ }),
333
397
  stalled && progress && /*#__PURE__*/ _jsxs("span", {
334
398
  style: {
335
399
  color: '#f59e0b',
@@ -359,18 +423,18 @@ export const RegenerationButton = ()=>{
359
423
  children: [
360
424
  /*#__PURE__*/ _jsxs("span", {
361
425
  children: [
362
- progress.complete,
426
+ Math.max(batchProcessed, 0),
363
427
  " / ",
364
- progress.total,
428
+ batchTotal,
365
429
  " complete"
366
430
  ]
367
431
  }),
368
- progress.errored > 0 && /*#__PURE__*/ _jsxs("span", {
432
+ batchErrored > 0 && /*#__PURE__*/ _jsxs("span", {
369
433
  style: {
370
434
  color: '#ef4444'
371
435
  },
372
436
  children: [
373
- progress.errored,
437
+ batchErrored,
374
438
  " errors"
375
439
  ]
376
440
  }),
@@ -394,15 +458,15 @@ export const RegenerationButton = ()=>{
394
458
  /*#__PURE__*/ _jsx("div", {
395
459
  style: {
396
460
  height: '100%',
397
- width: `${progress.total > 0 ? Math.round(progress.complete / progress.total * 100) : 0}%`,
461
+ width: `${batchTotal > 0 ? Math.min(Math.round((batchProcessed - batchErrored) / batchTotal * 100), 100) : 0}%`,
398
462
  backgroundColor: '#10b981',
399
463
  transition: 'width 0.3s ease'
400
464
  }
401
465
  }),
402
- progress.errored > 0 && /*#__PURE__*/ _jsx("div", {
466
+ batchErrored > 0 && /*#__PURE__*/ _jsx("div", {
403
467
  style: {
404
468
  height: '100%',
405
- width: `${progress.total > 0 ? Math.round(progress.errored / progress.total * 100) : 0}%`,
469
+ width: `${batchTotal > 0 ? Math.round(batchErrored / batchTotal * 100) : 0}%`,
406
470
  backgroundColor: '#ef4444',
407
471
  transition: 'width 0.3s ease'
408
472
  }
@@ -411,30 +475,30 @@ export const RegenerationButton = ()=>{
411
475
  })
412
476
  ]
413
477
  }),
414
- !isRunning && !stalled && progress && progress.complete > 0 && queued !== 0 && !confirming && /*#__PURE__*/ _jsxs("span", {
478
+ !isRunning && !stalled && !cancelled && progress && batchProcessed > 0 && queued !== 0 && !confirming && /*#__PURE__*/ _jsxs("span", {
415
479
  style: {
416
480
  fontSize: '13px'
417
481
  },
418
482
  children: [
419
483
  /*#__PURE__*/ _jsxs("span", {
420
484
  style: {
421
- color: progress.errored > 0 ? '#f59e0b' : '#10b981'
485
+ color: batchErrored > 0 ? '#f59e0b' : '#10b981'
422
486
  },
423
487
  children: [
424
488
  "Done! ",
425
- progress.complete,
489
+ Math.max(batchProcessed - batchErrored, 0),
426
490
  "/",
427
- progress.total,
428
- " optimized (across entire collection)."
491
+ batchTotal,
492
+ " optimized."
429
493
  ]
430
494
  }),
431
- progress.errored > 0 && /*#__PURE__*/ _jsxs("span", {
495
+ batchErrored > 0 && /*#__PURE__*/ _jsxs("span", {
432
496
  style: {
433
497
  color: '#ef4444'
434
498
  },
435
499
  children: [
436
500
  ' ',
437
- progress.errored,
501
+ batchErrored,
438
502
  " failed."
439
503
  ]
440
504
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/RegenerationButton.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState, useEffect, useCallback, useRef } from 'react'\nimport { useSelection } from '@payloadcms/ui'\n\ntype RegenerationProgress = {\n total: number\n complete: number\n errored: number\n pending: number\n}\n\nconst POLL_INTERVAL_MS = 2000\n// With sequential processing each image takes ~4-5s, so no progress for 30s\n// (15 polls) strongly suggests a real stall rather than slow processing.\nconst STALL_THRESHOLD = 15\nconst SESSION_KEY = 'imageOptimizer_running'\n\nexport const RegenerationButton: React.FC = () => {\n const { count: selectionCount, getSelectedIds } = useSelection()\n const hasSelection = selectionCount > 0\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 [confirming, setConfirming] = useState(false)\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 startPolling = useCallback(\n (pollFn: () => void) => {\n // Prevent duplicate intervals\n stopPolling()\n intervalRef.current = setInterval(pollFn, POLL_INTERVAL_MS)\n },\n [stopPolling],\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 setStalled(false)\n stopPolling()\n sessionStorage.removeItem(SESSION_KEY)\n return\n }\n\n // Stall detection — warn but keep polling so we detect when jobs resume\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 // Clear stall warning when progress resumes\n setStalled(false)\n }\n\n if (stallRef.current.stallCount >= STALL_THRESHOLD) {\n setStalled(true)\n // Keep polling — jobs may still be running server-side\n }\n }\n } catch {\n // ignore polling errors\n }\n }, [collectionSlug, stopPolling])\n\n // On mount: fetch stats for the counter display. If the user previously\n // triggered regeneration (sessionStorage flag) and there are still pending\n // images, resume polling so the UI reconnects after page navigation.\n useEffect(() => {\n if (!collectionSlug) return\n let cancelled = false\n const loadStats = 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 setStats(data)\n\n // Resume polling only if the user triggered regeneration in this session\n const wasRunning = sessionStorage.getItem(SESSION_KEY) === collectionSlug\n if (wasRunning && data.pending > 0) {\n setProgress(data)\n setIsRunning(true)\n setStalled(false)\n stallRef.current = { lastProcessed: data.complete + data.errored, stallCount: 0 }\n startPolling(pollProgress)\n } else if (wasRunning && data.pending <= 0) {\n // Jobs finished while we were away — clear the flag\n sessionStorage.removeItem(SESSION_KEY)\n }\n } catch {\n // ignore\n }\n }\n loadStats()\n return () => {\n cancelled = true\n stopPolling()\n }\n }, [collectionSlug, pollProgress, startPolling, stopPolling])\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 // Phase 1: Show confirmation with counts\n const handlePreflight = async () => {\n if (!collectionSlug) return\n setError(null)\n // Refresh stats to get the latest counts before confirming\n await fetchStats()\n setConfirming(true)\n }\n\n const handleCancel = () => {\n setConfirming(false)\n }\n\n // Phase 2: Actually start regeneration (after user confirms)\n const handleConfirm = async () => {\n if (!collectionSlug) return\n setConfirming(false)\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 requestBody: Record<string, unknown> = { collectionSlug, force }\n if (hasSelection) {\n requestBody.docIds = getSelectedIds().map(String)\n }\n\n const res = await fetch('/api/image-optimizer/regenerate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(requestBody),\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 // Persist running state so we can resume after page navigation\n sessionStorage.setItem(SESSION_KEY, collectionSlug)\n // Start polling\n startPolling(pollProgress)\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 () => stopPolling()\n }, [stopPolling])\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 {!confirming && (\n <button\n onClick={handlePreflight}\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\n ? 'Processing images...'\n : hasSelection\n ? `Regenerate ${selectionCount} Selected`\n : 'Regenerate All Images'}\n </button>\n )}\n\n {confirming && stats && (\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>\n <span style={{ fontSize: '13px', color: '#374151' }}>\n {hasSelection\n ? `Regenerate ${selectionCount} selected image${selectionCount !== 1 ? 's' : ''}?`\n : force\n ? `Re-process all ${stats.total} images across the entire collection?`\n : `Regenerate ${stats.pending} unoptimized image${stats.pending !== 1 ? 's' : ''} across the entire collection?`}\n </span>\n <button\n onClick={handleConfirm}\n style={{\n backgroundColor: '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Confirm\n </button>\n <button\n onClick={handleCancel}\n style={{\n backgroundColor: 'transparent',\n color: '#6b7280',\n border: '1px solid #d1d5db',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Cancel\n </button>\n </div>\n )}\n\n {!confirming && (\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\n {error && (\n <span style={{ color: '#ef4444', fontSize: '13px' }}>{error}</span>\n )}\n\n {queued !== null && queued > 0 && isRunning && !confirming && (\n <span style={{ color: '#4f46e5', fontSize: '13px' }}>\n Queued {queued} image{queued !== 1 ? 's' : ''} for processing\n </span>\n )}\n\n {queued === 0 && !isRunning && !stalled && !confirming && (\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 appears slow — {progress.pending} image{progress.pending !== 1 ? 's' : ''} still pending.\n Jobs may still be running server-side.\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 && !stalled && progress && progress.complete > 0 && queued !== 0 && !confirming && (\n <span style={{ fontSize: '13px' }}>\n <span style={{ color: progress.errored > 0 ? '#f59e0b' : '#10b981' }}>\n Done! {progress.complete}/{progress.total} optimized (across entire collection).\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 && !stalled && 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 &#10003; 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' }}>&middot;</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","useSelection","POLL_INTERVAL_MS","STALL_THRESHOLD","SESSION_KEY","RegenerationButton","count","selectionCount","getSelectedIds","hasSelection","isRunning","setIsRunning","progress","setProgress","queued","setQueued","force","setForce","error","setError","stalled","setStalled","collectionSlug","setCollectionSlug","stats","setStats","confirming","setConfirming","intervalRef","stallRef","lastProcessed","stallCount","prevIsRunningRef","slug","window","location","pathname","split","fetchStats","res","fetch","ok","data","json","stopPolling","current","clearInterval","startPolling","pollFn","setInterval","pollProgress","pending","sessionStorage","removeItem","processed","complete","errored","cancelled","loadStats","wasRunning","getItem","handlePreflight","handleCancel","handleConfirm","requestBody","docIds","map","String","method","headers","body","JSON","stringify","Error","setItem","err","message","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","span","label","input","type","checked","onChange","e","target","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'\nimport { useSelection } from '@payloadcms/ui'\n\ntype RegenerationProgress = {\n total: number\n complete: number\n errored: number\n pending: number\n}\n\nconst POLL_INTERVAL_MS = 2000\n// With sequential processing each image takes ~4-5s, so no progress for 30s\n// (15 polls) strongly suggests a real stall rather than slow processing.\nconst STALL_THRESHOLD = 15\nconst SESSION_KEY = 'imageOptimizer_running'\n\nexport const RegenerationButton: React.FC = () => {\n const { count: selectionCount, getSelectedIds } = useSelection()\n const hasSelection = selectionCount > 0\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 [cancelled, setCancelled] = useState(false)\n const [collectionSlug, setCollectionSlug] = useState<string | null>(null)\n const [stats, setStats] = useState<RegenerationProgress | null>(null)\n const [confirming, setConfirming] = useState(false)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const stallRef = useRef({ lastProcessed: 0, stallCount: 0 })\n // Snapshot of complete+errored at the moment regeneration starts,\n // so we can compute batch-relative progress for selective regeneration.\n const baselineRef = useRef<number | null>(null)\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 startPolling = useCallback(\n (pollFn: () => void) => {\n // Prevent duplicate intervals\n stopPolling()\n intervalRef.current = setInterval(pollFn, POLL_INTERVAL_MS)\n },\n [stopPolling],\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\n // Stop polling if server reports cancellation\n if (data.cancelled) {\n setCancelled(true)\n setIsRunning(false)\n setStalled(false)\n stopPolling()\n sessionStorage.removeItem(SESSION_KEY)\n return\n }\n\n // Stop polling when no more pending\n if (data.pending <= 0) {\n setIsRunning(false)\n setStalled(false)\n stopPolling()\n sessionStorage.removeItem(SESSION_KEY)\n return\n }\n\n // Stall detection — warn but keep polling so we detect when jobs resume\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 // Clear stall warning when progress resumes\n setStalled(false)\n }\n\n if (stallRef.current.stallCount >= STALL_THRESHOLD) {\n setStalled(true)\n // Keep polling — jobs may still be running server-side\n }\n }\n } catch {\n // ignore polling errors\n }\n }, [collectionSlug, stopPolling])\n\n // On mount: fetch stats for the counter display. If the user previously\n // triggered regeneration (sessionStorage flag) and there are still pending\n // images, resume polling so the UI reconnects after page navigation.\n useEffect(() => {\n if (!collectionSlug) return\n let cancelled = false\n const loadStats = 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 setStats(data)\n\n // Resume polling only if the user triggered regeneration in this session\n const wasRunning = sessionStorage.getItem(SESSION_KEY) === collectionSlug\n if (wasRunning && data.pending > 0) {\n setProgress(data)\n setIsRunning(true)\n setStalled(false)\n stallRef.current = { lastProcessed: data.complete + data.errored, stallCount: 0 }\n startPolling(pollProgress)\n } else if (wasRunning && data.pending <= 0) {\n // Jobs finished while we were away — clear the flag\n sessionStorage.removeItem(SESSION_KEY)\n }\n } catch {\n // ignore\n }\n }\n loadStats()\n return () => {\n cancelled = true\n stopPolling()\n }\n }, [collectionSlug, pollProgress, startPolling, stopPolling])\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 // Phase 1: Show confirmation with counts\n const handlePreflight = async () => {\n if (!collectionSlug) return\n setError(null)\n // Refresh stats to get the latest counts before confirming\n await fetchStats()\n setConfirming(true)\n }\n\n const handleCancel = () => {\n setConfirming(false)\n }\n\n const handleStop = async () => {\n if (!collectionSlug) return\n try {\n await fetch('/api/image-optimizer/regenerate', {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ collectionSlug }),\n })\n setCancelled(true)\n setIsRunning(false)\n setStalled(false)\n stopPolling()\n sessionStorage.removeItem(SESSION_KEY)\n fetchStats()\n } catch {\n // ignore cancel errors\n }\n }\n\n // Phase 2: Actually start regeneration (after user confirms)\n const handleConfirm = async () => {\n if (!collectionSlug) return\n setConfirming(false)\n setError(null)\n setStalled(false)\n setCancelled(false)\n setIsRunning(true)\n setQueued(null)\n setProgress(null)\n stallRef.current = { lastProcessed: 0, stallCount: 0 }\n // Capture current complete+errored as baseline before new jobs run\n baselineRef.current = stats ? stats.complete + stats.errored : 0\n\n try {\n const requestBody: Record<string, unknown> = { collectionSlug, force }\n if (hasSelection) {\n requestBody.docIds = getSelectedIds().map(String)\n }\n\n const res = await fetch('/api/image-optimizer/regenerate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(requestBody),\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 // Persist running state so we can resume after page navigation\n sessionStorage.setItem(SESSION_KEY, collectionSlug)\n // Start polling\n startPolling(pollProgress)\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 () => stopPolling()\n }, [stopPolling])\n\n if (!collectionSlug) return null\n\n // When a batch is running, compute progress relative to the queued count\n // (not the total collection) so selective regeneration shows e.g. 1/2, not 1/167.\n const batchTotal = queued ?? progress?.total ?? 0\n const batchProcessed = progress\n ? (progress.complete + progress.errored) - (baselineRef.current ?? 0)\n : 0\n const batchComplete = progress\n ? progress.complete - Math.max((baselineRef.current ?? 0) - (progress.errored), 0)\n : 0\n const batchErrored = progress ? Math.max(batchProcessed - Math.max(batchComplete, 0), 0) : 0\n\n const progressPercent =\n batchTotal > 0\n ? Math.min(Math.round((batchProcessed / batchTotal) * 100), 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 {!confirming && !isRunning && (\n <button\n onClick={handlePreflight}\n style={{\n backgroundColor: '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '8px 16px',\n fontSize: '14px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n {hasSelection\n ? `Regenerate ${selectionCount} Selected`\n : 'Regenerate All Images'}\n </button>\n )}\n\n {!confirming && isRunning && (\n <button\n onClick={handleStop}\n style={{\n backgroundColor: '#ef4444',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '8px 16px',\n fontSize: '14px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Stop Processing\n </button>\n )}\n\n {confirming && stats && (\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>\n <span style={{ fontSize: '13px', color: '#374151' }}>\n {hasSelection\n ? `Regenerate ${selectionCount} selected image${selectionCount !== 1 ? 's' : ''}?`\n : force\n ? `Re-process all ${stats.total} images across the entire collection?`\n : `Regenerate ${stats.pending} unoptimized image${stats.pending !== 1 ? 's' : ''} across the entire collection?`}\n </span>\n <button\n onClick={handleConfirm}\n style={{\n backgroundColor: '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Confirm\n </button>\n <button\n onClick={handleCancel}\n style={{\n backgroundColor: 'transparent',\n color: '#6b7280',\n border: '1px solid #d1d5db',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Cancel\n </button>\n </div>\n )}\n\n {!confirming && (\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\n {error && (\n <span style={{ color: '#ef4444', fontSize: '13px' }}>{error}</span>\n )}\n\n {queued !== null && queued > 0 && isRunning && !confirming && (\n <span style={{ color: '#4f46e5', fontSize: '13px' }}>\n Queued {queued} image{queued !== 1 ? 's' : ''} for processing\n </span>\n )}\n\n {queued === 0 && !isRunning && !stalled && !confirming && !cancelled && (\n <span style={{ color: '#10b981', fontSize: '13px' }}>\n All images already optimized.\n </span>\n )}\n\n {cancelled && !isRunning && !confirming && (\n <span style={{ color: '#f59e0b', fontSize: '13px' }}>\n Processing cancelled.\n </span>\n )}\n\n {stalled && progress && (\n <span style={{ color: '#f59e0b', fontSize: '13px' }}>\n Processing appears slow — {progress.pending} image{progress.pending !== 1 ? 's' : ''} still pending.\n Jobs may still be running server-side.\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 {Math.max(batchProcessed, 0)} / {batchTotal} complete\n </span>\n {batchErrored > 0 && (\n <span style={{ color: '#ef4444' }}>{batchErrored} 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: `${batchTotal > 0 ? Math.min(Math.round(((batchProcessed - batchErrored) / batchTotal) * 100), 100) : 0}%`,\n backgroundColor: '#10b981',\n transition: 'width 0.3s ease',\n }}\n />\n {batchErrored > 0 && (\n <div\n style={{\n height: '100%',\n width: `${batchTotal > 0 ? Math.round((batchErrored / batchTotal) * 100) : 0}%`,\n backgroundColor: '#ef4444',\n transition: 'width 0.3s ease',\n }}\n />\n )}\n </div>\n </div>\n )}\n\n {!isRunning && !stalled && !cancelled && progress && batchProcessed > 0 && queued !== 0 && !confirming && (\n <span style={{ fontSize: '13px' }}>\n <span style={{ color: batchErrored > 0 ? '#f59e0b' : '#10b981' }}>\n Done! {Math.max(batchProcessed - batchErrored, 0)}/{batchTotal} optimized.\n </span>\n {batchErrored > 0 && (\n <span style={{ color: '#ef4444' }}>\n {' '}{batchErrored} failed.\n </span>\n )}\n </span>\n )}\n\n {/* Persistent optimization stats — always visible when not actively regenerating */}\n {!isRunning && !stalled && 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 &#10003; 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' }}>&middot;</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","useSelection","POLL_INTERVAL_MS","STALL_THRESHOLD","SESSION_KEY","RegenerationButton","count","selectionCount","getSelectedIds","hasSelection","isRunning","setIsRunning","progress","setProgress","queued","setQueued","force","setForce","error","setError","stalled","setStalled","cancelled","setCancelled","collectionSlug","setCollectionSlug","stats","setStats","confirming","setConfirming","intervalRef","stallRef","lastProcessed","stallCount","baselineRef","prevIsRunningRef","slug","window","location","pathname","split","fetchStats","res","fetch","ok","data","json","stopPolling","current","clearInterval","startPolling","pollFn","setInterval","pollProgress","sessionStorage","removeItem","pending","processed","complete","errored","loadStats","wasRunning","getItem","handlePreflight","handleCancel","handleStop","method","headers","body","JSON","stringify","handleConfirm","requestBody","docIds","map","String","Error","setItem","err","message","batchTotal","total","batchProcessed","batchComplete","Math","max","batchErrored","progressPercent","min","round","showProgressBar","statsPercent","allOptimized","div","style","padding","borderBottom","display","alignItems","gap","flexWrap","button","onClick","backgroundColor","color","border","borderRadius","fontSize","fontWeight","cursor","span","label","input","type","checked","onChange","e","target","disabled","flex","minWidth","justifyContent","marginBottom","height","overflow","width","transition","marginLeft","flexDirection"],"mappings":"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"}
package/dist/defaults.js CHANGED
@@ -1,3 +1,14 @@
1
+ import { uuidFilename } from './utilities/filenameStrategies.js';
2
+ /**
3
+ * Resolve the generateFilename option:
4
+ * - Explicit `generateFilename` callback takes priority
5
+ * - `uniqueFileNames: true` maps to `uuidFilename` for backwards compat
6
+ * - Otherwise undefined (keep original filename)
7
+ */ const resolveGenerateFilename = (config)=>{
8
+ if (config.generateFilename) return config.generateFilename;
9
+ if (config.uniqueFileNames) return uuidFilename;
10
+ return undefined;
11
+ };
1
12
  export const resolveConfig = (config)=>({
2
13
  clientOptimization: config.clientOptimization ?? true,
3
14
  collections: config.collections,
@@ -8,14 +19,15 @@ export const resolveConfig = (config)=>({
8
19
  quality: 80
9
20
  }
10
21
  ],
22
+ generateFilename: resolveGenerateFilename(config),
11
23
  generateThumbHash: config.generateThumbHash ?? true,
12
24
  maxDimensions: config.maxDimensions ?? {
13
25
  width: 2560,
14
26
  height: 2560
15
27
  },
28
+ regenerateButton: config.regenerateButton ?? true,
16
29
  replaceOriginal: config.replaceOriginal ?? true,
17
- stripMetadata: config.stripMetadata ?? true,
18
- uniqueFileNames: config.uniqueFileNames ?? false
30
+ stripMetadata: config.stripMetadata ?? true
19
31
  });
20
32
  export const resolveCollectionConfig = (resolvedConfig, collectionSlug)=>{
21
33
  const collectionValue = resolvedConfig.collections[collectionSlug];
@@ -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 clientOptimization: config.clientOptimization ?? true,\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 uniqueFileNames: config.uniqueFileNames ?? false,\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","clientOptimization","collections","disabled","formats","format","quality","generateThumbHash","maxDimensions","width","height","replaceOriginal","stripMetadata","uniqueFileNames","resolveCollectionConfig","resolvedConfig","collectionSlug","collectionValue"],"mappings":"AAIA,OAAO,MAAMA,gBAAgB,CAACC,SAAgE,CAAA;QAC5FC,oBAAoBD,OAAOC,kBAAkB,IAAI;QACjDC,aAAaF,OAAOE,WAAW;QAC/BC,UAAUH,OAAOG,QAAQ,IAAI;QAC7BC,SAASJ,OAAOI,OAAO,IAAI;YACzB;gBAAEC,QAAQ;gBAAQC,SAAS;YAAG;SAC/B;QACDC,mBAAmBP,OAAOO,iBAAiB,IAAI;QAC/CC,eAAeR,OAAOQ,aAAa,IAAI;YAAEC,OAAO;YAAMC,QAAQ;QAAK;QACnEC,iBAAiBX,OAAOW,eAAe,IAAI;QAC3CC,eAAeZ,OAAOY,aAAa,IAAI;QACvCC,iBAAiBb,OAAOa,eAAe,IAAI;IAC7C,CAAA,EAAE;AAEF,OAAO,MAAMC,0BAA0B,CACrCC,gBACAC;IAEA,MAAMC,kBAAkBF,eAAeb,WAAW,CAACc,eAAiC;IAEpF,IAAI,CAACC,mBAAmBA,oBAAoB,MAAM;QAChD,OAAO;YACLb,SAASW,eAAeX,OAAO;YAC/BI,eAAeO,eAAeP,aAAa;YAC3CG,iBAAiBI,eAAeJ,eAAe;QACjD;IACF;IAEA,OAAO;QACLP,SAASa,gBAAgBb,OAAO,IAAIW,eAAeX,OAAO;QAC1DI,eAAeS,gBAAgBT,aAAa,IAAIO,eAAeP,aAAa;QAC5EG,iBAAiBM,gBAAgBN,eAAe,IAAII,eAAeJ,eAAe;IACpF;AACF,EAAC"}
1
+ {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nimport type { ImageOptimizerConfig, ResolvedCollectionOptimizerConfig, ResolvedImageOptimizerConfig } from './types.js'\nimport { uuidFilename } from './utilities/filenameStrategies.js'\n\n/**\n * Resolve the generateFilename option:\n * - Explicit `generateFilename` callback takes priority\n * - `uniqueFileNames: true` maps to `uuidFilename` for backwards compat\n * - Otherwise undefined (keep original filename)\n */\nconst resolveGenerateFilename = (config: ImageOptimizerConfig) => {\n if (config.generateFilename) return config.generateFilename\n if (config.uniqueFileNames) return uuidFilename\n return undefined\n}\n\nexport const resolveConfig = (config: ImageOptimizerConfig): ResolvedImageOptimizerConfig => ({\n clientOptimization: config.clientOptimization ?? true,\n collections: config.collections,\n disabled: config.disabled ?? false,\n formats: config.formats ?? [\n { format: 'webp', quality: 80 },\n ],\n generateFilename: resolveGenerateFilename(config),\n generateThumbHash: config.generateThumbHash ?? true,\n maxDimensions: config.maxDimensions ?? { width: 2560, height: 2560 },\n regenerateButton: config.regenerateButton ?? true,\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":["uuidFilename","resolveGenerateFilename","config","generateFilename","uniqueFileNames","undefined","resolveConfig","clientOptimization","collections","disabled","formats","format","quality","generateThumbHash","maxDimensions","width","height","regenerateButton","replaceOriginal","stripMetadata","resolveCollectionConfig","resolvedConfig","collectionSlug","collectionValue"],"mappings":"AAGA,SAASA,YAAY,QAAQ,oCAAmC;AAEhE;;;;;CAKC,GACD,MAAMC,0BAA0B,CAACC;IAC/B,IAAIA,OAAOC,gBAAgB,EAAE,OAAOD,OAAOC,gBAAgB;IAC3D,IAAID,OAAOE,eAAe,EAAE,OAAOJ;IACnC,OAAOK;AACT;AAEA,OAAO,MAAMC,gBAAgB,CAACJ,SAAgE,CAAA;QAC5FK,oBAAoBL,OAAOK,kBAAkB,IAAI;QACjDC,aAAaN,OAAOM,WAAW;QAC/BC,UAAUP,OAAOO,QAAQ,IAAI;QAC7BC,SAASR,OAAOQ,OAAO,IAAI;YACzB;gBAAEC,QAAQ;gBAAQC,SAAS;YAAG;SAC/B;QACDT,kBAAkBF,wBAAwBC;QAC1CW,mBAAmBX,OAAOW,iBAAiB,IAAI;QAC/CC,eAAeZ,OAAOY,aAAa,IAAI;YAAEC,OAAO;YAAMC,QAAQ;QAAK;QACnEC,kBAAkBf,OAAOe,gBAAgB,IAAI;QAC7CC,iBAAiBhB,OAAOgB,eAAe,IAAI;QAC3CC,eAAejB,OAAOiB,aAAa,IAAI;IACzC,CAAA,EAAE;AAEF,OAAO,MAAMC,0BAA0B,CACrCC,gBACAC;IAEA,MAAMC,kBAAkBF,eAAeb,WAAW,CAACc,eAAiC;IAEpF,IAAI,CAACC,mBAAmBA,oBAAoB,MAAM;QAChD,OAAO;YACLb,SAASW,eAAeX,OAAO;YAC/BI,eAAeO,eAAeP,aAAa;YAC3CI,iBAAiBG,eAAeH,eAAe;QACjD;IACF;IAEA,OAAO;QACLR,SAASa,gBAAgBb,OAAO,IAAIW,eAAeX,OAAO;QAC1DI,eAAeS,gBAAgBT,aAAa,IAAIO,eAAeP,aAAa;QAC5EI,iBAAiBK,gBAAgBL,eAAe,IAAIG,eAAeH,eAAe;IACpF;AACF,EAAC"}
@@ -2,3 +2,4 @@ import type { PayloadHandler } from 'payload';
2
2
  import type { ResolvedImageOptimizerConfig } from '../types.js';
3
3
  export declare const createRegenerateHandler: (resolvedConfig: ResolvedImageOptimizerConfig) => PayloadHandler;
4
4
  export declare const createRegenerateStatusHandler: (resolvedConfig: ResolvedImageOptimizerConfig) => PayloadHandler;
5
+ export declare const createCancelHandler: (resolvedConfig: ResolvedImageOptimizerConfig) => PayloadHandler;
@@ -1,4 +1,36 @@
1
1
  import { waitUntil } from '../utilities/waitUntil.js';
2
+ const GLOBAL_SLUG = 'image-optimizer-state';
3
+ async function getCollectionState(payload, slug) {
4
+ try {
5
+ const state = await payload.findGlobal({
6
+ slug: GLOBAL_SLUG
7
+ });
8
+ return state?.collections?.[slug] || {};
9
+ } catch {
10
+ return {};
11
+ }
12
+ }
13
+ async function setCollectionState(payload, slug, update) {
14
+ let existing = {};
15
+ try {
16
+ const state = await payload.findGlobal({
17
+ slug: GLOBAL_SLUG
18
+ });
19
+ existing = state?.collections || {};
20
+ } catch {
21
+ // Global may not exist yet
22
+ }
23
+ existing[slug] = {
24
+ ...existing[slug],
25
+ ...update
26
+ };
27
+ await payload.updateGlobal({
28
+ slug: GLOBAL_SLUG,
29
+ data: {
30
+ collections: existing
31
+ }
32
+ });
33
+ }
2
34
  export const createRegenerateHandler = (resolvedConfig)=>{
3
35
  const handler = async (req)=>{
4
36
  if (!req.user) {
@@ -91,6 +123,12 @@ export const createRegenerateHandler = (resolvedConfig)=>{
91
123
  }
92
124
  }
93
125
  req.payload.logger.info(`Image optimizer: queued ${queued} images from '${collectionSlug}' for regeneration`);
126
+ // Clear any previous cancellation and record the start time + batch size
127
+ await setCollectionState(req.payload, collectionSlug, {
128
+ startedAt: Date.now(),
129
+ cancelledAt: undefined,
130
+ queued
131
+ });
94
132
  // Fire the job runner — use waitUntil to keep the serverless function alive
95
133
  // after the response is sent, so jobs actually complete on Vercel/serverless.
96
134
  if (queued > 0) {
@@ -159,12 +197,50 @@ export const createRegenerateStatusHandler = (resolvedConfig)=>{
159
197
  }
160
198
  }
161
199
  });
200
+ // Include cancellation state so the UI can react
201
+ const collState = await getCollectionState(req.payload, collectionSlug);
202
+ const cancelled = !!(collState.cancelledAt && collState.startedAt && collState.cancelledAt > collState.startedAt);
162
203
  return Response.json({
163
204
  collectionSlug,
164
205
  total: total.totalDocs,
165
206
  complete: complete.totalDocs,
166
207
  errored: errored.totalDocs,
167
- pending: total.totalDocs - complete.totalDocs - errored.totalDocs
208
+ pending: total.totalDocs - complete.totalDocs - errored.totalDocs,
209
+ cancelled
210
+ });
211
+ };
212
+ return handler;
213
+ };
214
+ export const createCancelHandler = (resolvedConfig)=>{
215
+ const handler = async (req)=>{
216
+ if (!req.user) {
217
+ return Response.json({
218
+ error: 'Unauthorized'
219
+ }, {
220
+ status: 401
221
+ });
222
+ }
223
+ let body;
224
+ try {
225
+ body = await req.json();
226
+ } catch {
227
+ body = {};
228
+ }
229
+ const collectionSlug = body.collectionSlug;
230
+ if (!collectionSlug || !resolvedConfig.collections[collectionSlug]) {
231
+ return Response.json({
232
+ error: 'Invalid or unconfigured collection slug'
233
+ }, {
234
+ status: 400
235
+ });
236
+ }
237
+ await setCollectionState(req.payload, collectionSlug, {
238
+ cancelledAt: Date.now()
239
+ });
240
+ req.payload.logger.info(`Image optimizer: cancellation requested for '${collectionSlug}'`);
241
+ return Response.json({
242
+ cancelled: true,
243
+ collectionSlug
168
244
  });
169
245
  };
170
246
  return handler;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/regenerate.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\nimport type { CollectionSlug, Where } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { waitUntil } from '../utilities/waitUntil.js'\n\nexport const createRegenerateHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: { collectionSlug?: string; force?: boolean; docIds?: string[] }\n try {\n body = await req.json!()\n } catch {\n body = {}\n }\n\n const collectionSlug = body.collectionSlug\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json(\n { error: 'Invalid or unconfigured collection slug' },\n { status: 400 },\n )\n }\n\n let queued = 0\n\n if (body.docIds && body.docIds.length > 0) {\n // Regenerate specific documents by ID\n for (const docId of body.docIds) {\n await req.payload.jobs.queue({\n task: 'imageOptimizer_regenerateDocument',\n input: {\n collectionSlug,\n docId: String(docId),\n },\n })\n queued++\n }\n } else {\n // Find all image documents in the collection\n // Unless force=true, skip already-processed docs\n const where: Where = body.force\n ? { mimeType: { contains: 'image/' } }\n : {\n and: [\n { mimeType: { contains: 'image/' } },\n {\n or: [\n { 'imageOptimizer.status': { not_equals: 'complete' } },\n { 'imageOptimizer.status': { exists: false } },\n ],\n },\n ],\n }\n\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await req.payload.find({\n collection: collectionSlug as CollectionSlug,\n limit: 50,\n page,\n depth: 0,\n where,\n sort: 'createdAt',\n })\n\n for (const doc of result.docs) {\n await req.payload.jobs.queue({\n task: 'imageOptimizer_regenerateDocument',\n input: {\n collectionSlug,\n docId: String(doc.id),\n },\n })\n queued++\n }\n\n hasMore = result.hasNextPage\n page++\n }\n }\n\n req.payload.logger.info(`Image optimizer: queued ${queued} images from '${collectionSlug}' for regeneration`)\n\n // Fire the job runner — use waitUntil to keep the serverless function alive\n // after the response is sent, so jobs actually complete on Vercel/serverless.\n if (queued > 0) {\n const runPromise = req.payload.jobs.run({ limit: queued, sequential: true }).catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Regeneration job runner failed')\n })\n waitUntil(runPromise, req)\n }\n\n return Response.json({ queued, collectionSlug })\n }\n\n return handler\n}\n\nexport const createRegenerateStatusHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url!)\n const collectionSlug = url.searchParams.get('collection')\n\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json({ error: 'Invalid collection slug' }, { status: 400 })\n }\n\n const total = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: { mimeType: { contains: 'image/' } },\n })\n\n const complete = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: {\n mimeType: { contains: 'image/' },\n 'imageOptimizer.status': { equals: 'complete' },\n },\n })\n\n const errored = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: {\n mimeType: { contains: 'image/' },\n 'imageOptimizer.status': { equals: 'error' },\n },\n })\n\n return Response.json({\n collectionSlug,\n total: total.totalDocs,\n complete: complete.totalDocs,\n errored: errored.totalDocs,\n pending: total.totalDocs - complete.totalDocs - errored.totalDocs,\n })\n }\n\n return handler\n}\n"],"names":["waitUntil","createRegenerateHandler","resolvedConfig","handler","req","user","Response","json","error","status","body","collectionSlug","collections","queued","docIds","length","docId","payload","jobs","queue","task","input","String","where","force","mimeType","contains","and","or","not_equals","exists","page","hasMore","result","find","collection","limit","depth","sort","doc","docs","id","hasNextPage","logger","info","runPromise","run","sequential","catch","err","createRegenerateStatusHandler","url","URL","searchParams","get","total","count","complete","equals","errored","totalDocs","pending"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/regenerate.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\nimport type { CollectionSlug, Where } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { waitUntil } from '../utilities/waitUntil.js'\n\ntype CollectionState = { startedAt?: number; cancelledAt?: number; queued?: number }\ntype StateCollections = Record<string, CollectionState>\n\nconst GLOBAL_SLUG = 'image-optimizer-state'\n\nasync function getCollectionState(payload: any, slug: string): Promise<CollectionState> {\n try {\n const state = await payload.findGlobal({ slug: GLOBAL_SLUG })\n return (state?.collections as StateCollections)?.[slug] || {}\n } catch {\n return {}\n }\n}\n\nasync function setCollectionState(payload: any, slug: string, update: Partial<CollectionState>): Promise<void> {\n let existing: StateCollections = {}\n try {\n const state = await payload.findGlobal({ slug: GLOBAL_SLUG })\n existing = (state?.collections as StateCollections) || {}\n } catch {\n // Global may not exist yet\n }\n existing[slug] = { ...existing[slug], ...update }\n await payload.updateGlobal({ slug: GLOBAL_SLUG, data: { collections: existing } })\n}\n\nexport const createRegenerateHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: { collectionSlug?: string; force?: boolean; docIds?: string[] }\n try {\n body = await req.json!()\n } catch {\n body = {}\n }\n\n const collectionSlug = body.collectionSlug\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json(\n { error: 'Invalid or unconfigured collection slug' },\n { status: 400 },\n )\n }\n\n let queued = 0\n\n if (body.docIds && body.docIds.length > 0) {\n // Regenerate specific documents by ID\n for (const docId of body.docIds) {\n await req.payload.jobs.queue({\n task: 'imageOptimizer_regenerateDocument',\n input: {\n collectionSlug,\n docId: String(docId),\n },\n })\n queued++\n }\n } else {\n // Find all image documents in the collection\n // Unless force=true, skip already-processed docs\n const where: Where = body.force\n ? { mimeType: { contains: 'image/' } }\n : {\n and: [\n { mimeType: { contains: 'image/' } },\n {\n or: [\n { 'imageOptimizer.status': { not_equals: 'complete' } },\n { 'imageOptimizer.status': { exists: false } },\n ],\n },\n ],\n }\n\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await req.payload.find({\n collection: collectionSlug as CollectionSlug,\n limit: 50,\n page,\n depth: 0,\n where,\n sort: 'createdAt',\n })\n\n for (const doc of result.docs) {\n await req.payload.jobs.queue({\n task: 'imageOptimizer_regenerateDocument',\n input: {\n collectionSlug,\n docId: String(doc.id),\n },\n })\n queued++\n }\n\n hasMore = result.hasNextPage\n page++\n }\n }\n\n req.payload.logger.info(`Image optimizer: queued ${queued} images from '${collectionSlug}' for regeneration`)\n\n // Clear any previous cancellation and record the start time + batch size\n await setCollectionState(req.payload, collectionSlug, {\n startedAt: Date.now(),\n cancelledAt: undefined,\n queued,\n })\n\n // Fire the job runner — use waitUntil to keep the serverless function alive\n // after the response is sent, so jobs actually complete on Vercel/serverless.\n if (queued > 0) {\n const runPromise = req.payload.jobs.run({ limit: queued, sequential: true }).catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Regeneration job runner failed')\n })\n waitUntil(runPromise, req)\n }\n\n return Response.json({ queued, collectionSlug })\n }\n\n return handler\n}\n\nexport const createRegenerateStatusHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url!)\n const collectionSlug = url.searchParams.get('collection')\n\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json({ error: 'Invalid collection slug' }, { status: 400 })\n }\n\n const total = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: { mimeType: { contains: 'image/' } },\n })\n\n const complete = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: {\n mimeType: { contains: 'image/' },\n 'imageOptimizer.status': { equals: 'complete' },\n },\n })\n\n const errored = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: {\n mimeType: { contains: 'image/' },\n 'imageOptimizer.status': { equals: 'error' },\n },\n })\n\n // Include cancellation state so the UI can react\n const collState = await getCollectionState(req.payload, collectionSlug)\n const cancelled = !!(collState.cancelledAt && collState.startedAt && collState.cancelledAt > collState.startedAt)\n\n return Response.json({\n collectionSlug,\n total: total.totalDocs,\n complete: complete.totalDocs,\n errored: errored.totalDocs,\n pending: total.totalDocs - complete.totalDocs - errored.totalDocs,\n cancelled,\n })\n }\n\n return handler\n}\n\nexport const createCancelHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: { collectionSlug?: string }\n try {\n body = await req.json!()\n } catch {\n body = {}\n }\n\n const collectionSlug = body.collectionSlug\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json({ error: 'Invalid or unconfigured collection slug' }, { status: 400 })\n }\n\n await setCollectionState(req.payload, collectionSlug, {\n cancelledAt: Date.now(),\n })\n\n req.payload.logger.info(`Image optimizer: cancellation requested for '${collectionSlug}'`)\n\n return Response.json({ cancelled: true, collectionSlug })\n }\n\n return handler\n}\n"],"names":["waitUntil","GLOBAL_SLUG","getCollectionState","payload","slug","state","findGlobal","collections","setCollectionState","update","existing","updateGlobal","data","createRegenerateHandler","resolvedConfig","handler","req","user","Response","json","error","status","body","collectionSlug","queued","docIds","length","docId","jobs","queue","task","input","String","where","force","mimeType","contains","and","or","not_equals","exists","page","hasMore","result","find","collection","limit","depth","sort","doc","docs","id","hasNextPage","logger","info","startedAt","Date","now","cancelledAt","undefined","runPromise","run","sequential","catch","err","createRegenerateStatusHandler","url","URL","searchParams","get","total","count","complete","equals","errored","collState","cancelled","totalDocs","pending","createCancelHandler"],"mappings":"AAIA,SAASA,SAAS,QAAQ,4BAA2B;AAKrD,MAAMC,cAAc;AAEpB,eAAeC,mBAAmBC,OAAY,EAAEC,IAAY;IAC1D,IAAI;QACF,MAAMC,QAAQ,MAAMF,QAAQG,UAAU,CAAC;YAAEF,MAAMH;QAAY;QAC3D,OAAO,AAACI,OAAOE,aAAkC,CAACH,KAAK,IAAI,CAAC;IAC9D,EAAE,OAAM;QACN,OAAO,CAAC;IACV;AACF;AAEA,eAAeI,mBAAmBL,OAAY,EAAEC,IAAY,EAAEK,MAAgC;IAC5F,IAAIC,WAA6B,CAAC;IAClC,IAAI;QACF,MAAML,QAAQ,MAAMF,QAAQG,UAAU,CAAC;YAAEF,MAAMH;QAAY;QAC3DS,WAAW,AAACL,OAAOE,eAAoC,CAAC;IAC1D,EAAE,OAAM;IACN,2BAA2B;IAC7B;IACAG,QAAQ,CAACN,KAAK,GAAG;QAAE,GAAGM,QAAQ,CAACN,KAAK;QAAE,GAAGK,MAAM;IAAC;IAChD,MAAMN,QAAQQ,YAAY,CAAC;QAAEP,MAAMH;QAAaW,MAAM;YAAEL,aAAaG;QAAS;IAAE;AAClF;AAEA,OAAO,MAAMG,0BAA0B,CAACC;IACtC,MAAMC,UAA0B,OAAOC;QACrC,IAAI,CAACA,IAAIC,IAAI,EAAE;YACb,OAAOC,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAe,GAAG;gBAAEC,QAAQ;YAAI;QAChE;QAEA,IAAIC;QACJ,IAAI;YACFA,OAAO,MAAMN,IAAIG,IAAI;QACvB,EAAE,OAAM;YACNG,OAAO,CAAC;QACV;QAEA,MAAMC,iBAAiBD,KAAKC,cAAc;QAC1C,IAAI,CAACA,kBAAkB,CAACT,eAAeP,WAAW,CAACgB,eAAiC,EAAE;YACpF,OAAOL,SAASC,IAAI,CAClB;gBAAEC,OAAO;YAA0C,GACnD;gBAAEC,QAAQ;YAAI;QAElB;QAEA,IAAIG,SAAS;QAEb,IAAIF,KAAKG,MAAM,IAAIH,KAAKG,MAAM,CAACC,MAAM,GAAG,GAAG;YACzC,sCAAsC;YACtC,KAAK,MAAMC,SAASL,KAAKG,MAAM,CAAE;gBAC/B,MAAMT,IAAIb,OAAO,CAACyB,IAAI,CAACC,KAAK,CAAC;oBAC3BC,MAAM;oBACNC,OAAO;wBACLR;wBACAI,OAAOK,OAAOL;oBAChB;gBACF;gBACAH;YACF;QACF,OAAO;YACL,6CAA6C;YAC7C,iDAAiD;YACjD,MAAMS,QAAeX,KAAKY,KAAK,GAC3B;gBAAEC,UAAU;oBAAEC,UAAU;gBAAS;YAAE,IACnC;gBACEC,KAAK;oBACH;wBAAEF,UAAU;4BAAEC,UAAU;wBAAS;oBAAE;oBACnC;wBACEE,IAAI;4BACF;gCAAE,yBAAyB;oCAAEC,YAAY;gCAAW;4BAAE;4BACtD;gCAAE,yBAAyB;oCAAEC,QAAQ;gCAAM;4BAAE;yBAC9C;oBACH;iBACD;YACH;YAEJ,IAAIC,OAAO;YACX,IAAIC,UAAU;YAEd,MAAOA,QAAS;gBACd,MAAMC,SAAS,MAAM3B,IAAIb,OAAO,CAACyC,IAAI,CAAC;oBACpCC,YAAYtB;oBACZuB,OAAO;oBACPL;oBACAM,OAAO;oBACPd;oBACAe,MAAM;gBACR;gBAEA,KAAK,MAAMC,OAAON,OAAOO,IAAI,CAAE;oBAC7B,MAAMlC,IAAIb,OAAO,CAACyB,IAAI,CAACC,KAAK,CAAC;wBAC3BC,MAAM;wBACNC,OAAO;4BACLR;4BACAI,OAAOK,OAAOiB,IAAIE,EAAE;wBACtB;oBACF;oBACA3B;gBACF;gBAEAkB,UAAUC,OAAOS,WAAW;gBAC5BX;YACF;QACF;QAEAzB,IAAIb,OAAO,CAACkD,MAAM,CAACC,IAAI,CAAC,CAAC,wBAAwB,EAAE9B,OAAO,cAAc,EAAED,eAAe,kBAAkB,CAAC;QAE5G,yEAAyE;QACzE,MAAMf,mBAAmBQ,IAAIb,OAAO,EAAEoB,gBAAgB;YACpDgC,WAAWC,KAAKC,GAAG;YACnBC,aAAaC;YACbnC;QACF;QAEA,4EAA4E;QAC5E,8EAA8E;QAC9E,IAAIA,SAAS,GAAG;YACd,MAAMoC,aAAa5C,IAAIb,OAAO,CAACyB,IAAI,CAACiC,GAAG,CAAC;gBAAEf,OAAOtB;gBAAQsC,YAAY;YAAK,GAAGC,KAAK,CAAC,CAACC;gBAClFhD,IAAIb,OAAO,CAACkD,MAAM,CAACjC,KAAK,CAAC;oBAAE4C;gBAAI,GAAG;YACpC;YACAhE,UAAU4D,YAAY5C;QACxB;QAEA,OAAOE,SAASC,IAAI,CAAC;YAAEK;YAAQD;QAAe;IAChD;IAEA,OAAOR;AACT,EAAC;AAED,OAAO,MAAMkD,gCAAgC,CAACnD;IAC5C,MAAMC,UAA0B,OAAOC;QACrC,IAAI,CAACA,IAAIC,IAAI,EAAE;YACb,OAAOC,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAe,GAAG;gBAAEC,QAAQ;YAAI;QAChE;QAEA,MAAM6C,MAAM,IAAIC,IAAInD,IAAIkD,GAAG;QAC3B,MAAM3C,iBAAiB2C,IAAIE,YAAY,CAACC,GAAG,CAAC;QAE5C,IAAI,CAAC9C,kBAAkB,CAACT,eAAeP,WAAW,CAACgB,eAAiC,EAAE;YACpF,OAAOL,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAA0B,GAAG;gBAAEC,QAAQ;YAAI;QAC3E;QAEA,MAAMiD,QAAQ,MAAMtD,IAAIb,OAAO,CAACoE,KAAK,CAAC;YACpC1B,YAAYtB;YACZU,OAAO;gBAAEE,UAAU;oBAAEC,UAAU;gBAAS;YAAE;QAC5C;QAEA,MAAMoC,WAAW,MAAMxD,IAAIb,OAAO,CAACoE,KAAK,CAAC;YACvC1B,YAAYtB;YACZU,OAAO;gBACLE,UAAU;oBAAEC,UAAU;gBAAS;gBAC/B,yBAAyB;oBAAEqC,QAAQ;gBAAW;YAChD;QACF;QAEA,MAAMC,UAAU,MAAM1D,IAAIb,OAAO,CAACoE,KAAK,CAAC;YACtC1B,YAAYtB;YACZU,OAAO;gBACLE,UAAU;oBAAEC,UAAU;gBAAS;gBAC/B,yBAAyB;oBAAEqC,QAAQ;gBAAQ;YAC7C;QACF;QAEA,iDAAiD;QACjD,MAAME,YAAY,MAAMzE,mBAAmBc,IAAIb,OAAO,EAAEoB;QACxD,MAAMqD,YAAY,CAAC,CAAED,CAAAA,UAAUjB,WAAW,IAAIiB,UAAUpB,SAAS,IAAIoB,UAAUjB,WAAW,GAAGiB,UAAUpB,SAAS,AAAD;QAE/G,OAAOrC,SAASC,IAAI,CAAC;YACnBI;YACA+C,OAAOA,MAAMO,SAAS;YACtBL,UAAUA,SAASK,SAAS;YAC5BH,SAASA,QAAQG,SAAS;YAC1BC,SAASR,MAAMO,SAAS,GAAGL,SAASK,SAAS,GAAGH,QAAQG,SAAS;YACjED;QACF;IACF;IAEA,OAAO7D;AACT,EAAC;AAED,OAAO,MAAMgE,sBAAsB,CAACjE;IAClC,MAAMC,UAA0B,OAAOC;QACrC,IAAI,CAACA,IAAIC,IAAI,EAAE;YACb,OAAOC,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAe,GAAG;gBAAEC,QAAQ;YAAI;QAChE;QAEA,IAAIC;QACJ,IAAI;YACFA,OAAO,MAAMN,IAAIG,IAAI;QACvB,EAAE,OAAM;YACNG,OAAO,CAAC;QACV;QAEA,MAAMC,iBAAiBD,KAAKC,cAAc;QAC1C,IAAI,CAACA,kBAAkB,CAACT,eAAeP,WAAW,CAACgB,eAAiC,EAAE;YACpF,OAAOL,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAA0C,GAAG;gBAAEC,QAAQ;YAAI;QAC3F;QAEA,MAAMb,mBAAmBQ,IAAIb,OAAO,EAAEoB,gBAAgB;YACpDmC,aAAaF,KAAKC,GAAG;QACvB;QAEAzC,IAAIb,OAAO,CAACkD,MAAM,CAACC,IAAI,CAAC,CAAC,6CAA6C,EAAE/B,eAAe,CAAC,CAAC;QAEzF,OAAOL,SAASC,IAAI,CAAC;YAAEyD,WAAW;YAAMrD;QAAe;IACzD;IAEA,OAAOR;AACT,EAAC"}