@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,163 @@
1
+ /**
2
+ * The shared "give me the bytes for this variant" engine: compute the cache key, return the
3
+ * cached variant if one exists, otherwise generate it with Sharp once, persist it after the
4
+ * response, and return the bytes. Extracted from the transform endpoint so the on-demand
5
+ * `/api/img` route and `<ResponsiveImage>`'s inline LQIP run the EXACT same
6
+ * read-or-generate-or-persist path against the one `generated-images` cache — a given
7
+ * (source, size, fit, quality, format) variant is generated once, whichever door triggered it.
8
+ */ import { after } from "next/server";
9
+ import { variantCacheKey } from "../variants/key.js";
10
+ import { extForFormat, mimeForFormat } from "./params.js";
11
+ import { transformImage } from "./sharp.js";
12
+ import { readBytes, resolveStaticDir } from "./source.js";
13
+ /** True for a unique-constraint violation on the variant create. Two requests racing the same
14
+ * cache miss both persist; the loser is expected, not noise. The violation arrives in several
15
+ * shapes — a raw driver error mentioning duplicate/unique, a wrapper (drizzle's "Failed query:
16
+ * insert …") whose `cause` carries the code (e.g. SQLITE_CONSTRAINT_UNIQUE), or Payload's
17
+ * ValidationError ("The following field is invalid: cacheKey") — so walk the cause chain and
18
+ * the field errors. */ const isDuplicateKeyError = (err)=>{
19
+ let e = err;
20
+ for(let depth = 0; depth < 4 && e; depth++){
21
+ const msg = e instanceof Error ? e.message : String(e);
22
+ const code = e?.code;
23
+ if (/duplicate|unique/i.test(`${msg} ${typeof code === 'string' ? code : ''}`)) return true;
24
+ e = e?.cause;
25
+ }
26
+ const fieldErrors = err?.data?.errors;
27
+ return Array.isArray(fieldErrors) && fieldErrors.some((f)=>f.path === 'cacheKey' || /unique/i.test(f.message ?? ''));
28
+ };
29
+ /** True when Sharp itself failed to load (module missing or native binding broken) — the fix is
30
+ * the install, not this image, so the generic "transform failed" line alone would mislead. */ const isSharpLoadError = (err)=>{
31
+ const s = `${String(err)} ${String(err?.code ?? '')}`;
32
+ return /sharp|libvips/i.test(s) && /cannot find module|module_not_found|could not load|native|binding/i.test(s);
33
+ };
34
+ /** The actionable fix for a Sharp load failure — shared by the boot probe and the request-time catch. */ export const SHARP_INSTALL_HINT = "install it (`pnpm add sharp`) and externalize it in next.config (`serverExternalPackages: ['sharp']`)";
35
+ /**
36
+ * Return the bytes for one variant: cache hit → stored copy; miss → Sharp once, persist after
37
+ * the response (via Next's `after()`, falling back to fire-and-forget), then return the bytes.
38
+ * Generation is coalesced by cache key when a `genFlight` is supplied.
39
+ */ export const getOrCreateVariantBytes = async (args)=>{
40
+ const { payload, source: src, params: p, format, sourceSlug, variantSlug, base, maxInputPixels, genFlight } = args;
41
+ const key = variantCacheKey({
42
+ id: src.id,
43
+ filename: src.filename,
44
+ focalX: src.focalX,
45
+ focalY: src.focalY
46
+ }, p, format);
47
+ // Cache hit → the stored variant's bytes.
48
+ try {
49
+ const hit = await payload.find({
50
+ collection: variantSlug,
51
+ where: {
52
+ cacheKey: {
53
+ equals: key
54
+ }
55
+ },
56
+ limit: 1,
57
+ depth: 0,
58
+ overrideAccess: true
59
+ });
60
+ const variant = hit?.docs?.[0];
61
+ if (variant) {
62
+ const bytes = await readBytes(variant, resolveStaticDir(payload, variantSlug), base, {
63
+ payload,
64
+ slug: variantSlug
65
+ });
66
+ if (bytes) return {
67
+ ok: true,
68
+ data: bytes,
69
+ mimeType: mimeForFormat(format),
70
+ key
71
+ };
72
+ }
73
+ } catch {}
74
+ const generate = async ()=>{
75
+ const original = await readBytes(src, resolveStaticDir(payload, sourceSlug), base, {
76
+ payload,
77
+ slug: sourceSlug
78
+ });
79
+ if (!original) {
80
+ // Only the relative-URL path needs an origin to resolve; surface the serverURL hint
81
+ // just here, when a read has actually failed — not preemptively at boot.
82
+ const relative = !!src.url && !/^https?:\/\//i.test(src.url);
83
+ const hint = relative ? ' — relative-URL storage and the request origin did not resolve; set serverURL in buildConfig' : '';
84
+ payload.logger.warn(`[payload-images] source ${src.id} unreadable (filename=${src.filename ?? 'none'}, url=${src.url ?? 'none'})${hint}`);
85
+ return {
86
+ ok: false,
87
+ status: 502,
88
+ msg: 'Source unavailable'
89
+ };
90
+ }
91
+ let out;
92
+ try {
93
+ out = await transformImage(original, {
94
+ w: p.w,
95
+ h: p.h,
96
+ fit: p.fit,
97
+ quality: p.q,
98
+ format,
99
+ focalX: src.focalX,
100
+ focalY: src.focalY,
101
+ maxInputPixels
102
+ });
103
+ } catch (err) {
104
+ const hint = isSharpLoadError(err) ? ` — sharp failed to load; ${SHARP_INSTALL_HINT}` : '';
105
+ payload.logger.error(`[payload-images] transform failed for ${src.id}: ${String(err)}${hint}`);
106
+ return {
107
+ ok: false,
108
+ status: 500,
109
+ msg: 'Transform failed'
110
+ };
111
+ }
112
+ const persist = async ()=>{
113
+ try {
114
+ await payload.create({
115
+ collection: variantSlug,
116
+ file: {
117
+ data: out.data,
118
+ mimetype: out.mimeType,
119
+ name: `${key}.${extForFormat(format)}`,
120
+ size: out.data.byteLength
121
+ },
122
+ data: {
123
+ source: src.id,
124
+ cacheKey: key,
125
+ fit: p.fit,
126
+ format,
127
+ quality: p.q,
128
+ focalX: src.focalX ?? null,
129
+ focalY: src.focalY ?? null
130
+ },
131
+ overwriteExistingFiles: true,
132
+ overrideAccess: true
133
+ });
134
+ } catch (err) {
135
+ if (!isDuplicateKeyError(err)) payload.logger.warn(`[payload-images] failed to persist variant ${key} for source ${src.id}: ${String(err)}`);
136
+ }
137
+ };
138
+ try {
139
+ after(persist);
140
+ } catch {
141
+ void persist();
142
+ }
143
+ return {
144
+ ok: true,
145
+ data: out.data,
146
+ mimeType: out.mimeType
147
+ };
148
+ };
149
+ const result = genFlight ? await genFlight(key, generate) : await generate();
150
+ return result.ok ? {
151
+ ok: true,
152
+ data: result.data,
153
+ mimeType: result.mimeType,
154
+ key
155
+ } : {
156
+ ok: false,
157
+ status: result.status,
158
+ msg: result.msg,
159
+ key
160
+ };
161
+ };
162
+
163
+ //# sourceMappingURL=getVariantBytes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transform/getVariantBytes.ts"],"sourcesContent":["/**\n * The shared \"give me the bytes for this variant\" engine: compute the cache key, return the\n * cached variant if one exists, otherwise generate it with Sharp once, persist it after the\n * response, and return the bytes. Extracted from the transform endpoint so the on-demand\n * `/api/img` route and `<ResponsiveImage>`'s inline LQIP run the EXACT same\n * read-or-generate-or-persist path against the one `generated-images` cache — a given\n * (source, size, fit, quality, format) variant is generated once, whichever door triggered it.\n */\nimport { after } from 'next/server'\n\nimport type { CollectionSlug, Payload } from 'payload'\n\nimport { variantCacheKey } from '../variants/key'\nimport { extForFormat, mimeForFormat, type OutputFormat, type ParsedParams } from './params'\nimport { transformImage, type TransformOutput } from './sharp'\nimport { readBytes, resolveStaticDir, type UploadDocLike } from './source'\n\n/** A resolved source doc: id + where-the-bytes-live + focal point. */\nexport type VariantSourceDoc = UploadDocLike & { id: string | number; focalX?: number | null; focalY?: number | null }\n\n/** Generation outcome (bytes or a typed failure). */\nexport type GenBytes = { ok: true; data: Buffer; mimeType: string } | { ok: false; status: number; msg: string }\n\n/** Optional generation coalescer (the endpoint passes its per-process single-flight; omit at render). */\ntype GenFlight = (key: string, fn: () => Promise<GenBytes>) => Promise<GenBytes>\n\n/** Result of {@link getOrCreateVariantBytes} — bytes + the cache key (for ETag), or a typed failure. */\nexport type VariantBytes = { ok: true; data: Buffer; mimeType: string; key: string } | { ok: false; status: number; msg: string; key: string }\n\nexport interface GetVariantBytesArgs {\n payload: Payload\n /** Resolved source doc (id + filename/url + focal point). */\n source: VariantSourceDoc\n /** Parsed + snapped transform params. */\n params: ParsedParams\n /** Concrete output format (never `auto`). */\n format: OutputFormat\n sourceSlug: string\n variantSlug: string\n /** Origin used to read originals/variants served from a relative/cloud URL. */\n base: string\n /** Decompression-bomb / memory guard passed to Sharp. */\n maxInputPixels: number\n /** Generation coalescer; the endpoint passes its single-flight, render-time callers omit it. */\n genFlight?: GenFlight\n}\n\n/** True for a unique-constraint violation on the variant create. Two requests racing the same\n * cache miss both persist; the loser is expected, not noise. The violation arrives in several\n * shapes — a raw driver error mentioning duplicate/unique, a wrapper (drizzle's \"Failed query:\n * insert …\") whose `cause` carries the code (e.g. SQLITE_CONSTRAINT_UNIQUE), or Payload's\n * ValidationError (\"The following field is invalid: cacheKey\") — so walk the cause chain and\n * the field errors. */\nconst isDuplicateKeyError = (err: unknown): boolean => {\n let e: unknown = err\n for (let depth = 0; depth < 4 && e; depth++) {\n const msg = e instanceof Error ? e.message : String(e)\n const code = (e as { code?: unknown })?.code\n if (/duplicate|unique/i.test(`${msg} ${typeof code === 'string' ? code : ''}`)) return true\n e = (e as { cause?: unknown })?.cause\n }\n const fieldErrors = (err as { data?: { errors?: Array<{ message?: string; path?: string }> } })?.data?.errors\n return Array.isArray(fieldErrors) && fieldErrors.some((f) => f.path === 'cacheKey' || /unique/i.test(f.message ?? ''))\n}\n\n/** True when Sharp itself failed to load (module missing or native binding broken) — the fix is\n * the install, not this image, so the generic \"transform failed\" line alone would mislead. */\nconst isSharpLoadError = (err: unknown): boolean => {\n const s = `${String(err)} ${String((err as { code?: unknown })?.code ?? '')}`\n return /sharp|libvips/i.test(s) && /cannot find module|module_not_found|could not load|native|binding/i.test(s)\n}\n\n/** The actionable fix for a Sharp load failure — shared by the boot probe and the request-time catch. */\nexport const SHARP_INSTALL_HINT = \"install it (`pnpm add sharp`) and externalize it in next.config (`serverExternalPackages: ['sharp']`)\"\n\n/**\n * Return the bytes for one variant: cache hit → stored copy; miss → Sharp once, persist after\n * the response (via Next's `after()`, falling back to fire-and-forget), then return the bytes.\n * Generation is coalesced by cache key when a `genFlight` is supplied.\n */\nexport const getOrCreateVariantBytes = async (args: GetVariantBytesArgs): Promise<VariantBytes> => {\n const { payload, source: src, params: p, format, sourceSlug, variantSlug, base, maxInputPixels, genFlight } = args\n const key = variantCacheKey({ id: src.id, filename: src.filename, focalX: src.focalX, focalY: src.focalY }, p, format)\n\n // Cache hit → the stored variant's bytes.\n try {\n const hit = await payload.find({\n collection: variantSlug as CollectionSlug,\n where: { cacheKey: { equals: key } },\n limit: 1,\n depth: 0,\n overrideAccess: true,\n })\n const variant = hit?.docs?.[0] as (UploadDocLike & { id: string | number }) | undefined\n if (variant) {\n const bytes = await readBytes(variant, resolveStaticDir(payload, variantSlug), base, { payload, slug: variantSlug })\n if (bytes) return { ok: true, data: bytes, mimeType: mimeForFormat(format), key }\n }\n } catch {}\n\n const generate = async (): Promise<GenBytes> => {\n const original = await readBytes(src, resolveStaticDir(payload, sourceSlug), base, { payload, slug: sourceSlug })\n if (!original) {\n // Only the relative-URL path needs an origin to resolve; surface the serverURL hint\n // just here, when a read has actually failed — not preemptively at boot.\n const relative = !!src.url && !/^https?:\\/\\//i.test(src.url)\n const hint = relative ? ' — relative-URL storage and the request origin did not resolve; set serverURL in buildConfig' : ''\n payload.logger.warn(`[payload-images] source ${src.id} unreadable (filename=${src.filename ?? 'none'}, url=${src.url ?? 'none'})${hint}`)\n return { ok: false, status: 502, msg: 'Source unavailable' }\n }\n\n let out: TransformOutput\n try {\n out = await transformImage(original, {\n w: p.w,\n h: p.h,\n fit: p.fit,\n quality: p.q,\n format,\n focalX: src.focalX,\n focalY: src.focalY,\n maxInputPixels,\n })\n } catch (err) {\n const hint = isSharpLoadError(err) ? ` — sharp failed to load; ${SHARP_INSTALL_HINT}` : ''\n payload.logger.error(`[payload-images] transform failed for ${src.id}: ${String(err)}${hint}`)\n return { ok: false, status: 500, msg: 'Transform failed' }\n }\n\n const persist = async (): Promise<void> => {\n try {\n await payload.create({\n collection: variantSlug as CollectionSlug,\n file: { data: out.data, mimetype: out.mimeType, name: `${key}.${extForFormat(format)}`, size: out.data.byteLength },\n data: {\n source: src.id as never,\n cacheKey: key,\n fit: p.fit,\n format,\n quality: p.q,\n focalX: src.focalX ?? null,\n focalY: src.focalY ?? null,\n },\n overwriteExistingFiles: true,\n overrideAccess: true,\n })\n } catch (err) {\n if (!isDuplicateKeyError(err))\n payload.logger.warn(`[payload-images] failed to persist variant ${key} for source ${src.id}: ${String(err)}`)\n }\n }\n try {\n after(persist)\n } catch {\n void persist()\n }\n\n return { ok: true, data: out.data, mimeType: out.mimeType }\n }\n\n const result = genFlight ? await genFlight(key, generate) : await generate()\n return result.ok\n ? { ok: true, data: result.data, mimeType: result.mimeType, key }\n : { ok: false, status: result.status, msg: result.msg, key }\n}\n"],"names":["after","variantCacheKey","extForFormat","mimeForFormat","transformImage","readBytes","resolveStaticDir","isDuplicateKeyError","err","e","depth","msg","Error","message","String","code","test","cause","fieldErrors","data","errors","Array","isArray","some","f","path","isSharpLoadError","s","SHARP_INSTALL_HINT","getOrCreateVariantBytes","args","payload","source","src","params","p","format","sourceSlug","variantSlug","base","maxInputPixels","genFlight","key","id","filename","focalX","focalY","hit","find","collection","where","cacheKey","equals","limit","overrideAccess","variant","docs","bytes","slug","ok","mimeType","generate","original","relative","url","hint","logger","warn","status","out","w","h","fit","quality","q","error","persist","create","file","mimetype","name","size","byteLength","overwriteExistingFiles","result"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** Override the max concurrent transforms (no-op for non-positive / non-finite values). */
2
+ export declare const setTransformConcurrency: (n?: number) => void;
3
+ /** Run `fn` once a concurrency slot frees up; releases the slot when it settles. */
4
+ export declare const withTransformLimit: <T>(fn: () => Promise<T>) => Promise<T>;
5
+ //# sourceMappingURL=limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limit.d.ts","sourceRoot":"","sources":["../../src/transform/limit.ts"],"names":[],"mappings":"AAqBA,2FAA2F;AAC3F,eAAO,MAAM,uBAAuB,GAAI,IAAI,MAAM,KAAG,IAEpD,CAAA;AAUD,oFAAoF;AACpF,eAAO,MAAM,kBAAkB,GAAU,CAAC,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAW3E,CAAA"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A tiny FIFO concurrency gate for the CPU-bound Sharp transforms. On a cold page the
3
+ * browser fires every srcset width at once; without a cap each spawns a Sharp
4
+ * decode+encode in parallel and the burst can exhaust memory / CPU on a serverless
5
+ * function. This bounds how many run concurrently; the rest queue.
6
+ *
7
+ * The default is `cpus - 1` (min 1), overridable via the `IMAGES_TRANSFORM_CONCURRENCY`
8
+ * env var or the transform endpoint's `maxConcurrency` option. Module-level singleton —
9
+ * shared across every request in the process.
10
+ */ import os from "node:os";
11
+ const envLimit = ()=>{
12
+ const n = Number(process.env.IMAGES_TRANSFORM_CONCURRENCY);
13
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
14
+ };
15
+ let limit = envLimit() ?? Math.max(1, (os.cpus?.().length ?? 4) - 1);
16
+ let active = 0;
17
+ const queue = [];
18
+ /** Override the max concurrent transforms (no-op for non-positive / non-finite values). */ export const setTransformConcurrency = (n)=>{
19
+ if (n != null && Number.isFinite(n) && n > 0) limit = Math.floor(n);
20
+ };
21
+ const pump = ()=>{
22
+ if (active >= limit) return;
23
+ const run = queue.shift();
24
+ if (!run) return;
25
+ active++;
26
+ run();
27
+ };
28
+ /** Run `fn` once a concurrency slot frees up; releases the slot when it settles. */ export const withTransformLimit = async (fn)=>{
29
+ await new Promise((resolve)=>{
30
+ queue.push(resolve);
31
+ pump();
32
+ });
33
+ try {
34
+ return await fn();
35
+ } finally{
36
+ active--;
37
+ pump();
38
+ }
39
+ };
40
+
41
+ //# sourceMappingURL=limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transform/limit.ts"],"sourcesContent":["/**\n * A tiny FIFO concurrency gate for the CPU-bound Sharp transforms. On a cold page the\n * browser fires every srcset width at once; without a cap each spawns a Sharp\n * decode+encode in parallel and the burst can exhaust memory / CPU on a serverless\n * function. This bounds how many run concurrently; the rest queue.\n *\n * The default is `cpus - 1` (min 1), overridable via the `IMAGES_TRANSFORM_CONCURRENCY`\n * env var or the transform endpoint's `maxConcurrency` option. Module-level singleton —\n * shared across every request in the process.\n */\nimport os from 'node:os'\n\nconst envLimit = (): number | undefined => {\n const n = Number(process.env.IMAGES_TRANSFORM_CONCURRENCY)\n return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined\n}\n\nlet limit = envLimit() ?? Math.max(1, (os.cpus?.().length ?? 4) - 1)\nlet active = 0\nconst queue: Array<() => void> = []\n\n/** Override the max concurrent transforms (no-op for non-positive / non-finite values). */\nexport const setTransformConcurrency = (n?: number): void => {\n if (n != null && Number.isFinite(n) && n > 0) limit = Math.floor(n)\n}\n\nconst pump = (): void => {\n if (active >= limit) return\n const run = queue.shift()\n if (!run) return\n active++\n run()\n}\n\n/** Run `fn` once a concurrency slot frees up; releases the slot when it settles. */\nexport const withTransformLimit = async <T>(fn: () => Promise<T>): Promise<T> => {\n await new Promise<void>((resolve) => {\n queue.push(resolve)\n pump()\n })\n try {\n return await fn()\n } finally {\n active--\n pump()\n }\n}\n"],"names":["os","envLimit","n","Number","process","env","IMAGES_TRANSFORM_CONCURRENCY","isFinite","Math","floor","undefined","limit","max","cpus","length","active","queue","setTransformConcurrency","pump","run","shift","withTransformLimit","fn","Promise","resolve","push"],"mappings":"AAAA;;;;;;;;;CASC,GACD,OAAOA,QAAQ,UAAS;AAExB,MAAMC,WAAW;IACf,MAAMC,IAAIC,OAAOC,QAAQC,GAAG,CAACC,4BAA4B;IACzD,OAAOH,OAAOI,QAAQ,CAACL,MAAMA,IAAI,IAAIM,KAAKC,KAAK,CAACP,KAAKQ;AACvD;AAEA,IAAIC,QAAQV,cAAcO,KAAKI,GAAG,CAAC,GAAG,AAACZ,CAAAA,GAAGa,IAAI,KAAKC,UAAU,CAAA,IAAK;AAClE,IAAIC,SAAS;AACb,MAAMC,QAA2B,EAAE;AAEnC,yFAAyF,GACzF,OAAO,MAAMC,0BAA0B,CAACf;IACtC,IAAIA,KAAK,QAAQC,OAAOI,QAAQ,CAACL,MAAMA,IAAI,GAAGS,QAAQH,KAAKC,KAAK,CAACP;AACnE,EAAC;AAED,MAAMgB,OAAO;IACX,IAAIH,UAAUJ,OAAO;IACrB,MAAMQ,MAAMH,MAAMI,KAAK;IACvB,IAAI,CAACD,KAAK;IACVJ;IACAI;AACF;AAEA,kFAAkF,GAClF,OAAO,MAAME,qBAAqB,OAAUC;IAC1C,MAAM,IAAIC,QAAc,CAACC;QACvBR,MAAMS,IAAI,CAACD;QACXN;IACF;IACA,IAAI;QACF,OAAO,MAAMI;IACf,SAAU;QACRP;QACAG;IACF;AACF,EAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Pure LQIP size/quality resolution — no Sharp, no Payload, safe to unit-test in isolation.
3
+ *
4
+ * Trust split: the component (the developer's own JSX) *honors* a requested width up to an absolute
5
+ * typo guard — its feedback is a dev warning, not a silent cap. The external `context.lqip` door is
6
+ * untrusted, so it *clamps* to the project `maxWidth` and snaps to a /8 grid (bounding the variant
7
+ * space the way the endpoint's `dimensionStep` does). Both clamp quality to a small LQIP range.
8
+ */
9
+ export declare const LQIP_MIN_WIDTH = 8;
10
+ /** Absolute typo guard for the trusted (component) path — honored up to here, never beyond. */
11
+ export declare const LQIP_HARD_MAX_WIDTH = 256;
12
+ /** Quality clamp — LQIPs gain nothing from high quality, and a small range bounds the variant space. */
13
+ export declare const LQIP_QUALITY_RANGE: [number, number];
14
+ /**
15
+ * Resolve the LQIP width. `untrusted` (the external door) clamps to `[MIN, maxWidth]` and snaps to
16
+ * a /8 grid. Trusted (the component) honors the request up to {@link LQIP_HARD_MAX_WIDTH}.
17
+ */
18
+ export declare const resolveLqipWidth: (requested: number | undefined, defaultW: number, maxWidth: number, untrusted: boolean) => number;
19
+ /** Clamp a requested LQIP quality to {@link LQIP_QUALITY_RANGE}, falling back to `defaultQ`. */
20
+ export declare const clampLqipQuality: (requested: number | undefined, defaultQ: number) => number;
21
+ //# sourceMappingURL=lqip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lqip.d.ts","sourceRoot":"","sources":["../../src/transform/lqip.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,eAAO,MAAM,cAAc,IAAI,CAAA;AAC/B,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB,MAAM,CAAA;AACtC,wGAAwG;AACxG,eAAO,MAAM,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAY,CAAA;AAK5D;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,GAAG,SAAS,EAAE,UAAU,MAAM,EAAE,UAAU,MAAM,EAAE,WAAW,OAAO,KAAG,MAWxH,CAAA;AAED,gGAAgG;AAChG,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,GAAG,SAAS,EAAE,UAAU,MAAM,KAAG,MACe,CAAA"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Pure LQIP size/quality resolution — no Sharp, no Payload, safe to unit-test in isolation.
3
+ *
4
+ * Trust split: the component (the developer's own JSX) *honors* a requested width up to an absolute
5
+ * typo guard — its feedback is a dev warning, not a silent cap. The external `context.lqip` door is
6
+ * untrusted, so it *clamps* to the project `maxWidth` and snaps to a /8 grid (bounding the variant
7
+ * space the way the endpoint's `dimensionStep` does). Both clamp quality to a small LQIP range.
8
+ */ export const LQIP_MIN_WIDTH = 8;
9
+ /** Absolute typo guard for the trusted (component) path — honored up to here, never beyond. */ export const LQIP_HARD_MAX_WIDTH = 256;
10
+ /** Quality clamp — LQIPs gain nothing from high quality, and a small range bounds the variant space. */ export const LQIP_QUALITY_RANGE = [
11
+ 20,
12
+ 70
13
+ ];
14
+ const clamp = (n, lo, hi)=>Math.max(lo, Math.min(hi, n));
15
+ const positive = (n)=>typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : undefined;
16
+ /**
17
+ * Resolve the LQIP width. `untrusted` (the external door) clamps to `[MIN, maxWidth]` and snaps to
18
+ * a /8 grid. Trusted (the component) honors the request up to {@link LQIP_HARD_MAX_WIDTH}.
19
+ */ export const resolveLqipWidth = (requested, defaultW, maxWidth, untrusted)=>{
20
+ const w = Math.round(positive(requested) ?? defaultW);
21
+ if (untrusted) {
22
+ const cap = Math.max(LQIP_MIN_WIDTH, Math.round(maxWidth));
23
+ return Math.max(LQIP_MIN_WIDTH, Math.round(clamp(w, LQIP_MIN_WIDTH, cap) / 8) * 8);
24
+ }
25
+ const applied = clamp(w, LQIP_MIN_WIDTH, LQIP_HARD_MAX_WIDTH);
26
+ // The trusted path's promised feedback: honored up to the guard, but the guard is never silent.
27
+ if (applied !== w && process.env.NODE_ENV !== 'production') console.warn(`[payload-images] LQIP width ${w} is outside the ${LQIP_MIN_WIDTH}–${LQIP_HARD_MAX_WIDTH} guard — using ${applied}.`);
28
+ return applied;
29
+ };
30
+ /** Clamp a requested LQIP quality to {@link LQIP_QUALITY_RANGE}, falling back to `defaultQ`. */ export const clampLqipQuality = (requested, defaultQ)=>clamp(Math.round(positive(requested) ?? defaultQ), LQIP_QUALITY_RANGE[0], LQIP_QUALITY_RANGE[1]);
31
+
32
+ //# sourceMappingURL=lqip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transform/lqip.ts"],"sourcesContent":["/**\n * Pure LQIP size/quality resolution — no Sharp, no Payload, safe to unit-test in isolation.\n *\n * Trust split: the component (the developer's own JSX) *honors* a requested width up to an absolute\n * typo guard — its feedback is a dev warning, not a silent cap. The external `context.lqip` door is\n * untrusted, so it *clamps* to the project `maxWidth` and snaps to a /8 grid (bounding the variant\n * space the way the endpoint's `dimensionStep` does). Both clamp quality to a small LQIP range.\n */\n\nexport const LQIP_MIN_WIDTH = 8\n/** Absolute typo guard for the trusted (component) path — honored up to here, never beyond. */\nexport const LQIP_HARD_MAX_WIDTH = 256\n/** Quality clamp — LQIPs gain nothing from high quality, and a small range bounds the variant space. */\nexport const LQIP_QUALITY_RANGE: [number, number] = [20, 70]\n\nconst clamp = (n: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, n))\nconst positive = (n: number | undefined): number | undefined => (typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : undefined)\n\n/**\n * Resolve the LQIP width. `untrusted` (the external door) clamps to `[MIN, maxWidth]` and snaps to\n * a /8 grid. Trusted (the component) honors the request up to {@link LQIP_HARD_MAX_WIDTH}.\n */\nexport const resolveLqipWidth = (requested: number | undefined, defaultW: number, maxWidth: number, untrusted: boolean): number => {\n const w = Math.round(positive(requested) ?? defaultW)\n if (untrusted) {\n const cap = Math.max(LQIP_MIN_WIDTH, Math.round(maxWidth))\n return Math.max(LQIP_MIN_WIDTH, Math.round(clamp(w, LQIP_MIN_WIDTH, cap) / 8) * 8)\n }\n const applied = clamp(w, LQIP_MIN_WIDTH, LQIP_HARD_MAX_WIDTH)\n // The trusted path's promised feedback: honored up to the guard, but the guard is never silent.\n if (applied !== w && process.env.NODE_ENV !== 'production')\n console.warn(`[payload-images] LQIP width ${w} is outside the ${LQIP_MIN_WIDTH}–${LQIP_HARD_MAX_WIDTH} guard — using ${applied}.`)\n return applied\n}\n\n/** Clamp a requested LQIP quality to {@link LQIP_QUALITY_RANGE}, falling back to `defaultQ`. */\nexport const clampLqipQuality = (requested: number | undefined, defaultQ: number): number =>\n clamp(Math.round(positive(requested) ?? defaultQ), LQIP_QUALITY_RANGE[0], LQIP_QUALITY_RANGE[1])\n"],"names":["LQIP_MIN_WIDTH","LQIP_HARD_MAX_WIDTH","LQIP_QUALITY_RANGE","clamp","n","lo","hi","Math","max","min","positive","Number","isFinite","undefined","resolveLqipWidth","requested","defaultW","maxWidth","untrusted","w","round","cap","applied","process","env","NODE_ENV","console","warn","clampLqipQuality","defaultQ"],"mappings":"AAAA;;;;;;;CAOC,GAED,OAAO,MAAMA,iBAAiB,EAAC;AAC/B,6FAA6F,GAC7F,OAAO,MAAMC,sBAAsB,IAAG;AACtC,sGAAsG,GACtG,OAAO,MAAMC,qBAAuC;IAAC;IAAI;CAAG,CAAA;AAE5D,MAAMC,QAAQ,CAACC,GAAWC,IAAYC,KAAuBC,KAAKC,GAAG,CAACH,IAAIE,KAAKE,GAAG,CAACH,IAAIF;AACvF,MAAMM,WAAW,CAACN,IAA+C,OAAOA,MAAM,YAAYO,OAAOC,QAAQ,CAACR,MAAMA,IAAI,IAAIA,IAAIS;AAE5H;;;CAGC,GACD,OAAO,MAAMC,mBAAmB,CAACC,WAA+BC,UAAkBC,UAAkBC;IAClG,MAAMC,IAAIZ,KAAKa,KAAK,CAACV,SAASK,cAAcC;IAC5C,IAAIE,WAAW;QACb,MAAMG,MAAMd,KAAKC,GAAG,CAACR,gBAAgBO,KAAKa,KAAK,CAACH;QAChD,OAAOV,KAAKC,GAAG,CAACR,gBAAgBO,KAAKa,KAAK,CAACjB,MAAMgB,GAAGnB,gBAAgBqB,OAAO,KAAK;IAClF;IACA,MAAMC,UAAUnB,MAAMgB,GAAGnB,gBAAgBC;IACzC,gGAAgG;IAChG,IAAIqB,YAAYH,KAAKI,QAAQC,GAAG,CAACC,QAAQ,KAAK,cAC5CC,QAAQC,IAAI,CAAC,CAAC,4BAA4B,EAAER,EAAE,gBAAgB,EAAEnB,eAAe,CAAC,EAAEC,oBAAoB,eAAe,EAAEqB,QAAQ,CAAC,CAAC;IACnI,OAAOA;AACT,EAAC;AAED,8FAA8F,GAC9F,OAAO,MAAMM,mBAAmB,CAACb,WAA+Bc,WAC9D1B,MAAMI,KAAKa,KAAK,CAACV,SAASK,cAAcc,WAAW3B,kBAAkB,CAAC,EAAE,EAAEA,kBAAkB,CAAC,EAAE,EAAC"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Pure parsing / validation for the on-demand transform endpoint. No Sharp, no
3
+ * Payload, no Node APIs beyond standard JS — safe to unit test in isolation and
4
+ * (for the URL helpers it shares shapes with) reason about deterministically.
5
+ */
6
+ export type Fit = 'cover' | 'contain' | 'inside' | 'outside' | 'fill';
7
+ export type Format = 'auto' | 'avif' | 'webp' | 'jpeg' | 'png';
8
+ /** A concrete output format (never `auto`). */
9
+ export type OutputFormat = Exclude<Format, 'auto'>;
10
+ export declare const FITS: Fit[];
11
+ export interface TransformConstraints {
12
+ /** Hard ceiling on either output dimension. */
13
+ maxDimension: number;
14
+ /** [min, max] clamp for quality. */
15
+ qualityRange: [number, number];
16
+ /** Quality used when the request omits `q`. */
17
+ defaultQuality: number;
18
+ /** Formats the endpoint may emit. */
19
+ formats: Format[];
20
+ /** Format used when the request omits `fmt`. */
21
+ defaultFormat: Format;
22
+ /**
23
+ * Auto-negotiate AVIF when the browser accepts it. Off by default: AVIF encoding is
24
+ * far slower than WebP, so on-demand `fmt=auto` serves WebP for a fast cold path.
25
+ * AVIF stays available on an explicit `fmt=avif`; flip this on to prefer it in `auto`.
26
+ */
27
+ preferAvif: boolean;
28
+ /**
29
+ * Snap requested `w`/`h` to a grid of this many px before transforming + caching, so
30
+ * the continuous dimension space collapses to a finite set of buckets. This bounds
31
+ * how many distinct variants a source can ever spawn — an anti-DoS measure (a caller
32
+ * can't force unbounded generation with `w=1,2,3,…`), mirroring Next.js 16's mandatory
33
+ * quality allowlist. Default 50 (the default srcset `pixelStep`, so well-behaved
34
+ * widths pass through unchanged). Set `<= 1` to honor exact dimensions (no snapping).
35
+ */
36
+ dimensionStep: number;
37
+ /**
38
+ * Max source pixels (w×h) Sharp will decode — a decompression-bomb guard that also
39
+ * caps per-transform memory (a 100MP image is ~400MB decoded). Default 100,000,000
40
+ * (~100MP). Raise it if you legitimately serve very-high-resolution originals
41
+ * (e.g. 108/200MP phone photos); lower it to harden a public endpoint further.
42
+ */
43
+ maxInputPixels: number;
44
+ }
45
+ /**
46
+ * Default srcset width increment, and the default grid the endpoint snaps requested
47
+ * dimensions to ({@link TransformConstraints.dimensionStep}). The frontend steps the
48
+ * srcset by this; it's configurable per `<ResponsiveImage>` / `buildSrcset` (raise it
49
+ * to emit fewer widths and so generate fewer variants). Because the endpoint snaps to
50
+ * the same grid by default, well-behaved srcset widths pass through unchanged.
51
+ */
52
+ export declare const DEFAULT_PIXEL_STEP = 50;
53
+ export declare const DEFAULT_CONSTRAINTS: TransformConstraints;
54
+ export interface ParsedParams {
55
+ w?: number;
56
+ h?: number;
57
+ fit: Fit;
58
+ q: number;
59
+ fmt: Format;
60
+ }
61
+ export declare const clampInt: (n: number, lo: number, hi: number) => number;
62
+ /**
63
+ * Snap a requested quality to the nearest multiple of 5, then clamp to `[lo, hi]`.
64
+ * Bucketing collapses the otherwise-continuous `q` space to ~a dozen values, bounding
65
+ * how many distinct variants a single (source, size, format) can spawn.
66
+ */
67
+ export declare const bucketQuality: (q: number, [lo, hi]: [number, number]) => number;
68
+ /** Parse "16:9" | "16/9" | "1.78" → a number, or undefined when unparseable / non-positive. */
69
+ export declare const parseAspectRatio: (ar: number | string | null | undefined) => number | undefined;
70
+ export declare const extForFormat: (fmt: OutputFormat) => string;
71
+ export declare const mimeForFormat: (fmt: OutputFormat) => string;
72
+ /** The concrete (non-`auto`) formats the plugin can encode/accept, in preference order. */
73
+ export declare const ENCODABLE_FORMATS: OutputFormat[];
74
+ /** Upload mime types accepted by the image collections — derived from {@link ENCODABLE_FORMATS} so they can't drift. */
75
+ export declare const IMAGE_MIME_TYPES: string[];
76
+ /**
77
+ * Negotiate a concrete output format from the `Accept` header when `fmt=auto`,
78
+ * constrained to the configured `allowed` formats (so a consumer who omits avif/webp
79
+ * from `formats` never gets them served). AVIF is only auto-selected when `preferAvif`
80
+ * is set — by default `auto` serves WebP for a fast on-demand path (AVIF stays
81
+ * available on an explicit `fmt=avif`). Falls back jpeg → png → whatever's allowed.
82
+ */
83
+ export declare const negotiateFormat: (accept: string | null | undefined, allowed?: Format[], preferAvif?: boolean) => OutputFormat;
84
+ export type ParseResult = {
85
+ ok: true;
86
+ params: ParsedParams;
87
+ } | {
88
+ ok: false;
89
+ error: string;
90
+ };
91
+ type QuerySource = URLSearchParams | Record<string, string | null | undefined>;
92
+ /**
93
+ * Parse + validate the transform query params. Width snaps to the allowlist; a
94
+ * missing dimension is derived from `ar`; quality clamps to the configured range;
95
+ * unknown `fit`/`fmt` fall back to `cover` / the default format. Requires at least
96
+ * one of width/height.
97
+ */
98
+ export declare const parseTransformParams: (q: QuerySource, c: TransformConstraints) => ParseResult;
99
+ export {};
100
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/transform/params.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;AACrE,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AAC9D,+CAA+C;AAC/C,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAElD,eAAO,MAAM,IAAI,EAAE,GAAG,EAAsD,CAAA;AAE5E,MAAM,WAAW,oBAAoB;IACnC,+CAA+C;IAC/C,YAAY,EAAE,MAAM,CAAA;IACpB,oCAAoC;IACpC,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAA;IACtB,qCAAqC;IACrC,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,gDAAgD;IAChD,aAAa,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC,eAAO,MAAM,mBAAmB,EAAE,oBASjC,CAAA;AAED,MAAM,WAAW,YAAY;IAC3B,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,GAAG,CAAA;IACR,CAAC,EAAE,MAAM,CAAA;IACT,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,KAAG,MAAmD,CAAA;AAEhH;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,GAAG,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAAiD,CAAA;AAEvH,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,GAAI,IAAI,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,KAAG,MAAM,GAAG,SAalF,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,KAAK,YAAY,KAAG,MAAwC,CAAA;AACzF,eAAO,MAAM,aAAa,GAAI,KAAK,YAAY,KAAG,MAAwB,CAAA;AAE1E,2FAA2F;AAC3F,eAAO,MAAM,iBAAiB,EAAE,YAAY,EAAoC,CAAA;AAEhF,wHAAwH;AACxH,eAAO,MAAM,gBAAgB,EAAE,MAAM,EAAyC,CAAA;AAE9E;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,UAAU,MAAM,EAAE,EAAE,oBAAkB,KAAG,YAS3G,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3F,KAAK,WAAW,GAAG,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;AAY9E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,GAAG,WAAW,EAAE,GAAG,oBAAoB,KAAG,WAmC9E,CAAA"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Pure parsing / validation for the on-demand transform endpoint. No Sharp, no
3
+ * Payload, no Node APIs beyond standard JS — safe to unit test in isolation and
4
+ * (for the URL helpers it shares shapes with) reason about deterministically.
5
+ */ export const FITS = [
6
+ 'cover',
7
+ 'contain',
8
+ 'inside',
9
+ 'outside',
10
+ 'fill'
11
+ ];
12
+ /**
13
+ * Default srcset width increment, and the default grid the endpoint snaps requested
14
+ * dimensions to ({@link TransformConstraints.dimensionStep}). The frontend steps the
15
+ * srcset by this; it's configurable per `<ResponsiveImage>` / `buildSrcset` (raise it
16
+ * to emit fewer widths and so generate fewer variants). Because the endpoint snaps to
17
+ * the same grid by default, well-behaved srcset widths pass through unchanged.
18
+ */ export const DEFAULT_PIXEL_STEP = 50;
19
+ export const DEFAULT_CONSTRAINTS = {
20
+ maxDimension: 4096,
21
+ qualityRange: [
22
+ 40,
23
+ 95
24
+ ],
25
+ defaultQuality: 75,
26
+ formats: [
27
+ 'auto',
28
+ 'avif',
29
+ 'webp',
30
+ 'jpeg',
31
+ 'png'
32
+ ],
33
+ defaultFormat: 'auto',
34
+ preferAvif: false,
35
+ dimensionStep: DEFAULT_PIXEL_STEP,
36
+ maxInputPixels: 100_000_000
37
+ };
38
+ export const clampInt = (n, lo, hi)=>Math.max(lo, Math.min(hi, Math.round(n)));
39
+ /**
40
+ * Snap a requested quality to the nearest multiple of 5, then clamp to `[lo, hi]`.
41
+ * Bucketing collapses the otherwise-continuous `q` space to ~a dozen values, bounding
42
+ * how many distinct variants a single (source, size, format) can spawn.
43
+ */ export const bucketQuality = (q, [lo, hi])=>clampInt(Math.round(q / 5) * 5, lo, hi);
44
+ /** Parse "16:9" | "16/9" | "1.78" → a number, or undefined when unparseable / non-positive. */ export const parseAspectRatio = (ar)=>{
45
+ if (ar == null) return undefined;
46
+ if (typeof ar === 'number') return Number.isFinite(ar) && ar > 0 ? ar : undefined;
47
+ const s = ar.trim();
48
+ const m = s.match(/^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/);
49
+ if (m) {
50
+ const a = Number(m[1]);
51
+ const b = Number(m[2]);
52
+ const r = b > 0 ? a / b : 0;
53
+ return r > 0 ? r : undefined;
54
+ }
55
+ const n = Number(s);
56
+ return Number.isFinite(n) && n > 0 ? n : undefined;
57
+ };
58
+ export const extForFormat = (fmt)=>fmt === 'jpeg' ? 'jpg' : fmt;
59
+ export const mimeForFormat = (fmt)=>`image/${fmt}`;
60
+ /** The concrete (non-`auto`) formats the plugin can encode/accept, in preference order. */ export const ENCODABLE_FORMATS = [
61
+ 'avif',
62
+ 'webp',
63
+ 'jpeg',
64
+ 'png'
65
+ ];
66
+ /** Upload mime types accepted by the image collections — derived from {@link ENCODABLE_FORMATS} so they can't drift. */ export const IMAGE_MIME_TYPES = ENCODABLE_FORMATS.map(mimeForFormat);
67
+ /**
68
+ * Negotiate a concrete output format from the `Accept` header when `fmt=auto`,
69
+ * constrained to the configured `allowed` formats (so a consumer who omits avif/webp
70
+ * from `formats` never gets them served). AVIF is only auto-selected when `preferAvif`
71
+ * is set — by default `auto` serves WebP for a fast on-demand path (AVIF stays
72
+ * available on an explicit `fmt=avif`). Falls back jpeg → png → whatever's allowed.
73
+ */ export const negotiateFormat = (accept, allowed, preferAvif = false)=>{
74
+ const a = accept ?? '';
75
+ const ok = (f)=>!allowed || allowed.includes(f);
76
+ if (preferAvif && a.includes('image/avif') && ok('avif')) return 'avif';
77
+ if (a.includes('image/webp') && ok('webp')) return 'webp';
78
+ if (ok('jpeg')) return 'jpeg';
79
+ if (ok('png')) return 'png';
80
+ if (ok('webp')) return 'webp';
81
+ return ok('avif') ? 'avif' : 'jpeg';
82
+ };
83
+ const read = (q, key)=>{
84
+ const v = q instanceof URLSearchParams ? q.get(key) : q[key];
85
+ return v == null ? undefined : String(v);
86
+ };
87
+ const numeric = (s)=>{
88
+ if (s == null || s === '') return undefined;
89
+ const n = Number(s);
90
+ return Number.isFinite(n) ? n : undefined;
91
+ };
92
+ /**
93
+ * Parse + validate the transform query params. Width snaps to the allowlist; a
94
+ * missing dimension is derived from `ar`; quality clamps to the configured range;
95
+ * unknown `fit`/`fmt` fall back to `cover` / the default format. Requires at least
96
+ * one of width/height.
97
+ */ export const parseTransformParams = (q, c)=>{
98
+ const ar = parseAspectRatio(read(q, 'ar'));
99
+ const wRaw = numeric(read(q, 'w'));
100
+ const hRaw = numeric(read(q, 'h'));
101
+ if (wRaw != null && wRaw <= 0) return {
102
+ ok: false,
103
+ error: 'invalid w'
104
+ };
105
+ if (hRaw != null && hRaw <= 0) return {
106
+ ok: false,
107
+ error: 'invalid h'
108
+ };
109
+ const cap = (n)=>clampInt(n, 1, c.maxDimension);
110
+ let w = wRaw != null ? cap(Math.round(wRaw)) : undefined;
111
+ let h = hRaw != null ? cap(Math.round(hRaw)) : undefined;
112
+ if (ar) {
113
+ if (w != null && h == null) h = cap(Math.round(w / ar));
114
+ else if (h != null && w == null) w = cap(Math.round(h * ar));
115
+ }
116
+ if (w == null && h == null) return {
117
+ ok: false,
118
+ error: 'width or height required'
119
+ };
120
+ // Snap to the dimension grid so the variant space stays finite (anti-DoS); `<= 1` disables.
121
+ if (c.dimensionStep > 1) {
122
+ const snap = (n)=>n == null ? n : clampInt(Math.round(n / c.dimensionStep) * c.dimensionStep, Math.min(c.dimensionStep, c.maxDimension), c.maxDimension);
123
+ w = snap(w);
124
+ h = snap(h);
125
+ }
126
+ const fitRaw = read(q, 'fit');
127
+ const fmtRaw = read(q, 'fmt');
128
+ const qRaw = numeric(read(q, 'q'));
129
+ const quality = qRaw == null ? c.defaultQuality : bucketQuality(qRaw, c.qualityRange);
130
+ const fit = FITS.includes(fitRaw) ? fitRaw : 'cover';
131
+ const fmt = c.formats.includes(fmtRaw) ? fmtRaw : c.defaultFormat;
132
+ return {
133
+ ok: true,
134
+ params: {
135
+ w,
136
+ h,
137
+ fit,
138
+ q: quality,
139
+ fmt
140
+ }
141
+ };
142
+ };
143
+
144
+ //# sourceMappingURL=params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transform/params.ts"],"sourcesContent":["/**\n * Pure parsing / validation for the on-demand transform endpoint. No Sharp, no\n * Payload, no Node APIs beyond standard JS — safe to unit test in isolation and\n * (for the URL helpers it shares shapes with) reason about deterministically.\n */\n\nexport type Fit = 'cover' | 'contain' | 'inside' | 'outside' | 'fill'\nexport type Format = 'auto' | 'avif' | 'webp' | 'jpeg' | 'png'\n/** A concrete output format (never `auto`). */\nexport type OutputFormat = Exclude<Format, 'auto'>\n\nexport const FITS: Fit[] = ['cover', 'contain', 'inside', 'outside', 'fill']\n\nexport interface TransformConstraints {\n /** Hard ceiling on either output dimension. */\n maxDimension: number\n /** [min, max] clamp for quality. */\n qualityRange: [number, number]\n /** Quality used when the request omits `q`. */\n defaultQuality: number\n /** Formats the endpoint may emit. */\n formats: Format[]\n /** Format used when the request omits `fmt`. */\n defaultFormat: Format\n /**\n * Auto-negotiate AVIF when the browser accepts it. Off by default: AVIF encoding is\n * far slower than WebP, so on-demand `fmt=auto` serves WebP for a fast cold path.\n * AVIF stays available on an explicit `fmt=avif`; flip this on to prefer it in `auto`.\n */\n preferAvif: boolean\n /**\n * Snap requested `w`/`h` to a grid of this many px before transforming + caching, so\n * the continuous dimension space collapses to a finite set of buckets. This bounds\n * how many distinct variants a source can ever spawn — an anti-DoS measure (a caller\n * can't force unbounded generation with `w=1,2,3,…`), mirroring Next.js 16's mandatory\n * quality allowlist. Default 50 (the default srcset `pixelStep`, so well-behaved\n * widths pass through unchanged). Set `<= 1` to honor exact dimensions (no snapping).\n */\n dimensionStep: number\n /**\n * Max source pixels (w×h) Sharp will decode — a decompression-bomb guard that also\n * caps per-transform memory (a 100MP image is ~400MB decoded). Default 100,000,000\n * (~100MP). Raise it if you legitimately serve very-high-resolution originals\n * (e.g. 108/200MP phone photos); lower it to harden a public endpoint further.\n */\n maxInputPixels: number\n}\n\n/**\n * Default srcset width increment, and the default grid the endpoint snaps requested\n * dimensions to ({@link TransformConstraints.dimensionStep}). The frontend steps the\n * srcset by this; it's configurable per `<ResponsiveImage>` / `buildSrcset` (raise it\n * to emit fewer widths and so generate fewer variants). Because the endpoint snaps to\n * the same grid by default, well-behaved srcset widths pass through unchanged.\n */\nexport const DEFAULT_PIXEL_STEP = 50\n\nexport const DEFAULT_CONSTRAINTS: TransformConstraints = {\n maxDimension: 4096,\n qualityRange: [40, 95],\n defaultQuality: 75,\n formats: ['auto', 'avif', 'webp', 'jpeg', 'png'],\n defaultFormat: 'auto',\n preferAvif: false,\n dimensionStep: DEFAULT_PIXEL_STEP,\n maxInputPixels: 100_000_000,\n}\n\nexport interface ParsedParams {\n w?: number\n h?: number\n fit: Fit\n q: number\n fmt: Format\n}\n\nexport const clampInt = (n: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, Math.round(n)))\n\n/**\n * Snap a requested quality to the nearest multiple of 5, then clamp to `[lo, hi]`.\n * Bucketing collapses the otherwise-continuous `q` space to ~a dozen values, bounding\n * how many distinct variants a single (source, size, format) can spawn.\n */\nexport const bucketQuality = (q: number, [lo, hi]: [number, number]): number => clampInt(Math.round(q / 5) * 5, lo, hi)\n\n/** Parse \"16:9\" | \"16/9\" | \"1.78\" → a number, or undefined when unparseable / non-positive. */\nexport const parseAspectRatio = (ar: number | string | null | undefined): number | undefined => {\n if (ar == null) return undefined\n if (typeof ar === 'number') return Number.isFinite(ar) && ar > 0 ? ar : undefined\n const s = ar.trim()\n const m = s.match(/^(\\d+(?:\\.\\d+)?)\\s*[:/]\\s*(\\d+(?:\\.\\d+)?)$/)\n if (m) {\n const a = Number(m[1])\n const b = Number(m[2])\n const r = b > 0 ? a / b : 0\n return r > 0 ? r : undefined\n }\n const n = Number(s)\n return Number.isFinite(n) && n > 0 ? n : undefined\n}\n\nexport const extForFormat = (fmt: OutputFormat): string => (fmt === 'jpeg' ? 'jpg' : fmt)\nexport const mimeForFormat = (fmt: OutputFormat): string => `image/${fmt}`\n\n/** The concrete (non-`auto`) formats the plugin can encode/accept, in preference order. */\nexport const ENCODABLE_FORMATS: OutputFormat[] = ['avif', 'webp', 'jpeg', 'png']\n\n/** Upload mime types accepted by the image collections — derived from {@link ENCODABLE_FORMATS} so they can't drift. */\nexport const IMAGE_MIME_TYPES: string[] = ENCODABLE_FORMATS.map(mimeForFormat)\n\n/**\n * Negotiate a concrete output format from the `Accept` header when `fmt=auto`,\n * constrained to the configured `allowed` formats (so a consumer who omits avif/webp\n * from `formats` never gets them served). AVIF is only auto-selected when `preferAvif`\n * is set — by default `auto` serves WebP for a fast on-demand path (AVIF stays\n * available on an explicit `fmt=avif`). Falls back jpeg → png → whatever's allowed.\n */\nexport const negotiateFormat = (accept: string | null | undefined, allowed?: Format[], preferAvif = false): OutputFormat => {\n const a = accept ?? ''\n const ok = (f: OutputFormat): boolean => !allowed || allowed.includes(f)\n if (preferAvif && a.includes('image/avif') && ok('avif')) return 'avif'\n if (a.includes('image/webp') && ok('webp')) return 'webp'\n if (ok('jpeg')) return 'jpeg'\n if (ok('png')) return 'png'\n if (ok('webp')) return 'webp'\n return ok('avif') ? 'avif' : 'jpeg'\n}\n\nexport type ParseResult = { ok: true; params: ParsedParams } | { ok: false; error: string }\n\ntype QuerySource = URLSearchParams | Record<string, string | null | undefined>\n\nconst read = (q: QuerySource, key: string): string | undefined => {\n const v = q instanceof URLSearchParams ? q.get(key) : q[key]\n return v == null ? undefined : String(v)\n}\nconst numeric = (s: string | undefined): number | undefined => {\n if (s == null || s === '') return undefined\n const n = Number(s)\n return Number.isFinite(n) ? n : undefined\n}\n\n/**\n * Parse + validate the transform query params. Width snaps to the allowlist; a\n * missing dimension is derived from `ar`; quality clamps to the configured range;\n * unknown `fit`/`fmt` fall back to `cover` / the default format. Requires at least\n * one of width/height.\n */\nexport const parseTransformParams = (q: QuerySource, c: TransformConstraints): ParseResult => {\n const ar = parseAspectRatio(read(q, 'ar'))\n\n const wRaw = numeric(read(q, 'w'))\n const hRaw = numeric(read(q, 'h'))\n if (wRaw != null && wRaw <= 0) return { ok: false, error: 'invalid w' }\n if (hRaw != null && hRaw <= 0) return { ok: false, error: 'invalid h' }\n\n const cap = (n: number): number => clampInt(n, 1, c.maxDimension)\n let w = wRaw != null ? cap(Math.round(wRaw)) : undefined\n let h = hRaw != null ? cap(Math.round(hRaw)) : undefined\n\n if (ar) {\n if (w != null && h == null) h = cap(Math.round(w / ar))\n else if (h != null && w == null) w = cap(Math.round(h * ar))\n }\n\n if (w == null && h == null) return { ok: false, error: 'width or height required' }\n\n // Snap to the dimension grid so the variant space stays finite (anti-DoS); `<= 1` disables.\n if (c.dimensionStep > 1) {\n const snap = (n: number | undefined): number | undefined =>\n n == null ? n : clampInt(Math.round(n / c.dimensionStep) * c.dimensionStep, Math.min(c.dimensionStep, c.maxDimension), c.maxDimension)\n w = snap(w)\n h = snap(h)\n }\n\n const fitRaw = read(q, 'fit')\n const fmtRaw = read(q, 'fmt')\n const qRaw = numeric(read(q, 'q'))\n const quality = qRaw == null ? c.defaultQuality : bucketQuality(qRaw, c.qualityRange)\n const fit: Fit = FITS.includes(fitRaw as Fit) ? (fitRaw as Fit) : 'cover'\n const fmt: Format = c.formats.includes(fmtRaw as Format) ? (fmtRaw as Format) : c.defaultFormat\n\n return { ok: true, params: { w, h, fit, q: quality, fmt } }\n}\n"],"names":["FITS","DEFAULT_PIXEL_STEP","DEFAULT_CONSTRAINTS","maxDimension","qualityRange","defaultQuality","formats","defaultFormat","preferAvif","dimensionStep","maxInputPixels","clampInt","n","lo","hi","Math","max","min","round","bucketQuality","q","parseAspectRatio","ar","undefined","Number","isFinite","s","trim","m","match","a","b","r","extForFormat","fmt","mimeForFormat","ENCODABLE_FORMATS","IMAGE_MIME_TYPES","map","negotiateFormat","accept","allowed","ok","f","includes","read","key","v","URLSearchParams","get","String","numeric","parseTransformParams","c","wRaw","hRaw","error","cap","w","h","snap","fitRaw","fmtRaw","qRaw","quality","fit","params"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { type Fit, type OutputFormat } from './params';
2
+ export { coverCropGeometry, coverObjectPosition, fitWithinSource } from './geometry';
3
+ export type { CropGeometry } from './geometry';
4
+ export interface TransformInput {
5
+ w?: number;
6
+ h?: number;
7
+ fit: Fit;
8
+ quality: number;
9
+ format: OutputFormat;
10
+ focalX?: number | null;
11
+ focalY?: number | null;
12
+ /** Max source pixels Sharp will decode (decompression-bomb + memory guard). Defaults to {@link MAX_INPUT_PIXELS}. */
13
+ maxInputPixels?: number;
14
+ }
15
+ export interface TransformOutput {
16
+ data: Buffer;
17
+ format: OutputFormat;
18
+ width: number;
19
+ height: number;
20
+ mimeType: string;
21
+ }
22
+ /**
23
+ * Transform a source image buffer: focal cover-crop (or plain resize for other fits) +
24
+ * encode. The CPU-bound work runs behind {@link withTransformLimit} so a burst of
25
+ * concurrent srcset requests can't saturate the host.
26
+ */
27
+ export declare const transformImage: (src: Buffer, input: TransformInput) => Promise<TransformOutput>;
28
+ //# sourceMappingURL=sharp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharp.d.ts","sourceRoot":"","sources":["../../src/transform/sharp.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,GAAG,EAAiB,KAAK,YAAY,EAAE,MAAM,UAAU,CAAA;AAGrE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACpF,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,MAAM,WAAW,cAAc;IAC7B,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,GAAG,CAAA;IACR,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,YAAY,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,qHAAqH;IACrH,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,YAAY,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;CACjB;AAoBD;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,EAAE,OAAO,cAAc,KAAG,OAAO,CAAC,eAAe,CA2BvF,CAAA"}