ipx 2.0.0-1 → 2.0.1

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
@@ -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 and [#71](https://github.com/unjs/ipx/issues/171) for v2 roadmap.
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@next-2 serve --dir ./
20
+ npx ipx serve --dir ./
21
21
  ```
22
22
 
23
23
  Usin `bun`
24
24
 
25
25
  ```bash
26
- bun x npx ipx@next-2 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 "ipx";
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";
55
+
56
+ const ipx = createIPX({
57
+ storage: ipxFSStorage({ dir: "./public" }),
58
+ httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
59
+ });
56
60
 
57
- const app = createApp().use("/", fromNodeMiddleware(ipxMiddleware));
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,116 @@
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
+ if (node.attributes[event] != null) {
17
+ delete node.attributes[event];
18
+ }
19
+ }
20
+ },
21
+ exit: (node, parentNode) => {
22
+ if (node.name !== "a") {
23
+ return;
24
+ }
25
+ for (const attr of Object.keys(node.attributes)) {
26
+ if (attr === "href" || attr.endsWith(":href")) {
27
+ if (node.attributes[attr] == null || !node.attributes[attr].trimStart().startsWith("javascript:")) {
28
+ continue;
29
+ }
30
+ const index = parentNode.children.indexOf(node);
31
+ parentNode.children.splice(index, 1, ...node.children);
32
+ for (const child of node.children) {
33
+ Object.defineProperty(child, "parentNode", {
34
+ writable: true,
35
+ value: parentNode
36
+ });
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ };
43
+ }
44
+ };
45
+ const ALL_EVENTS = [
46
+ "onabort",
47
+ "onactivate",
48
+ "onbegin",
49
+ "oncancel",
50
+ "oncanplay",
51
+ "oncanplaythrough",
52
+ "onchange",
53
+ "onclick",
54
+ "onclose",
55
+ "oncopy",
56
+ "oncuechange",
57
+ "oncut",
58
+ "ondblclick",
59
+ "ondrag",
60
+ "ondragend",
61
+ "ondragenter",
62
+ "ondragleave",
63
+ "ondragover",
64
+ "ondragstart",
65
+ "ondrop",
66
+ "ondurationchange",
67
+ "onemptied",
68
+ "onend",
69
+ "onended",
70
+ "onerror",
71
+ "onfocus",
72
+ "onfocusin",
73
+ "onfocusout",
74
+ "oninput",
75
+ "oninvalid",
76
+ "onkeydown",
77
+ "onkeypress",
78
+ "onkeyup",
79
+ "onload",
80
+ "onloadeddata",
81
+ "onloadedmetadata",
82
+ "onloadstart",
83
+ "onmousedown",
84
+ "onmouseenter",
85
+ "onmouseleave",
86
+ "onmousemove",
87
+ "onmouseout",
88
+ "onmouseover",
89
+ "onmouseup",
90
+ "onmousewheel",
91
+ "onpaste",
92
+ "onpause",
93
+ "onplay",
94
+ "onplaying",
95
+ "onprogress",
96
+ "onratechange",
97
+ "onrepeat",
98
+ "onreset",
99
+ "onresize",
100
+ "onscroll",
101
+ "onseeked",
102
+ "onseeking",
103
+ "onselect",
104
+ "onshow",
105
+ "onstalled",
106
+ "onsubmit",
107
+ "onsuspend",
108
+ "ontimeupdate",
109
+ "ontoggle",
110
+ "onunload",
111
+ "onvolumechange",
112
+ "onwaiting",
113
+ "onzoom"
114
+ ];
115
+
116
+ exports.xss = xss;
@@ -0,0 +1,114 @@
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
+ if (node.attributes[event] != null) {
15
+ delete node.attributes[event];
16
+ }
17
+ }
18
+ },
19
+ exit: (node, parentNode) => {
20
+ if (node.name !== "a") {
21
+ return;
22
+ }
23
+ for (const attr of Object.keys(node.attributes)) {
24
+ if (attr === "href" || attr.endsWith(":href")) {
25
+ if (node.attributes[attr] == null || !node.attributes[attr].trimStart().startsWith("javascript:")) {
26
+ continue;
27
+ }
28
+ const index = parentNode.children.indexOf(node);
29
+ parentNode.children.splice(index, 1, ...node.children);
30
+ for (const child of node.children) {
31
+ Object.defineProperty(child, "parentNode", {
32
+ writable: true,
33
+ value: parentNode
34
+ });
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ };
41
+ }
42
+ };
43
+ const ALL_EVENTS = [
44
+ "onabort",
45
+ "onactivate",
46
+ "onbegin",
47
+ "oncancel",
48
+ "oncanplay",
49
+ "oncanplaythrough",
50
+ "onchange",
51
+ "onclick",
52
+ "onclose",
53
+ "oncopy",
54
+ "oncuechange",
55
+ "oncut",
56
+ "ondblclick",
57
+ "ondrag",
58
+ "ondragend",
59
+ "ondragenter",
60
+ "ondragleave",
61
+ "ondragover",
62
+ "ondragstart",
63
+ "ondrop",
64
+ "ondurationchange",
65
+ "onemptied",
66
+ "onend",
67
+ "onended",
68
+ "onerror",
69
+ "onfocus",
70
+ "onfocusin",
71
+ "onfocusout",
72
+ "oninput",
73
+ "oninvalid",
74
+ "onkeydown",
75
+ "onkeypress",
76
+ "onkeyup",
77
+ "onload",
78
+ "onloadeddata",
79
+ "onloadedmetadata",
80
+ "onloadstart",
81
+ "onmousedown",
82
+ "onmouseenter",
83
+ "onmouseleave",
84
+ "onmousemove",
85
+ "onmouseout",
86
+ "onmouseover",
87
+ "onmouseup",
88
+ "onmousewheel",
89
+ "onpaste",
90
+ "onpause",
91
+ "onplay",
92
+ "onplaying",
93
+ "onprogress",
94
+ "onratechange",
95
+ "onrepeat",
96
+ "onreset",
97
+ "onresize",
98
+ "onscroll",
99
+ "onseeked",
100
+ "onseeking",
101
+ "onselect",
102
+ "onshow",
103
+ "onstalled",
104
+ "onsubmit",
105
+ "onsuspend",
106
+ "ontimeupdate",
107
+ "ontoggle",
108
+ "onunload",
109
+ "onvolumechange",
110
+ "onwaiting",
111
+ "onzoom"
112
+ ];
113
+
114
+ export { xss };
package/dist/cli.cjs CHANGED
@@ -3,11 +3,11 @@
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.680a50a5.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');
@@ -15,7 +15,7 @@ require('ofetch');
15
15
  require('pathe');
16
16
 
17
17
  const name = "ipx";
18
- const version = "2.0.0-1";
18
+ const version = "2.0.1";
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,11 +1,11 @@
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.57fad794.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';
@@ -13,7 +13,7 @@ import 'ofetch';
13
13
  import 'pathe';
14
14
 
15
15
  const name = "ipx";
16
- const version = "2.0.0-1";
16
+ const version = "2.0.1";
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,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const nodeFs = require('./shared/ipx.680a50a5.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');
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,14 +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 | {
128
+ declare function createIPXH3Handler(ipx: IPX): h3.EventHandler<h3.EventHandlerRequest, Promise<string | void | Buffer | {
132
129
  error: {
133
130
  message: string;
134
131
  };
135
- } | null>>;
132
+ }>>;
136
133
  declare function createIPXH3App(ipx: IPX): h3.App;
137
134
  declare function createIPXWebServer(ipx: IPX): h3.WebHandler;
138
135
  declare function createIPXNodeServer(ipx: IPX): h3.NodeListener;
@@ -143,6 +140,7 @@ type HTTPStorageOptions = {
143
140
  maxAge?: number;
144
141
  domains?: string | string[];
145
142
  allowAllDomains?: boolean;
143
+ ignoreCacheControl?: boolean;
146
144
  };
147
145
  declare function ipxHttpStorage(_options?: HTTPStorageOptions): IPXStorage;
148
146
 
@@ -154,4 +152,4 @@ declare function ipxFSStorage(_options?: NodeFSSOptions): IPXStorage;
154
152
 
155
153
  declare function unstorageToIPXStorage(storage: Storage | Driver, prefix: string): IPXStorage;
156
154
 
157
- 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,14 +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 | {
128
+ declare function createIPXH3Handler(ipx: IPX): h3.EventHandler<h3.EventHandlerRequest, Promise<string | void | Buffer | {
132
129
  error: {
133
130
  message: string;
134
131
  };
135
- } | null>>;
132
+ }>>;
136
133
  declare function createIPXH3App(ipx: IPX): h3.App;
137
134
  declare function createIPXWebServer(ipx: IPX): h3.WebHandler;
138
135
  declare function createIPXNodeServer(ipx: IPX): h3.NodeListener;
@@ -143,6 +140,7 @@ type HTTPStorageOptions = {
143
140
  maxAge?: number;
144
141
  domains?: string | string[];
145
142
  allowAllDomains?: boolean;
143
+ ignoreCacheControl?: boolean;
146
144
  };
147
145
  declare function ipxHttpStorage(_options?: HTTPStorageOptions): IPXStorage;
148
146
 
@@ -154,4 +152,4 @@ declare function ipxFSStorage(_options?: NodeFSSOptions): IPXStorage;
154
152
 
155
153
  declare function unstorageToIPXStorage(storage: Storage | Driver, prefix: string): IPXStorage;
156
154
 
157
- 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,14 +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 | {
128
+ declare function createIPXH3Handler(ipx: IPX): h3.EventHandler<h3.EventHandlerRequest, Promise<string | void | Buffer | {
132
129
  error: {
133
130
  message: string;
134
131
  };
135
- } | null>>;
132
+ }>>;
136
133
  declare function createIPXH3App(ipx: IPX): h3.App;
137
134
  declare function createIPXWebServer(ipx: IPX): h3.WebHandler;
138
135
  declare function createIPXNodeServer(ipx: IPX): h3.NodeListener;
@@ -143,6 +140,7 @@ type HTTPStorageOptions = {
143
140
  maxAge?: number;
144
141
  domains?: string | string[];
145
142
  allowAllDomains?: boolean;
143
+ ignoreCacheControl?: boolean;
146
144
  };
147
145
  declare function ipxHttpStorage(_options?: HTTPStorageOptions): IPXStorage;
148
146
 
@@ -154,4 +152,4 @@ declare function ipxFSStorage(_options?: NodeFSSOptions): IPXStorage;
154
152
 
155
153
  declare function unstorageToIPXStorage(storage: Storage | Driver, prefix: string): IPXStorage;
156
154
 
157
- 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,8 +1,8 @@
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.57fad794.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';
@@ -1,7 +1,7 @@
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, setResponseStatus, createApp, toWebHandler, toNodeListener, toPlainHandler, getRequestHeader, setResponseHeader } 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';
@@ -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,6 +331,11 @@ 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({
@@ -362,8 +367,9 @@ function createIPX(userOptions) {
362
367
  message: `Resource not found: ${id}`
363
368
  });
364
369
  }
