@bleedingdev/ipx 3.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Pooya Parsa <pooya@pi0.io>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @bleedingdev/ipx
2
+
3
+ A minimal, distributable sidecar fork of [`ipx`](https://github.com/unjs/ipx) — "High
4
+ performance, secure and easy-to-use image optimizer" — by Pooya Parsa and the UnJS
5
+ contributors, MIT licensed. The upstream `LICENSE` is preserved verbatim next to this file.
6
+
7
+ Forked from **`ipx@3.1.1`**. Published as **`@bleedingdev/ipx@3.2.0`**.
8
+
9
+ The `dist/` tree here is the upstream published build, vendored byte-for-byte except for the
10
+ hunks listed below. Upstream ships no TypeScript sources in the npm tarball, so this fork
11
+ patches the published chunks directly rather than rebuilding from source; that keeps the diff
12
+ against `ipx@3.1.1` small enough to re-audit by eye on every upstream bump.
13
+
14
+ ## Why this fork exists
15
+
16
+ `@modern-js/image` depends on `sharp@^0.35.3`, but `ipx@3.1.1` declares `"sharp": "^0.34.3"`
17
+ as a **plain dependency**. A `0.x` caret does not admit `0.35.x`
18
+ (`semver.satisfies("0.35.3", "^0.34.3") === false`), so every consumer of the published
19
+ `@modern-js/image` also installs a second, older `sharp` — currently `sharp@0.34.5` — behind
20
+ IPX. Plain dependencies cannot be redirected from a consumer's manifest: `pnpm.overrides` and
21
+ `pnpm.patchedDependencies` apply only to the root project and are not carried into a published
22
+ package. Forking the dependency and aliasing it in `@modern-js/image` is the only distributable
23
+ fix.
24
+
25
+ The version is a **stable** `3.2.0` on purpose. Both `@rsbuild-image/core` and
26
+ `@rsbuild-image/react` declare the peer range `"ipx": ">=3.0.3"`, and npm evaluates non-wildcard
27
+ peer ranges with a loose-only `semver.satisfies` that **excludes prereleases**. A prerelease
28
+ version such as `3.2.0-ultramodern.1` would pass pnpm but fail strict npm and yarn-classic
29
+ consumers.
30
+
31
+ ## Fork delta vs `ipx@3.1.1`
32
+
33
+ ### 1. `sharp` dependency range: `^0.34.3` → `^0.35.3` (`package.json`)
34
+
35
+ Aligns IPX with the `sharp` version `@modern-js/image` already declares, so a single, current
36
+ `sharp` is installed for the whole image pipeline.
37
+
38
+ ### 2. `sharpen` modifier remapped to Sharp 0.35's object form (`dist/shared/*`, 2 hunks)
39
+
40
+ Sharp 0.35 **removed** the deprecated positional `sharpen(sigma, flat, jagged)` signature. Its
41
+ replacement (`sharp/dist/operation.cjs`, `function sharpen (options)`) reads a plain object and
42
+ falls through to `this.options.sharpenSigma = -1` — the "mild sharpen" default — for **any**
43
+ non-object argument. Upstream's call therefore does not throw under Sharp 0.35; it *silently
44
+ discards every parameter the user asked for*.
45
+
46
+ Upstream (`dist/shared/ipx.CXJeaylD.mjs:213-217` and `dist/shared/ipx.GUc23orS.cjs:220-224`):
47
+
48
+ ```js
49
+ apply: (_context, pipe, sigma, flat, jagged) => {
50
+ return pipe.sharpen(sigma, flat, jagged);
51
+ }
52
+ ```
53
+
54
+ This fork maps the IPX modifier arguments onto `{ sigma, m1, m2 }`, coercing each to a number
55
+ (IPX arguments arrive through `VArg`, i.e. `destr`, so a URL segment may still be a string) and
56
+ treating `undefined`, `null`, an empty segment, and a non-numeric segment as "absent" — which
57
+ yields Sharp's argument-less mild sharpen instead of a thrown parameter error. `NaN` is never
58
+ handed to Sharp.
59
+
60
+ Behaviour notes:
61
+
62
+ - `?sharpen` (bare, no value) now applies Sharp's mild sharpen. Under `ipx@3.1.1` + `sharp@0.34`
63
+ this threw `Expected number between 0.01 and 10000 for sigma`; the new behaviour matches
64
+ Sharp's own no-argument contract and IPX's convention that a bare modifier is a flag.
65
+ - The accepted `sigma` range narrows from Sharp 0.34's deprecated positional range
66
+ (`0.01`–`10000`) to Sharp 0.35's object-form range (`0.000001`–`10`). This is an upstream Sharp
67
+ restriction, not a fork decision; out-of-range values raise Sharp's own parameter error rather
68
+ than being silently clamped.
69
+ - `flat`/`jagged` map to `m1`/`m2`, whose valid range widens to `0`–`1000000` (Sharp 0.34's
70
+ deprecated path capped them at `10000`).
71
+
72
+ ### 3. CLI banner version string `3.1.1` → `3.2.0` (`dist/cli.mjs`, `dist/cli.cjs`)
73
+
74
+ Cosmetic and non-functional: the version is inlined into the published CLI at build time, so
75
+ without this hunk `ipx --version` would disagree with this package's manifest. The bin name
76
+ itself is unchanged (`"bin": "./bin/ipx.mjs"` in string form, which
77
+ `npm-normalize-package-bin` resolves to the basename `ipx`), so the `ipx` CLI contract is
78
+ preserved despite the scoped package name.
79
+
80
+ ## Sharp 0.34 → 0.35 audit (no other remaps required)
81
+
82
+ Every Sharp method IPX's modifier table reaches was compared between `sharp@0.34.5`
83
+ (`lib/*.js`) and `sharp@0.35.3` (`dist/*.cjs`): `resize`, `trim`, `extend`, `extract`, `rotate`,
84
+ `flip`, `flop`, `sharpen`, `median`, `blur`, `flatten`, `gamma`, `negate`, `normalize`,
85
+ `threshold`, `modulate`, `tint`, `grayscale`, plus the pipeline's `toFormat` and `toBuffer`.
86
+
87
+ - **`sharpen` is the only removal.** Everything else is byte-identical or differs only in added
88
+ upper-bound validation (`extend` now rejects > 10000 px per edge; `extract` now rejects
89
+ > 100000000; `trim` gained an optional `margin` option). None of those changes reject an input
90
+ that `ipx@3.1.1` previously produced valid output for.
91
+ - The `trim` modifier passes a bare number to `sharp.trim()`, which throws
92
+ `Expected object for trim`. That is a **pre-existing upstream IPX bug**, identical under Sharp
93
+ 0.34.5 and 0.35.3, and is deliberately left unchanged here so this fork stays a
94
+ security/compatibility fork rather than a behaviour fork.
95
+
96
+ ## Verification
97
+
98
+ `scripts/verify-sharpen.mjs` exercises the remap end-to-end against a real image through
99
+ `createIPX` + `ipxFSStorage` and asserts that `sigma`, `flat` and `jagged` actually reach
100
+ libvips (different values must produce different bytes). Run it from a checkout where the
101
+ runtime dependencies resolve:
102
+
103
+ ```sh
104
+ node packages/sidecar/ipx/scripts/verify-sharpen.mjs [imageDir]
105
+ ```
106
+
107
+ Running the same script against the unpatched `ipx@3.1.1` dist on `sharp@0.35.3` fails both
108
+ differentiation assertions — every `sharpen` variant returns the identical mild-sharpen buffer —
109
+ which is exactly the silent regression this fork removes.
110
+
111
+ ## Maintenance
112
+
113
+ On an upstream `ipx` release, re-vendor `bin/`, `dist/` and `LICENSE`, then re-apply the hunks in
114
+ "Fork delta" (or drop them if upstream has adopted Sharp 0.35). `diff -ru` against the upstream
115
+ tarball must show those hunks and nothing else.
package/bin/ipx.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/cli.mjs";
package/dist/cli.cjs ADDED
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const listhen = require('listhen');
4
+ const citty = require('citty');
5
+ const cli = require('listhen/cli');
6
+ const nodeFs = require('./shared/ipx.GUc23orS.cjs');
7
+ require('defu');
8
+ require('ufo');
9
+ require('h3');
10
+ require('image-meta');
11
+ require('destr');
12
+ require('@fastify/accept-negotiator');
13
+ require('etag');
14
+ require('ofetch');
15
+ require('pathe');
16
+
17
+ const name = "ipx";
18
+ const version = "3.2.0";
19
+ const description = "High performance, secure and easy-to-use image optimizer.";
20
+
21
+ const serve = citty.defineCommand({
22
+ meta: {
23
+ description: "Start IPX Server"
24
+ },
25
+ args: {
26
+ dir: {
27
+ type: "string",
28
+ required: false,
29
+ description: "Directory to serve (default: current directory) ENV: IPX_FS_DIR"
30
+ },
31
+ domains: {
32
+ type: "string",
33
+ required: false,
34
+ description: "Allowed domains (comma separated) ENV: IPX_HTTP_DOMAINS"
35
+ },
36
+ ...cli.getArgs()
37
+ },
38
+ async run({ args }) {
39
+ const ipx = nodeFs.createIPX({
40
+ storage: nodeFs.ipxFSStorage({
41
+ dir: args.dir
42
+ }),
43
+ httpStorage: nodeFs.ipxHttpStorage({
44
+ domains: args.domains
45
+ })
46
+ });
47
+ await listhen.listen(nodeFs.createIPXNodeServer(ipx), {
48
+ name: "IPX",
49
+ ...cli.parseArgs(args)
50
+ });
51
+ }
52
+ });
53
+ const main = citty.defineCommand({
54
+ meta: {
55
+ name,
56
+ version,
57
+ description
58
+ },
59
+ subCommands: {
60
+ serve
61
+ }
62
+ });
63
+ citty.runMain(main);
package/dist/cli.d.cts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { };
package/dist/cli.d.mts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,61 @@
1
+ import { listen } from 'listhen';
2
+ import { defineCommand, runMain } from 'citty';
3
+ import { getArgs, parseArgs } from 'listhen/cli';
4
+ import { c as createIPX, i as ipxHttpStorage, g as ipxFSStorage, e as createIPXNodeServer } from './shared/ipx.CXJeaylD.mjs';
5
+ import 'defu';
6
+ import 'ufo';
7
+ import 'h3';
8
+ import 'image-meta';
9
+ import 'destr';
10
+ import '@fastify/accept-negotiator';
11
+ import 'etag';
12
+ import 'ofetch';
13
+ import 'pathe';
14
+
15
+ const name = "ipx";
16
+ const version = "3.2.0";
17
+ const description = "High performance, secure and easy-to-use image optimizer.";
18
+
19
+ const serve = defineCommand({
20
+ meta: {
21
+ description: "Start IPX Server"
22
+ },
23
+ args: {
24
+ dir: {
25
+ type: "string",
26
+ required: false,
27
+ description: "Directory to serve (default: current directory) ENV: IPX_FS_DIR"
28
+ },
29
+ domains: {
30
+ type: "string",
31
+ required: false,
32
+ description: "Allowed domains (comma separated) ENV: IPX_HTTP_DOMAINS"
33
+ },
34
+ ...getArgs()
35
+ },
36
+ async run({ args }) {
37
+ const ipx = createIPX({
38
+ storage: ipxFSStorage({
39
+ dir: args.dir
40
+ }),
41
+ httpStorage: ipxHttpStorage({
42
+ domains: args.domains
43
+ })
44
+ });
45
+ await listen(createIPXNodeServer(ipx), {
46
+ name: "IPX",
47
+ ...parseArgs(args)
48
+ });
49
+ }
50
+ });
51
+ const main = defineCommand({
52
+ meta: {
53
+ name,
54
+ version,
55
+ description
56
+ },
57
+ subCommands: {
58
+ serve
59
+ }
60
+ });
61
+ runMain(main);
package/dist/index.cjs ADDED
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const nodeFs = require('./shared/ipx.GUc23orS.cjs');
4
+ const h3 = require('h3');
5
+ require('defu');
6
+ require('ufo');
7
+ require('image-meta');
8
+ require('destr');
9
+ require('@fastify/accept-negotiator');
10
+ require('etag');
11
+ require('ofetch');
12
+ require('pathe');
13
+
14
+ function unstorageToIPXStorage(storage, _options = {}) {
15
+ const options = typeof _options === "string" ? { prefix: _options } : _options;
16
+ const resolveKey = (id) => options.prefix ? `${options.prefix}:${id}` : id;
17
+ return {
18
+ name: "ipx:" + (storage.name || "unstorage"),
19
+ async getMeta(id, opts = {}) {
20
+ if (!storage.getMeta) {
21
+ return;
22
+ }
23
+ const storageKey = resolveKey(id);
24
+ const meta = await storage.getMeta(storageKey, opts);
25
+ return meta;
26
+ },
27
+ async getData(id, opts = {}) {
28
+ if (!storage.getItemRaw) {
29
+ return;
30
+ }
31
+ const storageKey = resolveKey(id);
32
+ let data = await storage.getItemRaw(storageKey, opts);
33
+ if (!data) {
34
+ return;
35
+ }
36
+ if (data instanceof Blob) {
37
+ data = await data.arrayBuffer();
38
+ }
39
+ try {
40
+ return Buffer.from(data);
41
+ } catch (error) {
42
+ throw h3.createError({
43
+ statusCode: 500,
44
+ statusText: `IPX_STORAGE_ERROR`,
45
+ message: `Failed to parse storage data to Buffer:
46
+ ${error.message}`,
47
+ cause: error
48
+ });
49
+ }
50
+ }
51
+ };
52
+ }
53
+
54
+ exports.createIPX = nodeFs.createIPX;
55
+ exports.createIPXH3App = nodeFs.createIPXH3App;
56
+ exports.createIPXH3Handler = nodeFs.createIPXH3Handler;
57
+ exports.createIPXNodeServer = nodeFs.createIPXNodeServer;
58
+ exports.createIPXPlainServer = nodeFs.createIPXPlainServer;
59
+ exports.createIPXWebServer = nodeFs.createIPXWebServer;
60
+ exports.ipxFSStorage = nodeFs.ipxFSStorage;
61
+ exports.ipxHttpStorage = nodeFs.ipxHttpStorage;
62
+ exports.unstorageToIPXStorage = unstorageToIPXStorage;