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