ipx 3.1.0 → 4.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -42
- package/dist/_chunks/libs/@fastify/accept-negotiator.mjs +1 -0
- package/dist/_chunks/libs/boolbase.mjs +1 -0
- package/dist/_chunks/libs/css-select.mjs +6 -0
- package/dist/_chunks/libs/css-tree.mjs +126 -0
- package/dist/_chunks/libs/csso.mjs +3 -0
- package/dist/_chunks/libs/etag.mjs +1 -0
- package/dist/_chunks/libs/h3.mjs +2 -0
- package/dist/_chunks/libs/image-meta.mjs +3 -0
- package/dist/_chunks/libs/sax.mjs +10 -0
- package/dist/_chunks/libs/svgo.mjs +78 -0
- package/dist/_chunks/libs/ufo.mjs +1 -0
- package/dist/_chunks/node-fs.mjs +728 -0
- package/dist/_chunks/rolldown-runtime.mjs +46 -0
- package/dist/_chunks/svgo-node.mjs +11 -0
- package/dist/cli.d.mts +1 -2
- package/dist/cli.mjs +76 -58
- package/dist/index.d.mts +226 -320
- package/dist/index.mjs +44 -48
- package/package.json +43 -48
- package/bin/ipx.mjs +0 -2
- package/dist/cli.cjs +0 -63
- package/dist/cli.d.cts +0 -2
- package/dist/cli.d.ts +0 -2
- package/dist/index.cjs +0 -62
- package/dist/index.d.cts +0 -397
- package/dist/index.d.ts +0 -397
- package/dist/shared/ipx.C8X6338M.mjs +0 -764
- package/dist/shared/ipx.CHfn1B1U.cjs +0 -778
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { a as __toESM, i as __toDynamicImportESM, n as __export } from "./rolldown-runtime.mjs";
|
|
2
|
+
import { i as withLeadingSlash, n as hasProtocol, r as joinURL, t as decode } from "./libs/ufo.mjs";
|
|
3
|
+
import { n as defineEventHandler, t as HTTPError } from "./libs/h3.mjs";
|
|
4
|
+
import { t as imageMeta } from "./libs/image-meta.mjs";
|
|
5
|
+
import { t as require_etag } from "./libs/etag.mjs";
|
|
6
|
+
import { t as require_accept_negotiator } from "./libs/@fastify/accept-negotiator.mjs";
|
|
7
|
+
import { join, parse, resolve } from "pathe";
|
|
8
|
+
|
|
9
|
+
//#region src/handlers/utils.ts
|
|
10
|
+
function VArg(argument) {
|
|
11
|
+
if (argument === "Infinity") return Infinity;
|
|
12
|
+
try {
|
|
13
|
+
const val = JSON.parse(argument);
|
|
14
|
+
const t = typeof val;
|
|
15
|
+
if (t === "boolean" || t === "number" || t === "string" || val === null) return val;
|
|
16
|
+
} catch {}
|
|
17
|
+
}
|
|
18
|
+
function parseArgs(arguments_, mappers) {
|
|
19
|
+
const vargs = arguments_.split("_");
|
|
20
|
+
return mappers.map((v, index) => v(vargs[index]));
|
|
21
|
+
}
|
|
22
|
+
function getHandler(key) {
|
|
23
|
+
return handlers_exports[key];
|
|
24
|
+
}
|
|
25
|
+
function applyHandler(context, pipe, handler, argumentsString) {
|
|
26
|
+
const arguments_ = handler.args ? parseArgs(argumentsString, handler.args) : [];
|
|
27
|
+
return handler.apply(context, pipe, ...arguments_);
|
|
28
|
+
}
|
|
29
|
+
function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
|
|
30
|
+
const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
|
|
31
|
+
let { width: width$1, height: height$1 } = desiredDimensions;
|
|
32
|
+
if (sourceDimensions.width && width$1 > sourceDimensions.width) {
|
|
33
|
+
width$1 = sourceDimensions.width;
|
|
34
|
+
height$1 = Math.round(sourceDimensions.width / desiredAspectRatio);
|
|
35
|
+
}
|
|
36
|
+
if (sourceDimensions.height && height$1 > sourceDimensions.height) {
|
|
37
|
+
height$1 = sourceDimensions.height;
|
|
38
|
+
width$1 = Math.round(sourceDimensions.height * desiredAspectRatio);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
width: width$1,
|
|
42
|
+
height: height$1
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/handlers/handlers.ts
|
|
48
|
+
var handlers_exports = /* @__PURE__ */ __export({
|
|
49
|
+
b: () => b,
|
|
50
|
+
background: () => background,
|
|
51
|
+
blur: () => blur,
|
|
52
|
+
crop: () => crop,
|
|
53
|
+
enlarge: () => enlarge,
|
|
54
|
+
extend: () => extend,
|
|
55
|
+
extract: () => extract,
|
|
56
|
+
fit: () => fit,
|
|
57
|
+
flatten: () => flatten,
|
|
58
|
+
flip: () => flip,
|
|
59
|
+
flop: () => flop,
|
|
60
|
+
gamma: () => gamma,
|
|
61
|
+
grayscale: () => grayscale,
|
|
62
|
+
h: () => h,
|
|
63
|
+
height: () => height,
|
|
64
|
+
kernel: () => kernel,
|
|
65
|
+
median: () => median,
|
|
66
|
+
modulate: () => modulate,
|
|
67
|
+
negate: () => negate,
|
|
68
|
+
normalize: () => normalize,
|
|
69
|
+
pos: () => pos,
|
|
70
|
+
position: () => position,
|
|
71
|
+
q: () => q,
|
|
72
|
+
quality: () => quality,
|
|
73
|
+
resize: () => resize,
|
|
74
|
+
rotate: () => rotate,
|
|
75
|
+
s: () => s,
|
|
76
|
+
sharpen: () => sharpen,
|
|
77
|
+
threshold: () => threshold,
|
|
78
|
+
tint: () => tint,
|
|
79
|
+
trim: () => trim,
|
|
80
|
+
w: () => w,
|
|
81
|
+
width: () => width
|
|
82
|
+
});
|
|
83
|
+
const quality = {
|
|
84
|
+
args: [VArg],
|
|
85
|
+
order: -1,
|
|
86
|
+
apply: (context, _pipe, quality$1) => {
|
|
87
|
+
context.quality = quality$1;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const fit = {
|
|
91
|
+
args: [VArg],
|
|
92
|
+
order: -1,
|
|
93
|
+
apply: (context, _pipe, fit$1) => {
|
|
94
|
+
context.fit = fit$1;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const position = {
|
|
98
|
+
args: [VArg],
|
|
99
|
+
order: -1,
|
|
100
|
+
apply: (context, _pipe, position$1) => {
|
|
101
|
+
context.position = position$1;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const HEX_RE = /^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i;
|
|
105
|
+
const SHORTHEX_RE = /^([\da-f])([\da-f])([\da-f])$/i;
|
|
106
|
+
const background = {
|
|
107
|
+
args: [VArg],
|
|
108
|
+
order: -1,
|
|
109
|
+
apply: (context, _pipe, background$1) => {
|
|
110
|
+
background$1 = String(background$1);
|
|
111
|
+
if (!background$1.startsWith("#") && (HEX_RE.test(background$1) || SHORTHEX_RE.test(background$1))) background$1 = "#" + background$1;
|
|
112
|
+
context.background = background$1;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const enlarge = {
|
|
116
|
+
args: [],
|
|
117
|
+
apply: (context) => {
|
|
118
|
+
context.enlarge = true;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const kernel = {
|
|
122
|
+
args: [VArg],
|
|
123
|
+
apply: (context, _pipe, kernel$1) => {
|
|
124
|
+
context.kernel = kernel$1;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const width = {
|
|
128
|
+
args: [VArg],
|
|
129
|
+
apply: (context, pipe, width$1) => {
|
|
130
|
+
return pipe.resize(width$1, void 0, { withoutEnlargement: !context.enlarge });
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const height = {
|
|
134
|
+
args: [VArg],
|
|
135
|
+
apply: (context, pipe, height$1) => {
|
|
136
|
+
return pipe.resize(void 0, height$1, { withoutEnlargement: !context.enlarge });
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const resize = {
|
|
140
|
+
args: [
|
|
141
|
+
VArg,
|
|
142
|
+
VArg,
|
|
143
|
+
VArg
|
|
144
|
+
],
|
|
145
|
+
apply: (context, pipe, size) => {
|
|
146
|
+
let [width$1, height$1] = String(size).split("x").map(Number);
|
|
147
|
+
if (!width$1) return;
|
|
148
|
+
if (!height$1) height$1 = width$1;
|
|
149
|
+
if (!context.enlarge) {
|
|
150
|
+
const clamped = clampDimensionsPreservingAspectRatio(context.meta, {
|
|
151
|
+
width: width$1,
|
|
152
|
+
height: height$1
|
|
153
|
+
});
|
|
154
|
+
width$1 = clamped.width;
|
|
155
|
+
height$1 = clamped.height;
|
|
156
|
+
}
|
|
157
|
+
return pipe.resize(width$1, height$1, {
|
|
158
|
+
fit: context.fit,
|
|
159
|
+
position: context.position,
|
|
160
|
+
background: context.background,
|
|
161
|
+
kernel: context.kernel
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const trim = {
|
|
166
|
+
args: [VArg],
|
|
167
|
+
apply: (_context, pipe, threshold$1) => {
|
|
168
|
+
return pipe.trim(threshold$1);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const extend = {
|
|
172
|
+
args: [
|
|
173
|
+
VArg,
|
|
174
|
+
VArg,
|
|
175
|
+
VArg,
|
|
176
|
+
VArg
|
|
177
|
+
],
|
|
178
|
+
apply: (context, pipe, top, right, bottom, left) => {
|
|
179
|
+
return pipe.extend({
|
|
180
|
+
top,
|
|
181
|
+
left,
|
|
182
|
+
bottom,
|
|
183
|
+
right,
|
|
184
|
+
background: context.background
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const extract = {
|
|
189
|
+
args: [
|
|
190
|
+
VArg,
|
|
191
|
+
VArg,
|
|
192
|
+
VArg,
|
|
193
|
+
VArg
|
|
194
|
+
],
|
|
195
|
+
apply: (_context, pipe, left, top, width$1, height$1) => {
|
|
196
|
+
return pipe.extract({
|
|
197
|
+
left,
|
|
198
|
+
top,
|
|
199
|
+
width: width$1,
|
|
200
|
+
height: height$1
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const rotate = {
|
|
205
|
+
args: [VArg],
|
|
206
|
+
apply: (context, pipe, angel) => {
|
|
207
|
+
return pipe.rotate(angel, { background: context.background });
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const flip = {
|
|
211
|
+
args: [],
|
|
212
|
+
apply: (_context, pipe) => {
|
|
213
|
+
return pipe.flip();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const flop = {
|
|
217
|
+
args: [],
|
|
218
|
+
apply: (_context, pipe) => {
|
|
219
|
+
return pipe.flop();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const sharpen = {
|
|
223
|
+
args: [
|
|
224
|
+
VArg,
|
|
225
|
+
VArg,
|
|
226
|
+
VArg
|
|
227
|
+
],
|
|
228
|
+
apply: (_context, pipe, sigma, flat, jagged) => {
|
|
229
|
+
return pipe.sharpen(sigma, flat, jagged);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const median = {
|
|
233
|
+
args: [
|
|
234
|
+
VArg,
|
|
235
|
+
VArg,
|
|
236
|
+
VArg
|
|
237
|
+
],
|
|
238
|
+
apply: (_context, pipe, size) => {
|
|
239
|
+
return pipe.median(size);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const blur = {
|
|
243
|
+
args: [
|
|
244
|
+
VArg,
|
|
245
|
+
VArg,
|
|
246
|
+
VArg
|
|
247
|
+
],
|
|
248
|
+
apply: (_context, pipe, sigma) => {
|
|
249
|
+
return pipe.blur(sigma);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const flatten = {
|
|
253
|
+
args: [
|
|
254
|
+
VArg,
|
|
255
|
+
VArg,
|
|
256
|
+
VArg
|
|
257
|
+
],
|
|
258
|
+
apply: (context, pipe) => {
|
|
259
|
+
return pipe.flatten({ background: context.background });
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const gamma = {
|
|
263
|
+
args: [
|
|
264
|
+
VArg,
|
|
265
|
+
VArg,
|
|
266
|
+
VArg
|
|
267
|
+
],
|
|
268
|
+
apply: (_context, pipe, gamma$1, gammaOut) => {
|
|
269
|
+
return pipe.gamma(gamma$1, gammaOut);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const negate = {
|
|
273
|
+
args: [
|
|
274
|
+
VArg,
|
|
275
|
+
VArg,
|
|
276
|
+
VArg
|
|
277
|
+
],
|
|
278
|
+
apply: (_context, pipe) => {
|
|
279
|
+
return pipe.negate();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const normalize = {
|
|
283
|
+
args: [
|
|
284
|
+
VArg,
|
|
285
|
+
VArg,
|
|
286
|
+
VArg
|
|
287
|
+
],
|
|
288
|
+
apply: (_context, pipe) => {
|
|
289
|
+
return pipe.normalize();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const threshold = {
|
|
293
|
+
args: [VArg],
|
|
294
|
+
apply: (_context, pipe, threshold$1) => {
|
|
295
|
+
return pipe.threshold(threshold$1);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const modulate = {
|
|
299
|
+
args: [VArg],
|
|
300
|
+
apply: (_context, pipe, brightness, saturation, hue) => {
|
|
301
|
+
return pipe.modulate({
|
|
302
|
+
brightness,
|
|
303
|
+
saturation,
|
|
304
|
+
hue
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const tint = {
|
|
309
|
+
args: [VArg],
|
|
310
|
+
apply: (_context, pipe, rgb) => {
|
|
311
|
+
return pipe.tint(rgb);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const grayscale = {
|
|
315
|
+
args: [VArg],
|
|
316
|
+
apply: (_context, pipe) => {
|
|
317
|
+
return pipe.grayscale();
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const crop = extract;
|
|
321
|
+
const q = quality;
|
|
322
|
+
const b = background;
|
|
323
|
+
const w = width;
|
|
324
|
+
const h = height;
|
|
325
|
+
const s = resize;
|
|
326
|
+
const pos = position;
|
|
327
|
+
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/utils.ts
|
|
330
|
+
function getEnv(name) {
|
|
331
|
+
const value = globalThis.process?.env?.[name];
|
|
332
|
+
if (value !== void 0) return JSON.parse(value);
|
|
333
|
+
}
|
|
334
|
+
function requireModule(id) {
|
|
335
|
+
const { createRequire } = globalThis.process.getBuiltinModule("node:module");
|
|
336
|
+
return createRequire(import.meta.url)(id);
|
|
337
|
+
}
|
|
338
|
+
function cachedPromise(function_) {
|
|
339
|
+
let p;
|
|
340
|
+
return (...arguments_) => {
|
|
341
|
+
if (p) return p;
|
|
342
|
+
p = Promise.resolve(function_(...arguments_));
|
|
343
|
+
return p;
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/ipx.ts
|
|
349
|
+
const SUPPORTED_FORMATS = new Set([
|
|
350
|
+
"jpeg",
|
|
351
|
+
"png",
|
|
352
|
+
"webp",
|
|
353
|
+
"avif",
|
|
354
|
+
"tiff",
|
|
355
|
+
"heif",
|
|
356
|
+
"gif",
|
|
357
|
+
"heic"
|
|
358
|
+
]);
|
|
359
|
+
/**
|
|
360
|
+
* Creates an IPX image processing instance with the specified options.
|
|
361
|
+
* @param {IPXOptions} userOptions - Configuration options for the IPX instance. See {@link IPXOptions}.
|
|
362
|
+
* @returns {IPX} An IPX processing function configured with the given options. See {@link IPX}.
|
|
363
|
+
* @throws {Error} If critical options such as storage are missing or incorrectly configured.
|
|
364
|
+
*/
|
|
365
|
+
function createIPX(userOptions) {
|
|
366
|
+
const options = {
|
|
367
|
+
...userOptions,
|
|
368
|
+
alias: userOptions.alias || getEnv("IPX_ALIAS") || {},
|
|
369
|
+
maxAge: userOptions.maxAge ?? getEnv("IPX_MAX_AGE") ?? 60,
|
|
370
|
+
sharpOptions: {
|
|
371
|
+
jpegProgressive: true,
|
|
372
|
+
...userOptions.sharpOptions
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
options.alias = Object.fromEntries(Object.entries(options.alias || {}).map((e) => [withLeadingSlash(e[0]), e[1]]));
|
|
376
|
+
const getSharp = cachedPromise(async () => {
|
|
377
|
+
return await import("sharp").then((r) => r.default || r);
|
|
378
|
+
});
|
|
379
|
+
const getSVGO = cachedPromise(async () => {
|
|
380
|
+
const { optimize } = await import("./svgo-node.mjs").then(__toDynamicImportESM(1));
|
|
381
|
+
return { optimize };
|
|
382
|
+
});
|
|
383
|
+
return function ipx(id, modifiers = {}, opts = {}) {
|
|
384
|
+
if (!id) throw new HTTPError({
|
|
385
|
+
statusCode: 400,
|
|
386
|
+
statusText: `IPX_MISSING_ID`,
|
|
387
|
+
message: `Resource id is missing`
|
|
388
|
+
});
|
|
389
|
+
id = hasProtocol(id) ? id : withLeadingSlash(id);
|
|
390
|
+
for (const base in options.alias) if (id.startsWith(base)) id = joinURL(options.alias[base], id.slice(base.length));
|
|
391
|
+
const storage = hasProtocol(id) ? options.httpStorage || options.storage : options.storage || options.httpStorage;
|
|
392
|
+
if (!storage) throw new HTTPError({
|
|
393
|
+
statusCode: 500,
|
|
394
|
+
statusText: `IPX_NO_STORAGE`,
|
|
395
|
+
message: "No storage configured!"
|
|
396
|
+
});
|
|
397
|
+
const getSourceMeta = cachedPromise(async () => {
|
|
398
|
+
const sourceMeta = await storage.getMeta(id, opts);
|
|
399
|
+
if (!sourceMeta) throw new HTTPError({
|
|
400
|
+
statusCode: 404,
|
|
401
|
+
statusText: `IPX_RESOURCE_NOT_FOUND`,
|
|
402
|
+
message: `Resource not found: ${id}`
|
|
403
|
+
});
|
|
404
|
+
const _maxAge = sourceMeta.maxAge ?? options.maxAge;
|
|
405
|
+
return {
|
|
406
|
+
maxAge: typeof _maxAge === "string" ? Number.parseInt(_maxAge) : _maxAge,
|
|
407
|
+
mtime: sourceMeta.mtime ? new Date(sourceMeta.mtime) : void 0
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
const getSourceData = cachedPromise(async () => {
|
|
411
|
+
const sourceData = await storage.getData(id, opts);
|
|
412
|
+
if (!sourceData) throw new HTTPError({
|
|
413
|
+
statusCode: 404,
|
|
414
|
+
statusText: `IPX_RESOURCE_NOT_FOUND`,
|
|
415
|
+
message: `Resource not found: ${id}`
|
|
416
|
+
});
|
|
417
|
+
return Buffer.from(sourceData);
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
getSourceMeta,
|
|
421
|
+
process: cachedPromise(async () => {
|
|
422
|
+
const sourceData = await getSourceData();
|
|
423
|
+
let imageMeta$1;
|
|
424
|
+
try {
|
|
425
|
+
imageMeta$1 = imageMeta(sourceData);
|
|
426
|
+
} catch {
|
|
427
|
+
throw new HTTPError({
|
|
428
|
+
statusCode: 400,
|
|
429
|
+
statusText: `IPX_INVALID_IMAGE`,
|
|
430
|
+
message: `Cannot parse image metadata: ${id}`
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
let mFormat = modifiers.f || modifiers.format;
|
|
434
|
+
if (mFormat === "jpg") mFormat = "jpeg";
|
|
435
|
+
const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type || "") ? imageMeta$1.type : "jpeg";
|
|
436
|
+
if (imageMeta$1.type === "svg" && !mFormat) if (options.svgo === false) return {
|
|
437
|
+
data: sourceData,
|
|
438
|
+
format: "svg+xml",
|
|
439
|
+
meta: imageMeta$1
|
|
440
|
+
};
|
|
441
|
+
else {
|
|
442
|
+
const { optimize } = await getSVGO();
|
|
443
|
+
return {
|
|
444
|
+
data: optimize(sourceData.toString("utf8"), {
|
|
445
|
+
...options.svgo,
|
|
446
|
+
plugins: ["removeScripts", ...options.svgo?.plugins || []]
|
|
447
|
+
}).data,
|
|
448
|
+
format: "svg+xml",
|
|
449
|
+
meta: imageMeta$1
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
|
|
453
|
+
let sharp = (await getSharp())(sourceData, {
|
|
454
|
+
animated,
|
|
455
|
+
...options.sharpOptions
|
|
456
|
+
});
|
|
457
|
+
Object.assign(sharp.options, options.sharpOptions);
|
|
458
|
+
const handlers = Object.entries(modifiers).map(([name, arguments_]) => ({
|
|
459
|
+
handler: getHandler(name),
|
|
460
|
+
name,
|
|
461
|
+
args: arguments_
|
|
462
|
+
})).filter((h$1) => h$1.handler).sort((a, b$1) => {
|
|
463
|
+
const aKey = (a.handler.order || a.name || "").toString();
|
|
464
|
+
const bKey = (b$1.handler.order || b$1.name || "").toString();
|
|
465
|
+
return aKey.localeCompare(bKey);
|
|
466
|
+
});
|
|
467
|
+
const handlerContext = { meta: imageMeta$1 };
|
|
468
|
+
for (const h$1 of handlers) sharp = applyHandler(handlerContext, sharp, h$1.handler, h$1.args.toString()) || sharp;
|
|
469
|
+
if (SUPPORTED_FORMATS.has(format || "")) sharp = sharp.toFormat(format, { quality: handlerContext.quality });
|
|
470
|
+
return {
|
|
471
|
+
data: await sharp.toBuffer(),
|
|
472
|
+
format,
|
|
473
|
+
meta: imageMeta$1
|
|
474
|
+
};
|
|
475
|
+
})
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/server.ts
|
|
482
|
+
var import_etag = /* @__PURE__ */ __toESM(require_etag(), 1);
|
|
483
|
+
var import_accept_negotiator = require_accept_negotiator();
|
|
484
|
+
function createIPXFetchHandler(ipx) {
|
|
485
|
+
return createIPXHandler(ipx).fetch;
|
|
486
|
+
}
|
|
487
|
+
function createIPXNodeHandler(ipx) {
|
|
488
|
+
const { toNodeHandler } = requireModule("srvx/node");
|
|
489
|
+
return toNodeHandler(createIPXFetchHandler(ipx));
|
|
490
|
+
}
|
|
491
|
+
function serveIPX(ipx, opts) {
|
|
492
|
+
const { serve } = requireModule("srvx");
|
|
493
|
+
const fetch$1 = createIPXFetchHandler(ipx);
|
|
494
|
+
return serve({
|
|
495
|
+
...opts,
|
|
496
|
+
fetch: fetch$1
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
const MODIFIER_SEP = /[&,]/g;
|
|
500
|
+
const MODIFIER_VAL_SEP = /[:=_]/;
|
|
501
|
+
function createIPXHandler(ipx) {
|
|
502
|
+
return defineEventHandler(async (event) => {
|
|
503
|
+
const [modifiersString = "", ...idSegments] = event.url.pathname.slice(1).split("/");
|
|
504
|
+
const id = safeString(decode(idSegments.join("/")));
|
|
505
|
+
if (!modifiersString) throw new HTTPError({
|
|
506
|
+
statusCode: 400,
|
|
507
|
+
statusText: "IPX_MISSING_MODIFIERS",
|
|
508
|
+
message: `Modifiers are missing: ${id}`
|
|
509
|
+
});
|
|
510
|
+
if (!id || id === "/") throw new HTTPError({
|
|
511
|
+
statusCode: 400,
|
|
512
|
+
statusText: "IPX_MISSING_ID",
|
|
513
|
+
message: `Resource id is missing: ${event.path}`
|
|
514
|
+
});
|
|
515
|
+
const modifiers = Object.create(null);
|
|
516
|
+
if (modifiersString !== "_") for (const p of modifiersString.split(MODIFIER_SEP)) {
|
|
517
|
+
const [key, ...values] = p.split(MODIFIER_VAL_SEP);
|
|
518
|
+
modifiers[safeString(key)] = values.map((v) => safeString(decode(v))).join("_");
|
|
519
|
+
}
|
|
520
|
+
if ((modifiers.f || modifiers.format) === "auto") {
|
|
521
|
+
const acceptHeader = event.req.headers.get("accept") || "";
|
|
522
|
+
const animated = modifiers.animated ?? modifiers.a;
|
|
523
|
+
const autoFormat = autoDetectFormat(acceptHeader, !!animated || animated === "");
|
|
524
|
+
delete modifiers.f;
|
|
525
|
+
delete modifiers.format;
|
|
526
|
+
if (autoFormat) {
|
|
527
|
+
modifiers.format = autoFormat;
|
|
528
|
+
event.res.headers.append("vary", "Accept");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const img = ipx(id, modifiers);
|
|
532
|
+
const sourceMeta = await img.getSourceMeta();
|
|
533
|
+
sendResponseHeaderIfNotSet(event, "content-security-policy", "default-src 'none'");
|
|
534
|
+
if (sourceMeta.mtime) {
|
|
535
|
+
sendResponseHeaderIfNotSet(event, "last-modified", sourceMeta.mtime.toUTCString());
|
|
536
|
+
const _ifModifiedSince = event.req.headers.get("if-modified-since");
|
|
537
|
+
if (_ifModifiedSince && new Date(_ifModifiedSince) >= sourceMeta.mtime) {
|
|
538
|
+
event.res.status = 304;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const { data, format } = await img.process();
|
|
543
|
+
if (typeof sourceMeta.maxAge === "number") sendResponseHeaderIfNotSet(event, "cache-control", `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`);
|
|
544
|
+
const etag = (0, import_etag.default)(data);
|
|
545
|
+
sendResponseHeaderIfNotSet(event, "etag", etag);
|
|
546
|
+
if (etag && event.req.headers.get("if-none-match") === etag) {
|
|
547
|
+
event.res.status = 304;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (format) sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
|
|
551
|
+
return data;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function sendResponseHeaderIfNotSet(event, name, value) {
|
|
555
|
+
if (!event.res.headers.has(name)) event.res.headers.set(name, value);
|
|
556
|
+
}
|
|
557
|
+
function autoDetectFormat(acceptHeader, animated) {
|
|
558
|
+
if (animated) return (0, import_accept_negotiator.negotiate)(acceptHeader, ["image/webp", "image/gif"])?.split("/")[1] || "gif";
|
|
559
|
+
return (0, import_accept_negotiator.negotiate)(acceptHeader, [
|
|
560
|
+
"image/avif",
|
|
561
|
+
"image/webp",
|
|
562
|
+
"image/jpeg",
|
|
563
|
+
"image/png",
|
|
564
|
+
"image/tiff",
|
|
565
|
+
"image/heif",
|
|
566
|
+
"image/gif"
|
|
567
|
+
])?.split("/")[1] || "jpeg";
|
|
568
|
+
}
|
|
569
|
+
function safeString(input) {
|
|
570
|
+
return JSON.stringify(input).replace(/^"|"$/g, "").replace(/\\+/g, "\\").replace(/\\"/g, "\"");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/storage/http.ts
|
|
575
|
+
const HTTP_RE = /^https?:\/\//;
|
|
576
|
+
/**
|
|
577
|
+
* Creates an HTTP storage handler for IPX that fetches image data from external URLs.
|
|
578
|
+
* This handler allows configuration to specify allowed domains, caching behaviour and custom fetch options.
|
|
579
|
+
*
|
|
580
|
+
* @param {HTTPStorageOptions} [_options={}] - Configuration options for HTTP storage, with defaults possibly taken from environment variables. See {@link HTTPStorageOptions}.
|
|
581
|
+
* @returns {IPXStorage} An IPXStorage interface implementation for retrieving images over HTTP. See {@link IPXStorage}.
|
|
582
|
+
* @throws {H3Error} If validation of the requested URL fails due to a missing hostname or denied host access. See {@link H3Error}.
|
|
583
|
+
*/
|
|
584
|
+
function ipxHttpStorage(_options = {}) {
|
|
585
|
+
const allowAllDomains = _options.allowAllDomains ?? getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
|
|
586
|
+
let _domains = _options.domains || getEnv("IPX_HTTP_DOMAINS") || [];
|
|
587
|
+
const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE") || 300;
|
|
588
|
+
const fetchOptions = _options.fetchOptions || getEnv("IPX_HTTP_FETCH_OPTIONS") || {};
|
|
589
|
+
if (typeof _domains === "string") _domains = _domains.split(",").map((s$1) => s$1.trim());
|
|
590
|
+
const domains = new Set(_domains.map((d) => {
|
|
591
|
+
if (!HTTP_RE.test(d)) d = "http://" + d;
|
|
592
|
+
return new URL(d).hostname;
|
|
593
|
+
}).filter(Boolean));
|
|
594
|
+
function validateId(id) {
|
|
595
|
+
const url = new URL(decodeURIComponent(id));
|
|
596
|
+
if (!url.hostname) throw new HTTPError({
|
|
597
|
+
statusCode: 403,
|
|
598
|
+
statusText: `IPX_MISSING_HOSTNAME`,
|
|
599
|
+
message: `Hostname is missing: ${id}`
|
|
600
|
+
});
|
|
601
|
+
if (!allowAllDomains && !domains.has(url.hostname)) throw new HTTPError({
|
|
602
|
+
statusCode: 403,
|
|
603
|
+
statusText: `IPX_FORBIDDEN_HOST`,
|
|
604
|
+
message: `Forbidden host: ${url.hostname}`
|
|
605
|
+
});
|
|
606
|
+
return url.toString();
|
|
607
|
+
}
|
|
608
|
+
function parseResponse(response) {
|
|
609
|
+
let maxAge = defaultMaxAge;
|
|
610
|
+
if (_options.ignoreCacheControl !== true) {
|
|
611
|
+
const _cacheControl = response.headers.get("cache-control");
|
|
612
|
+
if (_cacheControl) {
|
|
613
|
+
const m = _cacheControl.match(/max-age=(\d+)/);
|
|
614
|
+
if (m && m[1]) maxAge = Number.parseInt(m[1]);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
let mtime;
|
|
618
|
+
const _lastModified = response.headers.get("last-modified");
|
|
619
|
+
if (_lastModified) mtime = new Date(_lastModified);
|
|
620
|
+
return {
|
|
621
|
+
maxAge,
|
|
622
|
+
mtime
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
name: "ipx:http",
|
|
627
|
+
async getMeta(id) {
|
|
628
|
+
const url = validateId(id);
|
|
629
|
+
try {
|
|
630
|
+
const response = await fetch(url, {
|
|
631
|
+
...fetchOptions,
|
|
632
|
+
method: "HEAD"
|
|
633
|
+
});
|
|
634
|
+
if (!response.ok) return {};
|
|
635
|
+
const { maxAge, mtime } = parseResponse(response);
|
|
636
|
+
return {
|
|
637
|
+
mtime,
|
|
638
|
+
maxAge
|
|
639
|
+
};
|
|
640
|
+
} catch {
|
|
641
|
+
return {};
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
async getData(id) {
|
|
645
|
+
const url = validateId(id);
|
|
646
|
+
const response = await fetch(url, fetchOptions);
|
|
647
|
+
if (!response.ok) throw new HTTPError({
|
|
648
|
+
statusCode: response.status,
|
|
649
|
+
statusText: response.statusText,
|
|
650
|
+
message: `Failed to fetch ${id}: ${response.status} ${response.statusText}`
|
|
651
|
+
});
|
|
652
|
+
return await response.arrayBuffer();
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/storage/node-fs.ts
|
|
659
|
+
/**
|
|
660
|
+
* Creates a file system storage handler for IPX that allows images to be served from local directories specified in the options.
|
|
661
|
+
* This handler resolves directories and handles file access, ensuring that files are served safely.
|
|
662
|
+
*
|
|
663
|
+
* @param {NodeFSSOptions} [_options={}] - File system storage configuration options, with optional directory paths and caching configuration. See {@link NodeFSSOptions}.
|
|
664
|
+
* @returns {IPXStorage} An implementation of the IPXStorage interface for accessing images stored on the local file system. See {@link IPXStorage}.
|
|
665
|
+
* @throws {H3Error} If there is a problem accessing the file system module or resolving/reading files. See {@link H3Error}.
|
|
666
|
+
*/
|
|
667
|
+
function ipxFSStorage(_options = {}) {
|
|
668
|
+
const dirs = resolveDirs(_options.dir);
|
|
669
|
+
const maxAge = _options.maxAge || getEnv("IPX_FS_MAX_AGE");
|
|
670
|
+
const fs = globalThis.process.getBuiltinModule("node:fs/promises");
|
|
671
|
+
const resolveFile = async (id) => {
|
|
672
|
+
for (const dir of dirs) {
|
|
673
|
+
const filePath = join(dir, id);
|
|
674
|
+
if (!isValidPath(filePath) || !filePath.startsWith(dir + "/")) throw new HTTPError({
|
|
675
|
+
statusCode: 403,
|
|
676
|
+
statusText: `IPX_FORBIDDEN_PATH`,
|
|
677
|
+
message: `Forbidden path: ${id}`
|
|
678
|
+
});
|
|
679
|
+
try {
|
|
680
|
+
const stats = await fs.stat(filePath);
|
|
681
|
+
if (!stats.isFile()) continue;
|
|
682
|
+
return {
|
|
683
|
+
stats,
|
|
684
|
+
read: () => fs.readFile(filePath)
|
|
685
|
+
};
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (error.code === "ENOENT") continue;
|
|
688
|
+
throw new HTTPError({
|
|
689
|
+
statusCode: 403,
|
|
690
|
+
statusText: `IPX_FORBIDDEN_FILE`,
|
|
691
|
+
message: `Cannot access file: ${id}`
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
throw new HTTPError({
|
|
696
|
+
statusCode: 404,
|
|
697
|
+
statusText: `IPX_FILE_NOT_FOUND`,
|
|
698
|
+
message: `File not found: ${id}`
|
|
699
|
+
});
|
|
700
|
+
};
|
|
701
|
+
return {
|
|
702
|
+
name: "ipx:node-fs",
|
|
703
|
+
async getMeta(id) {
|
|
704
|
+
const { stats } = await resolveFile(id);
|
|
705
|
+
return {
|
|
706
|
+
mtime: stats.mtime,
|
|
707
|
+
maxAge
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
async getData(id) {
|
|
711
|
+
const { read } = await resolveFile(id);
|
|
712
|
+
return read();
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const isWindows = process.platform === "win32";
|
|
717
|
+
function isValidPath(fp) {
|
|
718
|
+
if (isWindows) fp = fp.slice(parse(fp).root.length);
|
|
719
|
+
if (/["*:<>?|]/.test(fp)) return false;
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
function resolveDirs(dirs) {
|
|
723
|
+
if (!dirs || !Array.isArray(dirs)) return [resolve(dirs || getEnv("IPX_FS_DIR") || ".")];
|
|
724
|
+
return dirs.map((dirs$1) => resolve(dirs$1));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
//#endregion
|
|
728
|
+
export { serveIPX as a, createIPXNodeHandler as i, ipxHttpStorage as n, createIPX as o, createIPXFetchHandler as r, ipxFSStorage as t };
|