koa-compress 2.0.0 → 4.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/HISTORY.md +31 -1
- package/README.md +35 -12
- package/lib/encodings.js +94 -0
- package/lib/index.js +87 -0
- package/package.json +24 -13
- package/index.js +0 -79
package/HISTORY.md
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
|
|
2
|
+
4.0.1 / 2020-04-29
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
* fix issue with state being preserved between requests
|
|
6
|
+
|
|
7
|
+
4.0.0 / 2020-04-27
|
|
8
|
+
==================
|
|
9
|
+
|
|
10
|
+
* drop support for versions of node below 10
|
|
11
|
+
* add brotli support for versions of node that support it
|
|
12
|
+
* changed compression options; `options` is no longer passed to each compression function. Use `options.gzip = {}`, `options.br = {}`, etc.
|
|
13
|
+
* added compression disabling option: set `options.br = false`
|
|
14
|
+
|
|
15
|
+
3.1.0 / 2020-04-15
|
|
16
|
+
==================
|
|
17
|
+
|
|
18
|
+
* support no-transform @Pawda
|
|
19
|
+
|
|
20
|
+
3.0.0 / 2018-04-14
|
|
21
|
+
==================
|
|
22
|
+
|
|
23
|
+
* republish 2.1.0 as it switches to async functions
|
|
24
|
+
|
|
25
|
+
2.1.0 / 2018-04-13 (unpublished)
|
|
26
|
+
==================
|
|
27
|
+
|
|
28
|
+
* do not compress if the response is not writable
|
|
29
|
+
* switch testing frameworks to jest
|
|
30
|
+
* implement linting
|
|
31
|
+
|
|
2
32
|
1.0.8 / 2014-09-14
|
|
3
33
|
==================
|
|
4
34
|
|
|
5
35
|
* bump compressible
|
|
6
|
-
|
|
36
|
+
|
|
7
37
|
1.0.7 / 2014-05-13
|
|
8
38
|
==================
|
|
9
39
|
|
package/README.md
CHANGED
|
@@ -1,41 +1,64 @@
|
|
|
1
|
-
# Koa Compress
|
|
1
|
+
# Koa Compress
|
|
2
|
+
|
|
3
|
+
[](https://github.com/koajs/compress/actions?query=workflow%3A%22Node.js+CI%22+branch%3Amaster)
|
|
4
|
+
[](https://codecov.io/gh/koajs/compress)
|
|
2
5
|
|
|
3
6
|
Compress middleware for Koa
|
|
4
7
|
|
|
5
8
|
## Example
|
|
6
9
|
|
|
7
10
|
```js
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const compress = require('koa-compress')
|
|
12
|
+
const Koa = require('koa')
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
const app = new Koa()
|
|
12
15
|
app.use(compress({
|
|
13
|
-
filter
|
|
16
|
+
filter (content_type) {
|
|
14
17
|
return /text/i.test(content_type)
|
|
15
18
|
},
|
|
16
19
|
threshold: 2048,
|
|
17
|
-
|
|
20
|
+
gzip: {
|
|
21
|
+
flush: require('zlib').Z_SYNC_FLUSH
|
|
22
|
+
},
|
|
23
|
+
deflate: {
|
|
24
|
+
flush: require('zlib').Z_SYNC_FLUSH,
|
|
25
|
+
},
|
|
26
|
+
br: false // disable brotli
|
|
18
27
|
}))
|
|
19
28
|
```
|
|
20
29
|
|
|
21
30
|
## Options
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
### filter\<Function\>
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
```ts
|
|
35
|
+
function (mimeType: string): Boolean {
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
```
|
|
26
39
|
|
|
27
40
|
An optional function that checks the response content type to decide whether to compress.
|
|
28
|
-
By default, it uses [compressible](https://github.com/
|
|
41
|
+
By default, it uses [compressible](https://github.com/jshttp/compressible).
|
|
29
42
|
|
|
30
|
-
### threshold
|
|
43
|
+
### options.threshold\<String|Number\>
|
|
31
44
|
|
|
32
45
|
Minimum response size in bytes to compress.
|
|
33
46
|
Default `1024` bytes or `1kb`.
|
|
34
47
|
|
|
48
|
+
### options[encoding]\<Object\>
|
|
49
|
+
|
|
50
|
+
The current encodings are, in order of preference: `br`, `gzip`, `deflate`.
|
|
51
|
+
Setting `options[encoding] = {}` will pass those options to the encoding function.
|
|
52
|
+
Setting `options[encoding] = false` will disable that encoding.
|
|
53
|
+
|
|
54
|
+
### options.br
|
|
55
|
+
|
|
56
|
+
[Brotli compression](https://en.wikipedia.org/wiki/Brotli) is supported in node v11.7.0+, which includes it natively.
|
|
57
|
+
|
|
35
58
|
## Manually turning compression on and off
|
|
36
59
|
|
|
37
|
-
You can always enable compression by setting `
|
|
38
|
-
You can always disable compression by setting `
|
|
60
|
+
You can always enable compression by setting `ctx.compress = true`.
|
|
61
|
+
You can always disable compression by setting `ctx.compress = false`.
|
|
39
62
|
This bypasses the filter check.
|
|
40
63
|
|
|
41
64
|
```js
|
package/lib/encodings.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
module.exports = Encodings
|
|
7
|
+
|
|
8
|
+
// TODO: convert this to a Class when we drop node@10 support
|
|
9
|
+
// because node@10 does not support class fields.
|
|
10
|
+
function Encodings (options = {}) {
|
|
11
|
+
this.wildcardAcceptEncoding = options.wildcardAcceptEncoding || Encodings.wildcardAcceptEncoding
|
|
12
|
+
this.preferredEncodings = options.preferredEncodings || Encodings.preferredEncodings
|
|
13
|
+
this.reDirective = options.reDirective || Encodings.reDirective
|
|
14
|
+
|
|
15
|
+
this.encodingWeights = new Map()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Encodings.encodingMethods = {
|
|
19
|
+
gzip: zlib.createGzip,
|
|
20
|
+
deflate: zlib.createDeflate,
|
|
21
|
+
br: zlib.createBrotliCompress
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// how we treat `Accept-Encoding: *`
|
|
25
|
+
Encodings.wildcardAcceptEncoding = ['gzip', 'deflate']
|
|
26
|
+
// our preferred encodings
|
|
27
|
+
Encodings.preferredEncodings = ['br', 'gzip', 'deflate']
|
|
28
|
+
Encodings.reDirective = /^\s*(gzip|compress|deflate|br|identity|\*)\s*(?:;\s*q\s*=\s*(\d(?:\.\d)?))?\s*$/
|
|
29
|
+
|
|
30
|
+
Encodings.prototype.parseAcceptEncoding = function (acceptEncoding = '*') {
|
|
31
|
+
const { encodingWeights, reDirective } = this
|
|
32
|
+
|
|
33
|
+
acceptEncoding.split(',').forEach((directive) => {
|
|
34
|
+
const match = reDirective.exec(directive)
|
|
35
|
+
if (!match) return // not a supported encoding above
|
|
36
|
+
|
|
37
|
+
const encoding = match[1]
|
|
38
|
+
|
|
39
|
+
// weight must be in [0, 1]
|
|
40
|
+
let weight = match[2] && !isNaN(match[2]) ? parseFloat(match[2], 10) : 1
|
|
41
|
+
weight = Math.max(weight, 0)
|
|
42
|
+
weight = Math.min(weight, 1)
|
|
43
|
+
|
|
44
|
+
if (encoding === '*') {
|
|
45
|
+
// set the weights for the default encodings
|
|
46
|
+
this.wildcardAcceptEncoding.forEach((enc) => {
|
|
47
|
+
if (!encodingWeights.has(enc)) encodingWeights.set(enc, weight)
|
|
48
|
+
})
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
encodingWeights.set(encoding, weight)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Encodings.prototype.getPreferredContentEncoding = function () {
|
|
57
|
+
const {
|
|
58
|
+
encodingWeights,
|
|
59
|
+
preferredEncodings
|
|
60
|
+
} = this
|
|
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
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// no encoding matches, check to see if the client set identity, q=0
|
|
90
|
+
if (encodingWeights.get('identity') === 0) throw errors(406, 'Please accept br, gzip, deflate, or identity.')
|
|
91
|
+
|
|
92
|
+
// by default, return nothing
|
|
93
|
+
return 'identity'
|
|
94
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Module dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const compressible = require('compressible')
|
|
8
|
+
const isJSON = require('koa-is-json')
|
|
9
|
+
const status = require('statuses')
|
|
10
|
+
const Stream = require('stream')
|
|
11
|
+
const bytes = require('bytes')
|
|
12
|
+
|
|
13
|
+
const Encodings = require('./encodings')
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Regex to match no-transform directive in a cache-control header
|
|
17
|
+
*/
|
|
18
|
+
const NO_TRANSFORM_REGEX = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Compress middleware.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} [options]
|
|
24
|
+
* @return {Function}
|
|
25
|
+
* @api public
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
module.exports = (options = {}) => {
|
|
29
|
+
let { filter = compressible, threshold = 1024 } = options
|
|
30
|
+
if (typeof threshold === 'string') threshold = bytes(threshold)
|
|
31
|
+
|
|
32
|
+
// `options.br = false` would remove it as a preferred encoding
|
|
33
|
+
const preferredEncodings = Encodings.preferredEncodings.filter((encoding) => options[encoding] !== false && options[encoding] !== null)
|
|
34
|
+
|
|
35
|
+
return async (ctx, next) => {
|
|
36
|
+
ctx.vary('Accept-Encoding')
|
|
37
|
+
|
|
38
|
+
await next()
|
|
39
|
+
|
|
40
|
+
// early exit if there's no content body or the body is already encoded
|
|
41
|
+
let { body } = ctx
|
|
42
|
+
if (!body) return
|
|
43
|
+
if (ctx.res.headersSent || !ctx.writable) return
|
|
44
|
+
if (ctx.compress === false) return
|
|
45
|
+
if (ctx.request.method === 'HEAD') return
|
|
46
|
+
if (status.empty[ctx.response.status]) return
|
|
47
|
+
if (ctx.response.get('Content-Encoding')) return
|
|
48
|
+
|
|
49
|
+
// forced compression or implied
|
|
50
|
+
if (!(ctx.compress === true || filter(ctx.response.type))) return
|
|
51
|
+
|
|
52
|
+
// don't compress for Cache-Control: no-transform
|
|
53
|
+
// https://tools.ietf.org/html/rfc7234#section-5.2.1.6
|
|
54
|
+
const cacheControl = ctx.response.get('Cache-Control')
|
|
55
|
+
if (cacheControl && NO_TRANSFORM_REGEX.test(cacheControl)) return
|
|
56
|
+
|
|
57
|
+
// don't compress if the current response is below the threshold
|
|
58
|
+
if (threshold && ctx.response.length < threshold) return
|
|
59
|
+
|
|
60
|
+
// get the preferred content encoding
|
|
61
|
+
const encodings = new Encodings({
|
|
62
|
+
preferredEncodings
|
|
63
|
+
})
|
|
64
|
+
encodings.parseAcceptEncoding(ctx.request.headers['accept-encoding'] || undefined)
|
|
65
|
+
const encoding = encodings.getPreferredContentEncoding()
|
|
66
|
+
|
|
67
|
+
// identity === no compression
|
|
68
|
+
if (encoding === 'identity') return
|
|
69
|
+
|
|
70
|
+
/** begin compression logic **/
|
|
71
|
+
|
|
72
|
+
// json
|
|
73
|
+
if (isJSON(body)) body = ctx.body = JSON.stringify(body)
|
|
74
|
+
|
|
75
|
+
ctx.set('Content-Encoding', encoding)
|
|
76
|
+
ctx.res.removeHeader('Content-Length')
|
|
77
|
+
|
|
78
|
+
const compress = Encodings.encodingMethods[encoding]
|
|
79
|
+
const stream = ctx.body = compress(options[encoding])
|
|
80
|
+
|
|
81
|
+
if (body instanceof Stream) {
|
|
82
|
+
body.pipe(stream)
|
|
83
|
+
} else {
|
|
84
|
+
stream.end(body)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-compress",
|
|
3
3
|
"description": "Compress middleware for koa",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonathan Ong",
|
|
7
7
|
"email": "me@jongleberry.com",
|
|
@@ -11,26 +11,37 @@
|
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"repository": "koajs/compress",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"bytes": "^
|
|
14
|
+
"bytes": "^3.0.0",
|
|
15
15
|
"compressible": "^2.0.0",
|
|
16
|
+
"http-errors": "^1.7.3",
|
|
16
17
|
"koa-is-json": "^1.0.0",
|
|
17
|
-
"statuses": "^
|
|
18
|
+
"statuses": "^2.0.0"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
21
|
+
"eslint": "^6.0.0",
|
|
22
|
+
"eslint-config-standard": "^14.1.1",
|
|
23
|
+
"eslint-plugin-import": "^2.9.0",
|
|
24
|
+
"eslint-plugin-node": "^11.0.0",
|
|
25
|
+
"eslint-plugin-promise": "^4.0.0",
|
|
26
|
+
"eslint-plugin-standard": "^4.0.1",
|
|
27
|
+
"jest": "^25.3.0",
|
|
28
|
+
"koa": "^2.0.1",
|
|
29
|
+
"supertest": "^4.0.2"
|
|
25
30
|
},
|
|
26
31
|
"scripts": {
|
|
27
|
-
"
|
|
28
|
-
"test
|
|
29
|
-
"test-travis": "NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- --require should"
|
|
32
|
+
"eslint": "eslint --ignore-path .gitignore .",
|
|
33
|
+
"test": "jest"
|
|
30
34
|
},
|
|
31
35
|
"files": [
|
|
32
|
-
"
|
|
36
|
+
"lib",
|
|
33
37
|
"LICENSE",
|
|
34
38
|
"HISTORY.md"
|
|
35
|
-
]
|
|
39
|
+
],
|
|
40
|
+
"main": "lib",
|
|
41
|
+
"jest": {
|
|
42
|
+
"testEnvironment": "node"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">= 8.0.0"
|
|
46
|
+
}
|
|
36
47
|
}
|
package/index.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Module dependencies.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
var compressible = require('compressible')
|
|
8
|
-
var isJSON = require('koa-is-json')
|
|
9
|
-
var status = require('statuses')
|
|
10
|
-
var Stream = require('stream')
|
|
11
|
-
var bytes = require('bytes')
|
|
12
|
-
var zlib = require('zlib')
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Encoding methods supported.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
var encodingMethods = {
|
|
19
|
-
gzip: zlib.createGzip,
|
|
20
|
-
deflate: zlib.createDeflate
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Compress middleware.
|
|
25
|
-
*
|
|
26
|
-
* @param {Object} [options]
|
|
27
|
-
* @return {Function}
|
|
28
|
-
* @api public
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
module.exports = (options) => {
|
|
32
|
-
options = options || {}
|
|
33
|
-
|
|
34
|
-
var filter = options.filter || compressible
|
|
35
|
-
|
|
36
|
-
var threshold = !options.threshold ? 1024
|
|
37
|
-
: typeof options.threshold === 'number' ? options.threshold
|
|
38
|
-
: typeof options.threshold === 'string' ? bytes(options.threshold)
|
|
39
|
-
: 1024
|
|
40
|
-
|
|
41
|
-
return function compress(ctx, next) {
|
|
42
|
-
ctx.vary('Accept-Encoding')
|
|
43
|
-
|
|
44
|
-
return next().then(() => {
|
|
45
|
-
var body = ctx.body
|
|
46
|
-
if (!body) return
|
|
47
|
-
if (ctx.compress === false) return
|
|
48
|
-
if (ctx.request.method === 'HEAD') return
|
|
49
|
-
if (status.empty[ctx.response.status]) return
|
|
50
|
-
if (ctx.response.get('Content-Encoding')) return
|
|
51
|
-
|
|
52
|
-
// forced compression or implied
|
|
53
|
-
if (!(ctx.compress === true || filter(ctx.response.type))) return
|
|
54
|
-
|
|
55
|
-
// identity
|
|
56
|
-
var encoding = ctx.acceptsEncodings('gzip', 'deflate', 'identity')
|
|
57
|
-
if (!encoding) ctx.throw(406, 'supported encodings: gzip, deflate, identity')
|
|
58
|
-
if (encoding === 'identity') return
|
|
59
|
-
|
|
60
|
-
// json
|
|
61
|
-
if (isJSON(body)) body = ctx.body = JSON.stringify(body)
|
|
62
|
-
|
|
63
|
-
// threshold
|
|
64
|
-
if (threshold && ctx.response.length < threshold) return
|
|
65
|
-
|
|
66
|
-
ctx.set('Content-Encoding', encoding)
|
|
67
|
-
ctx.res.removeHeader('Content-Length')
|
|
68
|
-
|
|
69
|
-
var stream =
|
|
70
|
-
ctx.body = encodingMethods[encoding](options)
|
|
71
|
-
|
|
72
|
-
if (body instanceof Stream) {
|
|
73
|
-
body.pipe(stream)
|
|
74
|
-
} else {
|
|
75
|
-
stream.end(body)
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
}
|