├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── LICENSE ├── README.md ├── bun.lockb ├── bunfig.toml ├── eslint.config.mjs ├── package.json ├── renovate.json ├── src ├── cache.ts ├── compression-stream.ts ├── index.ts ├── main.ts └── types.ts ├── tests ├── cache.test.ts ├── compression-stream.test.ts ├── data.json ├── index.test.ts ├── node │ ├── cjs │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ └── esm │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json ├── setup.ts └── waifu.png ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | root = true 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build code, Lint code and Test all code 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | name: Build code, Lint code and test code 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup bun 18 | uses: oven-sh/setup-bun@v2 19 | with: 20 | bun-version: latest 21 | 22 | # Setup Node.js environment 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 'lts/*' 27 | 28 | - name: Install packages 29 | run: bun install --frozen-lockfile 30 | 31 | - name: Lint code 32 | run: bun run lint 33 | 34 | - name: Build code 35 | run: bun run build 36 | 37 | - name: Test 38 | run: bun run test 39 | 40 | - name: Run codacy-coverage-reporter 41 | uses: codacy/codacy-coverage-reporter-action@v1.3.0 42 | with: 43 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 44 | # or 45 | # api-token: ${{ secrets.CODACY_API_TOKEN }} 46 | coverage-reports: coverage/lcov.info 47 | # or a comma-separated list for multiple reports 48 | # coverage-reports: , 49 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release package 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | release-type: 6 | description: 'Release type (one of): patch, minor, major, prepatch, preminor, premajor, prerelease' 7 | required: true 8 | 9 | concurrency: 10 | group: 'release' 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | # Checkout project repository 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | token: ${{ secrets.BUILD_TOKEN }} 22 | 23 | # Setup Node.js environment 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 'lts/*' 28 | registry-url: 'https://registry.npmjs.org' 29 | 30 | - name: Setup Bun 31 | uses: oven-sh/setup-bun@v2 32 | with: 33 | bun-version: latest 34 | 35 | # Install dependencies (required by Run tests step) 36 | - name: Install dependencies 37 | run: bun install --frozen-lockfile 38 | 39 | # Build application 40 | - name: Build application 41 | run: bun run build 42 | 43 | - name: Run tests 44 | run: bun run test 45 | 46 | # Configure Git 47 | - name: Git configuration 48 | run: | 49 | git config --global user.email "34608589+vermaysha@users.noreply.github.com" 50 | git config --global user.name "GitHub Actions" 51 | 52 | # Bump package version 53 | # Use tag latest 54 | - name: Bump release version 55 | if: startsWith(github.event.inputs.release-type, 'pre') != true 56 | run: | 57 | echo "NEW_VERSION=$(npm --no-git-tag-version version $RELEASE_TYPE)" >> $GITHUB_ENV 58 | echo "RELEASE_TAG=latest" >> $GITHUB_ENV 59 | env: 60 | RELEASE_TYPE: ${{ github.event.inputs.release-type }} 61 | 62 | # Bump package pre-release version 63 | # Use tag beta for pre-release versions 64 | - name: Bump pre-release version 65 | if: startsWith(github.event.inputs.release-type, 'pre') 66 | run: | 67 | echo "NEW_VERSION=$(npm --no-git-tag-version --preid=beta version $RELEASE_TYPE)" >> $GITHUB_ENV 68 | echo "RELEASE_TAG=beta" >> $GITHUB_ENV 69 | env: 70 | RELEASE_TYPE: ${{ github.event.inputs.release-type }} 71 | 72 | - name: Run tests again 73 | run: bun run test 74 | 75 | # Commit changes 76 | - name: Commit package.json changes and create tag 77 | run: | 78 | git add . 79 | git commit -m "chore: release ${{ env.NEW_VERSION }}" 80 | git tag ${{ env.NEW_VERSION }} 81 | 82 | # Publish version to public repository 83 | - name: Publish 84 | run: npm publish --verbose --access public --tag ${{ env.RELEASE_TAG }} 85 | env: 86 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }} 87 | 88 | # Push repository changes 89 | - name: Push changes to repository 90 | env: 91 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | run: | 93 | git push origin && git push --tags 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | bunx lint-staged 2 | bun run test 3 | bun run format 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "useTabs": false, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Ashary Vermaysha 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elysia-compress 2 | 3 | [![CI Test](https://github.com/vermaysha/elysia-compress/actions/workflows/ci.yml/badge.svg)](https://github.com/vermaysha/elysia-compress/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/elysia-compress.svg?style=flat)](https://www.npmjs.com/package/elysia-compress) 5 | ![Codacy coverage](https://img.shields.io/codacy/coverage/cac8faec654f452abf60133df31cf86d) 6 | ![GitHub License](https://img.shields.io/github/license/vermaysha/elysia-compress?style=flat) 7 | ![NPM Downloads](https://img.shields.io/npm/dy/elysia-compress?style=flat) 8 | 9 | Add compression to [Elysia Server](https://elysiajs.com/essential/handler.html#response). Supports `gzip`, `deflate`, and `brotli`. 10 | 11 | **Note** Brotli Compression is only available and supported by Bun v1.1.8 or higher 12 | 13 | ## Install 14 | 15 | ``` 16 | bun add elysia-compress 17 | ``` 18 | 19 | ## Usage 20 | 21 | This plugin provides a function to automatically compress every Response sent by Elysia Response. 22 | Especially on responses in the form of JSON Objects, Text and Stream (Server Sent Events). 23 | 24 | Currently, the following encoding tokens are supported, using the first acceptable token in this order: 25 | 26 | 1. `br` 27 | 2. `gzip` 28 | 3. `deflate` 29 | 30 | If an unsupported encoding is received or if the `'accept-encoding'` header is missing, it will not compress the payload. 31 | 32 | The plugin automatically decides if a payload should be compressed based on its `content-type`; if no content type is present, it will assume `text/plain`. But if you send a response in the form of an Object then it will be detected automatically as `application/json` 33 | 34 | To improve performance, and given data compression is a resource-intensive operation, caching compressed responses can significantly reduce the load on your server. By setting an appropriate `TTL` (time to live, or how long you want your responses cached), you can ensure that frequently accessed data is served quickly without repeatedly compressing the same content. elysia-compress saves the data in-memory, so it's probably best if you set some sensible defaults (maybe even per-route or group) so as to not increase unnecessarily your memory usage 35 | 36 | ### Global Hook 37 | 38 | The global compression hook is enabled by default. To disable it, pass the option `{ as: 'scoped' }` or `{ as: 'scoped' }` You can read in-depth about [Elysia Scope on this page](https://elysiajs.com/essential/scope.html) 39 | 40 | ```typescript 41 | import { Elysia } from 'elysia' 42 | import { compression } from 'elysia-compress' 43 | 44 | const app = new Elysia() 45 | .use( 46 | compression({ 47 | as: 'scoped', 48 | }), 49 | ) 50 | .get('/', () => ({ hello: 'world' })) 51 | ``` 52 | 53 | ## Compress Options 54 | 55 | ### threshold 56 | 57 | The minimum byte size for a response to be compressed. Defaults to `1024`. 58 | 59 | ```typescript 60 | const app = new Elysia().use( 61 | compression({ 62 | threshold: 2048, 63 | }), 64 | ) 65 | ``` 66 | 67 | ### Disable compression by header 68 | 69 | You can selectively disable response compression by using the `x-no-compression` header in the request. 70 | You can still disable this option by adding `disableByHeader: true` to options. Default to `false` 71 | 72 | ```typescript 73 | const app = new Elysia().use( 74 | compression({ 75 | disableByHeader: true, 76 | }), 77 | ) 78 | ``` 79 | 80 | ### brotliOptions and zlibOptions 81 | 82 | You can tune compression by setting the `brotliOptions` and `zlibOptions` properties. These properties are passed directly to native node `zlib` methods, so they should match the corresponding [class](https://nodejs.org/api/zlib.html#zlib_class_brotlioptions) [definitions](https://nodejs.org/api/zlib.html#zlib_class_options). 83 | 84 | ```typescript 85 | const app = new Elysia().use( 86 | compression({ 87 | brotliOptions: { 88 | params: { 89 | [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, // useful for APIs that primarily return text 90 | [zlib.constants.BROTLI_PARAM_QUALITY]: 4, // default is 4, max is 11, min is 0 91 | }, 92 | }, 93 | zlibOptions: { 94 | level: 6, // default is typically 6, max is 9, min is 0 95 | }, 96 | }), 97 | ) 98 | ``` 99 | 100 | ### Customize encoding priority 101 | 102 | By default, `elysia-compress` prioritizes compression as described [Usage](#usage). You can change that by passing an array of compression tokens to the `encodings` option: 103 | 104 | ```typescript 105 | const app = new Elysia().use( 106 | compression({ 107 | // Only support gzip and deflate, and prefer deflate to gzip 108 | encodings: ['deflate', 'gzip'], 109 | }), 110 | ) 111 | ``` 112 | 113 | ### Cache TTL 114 | 115 | You can specify a time-to-live (TTL) for the cache entries to define how long the compressed responses should be cached. The TTL is specified in seconds and defaults to `86400` (24 hours) 116 | 117 | ```typescript 118 | const app = new Elysia().use( 119 | compression({ 120 | TTL: 3600, // Cache TTL of 1 hour 121 | }), 122 | ) 123 | ``` 124 | 125 | This allows you to control how long the cached compressed responses are stored, helping to balance between performance and memory usage 126 | 127 | ### Cache Server-Sent-Events 128 | 129 | By default, `elysia-compress` will not compress responses in Server-Sent Events. If you want to enable compression in Server-Sent Events, you can set the `compressStream` option to `true`. 130 | 131 | ```typescript 132 | const app = new Elysia().use( 133 | compression({ 134 | compressStream: true, 135 | }), 136 | ) 137 | ``` 138 | 139 | ## Contributors 140 | 141 | 142 | 143 | 144 | 145 | ## License 146 | 147 | This plugins is licensed under the [MIT License](LICENSE). 148 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermaysha/elysia-compress/f2c309881c332e13c88e36de7135e5f408083c8d/bun.lockb -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | 3 | # always enable coverage 4 | coverage = true 5 | 6 | # to require 90% line-level and function-level coverage 7 | coverageThreshold = 0.9 8 | 9 | coverageReporter = ["text", "lcov"] # default ["text"] 10 | # coverageDir = "./reporter" # default "coverage" 11 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js' 2 | import tseslint from 'typescript-eslint' 3 | import eslintPlugin from 'eslint-plugin-prettier/recommended' 4 | 5 | export default tseslint.config( 6 | eslint.configs.recommended, 7 | ...tseslint.configs.recommended, 8 | { 9 | rules: { 10 | '@typescript-eslint/no-explicit-any': 'off', 11 | '@typescript-eslint/no-unused-vars': 'off', 12 | }, 13 | }, 14 | eslintPlugin, 15 | ) 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elysia-compress", 3 | "description": "Elysia Compression that supports Brotli, GZIP, and Deflate compression", 4 | "version": "1.2.1", 5 | "author": { 6 | "name": "Ashary Vermaysha", 7 | "email": "vermaysha@gmail.com", 8 | "url": "https://github.com/vermaysha" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/vermaysha/elysia-compress.git" 13 | }, 14 | "url": "https://github.com/vermaysha/elysia-compress", 15 | "bugs": "https://github.com/vermaysha/elysia-compress/issues", 16 | "homepage": "https://github.com/vermaysha/elysia-compress", 17 | "keywords": [ 18 | "elysia", 19 | "compression", 20 | "brotli", 21 | "gzip", 22 | "deflate" 23 | ], 24 | "license": "MIT", 25 | "exports": { 26 | "bun": "./dist/index.js", 27 | "node": "./dist/cjs/index.js", 28 | "require": "./dist/cjs/index.js", 29 | "import": "./dist/index.js", 30 | "default": "./dist/index.js", 31 | "types": "./dist/index.d.ts" 32 | }, 33 | "main": "./dist/index.js", 34 | "types": "./dist/index.d.ts", 35 | "scripts": { 36 | "prepare": "husky", 37 | "format": "eslint --fix {./src/,./tests/}*.ts", 38 | "lint": "eslint {./src/,./tests/}*.ts", 39 | "build": "rimraf dist && tsc --project tsconfig.esm.json && tsc --project tsconfig.cjs.json", 40 | "test": "bun test && npm run test:node", 41 | "test:node": "npm install --prefix ./tests/node/cjs/ && npm install --prefix ./tests/node/esm/ && node ./tests/node/cjs/index.js && node ./tests/node/esm/index.js", 42 | "release": "npm run build && npm run test && npm publish --access public" 43 | }, 44 | "devDependencies": { 45 | "@elysiajs/cors": "^1.3.3", 46 | "@elysiajs/stream": "^1.1.0", 47 | "@eslint/js": "^9.28.0", 48 | "@types/bun": "latest", 49 | "bun-types": "^1.2.15", 50 | "elysia": "^1.1.27", 51 | "eslint": "^9.28.0", 52 | "eslint-config-prettier": "^9.1.0", 53 | "eslint-plugin-prettier": "^5.4.1", 54 | "husky": "^9.1.7", 55 | "lint-staged": "^15.5.2", 56 | "prettier": "^3.5.3", 57 | "rimraf": "^6.0.1", 58 | "typescript": "^5.8.3", 59 | "typescript-eslint": "^7.18.0" 60 | }, 61 | "peerDependencies": { 62 | "typescript": "^5.8.3", 63 | "elysia": ">= 1.1.27" 64 | }, 65 | "engines": { 66 | "bun": ">=1.1.8", 67 | "node": ">=18.20.8" 68 | }, 69 | "lint-staged": { 70 | "*": "prettier --ignore-unknown --write", 71 | "*.ts": "eslint --fix" 72 | }, 73 | "files": [ 74 | "dist" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "rangeStrategy": "bump", 5 | "packageRules": [ 6 | { 7 | "description": "Automerge non-major updates", 8 | "matchUpdateTypes": ["minor", "patch"], 9 | "automerge": true 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple in-memory cache 3 | * 4 | */ 5 | class MemCache { 6 | constructor(private cache: Map = new Map()) {} 7 | 8 | /** 9 | * Sets a value in the cache with the specified key and optional time-to-live (TTL). 10 | * 11 | * @param {number | bigint} key - The key to set the value for. 12 | * @param {any} value - The value to set in the cache. 13 | * @param {number} [TTL=Number.MAX_SAFE_INTEGER] - The time-to-live (in seconds) for the value in the cache. 14 | * @return {void} This function does not return anything. 15 | */ 16 | set( 17 | key: number | bigint, 18 | value: any, 19 | TTL: number = Number.MAX_SAFE_INTEGER, 20 | ): void { 21 | this.cache.set(key, value) 22 | setTimeout(() => this.cache.delete(key), TTL * 1000) 23 | } 24 | 25 | /** 26 | * Gets a value from the cache with the specified key. 27 | * 28 | * @param {number | bigint} key - The key to get the value from the cache. 29 | * @return {any} The value from the cache if it exists, otherwise `undefined`. 30 | */ 31 | get(key: number | bigint) { 32 | return this.cache.get(key) 33 | } 34 | 35 | /** 36 | * Checks if a value exists in the cache with the specified key. 37 | * 38 | * @param {number | bigint} key - The key to check for in the cache. 39 | * @return {boolean} `true` if the value exists in the cache, `false` otherwise. 40 | */ 41 | has(key: number | bigint): boolean { 42 | return this.cache.has(key) 43 | } 44 | 45 | /** 46 | * Removes a value from the cache with the specified key. 47 | * 48 | * @return {void} This function does not return anything. 49 | */ 50 | clear(): void { 51 | this.cache.clear() 52 | } 53 | } 54 | 55 | const memCache = new MemCache() 56 | 57 | export default memCache 58 | -------------------------------------------------------------------------------- /src/compression-stream.ts: -------------------------------------------------------------------------------- 1 | import zlib from 'node:zlib' 2 | import { Transform } from 'stream' 3 | import type { CompressionEncoding, CompressionOptions } from './types' 4 | 5 | /** 6 | * Creates a compression stream based on the specified encoding and options. 7 | * 8 | * @param {CompressionEncoding} encoding - The compression encoding to use. 9 | * @param {CompressionOptions} [options] - The compression options. 10 | * @returns {{ readable: ReadableStream, writable: WritableStream }} The compression stream. 11 | */ 12 | export const CompressionStream = ( 13 | encoding: CompressionEncoding, 14 | options?: CompressionOptions, 15 | ) => { 16 | let handler: Transform 17 | 18 | const zlibOptions: zlib.ZlibOptions = { 19 | ...{ 20 | level: 6, 21 | }, 22 | ...options?.zlibOptions, 23 | } 24 | const brotliOptions: zlib.BrotliOptions = { 25 | ...{ 26 | params: { 27 | [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, 28 | [zlib.constants.BROTLI_PARAM_QUALITY]: 29 | zlib.constants.BROTLI_DEFAULT_QUALITY, 30 | }, 31 | }, 32 | ...options?.brotliOptions, 33 | } 34 | 35 | if (encoding === 'br') { 36 | handler = zlib.createBrotliCompress(brotliOptions) 37 | } else if (encoding === 'gzip') { 38 | handler = zlib.createGzip(zlibOptions) 39 | } else if (encoding === 'deflate') { 40 | handler = zlib.createDeflate(zlibOptions) 41 | } else { 42 | handler = new Transform({ 43 | /** 44 | * Transforms the given chunk of data and calls the callback with the transformed data. 45 | * 46 | * @param {any} chunk - The chunk of data to be transformed. 47 | * @param {any} _ - Unused parameter. 48 | * @param {any} callback - The callback function to be called with the transformed data. 49 | * @return {void} 50 | */ 51 | transform(chunk: any, _: any, callback: any): void { 52 | callback(null, chunk) 53 | }, 54 | }) 55 | } 56 | 57 | const readable = new ReadableStream({ 58 | /** 59 | * Starts the stream and sets up event listeners for 'data' and 'end' events. 60 | * 61 | * @param {ReadableStreamDefaultController} controller - The controller object for the readable stream. 62 | */ 63 | start(controller: ReadableStreamDefaultController) { 64 | handler.on('data', (chunk: Uint8Array) => controller.enqueue(chunk)) 65 | handler.once('end', () => controller.close()) 66 | }, 67 | }) 68 | 69 | const writable = new WritableStream({ 70 | /** 71 | * Writes a chunk of data to the writable stream. 72 | * 73 | * @param {Uint8Array} chunk - The chunk of data to write. 74 | * @returns {Promise} 75 | */ 76 | write: (chunk: Uint8Array): Promise => handler.write(chunk) as any, 77 | 78 | /** 79 | * Closes the writable stream. 80 | * 81 | * @returns {Promise} 82 | */ 83 | close: (): Promise => handler.end() as any, 84 | }) 85 | 86 | return { 87 | readable, 88 | writable, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { compression } from './main' 2 | 3 | export * from './types' 4 | export * from './compression-stream' 5 | export default compression 6 | export { compression } 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, mapResponse } from 'elysia' 2 | import type { 3 | CacheOptions, 4 | CompressionEncoding, 5 | CompressionOptions, 6 | LifeCycleOptions, 7 | } from './types' 8 | import { 9 | BrotliOptions, 10 | ZlibOptions, 11 | constants, 12 | brotliCompressSync, 13 | gzipSync, 14 | deflateSync, 15 | } from 'node:zlib' 16 | import { CompressionStream } from './compression-stream' 17 | import cacheStore from './cache' 18 | 19 | /** 20 | * Creates a compression middleware function that compresses the response body based on the client's accept-encoding header. 21 | * 22 | * @param {CompressionOptions & LifeCycleOptions & CacheOptions} [options] - Optional compression, caching, and life cycle options. 23 | * @param {CompressionOptions} [options.compressionOptions] - Compression options. 24 | * @param {LifeCycleOptions} [options.lifeCycleOptions] - Life cycle options. 25 | * @param {CacheOptions} [options.cacheOptions] - Cache options. 26 | * @param {CompressionEncoding[]} [options.compressionOptions.encodings] - An array of supported compression encodings. Defaults to ['br', 'gzip', 'deflate']. 27 | * @param {boolean} [options.compressionOptions.disableByHeader] - Disable compression by header. Defaults to false. 28 | * @param {BrotliOptions} [options.compressionOptions.brotliOptions] - Brotli compression options. 29 | * @param {ZlibOptions} [options.compressionOptions.zlibOptions] - Zlib compression options. 30 | * @param {LifeCycleType} [options.lifeCycleOptions.as] - The life cycle type. Defaults to 'scoped'. 31 | * @param {number} [options.compressionOptions.threshold] - The minimum byte size for a response to be compressed. Defaults to 1024. 32 | * @param {number} [options.cacheOptions.TTL] - The time-to-live for the cache. Defaults to 24 hours. 33 | * @returns {Elysia} - The Elysia app with compression middleware. 34 | */ 35 | export const compression = ( 36 | options?: CompressionOptions & LifeCycleOptions & CacheOptions, 37 | ) => { 38 | const zlibOptions: ZlibOptions = { 39 | ...{ 40 | level: 6, 41 | }, 42 | ...options?.zlibOptions, 43 | } 44 | const brotliOptions: BrotliOptions = { 45 | ...{ 46 | params: { 47 | [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_GENERIC, 48 | [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_DEFAULT_QUALITY, 49 | }, 50 | }, 51 | ...options?.brotliOptions, 52 | } 53 | const defaultEncodings = options?.encodings ?? ['br', 'gzip', 'deflate'] 54 | const defaultCompressibleTypes = 55 | /^text\/(?!event-stream)|(?:\+|\/)json(?:;|$)|(?:\+|\/)text(?:;|$)|(?:\+|\/)xml(?:;|$)|octet-stream(?:;|$)/u 56 | const lifeCycleType = options?.as ?? 'global' 57 | const threshold = options?.threshold ?? 1024 58 | const cacheTTL = options?.TTL ?? 24 * 60 * 60 // 24 hours 59 | const disableByHeader = options?.disableByHeader ?? true 60 | const compressStream = options?.compressStream ?? true 61 | const app = new Elysia({ 62 | name: 'elysia-compress', 63 | seed: options, 64 | }) 65 | 66 | const compressors = { 67 | br: (buffer: ArrayBuffer) => brotliCompressSync(buffer, brotliOptions), 68 | gzip: (buffer: ArrayBuffer) => gzipSync(buffer, zlibOptions), 69 | deflate: (buffer: ArrayBuffer) => deflateSync(buffer, zlibOptions), 70 | } as Record Buffer> 71 | const textDecoder = new TextDecoder() 72 | 73 | /** 74 | * Gets or compresses the response body based on the client's accept-encoding header. 75 | * 76 | * @param {CompressionEncoding} algorithm - The compression algorithm to use. 77 | * @param {ArrayBuffer} buffer - The buffer to compress. 78 | * @returns {Buffer} The compressed buffer. 79 | */ 80 | const getOrCompress = ( 81 | algorithm: CompressionEncoding, 82 | buffer: ArrayBuffer, 83 | ): Buffer => { 84 | const cacheKey = Bun.hash(`${algorithm}:${textDecoder.decode(buffer)}}`) 85 | if (cacheStore.has(cacheKey)) { 86 | return cacheStore.get(cacheKey) 87 | } 88 | 89 | const compressedOutput = compressors[algorithm](buffer) 90 | cacheStore.set(cacheKey, compressedOutput, cacheTTL) 91 | return compressedOutput 92 | } 93 | 94 | /** 95 | * Compresses the response body based on the client's accept-encoding header. 96 | * 97 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding 98 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding 99 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type 100 | */ 101 | app.mapResponse({ as: lifeCycleType }, async (ctx) => { 102 | // Disable compression when `x-no-compression` header is set 103 | if (disableByHeader && ctx.headers['x-no-compression']) { 104 | return 105 | } 106 | 107 | const { set } = ctx 108 | const response = ctx.response as any 109 | 110 | const acceptEncodings: string[] = 111 | ctx.headers['accept-encoding']?.split(', ') ?? [] 112 | const encodings: string[] = defaultEncodings.filter((encoding) => 113 | acceptEncodings.includes(encoding), 114 | ) 115 | 116 | if (encodings.length < 1 && !encodings[0]) { 117 | return 118 | } 119 | 120 | const encoding = encodings[0] as CompressionEncoding 121 | let compressed: Buffer | ReadableStream 122 | let contentType = 123 | set.headers['Content-Type'] ?? set.headers['content-type'] ?? '' 124 | 125 | /** 126 | * Compress ReadableStream Object if stream exists (SSE) 127 | * 128 | * @see https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream 129 | */ 130 | if (compressStream && response?.stream instanceof ReadableStream) { 131 | const stream = response.stream as ReadableStream 132 | compressed = stream.pipeThrough(CompressionStream(encoding, options)) 133 | } else { 134 | const res = mapResponse(response, { 135 | headers: {}, 136 | }) 137 | const resContentType = res.headers.get('Content-Type') 138 | 139 | contentType = resContentType ? resContentType : 'text/plain' 140 | 141 | const buffer = await res.arrayBuffer() 142 | // Disable compression when buffer size is less than threshold 143 | if (buffer.byteLength < threshold) { 144 | return 145 | } 146 | 147 | // Disable compression when Content-Type is not compressible 148 | const isCompressible = defaultCompressibleTypes.test(contentType) 149 | if (!isCompressible) { 150 | return 151 | } 152 | 153 | compressed = getOrCompress(encoding, buffer) // Will try cache first 154 | } 155 | 156 | /** 157 | * Send Vary HTTP Header 158 | * 159 | * The Vary HTTP response header describes the parts of the request message aside 160 | * from the method and URL that influenced the content of the response it occurs in. 161 | * Most often, this is used to create a cache key when content negotiation is in use. 162 | * 163 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary 164 | */ 165 | const vary = set.headers.Vary ?? set.headers.vary 166 | if (vary) { 167 | const rawHeaderValue = vary 168 | ?.split(',') 169 | .map((v: any) => v.trim().toLowerCase()) 170 | 171 | const headerValueArray = Array.isArray(rawHeaderValue) 172 | ? rawHeaderValue 173 | : [rawHeaderValue] 174 | 175 | // Add accept-encoding header if it doesn't exist 176 | // and if vary not set to * 177 | if (!headerValueArray.includes('*')) { 178 | set.headers.Vary = headerValueArray 179 | .concat('accept-encoding') 180 | .filter((value, index, array) => array.indexOf(value) === index) 181 | .join(', ') 182 | } 183 | } else { 184 | set.headers.Vary = 'accept-encoding' 185 | } 186 | set.headers['Content-Encoding'] = encoding 187 | 188 | return new Response(compressed, { 189 | headers: { 190 | 'Content-Type': contentType, 191 | }, 192 | }) 193 | }) 194 | return app 195 | } 196 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { LifeCycleType } from 'elysia' 2 | import type { BrotliOptions, ZlibOptions } from 'node:zlib' 3 | export type CompressionEncoding = 'br' | 'deflate' | 'gzip' 4 | 5 | export type CompressionOptions = { 6 | /** 7 | * The options use for brotli compression. 8 | * 9 | * @see https://nodejs.org/api/zlib.html#compressor-options 10 | */ 11 | brotliOptions?: BrotliOptions 12 | 13 | /** 14 | * The options use for gzip or deflate compression. 15 | * 16 | * @see https://nodejs.org/api/zlib.html#class-options 17 | */ 18 | zlibOptions?: ZlibOptions 19 | 20 | /** 21 | * The encodings to use. 22 | * 23 | * By default, we prioritize compression using 24 | * 1. br 25 | * 2. gzip 26 | * 3. deflate 27 | * If an unsupported encoding is received or if the 'accept-encoding' header is missing, 28 | * it will not compress the payload. 29 | * 30 | * You can change that by passing an array of compression tokens to the encodings option 31 | * example: `encodings: ['gzip', 'deflate']` 32 | */ 33 | encodings?: CompressionEncoding[] 34 | 35 | /** 36 | * You can disable the compression by using `x-no-compression` header in request 37 | * 38 | * By default, we will not compress the payload if the 'x-no-compression' header is present 39 | * 40 | * @default true 41 | */ 42 | disableByHeader?: boolean 43 | 44 | /** 45 | * The minimum byte size for a response to be compressed. 46 | * 47 | * Defaults to 1024. 48 | * @default 1024 49 | */ 50 | threshold?: number 51 | 52 | /** 53 | * Whether to compress the stream data or not. 54 | * This generally refers to Server-Sent-Events 55 | * 56 | * @link https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events 57 | * Defaults to `false` 58 | * @default false 59 | */ 60 | compressStream: boolean 61 | } 62 | 63 | export type LifeCycleOptions = { 64 | /** 65 | * By default, hook and schema is scope to current instance only not global. 66 | * Hook type is to specify the scope of hook whether is should be encapsulated or global. 67 | * 68 | * Elysia hook type are as the following: 69 | * local - apply to only current instance and descendant only 70 | * scoped - apply to parent, current instance and descendants 71 | * global - apply to all instance that apply the plugin (all parents, current, and descendants) 72 | * 73 | * @default 'scoped' 74 | * @see https://elysiajs.com/essential/scope.html#hook-type 75 | */ 76 | as?: LifeCycleType 77 | } 78 | 79 | export type CacheOptions = { 80 | /** 81 | * The time-to-live in seconds for the cache. 82 | * 83 | * @default 86400 (24 hours) 84 | */ 85 | TTL?: number 86 | } 87 | 88 | export type ElysiaCompressionOptions = CompressionOptions & 89 | LifeCycleOptions & 90 | CacheOptions 91 | -------------------------------------------------------------------------------- /tests/cache.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from 'bun:test' 2 | import cache from '../src/cache' 3 | import { it } from 'bun:test' 4 | 5 | describe('MemCache', () => { 6 | it('should set and get a value', () => { 7 | cache.set(1, 'value', 10) 8 | expect(cache.get(1)).toBe('value') 9 | }) 10 | 11 | it('should clear the cache', () => { 12 | cache.set(1, 'value', 10) 13 | cache.clear() 14 | expect(cache.get(1)).toBeUndefined() 15 | }) 16 | 17 | it('should check if a value exists', () => { 18 | cache.set(1, 'value', 10) 19 | expect(cache.has(1)).toBe(true) 20 | expect(cache.has(2)).toBe(false) 21 | }) 22 | 23 | it('should delete a value', async () => { 24 | cache.set(1, 'value', 0.1) 25 | await new Promise((resolve) => setTimeout(resolve, 0.1 * 1000)) 26 | expect(cache.get(1)).toBeUndefined() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/compression-stream.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { CompressionStream } from '../src/compression-stream' 3 | import zlib from 'node:zlib' 4 | import { responseShort } from './setup' 5 | 6 | describe('CompressionStream', () => { 7 | it('should create a compression stream', () => { 8 | const stream = CompressionStream('br') 9 | expect(stream).toBeDefined() 10 | expect(stream.readable).toBeDefined() 11 | expect(stream.writable).toBeDefined() 12 | }) 13 | 14 | it('compresses data using br encoding and verifies output', async () => { 15 | // Sample data to compress 16 | const testData = new TextEncoder().encode(responseShort) 17 | 18 | // Create a compression stream with Brotli encoding 19 | const { readable, writable } = CompressionStream('br') 20 | 21 | // Use the WritableStream to write the test data 22 | const writer = writable.getWriter() 23 | await writer.write(testData) 24 | await writer.close() 25 | 26 | // Read from the ReadableStream and collect the compressed data 27 | const reader = readable.getReader() 28 | let compressedData = new Uint8Array() 29 | let done = false 30 | while (!done) { 31 | const { value, done: streamDone } = await reader.read() 32 | if (value) { 33 | // This example simply concatenates chunks; for larger data, consider a more efficient method 34 | compressedData = new Uint8Array([...compressedData, ...value]) 35 | } 36 | done = streamDone 37 | } 38 | 39 | // Verify the compressed data 40 | // Expect the compressed data to exist and be different from the original 41 | expect(compressedData.byteLength).toBeGreaterThan(0) 42 | expect(compressedData).not.toEqual(testData) 43 | 44 | // Further verification could include decompressing `compressedData` and comparing with `testData` 45 | // and checking that the decompressed data matches the original. 46 | 47 | // Verify that the decompressed data matches the original 48 | const decompressedData = new TextDecoder().decode( 49 | zlib.brotliDecompressSync(compressedData), 50 | ) 51 | expect(decompressedData).toEqual(responseShort) 52 | }) 53 | 54 | it('compresses data using gzip encoding and verifies output', async () => { 55 | // Sample data to compress 56 | const testData = new TextEncoder().encode(responseShort) 57 | 58 | // Create a compression stream with Brotli encoding 59 | const { readable, writable } = CompressionStream('gzip') 60 | 61 | // Use the WritableStream to write the test data 62 | const writer = writable.getWriter() 63 | await writer.write(testData) 64 | await writer.close() 65 | 66 | // Read from the ReadableStream and collect the compressed data 67 | const reader = readable.getReader() 68 | let compressedData = new Uint8Array() 69 | let done = false 70 | while (!done) { 71 | const { value, done: streamDone } = await reader.read() 72 | if (value) { 73 | // This example simply concatenates chunks; for larger data, consider a more efficient method 74 | compressedData = new Uint8Array([...compressedData, ...value]) 75 | } 76 | done = streamDone 77 | } 78 | 79 | // Verify the compressed data 80 | // Expect the compressed data to exist and be different from the original 81 | expect(compressedData.byteLength).toBeGreaterThan(0) 82 | expect(compressedData).not.toEqual(testData) 83 | 84 | // Further verification could include decompressing `compressedData` and comparing with `testData` 85 | // and checking that the decompressed data matches the original. 86 | 87 | // Verify that the decompressed data matches the original 88 | const decompressedData = new TextDecoder().decode( 89 | zlib.gunzipSync(compressedData), 90 | ) 91 | expect(decompressedData).toEqual(responseShort) 92 | }) 93 | 94 | it('compresses data using deflate encoding and verifies output', async () => { 95 | // Sample data to compress 96 | const testData = new TextEncoder().encode(responseShort) 97 | 98 | // Create a compression stream with Brotli encoding 99 | const { readable, writable } = CompressionStream('deflate') 100 | 101 | // Use the WritableStream to write the test data 102 | const writer = writable.getWriter() 103 | await writer.write(testData) 104 | await writer.close() 105 | 106 | // Read from the ReadableStream and collect the compressed data 107 | const reader = readable.getReader() 108 | let compressedData = new Uint8Array() 109 | let done = false 110 | while (!done) { 111 | const { value, done: streamDone } = await reader.read() 112 | if (value) { 113 | // This example simply concatenates chunks; for larger data, consider a more efficient method 114 | compressedData = new Uint8Array([...compressedData, ...value]) 115 | } 116 | done = streamDone 117 | } 118 | 119 | // Verify the compressed data 120 | // Expect the compressed data to exist and be different from the original 121 | expect(compressedData.byteLength).toBeGreaterThan(0) 122 | expect(compressedData).not.toEqual(testData) 123 | 124 | // Further verification could include decompressing `compressedData` and comparing with `testData` 125 | // and checking that the decompressed data matches the original. 126 | 127 | // Verify that the decompressed data matches the original 128 | const decompressedData = new TextDecoder().decode( 129 | zlib.inflateSync(compressedData), 130 | ) 131 | expect(decompressedData).toEqual(responseShort) 132 | }) 133 | 134 | it(`Don't compress when algorithm is invalid`, async () => { 135 | // Sample data to compress 136 | const testData = new TextEncoder().encode(responseShort) 137 | 138 | // Create a compression stream with Brotli encoding 139 | const { readable, writable } = CompressionStream('' as any) 140 | 141 | // Use the WritableStream to write the test data 142 | const writer = writable.getWriter() 143 | await writer.write(testData) 144 | await writer.close() 145 | 146 | // Read from the ReadableStream and collect the compressed data 147 | const reader = readable.getReader() 148 | let compressedData = new Uint8Array() 149 | let done = false 150 | while (!done) { 151 | const { value, done: streamDone } = await reader.read() 152 | if (value) { 153 | // This example simply concatenates chunks; for larger data, consider a more efficient method 154 | compressedData = new Uint8Array([...compressedData, ...value]) 155 | } 156 | done = streamDone 157 | } 158 | 159 | // Verify the compressed data 160 | // Expect the compressed data to exist and be different from the original 161 | expect(compressedData.byteLength).toBeGreaterThan(0) 162 | expect(compressedData).toEqual(testData) 163 | }) 164 | }) 165 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import zlib from 'node:zlib' 3 | import Elysia from 'elysia' 4 | import { Stream } from '@elysiajs/stream' 5 | import { cors } from '@elysiajs/cors' 6 | 7 | import { req, responseShort, jsonResponse } from './setup' 8 | import compression from '../src' 9 | 10 | describe(`elysia-compress`, () => { 11 | it('Dont compress when the threshold is not met', async () => { 12 | const app = new Elysia() 13 | .use( 14 | compression({ 15 | encodings: ['br'], 16 | threshold: 1024, 17 | }), 18 | ) 19 | .get('/', () => responseShort) 20 | const res = await app.handle(req()) 21 | 22 | expect(res.headers.get('Content-Encoding')).toBeNull() 23 | expect(res.headers.get('vary')).toBeNull() 24 | }) 25 | 26 | it('handle brotli compression', async () => { 27 | const app = new Elysia() 28 | .use( 29 | compression({ 30 | encodings: ['br'], 31 | threshold: 1, 32 | }), 33 | ) 34 | .get('/', () => responseShort) 35 | const res = await app.handle(req()) 36 | 37 | expect(res.headers.get('Content-Encoding')).toBe('br') 38 | expect(res.headers.get('vary')).toBe('accept-encoding') 39 | }) 40 | 41 | it('handle deflate compression', async () => { 42 | const app = new Elysia() 43 | .use(compression({ encodings: ['deflate'], threshold: 1 })) 44 | .get('/', () => responseShort) 45 | const res = await app.handle(req()) 46 | 47 | expect(res.headers.get('Content-Encoding')).toBe('deflate') 48 | expect(res.headers.get('vary')).toBe('accept-encoding') 49 | }) 50 | 51 | it('handle gzip compression', async () => { 52 | const app = new Elysia() 53 | .use(compression({ encodings: ['gzip'], threshold: 1 })) 54 | .get('/', () => responseShort) 55 | const res = await app.handle(req()) 56 | 57 | expect(res.headers.get('Content-Encoding')).toBe('gzip') 58 | expect(res.headers.get('vary')).toBe('accept-encoding') 59 | }) 60 | 61 | it('accept additional headers', async () => { 62 | const app = new Elysia() 63 | .use(compression({ encodings: ['deflate'], threshold: 1 })) 64 | .get('/', ({ set }) => { 65 | set.headers['x-powered-by'] = 'Elysia' 66 | 67 | return responseShort 68 | }) 69 | const res = await app.handle(req()) 70 | 71 | expect(res.headers.get('Content-Encoding')).toBe('deflate') 72 | expect(res.headers.get('x-powered-by')).toBe('Elysia') 73 | expect(res.headers.get('vary')).toBe('accept-encoding') 74 | }) 75 | 76 | it('return correct plain/text', async () => { 77 | const app = new Elysia() 78 | .use(compression({ encodings: ['gzip'], threshold: 1 })) 79 | .get('/', () => responseShort) 80 | 81 | const res = await app.handle(req()) 82 | 83 | expect(res.headers.get('Content-Type')).toBe('text/plain') 84 | expect(res.headers.get('vary')).toBe('accept-encoding') 85 | }) 86 | 87 | it('return correct application/json', async () => { 88 | const app = new Elysia() 89 | .use(compression({ encodings: ['gzip'], threshold: 1 })) 90 | .get('/', () => ({ hello: 'world' })) 91 | 92 | const res = await app.handle(req()) 93 | 94 | expect(res.headers.get('Content-Type')).toBe( 95 | 'application/json;charset=utf-8', 96 | ) 97 | expect(res.headers.get('vary')).toBe('accept-encoding') 98 | }) 99 | 100 | it('return correct image type', async () => { 101 | const app = new Elysia() 102 | .use(compression({ encodings: ['gzip'], threshold: 1 })) 103 | .get('/', () => Bun.file('tests/waifu.png')) 104 | 105 | const res = await app.handle(req()) 106 | 107 | expect(res.headers.get('Content-Type')).toBe('image/png') 108 | expect(res.headers.get('vary')).toBeNull() 109 | }) 110 | 111 | it('must be redirected to /not-found', async () => { 112 | const app = new Elysia() 113 | .use(compression({ encodings: ['gzip'], threshold: 1 })) 114 | .get('/', ({ set }) => { 115 | set.redirect = '/not-found' 116 | }) 117 | 118 | const res = await app.handle(req()) 119 | 120 | expect(res.headers.get('Location')).toBe('/not-found') 121 | }) 122 | 123 | it('cookie should be set', async () => { 124 | const app = new Elysia() 125 | .use(compression({ encodings: ['gzip'], threshold: 1 })) 126 | .get('/', ({ cookie: { test } }) => { 127 | test?.set({ 128 | value: 'test', 129 | }) 130 | }) 131 | 132 | const res = await app.handle(req()) 133 | 134 | expect(res.headers.get('set-cookie')).toContain('test=test') 135 | }) 136 | 137 | it('stream should be compressed', async () => { 138 | const app = new Elysia() 139 | .use(compression({ encodings: ['gzip'], threshold: 1 })) 140 | .get('/', () => { 141 | return new Stream(async (stream) => { 142 | stream.send('hello') 143 | 144 | await stream.wait(1000) 145 | stream.send('world') 146 | 147 | stream.close() 148 | }) 149 | }) 150 | 151 | const res = await app.handle(req()) 152 | 153 | expect(res.headers.get('Content-Encoding')).toBe('gzip') 154 | expect(res.headers.get('vary')).toBe('accept-encoding') 155 | }) 156 | 157 | it('cors should be enable when threshold 1024', async () => { 158 | const app = new Elysia() 159 | .use( 160 | cors({ 161 | origin: true, 162 | }), 163 | ) 164 | .use(compression({ encodings: ['gzip'], threshold: 1024 })) 165 | .get('/', () => { 166 | return new Stream(async (stream) => { 167 | stream.send('hello') 168 | 169 | await stream.wait(1000) 170 | stream.send('world') 171 | 172 | stream.close() 173 | }) 174 | }) 175 | 176 | const res = await app.handle(req()) 177 | 178 | expect(res.headers.get('access-control-allow-origin')).toBe('*') 179 | }) 180 | 181 | it('cors should be enable when threshold 1', async () => { 182 | const app = new Elysia() 183 | .use( 184 | cors({ 185 | origin: true, 186 | }), 187 | ) 188 | .use(compression({ encodings: ['gzip'], threshold: 1 })) 189 | .get('/', () => { 190 | return new Stream(async (stream) => { 191 | stream.send('hello') 192 | 193 | await stream.wait(1000) 194 | stream.send('world') 195 | 196 | stream.close() 197 | }) 198 | }) 199 | 200 | const res = await app.handle(req()) 201 | 202 | expect(res.headers.get('access-control-allow-origin')).toBe('*') 203 | expect(res.headers.get('vary')).toBe('*') 204 | }) 205 | 206 | it(`Should't compress response if threshold is not met minimum size (1024)`, async () => { 207 | const app = new Elysia() 208 | .use(compression({ threshold: 1024 })) 209 | .get('/', () => { 210 | return responseShort 211 | }) 212 | 213 | const res = await app.handle(req()) 214 | 215 | expect(res.status).toBe(200) 216 | expect(res.headers.get('Content-Encoding')).toBeNull() 217 | expect(res.headers.get('Vary')).toBeNull() 218 | }) 219 | 220 | it(`Should't compress response if x-no-compression header is present`, async () => { 221 | const app = new Elysia() 222 | .use(compression({ disableByHeader: true })) 223 | .get('/', () => { 224 | return responseShort 225 | }) 226 | 227 | const res = await app.handle(req({ 'x-no-compression': 'true' })) 228 | 229 | expect(res.status).toBe(200) 230 | expect(res.headers.get('Content-Encoding')).toBeNull() 231 | expect(res.headers.get('Vary')).toBeNull() 232 | }) 233 | 234 | it(`When not compress response send original response`, async () => { 235 | const app = new Elysia() 236 | .use(compression({ threshold: 1024 })) 237 | .get('/', () => { 238 | return responseShort 239 | }) 240 | 241 | const res = await app.handle(req()) 242 | const test = await res.text() 243 | 244 | expect(res.status).toBe(200) 245 | expect(res.headers.get('Content-Encoding')).toBeNull() 246 | expect(res.headers.get('Vary')).toBeNull() 247 | expect(test).toBe(responseShort) 248 | }) 249 | 250 | it(`When not compress response should send original content-type`, async () => { 251 | const app = new Elysia() 252 | .use(compression({ threshold: Number.MAX_SAFE_INTEGER })) 253 | .get('/', () => { 254 | return jsonResponse 255 | }) 256 | 257 | const res = await app.handle(req()) 258 | const test = await res.text() 259 | 260 | expect(res.status).toBe(200) 261 | expect(res.headers.get('Content-Encoding')).toBeNull() 262 | expect(res.headers.get('Vary')).toBeNull() 263 | expect(test).toBe(await jsonResponse.text()) 264 | expect(res.headers.get('Content-Type')).toBe( 265 | 'application/json;charset=utf-8', 266 | ) 267 | }) 268 | 269 | it(`Should'nt compress response if browser not support any compression algorithm`, async () => { 270 | const app = new Elysia() 271 | .use(compression({ threshold: 1024 })) 272 | .get('/', () => { 273 | return responseShort 274 | }) 275 | 276 | const res = await app.handle(req({ 'accept-encoding': '*' })) 277 | 278 | expect(res.status).toBe(200) 279 | expect(res.headers.get('Content-Encoding')).toBeNull() 280 | expect(res.headers.get('Vary')).toBeNull() 281 | }) 282 | 283 | it(`Should return data from cache`, async () => { 284 | const app = new Elysia().use(compression({ threshold: 0 })).get('/', () => { 285 | return responseShort 286 | }) 287 | 288 | const res = await app.handle(req()) 289 | const test = zlib 290 | .brotliDecompressSync(await res.arrayBuffer()) 291 | .toString('utf-8') 292 | 293 | expect(res.status).toBe(200) 294 | expect(res.headers.get('Content-Encoding')).toBe('br') 295 | expect(res.headers.get('Vary')).toBe('accept-encoding') 296 | expect(test).toBe(responseShort) 297 | 298 | const res2 = await app.handle(req()) 299 | const test2 = zlib 300 | .brotliDecompressSync(await res2.arrayBuffer()) 301 | .toString('utf-8') 302 | 303 | expect(res2.status).toBe(200) 304 | expect(res2.headers.get('Content-Encoding')).toBe('br') 305 | expect(res2.headers.get('Vary')).toBe('accept-encoding') 306 | expect(test2).toBe(responseShort) 307 | expect(test2).toBe(test) 308 | }) 309 | 310 | it(`Don't append vary header if values are *`, async () => { 311 | const app = new Elysia() 312 | .use(compression({ threshold: 0 })) 313 | .get('/', (ctx) => { 314 | ctx.set.headers['Vary'] = 'location, header' 315 | return responseShort 316 | }) 317 | 318 | const res = await app.handle(req()) 319 | 320 | expect(res.status).toBe(200) 321 | expect(res.headers.get('Content-Encoding')).toBe('br') 322 | expect(res.headers.get('Vary')).toBe('location, header, accept-encoding') 323 | }) 324 | }) 325 | -------------------------------------------------------------------------------- /tests/node/cjs/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | if ('Bun' in globalThis) { 3 | throw new Error('❌ Use Node.js to run this test!') 4 | } 5 | 6 | const { compression } = require('elysia-compress') 7 | 8 | if (typeof compression !== 'function') { 9 | throw new Error('❌ CommonJS Node.js failed') 10 | } 11 | 12 | console.log('✅ CommonJS Node.js works!') 13 | -------------------------------------------------------------------------------- /tests/node/cjs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cjs", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "elysia-compress": "../../.." 9 | } 10 | }, 11 | "../../..": { 12 | "version": "1.2.1", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@elysiajs/cors": "^1.0.5", 16 | "@elysiajs/stream": "^1.0.3", 17 | "@eslint/js": "^9.7.0", 18 | "@types/bun": "latest", 19 | "bun-types": "^1.1.20", 20 | "elysia": "^1.0.27", 21 | "eslint": "^9.7.0", 22 | "eslint-config-prettier": "^9.1.0", 23 | "eslint-plugin-prettier": "^5.1.3", 24 | "husky": "^9.0.11", 25 | "lint-staged": "^15.2.7", 26 | "prettier": "^3.3.3", 27 | "rimraf": "^6.0.1", 28 | "typescript": "^5.5.3", 29 | "typescript-eslint": "^7.16.0" 30 | }, 31 | "engines": { 32 | "bun": ">=1.1.8", 33 | "node": ">=18.20.4" 34 | }, 35 | "peerDependencies": { 36 | "elysia": ">= 1.0.27", 37 | "typescript": "^5.5.3" 38 | } 39 | }, 40 | "node_modules/elysia-compress": { 41 | "resolved": "../../..", 42 | "link": true 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/node/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs", 3 | "dependencies": { 4 | "elysia-compress": "../../.." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/node/esm/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | if ('Bun' in globalThis) { 3 | throw new Error('❌ Use Node.js to run this test!') 4 | } 5 | 6 | import { compression } from 'elysia-compress' 7 | 8 | if (typeof compression !== 'function') { 9 | throw new Error('❌ ESM Node.js failed') 10 | } 11 | 12 | console.log('✅ ESM Node.js works!') 13 | -------------------------------------------------------------------------------- /tests/node/esm/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "elysia-compress": "../../.." 9 | } 10 | }, 11 | "../../..": { 12 | "version": "1.2.1", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@elysiajs/cors": "^1.0.5", 16 | "@elysiajs/stream": "^1.0.3", 17 | "@eslint/js": "^9.7.0", 18 | "@types/bun": "latest", 19 | "bun-types": "^1.1.20", 20 | "elysia": "^1.0.27", 21 | "eslint": "^9.7.0", 22 | "eslint-config-prettier": "^9.1.0", 23 | "eslint-plugin-prettier": "^5.1.3", 24 | "husky": "^9.0.11", 25 | "lint-staged": "^15.2.7", 26 | "prettier": "^3.3.3", 27 | "rimraf": "^6.0.1", 28 | "typescript": "^5.5.3", 29 | "typescript-eslint": "^7.16.0" 30 | }, 31 | "engines": { 32 | "bun": ">=1.1.8", 33 | "node": ">=18.20.4" 34 | }, 35 | "peerDependencies": { 36 | "elysia": ">= 1.0.27", 37 | "typescript": "^5.5.3" 38 | } 39 | }, 40 | "node_modules/elysia-compress": { 41 | "resolved": "../../..", 42 | "link": true 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/node/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "elysia-compress": "../../.." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | export const req = (headers: { [key: string]: string } = {}) => 2 | new Request('http://localhost/', { 3 | headers: { 4 | 'accept-encoding': 'br, deflate, gzip, zstd', 5 | ...headers, 6 | }, 7 | }) 8 | 9 | export const responseShort = ` 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 11 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 12 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 13 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.` 14 | 15 | export const responseLong = responseShort.repeat(100) 16 | 17 | export const jsonResponse = Bun.file('./tests/data.json') 18 | -------------------------------------------------------------------------------- /tests/waifu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermaysha/elysia-compress/f2c309881c332e13c88e36de7135e5f408083c8d/tests/waifu.png -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "ESNext", 17 | "DOM", 18 | "ScriptHost" 19 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 20 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 21 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 22 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 23 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 24 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 25 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 26 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 27 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 28 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 29 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 30 | 31 | /* Modules */ 32 | "module": "CommonJS" /* Specify what module code is generated. */, 33 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 34 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 35 | "baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */, 36 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 37 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 38 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 39 | "types": [ 40 | "bun-types" 41 | ] /* Specify type package names to be included without being referenced in a source file. */, 42 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 43 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 46 | 47 | /* JavaScript Support */ 48 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 49 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 50 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 51 | 52 | /* Emit */ 53 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 54 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 55 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 56 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./dist/cjs" /* Specify an output folder for all emitted files. */, 59 | "removeComments": true /* Disable emitting comments. */, 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 69 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 72 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 74 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 75 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 76 | 77 | /* Interop Constraints */ 78 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */, 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": ["src/**/*"] 110 | } 111 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "ESNext", 17 | "DOM", 18 | "ScriptHost" 19 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 20 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 21 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 22 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 23 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 24 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 25 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 26 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 27 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 28 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 29 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 30 | 31 | /* Modules */ 32 | "module": "ES2022" /* Specify what module code is generated. */, 33 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 34 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 35 | "baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */, 36 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 37 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 38 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 39 | "types": [ 40 | "bun-types" 41 | ] /* Specify type package names to be included without being referenced in a source file. */, 42 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 43 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 46 | 47 | /* JavaScript Support */ 48 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 49 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 50 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 51 | 52 | /* Emit */ 53 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 54 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 55 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 56 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 59 | "removeComments": true /* Disable emitting comments. */, 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 69 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 72 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 74 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 75 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 76 | 77 | /* Interop Constraints */ 78 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */, 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": ["src/**/*"] 110 | } 111 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "ESNext", 17 | "DOM", 18 | "ScriptHost" 19 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 20 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 21 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 22 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 23 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 24 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 25 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 26 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 27 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 28 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 29 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 30 | 31 | /* Modules */ 32 | "module": "ES2022" /* Specify what module code is generated. */, 33 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 34 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 35 | // "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 36 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 37 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 38 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // "resolveJsonModule": true, /* Enable importing .json files. */ 42 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 43 | 44 | /* JavaScript Support */ 45 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 48 | 49 | /* Emit */ 50 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 51 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 55 | // "outDir": "./dist", /* Specify an output folder for all emitted files. */ 56 | // "removeComments": true, /* Disable emitting comments. */ 57 | "noEmit": true /* Disable emitting files from a compilation. */, 58 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 59 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 64 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 65 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 66 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 67 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 68 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 69 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 70 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 71 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 72 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 80 | 81 | /* Type Checking */ 82 | "strict": true /* Enable all strict type-checking options. */, 83 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 84 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 86 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 88 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 89 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */, 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | 102 | /* Completeness */ 103 | "skipDefaultLibCheck": true /* Skip type checking .d.ts files that are included with TypeScript. */, 104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 105 | } 106 | // "include": ["src/**/*"] 107 | } 108 | --------------------------------------------------------------------------------