@inoo-ch/payload-image-optimizer 1.8.1 → 1.10.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 (48) hide show
  1. package/AGENT_DOCS.md +8 -9
  2. package/README.md +10 -9
  3. package/dist/components/RegenerationButton.js +85 -21
  4. package/dist/components/RegenerationButton.js.map +1 -1
  5. package/dist/defaults.js +14 -2
  6. package/dist/defaults.js.map +1 -1
  7. package/dist/endpoints/regenerate.d.ts +1 -0
  8. package/dist/endpoints/regenerate.js +77 -1
  9. package/dist/endpoints/regenerate.js.map +1 -1
  10. package/dist/hooks/afterChange.js +3 -0
  11. package/dist/hooks/afterChange.js.map +1 -1
  12. package/dist/hooks/beforeChange.js +48 -29
  13. package/dist/hooks/beforeChange.js.map +1 -1
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.js +32 -5
  16. package/dist/index.js.map +1 -1
  17. package/dist/processing/index.d.ts +21 -0
  18. package/dist/processing/index.js +29 -0
  19. package/dist/processing/index.js.map +1 -1
  20. package/dist/tasks/convertFormats.js +11 -4
  21. package/dist/tasks/convertFormats.js.map +1 -1
  22. package/dist/tasks/regenerateDocument.js +27 -4
  23. package/dist/tasks/regenerateDocument.js.map +1 -1
  24. package/dist/types.d.ts +49 -2
  25. package/dist/types.js.map +1 -1
  26. package/dist/utilities/filenameStrategies.d.ts +25 -0
  27. package/dist/utilities/filenameStrategies.js +46 -0
  28. package/dist/utilities/filenameStrategies.js.map +1 -0
  29. package/dist/utilities/stripDiacritics.d.ts +9 -0
  30. package/dist/utilities/stripDiacritics.js +10 -0
  31. package/dist/utilities/stripDiacritics.js.map +1 -0
  32. package/dist/utilities/toKebabCase.d.ts +10 -0
  33. package/dist/utilities/toKebabCase.js +11 -0
  34. package/dist/utilities/toKebabCase.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/components/RegenerationButton.tsx +92 -24
  37. package/src/defaults.ts +15 -1
  38. package/src/endpoints/regenerate.ts +68 -0
  39. package/src/hooks/afterChange.ts +4 -0
  40. package/src/hooks/beforeChange.ts +53 -35
  41. package/src/index.ts +27 -6
  42. package/src/processing/index.ts +39 -0
  43. package/src/tasks/convertFormats.ts +24 -16
  44. package/src/tasks/regenerateDocument.ts +24 -4
  45. package/src/types.ts +51 -2
  46. package/src/utilities/filenameStrategies.ts +61 -0
  47. package/src/utilities/stripDiacritics.ts +10 -0
  48. package/src/utilities/toKebabCase.ts +16 -0
