koa-compress 3.1.0 → 5.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 CHANGED
@@ -1,5 +1,19 @@
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
+
2
15
  3.1.0 / 2020-04-15
16
+ ==================
3
17
 
4
18
  * support no-transform @Pawda
5
19
 
package/README.md CHANGED
@@ -8,33 +8,63 @@ Compress middleware for Koa
8
8
  ## Example
9
9
 
10
10
  ```js
11
- var compress = require('koa-compress')
12
- var Koa = require('koa')
11
+ const compress = require('koa-compress')
12
+ const Koa = require('koa')
13
13
 
14
- var app = new Koa()
14
+ const app = new Koa()
15
15
  app.use(compress({
16
- filter: function (content_type) {
16
+ filter (content_type) {
17
17
  return /text/i.test(content_type)
18
18
  },
19
19
  threshold: 2048,
20
- flush: require('zlib').Z_SYNC_FLUSH
20
+ gzip: {
21
+ flush: require('zlib').constants.Z_SYNC_FLUSH
22
+ },
23
+ deflate: {
24
+ flush: require('zlib').constants.Z_SYNC_FLUSH,
25
+ },
26
+ br: false // disable brotli
21
27
  }))
22
28
  ```
23
29
 
24
30
  ## Options
25
31
 
26
- The options are passed to `zlib`: http://nodejs.org/api/zlib.html#zlib_options
32
+ ### filter\<Function\>
33
+
34
+ ```ts
35
+ function (mimeType: string): Boolean {
27
36
 
28
- ### filter
37
+ }
38
+ ```
29
39
 
30
40
  An optional function that checks the response content type to decide whether to compress.
31
41
  By default, it uses [compressible](https://github.com/jshttp/compressible).
32
42
 
33
- ### threshold
43
+ ### options.threshold\<String|Number\>
34
44
 
35
45
  Minimum response size in bytes to compress.
36
46
  Default `1024` bytes or `1kb`.
37
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<span></span>.br
55
+
56
+ [Brotli compression](https://en.wikipedia.org/wiki/Brotli) is supported in node v11.7.0+, which includes it natively.
57
+
58
+ ### options.defaultEncoding\<String\>
59
+
60
+ An optional string, which specifies what encoders to use for requests without
61
+ [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding).
62
+ Default `idenity`.
63
+
64
+ The standard dictates to treat such requests as `*` meaning that all compressions are permissible,
65
+ yet it causes very practical problems when debugging servers with manual tools like `curl`, `wget`, and so on.
66
+ If you want to enable the standard behavior, just set `defaultEncoding` to `*`.
67
+
38
68
  ## Manually turning compression on and off
39
69
 
40
70
  You can always enable compression by setting `ctx.compress = true`.
@@ -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
+ }
@@ -9,16 +9,8 @@ const isJSON = require('koa-is-json')
9
9
  const status = require('statuses')
10
10
  const Stream = require('stream')
11
11
  const bytes = require('bytes')
12
- const zlib = require('zlib')
13
12
 
14
- /**
15
- * Encoding methods supported.
16
- */
17
-
18
- const encodingMethods = {
19
- gzip: zlib.createGzip,
20
- deflate: zlib.createDeflate
21
- }
13
+ const Encodings = require('./encodings')
22
14
 
23
15
  /**
24
16
  * Regex to match no-transform directive in a cache-control header
@@ -34,14 +26,18 @@ const NO_TRANSFORM_REGEX = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
34
26
  */
35
27
 
36
28
  module.exports = (options = {}) => {
37
- let { filter = compressible, threshold = 1024 } = options
29
+ let { filter = compressible, threshold = 1024, defaultEncoding = 'identity' } = options
38
30
  if (typeof threshold === 'string') threshold = bytes(threshold)
39
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
+
40
35
  return async (ctx, next) => {
41
36
  ctx.vary('Accept-Encoding')
42
37
 
43
38
  await next()
44
39
 
40
+ // early exit if there's no content body or the body is already encoded
45
41
  let { body } = ctx
46
42
  if (!body) return
47
43
  if (ctx.res.headersSent || !ctx.writable) return
@@ -53,26 +49,34 @@ module.exports = (options = {}) => {
53
49
  // forced compression or implied
54
50
  if (!(ctx.compress === true || filter(ctx.response.type))) return
55
51
 
56
- // Don't compress for Cache-Control: no-transform
52
+ // don't compress for Cache-Control: no-transform
57
53
  // https://tools.ietf.org/html/rfc7234#section-5.2.1.6
58
54
  const cacheControl = ctx.response.get('Cache-Control')
59
55
  if (cacheControl && NO_TRANSFORM_REGEX.test(cacheControl)) return
60
56
 
61
- // identity
62
- const encoding = ctx.acceptsEncodings('gzip', 'deflate', 'identity')
63
- if (!encoding) ctx.throw(406, 'supported encodings: gzip, deflate, identity')
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'] || defaultEncoding)
65
+ const encoding = encodings.getPreferredContentEncoding()
66
+
67
+ // identity === no compression
64
68
  if (encoding === 'identity') return
65
69
 
70
+ /** begin compression logic **/
71
+
66
72
  // json
67
73
  if (isJSON(body)) body = ctx.body = JSON.stringify(body)
68
74
 
69
- // threshold
70
- if (threshold && ctx.response.length < threshold) return
71
-
72
75
  ctx.set('Content-Encoding', encoding)
73
76
  ctx.res.removeHeader('Content-Length')
74
77
 
75
- const stream = ctx.body = encodingMethods[encoding](options)
78
+ const compress = Encodings.encodingMethods[encoding]
79
+ const stream = ctx.body = compress(options[encoding])
76
80
 
77
81
  if (body instanceof Stream) {
78
82
  body.pipe(stream)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koa-compress",
3
3
  "description": "Compress middleware for koa",
4
- "version": "3.1.0",
4
+ "version": "5.0.1",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "me@jongleberry.com",
@@ -13,8 +13,9 @@
13
13
  "dependencies": {
14
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": "^1.0.0"
18
+ "statuses": "^2.0.0"
18
19
  },
19
20
  "devDependencies": {
20
21
  "eslint": "^6.0.0",
@@ -29,13 +30,14 @@
29
30
  },
30
31
  "scripts": {
31
32
  "eslint": "eslint --ignore-path .gitignore .",
32
- "test": "jest --forceExit"
33
+ "test": "jest"
33
34
  },
34
35
  "files": [
35
- "index.js",
36
+ "lib",
36
37
  "LICENSE",
37
38
  "HISTORY.md"
38
39
  ],
40
+ "main": "lib",
39
41
  "jest": {
40
42
  "testEnvironment": "node"
41
43
  },