ipx 0.8.1 → 0.9.3

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  High performance, secure and easy to use image proxy based on [sharp](https://github.com/lovell/sharp) and [libvips](https://github.com/libvips/libvips).
8
8
 
9
- <h2 align="center">Usage</h2>
9
+ ## Usage
10
10
 
11
11
  ### Quick Start
12
12
 
@@ -52,27 +52,29 @@ Resize to `200x200px` using `embed` method and change format to `webp`:
52
52
 
53
53
  ### Modifiers
54
54
 
55
- | Property | Docs | Example | Comments |
56
- | ----------- | :-------------------------------------------------------------- | :------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
- | width / w | \_ | `http://localhost:3000/width_200/buffalo.png` |
58
- | height / h | \_ | `http://localhost:3000/height_200/buffalo.png` |
59
- | resize / s | \_ | `http://localhost:3000/s_200x200/buffalo.png` |
60
- | trim | [Docs](https://sharp.pixelplumbing.com/api-resize#trim) | `http://localhost:3000/trim_100/buffalo.png` |
61
- | format | [Docs](https://sharp.pixelplumbing.com/api-output#toformat) | `http://localhost:3000/format_webp/buffalo.png` | Supported format: `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif`, `heif` |
62
- | quality / q | \_ | `http://localhost:3000/quality_50/buffalo.png` | Accepted values: 0 to 100 |
63
- | rotate | [Docs](https://sharp.pixelplumbing.com/api-operation#rotate) | `http://localhost:3000/rotate_45/buffalo.png` |
64
- | enlarge | \_ | `http://localhost:3000/enlarge,s_2000x2000/buffalo.png` | Allow the image to be upscaled. By default the returned image will never be larger than the source in any dimension, while preserving the requested aspect ratio. |
65
- | flip | [Docs](https://sharp.pixelplumbing.com/api-operation#flip) | `http://localhost:3000/flip/buffalo.png` |
66
- | flop | [Docs](https://sharp.pixelplumbing.com/api-operation#flop) | `http://localhost:3000/flop/buffalo.png` |
67
- | sharpen | [Docs](https://sharp.pixelplumbing.com/api-operation#sharpen) | `http://localhost:3000/sharpen_30/buffalo.png` |
68
- | median | [Docs](https://sharp.pixelplumbing.com/api-operation#median) | `http://localhost:3000/median_10/buffalo.png` |
69
- | gamma | [Docs](https://sharp.pixelplumbing.com/api-operation#gamma) | `http://localhost:3000/gamma_3/buffalo.png` |
70
- | negate | [Docs](https://sharp.pixelplumbing.com/api-operation#negate) | `http://localhost:3000/negate/buffalo.png` |
71
- | normalize | [Docs](https://sharp.pixelplumbing.com/api-operation#normalize) | `http://localhost:3000/normalize/buffalo.png` |
72
- | threshold | [Docs](https://sharp.pixelplumbing.com/api-operation#threshold) | `http://localhost:3000/threshold_10/buffalo.png` |
73
- | tint | [Docs](https://sharp.pixelplumbing.com/api-colour#tint) | `http://localhost:3000/tint_1098123/buffalo.png` |
74
- | grayscale | [Docs](https://sharp.pixelplumbing.com/api-colour#grayscale) | `http://localhost:3000/grayscale/buffalo.png` |
75
- | animated | - | `http://localhost:3000/animated/buffalo.gif` | Experimental |
55
+ | Property | Docs | Example | Comments |
56
+ | --------------- | :-------------------------------------------------------------- | :---------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
+ | width / w | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/width_200/buffalo.png` |
58
+ | height / h | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/height_200/buffalo.png` |
59
+ | resize / s | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/s_200x200/buffalo.png` |
60
+ | fit | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/s_200x200,fit_outside/buffalo.png` | Sets `fit` option for `resize`.
61
+ | position / pos | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/s_200x200,pos_top/buffalo.png` | Sets `position` option for `resize`.
62
+ | trim | [Docs](https://sharp.pixelplumbing.com/api-resize#trim) | `http://localhost:3000/trim_100/buffalo.png` |
63
+ | format | [Docs](https://sharp.pixelplumbing.com/api-output#toformat) | `http://localhost:3000/format_webp/buffalo.png` | Supported format: `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif`, `heif` |
64
+ | quality / q | \_ | `http://localhost:3000/quality_50/buffalo.png` | Accepted values: 0 to 100 |
65
+ | rotate | [Docs](https://sharp.pixelplumbing.com/api-operation#rotate) | `http://localhost:3000/rotate_45/buffalo.png` |
66
+ | enlarge | \_ | `http://localhost:3000/enlarge,s_2000x2000/buffalo.png` | Allow the image to be upscaled. By default the returned image will never be larger than the source in any dimension, while preserving the requested aspect ratio. |
67
+ | flip | [Docs](https://sharp.pixelplumbing.com/api-operation#flip) | `http://localhost:3000/flip/buffalo.png` |
68
+ | flop | [Docs](https://sharp.pixelplumbing.com/api-operation#flop) | `http://localhost:3000/flop/buffalo.png` |
69
+ | sharpen | [Docs](https://sharp.pixelplumbing.com/api-operation#sharpen) | `http://localhost:3000/sharpen_30/buffalo.png` |
70
+ | median | [Docs](https://sharp.pixelplumbing.com/api-operation#median) | `http://localhost:3000/median_10/buffalo.png` |
71
+ | gamma | [Docs](https://sharp.pixelplumbing.com/api-operation#gamma) | `http://localhost:3000/gamma_3/buffalo.png` |
72
+ | negate | [Docs](https://sharp.pixelplumbing.com/api-operation#negate) | `http://localhost:3000/negate/buffalo.png` |
73
+ | normalize | [Docs](https://sharp.pixelplumbing.com/api-operation#normalize) | `http://localhost:3000/normalize/buffalo.png` |
74
+ | threshold | [Docs](https://sharp.pixelplumbing.com/api-operation#threshold) | `http://localhost:3000/threshold_10/buffalo.png` |
75
+ | tint | [Docs](https://sharp.pixelplumbing.com/api-colour#tint) | `http://localhost:3000/tint_1098123/buffalo.png` |
76
+ | grayscale | [Docs](https://sharp.pixelplumbing.com/api-colour#grayscale) | `http://localhost:3000/grayscale/buffalo.png` |
77
+ | animated | - | `http://localhost:3000/animated/buffalo.gif` | Experimental |
76
78
 
77
79
  ### Config
78
80
 
@@ -85,6 +87,6 @@ Config can be customized using `IPX_*` environment variables.
85
87
  - `IPX_DOMAINS`
86
88
  - Default: `[]`
87
89
 
88
- <h2 align="center">License</h2>
90
+ ## License
89
91
 
90
92
  MIT
package/bin/ipx.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.mjs'
@@ -1,38 +1,21 @@
1
- 'use strict';
1
+ import defu from 'defu';
2
+ import { imageMeta } from 'image-meta';
3
+ import { parseURL, withLeadingSlash, hasProtocol, joinURL, decode } from 'ufo';
4
+ import { promises } from 'fs';
5
+ import { resolve, join } from 'pathe';
6
+ import isValidPath from 'is-valid-path';
7
+ import http from 'http';
8
+ import https from 'https';
9
+ import { fetch } from 'ohmyfetch';
10
+ import destr from 'destr';
11
+ import getEtag from 'etag';
12
+ import xss from 'xss';
2
13
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- const Sharp = require('sharp');
6
- const defu = require('defu');
7
- const imageMeta = require('image-meta');
8
- const ufo = require('ufo');
9
- const path = require('path');
10
- const isValidPath = require('is-valid-path');
11
- const fsExtra = require('fs-extra');
12
- const destr = require('destr');
13
- const http = require('http');
14
- const https = require('https');
15
- const fetch = require('node-fetch');
16
- const getEtag = require('etag');
17
- const xss = require('xss');
18
-
19
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
20
-
21
- const Sharp__default = /*#__PURE__*/_interopDefaultLegacy(Sharp);
22
- const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
23
- const imageMeta__default = /*#__PURE__*/_interopDefaultLegacy(imageMeta);
24
- const isValidPath__default = /*#__PURE__*/_interopDefaultLegacy(isValidPath);
25
- const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
26
- const http__default = /*#__PURE__*/_interopDefaultLegacy(http);
27
- const https__default = /*#__PURE__*/_interopDefaultLegacy(https);
28
- const fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
29
- const getEtag__default = /*#__PURE__*/_interopDefaultLegacy(getEtag);
30
- const xss__default = /*#__PURE__*/_interopDefaultLegacy(xss);
31
-
32
- const Handlers = /*#__PURE__*/Object.freeze({
14
+ const Handlers = {
33
15
  __proto__: null,
34
16
  get quality () { return quality; },
35
17
  get fit () { return fit; },
18
+ get position () { return position; },
36
19
  get background () { return background; },
37
20
  get enlarge () { return enlarge; },
38
21
  get width () { return width; },
@@ -60,12 +43,12 @@ const Handlers = /*#__PURE__*/Object.freeze({
60
43
  get b () { return b; },
61
44
  get w () { return w; },
62
45
  get h () { return h; },
63
- get s () { return s; }
64
- });
46
+ get s () { return s; },
47
+ get pos () { return pos; }
48
+ };
65
49
 
66
50
  function getEnv(name, defaultValue) {
67
- var _a;
68
- return (_a = destr__default['default'](process.env[name])) != null ? _a : defaultValue;
51
+ return destr(process.env[name]) ?? defaultValue;
69
52
  }
70
53
  function cachedPromise(fn) {
71
54
  let p;
@@ -87,15 +70,15 @@ function createError(message, statusCode) {
87
70
  }
88
71
 
89
72
  const createFilesystemSource = (options) => {
90
- const rootDir = path.resolve(options.dir);
73
+ const rootDir = resolve(options.dir);
91
74
  return async (id) => {
92
- const fsPath = path.resolve(path.join(rootDir, id));
93
- if (!isValidPath__default['default'](id) || id.includes("..") || !fsPath.startsWith(rootDir)) {
75
+ const fsPath = resolve(join(rootDir, id));
76
+ if (!isValidPath(id) || id.includes("..") || !fsPath.startsWith(rootDir)) {
94
77
  throw createError("Forbidden path:" + id, 403);
95
78
  }
96
79
  let stats;
97
80
  try {
98
- stats = await fsExtra.stat(fsPath);
81
+ stats = await promises.stat(fsPath);
99
82
  } catch (err) {
100
83
  if (err.code === "ENOENT") {
101
84
  throw createError("File not found: " + fsPath, 404);
@@ -109,28 +92,28 @@ const createFilesystemSource = (options) => {
109
92
  return {
110
93
  mtime: stats.mtime,
111
94
  maxAge: options.maxAge || 300,
112
- getData: cachedPromise(() => fsExtra.readFile(fsPath))
95
+ getData: cachedPromise(() => promises.readFile(fsPath))
113
96
  };
114
97
  };
115
98
  };
116
99
 
117
100
  const createHTTPSource = (options) => {
118
- const httpsAgent = new https__default['default'].Agent({ keepAlive: true });
119
- const httpAgent = new http__default['default'].Agent({ keepAlive: true });
101
+ const httpsAgent = new https.Agent({ keepAlive: true });
102
+ const httpAgent = new http.Agent({ keepAlive: true });
120
103
  let domains = options.domains || [];
121
104
  if (typeof domains === "string") {
122
105
  domains = domains.split(",").map((s) => s.trim());
123
106
  }
124
- const hosts = domains.map((domain) => ufo.parseURL(domain, "https://").host);
107
+ const hosts = domains.map((domain) => parseURL(domain, "https://").host);
125
108
  return async (id, reqOptions) => {
126
109
  const url = new URL(id);
127
110
  if (!url.hostname) {
128
111
  throw createError("Hostname is missing: " + id, 403);
129
112
  }
130
- if (!(reqOptions == null ? void 0 : reqOptions.bypassDomain) && !hosts.find((host) => url.hostname === host)) {
113
+ if (!reqOptions?.bypassDomain && !hosts.find((host) => url.hostname === host)) {
131
114
  throw createError("Forbidden host: " + url.hostname, 403);
132
115
  }
133
- const response = await fetch__default['default'](id, {
116
+ const response = await fetch(id, {
134
117
  agent: id.startsWith("https") ? httpsAgent : httpAgent
135
118
  });
136
119
  if (!response.ok) {
@@ -158,7 +141,7 @@ const createHTTPSource = (options) => {
158
141
  };
159
142
 
160
143
  function VArg(arg) {
161
- return destr__default['default'](arg);
144
+ return destr(arg);
162
145
  }
163
146
  function parseArgs(args, mappers) {
164
147
  const vargs = args.split("_");
@@ -199,6 +182,13 @@ const fit = {
199
182
  context.fit = fit2;
200
183
  }
201
184
  };
185
+ const position = {
186
+ args: [VArg],
187
+ order: -1,
188
+ apply: (context, _pipe, position2) => {
189
+ context.position = position2;
190
+ }
191
+ };
202
192
  const HEX_RE = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
203
193
  const SHORTHEX_RE = /^([a-f\d])([a-f\d])([a-f\d])$/i;
204
194
  const background = {
@@ -234,6 +224,12 @@ const resize = {
234
224
  args: [VArg, VArg, VArg],
235
225
  apply: (context, pipe, size) => {
236
226
  let [width2, height2] = String(size).split("x").map((v) => Number(v));
227
+ if (!width2) {
228
+ return;
229
+ }
230
+ if (!height2) {
231
+ height2 = width2;
232
+ }
237
233
  if (!context.enlarge) {
238
234
  const clamped = clampDimensionsPreservingAspectRatio(context.meta, { width: width2, height: height2 });
239
235
  width2 = clamped.width;
@@ -241,6 +237,7 @@ const resize = {
241
237
  }
242
238
  return pipe.resize(width2, height2, {
243
239
  fit: context.fit,
240
+ position: context.position,
244
241
  background: context.background
245
242
  });
246
243
  }
@@ -373,6 +370,7 @@ const b = background;
373
370
  const w = width;
374
371
  const h = height;
375
372
  const s = resize;
373
+ const pos = position;
376
374
 
377
375
  const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff"];
378
376
  function createIPX(userOptions) {
@@ -382,8 +380,8 @@ function createIPX(userOptions) {
382
380
  alias: getEnv("IPX_ALIAS", {}),
383
381
  sharp: {}
384
382
  };
385
- const options = defu__default['default'](userOptions, defaults);
386
- options.alias = Object.fromEntries(Object.entries(options.alias).map((e) => [ufo.withLeadingSlash(e[0]), e[1]]));
383
+ const options = defu(userOptions, defaults);
384
+ options.alias = Object.fromEntries(Object.entries(options.alias).map((e) => [withLeadingSlash(e[0]), e[1]]));
387
385
  const ctx = {
388
386
  sources: {}
389
387
  };
@@ -401,14 +399,14 @@ function createIPX(userOptions) {
401
399
  if (!id) {
402
400
  throw createError("resource id is missing", 400);
403
401
  }
404
- id = ufo.hasProtocol(id) ? id : ufo.withLeadingSlash(id);
402
+ id = hasProtocol(id) ? id : withLeadingSlash(id);
405
403
  for (const base in options.alias) {
406
404
  if (id.startsWith(base)) {
407
- id = ufo.joinURL(options.alias[base], id.substr(base.length));
405
+ id = joinURL(options.alias[base], id.substr(base.length));
408
406
  }
409
407
  }
410
408
  const getSrc = cachedPromise(() => {
411
- const source = ufo.hasProtocol(id) ? "http" : "filesystem";
409
+ const source = hasProtocol(id) ? "http" : "filesystem";
412
410
  if (!ctx.sources[source]) {
413
411
  throw createError("Unknown source: " + source, 400);
414
412
  }
@@ -417,7 +415,7 @@ function createIPX(userOptions) {
417
415
  const getData = cachedPromise(async () => {
418
416
  const src = await getSrc();
419
417
  const data = await src.getData();
420
- const meta = imageMeta__default['default'](data);
418
+ const meta = imageMeta(data);
421
419
  const mFormat = modifiers.f || modifiers.format;
422
420
  let format = mFormat || meta.type;
423
421
  if (format === "jpg") {
@@ -434,7 +432,8 @@ function createIPX(userOptions) {
434
432
  if (animated) {
435
433
  format = "webp";
436
434
  }
437
- let sharp = Sharp__default['default'](data, { animated });
435
+ const Sharp = await import('sharp').then((r) => r.default || r);
436
+ let sharp = Sharp(data, { animated });
438
437
  Object.assign(sharp.options, options.sharp);
439
438
  const handlers = Object.entries(modifiers).map(([name, args]) => ({ handler: getHandler(name), name, args })).filter((h) => h.handler).sort((a, b) => {
440
439
  const aKey = (a.handler.order || a.name || "").toString();
@@ -473,18 +472,18 @@ async function _handleRequest(req, ipx) {
473
472
  body: ""
474
473
  };
475
474
  const [modifiersStr = "", ...idSegments] = req.url.substr(1).split("/");
476
- const id = ufo.decode(idSegments.join("/"));
475
+ const id = decode(idSegments.join("/"));
477
476
  if (!modifiersStr) {
478
477
  throw createError("Modifiers is missing in path: " + req.url, 400);
479
478
  }
480
479
  if (!id || id === "/") {
481
480
  throw createError("Resource id is missing: " + req.url, 400);
482
481
  }
483
- const modifiers = Object.create(null);
482
+ const modifiers = /* @__PURE__ */ Object.create(null);
484
483
  if (modifiersStr !== "_") {
485
484
  for (const p of modifiersStr.split(",")) {
486
485
  const [key, value = ""] = p.split("_");
487
- modifiers[key] = ufo.decode(value);
486
+ modifiers[key] = decode(value);
488
487
  }
489
488
  }
490
489
  const img = ipx(id, modifiers, req.options);
@@ -502,7 +501,7 @@ async function _handleRequest(req, ipx) {
502
501
  res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
503
502
  }
504
503
  const { data, format } = await img.data();
505
- const etag = getEtag__default['default'](data);
504
+ const etag = getEtag(data);
506
505
  res.headers.ETag = etag;
507
506
  if (etag && req.headers["if-none-match"] === etag) {
508
507
  res.statusCode = 304;
@@ -517,7 +516,7 @@ async function _handleRequest(req, ipx) {
517
516
  function handleRequest(req, ipx) {
518
517
  return _handleRequest(req, ipx).catch((err) => {
519
518
  const statusCode = parseInt(err.statusCode) || 500;
520
- const statusMessage = err.statusMessage ? xss__default['default'](err.statusMessage) : `IPX Error (${statusCode})`;
519
+ const statusMessage = err.statusMessage ? xss(err.statusMessage) : `IPX Error (${statusCode})`;
521
520
  if (process.env.NODE_ENV !== "production" && statusCode === 500) {
522
521
  console.error(err);
523
522
  }
@@ -542,6 +541,4 @@ function createIPXMiddleware(ipx) {
542
541
  };
543
542
  }
544
543
 
545
- exports.createIPX = createIPX;
546
- exports.createIPXMiddleware = createIPXMiddleware;
547
- exports.handleRequest = handleRequest;
544
+ export { createIPXMiddleware as a, createIPX as c, handleRequest as h };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+
package/dist/cli.mjs ADDED
@@ -0,0 +1,27 @@
1
+ import consola from 'consola';
2
+ import { listen } from 'listhen';
3
+ import { c as createIPX, a as createIPXMiddleware } from './chunks/middleware.mjs';
4
+ import 'defu';
5
+ import 'image-meta';
6
+ import 'ufo';
7
+ import 'fs';
8
+ import 'pathe';
9
+ import 'is-valid-path';
10
+ import 'http';
11
+ import 'https';
12
+ import 'ohmyfetch';
13
+ import 'destr';
14
+ import 'etag';
15
+ import 'xss';
16
+
17
+ async function main() {
18
+ const ipx = createIPX({});
19
+ const middleware = createIPXMiddleware(ipx);
20
+ await listen(middleware, {
21
+ clipboard: false
22
+ });
23
+ }
24
+ main().catch((err) => {
25
+ consola.error(err);
26
+ process.exit(1);
27
+ });