koa-compress 5.1.1 → 5.2.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/HISTORY.md +41 -0
- package/README.md +81 -43
- package/lib/index.d.ts +162 -0
- package/lib/index.js +102 -25
- package/package.json +40 -14
- package/lib/encodings.js +0 -104
package/HISTORY.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
SEE https://github.com/koajs/compress/releases
|
|
2
2
|
|
|
3
|
+
5.2.1 / 2026-03-12
|
|
4
|
+
==================
|
|
5
|
+
|
|
6
|
+
* fixed TS typings and refactored them
|
|
7
|
+
* minor bug fix
|
|
8
|
+
* simplified code and removed unused code
|
|
9
|
+
* improved docs and typo fixes
|
|
10
|
+
* more tests
|
|
11
|
+
* updated dependencies
|
|
12
|
+
|
|
13
|
+
5.2.0 / 2026-02-10
|
|
14
|
+
==================
|
|
15
|
+
|
|
16
|
+
* add functional and local options
|
|
17
|
+
* updated dependencies
|
|
18
|
+
* copied the project setup from Koa
|
|
19
|
+
* add zstd
|
|
20
|
+
* use `negotiator` for content negotiation
|
|
21
|
+
* add TypeScript typings and tests
|
|
22
|
+
|
|
23
|
+
5.1.1 / 2023-04-08
|
|
24
|
+
==================
|
|
25
|
+
|
|
26
|
+
* minor refactoring + updated dependencies
|
|
27
|
+
|
|
28
|
+
5.1.0 / 2021-08-17
|
|
29
|
+
==================
|
|
30
|
+
|
|
31
|
+
* change brotli defaults (compression level to 4)
|
|
32
|
+
* updated dependencies
|
|
33
|
+
|
|
34
|
+
5.0.1 / 2020-07-06
|
|
35
|
+
==================
|
|
36
|
+
|
|
37
|
+
* updated to accomodate Node changes: zlib constants were moved to a different namespace
|
|
38
|
+
|
|
39
|
+
5.0.0 / 2020-07-05
|
|
40
|
+
==================
|
|
41
|
+
|
|
42
|
+
* add `defaultEncoding`
|
|
43
|
+
|
|
3
44
|
4.0.1 / 2020-04-29
|
|
4
45
|
==================
|
|
5
46
|
|
package/README.md
CHANGED
|
@@ -8,23 +8,28 @@ Compress middleware for Koa
|
|
|
8
8
|
## Example
|
|
9
9
|
|
|
10
10
|
```js
|
|
11
|
-
const compress = require(
|
|
12
|
-
const Koa = require(
|
|
13
|
-
|
|
14
|
-
const app = new Koa()
|
|
15
|
-
app.use(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
const compress = require("koa-compress");
|
|
12
|
+
const Koa = require("koa");
|
|
13
|
+
|
|
14
|
+
const app = new Koa();
|
|
15
|
+
app.use(
|
|
16
|
+
compress({
|
|
17
|
+
filter(content_type) {
|
|
18
|
+
return /text/i.test(content_type);
|
|
19
|
+
},
|
|
20
|
+
threshold: 2048,
|
|
21
|
+
gzip: {
|
|
22
|
+
flush: require("zlib").constants.Z_SYNC_FLUSH,
|
|
23
|
+
},
|
|
24
|
+
deflate: {
|
|
25
|
+
flush: require("zlib").constants.Z_SYNC_FLUSH,
|
|
26
|
+
},
|
|
27
|
+
zstd: {
|
|
28
|
+
flush: require("zlib").constants.Z_SYNC_FLUSH,
|
|
29
|
+
},
|
|
30
|
+
br: false, // disable brotli
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
28
33
|
```
|
|
29
34
|
|
|
30
35
|
## Maintainers
|
|
@@ -37,49 +42,82 @@ app.use(compress({
|
|
|
37
42
|
### filter\<Function\>
|
|
38
43
|
|
|
39
44
|
```ts
|
|
40
|
-
function (mimeType: string):
|
|
41
|
-
|
|
42
|
-
}
|
|
45
|
+
function (mimeType: string): boolean {}
|
|
43
46
|
```
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
A predicate that checks the response MIME type to decide whether to compress.
|
|
49
|
+
Default: [compressible](https://github.com/jshttp/compressible).
|
|
47
50
|
|
|
48
|
-
### options.threshold\<String|Number\>
|
|
51
|
+
### options.threshold\<String|Number|Function\>
|
|
49
52
|
|
|
50
53
|
Minimum response size in bytes to compress.
|
|
51
|
-
|
|
54
|
+
Can also be a string parsed by `bytes()` (e.g. `"1kb"`) or a function (see [Functional properties](#functional-properties)).
|
|
55
|
+
Default: `1024`.
|
|
56
|
+
|
|
57
|
+
### options[encoding]\<Object|Function\>
|
|
58
|
+
|
|
59
|
+
Supported encodings in default preference order: `zstd`, `br`, `gzip`, `deflate`.
|
|
60
|
+
Setting `options[encoding] = {}` passes those options to the corresponding `zlib` compressor.
|
|
61
|
+
Setting `options[encoding] = false` disables that encoding.
|
|
62
|
+
|
|
63
|
+
Can also be a function (see [Functional properties](#functional-properties)).
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
#### options.br
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Setting `options[encoding] = false` will disable that encoding.
|
|
67
|
+
[Brotli compression](https://en.wikipedia.org/wiki/Brotli) is available natively in all supported Node.js versions.
|
|
68
|
+
The default quality level is 4 for performance reasons.
|
|
58
69
|
|
|
59
|
-
#### options
|
|
70
|
+
#### options.zstd
|
|
60
71
|
|
|
61
|
-
[
|
|
62
|
-
As of v5.1.0, the default quality level is 4 for performance reasons.
|
|
72
|
+
[Zstandard compression](https://en.wikipedia.org/wiki/Zstandard) is natively supported starting from Node.js v22.15.0 (LTS) and v23.8.0 (Current). The middleware detects `zlib.createZstdCompress` at runtime; if available, zstd is enabled automatically, otherwise it is skipped.
|
|
63
73
|
|
|
64
74
|
### options.defaultEncoding\<String\>
|
|
65
75
|
|
|
66
|
-
|
|
67
|
-
[Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding).
|
|
68
|
-
Default `identity
|
|
76
|
+
Encoding assumed when the client sends no
|
|
77
|
+
[Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header.
|
|
78
|
+
Default: `"identity"` (no compression).
|
|
69
79
|
|
|
70
|
-
The
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
The HTTP spec treats a missing header as `*` (any encoding is acceptable),
|
|
81
|
+
but that causes problems when debugging with tools like `curl` or `wget`.
|
|
82
|
+
Set `defaultEncoding` to `"*"` to restore spec-compliant behavior.
|
|
73
83
|
|
|
74
84
|
## Manually turning compression on and off
|
|
75
85
|
|
|
76
|
-
You can
|
|
77
|
-
You can
|
|
78
|
-
This bypasses the filter check.
|
|
86
|
+
You can force compression by setting `ctx.compress = true` (bypasses the filter check).
|
|
87
|
+
You can disable compression by setting `ctx.compress = false`.
|
|
79
88
|
|
|
80
89
|
```js
|
|
81
90
|
app.use((ctx, next) => {
|
|
82
|
-
ctx.compress = true
|
|
83
|
-
ctx.body = fs.createReadStream(file)
|
|
84
|
-
})
|
|
91
|
+
ctx.compress = true;
|
|
92
|
+
ctx.body = fs.createReadStream(file);
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`ctx.compress` can also be an options object (same shape as the middleware options).
|
|
97
|
+
Its `threshold` and encoding properties override the global defaults for this response.
|
|
98
|
+
Note: an options object does **not** bypass the filter check — only `true` does.
|
|
99
|
+
|
|
100
|
+
## Functional properties
|
|
101
|
+
|
|
102
|
+
The `threshold` and per-encoding options can be functions. They are called
|
|
103
|
+
for every response with three arguments:
|
|
104
|
+
|
|
105
|
+
- `type` — same as `ctx.response.type`
|
|
106
|
+
- `size` — same as `ctx.response.length`
|
|
107
|
+
- `ctx` — the full Koa context object
|
|
108
|
+
|
|
109
|
+
The function should return a valid value for that property.
|
|
110
|
+
Returning another function of the same shape is allowed — it will be called in turn.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
app.use(
|
|
116
|
+
compress({
|
|
117
|
+
gzip: (type, size) => (size && size < 65536 ? { level: 9 } : false),
|
|
118
|
+
br: (type, size) => size && size >= 65536,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
85
121
|
```
|
|
122
|
+
|
|
123
|
+
See the Koa documentation for [`ctx`](https://koajs.com/#context) and [`ctx.response`](https://koajs.com/#response).
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { BrotliOptions, ZlibOptions, ZstdOptions } from "node:zlib";
|
|
2
|
+
import type Koa = require("koa");
|
|
3
|
+
|
|
4
|
+
declare namespace compress {
|
|
5
|
+
/**
|
|
6
|
+
* Calculates a threshold value dynamically from the MIME type,
|
|
7
|
+
* the response size, and the current context.
|
|
8
|
+
*/
|
|
9
|
+
type ThresholdFunction = (
|
|
10
|
+
/** MIME type of the response */
|
|
11
|
+
type: string,
|
|
12
|
+
/** Size of the response in bytes */
|
|
13
|
+
size: number,
|
|
14
|
+
/** Context of the request */
|
|
15
|
+
ctx: Koa.Context,
|
|
16
|
+
) => number | string | ThresholdFunction;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Computes `deflate`/`gzip` compression options from the MIME type,
|
|
20
|
+
* the response size, and the current context.
|
|
21
|
+
*/
|
|
22
|
+
type ZlibOptionsFunction = (
|
|
23
|
+
/** MIME type of the response */
|
|
24
|
+
type: string,
|
|
25
|
+
/** Size of the response in bytes */
|
|
26
|
+
size: number,
|
|
27
|
+
/** Context of the request */
|
|
28
|
+
ctx: Koa.Context,
|
|
29
|
+
) => boolean | null | ZlibOptions | ZlibOptionsFunction;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Computes `brotli` compression options from the MIME type,
|
|
33
|
+
* the response size, and the current context.
|
|
34
|
+
*/
|
|
35
|
+
type BrotliOptionsFunction = (
|
|
36
|
+
/** MIME type of the response */
|
|
37
|
+
type: string,
|
|
38
|
+
/** Size of the response in bytes */
|
|
39
|
+
size: number,
|
|
40
|
+
/** Context of the request */
|
|
41
|
+
ctx: Koa.Context,
|
|
42
|
+
) => boolean | null | BrotliOptions | BrotliOptionsFunction;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Computes `zstd` compression options from the MIME type,
|
|
46
|
+
* the response size, and the current context.
|
|
47
|
+
*/
|
|
48
|
+
type ZstdOptionsFunction = (
|
|
49
|
+
/** MIME type of the response */
|
|
50
|
+
type: string,
|
|
51
|
+
/** Size of the response in bytes */
|
|
52
|
+
size: number,
|
|
53
|
+
/** Context of the request */
|
|
54
|
+
ctx: Koa.Context,
|
|
55
|
+
) => boolean | null | ZstdOptions | ZstdOptionsFunction;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Options for the `koa-compress` middleware.
|
|
59
|
+
*/
|
|
60
|
+
type CompressOptions = {
|
|
61
|
+
/**
|
|
62
|
+
* Predicate that decides whether a given MIME type should be compressed.
|
|
63
|
+
* Default: `compressible()`.
|
|
64
|
+
* @param type - MIME type of the response
|
|
65
|
+
* @returns `true` to compress, `false` to skip
|
|
66
|
+
*/
|
|
67
|
+
filter?: (type: string) => boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Minimum response size to compress. A number (bytes), a string
|
|
70
|
+
* parsed by `bytes()` (e.g. `"1mb"`), or a {@link ThresholdFunction}.
|
|
71
|
+
* Default: `1024`.
|
|
72
|
+
*/
|
|
73
|
+
threshold?: number | string | ThresholdFunction;
|
|
74
|
+
/**
|
|
75
|
+
* Encoding assumed when the client sends no `Accept-Encoding` header.
|
|
76
|
+
* Set to `"*"` for spec-compliant behavior. Default: `"identity"`.
|
|
77
|
+
*/
|
|
78
|
+
defaultEncoding?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Encoding to use when `Accept-Encoding` is `"*"`. Default: `"gzip"`.
|
|
81
|
+
*/
|
|
82
|
+
wildcardAcceptEncoding?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Preferred encoding order used to break ties among equally weighted
|
|
85
|
+
* encodings. Lower index = higher priority.
|
|
86
|
+
* Default: `['zstd', 'br', 'gzip', 'deflate', 'identity']`.
|
|
87
|
+
*/
|
|
88
|
+
encodingPreference?: string[];
|
|
89
|
+
/**
|
|
90
|
+
* Options passed to `zlib.createDeflate()`, or `false`/`null` to disable.
|
|
91
|
+
* Can be a {@link ZlibOptionsFunction} for per-response values.
|
|
92
|
+
* Default: `{}`.
|
|
93
|
+
*/
|
|
94
|
+
deflate?: boolean | null | ZlibOptions | ZlibOptionsFunction;
|
|
95
|
+
/**
|
|
96
|
+
* Options passed to `zlib.createGzip()`, or `false`/`null` to disable.
|
|
97
|
+
* Can be a {@link ZlibOptionsFunction} for per-response values.
|
|
98
|
+
* Default: `{}`.
|
|
99
|
+
*/
|
|
100
|
+
gzip?: boolean | null | ZlibOptions | ZlibOptionsFunction;
|
|
101
|
+
/**
|
|
102
|
+
* Options passed to `zlib.createBrotliCompress()`, or `false`/`null` to disable.
|
|
103
|
+
* Can be a {@link BrotliOptionsFunction} for per-response values.
|
|
104
|
+
* Default: `{params: {[zlib.constants.BROTLI_PARAM_QUALITY]: 4}}`.
|
|
105
|
+
*/
|
|
106
|
+
br?: boolean | null | BrotliOptions | BrotliOptionsFunction;
|
|
107
|
+
/**
|
|
108
|
+
* Options passed to `zlib.createZstdCompress()`, or `false`/`null` to disable.
|
|
109
|
+
* Can be a {@link ZstdOptionsFunction} for per-response values.
|
|
110
|
+
* Default: `{}`.
|
|
111
|
+
*/
|
|
112
|
+
zstd?: boolean | null | ZstdOptions | ZstdOptionsFunction;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Per-encoding options: built-in defaults merged with user-supplied values.
|
|
117
|
+
*/
|
|
118
|
+
type EncodingOptions = {
|
|
119
|
+
[encoding: string]: BrotliOptions | ZlibOptions | ZstdOptions | undefined;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Koa middleware with additional properties for introspection.
|
|
124
|
+
*/
|
|
125
|
+
interface CompressMiddleware extends Koa.Middleware {
|
|
126
|
+
/**
|
|
127
|
+
* Supported content encodings available in the current Node.js runtime,
|
|
128
|
+
* ordered by preference.
|
|
129
|
+
*/
|
|
130
|
+
preferredEncodings: string[];
|
|
131
|
+
/**
|
|
132
|
+
* Resolved options for each supported encoding (built-in defaults
|
|
133
|
+
* merged with user-provided values).
|
|
134
|
+
*/
|
|
135
|
+
encodingOptions: EncodingOptions;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Creates a Koa middleware that compresses response bodies via content
|
|
141
|
+
* negotiation. The returned function also exposes
|
|
142
|
+
* {@link compress.CompressMiddleware.preferredEncodings | preferredEncodings} and
|
|
143
|
+
* {@link compress.CompressMiddleware.encodingOptions | encodingOptions}.
|
|
144
|
+
*
|
|
145
|
+
* @param options - Compression options. See {@link compress.CompressOptions}.
|
|
146
|
+
* @returns Koa middleware with introspection properties.
|
|
147
|
+
*/
|
|
148
|
+
declare function compress(options?: compress.CompressOptions): compress.CompressMiddleware;
|
|
149
|
+
|
|
150
|
+
declare module "koa" {
|
|
151
|
+
interface DefaultContext {
|
|
152
|
+
/**
|
|
153
|
+
* Per-response compression override.
|
|
154
|
+
* Set to `false` to disable compression, `true` to force it
|
|
155
|
+
* (bypassing the filter), or a `CompressOptions` object whose
|
|
156
|
+
* properties override the defaults for this response.
|
|
157
|
+
*/
|
|
158
|
+
compress?: boolean | compress.CompressOptions;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export = compress;
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @ts-self-types="./index.d.ts"
|
|
2
|
+
|
|
1
3
|
'use strict'
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -7,19 +9,43 @@
|
|
|
7
9
|
const compressible = require('compressible')
|
|
8
10
|
const isJSON = require('koa-is-json')
|
|
9
11
|
const Stream = require('stream')
|
|
12
|
+
const zlib = require('zlib')
|
|
10
13
|
const bytes = require('bytes')
|
|
14
|
+
const Negotiator = require('negotiator')
|
|
15
|
+
|
|
16
|
+
const encodingMethods = {
|
|
17
|
+
zstd: zlib.createZstdCompress,
|
|
18
|
+
br: zlib.createBrotliCompress,
|
|
19
|
+
gzip: zlib.createGzip,
|
|
20
|
+
deflate: zlib.createDeflate
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const encodingDefaultPreference = ['zstd', 'br', 'gzip', 'deflate', 'identity']
|
|
24
|
+
|
|
25
|
+
const preferredEncodings = encodingDefaultPreference.filter(
|
|
26
|
+
(encoding) => encodingMethods[encoding] || encoding === 'identity'
|
|
27
|
+
)
|
|
11
28
|
|
|
12
|
-
const
|
|
29
|
+
const encodingMethodDefaultOptions = {
|
|
30
|
+
gzip: {},
|
|
31
|
+
deflate: {},
|
|
32
|
+
br: {
|
|
33
|
+
params: {
|
|
34
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
zstd: {}
|
|
38
|
+
}
|
|
13
39
|
|
|
14
40
|
/**
|
|
15
|
-
* Regex to match no-transform directive in a cache-control header
|
|
16
|
-
*/
|
|
41
|
+
* Regex to match no-transform directive in a cache-control header
|
|
42
|
+
*/
|
|
17
43
|
const NO_TRANSFORM_REGEX = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
|
|
18
44
|
|
|
19
45
|
/**
|
|
20
|
-
* empty body
|
|
46
|
+
* empty body statuses.
|
|
21
47
|
*/
|
|
22
|
-
const
|
|
48
|
+
const emptyBodyStatuses = new Set([204, 205, 304])
|
|
23
49
|
|
|
24
50
|
/**
|
|
25
51
|
* Compress middleware.
|
|
@@ -30,18 +56,29 @@ const emptyBodyStatues = new Set([204, 205, 304])
|
|
|
30
56
|
*/
|
|
31
57
|
|
|
32
58
|
module.exports = (options = {}) => {
|
|
33
|
-
let {
|
|
34
|
-
|
|
59
|
+
let {
|
|
60
|
+
filter = compressible,
|
|
61
|
+
defaultEncoding,
|
|
62
|
+
wildcardAcceptEncoding,
|
|
63
|
+
encodingPreference = encodingDefaultPreference
|
|
64
|
+
} = options
|
|
65
|
+
if (!wildcardAcceptEncoding || wildcardAcceptEncoding === '*') {
|
|
66
|
+
wildcardAcceptEncoding = 'gzip'
|
|
67
|
+
}
|
|
68
|
+
defaultEncoding =
|
|
69
|
+
defaultEncoding === '*'
|
|
70
|
+
? wildcardAcceptEncoding
|
|
71
|
+
: !defaultEncoding
|
|
72
|
+
? 'identity'
|
|
73
|
+
: defaultEncoding
|
|
35
74
|
|
|
36
|
-
// `options.br = false` would remove it as a preferred encoding
|
|
37
|
-
const preferredEncodings = Encodings.preferredEncodings.filter((encoding) => options[encoding] !== false && options[encoding] !== null)
|
|
38
75
|
const encodingOptions = {}
|
|
39
|
-
|
|
76
|
+
for (const encoding of preferredEncodings) {
|
|
40
77
|
encodingOptions[encoding] = {
|
|
41
|
-
...
|
|
42
|
-
...
|
|
78
|
+
...encodingMethodDefaultOptions[encoding],
|
|
79
|
+
...options[encoding]
|
|
43
80
|
}
|
|
44
|
-
}
|
|
81
|
+
}
|
|
45
82
|
|
|
46
83
|
Object.assign(compressMiddleware, {
|
|
47
84
|
preferredEncodings,
|
|
@@ -56,27 +93,66 @@ module.exports = (options = {}) => {
|
|
|
56
93
|
await next()
|
|
57
94
|
|
|
58
95
|
let { body } = ctx
|
|
96
|
+
const { type, length: size } = ctx.response
|
|
59
97
|
if (
|
|
60
98
|
// early exit if there's no content body or the body is already encoded
|
|
61
99
|
!body ||
|
|
62
|
-
ctx.res.headersSent ||
|
|
100
|
+
ctx.res.headersSent ||
|
|
101
|
+
!ctx.writable ||
|
|
63
102
|
ctx.compress === false ||
|
|
64
103
|
ctx.request.method === 'HEAD' ||
|
|
65
|
-
|
|
104
|
+
emptyBodyStatuses.has(+ctx.response.status) ||
|
|
66
105
|
ctx.response.get('Content-Encoding') ||
|
|
67
106
|
// forced compression or implied
|
|
68
|
-
!(ctx.compress === true || filter(
|
|
107
|
+
!(ctx.compress === true || filter(type)) ||
|
|
69
108
|
// don't compress for Cache-Control: no-transform
|
|
70
109
|
// https://tools.ietf.org/html/rfc7234#section-5.2.1.6
|
|
71
|
-
NO_TRANSFORM_REGEX.test(ctx.response.get('Cache-Control'))
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
110
|
+
NO_TRANSFORM_REGEX.test(ctx.response.get('Cache-Control'))
|
|
111
|
+
) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// calculate "local" compression options
|
|
116
|
+
const responseOptions = { ...options, ...ctx.compress }
|
|
117
|
+
let { threshold = 1024 } = responseOptions
|
|
118
|
+
while (typeof threshold === 'function') {
|
|
119
|
+
threshold = threshold(type, size, ctx)
|
|
120
|
+
}
|
|
121
|
+
if (typeof threshold === 'string') threshold = bytes(threshold)
|
|
122
|
+
|
|
123
|
+
// don't compress if the current response is below the threshold
|
|
124
|
+
if (threshold && size < threshold) return
|
|
75
125
|
|
|
76
126
|
// get the preferred content encoding
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
127
|
+
for (const encoding of compressMiddleware.preferredEncodings) {
|
|
128
|
+
// calculate compressor options, if any
|
|
129
|
+
if (!(encoding in responseOptions)) continue
|
|
130
|
+
let compressor = responseOptions[encoding]
|
|
131
|
+
while (typeof compressor === 'function') {
|
|
132
|
+
compressor = compressor(type, size, ctx)
|
|
133
|
+
}
|
|
134
|
+
responseOptions[encoding] = compressor
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const finalEncodings = compressMiddleware.preferredEncodings.filter(
|
|
138
|
+
(encoding) =>
|
|
139
|
+
responseOptions[encoding] !== false &&
|
|
140
|
+
responseOptions[encoding] !== null
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const acceptEncoding = ctx.request.headers['accept-encoding']
|
|
144
|
+
let request = ctx.request
|
|
145
|
+
if (acceptEncoding === '*') {
|
|
146
|
+
request = { headers: { 'accept-encoding': wildcardAcceptEncoding } }
|
|
147
|
+
} else if (!acceptEncoding) {
|
|
148
|
+
request = { headers: { 'accept-encoding': defaultEncoding } }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const negotiator = new Negotiator(request)
|
|
152
|
+
// if no encodings are supported, negotiator returns 'identity'
|
|
153
|
+
const encoding = negotiator.encoding(finalEncodings, {
|
|
154
|
+
preferred: encodingPreference
|
|
155
|
+
})
|
|
80
156
|
|
|
81
157
|
// identity === no compression
|
|
82
158
|
if (encoding === 'identity') return
|
|
@@ -89,8 +165,9 @@ module.exports = (options = {}) => {
|
|
|
89
165
|
ctx.set('Content-Encoding', encoding)
|
|
90
166
|
ctx.res.removeHeader('Content-Length')
|
|
91
167
|
|
|
92
|
-
const compress =
|
|
93
|
-
const
|
|
168
|
+
const compress = encodingMethods[encoding]
|
|
169
|
+
const compressOptions = { ...encodingMethodDefaultOptions[encoding], ...responseOptions[encoding] }
|
|
170
|
+
const stream = (ctx.body = compress(compressOptions))
|
|
94
171
|
|
|
95
172
|
if (body instanceof Stream) return body.pipe(stream)
|
|
96
173
|
stream.end(body)
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-compress",
|
|
3
3
|
"description": "Compress middleware for koa",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.2.1",
|
|
5
|
+
"type": "commonjs",
|
|
5
6
|
"author": {
|
|
6
7
|
"name": "Jonathan Ong",
|
|
7
8
|
"email": "me@jongleberry.com",
|
|
@@ -13,6 +14,11 @@
|
|
|
13
14
|
"name": "Imed Jaberi",
|
|
14
15
|
"email": "imed-jaberi@outlook.com",
|
|
15
16
|
"url": "https://www.3imed-jaberi.com/"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "Eugene Lazutkin",
|
|
20
|
+
"email": "eugene.lazutkin@gmail.com",
|
|
21
|
+
"url": "https://www.lazutkin.com/"
|
|
16
22
|
}
|
|
17
23
|
],
|
|
18
24
|
"license": "MIT",
|
|
@@ -20,23 +26,29 @@
|
|
|
20
26
|
"dependencies": {
|
|
21
27
|
"bytes": "^3.1.2",
|
|
22
28
|
"compressible": "^2.0.18",
|
|
23
|
-
"http-errors": "^
|
|
24
|
-
"koa-is-json": "^1.0.0"
|
|
29
|
+
"http-errors": "^2.0.1",
|
|
30
|
+
"koa-is-json": "^1.0.0",
|
|
31
|
+
"negotiator": "^1.0.0"
|
|
25
32
|
},
|
|
26
33
|
"devDependencies": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
34
|
+
"@types/koa": "^3.0.1",
|
|
35
|
+
"@types/node": "^25.5.0",
|
|
36
|
+
"c8": "^11.0.0",
|
|
37
|
+
"jest": "^30.3.0",
|
|
38
|
+
"koa": "^3.1.2",
|
|
39
|
+
"snazzy": "^9.0.0",
|
|
40
|
+
"standard": "^17.1.2",
|
|
41
|
+
"supertest": "^7.2.2",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
36
43
|
},
|
|
37
44
|
"scripts": {
|
|
38
|
-
"
|
|
39
|
-
"test": "jest"
|
|
45
|
+
"test": "jest",
|
|
46
|
+
"test:coverage": "c8 --reporter=lcov --reporter=text-summary jest --coverage",
|
|
47
|
+
"lint": "standard",
|
|
48
|
+
"lint:fix": "standard --fix",
|
|
49
|
+
"lint:pretty": "standard | snazzy",
|
|
50
|
+
"ts-check": "tsc --noEmit",
|
|
51
|
+
"authors": "git log --format='%aN <%aE>' | sort -u > AUTHORS"
|
|
40
52
|
},
|
|
41
53
|
"files": [
|
|
42
54
|
"lib",
|
|
@@ -44,7 +56,21 @@
|
|
|
44
56
|
"HISTORY.md"
|
|
45
57
|
],
|
|
46
58
|
"main": "lib",
|
|
59
|
+
"types": "lib/index.d.ts",
|
|
47
60
|
"engines": {
|
|
48
61
|
"node": ">= 12"
|
|
62
|
+
},
|
|
63
|
+
"standard": {
|
|
64
|
+
"globals": [
|
|
65
|
+
"afterAll",
|
|
66
|
+
"beforeAll",
|
|
67
|
+
"afterEach",
|
|
68
|
+
"jasmine",
|
|
69
|
+
"describe",
|
|
70
|
+
"test",
|
|
71
|
+
"jest",
|
|
72
|
+
"expect",
|
|
73
|
+
"it"
|
|
74
|
+
]
|
|
49
75
|
}
|
|
50
76
|
}
|
package/lib/encodings.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
|
|
3
|
-
const errors = require('http-errors')
|
|
4
|
-
const zlib = require('zlib')
|
|
5
|
-
|
|
6
|
-
class Encodings {
|
|
7
|
-
constructor (options = {}) {
|
|
8
|
-
this.wildcardAcceptEncoding = options.wildcardAcceptEncoding || Encodings.wildcardAcceptEncoding
|
|
9
|
-
this.preferredEncodings = options.preferredEncodings || Encodings.preferredEncodings
|
|
10
|
-
this.reDirective = options.reDirective || Encodings.reDirective
|
|
11
|
-
|
|
12
|
-
this.encodingWeights = new Map()
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
parseAcceptEncoding (acceptEncoding = '*') {
|
|
16
|
-
const { encodingWeights, reDirective } = this
|
|
17
|
-
|
|
18
|
-
acceptEncoding.split(',').forEach((directive) => {
|
|
19
|
-
const match = reDirective.exec(directive)
|
|
20
|
-
if (!match) return // not a supported encoding above
|
|
21
|
-
|
|
22
|
-
const encoding = match[1]
|
|
23
|
-
|
|
24
|
-
// weight must be in [0, 1]
|
|
25
|
-
let weight = match[2] && !isNaN(match[2]) ? parseFloat(match[2], 10) : 1
|
|
26
|
-
weight = Math.max(weight, 0)
|
|
27
|
-
weight = Math.min(weight, 1)
|
|
28
|
-
|
|
29
|
-
if (encoding === '*') {
|
|
30
|
-
// set the weights for the default encodings
|
|
31
|
-
this.wildcardAcceptEncoding.forEach((enc) => {
|
|
32
|
-
if (!encodingWeights.has(enc)) encodingWeights.set(enc, weight)
|
|
33
|
-
})
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
encodingWeights.set(encoding, weight)
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
getPreferredContentEncoding () {
|
|
42
|
-
const {
|
|
43
|
-
encodingWeights,
|
|
44
|
-
preferredEncodings
|
|
45
|
-
} = this
|
|
46
|
-
|
|
47
|
-
// get ordered list of accepted encodings
|
|
48
|
-
const acceptedEncodings = Array.from(encodingWeights.keys())
|
|
49
|
-
// sort by weight
|
|
50
|
-
.sort((a, b) => encodingWeights.get(b) - encodingWeights.get(a))
|
|
51
|
-
// filter by supported encodings
|
|
52
|
-
.filter((encoding) => encoding === 'identity' || typeof Encodings.encodingMethods[encoding] === 'function')
|
|
53
|
-
|
|
54
|
-
// group them by weights
|
|
55
|
-
const weightClasses = new Map()
|
|
56
|
-
acceptedEncodings.forEach((encoding) => {
|
|
57
|
-
const weight = encodingWeights.get(encoding)
|
|
58
|
-
if (!weightClasses.has(weight)) weightClasses.set(weight, new Set())
|
|
59
|
-
weightClasses.get(weight).add(encoding)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// search by weight, descending
|
|
63
|
-
const weights = Array.from(weightClasses.keys()).sort((a, b) => b - a)
|
|
64
|
-
for (let i = 0; i < weights.length; i++) {
|
|
65
|
-
// encodings at this weight
|
|
66
|
-
const encodings = weightClasses.get(weights[i])
|
|
67
|
-
// return the first encoding in the preferred list
|
|
68
|
-
for (let j = 0; j < preferredEncodings.length; j++) {
|
|
69
|
-
const preferredEncoding = preferredEncodings[j]
|
|
70
|
-
if (encodings.has(preferredEncoding)) return preferredEncoding
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// no encoding matches, check to see if the client set identity, q=0
|
|
75
|
-
if (encodingWeights.get('identity') === 0) throw errors(406, 'Please accept br, gzip, deflate, or identity.')
|
|
76
|
-
|
|
77
|
-
// by default, return nothing
|
|
78
|
-
return 'identity'
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
Encodings.encodingMethods = {
|
|
83
|
-
gzip: zlib.createGzip,
|
|
84
|
-
deflate: zlib.createDeflate,
|
|
85
|
-
br: zlib.createBrotliCompress
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
Encodings.encodingMethodDefaultOptions = {
|
|
89
|
-
gzip: {},
|
|
90
|
-
deflate: {},
|
|
91
|
-
br: {
|
|
92
|
-
params: {
|
|
93
|
-
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// how we treat `Accept-Encoding: *`
|
|
99
|
-
Encodings.wildcardAcceptEncoding = ['gzip', 'deflate']
|
|
100
|
-
// our preferred encodings
|
|
101
|
-
Encodings.preferredEncodings = ['br', 'gzip', 'deflate']
|
|
102
|
-
Encodings.reDirective = /^\s*(gzip|compress|deflate|br|identity|\*)\s*(?:;\s*q\s*=\s*(\d(?:\.\d)?))?\s*$/
|
|
103
|
-
|
|
104
|
-
module.exports = Encodings
|