ipx 0.9.10 → 1.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2017-2021
3
+ Copyright (c) Pooya Parsa <pooya@pi0.io>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/cli.cjs CHANGED
@@ -2,23 +2,19 @@
2
2
 
3
3
  const consola = require('consola');
4
4
  const listhen = require('listhen');
5
- const middleware = require('./chunks/middleware.cjs');
5
+ const middleware = require('./shared/ipx.dd1c1144.cjs');
6
6
  require('defu');
7
7
  require('image-meta');
8
8
  require('ufo');
9
- require('fs');
9
+ require('node:fs');
10
10
  require('pathe');
11
- require('http');
12
- require('https');
11
+ require('node:http');
12
+ require('node:https');
13
13
  require('ohmyfetch');
14
14
  require('destr');
15
15
  require('etag');
16
16
  require('xss');
17
17
 
18
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
19
-
20
- const consola__default = /*#__PURE__*/_interopDefaultLegacy(consola);
21
-
22
18
  async function main() {
23
19
  const ipx = middleware.createIPX({});
24
20
  const middleware$1 = middleware.createIPXMiddleware(ipx);
@@ -26,7 +22,7 @@ async function main() {
26
22
  clipboard: false
27
23
  });
28
24
  }
29
- main().catch((err) => {
30
- consola__default.error(err);
25
+ main().catch((error) => {
26
+ consola.error(error);
31
27
  process.exit(1);
32
28
  });
package/dist/cli.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import consola from 'consola';
2
2
  import { listen } from 'listhen';
3
- import { c as createIPX, a as createIPXMiddleware } from './chunks/middleware.mjs';
3
+ import { c as createIPX, a as createIPXMiddleware } from './shared/ipx.d659cb77.mjs';
4
4
  import 'defu';
5
5
  import 'image-meta';
6
6
  import 'ufo';
7
- import 'fs';
7
+ import 'node:fs';
8
8
  import 'pathe';
9
- import 'http';
10
- import 'https';
9
+ import 'node:http';
10
+ import 'node:https';
11
11
  import 'ohmyfetch';
12
12
  import 'destr';
13
13
  import 'etag';
@@ -20,7 +20,7 @@ async function main() {
20
20
  clipboard: false
21
21
  });
22
22
  }
