@inoo-ch/payload-image-optimizer 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/components/RegenerationButton.js +85 -21
  2. package/dist/components/RegenerationButton.js.map +1 -1
  3. package/dist/defaults.js +14 -2
  4. package/dist/defaults.js.map +1 -1
  5. package/dist/endpoints/regenerate.d.ts +1 -0
  6. package/dist/endpoints/regenerate.js +77 -1
  7. package/dist/endpoints/regenerate.js.map +1 -1
  8. package/dist/hooks/beforeChange.js +16 -9
  9. package/dist/hooks/beforeChange.js.map +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.js +32 -5
  12. package/dist/index.js.map +1 -1
  13. package/dist/tasks/regenerateDocument.js +27 -4
  14. package/dist/tasks/regenerateDocument.js.map +1 -1
  15. package/dist/types.d.ts +49 -2
  16. package/dist/types.js.map +1 -1
  17. package/dist/utilities/filenameStrategies.d.ts +25 -0
  18. package/dist/utilities/filenameStrategies.js +46 -0
  19. package/dist/utilities/filenameStrategies.js.map +1 -0
  20. package/dist/utilities/stripDiacritics.d.ts +9 -0
  21. package/dist/utilities/stripDiacritics.js +10 -0
  22. package/dist/utilities/stripDiacritics.js.map +1 -0
  23. package/dist/utilities/toKebabCase.d.ts +10 -0
  24. package/dist/utilities/toKebabCase.js +11 -0
  25. package/dist/utilities/toKebabCase.js.map +1 -0
  26. package/package.json +1 -1
  27. package/src/components/RegenerationButton.tsx +92 -24
  28. package/src/defaults.ts +15 -1
  29. package/src/endpoints/regenerate.ts +68 -0
  30. package/src/hooks/beforeChange.ts +16 -9
  31. package/src/index.ts +27 -6
  32. package/src/tasks/regenerateDocument.ts +24 -4
  33. package/src/types.ts +51 -2
  34. package/src/utilities/filenameStrategies.ts +61 -0
  35. package/src/utilities/stripDiacritics.ts +10 -0
  36. package/src/utilities/toKebabCase.ts +16 -0