@@ -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"}
@@ -6,6 +6,9 @@ import { waitUntil } from '../utilities/waitUntil.js';
6
6
  export const createAfterChangeHook = (resolvedConfig, collectionSlug)=>{
7
7
  return async ({ context, doc, req })=>{
8
8
  if (context?.imageOptimizer_skip) return doc;
9
+ // Native re-uploads (focal point/crop changes): optimization was skipped in beforeChange.
10
+ // Payload's native image-size regeneration handles everything.
11
+ if (context?.imageOptimizer_nativeReupload) return doc;
9
12
  // Use context flag from beforeChange instead of checking req.file.data directly.
10
13
  // Cloud storage adapters may consume req.file.data in their own afterChange hook
11
14
  // before ours runs, which would cause this guard to bail out and leave status as 'pending'.
@@ -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 { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { isCloudStorage } from '../utilities/storage.js'\nimport { waitUntil } from '../utilities/waitUntil.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 // When status was already resolved in beforeChange (cloud storage, or\n // replaceOriginal with a single format), no async job or update is needed.\n // This avoids a separate update() call that fails with 404 on MongoDB due to\n // transaction isolation when cloud storage adapters are involved.\n if (context?.imageOptimizer_statusResolved) {\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 const runPromise = req.payload.jobs.run({ sequential: true }).catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Image optimizer job runner failed')\n })\n waitUntil(runPromise, req)\n\n return doc\n }\n}\n"],"names":["fs","path","resolveStaticDir","isCloudStorage","waitUntil","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","imageOptimizer_statusResolved","jobs","queue","task","input","docId","String","id","runPromise","run","sequential","err","logger","error"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,UAAU,OAAM;AAIvB,SAASC,gBAAgB,QAAQ,mCAAkC;AACnE,SAASC,cAAc,QAAQ,0BAAyB;AACxD,SAASC,SAAS,QAAQ,4BAA2B;AAErD,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,eAAed,eAAeU;QAEpC,kFAAkF;QAClF,6EAA6E;QAC7E,gFAAgF;QAChF,gFAAgF;QAChF,IAAI,CAACI,cAAc;YACjB,MAAMC,YAAYhB,iBAAiBW;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,sEAAsE;QACtE,2EAA2E;QAC3E,6EAA6E;QAC7E,kEAAkE;QAClE,IAAIvB,SAASwB,+BAA+B;YAC1C,OAAOvB;QACT;QAEA,gFAAgF;QAChF,MAAMC,IAAII,OAAO,CAACmB,IAAI,CAACC,KAAK,CAAC;YAC3BC,MAAM;YACNC,OAAO;gBACL7B;gBACA8B,OAAOC,OAAO7B,IAAI8B,EAAE;YACtB;QACF;QAEA,MAAMC,aAAa9B,IAAII,OAAO,CAACmB,IAAI,CAACQ,GAAG,CAAC;YAAEC,YAAY;QAAK,GAAGX,KAAK,CAAC,CAACY;YACnEjC,IAAII,OAAO,CAAC8B,MAAM,CAACC,KAAK,CAAC;gBAAEF;YAAI,GAAG;QACpC;QACAvC,UAAUoC,YAAY9B;QAEtB,OAAOD;IACT;AACF,EAAC"}
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 { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { isCloudStorage } from '../utilities/storage.js'\nimport { waitUntil } from '../utilities/waitUntil.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 // Native re-uploads (focal point/crop changes): optimization was skipped in beforeChange.\n // Payload's native image-size regeneration handles everything.\n if (context?.imageOptimizer_nativeReupload) 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 // When status was already resolved in beforeChange (cloud storage, or\n // replaceOriginal with a single format), no async job or update is needed.\n // This avoids a separate update() call that fails with 404 on MongoDB due to\n // transaction isolation when cloud storage adapters are involved.\n if (context?.imageOptimizer_statusResolved) {\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 const runPromise = req.payload.jobs.run({ sequential: true }).catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Image optimizer job runner failed')\n })\n waitUntil(runPromise, req)\n\n return doc\n }\n}\n"],"names":["fs","path","resolveStaticDir","isCloudStorage","waitUntil","createAfterChangeHook","resolvedConfig","collectionSlug","context","doc","req","imageOptimizer_skip","imageOptimizer_nativeReupload","imageOptimizer_hasUpload","collectionConfig","payload","collections","config","cloudStorage","staticDir","processedBuffer","imageOptimizer_processedBuffer","filename","safeFilename","basename","filePath","join","writeFile","originalFilename","imageOptimizer_originalFilename","oldFilePath","unlink","catch","imageOptimizer_statusResolved","jobs","queue","task","input","docId","String","id","runPromise","run","sequential","err","logger","error"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,UAAU,OAAM;AAIvB,SAASC,gBAAgB,QAAQ,mCAAkC;AACnE,SAASC,cAAc,QAAQ,0BAAyB;AACxD,SAASC,SAAS,QAAQ,4BAA2B;AAErD,OAAO,MAAMC,wBAAwB,CACnCC,gBACAC;IAEA,OAAO,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QACjC,IAAIF,SAASG,qBAAqB,OAAOF;QAEzC,0FAA0F;QAC1F,+DAA+D;QAC/D,IAAID,SAASI,+BAA+B,OAAOH;QAEnD,iFAAiF;QACjF,iFAAiF;QACjF,4FAA4F;QAC5F,IAAI,CAACD,SAASK,0BAA0B,OAAOJ;QAE/C,MAAMK,mBAAmBJ,IAAIK,OAAO,CAACC,WAAW,CAACT,eAAuD,CAACU,MAAM;QAC/G,MAAMC,eAAef,eAAeW;QAEpC,kFAAkF;QAClF,6EAA6E;QAC7E,gFAAgF;QAChF,gFAAgF;QAChF,IAAI,CAACI,cAAc;YACjB,MAAMC,YAAYjB,iBAAiBY;YACnC,MAAMM,kBAAkBZ,QAAQa,8BAA8B;YAC9D,IAAID,mBAAmBX,IAAIa,QAAQ,IAAIH,WAAW;gBAChD,MAAMI,eAAetB,KAAKuB,QAAQ,CAACf,IAAIa,QAAQ;gBAC/C,MAAMG,WAAWxB,KAAKyB,IAAI,CAACP,WAAWI;gBACtC,MAAMvB,GAAG2B,SAAS,CAACF,UAAUL;gBAE7B,+EAA+E;gBAC/E,MAAMQ,mBAAmBpB,QAAQqB,+BAA+B;gBAChE,IAAID,oBAAoBA,qBAAqBL,cAAc;oBACzD,MAAMO,cAAc7B,KAAKyB,IAAI,CAACP,WAAWlB,KAAKuB,QAAQ,CAACI;oBACvD,MAAM5B,GAAG+B,MAAM,CAACD,aAAaE,KAAK,CAAC;oBACjC,0DAA0D;oBAC5D;gBACF;YACF;QACF;QAEA,sEAAsE;QACtE,2EAA2E;QAC3E,6EAA6E;QAC7E,kEAAkE;QAClE,IAAIxB,SAASyB,+BAA+B;YAC1C,OAAOxB;QACT;QAEA,gFAAgF;QAChF,MAAMC,IAAIK,OAAO,CAACmB,IAAI,CAACC,KAAK,CAAC;YAC3BC,MAAM;YACNC,OAAO;gBACL9B;gBACA+B,OAAOC,OAAO9B,IAAI+B,EAAE;YACtB;QACF;QAEA,MAAMC,aAAa/B,IAAIK,OAAO,CAACmB,IAAI,CAACQ,GAAG,CAAC;YAAEC,YAAY;QAAK,GAAGX,KAAK,CAAC,CAACY;YACnElC,IAAIK,OAAO,CAAC8B,MAAM,CAACC,KAAK,CAAC;gBAAEF;YAAI,GAAG;QACpC;QACAxC,UAAUqC,YAAY/B;QAEtB,OAAOD;IACT;AACF,EAAC"}
@@ -1,49 +1,64 @@
1
- import crypto from 'crypto';
2
1
  import path from 'path';
3
2
  import { resolveCollectionConfig } from '../defaults.js';
4
- import { convertFormat, generateThumbHash, stripAndResize } from '../processing/index.js';
3
+ import { generateThumbHash, optimizeImage } from '../processing/index.js';
5
4
  import { isCloudStorage } from '../utilities/storage.js';
6
5
  export const createBeforeChangeHook = (resolvedConfig, collectionSlug)=>{
7
6
  return async ({ context, data, originalDoc, req })=>{
8
7
  if (context?.imageOptimizer_skip) return data;
9
8
  if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return data;
10
- // Rename file to UUID before any processing, so the storage adapter
11
- // never sees the original filename. Prevents Vercel Blob "already exists"
12
- // errors and avoids leaking original filenames to storage.
13
- // On focal-point or crop re-uploads (where Payload re-sends the same file),
14
- // reuse the existing UUID filename to avoid unnecessary file churn and
15
- // broken previews.
16
- if (resolvedConfig.uniqueFileNames) {
17
- const existingFilename = originalDoc?.filename;
18
- if (existingFilename) {
19
- // Reuse the existing filename (may get a new extension below if replaceOriginal changes format)
20
- req.file.name = existingFilename;
21
- data.filename = existingFilename;
22
- } else {
23
- const ext = path.extname(req.file.name);
24
- const uuid = crypto.randomUUID();
25
- req.file.name = `${uuid}${ext}`;
26
- data.filename = req.file.name;
9
+ // Detect re-upload triggered by Payload's shouldReupload() focal point or crop change.
10
+ // shouldReupload re-fetches the stored (already-optimized) file and sets req.file.
11
+ // When re-fetching, Payload sets req.file.name to the stored filename verbatim
12
+ // (via getFileByPath or getExternalFile). For genuine user uploads, req.file.name
13
+ // comes from the user's filesystem and will differ from the stored filename.
14
+ // Skip redundant optimization; let Payload's native image-size regeneration handle cropping.
15
+ if (originalDoc) {
16
+ const existingFilename = originalDoc.filename;
17
+ if (existingFilename && req.file.name === existingFilename) {
18
+ const existingOptimizer = originalDoc.imageOptimizer;
19
+ if (existingOptimizer) {
20
+ data.imageOptimizer = existingOptimizer;
21
+ }
22
+ context.imageOptimizer_nativeReupload = true;
23
+ return data;
27
24
  }
28
25
  }
26
+ // Apply custom filename strategy (seoFilename, uuidFilename, or user-provided).
27
+ // The callback returns a stem (no extension) — we append the original extension here,
28
+ // and replaceOriginal may swap it to the target format extension later.
29
+ if (resolvedConfig.generateFilename) {
30
+ const existingFilename = originalDoc?.filename;
31
+ const ext = path.extname(req.file.name);
32
+ const stem = resolvedConfig.generateFilename({
33
+ altText: data.alt,
34
+ originalFilename: req.file.name,
35
+ mimeType: req.file.mimetype,
36
+ collectionSlug,
37
+ existingFilename
38
+ });
39
+ const newFilename = `${stem}${ext}`;
40
+ req.file.name = newFilename;
41
+ data.filename = newFilename;
42
+ }
29
43
  const originalSize = req.file.data.length;
30
44
  const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug);
31
- // Process in memory: strip EXIF, resize, generate blur
32
- const processed = await stripAndResize(req.file.data, perCollectionConfig.maxDimensions, resolvedConfig.stripMetadata);
45
+ // Single-pipeline optimization: resize + strip metadata + optional format conversion.
46
+ // Skips .rotate() Payload's generateFileData() already auto-rotated before hooks run.
47
+ const primaryFormat = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0 ? perCollectionConfig.formats[0] : undefined;
48
+ const processed = await optimizeImage(req.file.data, {
49
+ maxDimensions: perCollectionConfig.maxDimensions,
50
+ stripMetadata: resolvedConfig.stripMetadata,
51
+ format: primaryFormat
52
+ });
33
53
  let finalBuffer = processed.buffer;
34
54
  let finalSize = processed.size;
35
- if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {
36
- // Convert to primary format (first in the formats array)
37
- const primaryFormat = perCollectionConfig.formats[0];
38
- const converted = await convertFormat(processed.buffer, primaryFormat.format, primaryFormat.quality);
39
- finalBuffer = converted.buffer;
40
- finalSize = converted.size;
55
+ if (primaryFormat && processed.mimeType) {
41
56
  // Update filename and mimeType so Payload stores the correct metadata
42
57
  const originalFilename = data.filename || req.file.name || '';
43
58
  const newFilename = `${path.parse(originalFilename).name}.${primaryFormat.format}`;
44
59
  context.imageOptimizer_originalFilename = originalFilename;
45
60
  data.filename = newFilename;
46
- data.mimeType = converted.mimeType;
61
+ data.mimeType = processed.mimeType;
47
62
  data.filesize = finalSize;
48
63
  }
49
64
  // Determine if async work (variant generation job) is needed after create.
@@ -63,7 +78,11 @@ export const createBeforeChangeHook = (resolvedConfig, collectionSlug)=>{
63
78
  if (!needsAsyncJob) {
64
79
  context.imageOptimizer_statusResolved = true;
65
80
  }
66
- if (resolvedConfig.generateThumbHash) {
81
+ // When no async job will run, compute ThumbHash now so it's included in the
82
+ // initial DB write. This avoids a separate update() call that would fail with
83
+ // 404 on MongoDB due to transaction isolation. When a job WILL run, the
84
+ // convertFormats task computes ThumbHash in the background instead.
85
+ if (resolvedConfig.generateThumbHash && !needsAsyncJob) {
67
86
  data.imageOptimizer.thumbHash = await generateThumbHash(finalBuffer);
68
87
  }
69
88
  // Write processed buffer back to req.file so cloud storage adapters
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/beforeChange.ts"],"sourcesContent":["import crypto from 'crypto'\nimport 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'\nimport { isCloudStorage } from '../utilities/storage.js'\n\nexport const createBeforeChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionBeforeChangeHook => {\n return async ({ context, data, originalDoc, 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 // Rename file to UUID before any processing, so the storage adapter\n // never sees the original filename. Prevents Vercel Blob \"already exists\"\n // errors and avoids leaking original filenames to storage.\n // On focal-point or crop re-uploads (where Payload re-sends the same file),\n // reuse the existing UUID filename to avoid unnecessary file churn and\n // broken previews.\n if (resolvedConfig.uniqueFileNames) {\n const existingFilename = (originalDoc as Record<string, unknown> | undefined)?.filename as string | undefined\n if (existingFilename) {\n // Reuse the existing filename (may get a new extension below if replaceOriginal changes format)\n req.file.name = existingFilename\n data.filename = existingFilename\n } else {\n const ext = path.extname(req.file.name)\n const uuid = crypto.randomUUID()\n req.file.name = `${uuid}${ext}`\n data.filename = req.file.name\n }\n }\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 // Determine if async work (variant generation job) is needed after create.\n // If not, set status to 'complete' now so afterChange doesn't need a separate\n // update() call — which fails with 404 on MongoDB due to transaction isolation\n // when cloud storage adapters are involved.\n const collectionConfig = req.payload.collections[collectionSlug as keyof typeof req.payload.collections].config\n const cloudStorage = isCloudStorage(collectionConfig)\n const needsAsyncJob = !cloudStorage && perCollectionConfig.formats.length > 0 && !(perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length <= 1)\n\n data.imageOptimizer = {\n originalSize,\n optimizedSize: finalSize,\n status: needsAsyncJob ? 'pending' : 'complete',\n variants: needsAsyncJob ? undefined : [],\n error: null,\n }\n\n if (!needsAsyncJob) {\n context.imageOptimizer_statusResolved = true\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":["crypto","path","resolveCollectionConfig","convertFormat","generateThumbHash","stripAndResize","isCloudStorage","createBeforeChangeHook","resolvedConfig","collectionSlug","context","data","originalDoc","req","imageOptimizer_skip","file","mimetype","startsWith","uniqueFileNames","existingFilename","filename","name","ext","extname","uuid","randomUUID","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","finalBuffer","buffer","finalSize","size","replaceOriginal","formats","primaryFormat","converted","format","quality","originalFilename","newFilename","parse","imageOptimizer_originalFilename","mimeType","filesize","collectionConfig","payload","collections","config","cloudStorage","needsAsyncJob","imageOptimizer","optimizedSize","status","variants","undefined","error","imageOptimizer_statusResolved","thumbHash","imageOptimizer_processedBuffer","imageOptimizer_hasUpload"],"mappings":"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"}
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 { generateThumbHash, optimizeImage } from '../processing/index.js'\nimport { isCloudStorage } from '../utilities/storage.js'\n\nexport const createBeforeChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionBeforeChangeHook => {\n return async ({ context, data, originalDoc, 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 // Detect re-upload triggered by Payload's shouldReupload() focal point or crop change.\n // shouldReupload re-fetches the stored (already-optimized) file and sets req.file.\n // When re-fetching, Payload sets req.file.name to the stored filename verbatim\n // (via getFileByPath or getExternalFile). For genuine user uploads, req.file.name\n // comes from the user's filesystem and will differ from the stored filename.\n // Skip redundant optimization; let Payload's native image-size regeneration handle cropping.\n if (originalDoc) {\n const existingFilename = (originalDoc as Record<string, unknown>).filename as string | undefined\n\n if (existingFilename && req.file.name === existingFilename) {\n const existingOptimizer = (originalDoc as Record<string, unknown>).imageOptimizer\n if (existingOptimizer) {\n data.imageOptimizer = existingOptimizer as typeof data.imageOptimizer\n }\n context.imageOptimizer_nativeReupload = true\n return data\n }\n }\n\n // Apply custom filename strategy (seoFilename, uuidFilename, or user-provided).\n // The callback returns a stem (no extension) — we append the original extension here,\n // and replaceOriginal may swap it to the target format extension later.\n if (resolvedConfig.generateFilename) {\n const existingFilename = (originalDoc as Record<string, unknown> | undefined)?.filename as string | undefined\n const ext = path.extname(req.file.name)\n const stem = resolvedConfig.generateFilename({\n altText: (data as Record<string, unknown>).alt as string | undefined,\n originalFilename: req.file.name,\n mimeType: req.file.mimetype,\n collectionSlug,\n existingFilename,\n })\n const newFilename = `${stem}${ext}`\n req.file.name = newFilename\n data.filename = newFilename\n }\n\n const originalSize = req.file.data.length\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)\n\n // Single-pipeline optimization: resize + strip metadata + optional format conversion.\n // Skips .rotate() Payload's generateFileData() already auto-rotated before hooks run.\n const primaryFormat = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0\n ? perCollectionConfig.formats[0]\n : undefined\n\n const processed = await optimizeImage(req.file.data, {\n maxDimensions: perCollectionConfig.maxDimensions,\n stripMetadata: resolvedConfig.stripMetadata,\n format: primaryFormat,\n })\n\n let finalBuffer = processed.buffer\n let finalSize = processed.size\n\n if (primaryFormat && processed.mimeType) {\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 = processed.mimeType\n data.filesize = finalSize\n }\n\n // Determine if async work (variant generation job) is needed after create.\n // If not, set status to 'complete' now so afterChange doesn't need a separate\n // update() call — which fails with 404 on MongoDB due to transaction isolation\n // when cloud storage adapters are involved.\n const collectionConfig = req.payload.collections[collectionSlug as keyof typeof req.payload.collections].config\n const cloudStorage = isCloudStorage(collectionConfig)\n const needsAsyncJob = !cloudStorage && perCollectionConfig.formats.length > 0 && !(perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length <= 1)\n\n data.imageOptimizer = {\n originalSize,\n optimizedSize: finalSize,\n status: needsAsyncJob ? 'pending' : 'complete',\n variants: needsAsyncJob ? undefined : [],\n error: null,\n }\n\n if (!needsAsyncJob) {\n context.imageOptimizer_statusResolved = true\n }\n\n // When no async job will run, compute ThumbHash now so it's included in the\n // initial DB write. This avoids a separate update() call that would fail with\n // 404 on MongoDB due to transaction isolation. When a job WILL run, the\n // convertFormats task computes ThumbHash in the background instead.\n if (resolvedConfig.generateThumbHash && !needsAsyncJob) {\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","generateThumbHash","optimizeImage","isCloudStorage","createBeforeChangeHook","resolvedConfig","collectionSlug","context","data","originalDoc","req","imageOptimizer_skip","file","mimetype","startsWith","existingFilename","filename","name","existingOptimizer","imageOptimizer","imageOptimizer_nativeReupload","generateFilename","ext","extname","stem","altText","alt","originalFilename","mimeType","newFilename","originalSize","length","perCollectionConfig","primaryFormat","replaceOriginal","formats","undefined","processed","maxDimensions","stripMetadata","format","finalBuffer","buffer","finalSize","size","parse","imageOptimizer_originalFilename","filesize","collectionConfig","payload","collections","config","cloudStorage","needsAsyncJob","optimizedSize","status","variants","error","imageOptimizer_statusResolved","thumbHash","imageOptimizer_processedBuffer","imageOptimizer_hasUpload"],"mappings":"AAAA,OAAOA,UAAU,OAAM;AAIvB,SAASC,uBAAuB,QAAQ,iBAAgB;AACxD,SAASC,iBAAiB,EAAEC,aAAa,QAAQ,yBAAwB;AACzE,SAASC,cAAc,QAAQ,0BAAyB;AAExD,OAAO,MAAMC,yBAAyB,CACpCC,gBACAC;IAEA,OAAO,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,WAAW,EAAEC,GAAG,EAAE;QAC/C,IAAIH,SAASI,qBAAqB,OAAOH;QAEzC,IAAI,CAACE,IAAIE,IAAI,IAAI,CAACF,IAAIE,IAAI,CAACJ,IAAI,IAAI,CAACE,IAAIE,IAAI,CAACC,QAAQ,EAAEC,WAAW,WAAW,OAAON;QAEpF,yFAAyF;QACzF,mFAAmF;QACnF,+EAA+E;QAC/E,kFAAkF;QAClF,6EAA6E;QAC7E,6FAA6F;QAC7F,IAAIC,aAAa;YACf,MAAMM,mBAAmB,AAACN,YAAwCO,QAAQ;YAE1E,IAAID,oBAAoBL,IAAIE,IAAI,CAACK,IAAI,KAAKF,kBAAkB;gBAC1D,MAAMG,oBAAoB,AAACT,YAAwCU,cAAc;gBACjF,IAAID,mBAAmB;oBACrBV,KAAKW,cAAc,GAAGD;gBACxB;gBACAX,QAAQa,6BAA6B,GAAG;gBACxC,OAAOZ;YACT;QACF;QAEA,gFAAgF;QAChF,sFAAsF;QACtF,wEAAwE;QACxE,IAAIH,eAAegB,gBAAgB,EAAE;YACnC,MAAMN,mBAAoBN,aAAqDO;YAC/E,MAAMM,MAAMvB,KAAKwB,OAAO,CAACb,IAAIE,IAAI,CAACK,IAAI;YACtC,MAAMO,OAAOnB,eAAegB,gBAAgB,CAAC;gBAC3CI,SAAS,AAACjB,KAAiCkB,GAAG;gBAC9CC,kBAAkBjB,IAAIE,IAAI,CAACK,IAAI;gBAC/BW,UAAUlB,IAAIE,IAAI,CAACC,QAAQ;gBAC3BP;gBACAS;YACF;YACA,MAAMc,cAAc,GAAGL,OAAOF,KAAK;YACnCZ,IAAIE,IAAI,CAACK,IAAI,GAAGY;YAChBrB,KAAKQ,QAAQ,GAAGa;QAClB;QAEA,MAAMC,eAAepB,IAAIE,IAAI,CAACJ,IAAI,CAACuB,MAAM;QAEzC,MAAMC,sBAAsBhC,wBAAwBK,gBAAgBC;QAEpE,sFAAsF;QACtF,wFAAwF;QACxF,MAAM2B,gBAAgBD,oBAAoBE,eAAe,IAAIF,oBAAoBG,OAAO,CAACJ,MAAM,GAAG,IAC9FC,oBAAoBG,OAAO,CAAC,EAAE,GAC9BC;QAEJ,MAAMC,YAAY,MAAMnC,cAAcQ,IAAIE,IAAI,CAACJ,IAAI,EAAE;YACnD8B,eAAeN,oBAAoBM,aAAa;YAChDC,eAAelC,eAAekC,aAAa;YAC3CC,QAAQP;QACV;QAEA,IAAIQ,cAAcJ,UAAUK,MAAM;QAClC,IAAIC,YAAYN,UAAUO,IAAI;QAE9B,IAAIX,iBAAiBI,UAAUT,QAAQ,EAAE;YACvC,sEAAsE;YACtE,MAAMD,mBAAmBnB,KAAKQ,QAAQ,IAAIN,IAAIE,IAAI,CAACK,IAAI,IAAI;YAC3D,MAAMY,cAAc,GAAG9B,KAAK8C,KAAK,CAAClB,kBAAkBV,IAAI,CAAC,CAAC,EAAEgB,cAAcO,MAAM,EAAE;YAClFjC,QAAQuC,+BAA+B,GAAGnB;YAC1CnB,KAAKQ,QAAQ,GAAGa;YAChBrB,KAAKoB,QAAQ,GAAGS,UAAUT,QAAQ;YAClCpB,KAAKuC,QAAQ,GAAGJ;QAClB;QAEA,2EAA2E;QAC3E,8EAA8E;QAC9E,+EAA+E;QAC/E,4CAA4C;QAC5C,MAAMK,mBAAmBtC,IAAIuC,OAAO,CAACC,WAAW,CAAC5C,eAAuD,CAAC6C,MAAM;QAC/G,MAAMC,eAAejD,eAAe6C;QACpC,MAAMK,gBAAgB,CAACD,gBAAgBpB,oBAAoBG,OAAO,CAACJ,MAAM,GAAG,KAAK,CAAEC,CAAAA,oBAAoBE,eAAe,IAAIF,oBAAoBG,OAAO,CAACJ,MAAM,IAAI,CAAA;QAEhKvB,KAAKW,cAAc,GAAG;YACpBW;YACAwB,eAAeX;YACfY,QAAQF,gBAAgB,YAAY;YACpCG,UAAUH,gBAAgBjB,YAAY,EAAE;YACxCqB,OAAO;QACT;QAEA,IAAI,CAACJ,eAAe;YAClB9C,QAAQmD,6BAA6B,GAAG;QAC1C;QAEA,4EAA4E;QAC5E,8EAA8E;QAC9E,wEAAwE;QACxE,oEAAoE;QACpE,IAAIrD,eAAeJ,iBAAiB,IAAI,CAACoD,eAAe;YACtD7C,KAAKW,cAAc,CAACwC,SAAS,GAAG,MAAM1D,kBAAkBwC;QAC1D;QAEA,oEAAoE;QACpE,gFAAgF;QAChF,8EAA8E;QAC9E,8EAA8E;QAC9E,6DAA6D;QAC7D/B,IAAIE,IAAI,CAACJ,IAAI,GAAGiC;QAChB/B,IAAIE,IAAI,CAACgC,IAAI,GAAGD;QAChB,IAAIX,oBAAoBE,eAAe,IAAIF,oBAAoBG,OAAO,CAACJ,MAAM,GAAG,GAAG;YACjFrB,IAAIE,IAAI,CAACK,IAAI,GAAGT,KAAKQ,QAAQ;YAC7BN,IAAIE,IAAI,CAACC,QAAQ,GAAGL,KAAKoB,QAAQ;QACnC;QACArB,QAAQqD,8BAA8B,GAAGnB;QACzClC,QAAQsD,wBAAwB,GAAG;QAEnC,OAAOrD;IACT;AACF,EAAC"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { Config } from 'payload';
2
2
  import type { ImageOptimizerConfig } from './types.js';
3
- export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, MediaSizeVariant, FieldsOverride } from './types.js';
3
+ export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, MediaSizeVariant, FieldsOverride, GenerateFilename, GenerateFilenameArgs } from './types.js';
4
4
  export { defaultImageOptimizerFields } from './fields/imageOptimizerField.js';
5
5
  export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js';
6
+ export { uuidFilename, seoFilename } from './utilities/filenameStrategies.js';
6
7
  /**
7
8
  * Recommended maxDuration for the Payload API route on Vercel.
8
9
  * Re-export this in your route file:
package/dist/index.js CHANGED
@@ -6,9 +6,10 @@ import { createBeforeChangeHook } from './hooks/beforeChange.js';
6
6
  import { createAfterChangeHook } from './hooks/afterChange.js';
7
7
  import { createConvertFormatsHandler } from './tasks/convertFormats.js';
8
8
  import { createRegenerateDocumentHandler } from './tasks/regenerateDocument.js';
9
- import { createRegenerateHandler, createRegenerateStatusHandler } from './endpoints/regenerate.js';
9
+ import { createRegenerateHandler, createRegenerateStatusHandler, createCancelHandler } from './endpoints/regenerate.js';
10
10
  export { defaultImageOptimizerFields } from './fields/imageOptimizerField.js';
11
11
  export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js';
12
+ export { uuidFilename, seoFilename } from './utilities/filenameStrategies.js';
12
13
  /**
13
14
  * Recommended maxDuration for the Payload API route on Vercel.
14
15
  * Re-export this in your route file:
@@ -58,10 +59,12 @@ export const imageOptimizer = (pluginOptions)=>(config)=>{
58
59
  Upload: '@inoo-ch/payload-image-optimizer/client#UploadOptimizer'
59
60
  }
60
61
  } : {},
61
- beforeListTable: [
62
- ...collection.admin?.components?.beforeListTable || [],
63
- '@inoo-ch/payload-image-optimizer/client#RegenerationButton'
64
- ]
62
+ ...resolvedConfig.regenerateButton ? {
63
+ beforeListTable: [
64
+ ...collection.admin?.components?.beforeListTable || [],
65
+ '@inoo-ch/payload-image-optimizer/client#RegenerationButton'
66
+ ]
67
+ } : {}
65
68
  }
66
69
  }
67
70
  };
@@ -81,6 +84,25 @@ export const imageOptimizer = (pluginOptions)=>(config)=>{
81
84
  return {
82
85
  ...config,
83
86
  collections,
87
+ globals: [
88
+ ...config.globals || [],
89
+ {
90
+ slug: 'image-optimizer-state',
91
+ admin: {
92
+ hidden: true
93
+ },
94
+ access: {
95
+ read: ()=>true,
96
+ update: ()=>true
97
+ },
98
+ fields: [
99
+ {
100
+ name: 'collections',
101
+ type: 'json'
102
+ }
103
+ ]
104
+ }
105
+ ],
84
106
  i18n,
85
107
  jobs: {
86
108
  ...config.jobs,
@@ -149,6 +171,11 @@ export const imageOptimizer = (pluginOptions)=>(config)=>{
149
171
  path: '/image-optimizer/regenerate',
150
172
  method: 'get',
151
173
  handler: createRegenerateStatusHandler(resolvedConfig)
174
+ },
175
+ {
176
+ path: '/image-optimizer/regenerate',
177
+ method: 'delete',
178
+ handler: createCancelHandler(resolvedConfig)
152
179
  }
153
180
  ]
154
181
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ImageOptimizerConfig } from './types.js'\nimport { resolveConfig } from './defaults.js'\nimport { translations } from './translations/index.js'\nimport { getImageOptimizerField } from './fields/imageOptimizerField.js'\nimport { createBeforeChangeHook } from './hooks/beforeChange.js'\nimport { createAfterChangeHook } from './hooks/afterChange.js'\nimport { createConvertFormatsHandler } from './tasks/convertFormats.js'\nimport { createRegenerateDocumentHandler } from './tasks/regenerateDocument.js'\nimport { createRegenerateHandler, createRegenerateStatusHandler } from './endpoints/regenerate.js'\n\nexport type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, MediaSizeVariant, FieldsOverride } from './types.js'\nexport { defaultImageOptimizerFields } from './fields/imageOptimizerField.js'\n\nexport { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js'\n\n/**\n * Recommended maxDuration for the Payload API route on Vercel.\n * Re-export this in your route file:\n *\n * export { maxDuration } from '@inoo-ch/payload-image-optimizer'\n */\nexport const maxDuration = 60\n\nexport const imageOptimizer =\n (pluginOptions: ImageOptimizerConfig) =>\n (config: Config): Config => {\n const resolvedConfig = resolveConfig(pluginOptions)\n const targetSlugs = Object.keys(resolvedConfig.collections)\n\n // Inject fields (and hooks when enabled) into targeted upload collections\n const collections = (config.collections || []).map((collection) => {\n if (!targetSlugs.includes(collection.slug)) {\n return collection\n }\n\n // Always inject fields for schema consistency (even when disabled)\n const fields = [...collection.fields, getImageOptimizerField(pluginOptions.fieldsOverride)]\n\n if (resolvedConfig.disabled) {\n return { ...collection, fields }\n }\n\n return {\n ...collection,\n fields,\n hooks: {\n ...collection.hooks,\n beforeChange: [\n ...(collection.hooks?.beforeChange || []),\n createBeforeChangeHook(resolvedConfig, collection.slug),\n ],\n afterChange: [\n ...(collection.hooks?.afterChange || []),\n createAfterChangeHook(resolvedConfig, collection.slug),\n ],\n },\n admin: {\n ...collection.admin,\n components: {\n ...collection.admin?.components,\n ...(resolvedConfig.clientOptimization && !collection.admin?.components?.edit?.Upload\n ? {\n edit: {\n ...collection.admin?.components?.edit,\n Upload: '@inoo-ch/payload-image-optimizer/client#UploadOptimizer',\n },\n }\n : {}),\n beforeListTable: [\n ...(collection.admin?.components?.beforeListTable || []),\n '@inoo-ch/payload-image-optimizer/client#RegenerationButton',\n ],\n },\n },\n }\n })\n\n const i18n = {\n ...config.i18n,\n translations: deepMergeSimple(translations, config.i18n?.translations ?? {}),\n }\n\n // If disabled, return with fields injected but no tasks/endpoints\n if (resolvedConfig.disabled) {\n return { ...config, collections, i18n }\n }\n\n return {\n ...config,\n collections,\n i18n,\n jobs: {\n ...config.jobs,\n tasks: [\n ...(config.jobs?.tasks || []),\n {\n slug: 'imageOptimizer_convertFormats',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'variantsGenerated', type: 'number' },\n ],\n retries: 2,\n handler: createConvertFormatsHandler(resolvedConfig),\n } as any,\n {\n slug: 'imageOptimizer_regenerateDocument',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'status', type: 'text' },\n { name: 'reason', type: 'text' },\n ],\n retries: 2,\n handler: createRegenerateDocumentHandler(resolvedConfig),\n } as any,\n ],\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n path: '/image-optimizer/regenerate',\n method: 'post',\n handler: createRegenerateHandler(resolvedConfig),\n },\n {\n path: '/image-optimizer/regenerate',\n method: 'get',\n handler: createRegenerateStatusHandler(resolvedConfig),\n },\n ],\n }\n }\n"],"names":["deepMergeSimple","resolveConfig","translations","getImageOptimizerField","createBeforeChangeHook","createAfterChangeHook","createConvertFormatsHandler","createRegenerateDocumentHandler","createRegenerateHandler","createRegenerateStatusHandler","defaultImageOptimizerFields","encodeImageToThumbHash","decodeThumbHashToDataURL","maxDuration","imageOptimizer","pluginOptions","config","resolvedConfig","targetSlugs","Object","keys","collections","map","collection","includes","slug","fields","fieldsOverride","disabled","hooks","beforeChange","afterChange","admin","components","clientOptimization","edit","Upload","beforeListTable","i18n","jobs","tasks","inputSchema","name","type","required","outputSchema","retries","handler","endpoints","path","method"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ImageOptimizerConfig } from './types.js'\nimport { resolveConfig } from './defaults.js'\nimport { translations } from './translations/index.js'\nimport { getImageOptimizerField } from './fields/imageOptimizerField.js'\nimport { createBeforeChangeHook } from './hooks/beforeChange.js'\nimport { createAfterChangeHook } from './hooks/afterChange.js'\nimport { createConvertFormatsHandler } from './tasks/convertFormats.js'\nimport { createRegenerateDocumentHandler } from './tasks/regenerateDocument.js'\nimport { createRegenerateHandler, createRegenerateStatusHandler, createCancelHandler } from './endpoints/regenerate.js'\n\nexport type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, MediaSizeVariant, FieldsOverride, GenerateFilename, GenerateFilenameArgs } from './types.js'\nexport { defaultImageOptimizerFields } from './fields/imageOptimizerField.js'\n\nexport { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js'\nexport { uuidFilename, seoFilename } from './utilities/filenameStrategies.js'\n\n/**\n * Recommended maxDuration for the Payload API route on Vercel.\n * Re-export this in your route file:\n *\n * export { maxDuration } from '@inoo-ch/payload-image-optimizer'\n */\nexport const maxDuration = 60\n\nexport const imageOptimizer =\n (pluginOptions: ImageOptimizerConfig) =>\n (config: Config): Config => {\n const resolvedConfig = resolveConfig(pluginOptions)\n const targetSlugs = Object.keys(resolvedConfig.collections)\n\n // Inject fields (and hooks when enabled) into targeted upload collections\n const collections = (config.collections || []).map((collection) => {\n if (!targetSlugs.includes(collection.slug)) {\n return collection\n }\n\n // Always inject fields for schema consistency (even when disabled)\n const fields = [...collection.fields, getImageOptimizerField(pluginOptions.fieldsOverride)]\n\n if (resolvedConfig.disabled) {\n return { ...collection, fields }\n }\n\n return {\n ...collection,\n fields,\n hooks: {\n ...collection.hooks,\n beforeChange: [\n ...(collection.hooks?.beforeChange || []),\n createBeforeChangeHook(resolvedConfig, collection.slug),\n ],\n afterChange: [\n ...(collection.hooks?.afterChange || []),\n createAfterChangeHook(resolvedConfig, collection.slug),\n ],\n },\n admin: {\n ...collection.admin,\n components: {\n ...collection.admin?.components,\n ...(resolvedConfig.clientOptimization && !collection.admin?.components?.edit?.Upload\n ? {\n edit: {\n ...collection.admin?.components?.edit,\n Upload: '@inoo-ch/payload-image-optimizer/client#UploadOptimizer',\n },\n }\n : {}),\n ...(resolvedConfig.regenerateButton\n ? {\n beforeListTable: [\n ...(collection.admin?.components?.beforeListTable || []),\n '@inoo-ch/payload-image-optimizer/client#RegenerationButton',\n ],\n }\n : {}),\n },\n },\n }\n })\n\n const i18n = {\n ...config.i18n,\n translations: deepMergeSimple(translations, config.i18n?.translations ?? {}),\n }\n\n // If disabled, return with fields injected but no tasks/endpoints\n if (resolvedConfig.disabled) {\n return { ...config, collections, i18n }\n }\n\n return {\n ...config,\n collections,\n globals: [\n ...(config.globals || []),\n {\n slug: 'image-optimizer-state',\n admin: { hidden: true },\n access: { read: () => true, update: () => true },\n fields: [\n { name: 'collections', type: 'json' },\n ],\n },\n ],\n i18n,\n jobs: {\n ...config.jobs,\n tasks: [\n ...(config.jobs?.tasks || []),\n {\n slug: 'imageOptimizer_convertFormats',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'variantsGenerated', type: 'number' },\n ],\n retries: 2,\n handler: createConvertFormatsHandler(resolvedConfig),\n } as any,\n {\n slug: 'imageOptimizer_regenerateDocument',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'status', type: 'text' },\n { name: 'reason', type: 'text' },\n ],\n retries: 2,\n handler: createRegenerateDocumentHandler(resolvedConfig),\n } as any,\n ],\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n path: '/image-optimizer/regenerate',\n method: 'post',\n handler: createRegenerateHandler(resolvedConfig),\n },\n {\n path: '/image-optimizer/regenerate',\n method: 'get',\n handler: createRegenerateStatusHandler(resolvedConfig),\n },\n {\n path: '/image-optimizer/regenerate',\n method: 'delete',\n handler: createCancelHandler(resolvedConfig),\n },\n ],\n }\n }\n"],"names":["deepMergeSimple","resolveConfig","translations","getImageOptimizerField","createBeforeChangeHook","createAfterChangeHook","createConvertFormatsHandler","createRegenerateDocumentHandler","createRegenerateHandler","createRegenerateStatusHandler","createCancelHandler","defaultImageOptimizerFields","encodeImageToThumbHash","decodeThumbHashToDataURL","uuidFilename","seoFilename","maxDuration","imageOptimizer","pluginOptions","config","resolvedConfig","targetSlugs","Object","keys","collections","map","collection","includes","slug","fields","fieldsOverride","disabled","hooks","beforeChange","afterChange","admin","components","clientOptimization","edit","Upload","regenerateButton","beforeListTable","i18n","globals","hidden","access","read","update","name","type","jobs","tasks","inputSchema","required","outputSchema","retries","handler","endpoints","path","method"],"mappings":"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"}
@@ -8,6 +8,27 @@ export declare function stripAndResize(buffer: Buffer, maxDimensions: {
8
8
  size: number;
9
9
  }>;
10
10
  export declare function generateThumbHash(buffer: Buffer): Promise<string>;
11
+ /**
12
+ * Single-pipeline image optimization: resize + metadata strip + optional format conversion.
13
+ * Skips .rotate() because Payload's generateFileData() already auto-rotates before hooks run.
14
+ */
15
+ export declare function optimizeImage(buffer: Buffer, options: {
16
+ maxDimensions: {
17
+ width: number;
18
+ height: number;
19
+ };
20
+ stripMetadata: boolean;
21
+ format?: {
22
+ format: 'webp' | 'avif';
23
+ quality: number;
24
+ };
25
+ }): Promise<{
26
+ buffer: Buffer;
27
+ width: number;
28
+ height: number;
29
+ size: number;
30
+ mimeType?: string;
31
+ }>;
11
32
  export declare function convertFormat(buffer: Buffer, format: 'webp' | 'avif', quality: number): Promise<{
12
33
  buffer: Buffer;
13
34
  width: number;
@@ -27,6 +27,35 @@ export async function generateThumbHash(buffer) {
27
27
  const thumbHash = rgbaToThumbHash(info.width, info.height, data);
28
28
  return Buffer.from(thumbHash).toString('base64');
29
29
  }
30
+ /**
31
+ * Single-pipeline image optimization: resize + metadata strip + optional format conversion.
32
+ * Skips .rotate() because Payload's generateFileData() already auto-rotates before hooks run.
33
+ */ export async function optimizeImage(buffer, options) {
34
+ let pipeline = sharp(buffer).resize(options.maxDimensions.width, options.maxDimensions.height, {
35
+ fit: 'inside',
36
+ withoutEnlargement: true
37
+ });
38
+ if (!options.stripMetadata) {
39
+ pipeline = pipeline.keepMetadata();
40
+ }
41
+ if (options.format) {
42
+ pipeline = pipeline.toFormat(options.format.format, {
43
+ quality: options.format.quality
44
+ });
45
+ }
46
+ const { data, info } = await pipeline.toBuffer({
47
+ resolveWithObject: true
48
+ });
49
+ return {
50
+ buffer: data,
51
+ width: info.width,
52
+ height: info.height,
53
+ size: info.size,
54
+ ...options.format && {
55
+ mimeType: options.format.format === 'webp' ? 'image/webp' : 'image/avif'
56
+ }
57
+ };
58
+ }
30
59
  export async function convertFormat(buffer, format, quality) {
31
60
  const { data, info } = await sharp(buffer).toFormat(format, {
32
61
  quality
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/processing/index.ts"],"sourcesContent":["import sharp from 'sharp'\nimport { rgbaToThumbHash } from 'thumbhash'\n\nexport async function stripAndResize(\n buffer: Buffer,\n maxDimensions: { width: number; height: number },\n stripMetadata: boolean,\n): Promise<{ buffer: Buffer; width: number; height: number; size: number }> {\n let pipeline = sharp(buffer)\n .rotate()\n .resize(maxDimensions.width, maxDimensions.height, {\n fit: 'inside',\n withoutEnlargement: true,\n })\n\n if (!stripMetadata) {\n pipeline = pipeline.keepMetadata()\n }\n\n const { data, info } = await pipeline.toBuffer({ resolveWithObject: true })\n\n return {\n buffer: data,\n width: info.width,\n height: info.height,\n size: info.size,\n }\n}\n\nexport async function generateThumbHash(buffer: Buffer): Promise<string> {\n const { data, info } = await sharp(buffer)\n .resize(100, 100, { fit: 'inside' })\n .raw()\n .ensureAlpha()\n .toBuffer({ resolveWithObject: true })\n\n const thumbHash = rgbaToThumbHash(info.width, info.height, data)\n return Buffer.from(thumbHash).toString('base64')\n}\n\nexport async function convertFormat(\n buffer: Buffer,\n format: 'webp' | 'avif',\n quality: number,\n): Promise<{ buffer: Buffer; width: number; height: number; size: number; mimeType: string }> {\n const { data, info } = await sharp(buffer)\n .toFormat(format, { quality })\n .toBuffer({ resolveWithObject: true })\n\n const mimeType = format === 'webp' ? 'image/webp' : 'image/avif'\n\n return {\n buffer: data,\n width: info.width,\n height: info.height,\n size: info.size,\n mimeType,\n }\n}\n"],"names":["sharp","rgbaToThumbHash","stripAndResize","buffer","maxDimensions","stripMetadata","pipeline","rotate","resize","width","height","fit","withoutEnlargement","keepMetadata","data","info","toBuffer","resolveWithObject","size","generateThumbHash","raw","ensureAlpha","thumbHash","Buffer","from","toString","convertFormat","format","quality","toFormat","mimeType"],"mappings":"AAAA,OAAOA,WAAW,QAAO;AACzB,SAASC,eAAe,QAAQ,YAAW;AAE3C,OAAO,eAAeC,eACpBC,MAAc,EACdC,aAAgD,EAChDC,aAAsB;IAEtB,IAAIC,WAAWN,MAAMG,QAClBI,MAAM,GACNC,MAAM,CAACJ,cAAcK,KAAK,EAAEL,cAAcM,MAAM,EAAE;QACjDC,KAAK;QACLC,oBAAoB;IACtB;IAEF,IAAI,CAACP,eAAe;QAClBC,WAAWA,SAASO,YAAY;IAClC;IAEA,MAAM,EAAEC,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMT,SAASU,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEzE,OAAO;QACLd,QAAQW;QACRL,OAAOM,KAAKN,KAAK;QACjBC,QAAQK,KAAKL,MAAM;QACnBQ,MAAMH,KAAKG,IAAI;IACjB;AACF;AAEA,OAAO,eAAeC,kBAAkBhB,MAAc;IACpD,MAAM,EAAEW,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMf,MAAMG,QAChCK,MAAM,CAAC,KAAK,KAAK;QAAEG,KAAK;IAAS,GACjCS,GAAG,GACHC,WAAW,GACXL,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEtC,MAAMK,YAAYrB,gBAAgBc,KAAKN,KAAK,EAAEM,KAAKL,MAAM,EAAEI;IAC3D,OAAOS,OAAOC,IAAI,CAACF,WAAWG,QAAQ,CAAC;AACzC;AAEA,OAAO,eAAeC,cACpBvB,MAAc,EACdwB,MAAuB,EACvBC,OAAe;IAEf,MAAM,EAAEd,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMf,MAAMG,QAChC0B,QAAQ,CAACF,QAAQ;QAAEC;IAAQ,GAC3BZ,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEtC,MAAMa,WAAWH,WAAW,SAAS,eAAe;IAEpD,OAAO;QACLxB,QAAQW;QACRL,OAAOM,KAAKN,KAAK;QACjBC,QAAQK,KAAKL,MAAM;QACnBQ,MAAMH,KAAKG,IAAI;QACfY;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/processing/index.ts"],"sourcesContent":["import sharp from 'sharp'\nimport { rgbaToThumbHash } from 'thumbhash'\n\nexport async function stripAndResize(\n buffer: Buffer,\n maxDimensions: { width: number; height: number },\n stripMetadata: boolean,\n): Promise<{ buffer: Buffer; width: number; height: number; size: number }> {\n let pipeline = sharp(buffer)\n .rotate()\n .resize(maxDimensions.width, maxDimensions.height, {\n fit: 'inside',\n withoutEnlargement: true,\n })\n\n if (!stripMetadata) {\n pipeline = pipeline.keepMetadata()\n }\n\n const { data, info } = await pipeline.toBuffer({ resolveWithObject: true })\n\n return {\n buffer: data,\n width: info.width,\n height: info.height,\n size: info.size,\n }\n}\n\nexport async function generateThumbHash(buffer: Buffer): Promise<string> {\n const { data, info } = await sharp(buffer)\n .resize(100, 100, { fit: 'inside' })\n .raw()\n .ensureAlpha()\n .toBuffer({ resolveWithObject: true })\n\n const thumbHash = rgbaToThumbHash(info.width, info.height, data)\n return Buffer.from(thumbHash).toString('base64')\n}\n\n/**\n * Single-pipeline image optimization: resize + metadata strip + optional format conversion.\n * Skips .rotate() because Payload's generateFileData() already auto-rotates before hooks run.\n */\nexport async function optimizeImage(\n buffer: Buffer,\n options: {\n maxDimensions: { width: number; height: number }\n stripMetadata: boolean\n format?: { format: 'webp' | 'avif'; quality: number }\n },\n): Promise<{ buffer: Buffer; width: number; height: number; size: number; mimeType?: string }> {\n let pipeline = sharp(buffer)\n .resize(options.maxDimensions.width, options.maxDimensions.height, {\n fit: 'inside',\n withoutEnlargement: true,\n })\n\n if (!options.stripMetadata) {\n pipeline = pipeline.keepMetadata()\n }\n\n if (options.format) {\n pipeline = pipeline.toFormat(options.format.format, { quality: options.format.quality })\n }\n\n const { data, info } = await pipeline.toBuffer({ resolveWithObject: true })\n\n return {\n buffer: data,\n width: info.width,\n height: info.height,\n size: info.size,\n ...(options.format && {\n mimeType: options.format.format === 'webp' ? 'image/webp' : 'image/avif',\n }),\n }\n}\n\nexport async function convertFormat(\n buffer: Buffer,\n format: 'webp' | 'avif',\n quality: number,\n): Promise<{ buffer: Buffer; width: number; height: number; size: number; mimeType: string }> {\n const { data, info } = await sharp(buffer)\n .toFormat(format, { quality })\n .toBuffer({ resolveWithObject: true })\n\n const mimeType = format === 'webp' ? 'image/webp' : 'image/avif'\n\n return {\n buffer: data,\n width: info.width,\n height: info.height,\n size: info.size,\n mimeType,\n }\n}\n"],"names":["sharp","rgbaToThumbHash","stripAndResize","buffer","maxDimensions","stripMetadata","pipeline","rotate","resize","width","height","fit","withoutEnlargement","keepMetadata","data","info","toBuffer","resolveWithObject","size","generateThumbHash","raw","ensureAlpha","thumbHash","Buffer","from","toString","optimizeImage","options","format","toFormat","quality","mimeType","convertFormat"],"mappings":"AAAA,OAAOA,WAAW,QAAO;AACzB,SAASC,eAAe,QAAQ,YAAW;AAE3C,OAAO,eAAeC,eACpBC,MAAc,EACdC,aAAgD,EAChDC,aAAsB;IAEtB,IAAIC,WAAWN,MAAMG,QAClBI,MAAM,GACNC,MAAM,CAACJ,cAAcK,KAAK,EAAEL,cAAcM,MAAM,EAAE;QACjDC,KAAK;QACLC,oBAAoB;IACtB;IAEF,IAAI,CAACP,eAAe;QAClBC,WAAWA,SAASO,YAAY;IAClC;IAEA,MAAM,EAAEC,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMT,SAASU,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEzE,OAAO;QACLd,QAAQW;QACRL,OAAOM,KAAKN,KAAK;QACjBC,QAAQK,KAAKL,MAAM;QACnBQ,MAAMH,KAAKG,IAAI;IACjB;AACF;AAEA,OAAO,eAAeC,kBAAkBhB,MAAc;IACpD,MAAM,EAAEW,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMf,MAAMG,QAChCK,MAAM,CAAC,KAAK,KAAK;QAAEG,KAAK;IAAS,GACjCS,GAAG,GACHC,WAAW,GACXL,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEtC,MAAMK,YAAYrB,gBAAgBc,KAAKN,KAAK,EAAEM,KAAKL,MAAM,EAAEI;IAC3D,OAAOS,OAAOC,IAAI,CAACF,WAAWG,QAAQ,CAAC;AACzC;AAEA;;;CAGC,GACD,OAAO,eAAeC,cACpBvB,MAAc,EACdwB,OAIC;IAED,IAAIrB,WAAWN,MAAMG,QAClBK,MAAM,CAACmB,QAAQvB,aAAa,CAACK,KAAK,EAAEkB,QAAQvB,aAAa,CAACM,MAAM,EAAE;QACjEC,KAAK;QACLC,oBAAoB;IACtB;IAEF,IAAI,CAACe,QAAQtB,aAAa,EAAE;QAC1BC,WAAWA,SAASO,YAAY;IAClC;IAEA,IAAIc,QAAQC,MAAM,EAAE;QAClBtB,WAAWA,SAASuB,QAAQ,CAACF,QAAQC,MAAM,CAACA,MAAM,EAAE;YAAEE,SAASH,QAAQC,MAAM,CAACE,OAAO;QAAC;IACxF;IAEA,MAAM,EAAEhB,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMT,SAASU,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEzE,OAAO;QACLd,QAAQW;QACRL,OAAOM,KAAKN,KAAK;QACjBC,QAAQK,KAAKL,MAAM;QACnBQ,MAAMH,KAAKG,IAAI;QACf,GAAIS,QAAQC,MAAM,IAAI;YACpBG,UAAUJ,QAAQC,MAAM,CAACA,MAAM,KAAK,SAAS,eAAe;QAC9D,CAAC;IACH;AACF;AAEA,OAAO,eAAeI,cACpB7B,MAAc,EACdyB,MAAuB,EACvBE,OAAe;IAEf,MAAM,EAAEhB,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMf,MAAMG,QAChC0B,QAAQ,CAACD,QAAQ;QAAEE;IAAQ,GAC3Bd,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEtC,MAAMc,WAAWH,WAAW,SAAS,eAAe;IAEpD,OAAO;QACLzB,QAAQW;QACRL,OAAOM,KAAKN,KAAK;QACjBC,QAAQK,KAAKL,MAAM;QACnBQ,MAAMH,KAAKG,IAAI;QACfa;IACF;AACF"}
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { resolveCollectionConfig } from '../defaults.js';
4
- import { convertFormat } from '../processing/index.js';
4
+ import { convertFormat, generateThumbHash } from '../processing/index.js';
5
5
  import { resolveStaticDir } from '../utilities/resolveStaticDir.js';
6
6
  import { fetchFileBuffer, isCloudStorage } from '../utilities/storage.js';
7
7
  export const createConvertFormatsHandler = (resolvedConfig)=>{
@@ -48,11 +48,11 @@ export const createConvertFormatsHandler = (resolvedConfig)=>{
48
48
  // skip it and only generate variants for the remaining formats.
49
49
  const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0 ? perCollectionConfig.formats.slice(1) : perCollectionConfig.formats;
50
50
  const safeFilename = path.basename(doc.filename);
51
- for (const format of formatsToGenerate){
51
+ const variantResults = await Promise.all(formatsToGenerate.map(async (format)=>{
52
52
  const result = await convertFormat(fileBuffer, format.format, format.quality);
53
53
  const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`;
54
54
  await fs.writeFile(path.join(staticDir, variantFilename), result.buffer);
55
- variants.push({
55
+ return {
56
56
  format: format.format,
57
57
  filename: variantFilename,
58
58
  filesize: result.size,
@@ -60,7 +60,13 @@ export const createConvertFormatsHandler = (resolvedConfig)=>{
60
60
  height: result.height,
61
61
  mimeType: result.mimeType,
62
62
  url: `/api/${input.collectionSlug}/file/${variantFilename}`
63
- });
63
+ };
64
+ }));
65
+ variants.push(...variantResults);
66
+ // Compute ThumbHash in the background job to avoid blocking the sync save path
67
+ let thumbHash;
68
+ if (resolvedConfig.generateThumbHash) {
69
+ thumbHash = await generateThumbHash(fileBuffer);
64
70
  }
65
71
  await req.payload.update({
66
72
  collection: input.collectionSlug,
@@ -70,6 +76,7 @@ export const createConvertFormatsHandler = (resolvedConfig)=>{
70
76
  ...doc.imageOptimizer,
71
77
  status: 'complete',
72
78
  variants,
79
+ thumbHash,
73
80
  error: null
74
81
  }
75
82
  },
@@ -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 ...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, docId: input.docId, collectionSlug: input.collectionSlug },\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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"}
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, generateThumbHash } 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 const variantResults = await Promise.all(\n formatsToGenerate.map(async (format) => {\n const result = await convertFormat(fileBuffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n return {\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 variants.push(...variantResults)\n\n // Compute ThumbHash in the background job to avoid blocking the sync save path\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(fileBuffer)\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 thumbHash,\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, docId: input.docId, collectionSlug: input.collectionSlug },\n 'Failed to persist error status for image optimizer',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","convertFormat","generateThumbHash","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","variantResults","Promise","all","map","format","result","quality","variantFilename","parse","name","writeFile","join","buffer","filesize","size","width","height","mimeType","url","push","thumbHash","err","errorMessage","message","String","updateErr","logger"],"mappings":"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"}