23
- main().catch((err) => {
24
- consola.error(err);
23
+ main().catch((error) => {
24
+ consola.error(error);
25
25
  process.exit(1);
26
26
  });
package/dist/index.cjs CHANGED
@@ -1,15 +1,13 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- const middleware = require('./chunks/middleware.cjs');
3
+ const middleware = require('./shared/ipx.dd1c1144.cjs');
6
4
  require('defu');
7
5
  require('image-meta');
8
6
  require('ufo');
9
- require('fs');
7
+ require('node:fs');
10
8
  require('pathe');
11
- require('http');
12
- require('https');
9
+ require('node:http');
10
+ require('node:https');
13
11
  require('ohmyfetch');
14
12
  require('destr');
15
13
  require('etag');
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { IncomingMessage, ServerResponse } from 'http';
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
2
 
3
3
  interface SourceData {
4
4
  mtime?: Date;
5
5
  maxAge?: number;
6
6
  getData: () => Promise<Buffer>;
7
7
  }
8
- declare type Source = (src: string, reqOptions?: any) => Promise<SourceData>;
9
- declare type SourceFactory<T = Record<string, any>> = (options: T) => Source;
8
+ type Source = (source: string, requestOptions?: any) => Promise<SourceData>;
9
+ type SourceFactory<T = Record<string, any>> = (options: T) => Source;
10
10
 
11
11
  interface ImageMeta {
12
12
  width: number;
@@ -17,7 +17,7 @@ interface ImageMeta {
17
17
  interface IPXCTX {
18
18
  sources: Record<string, Source>;
19
19
  }
20
- declare type IPX = (id: string, modifiers?: Record<string, string>, reqOptions?: any) => {
20
+ type IPX = (id: string, modifiers?: Record<string, string>, requestOptions?: any) => {
21
21
  src: () => Promise<SourceData>;
22
22
  data: () => Promise<{
23
23
  data: Buffer;
@@ -48,7 +48,7 @@ interface IPXHResponse {
48
48
  headers: Record<string, string>;
49
49
  body: any;
50
50
  }
51
- declare function handleRequest(req: IPXHRequest, ipx: IPX): Promise<IPXHResponse>;
52
- declare function createIPXMiddleware(ipx: IPX): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
51
+ declare function handleRequest(request: IPXHRequest, ipx: IPX): Promise<IPXHResponse>;
52
+ declare function createIPXMiddleware(ipx: IPX): (request: IncomingMessage, res: ServerResponse) => Promise<void>;
53
53
 
54
54
  export { IPX, IPXCTX, IPXHRequest, IPXHResponse, IPXOptions, ImageMeta, Source, SourceData, SourceFactory, createIPX, createIPXMiddleware, handleRequest };
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
- export { c as createIPX, a as createIPXMiddleware, h as handleRequest } from './chunks/middleware.mjs';
1
+ export { c as createIPX, a as createIPXMiddleware, h as handleRequest } from './shared/ipx.d659cb77.mjs';
2
2
  import 'defu';
3
3
  import 'image-meta';
4
4
  import 'ufo';
5
- import 'fs';
5
+ import 'node:fs';
6
6
  import 'pathe';
7
- import 'http';
8
- import 'https';
7
+ import 'node:http';
8
+ import 'node:https';
9
9
  import 'ohmyfetch';
10
10
  import 'destr';
11
11
  import 'etag';
@@ -1,10 +1,10 @@
1
1
  import defu from 'defu';
2
2
  import { imageMeta } from 'image-meta';
3
3
  import { withLeadingSlash, hasProtocol, joinURL, decode } from 'ufo';
4
- import { promises } from 'fs';
4
+ import { promises } from 'node:fs';
5
5
  import { resolve, join, parse } from 'pathe';
6
- import http from 'http';
7
- import https from 'https';
6
+ import http from 'node:http';
7
+ import https from 'node:https';
8
8
  import { fetch } from 'ohmyfetch';
9
9
  import destr from 'destr';
10
10
  import getEtag from 'etag';
@@ -49,23 +49,23 @@ const Handlers = {
49
49
  function getEnv(name, defaultValue) {
50
50
  return destr(process.env[name]) ?? defaultValue;
51
51
  }
52
- function cachedPromise(fn) {
52
+ function cachedPromise(function_) {
53
53
  let p;
54
- return (...args) => {
54
+ return (...arguments_) => {
55
55
  if (p) {
56
56
  return p;
57
57
  }
58
- p = Promise.resolve(fn(...args));
58
+ p = Promise.resolve(function_(...arguments_));
59
59
  return p;
60
60
  };
61
61
  }
62
62
  class IPXError extends Error {
63
63
  }
64
64
  function createError(statusMessage, statusCode, trace) {
65
- const err = new IPXError(statusMessage + (trace ? ` (${trace})` : ""));
66
- err.statusMessage = "IPX: " + statusMessage;
67
- err.statusCode = statusCode;
68
- return err;
65
+ const error = new IPXError(statusMessage + (trace ? ` (${trace})` : ""));
66
+ error.statusMessage = "IPX: " + statusMessage;
67
+ error.statusCode = statusCode;
68
+ return error;
69
69
  }
70
70
 
71
71
  const createFilesystemSource = (options) => {
@@ -78,12 +78,9 @@ const createFilesystemSource = (options) => {
78
78
  let stats;
79
79
  try {
80
80
  stats = await promises.stat(fsPath);
81
- } catch (err) {
82
- if (err.code === "ENOENT") {
83
- throw createError("File not found", 404, fsPath);
84
- } else {
85
- throw createError("File access error " + err.code, 403, fsPath);
86
- }
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;
87
84
  }
88
85
  if (!stats.isFile()) {
89
86
  throw createError("Path should be a file", 400, fsPath);
@@ -100,12 +97,13 @@ function isValidPath(fp) {
100
97
  if (isWindows) {
101
98
  fp = fp.slice(parse(fp).root.length);
102
99
  }
103
- if (/[<>:"|?*]/.test(fp)) {
100
+ if (/["*:<>?|]/.test(fp)) {
104
101
  return false;
105
102
  }
106
103
  return true;
107
104
  }
108
105
 
106
+ const HTTP_RE = /^https?:\/\//;
109
107
  const createHTTPSource = (options) => {
110
108
  const httpsAgent = new https.Agent({ keepAlive: true });
111
109
  const httpAgent = new http.Agent({ keepAlive: true });
@@ -113,18 +111,18 @@ const createHTTPSource = (options) => {
113
111
  if (typeof _domains === "string") {
114
112
  _domains = _domains.split(",").map((s) => s.trim());
115
113
  }
116
- const domains = _domains.map((d) => {
117
- if (!d.startsWith("http")) {
114
+ const domains = new Set(_domains.map((d) => {
115
+ if (!HTTP_RE.test(d)) {
118
116
  d = "http://" + d;
119
117
  }
120
118
  return new URL(d).hostname;
121
- }).filter(Boolean);
122
- return async (id, reqOptions) => {
119
+ }).filter(Boolean));
120
+ return async (id, requestOptions) => {
123
121
  const hostname = new URL(id).hostname;
124
122
  if (!hostname) {
125
123
  throw createError("Hostname is missing", 403, id);
126
124
  }
127
- if (!reqOptions?.bypassDomain && !domains.find((domain) => hostname === domain)) {
125
+ if (!requestOptions?.bypassDomain && !domains.has(hostname)) {
128
126
  throw createError("Forbidden host", 403, hostname);
129
127
  }
130
128
  const response = await fetch(id, {
@@ -139,7 +137,7 @@ const createHTTPSource = (options) => {
139
137
  if (_cacheControl) {
140
138
  const m = _cacheControl.match(/max-age=(\d+)/);
141
139
  if (m && m[1]) {
142
- maxAge = parseInt(m[1]);
140
+ maxAge = Number.parseInt(m[1]);
143
141
  }
144
142
  }
145
143
  let mtime;
@@ -155,19 +153,19 @@ const createHTTPSource = (options) => {
155
153
  };
156
154
  };
157
155
 
158
- function VArg(arg) {
159
- return destr(arg);
156
+ function VArg(argument) {
157
+ return destr(argument);
160
158
  }
161
- function parseArgs(args, mappers) {
162
- const vargs = args.split("_");
163
- return mappers.map((v, i) => v(vargs[i]));
159
+ function parseArgs(arguments_, mappers) {
160
+ const vargs = arguments_.split("_");
161
+ return mappers.map((v, index) => v(vargs[index]));
164
162
  }
165
163
  function getHandler(key) {
166
164
  return Handlers[key];
167
165
  }
168
- function applyHandler(ctx, pipe, handler, argsStr) {
169
- const args = handler.args ? parseArgs(argsStr, handler.args) : [];
170
- return handler.apply(ctx, pipe, ...args);
166
+ function applyHandler(context, pipe, handler, argumentsString) {
167
+ const arguments_ = handler.args ? parseArgs(argumentsString, handler.args) : [];
168
+ return handler.apply(context, pipe, ...arguments_);
171
169
  }
172
170
  function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
173
171
  const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
@@ -204,8 +202,8 @@ const position = {
204
202
  context.position = position2;
205
203
  }
206
204
  };
207
- const HEX_RE = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
208
- const SHORTHEX_RE = /^([a-f\d])([a-f\d])([a-f\d])$/i;
205
+ const HEX_RE = /^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i;
206
+ const SHORTHEX_RE = /^([\da-f])([\da-f])([\da-f])$/i;
209
207
  const background = {
210
208
  args: [VArg],
211
209
  order: -1,
@@ -226,19 +224,19 @@ const enlarge = {
226
224
  const width = {
227
225
  args: [VArg],
228
226
  apply: (context, pipe, width2) => {
229
- return pipe.resize(width2, null, { withoutEnlargement: !context.enlarge });
227
+ return pipe.resize(width2, void 0, { withoutEnlargement: !context.enlarge });
230
228
  }
231
229
  };
232
230
  const height = {
233
231
  args: [VArg],
234
232
  apply: (context, pipe, height2) => {
235
- return pipe.resize(null, height2, { withoutEnlargement: !context.enlarge });
233
+ return pipe.resize(void 0, height2, { withoutEnlargement: !context.enlarge });
236
234
  }
237
235
  };
238
236
  const resize = {
239
237
  args: [VArg, VArg, VArg],
240
238
  apply: (context, pipe, size) => {
241
- let [width2, height2] = String(size).split("x").map((v) => Number(v));
239
+ let [width2, height2] = String(size).split("x").map(Number);
242
240
  if (!width2) {
243
241
  return;
244
242
  }
@@ -387,7 +385,7 @@ const h = height;
387
385
  const s = resize;
388
386
  const pos = position;
389
387
 
390
- const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff", "gif"];
388
+ const SUPPORTED_FORMATS = /* @__PURE__ */ new Set(["jpeg", "png", "webp", "avif", "tiff", "gif"]);
391
389
  function createIPX(userOptions) {
392
390
  const defaults = {
393
391
  dir: getEnv("IPX_DIR", "."),
@@ -399,42 +397,42 @@ function createIPX(userOptions) {
399
397
  };
400
398
  const options = defu(userOptions, defaults);
401
399
  options.alias = Object.fromEntries(Object.entries(options.alias).map((e) => [withLeadingSlash(e[0]), e[1]]));
402
- const ctx = {
400
+ const context = {
403
401
  sources: {}
404
402
  };
405
403
  if (options.dir) {
406
- ctx.sources.filesystem = createFilesystemSource({
404
+ context.sources.filesystem = createFilesystemSource({
407
405
  dir: options.dir,
408
406
  maxAge: options.maxAge
409
407
  });
410
408
  }
411
409
  if (options.domains) {
412
- ctx.sources.http = createHTTPSource({
410
+ context.sources.http = createHTTPSource({
413
411
  domains: options.domains,
414
412
  fetchOptions: options.fetchOptions,
415
413
  maxAge: options.maxAge
416
414
  });
417
415
  }
418
- return function ipx(id, modifiers = {}, reqOptions = {}) {
416
+ return function ipx(id, modifiers = {}, requestOptions = {}) {
419
417
  if (!id) {
420
418
  throw createError("resource id is missing", 400);
421
419
  }
422
420
  id = hasProtocol(id) ? id : withLeadingSlash(id);
423
421
  for (const base in options.alias) {
424
422
  if (id.startsWith(base)) {
425
- id = joinURL(options.alias[base], id.substr(base.length));
423
+ id = joinURL(options.alias[base], id.slice(base.length));
426
424
  }
427
425
  }
428
- const getSrc = cachedPromise(() => {
426
+ const getSource = cachedPromise(() => {
429
427
  const source = hasProtocol(id) ? "http" : "filesystem";
430
- if (!ctx.sources[source]) {
428
+ if (!context.sources[source]) {
431
429
  throw createError("Unknown source", 400, source);
432
430
  }
433
- return ctx.sources[source](id, reqOptions);
431
+ return context.sources[source](id, requestOptions);
434
432
  });
435
433
  const getData = cachedPromise(async () => {
436
- const src = await getSrc();
437
- const data = await src.getData();
434
+ const source = await getSource();
435
+ const data = await source.getData();
438
436
  const meta = imageMeta(data);
439
437
  const mFormat = modifiers.f || modifiers.format;
440
438
  let format = mFormat || meta.type;
@@ -452,18 +450,18 @@ function createIPX(userOptions) {
452
450
  const Sharp = await import('sharp').then((r) => r.default || r);
453
451
  let sharp = Sharp(data, { animated });
454
452
  Object.assign(sharp.options, options.sharp);
455
- const handlers = Object.entries(modifiers).map(([name, args]) => ({ handler: getHandler(name), name, args })).filter((h) => h.handler).sort((a, b) => {
453
+ const handlers = Object.entries(modifiers).map(([name, arguments_]) => ({ handler: getHandler(name), name, args: arguments_ })).filter((h) => h.handler).sort((a, b) => {
456
454
  const aKey = (a.handler.order || a.name || "").toString();
457
455
  const bKey = (b.handler.order || b.name || "").toString();
458
456
  return aKey.localeCompare(bKey);
459
457
  });
460
- const handlerCtx = { meta };
458
+ const handlerContext = { meta };
461
459
  for (const h of handlers) {
462
- sharp = applyHandler(handlerCtx, sharp, h.handler, h.args) || sharp;
460
+ sharp = applyHandler(handlerContext, sharp, h.handler, h.args) || sharp;
463
461
  }
464
- if (SUPPORTED_FORMATS.includes(format)) {
462
+ if (SUPPORTED_FORMATS.has(format)) {
465
463
  sharp = sharp.toFormat(format, {
466
- quality: handlerCtx.quality,
464
+ quality: handlerContext.quality,
467
465
  progressive: format === "jpeg"
468
466
  });
469
467
  }
@@ -475,81 +473,80 @@ function createIPX(userOptions) {
475
473
  };
476
474
  });
477
475
  return {
478
- src: getSrc,
476
+ src: getSource,
479
477
  data: getData
480
478
  };
481
479
  };
482
480
  }
483
481
 
484
- const MODIFIER_SEP = /[,&]/g;
485
- const MODIFIER_VAL_SEP = /[_=:]/g;
486
- async function _handleRequest(req, ipx) {
482
+ const MODIFIER_SEP = /[&,]/g;
483
+ const MODIFIER_VAL_SEP = /[:=_]/g;
484
+ async function _handleRequest(request, ipx) {
487
485
  const res = {
488
486
  statusCode: 200,
489
487
  statusMessage: "",
490
488
  headers: {},
491
489
  body: ""
492
490
  };
493
- const [modifiersStr = "", ...idSegments] = req.url.substring(1).split("/");
491
+ const [modifiersString = "", ...idSegments] = request.url.slice(1).split("/");
494
492
  const id = safeString(decode(idSegments.join("/")));
495
- if (!modifiersStr) {
496
- throw createError("Modifiers are missing", 400, req.url);
493
+ if (!modifiersString) {
494
+ throw createError("Modifiers are missing", 400, request.url);
497
495
  }
498
496
  if (!id || id === "/") {
499
- throw createError("Resource id is missing", 400, req.url);
497
+ throw createError("Resource id is missing", 400, request.url);
500
498
  }
501
499
  const modifiers = /* @__PURE__ */ Object.create(null);
502
- if (modifiersStr !== "_") {
503
- for (const p of modifiersStr.split(MODIFIER_SEP)) {
500
+ if (modifiersString !== "_") {
501
+ for (const p of modifiersString.split(MODIFIER_SEP)) {
504
502
  const [key, value = ""] = p.split(MODIFIER_VAL_SEP);
505
503
  modifiers[safeString(key)] = safeString(decode(value));
506
504
  }
507
505
  }
508
- const img = ipx(id, modifiers, req.options);
509
- const src = await img.src();
510
- if (src.mtime) {
511
- if (req.headers["if-modified-since"]) {
512
- if (new Date(req.headers["if-modified-since"]) >= src.mtime) {
513
- res.statusCode = 304;
514
- return res;
515
- }
506
+ const img = ipx(id, modifiers, request.options);
507
+ const source = await img.src();
508
+ if (source.mtime) {
509
+ if (request.headers["if-modified-since"] && new Date(request.headers["if-modified-since"]) >= source.mtime) {
510
+ res.statusCode = 304;
511
+ return res;
516
512
  }
517
- res.headers["Last-Modified"] = +src.mtime + "";
513
+ res.headers["Last-Modified"] = source.mtime.toUTCString();
518
514
  }
519
- if (typeof src.maxAge === "number") {
520
- res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
515
+ if (typeof source.maxAge === "number") {
516
+ res.headers["Cache-Control"] = `max-age=${+source.maxAge}, public, s-maxage=${+source.maxAge}`;
521
517
  }
522
518
  const { data, format } = await img.data();
523
519
  const etag = getEtag(data);
524
520
  res.headers.ETag = etag;
525
- if (etag && req.headers["if-none-match"] === etag) {
521
+ if (etag && request.headers["if-none-match"] === etag) {
526
522
  res.statusCode = 304;
527
523
  return res;
528
524
  }
529
525
  if (format) {
530
526
  res.headers["Content-Type"] = `image/${format}`;
531
527
  }
528
+ res.headers["Content-Security-Policy"] = "default-src 'none'";
532
529
  res.body = data;
533
530
  return sanetizeReponse(res);
534
531
  }
535
- function handleRequest(req, ipx) {
536
- return _handleRequest(req, ipx).catch((err) => {
537
- const statusCode = parseInt(err.statusCode) || 500;
538
- const statusMessage = err.statusMessage ? err.statusMessage : `IPX Error (${statusCode})`;
532
+ function handleRequest(request, ipx) {
533
+ return _handleRequest(request, ipx).catch((error) => {
534
+ const statusCode = Number.parseInt(error.statusCode) || 500;
535
+ const statusMessage = error.statusMessage ? error.statusMessage : `IPX Error (${statusCode})`;
539
536
  if (process.env.NODE_ENV !== "production" && statusCode === 500) {
540
- console.error(err);
537
+ console.error(error);
541
538
  }
542
539
  return sanetizeReponse({
543
540
  statusCode,
544
541
  statusMessage,
545
- body: "IPX Error: " + err,
542
+ body: "IPX Error: " + error,
546
543
  headers: {}
547
544
  });
548
545
  });
549
546
  }
550
547
  function createIPXMiddleware(ipx) {
551
- return function IPXMiddleware(req, res) {
552
- return handleRequest({ url: req.url, headers: req.headers }, ipx).then((_res) => {
548
+ return function IPXMiddleware(request, res) {
549
+ return handleRequest({ url: request.url, headers: request.headers }, ipx).then((_res) => {
553
550
  res.statusCode = _res.statusCode;
554
551
  res.statusMessage = _res.statusMessage;
555
552
  for (const name in _res.headers) {
@@ -3,24 +3,15 @@
3
3
  const defu = require('defu');
4
4
  const imageMeta = require('image-meta');
5
5
  const ufo = require('ufo');
6
- const fs = require('fs');
6
+ const node_fs = require('node:fs');
7
7
  const pathe = require('pathe');
8
- const http = require('http');
9
- const https = require('https');
8
+ const http = require('node:http');
9
+ const https = require('node:https');
10
10
  const ohmyfetch = require('ohmyfetch');
11
11
  const destr = require('destr');
12
12
  const getEtag = require('etag');
13
13
  const xss = require('xss');
14
14
 
15
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
16
-
17
- const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
18
- const http__default = /*#__PURE__*/_interopDefaultLegacy(http);
19
- const https__default = /*#__PURE__*/_interopDefaultLegacy(https);
20
- const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
21
- const getEtag__default = /*#__PURE__*/_interopDefaultLegacy(getEtag);
22
- const xss__default = /*#__PURE__*/_interopDefaultLegacy(xss);
23
-
24
15
  const Handlers = {
25
16
  __proto__: null,
26
17
  get quality () { return quality; },
@@ -58,25 +49,25 @@ const Handlers = {
58
49
  };
59
50
 
60
51
  function getEnv(name, defaultValue) {
61
- return destr__default(process.env[name]) ?? defaultValue;
52
+ return destr(process.env[name]) ?? defaultValue;
62
53
  }
63
- function cachedPromise(fn) {
54
+ function cachedPromise(function_) {
64
55
  let p;
65
- return (...args) => {
56
+ return (...arguments_) => {
66
57
  if (p) {
67
58
  return p;
68
59
  }
69
- p = Promise.resolve(fn(...args));
60
+ p = Promise.resolve(function_(...arguments_));
70
61
  return p;
71
62
  };
72
63
  }
73
64
  class IPXError extends Error {
74
65
  }
75
66
  function createError(statusMessage, statusCode, trace) {
76
- const err = new IPXError(statusMessage + (trace ? ` (${trace})` : ""));
77
- err.statusMessage = "IPX: " + statusMessage;
78
- err.statusCode = statusCode;
79
- return err;
67
+ const error = new IPXError(statusMessage + (trace ? ` (${trace})` : ""));
68
+ error.statusMessage = "IPX: " + statusMessage;
69
+ error.statusCode = statusCode;
70
+ return error;
80
71
  }
81
72
 
82
73
  const createFilesystemSource = (options) => {
@@ -88,13 +79,10 @@ const createFilesystemSource = (options) => {
88
79
  }
89
80
  let stats;
90
81
  try {
91
- stats = await fs.promises.stat(fsPath);
92
- } catch (err) {
93
- if (err.code === "ENOENT") {
94
- throw createError("File not found", 404, fsPath);
95
- } else {
96
- throw createError("File access error " + err.code, 403, fsPath);
97
- }
82
+ stats = await node_fs.promises.stat(fsPath);
83
+ } catch (error_) {
84
+ const error = error_.code === "ENOENT" ? createError("File not found", 404, fsPath) : createError("File access error " + error_.code, 403, fsPath);
85
+ throw error;
98
86
  }
99
87
  if (!stats.isFile()) {
100
88
  throw createError("Path should be a file", 400, fsPath);
@@ -102,7 +90,7 @@ const createFilesystemSource = (options) => {
102
90
  return {
103
91
  mtime: stats.mtime,
104
92
  maxAge: options.maxAge,
105
- getData: cachedPromise(() => fs.promises.readFile(fsPath))
93
+ getData: cachedPromise(() => node_fs.promises.readFile(fsPath))
106
94
  };
107
95
  };
108
96
  };
@@ -111,31 +99,32 @@ function isValidPath(fp) {
111
99
  if (isWindows) {
112
100
  fp = fp.slice(pathe.parse(fp).root.length);
113
101
  }
114
- if (/[<>:"|?*]/.test(fp)) {
102
+ if (/["*:<>?|]/.test(fp)) {
115
103
  return false;
116
104
  }
117
105
  return true;
118
106
  }
119
107
 
108
+ const HTTP_RE = /^https?:\/\//;
120
109
  const createHTTPSource = (options) => {
121
- const httpsAgent = new https__default.Agent({ keepAlive: true });
122
- const httpAgent = new http__default.Agent({ keepAlive: true });
110
+ const httpsAgent = new https.Agent({ keepAlive: true });
111
+ const httpAgent = new http.Agent({ keepAlive: true });
123
112
  let _domains = options.domains || [];
124
113
  if (typeof _domains === "string") {
125
114
  _domains = _domains.split(",").map((s) => s.trim());
126
115
  }
127
- const domains = _domains.map((d) => {
128
- if (!d.startsWith("http")) {
116
+ const domains = new Set(_domains.map((d) => {
117
+ if (!HTTP_RE.test(d)) {
129
118
  d = "http://" + d;
130
119
  }
131
120
  return new URL(d).hostname;
132
- }).filter(Boolean);
133
- return async (id, reqOptions) => {
121
+ }).filter(Boolean));
122
+ return async (id, requestOptions) => {
134
123
  const hostname = new URL(id).hostname;
135
124
  if (!hostname) {
136
125
  throw createError("Hostname is missing", 403, id);
137
126
  }
138
- if (!reqOptions?.bypassDomain && !domains.find((domain) => hostname === domain)) {
127
+ if (!requestOptions?.bypassDomain && !domains.has(hostname)) {
139
128
  throw createError("Forbidden host", 403, hostname);
140
129
  }
141
130
  const response = await ohmyfetch.fetch(id, {
@@ -150,7 +139,7 @@ const createHTTPSource = (options) => {
150
139
  if (_cacheControl) {
151
140
  const m = _cacheControl.match(/max-age=(\d+)/);
152
141
  if (m && m[1]) {
153
- maxAge = parseInt(m[1]);
142
+ maxAge = Number.parseInt(m[1]);
154
143
  }
155
144
  }
156
145
  let mtime;
@@ -166,19 +155,19 @@ const createHTTPSource = (options) => {
166
155
  };
167
156
  };
168
157
 
169
- function VArg(arg) {
170
- return destr__default(arg);
158
+ function VArg(argument) {
159
+ return destr(argument);
171
160
  }
172
- function parseArgs(args, mappers) {
173
- const vargs = args.split("_");
174
- return mappers.map((v, i) => v(vargs[i]));
161
+ function parseArgs(arguments_, mappers) {
162
+ const vargs = arguments_.split("_");
163
+ return mappers.map((v, index) => v(vargs[index]));
175
164
  }
176
165
  function getHandler(key) {
177
166
  return Handlers[key];
178
167
  }
179
- function applyHandler(ctx, pipe, handler, argsStr) {
180
- const args = handler.args ? parseArgs(argsStr, handler.args) : [];
181
- return handler.apply(ctx, pipe, ...args);
168
+ function applyHandler(context, pipe, handler, argumentsString) {
169
+ const arguments_ = handler.args ? parseArgs(argumentsString, handler.args) : [];
170
+ return handler.apply(context, pipe, ...arguments_);
182
171
  }
183
172
  function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
184
173
  const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
@@ -215,8 +204,8 @@ const position = {
215
204
  context.position = position2;
216
205
  }
217
206
  };
218
- const HEX_RE = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
219
- const SHORTHEX_RE = /^([a-f\d])([a-f\d])([a-f\d])$/i;
207
+ const HEX_RE = /^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i;
208
+ const SHORTHEX_RE = /^([\da-f])([\da-f])([\da-f])$/i;
220
209
  const background = {
221
210
  args: [VArg],
222
211
  order: -1,
@@ -237,19 +226,19 @@ const enlarge = {
237
226
  const width = {
238
227
  args: [VArg],
239
228
  apply: (context, pipe, width2) => {
240
- return pipe.resize(width2, null, { withoutEnlargement: !context.enlarge });
229
+ return pipe.resize(width2, void 0, { withoutEnlargement: !context.enlarge });
241
230
  }
242
231
  };
243
232
  const height = {
244
233
  args: [VArg],
245
234
  apply: (context, pipe, height2) => {
246
- return pipe.resize(null, height2, { withoutEnlargement: !context.enlarge });
235
+ return pipe.resize(void 0, height2, { withoutEnlargement: !context.enlarge });
247
236
  }
248
237
  };
249
238
  const resize = {
250
239
  args: [VArg, VArg, VArg],
251
240
  apply: (context, pipe, size) => {
252
- let [width2, height2] = String(size).split("x").map((v) => Number(v));
241
+ let [width2, height2] = String(size).split("x").map(Number);
253
242
  if (!width2) {
254
243
  return;
255
244
  }
@@ -398,7 +387,7 @@ const h = height;
398
387
  const s = resize;
399
388
  const pos = position;
400
389
 
401
- const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff", "gif"];
390
+ const SUPPORTED_FORMATS = /* @__PURE__ */ new Set(["jpeg", "png", "webp", "avif", "tiff", "gif"]);
402
391
  function createIPX(userOptions) {
403
392
  const defaults = {
404
393
  dir: getEnv("IPX_DIR", "."),
@@ -408,44 +397,44 @@ function createIPX(userOptions) {
408
397
  maxAge: getEnv("IPX_MAX_AGE", 300),
409
398
  sharp: {}
410
399
  };
411
- const options = defu__default(userOptions, defaults);
400
+ const options = defu(userOptions, defaults);
412
401
  options.alias = Object.fromEntries(Object.entries(options.alias).map((e) => [ufo.withLeadingSlash(e[0]), e[1]]));
413
- const ctx = {
402
+ const context = {
414
403
  sources: {}
415
404
  };
416
405
  if (options.dir) {
417
- ctx.sources.filesystem = createFilesystemSource({
406
+ context.sources.filesystem = createFilesystemSource({
418
407
  dir: options.dir,
419
408
  maxAge: options.maxAge
420
409
  });
421
410
  }
422
411
  if (options.domains) {
423
- ctx.sources.http = createHTTPSource({
412
+ context.sources.http = createHTTPSource({
424
413
  domains: options.domains,
425
414
  fetchOptions: options.fetchOptions,
426
415
  maxAge: options.maxAge
427
416
  });
428
417
  }
429
- return function ipx(id, modifiers = {}, reqOptions = {}) {
418
+ return function ipx(id, modifiers = {}, requestOptions = {}) {
430
419
  if (!id) {
431
420
  throw createError("resource id is missing", 400);
432
421
  }
433
422
  id = ufo.hasProtocol(id) ? id : ufo.withLeadingSlash(id);
434
423
  for (const base in options.alias) {
435
424
  if (id.startsWith(base)) {
436
- id = ufo.joinURL(options.alias[base], id.substr(base.length));
425
+ id = ufo.joinURL(options.alias[base], id.slice(base.length));
437
426
  }
438
427
  }
439
- const getSrc = cachedPromise(() => {
428
+ const getSource = cachedPromise(() => {
440
429
  const source = ufo.hasProtocol(id) ? "http" : "filesystem";
441
- if (!ctx.sources[source]) {
430
+ if (!context.sources[source]) {
442
431
  throw createError("Unknown source", 400, source);
443
432
  }
444
- return ctx.sources[source](id, reqOptions);
433
+ return context.sources[source](id, requestOptions);
445
434
  });
446
435
  const getData = cachedPromise(async () => {
447
- const src = await getSrc();
448
- const data = await src.getData();
436
+ const source = await getSource();
437
+ const data = await source.getData();
449
438
  const meta = imageMeta.imageMeta(data);
450
439
  const mFormat = modifiers.f || modifiers.format;
451
440
  let format = mFormat || meta.type;
@@ -463,18 +452,18 @@ function createIPX(userOptions) {
463
452
  const Sharp = await import('sharp').then((r) => r.default || r);
464
453
  let sharp = Sharp(data, { animated });
465
454
  Object.assign(sharp.options, options.sharp);
466
- const handlers = Object.entries(modifiers).map(([name, args]) => ({ handler: getHandler(name), name, args })).filter((h) => h.handler).sort((a, b) => {
455
+ const handlers = Object.entries(modifiers).map(([name, arguments_]) => ({ handler: getHandler(name), name, args: arguments_ })).filter((h) => h.handler).sort((a, b) => {
467
456
  const aKey = (a.handler.order || a.name || "").toString();
468
457
  const bKey = (b.handler.order || b.name || "").toString();
469
458
  return aKey.localeCompare(bKey);
470
459
  });
471
- const handlerCtx = { meta };
460
+ const handlerContext = { meta };
472
461
  for (const h of handlers) {
473
- sharp = applyHandler(handlerCtx, sharp, h.handler, h.args) || sharp;
462
+ sharp = applyHandler(handlerContext, sharp, h.handler, h.args) || sharp;
474
463
  }
475
- if (SUPPORTED_FORMATS.includes(format)) {
464
+ if (SUPPORTED_FORMATS.has(format)) {
476
465
  sharp = sharp.toFormat(format, {
477
- quality: handlerCtx.quality,
466
+ quality: handlerContext.quality,
478
467
  progressive: format === "jpeg"
479
468
  });
480
469
  }
@@ -486,81 +475,80 @@ function createIPX(userOptions) {
486
475
  };
487
476
  });
488
477
  return {
489
- src: getSrc,
478
+ src: getSource,
490
479
  data: getData
491
480
  };
492
481
  };
493
482
  }
494
483
 
495
- const MODIFIER_SEP = /[,&]/g;
496
- const MODIFIER_VAL_SEP = /[_=:]/g;
497
- async function _handleRequest(req, ipx) {
484
+ const MODIFIER_SEP = /[&,]/g;
485
+ const MODIFIER_VAL_SEP = /[:=_]/g;
486
+ async function _handleRequest(request, ipx) {
498
487
  const res = {
499
488
  statusCode: 200,
500
489
  statusMessage: "",
501
490
  headers: {},
502
491
  body: ""
503
492
  };
504
- const [modifiersStr = "", ...idSegments] = req.url.substring(1).split("/");
493
+ const [modifiersString = "", ...idSegments] = request.url.slice(1).split("/");
505
494
  const id = safeString(ufo.decode(idSegments.join("/")));
506
- if (!modifiersStr) {
507
- throw createError("Modifiers are missing", 400, req.url);
495
+ if (!modifiersString) {
496
+ throw createError("Modifiers are missing", 400, request.url);
508
497
  }
509
498
  if (!id || id === "/") {
510
- throw createError("Resource id is missing", 400, req.url);
499
+ throw createError("Resource id is missing", 400, request.url);
511
500
  }
512
501
  const modifiers = /* @__PURE__ */ Object.create(null);
513
- if (modifiersStr !== "_") {
514
- for (const p of modifiersStr.split(MODIFIER_SEP)) {
502
+ if (modifiersString !== "_") {
503
+ for (const p of modifiersString.split(MODIFIER_SEP)) {
515
504
  const [key, value = ""] = p.split(MODIFIER_VAL_SEP);
516
505
  modifiers[safeString(key)] = safeString(ufo.decode(value));
517
506
  }
518
507
  }
519
- const img = ipx(id, modifiers, req.options);
520
- const src = await img.src();
521
- if (src.mtime) {
522
- if (req.headers["if-modified-since"]) {
523
- if (new Date(req.headers["if-modified-since"]) >= src.mtime) {
524
- res.statusCode = 304;
525
- return res;
526
- }
508
+ const img = ipx(id, modifiers, request.options);
509
+ const source = await img.src();
510
+ if (source.mtime) {
511
+ if (request.headers["if-modified-since"] && new Date(request.headers["if-modified-since"]) >= source.mtime) {
512
+ res.statusCode = 304;
513
+ return res;
527
514
  }
528
- res.headers["Last-Modified"] = +src.mtime + "";
515
+ res.headers["Last-Modified"] = source.mtime.toUTCString();
529
516
  }
530
- if (typeof src.maxAge === "number") {
531
- res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
517
+ if (typeof source.maxAge === "number") {
518
+ res.headers["Cache-Control"] = `max-age=${+source.maxAge}, public, s-maxage=${+source.maxAge}`;
532
519
  }
533
520
  const { data, format } = await img.data();
534
- const etag = getEtag__default(data);
521
+ const etag = getEtag(data);
535
522
  res.headers.ETag = etag;
536
- if (etag && req.headers["if-none-match"] === etag) {
523
+ if (etag && request.headers["if-none-match"] === etag) {
537
524
  res.statusCode = 304;
538
525
  return res;
539
526
  }
540
527
  if (format) {
541
528
  res.headers["Content-Type"] = `image/${format}`;
542
529
  }
530
+ res.headers["Content-Security-Policy"] = "default-src 'none'";
543
531
  res.body = data;
544
532
  return sanetizeReponse(res);
545
533
  }
546
- function handleRequest(req, ipx) {
547
- return _handleRequest(req, ipx).catch((err) => {
548
- const statusCode = parseInt(err.statusCode) || 500;
549
- const statusMessage = err.statusMessage ? err.statusMessage : `IPX Error (${statusCode})`;
534
+ function handleRequest(request, ipx) {
535
+ return _handleRequest(request, ipx).catch((error) => {
536
+ const statusCode = Number.parseInt(error.statusCode) || 500;
537
+ const statusMessage = error.statusMessage ? error.statusMessage : `IPX Error (${statusCode})`;
550
538
  if (process.env.NODE_ENV !== "production" && statusCode === 500) {
551
- console.error(err);
539
+ console.error(error);
552
540
  }
553
541
  return sanetizeReponse({
554
542
  statusCode,
555
543
  statusMessage,
556
- body: "IPX Error: " + err,
544
+ body: "IPX Error: " + error,
557
545
  headers: {}
558
546
  });
559
547
  });
560
548
  }
561
549
  function createIPXMiddleware(ipx) {
562
- return function IPXMiddleware(req, res) {
563
- return handleRequest({ url: req.url, headers: req.headers }, ipx).then((_res) => {
550
+ return function IPXMiddleware(request, res) {
551
+ return handleRequest({ url: request.url, headers: request.headers }, ipx).then((_res) => {
564
552
  res.statusCode = _res.statusCode;
565
553
  res.statusMessage = _res.statusMessage;
566
554
  for (const name in _res.headers) {
@@ -575,7 +563,7 @@ function sanetizeReponse(res) {
575
563
  statusCode: res.statusCode || 200,
576
564
  statusMessage: res.statusMessage ? safeString(res.statusMessage) : "OK",
577
565
  headers: safeStringObject(res.headers || {}),
578
- body: typeof res.body === "string" ? xss__default(safeString(res.body)) : res.body || ""
566
+ body: typeof res.body === "string" ? xss(safeString(res.body)) : res.body || ""
579
567
  };
580
568
  }
581
569
  function safeString(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ipx",
3
- "version": "0.9.10",
3
+ "version": "1.0.0-0",
4
4
  "repository": "unjs/ipx",
5
5
  "license": "MIT",
6
6
  "exports": {
@@ -17,42 +17,45 @@
17
17
  "dist",
18
18
  "bin"
19
19
  ],
20
- "dependencies": {
21
- "consola": "^2.15.3",
22
- "defu": "^6.0.0",
23
- "destr": "^1.1.1",
24
- "etag": "^1.8.1",
25
- "image-meta": "^0.1.1",
26
- "listhen": "^0.2.13",
27
- "ohmyfetch": "^0.4.18",
28
- "pathe": "^0.3.2",
29
- "sharp": "^0.30.7",
30
- "ufo": "^0.8.5",
31
- "xss": "^1.0.13"
32
- },
33
- "devDependencies": {
34
- "@nuxtjs/eslint-config-typescript": "latest",
35
- "@types/etag": "latest",
36
- "@types/is-valid-path": "latest",
37
- "@types/node-fetch": "latest",
38
- "@types/sharp": "latest",
39
- "c8": "latest",
40
- "eslint": "latest",
41
- "jiti": "latest",
42
- "nodemon": "latest",
43
- "serve-handler": "^6.1.3",
44
- "standard-version": "latest",
45
- "typescript": "latest",
46
- "unbuild": "latest",
47
- "vitest": "latest"
48
- },
49
- "packageManager": "pnpm@7.5.0",
50
20
  "scripts": {
51
21
  "build": "unbuild",
52
22
  "dev": "nodemon",
53
23
  "lint": "eslint --ext .ts .",
24
+ "prepack": "pnpm build",
54
25
  "release": "pnpm test && standard-version && git push --follow-tags && pnpm publish",
55
26
  "start": "node bin/ipx.js",
56
27
  "test": "pnpm lint && vitest run --coverage"
57
- }
58
- }
28
+ },
29
+ "dependencies": {
30
+ "consola": "^2.15.3",
31
+ "defu": "^6.1.1",
32
+ "destr": "^1.2.1",
33
+ "etag": "^1.8.1",
34
+ "image-meta": "^0.1.1",
35
+ "listhen": "^1.0.0",
36
+ "ohmyfetch": "^0.4.21",
37
+ "pathe": "^1.0.0",
38
+ "sharp": "^0.31.2",
39
+ "ufo": "^1.0.0",
40
+ "xss": "^1.0.14"
41
+ },
42
+ "devDependencies": {
43
+ "@nuxtjs/eslint-config-typescript": "^12.0.0",
44
+ "@types/etag": "^1.8.1",
45
+ "@types/is-valid-path": "^0.1.0",
46
+ "@types/node-fetch": "^2.6.2",
47
+ "@types/sharp": "^0.31.0",
48
+ "@vitest/coverage-c8": "^0.25.3",
49
+ "changelogen": "^0.4.0",
50
+ "eslint": "^8.28.0",
51
+ "eslint-config-unjs": "^0.0.2",
52
+ "jiti": "^1.16.0",
53
+ "nodemon": "^2.0.20",
54
+ "serve-handler": "^6.1.5",
55
+ "standard-version": "^9.5.0",
56
+ "typescript": "^4.9.3",
57
+ "unbuild": "^1.0.1",
58
+ "vitest": "^0.25.3"
59
+ },
60
+ "packageManager": "pnpm@7.17.0"
61
+ }