@fastify/compress 5.0.0

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/index.js ADDED
@@ -0,0 +1,556 @@
1
+ 'use strict'
2
+
3
+ const fp = require('fastify-plugin')
4
+ const zlib = require('zlib')
5
+ const pump = require('pump')
6
+ const mimedb = require('mime-db')
7
+ const intoStream = require('into-stream')
8
+ const peek = require('peek-stream')
9
+ const Minipass = require('minipass')
10
+ const pumpify = require('pumpify')
11
+ const isGzip = require('is-gzip')
12
+ const isDeflate = require('is-deflate')
13
+ const encodingNegotiator = require('encoding-negotiator')
14
+ const { inherits, format } = require('util')
15
+
16
+ const { isStream } = require('./lib/utils')
17
+
18
+ const InvalidRequestEncodingError = createError('FST_CP_ERR_INVALID_CONTENT_ENCODING', 'Unsupported Content-Encoding: %s', 415)
19
+ const InvalidRequestCompressedPayloadError = createError('FST_CP_ERR_INVALID_CONTENT', 'Could not decompress the request payload using the provided encoding', 400)
20
+
21
+ function compressPlugin (fastify, opts, next) {
22
+ const globalCompressParams = processCompressParams(opts)
23
+ const globalDecompressParams = processDecompressParams(opts)
24
+
25
+ if (opts.encodings && opts.encodings.length < 1) {
26
+ next(new Error('The `encodings` option array must have at least 1 item.'))
27
+ return
28
+ }
29
+
30
+ if (opts.requestEncodings && opts.requestEncodings.length < 1) {
31
+ next(new Error('The `requestEncodings` option array must have at least 1 item.'))
32
+ return
33
+ }
34
+
35
+ if (globalCompressParams.encodings.length < 1) {
36
+ next(new Error('None of the passed `encodings` were supported — compression not possible.'))
37
+ return
38
+ }
39
+
40
+ if (globalDecompressParams.encodings.length < 1) {
41
+ next(new Error('None of the passed `requestEncodings` were supported — request decompression not possible.'))
42
+ return
43
+ }
44
+
45
+ if (globalDecompressParams.forceEncoding && !globalDecompressParams.encodings.includes(globalDecompressParams.forceEncoding)) {
46
+ next(new Error(`Unsupported decompression encoding ${opts.forceRequestEncoding}.`))
47
+ return
48
+ }
49
+
50
+ fastify.decorateReply('compress', null)
51
+
52
+ // add onSend hook onto each route as needed
53
+ fastify.addHook('onRoute', (routeOptions) => {
54
+ // If route config.compress has been set it takes precedence over compress
55
+ if (routeOptions.config && typeof routeOptions.config.compress !== 'undefined') {
56
+ routeOptions.compress = routeOptions.config.compress
57
+ }
58
+
59
+ // Manage compression options
60
+ if (typeof routeOptions.compress !== 'undefined') {
61
+ if (typeof routeOptions.compress === 'object') {
62
+ const mergedCompressParams = Object.assign(
63
+ {}, globalCompressParams, processCompressParams(routeOptions.compress)
64
+ )
65
+
66
+ // if the current endpoint has a custom compress configuration ...
67
+ buildRouteCompress(fastify, mergedCompressParams, routeOptions)
68
+ } else if (routeOptions.compress === false) {
69
+ // don't apply any compress settings
70
+ } else {
71
+ throw new Error('Unknown value for route compress configuration')
72
+ }
73
+ } else if (globalCompressParams.global) {
74
+ // if the plugin is set globally (meaning that all the routes will be compressed)
75
+ // As the endpoint, does not have a custom rateLimit configuration, use the global one.
76
+ buildRouteCompress(fastify, globalCompressParams, routeOptions)
77
+ } else {
78
+ // if no options are specified and the plugin is not global, then we still want to decorate
79
+ // the reply in this case
80
+ buildRouteCompress(fastify, globalCompressParams, routeOptions, true)
81
+ }
82
+
83
+ // If route config.decompress has been set it takes precedence over compress
84
+ if (routeOptions.config && typeof routeOptions.config.decompress !== 'undefined') {
85
+ routeOptions.decompress = routeOptions.config.decompress
86
+ }
87
+
88
+ // Manage decompression options
89
+ if (typeof routeOptions.decompress !== 'undefined') {
90
+ if (typeof routeOptions.decompress === 'object') {
91
+ // if the current endpoint has a custom compress configuration ...
92
+ const mergedDecompressParams = Object.assign(
93
+ {}, globalDecompressParams, processDecompressParams(routeOptions.decompress)
94
+ )
95
+
96
+ buildRouteDecompress(fastify, mergedDecompressParams, routeOptions)
97
+ } else if (routeOptions.decompress === false) {
98
+ // don't apply any decompress settings
99
+ } else {
100
+ throw new Error('Unknown value for route decompress configuration')
101
+ }
102
+ } else if (globalDecompressParams.global) {
103
+ // if the plugin is set globally (meaning that all the routes will be decompressed)
104
+ // As the endpoint, does not have a custom rateLimit configuration, use the global one.
105
+ buildRouteDecompress(fastify, globalDecompressParams, routeOptions)
106
+ }
107
+ })
108
+
109
+ next()
110
+ }
111
+
112
+ function processCompressParams (opts) {
113
+ /* istanbul ignore next */
114
+ if (!opts) {
115
+ return
116
+ }
117
+
118
+ const params = {
119
+ global: (typeof opts.global === 'boolean') ? opts.global : true
120
+ }
121
+
122
+ params.removeContentLengthHeader = typeof opts.removeContentLengthHeader === 'boolean' ? opts.removeContentLengthHeader : true
123
+ params.brotliOptions = opts.brotliOptions
124
+ params.zlibOptions = opts.zlibOptions
125
+ params.onUnsupportedEncoding = opts.onUnsupportedEncoding
126
+ params.inflateIfDeflated = opts.inflateIfDeflated === true
127
+ params.threshold = typeof opts.threshold === 'number' ? opts.threshold : 1024
128
+ params.compressibleTypes = opts.customTypes instanceof RegExp ? opts.customTypes : /^text\/(?!event-stream)|\+json$|\+text$|\+xml$|octet-stream$/
129
+ params.compressStream = {
130
+ br: () => ((opts.zlib || zlib).createBrotliCompress || zlib.createBrotliCompress)(params.brotliOptions),
131
+ gzip: () => ((opts.zlib || zlib).createGzip || zlib.createGzip)(params.zlibOptions),
132
+ deflate: () => ((opts.zlib || zlib).createDeflate || zlib.createDeflate)(params.zlibOptions)
133
+ }
134
+ params.uncompressStream = {
135
+ // Currently params.uncompressStream.br() is never called as we do not have any way to autodetect brotli compression in `fastify-compress`
136
+ // Brotli documentation reference: [RFC 7932](https://www.rfc-editor.org/rfc/rfc7932)
137
+ br: /* istanbul ignore next */ () => ((opts.zlib || zlib).createBrotliDecompress || zlib.createBrotliDecompress)(params.brotliOptions),
138
+ gzip: () => ((opts.zlib || zlib).createGunzip || zlib.createGunzip)(params.zlibOptions),
139
+ deflate: () => ((opts.zlib || zlib).createInflate || zlib.createInflate)(params.zlibOptions)
140
+ }
141
+
142
+ const supportedEncodings = ['br', 'gzip', 'deflate', 'identity']
143
+
144
+ params.encodings = Array.isArray(opts.encodings)
145
+ ? supportedEncodings
146
+ .filter(encoding => opts.encodings.includes(encoding))
147
+ .sort((a, b) => opts.encodings.indexOf(a) - supportedEncodings.indexOf(b))
148
+ : supportedEncodings
149
+
150
+ return params
151
+ }
152
+
153
+ function processDecompressParams (opts) {
154
+ /* istanbul ignore next */
155
+ if (!opts) {
156
+ return
157
+ }
158
+
159
+ const customZlib = opts.zlib || zlib
160
+
161
+ const params = {
162
+ global: (typeof opts.global === 'boolean') ? opts.global : true,
163
+ onUnsupportedRequestEncoding: opts.onUnsupportedRequestEncoding,
164
+ onInvalidRequestPayload: opts.onInvalidRequestPayload,
165
+ decompressStream: {
166
+ br: customZlib.createBrotliDecompress || zlib.createBrotliDecompress,
167
+ gzip: customZlib.createGunzip || zlib.createGunzip,
168
+ deflate: customZlib.createInflate || zlib.createInflate
169
+ },
170
+ encodings: [],
171
+ forceEncoding: null
172
+ }
173
+
174
+ const supportedEncodings = ['br', 'gzip', 'deflate', 'identity']
175
+
176
+ params.encodings = Array.isArray(opts.requestEncodings)
177
+ ? supportedEncodings
178
+ .filter(encoding => opts.requestEncodings.includes(encoding))
179
+ .sort((a, b) => opts.requestEncodings.indexOf(a) - supportedEncodings.indexOf(b))
180
+ : supportedEncodings
181
+
182
+ if (opts.forceRequestEncoding) {
183
+ params.forceEncoding = opts.forceRequestEncoding
184
+
185
+ if (params.encodings.includes(opts.forceRequestEncoding)) {
186
+ params.encodings = [opts.forceRequestEncoding]
187
+ }
188
+ }
189
+
190
+ return params
191
+ }
192
+
193
+ function buildRouteCompress (fastify, params, routeOptions, decorateOnly) {
194
+ // In order to provide a compress method with the same parameter set as the route itself,
195
+ // we decorate the reply at the start of the request
196
+ if (Array.isArray(routeOptions.onRequest)) {
197
+ routeOptions.onRequest.push(onRequest)
198
+ } else if (typeof routeOptions.onRequest === 'function') {
199
+ routeOptions.onRequest = [routeOptions.onRequest, onRequest]
200
+ } else {
201
+ routeOptions.onRequest = [onRequest]
202
+ }
203
+
204
+ const compressFn = compress(params)
205
+ function onRequest (req, reply, next) {
206
+ reply.compress = compressFn
207
+ next()
208
+ }
209
+
210
+ if (decorateOnly) {
211
+ return
212
+ }
213
+
214
+ if (Array.isArray(routeOptions.onSend)) {
215
+ routeOptions.onSend.push(onSend)
216
+ } else if (typeof routeOptions.onSend === 'function') {
217
+ routeOptions.onSend = [routeOptions.onSend, onSend]
218
+ } else {
219
+ routeOptions.onSend = [onSend]
220
+ }
221
+
222
+ function onSend (req, reply, payload, next) {
223
+ if (payload == null) {
224
+ return next()
225
+ }
226
+ const responseEncoding = reply.getHeader('Content-Encoding')
227
+ if (responseEncoding && responseEncoding !== 'identity') {
228
+ // response is already compressed
229
+ return next()
230
+ }
231
+ setVaryHeader(reply)
232
+
233
+ let stream, encoding
234
+ const noCompress =
235
+ // don't compress on x-no-compression header
236
+ (req.headers['x-no-compression'] !== undefined) ||
237
+ // don't compress if not one of the indicated compressible types
238
+ (shouldCompress(reply.getHeader('Content-Type') || 'application/json', params.compressibleTypes) === false) ||
239
+ // don't compress on missing or identity `accept-encoding` header
240
+ ((encoding = getEncodingHeader(params.encodings, req)) == null || encoding === 'identity')
241
+
242
+ if (encoding == null && params.onUnsupportedEncoding != null) {
243
+ const encodingHeader = req.headers['accept-encoding']
244
+ try {
245
+ const errorPayload = params.onUnsupportedEncoding(encodingHeader, reply.request, reply)
246
+ return next(null, errorPayload)
247
+ } catch (err) {
248
+ return next(err)
249
+ }
250
+ }
251
+
252
+ if (noCompress) {
253
+ if (params.inflateIfDeflated && isStream(stream = maybeUnzip(payload))) {
254
+ encoding === undefined
255
+ ? reply.removeHeader('Content-Encoding')
256
+ : reply.header('Content-Encoding', 'identity')
257
+ pump(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(reply))
258
+ }
259
+ return next(null, payload)
260
+ }
261
+
262
+ if (typeof payload.pipe !== 'function') {
263
+ if (Buffer.byteLength(payload) < params.threshold) {
264
+ return next()
265
+ }
266
+ payload = intoStream(payload)
267
+ }
268
+
269
+ params.removeContentLengthHeader
270
+ ? reply
271
+ .header('Content-Encoding', encoding)
272
+ .removeHeader('content-length')
273
+ : reply.header('Content-Encoding', encoding)
274
+
275
+ stream = zipStream(params.compressStream, encoding)
276
+ pump(payload, stream, onEnd.bind(reply))
277
+ next(null, stream)
278
+ }
279
+ }
280
+
281
+ function buildRouteDecompress (fastify, params, routeOptions) {
282
+ // Add our decompress handler in the preParsing hook
283
+ if (Array.isArray(routeOptions.preParsing)) {
284
+ routeOptions.preParsing.unshift(preParsing)
285
+ } else if (typeof routeOptions.preParsing === 'function') {
286
+ routeOptions.preParsing = [preParsing, routeOptions.preParsing]
287
+ } else {
288
+ routeOptions.preParsing = [preParsing]
289
+ }
290
+
291
+ function preParsing (request, reply, raw, next) {
292
+ // Get the encoding from the options or from the headers
293
+ let encoding = params.forceEncoding
294
+
295
+ if (!encoding) {
296
+ encoding = request.headers['content-encoding']
297
+ }
298
+
299
+ // The request is not compressed, nothing to do here
300
+ if (!encoding) {
301
+ return next(null, raw)
302
+ }
303
+
304
+ // Check that encoding is supported
305
+ if (!params.encodings.includes(encoding)) {
306
+ let errorPayload
307
+
308
+ if (params.onUnsupportedRequestEncoding) {
309
+ try {
310
+ errorPayload = params.onUnsupportedRequestEncoding(encoding, request)
311
+ } catch (ex) {
312
+ errorPayload = undefined
313
+ }
314
+ }
315
+
316
+ if (!errorPayload) {
317
+ errorPayload = new InvalidRequestEncodingError(encoding)
318
+ }
319
+
320
+ return next(errorPayload)
321
+ }
322
+
323
+ // No action on identity
324
+ if (encoding === 'identity') {
325
+ return next(null, raw)
326
+ }
327
+
328
+ // Prepare decompression - If there is a decompress error, prepare the error for fastify handing
329
+ const decompresser = params.decompressStream[encoding]()
330
+ decompresser.receivedEncodedLength = 0
331
+ decompresser.on('error', onDecompressError.bind(this, request, params, encoding))
332
+ decompresser.pause()
333
+
334
+ // Track length of encoded length to handle receivedEncodedLength
335
+ raw.on('data', trackEncodedLength.bind(decompresser))
336
+ raw.on('end', removeEncodedLengthTracking)
337
+
338
+ next(null, pump(raw, decompresser))
339
+ }
340
+ }
341
+
342
+ function compress (params) {
343
+ return function (payload) {
344
+ if (payload == null) {
345
+ this.send(new Error('Internal server error'))
346
+ return
347
+ }
348
+
349
+ setVaryHeader(this)
350
+ let stream, encoding
351
+ const noCompress =
352
+ // don't compress on x-no-compression header
353
+ (this.request.headers['x-no-compression'] !== undefined) ||
354
+ // don't compress if not one of the indicated compressible types
355
+ (shouldCompress(this.getHeader('Content-Type') || 'application/json', params.compressibleTypes) === false) ||
356
+ // don't compress on missing or identity `accept-encoding` header
357
+ ((encoding = getEncodingHeader(params.encodings, this.request)) == null || encoding === 'identity')
358
+
359
+ if (encoding == null && params.onUnsupportedEncoding != null) {
360
+ const encodingHeader = this.request.headers['accept-encoding']
361
+
362
+ let errorPayload
363
+ try {
364
+ errorPayload = params.onUnsupportedEncoding(encodingHeader, this.request, this)
365
+ } catch (ex) {
366
+ errorPayload = ex
367
+ }
368
+ return this.send(errorPayload)
369
+ }
370
+
371
+ if (noCompress) {
372
+ if (params.inflateIfDeflated && isStream(stream = maybeUnzip(payload, this.serialize.bind(this)))) {
373
+ encoding === undefined
374
+ ? this.removeHeader('Content-Encoding')
375
+ : this.header('Content-Encoding', 'identity')
376
+ pump(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(this))
377
+ }
378
+ return this.send(payload)
379
+ }
380
+
381
+ if (typeof payload.pipe !== 'function') {
382
+ if (!Buffer.isBuffer(payload) && typeof payload !== 'string') {
383
+ payload = this.serialize(payload)
384
+ }
385
+ }
386
+
387
+ if (typeof payload.pipe !== 'function') {
388
+ if (Buffer.byteLength(payload) < params.threshold) {
389
+ return this.send(payload)
390
+ }
391
+ payload = intoStream(payload)
392
+ }
393
+
394
+ params.removeContentLengthHeader
395
+ ? this
396
+ .header('Content-Encoding', encoding)
397
+ .removeHeader('content-length')
398
+ : this.header('Content-Encoding', encoding)
399
+
400
+ stream = zipStream(params.compressStream, encoding)
401
+ pump(payload, stream, onEnd.bind(this))
402
+ this.send(stream)
403
+ }
404
+ }
405
+
406
+ function setVaryHeader (reply) {
407
+ if (reply.hasHeader('Vary')) {
408
+ const varyHeader = Array.isArray(reply.getHeader('Vary')) ? reply.getHeader('Vary') : [reply.getHeader('Vary')]
409
+ if (!varyHeader.some((h) => h.includes('accept-encoding'))) {
410
+ reply.header('Vary', `${varyHeader.join(', ')}, accept-encoding`)
411
+ }
412
+ } else {
413
+ reply.header('Vary', 'accept-encoding')
414
+ }
415
+ }
416
+
417
+ function onEnd (err) {
418
+ if (err) this.log.error(err)
419
+ }
420
+
421
+ function trackEncodedLength (chunk) {
422
+ this.receivedEncodedLength += chunk.length
423
+ }
424
+
425
+ function removeEncodedLengthTracking () {
426
+ this.removeListener('data', trackEncodedLength)
427
+ this.removeListener('end', removeEncodedLengthTracking)
428
+ }
429
+
430
+ function onDecompressError (request, params, encoding, error) {
431
+ this.log.debug(`compress: invalid request payload - ${error}`)
432
+
433
+ let errorPayload
434
+
435
+ if (params.onInvalidRequestPayload) {
436
+ try {
437
+ errorPayload = params.onInvalidRequestPayload(encoding, request, error)
438
+ } catch (ex) {
439
+ errorPayload = undefined
440
+ }
441
+ }
442
+
443
+ if (!errorPayload) {
444
+ errorPayload = new InvalidRequestCompressedPayloadError()
445
+ }
446
+
447
+ error.decompressError = error
448
+ Object.assign(error, errorPayload)
449
+ }
450
+
451
+ function getEncodingHeader (encodings, request) {
452
+ let header = request.headers['accept-encoding']
453
+ if (header != null) {
454
+ header = header.toLowerCase()
455
+ // consider the no-preference token as gzip for downstream compat
456
+ // and x-gzip as an alias of gzip
457
+ // ref.: [HTTP/1.1 RFC 7230 section 4.2.3](https://datatracker.ietf.org/doc/html/rfc7230#section-4.2.3)
458
+ .replace(/\*|x-gzip/g, 'gzip')
459
+ return encodingNegotiator.negotiate(header, encodings)
460
+ } else {
461
+ return undefined
462
+ }
463
+ }
464
+
465
+ function shouldCompress (type, compressibleTypes) {
466
+ if (compressibleTypes.test(type)) return true
467
+ const data = mimedb[type.split(';', 1)[0].trim().toLowerCase()]
468
+ if (data === undefined) return false
469
+ return data.compressible === true
470
+ }
471
+
472
+ function isCompressed (data) {
473
+ if (isGzip(data)) return 1
474
+ if (isDeflate(data)) return 2
475
+ return 0
476
+ }
477
+
478
+ function maybeUnzip (payload, serialize) {
479
+ if (isStream(payload)) return payload
480
+
481
+ let buf = payload; let result = payload
482
+
483
+ if (ArrayBuffer.isView(payload)) {
484
+ // Cast non-Buffer DataViews into a Buffer
485
+ buf = result = Buffer.from(
486
+ payload.buffer,
487
+ payload.byteOffset,
488
+ payload.byteLength
489
+ )
490
+ } else if (serialize && typeof payload !== 'string') {
491
+ buf = result = serialize(payload)
492
+ }
493
+
494
+ // handle case where serialize doesn't return a string or Buffer
495
+ if (!Buffer.isBuffer(buf)) return result
496
+ if (isCompressed(buf) === 0) return result
497
+ return intoStream(result)
498
+ }
499
+
500
+ function zipStream (deflate, encoding) {
501
+ return peek({ newline: false, maxBuffer: 10 }, function (data, swap) {
502
+ switch (isCompressed(data)) {
503
+ case 1: return swap(null, new Minipass())
504
+ case 2: return swap(null, new Minipass())
505
+ }
506
+ return swap(null, deflate[encoding]())
507
+ })
508
+ }
509
+
510
+ function unzipStream (inflate, maxRecursion) {
511
+ if (!(maxRecursion >= 0)) maxRecursion = 3
512
+ return peek({ newline: false, maxBuffer: 10 }, function (data, swap) {
513
+ /* istanbul ignore if */
514
+ // This path is never taken, when `maxRecursion` < 0 it is automatically set back to 3
515
+ if (maxRecursion < 0) return swap(new Error('Maximum recursion reached'))
516
+ switch (isCompressed(data)) {
517
+ case 1: return swap(null, pumpify(inflate.gzip(), unzipStream(inflate, maxRecursion - 1)))
518
+ case 2: return swap(null, pumpify(inflate.deflate(), unzipStream(inflate, maxRecursion - 1)))
519
+ }
520
+ return swap(null, new Minipass())
521
+ })
522
+ }
523
+
524
+ function createError (code, message, statusCode) {
525
+ code = code.toUpperCase()
526
+
527
+ function FastifyCompressError (a) {
528
+ Error.captureStackTrace(this, FastifyCompressError)
529
+ this.name = 'FastifyCompressError'
530
+ this.code = code
531
+
532
+ if (a) {
533
+ this.message = format(message, a)
534
+ } else {
535
+ this.message = message
536
+ }
537
+
538
+ this.statusCode = statusCode
539
+ }
540
+
541
+ FastifyCompressError.prototype[Symbol.toStringTag] = 'Error'
542
+
543
+ /* istanbul ignore next */
544
+ FastifyCompressError.prototype.toString = function () {
545
+ return `${this.name} [${this.code}]: ${this.message}`
546
+ }
547
+
548
+ inherits(FastifyCompressError, Error)
549
+
550
+ return FastifyCompressError
551
+ }
552
+
553
+ module.exports = fp(compressPlugin, {
554
+ fastify: '3.x',
555
+ name: 'fastify-compress'
556
+ })
package/lib/utils.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function isStream (stream) {
4
+ return stream !== null && typeof stream === 'object' && typeof stream.pipe === 'function'
5
+ }
6
+
7
+ module.exports = { isStream }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@fastify/compress",
3
+ "version": "5.0.0",
4
+ "description": "Fastify compression utils",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "dependencies": {
8
+ "encoding-negotiator": "^2.0.1",
9
+ "fastify-plugin": "^3.0.0",
10
+ "into-stream": "^6.0.0",
11
+ "is-deflate": "^1.0.0",
12
+ "is-gzip": "^2.0.0",
13
+ "is-zip": "^1.0.0",
14
+ "mime-db": "^1.51.0",
15
+ "minipass": "^3.1.6",
16
+ "peek-stream": "^1.1.3",
17
+ "pump": "^3.0.0",
18
+ "pumpify": "^2.0.1",
19
+ "string-to-stream": "^3.0.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^17.0.8",
23
+ "@typescript-eslint/eslint-plugin": "^5.9.0",
24
+ "@typescript-eslint/parser": "^5.9.0",
25
+ "adm-zip": "^0.5.9",
26
+ "fastify": "^3.25.3",
27
+ "jsonstream": "^1.0.3",
28
+ "pre-commit": "^1.2.2",
29
+ "standard": "^16.0.4",
30
+ "tap": "^16.0.0",
31
+ "tsd": "^0.20.0",
32
+ "typescript": "^4.5.4"
33
+ },
34
+ "scripts": {
35
+ "coverage": "npm run unit -- --cov",
36
+ "coverage-report": "npm run coverage -- --coverage-report=lcov",
37
+ "lint": "standard",
38
+ "lint:fix": "npm run lint -- --fix",
39
+ "lint:typescript": "npm run lint:fix -- --parser @typescript-eslint/parser --plugin typescript test/types/*.ts",
40
+ "lint:types": "npm run lint:fix -- --parser @typescript-eslint/parser --plugin typescript *.d.ts",
41
+ "test": "npm run lint && npm run unit && npm run test:typescript",
42
+ "test:typescript": "tsd",
43
+ "unit": "tap -J test/*.test.js",
44
+ "unit:report": "npm run unit -- --cov --coverage-report=html",
45
+ "unit:verbose": "npm run unit -- -Rspec"
46
+ },
47
+ "keywords": [
48
+ "fastify",
49
+ "compression",
50
+ "deflate",
51
+ "gzip",
52
+ "brotli"
53
+ ],
54
+ "author": "Tomas Della Vedova - @delvedor (http://delved.org)",
55
+ "license": "MIT",
56
+ "bugs": {
57
+ "url": "https://github.com/fastify/fastify-compress/issues"
58
+ },
59
+ "homepage": "https://github.com/fastify/fastify-compress#readme",
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/fastify/fastify-compress.git"
63
+ },
64
+ "engines": {
65
+ "node": ">=10.16"
66
+ },
67
+ "tsd": {
68
+ "directory": "test/types"
69
+ },
70
+ "publishConfig": {
71
+ "access": "public"
72
+ }
73
+ }