@payloadcms/transformer-sharp 4.0.0-canary.38

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 (75) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +105 -0
  3. package/dist/canResizeImage.d.ts +2 -0
  4. package/dist/canResizeImage.d.ts.map +1 -0
  5. package/dist/canResizeImage.js +12 -0
  6. package/dist/canResizeImage.js.map +1 -0
  7. package/dist/createSharpFromFile.d.ts +8 -0
  8. package/dist/createSharpFromFile.d.ts.map +1 -0
  9. package/dist/createSharpFromFile.js +31 -0
  10. package/dist/createSharpFromFile.js.map +1 -0
  11. package/dist/createSharpFromFile.spec.js +120 -0
  12. package/dist/createSharpFromFile.spec.js.map +1 -0
  13. package/dist/getImageResizeAction.d.ts +24 -0
  14. package/dist/getImageResizeAction.d.ts.map +1 -0
  15. package/dist/getImageResizeAction.js +56 -0
  16. package/dist/getImageResizeAction.js.map +1 -0
  17. package/dist/handleRequest.d.ts +14 -0
  18. package/dist/handleRequest.d.ts.map +1 -0
  19. package/dist/handleRequest.js +143 -0
  20. package/dist/handleRequest.js.map +1 -0
  21. package/dist/handleRequest.spec.js +188 -0
  22. package/dist/handleRequest.spec.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +3 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/initSharpCollections.d.ts +14 -0
  28. package/dist/initSharpCollections.d.ts.map +1 -0
  29. package/dist/initSharpCollections.js +84 -0
  30. package/dist/initSharpCollections.js.map +1 -0
  31. package/dist/initSharpCollections.spec.js +185 -0
  32. package/dist/initSharpCollections.spec.js.map +1 -0
  33. package/dist/mapWithBoundedConcurrency.d.ts +7 -0
  34. package/dist/mapWithBoundedConcurrency.d.ts.map +1 -0
  35. package/dist/mapWithBoundedConcurrency.js +26 -0
  36. package/dist/mapWithBoundedConcurrency.js.map +1 -0
  37. package/dist/mapWithBoundedConcurrency.spec.js +35 -0
  38. package/dist/mapWithBoundedConcurrency.spec.js.map +1 -0
  39. package/dist/optionallyAppendMetadata.d.ts +9 -0
  40. package/dist/optionallyAppendMetadata.d.ts.map +1 -0
  41. package/dist/optionallyAppendMetadata.js +17 -0
  42. package/dist/optionallyAppendMetadata.js.map +1 -0
  43. package/dist/parseDynamicResize.d.ts +17 -0
  44. package/dist/parseDynamicResize.d.ts.map +1 -0
  45. package/dist/parseDynamicResize.js +117 -0
  46. package/dist/parseDynamicResize.js.map +1 -0
  47. package/dist/parseDynamicResize.spec.js +123 -0
  48. package/dist/parseDynamicResize.spec.js.map +1 -0
  49. package/dist/prepareLegacyUpload.d.ts +14 -0
  50. package/dist/prepareLegacyUpload.d.ts.map +1 -0
  51. package/dist/prepareLegacyUpload.js +147 -0
  52. package/dist/prepareLegacyUpload.js.map +1 -0
  53. package/dist/prepareLegacyUpload.spec.js +95 -0
  54. package/dist/prepareLegacyUpload.spec.js.map +1 -0
  55. package/dist/sanitizeResizeConfig.d.ts +11 -0
  56. package/dist/sanitizeResizeConfig.d.ts.map +1 -0
  57. package/dist/sanitizeResizeConfig.js +20 -0
  58. package/dist/sanitizeResizeConfig.js.map +1 -0
  59. package/dist/sharpTransformer.d.ts +10 -0
  60. package/dist/sharpTransformer.d.ts.map +1 -0
  61. package/dist/sharpTransformer.js +122 -0
  62. package/dist/sharpTransformer.js.map +1 -0
  63. package/dist/sharpTransformer.spec.js +117 -0
  64. package/dist/sharpTransformer.spec.js.map +1 -0
  65. package/dist/transformFile.d.ts +12 -0
  66. package/dist/transformFile.d.ts.map +1 -0
  67. package/dist/transformFile.js +264 -0
  68. package/dist/transformFile.js.map +1 -0
  69. package/dist/transformFile.spec.js +259 -0
  70. package/dist/transformFile.spec.js.map +1 -0
  71. package/dist/types.d.ts +150 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/types.js +7 -0
  74. package/dist/types.js.map +1 -0
  75. package/package.json +56 -0