@@ -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, 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 if (resolvedConfig.uniqueFileNames) {\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 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","req","imageOptimizer_skip","file","mimetype","startsWith","uniqueFileNames","ext","extname","name","uuid","randomUUID","filename","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 { 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 // 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 // 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":["path","resolveCollectionConfig","convertFormat","generateThumbHash","stripAndResize","isCloudStorage","createBeforeChangeHook","resolvedConfig","collectionSlug","context","data","originalDoc","req","imageOptimizer_skip","file","mimetype","startsWith","generateFilename","existingFilename","filename","ext","extname","name","stem","altText","alt","originalFilename","mimeType","newFilename","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","finalBuffer","buffer","finalSize","size","replaceOriginal","formats","primaryFormat","converted","format","quality","parse","imageOptimizer_originalFilename","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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"}
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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"}
@@ -1,13 +1,30 @@
1
- import crypto from 'crypto';
2
1
  import fs from 'fs/promises';
3
2
  import path from 'path';
4
3
  import { resolveCollectionConfig } from '../defaults.js';
5
4
  import { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js';
6
5
  import { resolveStaticDir } from '../utilities/resolveStaticDir.js';
7
6
  import { fetchFileBuffer, isCloudStorage } from '../utilities/storage.js';
7
+ const GLOBAL_SLUG = 'image-optimizer-state';
8
8
  export const createRegenerateDocumentHandler = (resolvedConfig)=>{
9
9
  return async ({ input, req })=>{
10
10
  try {
11
+ // Check cancellation before processing
12
+ try {
13
+ const state = await req.payload.findGlobal({
14
+ slug: GLOBAL_SLUG
15
+ });
16
+ const collState = state?.collections?.[input.collectionSlug];
17
+ if (collState?.cancelledAt && collState.cancelledAt > (collState.startedAt || 0)) {
18
+ return {
19
+ output: {
20
+ status: 'cancelled',
21
+ reason: 'user-cancelled'
22
+ }
23
+ };
24
+ }
25
+ } catch {
26
+ // Global may not exist yet — proceed normally
27
+ }
11
28
  const doc = await req.payload.findByID({
12
29
  collection: input.collectionSlug,
13
30
  id: input.docId
@@ -53,11 +70,17 @@ export const createRegenerateDocumentHandler = (resolvedConfig)=>{
53
70
  if (cloudStorage) {
54
71
  // Cloud storage: re-upload the optimized file via Payload's update API.
55
72
  // This triggers the cloud adapter's afterChange hook which uploads to cloud.
56
- // When uniqueFileNames is enabled, generate a new UUID filename to avoid
73
+ // When a filename strategy is configured, generate a new filename to avoid
57
74
  // Vercel Blob "already exists" errors (the adapter doesn't support allowOverwrite).
58
- if (resolvedConfig.uniqueFileNames) {
75
+ if (resolvedConfig.generateFilename) {
59
76
  const ext = path.extname(newFilename);
60
- newFilename = `${crypto.randomUUID()}${ext}`;
77
+ const stem = resolvedConfig.generateFilename({
78
+ altText: doc.alt,
79
+ originalFilename: safeFilename,
80
+ mimeType: doc.mimeType,
81
+ collectionSlug: input.collectionSlug
82
+ });
83
+ newFilename = `${stem}${ext}`;
61
84
  }
62
85
  const updateData = {
63
86
  imageOptimizer: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tasks/regenerateDocument.ts"],"sourcesContent":["import crypto from 'crypto'\nimport 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'\nimport { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { fetchFileBuffer, isCloudStorage } from '../utilities/storage.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 const cloudStorage = isCloudStorage(collectionConfig)\n\n const fileBuffer = await fetchFileBuffer(doc, collectionConfig)\n const originalSize = fileBuffer.length\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\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 // Step 2: Generate ThumbHash\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(mainBuffer)\n }\n\n // Step 3: Store the optimized file\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 if (cloudStorage) {\n // Cloud storage: re-upload the optimized file via Payload's update API.\n // This triggers the cloud adapter's afterChange hook which uploads to cloud.\n // When uniqueFileNames is enabled, generate a new UUID filename to avoid\n // Vercel Blob \"already exists\" errors (the adapter doesn't support allowOverwrite).\n if (resolvedConfig.uniqueFileNames) {\n const ext = path.extname(newFilename)\n newFilename = `${crypto.randomUUID()}${ext}`\n }\n\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 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 file: {\n data: mainBuffer,\n mimetype: newMimeType || doc.mimeType,\n name: newFilename,\n size: mainSize,\n },\n context: { imageOptimizer_skip: true },\n })\n } else {\n // Local storage: write files to disk\n const staticDir = resolveStaticDir(collectionConfig)\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 const oldFilePath = path.join(staticDir, safeFilename)\n await fs.unlink(oldFilePath).catch(() => {})\n }\n\n // Generate variant files (local storage only)\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(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 // Update the document with 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 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\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, docId: input.docId, collectionSlug: input.collectionSlug },\n 'Failed to persist error status for image optimizer regeneration',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["crypto","fs","path","resolveCollectionConfig","stripAndResize","generateThumbHash","convertFormat","resolveStaticDir","fetchFileBuffer","isCloudStorage","createRegenerateDocumentHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","mimeType","startsWith","output","status","reason","collectionConfig","collections","config","cloudStorage","fileBuffer","originalSize","length","perCollectionConfig","safeFilename","basename","filename","processed","maxDimensions","stripMetadata","mainBuffer","buffer","mainSize","size","newFilename","newMimeType","replaceOriginal","formats","primaryFormat","converted","format","quality","parse","name","thumbHash","variants","uniqueFileNames","ext","extname","randomUUID","updateData","imageOptimizer","optimizedSize","error","filesize","update","data","file","mimetype","context","imageOptimizer_skip","staticDir","newFilePath","join","writeFile","oldFilePath","unlink","catch","formatsToGenerate","slice","result","variantFilename","push","width","height","url","err","errorMessage","Error","message","String","updateErr","logger"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/tasks/regenerateDocument.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js'\nimport { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { fetchFileBuffer, isCloudStorage } from '../utilities/storage.js'\n\nconst GLOBAL_SLUG = 'image-optimizer-state'\n\nexport const createRegenerateDocumentHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n // Check cancellation before processing\n try {\n const state = await req.payload.findGlobal({ slug: GLOBAL_SLUG })\n const collState = (state?.collections as Record<string, any>)?.[input.collectionSlug]\n if (collState?.cancelledAt && collState.cancelledAt > (collState.startedAt || 0)) {\n return { output: { status: 'cancelled', reason: 'user-cancelled' } }\n }\n } catch {\n // Global may not exist yet — proceed normally\n }\n\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 const cloudStorage = isCloudStorage(collectionConfig)\n\n const fileBuffer = await fetchFileBuffer(doc, collectionConfig)\n const originalSize = fileBuffer.length\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\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 // Step 2: Generate ThumbHash\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(mainBuffer)\n }\n\n // Step 3: Store the optimized file\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 if (cloudStorage) {\n // Cloud storage: re-upload the optimized file via Payload's update API.\n // This triggers the cloud adapter's afterChange hook which uploads to cloud.\n // When a filename strategy is configured, generate a new filename to avoid\n // Vercel Blob \"already exists\" errors (the adapter doesn't support allowOverwrite).\n if (resolvedConfig.generateFilename) {\n const ext = path.extname(newFilename)\n const stem = resolvedConfig.generateFilename({\n altText: doc.alt as string | undefined,\n originalFilename: safeFilename,\n mimeType: doc.mimeType as string,\n collectionSlug: input.collectionSlug,\n // No existingFilename — regeneration should always create a fresh name\n // to avoid cloud storage \"already exists\" errors\n })\n newFilename = `${stem}${ext}`\n }\n\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 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 file: {\n data: mainBuffer,\n mimetype: newMimeType || doc.mimeType,\n name: newFilename,\n size: mainSize,\n },\n context: { imageOptimizer_skip: true },\n })\n } else {\n // Local storage: write files to disk\n const staticDir = resolveStaticDir(collectionConfig)\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 const oldFilePath = path.join(staticDir, safeFilename)\n await fs.unlink(oldFilePath).catch(() => {})\n }\n\n // Generate variant files (local storage only)\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(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 // Update the document with 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 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\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, docId: input.docId, collectionSlug: input.collectionSlug },\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","resolveStaticDir","fetchFileBuffer","isCloudStorage","GLOBAL_SLUG","createRegenerateDocumentHandler","resolvedConfig","input","req","state","payload","findGlobal","slug","collState","collections","collectionSlug","cancelledAt","startedAt","output","status","reason","doc","findByID","collection","id","docId","mimeType","startsWith","collectionConfig","config","cloudStorage","fileBuffer","originalSize","length","perCollectionConfig","safeFilename","basename","filename","processed","maxDimensions","stripMetadata","mainBuffer","buffer","mainSize","size","newFilename","newMimeType","replaceOriginal","formats","primaryFormat","converted","format","quality","parse","name","thumbHash","variants","generateFilename","ext","extname","stem","altText","alt","originalFilename","updateData","imageOptimizer","optimizedSize","error","filesize","update","data","file","mimetype","context","imageOptimizer_skip","staticDir","newFilePath","join","writeFile","oldFilePath","unlink","catch","formatsToGenerate","slice","result","variantFilename","push","width","height","url","err","errorMessage","Error","message","String","updateErr","logger"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,5 +1,26 @@
1
1
  import type { CollectionSlug, Field } from 'payload';
2
2
  export type ImageFormat = 'webp' | 'avif';
3
+ export type GenerateFilenameArgs = {
4
+ /** Alt text from the document (if the collection has an `alt` field) */
5
+ altText?: string;
6
+ /** Original uploaded filename (e.g., "IMG_2847.jpg") */
7
+ originalFilename: string;
8
+ /** The MIME type (e.g., "image/jpeg") */
9
+ mimeType: string;
10
+ /** The collection slug this file belongs to */
11
+ collectionSlug: string;
12
+ /** Existing filename from a previous upload (set on re-uploads / focal point changes).
13
+ * Strategies should typically reuse this to avoid cloud storage churn. */
14
+ existingFilename?: string;
15
+ };
16
+ /**
17
+ * Custom filename generation function.
18
+ * Return the filename **stem** (without extension) — the plugin appends the
19
+ * correct extension based on format conversion settings.
20
+ *
21
+ * Built-in strategies: `uuidFilename`, `seoFilename`
22
+ */
23
+ export type GenerateFilename = (args: GenerateFilenameArgs) => string;
3
24
  export type FormatQuality = {
4
25
  format: ImageFormat;
5
26
  quality: number;
@@ -21,16 +42,40 @@ export type ImageOptimizerConfig = {
21
42
  disabled?: boolean;
22
43
  fieldsOverride?: FieldsOverride;
23
44
  formats?: FormatQuality[];
45
+ /** Custom filename generation strategy. Return the filename **stem** (no extension).
46
+ * The plugin appends the correct extension based on format conversion settings.
47
+ *
48
+ * Built-in strategies:
49
+ * - `uuidFilename` — UUID-based, collision-free (same as `uniqueFileNames: true`)
50
+ * - `seoFilename` — Human-readable from alt text + timestamp
51
+ *
52
+ * When set, `uniqueFileNames` is ignored.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { imageOptimizer, seoFilename } from '@inoo-ch/payload-image-optimizer'
57
+ *
58
+ * imageOptimizer({
59
+ * collections: { media: true },
60
+ * generateFilename: seoFilename,
61
+ * })
62
+ * ```
63
+ */
64
+ generateFilename?: GenerateFilename;
24
65
  generateThumbHash?: boolean;
25
66
  maxDimensions?: {
26
67
  width: number;
27
68
  height: number;
28
69
  };
70
+ /** Show the "Regenerate All Images" button in the collection list view.
71
+ * Defaults to `true`. */
72
+ regenerateButton?: boolean;
29
73
  replaceOriginal?: boolean;
30
74
  stripMetadata?: boolean;
31
75
  /** Replace original filenames with UUIDs (e.g., `photo.jpg` → `a1b2c3d4.webp`).
32
76
  * Prevents Vercel Blob "already exists" errors and avoids leaking original filenames.
33
- * Defaults to `false`. */
77
+ * Defaults to `false`.
78
+ * @deprecated Use `generateFilename: uuidFilename` instead. */
34
79
  uniqueFileNames?: boolean;
35
80
  };
36
81
  export type ResolvedCollectionOptimizerConfig = {
@@ -45,8 +90,10 @@ export type ResolvedImageOptimizerConfig = Required<Pick<ImageOptimizerConfig, '
45
90
  clientOptimization: boolean;
46
91
  collections: ImageOptimizerConfig['collections'];
47
92
  disabled: boolean;
93
+ /** Resolved filename generator. `undefined` means keep original filename. */
94
+ generateFilename?: GenerateFilename;
95
+ regenerateButton: boolean;
48
96
  replaceOriginal: boolean;
49
- uniqueFileNames: boolean;
50
97
  };
51
98
  export type ImageOptimizerData = {
52
99
  thumbHash?: string | null;
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug, Field } 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 FieldsOverride = (args: { defaultFields: Field[] }) => Field[]\n\nexport type ImageOptimizerConfig = {\n clientOptimization?: boolean\n collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>\n disabled?: boolean\n fieldsOverride?: FieldsOverride\n formats?: FormatQuality[]\n generateThumbHash?: boolean\n maxDimensions?: { width: number; height: number }\n replaceOriginal?: boolean\n stripMetadata?: boolean\n /** Replace original filenames with UUIDs (e.g., `photo.jpg` → `a1b2c3d4.webp`).\n * Prevents Vercel Blob \"already exists\" errors and avoids leaking original filenames.\n * Defaults to `false`. */\n uniqueFileNames?: 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 clientOptimization: boolean\n collections: ImageOptimizerConfig['collections']\n disabled: boolean\n replaceOriginal: boolean\n uniqueFileNames: boolean\n}\n\nexport type ImageOptimizerData = {\n thumbHash?: string | null\n}\n\nexport type MediaSizeVariant = {\n url?: string | null\n width?: number | null\n height?: number | null\n mimeType?: string | null\n filesize?: number | null\n filename?: string | null\n}\n\nexport type MediaResource = {\n url?: string | null\n alt?: string | null\n width?: number | null\n height?: number | null\n filename?: string | null\n focalX?: number | null\n focalY?: number | null\n imageOptimizer?: ImageOptimizerData | null\n updatedAt?: string\n sizes?: Record<string, MediaSizeVariant | undefined>\n}\n"],"names":[],"mappings":"AA8DA,WAWC"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug, Field } from 'payload'\n\nexport type ImageFormat = 'webp' | 'avif'\n\nexport type GenerateFilenameArgs = {\n /** Alt text from the document (if the collection has an `alt` field) */\n altText?: string\n /** Original uploaded filename (e.g., \"IMG_2847.jpg\") */\n originalFilename: string\n /** The MIME type (e.g., \"image/jpeg\") */\n mimeType: string\n /** The collection slug this file belongs to */\n collectionSlug: string\n /** Existing filename from a previous upload (set on re-uploads / focal point changes).\n * Strategies should typically reuse this to avoid cloud storage churn. */\n existingFilename?: string\n}\n\n/**\n * Custom filename generation function.\n * Return the filename **stem** (without extension) — the plugin appends the\n * correct extension based on format conversion settings.\n *\n * Built-in strategies: `uuidFilename`, `seoFilename`\n */\nexport type GenerateFilename = (args: GenerateFilenameArgs) => string\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 FieldsOverride = (args: { defaultFields: Field[] }) => Field[]\n\nexport type ImageOptimizerConfig = {\n clientOptimization?: boolean\n collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>\n disabled?: boolean\n fieldsOverride?: FieldsOverride\n formats?: FormatQuality[]\n /** Custom filename generation strategy. Return the filename **stem** (no extension).\n * The plugin appends the correct extension based on format conversion settings.\n *\n * Built-in strategies:\n * - `uuidFilename` — UUID-based, collision-free (same as `uniqueFileNames: true`)\n * - `seoFilename` — Human-readable from alt text + timestamp\n *\n * When set, `uniqueFileNames` is ignored.\n *\n * @example\n * ```ts\n * import { imageOptimizer, seoFilename } from '@inoo-ch/payload-image-optimizer'\n *\n * imageOptimizer({\n * collections: { media: true },\n * generateFilename: seoFilename,\n * })\n * ```\n */\n generateFilename?: GenerateFilename\n generateThumbHash?: boolean\n maxDimensions?: { width: number; height: number }\n /** Show the \"Regenerate All Images\" button in the collection list view.\n * Defaults to `true`. */\n regenerateButton?: boolean\n replaceOriginal?: boolean\n stripMetadata?: boolean\n /** Replace original filenames with UUIDs (e.g., `photo.jpg` → `a1b2c3d4.webp`).\n * Prevents Vercel Blob \"already exists\" errors and avoids leaking original filenames.\n * Defaults to `false`.\n * @deprecated Use `generateFilename: uuidFilename` instead. */\n uniqueFileNames?: 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 clientOptimization: boolean\n collections: ImageOptimizerConfig['collections']\n disabled: boolean\n /** Resolved filename generator. `undefined` means keep original filename. */\n generateFilename?: GenerateFilename\n regenerateButton: boolean\n replaceOriginal: boolean\n}\n\nexport type ImageOptimizerData = {\n thumbHash?: string | null\n}\n\nexport type MediaSizeVariant = {\n url?: string | null\n width?: number | null\n height?: number | null\n mimeType?: string | null\n filesize?: number | null\n filename?: string | null\n}\n\nexport type MediaResource = {\n url?: string | null\n alt?: string | null\n width?: number | null\n height?: number | null\n filename?: string | null\n focalX?: number | null\n focalY?: number | null\n imageOptimizer?: ImageOptimizerData | null\n updatedAt?: string\n sizes?: Record<string, MediaSizeVariant | undefined>\n}\n"],"names":[],"mappings":"AA+GA,WAWC"}
@@ -0,0 +1,25 @@
1
+ import type { GenerateFilenameArgs } from '../types.js';
2
+ /**
3
+ * UUID-based filename strategy.
4
+ *
5
+ * Generates collision-free filenames like `a1b2c3d4-e5f6-7890-abcd-ef1234567890`.
6
+ * On re-uploads (focal point / crop changes), reuses the existing filename stem
7
+ * to avoid unnecessary file churn on cloud storage.
8
+ */
9
+ export declare const uuidFilename: ({ existingFilename }: GenerateFilenameArgs) => string;
10
+ /**
11
+ * SEO-friendly filename strategy.
12
+ *
13
+ * Generates human-readable, URL-safe filenames from alt text:
14
+ * "Geländer aus Edelstahl" → `gelander-aus-edelstahl-20260327T120000Z`
15
+ *
16
+ * Processing pipeline:
17
+ * 1. Uses alt text, falls back to original filename stem, then "media"
18
+ * 2. Strips diacritics (ä→a, ö→o, ü→u, é→e)
19
+ * 3. Converts to kebab-case
20
+ * 4. Truncates to 60 characters (clean break, no trailing hyphens)
21
+ * 5. Appends ISO timestamp for uniqueness (YYYYMMDDTHHMMSSmmm)
22
+ *
23
+ * On re-uploads, reuses the existing filename stem to avoid cloud storage churn.
24
+ */
25
+ export declare const seoFilename: ({ altText, existingFilename, originalFilename, }: GenerateFilenameArgs) => string;
@@ -0,0 +1,46 @@
1
+ import crypto from 'crypto';
2
+ import path from 'path';
3
+ import { stripDiacritics } from './stripDiacritics.js';
4
+ import { toKebabCase } from './toKebabCase.js';
5
+ const MAX_STEM_LENGTH = 60;
6
+ /**
7
+ * UUID-based filename strategy.
8
+ *
9
+ * Generates collision-free filenames like `a1b2c3d4-e5f6-7890-abcd-ef1234567890`.
10
+ * On re-uploads (focal point / crop changes), reuses the existing filename stem
11
+ * to avoid unnecessary file churn on cloud storage.
12
+ */ export const uuidFilename = ({ existingFilename })=>{
13
+ if (existingFilename) {
14
+ return path.parse(existingFilename).name;
15
+ }
16
+ return crypto.randomUUID();
17
+ };
18
+ /**
19
+ * SEO-friendly filename strategy.
20
+ *
21
+ * Generates human-readable, URL-safe filenames from alt text:
22
+ * "Geländer aus Edelstahl" → `gelander-aus-edelstahl-20260327T120000Z`
23
+ *
24
+ * Processing pipeline:
25
+ * 1. Uses alt text, falls back to original filename stem, then "media"
26
+ * 2. Strips diacritics (ä→a, ö→o, ü→u, é→e)
27
+ * 3. Converts to kebab-case
28
+ * 4. Truncates to 60 characters (clean break, no trailing hyphens)
29
+ * 5. Appends ISO timestamp for uniqueness (YYYYMMDDTHHMMSSmmm)
30
+ *
31
+ * On re-uploads, reuses the existing filename stem to avoid cloud storage churn.
32
+ */ export const seoFilename = ({ altText, existingFilename, originalFilename })=>{
33
+ if (existingFilename) {
34
+ return path.parse(existingFilename).name;
35
+ }
36
+ const source = altText?.trim() || path.parse(originalFilename).name || 'media';
37
+ const slug = toKebabCase(stripDiacritics(source));
38
+ // Truncate cleanly — don't leave a trailing hyphen
39
+ const truncated = slug.length > MAX_STEM_LENGTH ? slug.slice(0, MAX_STEM_LENGTH).replace(/-$/, '') : slug;
40
+ // Append timestamp for uniqueness (ISO-ish, no colons for filesystem safety)
41
+ const now = new Date();
42
+ const timestamp = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
43
+ return `${truncated || 'media'}-${timestamp}`;
44
+ };
45
+
46
+ //# sourceMappingURL=filenameStrategies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/filenameStrategies.ts"],"sourcesContent":["import crypto from 'crypto'\nimport path from 'path'\n\nimport type { GenerateFilenameArgs } from '../types.js'\nimport { stripDiacritics } from './stripDiacritics.js'\nimport { toKebabCase } from './toKebabCase.js'\n\nconst MAX_STEM_LENGTH = 60\n\n/**\n * UUID-based filename strategy.\n *\n * Generates collision-free filenames like `a1b2c3d4-e5f6-7890-abcd-ef1234567890`.\n * On re-uploads (focal point / crop changes), reuses the existing filename stem\n * to avoid unnecessary file churn on cloud storage.\n */\nexport const uuidFilename = ({ existingFilename }: GenerateFilenameArgs): string => {\n if (existingFilename) {\n return path.parse(existingFilename).name\n }\n return crypto.randomUUID()\n}\n\n/**\n * SEO-friendly filename strategy.\n *\n * Generates human-readable, URL-safe filenames from alt text:\n * \"Geländer aus Edelstahl\" → `gelander-aus-edelstahl-20260327T120000Z`\n *\n * Processing pipeline:\n * 1. Uses alt text, falls back to original filename stem, then \"media\"\n * 2. Strips diacritics (ä→a, ö→o, ü→u, é→e)\n * 3. Converts to kebab-case\n * 4. Truncates to 60 characters (clean break, no trailing hyphens)\n * 5. Appends ISO timestamp for uniqueness (YYYYMMDDTHHMMSSmmm)\n *\n * On re-uploads, reuses the existing filename stem to avoid cloud storage churn.\n */\nexport const seoFilename = ({\n altText,\n existingFilename,\n originalFilename,\n}: GenerateFilenameArgs): string => {\n if (existingFilename) {\n return path.parse(existingFilename).name\n }\n\n const source = altText?.trim() || path.parse(originalFilename).name || 'media'\n const slug = toKebabCase(stripDiacritics(source))\n\n // Truncate cleanly — don't leave a trailing hyphen\n const truncated = slug.length > MAX_STEM_LENGTH\n ? slug.slice(0, MAX_STEM_LENGTH).replace(/-$/, '')\n : slug\n\n // Append timestamp for uniqueness (ISO-ish, no colons for filesystem safety)\n const now = new Date()\n const timestamp = now.toISOString().replace(/[-:]/g, '').replace(/\\.\\d{3}Z$/, 'Z')\n\n return `${truncated || 'media'}-${timestamp}`\n}\n"],"names":["crypto","path","stripDiacritics","toKebabCase","MAX_STEM_LENGTH","uuidFilename","existingFilename","parse","name","randomUUID","seoFilename","altText","originalFilename","source","trim","slug","truncated","length","slice","replace","now","Date","timestamp","toISOString"],"mappings":"AAAA,OAAOA,YAAY,SAAQ;AAC3B,OAAOC,UAAU,OAAM;AAGvB,SAASC,eAAe,QAAQ,uBAAsB;AACtD,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,MAAMC,kBAAkB;AAExB;;;;;;CAMC,GACD,OAAO,MAAMC,eAAe,CAAC,EAAEC,gBAAgB,EAAwB;IACrE,IAAIA,kBAAkB;QACpB,OAAOL,KAAKM,KAAK,CAACD,kBAAkBE,IAAI;IAC1C;IACA,OAAOR,OAAOS,UAAU;AAC1B,EAAC;AAED;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,cAAc,CAAC,EAC1BC,OAAO,EACPL,gBAAgB,EAChBM,gBAAgB,EACK;IACrB,IAAIN,kBAAkB;QACpB,OAAOL,KAAKM,KAAK,CAACD,kBAAkBE,IAAI;IAC1C;IAEA,MAAMK,SAASF,SAASG,UAAUb,KAAKM,KAAK,CAACK,kBAAkBJ,IAAI,IAAI;IACvE,MAAMO,OAAOZ,YAAYD,gBAAgBW;IAEzC,mDAAmD;IACnD,MAAMG,YAAYD,KAAKE,MAAM,GAAGb,kBAC5BW,KAAKG,KAAK,CAAC,GAAGd,iBAAiBe,OAAO,CAAC,MAAM,MAC7CJ;IAEJ,6EAA6E;IAC7E,MAAMK,MAAM,IAAIC;IAChB,MAAMC,YAAYF,IAAIG,WAAW,GAAGJ,OAAO,CAAC,SAAS,IAAIA,OAAO,CAAC,aAAa;IAE9E,OAAO,GAAGH,aAAa,QAAQ,CAAC,EAAEM,WAAW;AAC/C,EAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Strip diacritics (combining marks) from a string using Unicode NFKD normalization.
3
+ *
4
+ * Examples: ä→a, ö→o, ü→u, é→e, ñ→n
5
+ *
6
+ * Note: This maps ä→a (not ä→ae). For German, the ae/oe/ue transliteration
7
+ * is sometimes preferred for SEO — but plain ASCII is simpler and works well for URLs.
8
+ */
9
+ export declare const stripDiacritics: (input: string) => string;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Strip diacritics (combining marks) from a string using Unicode NFKD normalization.
3
+ *
4
+ * Examples: ä→a, ö→o, ü→u, é→e, ñ→n
5
+ *
6
+ * Note: This maps ä→a (not ä→ae). For German, the ae/oe/ue transliteration
7
+ * is sometimes preferred for SEO — but plain ASCII is simpler and works well for URLs.
8
+ */ export const stripDiacritics = (input)=>input.normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
9
+
10
+ //# sourceMappingURL=stripDiacritics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/stripDiacritics.ts"],"sourcesContent":["/**\n * Strip diacritics (combining marks) from a string using Unicode NFKD normalization.\n *\n * Examples: ä→a, ö→o, ü→u, é→e, ñ→n\n *\n * Note: This maps ä→a (not ä→ae). For German, the ae/oe/ue transliteration\n * is sometimes preferred for SEO — but plain ASCII is simpler and works well for URLs.\n */\nexport const stripDiacritics = (input: string): string =>\n input.normalize('NFKD').replace(/[\\u0300-\\u036f]/g, '')\n"],"names":["stripDiacritics","input","normalize","replace"],"mappings":"AAAA;;;;;;;CAOC,GACD,OAAO,MAAMA,kBAAkB,CAACC,QAC9BA,MAAMC,SAAS,CAAC,QAAQC,OAAO,CAAC,oBAAoB,IAAG"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Convert a string to kebab-case.
3
+ *
4
+ * - Lowercases
5
+ * - Replaces whitespace, underscores, and dots with hyphens
6
+ * - Removes non-alphanumeric characters (except hyphens)
7
+ * - Collapses consecutive hyphens
8
+ * - Trims leading/trailing hyphens
9
+ */
10
+ export declare const toKebabCase: (input: string) => string;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Convert a string to kebab-case.
3
+ *
4
+ * - Lowercases
5
+ * - Replaces whitespace, underscores, and dots with hyphens
6
+ * - Removes non-alphanumeric characters (except hyphens)
7
+ * - Collapses consecutive hyphens
8
+ * - Trims leading/trailing hyphens
9
+ */ export const toKebabCase = (input)=>input.toLowerCase().replace(/[\s_.]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-{2,}/g, '-').replace(/^-|-$/g, '');
10
+
11
+ //# sourceMappingURL=toKebabCase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/toKebabCase.ts"],"sourcesContent":["/**\n * Convert a string to kebab-case.\n *\n * - Lowercases\n * - Replaces whitespace, underscores, and dots with hyphens\n * - Removes non-alphanumeric characters (except hyphens)\n * - Collapses consecutive hyphens\n * - Trims leading/trailing hyphens\n */\nexport const toKebabCase = (input: string): string =>\n input\n .toLowerCase()\n .replace(/[\\s_.]+/g, '-')\n .replace(/[^a-z0-9-]/g, '')\n .replace(/-{2,}/g, '-')\n .replace(/^-|-$/g, '')\n"],"names":["toKebabCase","input","toLowerCase","replace"],"mappings":"AAAA;;;;;;;;CAQC,GACD,OAAO,MAAMA,cAAc,CAACC,QAC1BA,MACGC,WAAW,GACXC,OAAO,CAAC,YAAY,KACpBA,OAAO,CAAC,eAAe,IACvBA,OAAO,CAAC,UAAU,KAClBA,OAAO,CAAC,UAAU,IAAG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoo-ch/payload-image-optimizer",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Payload CMS plugin for automatic image optimization — WebP/AVIF conversion, resize, EXIF strip, ThumbHash placeholders, and bulk regeneration",
5
5
  "license": "MIT",
6
6
  "keywords": [