@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 @@
1
+ {"version":3,"sources":["../../src/fields/virtualUrls.ts"],"sourcesContent":["/**\n * Virtual (computed, never stored) URL fields for the image doc. An `afterRead` hook builds\n * each URL from the doc's own id + dimensions + filename + focal point, so the optimized URLs\n * ride along in EVERY read — REST, GraphQL, and the Local API — and through relationship\n * population (a populated `heroImage` carries `srcset` / `placeholderURL` ready to render),\n * with no client code and no knowledge of the `/api/img` endpoint.\n *\n * URLs are absolute when `config.serverURL` is set (handy for mobile / email / OG consumers),\n * relative otherwise. The builders are pure/isomorphic, so importing them here is server-safe.\n */\nimport type { Field, FieldHook } from 'payload'\n\nimport { buildSrcset, buildVariantUrl, deriveVersion, getImageUrl } from '../utils/urls'\nimport { type Fit, parseAspectRatio } from '../transform/params'\n\ninterface ImageDocLike {\n id?: string | number\n width?: number | null\n height?: number | null\n filename?: string | null\n url?: string | null\n focalX?: number | null\n focalY?: number | null\n}\n\n/** Fields the virtual URLs are computed from — kept selected via the collection's `forceSelect`. */\nexport const VIRTUAL_URL_INPUTS = ['width', 'height', 'filename', 'focalX', 'focalY'] as const\n\nconst naturalAspectRatio = (d: ImageDocLike): number | undefined => (d.width && d.height ? d.width / d.height : undefined)\n\n/** Build a single virtual `text` field from a `(doc, baseUrl) => url` computer. */\nconst virtualUrl = (\n name: string,\n description: string,\n compute: (doc: ImageDocLike, baseUrl?: string, pixelStep?: number | number[]) => string | null,\n): Field => {\n const afterRead: FieldHook = ({ data, req }) => {\n const doc = (data ?? {}) as ImageDocLike\n if (doc.id == null || !doc.filename) return null\n const cfg = req?.payload?.config\n // Explicit '' (not undefined) so these fields keep their serverURL-or-relative rule — passing\n // undefined would let getImageUrl's NEXT_PUBLIC_SERVER_URL default leak in for `src`.\n const baseUrl = cfg?.serverURL || ''\n const pixelStep = (cfg?.custom?.payloadImages as { pixelStep?: number | number[] } | undefined)?.pixelStep\n return compute(doc, baseUrl, pixelStep)\n }\n return { name, type: 'text', virtual: true, admin: { hidden: true, description }, hooks: { afterRead: [afterRead] } }\n}\n\ninterface LqipRequest {\n ar?: number\n fit?: Fit\n width?: number\n quality?: number\n}\n\nconst numOrUndef = (v: unknown): number | undefined => {\n const n = Number(v)\n return Number.isFinite(n) ? n : undefined\n}\n\n/** Parse an `X-LQIP` header: a bare ratio (`16/9`) or a `;`-list (`16/9; w=48; q=50` / `ar=16/9; w=48`). */\nconst parseLqipHeader = (h: string): LqipRequest => {\n const out: LqipRequest = {}\n for (const part of h.split(';')) {\n const s = part.trim()\n if (!s) continue\n const eq = s.indexOf('=')\n if (eq === -1) {\n out.ar = out.ar ?? parseAspectRatio(s)\n continue\n }\n const k = s.slice(0, eq).trim().toLowerCase()\n const v = s.slice(eq + 1).trim()\n if (k === 'ar') out.ar = parseAspectRatio(v)\n else if (k === 'w' || k === 'width') out.width = numOrUndef(v)\n else if (k === 'q' || k === 'quality') out.quality = numOrUndef(v)\n }\n return out\n}\n\n/** Read an LQIP request off the operation: `req.context.lqip = { ar?, fit?, width?, quality? }` (or `true`), or an `X-LQIP` header. */\nconst readLqipRequest = (\n req: { context?: Record<string, unknown>; headers?: { get?: (k: string) => string | null } } | undefined,\n): LqipRequest | undefined => {\n const ctx = req?.context?.lqip\n if (ctx === true) return {}\n if (ctx && typeof ctx === 'object') {\n const o = ctx as { ar?: number | string; fit?: Fit; width?: number; quality?: number }\n return { ar: parseAspectRatio(o.ar), fit: o.fit, width: numOrUndef(o.width), quality: numOrUndef(o.quality) }\n }\n const header = req?.headers?.get?.('x-lqip')\n if (header) return parseLqipHeader(header)\n return undefined\n}\n\n/**\n * A virtual `blurDataURL` field — the external door onto the inline-LQIP engine. It's a cheap\n * no-op on every normal read; only when an operation opts in (`req.context.lqip = { ar, fit }`,\n * or an `X-LQIP: 16/9` header) does it generate a faithful base64 data-URI via the shared variant\n * cache. The engine is dynamic-imported so the collection module never eagerly pulls in Sharp.\n * Deliberately kept OUT of `defaultPopulate` so it never runs during relationship population.\n */\nconst blurDataUrlField = (): Field => {\n const afterRead: FieldHook = async ({ data, req }) => {\n const lqipReq = readLqipRequest(req)\n if (!lqipReq) return null // not requested → no work\n const doc = (data ?? {}) as ImageDocLike\n if (doc.id == null || !doc.filename) return null\n const cfg = req?.payload?.config\n if (!cfg) return null\n try {\n const { generateInlineLqip } = await import('../components/inlineLqip')\n // Untrusted door: width/quality come from the caller, so clamp (untrusted: true).\n const uri = await generateInlineLqip({\n config: cfg,\n payload: req.payload,\n source: { id: doc.id, filename: doc.filename, url: doc.url, focalX: doc.focalX, focalY: doc.focalY },\n ar: lqipReq.ar,\n fit: lqipReq.fit ?? 'cover',\n width: lqipReq.width,\n quality: lqipReq.quality,\n untrusted: true,\n })\n return uri ?? null\n } catch {\n return null\n }\n }\n return {\n name: 'blurDataURL',\n type: 'text',\n virtual: true,\n admin: {\n hidden: true,\n description:\n 'Inline LQIP data-URI; generated only when a read sets req.context.lqip ({ ar, fit }) or sends an X-LQIP header. Null otherwise.',\n },\n hooks: { afterRead: [afterRead] },\n }\n}\n\n/**\n * The virtual URL fields appended to the image collection:\n * - `src` — a single optimized URL (capped at 1280px) for a plain `<img>` / OG tag.\n * - `srcset` — a responsive srcset at the image's natural ratio, stepped up to its width.\n * - `placeholderURL` — the tiny LQIP URL (32px, q40) for a blur-up / CSS background (URL form, for\n * non-React consumers; `<ResponsiveImage>` inlines its own LQIP instead).\n * - `thumbnailURL` — a 160px focal-cropped square for cards, lists, and feeds.\n * - `blurDataURL` — an on-demand inline LQIP data-URI (gated; see {@link blurDataUrlField}).\n */\nexport const virtualUrlFields = (): Field[] => [\n virtualUrl('src', 'Optimized URL (≤1280px) for a plain <img> or OG tag.', (d, baseUrl) =>\n getImageUrl(\n { id: d.id as string | number, width: d.width, filename: d.filename, focalX: d.focalX, focalY: d.focalY },\n {\n width: Math.min(d.width ?? 1280, 1280),\n baseUrl,\n },\n ),\n ),\n virtualUrl(\n 'srcset',\n 'Responsive srcset at the natural ratio, up to the source width.',\n (d, baseUrl, pixelStep) =>\n buildSrcset(String(d.id), {\n sourceWidth: d.width ?? undefined,\n aspectRatio: naturalAspectRatio(d),\n version: deriveVersion(d),\n baseUrl,\n pixelStep,\n }).srcset,\n ),\n virtualUrl('placeholderURL', 'Tiny low-quality placeholder (LQIP) for a blur-up / CSS background.', (d, baseUrl) =>\n buildVariantUrl(String(d.id), 32, { quality: 40, aspectRatio: naturalAspectRatio(d), version: deriveVersion(d), baseUrl }),\n ),\n virtualUrl('thumbnailURL', 'Small focal-cropped square (160px) for cards, lists, and feeds.', (d, baseUrl) =>\n buildVariantUrl(String(d.id), 160, { fit: 'cover', aspectRatio: 1, version: deriveVersion(d), baseUrl }),\n ),\n blurDataUrlField(),\n]\n\n/** Field names produced by {@link virtualUrlFields} — used to wire `defaultPopulate`. */\nexport const VIRTUAL_URL_FIELDS = ['src', 'srcset', 'placeholderURL', 'thumbnailURL'] as const\n"],"names":["buildSrcset","buildVariantUrl","deriveVersion","getImageUrl","parseAspectRatio","VIRTUAL_URL_INPUTS","naturalAspectRatio","d","width","height","undefined","virtualUrl","name","description","compute","afterRead","data","req","doc","id","filename","cfg","payload","config","baseUrl","serverURL","pixelStep","custom","payloadImages","type","virtual","admin","hidden","hooks","numOrUndef","v","n","Number","isFinite","parseLqipHeader","h","out","part","split","s","trim","eq","indexOf","ar","k","slice","toLowerCase","quality","readLqipRequest","ctx","context","lqip","o","fit","header","headers","get","blurDataUrlField","lqipReq","generateInlineLqip","uri","source","url","focalX","focalY","untrusted","virtualUrlFields","Math","min","String","sourceWidth","aspectRatio","version","srcset","VIRTUAL_URL_FIELDS"],"mappings":"AAAA;;;;;;;;;CASC,GAGD,SAASA,WAAW,EAAEC,eAAe,EAAEC,aAAa,EAAEC,WAAW,QAAQ,mBAAe;AACxF,SAAmBC,gBAAgB,QAAQ,yBAAqB;AAYhE,kGAAkG,GAClG,OAAO,MAAMC,qBAAqB;IAAC;IAAS;IAAU;IAAY;IAAU;CAAS,CAAS;AAE9F,MAAMC,qBAAqB,CAACC,IAAyCA,EAAEC,KAAK,IAAID,EAAEE,MAAM,GAAGF,EAAEC,KAAK,GAAGD,EAAEE,MAAM,GAAGC;AAEhH,iFAAiF,GACjF,MAAMC,aAAa,CACjBC,MACAC,aACAC;IAEA,MAAMC,YAAuB,CAAC,EAAEC,IAAI,EAAEC,GAAG,EAAE;QACzC,MAAMC,MAAOF,QAAQ,CAAC;QACtB,IAAIE,IAAIC,EAAE,IAAI,QAAQ,CAACD,IAAIE,QAAQ,EAAE,OAAO;QAC5C,MAAMC,MAAMJ,KAAKK,SAASC;QAC1B,8FAA8F;QAC9F,sFAAsF;QACtF,MAAMC,UAAUH,KAAKI,aAAa;QAClC,MAAMC,YAAaL,KAAKM,QAAQC,eAAiEF;QACjG,OAAOZ,QAAQI,KAAKM,SAASE;IAC/B;IACA,OAAO;QAAEd;QAAMiB,MAAM;QAAQC,SAAS;QAAMC,OAAO;YAAEC,QAAQ;YAAMnB;QAAY;QAAGoB,OAAO;YAAElB,WAAW;gBAACA;aAAU;QAAC;IAAE;AACtH;AASA,MAAMmB,aAAa,CAACC;IAClB,MAAMC,IAAIC,OAAOF;IACjB,OAAOE,OAAOC,QAAQ,CAACF,KAAKA,IAAI1B;AAClC;AAEA,0GAA0G,GAC1G,MAAM6B,kBAAkB,CAACC;IACvB,MAAMC,MAAmB,CAAC;IAC1B,KAAK,MAAMC,QAAQF,EAAEG,KAAK,CAAC,KAAM;QAC/B,MAAMC,IAAIF,KAAKG,IAAI;QACnB,IAAI,CAACD,GAAG;QACR,MAAME,KAAKF,EAAEG,OAAO,CAAC;QACrB,IAAID,OAAO,CAAC,GAAG;YACbL,IAAIO,EAAE,GAAGP,IAAIO,EAAE,IAAI5C,iBAAiBwC;YACpC;QACF;QACA,MAAMK,IAAIL,EAAEM,KAAK,CAAC,GAAGJ,IAAID,IAAI,GAAGM,WAAW;QAC3C,MAAMhB,IAAIS,EAAEM,KAAK,CAACJ,KAAK,GAAGD,IAAI;QAC9B,IAAII,MAAM,MAAMR,IAAIO,EAAE,GAAG5C,iBAAiB+B;aACrC,IAAIc,MAAM,OAAOA,MAAM,SAASR,IAAIjC,KAAK,GAAG0B,WAAWC;aACvD,IAAIc,MAAM,OAAOA,MAAM,WAAWR,IAAIW,OAAO,GAAGlB,WAAWC;IAClE;IACA,OAAOM;AACT;AAEA,qIAAqI,GACrI,MAAMY,kBAAkB,CACtBpC;IAEA,MAAMqC,MAAMrC,KAAKsC,SAASC;IAC1B,IAAIF,QAAQ,MAAM,OAAO,CAAC;IAC1B,IAAIA,OAAO,OAAOA,QAAQ,UAAU;QAClC,MAAMG,IAAIH;QACV,OAAO;YAAEN,IAAI5C,iBAAiBqD,EAAET,EAAE;YAAGU,KAAKD,EAAEC,GAAG;YAAElD,OAAO0B,WAAWuB,EAAEjD,KAAK;YAAG4C,SAASlB,WAAWuB,EAAEL,OAAO;QAAE;IAC9G;IACA,MAAMO,SAAS1C,KAAK2C,SAASC,MAAM;IACnC,IAAIF,QAAQ,OAAOpB,gBAAgBoB;IACnC,OAAOjD;AACT;AAEA;;;;;;CAMC,GACD,MAAMoD,mBAAmB;IACvB,MAAM/C,YAAuB,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;QAC/C,MAAM8C,UAAUV,gBAAgBpC;QAChC,IAAI,CAAC8C,SAAS,OAAO,KAAK,0BAA0B;;QACpD,MAAM7C,MAAOF,QAAQ,CAAC;QACtB,IAAIE,IAAIC,EAAE,IAAI,QAAQ,CAACD,IAAIE,QAAQ,EAAE,OAAO;QAC5C,MAAMC,MAAMJ,KAAKK,SAASC;QAC1B,IAAI,CAACF,KAAK,OAAO;QACjB,IAAI;YACF,MAAM,EAAE2C,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC;YAC5C,kFAAkF;YAClF,MAAMC,MAAM,MAAMD,mBAAmB;gBACnCzC,QAAQF;gBACRC,SAASL,IAAIK,OAAO;gBACpB4C,QAAQ;oBAAE/C,IAAID,IAAIC,EAAE;oBAAEC,UAAUF,IAAIE,QAAQ;oBAAE+C,KAAKjD,IAAIiD,GAAG;oBAAEC,QAAQlD,IAAIkD,MAAM;oBAAEC,QAAQnD,IAAImD,MAAM;gBAAC;gBACnGrB,IAAIe,QAAQf,EAAE;gBACdU,KAAKK,QAAQL,GAAG,IAAI;gBACpBlD,OAAOuD,QAAQvD,KAAK;gBACpB4C,SAASW,QAAQX,OAAO;gBACxBkB,WAAW;YACb;YACA,OAAOL,OAAO;QAChB,EAAE,OAAM;YACN,OAAO;QACT;IACF;IACA,OAAO;QACLrD,MAAM;QACNiB,MAAM;QACNC,SAAS;QACTC,OAAO;YACLC,QAAQ;YACRnB,aACE;QACJ;QACAoB,OAAO;YAAElB,WAAW;gBAACA;aAAU;QAAC;IAClC;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMwD,mBAAmB,IAAe;QAC7C5D,WAAW,OAAO,wDAAwD,CAACJ,GAAGiB,UAC5ErB,YACE;gBAAEgB,IAAIZ,EAAEY,EAAE;gBAAqBX,OAAOD,EAAEC,KAAK;gBAAEY,UAAUb,EAAEa,QAAQ;gBAAEgD,QAAQ7D,EAAE6D,MAAM;gBAAEC,QAAQ9D,EAAE8D,MAAM;YAAC,GACxG;gBACE7D,OAAOgE,KAAKC,GAAG,CAAClE,EAAEC,KAAK,IAAI,MAAM;gBACjCgB;YACF;QAGJb,WACE,UACA,mEACA,CAACJ,GAAGiB,SAASE,YACX1B,YAAY0E,OAAOnE,EAAEY,EAAE,GAAG;gBACxBwD,aAAapE,EAAEC,KAAK,IAAIE;gBACxBkE,aAAatE,mBAAmBC;gBAChCsE,SAAS3E,cAAcK;gBACvBiB;gBACAE;YACF,GAAGoD,MAAM;QAEbnE,WAAW,kBAAkB,uEAAuE,CAACJ,GAAGiB,UACtGvB,gBAAgByE,OAAOnE,EAAEY,EAAE,GAAG,IAAI;gBAAEiC,SAAS;gBAAIwB,aAAatE,mBAAmBC;gBAAIsE,SAAS3E,cAAcK;gBAAIiB;YAAQ;QAE1Hb,WAAW,gBAAgB,mEAAmE,CAACJ,GAAGiB,UAChGvB,gBAAgByE,OAAOnE,EAAEY,EAAE,GAAG,KAAK;gBAAEuC,KAAK;gBAASkB,aAAa;gBAAGC,SAAS3E,cAAcK;gBAAIiB;YAAQ;QAExGsC;KACD,CAAA;AAED,uFAAuF,GACvF,OAAO,MAAMiB,qBAAqB;IAAC;IAAO;IAAU;IAAkB;CAAe,CAAS"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Variant purge. Deleting a generated-image doc cascades its stored file removal
3
+ * through the configured storage adapter's `afterDelete` hook, so purging is just a
4
+ * bulk delete by `source` — no direct storage SDK, fully adapter-agnostic. Hooks
5
+ * are best-effort and logged: a failed purge must never block the source write.
6
+ */
7
+ import type { CollectionAfterChangeHook, CollectionBeforeDeleteHook, Payload, PayloadRequest } from 'payload';
8
+ export interface PurgeOptions {
9
+ /** Slug of the generated-images collection. Default `generated-images`. */
10
+ variantSlug?: string;
11
+ }
12
+ /**
13
+ * Delete every generated variant whose `source` is the given image id. Returns the
14
+ * count removed. Pass the calling hook's `req` so the bulk delete joins that
15
+ * operation's transaction (atomicity on transactional adapters); omit it for
16
+ * out-of-request callers.
17
+ */
18
+ export declare const purgeVariantsForSource: (payload: Payload, variantSlug: string, sourceId: string | number, req?: PayloadRequest) => Promise<number>;
19
+ /**
20
+ * beforeDelete on the source collection → remove all of its generated variants.
21
+ * Runs BEFORE the source row is deleted: on SQL adapters the variants' required
22
+ * `source` FK would otherwise be NULL-ed during the parent delete and trip a
23
+ * NOT NULL constraint. Removing the variants first sidesteps that (and is harmless
24
+ * on Mongo).
25
+ */
26
+ export declare const purgeVariantsBeforeDelete: (opts?: PurgeOptions) => CollectionBeforeDeleteHook;
27
+ /**
28
+ * afterChange on the source collection → purge stale variants when the underlying
29
+ * file or focal point changed (their cache keys are now unreachable). A metadata-only
30
+ * edit (e.g. `alt`) leaves variants valid and is left untouched.
31
+ */
32
+ export declare const purgeStaleVariantsAfterChange: (opts?: PurgeOptions) => CollectionAfterChangeHook;
33
+ //# sourceMappingURL=purge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"purge.d.ts","sourceRoot":"","sources":["../../src/hooks/purge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,yBAAyB,EAAE,0BAA0B,EAAkB,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAI7H,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAS,OAAO,EAChB,aAAa,MAAM,EACnB,UAAU,MAAM,GAAG,MAAM,EACzB,MAAM,cAAc,KACnB,OAAO,CAAC,MAAM,CAUhB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAM,YAAiB,KAAG,0BASnE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GAAI,OAAM,YAAiB,KAAG,yBAevE,CAAA"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Variant purge. Deleting a generated-image doc cascades its stored file removal
3
+ * through the configured storage adapter's `afterDelete` hook, so purging is just a
4
+ * bulk delete by `source` — no direct storage SDK, fully adapter-agnostic. Hooks
5
+ * are best-effort and logged: a failed purge must never block the source write.
6
+ */ import { GENERATED_IMAGES_SLUG } from "../collections/generatedImages.js";
7
+ /**
8
+ * Delete every generated variant whose `source` is the given image id. Returns the
9
+ * count removed. Pass the calling hook's `req` so the bulk delete joins that
10
+ * operation's transaction (atomicity on transactional adapters); omit it for
11
+ * out-of-request callers.
12
+ */ export const purgeVariantsForSource = async (payload, variantSlug, sourceId, req)=>{
13
+ const res = await payload.delete({
14
+ collection: variantSlug,
15
+ where: {
16
+ source: {
17
+ equals: sourceId
18
+ }
19
+ },
20
+ overrideAccess: true,
21
+ req
22
+ });
23
+ if (res?.errors?.length) payload.logger.warn(`[payload-images] ${res.errors.length} generated variant(s) failed to purge for source ${sourceId}`);
24
+ return res?.docs?.length ?? 0;
25
+ };
26
+ /**
27
+ * beforeDelete on the source collection → remove all of its generated variants.
28
+ * Runs BEFORE the source row is deleted: on SQL adapters the variants' required
29
+ * `source` FK would otherwise be NULL-ed during the parent delete and trip a
30
+ * NOT NULL constraint. Removing the variants first sidesteps that (and is harmless
31
+ * on Mongo).
32
+ */ export const purgeVariantsBeforeDelete = (opts = {})=>{
33
+ const variantSlug = opts.variantSlug || GENERATED_IMAGES_SLUG;
34
+ return async ({ id, req })=>{
35
+ try {
36
+ await purgeVariantsForSource(req.payload, variantSlug, id, req);
37
+ } catch (err) {
38
+ req.payload.logger.warn(`[payload-images] failed to purge variants on delete of ${id}: ${String(err)}`);
39
+ }
40
+ };
41
+ };
42
+ /**
43
+ * afterChange on the source collection → purge stale variants when the underlying
44
+ * file or focal point changed (their cache keys are now unreachable). A metadata-only
45
+ * edit (e.g. `alt`) leaves variants valid and is left untouched.
46
+ */ export const purgeStaleVariantsAfterChange = (opts = {})=>{
47
+ const variantSlug = opts.variantSlug || GENERATED_IMAGES_SLUG;
48
+ return async ({ doc, previousDoc, operation, req })=>{
49
+ if (operation !== 'update') return doc;
50
+ const fileChanged = previousDoc?.filename !== doc?.filename;
51
+ const focalChanged = previousDoc?.focalX !== doc?.focalX || previousDoc?.focalY !== doc?.focalY;
52
+ if (fileChanged || focalChanged) {
53
+ try {
54
+ await purgeVariantsForSource(req.payload, variantSlug, doc.id, req);
55
+ } catch (err) {
56
+ req.payload.logger.warn(`[payload-images] failed to purge stale variants on change of ${doc?.id}: ${String(err)}`);
57
+ }
58
+ }
59
+ return doc;
60
+ };
61
+ };
62
+
63
+ //# sourceMappingURL=purge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/purge.ts"],"sourcesContent":["/**\n * Variant purge. Deleting a generated-image doc cascades its stored file removal\n * through the configured storage adapter's `afterDelete` hook, so purging is just a\n * bulk delete by `source` — no direct storage SDK, fully adapter-agnostic. Hooks\n * are best-effort and logged: a failed purge must never block the source write.\n */\nimport type { CollectionAfterChangeHook, CollectionBeforeDeleteHook, CollectionSlug, Payload, PayloadRequest } from 'payload'\n\nimport { GENERATED_IMAGES_SLUG } from '../collections/generatedImages'\n\nexport interface PurgeOptions {\n /** Slug of the generated-images collection. Default `generated-images`. */\n variantSlug?: string\n}\n\n/**\n * Delete every generated variant whose `source` is the given image id. Returns the\n * count removed. Pass the calling hook's `req` so the bulk delete joins that\n * operation's transaction (atomicity on transactional adapters); omit it for\n * out-of-request callers.\n */\nexport const purgeVariantsForSource = async (\n payload: Payload,\n variantSlug: string,\n sourceId: string | number,\n req?: PayloadRequest,\n): Promise<number> => {\n const res = await payload.delete({\n collection: variantSlug as CollectionSlug,\n where: { source: { equals: sourceId } },\n overrideAccess: true,\n req,\n })\n if (res?.errors?.length)\n payload.logger.warn(`[payload-images] ${res.errors.length} generated variant(s) failed to purge for source ${sourceId}`)\n return res?.docs?.length ?? 0\n}\n\n/**\n * beforeDelete on the source collection → remove all of its generated variants.\n * Runs BEFORE the source row is deleted: on SQL adapters the variants' required\n * `source` FK would otherwise be NULL-ed during the parent delete and trip a\n * NOT NULL constraint. Removing the variants first sidesteps that (and is harmless\n * on Mongo).\n */\nexport const purgeVariantsBeforeDelete = (opts: PurgeOptions = {}): CollectionBeforeDeleteHook => {\n const variantSlug = opts.variantSlug || GENERATED_IMAGES_SLUG\n return async ({ id, req }) => {\n try {\n await purgeVariantsForSource(req.payload, variantSlug, id, req)\n } catch (err) {\n req.payload.logger.warn(`[payload-images] failed to purge variants on delete of ${id}: ${String(err)}`)\n }\n }\n}\n\n/**\n * afterChange on the source collection → purge stale variants when the underlying\n * file or focal point changed (their cache keys are now unreachable). A metadata-only\n * edit (e.g. `alt`) leaves variants valid and is left untouched.\n */\nexport const purgeStaleVariantsAfterChange = (opts: PurgeOptions = {}): CollectionAfterChangeHook => {\n const variantSlug = opts.variantSlug || GENERATED_IMAGES_SLUG\n return async ({ doc, previousDoc, operation, req }) => {\n if (operation !== 'update') return doc\n const fileChanged = previousDoc?.filename !== doc?.filename\n const focalChanged = previousDoc?.focalX !== doc?.focalX || previousDoc?.focalY !== doc?.focalY\n if (fileChanged || focalChanged) {\n try {\n await purgeVariantsForSource(req.payload, variantSlug, doc.id, req)\n } catch (err) {\n req.payload.logger.warn(`[payload-images] failed to purge stale variants on change of ${doc?.id}: ${String(err)}`)\n }\n }\n return doc\n }\n}\n"],"names":["GENERATED_IMAGES_SLUG","purgeVariantsForSource","payload","variantSlug","sourceId","req","res","delete","collection","where","source","equals","overrideAccess","errors","length","logger","warn","docs","purgeVariantsBeforeDelete","opts","id","err","String","purgeStaleVariantsAfterChange","doc","previousDoc","operation","fileChanged","filename","focalChanged","focalX","focalY"],"mappings":"AAAA;;;;;CAKC,GAGD,SAASA,qBAAqB,QAAQ,oCAAgC;AAOtE;;;;;CAKC,GACD,OAAO,MAAMC,yBAAyB,OACpCC,SACAC,aACAC,UACAC;IAEA,MAAMC,MAAM,MAAMJ,QAAQK,MAAM,CAAC;QAC/BC,YAAYL;QACZM,OAAO;YAAEC,QAAQ;gBAAEC,QAAQP;YAAS;QAAE;QACtCQ,gBAAgB;QAChBP;IACF;IACA,IAAIC,KAAKO,QAAQC,QACfZ,QAAQa,MAAM,CAACC,IAAI,CAAC,CAAC,iBAAiB,EAAEV,IAAIO,MAAM,CAACC,MAAM,CAAC,iDAAiD,EAAEV,UAAU;IACzH,OAAOE,KAAKW,MAAMH,UAAU;AAC9B,EAAC;AAED;;;;;;CAMC,GACD,OAAO,MAAMI,4BAA4B,CAACC,OAAqB,CAAC,CAAC;IAC/D,MAAMhB,cAAcgB,KAAKhB,WAAW,IAAIH;IACxC,OAAO,OAAO,EAAEoB,EAAE,EAAEf,GAAG,EAAE;QACvB,IAAI;YACF,MAAMJ,uBAAuBI,IAAIH,OAAO,EAAEC,aAAaiB,IAAIf;QAC7D,EAAE,OAAOgB,KAAK;YACZhB,IAAIH,OAAO,CAACa,MAAM,CAACC,IAAI,CAAC,CAAC,uDAAuD,EAAEI,GAAG,EAAE,EAAEE,OAAOD,MAAM;QACxG;IACF;AACF,EAAC;AAED;;;;CAIC,GACD,OAAO,MAAME,gCAAgC,CAACJ,OAAqB,CAAC,CAAC;IACnE,MAAMhB,cAAcgB,KAAKhB,WAAW,IAAIH;IACxC,OAAO,OAAO,EAAEwB,GAAG,EAAEC,WAAW,EAAEC,SAAS,EAAErB,GAAG,EAAE;QAChD,IAAIqB,cAAc,UAAU,OAAOF;QACnC,MAAMG,cAAcF,aAAaG,aAAaJ,KAAKI;QACnD,MAAMC,eAAeJ,aAAaK,WAAWN,KAAKM,UAAUL,aAAaM,WAAWP,KAAKO;QACzF,IAAIJ,eAAeE,cAAc;YAC/B,IAAI;gBACF,MAAM5B,uBAAuBI,IAAIH,OAAO,EAAEC,aAAaqB,IAAIJ,EAAE,EAAEf;YACjE,EAAE,OAAOgB,KAAK;gBACZhB,IAAIH,OAAO,CAACa,MAAM,CAACC,IAAI,CAAC,CAAC,6DAA6D,EAAEQ,KAAKJ,GAAG,EAAE,EAAEE,OAAOD,MAAM;YACnH;QACF;QACA,OAAOG;IACT;AACF,EAAC"}
@@ -0,0 +1,4 @@
1
+ export { default, imagesPlugin } from './plugin';
2
+ export type { ImagesPluginOptions, PlaceholderConfig, ResolvedPlaceholder } from './plugin';
3
+ export type { TransformEndpointConfig } from './endpoints/transform';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAChD,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAE3F,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // The plugin
2
+ export { default, imagesPlugin } from "./plugin.js";
3
+ // NOTE: the frontend surface is intentionally NOT re-exported here. This root entry pulls the
4
+ // server-only plugin (which statically imports `node:fs` / `next/server`), so importing the
5
+ // frontend from root would drag that into the consumer's bundle. Import it from the subpaths:
6
+ // import { ResponsiveImage } from '@pro-laico/payload-images/components/image' // async server component
7
+ // import { getImageUrl } from '@pro-laico/payload-images/utils/urls' // pure, client-safe builders
8
+
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// The plugin\nexport { default, imagesPlugin } from './plugin'\nexport type { ImagesPluginOptions, PlaceholderConfig, ResolvedPlaceholder } from './plugin'\n// Type-only (erased at build), so this pulls no endpoint runtime into the root entry.\nexport type { TransformEndpointConfig } from './endpoints/transform'\n\n// NOTE: the frontend surface is intentionally NOT re-exported here. This root entry pulls the\n// server-only plugin (which statically imports `node:fs` / `next/server`), so importing the\n// frontend from root would drag that into the consumer's bundle. Import it from the subpaths:\n// import { ResponsiveImage } from '@pro-laico/payload-images/components/image' // async server component\n// import { getImageUrl } from '@pro-laico/payload-images/utils/urls' // pure, client-safe builders\n"],"names":["default","imagesPlugin"],"mappings":"AAAA,aAAa;AACb,SAASA,OAAO,EAAEC,YAAY,QAAQ,cAAU;CAKhD,8FAA8F;CAC9F,4FAA4F;CAC5F,8FAA8F;CAC9F,4GAA4G;CAC5G,gHAAgH"}
@@ -0,0 +1,6 @@
1
+ import type { SanitizedConfig } from 'payload';
2
+ /** Called from the plugin's `onInit`: remember the running app's config for the server components. */
3
+ export declare const stashConfig: (config: SanitizedConfig) => void;
4
+ /** The stashed config, once Payload has booted in this process (undefined before). */
5
+ export declare const stashedConfig: () => SanitizedConfig | undefined;
6
+ //# sourceMappingURL=configStash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configStash.d.ts","sourceRoot":"","sources":["../../src/lib/configStash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAO9C,sGAAsG;AACtG,eAAO,MAAM,WAAW,GAAI,QAAQ,eAAe,KAAG,IAErD,CAAA;AAED,sFAAsF;AACtF,eAAO,MAAM,aAAa,QAAO,eAAe,GAAG,SACkC,CAAA"}
@@ -0,0 +1,10 @@
1
+ /** The globalThis slot the plugin's `onInit` fills with the app's sanitized config.
2
+ * `Symbol.for` → one shared registry entry, so every pro-laico package reads the same
3
+ * stash without importing each other. */ const CONFIG_SLOT = Symbol.for('pro-laico.payload-config');
4
+ /** Called from the plugin's `onInit`: remember the running app's config for the server components. */ export const stashConfig = (config)=>{
5
+ ;
6
+ globalThis[CONFIG_SLOT] = config;
7
+ };
8
+ /** The stashed config, once Payload has booted in this process (undefined before). */ export const stashedConfig = ()=>globalThis[CONFIG_SLOT];
9
+
10
+ //# sourceMappingURL=configStash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/configStash.ts"],"sourcesContent":["import type { SanitizedConfig } from 'payload'\n\n/** The globalThis slot the plugin's `onInit` fills with the app's sanitized config.\n * `Symbol.for` → one shared registry entry, so every pro-laico package reads the same\n * stash without importing each other. */\nconst CONFIG_SLOT = Symbol.for('pro-laico.payload-config')\n\n/** Called from the plugin's `onInit`: remember the running app's config for the server components. */\nexport const stashConfig = (config: SanitizedConfig): void => {\n ;(globalThis as Record<symbol, unknown>)[CONFIG_SLOT] = config\n}\n\n/** The stashed config, once Payload has booted in this process (undefined before). */\nexport const stashedConfig = (): SanitizedConfig | undefined =>\n (globalThis as Record<symbol, unknown>)[CONFIG_SLOT] as SanitizedConfig | undefined\n"],"names":["CONFIG_SLOT","Symbol","for","stashConfig","config","globalThis","stashedConfig"],"mappings":"AAEA;;wCAEwC,GACxC,MAAMA,cAAcC,OAAOC,GAAG,CAAC;AAE/B,oGAAoG,GACpG,OAAO,MAAMC,cAAc,CAACC;;IACxBC,UAAsC,CAACL,YAAY,GAAGI;AAC1D,EAAC;AAED,oFAAoF,GACpF,OAAO,MAAME,gBAAgB,IAC3B,AAACD,UAAsC,CAACL,YAAY,CAA+B"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The configured public origin from the environment, used by the transform endpoint as a
3
+ * fallback when reading an original served from a relative/cloud URL. Prefer Payload's own
4
+ * `config.serverURL` (the endpoint checks that first); this only covers the case where it's
5
+ * unset. Returns `''` when nothing is configured, so callers can fall back to `req.origin`.
6
+ */
7
+ export declare const getServerSideURL: () => string;
8
+ //# sourceMappingURL=getServerSideURL.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getServerSideURL.d.ts","sourceRoot":"","sources":["../../src/lib/getServerSideURL.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,MAInC,CAAA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The configured public origin from the environment, used by the transform endpoint as a
3
+ * fallback when reading an original served from a relative/cloud URL. Prefer Payload's own
4
+ * `config.serverURL` (the endpoint checks that first); this only covers the case where it's
5
+ * unset. Returns `''` when nothing is configured, so callers can fall back to `req.origin`.
6
+ */ export const getServerSideURL = ()=>{
7
+ if (process.env.NEXT_PUBLIC_SERVER_URL) return process.env.NEXT_PUBLIC_SERVER_URL;
8
+ if (process.env.VERCEL_PROJECT_PRODUCTION_URL) return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
9
+ return '';
10
+ };
11
+
12
+ //# sourceMappingURL=getServerSideURL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/getServerSideURL.ts"],"sourcesContent":["/**\n * The configured public origin from the environment, used by the transform endpoint as a\n * fallback when reading an original served from a relative/cloud URL. Prefer Payload's own\n * `config.serverURL` (the endpoint checks that first); this only covers the case where it's\n * unset. Returns `''` when nothing is configured, so callers can fall back to `req.origin`.\n */\nexport const getServerSideURL = (): string => {\n if (process.env.NEXT_PUBLIC_SERVER_URL) return process.env.NEXT_PUBLIC_SERVER_URL\n if (process.env.VERCEL_PROJECT_PRODUCTION_URL) return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`\n return ''\n}\n"],"names":["getServerSideURL","process","env","NEXT_PUBLIC_SERVER_URL","VERCEL_PROJECT_PRODUCTION_URL"],"mappings":"AAAA;;;;;CAKC,GACD,OAAO,MAAMA,mBAAmB;IAC9B,IAAIC,QAAQC,GAAG,CAACC,sBAAsB,EAAE,OAAOF,QAAQC,GAAG,CAACC,sBAAsB;IACjF,IAAIF,QAAQC,GAAG,CAACE,6BAA6B,EAAE,OAAO,CAAC,QAAQ,EAAEH,QAAQC,GAAG,CAACE,6BAA6B,EAAE;IAC5G,OAAO;AACT,EAAC"}
@@ -0,0 +1,17 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ /**
3
+ * Deep-merge a partial override onto a base `CollectionConfig` without clobbering the
4
+ * nested config a top-level spread would otherwise replace.
5
+ *
6
+ * Top-level keys replace, but:
7
+ * - `access` / `admin` are shallow-merged (override keys win, base keys kept).
8
+ * - `fields` are APPENDED (base fields first, then override fields).
9
+ * - `upload` is shallow-merged when both sides are objects (so a partial
10
+ * `upload: { staticDir }` keeps the base `mimeTypes` whitelist).
11
+ * - `hooks` are merged per-phase via {@link mergeHooks} (override hooks run AFTER the
12
+ * base hooks within each phase).
13
+ *
14
+ * When `override` is undefined, `base` is returned unchanged.
15
+ */
16
+ export declare const mergeCollection: (base: CollectionConfig, override?: Partial<CollectionConfig>) => CollectionConfig;
17
+ //# sourceMappingURL=mergeCollection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeCollection.d.ts","sourceRoot":"","sources":["../../src/lib/mergeCollection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAkB/C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,gBAAgB,EAAE,WAAW,OAAO,CAAC,gBAAgB,CAAC,KAAG,gBAcrF,CAAA"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Additively merge two Payload hooks objects: each per-phase array in `extra` is
3
+ * appended to the corresponding array in `base`. Phases present in only one of the two
4
+ * are preserved as-is. User hooks always run AFTER base hooks within each phase.
5
+ */ const mergeHooks = (base, extra)=>{
6
+ if (!extra) return base;
7
+ const b = base;
8
+ const e = extra;
9
+ const out = {
10
+ ...b
11
+ };
12
+ for (const key of Object.keys(e)){
13
+ out[key] = [
14
+ ...b[key] ?? [],
15
+ ...e[key] ?? []
16
+ ];
17
+ }
18
+ return out;
19
+ };
20
+ /**
21
+ * Deep-merge a partial override onto a base `CollectionConfig` without clobbering the
22
+ * nested config a top-level spread would otherwise replace.
23
+ *
24
+ * Top-level keys replace, but:
25
+ * - `access` / `admin` are shallow-merged (override keys win, base keys kept).
26
+ * - `fields` are APPENDED (base fields first, then override fields).
27
+ * - `upload` is shallow-merged when both sides are objects (so a partial
28
+ * `upload: { staticDir }` keeps the base `mimeTypes` whitelist).
29
+ * - `hooks` are merged per-phase via {@link mergeHooks} (override hooks run AFTER the
30
+ * base hooks within each phase).
31
+ *
32
+ * When `override` is undefined, `base` is returned unchanged.
33
+ */ export const mergeCollection = (base, override)=>override ? {
34
+ ...base,
35
+ ...override,
36
+ access: {
37
+ ...base.access,
38
+ ...override.access
39
+ },
40
+ admin: {
41
+ ...base.admin,
42
+ ...override.admin
43
+ },
44
+ fields: [
45
+ ...base.fields,
46
+ ...override.fields ?? []
47
+ ],
48
+ upload: override.upload && typeof override.upload === 'object' && typeof base.upload === 'object' ? {
49
+ ...base.upload,
50
+ ...override.upload
51
+ } : override.upload ?? base.upload,
52
+ hooks: override.hooks ? mergeHooks(base.hooks ?? {}, override.hooks) : base.hooks
53
+ } : base;
54
+
55
+ //# sourceMappingURL=mergeCollection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/mergeCollection.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\n/**\n * Additively merge two Payload hooks objects: each per-phase array in `extra` is\n * appended to the corresponding array in `base`. Phases present in only one of the two\n * are preserved as-is. User hooks always run AFTER base hooks within each phase.\n */\nconst mergeHooks = <T>(base: T, extra?: T): T => {\n if (!extra) return base\n const b = base as unknown as Record<string, unknown[] | undefined>\n const e = extra as unknown as Record<string, unknown[] | undefined>\n const out: Record<string, unknown[]> = { ...b } as Record<string, unknown[]>\n for (const key of Object.keys(e)) {\n out[key] = [...(b[key] ?? []), ...(e[key] ?? [])]\n }\n return out as unknown as T\n}\n\n/**\n * Deep-merge a partial override onto a base `CollectionConfig` without clobbering the\n * nested config a top-level spread would otherwise replace.\n *\n * Top-level keys replace, but:\n * - `access` / `admin` are shallow-merged (override keys win, base keys kept).\n * - `fields` are APPENDED (base fields first, then override fields).\n * - `upload` is shallow-merged when both sides are objects (so a partial\n * `upload: { staticDir }` keeps the base `mimeTypes` whitelist).\n * - `hooks` are merged per-phase via {@link mergeHooks} (override hooks run AFTER the\n * base hooks within each phase).\n *\n * When `override` is undefined, `base` is returned unchanged.\n */\nexport const mergeCollection = (base: CollectionConfig, override?: Partial<CollectionConfig>): CollectionConfig =>\n override\n ? {\n ...base,\n ...override,\n access: { ...base.access, ...override.access },\n admin: { ...base.admin, ...override.admin },\n fields: [...base.fields, ...(override.fields ?? [])],\n upload:\n override.upload && typeof override.upload === 'object' && typeof base.upload === 'object'\n ? { ...base.upload, ...override.upload }\n : (override.upload ?? base.upload),\n hooks: override.hooks ? mergeHooks(base.hooks ?? {}, override.hooks) : base.hooks,\n }\n : base\n"],"names":["mergeHooks","base","extra","b","e","out","key","Object","keys","mergeCollection","override","access","admin","fields","upload","hooks"],"mappings":"AAEA;;;;CAIC,GACD,MAAMA,aAAa,CAAIC,MAASC;IAC9B,IAAI,CAACA,OAAO,OAAOD;IACnB,MAAME,IAAIF;IACV,MAAMG,IAAIF;IACV,MAAMG,MAAiC;QAAE,GAAGF,CAAC;IAAC;IAC9C,KAAK,MAAMG,OAAOC,OAAOC,IAAI,CAACJ,GAAI;QAChCC,GAAG,CAACC,IAAI,GAAG;eAAKH,CAAC,CAACG,IAAI,IAAI,EAAE;eAAOF,CAAC,CAACE,IAAI,IAAI,EAAE;SAAE;IACnD;IACA,OAAOD;AACT;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMI,kBAAkB,CAACR,MAAwBS,WACtDA,WACI;QACE,GAAGT,IAAI;QACP,GAAGS,QAAQ;QACXC,QAAQ;YAAE,GAAGV,KAAKU,MAAM;YAAE,GAAGD,SAASC,MAAM;QAAC;QAC7CC,OAAO;YAAE,GAAGX,KAAKW,KAAK;YAAE,GAAGF,SAASE,KAAK;QAAC;QAC1CC,QAAQ;eAAIZ,KAAKY,MAAM;eAAMH,SAASG,MAAM,IAAI,EAAE;SAAE;QACpDC,QACEJ,SAASI,MAAM,IAAI,OAAOJ,SAASI,MAAM,KAAK,YAAY,OAAOb,KAAKa,MAAM,KAAK,WAC7E;YAAE,GAAGb,KAAKa,MAAM;YAAE,GAAGJ,SAASI,MAAM;QAAC,IACpCJ,SAASI,MAAM,IAAIb,KAAKa,MAAM;QACrCC,OAAOL,SAASK,KAAK,GAAGf,WAAWC,KAAKc,KAAK,IAAI,CAAC,GAAGL,SAASK,KAAK,IAAId,KAAKc,KAAK;IACnF,IACAd,KAAI"}
@@ -0,0 +1,128 @@
1
+ import type { CollectionConfig, Plugin } from 'payload';
2
+ import { type TransformEndpointConfig } from './endpoints/transform';
3
+ export interface ImagesPluginOptions {
4
+ /**
5
+ * When false, the plugin registers NO collections, endpoints, or hooks. This is
6
+ * "not installed", not "temporarily disabled": on SQL adapters, flipping it off for
7
+ * an existing project produces a migration that DROPS the images / generated-images
8
+ * tables (and their data). Defaults to true.
9
+ */
10
+ enabled?: boolean;
11
+ /**
12
+ * Slug of an EXISTING upload collection to add the image pipeline to (focal UI, the
13
+ * `variants` join, the purge hooks, and `upload.focalPoint`), instead of creating the
14
+ * default `images` collection. Use this when your project already has a `media`/`images`
15
+ * upload collection — no second collection, no migration. The target must be an upload
16
+ * collection (you own the collection's `upload` config, including any `imageSizes`).
17
+ */
18
+ extendCollection?: string;
19
+ /**
20
+ * Override for the Images collection. Top-level keys replace, but
21
+ * `upload`/`access`/`admin` are deep-merged and `fields`/`hooks` are merged. Note
22
+ * `fields` are APPENDED (not replaced) — don't redeclare a base field's `name`
23
+ * (e.g. `alt`/`variants`), or Payload errors on the duplicate. With `extendCollection`,
24
+ * these tweaks are merged onto the target collection instead.
25
+ */
26
+ imagesOverrides?: Partial<CollectionConfig>;
27
+ /** Override for the hidden generated-images (variant cache) collection. */
28
+ generatedImagesOverrides?: Partial<CollectionConfig>;
29
+ /**
30
+ * The project-wide srcset widths, set once and applied uniformly to the API virtual `srcset`,
31
+ * `<ResponsiveImage>`, and the endpoint's anti-DoS dimension grid. Two forms:
32
+ * - a **number** (default 50): the width increment AND the grid the endpoint snaps requests
33
+ * to — a bigger step means fewer srcset widths and fewer cached variants.
34
+ * - an **array**: an explicit, non-linear width ladder (e.g. `[200, 450, 750, 1200, 2000]`)
35
+ * for the srcset — denser where it matters, fewer entries than a fine linear step. The
36
+ * endpoint's snap grid then falls back to the default 50, so use ladder widths that are
37
+ * multiples of 50 (or set `transform.dimensionStep`) to have them pass through unchanged.
38
+ *
39
+ * `transform.maxDimension` caps the largest width in either form.
40
+ */
41
+ pixelStep?: number | number[];
42
+ /** On-demand transform endpoint config. Pass `false` to not register the endpoints. */
43
+ transform?: TransformEndpointConfig | false;
44
+ /** Render the focal + ratio-preview field and purge-variants button. Default true. */
45
+ focalUI?: boolean;
46
+ /** Aspect ratios shown in the focal preview tiles. */
47
+ previewRatios?: string[];
48
+ /**
49
+ * Add virtual `src` / `srcset` / `placeholderURL` / `thumbnailURL` fields, computed on read,
50
+ * so optimized URLs ride along in every REST / GraphQL / Local-API response (and through
51
+ * relationship population). Absolute when `serverURL` is set, relative otherwise. Default true;
52
+ * defaults to false with `transform: false` (the URLs would 404 — an explicit true is honored,
53
+ * with a boot warning).
54
+ */
55
+ virtualFields?: boolean;
56
+ /** Mark the `alt` field `localized: true` (requires Payload localization). Ignored with
57
+ * `extendCollection`. Default false. */
58
+ localizeAlt?: boolean;
59
+ /**
60
+ * Accepted upload mime types for the `images` collection. Defaults to the raster formats the
61
+ * transform pipeline can process (avif/webp/jpeg/png). Widen it to accept more (e.g. add
62
+ * `'image/svg+xml'`) or narrow it — but the endpoint only meaningfully resizes/crops raster
63
+ * images, so non-raster uploads are stored and served as-is. Ignored with `extendCollection`
64
+ * (you own that collection's `upload.mimeTypes`).
65
+ */
66
+ mimeTypes?: string[];
67
+ /**
68
+ * Enable Payload's native **folder organization** on the managed collection (the created `images`,
69
+ * or the `extendCollection` target) — adds a folder relationship to the schema so editors can
70
+ * organize a large library. Default false.
71
+ */
72
+ folders?: boolean;
73
+ /**
74
+ * Cap the *stored* original's longest edge, in px (applied once on upload via Payload's
75
+ * `resizeOptions`). **Off by default — your original stays untouched** (so the collection can
76
+ * double as original storage); set it only to bound storage. Ignored with `extendCollection`.
77
+ */
78
+ maxOriginalSize?: number;
79
+ /**
80
+ * Inline LQIP placeholder for `<ResponsiveImage>`. A tiny, faithful (per aspect-ratio +
81
+ * focal point) image is generated server-side, base64-inlined behind the real image, and
82
+ * painted instantly with zero network. Pass `false` to disable it project-wide. Defaults:
83
+ * 24px / quality 40 / webp.
84
+ */
85
+ placeholder?: false | PlaceholderConfig;
86
+ }
87
+ /** Inline-LQIP placeholder settings (see {@link ImagesPluginOptions.placeholder}). */
88
+ export interface PlaceholderConfig {
89
+ /**
90
+ * Default longest edge of the LQIP, in px. Default 24. Keep it tiny — the LQIP is inlined as
91
+ * base64 in **every** response, so size compounds: ~0.6 KB at 24px, ~2 KB at 48, ~3–4 KB at 64.
92
+ * **24–64 is the sensible range**; past ~64 it stops being a placeholder and bloats payloads.
93
+ */
94
+ width?: number;
95
+ /** Encode quality for the LQIP. Default 40 (clamped to 20–70 — LQIPs gain nothing from more). */
96
+ quality?: number;
97
+ /** LQIP encode format. Default `webp`. */
98
+ format?: 'webp' | 'jpeg';
99
+ /**
100
+ * Hard ceiling for a per-read width override coming from the **untrusted** external door
101
+ * (`req.context.lqip` / `X-LQIP`): such widths are clamped to this and snapped to a /8 grid.
102
+ * Default 64. The trusted `<ResponsiveImage placeholder={…}>` prop is *not* bound by this
103
+ * (it honors your value up to a typo guard) — raise this only if you let external callers pick
104
+ * larger LQIPs, knowing each px inlines more base64. Recommended ≤ ~96.
105
+ */
106
+ maxWidth?: number;
107
+ }
108
+ /** The resolved placeholder settings stashed for the component, or `false` when disabled. */
109
+ export type ResolvedPlaceholder = false | Required<PlaceholderConfig>;
110
+ /** Fill in the placeholder defaults (24px / q40 / webp / maxWidth 64), or `false` when disabled. */
111
+ export declare const resolvePlaceholder: (p: ImagesPluginOptions["placeholder"]) => ResolvedPlaceholder;
112
+ /**
113
+ * Registers the `images` (source) and hidden `generated-images` (variant cache) collections,
114
+ * plus the on-demand transform + purge endpoints.
115
+ *
116
+ * Uploads store only the original; every rendered size is generated the first time a page asks
117
+ * for it (resized and cropped to the focal point set in the admin), then cached in
118
+ * `generated-images` so it's only ever built once. The LQIP placeholder is generated on demand by
119
+ * `<ResponsiveImage>` (a tiny inline base64, via the same variant cache) — nothing is stored on the
120
+ * source doc.
121
+ *
122
+ * Pass `extendCollection: '<slug>'` to add the pipeline to an upload collection you already have
123
+ * instead of creating `images`. The transform endpoint mounts at `/api/img`; do not name a
124
+ * collection `img` or it shadows the endpoint.
125
+ */
126
+ export declare const imagesPlugin: (opts?: ImagesPluginOptions) => Plugin;
127
+ export default imagesPlugin;
128
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAU,MAAM,EAAE,MAAM,SAAS,CAAA;AAI/D,OAAO,EAAgD,KAAK,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AAOlH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC3C,2EAA2E;IAC3E,wBAAwB,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IACpD;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC7B,uFAAuF;IACvF,SAAS,CAAC,EAAE,uBAAuB,GAAG,KAAK,CAAA;IAC3C,sFAAsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,sDAAsD;IACtD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;6CACyC;IACzC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,KAAK,GAAG,iBAAiB,CAAA;CACxC;AAED,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,6FAA6F;AAC7F,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,QAAQ,CAAC,iBAAiB,CAAC,CAAA;AAErE,oGAAoG;AACpG,eAAO,MAAM,kBAAkB,GAAI,GAAG,mBAAmB,CAAC,aAAa,CAAC,KAAG,mBAC2D,CAAA;AAEtI;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,GACtB,OAAM,mBAAwB,KAAG,MAkLjC,CAAA;AAEH,eAAe,YAAY,CAAA"}