├── .github
├── FUNDING.yml
└── workflows
│ └── main.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── deno.json
├── egg.json
├── example.ts
├── mod.ts
└── mod_test.ts
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: v1rtl
2 | liberapay: v1rtl
3 | github: [talentlessguy]
4 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | # Controls when the action will run. Triggers the workflow on push or pull request
4 | # events but only for the master branch
5 | on:
6 | push:
7 | branches: [master]
8 | pull_request:
9 | branches: [master]
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: denoland/setup-deno@v1
17 | with:
18 | deno-version: v1.x
19 | - name: Run tests
20 | run: deno test -A --coverage=coverage
21 | - name: Create coverage report
22 | run: deno coverage ./coverage --lcov > coverage.lcov
23 | - name: Coveralls
24 | uses: coverallsapp/github-action@master
25 | with:
26 | github-token: ${{ secrets.GITHUB_TOKEN }}
27 | path-to-lcov: ./coverage.lcov
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage*
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": true,
4 | "deno.unstable": false,
5 | "editor.formatOnSave": true,
6 | "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno" }
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Deno libraries
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # http_compression
4 |
5 | [![nest badge][nest-badge]](https://nest.land/package/compression)
6 | [![GitHub Workflow Status][gh-actions-img]][github-actions]
7 | [![Codecov][cov-badge]][cov] [![][docs-badge]][docs]
8 | [![][code-quality-img]][code-quality]
9 |
10 |
11 |
12 | Deno HTTP compression middleware.
13 |
14 | ## Features
15 |
16 | - `gzip`, `deflate` and `brotli` support
17 | - Detects supported encodings with `Accept-Encoding` header
18 | - Respects encodings order (depending on `Accept-Encoding` value)
19 | - Creates a `Content-Encoding` header with applied compression
20 | - Send `409 Not Acceptable` if encoding is not supported
21 |
22 | ## Example
23 |
24 | ```ts
25 | import { compression } from 'https://deno.land/x/http_compression/mod.ts'
26 | import { Server } from 'https://deno.land/http/server.ts'
27 |
28 | const s = new Server({
29 | handler: async (req) => {
30 | return await compression({
31 | path: 'README.md',
32 | compression: ['br', 'gzip', 'deflate'],
33 | })(req)
34 | },
35 | addr: ':3000',
36 | })
37 |
38 | s.listenAndServe()
39 | ```
40 |
41 | Now try to send a `HEAD` request with `curl`:
42 |
43 | ```sh
44 | $ curl localhost:3000 --head -H "Accept-Encoding: br, gzip, deflate" --compressed
45 | HTTP/1.1 200 OK
46 | content-length: 550
47 | content-encoding: br, gzip, deflate
48 | ```
49 |
50 | [docs-badge]: https://img.shields.io/github/v/release/deno-libs/http_compression?label=Docs&logo=deno&style=for-the-badge&color=black
51 | [docs]: https://doc.deno.land/https/deno.land/x/http_compression/mod.ts
52 | [gh-actions-img]: http://img.shields.io/github/actions/workflow/status/deno-libs/http_compression/main.yml?branch=master&style=for-the-badge&logo=github&label=&color=black
53 | [github-actions]: https://github.com/deno-libs/http_compression/actions
54 | [cov]: https://coveralls.io/github/deno-libs/http_compression
55 | [cov-badge]: https://img.shields.io/coveralls/github/deno-libs/http_compression?style=for-the-badge&color=black
56 | [nest-badge]: https://img.shields.io/badge/publushed%20on-nest.land-black?style=for-the-badge
57 | [code-quality-img]: https://img.shields.io/codefactor/grade/github/deno-libs/http_compression?style=for-the-badge&color=black
58 | [code-quality]: https://www.codefactor.io/repository/github/deno-libs/http_compression
59 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "fmt": {
3 | "options": {
4 | "useTabs": false,
5 | "lineWidth": 80,
6 | "indentWidth": 2,
7 | "singleQuote": true,
8 | "semiColons": false
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/egg.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://x.nest.land/eggs@0.3.8/src/schema.json",
3 | "name": "http_compression",
4 | "entry": "./mod.ts",
5 | "description": "🗜️ Deno HTTP compression middleware",
6 | "homepage": "https://deno.land/x/http_compression",
7 | "version": "0.3.1",
8 | "releaseType": "patch",
9 | "unstable": true,
10 | "unlisted": false,
11 | "files": [
12 | "README.md",
13 | "./**/*.ts"
14 | ],
15 | "ignore": [
16 | "example.ts"
17 | ],
18 | "checkFormat": false,
19 | "checkTests": true,
20 | "checkInstallation": true,
21 | "check": true,
22 | "checkAll": true,
23 | "repository": "https://deno.land/x/http_compression"
24 | }
25 |
--------------------------------------------------------------------------------
/example.ts:
--------------------------------------------------------------------------------
1 | import { compression } from './mod.ts'
2 | import { Server } from 'https://deno.land/std@0.181.0/http/server.ts'
3 |
4 | const s = new Server({
5 | handler: async (req) => {
6 | return await compression({
7 | path: 'README.md',
8 | })(req)
9 | },
10 | port: 3000,
11 | })
12 |
13 | s.listenAndServe()
14 | console.log(
15 | 'Server available at http://localhost:3000. Set Accept-Encoding header to \'gzip\', for example, to get a compressed response.',
16 | )
17 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | import { readAll } from 'https://deno.land/std@0.181.0/streams/read_all.ts'
2 | import { compress as brotli } from 'https://deno.land/x/brotli@0.1.7/mod.ts'
3 | import { Foras } from 'https://deno.land/x/foras@2.0.8/src/deno/mod.ts'
4 | import { Accepts } from 'https://deno.land/x/accepts@2.1.1/mod.ts'
5 |
6 | await Foras.initBundledOnce()
7 |
8 | const funcs = {
9 | br: brotli,
10 | gzip: (body: Uint8Array) => Foras.gzip(body, undefined),
11 | deflate: (body: Uint8Array) => Foras.deflate(body, undefined),
12 | }
13 |
14 | /**
15 | * Supported compression algorithms
16 | */
17 | type Compression = 'gzip' | 'br' | 'deflate'
18 |
19 | export type CompressionOptions = Partial<{
20 | /**
21 | * Path to file
22 | */
23 | path: string
24 |
25 | /**
26 | * Body as a byte array (as returned from Deno.readFile methods)
27 | */
28 | bodyBinary: Uint8Array
29 |
30 | /**
31 | * Body as a string (as returned from Deno.readTextFile)
32 | */
33 | bodyText: string
34 | }>
35 |
36 | /**
37 | * HTTP Compression middleware.
38 | * @param {CompressionOptions} opts
39 | *
40 | * @example
41 | * ```ts
42 | import { compression } from 'https://deno.land/x/http_compression/mod.ts'
43 | import { Server } from 'https://deno.land/std@0.181.0/http/server.ts'
44 |
45 | new Server({
46 | handler: async (req) => {
47 | return await compression({ path, compression: ['br', 'gzip', 'deflate'] })(req)
48 | }, port: 3000
49 | }).listenAndServe()
50 | * ```
51 | */
52 | export const compression =
53 | (opts: CompressionOptions) => async (req: Request): Promise => {
54 | const acceptHeader = req.headers.get('Accept-Encoding')
55 |
56 | const accepts = new Accepts(req.headers)
57 |
58 | const encodings = accepts.encodings()
59 |
60 | let buf: Uint8Array
61 | if (opts.bodyBinary) {
62 | buf = opts.bodyBinary
63 | } else if (opts.bodyText) {
64 | const encoder = new TextEncoder()
65 | buf = encoder.encode(opts.bodyText)
66 | } else if (opts.path) {
67 | const file = await Deno.open(opts.path)
68 | buf = await readAll(file)
69 | file.close()
70 | } else {
71 | throw Error('Must specify either bodyBinary, bodyText, or path.')
72 | }
73 |
74 | if (
75 | !acceptHeader || acceptHeader === 'identity' ||
76 | (Array.isArray(encodings) && encodings[0] === 'identity')
77 | ) {
78 | return new Response(buf, {
79 | status: 200,
80 | headers: new Headers({
81 | 'Content-Encoding': 'identity',
82 | }),
83 | })
84 | } else if (acceptHeader === '*') {
85 | const compressed = funcs.gzip(buf)
86 |
87 | return new Response(compressed, {
88 | headers: new Headers({
89 | 'Content-Encoding': 'gzip',
90 | }),
91 | })
92 | } else {
93 | if (Array.isArray(encodings)) {
94 | let compressed: Uint8Array = buf
95 | const encs: string[] = []
96 |
97 | for (let enc of encodings.filter((x) => x !== 'identity')) {
98 | if (enc === 'brotli') enc = 'br'
99 |
100 | if (Object.keys(funcs).includes(enc as string)) {
101 | compressed = funcs[enc as Compression](compressed)
102 | encs.push(enc)
103 | }
104 | }
105 |
106 | return new Response(compressed, {
107 | headers: new Headers({
108 | 'Content-Encoding': encs.join(', '),
109 | }),
110 | })
111 | } else {
112 | return Object.keys(funcs).includes(encodings as string)
113 | ? new Response(funcs[encodings as Compression](buf), {
114 | headers: new Headers({
115 | 'Content-Encoding': encodings as string,
116 | }),
117 | })
118 | : new Response('Not Acceptable', {
119 | status: 406,
120 | })
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { superdeno } from 'https://deno.land/x/superdeno@4.8.0/mod.ts'
2 | import { describe, it, run } from 'https://deno.land/x/tincan@1.0.1/mod.ts'
3 | import { compression } from './mod.ts'
4 |
5 | describe('options', () => {
6 | it('does not apply compression if "Accept-Encoding" is "identity"', async () => {
7 | const request = superdeno(compression({ path: 'LICENSE' }))
8 |
9 | await request
10 | .get('/')
11 | .set('Accept-Encoding', 'identity')
12 | .expect(200)
13 | .expect('Content-Encoding', 'identity')
14 | .expect('Content-Length', '1071')
15 | })
16 | it('applies compression to a file', async () => {
17 | const request = superdeno(
18 | compression({
19 | path: 'LICENSE',
20 | }),
21 | )
22 | await request
23 | .get('/')
24 | .set('Accept-Encoding', 'br, gzip, deflate')
25 | .expect('Content-Length', '628')
26 | .expect('Content-Encoding', 'br, gzip, deflate')
27 | })
28 | it('applies compression to a string', async () => {
29 | const bodyText = await Deno.readTextFile('LICENSE')
30 | const request = superdeno(compression({ bodyText }))
31 |
32 | await request
33 | .get('/')
34 | .set('Accept-Encoding', 'br, gzip, deflate')
35 | .expect('Content-Length', '628')
36 | .expect('Content-Encoding', 'br, gzip, deflate')
37 | })
38 | it('applies compression to a byte array', async () => {
39 | const bodyBinary = await Deno.readFile('LICENSE')
40 | const request = superdeno(compression({ bodyBinary }))
41 |
42 | await request
43 | .get('/')
44 | .set('Accept-Encoding', 'br, gzip, deflate')
45 | .expect('Content-Length', '628')
46 | .expect('Content-Encoding', 'br, gzip, deflate')
47 | })
48 | })
49 |
50 | run()
51 |
--------------------------------------------------------------------------------