@inoo-ch/payload-image-optimizer 1.5.1 → 1.7.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.
- package/AGENT_DOCS.md +207 -38
- package/README.md +80 -19
- package/dist/components/ImageBox.js +8 -3
- package/dist/components/ImageBox.js.map +1 -1
- package/dist/defaults.js +2 -1
- package/dist/defaults.js.map +1 -1
- package/dist/exports/client.d.ts +3 -0
- package/dist/exports/client.js +2 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/hooks/beforeChange.js +10 -0
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tasks/regenerateDocument.js +7 -0
- package/dist/tasks/regenerateDocument.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/getOptimizedImageProps.d.ts +44 -0
- package/dist/utilities/getOptimizedImageProps.js +43 -0
- package/dist/utilities/getOptimizedImageProps.js.map +1 -0
- package/dist/utilities/responsiveImage.d.ts +46 -0
- package/dist/utilities/responsiveImage.js +64 -0
- package/dist/utilities/responsiveImage.js.map +1 -0
- package/package.json +1 -1
- package/src/components/ImageBox.tsx +6 -3
- package/src/defaults.ts +1 -0
- package/src/exports/client.ts +3 -0
- package/src/hooks/beforeChange.ts +11 -0
- package/src/index.ts +1 -1
- package/src/tasks/regenerateDocument.ts +8 -0
- package/src/types.ts +15 -0
- package/src/utilities/getOptimizedImageProps.ts +53 -0
- package/src/utilities/responsiveImage.ts +91 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/beforeChange.ts"],"sourcesContent":["import path from 'path'\nimport type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { convertFormat, generateThumbHash, stripAndResize } from '../processing/index.js'\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 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","req","imageOptimizer_skip","file","mimetype","startsWith","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","finalBuffer","buffer","finalSize","size","replaceOriginal","formats","primaryFormat","converted","format","quality","originalFilename","
|
|
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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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Config } from 'payload';
|
|
2
2
|
import type { ImageOptimizerConfig } from './types.js';
|
|
3
|
-
export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, FieldsOverride } from './types.js';
|
|
3
|
+
export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, MediaSizeVariant, FieldsOverride } from './types.js';
|
|
4
4
|
export { defaultImageOptimizerFields } from './fields/imageOptimizerField.js';
|
|
5
5
|
export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js';
|
|
6
6
|
/**
|
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, 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 } 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":"AACA,SAASA,eAAe,QAAQ,iBAAgB;AAGhD,SAASC,aAAa,QAAQ,gBAAe;AAC7C,SAASC,YAAY,QAAQ,0BAAyB;AACtD,SAASC,sBAAsB,QAAQ,kCAAiC;AACxE,SAASC,sBAAsB,QAAQ,0BAAyB;AAChE,SAASC,qBAAqB,QAAQ,yBAAwB;AAC9D,SAASC,2BAA2B,QAAQ,4BAA2B;AACvE,SAASC,+BAA+B,QAAQ,gCAA+B;AAC/E,SAASC,uBAAuB,EAAEC,6BAA6B,QAAQ,4BAA2B;AAGlG,SAASC,2BAA2B,QAAQ,kCAAiC;AAE7E,SAASC,sBAAsB,EAAEC,wBAAwB,QAAQ,2BAA0B;AAE3F;;;;;CAKC,GACD,OAAO,MAAMC,cAAc,GAAE;AAE7B,OAAO,MAAMC,iBACX,CAACC,gBACD,CAACC;QACC,MAAMC,iBAAiBhB,cAAcc;QACrC,MAAMG,cAAcC,OAAOC,IAAI,CAACH,eAAeI,WAAW;QAE1D,0EAA0E;QAC1E,MAAMA,cAAc,AAACL,CAAAA,OAAOK,WAAW,IAAI,EAAE,AAAD,EAAGC,GAAG,CAAC,CAACC;YAClD,IAAI,CAACL,YAAYM,QAAQ,CAACD,WAAWE,IAAI,GAAG;gBAC1C,OAAOF;YACT;YAEA,mEAAmE;YACnE,MAAMG,SAAS;mBAAIH,WAAWG,MAAM;gBAAEvB,uBAAuBY,cAAcY,cAAc;aAAE;YAE3F,IAAIV,eAAeW,QAAQ,EAAE;gBAC3B,OAAO;oBAAE,GAAGL,UAAU;oBAAEG;gBAAO;YACjC;YAEA,OAAO;gBACL,GAAGH,UAAU;gBACbG;gBACAG,OAAO;oBACL,GAAGN,WAAWM,KAAK;oBACnBC,cAAc;2BACRP,WAAWM,KAAK,EAAEC,gBAAgB,EAAE;wBACxC1B,uBAAuBa,gBAAgBM,WAAWE,IAAI;qBACvD;oBACDM,aAAa;2BACPR,WAAWM,KAAK,EAAEE,eAAe,EAAE;wBACvC1B,sBAAsBY,gBAAgBM,WAAWE,IAAI;qBACtD;gBACH;gBACAO,OAAO;oBACL,GAAGT,WAAWS,KAAK;oBACnBC,YAAY;wBACV,GAAGV,WAAWS,KAAK,EAAEC,UAAU;wBAC/B,GAAIhB,eAAeiB,kBAAkB,IAAI,CAACX,WAAWS,KAAK,EAAEC,YAAYE,MAAMC,SAC1E;4BACED,MAAM;gCACJ,GAAGZ,WAAWS,KAAK,EAAEC,YAAYE,IAAI;gCACrCC,QAAQ;4BACV;wBACF,IACA,CAAC,CAAC;wBACNC,iBAAiB;+BACXd,WAAWS,KAAK,EAAEC,YAAYI,mBAAmB,EAAE;4BACvD;yBACD;oBACH;gBACF;YACF;QACF;QAEA,MAAMC,OAAO;YACX,GAAGtB,OAAOsB,IAAI;YACdpC,cAAcF,gBAAgBE,cAAcc,OAAOsB,IAAI,EAAEpC,gBAAgB,CAAC;QAC5E;QAEA,kEAAkE;QAClE,IAAIe,eAAeW,QAAQ,EAAE;YAC3B,OAAO;gBAAE,GAAGZ,MAAM;gBAAEK;gBAAaiB;YAAK;QACxC;QAEA,OAAO;YACL,GAAGtB,MAAM;YACTK;YACAiB;YACAC,MAAM;gBACJ,GAAGvB,OAAOuB,IAAI;gBACdC,OAAO;uBACDxB,OAAOuB,IAAI,EAAEC,SAAS,EAAE;oBAC5B;wBACEf,MAAM;wBACNgB,aAAa;4BACX;gCAAEC,MAAM;gCAAkBC,MAAM;gCAAQC,UAAU;4BAAK;4BACvD;gCAAEF,MAAM;gCAASC,MAAM;gCAAQC,UAAU;4BAAK;yBAC/C;wBACDC,cAAc;4BACZ;gCAAEH,MAAM;gCAAqBC,MAAM;4BAAS;yBAC7C;wBACDG,SAAS;wBACTC,SAASzC,4BAA4BW;oBACvC;oBACA;wBACEQ,MAAM;wBACNgB,aAAa;4BACX;gCAAEC,MAAM;gCAAkBC,MAAM;gCAAQC,UAAU;4BAAK;4BACvD;gCAAEF,MAAM;gCAASC,MAAM;gCAAQC,UAAU;4BAAK;yBAC/C;wBACDC,cAAc;4BACZ;gCAAEH,MAAM;gCAAUC,MAAM;4BAAO;4BAC/B;gCAAED,MAAM;gCAAUC,MAAM;4BAAO;yBAChC;wBACDG,SAAS;wBACTC,SAASxC,gCAAgCU;oBAC3C;iBACD;YACH;YACA+B,WAAW;mBACLhC,OAAOgC,SAAS,IAAI,EAAE;gBAC1B;oBACEC,MAAM;oBACNC,QAAQ;oBACRH,SAASvC,wBAAwBS;gBACnC;gBACA;oBACEgC,MAAM;oBACNC,QAAQ;oBACRH,SAAStC,8BAA8BQ;gBACzC;aACD;QACH;IACF,EAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
1
2
|
import fs from 'fs/promises';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { resolveCollectionConfig } from '../defaults.js';
|
|
@@ -52,6 +53,12 @@ export const createRegenerateDocumentHandler = (resolvedConfig)=>{
|
|
|
52
53
|
if (cloudStorage) {
|
|
53
54
|
// Cloud storage: re-upload the optimized file via Payload's update API.
|
|
54
55
|
// This triggers the cloud adapter's afterChange hook which uploads to cloud.
|
|
56
|
+
// When uniqueFileNames is enabled, generate a new UUID filename to avoid
|
|
57
|
+
// Vercel Blob "already exists" errors (the adapter doesn't support allowOverwrite).
|
|
58
|
+
if (resolvedConfig.uniqueFileNames) {
|
|
59
|
+
const ext = path.extname(newFilename);
|
|
60
|
+
newFilename = `${crypto.randomUUID()}${ext}`;
|
|
61
|
+
}
|
|
55
62
|
const updateData = {
|
|
56
63
|
imageOptimizer: {
|
|
57
64
|
originalSize,
|
|
@@ -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'\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 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","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","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 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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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export type ImageOptimizerConfig = {
|
|
|
28
28
|
};
|
|
29
29
|
replaceOriginal?: boolean;
|
|
30
30
|
stripMetadata?: boolean;
|
|
31
|
+
/** Replace original filenames with UUIDs (e.g., `photo.jpg` → `a1b2c3d4.webp`).
|
|
32
|
+
* Prevents Vercel Blob "already exists" errors and avoids leaking original filenames.
|
|
33
|
+
* Defaults to `false`. */
|
|
34
|
+
uniqueFileNames?: boolean;
|
|
31
35
|
};
|
|
32
36
|
export type ResolvedCollectionOptimizerConfig = {
|
|
33
37
|
formats: FormatQuality[];
|
|
@@ -42,10 +46,19 @@ export type ResolvedImageOptimizerConfig = Required<Pick<ImageOptimizerConfig, '
|
|
|
42
46
|
collections: ImageOptimizerConfig['collections'];
|
|
43
47
|
disabled: boolean;
|
|
44
48
|
replaceOriginal: boolean;
|
|
49
|
+
uniqueFileNames: boolean;
|
|
45
50
|
};
|
|
46
51
|
export type ImageOptimizerData = {
|
|
47
52
|
thumbHash?: string | null;
|
|
48
53
|
};
|
|
54
|
+
export type MediaSizeVariant = {
|
|
55
|
+
url?: string | null;
|
|
56
|
+
width?: number | null;
|
|
57
|
+
height?: number | null;
|
|
58
|
+
mimeType?: string | null;
|
|
59
|
+
filesize?: number | null;
|
|
60
|
+
filename?: string | null;
|
|
61
|
+
};
|
|
49
62
|
export type MediaResource = {
|
|
50
63
|
url?: string | null;
|
|
51
64
|
alt?: string | null;
|
|
@@ -56,4 +69,5 @@ export type MediaResource = {
|
|
|
56
69
|
focalY?: number | null;
|
|
57
70
|
imageOptimizer?: ImageOptimizerData | null;
|
|
58
71
|
updatedAt?: string;
|
|
72
|
+
sizes?: Record<string, MediaSizeVariant | undefined>;
|
|
59
73
|
};
|
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}\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}\n\nexport type ImageOptimizerData = {\n thumbHash?: 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}\n"],"names":[],"mappings":"
|
|
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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { MediaResource } from '../types.js';
|
|
2
|
+
import { type ImageOptimizerProps } from './getImageOptimizerProps.js';
|
|
3
|
+
type ImageLoaderProps = {
|
|
4
|
+
src: string;
|
|
5
|
+
width: number;
|
|
6
|
+
quality?: number | undefined;
|
|
7
|
+
};
|
|
8
|
+
type ImageLoader = (props: ImageLoaderProps) => string;
|
|
9
|
+
export type OptimizedImageProps = ImageOptimizerProps & {
|
|
10
|
+
loader?: ImageLoader;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Returns all optimization props for a Next.js `<Image>` component in a single
|
|
14
|
+
* spread-friendly object: ThumbHash blur placeholder, focal-point positioning,
|
|
15
|
+
* and a variant-aware responsive loader.
|
|
16
|
+
*
|
|
17
|
+
* Designed as a drop-in enhancement for the Payload website template's `ImageMedia`:
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* // In your ImageMedia component — just add the import and spread:
|
|
21
|
+
* import { getOptimizedImageProps } from '@inoo-ch/payload-image-optimizer/client'
|
|
22
|
+
*
|
|
23
|
+
* const optimizedProps = getOptimizedImageProps(resource)
|
|
24
|
+
*
|
|
25
|
+
* <NextImage
|
|
26
|
+
* {...optimizedProps}
|
|
27
|
+
* src={src}
|
|
28
|
+
* alt={alt}
|
|
29
|
+
* fill={fill}
|
|
30
|
+
* sizes={sizes}
|
|
31
|
+
* priority={priority}
|
|
32
|
+
* loading={loading}
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* What it returns:
|
|
37
|
+
* - `placeholder` / `blurDataURL` — per-image ThumbHash (replaces the template's hardcoded blur)
|
|
38
|
+
* - `style.objectPosition` — focal-point-based positioning
|
|
39
|
+
* - `loader` — hybrid loader that serves pre-generated Payload size variants directly,
|
|
40
|
+
* falling back to `/_next/image` when no close match exists (only present when
|
|
41
|
+
* `resource.sizes` has variants)
|
|
42
|
+
*/
|
|
43
|
+
export declare function getOptimizedImageProps(resource: MediaResource | null | undefined): OptimizedImageProps;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getImageOptimizerProps } from './getImageOptimizerProps.js';
|
|
2
|
+
import { createVariantLoader } from './responsiveImage.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns all optimization props for a Next.js `<Image>` component in a single
|
|
5
|
+
* spread-friendly object: ThumbHash blur placeholder, focal-point positioning,
|
|
6
|
+
* and a variant-aware responsive loader.
|
|
7
|
+
*
|
|
8
|
+
* Designed as a drop-in enhancement for the Payload website template's `ImageMedia`:
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // In your ImageMedia component — just add the import and spread:
|
|
12
|
+
* import { getOptimizedImageProps } from '@inoo-ch/payload-image-optimizer/client'
|
|
13
|
+
*
|
|
14
|
+
* const optimizedProps = getOptimizedImageProps(resource)
|
|
15
|
+
*
|
|
16
|
+
* <NextImage
|
|
17
|
+
* {...optimizedProps}
|
|
18
|
+
* src={src}
|
|
19
|
+
* alt={alt}
|
|
20
|
+
* fill={fill}
|
|
21
|
+
* sizes={sizes}
|
|
22
|
+
* priority={priority}
|
|
23
|
+
* loading={loading}
|
|
24
|
+
* />
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* What it returns:
|
|
28
|
+
* - `placeholder` / `blurDataURL` — per-image ThumbHash (replaces the template's hardcoded blur)
|
|
29
|
+
* - `style.objectPosition` — focal-point-based positioning
|
|
30
|
+
* - `loader` — hybrid loader that serves pre-generated Payload size variants directly,
|
|
31
|
+
* falling back to `/_next/image` when no close match exists (only present when
|
|
32
|
+
* `resource.sizes` has variants)
|
|
33
|
+
*/ export function getOptimizedImageProps(resource) {
|
|
34
|
+
const base = getImageOptimizerProps(resource);
|
|
35
|
+
if (!resource) return base;
|
|
36
|
+
const loader = createVariantLoader(resource);
|
|
37
|
+
return loader ? {
|
|
38
|
+
...base,
|
|
39
|
+
loader
|
|
40
|
+
} : base;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=getOptimizedImageProps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/getOptimizedImageProps.ts"],"sourcesContent":["import type { MediaResource } from '../types.js'\nimport { getImageOptimizerProps, type ImageOptimizerProps } from './getImageOptimizerProps.js'\nimport { createVariantLoader } from './responsiveImage.js'\n\ntype ImageLoaderProps = { src: string; width: number; quality?: number | undefined }\ntype ImageLoader = (props: ImageLoaderProps) => string\n\nexport type OptimizedImageProps = ImageOptimizerProps & {\n loader?: ImageLoader\n}\n\n/**\n * Returns all optimization props for a Next.js `<Image>` component in a single\n * spread-friendly object: ThumbHash blur placeholder, focal-point positioning,\n * and a variant-aware responsive loader.\n *\n * Designed as a drop-in enhancement for the Payload website template's `ImageMedia`:\n *\n * ```tsx\n * // In your ImageMedia component — just add the import and spread:\n * import { getOptimizedImageProps } from '@inoo-ch/payload-image-optimizer/client'\n *\n * const optimizedProps = getOptimizedImageProps(resource)\n *\n * <NextImage\n * {...optimizedProps}\n * src={src}\n * alt={alt}\n * fill={fill}\n * sizes={sizes}\n * priority={priority}\n * loading={loading}\n * />\n * ```\n *\n * What it returns:\n * - `placeholder` / `blurDataURL` — per-image ThumbHash (replaces the template's hardcoded blur)\n * - `style.objectPosition` — focal-point-based positioning\n * - `loader` — hybrid loader that serves pre-generated Payload size variants directly,\n * falling back to `/_next/image` when no close match exists (only present when\n * `resource.sizes` has variants)\n */\nexport function getOptimizedImageProps(\n resource: MediaResource | null | undefined,\n): OptimizedImageProps {\n const base = getImageOptimizerProps(resource)\n\n if (!resource) return base\n\n const loader = createVariantLoader(resource)\n\n return loader ? { ...base, loader } : base\n}\n"],"names":["getImageOptimizerProps","createVariantLoader","getOptimizedImageProps","resource","base","loader"],"mappings":"AACA,SAASA,sBAAsB,QAAkC,8BAA6B;AAC9F,SAASC,mBAAmB,QAAQ,uBAAsB;AAS1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASC,uBACdC,QAA0C;IAE1C,MAAMC,OAAOJ,uBAAuBG;IAEpC,IAAI,CAACA,UAAU,OAAOC;IAEtB,MAAMC,SAASJ,oBAAoBE;IAEnC,OAAOE,SAAS;QAAE,GAAGD,IAAI;QAAEC;IAAO,IAAID;AACxC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { MediaResource } from '../types.js';
|
|
2
|
+
type ImageLoaderProps = {
|
|
3
|
+
src: string;
|
|
4
|
+
width: number;
|
|
5
|
+
quality?: number | undefined;
|
|
6
|
+
};
|
|
7
|
+
type ImageLoader = (props: ImageLoaderProps) => string;
|
|
8
|
+
type ValidVariant = {
|
|
9
|
+
url: string;
|
|
10
|
+
width: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Finds the best pre-generated variant for a requested width.
|
|
14
|
+
*
|
|
15
|
+
* Strategy:
|
|
16
|
+
* 1. Pick the smallest variant with width >= requested (no quality loss from upscaling)
|
|
17
|
+
* 2. If none is large enough, use the largest variant — but only if it covers >= 80%
|
|
18
|
+
* of the requested width (minor downscale is acceptable, large gap is not)
|
|
19
|
+
* 3. Returns null when no suitable variant exists → caller should fall back to /_next/image
|
|
20
|
+
*/
|
|
21
|
+
export declare function findBestVariant(variants: ValidVariant[], requestedWidth: number): ValidVariant | null;
|
|
22
|
+
/**
|
|
23
|
+
* Creates a Next.js Image `loader` that maps requested widths to pre-generated
|
|
24
|
+
* Payload size variants when a close match exists, falling back to the default
|
|
25
|
+
* `/_next/image` optimization pipeline when no suitable variant is available.
|
|
26
|
+
*
|
|
27
|
+
* Returns `undefined` when the media has no usable size variants (i.e. no custom
|
|
28
|
+
* loader needed — let next/image use its default behavior).
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* import { createVariantLoader } from '@inoo-ch/payload-image-optimizer/client'
|
|
32
|
+
*
|
|
33
|
+
* const loader = createVariantLoader(media)
|
|
34
|
+
* <NextImage loader={loader} src={media.url} ... />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function createVariantLoader(media: MediaResource): ImageLoader | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Returns a sensible default `sizes` attribute for responsive images.
|
|
40
|
+
*
|
|
41
|
+
* For `fill` mode images without an explicit `sizes` prop, this prevents the
|
|
42
|
+
* browser from assuming `100vw` (which causes it to always download the
|
|
43
|
+
* largest srcSet variant regardless of actual display area).
|
|
44
|
+
*/
|
|
45
|
+
export declare function getDefaultSizes(fill: boolean | undefined): string | undefined;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts usable variants from a Payload media resource's `sizes` field.
|
|
3
|
+
* Filters out entries missing url or width and sorts by width ascending.
|
|
4
|
+
*/ function getValidVariants(media) {
|
|
5
|
+
if (!media.sizes) return [];
|
|
6
|
+
return Object.values(media.sizes).filter((v)=>v != null && typeof v.url === 'string' && typeof v.width === 'number').sort((a, b)=>a.width - b.width);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Finds the best pre-generated variant for a requested width.
|
|
10
|
+
*
|
|
11
|
+
* Strategy:
|
|
12
|
+
* 1. Pick the smallest variant with width >= requested (no quality loss from upscaling)
|
|
13
|
+
* 2. If none is large enough, use the largest variant — but only if it covers >= 80%
|
|
14
|
+
* of the requested width (minor downscale is acceptable, large gap is not)
|
|
15
|
+
* 3. Returns null when no suitable variant exists → caller should fall back to /_next/image
|
|
16
|
+
*/ export function findBestVariant(variants, requestedWidth) {
|
|
17
|
+
if (variants.length === 0) return null;
|
|
18
|
+
// Smallest variant >= requested width
|
|
19
|
+
const larger = variants.find((v)=>v.width >= requestedWidth);
|
|
20
|
+
if (larger) return larger;
|
|
21
|
+
// No variant large enough — use the largest if it's close
|
|
22
|
+
const largest = variants[variants.length - 1];
|
|
23
|
+
if (largest.width >= requestedWidth * 0.8) return largest;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Creates a Next.js Image `loader` that maps requested widths to pre-generated
|
|
28
|
+
* Payload size variants when a close match exists, falling back to the default
|
|
29
|
+
* `/_next/image` optimization pipeline when no suitable variant is available.
|
|
30
|
+
*
|
|
31
|
+
* Returns `undefined` when the media has no usable size variants (i.e. no custom
|
|
32
|
+
* loader needed — let next/image use its default behavior).
|
|
33
|
+
*
|
|
34
|
+
* ```tsx
|
|
35
|
+
* import { createVariantLoader } from '@inoo-ch/payload-image-optimizer/client'
|
|
36
|
+
*
|
|
37
|
+
* const loader = createVariantLoader(media)
|
|
38
|
+
* <NextImage loader={loader} src={media.url} ... />
|
|
39
|
+
* ```
|
|
40
|
+
*/ export function createVariantLoader(media) {
|
|
41
|
+
const variants = getValidVariants(media);
|
|
42
|
+
if (variants.length === 0) return undefined;
|
|
43
|
+
const cacheBust = media.updatedAt ? `?${media.updatedAt}` : '';
|
|
44
|
+
return ({ src, width, quality })=>{
|
|
45
|
+
const match = findBestVariant(variants, width);
|
|
46
|
+
if (match) {
|
|
47
|
+
return `${match.url}${cacheBust}`;
|
|
48
|
+
}
|
|
49
|
+
// Fall back to next/image optimization for unmatched widths
|
|
50
|
+
return `/_next/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 80}`;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns a sensible default `sizes` attribute for responsive images.
|
|
55
|
+
*
|
|
56
|
+
* For `fill` mode images without an explicit `sizes` prop, this prevents the
|
|
57
|
+
* browser from assuming `100vw` (which causes it to always download the
|
|
58
|
+
* largest srcSet variant regardless of actual display area).
|
|
59
|
+
*/ export function getDefaultSizes(fill) {
|
|
60
|
+
if (!fill) return undefined;
|
|
61
|
+
return '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=responsiveImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/responsiveImage.ts"],"sourcesContent":["import type { MediaResource, MediaSizeVariant } from '../types.js'\n\ntype ImageLoaderProps = { src: string; width: number; quality?: number | undefined }\ntype ImageLoader = (props: ImageLoaderProps) => string\n\ntype ValidVariant = { url: string; width: number }\n\n/**\n * Extracts usable variants from a Payload media resource's `sizes` field.\n * Filters out entries missing url or width and sorts by width ascending.\n */\nfunction getValidVariants(media: MediaResource): ValidVariant[] {\n if (!media.sizes) return []\n\n return Object.values(media.sizes)\n .filter((v): v is MediaSizeVariant & { url: string; width: number } =>\n v != null && typeof v.url === 'string' && typeof v.width === 'number',\n )\n .sort((a, b) => a.width - b.width)\n}\n\n/**\n * Finds the best pre-generated variant for a requested width.\n *\n * Strategy:\n * 1. Pick the smallest variant with width >= requested (no quality loss from upscaling)\n * 2. If none is large enough, use the largest variant — but only if it covers >= 80%\n * of the requested width (minor downscale is acceptable, large gap is not)\n * 3. Returns null when no suitable variant exists → caller should fall back to /_next/image\n */\nexport function findBestVariant(\n variants: ValidVariant[],\n requestedWidth: number,\n): ValidVariant | null {\n if (variants.length === 0) return null\n\n // Smallest variant >= requested width\n const larger = variants.find((v) => v.width >= requestedWidth)\n if (larger) return larger\n\n // No variant large enough — use the largest if it's close\n const largest = variants[variants.length - 1]!\n if (largest.width >= requestedWidth * 0.8) return largest\n\n return null\n}\n\n/**\n * Creates a Next.js Image `loader` that maps requested widths to pre-generated\n * Payload size variants when a close match exists, falling back to the default\n * `/_next/image` optimization pipeline when no suitable variant is available.\n *\n * Returns `undefined` when the media has no usable size variants (i.e. no custom\n * loader needed — let next/image use its default behavior).\n *\n * ```tsx\n * import { createVariantLoader } from '@inoo-ch/payload-image-optimizer/client'\n *\n * const loader = createVariantLoader(media)\n * <NextImage loader={loader} src={media.url} ... />\n * ```\n */\nexport function createVariantLoader(media: MediaResource): ImageLoader | undefined {\n const variants = getValidVariants(media)\n if (variants.length === 0) return undefined\n\n const cacheBust = media.updatedAt ? `?${media.updatedAt}` : ''\n\n return ({ src, width, quality }) => {\n const match = findBestVariant(variants, width)\n\n if (match) {\n return `${match.url}${cacheBust}`\n }\n\n // Fall back to next/image optimization for unmatched widths\n return `/_next/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 80}`\n }\n}\n\n/**\n * Returns a sensible default `sizes` attribute for responsive images.\n *\n * For `fill` mode images without an explicit `sizes` prop, this prevents the\n * browser from assuming `100vw` (which causes it to always download the\n * largest srcSet variant regardless of actual display area).\n */\nexport function getDefaultSizes(fill: boolean | undefined): string | undefined {\n if (!fill) return undefined\n return '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'\n}\n"],"names":["getValidVariants","media","sizes","Object","values","filter","v","url","width","sort","a","b","findBestVariant","variants","requestedWidth","length","larger","find","largest","createVariantLoader","undefined","cacheBust","updatedAt","src","quality","match","encodeURIComponent","getDefaultSizes","fill"],"mappings":"AAOA;;;CAGC,GACD,SAASA,iBAAiBC,KAAoB;IAC5C,IAAI,CAACA,MAAMC,KAAK,EAAE,OAAO,EAAE;IAE3B,OAAOC,OAAOC,MAAM,CAACH,MAAMC,KAAK,EAC7BG,MAAM,CAAC,CAACC,IACPA,KAAK,QAAQ,OAAOA,EAAEC,GAAG,KAAK,YAAY,OAAOD,EAAEE,KAAK,KAAK,UAE9DC,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEF,KAAK,GAAGG,EAAEH,KAAK;AACrC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASI,gBACdC,QAAwB,EACxBC,cAAsB;IAEtB,IAAID,SAASE,MAAM,KAAK,GAAG,OAAO;IAElC,sCAAsC;IACtC,MAAMC,SAASH,SAASI,IAAI,CAAC,CAACX,IAAMA,EAAEE,KAAK,IAAIM;IAC/C,IAAIE,QAAQ,OAAOA;IAEnB,0DAA0D;IAC1D,MAAME,UAAUL,QAAQ,CAACA,SAASE,MAAM,GAAG,EAAE;IAC7C,IAAIG,QAAQV,KAAK,IAAIM,iBAAiB,KAAK,OAAOI;IAElD,OAAO;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,oBAAoBlB,KAAoB;IACtD,MAAMY,WAAWb,iBAAiBC;IAClC,IAAIY,SAASE,MAAM,KAAK,GAAG,OAAOK;IAElC,MAAMC,YAAYpB,MAAMqB,SAAS,GAAG,CAAC,CAAC,EAAErB,MAAMqB,SAAS,EAAE,GAAG;IAE5D,OAAO,CAAC,EAAEC,GAAG,EAAEf,KAAK,EAAEgB,OAAO,EAAE;QAC7B,MAAMC,QAAQb,gBAAgBC,UAAUL;QAExC,IAAIiB,OAAO;YACT,OAAO,GAAGA,MAAMlB,GAAG,GAAGc,WAAW;QACnC;QAEA,4DAA4D;QAC5D,OAAO,CAAC,iBAAiB,EAAEK,mBAAmBH,KAAK,GAAG,EAAEf,MAAM,GAAG,EAAEgB,WAAW,IAAI;IACpF;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASG,gBAAgBC,IAAyB;IACvD,IAAI,CAACA,MAAM,OAAOR;IAClB,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inoo-ch/payload-image-optimizer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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": [
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react'
|
|
3
|
+
import React, { useMemo, useState } from 'react'
|
|
4
4
|
import NextImage, { type ImageProps } from 'next/image'
|
|
5
5
|
import type { MediaResource } from '../types.js'
|
|
6
6
|
import { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
|
|
7
|
+
import { createVariantLoader, getDefaultSizes } from '../utilities/responsiveImage.js'
|
|
7
8
|
|
|
8
9
|
export interface ImageBoxProps extends Omit<ImageProps, 'src' | 'alt'> {
|
|
9
10
|
media: MediaResource | string
|
|
@@ -44,7 +45,7 @@ export const ImageBox: React.FC<ImageBoxProps> = ({
|
|
|
44
45
|
alt={altFromProps || ''}
|
|
45
46
|
quality={80}
|
|
46
47
|
fill={fill}
|
|
47
|
-
sizes={sizes}
|
|
48
|
+
sizes={sizes ?? getDefaultSizes(fill)}
|
|
48
49
|
style={{ objectFit: 'cover', objectPosition: 'center', ...fadeStyle, ...styleFromProps }}
|
|
49
50
|
priority={priority}
|
|
50
51
|
loading={loading}
|
|
@@ -59,6 +60,7 @@ export const ImageBox: React.FC<ImageBoxProps> = ({
|
|
|
59
60
|
const src = media.url ? `${media.url}${media.updatedAt ? `?${media.updatedAt}` : ''}` : ''
|
|
60
61
|
|
|
61
62
|
const optimizerProps = getImageOptimizerProps(media)
|
|
63
|
+
const variantLoader = useMemo(() => createVariantLoader(media), [media])
|
|
62
64
|
|
|
63
65
|
return (
|
|
64
66
|
<NextImage
|
|
@@ -69,7 +71,8 @@ export const ImageBox: React.FC<ImageBoxProps> = ({
|
|
|
69
71
|
fill={fill}
|
|
70
72
|
width={!fill ? width : undefined}
|
|
71
73
|
height={!fill ? height : undefined}
|
|
72
|
-
sizes={sizes}
|
|
74
|
+
sizes={sizes ?? getDefaultSizes(fill)}
|
|
75
|
+
loader={variantLoader}
|
|
73
76
|
style={{ objectFit: 'cover', ...optimizerProps.style, ...fadeStyle, ...styleFromProps }}
|
|
74
77
|
placeholder={optimizerProps.placeholder}
|
|
75
78
|
blurDataURL={optimizerProps.blurDataURL}
|
package/src/defaults.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const resolveConfig = (config: ImageOptimizerConfig): ResolvedImageOptimi
|
|
|
13
13
|
maxDimensions: config.maxDimensions ?? { width: 2560, height: 2560 },
|
|
14
14
|
replaceOriginal: config.replaceOriginal ?? true,
|
|
15
15
|
stripMetadata: config.stripMetadata ?? true,
|
|
16
|
+
uniqueFileNames: config.uniqueFileNames ?? false,
|
|
16
17
|
})
|
|
17
18
|
|
|
18
19
|
export const resolveCollectionConfig = (
|
package/src/exports/client.ts
CHANGED
|
@@ -5,5 +5,8 @@ export { FadeImage } from '../components/FadeImage.js'
|
|
|
5
5
|
export type { FadeImageProps } from '../components/FadeImage.js'
|
|
6
6
|
export { getImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
|
|
7
7
|
export type { ImageOptimizerProps } from '../utilities/getImageOptimizerProps.js'
|
|
8
|
+
export { getOptimizedImageProps } from '../utilities/getOptimizedImageProps.js'
|
|
9
|
+
export type { OptimizedImageProps } from '../utilities/getOptimizedImageProps.js'
|
|
10
|
+
export { createVariantLoader, getDefaultSizes } from '../utilities/responsiveImage.js'
|
|
8
11
|
export { RegenerationButton } from '../components/RegenerationButton.js'
|
|
9
12
|
export { UploadOptimizer } from '../components/UploadOptimizer.js'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
1
2
|
import path from 'path'
|
|
2
3
|
import type { CollectionBeforeChangeHook } from 'payload'
|
|
3
4
|
|
|
@@ -15,6 +16,16 @@ export const createBeforeChangeHook = (
|
|
|
15
16
|
|
|
16
17
|
if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return data
|
|
17
18
|
|
|
19
|
+
// Rename file to UUID before any processing, so the storage adapter
|
|
20
|
+
// never sees the original filename. Prevents Vercel Blob "already exists"
|
|
21
|
+
// errors and avoids leaking original filenames to storage.
|
|
22
|
+
if (resolvedConfig.uniqueFileNames) {
|
|
23
|
+
const ext = path.extname(req.file.name)
|
|
24
|
+
const uuid = crypto.randomUUID()
|
|
25
|
+
req.file.name = `${uuid}${ext}`
|
|
26
|
+
data.filename = req.file.name
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
const originalSize = req.file.data.length
|
|
19
30
|
|
|
20
31
|
const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { createConvertFormatsHandler } from './tasks/convertFormats.js'
|
|
|
11
11
|
import { createRegenerateDocumentHandler } from './tasks/regenerateDocument.js'
|
|
12
12
|
import { createRegenerateHandler, createRegenerateStatusHandler } from './endpoints/regenerate.js'
|
|
13
13
|
|
|
14
|
-
export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, FieldsOverride } from './types.js'
|
|
14
|
+
export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig, ImageOptimizerData, MediaResource, MediaSizeVariant, FieldsOverride } from './types.js'
|
|
15
15
|
export { defaultImageOptimizerFields } from './fields/imageOptimizerField.js'
|
|
16
16
|
|
|
17
17
|
export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
1
2
|
import fs from 'fs/promises'
|
|
2
3
|
import path from 'path'
|
|
3
4
|
|
|
@@ -74,6 +75,13 @@ export const createRegenerateDocumentHandler = (resolvedConfig: ResolvedImageOpt
|
|
|
74
75
|
if (cloudStorage) {
|
|
75
76
|
// Cloud storage: re-upload the optimized file via Payload's update API.
|
|
76
77
|
// This triggers the cloud adapter's afterChange hook which uploads to cloud.
|
|
78
|
+
// When uniqueFileNames is enabled, generate a new UUID filename to avoid
|
|
79
|
+
// Vercel Blob "already exists" errors (the adapter doesn't support allowOverwrite).
|
|
80
|
+
if (resolvedConfig.uniqueFileNames) {
|
|
81
|
+
const ext = path.extname(newFilename)
|
|
82
|
+
newFilename = `${crypto.randomUUID()}${ext}`
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
const updateData: Record<string, any> = {
|
|
78
86
|
imageOptimizer: {
|
|
79
87
|
originalSize,
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,10 @@ export type ImageOptimizerConfig = {
|
|
|
25
25
|
maxDimensions?: { width: number; height: number }
|
|
26
26
|
replaceOriginal?: boolean
|
|
27
27
|
stripMetadata?: boolean
|
|
28
|
+
/** Replace original filenames with UUIDs (e.g., `photo.jpg` → `a1b2c3d4.webp`).
|
|
29
|
+
* Prevents Vercel Blob "already exists" errors and avoids leaking original filenames.
|
|
30
|
+
* Defaults to `false`. */
|
|
31
|
+
uniqueFileNames?: boolean
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export type ResolvedCollectionOptimizerConfig = {
|
|
@@ -40,12 +44,22 @@ export type ResolvedImageOptimizerConfig = Required<
|
|
|
40
44
|
collections: ImageOptimizerConfig['collections']
|
|
41
45
|
disabled: boolean
|
|
42
46
|
replaceOriginal: boolean
|
|
47
|
+
uniqueFileNames: boolean
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export type ImageOptimizerData = {
|
|
46
51
|
thumbHash?: string | null
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
export type MediaSizeVariant = {
|
|
55
|
+
url?: string | null
|
|
56
|
+
width?: number | null
|
|
57
|
+
height?: number | null
|
|
58
|
+
mimeType?: string | null
|
|
59
|
+
filesize?: number | null
|
|
60
|
+
filename?: string | null
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
export type MediaResource = {
|
|
50
64
|
url?: string | null
|
|
51
65
|
alt?: string | null
|
|
@@ -56,4 +70,5 @@ export type MediaResource = {
|
|
|
56
70
|
focalY?: number | null
|
|
57
71
|
imageOptimizer?: ImageOptimizerData | null
|
|
58
72
|
updatedAt?: string
|
|
73
|
+
sizes?: Record<string, MediaSizeVariant | undefined>
|
|
59
74
|
}
|