370
+ const _maxAge = sourceMeta.maxAge ?? options.maxAge;
365
371
  return {
366
- maxAge: typeof sourceMeta.maxAge === "string" ? Number.parseInt(sourceMeta.maxAge) : sourceMeta.maxAge,
372
+ maxAge: typeof _maxAge === "string" ? Number.parseInt(_maxAge) : _maxAge,
367
373
  mtime: sourceMeta.mtime ? new Date(sourceMeta.mtime) : void 0
368
374
  };
369
375
  });
@@ -394,13 +400,26 @@ function createIPX(userOptions) {
394
400
  if (mFormat === "jpg") {
395
401
  mFormat = "jpeg";
396
402
  }
397
- 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";
398
404
  if (imageMeta$1.type === "svg" && !mFormat) {
399
- return {
400
- data: sourceData,
401
- format: "svg+xml",
402
- meta: imageMeta$1
403
- };
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
+ }
404
423
  }
405
424
  const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
406
425
  const Sharp = await getSharp();
@@ -422,7 +441,7 @@ function createIPX(userOptions) {
422
441
  for (const h of handlers) {
423
442
  sharp = applyHandler(handlerContext, sharp, h.handler, h.args) || sharp;
424
443
  }
425
- if (SUPPORTED_FORMATS.has(format)) {
444
+ if (SUPPORTED_FORMATS.has(format || "")) {
426
445
  sharp = sharp.toFormat(format, {
427
446
  quality: handlerContext.quality,
428
447
  progressive: format === "jpeg"
@@ -483,36 +502,45 @@ function createIPXH3Handler(ipx) {
483
502
  delete modifiers.format;
484
503
  if (autoFormat) {
485
504
  modifiers.format = autoFormat;
486
- setResponseHeader(event, "vary", "Accept");
505
+ appendResponseHeader(event, "vary", "Accept");
487
506
  }
488
507
  }
489
508
  const img = ipx(id, modifiers);
490
509
  const sourceMeta = await img.getSourceMeta();
491
- if (sourceMeta.mtime) {
492
- if (getRequestHeader(event, "if-modified-since") && new Date(getRequestHeader(event, "if-modified-since") || "") >= sourceMeta.mtime) {
493
- setResponseStatus(event, 304);
494
- return null;
495
- }
496
- setResponseHeader(event, "last-modified", sourceMeta.mtime.toUTCString());
497
- }
510
+ sendResponseHeaderIfNotSet(
511
+ event,
512
+ "content-security-policy",
513
+ "default-src 'none'"
514
+ );
498
515
  if (typeof sourceMeta.maxAge === "number") {
499
- setResponseHeader(
516
+ sendResponseHeaderIfNotSet(
500
517
  event,
501
518
  "cache-control",
502
519
  `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`
503
520
  );
504
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
+ }
505
534
  const { data, format } = await img.process();
506
535
  const etag = getEtag(data);
507
- setResponseHeader(event, "etag", etag);
536
+ sendResponseHeaderIfNotSet(event, "etag", etag);
508
537
  if (etag && getRequestHeader(event, "if-none-match") === etag) {
509
538
  setResponseStatus(event, 304);
510
- return null;
539
+ return send(event);
511
540
  }
512
541
  if (format) {
513
- setResponseHeader(event, "content-type", `image/${format}`);
542
+ sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
514
543
  }
515
- setResponseHeader(event, "content-security-policy", "default-src 'none'");
516
544
  return data;
517
545
  };
518
546
  return defineEventHandler(async (event) => {
@@ -543,6 +571,11 @@ function createIPXNodeServer(ipx) {
543
571
  function createIPXPlainServer(ipx) {
544
572
  return toPlainHandler(createIPXH3App(ipx));
545
573
  }
574
+ function sendResponseHeaderIfNotSet(event, name, value) {
575
+ if (!getResponseHeader(event, name)) {
576
+ setResponseHeader(event, name, value);
577
+ }
578
+ }
546
579
  function autoDetectFormat(acceptHeader, animated) {
547
580
  if (animated) {
548
581
  const acceptMime2 = negotiate(acceptHeader, ["image/webp", "image/gif"]);
@@ -600,11 +633,13 @@ function ipxHttpStorage(_options = {}) {
600
633
  }
601
634
  function parseResponse(response) {
602
635
  let maxAge = defaultMaxAge;
603
- const _cacheControl = response.headers.get("cache-control");
604
- if (_cacheControl) {
605
- const m = _cacheControl.match(/max-age=(\d+)/);
606
- if (m && m[1]) {
607
- 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
+ }
608
643
  }
609
644
  }
610
645
  let mtime;
@@ -1,9 +1,9 @@
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');
@@ -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,6 +338,11 @@ 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({
@@ -369,8 +374,9 @@ function createIPX(userOptions) {
369
374
  message: `Resource not found: ${id}`
370
375
  });
371
376
  }
377
+ const _maxAge = sourceMeta.maxAge ?? options.maxAge;
372
378
  return {
373
- maxAge: typeof sourceMeta.maxAge === "string" ? Number.parseInt(sourceMeta.maxAge) : sourceMeta.maxAge,
379
+ maxAge: typeof _maxAge === "string" ? Number.parseInt(_maxAge) : _maxAge,
374
380
  mtime: sourceMeta.mtime ? new Date(sourceMeta.mtime) : void 0
375
381
  };
376
382
  });
@@ -401,13 +407,26 @@ function createIPX(userOptions) {
401
407
  if (mFormat === "jpg") {
402
408
  mFormat = "jpeg";
403
409
  }
404
- 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";
405
411
  if (imageMeta$1.type === "svg" && !mFormat) {
406
- return {
407
- data: sourceData,
408
- format: "svg+xml",
409
- meta: imageMeta$1
410
- };
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
+ }
411
430
  }
412
431
  const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
413
432
  const Sharp = await getSharp();
@@ -429,7 +448,7 @@ function createIPX(userOptions) {
429
448
  for (const h of handlers) {
430
449
  sharp = applyHandler(handlerContext, sharp, h.handler, h.args) || sharp;
431
450
  }
432
- if (SUPPORTED_FORMATS.has(format)) {
451
+ if (SUPPORTED_FORMATS.has(format || "")) {
433
452
  sharp = sharp.toFormat(format, {
434
453
  quality: handlerContext.quality,
435
454
  progressive: format === "jpeg"
@@ -490,36 +509,45 @@ function createIPXH3Handler(ipx) {
490
509
  delete modifiers.format;
491
510
  if (autoFormat) {
492
511
  modifiers.format = autoFormat;
493
- h3.setResponseHeader(event, "vary", "Accept");
512
+ h3.appendResponseHeader(event, "vary", "Accept");
494
513
  }
495
514
  }
496
515
  const img = ipx(id, modifiers);
497
516
  const sourceMeta = await img.getSourceMeta();
498
- if (sourceMeta.mtime) {
499
- if (h3.getRequestHeader(event, "if-modified-since") && new Date(h3.getRequestHeader(event, "if-modified-since") || "") >= sourceMeta.mtime) {
500
- h3.setResponseStatus(event, 304);
501
- return null;
502
- }
503
- h3.setResponseHeader(event, "last-modified", sourceMeta.mtime.toUTCString());
504
- }
517
+ sendResponseHeaderIfNotSet(
518
+ event,
519
+ "content-security-policy",
520
+ "default-src 'none'"
521
+ );
505
522
  if (typeof sourceMeta.maxAge === "number") {
506
- h3.setResponseHeader(
523
+ sendResponseHeaderIfNotSet(
507
524
  event,
508
525
  "cache-control",
509
526
  `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`
510
527
  );
511
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
+ }
512
541
  const { data, format } = await img.process();
513
542
  const etag = getEtag__default(data);
514
- h3.setResponseHeader(event, "etag", etag);
543
+ sendResponseHeaderIfNotSet(event, "etag", etag);
515
544
  if (etag && h3.getRequestHeader(event, "if-none-match") === etag) {
516
545
  h3.setResponseStatus(event, 304);
517
- return null;
546
+ return h3.send(event);
518
547
  }
519
548
  if (format) {
520
- h3.setResponseHeader(event, "content-type", `image/${format}`);
549
+ sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
521
550
  }
522
- h3.setResponseHeader(event, "content-security-policy", "default-src 'none'");
523
551
  return data;
524
552
  };
525
553
  return h3.defineEventHandler(async (event) => {
@@ -550,6 +578,11 @@ function createIPXNodeServer(ipx) {
550
578
  function createIPXPlainServer(ipx) {
551
579
  return h3.toPlainHandler(createIPXH3App(ipx));
552
580
  }
581
+ function sendResponseHeaderIfNotSet(event, name, value) {
582
+ if (!h3.getResponseHeader(event, name)) {
583
+ h3.setResponseHeader(event, name, value);
584
+ }
585
+ }
553
586
  function autoDetectFormat(acceptHeader, animated) {
554
587
  if (animated) {
555
588
  const acceptMime2 = acceptNegotiator.negotiate(acceptHeader, ["image/webp", "image/gif"]);
@@ -607,11 +640,13 @@ function ipxHttpStorage(_options = {}) {
607
640
  }
608
641
  function parseResponse(response) {
609
642
  let maxAge = defaultMaxAge;
610
- const _cacheControl = response.headers.get("cache-control");
611
- if (_cacheControl) {
612
- const m = _cacheControl.match(/max-age=(\d+)/);
613
- if (m && m[1]) {
614
- 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
+ }
615
650
  }
616
651
  }
617
652
  let mtime;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ipx",
3
- "version": "2.0.0-1",
3
+ "version": "2.0.1",
4
4
  "repository": "unjs/ipx",
5
5
  "description": "High performance, secure and easy-to-use image optimizer.",
6
6
  "license": "MIT",
@@ -40,31 +40,33 @@
40
40
  "@fastify/accept-negotiator": "^1.1.0",
41
41
  "citty": "^0.1.4",
42
42
  "consola": "^3.2.3",
43
- "defu": "^6.1.2",
44
- "destr": "^2.0.1",
43
+ "defu": "^6.1.3",
44
+ "destr": "^2.0.2",
45
45
  "etag": "^1.8.1",
46
46
  "h3": "^1.8.2",
47
- "image-meta": "^0.1.1",
47
+ "image-meta": "^0.2.0",
48
48
  "listhen": "^1.5.5",
49
49
  "ofetch": "^1.3.3",
50
50
  "pathe": "^1.1.1",
51
51
  "sharp": "^0.32.6",
52
- "ufo": "^1.3.0",
53
- "unstorage": "^1.9.0"
52
+ "svgo": "^3.0.2",
53
+ "ufo": "^1.3.1",
54
+ "unstorage": "^1.9.0",
55
+ "xss": "^1.0.14"
54
56
  },
55
57
  "devDependencies": {
56
- "@types/etag": "^1.8.1",
57
- "@types/is-valid-path": "^0.1.0",
58
- "@vitest/coverage-v8": "^0.34.5",
58
+ "@types/etag": "^1.8.2",
59
+ "@types/is-valid-path": "^0.1.1",
60
+ "@vitest/coverage-v8": "^0.34.6",
59
61
  "changelogen": "^0.5.5",
60
- "eslint": "^8.50.0",
62
+ "eslint": "^8.53.0",
61
63
  "eslint-config-unjs": "^0.2.1",
62
- "jiti": "^1.20.0",
64
+ "jiti": "^1.21.0",
63
65
  "prettier": "^3.0.3",
64
66
  "serve-handler": "^6.1.5",
65
67
  "typescript": "^5.2.2",
66
68
  "unbuild": "^2.0.0",
67
- "vitest": "^0.34.5"
69
+ "vitest": "^0.34.6"
68
70
  },
69
- "packageManager": "pnpm@8.8.0"
71
+ "packageManager": "pnpm@8.10.2"
70
72
  }