ipx 1.1.0 → 1.2.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.
- package/README.md +62 -35
- package/dist/cli.cjs +6 -5
- package/dist/cli.mjs +3 -2
- package/dist/index.cjs +661 -18
- package/dist/index.d.ts +131 -11
- package/dist/index.mjs +651 -12
- package/package.json +11 -9
- package/dist/shared/ipx.cc3515c9.mjs +0 -616
- package/dist/shared/ipx.e9f7a9b5.cjs +0 -628
package/dist/index.cjs
CHANGED
|
@@ -1,20 +1,663 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
require('
|
|
5
|
-
require('
|
|
6
|
-
require('
|
|
7
|
-
require('
|
|
8
|
-
require('
|
|
9
|
-
require('node:http');
|
|
10
|
-
require('node:https');
|
|
11
|
-
require('node-fetch-native');
|
|
12
|
-
require('
|
|
13
|
-
require('etag');
|
|
14
|
-
require('xss');
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
3
|
+
const defu = require('defu');
|
|
4
|
+
const imageMeta = require('image-meta');
|
|
5
|
+
const ufo = require('ufo');
|
|
6
|
+
const node_fs = require('node:fs');
|
|
7
|
+
const pathe = require('pathe');
|
|
8
|
+
const destr = require('destr');
|
|
9
|
+
const http = require('node:http');
|
|
10
|
+
const https = require('node:https');
|
|
11
|
+
const nodeFetchNative = require('node-fetch-native');
|
|
12
|
+
const acceptNegotiator = require('@fastify/accept-negotiator');
|
|
13
|
+
const getEtag = require('etag');
|
|
14
|
+
const xss = require('xss');
|
|
15
|
+
|
|
16
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
17
|
+
|
|
18
|
+
const destr__default = /*#__PURE__*/_interopDefaultCompat(destr);
|
|
19
|
+
const http__default = /*#__PURE__*/_interopDefaultCompat(http);
|
|
20
|
+
const https__default = /*#__PURE__*/_interopDefaultCompat(https);
|
|
21
|
+
const getEtag__default = /*#__PURE__*/_interopDefaultCompat(getEtag);
|
|
22
|
+
const xss__default = /*#__PURE__*/_interopDefaultCompat(xss);
|
|
23
|
+
|
|
24
|
+
const Handlers = {
|
|
25
|
+
__proto__: null,
|
|
26
|
+
get b () { return b; },
|
|
27
|
+
get background () { return background; },
|
|
28
|
+
get blur () { return blur; },
|
|
29
|
+
get crop () { return crop; },
|
|
30
|
+
get enlarge () { return enlarge; },
|
|
31
|
+
get extend () { return extend; },
|
|
32
|
+
get extract () { return extract; },
|
|
33
|
+
get fit () { return fit; },
|
|
34
|
+
get flatten () { return flatten; },
|
|
35
|
+
get flip () { return flip; },
|
|
36
|
+
get flop () { return flop; },
|
|
37
|
+
get gamma () { return gamma; },
|
|
38
|
+
get grayscale () { return grayscale; },
|
|
39
|
+
get h () { return h; },
|
|
40
|
+
get height () { return height; },
|
|
41
|
+
get median () { return median; },
|
|
42
|
+
get modulate () { return modulate; },
|
|
43
|
+
get negate () { return negate; },
|
|
44
|
+
get normalize () { return normalize; },
|
|
45
|
+
get pos () { return pos; },
|
|
46
|
+
get position () { return position; },
|
|
47
|
+
get q () { return q; },
|
|
48
|
+
get quality () { return quality; },
|
|
49
|
+
get resize () { return resize; },
|
|
50
|
+
get rotate () { return rotate; },
|
|
51
|
+
get s () { return s; },
|
|
52
|
+
get sharpen () { return sharpen; },
|
|
53
|
+
get threshold () { return threshold; },
|
|
54
|
+
get tint () { return tint; },
|
|
55
|
+
get trim () { return trim; },
|
|
56
|
+
get w () { return w; },
|
|
57
|
+
get width () { return width; }
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function getEnv(name, defaultValue) {
|
|
61
|
+
return destr__default(process.env[name]) ?? defaultValue;
|
|
62
|
+
}
|
|
63
|
+
function cachedPromise(function_) {
|
|
64
|
+
let p;
|
|
65
|
+
return (...arguments_) => {
|
|
66
|
+
if (p) {
|
|
67
|
+
return p;
|
|
68
|
+
}
|
|
69
|
+
p = Promise.resolve(function_(...arguments_));
|
|
70
|
+
return p;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
class IPXError extends Error {
|
|
74
|
+
}
|
|
75
|
+
function createError(statusMessage, statusCode, trace) {
|
|
76
|
+
const error = new IPXError(statusMessage + (trace ? ` (${trace})` : ""));
|
|
77
|
+
error.statusMessage = "IPX: " + statusMessage;
|
|
78
|
+
error.statusCode = statusCode;
|
|
79
|
+
return error;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const createFilesystemSource = (options) => {
|
|
83
|
+
const rootDir = pathe.resolve(options.dir);
|
|
84
|
+
return async (id) => {
|
|
85
|
+
const fsPath = pathe.resolve(pathe.join(rootDir, id));
|
|
86
|
+
if (!isValidPath(fsPath) || !fsPath.startsWith(rootDir)) {
|
|
87
|
+
throw createError("Forbidden path", 403, id);
|
|
88
|
+
}
|
|
89
|
+
let stats;
|
|
90
|
+
try {
|
|
91
|
+
stats = await node_fs.promises.stat(fsPath);
|
|
92
|
+
} catch (error_) {
|
|
93
|
+
const error = error_.code === "ENOENT" ? createError("File not found", 404, fsPath) : createError("File access error " + error_.code, 403, fsPath);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
if (!stats.isFile()) {
|
|
97
|
+
throw createError("Path should be a file", 400, fsPath);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
mtime: stats.mtime,
|
|
101
|
+
maxAge: options.maxAge,
|
|
102
|
+
getData: cachedPromise(() => node_fs.promises.readFile(fsPath))
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
const isWindows = process.platform === "win32";
|
|
107
|
+
function isValidPath(fp) {
|
|
108
|
+
if (isWindows) {
|
|
109
|
+
fp = fp.slice(pathe.parse(fp).root.length);
|
|
110
|
+
}
|
|
111
|
+
if (/["*:<>?|]/.test(fp)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const HTTP_RE = /^https?:\/\//;
|
|
118
|
+
const createHTTPSource = (options) => {
|
|
119
|
+
const httpsAgent = new https__default.Agent({ keepAlive: true });
|
|
120
|
+
const httpAgent = new http__default.Agent({ keepAlive: true });
|
|
121
|
+
let _domains = options.domains || [];
|
|
122
|
+
if (typeof _domains === "string") {
|
|
123
|
+
_domains = _domains.split(",").map((s) => s.trim());
|
|
124
|
+
}
|
|
125
|
+
const domains = new Set(
|
|
126
|
+
_domains.map((d) => {
|
|
127
|
+
if (!HTTP_RE.test(d)) {
|
|
128
|
+
d = "http://" + d;
|
|
129
|
+
}
|
|
130
|
+
return new URL(d).hostname;
|
|
131
|
+
}).filter(Boolean)
|
|
132
|
+
);
|
|
133
|
+
return async (id, requestOptions) => {
|
|
134
|
+
const hostname = new URL(id).hostname;
|
|
135
|
+
if (!hostname) {
|
|
136
|
+
throw createError("Hostname is missing", 403, id);
|
|
137
|
+
}
|
|
138
|
+
if (!requestOptions?.bypassDomain && !domains.has(hostname)) {
|
|
139
|
+
throw createError("Forbidden host", 403, hostname);
|
|
140
|
+
}
|
|
141
|
+
const response = await nodeFetchNative.fetch(id, {
|
|
142
|
+
// @ts-ignore
|
|
143
|
+
agent: id.startsWith("https") ? httpsAgent : httpAgent,
|
|
144
|
+
...options.fetchOptions
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
throw createError(
|
|
148
|
+
"Fetch error",
|
|
149
|
+
response.status || 500,
|
|
150
|
+
response.statusText
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
let maxAge = options.maxAge;
|
|
154
|
+
const _cacheControl = response.headers.get("cache-control");
|
|
155
|
+
if (_cacheControl) {
|
|
156
|
+
const m = _cacheControl.match(/max-age=(\d+)/);
|
|
157
|
+
if (m && m[1]) {
|
|
158
|
+
maxAge = Number.parseInt(m[1]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
let mtime;
|
|
162
|
+
const _lastModified = response.headers.get("last-modified");
|
|
163
|
+
if (_lastModified) {
|
|
164
|
+
mtime = new Date(_lastModified);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
mtime,
|
|
168
|
+
maxAge,
|
|
169
|
+
// @ts-ignore
|
|
170
|
+
getData: cachedPromise(
|
|
171
|
+
() => response.arrayBuffer().then((ab) => Buffer.from(ab))
|
|
172
|
+
)
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function VArg(argument) {
|
|
178
|
+
return destr__default(argument);
|
|
179
|
+
}
|
|
180
|
+
function parseArgs(arguments_, mappers) {
|
|
181
|
+
const vargs = arguments_.split("_");
|
|
182
|
+
return mappers.map((v, index) => v(vargs[index]));
|
|
183
|
+
}
|
|
184
|
+
function getHandler(key) {
|
|
185
|
+
return Handlers[key];
|
|
186
|
+
}
|
|
187
|
+
function applyHandler(context, pipe, handler, argumentsString) {
|
|
188
|
+
const arguments_ = handler.args ? parseArgs(argumentsString, handler.args) : [];
|
|
189
|
+
return handler.apply(context, pipe, ...arguments_);
|
|
190
|
+
}
|
|
191
|
+
function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
|
|
192
|
+
const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
|
|
193
|
+
let { width, height } = desiredDimensions;
|
|
194
|
+
if (width > sourceDimensions.width) {
|
|
195
|
+
width = sourceDimensions.width;
|
|
196
|
+
height = Math.round(sourceDimensions.width / desiredAspectRatio);
|
|
197
|
+
}
|
|
198
|
+
if (height > sourceDimensions.height) {
|
|
199
|
+
height = sourceDimensions.height;
|
|
200
|
+
width = Math.round(sourceDimensions.height * desiredAspectRatio);
|
|
201
|
+
}
|
|
202
|
+
return { width, height };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const quality = {
|
|
206
|
+
args: [VArg],
|
|
207
|
+
order: -1,
|
|
208
|
+
apply: (context, _pipe, quality2) => {
|
|
209
|
+
context.quality = quality2;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const fit = {
|
|
213
|
+
args: [VArg],
|
|
214
|
+
order: -1,
|
|
215
|
+
apply: (context, _pipe, fit2) => {
|
|
216
|
+
context.fit = fit2;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const position = {
|
|
220
|
+
args: [VArg],
|
|
221
|
+
order: -1,
|
|
222
|
+
apply: (context, _pipe, position2) => {
|
|
223
|
+
context.position = position2;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const HEX_RE = /^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i;
|
|
227
|
+
const SHORTHEX_RE = /^([\da-f])([\da-f])([\da-f])$/i;
|
|
228
|
+
const background = {
|
|
229
|
+
args: [VArg],
|
|
230
|
+
order: -1,
|
|
231
|
+
apply: (context, _pipe, background2) => {
|
|
232
|
+
background2 = String(background2);
|
|
233
|
+
if (!background2.startsWith("#") && (HEX_RE.test(background2) || SHORTHEX_RE.test(background2))) {
|
|
234
|
+
background2 = "#" + background2;
|
|
235
|
+
}
|
|
236
|
+
context.background = background2;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const enlarge = {
|
|
240
|
+
args: [],
|
|
241
|
+
apply: (context) => {
|
|
242
|
+
context.enlarge = true;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const width = {
|
|
246
|
+
args: [VArg],
|
|
247
|
+
apply: (context, pipe, width2) => {
|
|
248
|
+
return pipe.resize(width2, void 0, {
|
|
249
|
+
withoutEnlargement: !context.enlarge
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const height = {
|
|
254
|
+
args: [VArg],
|
|
255
|
+
apply: (context, pipe, height2) => {
|
|
256
|
+
return pipe.resize(void 0, height2, {
|
|
257
|
+
withoutEnlargement: !context.enlarge
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const resize = {
|
|
262
|
+
args: [VArg, VArg, VArg],
|
|
263
|
+
apply: (context, pipe, size) => {
|
|
264
|
+
let [width2, height2] = String(size).split("x").map(Number);
|
|
265
|
+
if (!width2) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (!height2) {
|
|
269
|
+
height2 = width2;
|
|
270
|
+
}
|
|
271
|
+
if (!context.enlarge) {
|
|
272
|
+
const clamped = clampDimensionsPreservingAspectRatio(context.meta, {
|
|
273
|
+
width: width2,
|
|
274
|
+
height: height2
|
|
275
|
+
});
|
|
276
|
+
width2 = clamped.width;
|
|
277
|
+
height2 = clamped.height;
|
|
278
|
+
}
|
|
279
|
+
return pipe.resize(width2, height2, {
|
|
280
|
+
fit: context.fit,
|
|
281
|
+
position: context.position,
|
|
282
|
+
background: context.background
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const trim = {
|
|
287
|
+
args: [VArg],
|
|
288
|
+
apply: (_context, pipe, threshold2) => {
|
|
289
|
+
return pipe.trim(threshold2);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const extend = {
|
|
293
|
+
args: [VArg, VArg, VArg, VArg],
|
|
294
|
+
apply: (context, pipe, top, right, bottom, left) => {
|
|
295
|
+
return pipe.extend({
|
|
296
|
+
top,
|
|
297
|
+
left,
|
|
298
|
+
bottom,
|
|
299
|
+
right,
|
|
300
|
+
background: context.background
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const extract = {
|
|
305
|
+
args: [VArg, VArg, VArg, VArg],
|
|
306
|
+
apply: (_context, pipe, left, top, width2, height2) => {
|
|
307
|
+
return pipe.extract({
|
|
308
|
+
left,
|
|
309
|
+
top,
|
|
310
|
+
width: width2,
|
|
311
|
+
height: height2
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const rotate = {
|
|
316
|
+
args: [VArg],
|
|
317
|
+
apply: (context, pipe, angel) => {
|
|
318
|
+
return pipe.rotate(angel, {
|
|
319
|
+
background: context.background
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const flip = {
|
|
324
|
+
args: [],
|
|
325
|
+
apply: (_context, pipe) => {
|
|
326
|
+
return pipe.flip();
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const flop = {
|
|
330
|
+
args: [],
|
|
331
|
+
apply: (_context, pipe) => {
|
|
332
|
+
return pipe.flop();
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const sharpen = {
|
|
336
|
+
args: [VArg, VArg, VArg],
|
|
337
|
+
apply: (_context, pipe, sigma, flat, jagged) => {
|
|
338
|
+
return pipe.sharpen(sigma, flat, jagged);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const median = {
|
|
342
|
+
args: [VArg, VArg, VArg],
|
|
343
|
+
apply: (_context, pipe, size) => {
|
|
344
|
+
return pipe.median(size);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const blur = {
|
|
348
|
+
args: [VArg, VArg, VArg],
|
|
349
|
+
apply: (_context, pipe, sigma) => {
|
|
350
|
+
return pipe.blur(sigma);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const flatten = {
|
|
354
|
+
args: [VArg, VArg, VArg],
|
|
355
|
+
apply: (context, pipe) => {
|
|
356
|
+
return pipe.flatten({
|
|
357
|
+
background: context.background
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const gamma = {
|
|
362
|
+
args: [VArg, VArg, VArg],
|
|
363
|
+
apply: (_context, pipe, gamma2, gammaOut) => {
|
|
364
|
+
return pipe.gamma(gamma2, gammaOut);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const negate = {
|
|
368
|
+
args: [VArg, VArg, VArg],
|
|
369
|
+
apply: (_context, pipe) => {
|
|
370
|
+
return pipe.negate();
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const normalize = {
|
|
374
|
+
args: [VArg, VArg, VArg],
|
|
375
|
+
apply: (_context, pipe) => {
|
|
376
|
+
return pipe.normalize();
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const threshold = {
|
|
380
|
+
args: [VArg],
|
|
381
|
+
apply: (_context, pipe, threshold2) => {
|
|
382
|
+
return pipe.threshold(threshold2);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
const modulate = {
|
|
386
|
+
args: [VArg],
|
|
387
|
+
apply: (_context, pipe, brightness, saturation, hue) => {
|
|
388
|
+
return pipe.modulate({
|
|
389
|
+
brightness,
|
|
390
|
+
saturation,
|
|
391
|
+
hue
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const tint = {
|
|
396
|
+
args: [VArg],
|
|
397
|
+
apply: (_context, pipe, rgb) => {
|
|
398
|
+
return pipe.tint(rgb);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
const grayscale = {
|
|
402
|
+
args: [VArg],
|
|
403
|
+
apply: (_context, pipe) => {
|
|
404
|
+
return pipe.grayscale();
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const crop = extract;
|
|
408
|
+
const q = quality;
|
|
409
|
+
const b = background;
|
|
410
|
+
const w = width;
|
|
411
|
+
const h = height;
|
|
412
|
+
const s = resize;
|
|
413
|
+
const pos = position;
|
|
414
|
+
|
|
415
|
+
const SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
|
|
416
|
+
"jpeg",
|
|
417
|
+
"png",
|
|
418
|
+
"webp",
|
|
419
|
+
"avif",
|
|
420
|
+
"tiff",
|
|
421
|
+
"heif",
|
|
422
|
+
"gif"
|
|
423
|
+
]);
|
|
424
|
+
function createIPX(userOptions) {
|
|
425
|
+
const defaults = {
|
|
426
|
+
dir: getEnv("IPX_DIR", "."),
|
|
427
|
+
domains: getEnv("IPX_DOMAINS", []),
|
|
428
|
+
alias: getEnv("IPX_ALIAS", {}),
|
|
429
|
+
fetchOptions: getEnv("IPX_FETCH_OPTIONS", {}),
|
|
430
|
+
maxAge: getEnv("IPX_MAX_AGE", 300),
|
|
431
|
+
sharp: {}
|
|
432
|
+
};
|
|
433
|
+
const options = defu.defu(userOptions, defaults);
|
|
434
|
+
options.alias = Object.fromEntries(
|
|
435
|
+
Object.entries(options.alias).map((e) => [ufo.withLeadingSlash(e[0]), e[1]])
|
|
436
|
+
);
|
|
437
|
+
const context = {
|
|
438
|
+
sources: {}
|
|
439
|
+
};
|
|
440
|
+
if (options.dir) {
|
|
441
|
+
context.sources.filesystem = createFilesystemSource({
|
|
442
|
+
dir: options.dir,
|
|
443
|
+
maxAge: options.maxAge
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (options.domains) {
|
|
447
|
+
context.sources.http = createHTTPSource({
|
|
448
|
+
domains: options.domains,
|
|
449
|
+
fetchOptions: options.fetchOptions,
|
|
450
|
+
maxAge: options.maxAge
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return function ipx(id, modifiers = {}, requestOptions = {}) {
|
|
454
|
+
if (!id) {
|
|
455
|
+
throw createError("resource id is missing", 400);
|
|
456
|
+
}
|
|
457
|
+
id = ufo.hasProtocol(id) ? id : ufo.withLeadingSlash(id);
|
|
458
|
+
for (const base in options.alias) {
|
|
459
|
+
if (id.startsWith(base)) {
|
|
460
|
+
id = ufo.joinURL(options.alias[base], id.slice(base.length));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const getSource = cachedPromise(() => {
|
|
464
|
+
const source = ufo.hasProtocol(id) ? "http" : "filesystem";
|
|
465
|
+
if (!context.sources[source]) {
|
|
466
|
+
throw createError("Unknown source", 400, source);
|
|
467
|
+
}
|
|
468
|
+
return context.sources[source](id, requestOptions);
|
|
469
|
+
});
|
|
470
|
+
const getData = cachedPromise(async () => {
|
|
471
|
+
const source = await getSource();
|
|
472
|
+
const data = await source.getData();
|
|
473
|
+
const meta = imageMeta.imageMeta(data);
|
|
474
|
+
let mFormat = modifiers.f || modifiers.format;
|
|
475
|
+
if (mFormat === "jpg") {
|
|
476
|
+
mFormat = "jpeg";
|
|
477
|
+
}
|
|
478
|
+
const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(meta.type) ? meta.type : "jpeg";
|
|
479
|
+
if (meta.type === "svg" && !mFormat) {
|
|
480
|
+
return {
|
|
481
|
+
data,
|
|
482
|
+
format: "svg+xml",
|
|
483
|
+
meta
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
|
|
487
|
+
const Sharp = await import('sharp').then(
|
|
488
|
+
(r) => r.default || r
|
|
489
|
+
);
|
|
490
|
+
let sharp = Sharp(data, { animated });
|
|
491
|
+
Object.assign(sharp.options, options.sharp);
|
|
492
|
+
const handlers = Object.entries(modifiers).map(([name, arguments_]) => ({
|
|
493
|
+
handler: getHandler(name),
|
|
494
|
+
name,
|
|
495
|
+
args: arguments_
|
|
496
|
+
})).filter((h) => h.handler).sort((a, b) => {
|
|
497
|
+
const aKey = (a.handler.order || a.name || "").toString();
|
|
498
|
+
const bKey = (b.handler.order || b.name || "").toString();
|
|
499
|
+
return aKey.localeCompare(bKey);
|
|
500
|
+
});
|
|
501
|
+
const handlerContext = { meta };
|
|
502
|
+
for (const h of handlers) {
|
|
503
|
+
sharp = applyHandler(handlerContext, sharp, h.handler, h.args) || sharp;
|
|
504
|
+
}
|
|
505
|
+
if (SUPPORTED_FORMATS.has(format)) {
|
|
506
|
+
sharp = sharp.toFormat(format, {
|
|
507
|
+
quality: handlerContext.quality,
|
|
508
|
+
progressive: format === "jpeg"
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
const newData = await sharp.toBuffer();
|
|
512
|
+
return {
|
|
513
|
+
data: newData,
|
|
514
|
+
format,
|
|
515
|
+
meta
|
|
516
|
+
};
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
src: getSource,
|
|
520
|
+
data: getData
|
|
521
|
+
};
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const MODIFIER_SEP = /[&,]/g;
|
|
526
|
+
const MODIFIER_VAL_SEP = /[:=_]/;
|
|
527
|
+
async function _handleRequest(request, ipx) {
|
|
528
|
+
const res = {
|
|
529
|
+
statusCode: 200,
|
|
530
|
+
statusMessage: "",
|
|
531
|
+
headers: {},
|
|
532
|
+
body: ""
|
|
533
|
+
};
|
|
534
|
+
const [modifiersString = "", ...idSegments] = request.url.slice(
|
|
535
|
+
1
|
|
536
|
+
/* leading slash */
|
|
537
|
+
).split("/");
|
|
538
|
+
const id = safeString(ufo.decode(idSegments.join("/")));
|
|
539
|
+
if (!modifiersString) {
|
|
540
|
+
throw createError("Modifiers are missing", 400, request.url);
|
|
541
|
+
}
|
|
542
|
+
if (!id || id === "/") {
|
|
543
|
+
throw createError("Resource id is missing", 400, request.url);
|
|
544
|
+
}
|
|
545
|
+
const modifiers = /* @__PURE__ */ Object.create(null);
|
|
546
|
+
if (modifiersString !== "_") {
|
|
547
|
+
for (const p of modifiersString.split(MODIFIER_SEP)) {
|
|
548
|
+
const [key, ...values] = p.split(MODIFIER_VAL_SEP);
|
|
549
|
+
modifiers[safeString(key)] = values.map((v) => safeString(ufo.decode(v))).join("_");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const mFormat = modifiers.f || modifiers.format;
|
|
553
|
+
if (mFormat === "auto") {
|
|
554
|
+
const acceptHeader = request.headers?.accept || "";
|
|
555
|
+
const autoFormat = autoDetectFormat(
|
|
556
|
+
acceptHeader,
|
|
557
|
+
!!(modifiers.a || modifiers.animated)
|
|
558
|
+
);
|
|
559
|
+
delete modifiers.f;
|
|
560
|
+
delete modifiers.format;
|
|
561
|
+
if (autoFormat) {
|
|
562
|
+
modifiers.format = autoFormat;
|
|
563
|
+
res.headers.vary = "Accept";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const img = ipx(id, modifiers, request.options);
|
|
567
|
+
const source = await img.src();
|
|
568
|
+
if (source.mtime) {
|
|
569
|
+
if (request.headers?.["if-modified-since"] && new Date(request.headers["if-modified-since"]) >= source.mtime) {
|
|
570
|
+
res.statusCode = 304;
|
|
571
|
+
return res;
|
|
572
|
+
}
|
|
573
|
+
res.headers["Last-Modified"] = source.mtime.toUTCString();
|
|
574
|
+
}
|
|
575
|
+
if (typeof source.maxAge === "number") {
|
|
576
|
+
res.headers["Cache-Control"] = `max-age=${+source.maxAge}, public, s-maxage=${+source.maxAge}`;
|
|
577
|
+
}
|
|
578
|
+
const { data, format } = await img.data();
|
|
579
|
+
const etag = getEtag__default(data);
|
|
580
|
+
res.headers.ETag = etag;
|
|
581
|
+
if (etag && request.headers?.["if-none-match"] === etag) {
|
|
582
|
+
res.statusCode = 304;
|
|
583
|
+
return res;
|
|
584
|
+
}
|
|
585
|
+
if (format) {
|
|
586
|
+
res.headers["Content-Type"] = `image/${format}`;
|
|
587
|
+
}
|
|
588
|
+
res.headers["Content-Security-Policy"] = "default-src 'none'";
|
|
589
|
+
res.body = data;
|
|
590
|
+
return sanetizeReponse(res);
|
|
591
|
+
}
|
|
592
|
+
function handleRequest(request, ipx) {
|
|
593
|
+
return _handleRequest(request, ipx).catch((error) => {
|
|
594
|
+
const statusCode = Number.parseInt(error.statusCode) || 500;
|
|
595
|
+
const statusMessage = error.statusMessage ? error.statusMessage : `IPX Error (${statusCode})`;
|
|
596
|
+
if (process.env.NODE_ENV !== "production" && statusCode === 500) {
|
|
597
|
+
console.error(error);
|
|
598
|
+
}
|
|
599
|
+
return sanetizeReponse({
|
|
600
|
+
statusCode,
|
|
601
|
+
statusMessage,
|
|
602
|
+
body: "IPX Error: " + error,
|
|
603
|
+
headers: {},
|
|
604
|
+
error
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function createIPXMiddleware(ipx, options = {}) {
|
|
609
|
+
return function IPXMiddleware(request, res, next) {
|
|
610
|
+
return handleRequest(
|
|
611
|
+
{ url: request.url || "/", headers: request.headers },
|
|
612
|
+
ipx
|
|
613
|
+
).then((_res) => {
|
|
614
|
+
if (options.fallthrough && next && _res.error) {
|
|
615
|
+
return next(_res.error);
|
|
616
|
+
}
|
|
617
|
+
res.statusCode = _res.statusCode;
|
|
618
|
+
res.statusMessage = _res.statusMessage;
|
|
619
|
+
for (const name in _res.headers) {
|
|
620
|
+
res.setHeader(name, _res.headers[name]);
|
|
621
|
+
}
|
|
622
|
+
res.end(_res.body);
|
|
623
|
+
});
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function autoDetectFormat(acceptHeader, animated) {
|
|
627
|
+
if (animated) {
|
|
628
|
+
const acceptMime2 = acceptNegotiator.negotiate(acceptHeader, ["image/webp", "image/gif"]);
|
|
629
|
+
return acceptMime2?.split("/")[1] || "gif";
|
|
630
|
+
}
|
|
631
|
+
const acceptMime = acceptNegotiator.negotiate(acceptHeader, [
|
|
632
|
+
"image/avif",
|
|
633
|
+
"image/webp",
|
|
634
|
+
"image/jpeg",
|
|
635
|
+
"image/png",
|
|
636
|
+
"image/tiff",
|
|
637
|
+
"image/heif",
|
|
638
|
+
"image/gif"
|
|
639
|
+
]);
|
|
640
|
+
return acceptMime?.split("/")[1] || "jpeg";
|
|
641
|
+
}
|
|
642
|
+
function sanetizeReponse(res) {
|
|
643
|
+
return {
|
|
644
|
+
statusCode: res.statusCode || 200,
|
|
645
|
+
statusMessage: res.statusMessage ? safeString(res.statusMessage) : "OK",
|
|
646
|
+
headers: safeStringObject(res.headers || {}),
|
|
647
|
+
body: typeof res.body === "string" ? xss__default(safeString(res.body)) : res.body || ""
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function safeString(input) {
|
|
651
|
+
return JSON.stringify(input).replace(/^"|"$/g, "").replace(/\\+/g, "\\").replace(/\\"/g, '"');
|
|
652
|
+
}
|
|
653
|
+
function safeStringObject(input) {
|
|
654
|
+
const dst = {};
|
|
655
|
+
for (const key in input) {
|
|
656
|
+
dst[key] = safeString(input[key]);
|
|
657
|
+
}
|
|
658
|
+
return dst;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
exports.createIPX = createIPX;
|
|
662
|
+
exports.createIPXMiddleware = createIPXMiddleware;
|
|
663
|
+
exports.handleRequest = handleRequest;
|