@pro-laico/payload-images 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +11 -0
  3. package/dist/access.d.ts +4 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +4 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/collections/generatedImages.d.ts +26 -0
  8. package/dist/collections/generatedImages.d.ts.map +1 -0
  9. package/dist/collections/generatedImages.js +91 -0
  10. package/dist/collections/generatedImages.js.map +1 -0
  11. package/dist/collections/images.d.ts +60 -0
  12. package/dist/collections/images.d.ts.map +1 -0
  13. package/dist/collections/images.js +223 -0
  14. package/dist/collections/images.js.map +1 -0
  15. package/dist/components/admin/focalPreview.d.ts +8 -0
  16. package/dist/components/admin/focalPreview.d.ts.map +1 -0
  17. package/dist/components/admin/focalPreview.js +234 -0
  18. package/dist/components/admin/focalPreview.js.map +1 -0
  19. package/dist/components/admin/purgeVariants.d.ts +16 -0
  20. package/dist/components/admin/purgeVariants.d.ts.map +1 -0
  21. package/dist/components/admin/purgeVariants.js +66 -0
  22. package/dist/components/admin/purgeVariants.js.map +1 -0
  23. package/dist/components/image.d.ts +90 -0
  24. package/dist/components/image.d.ts.map +1 -0
  25. package/dist/components/image.js +162 -0
  26. package/dist/components/image.js.map +1 -0
  27. package/dist/components/inlineLqip.d.ts +36 -0
  28. package/dist/components/inlineLqip.d.ts.map +1 -0
  29. package/dist/components/inlineLqip.js +54 -0
  30. package/dist/components/inlineLqip.js.map +1 -0
  31. package/dist/endpoints/transform.d.ts +40 -0
  32. package/dist/endpoints/transform.d.ts.map +1 -0
  33. package/dist/endpoints/transform.js +181 -0
  34. package/dist/endpoints/transform.js.map +1 -0
  35. package/dist/fields/virtualUrls.d.ts +26 -0
  36. package/dist/fields/virtualUrls.d.ts.map +1 -0
  37. package/dist/fields/virtualUrls.js +185 -0
  38. package/dist/fields/virtualUrls.js.map +1 -0
  39. package/dist/hooks/purge.d.ts +33 -0
  40. package/dist/hooks/purge.d.ts.map +1 -0
  41. package/dist/hooks/purge.js +63 -0
  42. package/dist/hooks/purge.js.map +1 -0
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +9 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/lib/configStash.d.ts +6 -0
  48. package/dist/lib/configStash.d.ts.map +1 -0
  49. package/dist/lib/configStash.js +10 -0
  50. package/dist/lib/configStash.js.map +1 -0
  51. package/dist/lib/getServerSideURL.d.ts +8 -0
  52. package/dist/lib/getServerSideURL.d.ts.map +1 -0
  53. package/dist/lib/getServerSideURL.js +12 -0
  54. package/dist/lib/getServerSideURL.js.map +1 -0
  55. package/dist/lib/mergeCollection.d.ts +17 -0
  56. package/dist/lib/mergeCollection.d.ts.map +1 -0
  57. package/dist/lib/mergeCollection.js +55 -0
  58. package/dist/lib/mergeCollection.js.map +1 -0
  59. package/dist/plugin.d.ts +128 -0
  60. package/dist/plugin.d.ts.map +1 -0
  61. package/dist/plugin.js +184 -0
  62. package/dist/plugin.js.map +1 -0
  63. package/dist/transform/coalesce.d.ts +16 -0
  64. package/dist/transform/coalesce.d.ts.map +1 -0
  65. package/dist/transform/coalesce.js +25 -0
  66. package/dist/transform/coalesce.js.map +1 -0
  67. package/dist/transform/geometry.d.ts +46 -0
  68. package/dist/transform/geometry.d.ts.map +1 -0
  69. package/dist/transform/geometry.js +64 -0
  70. package/dist/transform/geometry.js.map +1 -0
  71. package/dist/transform/getVariantBytes.d.ts +60 -0
  72. package/dist/transform/getVariantBytes.d.ts.map +1 -0
  73. package/dist/transform/getVariantBytes.js +163 -0
  74. package/dist/transform/getVariantBytes.js.map +1 -0
  75. package/dist/transform/limit.d.ts +5 -0
  76. package/dist/transform/limit.d.ts.map +1 -0
  77. package/dist/transform/limit.js +41 -0
  78. package/dist/transform/limit.js.map +1 -0
  79. package/dist/transform/lqip.d.ts +21 -0
  80. package/dist/transform/lqip.d.ts.map +1 -0
  81. package/dist/transform/lqip.js +32 -0
  82. package/dist/transform/lqip.js.map +1 -0
  83. package/dist/transform/params.d.ts +100 -0
  84. package/dist/transform/params.d.ts.map +1 -0
  85. package/dist/transform/params.js +144 -0
  86. package/dist/transform/params.js.map +1 -0
  87. package/dist/transform/sharp.d.ts +28 -0
  88. package/dist/transform/sharp.d.ts.map +1 -0
  89. package/dist/transform/sharp.js +85 -0
  90. package/dist/transform/sharp.js.map +1 -0
  91. package/dist/transform/sharpInstance.d.ts +21 -0
  92. package/dist/transform/sharpInstance.d.ts.map +1 -0
  93. package/dist/transform/sharpInstance.js +38 -0
  94. package/dist/transform/sharpInstance.js.map +1 -0
  95. package/dist/transform/source.d.ts +32 -0
  96. package/dist/transform/source.d.ts.map +1 -0
  97. package/dist/transform/source.js +121 -0
  98. package/dist/transform/source.js.map +1 -0
  99. package/dist/utils/urls.d.ts +108 -0
  100. package/dist/utils/urls.d.ts.map +1 -0
  101. package/dist/utils/urls.js +113 -0
  102. package/dist/utils/urls.js.map +1 -0
  103. package/dist/variants/key.d.ts +9 -0
  104. package/dist/variants/key.d.ts.map +1 -0
  105. package/dist/variants/key.js +29 -0
  106. package/dist/variants/key.js.map +1 -0
  107. package/package.json +98 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Server-only Sharp transform. `sharp` is a lazy import so the package carries no
