koa-compress 5.0.1 → 5.1.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 +1 -0
- package/README.md +7 -1
- package/lib/encodings.js +83 -73
- package/lib/index.js +40 -29
- package/package.json +22 -19
package/HISTORY.md
CHANGED
package/README.md
CHANGED
|
@@ -27,6 +27,11 @@ app.use(compress({
|
|
|
27
27
|
}))
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Maintainers
|
|
31
|
+
|
|
32
|
+
- Lead: @jonathanong [@jongleberry](https://twitter.com/jongleberry)
|
|
33
|
+
- Team: @koajs/compress
|
|
34
|
+
|
|
30
35
|
## Options
|
|
31
36
|
|
|
32
37
|
### filter\<Function\>
|
|
@@ -54,12 +59,13 @@ Setting `options[encoding] = false` will disable that encoding.
|
|
|
54
59
|
#### options<span></span>.br
|
|
55
60
|
|
|
56
61
|
[Brotli compression](https://en.wikipedia.org/wiki/Brotli) is supported in node v11.7.0+, which includes it natively.
|
|
62
|
+
As of v5.1.0, the default quality level is 4 for performance reasons.
|
|
57
63
|
|
|
58
64
|
### options.defaultEncoding\<String\>
|
|
59
65
|
|
|
60
66
|
An optional string, which specifies what encoders to use for requests without
|
|
61
67
|
[Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding).
|
|
62
|
-
Default `
|
|
68
|
+
Default `identity`.
|
|
63
69
|
|
|
64
70
|
The standard dictates to treat such requests as `*` meaning that all compressions are permissible,
|
|
65
71
|
yet it causes very practical problems when debugging servers with manual tools like `curl`, `wget`, and so on.
|
package/lib/encodings.js
CHANGED
|
@@ -3,92 +3,102 @@
|
|
|
3
3
|
const errors = require('http-errors')
|
|
4
4
|
const zlib = require('zlib')
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this.wildcardAcceptEncoding = options.wildcardAcceptEncoding || Encodings.wildcardAcceptEncoding
|
|
12
|
-
this.preferredEncodings = options.preferredEncodings || Encodings.preferredEncodings
|
|
13
|
-
this.reDirective = options.reDirective || Encodings.reDirective
|
|
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
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
}
|
|
12
|
+
this.encodingWeights = new Map()
|
|
13
|
+
}
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
deflate: zlib.createDeflate,
|
|
21
|
-
br: zlib.createBrotliCompress
|
|
22
|
-
}
|
|
15
|
+
parseAcceptEncoding (acceptEncoding = '*') {
|
|
16
|
+
const { encodingWeights, reDirective } = this
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
Encodings.preferredEncodings = ['br', 'gzip', 'deflate']
|
|
28
|
-
Encodings.reDirective = /^\s*(gzip|compress|deflate|br|identity|\*)\s*(?:;\s*q\s*=\s*(\d(?:\.\d)?))?\s*$/
|
|
18
|
+
acceptEncoding.split(',').forEach((directive) => {
|
|
19
|
+
const match = reDirective.exec(directive)
|
|
20
|
+
if (!match) return // not a supported encoding above
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
const { encodingWeights, reDirective } = this
|
|
22
|
+
const encoding = match[1]
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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)
|
|
36
28
|
|
|
37
|
-
|
|
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
|
+
}
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
weight = Math.min(weight, 1)
|
|
37
|
+
encodingWeights.set(encoding, weight)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
|
50
72
|
}
|
|
51
73
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
54
86
|
}
|
|
55
87
|
|
|
56
|
-
Encodings.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// get ordered list of accepted encodings
|
|
63
|
-
const acceptedEncodings = Array.from(encodingWeights.keys())
|
|
64
|
-
// sort by weight
|
|
65
|
-
.sort((a, b) => encodingWeights.get(b) - encodingWeights.get(a))
|
|
66
|
-
// filter by supported encodings
|
|
67
|
-
.filter((encoding) => encoding === 'identity' || typeof Encodings.encodingMethods[encoding] === 'function')
|
|
68
|
-
|
|
69
|
-
// group them by weights
|
|
70
|
-
const weightClasses = new Map()
|
|
71
|
-
acceptedEncodings.forEach((encoding) => {
|
|
72
|
-
const weight = encodingWeights.get(encoding)
|
|
73
|
-
if (!weightClasses.has(weight)) weightClasses.set(weight, new Set())
|
|
74
|
-
weightClasses.get(weight).add(encoding)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// search by weight, descending
|
|
78
|
-
const weights = Array.from(weightClasses.keys()).sort((a, b) => b - a)
|
|
79
|
-
for (let i = 0; i < weights.length; i++) {
|
|
80
|
-
// encodings at this weight
|
|
81
|
-
const encodings = weightClasses.get(weights[i])
|
|
82
|
-
// return the first encoding in the preferred list
|
|
83
|
-
for (let j = 0; j < preferredEncodings.length; j++) {
|
|
84
|
-
const preferredEncoding = preferredEncodings[j]
|
|
85
|
-
if (encodings.has(preferredEncoding)) return preferredEncoding
|
|
88
|
+
Encodings.encodingMethodDefaultOptions = {
|
|
89
|
+
gzip: {},
|
|
90
|
+
deflate: {},
|
|
91
|
+
br: {
|
|
92
|
+
params: {
|
|
93
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
86
94
|
}
|
|
87
95
|
}
|
|
96
|
+
}
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
|
|
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*$/
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
return 'identity'
|
|
94
|
-
}
|
|
104
|
+
module.exports = Encodings
|
package/lib/index.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
const compressible = require('compressible')
|
|
8
8
|
const isJSON = require('koa-is-json')
|
|
9
|
-
const status = require('statuses')
|
|
10
9
|
const Stream = require('stream')
|
|
11
10
|
const bytes = require('bytes')
|
|
12
11
|
|
|
@@ -17,6 +16,11 @@ const Encodings = require('./encodings')
|
|
|
17
16
|
*/
|
|
18
17
|
const NO_TRANSFORM_REGEX = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
|
|
19
18
|
|
|
19
|
+
/**
|
|
20
|
+
* empty body statues.
|
|
21
|
+
*/
|
|
22
|
+
const emptyBodyStatues = new Set([204, 205, 304])
|
|
23
|
+
|
|
20
24
|
/**
|
|
21
25
|
* Compress middleware.
|
|
22
26
|
*
|
|
@@ -31,36 +35,46 @@ module.exports = (options = {}) => {
|
|
|
31
35
|
|
|
32
36
|
// `options.br = false` would remove it as a preferred encoding
|
|
33
37
|
const preferredEncodings = Encodings.preferredEncodings.filter((encoding) => options[encoding] !== false && options[encoding] !== null)
|
|
38
|
+
const encodingOptions = {}
|
|
39
|
+
preferredEncodings.forEach((encoding) => {
|
|
40
|
+
encodingOptions[encoding] = {
|
|
41
|
+
...Encodings.encodingMethodDefaultOptions[encoding],
|
|
42
|
+
...(options[encoding] || {})
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
Object.assign(compressMiddleware, {
|
|
47
|
+
preferredEncodings,
|
|
48
|
+
encodingOptions
|
|
49
|
+
})
|
|
34
50
|
|
|
35
|
-
return
|
|
51
|
+
return compressMiddleware
|
|
52
|
+
|
|
53
|
+
async function compressMiddleware (ctx, next) {
|
|
36
54
|
ctx.vary('Accept-Encoding')
|
|
37
55
|
|
|
38
56
|
await next()
|
|
39
57
|
|
|
40
|
-
// early exit if there's no content body or the body is already encoded
|
|
41
58
|
let { body } = ctx
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (threshold && ctx.response.length < threshold) return
|
|
59
|
+
if (
|
|
60
|
+
// early exit if there's no content body or the body is already encoded
|
|
61
|
+
!body ||
|
|
62
|
+
ctx.res.headersSent || !ctx.writable ||
|
|
63
|
+
ctx.compress === false ||
|
|
64
|
+
ctx.request.method === 'HEAD' ||
|
|
65
|
+
emptyBodyStatues.has(+ctx.response.status) ||
|
|
66
|
+
ctx.response.get('Content-Encoding') ||
|
|
67
|
+
// forced compression or implied
|
|
68
|
+
!(ctx.compress === true || filter(ctx.response.type)) ||
|
|
69
|
+
// don't compress for Cache-Control: no-transform
|
|
70
|
+
// https://tools.ietf.org/html/rfc7234#section-5.2.1.6
|
|
71
|
+
NO_TRANSFORM_REGEX.test(ctx.response.get('Cache-Control')) ||
|
|
72
|
+
// don't compress if the current response is below the threshold
|
|
73
|
+
(threshold && ctx.response.length < threshold)
|
|
74
|
+
) return
|
|
59
75
|
|
|
60
76
|
// get the preferred content encoding
|
|
61
|
-
const encodings = new Encodings({
|
|
62
|
-
preferredEncodings
|
|
63
|
-
})
|
|
77
|
+
const encodings = new Encodings({ preferredEncodings })
|
|
64
78
|
encodings.parseAcceptEncoding(ctx.request.headers['accept-encoding'] || defaultEncoding)
|
|
65
79
|
const encoding = encodings.getPreferredContentEncoding()
|
|
66
80
|
|
|
@@ -76,12 +90,9 @@ module.exports = (options = {}) => {
|
|
|
76
90
|
ctx.res.removeHeader('Content-Length')
|
|
77
91
|
|
|
78
92
|
const compress = Encodings.encodingMethods[encoding]
|
|
79
|
-
const stream = ctx.body = compress(
|
|
93
|
+
const stream = ctx.body = compress(encodingOptions[encoding])
|
|
80
94
|
|
|
81
|
-
if (body instanceof Stream)
|
|
82
|
-
|
|
83
|
-
} else {
|
|
84
|
-
stream.end(body)
|
|
85
|
-
}
|
|
95
|
+
if (body instanceof Stream) return body.pipe(stream)
|
|
96
|
+
stream.end(body)
|
|
86
97
|
}
|
|
87
98
|
}
|
package/package.json
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-compress",
|
|
3
3
|
"description": "Compress middleware for koa",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.1.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonathan Ong",
|
|
7
7
|
"email": "me@jongleberry.com",
|
|
8
8
|
"url": "http://jongleberry.com",
|
|
9
9
|
"twitter": "https://twitter.com/jongleberry"
|
|
10
10
|
},
|
|
11
|
+
"contributors": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Imed Jaberi",
|
|
14
|
+
"email": "imed-jaberi@outlook.com",
|
|
15
|
+
"url": "https://www.3imed-jaberi.com/"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
11
18
|
"license": "MIT",
|
|
12
19
|
"repository": "koajs/compress",
|
|
13
20
|
"dependencies": {
|
|
14
|
-
"bytes": "^3.
|
|
15
|
-
"compressible": "^2.0.
|
|
16
|
-
"http-errors": "^1.
|
|
17
|
-
"koa-is-json": "^1.0.0"
|
|
18
|
-
"statuses": "^2.0.0"
|
|
21
|
+
"bytes": "^3.1.2",
|
|
22
|
+
"compressible": "^2.0.18",
|
|
23
|
+
"http-errors": "^1.8.1",
|
|
24
|
+
"koa-is-json": "^1.0.0"
|
|
19
25
|
},
|
|
20
26
|
"devDependencies": {
|
|
21
|
-
"eslint": "^
|
|
22
|
-
"eslint-config-standard": "^
|
|
23
|
-
"eslint-plugin-import": "^2.
|
|
24
|
-
"eslint-plugin-node": "^11.
|
|
25
|
-
"eslint-plugin-promise": "^
|
|
26
|
-
"eslint-plugin-standard": "^
|
|
27
|
-
"jest": "^
|
|
28
|
-
"koa": "^2.
|
|
29
|
-
"supertest": "^
|
|
27
|
+
"eslint": "^7.32.0",
|
|
28
|
+
"eslint-config-standard": "^16.0.3",
|
|
29
|
+
"eslint-plugin-import": "^2.26.0",
|
|
30
|
+
"eslint-plugin-node": "^11.1.0",
|
|
31
|
+
"eslint-plugin-promise": "^5.2.0",
|
|
32
|
+
"eslint-plugin-standard": "^5.0.0",
|
|
33
|
+
"jest": "^27.5.1",
|
|
34
|
+
"koa": "^2.13.4",
|
|
35
|
+
"supertest": "^6.2.4"
|
|
30
36
|
},
|
|
31
37
|
"scripts": {
|
|
32
38
|
"eslint": "eslint --ignore-path .gitignore .",
|
|
@@ -38,10 +44,7 @@
|
|
|
38
44
|
"HISTORY.md"
|
|
39
45
|
],
|
|
40
46
|
"main": "lib",
|
|
41
|
-
"jest": {
|
|
42
|
-
"testEnvironment": "node"
|
|
43
|
-
},
|
|
44
47
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
48
|
+
"node": ">= 12"
|
|
46
49
|
}
|
|
47
50
|
}
|