-
-
Notifications
You must be signed in to change notification settings - Fork 173
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
@polka/compression
package (#148)
* Add @polka/compression package * skip brotli tests on unsupported node versions * allow Node>=6 * add stream piping test * kick CI * debug: drop action cache * debug: reattach cache step * fix: always call `writeHead` step - see preactjs/wmr#648 * chore: backport fixes; - now aligns w/ current wmr (and vite) impls * feat: add dual esm/cjs typescript definitions * chore: test types --------- Co-authored-by: Luke Edwards <luke.edwards05@gmail.com>
- Loading branch information
Showing
7 changed files
with
504 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import type { IncomingMessage, ServerResponse } from 'node:http'; | ||
|
||
export type Options = { | ||
/** | ||
* Don't compress responses below this size (in bytes). | ||
* @default 1024 | ||
*/ | ||
threshold?: number; | ||
/** | ||
* Gzip/Brotli compression effort (1-11, or -1 for default) | ||
* @default -1 | ||
*/ | ||
level?: number; | ||
/** | ||
* Generate and serve Brotli-compressed responses. | ||
* @default false | ||
*/ | ||
brotli?: boolean; | ||
/** | ||
* Generate and serve Gzip-compressed responses. | ||
* @default true | ||
*/ | ||
gzip?: boolean; | ||
/** | ||
* Regular expression of response MIME types to compress. | ||
* @default /text|javascript|\/json|xml/i | ||
*/ | ||
mimes?: RegExp; | ||
}; | ||
|
||
export type Middleware = ( | ||
request: Pick<IncomingMessage, 'method' | 'headers'>, | ||
response: ServerResponse, | ||
next?: (error?: Error | string) => any, | ||
) => void; | ||
|
||
export default function (options?: Options): Middleware; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import type { IncomingMessage, ServerResponse } from 'node:http'; | ||
|
||
declare namespace compression { | ||
export type Options = { | ||
/** | ||
* Don't compress responses below this size (in bytes). | ||
* @default 1024 | ||
*/ | ||
threshold?: number; | ||
/** | ||
* Gzip/Brotli compression effort (1-11, or -1 for default) | ||
* @default -1 | ||
*/ | ||
level?: number; | ||
/** | ||
* Generate and serve Brotli-compressed responses. | ||
* @default false | ||
*/ | ||
brotli?: boolean; | ||
/** | ||
* Generate and serve Gzip-compressed responses. | ||
* @default true | ||
*/ | ||
gzip?: boolean; | ||
/** | ||
* Regular expression of response MIME types to compress. | ||
* @default /text|javascript|\/json|xml/i | ||
*/ | ||
mimes?: RegExp; | ||
}; | ||
|
||
export type Middleware = ( | ||
request: Pick<IncomingMessage, 'method' | 'headers'>, | ||
response: ServerResponse, | ||
next?: (error?: Error | string) => any, | ||
) => void; | ||
} | ||
|
||
declare function compression(options?: compression.Options): compression.Middleware; | ||
|
||
export = compression; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// NOTE: supports Node 6.x | ||
|
||
import zlib from 'zlib'; | ||
|
||
const NOOP = () => {}; | ||
const MIMES = /text|javascript|\/json|xml/i; | ||
|
||
/** | ||
* @param {any} chunk | ||
* @param {BufferEncoding} enc | ||
* @returns {number} | ||
*/ | ||
function getChunkSize(chunk, enc) { | ||
return chunk ? Buffer.byteLength(chunk, enc) : 0; | ||
} | ||
|
||
/** | ||
* @param {import('./index.d.mts').Options} [options] | ||
* @returns {import('./index.d.mts').Middleware} | ||
*/ | ||
export default function ({ threshold = 1024, level = -1, brotli = false, gzip = true, mimes = MIMES } = {}) { | ||
const brotliOpts = (typeof brotli === 'object' && brotli) || {}; | ||
const gzipOpts = (typeof gzip === 'object' && gzip) || {}; | ||
|
||
// disable Brotli on Node<12.7 where it is unsupported: | ||
if (!zlib.createBrotliCompress) brotli = false; | ||
|
||
return (req, res, next = NOOP) => { | ||
const accept = req.headers['accept-encoding'] + ''; | ||
const encoding = ((brotli && accept.match(/\bbr\b/)) || (gzip && accept.match(/\bgzip\b/)) || [])[0]; | ||
|
||
// skip if no response body or no supported encoding: | ||
if (req.method === 'HEAD' || !encoding) return next(); | ||
|
||
/** @type {zlib.Gzip | zlib.BrotliCompress} */ | ||
let compress; | ||
/** @type {Array<[string, function]>?} */ | ||
let pendingListeners = []; | ||
let pendingStatus = 0; | ||
let started = false; | ||
let size = 0; | ||
|
||
function start() { | ||
started = true; | ||
// @ts-ignore | ||
size = res.getHeader('Content-Length') | 0 || size; | ||
const compressible = mimes.test( | ||
String(res.getHeader('Content-Type') || 'text/plain') | ||
); | ||
const cleartext = !res.getHeader('Content-Encoding'); | ||
const listeners = pendingListeners || []; | ||
|
||
if (compressible && cleartext && size >= threshold) { | ||
res.setHeader('Content-Encoding', encoding); | ||
res.removeHeader('Content-Length'); | ||
if (encoding === 'br') { | ||
compress = zlib.createBrotliCompress({ | ||
params: Object.assign({ | ||
[zlib.constants.BROTLI_PARAM_QUALITY]: level, | ||
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: size, | ||
}, brotliOpts) | ||
}); | ||
} else { | ||
compress = zlib.createGzip( | ||
Object.assign({ level }, gzipOpts) | ||
); | ||
} | ||
// backpressure | ||
compress.on('data', chunk => write.call(res, chunk) || compress.pause()); | ||
on.call(res, 'drain', () => compress.resume()); | ||
compress.on('end', () => end.call(res)); | ||
listeners.forEach(p => compress.on.apply(compress, p)); | ||
} else { | ||
pendingListeners = null; | ||
listeners.forEach(p => on.apply(res, p)); | ||
} | ||
|
||
writeHead.call(res, pendingStatus || res.statusCode); | ||
} | ||
|
||
const { end, write, on, writeHead } = res; | ||
|
||
res.writeHead = function (status, reason, headers) { | ||
if (typeof reason !== 'string') [headers, reason] = [reason, headers]; | ||
if (headers) for (let k in headers) res.setHeader(k, headers[k]); | ||
pendingStatus = status; | ||
return this; | ||
}; | ||
|
||
res.write = function (chunk, enc) { | ||
size += getChunkSize(chunk, enc); | ||
if (!started) start(); | ||
if (!compress) return write.apply(this, arguments); | ||
return compress.write.apply(compress, arguments); | ||
}; | ||
|
||
res.end = function (chunk, enc) { | ||
if (arguments.length > 0 && typeof chunk !== 'function') { | ||
size += getChunkSize(chunk, enc); | ||
} | ||
if (!started) start(); | ||
if (!compress) return end.apply(this, arguments); | ||
return compress.end.apply(compress, arguments); | ||
}; | ||
|
||
res.on = function (type, listener) { | ||
if (!pendingListeners) on.call(this, type, listener); | ||
else if (compress) compress.on(type, listener); | ||
else pendingListeners.push([type, listener]); | ||
return this; | ||
}; | ||
|
||
next(); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"version": "1.0.0-next.11", | ||
"name": "@polka/compression", | ||
"repository": "lukeed/polka", | ||
"description": "Fast gzip+brotli compression middleware for polka & express with zero dependencies.", | ||
"homepage": "https://github.com/lukeed/polka/tree/next/packages/compression", | ||
"module": "build.mjs", | ||
"types": "index.d.ts", | ||
"main": "build.js", | ||
"license": "MIT", | ||
"exports": { | ||
".": { | ||
"import": { | ||
"types": "./index.d.mts", | ||
"default": "./build.mjs" | ||
}, | ||
"require": { | ||
"types": "./index.d.ts", | ||
"default": "./build.js" | ||
} | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
"files": [ | ||
"build.*", | ||
"index.d.*" | ||
], | ||
"authors": [ | ||
"Jason Miller (https://github.com/developit)" | ||
], | ||
"engines": { | ||
"node": ">=6" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
# @polka/compression [![npm](https://badgen.now.sh/npm/v/@polka/compression)](https://npmjs.org/package/@polka/compression) | ||
|
||
> An HTTP response compression middleware that supports native Gzip and Brotli. Works with [Polka][polka] and Express! | ||
|
||
## Install | ||
|
||
``` | ||
$ npm install --save @polka/compression | ||
``` | ||
|
||
|
||
## Usage | ||
|
||
```js | ||
const polka = require('polka'); | ||
const compression = require('@polka/compression'); | ||
|
||
polka() | ||
.use(compression({ /* see options below */ })) | ||
.use((req, res) => { | ||
// this will get compressed: | ||
res.end('hello world!'.repeat(1000)); | ||
}) | ||
.listen(); | ||
``` | ||
|
||
|
||
## API | ||
|
||
The `compression(options)` function returns a polka/express -style middleware of the form `(req, res, next)`. | ||
|
||
### Options | ||
|
||
* @param {number} [options.threshold = 1024] Don't compress responses below this size (in bytes) | ||
* @param {number} [options.level = -1] Gzip/Brotli compression effort (1-11, or -1 for default) | ||
* @param {boolean} [options.brotli = false] Generate and serve Brotli-compressed responses | ||
* @param {boolean} [options.gzip = true] Generate and serve Gzip-compressed responses | ||
* @param {RegExp} [options.mimes] Regular expression of response MIME types to compress (default: text|javascript|json|xml) | ||
|
||
#### threshold | ||
Type: `Number`<br> | ||
Default: `1024` | ||
|
||
Responses below this threshold (in bytes) are not compressed. The default value of `1024` is recommended, and avoids sharply diminishing compression returns. | ||
|
||
#### level | ||
Type: `Number`<br> | ||
Default: `-1` | ||
|
||
The compression effort/level/quality setting, used by both Gzip and Brotli. The scale ranges from 1 to 11, where lower values are faster and higher values produce smaller output. The default value of `-1` uses the default compression level as defined by Gzip (6) and Brotli (6). | ||
|
||
#### brotli | ||
Type: `boolean`<br> | ||
Default: `false` | ||
|
||
Enables response compression using Brotli for requests that support it. This is not enabled by default because Brotli incurs more performance overhead than Gzip. | ||
|
||
#### gzip | ||
Type: `boolean`<br> | ||
Default: `true` | ||
|
||
Enables response compression using Gzip for requests that support it, as determined by the `Accept-Encoding` request header. | ||
|
||
#### mimes | ||
Type: `RegExp`<br> | ||
Default: `/text|javascript|\/json|xml/i` | ||
|
||
The `Content-Type` response header is evaluated against this Regular Expression to determine if it is a MIME type that should be compressed. | ||
Remember that compression is generally only effective on textual content. | ||
|
||
|
||
## Support | ||
|
||
Any issues or questions can be sent to the [Polka][polka] repo, but please specify that you are using `@polka/compression`. | ||
|
||
|
||
## License | ||
|
||
MIT | ||
|
||
[polka]: https://github.com/lukeed/polka |
Oops, something went wrong.