@inoo-ch/payload-image-optimizer 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tasks/convertFormats.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { convertFormat } from '../processing/index.js'\n\nexport const createConvertFormatsHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n const fileBuffer = await fs.readFile(filePath)\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n for (const format of perCollectionConfig.formats) {\n const result = await convertFormat(fileBuffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'complete',\n variants,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { variantsGenerated: variants.length } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","convertFormat","createConvertFormatsHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","variants","perCollectionConfig","format","formats","result","quality","variantFilename","parse","name","writeFile","buffer","push","filesize","size","width","height","mimeType","url","update","data","imageOptimizer","status","context","imageOptimizer_skip","output","variantsGenerated","length","err","errorMessage","message","String","error","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 } from '../processing/index.js'\n\nexport const createConvertFormatsHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n const fileBuffer = await fs.readFile(filePath)\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // When replaceOriginal is on, the main file is already in the primary format —\n // skip it and only generate variants for the remaining formats.\n const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0\n ? perCollectionConfig.formats.slice(1)\n : perCollectionConfig.formats\n\n for (const format of formatsToGenerate) {\n const result = await convertFormat(fileBuffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'complete',\n variants,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { variantsGenerated: variants.length } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","convertFormat","createConvertFormatsHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","variants","perCollectionConfig","formatsToGenerate","replaceOriginal","formats","length","slice","format","result","quality","variantFilename","parse","name","writeFile","buffer","push","filesize","size","width","height","mimeType","url","update","data","imageOptimizer","status","context","imageOptimizer_skip","output","variantsGenerated","err","errorMessage","message","String","error","updateErr","logger"],"mappings":"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"}
@@ -46,18 +46,37 @@ export const createRegenerateDocumentHandler = (resolvedConfig)=>{
46
46
  const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug);
47
47
  // Step 1: Strip metadata + resize
48
48
  const processed = await stripAndResize(fileBuffer, perCollectionConfig.maxDimensions, resolvedConfig.stripMetadata);
49
- // Write optimized file back to disk
50
- await fs.writeFile(filePath, processed.buffer);
49
+ let mainBuffer = processed.buffer;
50
+ let mainSize = processed.size;
51
+ let newFilename = safeFilename;
52
+ let newMimeType;
53
+ // Step 1b: If replaceOriginal, convert main file to primary format
54
+ if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {
55
+ const primaryFormat = perCollectionConfig.formats[0];
56
+ const converted = await convertFormat(processed.buffer, primaryFormat.format, primaryFormat.quality);
57
+ mainBuffer = converted.buffer;
58
+ mainSize = converted.size;
59
+ newFilename = `${path.parse(safeFilename).name}.${primaryFormat.format}`;
60
+ newMimeType = converted.mimeType;
61
+ }
62
+ // Write optimized file to disk
63
+ const newFilePath = path.join(staticDir, newFilename);
64
+ await fs.writeFile(newFilePath, mainBuffer);
65
+ // Clean up old file if filename changed
66
+ if (newFilename !== safeFilename) {
67
+ await fs.unlink(filePath).catch(()=>{});
68
+ }
51
69
  // Step 2: Generate ThumbHash
52
70
  let thumbHash;
53
71
  if (resolvedConfig.generateThumbHash) {
54
- thumbHash = await generateThumbHash(processed.buffer);
72
+ thumbHash = await generateThumbHash(mainBuffer);
55
73
  }
56
- // Step 3: Convert to all configured formats
74
+ // Step 3: Convert to configured formats (skip primary when replaceOriginal)
75
+ const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0 ? perCollectionConfig.formats.slice(1) : perCollectionConfig.formats;
57
76
  const variants = [];
58
- for (const format of perCollectionConfig.formats){
59
- const result = await convertFormat(processed.buffer, format.format, format.quality);
60
- const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`;
77
+ for (const format of formatsToGenerate){
78
+ const result = await convertFormat(mainBuffer, format.format, format.quality);
79
+ const variantFilename = `${path.parse(newFilename).name}-optimized.${format.format}`;
61
80
  await fs.writeFile(path.join(staticDir, variantFilename), result.buffer);
62
81
  variants.push({
63
82
  format: format.format,
@@ -70,19 +89,26 @@ export const createRegenerateDocumentHandler = (resolvedConfig)=>{
70
89
  });
71
90
  }
72
91
  // Step 4: Update the document with all optimization data
92
+ const updateData = {
93
+ imageOptimizer: {
94
+ originalSize,
95
+ optimizedSize: mainSize,
96
+ status: 'complete',
97
+ thumbHash,
98
+ variants,
99
+ error: null
100
+ }
101
+ };
102
+ // Update filename, mimeType, and filesize when replaceOriginal changed them
103
+ if (newFilename !== safeFilename) {
104
+ updateData.filename = newFilename;
105
+ updateData.filesize = mainSize;
106
+ updateData.mimeType = newMimeType;
107
+ }
73
108
  await req.payload.update({
74
109
  collection: input.collectionSlug,
75
110
  id: input.docId,
76
- data: {
77
- imageOptimizer: {
78
- originalSize,
79
- optimizedSize: processed.size,
80
- status: 'complete',
81
- thumbHash,
82
- variants,
83
- error: null
84
- }
85
- },
111
+ data: updateData,
86
112
  context: {
87
113
  imageOptimizer_skip: true
88
114
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tasks/regenerateDocument.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js'\n\nexport const createRegenerateDocumentHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n // Skip non-image documents\n if (!doc.mimeType || !doc.mimeType.startsWith('image/')) {\n return { output: { status: 'skipped', reason: 'not-image' } }\n }\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n\n let fileBuffer: Buffer\n try {\n fileBuffer = await fs.readFile(filePath)\n } catch {\n // If file not on disk, try fetching from URL\n if (doc.url) {\n const url = doc.url.startsWith('http')\n ? doc.url\n : `${process.env.NEXT_PUBLIC_SERVER_URL || ''}${doc.url}`\n const response = await fetch(url)\n fileBuffer = Buffer.from(await response.arrayBuffer())\n } else {\n throw new Error(`File not found: ${filePath}`)\n }\n }\n\n const originalSize = fileBuffer.length\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // Step 1: Strip metadata + resize\n const processed = await stripAndResize(\n fileBuffer,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n // Write optimized file back to disk\n await fs.writeFile(filePath, processed.buffer)\n\n // Step 2: Generate ThumbHash\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(processed.buffer)\n }\n\n // Step 3: Convert to all configured formats\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n for (const format of perCollectionConfig.formats) {\n const result = await convertFormat(processed.buffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n // Step 4: Update the document with all optimization data\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n originalSize,\n optimizedSize: processed.size,\n status: 'complete',\n thumbHash,\n variants,\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { status: 'complete' } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer regeneration',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","stripAndResize","generateThumbHash","convertFormat","createRegenerateDocumentHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","mimeType","startsWith","output","status","reason","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","url","env","NEXT_PUBLIC_SERVER_URL","response","fetch","Buffer","from","arrayBuffer","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","writeFile","buffer","thumbHash","variants","format","formats","result","quality","variantFilename","parse","name","push","filesize","size","width","height","update","data","imageOptimizer","optimizedSize","error","context","imageOptimizer_skip","err","errorMessage","message","String","updateErr","logger"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,UAAU,OAAM;AAKvB,SAASC,uBAAuB,QAAQ,iBAAgB;AACxD,SAASC,cAAc,EAAEC,iBAAiB,EAAEC,aAAa,QAAQ,yBAAwB;AAEzF,OAAO,MAAMC,kCAAkC,CAACC;IAC9C,OAAO,OAAO,EAAEC,KAAK,EAAEC,GAAG,EAAkE;QAC1F,IAAI;YACF,MAAMC,MAAM,MAAMD,IAAIE,OAAO,CAACC,QAAQ,CAAC;gBACrCC,YAAYL,MAAMM,cAAc;gBAChCC,IAAIP,MAAMQ,KAAK;YACjB;YAEA,2BAA2B;YAC3B,IAAI,CAACN,IAAIO,QAAQ,IAAI,CAACP,IAAIO,QAAQ,CAACC,UAAU,CAAC,WAAW;gBACvD,OAAO;oBAAEC,QAAQ;wBAAEC,QAAQ;wBAAWC,QAAQ;oBAAY;gBAAE;YAC9D;YAEA,MAAMC,mBAAmBb,IAAIE,OAAO,CAACY,WAAW,CAACf,MAAMM,cAAc,CAAyC,CAACU,MAAM;YAErH,IAAIC,YACF,OAAOH,iBAAiBI,MAAM,KAAK,WAAWJ,iBAAiBI,MAAM,CAACD,SAAS,IAAI,KAAK;YAC1F,IAAI,CAACA,WAAW;gBACd,MAAM,IAAIE,MAAM,CAAC,wCAAwC,EAAEnB,MAAMM,cAAc,CAAC,CAAC,CAAC;YACpF;YACA,IAAI,CAACb,KAAK2B,UAAU,CAACH,YAAY;gBAC/BA,YAAYxB,KAAK4B,OAAO,CAACC,QAAQC,GAAG,IAAIN;YAC1C;YAEA,8CAA8C;YAC9C,MAAMO,eAAe/B,KAAKgC,QAAQ,CAACvB,IAAIwB,QAAQ;YAC/C,MAAMC,WAAWlC,KAAKmC,IAAI,CAACX,WAAWO;YAEtC,IAAIK;YACJ,IAAI;gBACFA,aAAa,MAAMrC,GAAGsC,QAAQ,CAACH;YACjC,EAAE,OAAM;gBACN,6CAA6C;gBAC7C,IAAIzB,IAAI6B,GAAG,EAAE;oBACX,MAAMA,MAAM7B,IAAI6B,GAAG,CAACrB,UAAU,CAAC,UAC3BR,IAAI6B,GAAG,GACP,GAAGT,QAAQU,GAAG,CAACC,sBAAsB,IAAI,KAAK/B,IAAI6B,GAAG,EAAE;oBAC3D,MAAMG,WAAW,MAAMC,MAAMJ;oBAC7BF,aAAaO,OAAOC,IAAI,CAAC,MAAMH,SAASI,WAAW;gBACrD,OAAO;oBACL,MAAM,IAAInB,MAAM,CAAC,gBAAgB,EAAEQ,UAAU;gBAC/C;YACF;YAEA,MAAMY,eAAeV,WAAWW,MAAM;YACtC,MAAMC,sBAAsB/C,wBAAwBK,gBAAgBC,MAAMM,cAAc;YAExF,kCAAkC;YAClC,MAAMoC,YAAY,MAAM/C,eACtBkC,YACAY,oBAAoBE,aAAa,EACjC5C,eAAe6C,aAAa;YAG9B,oCAAoC;YACpC,MAAMpD,GAAGqD,SAAS,CAAClB,UAAUe,UAAUI,MAAM;YAE7C,6BAA6B;YAC7B,IAAIC;YACJ,IAAIhD,eAAeH,iBAAiB,EAAE;gBACpCmD,YAAY,MAAMnD,kBAAkB8C,UAAUI,MAAM;YACtD;YAEA,4CAA4C;YAC5C,MAAME,WAQD,EAAE;YAEP,KAAK,MAAMC,UAAUR,oBAAoBS,OAAO,CAAE;gBAChD,MAAMC,SAAS,MAAMtD,cAAc6C,UAAUI,MAAM,EAAEG,OAAOA,MAAM,EAAEA,OAAOG,OAAO;gBAClF,MAAMC,kBAAkB,GAAG5D,KAAK6D,KAAK,CAAC9B,cAAc+B,IAAI,CAAC,WAAW,EAAEN,OAAOA,MAAM,EAAE;gBACrF,MAAMzD,GAAGqD,SAAS,CAACpD,KAAKmC,IAAI,CAACX,WAAWoC,kBAAkBF,OAAOL,MAAM;gBAEvEE,SAASQ,IAAI,CAAC;oBACZP,QAAQA,OAAOA,MAAM;oBACrBvB,UAAU2B;oBACVI,UAAUN,OAAOO,IAAI;oBACrBC,OAAOR,OAAOQ,KAAK;oBACnBC,QAAQT,OAAOS,MAAM;oBACrBnD,UAAU0C,OAAO1C,QAAQ;oBACzBsB,KAAK,CAAC,KAAK,EAAE/B,MAAMM,cAAc,CAAC,MAAM,EAAE+C,iBAAiB;gBAC7D;YACF;YAEA,yDAAyD;YACzD,MAAMpD,IAAIE,OAAO,CAAC0D,MAAM,CAAC;gBACvBxD,YAAYL,MAAMM,cAAc;gBAChCC,IAAIP,MAAMQ,KAAK;gBACfsD,MAAM;oBACJC,gBAAgB;wBACdxB;wBACAyB,eAAetB,UAAUgB,IAAI;wBAC7B9C,QAAQ;wBACRmC;wBACAC;wBACAiB,OAAO;oBACT;gBACF;gBACAC,SAAS;oBAAEC,qBAAqB;gBAAK;YACvC;YAEA,OAAO;gBAAExD,QAAQ;oBAAEC,QAAQ;gBAAW;YAAE;QAC1C,EAAE,OAAOwD,KAAK;YACZ,MAAMC,eAAeD,eAAejD,QAAQiD,IAAIE,OAAO,GAAGC,OAAOH;YAEjE,IAAI;gBACF,MAAMnE,IAAIE,OAAO,CAAC0D,MAAM,CAAC;oBACvBxD,YAAYL,MAAMM,cAAc;oBAChCC,IAAIP,MAAMQ,KAAK;oBACfsD,MAAM;wBACJC,gBAAgB;4BACdnD,QAAQ;4BACRqD,OAAOI;wBACT;oBACF;oBACAH,SAAS;wBAAEC,qBAAqB;oBAAK;gBACvC;YACF,EAAE,OAAOK,WAAW;gBAClBvE,IAAIE,OAAO,CAACsE,MAAM,CAACR,KAAK,CACtB;oBAAEG,KAAKI;gBAAU,GACjB;YAEJ;YAEA,MAAMJ;QACR;IACF;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/tasks/regenerateDocument.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js'\n\nexport const createRegenerateDocumentHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n // Skip non-image documents\n if (!doc.mimeType || !doc.mimeType.startsWith('image/')) {\n return { output: { status: 'skipped', reason: 'not-image' } }\n }\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n\n let fileBuffer: Buffer\n try {\n fileBuffer = await fs.readFile(filePath)\n } catch {\n // If file not on disk, try fetching from URL\n if (doc.url) {\n const url = doc.url.startsWith('http')\n ? doc.url\n : `${process.env.NEXT_PUBLIC_SERVER_URL || ''}${doc.url}`\n const response = await fetch(url)\n fileBuffer = Buffer.from(await response.arrayBuffer())\n } else {\n throw new Error(`File not found: ${filePath}`)\n }\n }\n\n const originalSize = fileBuffer.length\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // Step 1: Strip metadata + resize\n const processed = await stripAndResize(\n fileBuffer,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n let mainBuffer = processed.buffer\n let mainSize = processed.size\n let newFilename = safeFilename\n let newMimeType: string | undefined\n\n // Step 1b: If replaceOriginal, convert main file to primary format\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0) {\n const primaryFormat = perCollectionConfig.formats[0]\n const converted = await convertFormat(processed.buffer, primaryFormat.format, primaryFormat.quality)\n mainBuffer = converted.buffer\n mainSize = converted.size\n newFilename = `${path.parse(safeFilename).name}.${primaryFormat.format}`\n newMimeType = converted.mimeType\n }\n\n // Write optimized file to disk\n const newFilePath = path.join(staticDir, newFilename)\n await fs.writeFile(newFilePath, mainBuffer)\n\n // Clean up old file if filename changed\n if (newFilename !== safeFilename) {\n await fs.unlink(filePath).catch(() => {})\n }\n\n // Step 2: Generate ThumbHash\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(mainBuffer)\n }\n\n // Step 3: Convert to configured formats (skip primary when replaceOriginal)\n const formatsToGenerate = perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length > 0\n ? perCollectionConfig.formats.slice(1)\n : perCollectionConfig.formats\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n for (const format of formatsToGenerate) {\n const result = await convertFormat(mainBuffer, format.format, format.quality)\n const variantFilename = `${path.parse(newFilename).name}-optimized.${format.format}`\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n // Step 4: Update the document with all optimization data\n const updateData: Record<string, any> = {\n imageOptimizer: {\n originalSize,\n optimizedSize: mainSize,\n status: 'complete',\n thumbHash,\n variants,\n error: null,\n },\n }\n\n // Update filename, mimeType, and filesize when replaceOriginal changed them\n if (newFilename !== safeFilename) {\n updateData.filename = newFilename\n updateData.filesize = mainSize\n updateData.mimeType = newMimeType\n }\n\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: updateData,\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { status: 'complete' } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer regeneration',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","stripAndResize","generateThumbHash","convertFormat","createRegenerateDocumentHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","mimeType","startsWith","output","status","reason","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","url","env","NEXT_PUBLIC_SERVER_URL","response","fetch","Buffer","from","arrayBuffer","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","mainBuffer","buffer","mainSize","size","newFilename","newMimeType","replaceOriginal","formats","primaryFormat","converted","format","quality","parse","name","newFilePath","writeFile","unlink","catch","thumbHash","formatsToGenerate","slice","variants","result","variantFilename","push","filesize","width","height","updateData","imageOptimizer","optimizedSize","error","update","data","context","imageOptimizer_skip","err","errorMessage","message","String","updateErr","logger"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -10,6 +10,7 @@ export type CollectionOptimizerConfig = {
10
10
  width: number;
11
11
  height: number;
12
12
  };
13
+ replaceOriginal?: boolean;
13
14
  };
14
15
  export type ImageOptimizerConfig = {
15
16
  collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>;
@@ -20,6 +21,7 @@ export type ImageOptimizerConfig = {
20
21
  width: number;
21
22
  height: number;
22
23
  };
24
+ replaceOriginal?: boolean;
23
25
  stripMetadata?: boolean;
24
26
  };
25
27
  export type ResolvedCollectionOptimizerConfig = {
@@ -28,8 +30,10 @@ export type ResolvedCollectionOptimizerConfig = {
28
30
  width: number;
29
31
  height: number;
30
32
  };
33
+ replaceOriginal: boolean;
31
34
  };
32
35
  export type ResolvedImageOptimizerConfig = Required<Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>> & {
33
36
  collections: ImageOptimizerConfig['collections'];
34
37
  disabled: boolean;
38
+ replaceOriginal: boolean;
35
39
  };
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nexport type ImageFormat = 'webp' | 'avif'\n\nexport type FormatQuality = {\n format: ImageFormat\n quality: number // 1-100\n}\n\nexport type CollectionOptimizerConfig = {\n formats?: FormatQuality[]\n maxDimensions?: { width: number; height: number }\n}\n\nexport type ImageOptimizerConfig = {\n collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>\n disabled?: boolean\n formats?: FormatQuality[]\n generateThumbHash?: boolean\n maxDimensions?: { width: number; height: number }\n stripMetadata?: boolean\n}\n\nexport type ResolvedCollectionOptimizerConfig = {\n formats: FormatQuality[]\n maxDimensions: { width: number; height: number }\n}\n\nexport type ResolvedImageOptimizerConfig = Required<\n Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>\n> & {\n collections: ImageOptimizerConfig['collections']\n disabled: boolean\n}\n"],"names":[],"mappings":"AA4BA,WAKC"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nexport type ImageFormat = 'webp' | 'avif'\n\nexport type FormatQuality = {\n format: ImageFormat\n quality: number // 1-100\n}\n\nexport type CollectionOptimizerConfig = {\n formats?: FormatQuality[]\n maxDimensions?: { width: number; height: number }\n replaceOriginal?: boolean\n}\n\nexport type ImageOptimizerConfig = {\n collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>\n disabled?: boolean\n formats?: FormatQuality[]\n generateThumbHash?: boolean\n maxDimensions?: { width: number; height: number }\n replaceOriginal?: boolean\n stripMetadata?: boolean\n}\n\nexport type ResolvedCollectionOptimizerConfig = {\n formats: FormatQuality[]\n maxDimensions: { width: number; height: number }\n replaceOriginal: boolean\n}\n\nexport type ResolvedImageOptimizerConfig = Required<\n Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>\n> & {\n collections: ImageOptimizerConfig['collections']\n disabled: boolean\n replaceOriginal: boolean\n}\n"],"names":[],"mappings":"AA+BA,WAMC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoo-ch/payload-image-optimizer",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Payload CMS plugin for automatic image optimization — WebP/AVIF conversion, resize, EXIF strip, ThumbHash placeholders, and bulk regeneration",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -24,44 +24,28 @@
24
24
  "type": "module",
25
25
  "exports": {
26
26
  ".": {
27
- "import": "./src/index.ts",
28
- "types": "./src/index.ts",
29
- "default": "./src/index.ts"
27
+ "import": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
30
  },
31
31
  "./client": {
32
- "import": "./src/exports/client.ts",
33
- "types": "./src/exports/client.ts",
34
- "default": "./src/exports/client.ts"
32
+ "import": "./dist/exports/client.js",
33
+ "types": "./dist/exports/client.d.ts",
34
+ "default": "./dist/exports/client.js"
35
35
  },
36
36
  "./rsc": {
37
- "import": "./src/exports/rsc.ts",
38
- "types": "./src/exports/rsc.ts",
39
- "default": "./src/exports/rsc.ts"
37
+ "import": "./dist/exports/rsc.js",
38
+ "types": "./dist/exports/rsc.d.ts",
39
+ "default": "./dist/exports/rsc.js"
40
40
  }
41
41
  },
42
- "main": "./src/index.ts",
43
- "types": "./src/index.ts",
42
+ "main": "./dist/index.js",
43
+ "types": "./dist/index.d.ts",
44
44
  "files": [
45
- "dist"
45
+ "dist",
46
+ "src",
47
+ "AGENT_DOCS.md"
46
48
  ],
47
- "scripts": {
48
- "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
49
- "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
50
- "build:types": "tsc --outDir dist --rootDir ./src",
51
- "clean": "rimraf {dist,*.tsbuildinfo}",
52
- "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
53
- "dev": "next dev dev --turbo",
54
- "dev:generate-importmap": "pnpm dev:payload generate:importmap",
55
- "dev:generate-types": "pnpm dev:payload generate:types",
56
- "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
57
- "generate:importmap": "pnpm dev:generate-importmap",
58
- "generate:types": "pnpm dev:generate-types",
59
- "lint": "eslint",
60
- "lint:fix": "eslint ./src --fix",
61
- "test": "pnpm test:int && pnpm test:e2e",
62
- "test:e2e": "playwright test",
63
- "test:int": "vitest"
64
- },
65
49
  "devDependencies": {
66
50
  "@eslint/eslintrc": "^3.2.0",
67
51
  "@payloadcms/db-mongodb": "3.79.0",
@@ -106,36 +90,26 @@
106
90
  "node": "^18.20.2 || >=20.9.0",
107
91
  "pnpm": "^9 || ^10"
108
92
  },
109
- "publishConfig": {
110
- "exports": {
111
- ".": {
112
- "import": "./dist/index.js",
113
- "types": "./dist/index.d.ts",
114
- "default": "./dist/index.js"
115
- },
116
- "./client": {
117
- "import": "./dist/exports/client.js",
118
- "types": "./dist/exports/client.d.ts",
119
- "default": "./dist/exports/client.js"
120
- },
121
- "./rsc": {
122
- "import": "./dist/exports/rsc.js",
123
- "types": "./dist/exports/rsc.d.ts",
124
- "default": "./dist/exports/rsc.js"
125
- }
126
- },
127
- "main": "./dist/index.js",
128
- "types": "./dist/index.d.ts"
129
- },
130
- "pnpm": {
131
- "onlyBuiltDependencies": [
132
- "sharp",
133
- "esbuild",
134
- "unrs-resolver"
135
- ]
136
- },
137
93
  "registry": "https://registry.npmjs.org/",
138
94
  "dependencies": {
139
95
  "thumbhash": "^0.1.1"
96
+ },
97
+ "scripts": {
98
+ "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
99
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
100
+ "build:types": "tsc --outDir dist --rootDir ./src",
101
+ "clean": "rimraf {dist,*.tsbuildinfo}",
102
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
103
+ "dev": "next dev dev --turbo",
104
+ "dev:generate-importmap": "pnpm dev:payload generate:importmap",
105
+ "dev:generate-types": "pnpm dev:payload generate:types",
106
+ "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
107
+ "generate:importmap": "pnpm dev:generate-importmap",
108
+ "generate:types": "pnpm dev:generate-types",
109
+ "lint": "eslint",
110
+ "lint:fix": "eslint ./src --fix",
111
+ "test": "pnpm test:int && pnpm test:e2e",
112
+ "test:e2e": "playwright test",
113
+ "test:int": "vitest"
140
114
  }
141
- }
115
+ }
@@ -0,0 +1,80 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import NextImage, { type ImageProps } from 'next/image'
5
+ import { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
6
+
7
+ type ImageOptimizerData = {
8
+ thumbHash?: string | null
9
+ }
10
+
11
+ type MediaResource = {
12
+ url?: string | null
13
+ alt?: string | null
14
+ width?: number | null
15
+ height?: number | null
16
+ filename?: string | null
17
+ focalX?: number | null
18
+ focalY?: number | null
19
+ imageOptimizer?: ImageOptimizerData | null
20
+ updatedAt?: string
21
+ }
22
+
23
+ export interface ImageBoxProps extends Omit<ImageProps, 'src' | 'alt'> {
24
+ media: MediaResource | string
25
+ alt?: string
26
+ }
27
+
28
+ export const ImageBox: React.FC<ImageBoxProps> = ({
29
+ media,
30
+ alt: altFromProps,
31
+ fill,
32
+ sizes,
33
+ priority,
34
+ loading: loadingFromProps,
35
+ style: styleFromProps,
36
+ ...props
37
+ }) => {
38
+ const loading = priority ? undefined : (loadingFromProps ?? 'lazy')
39
+
40
+ if (typeof media === 'string') {
41
+ return (
42
+ <NextImage
43
+ {...props}
44
+ src={media}
45
+ alt={altFromProps || ''}
46
+ quality={80}
47
+ fill={fill}
48
+ sizes={sizes}
49
+ style={{ objectFit: 'cover', objectPosition: 'center', ...styleFromProps }}
50
+ priority={priority}
51
+ loading={loading}
52
+ />
53
+ )
54
+ }
55
+
56
+ const width = media.width ?? undefined
57
+ const height = media.height ?? undefined
58
+ const alt = altFromProps || (media as any).alt || media.filename || ''
59
+ const src = media.url ? `${media.url}${media.updatedAt ? `?${media.updatedAt}` : ''}` : ''
60
+
61
+ const optimizerProps = getImageOptimizerProps(media)
62
+
63
+ return (
64
+ <NextImage
65
+ {...props}
66
+ src={src}
67
+ alt={alt}
68
+ quality={80}
69
+ fill={fill}
70
+ width={!fill ? width : undefined}
71
+ height={!fill ? height : undefined}
72
+ sizes={sizes}
73
+ style={{ objectFit: 'cover', ...optimizerProps.style, ...styleFromProps }}
74
+ placeholder={optimizerProps.placeholder}
75
+ blurDataURL={optimizerProps.blurDataURL}
76
+ priority={priority}
77
+ loading={loading}
78
+ />
79
+ )
80
+ }
@@ -0,0 +1,137 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { thumbHashToDataURL } from 'thumbhash'
5
+ import { useAllFormFields } from '@payloadcms/ui'
6
+
7
+ const formatBytes = (bytes: number): string => {
8
+ if (bytes === 0) return '0 B'
9
+ const k = 1024
10
+ const sizes = ['B', 'KB', 'MB', 'GB']
11
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
12
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
13
+ }
14
+
15
+ const statusColors: Record<string, string> = {
16
+ pending: '#f59e0b',
17
+ processing: '#3b82f6',
18
+ complete: '#10b981',
19
+ error: '#ef4444',
20
+ }
21
+
22
+ export const OptimizationStatus: React.FC<{ path?: string }> = (props) => {
23
+ const [formState] = useAllFormFields()
24
+ const basePath = props.path ?? 'imageOptimizer'
25
+
26
+ const status = formState[`${basePath}.status`]?.value as string | undefined
27
+ const originalSize = formState[`${basePath}.originalSize`]?.value as number | undefined
28
+ const optimizedSize = formState[`${basePath}.optimizedSize`]?.value as number | undefined
29
+ const thumbHash = formState[`${basePath}.thumbHash`]?.value as string | undefined
30
+ const error = formState[`${basePath}.error`]?.value as string | undefined
31
+
32
+ const thumbHashUrl = React.useMemo(() => {
33
+ if (!thumbHash) return null
34
+ try {
35
+ const bytes = Uint8Array.from(atob(thumbHash), c => c.charCodeAt(0))
36
+ return thumbHashToDataURL(bytes)
37
+ } catch {
38
+ return null
39
+ }
40
+ }, [thumbHash])
41
+
42
+ // Read variants array from form state
43
+ const variantsField = formState[`${basePath}.variants`]
44
+ const rowCount = (variantsField as any)?.rows?.length ?? 0
45
+ const variants: Array<{
46
+ format?: string
47
+ filename?: string
48
+ filesize?: number
49
+ width?: number
50
+ height?: number
51
+ }> = []
52
+
53
+ for (let i = 0; i < rowCount; i++) {
54
+ variants.push({
55
+ format: formState[`${basePath}.variants.${i}.format`]?.value as string | undefined,
56
+ filename: formState[`${basePath}.variants.${i}.filename`]?.value as string | undefined,
57
+ filesize: formState[`${basePath}.variants.${i}.filesize`]?.value as number | undefined,
58
+ width: formState[`${basePath}.variants.${i}.width`]?.value as number | undefined,
59
+ height: formState[`${basePath}.variants.${i}.height`]?.value as number | undefined,
60
+ })
61
+ }
62
+
63
+ if (!status) {
64
+ return (
65
+ <div style={{ padding: '12px 0' }}>
66
+ <div style={{ color: '#6b7280', fontSize: '13px' }}>
67
+ No optimization data yet. Upload an image to optimize.
68
+ </div>
69
+ </div>
70
+ )
71
+ }
72
+
73
+ const savings =
74
+ originalSize && optimizedSize
75
+ ? Math.round((1 - optimizedSize / originalSize) * 100)
76
+ : null
77
+
78
+ return (
79
+ <div style={{ padding: '12px 0' }}>
80
+ <div style={{ marginBottom: '8px' }}>
81
+ <span
82
+ style={{
83
+ backgroundColor: statusColors[status] || '#6b7280',
84
+ borderRadius: '4px',
85
+ color: '#fff',
86
+ display: 'inline-block',
87
+ fontSize: '12px',
88
+ fontWeight: 600,
89
+ padding: '2px 8px',
90
+ textTransform: 'uppercase',
91
+ }}
92
+ >
93
+ {status}
94
+ </span>
95
+ </div>
96
+
97
+ {error && (
98
+ <div style={{ color: '#ef4444', fontSize: '13px', marginBottom: '8px' }}>{error}</div>
99
+ )}
100
+
101
+ {originalSize != null && optimizedSize != null && (
102
+ <div style={{ fontSize: '13px', marginBottom: '8px' }}>
103
+ <div>Original: <strong>{formatBytes(originalSize)}</strong></div>
104
+ <div>
105
+ Optimized: <strong>{formatBytes(optimizedSize)}</strong>
106
+ {savings != null && savings > 0 && (
107
+ <span style={{ color: '#10b981', marginLeft: '4px' }}>(-{savings}%)</span>
108
+ )}
109
+ </div>
110
+ </div>
111
+ )}
112
+
113
+ {thumbHashUrl && (
114
+ <div style={{ marginBottom: '8px' }}>
115
+ <div style={{ fontSize: '12px', marginBottom: '4px', opacity: 0.7 }}>Blur Preview</div>
116
+ <img
117
+ alt="Blur placeholder"
118
+ src={thumbHashUrl}
119
+ style={{ borderRadius: '4px', height: '40px', width: 'auto' }}
120
+ />
121
+ </div>
122
+ )}
123
+
124
+ {variants.length > 0 && (
125
+ <div>
126
+ <div style={{ fontSize: '12px', marginBottom: '4px', opacity: 0.7 }}>Variants</div>
127
+ {variants.map((v, i) => (
128
+ <div key={i} style={{ fontSize: '12px', marginBottom: '2px' }}>
129
+ <strong>{v.format?.toUpperCase()}</strong> — {v.filesize ? formatBytes(v.filesize) : '?'}{' '}
130
+ ({v.width}x{v.height})
131
+ </div>
132
+ ))}
133
+ </div>
134
+ )}
135
+ </div>
136
+ )
137
+ }