ipx 2.0.0-0 → 2.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
File without changes
package/README.md CHANGED
@@ -3,12 +3,12 @@
3
3
  [![npm version][npm-version-src]][npm-version-href]
4
4
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
5
 
6
- High performance, secure and easy-to-use image optimizer.
6
+ > [!NOTE]
7
+ > This is the active branch for IPX v2. Check out [ipx/v1](https://github.com/unjs/ipx/tree/v1) for v1 docs.
7
8
 
8
- Powered by [sharp](https://github.com/lovell/sharp) and [libvips](https://github.com/libvips/libvips).
9
+ High performance, secure and easy-to-use image optimizer powered by [sharp](https://github.com/lovell/sharp) and [svgo](https://github.com/svg/svgo).
9
10
 
10
- > [!IMPORTANT]
11
- > This is the development branch for IPX v2. Check out [ipx/v1](https://github.com/unjs/ipx/tree/v1) for latest stable docs.
11
+ Used by [Nuxt Image](https://image.nuxt.com/) and [Netlify](https://www.npmjs.com/package/@netlify/ipx) and open to everyone!
12
12
 
13
13
  ## Using CLI
14
14
 
@@ -17,13 +17,13 @@ You can use `ipx` command to start server.
17
17
  Using `npx`:
18
18
 
19
19
  ```bash
20
- npx ipx@latest serve --dir ./
20
+ npx ipx serve --dir ./
21
21
  ```
22
22
 
23
23
  Usin `bun`
24
24
 
25
25
  ```bash
26
- bun x ipx@latest serve --dir ./
26
+ bun x npx ipx serve --dir ./
27
27
  ```
28
28
 
29
29
  The default serve directory is the current working directory.
@@ -33,28 +33,32 @@ The default serve directory is the current working directory.
33
33
  You can use IPX as a middleware or directly use IPX interface.
34
34
 
35
35
  ```ts
36
- import {
37
- createIPX,
38
- createIPXMiddleware,
39
- ipxFSStorage,
40
- ipxHttpStorage,
41
- } from "./src";
36
+ import { createIPX, ipxFSStorage, ipxHttpStorage } from "ipx";
42
37
 
43
38
  const ipx = createIPX({
44
39
  storage: ipxFSStorage({ dir: "./public" }),
45
40
  httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
46
41
  });
47
-
48
- const ipxMiddleware = createIPXMiddleware(ipx);
49
42
  ```
50
43
 
51
44
  **Example**: Using with [unjs/h3](https://github.com/unjs/h3):
52
45
 
53
46
  ```js
54
- import { createIPX, createIPXMiddleware } from "ipx";
55
47
  import { listen } from "listhen";
48
+ import { createApp, toNodeListener } from "h3";
49
+ import {
50
+ createIPX,
51
+ ipxFSStorage,
52
+ ipxHttpStorage,
53
+ createIPXH3Handler,
54
+ } from "ipx";
56
55
 
57
- const app = createApp().use("/", fromNodeMiddleware(ipxMiddleware));
56
+ const ipx = createIPX({
57
+ storage: ipxFSStorage({ dir: "./public" }),
58
+ httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
59
+ });
60
+
61
+ const app = createApp().use("/", createIPXH3Handler(ipx));
58
62
 
59
63
  listen(toNodeListener(app));
60
64
  ```
@@ -64,8 +68,19 @@ listen(toNodeListener(app));
64
68
  ```js
65
69
  import { listen } from "listhen";
66
70
  import express from "express";
71
+ import {
72
+ createIPX,
73
+ ipxFSStorage,
74
+ ipxHttpStorage,
75
+ createIPXNodeServer,
76
+ } from "ipx";
77
+
78
+ const ipx = createIPX({
79
+ storage: ipxFSStorage({ dir: "./public" }),
80
+ httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
81
+ });
67
82
 
68
- const app = express().use("/", ipxMiddleware);
83
+ const app = express().use("/", createIPXNodeServer(ipx));
69
84
 
70
85
  listen(app);
71
86
  ```
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const xss = {
4
+ name: "removeXSS",
5
+ fn() {
6
+ return {
7
+ element: {
8
+ enter: (node, parentNode) => {
9
+ if (node.name === "script") {
10
+ parentNode.children = parentNode.children.filter(
11
+ (child) => child !== node
12
+ );
13
+ return;
14
+ }
15
+ for (const event of ALL_EVENTS) {
16
+ for (const [name] of Object.entries(node.attributes)) {
17
+ if (name === event) {
18
+ delete node.attributes[name];
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ };
25
+ }
26
+ };
27
+ const ALL_EVENTS = [
28
+ "onbegin",
29
+ "onend",
30
+ "onrepeat",
31
+ "onabort",
32
+ "onerror",
33
+ "onresize",
34
+ "onscroll",
35
+ "onunload",
36
+ "onbegin",
37
+ "onend",
38
+ "onrepeat",
39
+ "oncancel",
40
+ "oncanplay",
41
+ "oncanplaythrough",
42
+ "onchange",
43
+ "onclick",
44
+ "onclose",
45
+ "oncuechange",
46
+ "ondblclick",
47
+ "ondrag",
48
+ "ondragend",
49
+ "ondragenter",
50
+ "ondragleave",
51
+ "ondragover",
52
+ "ondragstart",
53
+ "ondrop",
54
+ "ondurationchange",
55
+ "onemptied",
56
+ "onended",
57
+ "onerror",
58
+ "onfocus",
59
+ "oninput",
60
+ "oninvalid",
61
+ "onkeydown",
62
+ "onkeypress",
63
+ "onkeyup",
64
+ "onload",
65
+ "onloadeddata",
66
+ "onloadedmetadata",
67
+ "onloadstart",
68
+ "onmousedown",
69
+ "onmouseenter",
70
+ "onmouseleave",
71
+ "onmousemove",
72
+ "onmouseout",
73
+ "onmouseover",
74
+ "onmouseup",
75
+ "onmousewheel",
76
+ "onpause",
77
+ "onplay",
78
+ "onplaying",
79
+ "onprogress",
80
+ "onratechange",
81
+ "onreset",
82
+ "onresize",
83
+ "onscroll",
84
+ "onseeked",
85
+ "onseeking",
86
+ "onselect",
87
+ "onshow",
88
+ "onstalled",
89
+ "onsubmit",
90
+ "onsuspend",
91
+ "ontimeupdate",
92
+ "ontoggle",
93
+ "onvolumechange",
94
+ "onwaiting",
95
+ "oncopy",
96
+ "oncut",
97
+ "onpaste",
98
+ "onactivate",
99
+ "onfocusin",
100
+ "onfocusout"
101
+ ];
102
+
103
+ exports.xss = xss;
@@ -0,0 +1,101 @@
1
+ const xss = {
2
+ name: "removeXSS",
3
+ fn() {
4
+ return {
5
+ element: {
6
+ enter: (node, parentNode) => {
7
+ if (node.name === "script") {
8
+ parentNode.children = parentNode.children.filter(
9
+ (child) => child !== node
10
+ );
11
+ return;
12
+ }
13
+ for (const event of ALL_EVENTS) {
14
+ for (const [name] of Object.entries(node.attributes)) {
15
+ if (name === event) {
16
+ delete node.attributes[name];
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }
22
+ };
23
+ }
24
+ };
25
+ const ALL_EVENTS = [
26
+ "onbegin",
27
+ "onend",
28
+ "onrepeat",
29
+ "onabort",
30
+ "onerror",
31
+ "onresize",
32
+ "onscroll",
33
+ "onunload",
34
+ "onbegin",
35
+ "onend",
36
+ "onrepeat",
37
+ "oncancel",
38
+ "oncanplay",
39
+ "oncanplaythrough",
40
+ "onchange",
41
+ "onclick",
42
+ "onclose",
43
+ "oncuechange",
44
+ "ondblclick",
45
+ "ondrag",
46
+ "ondragend",
47
+ "ondragenter",
48
+ "ondragleave",
49
+ "ondragover",
50
+ "ondragstart",
51
+ "ondrop",
52
+ "ondurationchange",
53
+ "onemptied",
54
+ "onended",
55
+ "onerror",
56
+ "onfocus",
57
+ "oninput",
58
+ "oninvalid",
59
+ "onkeydown",
60
+ "onkeypress",
61
+ "onkeyup",
62
+ "onload",
63
+ "onloadeddata",
64
+ "onloadedmetadata",
65
+ "onloadstart",
66
+ "onmousedown",
67
+ "onmouseenter",
68
+ "onmouseleave",
69
+ "onmousemove",
70
+ "onmouseout",
71
+ "onmouseover",
72
+ "onmouseup",
73
+ "onmousewheel",
74
+ "onpause",
75
+ "onplay",
76
+ "onplaying",
77
+ "onprogress",
78
+ "onratechange",
79
+ "onreset",
80
+ "onresize",
81
+ "onscroll",
82
+ "onseeked",
83
+ "onseeking",
84
+ "onselect",
85
+ "onshow",
86
+ "onstalled",
87
+ "onsubmit",
88
+ "onsuspend",
89
+ "ontimeupdate",
90
+ "ontoggle",
91
+ "onvolumechange",
92
+ "onwaiting",
93
+ "oncopy",
94
+ "oncut",
95
+ "onpaste",
96
+ "onactivate",
97
+ "onfocusin",
98
+ "onfocusout"
99
+ ];
100
+
101
+ export { xss };
package/dist/cli.cjs CHANGED
@@ -3,19 +3,19 @@
3
3
  const listhen = require('listhen');
4
4
  const citty = require('citty');
5
5
  const cli = require('listhen/cli');
6
- const nodeFs = require('./shared/ipx.0fc4e4c7.cjs');
6
+ const nodeFs = require('./shared/ipx.ebaf2d0c.cjs');
7
7
  require('defu');
8
- require('image-meta');
9
8
  require('ufo');
10
9
  require('h3');
10
+ require('image-meta');
11
11
  require('destr');
12
12
  require('@fastify/accept-negotiator');
13
13
  require('etag');
14
- require('node-fetch-native');
14
+ require('ofetch');
15
15
  require('pathe');
16
16
 
17
17
  const name = "ipx";
18
- const version = "2.0.0-0";
18
+ const version = "2.0.0";
19
19
  const description = "High performance, secure and easy-to-use image optimizer.";
20
20
 
21
21
  const serve = citty.defineCommand({
package/dist/cli.mjs CHANGED
@@ -1,19 +1,19 @@
1
1
  import { listen } from 'listhen';
2
2
  import { defineCommand, runMain } from 'citty';
3
3
  import { getArgs, parseArgs } from 'listhen/cli';
4
- import { c as createIPX, g as ipxFSStorage, i as ipxHttpStorage, e as createIPXNodeServer } from './shared/ipx.42c0c175.mjs';
4
+ import { c as createIPX, g as ipxFSStorage, i as ipxHttpStorage, e as createIPXNodeServer } from './shared/ipx.4d79c6c9.mjs';
5
5
  import 'defu';
6
- import 'image-meta';
7
6
  import 'ufo';
8
7
  import 'h3';
8
+ import 'image-meta';
9
9
  import 'destr';
10
10
  import '@fastify/accept-negotiator';
11
11
  import 'etag';
12
- import 'node-fetch-native';
12
+ import 'ofetch';
13
13
  import 'pathe';
14
14
 
15
15
  const name = "ipx";
16
- const version = "2.0.0-0";
16
+ const version = "2.0.0";
17
17
  const description = "High performance, secure and easy-to-use image optimizer.";
18
18
 
19
19
  const serve = defineCommand({
package/dist/index.cjs CHANGED
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
- const nodeFs = require('./shared/ipx.0fc4e4c7.cjs');
3
+ const nodeFs = require('./shared/ipx.ebaf2d0c.cjs');
4
4
  require('defu');
5
- require('image-meta');
6
5
  require('ufo');
7
6
  require('h3');
7
+ require('image-meta');
8
8
  require('destr');
9
9
  require('@fastify/accept-negotiator');
10
10
  require('etag');
11
- require('node-fetch-native');
11
+ require('ofetch');
12
12
  require('pathe');
13
13
 
14
14
  function unstorageToIPXStorage(storage, prefix) {
package/dist/index.d.cts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { Color, KernelEnum, Sharp, SharpOptions } from 'sharp';
2
+ import { ImageMeta } from 'image-meta';
3
+ import { Config } from 'svgo';
2
4
  import * as h3 from 'h3';
3
5
  import { Storage, Driver } from 'unstorage';
4
6
 
@@ -27,12 +29,6 @@ interface IPXStorage {
27
29
  getMeta: (id: string, opts?: IPXStorageOptions) => MaybePromise<IPXStorageMeta | undefined>;
28
30
  getData: (id: string, opts?: IPXStorageOptions) => MaybePromise<ArrayBuffer | undefined>;
29
31
  }
30
- interface ImageMeta {
31
- width: number;
32
- height: number;
33
- type: string;
34
- mimeType: string;
35
- }
36
32
 
37
33
  declare const quality: Handler;
38
34
  declare const fit: Handler;
@@ -114,9 +110,9 @@ type IPXSourceMeta = {
114
110
  type IPX = (id: string, modifiers?: Partial<Record<HandlerName | "f" | "format" | "a" | "animated", string>>, requestOptions?: any) => {
115
111
  getSourceMeta: () => Promise<IPXSourceMeta>;
116
112
  process: () => Promise<{
117
- data: Buffer;
118
- meta: ImageMeta;
119
- format: string;
113
+ data: Buffer | string;
114
+ meta?: ImageMeta;
115
+ format?: string;
120
116
  }>;
121
117
  };
122
118
  type IPXOptions = {
@@ -125,10 +121,15 @@ type IPXOptions = {
125
121
  sharpOptions?: SharpOptions;
126
122
  storage: IPXStorage;
127
123
  httpStorage?: IPXStorage;
124
+ svgo?: false | Config;
128
125
  };
129
126
  declare function createIPX(userOptions: IPXOptions): IPX;
130
127
 
131
- declare function createIPXH3Handler(ipx: IPX): h3.EventHandler<h3.EventHandlerRequest, Promise<Buffer | null>>;
128
+ declare function createIPXH3Handler(ipx: IPX): h3.EventHandler<h3.EventHandlerRequest, Promise<string | void | Buffer | {
129
+ error: {
130
+ message: string;
131
+ };
132
+ }>>;
132
133
  declare function createIPXH3App(ipx: IPX): h3.App;
133
134
  declare function createIPXWebServer(ipx: IPX): h3.WebHandler;
134
135
  declare function createIPXNodeServer(ipx: IPX): h3.NodeListener;
@@ -139,6 +140,7 @@ type HTTPStorageOptions = {
139
140
  maxAge?: number;
140
141
  domains?: string | string[];
141
142
  allowAllDomains?: boolean;
143
+ ignoreCacheControl?: boolean;
142
144
  };
143
145
  declare function ipxHttpStorage(_options?: HTTPStorageOptions): IPXStorage;
144
146
 
@@ -150,4 +152,4 @@ declare function ipxFSStorage(_options?: NodeFSSOptions): IPXStorage;
150
152
 
151
153
  declare function unstorageToIPXStorage(storage: Storage | Driver, prefix: string): IPXStorage;
152
154
 
153
- export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type ImageMeta, type NodeFSSOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
155
+ export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type NodeFSSOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { Color, KernelEnum, Sharp, SharpOptions } from 'sharp';
2
+ import { ImageMeta } from 'image-meta';
3
+ import { Config } from 'svgo';
2
4
  import * as h3 from 'h3';
3
5
  import { Storage, Driver } from 'unstorage';
4
6
 
@@ -27,12 +29,6 @@ interface IPXStorage {
27
29
  getMeta: (id: string, opts?: IPXStorageOptions) => MaybePromise<IPXStorageMeta | undefined>;
28
30
  getData: (id: string, opts?: IPXStorageOptions) => MaybePromise<ArrayBuffer | undefined>;
29
31
  }
30
- interface ImageMeta {
31
- width: number;
32
- height: number;
33
- type: string;
34
- mimeType: string;
35
- }
36
32
 
37
33
  declare const quality: Handler;
38
34
  declare const fit: Handler;
@@ -114,9 +110,9 @@ type IPXSourceMeta = {
114
110
  type IPX = (id: string, modifiers?: Partial<Record<HandlerName | "f" | "format" | "a" | "animated", string>>, requestOptions?: any) => {
115
111
  getSourceMeta: () => Promise<IPXSourceMeta>;
116
112
  process: () => Promise<{
117
- data: Buffer;
118
- meta: ImageMeta;
119
- format: string;
113
+ data: Buffer | string;
114
+ meta?: ImageMeta;
115
+ format?: string;
120
116
  }>;
121
117
  };
122
118
  type IPXOptions = {
@@ -125,10 +121,15 @@ type IPXOptions = {
125
121
  sharpOptions?: SharpOptions;
126
122
  storage: IPXStorage;
127
123
  httpStorage?: IPXStorage;
124
+ svgo?: false | Config;
128
125
  };
129
126
  declare function createIPX(userOptions: IPXOptions): IPX;
130
127
 
131
- declare function createIPXH3Handler(ipx: IPX): h3.EventHandler<h3.EventHandlerRequest, Promise<Buffer | null>>;
128
+ declare function createIPXH3Handler(ipx: IPX): h3.EventHandler<h3.EventHandlerRequest, Promise<string | void | Buffer | {
129
+ error: {
130
+ message: string;
131
+ };
132
+ }>>;
132
133
  declare function createIPXH3App(ipx: IPX): h3.App;
133
134
  declare function createIPXWebServer(ipx: IPX): h3.WebHandler;
134
135
  declare function createIPXNodeServer(ipx: IPX): h3.NodeListener;
@@ -139,6 +140,7 @@ type HTTPStorageOptions = {
139
140
  maxAge?: number;
140
141
  domains?: string | string[];
141
142
  allowAllDomains?: boolean;
143
+ ignoreCacheControl?: boolean;
142
144
  };
143
145
  declare function ipxHttpStorage(_options?: HTTPStorageOptions): IPXStorage;
144
146
 
@@ -150,4 +152,4 @@ declare function ipxFSStorage(_options?: NodeFSSOptions): IPXStorage;
150
152
 
151
153
  declare function unstorageToIPXStorage(storage: Storage | Driver, prefix: string): IPXStorage;
152
154
 
153
- export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type ImageMeta, type NodeFSSOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
155
+ export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type NodeFSSOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { Color, KernelEnum, Sharp, SharpOptions } from 'sharp';
2
+ import { ImageMeta } from 'image-meta';
3
+ import { Config } from 'svgo';
2
4
  import * as h3 from 'h3';
3
5
  import { Storage, Driver } from 'unstorage';
4
6
 
@@ -27,12 +29,6 @@ interface IPXStorage {
27
29
  getMeta: (id: string, opts?: IPXStorageOptions) => MaybePromise<IPXStorageMeta | undefined>;
28
30
  getData: (id: string, opts?: IPXStorageOptions) => MaybePromise<ArrayBuffer | undefined>;
29
31
  }
30
- interface ImageMeta {
31
- width: number;
32
- height: number;
33
- type: string;
34
- mimeType: string;
35
- }
36
32
 
37
33
  declare const quality: Handler;
38
34
  declare const fit: Handler;
@@ -114,9 +110,9 @@ type IPXSourceMeta = {
114
110
  type IPX = (id: string, modifiers?: Partial<Record<HandlerName | "f" | "format" | "a" | "animated", string>>, requestOptions?: any) => {
115
111
  getSourceMeta: () => Promise<IPXSourceMeta>;
116
112
  process: () => Promise<{
117
- data: Buffer;
118
- meta: ImageMeta;
119
- format: string;
113
+ data: Buffer | string;
114
+ meta?: ImageMeta;
115
+ format?: string;
120
116
  }>;
121
117
  };
122
118
  type IPXOptions = {
@@ -125,10 +121,15 @@ type IPXOptions = {
125
121
  sharpOptions?: SharpOptions;
126
122
  storage: IPXStorage;
127
123
  httpStorage?: IPXStorage;
124
+ svgo?: false | Config;
128
125
  };
129
126
  declare function createIPX(userOptions: IPXOptions): IPX;
130
127
 
131
- declare function createIPXH3Handler(ipx: IPX): h3.EventHandler<h3.EventHandlerRequest, Promise<Buffer | null>>;
128
+ declare function createIPXH3Handler(ipx: IPX): h3.EventHandler<h3.EventHandlerRequest, Promise<string | void | Buffer | {
129
+ error: {
130
+ message: string;
131
+ };
132
+ }>>;
132
133
  declare function createIPXH3App(ipx: IPX): h3.App;
133
134
  declare function createIPXWebServer(ipx: IPX): h3.WebHandler;
134
135
  declare function createIPXNodeServer(ipx: IPX): h3.NodeListener;
@@ -139,6 +140,7 @@ type HTTPStorageOptions = {
139
140
  maxAge?: number;
140
141
  domains?: string | string[];
141
142
  allowAllDomains?: boolean;
143
+ ignoreCacheControl?: boolean;
142
144
  };
143
145
  declare function ipxHttpStorage(_options?: HTTPStorageOptions): IPXStorage;
144
146
 
@@ -150,4 +152,4 @@ declare function ipxFSStorage(_options?: NodeFSSOptions): IPXStorage;
150
152
 
151
153
  declare function unstorageToIPXStorage(storage: Storage | Driver, prefix: string): IPXStorage;
152
154
 
153
- export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type ImageMeta, type NodeFSSOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
155
+ export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type NodeFSSOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
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';
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.4d79c6c9.mjs';
2
2
  import 'defu';
3
- import 'image-meta';
4
3
  import 'ufo';
5
4
  import 'h3';
5
+ import 'image-meta';
6
6
  import 'destr';
7
7
  import '@fastify/accept-negotiator';
8
8
  import 'etag';
9
- import 'node-fetch-native';
9
+ import 'ofetch';
10
10
  import 'pathe';
11
11
 
12
12
  function unstorageToIPXStorage(storage, prefix) {
@@ -1,11 +1,11 @@
1
1
  import { defu } from 'defu';
2
- import { imageMeta } from 'image-meta';
3
2
  import { withLeadingSlash, hasProtocol, joinURL, decode } from 'ufo';
4
- import { createError, defineEventHandler, getRequestHeader, setResponseHeader, setResponseStatus, createApp, toWebHandler, toNodeListener, toPlainHandler } from 'h3';
3
+ import { createError, defineEventHandler, setResponseStatus, createApp, toWebHandler, toNodeListener, toPlainHandler, getRequestHeader, appendResponseHeader, send, getResponseHeader, setResponseHeader } from 'h3';
4
+ import { imageMeta } from 'image-meta';
5
5
  import destr from 'destr';
6
6
  import { negotiate } from '@fastify/accept-negotiator';
7
7
  import getEtag from 'etag';
8
- import { fetch } from 'node-fetch-native';
8
+ import { ofetch } from 'ofetch';
9
9
  import { resolve, join, parse } from 'pathe';
10
10
 
11
11
  const Handlers = {
@@ -62,11 +62,11 @@ function applyHandler(context, pipe, handler, argumentsString) {
62
62
  function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
63
63
  const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
64
64
  let { width, height } = desiredDimensions;
65
- if (width > sourceDimensions.width) {
65
+ if (sourceDimensions.width && width > sourceDimensions.width) {
66
66
  width = sourceDimensions.width;
67
67
  height = Math.round(sourceDimensions.width / desiredAspectRatio);
68
68
  }
69
- if (height > sourceDimensions.height) {
69
+ if (sourceDimensions.height && height > sourceDimensions.height) {
70
70
  height = sourceDimensions.height;
71
71
  width = Math.round(sourceDimensions.height * desiredAspectRatio);
72
72
  }
@@ -317,7 +317,7 @@ const SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
317
317
  function createIPX(userOptions) {
318
318
  const options = defu(userOptions, {
319
319
  alias: getEnv("IPX_ALIAS") || {},
320
- maxAge: getEnv("IPX_MAX_AGE") || 300,
320
+ maxAge: getEnv("IPX_MAX_AGE") ?? 60,
321
321
  sharpOptions: {}
322
322
  });
323
323
  options.alias = Object.fromEntries(
@@ -331,10 +331,16 @@ function createIPX(userOptions) {
331
331
  (r) => r.default || r
332
332
  );
333
333
  });
334
+ const getSVGO = cachedPromise(async () => {
335
+ const { optimize } = await import('svgo');
336
+ const { xss } = await import('../chunks/svgo-xss.mjs');
337
+ return { optimize, xss };
338
+ });
334
339
  return function ipx(id, modifiers = {}, opts = {}) {
335
340
  if (!id) {
336
341
  throw createError({
337
342
  statusCode: 400,
343
+ statusText: `IPX_MISSING_ID`,
338
344
  message: `Resource id is missing`
339
345
  });
340
346
  }
@@ -348,6 +354,7 @@ function createIPX(userOptions) {
348
354
  if (!storage) {
349
355
  throw createError({
350
356
  statusCode: 500,
357
+ statusText: `IPX_NO_STORAGE`,
351
358
  message: "No storage configured!"
352
359
  });
353
360
  }
@@ -356,11 +363,13 @@ function createIPX(userOptions) {
356
363
  if (!sourceMeta) {
357
364
  throw createError({
358
365
  statusCode: 404,
366
+ statusText: `IPX_RESOURCE_NOT_FOUND`,
359
367
  message: `Resource not found: ${id}`
360
368
  });
361
369
  }
370
+ const _maxAge = sourceMeta.maxAge ?? options.maxAge;
362
371
  return {
363
- maxAge: typeof sourceMeta.maxAge === "string" ? Number.parseInt(sourceMeta.maxAge) : sourceMeta.maxAge,
372
+ maxAge: typeof _maxAge === "string" ? Number.parseInt(_maxAge) : _maxAge,
364
373
  mtime: sourceMeta.mtime ? new Date(sourceMeta.mtime) : void 0
365
374
  };
366
375
  });
@@ -369,6 +378,7 @@ function createIPX(userOptions) {
369
378
  if (!sourceData) {
370
379
  throw createError({
371
380
  statusCode: 404,
381
+ statusText: `IPX_RESOURCE_NOT_FOUND`,
372
382
  message: `Resource not found: ${id}`
373
383
  });
374
384
  }
@@ -376,18 +386,40 @@ function createIPX(userOptions) {
376
386
  });
377
387
  const process = cachedPromise(async () => {
378
388
  const sourceData = await getSourceData();
379
- const imageMeta$1 = imageMeta(sourceData);
389
+ let imageMeta$1;
390
+ try {
391
+ imageMeta$1 = imageMeta(sourceData);
392
+ } catch {
393
+ throw createError({
394
+ statusCode: 400,
395
+ statusText: `IPX_INVALID_IMAGE`,
396
+ message: `Cannot parse image metadata: ${id}`
397
+ });
398
+ }
380
399
  let mFormat = modifiers.f || modifiers.format;
381
400
  if (mFormat === "jpg") {
382
401
  mFormat = "jpeg";
383
402
  }
384
- const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type) ? imageMeta$1.type : "jpeg";
403
+ const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type || "") ? imageMeta$1.type : "jpeg";
385
404
  if (imageMeta$1.type === "svg" && !mFormat) {
386
- return {
387
- data: sourceData,
388
- format: "svg+xml",
389
- meta: imageMeta$1
390
- };
405
+ if (options.svgo === false) {
406
+ return {
407
+ data: sourceData,
408
+ format: "svg+xml",
409
+ meta: imageMeta$1
410
+ };
411
+ } else {
412
+ const { optimize, xss } = await getSVGO();
413
+ const svg = optimize(sourceData.toString("utf8"), {
414
+ ...options.svgo,
415
+ plugins: [xss, ...options.svgo?.plugins || []]
416
+ }).data;
417
+ return {
418
+ data: svg,
419
+ format: "svg+xml",
420
+ meta: imageMeta$1
421
+ };
422
+ }
391
423
  }
392
424
  const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
393
425
  const Sharp = await getSharp();
@@ -409,7 +441,7 @@ function createIPX(userOptions) {
409
441
  for (const h of handlers) {
410
442
  sharp = applyHandler(handlerContext, sharp, h.handler, h.args) || sharp;
411
443
  }
412
- if (SUPPORTED_FORMATS.has(format)) {
444
+ if (SUPPORTED_FORMATS.has(format || "")) {
413
445
  sharp = sharp.toFormat(format, {
414
446
  quality: handlerContext.quality,
415
447
  progressive: format === "jpeg"
@@ -432,7 +464,7 @@ function createIPX(userOptions) {
432
464
  const MODIFIER_SEP = /[&,]/g;
433
465
  const MODIFIER_VAL_SEP = /[:=_]/;
434
466
  function createIPXH3Handler(ipx) {
435
- return defineEventHandler(async (event) => {
467
+ const _handler = async (event) => {
436
468
  const [modifiersString = "", ...idSegments] = event.path.slice(
437
469
  1
438
470
  /* leading slash */
@@ -441,12 +473,14 @@ function createIPXH3Handler(ipx) {
441
473
  if (!modifiersString) {
442
474
  throw createError({
443
475
  statusCode: 400,
476
+ statusText: `IPX_MISSING_MODIFIERS`,
444
477
  message: `Modifiers are missing: ${id}`
445
478
  });
446
479
  }
447
480
  if (!id || id === "/") {
448
481
  throw createError({
449
482
  statusCode: 400,
483
+ statusText: `IPX_MISSING_ID`,
450
484
  message: `Resource id is missing: ${event.path}`
451
485
  });
452
486
  }
@@ -468,37 +502,59 @@ function createIPXH3Handler(ipx) {
468
502
  delete modifiers.format;
469
503
  if (autoFormat) {
470
504
  modifiers.format = autoFormat;
471
- setResponseHeader(event, "vary", "Accept");
505
+ appendResponseHeader(event, "vary", "Accept");
472
506
  }
473
507
  }
474
508
  const img = ipx(id, modifiers);
475
509
  const sourceMeta = await img.getSourceMeta();
476
- if (sourceMeta.mtime) {
477
- if (getRequestHeader(event, "if-modified-since") && new Date(getRequestHeader(event, "if-modified-since") || "") >= sourceMeta.mtime) {
478
- setResponseStatus(event, 304);
479
- return null;
480
- }
481
- setResponseHeader(event, "last-modified", sourceMeta.mtime.toUTCString());
482
- }
510
+ sendResponseHeaderIfNotSet(
511
+ event,
512
+ "content-security-policy",
513
+ "default-src 'none'"
514
+ );
483
515
  if (typeof sourceMeta.maxAge === "number") {
484
- setResponseHeader(
516
+ sendResponseHeaderIfNotSet(
485
517
  event,
486
518
  "cache-control",
487
519
  `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`
488
520
  );
489
521
  }
522
+ if (sourceMeta.mtime) {
523
+ sendResponseHeaderIfNotSet(
524
+ event,
525
+ "last-modified",
526
+ sourceMeta.mtime.toUTCString()
527
+ );
528
+ const _ifModifiedSince = getRequestHeader(event, "if-modified-since");
529
+ if (_ifModifiedSince && new Date(_ifModifiedSince) >= sourceMeta.mtime) {
530
+ setResponseStatus(event, 304);
531
+ return send(event);
532
+ }
533
+ }
490
534
  const { data, format } = await img.process();
491
535
  const etag = getEtag(data);
492
- setResponseHeader(event, "etag", etag);
536
+ sendResponseHeaderIfNotSet(event, "etag", etag);
493
537
  if (etag && getRequestHeader(event, "if-none-match") === etag) {
494
538
  setResponseStatus(event, 304);
495
- return null;
539
+ return send(event);
496
540
  }
497
541
  if (format) {
498
- setResponseHeader(event, "content-type", `image/${format}`);
542
+ sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
499
543
  }
500
- setResponseHeader(event, "content-security-policy", "default-src 'none'");
501
544
  return data;
545
+ };
546
+ return defineEventHandler(async (event) => {
547
+ try {
548
+ return await _handler(event);
549
+ } catch (_error) {
550
+ const error = createError(_error);
551
+ setResponseStatus(event, error.statusCode, error.statusMessage);
552
+ return {
553
+ error: {
554
+ message: `[${error.statusCode}] [${error.statusMessage || "IPX_ERROR"}] ${error.message}`
555
+ }
556
+ };
557
+ }
502
558
  });
503
559
  }
504
560
  function createIPXH3App(ipx) {
@@ -515,6 +571,11 @@ function createIPXNodeServer(ipx) {
515
571
  function createIPXPlainServer(ipx) {
516
572
  return toPlainHandler(createIPXH3App(ipx));
517
573
  }
574
+ function sendResponseHeaderIfNotSet(event, name, value) {
575
+ if (!getResponseHeader(event, name)) {
576
+ setResponseHeader(event, name, value);
577
+ }
578
+ }
518
579
  function autoDetectFormat(acceptHeader, animated) {
519
580
  if (animated) {
520
581
  const acceptMime2 = negotiate(acceptHeader, ["image/webp", "image/gif"]);
@@ -557,30 +618,28 @@ function ipxHttpStorage(_options = {}) {
557
618
  if (!url.hostname) {
558
619
  throw createError({
559
620
  statusCode: 403,
621
+ statusText: `IPX_MISSING_HOSTNAME`,
560
622
  message: `Hostname is missing: ${id}`
561
623
  });
562
624
  }
563
625
  if (!allowAllDomains && !domains.has(url.hostname)) {
564
626
  throw createError({
565
627
  statusCode: 403,
628
+ statusText: `IPX_FORBIDDEN_HOST`,
566
629
  message: `Forbidden host: ${url.hostname}`
567
630
  });
568
631
  }
569
632
  return url.toString();
570
633
  }
571
634
  function parseResponse(response) {
572
- if (!response.ok) {
573
- throw createError({
574
- statusCode: response.status || 500,
575
- message: `Fetch error: ${response.statusText}`
576
- });
577
- }
578
635
  let maxAge = defaultMaxAge;
579
- const _cacheControl = response.headers.get("cache-control");
580
- if (_cacheControl) {
581
- const m = _cacheControl.match(/max-age=(\d+)/);
582
- if (m && m[1]) {
583
- maxAge = Number.parseInt(m[1]);
636
+ if (_options.ignoreCacheControl) {
637
+ const _cacheControl = response.headers.get("cache-control");
638
+ if (_cacheControl) {
639
+ const m = _cacheControl.match(/max-age=(\d+)/);
640
+ if (m && m[1]) {
641
+ maxAge = Number.parseInt(m[1]);
642
+ }
584
643
  }
585
644
  }
586
645
  let mtime;
@@ -595,7 +654,10 @@ function ipxHttpStorage(_options = {}) {
595
654
  async getMeta(id) {
596
655
  const url = validateId(id);
597
656
  try {
598
- const response = await fetch(url, { ...fetchOptions, method: "HEAD" });
657
+ const response = await ofetch.raw(url, {
658
+ ...fetchOptions,
659
+ method: "HEAD"
660
+ });
599
661
  const { maxAge, mtime } = parseResponse(response);
600
662
  return { mtime, maxAge };
601
663
  } catch {
@@ -604,8 +666,12 @@ function ipxHttpStorage(_options = {}) {
604
666
  },
605
667
  async getData(id) {
606
668
  const url = validateId(id);
607
- const response = await fetch(url, { ...fetchOptions, method: "GET" });
608
- return response.arrayBuffer();
669
+ const response = await ofetch(url, {
670
+ ...fetchOptions,
671
+ method: "GET",
672
+ responseType: "arrayBuffer"
673
+ });
674
+ return response;
609
675
  }
610
676
  };
611
677
  }
@@ -618,6 +684,7 @@ function ipxFSStorage(_options = {}) {
618
684
  if (!isValidPath(resolved) || !resolved.startsWith(rootDir)) {
619
685
  throw createError({
620
686
  statusCode: 403,
687
+ statusText: `IPX_FORBIDDEN_PATH`,
621
688
  message: `Forbidden path: ${id}`
622
689
  });
623
690
  }
@@ -635,16 +702,19 @@ function ipxFSStorage(_options = {}) {
635
702
  } catch (error) {
636
703
  throw error.code === "ENOENT" ? createError({
637
704
  statusCode: 404,
638
- message: `File not found: ${fsPath}`
705
+ statusText: `IPX_FILE_NOT_FOUND`,
706
+ message: `File not found: ${id}`
639
707
  }) : createError({
640
708
  statusCode: 403,
641
- message: `File access error: (${error.code}) ${fsPath}`
709
+ statusText: `IPX_FORBIDDEN_FILE`,
710
+ message: `File access forbidden: (${error.code}) ${id}`
642
711
  });
643
712
  }
644
713
  if (!stats.isFile()) {
645
714
  throw createError({
646
715
  statusCode: 400,
647
- message: `Path should be a file: ${fsPath}`
716
+ statusText: `IPX_INVALID_FILE`,
717
+ message: `Path should be a file: ${id}`
648
718
  });
649
719
  }
650
720
  return {
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  const defu = require('defu');
4
- const imageMeta = require('image-meta');
5
4
  const ufo = require('ufo');
6
5
  const h3 = require('h3');
6
+ const imageMeta = require('image-meta');
7
7
  const destr = require('destr');
8
8
  const acceptNegotiator = require('@fastify/accept-negotiator');
9
9
  const getEtag = require('etag');
10
- const nodeFetchNative = require('node-fetch-native');
10
+ const ofetch = require('ofetch');
11
11
  const pathe = require('pathe');
12
12
 
13
13
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
@@ -69,11 +69,11 @@ function applyHandler(context, pipe, handler, argumentsString) {
69
69
  function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
70
70
  const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
71
71
  let { width, height } = desiredDimensions;
72
- if (width > sourceDimensions.width) {
72
+ if (sourceDimensions.width && width > sourceDimensions.width) {
73
73
  width = sourceDimensions.width;
74
74
  height = Math.round(sourceDimensions.width / desiredAspectRatio);
75
75
  }
76
- if (height > sourceDimensions.height) {
76
+ if (sourceDimensions.height && height > sourceDimensions.height) {
77
77
  height = sourceDimensions.height;
78
78
  width = Math.round(sourceDimensions.height * desiredAspectRatio);
79
79
  }
@@ -324,7 +324,7 @@ const SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
324
324
  function createIPX(userOptions) {
325
325
  const options = defu.defu(userOptions, {
326
326
  alias: getEnv("IPX_ALIAS") || {},
327
- maxAge: getEnv("IPX_MAX_AGE") || 300,
327
+ maxAge: getEnv("IPX_MAX_AGE") ?? 60,
328
328
  sharpOptions: {}
329
329
  });
330
330
  options.alias = Object.fromEntries(
@@ -338,10 +338,16 @@ function createIPX(userOptions) {
338
338
  (r) => r.default || r
339
339
  );
340
340
  });
341
+ const getSVGO = cachedPromise(async () => {
342
+ const { optimize } = await import('svgo');
343
+ const { xss } = await import('../chunks/svgo-xss.cjs');
344
+ return { optimize, xss };
345
+ });
341
346
  return function ipx(id, modifiers = {}, opts = {}) {
342
347
  if (!id) {
343
348
  throw h3.createError({
344
349
  statusCode: 400,
350
+ statusText: `IPX_MISSING_ID`,
345
351
  message: `Resource id is missing`
346
352
  });
347
353
  }
@@ -355,6 +361,7 @@ function createIPX(userOptions) {
355
361
  if (!storage) {
356
362
  throw h3.createError({
357
363
  statusCode: 500,
364
+ statusText: `IPX_NO_STORAGE`,
358
365
  message: "No storage configured!"
359
366
  });
360
367
  }
@@ -363,11 +370,13 @@ function createIPX(userOptions) {
363
370
  if (!sourceMeta) {
364
371
  throw h3.createError({
365
372
  statusCode: 404,
373
+ statusText: `IPX_RESOURCE_NOT_FOUND`,
366
374
  message: `Resource not found: ${id}`
367
375
  });
368
376
  }
377
+ const _maxAge = sourceMeta.maxAge ?? options.maxAge;
369
378
  return {
370
- maxAge: typeof sourceMeta.maxAge === "string" ? Number.parseInt(sourceMeta.maxAge) : sourceMeta.maxAge,
379
+ maxAge: typeof _maxAge === "string" ? Number.parseInt(_maxAge) : _maxAge,
371
380
  mtime: sourceMeta.mtime ? new Date(sourceMeta.mtime) : void 0
372
381
  };
373
382
  });
@@ -376,6 +385,7 @@ function createIPX(userOptions) {
376
385
  if (!sourceData) {
377
386
  throw h3.createError({
378
387
  statusCode: 404,
388
+ statusText: `IPX_RESOURCE_NOT_FOUND`,
379
389
  message: `Resource not found: ${id}`
380
390
  });
381
391
  }
@@ -383,18 +393,40 @@ function createIPX(userOptions) {
383
393
  });
384
394
  const process = cachedPromise(async () => {
385
395
  const sourceData = await getSourceData();
386
- const imageMeta$1 = imageMeta.imageMeta(sourceData);
396
+ let imageMeta$1;
397
+ try {
398
+ imageMeta$1 = imageMeta.imageMeta(sourceData);
399
+ } catch {
400
+ throw h3.createError({
401
+ statusCode: 400,
402
+ statusText: `IPX_INVALID_IMAGE`,
403
+ message: `Cannot parse image metadata: ${id}`
404
+ });
405
+ }
387
406
  let mFormat = modifiers.f || modifiers.format;
388
407
  if (mFormat === "jpg") {
389
408
  mFormat = "jpeg";
390
409
  }
391
- const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type) ? imageMeta$1.type : "jpeg";
410
+ const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type || "") ? imageMeta$1.type : "jpeg";
392
411
  if (imageMeta$1.type === "svg" && !mFormat) {
393
- return {
394
- data: sourceData,
395
- format: "svg+xml",
396
- meta: imageMeta$1
397
- };
412
+ if (options.svgo === false) {
413
+ return {
414
+ data: sourceData,
415
+ format: "svg+xml",
416
+ meta: imageMeta$1
417
+ };
418
+ } else {
419
+ const { optimize, xss } = await getSVGO();
420
+ const svg = optimize(sourceData.toString("utf8"), {
421
+ ...options.svgo,
422
+ plugins: [xss, ...options.svgo?.plugins || []]
423
+ }).data;
424
+ return {
425
+ data: svg,
426
+ format: "svg+xml",
427
+ meta: imageMeta$1
428
+ };
429
+ }
398
430
  }
399
431
  const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
400
432
  const Sharp = await getSharp();
@@ -416,7 +448,7 @@ function createIPX(userOptions) {
416
448
  for (const h of handlers) {
417
449
  sharp = applyHandler(handlerContext, sharp, h.handler, h.args) || sharp;
418
450
  }
419
- if (SUPPORTED_FORMATS.has(format)) {
451
+ if (SUPPORTED_FORMATS.has(format || "")) {
420
452
  sharp = sharp.toFormat(format, {
421
453
  quality: handlerContext.quality,
422
454
  progressive: format === "jpeg"
@@ -439,7 +471,7 @@ function createIPX(userOptions) {
439
471
  const MODIFIER_SEP = /[&,]/g;
440
472
  const MODIFIER_VAL_SEP = /[:=_]/;
441
473
  function createIPXH3Handler(ipx) {
442
- return h3.defineEventHandler(async (event) => {
474
+ const _handler = async (event) => {
443
475
  const [modifiersString = "", ...idSegments] = event.path.slice(
444
476
  1
445
477
  /* leading slash */
@@ -448,12 +480,14 @@ function createIPXH3Handler(ipx) {
448
480
  if (!modifiersString) {
449
481
  throw h3.createError({
450
482
  statusCode: 400,
483
+ statusText: `IPX_MISSING_MODIFIERS`,
451
484
  message: `Modifiers are missing: ${id}`
452
485
  });
453
486
  }
454
487
  if (!id || id === "/") {
455
488
  throw h3.createError({
456
489
  statusCode: 400,
490
+ statusText: `IPX_MISSING_ID`,
457
491
  message: `Resource id is missing: ${event.path}`
458
492
  });
459
493
  }
@@ -475,37 +509,59 @@ function createIPXH3Handler(ipx) {
475
509
  delete modifiers.format;
476
510
  if (autoFormat) {
477
511
  modifiers.format = autoFormat;
478
- h3.setResponseHeader(event, "vary", "Accept");
512
+ h3.appendResponseHeader(event, "vary", "Accept");
479
513
  }
480
514
  }
481
515
  const img = ipx(id, modifiers);
482
516
  const sourceMeta = await img.getSourceMeta();
483
- if (sourceMeta.mtime) {
484
- if (h3.getRequestHeader(event, "if-modified-since") && new Date(h3.getRequestHeader(event, "if-modified-since") || "") >= sourceMeta.mtime) {
485
- h3.setResponseStatus(event, 304);
486
- return null;
487
- }
488
- h3.setResponseHeader(event, "last-modified", sourceMeta.mtime.toUTCString());
489
- }
517
+ sendResponseHeaderIfNotSet(
518
+ event,
519
+ "content-security-policy",
520
+ "default-src 'none'"
521
+ );
490
522
  if (typeof sourceMeta.maxAge === "number") {
491
- h3.setResponseHeader(
523
+ sendResponseHeaderIfNotSet(
492
524
  event,
493
525
  "cache-control",
494
526
  `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`
495
527
  );
496
528
  }
529
+ if (sourceMeta.mtime) {
530
+ sendResponseHeaderIfNotSet(
531
+ event,
532
+ "last-modified",
533
+ sourceMeta.mtime.toUTCString()
534
+ );
535
+ const _ifModifiedSince = h3.getRequestHeader(event, "if-modified-since");
536
+ if (_ifModifiedSince && new Date(_ifModifiedSince) >= sourceMeta.mtime) {
537
+ h3.setResponseStatus(event, 304);
538
+ return h3.send(event);
539
+ }
540
+ }
497
541
  const { data, format } = await img.process();
498
542
  const etag = getEtag__default(data);
499
- h3.setResponseHeader(event, "etag", etag);
543
+ sendResponseHeaderIfNotSet(event, "etag", etag);
500
544
  if (etag && h3.getRequestHeader(event, "if-none-match") === etag) {
501
545
  h3.setResponseStatus(event, 304);
502
- return null;
546
+ return h3.send(event);
503
547
  }
504
548
  if (format) {
505
- h3.setResponseHeader(event, "content-type", `image/${format}`);
549
+ sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
506
550
  }
507
- h3.setResponseHeader(event, "content-security-policy", "default-src 'none'");
508
551
  return data;
552
+ };
553
+ return h3.defineEventHandler(async (event) => {
554
+ try {
555
+ return await _handler(event);
556
+ } catch (_error) {
557
+ const error = h3.createError(_error);
558
+ h3.setResponseStatus(event, error.statusCode, error.statusMessage);
559
+ return {
560
+ error: {
561
+ message: `[${error.statusCode}] [${error.statusMessage || "IPX_ERROR"}] ${error.message}`
562
+ }
563
+ };
564
+ }
509
565
  });
510
566
  }
511
567
  function createIPXH3App(ipx) {
@@ -522,6 +578,11 @@ function createIPXNodeServer(ipx) {
522
578
  function createIPXPlainServer(ipx) {
523
579
  return h3.toPlainHandler(createIPXH3App(ipx));
524
580
  }
581
+ function sendResponseHeaderIfNotSet(event, name, value) {
582
+ if (!h3.getResponseHeader(event, name)) {
583
+ h3.setResponseHeader(event, name, value);
584
+ }
585
+ }
525
586
  function autoDetectFormat(acceptHeader, animated) {
526
587
  if (animated) {
527
588
  const acceptMime2 = acceptNegotiator.negotiate(acceptHeader, ["image/webp", "image/gif"]);
@@ -564,30 +625,28 @@ function ipxHttpStorage(_options = {}) {
564
625
  if (!url.hostname) {
565
626
  throw h3.createError({
566
627
  statusCode: 403,
628
+ statusText: `IPX_MISSING_HOSTNAME`,
567
629
  message: `Hostname is missing: ${id}`
568
630
  });
569
631
  }
570
632
  if (!allowAllDomains && !domains.has(url.hostname)) {
571
633
  throw h3.createError({
572
634
  statusCode: 403,
635
+ statusText: `IPX_FORBIDDEN_HOST`,
573
636
  message: `Forbidden host: ${url.hostname}`
574
637
  });
575
638
  }
576
639
  return url.toString();
577
640
  }
578
641
  function parseResponse(response) {
579
- if (!response.ok) {
580
- throw h3.createError({
581
- statusCode: response.status || 500,
582
- message: `Fetch error: ${response.statusText}`
583
- });
584
- }
585
642
  let maxAge = defaultMaxAge;
586
- const _cacheControl = response.headers.get("cache-control");
587
- if (_cacheControl) {
588
- const m = _cacheControl.match(/max-age=(\d+)/);
589
- if (m && m[1]) {
590
- maxAge = Number.parseInt(m[1]);
643
+ if (_options.ignoreCacheControl) {
644
+ const _cacheControl = response.headers.get("cache-control");
645
+ if (_cacheControl) {
646
+ const m = _cacheControl.match(/max-age=(\d+)/);
647
+ if (m && m[1]) {
648
+ maxAge = Number.parseInt(m[1]);
649
+ }
591
650
  }
592
651
  }
593
652
  let mtime;
@@ -602,7 +661,10 @@ function ipxHttpStorage(_options = {}) {
602
661
  async getMeta(id) {
603
662
  const url = validateId(id);
604
663
  try {
605
- const response = await nodeFetchNative.fetch(url, { ...fetchOptions, method: "HEAD" });
664
+ const response = await ofetch.ofetch.raw(url, {
665
+ ...fetchOptions,
666
+ method: "HEAD"
667
+ });
606
668
  const { maxAge, mtime } = parseResponse(response);
607
669
  return { mtime, maxAge };
608
670
  } catch {
@@ -611,8 +673,12 @@ function ipxHttpStorage(_options = {}) {
611
673
  },
612
674
  async getData(id) {
613
675
  const url = validateId(id);
614
- const response = await nodeFetchNative.fetch(url, { ...fetchOptions, method: "GET" });
615
- return response.arrayBuffer();
676
+ const response = await ofetch.ofetch(url, {
677
+ ...fetchOptions,
678
+ method: "GET",
679
+ responseType: "arrayBuffer"
680
+ });
681
+ return response;
616
682
  }
617
683
  };
618
684
  }
@@ -625,6 +691,7 @@ function ipxFSStorage(_options = {}) {
625
691
  if (!isValidPath(resolved) || !resolved.startsWith(rootDir)) {
626
692
  throw h3.createError({
627
693
  statusCode: 403,
694
+ statusText: `IPX_FORBIDDEN_PATH`,
628
695
  message: `Forbidden path: ${id}`
629
696
  });
630
697
  }
@@ -642,16 +709,19 @@ function ipxFSStorage(_options = {}) {
642
709
  } catch (error) {
643
710
  throw error.code === "ENOENT" ? h3.createError({
644
711
  statusCode: 404,
645
- message: `File not found: ${fsPath}`
712
+ statusText: `IPX_FILE_NOT_FOUND`,
713
+ message: `File not found: ${id}`
646
714
  }) : h3.createError({
647
715
  statusCode: 403,
648
- message: `File access error: (${error.code}) ${fsPath}`
716
+ statusText: `IPX_FORBIDDEN_FILE`,
717
+ message: `File access forbidden: (${error.code}) ${id}`
649
718
  });
650
719
  }
651
720
  if (!stats.isFile()) {
652
721
  throw h3.createError({
653
722
  statusCode: 400,
654
- message: `Path should be a file: ${fsPath}`
723
+ statusText: `IPX_INVALID_FILE`,
724
+ message: `Path should be a file: ${id}`
655
725
  });
656
726
  }
657
727
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ipx",
3
- "version": "2.0.0-0",
3
+ "version": "2.0.0",
4
4
  "repository": "unjs/ipx",
5
5
  "description": "High performance, secure and easy-to-use image optimizer.",
6
6
  "license": "MIT",
@@ -24,6 +24,18 @@
24
24
  "dist",
25
25
  "bin"
26
26
  ],
27
+ "scripts": {
28
+ "build": "unbuild",
29
+ "dev": "listhen -w playground",
30
+ "ipx": "jiti ./src/cli.ts",
31
+ "lint": "eslint --ext .ts . && prettier -c src test",
32
+ "lint:fix": "eslint --ext .ts . --fix && prettier -w src test",
33
+ "prepack": "pnpm build",
34
+ "release": "pnpm test && changelogen --release --push && npm publish",
35
+ "prerelease": "pnpm test && pnpm build && changelogen --release --prerelease --push --publish --publishTag next-2",
36
+ "start": "node bin/ipx.js",
37
+ "test": "pnpm lint && vitest run --coverage"
38
+ },
27
39
  "dependencies": {
28
40
  "@fastify/accept-negotiator": "^1.1.0",
29
41
  "citty": "^0.1.4",
@@ -31,39 +43,30 @@
31
43
  "defu": "^6.1.2",
32
44
  "destr": "^2.0.1",
33
45
  "etag": "^1.8.1",
34
- "h3": "^1.8.1",
35
- "image-meta": "^0.1.1",
36
- "listhen": "^1.5.2",
37
- "node-fetch-native": "^1.4.0",
46
+ "h3": "^1.8.2",
47
+ "image-meta": "^0.2.0",
48
+ "listhen": "^1.5.5",
49
+ "ofetch": "^1.3.3",
38
50
  "pathe": "^1.1.1",
39
- "sharp": "^0.32.5",
40
- "ufo": "^1.3.0",
41
- "unstorage": "^1.9.0"
51
+ "sharp": "^0.32.6",
52
+ "svgo": "^3.0.2",
53
+ "ufo": "^1.3.1",
54
+ "unstorage": "^1.9.0",
55
+ "xss": "^1.0.14"
42
56
  },
43
57
  "devDependencies": {
44
58
  "@types/etag": "^1.8.1",
45
59
  "@types/is-valid-path": "^0.1.0",
46
- "@vitest/coverage-v8": "^0.34.4",
60
+ "@vitest/coverage-v8": "^0.34.6",
47
61
  "changelogen": "^0.5.5",
48
- "eslint": "^8.49.0",
62
+ "eslint": "^8.51.0",
49
63
  "eslint-config-unjs": "^0.2.1",
50
64
  "jiti": "^1.20.0",
51
65
  "prettier": "^3.0.3",
52
66
  "serve-handler": "^6.1.5",
53
67
  "typescript": "^5.2.2",
54
68
  "unbuild": "^2.0.0",
55
- "vitest": "^0.34.4"
69
+ "vitest": "^0.34.6"
56
70
  },
57
- "packageManager": "pnpm@8.7.5",
58
- "scripts": {
59
- "build": "unbuild",
60
- "dev": "listhen -w playground",
61
- "ipx": "jiti ./src/cli.ts",
62
- "lint": "eslint --ext .ts . && prettier -c src test",
63
- "lint:fix": "eslint --ext .ts . --fix && prettier -w src test",
64
- "release": "pnpm test && changelogen --release --push && npm publish",
65
- "prerelease": "pnpm test && pnpm build && changelogen --release --prerelease --push --publish --publishTag v2",
66
- "start": "node bin/ipx.js",
67
- "test": "pnpm lint && vitest run --coverage"
68
- }
71
+ "packageManager": "pnpm@8.8.0"
69
72
  }