@@ -0,0 +1,188 @@
1
+ import { readFileSync } from 'fs';
2
+ import path from 'path';
3
+ import sharp from 'sharp';
4
+ import { fileURLToPath } from 'url';
5
+ import { describe, expect, it, vi } from 'vitest';
6
+ import { createHandleRequest } from './handleRequest.js';
7
+ import { resolveSharpDynamicDefaults } from './sharpTransformer.js';
8
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ // 200x200, 44 frames.
10
+ const animatedWebp = readFileSync(path.resolve(dirname, '../../../test/uploads/animated.webp'));
11
+ const makeReq = ({ query = '' } = {})=>({
12
+ headers: new Headers(),
13
+ method: 'GET',
14
+ payload: {
15
+ logger: {
16
+ error: vi.fn()
17
+ }
18
+ },
19
+ searchParams: new URLSearchParams(query)
20
+ });
21
+ const makeSourceImage = async ({ background = {
22
+ b: 0,
23
+ g: 128,
24
+ r: 255
25
+ }, format = 'png', height = 200, width = 400 } = {})=>sharp({
26
+ create: {
27
+ background,
28
+ channels: 3,
29
+ height,
30
+ width
31
+ }
32
+ }).toFormat(format).toBuffer();
33
+ const resizeReal = async ({ dynamicDefaults = resolveSharpDynamicDefaults(), mimeType = 'image/png', query, sourceBuffer })=>{
34
+ const handleRequest = createHandleRequest({
35
+ dynamicDefaults,
36
+ sharpDependency: sharp
37
+ });
38
+ const getSourceFile = vi.fn().mockResolvedValue(new Response(sourceBuffer));
39
+ const result = await handleRequest({
40
+ collectionSlug: 'media',
41
+ documentID: '1',
42
+ filename: 'logo.png',
43
+ getSourceFile,
44
+ mimeType,
45
+ req: makeReq({
46
+ query
47
+ })
48
+ });
49
+ return result;
50
+ };
51
+ const getOutputMetadata = async (result)=>sharp(Buffer.from(await result.response.arrayBuffer())).metadata();
52
+ describe('createHandleRequest', ()=>{
53
+ it.each([
54
+ // 10x100 source.
55
+ {
56
+ orientation: undefined,
57
+ sourceHeight: 100,
58
+ sourceWidth: 10
59
+ },
60
+ // Stored 100x10 but tagged orientation 6, so it displays as 10x100.
61
+ {
62
+ orientation: 6,
63
+ sourceHeight: 10,
64
+ sourceWidth: 100
65
+ }
66
+ ])('should reject a width-only request whose displayed aspect ratio exceeds maxPixels (orientation $orientation)', async ({ orientation, sourceHeight, sourceWidth })=>{
67
+ const source = sharp(await makeSourceImage({
68
+ format: 'jpeg',
69
+ height: sourceHeight,
70
+ width: sourceWidth
71
+ }));
72
+ const sourceBuffer = await (orientation ? source.withMetadata({
73
+ orientation
74
+ }) : source).toBuffer();
75
+ // Displayed 10x100 at width=100 renders 100x1000 = 100,000 pixels, 10x the limit.
76
+ const result = await resizeReal({
77
+ dynamicDefaults: resolveSharpDynamicDefaults({
78
+ maxPixels: 10_000
79
+ }),
80
+ mimeType: 'image/jpeg',
81
+ query: 'width=100',
82
+ sourceBuffer
83
+ });
84
+ expect(result.response?.status).toBe(400);
85
+ });
86
+ it.each([
87
+ 'width=1000&height=1000',
88
+ 'width=1000'
89
+ ])('should count every frame of an animated source against maxPixels (%s)', async (query)=>{
90
+ // 1000x1000 per frame is within the 16,777,216 default, but across 44 frames
91
+ // Sharp would render 44,000,000 pixels.
92
+ const result = await resizeReal({
93
+ mimeType: 'image/webp',
94
+ query,
95
+ sourceBuffer: animatedWebp
96
+ });
97
+ expect(result.response?.status).toBe(400);
98
+ });
99
+ it('should resize every frame of an animated source within maxPixels', async ()=>{
100
+ const metadata = await sharp(Buffer.from(await (await resizeReal({
101
+ mimeType: 'image/webp',
102
+ query: 'width=100',
103
+ sourceBuffer: animatedWebp
104
+ })).response.arrayBuffer()), {
105
+ animated: true
106
+ }).metadata();
107
+ expect(metadata.width).toBe(100);
108
+ expect(metadata.pages).toBe(44);
109
+ });
110
+ it('should apply the EXIF orientation of the source before resizing', async ()=>{
111
+ // Stored 400x200 but tagged orientation 6, so it displays as 200x400.
112
+ const sourceBuffer = await sharp(await makeSourceImage({
113
+ format: 'jpeg'
114
+ })).withMetadata({
115
+ orientation: 6
116
+ }).toBuffer();
117
+ const metadata = await getOutputMetadata(await resizeReal({
118
+ mimeType: 'image/jpeg',
119
+ query: 'width=100',
120
+ sourceBuffer
121
+ }));
122
+ expect(metadata.width).toBe(100);
123
+ expect(metadata.height).toBe(200);
124
+ });
125
+ it.each([
126
+ {
127
+ expectedWidth: 100,
128
+ query: 'width=300'
129
+ },
130
+ {
131
+ expectedWidth: 300,
132
+ query: 'width=300&withoutEnlargement=false'
133
+ }
134
+ ])('should apply a configured withoutEnlargement=true default unless the request overrides it ($query)', async ({ expectedWidth, query })=>{
135
+ const sourceBuffer = await makeSourceImage({
136
+ height: 100,
137
+ width: 100
138
+ });
139
+ const metadata = await getOutputMetadata(await resizeReal({
140
+ dynamicDefaults: resolveSharpDynamicDefaults({
141
+ withoutEnlargement: true
142
+ }),
143
+ query,
144
+ sourceBuffer
145
+ }));
146
+ expect(metadata.width).toBe(expectedWidth);
147
+ });
148
+ it.each([
149
+ [
150
+ 'jpeg',
151
+ 'jpeg'
152
+ ],
153
+ [
154
+ 'png',
155
+ 'png'
156
+ ],
157
+ [
158
+ 'webp',
159
+ 'webp'
160
+ ],
161
+ [
162
+ 'tiff',
163
+ 'tiff'
164
+ ],
165
+ [
166
+ 'gif',
167
+ 'gif'
168
+ ],
169
+ // AVIF is stored in a HEIF container — Sharp reports its metadata format as
170
+ // "heif", not "avif", even though the MIME type and file extension are AVIF.
171
+ [
172
+ 'avif',
173
+ 'heif'
174
+ ]
175
+ ])('should retain the source format (%s) in the resized output', async (format, expectedMetadataFormat)=>{
176
+ const sourceBuffer = await makeSourceImage({
177
+ format
178
+ });
179
+ const metadata = await getOutputMetadata(await resizeReal({
180
+ mimeType: `image/${format}`,
181
+ query: 'width=100',
182
+ sourceBuffer
183
+ }));
184
+ expect(metadata.format).toBe(expectedMetadataFormat);
185
+ });
186
+ });
187
+
188
+ //# sourceMappingURL=handleRequest.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/handleRequest.spec.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { readFileSync } from 'fs'\nimport path from 'path'\nimport sharp from 'sharp'\nimport { fileURLToPath } from 'url'\nimport { describe, expect, it, vi } from 'vitest'\n\nimport { createHandleRequest } from './handleRequest.js'\nimport { resolveSharpDynamicDefaults } from './sharpTransformer.js'\n\nconst dirname = path.dirname(fileURLToPath(import.meta.url))\n// 200x200, 44 frames.\nconst animatedWebp = readFileSync(path.resolve(dirname, '../../../test/uploads/animated.webp'))\n\nconst makeReq = ({ query = '' }: { query?: string } = {}): PayloadRequest =>\n ({\n headers: new Headers(),\n method: 'GET',\n payload: { logger: { error: vi.fn() } },\n searchParams: new URLSearchParams(query),\n }) as unknown as PayloadRequest\n\nconst makeSourceImage = async ({\n background = { b: 0, g: 128, r: 255 },\n format = 'png' as Parameters<ReturnType<typeof sharp>['toFormat']>[0],\n height = 200,\n width = 400,\n} = {}): Promise<Buffer> =>\n sharp({ create: { background, channels: 3, height, width } })\n .toFormat(format)\n .toBuffer()\n\nconst resizeReal = async ({\n dynamicDefaults = resolveSharpDynamicDefaults(),\n mimeType = 'image/png',\n query,\n sourceBuffer,\n}: {\n dynamicDefaults?: ReturnType<typeof resolveSharpDynamicDefaults>\n mimeType?: string\n query: string\n sourceBuffer: Buffer\n}) => {\n const handleRequest = createHandleRequest({ dynamicDefaults, sharpDependency: sharp })\n const getSourceFile = vi.fn().mockResolvedValue(new Response(sourceBuffer))\n\n const result = await handleRequest({\n collectionSlug: 'media',\n documentID: '1',\n filename: 'logo.png',\n getSourceFile,\n mimeType,\n req: makeReq({ query }),\n })\n\n return result\n}\n\nconst getOutputMetadata = async (result: Awaited<ReturnType<typeof resizeReal>>) =>\n sharp(Buffer.from(await result.response!.arrayBuffer())).metadata()\n\ndescribe('createHandleRequest', () => {\n it.each([\n // 10x100 source.\n { orientation: undefined, sourceHeight: 100, sourceWidth: 10 },\n // Stored 100x10 but tagged orientation 6, so it displays as 10x100.\n { orientation: 6, sourceHeight: 10, sourceWidth: 100 },\n ])(\n 'should reject a width-only request whose displayed aspect ratio exceeds maxPixels (orientation $orientation)',\n async ({ orientation, sourceHeight, sourceWidth }) => {\n const source = sharp(\n await makeSourceImage({ format: 'jpeg', height: sourceHeight, width: sourceWidth }),\n )\n const sourceBuffer = await (\n orientation ? source.withMetadata({ orientation }) : source\n ).toBuffer()\n\n // Displayed 10x100 at width=100 renders 100x1000 = 100,000 pixels, 10x the limit.\n const result = await resizeReal({\n dynamicDefaults: resolveSharpDynamicDefaults({ maxPixels: 10_000 }),\n mimeType: 'image/jpeg',\n query: 'width=100',\n sourceBuffer,\n })\n\n expect(result.response?.status).toBe(400)\n },\n )\n\n it.each(['width=1000&height=1000', 'width=1000'])(\n 'should count every frame of an animated source against maxPixels (%s)',\n async (query) => {\n // 1000x1000 per frame is within the 16,777,216 default, but across 44 frames\n // Sharp would render 44,000,000 pixels.\n const result = await resizeReal({\n mimeType: 'image/webp',\n query,\n sourceBuffer: animatedWebp,\n })\n\n expect(result.response?.status).toBe(400)\n },\n )\n\n it('should resize every frame of an animated source within maxPixels', async () => {\n const metadata = await sharp(\n Buffer.from(\n await (\n await resizeReal({\n mimeType: 'image/webp',\n query: 'width=100',\n sourceBuffer: animatedWebp,\n })\n ).response!.arrayBuffer(),\n ),\n { animated: true },\n ).metadata()\n\n expect(metadata.width).toBe(100)\n expect(metadata.pages).toBe(44)\n })\n\n it('should apply the EXIF orientation of the source before resizing', async () => {\n // Stored 400x200 but tagged orientation 6, so it displays as 200x400.\n const sourceBuffer = await sharp(await makeSourceImage({ format: 'jpeg' }))\n .withMetadata({ orientation: 6 })\n .toBuffer()\n\n const metadata = await getOutputMetadata(\n await resizeReal({ mimeType: 'image/jpeg', query: 'width=100', sourceBuffer }),\n )\n\n expect(metadata.width).toBe(100)\n expect(metadata.height).toBe(200)\n })\n\n it.each([\n { expectedWidth: 100, query: 'width=300' },\n { expectedWidth: 300, query: 'width=300&withoutEnlargement=false' },\n ])(\n 'should apply a configured withoutEnlargement=true default unless the request overrides it ($query)',\n async ({ expectedWidth, query }) => {\n const sourceBuffer = await makeSourceImage({ height: 100, width: 100 })\n\n const metadata = await getOutputMetadata(\n await resizeReal({\n dynamicDefaults: resolveSharpDynamicDefaults({ withoutEnlargement: true }),\n query,\n sourceBuffer,\n }),\n )\n\n expect(metadata.width).toBe(expectedWidth)\n },\n )\n\n it.each([\n ['jpeg', 'jpeg'],\n ['png', 'png'],\n ['webp', 'webp'],\n ['tiff', 'tiff'],\n ['gif', 'gif'],\n // AVIF is stored in a HEIF container — Sharp reports its metadata format as\n // \"heif\", not \"avif\", even though the MIME type and file extension are AVIF.\n ['avif', 'heif'],\n ] as const)(\n 'should retain the source format (%s) in the resized output',\n async (format, expectedMetadataFormat) => {\n const sourceBuffer = await makeSourceImage({ format })\n\n const metadata = await getOutputMetadata(\n await resizeReal({ mimeType: `image/${format}`, query: 'width=100', sourceBuffer }),\n )\n\n expect(metadata.format).toBe(expectedMetadataFormat)\n },\n )\n})\n"],"names":["readFileSync","path","sharp","fileURLToPath","describe","expect","it","vi","createHandleRequest","resolveSharpDynamicDefaults","dirname","url","animatedWebp","resolve","makeReq","query","headers","Headers","method","payload","logger","error","fn","searchParams","URLSearchParams","makeSourceImage","background","b","g","r","format","height","width","create","channels","toFormat","toBuffer","resizeReal","dynamicDefaults","mimeType","sourceBuffer","handleRequest","sharpDependency","getSourceFile","mockResolvedValue","Response","result","collectionSlug","documentID","filename","req","getOutputMetadata","Buffer","from","response","arrayBuffer","metadata","each","orientation","undefined","sourceHeight","sourceWidth","source","withMetadata","maxPixels","status","toBe","animated","pages","expectedWidth","withoutEnlargement","expectedMetadataFormat"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { sharpTransformer } from './sharpTransformer.js';
2
+ export type { DynamicResizeParseResult, SharpCollectionConfig, SharpDependency, SharpDynamicDefaults, SharpDynamicOptions, SharpTransformerOptions, } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,YAAY,EACV,wBAAwB,EACxB,qBAAqB,EACrB,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { sharpTransformer } from './sharpTransformer.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { sharpTransformer } from './sharpTransformer.js'\nexport type {\n DynamicResizeParseResult,\n SharpCollectionConfig,\n SharpDependency,\n SharpDynamicDefaults,\n SharpDynamicOptions,\n SharpTransformerOptions,\n} from './types.js'\n"],"names":["sharpTransformer"],"mappings":"AAAA,SAASA,gBAAgB,QAAQ,wBAAuB"}
@@ -0,0 +1,14 @@
1
+ import type { Config } from 'payload';
2
+ import type { SharpCollectionConfig } from './types.js';
3
+ /**
4
+ * Validates `sharpTransformer({ collections })` against the config's real
5
+ * collections, then writes a narrowed, Sharp-agnostic projection of
6
+ * `variants`/`crop`/`focalPoint`/`hasImageAdjustments` back onto each
7
+ * targeted collection's sanitized `upload` config, so core's own field
8
+ * generation and Admin UI keep working without knowing about Sharp.
9
+ */
10
+ export declare function initSharpCollections({ collections, config, }: {
11
+ collections: Partial<Record<string, SharpCollectionConfig>>;
12
+ config: Config;
13
+ }): Config;
14
+ //# sourceMappingURL=initSharpCollections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"initSharpCollections.d.ts","sourceRoot":"","sources":["../src/initSharpCollections.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAyB,MAAM,SAAS,CAAA;AAE5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAevD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,WAAW,EACX,MAAM,GACP,EAAE;IACD,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAA;IAC3D,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,MAAM,CA0FT"}
@@ -0,0 +1,84 @@
1
+ const RESERVED_IMAGE_SIZE_NAMES = [
2
+ 'filename',
3
+ 'mimeType',
4
+ 'filesize',
5
+ 'width',
6
+ 'height',
7
+ 'url',
8
+ 'thumbnailURL',
9
+ 'variants',
10
+ 'focalX',
11
+ 'focalY'
12
+ ];
13
+ /**
14
+ * Validates `sharpTransformer({ collections })` against the config's real
15
+ * collections, then writes a narrowed, Sharp-agnostic projection of
16
+ * `variants`/`crop`/`focalPoint`/`hasImageAdjustments` back onto each
17
+ * targeted collection's sanitized `upload` config, so core's own field
18
+ * generation and Admin UI keep working without knowing about Sharp.
19
+ */ export function initSharpCollections({ collections, config }) {
20
+ const errors = [];
21
+ for (const [slug, sharpConfig] of Object.entries(collections)){
22
+ if (!sharpConfig) {
23
+ continue;
24
+ }
25
+ const collection = config.collections?.find((candidate)=>candidate.slug === slug);
26
+ if (!collection) {
27
+ errors.push(`sharpTransformer collections."${slug}" does not match any configured collection slug.`);
28
+ continue;
29
+ }
30
+ if (!collection.upload) {
31
+ errors.push(`sharpTransformer collections."${slug}" targets a collection whose \`upload\` option is not set. Enable uploads on "${slug}" first.`);
32
+ continue;
33
+ }
34
+ const seenSizeNames = new Set();
35
+ for (const size of sharpConfig.variants ?? []){
36
+ if (typeof size.name !== 'string' || size.name.trim().length === 0) {
37
+ errors.push(`sharpTransformer collections."${slug}".variants has an entry missing a valid \`name\`.`);
38
+ continue;
39
+ }
40
+ if (seenSizeNames.has(size.name)) {
41
+ errors.push(`sharpTransformer collections."${slug}".variants has a duplicate size name: "${size.name}".`);
42
+ }
43
+ seenSizeNames.add(size.name);
44
+ if (RESERVED_IMAGE_SIZE_NAMES.includes(size.name)) {
45
+ errors.push(`sharpTransformer collections."${slug}".variants uses reserved name "${size.name}", which collides with a built-in upload field. Choose a different name.`);
46
+ }
47
+ }
48
+ }
49
+ if (errors.length > 0) {
50
+ throw new Error(`Invalid \`sharpTransformer({ collections })\` configuration:\n${errors.join('\n')}`);
51
+ }
52
+ // Write onto copies, never the caller's config or collection objects: a rebuilt config (e.g. on
53
+ // a dev reload) would otherwise see `upload.variants` on the collection and reject it as authored.
54
+ const sanitizedCollections = config.collections?.map((collection)=>{
55
+ const sharpConfig = collections[collection.slug];
56
+ if (!sharpConfig || !collection.upload) {
57
+ return collection;
58
+ }
59
+ const authoredUpload = typeof collection.upload === 'object' ? collection.upload : {};
60
+ const upload = {
61
+ ...authoredUpload,
62
+ // Sharp's setting wins when given; otherwise keep the collection's own, so an
63
+ // unset Sharp option can't silently re-enable a collection's `crop: false`.
64
+ crop: sharpConfig.crop ?? authoredUpload.crop,
65
+ focalPoint: sharpConfig.focalPoint ?? authoredUpload.focalPoint,
66
+ hasImageAdjustments: Boolean(sharpConfig.resizeOptions || sharpConfig.formatOptions || sharpConfig.trimOptions || sharpConfig.constructorOptions || sharpConfig.withMetadata),
67
+ variants: sharpConfig.variants?.map(({ name, admin, generateImageName })=>({
68
+ name,
69
+ admin,
70
+ generateImageName
71
+ }))
72
+ };
73
+ return {
74
+ ...collection,
75
+ upload
76
+ };
77
+ });
78
+ return {
79
+ ...config,
80
+ collections: sanitizedCollections
81
+ };
82
+ }
83
+
84
+ //# sourceMappingURL=initSharpCollections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/initSharpCollections.ts"],"sourcesContent":["import type { Config, SanitizedUploadConfig } from 'payload'\n\nimport type { SharpCollectionConfig } from './types.js'\n\nconst RESERVED_IMAGE_SIZE_NAMES = [\n 'filename',\n 'mimeType',\n 'filesize',\n 'width',\n 'height',\n 'url',\n 'thumbnailURL',\n 'variants',\n 'focalX',\n 'focalY',\n]\n\n/**\n * Validates `sharpTransformer({ collections })` against the config's real\n * collections, then writes a narrowed, Sharp-agnostic projection of\n * `variants`/`crop`/`focalPoint`/`hasImageAdjustments` back onto each\n * targeted collection's sanitized `upload` config, so core's own field\n * generation and Admin UI keep working without knowing about Sharp.\n */\nexport function initSharpCollections({\n collections,\n config,\n}: {\n collections: Partial<Record<string, SharpCollectionConfig>>\n config: Config\n}): Config {\n const errors: string[] = []\n\n for (const [slug, sharpConfig] of Object.entries(collections)) {\n if (!sharpConfig) {\n continue\n }\n\n const collection = config.collections?.find((candidate) => candidate.slug === slug)\n\n if (!collection) {\n errors.push(\n `sharpTransformer collections.\"${slug}\" does not match any configured collection slug.`,\n )\n continue\n }\n\n if (!collection.upload) {\n errors.push(\n `sharpTransformer collections.\"${slug}\" targets a collection whose \\`upload\\` option is not set. Enable uploads on \"${slug}\" first.`,\n )\n continue\n }\n\n const seenSizeNames = new Set<string>()\n\n for (const size of sharpConfig.variants ?? []) {\n if (typeof size.name !== 'string' || size.name.trim().length === 0) {\n errors.push(\n `sharpTransformer collections.\"${slug}\".variants has an entry missing a valid \\`name\\`.`,\n )\n continue\n }\n\n if (seenSizeNames.has(size.name)) {\n errors.push(\n `sharpTransformer collections.\"${slug}\".variants has a duplicate size name: \"${size.name}\".`,\n )\n }\n seenSizeNames.add(size.name)\n\n if (RESERVED_IMAGE_SIZE_NAMES.includes(size.name)) {\n errors.push(\n `sharpTransformer collections.\"${slug}\".variants uses reserved name \"${size.name}\", which collides with a built-in upload field. Choose a different name.`,\n )\n }\n }\n }\n\n if (errors.length > 0) {\n throw new Error(\n `Invalid \\`sharpTransformer({ collections })\\` configuration:\\n${errors.join('\\n')}`,\n )\n }\n\n // Write onto copies, never the caller's config or collection objects: a rebuilt config (e.g. on\n // a dev reload) would otherwise see `upload.variants` on the collection and reject it as authored.\n const sanitizedCollections = config.collections?.map((collection) => {\n const sharpConfig = collections[collection.slug]\n\n if (!sharpConfig || !collection.upload) {\n return collection\n }\n\n const authoredUpload = typeof collection.upload === 'object' ? collection.upload : {}\n\n const upload: Partial<SanitizedUploadConfig> = {\n ...authoredUpload,\n // Sharp's setting wins when given; otherwise keep the collection's own, so an\n // unset Sharp option can't silently re-enable a collection's `crop: false`.\n crop: sharpConfig.crop ?? authoredUpload.crop,\n focalPoint: sharpConfig.focalPoint ?? authoredUpload.focalPoint,\n hasImageAdjustments: Boolean(\n sharpConfig.resizeOptions ||\n sharpConfig.formatOptions ||\n sharpConfig.trimOptions ||\n sharpConfig.constructorOptions ||\n sharpConfig.withMetadata,\n ),\n variants: sharpConfig.variants?.map(({ name, admin, generateImageName }) => ({\n name,\n admin,\n generateImageName,\n })),\n }\n\n return { ...collection, upload }\n })\n\n return { ...config, collections: sanitizedCollections }\n}\n"],"names":["RESERVED_IMAGE_SIZE_NAMES","initSharpCollections","collections","config","errors","slug","sharpConfig","Object","entries","collection","find","candidate","push","upload","seenSizeNames","Set","size","variants","name","trim","length","has","add","includes","Error","join","sanitizedCollections","map","authoredUpload","crop","focalPoint","hasImageAdjustments","Boolean","resizeOptions","formatOptions","trimOptions","constructorOptions","withMetadata","admin","generateImageName"],"mappings":"AAIA,MAAMA,4BAA4B;IAChC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED;;;;;;CAMC,GACD,OAAO,SAASC,qBAAqB,EACnCC,WAAW,EACXC,MAAM,EAIP;IACC,MAAMC,SAAmB,EAAE;IAE3B,KAAK,MAAM,CAACC,MAAMC,YAAY,IAAIC,OAAOC,OAAO,CAACN,aAAc;QAC7D,IAAI,CAACI,aAAa;YAChB;QACF;QAEA,MAAMG,aAAaN,OAAOD,WAAW,EAAEQ,KAAK,CAACC,YAAcA,UAAUN,IAAI,KAAKA;QAE9E,IAAI,CAACI,YAAY;YACfL,OAAOQ,IAAI,CACT,CAAC,8BAA8B,EAAEP,KAAK,gDAAgD,CAAC;YAEzF;QACF;QAEA,IAAI,CAACI,WAAWI,MAAM,EAAE;YACtBT,OAAOQ,IAAI,CACT,CAAC,8BAA8B,EAAEP,KAAK,8EAA8E,EAAEA,KAAK,QAAQ,CAAC;YAEtI;QACF;QAEA,MAAMS,gBAAgB,IAAIC;QAE1B,KAAK,MAAMC,QAAQV,YAAYW,QAAQ,IAAI,EAAE,CAAE;YAC7C,IAAI,OAAOD,KAAKE,IAAI,KAAK,YAAYF,KAAKE,IAAI,CAACC,IAAI,GAAGC,MAAM,KAAK,GAAG;gBAClEhB,OAAOQ,IAAI,CACT,CAAC,8BAA8B,EAAEP,KAAK,iDAAiD,CAAC;gBAE1F;YACF;YAEA,IAAIS,cAAcO,GAAG,CAACL,KAAKE,IAAI,GAAG;gBAChCd,OAAOQ,IAAI,CACT,CAAC,8BAA8B,EAAEP,KAAK,uCAAuC,EAAEW,KAAKE,IAAI,CAAC,EAAE,CAAC;YAEhG;YACAJ,cAAcQ,GAAG,CAACN,KAAKE,IAAI;YAE3B,IAAIlB,0BAA0BuB,QAAQ,CAACP,KAAKE,IAAI,GAAG;gBACjDd,OAAOQ,IAAI,CACT,CAAC,8BAA8B,EAAEP,KAAK,+BAA+B,EAAEW,KAAKE,IAAI,CAAC,wEAAwE,CAAC;YAE9J;QACF;IACF;IAEA,IAAId,OAAOgB,MAAM,GAAG,GAAG;QACrB,MAAM,IAAII,MACR,CAAC,8DAA8D,EAAEpB,OAAOqB,IAAI,CAAC,OAAO;IAExF;IAEA,gGAAgG;IAChG,mGAAmG;IACnG,MAAMC,uBAAuBvB,OAAOD,WAAW,EAAEyB,IAAI,CAAClB;QACpD,MAAMH,cAAcJ,WAAW,CAACO,WAAWJ,IAAI,CAAC;QAEhD,IAAI,CAACC,eAAe,CAACG,WAAWI,MAAM,EAAE;YACtC,OAAOJ;QACT;QAEA,MAAMmB,iBAAiB,OAAOnB,WAAWI,MAAM,KAAK,WAAWJ,WAAWI,MAAM,GAAG,CAAC;QAEpF,MAAMA,SAAyC;YAC7C,GAAGe,cAAc;YACjB,8EAA8E;YAC9E,4EAA4E;YAC5EC,MAAMvB,YAAYuB,IAAI,IAAID,eAAeC,IAAI;YAC7CC,YAAYxB,YAAYwB,UAAU,IAAIF,eAAeE,UAAU;YAC/DC,qBAAqBC,QACnB1B,YAAY2B,aAAa,IACvB3B,YAAY4B,aAAa,IACzB5B,YAAY6B,WAAW,IACvB7B,YAAY8B,kBAAkB,IAC9B9B,YAAY+B,YAAY;YAE5BpB,UAAUX,YAAYW,QAAQ,EAAEU,IAAI,CAAC,EAAET,IAAI,EAAEoB,KAAK,EAAEC,iBAAiB,EAAE,GAAM,CAAA;oBAC3ErB;oBACAoB;oBACAC;gBACF,CAAA;QACF;QAEA,OAAO;YAAE,GAAG9B,UAAU;YAAEI;QAAO;IACjC;IAEA,OAAO;QAAE,GAAGV,MAAM;QAAED,aAAawB;IAAqB;AACxD"}
@@ -0,0 +1,185 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { initSharpCollections } from './initSharpCollections.js';
3
+ const makeConfig = (collections)=>({
4
+ collections
5
+ });
6
+ const uploadCollection = ({ slug, upload = {} })=>({
7
+ slug,
8
+ upload
9
+ });
10
+ describe('initSharpCollections', ()=>{
11
+ it('should not throw when the config has no collections', ()=>{
12
+ const config = makeConfig(undefined);
13
+ expect(()=>initSharpCollections({
14
+ collections: {},
15
+ config
16
+ })).not.toThrow();
17
+ });
18
+ it.each([
19
+ {
20
+ expected: {
21
+ crop: false,
22
+ focalPoint: false
23
+ },
24
+ sharpConfig: {}
25
+ },
26
+ {
27
+ expected: {
28
+ crop: true,
29
+ focalPoint: true
30
+ },
31
+ sharpConfig: {
32
+ crop: true,
33
+ focalPoint: true
34
+ }
35
+ }
36
+ ])("should let Sharp's crop and focalPoint override the collection's only when set (%#)", ({ expected, sharpConfig })=>{
37
+ const config = makeConfig([
38
+ uploadCollection({
39
+ slug: 'media',
40
+ upload: {
41
+ crop: false,
42
+ focalPoint: false
43
+ }
44
+ })
45
+ ]);
46
+ const result = initSharpCollections({
47
+ collections: {
48
+ media: sharpConfig
49
+ },
50
+ config
51
+ });
52
+ expect(result.collections?.[0]?.upload).toMatchObject(expected);
53
+ });
54
+ it('should write variants onto a copy, leaving the caller config untouched', ()=>{
55
+ const authoredCollection = uploadCollection({
56
+ slug: 'media',
57
+ upload: {
58
+ staticDir: 'media'
59
+ }
60
+ });
61
+ const authoredCollections = [
62
+ authoredCollection
63
+ ];
64
+ const config = makeConfig(authoredCollections);
65
+ const result = initSharpCollections({
66
+ collections: {
67
+ media: {
68
+ variants: [
69
+ {
70
+ name: 'thumbnail',
71
+ width: 100
72
+ }
73
+ ]
74
+ }
75
+ },
76
+ config
77
+ });
78
+ expect(result.collections?.[0]?.upload).toMatchObject({
79
+ variants: [
80
+ {
81
+ name: 'thumbnail'
82
+ }
83
+ ],
84
+ staticDir: 'media'
85
+ });
86
+ expect(result).not.toBe(config);
87
+ expect(result.collections?.[0]).not.toBe(authoredCollection);
88
+ expect(config.collections).toBe(authoredCollections);
89
+ expect(authoredCollection.upload).toEqual({
90
+ staticDir: 'media'
91
+ });
92
+ });
93
+ it.each([
94
+ {
95
+ collection: uploadCollection({
96
+ slug: 'media'
97
+ }),
98
+ slug: 'missing'
99
+ },
100
+ {
101
+ collection: uploadCollection({
102
+ slug: 'posts',
103
+ upload: false
104
+ }),
105
+ slug: 'posts'
106
+ }
107
+ ])('should throw when the configured collection "$slug" is missing or not upload-enabled', ({ collection, slug })=>{
108
+ const config = makeConfig([
109
+ collection
110
+ ]);
111
+ expect(()=>initSharpCollections({
112
+ collections: {
113
+ [slug]: {}
114
+ },
115
+ config
116
+ })).toThrow(new RegExp(`"${slug}"`));
117
+ });
118
+ it.each([
119
+ {
120
+ error: /duplicate/i,
121
+ variants: [
122
+ {
123
+ name: 'square',
124
+ width: 100
125
+ },
126
+ {
127
+ name: 'square',
128
+ width: 200
129
+ }
130
+ ]
131
+ },
132
+ {
133
+ error: /reserved/i,
134
+ variants: [
135
+ {
136
+ name: 'filename',
137
+ width: 100
138
+ }
139
+ ]
140
+ },
141
+ {
142
+ error: /name/i,
143
+ variants: [
144
+ {
145
+ width: 100
146
+ }
147
+ ]
148
+ }
149
+ ])('should reject invalid variants ($error)', ({ error, variants })=>{
150
+ const config = makeConfig([
151
+ uploadCollection({
152
+ slug: 'media'
153
+ })
154
+ ]);
155
+ expect(()=>initSharpCollections({
156
+ collections: {
157
+ media: {
158
+ variants: variants
159
+ }
160
+ },
161
+ config
162
+ })).toThrow(error);
163
+ });
164
+ it('should allow a variants entry with neither width nor height (format-only/pass-through size)', ()=>{
165
+ const config = makeConfig([
166
+ uploadCollection({
167
+ slug: 'media'
168
+ })
169
+ ]);
170
+ expect(()=>initSharpCollections({
171
+ collections: {
172
+ media: {
173
+ variants: [
174
+ {
175
+ name: 'noDimensions'
176
+ }
177
+ ]
178
+ }
179
+ },
180
+ config
181
+ })).not.toThrow();
182
+ });
183
+ });
184
+
185
+ //# sourceMappingURL=initSharpCollections.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/initSharpCollections.spec.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport { describe, expect, it } from 'vitest'\n\nimport type { SharpCollectionConfig } from './types.js'\n\nimport { initSharpCollections } from './initSharpCollections.js'\n\nconst makeConfig = (collections: Config['collections']): Config =>\n ({\n collections,\n }) as unknown as Config\n\nconst uploadCollection = ({\n slug,\n upload = {},\n}: {\n slug: string\n upload?: boolean | Record<string, unknown>\n}) => ({ slug, upload }) as unknown as NonNullable<Config['collections']>[number]\n\ndescribe('initSharpCollections', () => {\n it('should not throw when the config has no collections', () => {\n const config = makeConfig(undefined)\n\n expect(() => initSharpCollections({ collections: {}, config })).not.toThrow()\n })\n\n it.each([\n { expected: { crop: false, focalPoint: false }, sharpConfig: {} },\n { expected: { crop: true, focalPoint: true }, sharpConfig: { crop: true, focalPoint: true } },\n ])(\n \"should let Sharp's crop and focalPoint override the collection's only when set (%#)\",\n ({ expected, sharpConfig }) => {\n const config = makeConfig([\n uploadCollection({ slug: 'media', upload: { crop: false, focalPoint: false } }),\n ])\n\n const result = initSharpCollections({ collections: { media: sharpConfig }, config })\n\n expect(result.collections?.[0]?.upload).toMatchObject(expected)\n },\n )\n\n it('should write variants onto a copy, leaving the caller config untouched', () => {\n const authoredCollection = uploadCollection({ slug: 'media', upload: { staticDir: 'media' } })\n const authoredCollections = [authoredCollection]\n const config = makeConfig(authoredCollections)\n\n const result = initSharpCollections({\n collections: { media: { variants: [{ name: 'thumbnail', width: 100 }] } },\n config,\n })\n\n expect(result.collections?.[0]?.upload).toMatchObject({\n variants: [{ name: 'thumbnail' }],\n staticDir: 'media',\n })\n expect(result).not.toBe(config)\n expect(result.collections?.[0]).not.toBe(authoredCollection)\n expect(config.collections).toBe(authoredCollections)\n expect(authoredCollection.upload).toEqual({ staticDir: 'media' })\n })\n\n it.each([\n { collection: uploadCollection({ slug: 'media' }), slug: 'missing' },\n { collection: uploadCollection({ slug: 'posts', upload: false }), slug: 'posts' },\n ])(\n 'should throw when the configured collection \"$slug\" is missing or not upload-enabled',\n ({ collection, slug }) => {\n const config = makeConfig([collection])\n\n expect(() => initSharpCollections({ collections: { [slug]: {} }, config })).toThrow(\n new RegExp(`\"${slug}\"`),\n )\n },\n )\n\n it.each([\n {\n error: /duplicate/i,\n variants: [\n { name: 'square', width: 100 },\n { name: 'square', width: 200 },\n ],\n },\n { error: /reserved/i, variants: [{ name: 'filename', width: 100 }] },\n { error: /name/i, variants: [{ width: 100 }] },\n ])('should reject invalid variants ($error)', ({ error, variants }) => {\n const config = makeConfig([uploadCollection({ slug: 'media' })])\n\n expect(() =>\n initSharpCollections({\n collections: { media: { variants: variants as SharpCollectionConfig['variants'] } },\n config,\n }),\n ).toThrow(error)\n })\n\n it('should allow a variants entry with neither width nor height (format-only/pass-through size)', () => {\n const config = makeConfig([uploadCollection({ slug: 'media' })])\n\n expect(() =>\n initSharpCollections({\n collections: { media: { variants: [{ name: 'noDimensions' }] } },\n config,\n }),\n ).not.toThrow()\n })\n})\n"],"names":["describe","expect","it","initSharpCollections","makeConfig","collections","uploadCollection","slug","upload","config","undefined","not","toThrow","each","expected","crop","focalPoint","sharpConfig","result","media","toMatchObject","authoredCollection","staticDir","authoredCollections","variants","name","width","toBe","toEqual","collection","RegExp","error"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runs `mapper` over every item, at most `concurrency` items in flight at
3
+ * once. Results preserve `items`' order regardless of completion order —
4
+ * used so configured image sizes keep their declared metadata order.
5
+ */
6
+ export declare function mapWithBoundedConcurrency<TItem, TResult>(items: TItem[], mapper: (item: TItem, index: number) => Promise<TResult>, concurrency?: number): Promise<TResult[]>;
7
+ //# sourceMappingURL=mapWithBoundedConcurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapWithBoundedConcurrency.d.ts","sourceRoot":"","sources":["../src/mapWithBoundedConcurrency.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAC5D,KAAK,EAAE,KAAK,EAAE,EACd,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EACxD,WAAW,GAAE,MAA4B,GACxC,OAAO,CAAC,OAAO,EAAE,CAAC,CAqBpB"}
@@ -0,0 +1,26 @@
1
+ const DEFAULT_CONCURRENCY = 4;
2
+ /**
3
+ * Runs `mapper` over every item, at most `concurrency` items in flight at
4
+ * once. Results preserve `items`' order regardless of completion order —
5
+ * used so configured image sizes keep their declared metadata order.
6
+ */ export async function mapWithBoundedConcurrency(items, mapper, concurrency = DEFAULT_CONCURRENCY) {
7
+ const results = new Array(items.length);
8
+ let nextIndex = 0;
9
+ async function runWorker() {
10
+ while(true){
11
+ const currentIndex = nextIndex;
12
+ nextIndex += 1;
13
+ if (currentIndex >= items.length) {
14
+ return;
15
+ }
16
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
17
+ }
18
+ }
19
+ const workerCount = Math.min(concurrency, items.length);
20
+ await Promise.all(Array.from({
21
+ length: workerCount
22
+ }, runWorker));
23
+ return results;
24
+ }
25
+
26
+ //# sourceMappingURL=mapWithBoundedConcurrency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mapWithBoundedConcurrency.ts"],"sourcesContent":["const DEFAULT_CONCURRENCY = 4\n\n/**\n * Runs `mapper` over every item, at most `concurrency` items in flight at\n * once. Results preserve `items`' order regardless of completion order —\n * used so configured image sizes keep their declared metadata order.\n */\nexport async function mapWithBoundedConcurrency<TItem, TResult>(\n items: TItem[],\n mapper: (item: TItem, index: number) => Promise<TResult>,\n concurrency: number = DEFAULT_CONCURRENCY,\n): Promise<TResult[]> {\n const results: TResult[] = new Array(items.length)\n let nextIndex = 0\n\n async function runWorker(): Promise<void> {\n while (true) {\n const currentIndex = nextIndex\n nextIndex += 1\n\n if (currentIndex >= items.length) {\n return\n }\n\n results[currentIndex] = await mapper(items[currentIndex]!, currentIndex)\n }\n }\n\n const workerCount = Math.min(concurrency, items.length)\n await Promise.all(Array.from({ length: workerCount }, runWorker))\n\n return results\n}\n"],"names":["DEFAULT_CONCURRENCY","mapWithBoundedConcurrency","items","mapper","concurrency","results","Array","length","nextIndex","runWorker","currentIndex","workerCount","Math","min","Promise","all","from"],"mappings":"AAAA,MAAMA,sBAAsB;AAE5B;;;;CAIC,GACD,OAAO,eAAeC,0BACpBC,KAAc,EACdC,MAAwD,EACxDC,cAAsBJ,mBAAmB;IAEzC,MAAMK,UAAqB,IAAIC,MAAMJ,MAAMK,MAAM;IACjD,IAAIC,YAAY;IAEhB,eAAeC;QACb,MAAO,KAAM;YACX,MAAMC,eAAeF;YACrBA,aAAa;YAEb,IAAIE,gBAAgBR,MAAMK,MAAM,EAAE;gBAChC;YACF;YAEAF,OAAO,CAACK,aAAa,GAAG,MAAMP,OAAOD,KAAK,CAACQ,aAAa,EAAGA;QAC7D;IACF;IAEA,MAAMC,cAAcC,KAAKC,GAAG,CAACT,aAAaF,MAAMK,MAAM;IACtD,MAAMO,QAAQC,GAAG,CAACT,MAAMU,IAAI,CAAC;QAAET,QAAQI;IAAY,GAAGF;IAEtD,OAAOJ;AACT"}
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mapWithBoundedConcurrency } from './mapWithBoundedConcurrency.js';
3
+ describe('mapWithBoundedConcurrency', ()=>{
4
+ it('should preserve result order regardless of completion order', async ()=>{
5
+ const items = [
6
+ 30,
7
+ 10,
8
+ 20
9
+ ];
10
+ const results = await mapWithBoundedConcurrency(items, async (delay)=>{
11
+ await new Promise((resolve)=>setTimeout(resolve, delay));
12
+ return delay;
13
+ });
14
+ expect(results).toEqual([
15
+ 30,
16
+ 10,
17
+ 20
18
+ ]);
19
+ });
20
+ it('should never run more than the configured concurrency at once', async ()=>{
21
+ let inFlight = 0;
22
+ let maxInFlight = 0;
23
+ await mapWithBoundedConcurrency(Array.from({
24
+ length: 10
25
+ }, (_, i)=>i), async ()=>{
26
+ inFlight += 1;
27
+ maxInFlight = Math.max(maxInFlight, inFlight);
28
+ await new Promise((resolve)=>setTimeout(resolve, 5));
29
+ inFlight -= 1;
30
+ }, 2);
31
+ expect(maxInFlight).toBeLessThanOrEqual(2);
32
+ });
33
+ });
34
+
35
+ //# sourceMappingURL=mapWithBoundedConcurrency.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mapWithBoundedConcurrency.spec.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest'\n\nimport { mapWithBoundedConcurrency } from './mapWithBoundedConcurrency.js'\n\ndescribe('mapWithBoundedConcurrency', () => {\n it('should preserve result order regardless of completion order', async () => {\n const items = [30, 10, 20]\n\n const results = await mapWithBoundedConcurrency(items, async (delay) => {\n await new Promise((resolve) => setTimeout(resolve, delay))\n return delay\n })\n\n expect(results).toEqual([30, 10, 20])\n })\n\n it('should never run more than the configured concurrency at once', async () => {\n let inFlight = 0\n let maxInFlight = 0\n\n await mapWithBoundedConcurrency(\n Array.from({ length: 10 }, (_, i) => i),\n async () => {\n inFlight += 1\n maxInFlight = Math.max(maxInFlight, inFlight)\n await new Promise((resolve) => setTimeout(resolve, 5))\n inFlight -= 1\n },\n 2,\n )\n\n expect(maxInFlight).toBeLessThanOrEqual(2)\n })\n})\n"],"names":["describe","expect","it","mapWithBoundedConcurrency","items","results","delay","Promise","resolve","setTimeout","toEqual","inFlight","maxInFlight","Array","from","length","_","i","Math","max","toBeLessThanOrEqual"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7C,SAASC,yBAAyB,QAAQ,iCAAgC;AAE1EH,SAAS,6BAA6B;IACpCE,GAAG,+DAA+D;QAChE,MAAME,QAAQ;YAAC;YAAI;YAAI;SAAG;QAE1B,MAAMC,UAAU,MAAMF,0BAA0BC,OAAO,OAAOE;YAC5D,MAAM,IAAIC,QAAQ,CAACC,UAAYC,WAAWD,SAASF;YACnD,OAAOA;QACT;QAEAL,OAAOI,SAASK,OAAO,CAAC;YAAC;YAAI;YAAI;SAAG;IACtC;IAEAR,GAAG,iEAAiE;QAClE,IAAIS,WAAW;QACf,IAAIC,cAAc;QAElB,MAAMT,0BACJU,MAAMC,IAAI,CAAC;YAAEC,QAAQ;QAAG,GAAG,CAACC,GAAGC,IAAMA,IACrC;YACEN,YAAY;YACZC,cAAcM,KAAKC,GAAG,CAACP,aAAaD;YACpC,MAAM,IAAIJ,QAAQ,CAACC,UAAYC,WAAWD,SAAS;YACnDG,YAAY;QACd,GACA;QAGFV,OAAOW,aAAaQ,mBAAmB,CAAC;IAC1C;AACF"}
@@ -0,0 +1,9 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { Sharp } from 'sharp';
3
+ import type { WithMetadata } from './types.js';
4
+ export declare function optionallyAppendMetadata({ req, sharpFile, withMetadata, }: {
5
+ req: PayloadRequest;
6
+ sharpFile: Sharp;
7
+ withMetadata: undefined | WithMetadata;
8
+ }): Promise<Sharp>;
9
+ //# sourceMappingURL=optionallyAppendMetadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optionallyAppendMetadata.d.ts","sourceRoot":"","sources":["../src/optionallyAppendMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAA;AAElC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,wBAAsB,wBAAwB,CAAC,EAC7C,GAAG,EACH,SAAS,EACT,YAAY,GACb,EAAE;IACD,GAAG,EAAE,cAAc,CAAA;IACnB,SAAS,EAAE,KAAK,CAAA;IAChB,YAAY,EAAE,SAAS,GAAG,YAAY,CAAA;CACvC,GAAG,OAAO,CAAC,KAAK,CAAC,CAcjB"}