Skip to content

Commit 0deefb8

Browse files
authored
Functional and local options (#122)
* Refactor to use functional and local options. Passes eslint. Passes the existing tests. * Added tests for functional and local options. * Added the super argument. * Updated docs. * Updated versions: - Node to still supported: 20, 22, 24 - See https://endoflife.date/nodejs - Actions: to v4 - Syntax - Yes, it has changed. * Codecov is deprecated. * Updates of non-working dependencies: - All harness/utilities copied from koajs/koa - Removed unused dependencies. - The rest is upgraded to the latest version. * Removed unused file. * Added standard exceptions to accommodate `jest`. * Removed empty lines at the beginning flagged by `standard`.
1 parent 0852175 commit 0deefb8

File tree

11 files changed

+5732
-7748
lines changed

11 files changed

+5732
-7748
lines changed

.eslintrc

Lines changed: 0 additions & 2 deletions
This file was deleted.

.github/workflows/node.js.yml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ name: Node.js CI
22

33
on:
44
push:
5-
branch: master
5+
branches:
6+
- '*'
67
pull_request:
7-
branch: master
8+
branches:
9+
- master
810

911
jobs:
1012
build:
@@ -13,15 +15,18 @@ jobs:
1315

1416
strategy:
1517
matrix:
16-
node-version: [14.x, 16.x, 18.x]
18+
node-version: [20, 22, 24]
1719

1820
steps:
19-
- uses: actions/checkout@v2
21+
- uses: actions/checkout@v4
2022
- name: Use Node.js ${{ matrix.node-version }}
21-
uses: actions/setup-node@v1
23+
uses: actions/setup-node@v4
2224
with:
2325
node-version: ${{ matrix.node-version }}
2426
- run: npm ci
25-
- run: npm run eslint
26-
- run: npm run test -- --coverage
27-
- run: npx codecov
27+
- run: npm run lint
28+
- run: npm run test:coverage
29+
- name: Upload coverage to Codecov
30+
uses: codecov/codecov-action@v5
31+
with:
32+
token: ${{ secrets.CODECOV_TOKEN }}

README.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,19 @@ function (mimeType: string): Boolean {
4545
An optional function that checks the response content type to decide whether to compress.
4646
By default, it uses [compressible](https://github.com/jshttp/compressible).
4747

48-
### options.threshold\<String|Number\>
48+
### options.threshold\<String|Number|Function\>
4949

50-
Minimum response size in bytes to compress.
50+
Minimum response size in bytes to compress or a function that returns such response (see below).
5151
Default `1024` bytes or `1kb`.
5252

53-
### options[encoding]\<Object\>
53+
### options[encoding]\<Object|Function\>
5454

5555
The current encodings are, in order of preference: `br`, `gzip`, `deflate`.
5656
Setting `options[encoding] = {}` will pass those options to the encoding function.
5757
Setting `options[encoding] = false` will disable that encoding.
5858

59+
It can be a function that returns options (see below).
60+
5961
#### options<span></span>.br
6062

6163
[Brotli compression](https://en.wikipedia.org/wiki/Brotli) is supported in node v11.7.0+, which includes it natively.
@@ -83,3 +85,33 @@ app.use((ctx, next) => {
8385
ctx.body = fs.createReadStream(file)
8486
})
8587
```
88+
89+
`ctx.compress` can be an object similar to `options` above, whose properties (`threshold` and encoding options)
90+
override the global `options` for this response and bypass the filter check.
91+
92+
## Functional properties
93+
94+
Certain properties (`threshold` and encoding options) can be specified as functions. Such functions will be called
95+
for every response with three arguments:
96+
97+
* `type` &mdash; the same as `ctx.response.type` (provided for convenience)
98+
* `size` &mdash; the same as `ctx.response.length` (provided for convenience)
99+
* `ctx` &mdash; the whole context object, if you want to do something unique
100+
101+
It should return a valid value for that property. It is possible to return a function of the same shape,
102+
which will be used to calculate the actual property.
103+
104+
Example:
105+
106+
```js
107+
app.use((ctx, next) => {
108+
// ...
109+
ctx.compress = (type, size, ctx) => ({
110+
br: size && size >= 65536,
111+
gzip: size && size < 65536
112+
})
113+
ctx.body = payload
114+
})
115+
```
116+
117+
Read all about `ctx` in https://koajs.com/#context and `ctx.response` in https://koajs.com/#response

__tests__/.eslintrc

Lines changed: 0 additions & 3 deletions
This file was deleted.

__tests__/defaults.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
const assert = require('assert')
32
const zlib = require('zlib')
43

__tests__/encodings.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
const assert = require('assert')
32

43
const Encodings = require('../lib/encodings')

__tests__/index.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,124 @@ describe('Compress', () => {
387387
assert.strictEqual(res.headers.vary, 'Accept-Encoding')
388388
assert.strictEqual(res.headers['content-encoding'], 'gzip')
389389
})
390+
391+
it('functional threshold: should not compress', (done) => {
392+
const app = new Koa()
393+
394+
app.use(compress({
395+
threshold: () => '1mb'
396+
}))
397+
app.use(sendString)
398+
server = app.listen()
399+
400+
request(server)
401+
.get('/')
402+
.expect(200)
403+
.end((err, res) => {
404+
if (err) { return done(err) }
405+
406+
assert.equal(res.headers['content-length'], '2048')
407+
assert.equal(res.headers.vary, 'Accept-Encoding')
408+
assert(!res.headers['content-encoding'])
409+
assert(!res.headers['transfer-encoding'])
410+
assert.equal(res.text, string)
411+
412+
done()
413+
})
414+
})
415+
416+
it('functional compressors: should not compress', (done) => {
417+
const app = new Koa()
418+
419+
app.use(compress({ br: false, gzip: (type, size) => /^text\//i.test(type) && size > 1000000 }))
420+
app.use(sendBuffer)
421+
server = app.listen()
422+
423+
request(server)
424+
.get('/')
425+
.set('Accept-Encoding', 'br, gzip')
426+
.expect(200)
427+
.end((err, res) => {
428+
if (err) { return done(err) }
429+
430+
assert.equal(res.headers.vary, 'Accept-Encoding')
431+
assert(!res.headers['content-encoding'])
432+
433+
done()
434+
})
435+
})
436+
437+
it('functional compressors: should compress with gzip', (done) => {
438+
const app = new Koa()
439+
440+
app.use(compress({ br: false, gzip: false }))
441+
app.use((ctx) => {
442+
ctx.compress = { gzip: (type, size) => size < 1000000 }
443+
ctx.body = string
444+
})
445+
server = app.listen()
446+
447+
request(server)
448+
.get('/')
449+
.set('Accept-Encoding', 'br, gzip')
450+
.expect(200)
451+
.end((err, res) => {
452+
if (err) { return done(err) }
453+
454+
assert.equal(res.headers.vary, 'Accept-Encoding')
455+
assert.equal(res.headers['content-encoding'], 'gzip')
456+
457+
done()
458+
})
459+
})
460+
461+
it('functional compressors: should NOT compress with gzip', (done) => {
462+
const app = new Koa()
463+
464+
app.use(compress({ br: false, gzip: false }))
465+
app.use((ctx) => {
466+
ctx.compress = { gzip: (type, size) => size < 100 }
467+
ctx.body = string
468+
})
469+
server = app.listen()
470+
471+
request(server)
472+
.get('/')
473+
.set('Accept-Encoding', 'br, gzip')
474+
.expect(200)
475+
.end((err, res) => {
476+
if (err) { return done(err) }
477+
478+
assert.equal(res.headers.vary, 'Accept-Encoding')
479+
assert(!res.headers['content-encoding'])
480+
481+
done()
482+
})
483+
})
484+
485+
it('functional compressors: should compress with br', (done) => {
486+
if (!process.versions.brotli) return done()
487+
488+
const app = new Koa()
489+
490+
app.use(compress({ br: false, gzip: false }))
491+
app.use((ctx) => {
492+
ctx.compress = { gzip: () => false, br: () => true }
493+
ctx.body = string
494+
})
495+
server = app.listen()
496+
497+
request(server)
498+
.get('/')
499+
.set('Accept-Encoding', 'br, gzip')
500+
.expect(200)
501+
.end((err, res) => {
502+
if (err) { return done(err) }
503+
504+
assert.equal(res.headers.vary, 'Accept-Encoding')
505+
assert.equal(res.headers['content-encoding'], 'br')
506+
507+
done()
508+
})
509+
})
390510
})

lib/encodings.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
32
const errors = require('http-errors')
43
const zlib = require('zlib')

lib/index.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ module.exports = (options = {}) => {
5656
await next()
5757

5858
let { body } = ctx
59+
const { type, length: size } = ctx.response
5960
if (
6061
// early exit if there's no content body or the body is already encoded
6162
!body ||
@@ -65,15 +66,30 @@ module.exports = (options = {}) => {
6566
emptyBodyStatues.has(+ctx.response.status) ||
6667
ctx.response.get('Content-Encoding') ||
6768
// forced compression or implied
68-
!(ctx.compress === true || filter(ctx.response.type)) ||
69+
!(ctx.compress === true || filter(type)) ||
6970
// don't compress for Cache-Control: no-transform
7071
// 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)
72+
NO_TRANSFORM_REGEX.test(ctx.response.get('Cache-Control'))
7473
) return
7574

75+
// calculate "local" compression options
76+
const responseOptions = { ...options, ...ctx.compress }
77+
let { threshold = 1024 } = responseOptions
78+
while (typeof threshold === 'function') threshold = threshold(type, size, ctx)
79+
if (typeof threshold === 'string') threshold = bytes(threshold)
80+
81+
// don't compress if the current response is below the threshold
82+
if (threshold && size < threshold) return
83+
7684
// get the preferred content encoding
85+
Encodings.preferredEncodings.forEach((encoding) => {
86+
// calculate compressor options, if any
87+
if (!(encoding in responseOptions)) return
88+
let compressor = responseOptions[encoding]
89+
while (typeof compressor === 'function') compressor = compressor(type, size, ctx)
90+
responseOptions[encoding] = compressor
91+
})
92+
const preferredEncodings = Encodings.preferredEncodings.filter((encoding) => responseOptions[encoding] !== false && responseOptions[encoding] !== null)
7793
const encodings = new Encodings({ preferredEncodings })
7894
encodings.parseAcceptEncoding(ctx.request.headers['accept-encoding'] || defaultEncoding)
7995
const encoding = encodings.getPreferredContentEncoding()
@@ -90,7 +106,7 @@ module.exports = (options = {}) => {
90106
ctx.res.removeHeader('Content-Length')
91107

92108
const compress = Encodings.encodingMethods[encoding]
93-
const stream = ctx.body = compress(encodingOptions[encoding])
109+
const stream = ctx.body = compress(responseOptions[encoding])
94110

95111
if (body instanceof Stream) return body.pipe(stream)
96112
stream.end(body)

0 commit comments

Comments
 (0)