3
+ * hard runtime dependency (it's an optional peer) and so client bundles never pull
4
+ * it in. The focal-crop geometry is split out as a pure function for unit testing.
5
+ */ import { coverCropGeometry, fitWithinSource } from "./geometry.js";
6
+ import { withTransformLimit } from "./limit.js";
7
+ import { mimeForFormat } from "./params.js";
8
+ import { loadSharp } from "./sharpInstance.js";
9
+ export { coverCropGeometry, coverObjectPosition, fitWithinSource } from "./geometry.js";
10
+ /** Default decompression-bomb / memory guard (~100MP); overridable per call via {@link TransformInput.maxInputPixels}. */ const MAX_INPUT_PIXELS = 100_000_000;
11
+ const encode = (pipeline, format, quality)=>{
12
+ switch(format){
13
+ case 'avif':
14
+ return pipeline.avif({
15
+ quality
16
+ });
17
+ case 'webp':
18
+ return pipeline.webp({
19
+ quality
20
+ });
21
+ case 'png':
22
+ return pipeline.png();
23
+ default:
24
+ // JPEG has no alpha channel — composite transparent sources (logos, contain letterboxing)
25
+ // onto white instead of Sharp's default black.
26
+ return pipeline.flatten({
27
+ background: '#ffffff'
28
+ }).jpeg({
29
+ quality,
30
+ mozjpeg: true
31
+ });
32
+ }
33
+ };
34
+ /**
35
+ * Transform a source image buffer: focal cover-crop (or plain resize for other fits) +
36
+ * encode. The CPU-bound work runs behind {@link withTransformLimit} so a burst of
37
+ * concurrent srcset requests can't saturate the host.
38
+ */ export const transformImage = (src, input)=>withTransformLimit(async ()=>{
39
+ const sharp = await loadSharp();
40
+ let pipeline = sharp(src, {
41
+ failOn: 'none',
42
+ limitInputPixels: input.maxInputPixels ?? MAX_INPUT_PIXELS
43
+ }).rotate();
44
+ const meta = await pipeline.metadata();
45
+ const swapped = (meta.orientation ?? 1) >= 5;
46
+ const sw = (swapped ? meta.height : meta.width) ?? 0;
47
+ const sh = (swapped ? meta.width : meta.height) ?? 0;
48
+ if (input.fit === 'cover' && input.w != null && input.h != null && sw > 0 && sh > 0) {
49
+ const { w: tw, h: th } = fitWithinSource(input.w, input.h, sw, sh);
50
+ const g = coverCropGeometry(sw, sh, tw, th, input.focalX ?? 50, input.focalY ?? 50);
51
+ pipeline = pipeline.resize(g.resizeWidth, g.resizeHeight).extract({
52
+ left: g.left,
53
+ top: g.top,
54
+ width: g.width,
55
+ height: g.height
56
+ });
57
+ } else {
58
+ // Transparent padding: 'contain' letterboxes when the box aspect differs from the source,
59
+ // and Sharp's default pad color is opaque black.
60
+ pipeline = pipeline.resize({
61
+ width: input.w,
62
+ height: input.h,
63
+ fit: input.fit,
64
+ withoutEnlargement: true,
65
+ background: {
66
+ r: 0,
67
+ g: 0,
68
+ b: 0,
69
+ alpha: 0
70
+ }
71
+ });
72
+ }
73
+ const { data, info } = await encode(pipeline, input.format, input.quality).toBuffer({
74
+ resolveWithObject: true
75
+ });
76
+ return {
77
+ data,
78
+ format: input.format,
79
+ width: info.width,
80
+ height: info.height,
81
+ mimeType: mimeForFormat(input.format)
82
+ };
83
+ });
84
+
85
+ //# sourceMappingURL=sharp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transform/sharp.ts"],"sourcesContent":["/**\n * Server-only Sharp transform. `sharp` is a lazy import so the package carries no\n * hard runtime dependency (it's an optional peer) and so client bundles never pull\n * it in. The focal-crop geometry is split out as a pure function for unit testing.\n */\nimport type { Sharp } from 'sharp'\n\nimport { coverCropGeometry, fitWithinSource } from './geometry'\nimport { withTransformLimit } from './limit'\nimport { type Fit, mimeForFormat, type OutputFormat } from './params'\nimport { loadSharp } from './sharpInstance'\n\nexport { coverCropGeometry, coverObjectPosition, fitWithinSource } from './geometry'\nexport type { CropGeometry } from './geometry'\n\nexport interface TransformInput {\n w?: number\n h?: number\n fit: Fit\n quality: number\n format: OutputFormat\n focalX?: number | null\n focalY?: number | null\n /** Max source pixels Sharp will decode (decompression-bomb + memory guard). Defaults to {@link MAX_INPUT_PIXELS}. */\n maxInputPixels?: number\n}\n\nexport interface TransformOutput {\n data: Buffer\n format: OutputFormat\n width: number\n height: number\n mimeType: string\n}\n\n/** Default decompression-bomb / memory guard (~100MP); overridable per call via {@link TransformInput.maxInputPixels}. */\nconst MAX_INPUT_PIXELS = 100_000_000\n\nconst encode = (pipeline: Sharp, format: OutputFormat, quality: number): Sharp => {\n switch (format) {\n case 'avif':\n return pipeline.avif({ quality })\n case 'webp':\n return pipeline.webp({ quality })\n case 'png':\n return pipeline.png()\n default:\n // JPEG has no alpha channel — composite transparent sources (logos, contain letterboxing)\n // onto white instead of Sharp's default black.\n return pipeline.flatten({ background: '#ffffff' }).jpeg({ quality, mozjpeg: true })\n }\n}\n\n/**\n * Transform a source image buffer: focal cover-crop (or plain resize for other fits) +\n * encode. The CPU-bound work runs behind {@link withTransformLimit} so a burst of\n * concurrent srcset requests can't saturate the host.\n */\nexport const transformImage = (src: Buffer, input: TransformInput): Promise<TransformOutput> =>\n withTransformLimit(async () => {\n const sharp = await loadSharp()\n let pipeline = sharp(src, { failOn: 'none', limitInputPixels: input.maxInputPixels ?? MAX_INPUT_PIXELS }).rotate()\n const meta = await pipeline.metadata()\n const swapped = (meta.orientation ?? 1) >= 5\n const sw = (swapped ? meta.height : meta.width) ?? 0\n const sh = (swapped ? meta.width : meta.height) ?? 0\n\n if (input.fit === 'cover' && input.w != null && input.h != null && sw > 0 && sh > 0) {\n const { w: tw, h: th } = fitWithinSource(input.w, input.h, sw, sh)\n const g = coverCropGeometry(sw, sh, tw, th, input.focalX ?? 50, input.focalY ?? 50)\n pipeline = pipeline.resize(g.resizeWidth, g.resizeHeight).extract({ left: g.left, top: g.top, width: g.width, height: g.height })\n } else {\n // Transparent padding: 'contain' letterboxes when the box aspect differs from the source,\n // and Sharp's default pad color is opaque black.\n pipeline = pipeline.resize({\n width: input.w,\n height: input.h,\n fit: input.fit,\n withoutEnlargement: true,\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n }\n\n const { data, info } = await encode(pipeline, input.format, input.quality).toBuffer({ resolveWithObject: true })\n return { data, format: input.format, width: info.width, height: info.height, mimeType: mimeForFormat(input.format) }\n })\n"],"names":["coverCropGeometry","fitWithinSource","withTransformLimit","mimeForFormat","loadSharp","coverObjectPosition","MAX_INPUT_PIXELS","encode","pipeline","format","quality","avif","webp","png","flatten","background","jpeg","mozjpeg","transformImage","src","input","sharp","failOn","limitInputPixels","maxInputPixels","rotate","meta","metadata","swapped","orientation","sw","height","width","sh","fit","w","h","tw","th","g","focalX","focalY","resize","resizeWidth","resizeHeight","extract","left","top","withoutEnlargement","r","b","alpha","data","info","toBuffer","resolveWithObject","mimeType"],"mappings":"AAAA;;;;CAIC,GAGD,SAASA,iBAAiB,EAAEC,eAAe,QAAQ,gBAAY;AAC/D,SAASC,kBAAkB,QAAQ,aAAS;AAC5C,SAAmBC,aAAa,QAA2B,cAAU;AACrE,SAASC,SAAS,QAAQ,qBAAiB;AAE3C,SAASJ,iBAAiB,EAAEK,mBAAmB,EAAEJ,eAAe,QAAQ,gBAAY;AAuBpF,wHAAwH,GACxH,MAAMK,mBAAmB;AAEzB,MAAMC,SAAS,CAACC,UAAiBC,QAAsBC;IACrD,OAAQD;QACN,KAAK;YACH,OAAOD,SAASG,IAAI,CAAC;gBAAED;YAAQ;QACjC,KAAK;YACH,OAAOF,SAASI,IAAI,CAAC;gBAAEF;YAAQ;QACjC,KAAK;YACH,OAAOF,SAASK,GAAG;QACrB;YACE,0FAA0F;YAC1F,+CAA+C;YAC/C,OAAOL,SAASM,OAAO,CAAC;gBAAEC,YAAY;YAAU,GAAGC,IAAI,CAAC;gBAAEN;gBAASO,SAAS;YAAK;IACrF;AACF;AAEA;;;;CAIC,GACD,OAAO,MAAMC,iBAAiB,CAACC,KAAaC,QAC1ClB,mBAAmB;QACjB,MAAMmB,QAAQ,MAAMjB;QACpB,IAAII,WAAWa,MAAMF,KAAK;YAAEG,QAAQ;YAAQC,kBAAkBH,MAAMI,cAAc,IAAIlB;QAAiB,GAAGmB,MAAM;QAChH,MAAMC,OAAO,MAAMlB,SAASmB,QAAQ;QACpC,MAAMC,UAAU,AAACF,CAAAA,KAAKG,WAAW,IAAI,CAAA,KAAM;QAC3C,MAAMC,KAAK,AAACF,CAAAA,UAAUF,KAAKK,MAAM,GAAGL,KAAKM,KAAK,AAAD,KAAM;QACnD,MAAMC,KAAK,AAACL,CAAAA,UAAUF,KAAKM,KAAK,GAAGN,KAAKK,MAAM,AAAD,KAAM;QAEnD,IAAIX,MAAMc,GAAG,KAAK,WAAWd,MAAMe,CAAC,IAAI,QAAQf,MAAMgB,CAAC,IAAI,QAAQN,KAAK,KAAKG,KAAK,GAAG;YACnF,MAAM,EAAEE,GAAGE,EAAE,EAAED,GAAGE,EAAE,EAAE,GAAGrC,gBAAgBmB,MAAMe,CAAC,EAAEf,MAAMgB,CAAC,EAAEN,IAAIG;YAC/D,MAAMM,IAAIvC,kBAAkB8B,IAAIG,IAAII,IAAIC,IAAIlB,MAAMoB,MAAM,IAAI,IAAIpB,MAAMqB,MAAM,IAAI;YAChFjC,WAAWA,SAASkC,MAAM,CAACH,EAAEI,WAAW,EAAEJ,EAAEK,YAAY,EAAEC,OAAO,CAAC;gBAAEC,MAAMP,EAAEO,IAAI;gBAAEC,KAAKR,EAAEQ,GAAG;gBAAEf,OAAOO,EAAEP,KAAK;gBAAED,QAAQQ,EAAER,MAAM;YAAC;QACjI,OAAO;YACL,0FAA0F;YAC1F,iDAAiD;YACjDvB,WAAWA,SAASkC,MAAM,CAAC;gBACzBV,OAAOZ,MAAMe,CAAC;gBACdJ,QAAQX,MAAMgB,CAAC;gBACfF,KAAKd,MAAMc,GAAG;gBACdc,oBAAoB;gBACpBjC,YAAY;oBAAEkC,GAAG;oBAAGV,GAAG;oBAAGW,GAAG;oBAAGC,OAAO;gBAAE;YAC3C;QACF;QAEA,MAAM,EAAEC,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAM9C,OAAOC,UAAUY,MAAMX,MAAM,EAAEW,MAAMV,OAAO,EAAE4C,QAAQ,CAAC;YAAEC,mBAAmB;QAAK;QAC9G,OAAO;YAAEH;YAAM3C,QAAQW,MAAMX,MAAM;YAAEuB,OAAOqB,KAAKrB,KAAK;YAAED,QAAQsB,KAAKtB,MAAM;YAAEyB,UAAUrD,cAAciB,MAAMX,MAAM;QAAE;IACrH,GAAE"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Lazy Sharp loader with one-time global tuning. `sharp` is an optional peer and kept
3
+ * out of client bundles, so it's imported on first use. On first load we cap the
4
+ * per-image libvips thread pool: the transform endpoint's {@link withTransformLimit}
5
+ * gate already provides cross-image parallelism, so each transform defaults to a single
6
+ * libvips thread to bound memory on serverless (libaom still manages its own threads
7
+ * for AVIF regardless of this). Override via the endpoint's `sharpConcurrency` option or
8
+ * `IMAGES_SHARP_CONCURRENCY`; `0` resets Sharp to its default (CPU-core count).
9
+ *
10
+ * The libvips operation cache stays at Sharp's defaults (50MB / 100 ops) — EXCEPT its
11
+ * file-descriptor cache on Windows: libvips keeps up to 20 recently-read files OPEN, and
12
+ * Windows can't unlink an open file, so a transformed original would block its own
13
+ * deletion (a reseed, a replaced upload) for as long as the process lives. POSIX unlinks
14
+ * open files fine, so the fd cache is kept there.
15
+ */
16
+ type SharpFactory = typeof import('sharp')['default'];
17
+ /** Set the per-image libvips thread cap applied on the next Sharp load (negative ignored). */
18
+ export declare const setSharpConcurrency: (n?: number) => void;
19
+ export declare const loadSharp: () => Promise<SharpFactory>;
20
+ export {};
21
+ //# sourceMappingURL=sharpInstance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharpInstance.d.ts","sourceRoot":"","sources":["../../src/transform/sharpInstance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,KAAK,YAAY,GAAG,cAAc,OAAO,CAAC,CAAC,SAAS,CAAC,CAAA;AAKrD,8FAA8F;AAC9F,eAAO,MAAM,mBAAmB,GAAI,IAAI,MAAM,KAAG,IAEhD,CAAA;AAED,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,YAAY,CAYtD,CAAA"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Lazy Sharp loader with one-time global tuning. `sharp` is an optional peer and kept
3
+ * out of client bundles, so it's imported on first use. On first load we cap the
4
+ * per-image libvips thread pool: the transform endpoint's {@link withTransformLimit}
5
+ * gate already provides cross-image parallelism, so each transform defaults to a single
6
+ * libvips thread to bound memory on serverless (libaom still manages its own threads
7
+ * for AVIF regardless of this). Override via the endpoint's `sharpConcurrency` option or
8
+ * `IMAGES_SHARP_CONCURRENCY`; `0` resets Sharp to its default (CPU-core count).
9
+ *
10
+ * The libvips operation cache stays at Sharp's defaults (50MB / 100 ops) — EXCEPT its
11
+ * file-descriptor cache on Windows: libvips keeps up to 20 recently-read files OPEN, and
12
+ * Windows can't unlink an open file, so a transformed original would block its own
13
+ * deletion (a reseed, a replaced upload) for as long as the process lives. POSIX unlinks
14
+ * open files fine, so the fd cache is kept there.
15
+ */ let override;
16
+ let configured = false;
17
+ /** Set the per-image libvips thread cap applied on the next Sharp load (negative ignored). */ export const setSharpConcurrency = (n)=>{
18
+ if (n != null && Number.isFinite(n) && n >= 0) override = Math.floor(n);
19
+ };
20
+ export const loadSharp = async ()=>{
21
+ const sharp = (await import("sharp")).default;
22
+ if (!configured) {
23
+ configured = true;
24
+ const env = Number(process.env.IMAGES_SHARP_CONCURRENCY);
25
+ const n = override ?? (Number.isFinite(env) && env >= 0 ? Math.floor(env) : 1);
26
+ try {
27
+ sharp.concurrency(n);
28
+ if (process.platform === 'win32') sharp.cache({
29
+ memory: 50,
30
+ files: 0,
31
+ items: 100
32
+ });
33
+ } catch {}
34
+ }
35
+ return sharp;
36
+ };
37
+
38
+ //# sourceMappingURL=sharpInstance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transform/sharpInstance.ts"],"sourcesContent":["/**\n * Lazy Sharp loader with one-time global tuning. `sharp` is an optional peer and kept\n * out of client bundles, so it's imported on first use. On first load we cap the\n * per-image libvips thread pool: the transform endpoint's {@link withTransformLimit}\n * gate already provides cross-image parallelism, so each transform defaults to a single\n * libvips thread to bound memory on serverless (libaom still manages its own threads\n * for AVIF regardless of this). Override via the endpoint's `sharpConcurrency` option or\n * `IMAGES_SHARP_CONCURRENCY`; `0` resets Sharp to its default (CPU-core count).\n *\n * The libvips operation cache stays at Sharp's defaults (50MB / 100 ops) — EXCEPT its\n * file-descriptor cache on Windows: libvips keeps up to 20 recently-read files OPEN, and\n * Windows can't unlink an open file, so a transformed original would block its own\n * deletion (a reseed, a replaced upload) for as long as the process lives. POSIX unlinks\n * open files fine, so the fd cache is kept there.\n */\ntype SharpFactory = typeof import('sharp')['default']\n\nlet override: number | undefined\nlet configured = false\n\n/** Set the per-image libvips thread cap applied on the next Sharp load (negative ignored). */\nexport const setSharpConcurrency = (n?: number): void => {\n if (n != null && Number.isFinite(n) && n >= 0) override = Math.floor(n)\n}\n\nexport const loadSharp = async (): Promise<SharpFactory> => {\n const sharp = (await import('sharp')).default\n if (!configured) {\n configured = true\n const env = Number(process.env.IMAGES_SHARP_CONCURRENCY)\n const n = override ?? (Number.isFinite(env) && env >= 0 ? Math.floor(env) : 1)\n try {\n sharp.concurrency(n)\n if (process.platform === 'win32') sharp.cache({ memory: 50, files: 0, items: 100 })\n } catch {}\n }\n return sharp\n}\n"],"names":["override","configured","setSharpConcurrency","n","Number","isFinite","Math","floor","loadSharp","sharp","default","env","process","IMAGES_SHARP_CONCURRENCY","concurrency","platform","cache","memory","files","items"],"mappings":"AAAA;;;;;;;;;;;;;;CAcC,GAGD,IAAIA;AACJ,IAAIC,aAAa;AAEjB,4FAA4F,GAC5F,OAAO,MAAMC,sBAAsB,CAACC;IAClC,IAAIA,KAAK,QAAQC,OAAOC,QAAQ,CAACF,MAAMA,KAAK,GAAGH,WAAWM,KAAKC,KAAK,CAACJ;AACvE,EAAC;AAED,OAAO,MAAMK,YAAY;IACvB,MAAMC,QAAQ,AAAC,CAAA,MAAM,MAAM,CAAC,QAAO,EAAGC,OAAO;IAC7C,IAAI,CAACT,YAAY;QACfA,aAAa;QACb,MAAMU,MAAMP,OAAOQ,QAAQD,GAAG,CAACE,wBAAwB;QACvD,MAAMV,IAAIH,YAAaI,CAAAA,OAAOC,QAAQ,CAACM,QAAQA,OAAO,IAAIL,KAAKC,KAAK,CAACI,OAAO,CAAA;QAC5E,IAAI;YACFF,MAAMK,WAAW,CAACX;YAClB,IAAIS,QAAQG,QAAQ,KAAK,SAASN,MAAMO,KAAK,CAAC;gBAAEC,QAAQ;gBAAIC,OAAO;gBAAGC,OAAO;YAAI;QACnF,EAAE,OAAM,CAAC;IACX;IACA,OAAOV;AACT,EAAC"}
@@ -0,0 +1,32 @@
1
+ import { type Payload } from 'payload';
2
+ export interface UploadDocLike {
3
+ filename?: string | null;
4
+ url?: string | null;
5
+ /** Cloud-storage adapters store their key prefix on the doc. */
6
+ prefix?: string | null;
7
+ }
8
+ /**
9
+ * SSRF guard for the remote-source fetch: always allow the configured server origin
10
+ * itself (so self-fetching Payload's own static route works, even on localhost in
11
+ * dev), but otherwise refuse private/loopback/link-local hosts — blocking cloud
12
+ * metadata endpoints and internal services reachable from a malicious `url`.
13
+ */
14
+ export declare const isAllowedFetchTarget: (target: URL, trusted: URL | null) => boolean;
15
+ /** Resolve a collection's on-disk upload directory (absolute). */
16
+ export declare const resolveStaticDir: (payload: Payload, slug: string) => string;
17
+ /**
18
+ * Read an upload's bytes. In order:
19
+ * 1. the local file under `staticDir` (verifying the resolved path stays inside it),
20
+ * 2. the collection's storage-adapter `upload.handlers`, invoked directly (cloud storage — no
21
+ * HTTP, no access control; pass `via` to enable),
22
+ * 3. the absolute URL the doc reports (cloud storage), or
23
+ * 4. its relative URL resolved against `baseUrl` — i.e. self-fetch Payload's own
24
+ * static file route, which serves the file whatever the adapter/storage is.
25
+ *
26
+ * Returns null when nothing yields bytes.
27
+ */
28
+ export declare const readBytes: (doc: UploadDocLike, staticDir: string, baseUrl?: string, via?: {
29
+ payload: Payload;
30
+ slug: string;
31
+ }) => Promise<Buffer | null>;
32
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/transform/source.ts"],"names":[],"mappings":"AAQA,OAAO,EAAkB,KAAK,OAAO,EAAuB,MAAM,SAAS,CAAA;AAE3E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AA4BD;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,SAAS,GAAG,GAAG,IAAI,KAAG,OAGvE,CAAA;AAED,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,GAAI,SAAS,OAAO,EAAE,MAAM,MAAM,KAAG,MAKjE,CAAA;AAyBD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GACpB,KAAK,aAAa,EAClB,WAAW,MAAM,EACjB,UAAU,MAAM,EAChB,MAAM;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACvC,OAAO,CAAC,MAAM,GAAG,IAAI,CAuCvB,CAAA"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Read the raw bytes of an upload doc from either local disk (the collection's
3
+ * `staticDir`) or cloud storage (Vercel Blob, S3, …) by fetching the absolute URL
4
+ * Payload reports. Includes a path-traversal guard on the local read, a bounded fetch
5
+ * timeout, and an SSRF guard on the remote-fetch path.
6
+ */ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { createLocalReq } from "payload";
9
+ const MAX_FETCH_BYTES = 64 * 1024 * 1024;
10
+ /** True for hostnames in loopback / private / link-local space (the SSRF sinks). */ const isPrivateHost = (hostname)=>{
11
+ const h = hostname.toLowerCase().replace(/^\[|\]$/g, '');
12
+ if (h === 'localhost' || h.endsWith('.localhost') || h === '0.0.0.0' || h === '::1') return true;
13
+ const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
14
+ if (m) {
15
+ const a = Number(m[1]);
16
+ const b = Number(m[2]);
17
+ if (a === 0 || a === 10 || a === 127) return true;
18
+ if (a === 169 && b === 254) return true;
19
+ if (a === 192 && b === 168) return true;
20
+ if (a === 172 && b >= 16 && b <= 31) return true;
21
+ if (a === 100 && b >= 64 && b <= 127) return true;
22
+ return false;
23
+ }
24
+ return /^fe80:/i.test(h) || /^f[cd][0-9a-f]{2}:/i.test(h);
25
+ };
26
+ /**
27
+ * SSRF guard for the remote-source fetch: always allow the configured server origin
28
+ * itself (so self-fetching Payload's own static route works, even on localhost in
29
+ * dev), but otherwise refuse private/loopback/link-local hosts — blocking cloud
30
+ * metadata endpoints and internal services reachable from a malicious `url`.
31
+ */ export const isAllowedFetchTarget = (target, trusted)=>{
32
+ if (trusted && target.host === trusted.host) return true;
33
+ return !isPrivateHost(target.hostname);
34
+ };
35
+ /** Resolve a collection's on-disk upload directory (absolute). */ export const resolveStaticDir = (payload, slug)=>{
36
+ const collections = payload.collections;
37
+ const dir = collections?.[slug]?.config?.upload?.staticDir;
38
+ const base = dir?.length ? dir : slug;
39
+ return path.isAbsolute(base) ? base : path.resolve(process.cwd(), base);
40
+ };
41
+ /**
42
+ * Read the bytes through the collection's storage-adapter `upload.handlers` (Vercel Blob, S3, …).
43
+ * This is the same server-side path Payload's own file route takes after its access check —
44
+ * invoked directly, not over HTTP, so it needs no origin, cookie, or open read access. (The
45
+ * self-fetch fallback below can't serve an access-controlled collection like `generated-images`:
46
+ * its unauthenticated request 403s and every variant read becomes a cache miss.)
47
+ */ const readViaStorageHandlers = async (payload, slug, doc)=>{
48
+ const collections = payload.collections;
49
+ const handlers = collections?.[slug]?.config?.upload?.handlers;
50
+ if (!handlers?.length || !doc.filename) return null;
51
+ try {
52
+ const req = await createLocalReq({}, payload);
53
+ for (const handler of handlers){
54
+ const res = await handler(req, {
55
+ doc,
56
+ params: {
57
+ collection: slug,
58
+ filename: doc.filename,
59
+ prefix: doc.prefix ?? undefined
60
+ }
61
+ });
62
+ if (res instanceof Response) return res.ok ? Buffer.from(await res.arrayBuffer()) : null;
63
+ }
64
+ } catch {
65
+ // fall through to the URL fetch
66
+ }
67
+ return null;
68
+ };
69
+ /**
70
+ * Read an upload's bytes. In order:
71
+ * 1. the local file under `staticDir` (verifying the resolved path stays inside it),
72
+ * 2. the collection's storage-adapter `upload.handlers`, invoked directly (cloud storage — no
73
+ * HTTP, no access control; pass `via` to enable),
74
+ * 3. the absolute URL the doc reports (cloud storage), or
75
+ * 4. its relative URL resolved against `baseUrl` — i.e. self-fetch Payload's own
76
+ * static file route, which serves the file whatever the adapter/storage is.
77
+ *
78
+ * Returns null when nothing yields bytes.
79
+ */ export const readBytes = async (doc, staticDir, baseUrl, via)=>{
80
+ if (doc.filename) {
81
+ const base = path.resolve(staticDir);
82
+ const filePath = path.resolve(base, doc.filename);
83
+ if ((filePath === base || filePath.startsWith(base + path.sep)) && fs.existsSync(filePath)) return fs.readFileSync(filePath);
84
+ }
85
+ if (via) {
86
+ const bytes = await readViaStorageHandlers(via.payload, via.slug, doc);
87
+ if (bytes) return bytes;
88
+ }
89
+ if (typeof doc.url === 'string' && doc.url) {
90
+ const isAbsolute = /^https?:\/\//i.test(doc.url);
91
+ const base = (baseUrl ?? '').replace(/\/$/, '');
92
+ const targetStr = isAbsolute ? doc.url : base ? `${base}${doc.url.startsWith('/') ? '' : '/'}${doc.url}` : '';
93
+ if (/^https?:\/\//i.test(targetStr)) {
94
+ let target;
95
+ let trusted = null;
96
+ try {
97
+ target = new URL(targetStr);
98
+ } catch {
99
+ return null;
100
+ }
101
+ try {
102
+ trusted = base ? new URL(base) : null;
103
+ } catch {}
104
+ if (!isAllowedFetchTarget(target, trusted)) return null;
105
+ try {
106
+ const res = await fetch(targetStr, {
107
+ signal: AbortSignal.timeout(15_000),
108
+ redirect: 'manual'
109
+ });
110
+ if (!res.ok) return null;
111
+ const declared = Number(res.headers?.get?.('content-length') ?? '');
112
+ if (Number.isFinite(declared) && declared > MAX_FETCH_BYTES) return null;
113
+ const buf = Buffer.from(await res.arrayBuffer());
114
+ return buf.byteLength > MAX_FETCH_BYTES ? null : buf;
115
+ } catch {}
116
+ }
117
+ }
118
+ return null;
119
+ };
120
+
121
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transform/source.ts"],"sourcesContent":["/**\n * Read the raw bytes of an upload doc from either local disk (the collection's\n * `staticDir`) or cloud storage (Vercel Blob, S3, …) by fetching the absolute URL\n * Payload reports. Includes a path-traversal guard on the local read, a bounded fetch\n * timeout, and an SSRF guard on the remote-fetch path.\n */\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { createLocalReq, type Payload, type PayloadRequest } from 'payload'\n\nexport interface UploadDocLike {\n filename?: string | null\n url?: string | null\n /** Cloud-storage adapters store their key prefix on the doc. */\n prefix?: string | null\n}\n\ntype UploadHandler = (\n req: PayloadRequest,\n args: { doc: unknown; headers?: Headers; params: { collection: string; filename: string; prefix?: string } },\n // biome-ignore lint/suspicious/noConfusingVoidType: mirrors Payload's upload handler signature, which returns void\n) => Promise<Response | void> | Response | void\n\nconst MAX_FETCH_BYTES = 64 * 1024 * 1024\n\n/** True for hostnames in loopback / private / link-local space (the SSRF sinks). */\nconst isPrivateHost = (hostname: string): boolean => {\n const h = hostname.toLowerCase().replace(/^\\[|\\]$/g, '')\n if (h === 'localhost' || h.endsWith('.localhost') || h === '0.0.0.0' || h === '::1') return true\n const m = h.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/)\n if (m) {\n const a = Number(m[1])\n const b = Number(m[2])\n if (a === 0 || a === 10 || a === 127) return true\n if (a === 169 && b === 254) return true\n if (a === 192 && b === 168) return true\n if (a === 172 && b >= 16 && b <= 31) return true\n if (a === 100 && b >= 64 && b <= 127) return true\n return false\n }\n return /^fe80:/i.test(h) || /^f[cd][0-9a-f]{2}:/i.test(h)\n}\n\n/**\n * SSRF guard for the remote-source fetch: always allow the configured server origin\n * itself (so self-fetching Payload's own static route works, even on localhost in\n * dev), but otherwise refuse private/loopback/link-local hosts — blocking cloud\n * metadata endpoints and internal services reachable from a malicious `url`.\n */\nexport const isAllowedFetchTarget = (target: URL, trusted: URL | null): boolean => {\n if (trusted && target.host === trusted.host) return true\n return !isPrivateHost(target.hostname)\n}\n\n/** Resolve a collection's on-disk upload directory (absolute). */\nexport const resolveStaticDir = (payload: Payload, slug: string): string => {\n const collections = payload.collections as Record<string, { config?: { upload?: { staticDir?: string } } }>\n const dir = collections?.[slug]?.config?.upload?.staticDir\n const base = dir?.length ? dir : slug\n return path.isAbsolute(base) ? base : path.resolve(process.cwd(), base)\n}\n\n/**\n * Read the bytes through the collection's storage-adapter `upload.handlers` (Vercel Blob, S3, …).\n * This is the same server-side path Payload's own file route takes after its access check —\n * invoked directly, not over HTTP, so it needs no origin, cookie, or open read access. (The\n * self-fetch fallback below can't serve an access-controlled collection like `generated-images`:\n * its unauthenticated request 403s and every variant read becomes a cache miss.)\n */\nconst readViaStorageHandlers = async (payload: Payload, slug: string, doc: UploadDocLike): Promise<Buffer | null> => {\n const collections = payload.collections as Record<string, { config?: { upload?: { handlers?: UploadHandler[] } } }>\n const handlers = collections?.[slug]?.config?.upload?.handlers\n if (!handlers?.length || !doc.filename) return null\n try {\n const req = await createLocalReq({}, payload)\n for (const handler of handlers) {\n const res = await handler(req, { doc, params: { collection: slug, filename: doc.filename, prefix: doc.prefix ?? undefined } })\n if (res instanceof Response) return res.ok ? Buffer.from(await res.arrayBuffer()) : null\n }\n } catch {\n // fall through to the URL fetch\n }\n return null\n}\n\n/**\n * Read an upload's bytes. In order:\n * 1. the local file under `staticDir` (verifying the resolved path stays inside it),\n * 2. the collection's storage-adapter `upload.handlers`, invoked directly (cloud storage — no\n * HTTP, no access control; pass `via` to enable),\n * 3. the absolute URL the doc reports (cloud storage), or\n * 4. its relative URL resolved against `baseUrl` — i.e. self-fetch Payload's own\n * static file route, which serves the file whatever the adapter/storage is.\n *\n * Returns null when nothing yields bytes.\n */\nexport const readBytes = async (\n doc: UploadDocLike,\n staticDir: string,\n baseUrl?: string,\n via?: { payload: Payload; slug: string },\n): Promise<Buffer | null> => {\n if (doc.filename) {\n const base = path.resolve(staticDir)\n const filePath = path.resolve(base, doc.filename)\n if ((filePath === base || filePath.startsWith(base + path.sep)) && fs.existsSync(filePath)) return fs.readFileSync(filePath)\n }\n\n if (via) {\n const bytes = await readViaStorageHandlers(via.payload, via.slug, doc)\n if (bytes) return bytes\n }\n\n if (typeof doc.url === 'string' && doc.url) {\n const isAbsolute = /^https?:\\/\\//i.test(doc.url)\n const base = (baseUrl ?? '').replace(/\\/$/, '')\n const targetStr = isAbsolute ? doc.url : base ? `${base}${doc.url.startsWith('/') ? '' : '/'}${doc.url}` : ''\n if (/^https?:\\/\\//i.test(targetStr)) {\n let target: URL\n let trusted: URL | null = null\n try {\n target = new URL(targetStr)\n } catch {\n return null\n }\n try {\n trusted = base ? new URL(base) : null\n } catch {}\n if (!isAllowedFetchTarget(target, trusted)) return null\n try {\n const res = await fetch(targetStr, { signal: AbortSignal.timeout(15_000), redirect: 'manual' })\n if (!res.ok) return null\n const declared = Number(res.headers?.get?.('content-length') ?? '')\n if (Number.isFinite(declared) && declared > MAX_FETCH_BYTES) return null\n const buf = Buffer.from(await res.arrayBuffer())\n return buf.byteLength > MAX_FETCH_BYTES ? null : buf\n } catch {}\n }\n }\n return null\n}\n"],"names":["fs","path","createLocalReq","MAX_FETCH_BYTES","isPrivateHost","hostname","h","toLowerCase","replace","endsWith","m","match","a","Number","b","test","isAllowedFetchTarget","target","trusted","host","resolveStaticDir","payload","slug","collections","dir","config","upload","staticDir","base","length","isAbsolute","resolve","process","cwd","readViaStorageHandlers","doc","handlers","filename","req","handler","res","params","collection","prefix","undefined","Response","ok","Buffer","from","arrayBuffer","readBytes","baseUrl","via","filePath","startsWith","sep","existsSync","readFileSync","bytes","url","targetStr","URL","fetch","signal","AbortSignal","timeout","redirect","declared","headers","get","isFinite","buf","byteLength"],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Isomorphic URL builders for the on-demand transform endpoint. NO Node / Sharp / server imports —
3
+ * safe to bundle into client components. `buildVariantUrl` / `buildSrcset` produce same-origin
4
+ * relative URLs by default (pass `baseUrl` for absolute); `getImageUrl` — the one-off/shareable
5
+ * helper — instead defaults `baseUrl` to `process.env.NEXT_PUBLIC_SERVER_URL` (a Next build-time
6
+ * constant, inlined in client bundles) so OG/email URLs come out absolute with no extra wiring.
7
+ */
8
+ import { DEFAULT_PIXEL_STEP, type Fit, type Format } from '../transform/params';
9
+ export { DEFAULT_PIXEL_STEP };
10
+ /**
11
+ * Default base for transform URLs: `/api` + the endpoint's fixed `/img` path. Override
12
+ * `path` only if your Payload API route or Next.js basePath differs from the default.
13
+ */
14
+ export declare const DEFAULT_TRANSFORM_API_PATH = "/api/img";
15
+ export interface BuildUrlOptions {
16
+ fit?: Fit;
17
+ quality?: number;
18
+ format?: Format;
19
+ /** Render aspect ratio (`16/9` | `"16:9"`); derives `h` from each width. */
20
+ aspectRatio?: number | string;
21
+ /** Prefix for absolute URLs (e.g. `https://site.com`). Default '' (same-origin). */
22
+ baseUrl?: string;
23
+ /** Endpoint base. Default `/api/img` ({@link DEFAULT_TRANSFORM_API_PATH}); override only for a custom API route / basePath. */
24
+ path?: string;
25
+ /**
26
+ * Cache-busting version token appended as `v=`. Derive it from the source doc with
27
+ * {@link deriveVersion} so replacing the file or moving the focal point yields a new
28
+ * URL — busting immutable browser/CDN caches (the server ignores `v`, reading focal
29
+ * from the doc; it exists only to make the immutable URL honest).
30
+ */
31
+ version?: string;
32
+ }
33
+ /** Source-identity fields that determine the rendered pixels (independent of size/quality). */
34
+ export interface VersionSource {
35
+ filename?: string | null;
36
+ focalX?: number | null;
37
+ focalY?: number | null;
38
+ }
39
+ /**
40
+ * A short, stable version token from the source-identity fields that change the
41
+ * rendered pixels (filename + focal point) — the same inputs the server folds into
42
+ * the variant cache key. Returns undefined when no identity is available (a bare id),
43
+ * in which case URLs carry no `v` — so pass a populated doc to get cache-correct
44
+ * busting on file replace / focal edits.
45
+ */
46
+ export declare const deriveVersion: (src?: VersionSource | null) => string | undefined;
47
+ /** A bare id, or a populated image doc (so the version token + a default width can be read off it). */
48
+ export type ImageResource = string | number | ({
49
+ id: string | number;
50
+ width?: number | null;
51
+ } & VersionSource) | null | undefined;
52
+ export interface GetImageUrlOptions extends BuildUrlOptions {
53
+ /** Output width. Falls back to a populated doc's intrinsic width, else 1280. */
54
+ width?: number;
55
+ /** Prefix for absolute URLs. Defaults to `NEXT_PUBLIC_SERVER_URL`; pass `''` for a relative URL. */
56
+ baseUrl?: string;
57
+ }
58
+ /**
59
+ * One transform URL for an image, taking the id OR a populated doc directly: it resolves the
60
+ * id, picks a sensible default width, and (for a doc) folds in the cache-busting version
61
+ * automatically — so you don't re-implement the resolve-and-`deriveVersion` dance every time
62
+ * you need a raw URL (OG tags, CSS backgrounds, emails). Returns null when there's no id.
63
+ * For a responsive `<img>`, prefer `<ResponsiveImage>` / {@link buildSrcset}.
64
+ *
65
+ * Because these URLs are usually for external/shareable contexts (OG, email), `baseUrl` defaults
66
+ * to `process.env.NEXT_PUBLIC_SERVER_URL` so the result is absolute with no extra wiring. Pass an
67
+ * explicit `baseUrl` to override, or `baseUrl: ''` to force a relative same-origin URL.
68
+ */
69
+ export declare const getImageUrl: (resource: ImageResource, o?: GetImageUrlOptions) => string | null;
70
+ export declare const buildVariantUrl: (id: string, width: number, o?: BuildUrlOptions) => string;
71
+ /**
72
+ * The widths for a srcset. Two modes via `pixelStep`:
73
+ *
74
+ * - a **number** (the step): every multiple of it up to the source's intrinsic width,
75
+ * then the exact source width as the final candidate (so the srcset always tops out
76
+ * at the true native resolution, not the largest step multiple below it). With no
77
+ * source width, steps up to `maxWidth`.
78
+ * - an **array** (an explicit, possibly non-linear ladder à la a curated width list):
79
+ * the ladder values that fall below the source width, then the exact source width on
80
+ * top. With no source width, the ladder is used as-is (its author-chosen cap is kept).
81
+ *
82
+ * Either way no width exceeds the source (no upscaling) or `maxWidth`. The entry count is
83
+ * bounded by the size ceiling: a bigger step / a shorter ladder / a smaller source yields
84
+ * fewer widths (and so fewer cached variants).
85
+ */
86
+ export declare const stepWidths: (sourceWidth?: number, pixelStep?: number | number[], maxWidth?: number) => number[];
87
+ export interface BuildSrcsetOptions extends BuildUrlOptions {
88
+ /**
89
+ * The srcset widths. A number is the step increment (default 50; bigger = fewer widths).
90
+ * An array is an explicit, curated ladder of widths (e.g. `[200, 450, 750, 1200, 2000]`)
91
+ * — non-linear, denser where it matters, fewer entries than a fine linear step.
92
+ */
93
+ pixelStep?: number | number[];
94
+ /** The source image's intrinsic width — caps the srcset (no upscaling). */
95
+ sourceWidth?: number;
96
+ /** Hard ceiling. Default 4096. */
97
+ maxWidth?: number;
98
+ /** Width used for the plain `src` fallback. Defaults to min(top, 1280). */
99
+ defaultWidth?: number;
100
+ }
101
+ export interface BuildSrcsetResult {
102
+ srcset: string;
103
+ src: string;
104
+ }
105
+ /** Build a responsive `srcset` (pixelStep multiples up to the source width) + a default `src`. */
106
+ export declare const buildSrcset: (id: string, o?: BuildSrcsetOptions) => BuildSrcsetResult;
107
+ export type { Fit, Format };
108
+ //# sourceMappingURL=urls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urls.d.ts","sourceRoot":"","sources":["../../src/utils/urls.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,KAAK,GAAG,EAAE,KAAK,MAAM,EAAoB,MAAM,qBAAqB,CAAA;AAEjG,OAAO,EAAE,kBAAkB,EAAE,CAAA;AAE7B;;;GAGG;AACH,eAAO,MAAM,0BAA0B,aAAa,CAAA;AAEpD,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,GAAG,CAAA;IACT,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC7B,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+HAA+H;IAC/H,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,+FAA+F;AAC/F,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAWD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,aAAa,GAAG,IAAI,KAAG,MAAM,GAAG,SAKnE,CAAA;AAED,uGAAuG;AACvG,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,aAAa,CAAC,GAAG,IAAI,GAAG,SAAS,CAAA;AAEjI,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oGAAoG;IACpG,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,aAAa,EAAE,IAAG,kBAAuB,KAAG,MAAM,GAAG,IAW1F,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,EAAE,OAAO,MAAM,EAAE,IAAG,eAAoB,KAAG,MAYpF,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,GAAI,cAAc,MAAM,EAAE,YAAW,MAAM,GAAG,MAAM,EAAuB,EAAE,iBAAe,KAAG,MAAM,EAwB3H,CAAA;AAED,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC7B,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,kGAAkG;AAClG,eAAO,MAAM,WAAW,GAAI,IAAI,MAAM,EAAE,IAAG,kBAAuB,KAAG,iBAMpE,CAAA;AAED,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,CAAA"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Isomorphic URL builders for the on-demand transform endpoint. NO Node / Sharp / server imports —
3
+ * safe to bundle into client components. `buildVariantUrl` / `buildSrcset` produce same-origin
4
+ * relative URLs by default (pass `baseUrl` for absolute); `getImageUrl` — the one-off/shareable
5
+ * helper — instead defaults `baseUrl` to `process.env.NEXT_PUBLIC_SERVER_URL` (a Next build-time
6
+ * constant, inlined in client bundles) so OG/email URLs come out absolute with no extra wiring.
7
+ */ import { DEFAULT_PIXEL_STEP, parseAspectRatio } from "../transform/params.js";
8
+ export { DEFAULT_PIXEL_STEP };
9
+ /**
10
+ * Default base for transform URLs: `/api` + the endpoint's fixed `/img` path. Override
11
+ * `path` only if your Payload API route or Next.js basePath differs from the default.
12
+ */ export const DEFAULT_TRANSFORM_API_PATH = '/api/img';
13
+ const fnv1a = (s)=>{
14
+ let h = 0x811c9dc5;
15
+ for(let i = 0; i < s.length; i++){
16
+ h ^= s.charCodeAt(i);
17
+ h = Math.imul(h, 0x01000193);
18
+ }
19
+ return (h >>> 0).toString(36);
20
+ };
21
+ /**
22
+ * A short, stable version token from the source-identity fields that change the
23
+ * rendered pixels (filename + focal point) — the same inputs the server folds into
24
+ * the variant cache key. Returns undefined when no identity is available (a bare id),
25
+ * in which case URLs carry no `v` — so pass a populated doc to get cache-correct
26
+ * busting on file replace / focal edits.
27
+ */ export const deriveVersion = (src)=>{
28
+ if (!src) return undefined;
29
+ const { filename, focalX, focalY } = src;
30
+ if (filename == null && focalX == null && focalY == null) return undefined;
31
+ return fnv1a(`${filename ?? ''}|${focalX ?? ''}|${focalY ?? ''}`);
32
+ };
33
+ /**
34
+ * One transform URL for an image, taking the id OR a populated doc directly: it resolves the
35
+ * id, picks a sensible default width, and (for a doc) folds in the cache-busting version
36
+ * automatically — so you don't re-implement the resolve-and-`deriveVersion` dance every time
37
+ * you need a raw URL (OG tags, CSS backgrounds, emails). Returns null when there's no id.
38
+ * For a responsive `<img>`, prefer `<ResponsiveImage>` / {@link buildSrcset}.
39
+ *
40
+ * Because these URLs are usually for external/shareable contexts (OG, email), `baseUrl` defaults
41
+ * to `process.env.NEXT_PUBLIC_SERVER_URL` so the result is absolute with no extra wiring. Pass an
42
+ * explicit `baseUrl` to override, or `baseUrl: ''` to force a relative same-origin URL.
43
+ */ export const getImageUrl = (resource, o = {})=>{
44
+ if (resource == null) return null;
45
+ const doc = typeof resource === 'object' ? resource : undefined;
46
+ const id = doc ? doc.id == null ? '' : String(doc.id) : String(resource);
47
+ if (!id) return null;
48
+ const width = o.width ?? doc?.width ?? 1280;
49
+ return buildVariantUrl(id, width, {
50
+ ...o,
51
+ baseUrl: o.baseUrl ?? (process.env.NEXT_PUBLIC_SERVER_URL || undefined),
52
+ version: o.version ?? deriveVersion(doc)
53
+ });
54
+ };
55
+ export const buildVariantUrl = (id, width, o = {})=>{
56
+ const base = o.baseUrl ?? '';
57
+ const apiPath = o.path ?? DEFAULT_TRANSFORM_API_PATH;
58
+ const ar = parseAspectRatio(o.aspectRatio);
59
+ const params = new URLSearchParams();
60
+ params.set('w', String(Math.round(width)));
61
+ if (ar) params.set('h', String(Math.round(width / ar)));
62
+ params.set('fit', o.fit ?? 'cover');
63
+ params.set('q', String(o.quality ?? 75));
64
+ params.set('fmt', o.format ?? 'auto');
65
+ if (o.version) params.set('v', o.version);
66
+ return `${base}${apiPath}/${encodeURIComponent(id)}?${params.toString()}`;
67
+ };
68
+ /**
69
+ * The widths for a srcset. Two modes via `pixelStep`:
70
+ *
71
+ * - a **number** (the step): every multiple of it up to the source's intrinsic width,
72
+ * then the exact source width as the final candidate (so the srcset always tops out
73
+ * at the true native resolution, not the largest step multiple below it). With no
74
+ * source width, steps up to `maxWidth`.
75
+ * - an **array** (an explicit, possibly non-linear ladder à la a curated width list):
76
+ * the ladder values that fall below the source width, then the exact source width on
77
+ * top. With no source width, the ladder is used as-is (its author-chosen cap is kept).
78
+ *
79
+ * Either way no width exceeds the source (no upscaling) or `maxWidth`. The entry count is
80
+ * bounded by the size ceiling: a bigger step / a shorter ladder / a smaller source yields
81
+ * fewer widths (and so fewer cached variants).
82
+ */ export const stepWidths = (sourceWidth, pixelStep = DEFAULT_PIXEL_STEP, maxWidth = 4096)=>{
83
+ const known = sourceWidth && sourceWidth > 0 ? Math.min(maxWidth, Math.round(sourceWidth)) : undefined;
84
+ if (Array.isArray(pixelStep)) {
85
+ const ladder = [
86
+ ...new Set(pixelStep.filter((w)=>Number.isFinite(w) && w > 0).map((w)=>Math.round(w)).filter((w)=>w <= maxWidth))
87
+ ].sort((a, b)=>a - b);
88
+ if (known == null) return ladder.length ? ladder : [
89
+ maxWidth
90
+ ];
91
+ const widths = ladder.filter((w)=>w < known);
92
+ widths.push(known); // exact source width as the top candidate
93
+ return widths;
94
+ }
95
+ const step = pixelStep > 0 ? pixelStep : DEFAULT_PIXEL_STEP;
96
+ const top = known ?? maxWidth;
97
+ const widths = [];
98
+ for(let w = step; w < top; w += step)widths.push(w);
99
+ widths.push(top);
100
+ return widths;
101
+ };
102
+ /** Build a responsive `srcset` (pixelStep multiples up to the source width) + a default `src`. */ export const buildSrcset = (id, o = {})=>{
103
+ const widths = stepWidths(o.sourceWidth, o.pixelStep, o.maxWidth);
104
+ const srcset = widths.map((w)=>`${buildVariantUrl(id, w, o)} ${w}w`).join(', ');
105
+ const top = widths[widths.length - 1] ?? o.maxWidth ?? 4096;
106
+ const src = buildVariantUrl(id, o.defaultWidth ?? Math.min(top, 1280), o);
107
+ return {
108
+ srcset,
109
+ src
110
+ };
111
+ };
112
+
113
+ //# sourceMappingURL=urls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/urls.ts"],"sourcesContent":["/**\n * Isomorphic URL builders for the on-demand transform endpoint. NO Node / Sharp / server imports —\n * safe to bundle into client components. `buildVariantUrl` / `buildSrcset` produce same-origin\n * relative URLs by default (pass `baseUrl` for absolute); `getImageUrl` — the one-off/shareable\n * helper — instead defaults `baseUrl` to `process.env.NEXT_PUBLIC_SERVER_URL` (a Next build-time\n * constant, inlined in client bundles) so OG/email URLs come out absolute with no extra wiring.\n */\nimport { DEFAULT_PIXEL_STEP, type Fit, type Format, parseAspectRatio } from '../transform/params'\n\nexport { DEFAULT_PIXEL_STEP }\n\n/**\n * Default base for transform URLs: `/api` + the endpoint's fixed `/img` path. Override\n * `path` only if your Payload API route or Next.js basePath differs from the default.\n */\nexport const DEFAULT_TRANSFORM_API_PATH = '/api/img'\n\nexport interface BuildUrlOptions {\n fit?: Fit\n quality?: number\n format?: Format\n /** Render aspect ratio (`16/9` | `\"16:9\"`); derives `h` from each width. */\n aspectRatio?: number | string\n /** Prefix for absolute URLs (e.g. `https://site.com`). Default '' (same-origin). */\n baseUrl?: string\n /** Endpoint base. Default `/api/img` ({@link DEFAULT_TRANSFORM_API_PATH}); override only for a custom API route / basePath. */\n path?: string\n /**\n * Cache-busting version token appended as `v=`. Derive it from the source doc with\n * {@link deriveVersion} so replacing the file or moving the focal point yields a new\n * URL — busting immutable browser/CDN caches (the server ignores `v`, reading focal\n * from the doc; it exists only to make the immutable URL honest).\n */\n version?: string\n}\n\n/** Source-identity fields that determine the rendered pixels (independent of size/quality). */\nexport interface VersionSource {\n filename?: string | null\n focalX?: number | null\n focalY?: number | null\n}\n\nconst fnv1a = (s: string): string => {\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return (h >>> 0).toString(36)\n}\n\n/**\n * A short, stable version token from the source-identity fields that change the\n * rendered pixels (filename + focal point) — the same inputs the server folds into\n * the variant cache key. Returns undefined when no identity is available (a bare id),\n * in which case URLs carry no `v` — so pass a populated doc to get cache-correct\n * busting on file replace / focal edits.\n */\nexport const deriveVersion = (src?: VersionSource | null): string | undefined => {\n if (!src) return undefined\n const { filename, focalX, focalY } = src\n if (filename == null && focalX == null && focalY == null) return undefined\n return fnv1a(`${filename ?? ''}|${focalX ?? ''}|${focalY ?? ''}`)\n}\n\n/** A bare id, or a populated image doc (so the version token + a default width can be read off it). */\nexport type ImageResource = string | number | ({ id: string | number; width?: number | null } & VersionSource) | null | undefined\n\nexport interface GetImageUrlOptions extends BuildUrlOptions {\n /** Output width. Falls back to a populated doc's intrinsic width, else 1280. */\n width?: number\n /** Prefix for absolute URLs. Defaults to `NEXT_PUBLIC_SERVER_URL`; pass `''` for a relative URL. */\n baseUrl?: string\n}\n\n/**\n * One transform URL for an image, taking the id OR a populated doc directly: it resolves the\n * id, picks a sensible default width, and (for a doc) folds in the cache-busting version\n * automatically — so you don't re-implement the resolve-and-`deriveVersion` dance every time\n * you need a raw URL (OG tags, CSS backgrounds, emails). Returns null when there's no id.\n * For a responsive `<img>`, prefer `<ResponsiveImage>` / {@link buildSrcset}.\n *\n * Because these URLs are usually for external/shareable contexts (OG, email), `baseUrl` defaults\n * to `process.env.NEXT_PUBLIC_SERVER_URL` so the result is absolute with no extra wiring. Pass an\n * explicit `baseUrl` to override, or `baseUrl: ''` to force a relative same-origin URL.\n */\nexport const getImageUrl = (resource: ImageResource, o: GetImageUrlOptions = {}): string | null => {\n if (resource == null) return null\n const doc = typeof resource === 'object' ? resource : undefined\n const id = doc ? (doc.id == null ? '' : String(doc.id)) : String(resource)\n if (!id) return null\n const width = o.width ?? doc?.width ?? 1280\n return buildVariantUrl(id, width, {\n ...o,\n baseUrl: o.baseUrl ?? (process.env.NEXT_PUBLIC_SERVER_URL || undefined),\n version: o.version ?? deriveVersion(doc),\n })\n}\n\nexport const buildVariantUrl = (id: string, width: number, o: BuildUrlOptions = {}): string => {\n const base = o.baseUrl ?? ''\n const apiPath = o.path ?? DEFAULT_TRANSFORM_API_PATH\n const ar = parseAspectRatio(o.aspectRatio)\n const params = new URLSearchParams()\n params.set('w', String(Math.round(width)))\n if (ar) params.set('h', String(Math.round(width / ar)))\n params.set('fit', o.fit ?? 'cover')\n params.set('q', String(o.quality ?? 75))\n params.set('fmt', o.format ?? 'auto')\n if (o.version) params.set('v', o.version)\n return `${base}${apiPath}/${encodeURIComponent(id)}?${params.toString()}`\n}\n\n/**\n * The widths for a srcset. Two modes via `pixelStep`:\n *\n * - a **number** (the step): every multiple of it up to the source's intrinsic width,\n * then the exact source width as the final candidate (so the srcset always tops out\n * at the true native resolution, not the largest step multiple below it). With no\n * source width, steps up to `maxWidth`.\n * - an **array** (an explicit, possibly non-linear ladder à la a curated width list):\n * the ladder values that fall below the source width, then the exact source width on\n * top. With no source width, the ladder is used as-is (its author-chosen cap is kept).\n *\n * Either way no width exceeds the source (no upscaling) or `maxWidth`. The entry count is\n * bounded by the size ceiling: a bigger step / a shorter ladder / a smaller source yields\n * fewer widths (and so fewer cached variants).\n */\nexport const stepWidths = (sourceWidth?: number, pixelStep: number | number[] = DEFAULT_PIXEL_STEP, maxWidth = 4096): number[] => {\n const known = sourceWidth && sourceWidth > 0 ? Math.min(maxWidth, Math.round(sourceWidth)) : undefined\n\n if (Array.isArray(pixelStep)) {\n const ladder = [\n ...new Set(\n pixelStep\n .filter((w) => Number.isFinite(w) && w > 0)\n .map((w) => Math.round(w))\n .filter((w) => w <= maxWidth),\n ),\n ].sort((a, b) => a - b)\n if (known == null) return ladder.length ? ladder : [maxWidth]\n const widths = ladder.filter((w) => w < known)\n widths.push(known) // exact source width as the top candidate\n return widths\n }\n\n const step = pixelStep > 0 ? pixelStep : DEFAULT_PIXEL_STEP\n const top = known ?? maxWidth\n const widths: number[] = []\n for (let w = step; w < top; w += step) widths.push(w)\n widths.push(top)\n return widths\n}\n\nexport interface BuildSrcsetOptions extends BuildUrlOptions {\n /**\n * The srcset widths. A number is the step increment (default 50; bigger = fewer widths).\n * An array is an explicit, curated ladder of widths (e.g. `[200, 450, 750, 1200, 2000]`)\n * — non-linear, denser where it matters, fewer entries than a fine linear step.\n */\n pixelStep?: number | number[]\n /** The source image's intrinsic width — caps the srcset (no upscaling). */\n sourceWidth?: number\n /** Hard ceiling. Default 4096. */\n maxWidth?: number\n /** Width used for the plain `src` fallback. Defaults to min(top, 1280). */\n defaultWidth?: number\n}\n\nexport interface BuildSrcsetResult {\n srcset: string\n src: string\n}\n\n/** Build a responsive `srcset` (pixelStep multiples up to the source width) + a default `src`. */\nexport const buildSrcset = (id: string, o: BuildSrcsetOptions = {}): BuildSrcsetResult => {\n const widths = stepWidths(o.sourceWidth, o.pixelStep, o.maxWidth)\n const srcset = widths.map((w) => `${buildVariantUrl(id, w, o)} ${w}w`).join(', ')\n const top = widths[widths.length - 1] ?? o.maxWidth ?? 4096\n const src = buildVariantUrl(id, o.defaultWidth ?? Math.min(top, 1280), o)\n return { srcset, src }\n}\n\nexport type { Fit, Format }\n"],"names":["DEFAULT_PIXEL_STEP","parseAspectRatio","DEFAULT_TRANSFORM_API_PATH","fnv1a","s","h","i","length","charCodeAt","Math","imul","toString","deriveVersion","src","undefined","filename","focalX","focalY","getImageUrl","resource","o","doc","id","String","width","buildVariantUrl","baseUrl","process","env","NEXT_PUBLIC_SERVER_URL","version","base","apiPath","path","ar","aspectRatio","params","URLSearchParams","set","round","fit","quality","format","encodeURIComponent","stepWidths","sourceWidth","pixelStep","maxWidth","known","min","Array","isArray","ladder","Set","filter","w","Number","isFinite","map","sort","a","b","widths","push","step","top","buildSrcset","srcset","join","defaultWidth"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { ParsedParams } from '../transform/params';
2
+ export interface CacheKeyDoc {
3
+ id: string | number;
4
+ filename?: string | null;
5
+ focalX?: number | null;
6
+ focalY?: number | null;
7
+ }
8
+ export declare const variantCacheKey: (doc: CacheKeyDoc, p: ParsedParams, resolvedFormat: string) => string;
9
+ //# sourceMappingURL=key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../../src/variants/key.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,eAAO,MAAM,eAAe,GAAI,KAAK,WAAW,EAAE,GAAG,YAAY,EAAE,gBAAgB,MAAM,KAAG,MAI3F,CAAA"}