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