├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── cli.js ├── bun.lockb ├── examples ├── deno │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── README.md │ ├── deno.jsonc │ ├── main.ts │ └── test.ts ├── node-cjs │ ├── .gitignore │ ├── README.md │ ├── main.js │ ├── package.json │ ├── test.ts │ └── tsconfig.json └── node-esm │ ├── .gitignore │ ├── .vscode │ └── launch.json │ ├── README.md │ ├── main.ts │ ├── package.json │ ├── test.ts │ └── tsconfig.json ├── fixup ├── package.json ├── scripts ├── runtime.js └── utils.js ├── src ├── config.ts ├── elysia-bun-types.ts ├── env │ ├── deno │ │ ├── deno-types.d.ts │ │ ├── index.ts │ │ └── server.ts │ ├── error.ts │ ├── headers.ts │ └── node │ │ ├── index.ts │ │ ├── request.ts │ │ ├── response.ts │ │ ├── server.ts │ │ └── utils.ts └── modules.d.ts ├── tests ├── TEST.txt ├── index.ts ├── modules.d.ts └── nested │ └── TEST.txt ├── tsconfig.base.json ├── tsconfig.cjs.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | NOTES.txt 4 | 5 | keys/ 6 | dist/ 7 | node_modules/ 8 | sandbox/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .env 4 | 5 | keys/ 6 | sandbox/ 7 | node_modules/ 8 | 9 | examples 10 | tests 11 | CHANGELOG.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.6.4 - 12 Jan 2024 2 | 3 | Bug fix: 4 | 5 | - status code 204 responses [#3](https://github.com/bogeychan/elysia-polyfills/issues/3) 6 | 7 | # 0.6.3 - 07 Jan 2024 8 | 9 | Bug fix: 10 | 11 | - returning raw response from handler [#4](https://github.com/bogeychan/elysia-polyfills/issues/4) 12 | 13 | # 0.6.2 - 06 Jan 2024 14 | 15 | Improvement: 16 | 17 | - use `tsx` instead of `ts-node` 18 | 19 | Bug fix: 20 | 21 | - setting multiple headers with `Node.js` 22 | 23 | # 0.6.1 - 16 Sep 2023 24 | 25 | Improvement: 26 | 27 | - using `Deno.serve` instead of `std` module 28 | 29 | # 0.6.0 - 08 Sep 2023 30 | 31 | Feature: 32 | 33 | - update to elysia 0.6.19 34 | 35 | Bug fix: 36 | 37 | - missing implementation of `server.reload` [#2](https://github.com/bogeychan/elysia-polyfills/issues/2) 38 | 39 | # 0.5.3 - 08 Jun 2023 40 | 41 | Feature: 42 | 43 | - `https` support for `Node.js` and `Deno` 44 | 45 | Bug fix: 46 | 47 | - handle outer errors 48 | 49 | # 0.5.2 - 04 Jun 2023 50 | 51 | Improvement: 52 | 53 | - with version [0.5.15](https://github.com/elysiajs/elysia/issues/50), `Elysia.js` supports `CommonJS`. Therefore, a build step is no longer required. 54 | 55 | # 0.5.1 - 01 Jun 2023 56 | 57 | Feature: 58 | 59 | - CommonJS support 60 | 61 | Bug fix: 62 | 63 | - elysia runtime function check 64 | 65 | # 0.5.0 - 31 Mai 2023 66 | 67 | Bug fix: 68 | 69 | - updated dependencies including support for `Elysia.js` Version `0.5.9` 70 | 71 | # 0.0.7 - 01 Mai 2023 72 | 73 | Feature: 74 | 75 | - support & tests for `@elysiajs/static` 76 | 77 | Bug fix: 78 | 79 | - nested dependencies with `Deno` & `Node.js` 80 | 81 | # 0.0.6 - 28 Apr 2023 82 | 83 | Bug fix: 84 | 85 | - with release [1.33.0](https://github.com/denoland/deno/releases/tag/v1.33.0), `Deno` introduced a new way to resolve `node_modules` dependencies 86 | 87 | # 0.0.5 - 24 Apr 2023 88 | 89 | Improvement: 90 | 91 | - add `Deno` tests for `cors` & `html` 92 | - move `typescript` to `devDependencies` 93 | 94 | # 0.0.4 - 24 Apr 2023 95 | 96 | Feature: 97 | 98 | - cli transpile additional packages 99 | 100 | Improvement: 101 | 102 | - `Node.js` example & tests for `@elysiajs/cors`, `@elysiajs/html` 103 | 104 | # 0.0.3 - 24 Apr 2023 105 | 106 | Improvement: 107 | 108 | - use `Deno.env.get` instead of `process.env.NODE_ENV` 109 | - use `Deno.listen` instead of `Node.js` polyfill 110 | 111 | # 0.0.2 - 23 Apr 2023 112 | 113 | Bug fix: 114 | 115 | - add `Node.js` request body `duplex` 116 | 117 | # 0.0.1 - 23 Apr 2023 118 | 119 | Feature: 120 | 121 | - `Node.js` & `Deno` 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 bogeychan 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 | # @bogeychan/elysia-polyfills 2 | 3 | Collection of experimental [Elysia.js](https://elysiajs.com) polyfills: 4 | 5 | | Package | [Node.js](https://nodejs.org) (v18.16.0) | [Deno](https://deno.land) (1.36.4#1,3) | 6 | | ---------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------- | 7 | | [elysia](https://npmjs.com/package/elysia) (0.6.19#2) | 🔬 | 🔬 | 8 | | [@elysiajs/cors](https://www.npmjs.com/package/@elysiajs/cors) (0.6.0) | ✅ | ✅ | 9 | | [@elysiajs/html](https://www.npmjs.com/package/@elysiajs/html) (0.6.4) | ✅ | ✅ | 10 | | [@elysiajs/bearer](https://www.npmjs.com/package/@elysiajs/bearer) (0.6.0) | ✅ | ✅ | 11 | | [@elysiajs/cookie](https://www.npmjs.com/package/@elysiajs/cookie) (0.6.1) | ✅ | ✅ | 12 | | [@elysiajs/swagger](https://www.npmjs.com/package/@elysiajs/swagger) (0.6.1) | ✅ | ✅ | 13 | | [@elysiajs/static](https://www.npmjs.com/package/@elysiajs/static) (0.6.0) | ✅ | ✅ | 14 | | ... | ... | ... | 15 | 16 | **_Legend_** 17 | 18 | 🔬 - Under testing 19 | 20 | ✅ - Fully supported 21 | 22 | ⚠️ - Partial supported 23 | 24 | ❌ - Unsupported 25 | 26 | ## 🚩Notes 27 | 28 | #1 With release [1.33.0](https://github.com/denoland/deno/releases/tag/v1.33.0), `Deno` introduced a new way to resolve `node_modules` dependencies. You need to update `@bogeychan/elysia-polyfills` to at least version `0.0.7` in order to be compatible. 29 | 30 | #2 With version [0.5.15](https://github.com/elysiajs/elysia/issues/50), `Elysia.js` supports `CommonJS`. Therefore, a build step is no longer required. The plugins listed above can be used out of the box for `ESM` and `CommonJS` projects. 31 | 32 | #3 With release [1.35.0](https://github.com/denoland/deno/releases/tag/v1.35.0), `Deno` stabilized the `Deno.serve()` API. From now on `Deno.serve()` is used instead of the [Deno Standard Modules](https://github.com/denoland/deno_std). 33 | 34 | ## Installation 35 | 36 | ```bash 37 | yarn add @bogeychan/elysia-polyfills 38 | ``` 39 | 40 | ## Usage 41 | 42 | Checkout the [examples](./examples) folder on Github and follow its setup guide. 43 | 44 | **_OR_** use an [Elysia.js scaffold](https://www.npmjs.com/package/create-elysia). 45 | 46 | ### Node.js 47 | 48 | ```ts 49 | import '@bogeychan/elysia-polyfills/node/index.js'; 50 | 51 | import { Elysia } from 'elysia'; 52 | 53 | new Elysia().get('/', () => ({ hello: 'Node.js👋' })).listen(8080); 54 | ``` 55 | 56 | Create a new `Node.js` project: 57 | 58 | ```bash 59 | npm create elysia@latest my-elysia-app --template node-ts 60 | ``` 61 | 62 | ### Deno 63 | 64 | ```ts 65 | import 'npm:@bogeychan/elysia-polyfills/deno/index.js'; 66 | 67 | import { Elysia } from 'npm:elysia'; 68 | 69 | new Elysia().get('/', () => ({ hello: 'Deno👋' })).listen(8080); 70 | ``` 71 | 72 | Create a new `Deno` project: 73 | 74 | ```bash 75 | deno run -r=npm:create-elysia --allow-read --allow-write npm:create-elysia my-elysia-app --template deno 76 | ``` 77 | 78 | ## Author 79 | 80 | [bogeychan](https://github.com/bogeychan) 81 | 82 | ## License 83 | 84 | [MIT](LICENSE) 85 | 86 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | 6 | import { updateScriptFolders } from '../scripts/utils.js'; 7 | import { updateElysiaRuntime } from '../scripts/runtime.js'; 8 | 9 | const CWD = process.cwd(); 10 | const NODE_MODULES_PATH = path.resolve(CWD, 'node_modules'); 11 | const DENO_NESTED_MODULES_PATH = path.resolve(NODE_MODULES_PATH, '.deno'); 12 | 13 | const ELYSIA_MODULE_NAME = 'elysia'; 14 | const MEMOIRIST_MODULE_NAME = 'memoirist'; 15 | const EXTRA_MODULE_NAMES = process.argv.slice(2); 16 | 17 | /** 18 | * @param {string} filePath 19 | */ 20 | function exitOnMissingFile(filePath) { 21 | console.error( 22 | `❌ "${path.dirname(filePath)}" doesn't contain '${path.basename( 23 | filePath 24 | )}'` 25 | ); 26 | process.exit(1); 27 | } 28 | 29 | if (!fs.existsSync(NODE_MODULES_PATH)) { 30 | exitOnMissingFile(NODE_MODULES_PATH); 31 | } 32 | 33 | /** 34 | * @type {string[]} 35 | */ 36 | let denoModules = []; 37 | 38 | if (fs.existsSync(DENO_NESTED_MODULES_PATH)) { 39 | denoModules = fs.readdirSync(DENO_NESTED_MODULES_PATH); 40 | } 41 | 42 | /** 43 | * @param {string} moduleName 44 | * @returns {string[]} 45 | */ 46 | function resolveModulePaths(moduleName) { 47 | const paths = []; 48 | 49 | // Check -> ./node_modules/elysia 50 | const nodeModulePath = path.join(NODE_MODULES_PATH, moduleName); 51 | 52 | if (fs.existsSync(nodeModulePath)) { 53 | paths.push(nodeModulePath); 54 | } 55 | 56 | /** 57 | * @param {string} mainModuleName 58 | */ 59 | function pushNestedNodeModulePath(mainModuleName) { 60 | // Check -> ./node_modules/elysia/node_modules/raikiri 61 | const nestedNodeModulePath = path.join( 62 | NODE_MODULES_PATH, 63 | mainModuleName, 64 | 'node_modules', 65 | moduleName 66 | ); 67 | 68 | if (fs.existsSync(nestedNodeModulePath)) { 69 | paths.push(nestedNodeModulePath); 70 | } 71 | } 72 | 73 | pushNestedNodeModulePath(ELYSIA_MODULE_NAME); 74 | pushNestedNodeModulePath(MEMOIRIST_MODULE_NAME); 75 | 76 | // Check -> ./node_modules/.deno/elysia@0.4.9 77 | const denoModuleNamePrefix = moduleName.replaceAll('/', '+'); 78 | 79 | for (const denoModuleName of denoModules) { 80 | if (denoModuleName.startsWith(`${denoModuleNamePrefix}@`)) { 81 | const denoModulePath = path.join( 82 | DENO_NESTED_MODULES_PATH, 83 | denoModuleName, 84 | 'node_modules', 85 | moduleName 86 | ); 87 | 88 | paths.push(denoModulePath); 89 | } 90 | } 91 | 92 | if (paths.length === 0) { 93 | exitOnMissingFile(nodeModulePath); 94 | } 95 | 96 | return paths; 97 | } 98 | 99 | const elysiaPaths = resolveModulePaths(ELYSIA_MODULE_NAME); 100 | const memoiristPaths = resolveModulePaths(MEMOIRIST_MODULE_NAME); 101 | 102 | console.log("Let's goo\n"); 103 | 104 | /** 105 | * @param {string[]} folderPaths 106 | * @see https://github.com/denoland/deno/issues/17784#issuecomment-1445195226 107 | */ 108 | function modifyPackageJson(folderPaths) { 109 | for (const folderPath of folderPaths) { 110 | const filePath = path.resolve(folderPath, 'package.json'); 111 | const json = JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' })); 112 | 113 | json['type'] = 'module'; 114 | 115 | fs.writeFileSync(filePath, JSON.stringify(json)); 116 | console.log(`✅ Updated package for "${filePath}"`); 117 | } 118 | } 119 | 120 | modifyPackageJson(elysiaPaths); 121 | modifyPackageJson(memoiristPaths); 122 | 123 | const extraPaths = EXTRA_MODULE_NAMES.map((extraPath) => 124 | resolveModulePaths(extraPath) 125 | ); 126 | 127 | for (const extraPath of extraPaths) { 128 | modifyPackageJson(extraPath); 129 | } 130 | 131 | console.log('\n'); 132 | 133 | updateScriptFolders(elysiaPaths); 134 | updateScriptFolders(memoiristPaths); 135 | 136 | for (const extraPath of extraPaths) { 137 | updateScriptFolders(extraPath); 138 | } 139 | 140 | console.log('\n'); 141 | 142 | updateElysiaRuntime(elysiaPaths); 143 | 144 | console.log('\ndone.'); 145 | process.exit(0); 146 | 147 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bogeychan/elysia-polyfills/29a8827814b1502dd7633d5c9ee4f3cbf62f3f66/bun.lockb -------------------------------------------------------------------------------- /examples/deno/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | deno.lock 4 | 5 | tests/ 6 | node_modules/ -------------------------------------------------------------------------------- /examples/deno/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno" 4 | ] 5 | } -------------------------------------------------------------------------------- /examples/deno/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } -------------------------------------------------------------------------------- /examples/deno/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Follow these steps to run [Elysia.js](https://elysiajs.com) under [Deno](https://deno.land): 4 | 5 | 1. You're ready to go. Checkout the deno tasks inside [deno.jsonc](./deno.jsonc)! 6 | ```bash 7 | deno task start 8 | ``` 9 | 10 | -------------------------------------------------------------------------------- /examples/deno/deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "dev": "deno run --allow-read --allow-net --watch main.ts", 4 | "start": "deno run --allow-read --allow-net main.ts", 5 | "test": "rm -fr ./tests && cp -r ../../tests ./tests && deno run --allow-read ./test.ts" 6 | }, 7 | "imports": { 8 | "@bogeychan/elysia-polyfills/deno/index.js": "../../dist/mjs/env/deno/index.js", 9 | "elysia": "npm:elysia@0.6.19", 10 | "@sinclair/typebox": "npm:@sinclair/typebox@0.30.4", 11 | // --- test dependencies 12 | "@elysiajs/cors": "npm:@elysiajs/cors@0.6.0", 13 | "@elysiajs/html": "npm:@elysiajs/html@0.6.4", 14 | "@elysiajs/bearer": "npm:@elysiajs/bearer@0.6.0", 15 | "@elysiajs/cookie": "npm:@elysiajs/cookie@0.6.1", 16 | "@elysiajs/static": "npm:@elysiajs/static@0.6.0", 17 | "@elysiajs/swagger": "npm:@elysiajs/swagger@0.6.1", 18 | "chai": "npm:chai" 19 | // --- test dependencies 20 | } 21 | } -------------------------------------------------------------------------------- /examples/deno/main.ts: -------------------------------------------------------------------------------- 1 | import '@bogeychan/elysia-polyfills/deno/index.js'; 2 | import '@sinclair/typebox'; // deno doesn't download peerDependencies. this one is required 3 | import { Elysia } from 'elysia'; 4 | 5 | import 'chai'; // test dependency 6 | import '@elysiajs/html'; 7 | import '@elysiajs/cors'; 8 | import '@elysiajs/bearer'; 9 | import '@elysiajs/static'; 10 | import { cookie } from '@elysiajs/cookie'; 11 | import { swagger } from '@elysiajs/swagger'; 12 | 13 | // import 'npm:@bogeychan/elysia-polyfills/deno/index.js'; 14 | // import 'npm:@sinclair/typebox@0.26.0'; 15 | // import { Elysia } from 'npm:elysia@0.4.9'; 16 | 17 | const key = Deno.readTextFileSync('../../keys/localhost-key.pem'); 18 | const cert = Deno.readTextFileSync('../../keys/localhost.pem'); 19 | 20 | new Elysia() 21 | .use(cookie()) 22 | .use(swagger()) 23 | .get('/', () => ({ hello: 'Deno👋' })) 24 | .post('/:world', (ctx) => `Hello ${ctx.params.world}`) 25 | .get('/teapot', () => { 26 | throw { message: "I'm a teapot", status: 418 }; 27 | }) 28 | .get('/api', ({ setCookie }) => { 29 | setCookie('a', 'b'); 30 | setCookie('c', 'd'); 31 | 32 | return { my: 'json' }; 33 | }) 34 | .listen({ key, cert, port: 8443 }); 35 | 36 | -------------------------------------------------------------------------------- /examples/deno/test.ts: -------------------------------------------------------------------------------- 1 | import '@bogeychan/elysia-polyfills/deno/index.js'; 2 | 3 | import { runTests } from './tests/index.ts'; 4 | 5 | runTests('deno'); 6 | -------------------------------------------------------------------------------- /examples/node-cjs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | package-lock.json 4 | 5 | tests/ 6 | node_modules/ -------------------------------------------------------------------------------- /examples/node-cjs/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Follow these steps to run [Elysia.js](https://elysiajs.com) under [Node.js](https://nodejs.org): 4 | 5 | 1. Download dependencies 6 | ```bash 7 | npm i 8 | ``` 9 | 2. You're ready to go. Checkout the scripts inside [package.json](./package.json)! 10 | ```bash 11 | npm start 12 | ``` 13 | 14 | -------------------------------------------------------------------------------- /examples/node-cjs/main.js: -------------------------------------------------------------------------------- 1 | require('@bogeychan/elysia-polyfills/node/index.js'); 2 | 3 | const { Elysia } = require('elysia'); 4 | const { cookie } = require('@elysiajs/cookie'); 5 | const { swagger } = require('@elysiajs/swagger'); 6 | 7 | const app = new Elysia() 8 | .use(cookie()) 9 | .use(swagger()) 10 | .get('/', () => ({ hello: 'Node.js👋' })) 11 | .post('/:world', (ctx) => `Hello ${ctx.params.world}`) 12 | .get('/teapot', () => { 13 | throw { message: "I'm a teapot", status: 418 }; 14 | }) 15 | .get('/api', ({ setCookie }) => { 16 | setCookie('a', 'b'); 17 | setCookie('c', 'd'); 18 | 19 | return { my: 'json' }; 20 | }) 21 | .listen(8080); 22 | 23 | console.log(`Listening on http://localhost:${app.server?.port}`); 24 | -------------------------------------------------------------------------------- /examples/node-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "node ./main.js", 5 | "test": "rm -fr ./tests && cp -r ../../tests ./tests && tsx ./test.ts" 6 | }, 7 | "dependencies": { 8 | "@bogeychan/elysia-polyfills": "../..", 9 | "elysia": "^0.6.19" 10 | }, 11 | "devDependencies": { 12 | "@elysiajs/bearer": "^0.6.0", 13 | "@elysiajs/cookie": "^0.6.1", 14 | "@elysiajs/cors": "^0.6.0", 15 | "@elysiajs/html": "^0.6.4", 16 | "@elysiajs/static": "^0.6.0", 17 | "@elysiajs/swagger": "^0.6.1", 18 | "@types/chai": "^4.3.4", 19 | "@types/node": "^18.15.13", 20 | "chai": "^4.3.7", 21 | "tsx": "^4.7.0" 22 | } 23 | } -------------------------------------------------------------------------------- /examples/node-cjs/test.ts: -------------------------------------------------------------------------------- 1 | import '@bogeychan/elysia-polyfills/node/index.js'; 2 | 3 | import { runTests } from './tests/index.js'; 4 | 5 | runTests('node'); 6 | -------------------------------------------------------------------------------- /examples/node-cjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2016", 5 | "skipLibCheck": true, 6 | "moduleResolution": "node", 7 | "outDir": "./tests/out" 8 | } 9 | } -------------------------------------------------------------------------------- /examples/node-esm/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | package-lock.json 4 | 5 | tests/ 6 | node_modules/ -------------------------------------------------------------------------------- /examples/node-esm/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Test", 6 | "command": "npm run lt", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /examples/node-esm/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Follow these steps to run [Elysia.js](https://elysiajs.com) under [Node.js](https://nodejs.org): 4 | 5 | 1. Install dependencies 6 | ```bash 7 | npm i 8 | ``` 9 | 2. You're ready to go. Checkout the scripts inside [package.json](./package.json)! 10 | ```bash 11 | npm start 12 | ``` 13 | 14 | -------------------------------------------------------------------------------- /examples/node-esm/main.ts: -------------------------------------------------------------------------------- 1 | import '@bogeychan/elysia-polyfills/node/index.js'; 2 | 3 | import { Elysia } from 'elysia'; 4 | import { cookie } from '@elysiajs/cookie'; 5 | import { swagger } from '@elysiajs/swagger'; 6 | 7 | import * as fs from 'node:fs'; 8 | const key = fs.readFileSync('../../keys/localhost-key.pem', { 9 | encoding: 'utf-8' 10 | }); 11 | const cert = fs.readFileSync('../../keys/localhost.pem', { encoding: 'utf-8' }); 12 | 13 | const app = new Elysia() 14 | .use(cookie()) 15 | .use(swagger()) 16 | .get('/', () => ({ hello: 'Node.js👋' })) 17 | .post('/:world', (ctx) => `Hello ${ctx.params.world}`) 18 | .get('/teapot', () => { 19 | throw { message: "I'm a teapot", status: 418 }; 20 | }) 21 | .get('/api', ({ setCookie }) => { 22 | setCookie('a', 'b'); 23 | setCookie('c', 'd'); 24 | 25 | return { my: 'json' }; 26 | }) 27 | .listen({ key, cert, port: 8443 }); 28 | 29 | console.log(`Listening on https://localhost:${app.server!.port}`); 30 | 31 | -------------------------------------------------------------------------------- /examples/node-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "tsx ./main.ts", 6 | "test": "rm -fr ./tests && cp -r ../../tests ./tests && npm run lt", 7 | "lt": "tsx ./test.ts" 8 | }, 9 | "dependencies": { 10 | "@bogeychan/elysia-polyfills": "../..", 11 | "elysia": "^0.6.19" 12 | }, 13 | "devDependencies": { 14 | "@elysiajs/bearer": "^0.6.0", 15 | "@elysiajs/cookie": "^0.6.1", 16 | "@elysiajs/cors": "^0.6.0", 17 | "@elysiajs/html": "^0.6.4", 18 | "@elysiajs/static": "^0.6.0", 19 | "@elysiajs/swagger": "^0.6.1", 20 | "@types/chai": "^4.3.4", 21 | "@types/node": "^18.15.13", 22 | "chai": "^4.3.7", 23 | "tsx": "^4.7.0" 24 | } 25 | } -------------------------------------------------------------------------------- /examples/node-esm/test.ts: -------------------------------------------------------------------------------- 1 | import '@bogeychan/elysia-polyfills/node/index.js'; 2 | 3 | import { runTests } from './tests/index.js'; 4 | 5 | runTests('node'); 6 | -------------------------------------------------------------------------------- /examples/node-esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "strict": true, 6 | "moduleResolution": "node" 7 | } 8 | } -------------------------------------------------------------------------------- /fixup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Add package.json files to cjs/mjs subtrees 4 | # Based on https://github.com/sensedeep/dynamodb-onetable/blob/956211b862995d3d8e6730ab30bb05e2cfcb27e6/fixup 5 | # See https://www.sensedeep.com/blog/posts/2021/how-to-create-single-source-npm-module.html 6 | # 7 | 8 | cat >dist/cjs/package.json <dist/mjs/package.json <= 0.6.19" 35 | }, 36 | "devDependencies": { 37 | "@types/chai": "^4.3.4", 38 | "@types/node": "18", 39 | "bun-types": "^1.0.1", 40 | "elysia": "^0.6.19", 41 | "typescript": "^5.2.2" 42 | }, 43 | "dependencies": { 44 | "acorn": "^8.8.2", 45 | "acorn-walk": "^8.2.0", 46 | "astring": "^1.8.4", 47 | "glob": "^10.2.1", 48 | "ts-morph": "^18.0.0" 49 | }, 50 | "homepage": "https://github.com/bogeychan/elysia-polyfills", 51 | "bugs": "https://github.com/bogeychan/elysia-polyfills/issues", 52 | "license": "MIT", 53 | "keywords": [ 54 | "elysia", 55 | "polyfills", 56 | "deno", 57 | "node" 58 | ] 59 | } -------------------------------------------------------------------------------- /scripts/runtime.js: -------------------------------------------------------------------------------- 1 | import { Project } from 'ts-morph'; 2 | 3 | /** 4 | * @param {string} path 5 | * @see https://github.com/bogeychan/elysia-polyfills/issues/1#issuecomment-1568902510 6 | */ 7 | function updateElysia(path) { 8 | const project = new Project(); 9 | project.addSourceFilesAtPaths(`${path}/dist/**/*.js`); 10 | 11 | const compose = project.getSourceFile('compose.js'); 12 | 13 | compose 14 | ?.getVariableDeclaration('isFnUse') 15 | ?.getInitializer() 16 | ?.replaceWithText(isFnUse.toString()); 17 | 18 | project.saveSync(); 19 | 20 | console.log(`✅ Updated runtime for "${path}"`); 21 | } 22 | 23 | /** 24 | * @param {string[]} elysiaPaths 25 | */ 26 | export function updateElysiaRuntime(elysiaPaths) { 27 | for (const elysiaPath of elysiaPaths) { 28 | updateElysia(elysiaPath); 29 | } 30 | } 31 | 32 | /** 33 | * @param {string} keyword 34 | * @param {string} fnLiteral 35 | * @see https://github.com/elysiajs/elysia/blob/08a4375b91d9d359fe7a0264b29d7512a729ea8f/src/compose.ts#L73 36 | */ 37 | const isFnUse = (keyword, fnLiteral) => { 38 | // >> INSERT 39 | fnLiteral = fnLiteral.trimStart(); 40 | 41 | const argument = 42 | fnLiteral.startsWith('(') || fnLiteral.startsWith('function') 43 | ? // (context) => {} 44 | fnLiteral.slice(fnLiteral.indexOf('(') + 1, fnLiteral.indexOf(')')) 45 | : // context => {} 46 | fnLiteral.slice(0, fnLiteral.indexOf('=') - 1); 47 | // << INSERT 48 | 49 | if (argument === '') return false; 50 | 51 | if (argument.charCodeAt(0) === 123) { 52 | // >> INSERT 53 | if (argument.includes(`{${keyword}`) || argument.includes(`,${keyword}`)) 54 | return true; 55 | // << INSERT 56 | 57 | if (argument.includes(`{ ${keyword}`) || argument.includes(`, ${keyword}`)) 58 | return true; 59 | 60 | return false; 61 | } 62 | 63 | if ( 64 | fnLiteral.match(new RegExp(`${argument}(.${keyword}|\\["${keyword}"\\])`)) 65 | ) { 66 | return true; 67 | } 68 | 69 | const aliases = [argument]; 70 | 71 | const findAliases = new RegExp(` (\\w+) = context`, 'g'); 72 | for (const found of fnLiteral.matchAll(findAliases)) aliases.push(found[1]); 73 | 74 | const destructuringRegex = new RegExp(`{.*?} = (${aliases.join('|')})`, 'g'); 75 | 76 | for (const [params] of fnLiteral.matchAll(destructuringRegex)) { 77 | if (params.includes(`{ ${keyword}`) || params.includes(`, ${keyword}`)) 78 | return true; 79 | } 80 | 81 | return false; 82 | }; 83 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | 4 | import { globSync } from 'glob'; 5 | import * as acorn from 'acorn'; 6 | import * as walk from 'acorn-walk'; 7 | import * as astring from 'astring'; 8 | 9 | /** 10 | * @param {string} filePath 11 | * @param {string} fileExtension 12 | */ 13 | export function updateScript(filePath, fileExtension = '.js') { 14 | const folderPath = path.dirname(filePath); 15 | const script = fs.readFileSync(filePath, { encoding: 'utf-8' }); 16 | 17 | const ast = acorn.parse(script, { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module' 20 | }); 21 | 22 | walk.simple(ast, { 23 | // import {} from "./file" 24 | // https://github.com/estree/estree/blob/749e8f0bf3de3c04708e3250c92641b3eeefbb15/experimental/import-attributes.md 25 | ImportDeclaration(node) { 26 | // @ts-ignore 27 | ensureUpdateImportNode({ node, fileExtension, folderPath }); 28 | }, 29 | // export * from "./file" 30 | // https://github.com/estree/estree/blob/749e8f0bf3de3c04708e3250c92641b3eeefbb15/experimental/import-attributes.md 31 | ExportAllDeclaration(node) { 32 | // @ts-ignore 33 | ensureUpdateImportNode({ node, fileExtension, folderPath }); 34 | }, 35 | // export {} from "./file" 36 | // https://github.com/estree/estree/blob/749e8f0bf3de3c04708e3250c92641b3eeefbb15/experimental/import-attributes.md 37 | ExportNamedDeclaration(node) { 38 | // @ts-ignore 39 | ensureUpdateImportNode({ node, fileExtension, folderPath }); 40 | } 41 | }); 42 | 43 | fs.writeFileSync(filePath, astring.generate(ast)); 44 | 45 | console.log(`✅ Updated imports for "${filePath}"`); 46 | } 47 | 48 | /** 49 | * @param {string[]} folderPaths 50 | * @param {string} destinationFolder 51 | */ 52 | export function updateScriptFolders(folderPaths, destinationFolder = 'dist') { 53 | for (const folderPath of folderPaths) { 54 | updateScriptFolder(path.resolve(folderPath, destinationFolder)); 55 | } 56 | } 57 | 58 | /** 59 | * @param {string} folderPath 60 | * @param {string} fileExtension 61 | */ 62 | export function updateScriptFolder(folderPath, fileExtension = '.js') { 63 | const files = globSync( 64 | path 65 | .resolve(folderPath, '**', '*.js') 66 | //! -> windows 67 | .replace(/\\/g, '/') 68 | ); 69 | 70 | for (const file of files) { 71 | updateScript(file, fileExtension); 72 | } 73 | } 74 | 75 | /** 76 | * TODO: cache scriptPath's lstat -> folder or not 77 | * 78 | * @param {{ node: typeof acorn.Node & { source: { value: string, raw: string } }, fileExtension: string, folderPath: string }} options 79 | */ 80 | function ensureUpdateImportNode({ node, fileExtension, folderPath }) { 81 | const value = node?.source?.value; 82 | 83 | if ( 84 | typeof value === 'string' && 85 | value.startsWith('.') && 86 | !value.endsWith(fileExtension) 87 | ) { 88 | const scriptPath = path.resolve(folderPath, value); 89 | 90 | const lstat = fs.lstatSync(scriptPath, { throwIfNoEntry: false }); 91 | if (lstat && lstat.isDirectory()) { 92 | node.source.value = `${node.source.value}/index`; 93 | } 94 | 95 | node.source.raw = `"${node.source.value}${fileExtension}"`; 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { TBunServeOptions, TBunTLSServeOptions } from './elysia-bun-types.js'; 2 | 3 | type ValidOptions = Required> & { 4 | port: number; 5 | key?: string; 6 | cert?: string; 7 | }; 8 | 9 | export function ensureDefaults(options: TBunServeOptions) { 10 | if (typeof options.port === 'undefined') { 11 | options.port = process.env.PORT ?? '3000'; 12 | } 13 | 14 | if (typeof options.port === 'string') { 15 | options.port = parseInt(options.port); 16 | } 17 | 18 | if (isNaN(options.port)) { 19 | throw new Error(`Invalid port "${options.port}"`); 20 | } 21 | 22 | if (!options.hostname) { 23 | options.hostname = '0.0.0.0'; 24 | } 25 | 26 | const { key, cert } = options as TBunTLSServeOptions; 27 | if (typeof key !== 'undefined' && typeof cert !== 'undefined') { 28 | if (typeof key !== 'string' || typeof cert !== 'string') { 29 | throw new Error( 30 | `Key and Cert are only supported in "string" (i.e. PEM) format` 31 | ); 32 | } 33 | } 34 | 35 | return options as ValidOptions; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/elysia-bun-types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ServeOptions, 3 | Server, 4 | TLSServeOptions, 5 | TLSWebSocketServeOptions, 6 | WebSocketServeOptions 7 | } from 'bun'; 8 | 9 | type TElysiaBun = Pick; 10 | type TElysiaServer = Omit; 11 | type TBunHeaders = Headers; 12 | 13 | type TBunServer = Server; 14 | type TBunTLSServeOptions = TLSServeOptions; 15 | type TBunServeOptions = 16 | | ServeOptions 17 | | TLSServeOptions 18 | | WebSocketServeOptions 19 | | TLSWebSocketServeOptions; 20 | type TBunRequest = Request; 21 | type TBunFileBlob = ReturnType; 22 | 23 | export type { 24 | TElysiaBun, 25 | TElysiaServer, 26 | TBunServer, 27 | TBunFileBlob, 28 | TBunServeOptions, 29 | TBunRequest, 30 | TBunHeaders, 31 | TBunTLSServeOptions 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /src/env/deno/deno-types.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Deno { 2 | interface Server { 3 | close(): void; 4 | } 5 | 6 | interface Closer { 7 | close(): void; 8 | } 9 | 10 | type ServeOptions = { 11 | port?: number; 12 | hostname?: string; 13 | onError?: (error: unknown) => Response | Promise; 14 | }; 15 | 16 | type ServeTlsOptions = ServeOptions & { 17 | key: string; 18 | cert: string; 19 | }; 20 | 21 | type ServeHandler = (request: Request) => Response | Promise; 22 | } 23 | 24 | const Deno: { 25 | env: { 26 | get(name: string): string | undefined; 27 | }; 28 | readFileSync(path: string | URL): Uint8Array; // https://deno.land/api@v1.33.1?s=Deno.readFileSync 29 | readTextFileSync(path: string): string; 30 | 31 | // https://deno.land/api@v1.35.0?s=Deno.serve 32 | serve( 33 | options: Deno.ServeOptions | Deno.ServeTlsOptions, 34 | handler: Deno.ServeHandler 35 | ): Deno.Server; 36 | 37 | errors: { 38 | BadResource; 39 | InvalidData; 40 | UnexpectedEof; 41 | ConnectionReset; 42 | NotConnected; 43 | Http; 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/env/deno/index.ts: -------------------------------------------------------------------------------- 1 | import './server.js'; 2 | import '../headers.js'; 3 | 4 | -------------------------------------------------------------------------------- /src/env/deno/server.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type { 4 | TBunServeOptions, 5 | TBunServer, 6 | TElysiaBun, 7 | TBunFileBlob, 8 | TElysiaServer 9 | } from '../../elysia-bun-types.js'; 10 | 11 | import { ensureDefaults } from '../../config.js'; 12 | 13 | import { handleError } from '../error.js'; 14 | 15 | const ElysiaBun: TElysiaBun = { 16 | // @ts-ignore UnixServeOptions 17 | serve(options: TBunServeOptions) { 18 | const { hostname, port, key, cert } = ensureDefaults(options); 19 | 20 | if (typeof options.development === 'undefined') { 21 | options.development = Deno.env.get('NODE_ENV') !== 'production'; 22 | } 23 | 24 | const serveOptions: Deno.ServeOptions = { 25 | hostname, 26 | port, 27 | onError: (error) => handleError(options, server, error) 28 | }; 29 | 30 | if (key && cert) { 31 | const serveTlsOptions = serveOptions as Deno.ServeTlsOptions; 32 | serveTlsOptions.key = key; 33 | serveTlsOptions.cert = cert; 34 | } 35 | 36 | let denoServer: Deno.Server; 37 | 38 | // @ts-expect-error 39 | const server: TElysiaServer = { 40 | id: '', 41 | port, 42 | hostname, 43 | development: options.development, 44 | pendingRequests: 0, 45 | pendingWebSockets: 0, 46 | fetch: (request: Request) => 47 | options.fetch.call( 48 | server as TBunServer, 49 | request, 50 | server as TBunServer 51 | ) as ReturnType, 52 | stop() { 53 | try { 54 | denoServer.close(); 55 | } catch { 56 | // Server has already been closed. 57 | } 58 | }, 59 | reload(newOptions) { 60 | if (newOptions.fetch) { 61 | options.fetch = newOptions.fetch; 62 | } 63 | }, 64 | url: new URL(`${key && cert ? 'https' : 'http'}://${hostname}:${port}`) 65 | }; 66 | 67 | denoServer = Deno.serve(serveOptions, (request) => server.fetch(request)); 68 | 69 | return server as TBunServer; 70 | }, 71 | gc() {}, // noop 72 | file(path, options) { 73 | const bytes = Deno.readFileSync(path as string); 74 | return new Blob([bytes], { type: options?.type }) as TBunFileBlob; 75 | } 76 | }; 77 | 78 | // @ts-ignore 79 | globalThis.Bun = ElysiaBun; 80 | -------------------------------------------------------------------------------- /src/env/error.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | TBunServeOptions, 3 | TBunServer, 4 | TElysiaServer 5 | } from '../elysia-bun-types.js'; 6 | 7 | const statusText = 'Internal Server Error'; 8 | 9 | export async function handleError( 10 | options: TBunServeOptions, 11 | server: TElysiaServer, 12 | error: any 13 | ) { 14 | if (options.error) { 15 | const response = await options.error.call(server as TBunServer, error); 16 | 17 | if (response) { 18 | return response; 19 | } 20 | } 21 | 22 | return new Response(statusText, { status: 500, statusText }); 23 | } 24 | -------------------------------------------------------------------------------- /src/env/headers.ts: -------------------------------------------------------------------------------- 1 | import type { TBunHeaders } from '../elysia-bun-types.js'; 2 | 3 | // @ts-expect-error 4 | globalThis.Headers = class Headers 5 | extends globalThis.Headers 6 | implements TBunHeaders 7 | { 8 | toJSON() { 9 | const entries: Record = {}; 10 | 11 | for (const [name, value] of this.entries()) { 12 | entries[name] = value; 13 | } 14 | 15 | return entries; 16 | } 17 | }; 18 | 19 | globalThis.Request = class Request extends globalThis.Request { 20 | // @ts-expect-error 21 | get headers() { 22 | return new globalThis.Headers( 23 | // @ts-expect-error 24 | super.headers 25 | ); 26 | } 27 | }; 28 | 29 | globalThis.Response = class Response extends globalThis.Response { 30 | constructor(body?: Bun.BodyInit | null, init?: Bun.ResponseInit) { 31 | super(init?.status === 204 ? null : body, init); 32 | } 33 | 34 | // @ts-expect-error 35 | get headers() { 36 | return new globalThis.Headers( 37 | // @ts-expect-error 38 | super.headers 39 | ); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/env/node/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import './server.js'; 4 | import '../headers.js'; 5 | 6 | -------------------------------------------------------------------------------- /src/env/node/request.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type http from 'node:http'; 4 | import type { TBunServeOptions } from '../../elysia-bun-types.js'; 5 | 6 | import { toHeaders, toReadableStream } from './utils.js'; 7 | 8 | export async function request( 9 | req: http.IncomingMessage, 10 | { hostname, port }: TBunServeOptions 11 | ): Promise { 12 | const stream = toReadableStream(req); 13 | 14 | return new Promise((res, rej) => { 15 | req.on('end', async () => { 16 | try { 17 | //! TypeError: Failed to parse URL from / 18 | const url = `http://${hostname}:${port}${req.url}`; 19 | 20 | const init: RequestInit = { 21 | headers: toHeaders(req.headers), 22 | method: req.method! 23 | }; 24 | 25 | //! TypeError: Request with GET/HEAD method cannot have body. 26 | switch (req.method) { 27 | case 'GET': 28 | case 'HEAD': 29 | stream.cancel('Request with GET/HEAD method cannot have body'); 30 | break; 31 | default: 32 | init.body = stream; 33 | // @ts-ignore 34 | init.duplex = 'half'; 35 | break; 36 | } 37 | 38 | res(new Request(url, init)); 39 | } catch (error) { 40 | rej(error); 41 | } 42 | }); 43 | 44 | req.on('error', (error) => { 45 | rej(error); 46 | }); 47 | }); 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/env/node/response.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type http from 'node:http'; 4 | 5 | export async function response( 6 | response: Response, 7 | httpResponse: http.ServerResponse & { 8 | req: http.IncomingMessage; 9 | } 10 | ) { 11 | httpResponse.statusCode = response.status; 12 | httpResponse.statusMessage = response.statusText; 13 | 14 | for (const [name, value] of response.headers) { 15 | httpResponse.appendHeader(name, value); 16 | } 17 | 18 | await response.body?.pipeTo( 19 | new WritableStream({ 20 | //! make sure to end the response in outer scope!!! 21 | write(chunk: Buffer) { 22 | httpResponse.write(chunk); 23 | } 24 | }) 25 | ); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/env/node/server.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type { 4 | TBunServeOptions, 5 | TBunFileBlob, 6 | TBunServer, 7 | TElysiaBun, 8 | TElysiaServer 9 | } from '../../elysia-bun-types.js'; 10 | 11 | import fs from 'node:fs'; 12 | import http from 'node:http'; 13 | import https from 'node:https'; 14 | import { Blob } from 'node:buffer'; 15 | import { request } from './request.js'; 16 | import { response } from './response.js'; 17 | import { ensureDefaults } from '../../config.js'; 18 | import { handleError } from '../error.js'; 19 | 20 | const ElysiaBun: TElysiaBun = { 21 | // @ts-ignore UnixServeOptions 22 | serve(options: TBunServeOptions) { 23 | const { hostname, port, key, cert } = ensureDefaults(options); 24 | 25 | let isRunning = false; 26 | let shutdown: { closeAll?: boolean }; 27 | 28 | // @ts-expect-error 29 | const server: TElysiaServer = { 30 | id: '', 31 | port, 32 | hostname, 33 | pendingRequests: 0, 34 | pendingWebSockets: 0, 35 | development: options.development ?? process.env.NODE_ENV !== 'production', 36 | fetch: (request: Request) => 37 | options.fetch.call( 38 | server as TBunServer, 39 | request, 40 | server as TBunServer 41 | ) as ReturnType, 42 | stop() {}, // lazy 43 | reload(newOptions) { 44 | if (newOptions.fetch) { 45 | options.fetch = newOptions.fetch; 46 | } 47 | }, 48 | url: new URL(`${key && cert ? 'https' : 'http'}://${hostname}:${port}`) 49 | }; 50 | 51 | const handler: http.RequestListener< 52 | typeof http.IncomingMessage, 53 | typeof http.ServerResponse 54 | > = async (req, res) => { 55 | try { 56 | const my_request = await request(req as http.IncomingMessage, options); 57 | 58 | let my_response: Response; 59 | 60 | try { 61 | my_response = await server.fetch(my_request); 62 | } catch (error) { 63 | my_response = await handleError(options, server, error); 64 | } 65 | 66 | await response(my_response, res); 67 | } finally { 68 | res.end(); 69 | } 70 | }; 71 | 72 | const httpServer = 73 | key && cert 74 | ? https.createServer({ key, cert }, handler) 75 | : http.createServer(handler); 76 | 77 | server.stop = (closeActiveConnections) => { 78 | if (!isRunning) { 79 | // lazy 80 | shutdown = { closeAll: closeActiveConnections }; 81 | return; 82 | } 83 | 84 | if (closeActiveConnections) { 85 | httpServer.closeAllConnections(); 86 | } 87 | httpServer.close((error) => { 88 | if (error) { 89 | console.error(error); 90 | } 91 | }); 92 | setImmediate(() => httpServer.emit('close')); 93 | }; 94 | 95 | httpServer.listen(port, hostname, () => { 96 | isRunning = true; 97 | 98 | if (shutdown) { 99 | server.stop(shutdown.closeAll); 100 | } 101 | }); 102 | 103 | return server as TBunServer; 104 | }, 105 | gc() {}, // noop 106 | file(path, options) { 107 | const buffer = fs.readFileSync(path as string); 108 | return new Blob([buffer], { type: options?.type }) as TBunFileBlob; 109 | } 110 | }; 111 | 112 | // @ts-ignore 113 | globalThis.Bun = ElysiaBun; 114 | 115 | -------------------------------------------------------------------------------- /src/env/node/utils.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type http from 'node:http'; 4 | 5 | export function toReadableStream(req: http.IncomingMessage) { 6 | return new ReadableStream({ 7 | start(controller) { 8 | req.on('data', (chunk) => { 9 | controller.enqueue(chunk); 10 | }); 11 | req.on('end', () => { 12 | controller.close(); 13 | }); 14 | }, 15 | cancel(reason) { 16 | req.destroy(reason); 17 | } 18 | }); 19 | } 20 | 21 | export function toHeaders(httpHeaders: http.IncomingHttpHeaders): Headers { 22 | const headers = new Headers(); 23 | 24 | for (const name in httpHeaders) { 25 | const values = httpHeaders[name]!; 26 | if (typeof values === 'string') { 27 | headers.set(name, values); 28 | } else { 29 | for (const value in values) { 30 | headers.set(name, value); 31 | } 32 | } 33 | } 34 | 35 | // @ts-expect-error 36 | return headers; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@bogeychan/elysia-polyfills/node/index.js'; 2 | declare module '@bogeychan/elysia-polyfills/deno/index.js'; 3 | -------------------------------------------------------------------------------- /tests/TEST.txt: -------------------------------------------------------------------------------- 1 | TEST -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { assert } from 'chai'; 4 | import { Elysia } from 'elysia'; 5 | 6 | const TEST_BLOCKS: { [moduleName: string]: TestBlock } = {}; // ignore this :D 7 | 8 | const req = (path: string = '/') => new Request(`http://localhost${path}`); 9 | 10 | desc('elysia', () => { 11 | it('return raw response', async () => { 12 | const app = new Elysia().get('/', () => new Response('foo')); 13 | 14 | const res = await app.handle(req()); 15 | assert.equal(await res.text(), 'foo'); 16 | }); 17 | 18 | it('behave like bun with status code 204 responses', async () => { 19 | try { 20 | const res = new Response('', { status: 204 }); 21 | assert.equal(res.status, 204); 22 | } catch (err) { 23 | assert.isTrue(false, err); 24 | } 25 | }); 26 | }); 27 | 28 | use( 29 | '@elysiajs/cors', 30 | () => import('@elysiajs/cors'), 31 | ({ cors }) => { 32 | it('default', async () => { 33 | const app = new Elysia().use(cors()).get('/', () => '@elysiajs/cors'); 34 | 35 | const res = await app.handle(req()); 36 | assert.equal(res.headers.get('Access-Control-Allow-Origin'), '*'); 37 | }); 38 | } 39 | ); 40 | 41 | use( 42 | '@elysiajs/html', 43 | () => import('@elysiajs/html'), 44 | ({ html }) => { 45 | const page = ` 46 | 47 | 48 | Hello World 49 | 50 | 51 |

Hello World

52 | 53 | `; 54 | 55 | it('default', async () => { 56 | const app = new Elysia() 57 | .use(html()) 58 | .get('/', ({ html }: any) => html(page)); 59 | 60 | const res = await app.handle(req()); 61 | assert.equal(await res.text(), page); 62 | assert.isTrue(res.headers.get('Content-Type')?.includes('text/html')); 63 | }); 64 | } 65 | ); 66 | 67 | use( 68 | '@elysiajs/bearer', 69 | () => import('@elysiajs/bearer'), 70 | ({ bearer }) => { 71 | const app = new Elysia() 72 | .use(bearer()) 73 | .get('/sign', ({ bearer }: any) => bearer, { 74 | beforeHandle({ bearer, set }: any) { 75 | if (!bearer) { 76 | set.status = 400; 77 | set.headers[ 78 | 'WWW-Authenticate' 79 | ] = `Bearer realm='sign', error="invalid_request"`; 80 | 81 | return 'Unauthorized'; 82 | } 83 | } 84 | }); 85 | 86 | it('Header', async () => { 87 | const res = await app 88 | .handle( 89 | new Request('http://localhost/sign', { 90 | headers: { 91 | Authorization: 'Bearer elysia' 92 | } 93 | }) 94 | ) 95 | .then((r) => r.text()); 96 | 97 | assert.equal(res, 'elysia'); 98 | }); 99 | 100 | it('Query', async () => { 101 | const res = await app 102 | .handle(new Request('http://localhost/sign?access_token=elysia')) 103 | .then((r) => r.text()); 104 | 105 | assert.equal(res, 'elysia'); 106 | }); 107 | } 108 | ); 109 | 110 | use( 111 | '@elysiajs/cookie', 112 | () => import('@elysiajs/cookie'), 113 | ({ cookie }) => { 114 | it('set cookie', async () => { 115 | const app = new Elysia() 116 | .use(cookie()) 117 | .get('/', ({ cookie: { user }, setCookie }: any) => { 118 | setCookie('user', 'elysia'); 119 | 120 | return user; 121 | }); 122 | 123 | const res = await app.handle(req()); 124 | assert.equal(res.headers.get('set-cookie'), 'user=elysia; Path=/'); 125 | }); 126 | 127 | it('remove cookie', async () => { 128 | const app = new Elysia() 129 | .use(cookie()) 130 | .get('/', ({ removeCookie }: any) => { 131 | removeCookie('user'); 132 | 133 | return 'unset'; 134 | }); 135 | 136 | const res = await app.handle( 137 | new Request('http://localhost/', { 138 | headers: { 139 | cookie: 'user=elysia' 140 | } 141 | }) 142 | ); 143 | 144 | assert.equal( 145 | res.headers.get('set-cookie'), 146 | 'user=; Expires=Thu, 01 Jan 1970 00:00:00 GMT' 147 | ); 148 | }); 149 | 150 | it('sign cookie', async () => { 151 | const app = new Elysia() 152 | .use( 153 | cookie({ 154 | secret: 'Bun' 155 | }) 156 | ) 157 | .get('/', ({ setCookie }: any) => { 158 | setCookie('name', 'elysia', { 159 | signed: true 160 | }); 161 | 162 | return 'unset'; 163 | }); 164 | 165 | const res = await app.handle(req()); 166 | assert.isTrue(res.headers.get('set-cookie')?.includes('.')); 167 | }); 168 | 169 | it('unsign cookie', async () => { 170 | const app = new Elysia() 171 | .use( 172 | cookie({ 173 | secret: 'Bun' 174 | }) 175 | ) 176 | .get('/', ({ setCookie }: any) => { 177 | setCookie('name', 'elysia', { 178 | signed: true 179 | }); 180 | 181 | return 'unset'; 182 | }) 183 | .get('/unsign', ({ cookie, unsignCookie }: any) => { 184 | const { value } = unsignCookie(cookie.name); 185 | return value; 186 | }); 187 | 188 | const authen = await app.handle(req()); 189 | const res = await app 190 | .handle( 191 | new Request('http://localhost:8080/unsign', { 192 | headers: { 193 | cookie: authen.headers.get('set-cookie') ?? '' 194 | } 195 | }) 196 | ) 197 | .then((r) => r.text()); 198 | 199 | assert.equal(res, 'elysia'); 200 | }); 201 | 202 | it('set multiple cookies', async () => { 203 | // TODO -> how to handle "headers.getAll" ? 204 | //* Node.js -> Property 'getAll' does not exist on type 'Headers' 205 | //* Deno -> error: Uncaught TypeError: res.headers.getAll is not a function 206 | // { 207 | // const app = new Elysia() 208 | // .use(cookie()) 209 | // .get('/', ({ cookie: { user }, setCookie }) => { 210 | // setCookie('a', 'b'); 211 | // setCookie('c', 'd'); 212 | // return user; 213 | // }); 214 | // const res = await app.handle(req()); 215 | // assert.deepEqual(res.headers.getAll('Set-Cookie'), [ 216 | // 'a=b; Path=/', 217 | // 'c=d; Path=/' 218 | // ]); 219 | // } 220 | }); 221 | } 222 | ); 223 | 224 | use( 225 | '@elysiajs/swagger', 226 | () => import('@elysiajs/swagger'), 227 | ({ swagger }) => { 228 | it('show swagger page', async () => { 229 | const app = new Elysia().use(swagger()); 230 | const res = await app.handle(req('/swagger')); 231 | assert.equal(res.status, 200); 232 | }); 233 | 234 | it('use custom Swagger version', async () => { 235 | const app = new Elysia().use( 236 | swagger({ 237 | version: '4.5.0' 238 | }) 239 | ); 240 | const res = await app.handle(req('/swagger')).then((x) => x.text()); 241 | assert.isTrue( 242 | res.includes( 243 | 'https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js' 244 | ) 245 | ); 246 | }); 247 | 248 | it('follow title and description', async () => { 249 | const app = new Elysia().use( 250 | swagger({ 251 | version: '4.5.0', 252 | documentation: { 253 | info: { 254 | title: 'Elysia Documentation', 255 | description: 'Herrscher of Human', 256 | version: '1.0.0' 257 | } 258 | } 259 | }) 260 | ); 261 | const res = await app.handle(req('/swagger')).then((x) => x.text()); 262 | assert.isTrue(res.includes('Elysia Documentation')); 263 | assert.isTrue( 264 | res.includes( 265 | `` 269 | ) 270 | ); 271 | }); 272 | 273 | it('use custom path', async () => { 274 | const app = new Elysia().use( 275 | swagger({ 276 | path: '/v2/swagger' 277 | }) 278 | ); 279 | const res = await app.handle(req('/v2/swagger')); 280 | assert.equal(res.status, 200); 281 | }); 282 | } 283 | ); 284 | 285 | use( 286 | '@elysiajs/static', 287 | () => import('@elysiajs/static'), 288 | async ({ staticPlugin }) => { 289 | const { readFileSync } = await import('node:fs'); 290 | 291 | const options = { 292 | assets: 'tests' 293 | }; 294 | 295 | const testFile = readFileSync(`./${options.assets}/TEST.txt`, { 296 | encoding: 'utf-8' 297 | }); 298 | 299 | it('should get root path', async () => { 300 | const app = new Elysia().use(staticPlugin(options)); 301 | 302 | await app.modules; 303 | 304 | const res = await app 305 | .handle(req('/public/TEST.txt')) 306 | .then((r) => r.blob()) 307 | .then((r) => r.text()); 308 | 309 | assert.equal(res, testFile); 310 | }); 311 | 312 | it('should get nested path', async () => { 313 | const app = new Elysia().use(staticPlugin(options)); 314 | 315 | await app.modules; 316 | 317 | const res = await app.handle(req('/public/nested/TEST.txt')); 318 | const blob = await res.blob(); 319 | 320 | assert.equal(await blob.text(), testFile); 321 | }); 322 | 323 | it('should handle prefix', async () => { 324 | const app = new Elysia().use( 325 | staticPlugin({ 326 | ...options, 327 | prefix: '/static' 328 | }) 329 | ); 330 | 331 | await app.modules; 332 | 333 | const res = await app.handle(req('/static/TEST.txt')); 334 | const blob = await res.blob(); 335 | 336 | assert.equal(await blob.text(), testFile); 337 | }); 338 | 339 | it('should handle empty prefix', async () => { 340 | const app = new Elysia().use( 341 | staticPlugin({ 342 | ...options, 343 | prefix: '' 344 | }) 345 | ); 346 | 347 | await app.modules; 348 | 349 | const res = await app.handle(req('/TEST.txt')); 350 | const blob = await res.blob(); 351 | 352 | assert.equal(await blob.text(), testFile); 353 | }); 354 | 355 | it('should supports multiple public', async () => { 356 | const app = new Elysia() 357 | .use( 358 | staticPlugin({ 359 | ...options, 360 | prefix: options.assets 361 | }) 362 | ) 363 | .use( 364 | staticPlugin({ 365 | ...options, 366 | prefix: '/public' 367 | }) 368 | ); 369 | 370 | await app.modules; 371 | 372 | const res = await app.handle(req('/public/TEST.txt')); 373 | 374 | assert.equal(res.status, 200); 375 | }); 376 | 377 | it('ignore string pattern', async () => { 378 | const app = new Elysia().use( 379 | staticPlugin({ 380 | ...options, 381 | ignorePatterns: ['tests/TEST.txt'] 382 | }) 383 | ); 384 | 385 | await app.modules; 386 | 387 | const res = await app.handle(req('tests/TEST.txt')); 388 | const blob = await res.blob(); 389 | 390 | assert.equal(await blob.text(), 'NOT_FOUND'); 391 | }); 392 | 393 | it('ignore regex pattern', async () => { 394 | const app = new Elysia().use( 395 | staticPlugin({ 396 | ...options, 397 | ignorePatterns: [/TEST.txt$/] 398 | }) 399 | ); 400 | 401 | const file = await app.handle(req('tests/TEST.txt')); 402 | 403 | assert.equal(file.status, 404); 404 | }); 405 | 406 | it('always static', async () => { 407 | const app = new Elysia().use( 408 | staticPlugin({ 409 | ...options, 410 | alwaysStatic: true 411 | }) 412 | ); 413 | 414 | await app.modules; 415 | 416 | const res = await app 417 | .handle(req('/public/TEST.txt')) 418 | .then((r) => r.blob()) 419 | .then((r) => r.text()); 420 | 421 | assert.equal(res, testFile); 422 | }); 423 | 424 | it('exclude extension', async () => { 425 | const app = new Elysia().use( 426 | staticPlugin({ 427 | ...options, 428 | alwaysStatic: true, 429 | noExtension: true 430 | }) 431 | ); 432 | 433 | await app.modules; 434 | 435 | const res = await app 436 | .handle(req('/public/TEST')) 437 | .then((r) => r.blob()) 438 | .then((r) => r.text()); 439 | 440 | assert.equal(res, testFile); 441 | }); 442 | } 443 | ); 444 | 445 | // --- TEST UTILS 446 | 447 | type TestResult = void | Promise; 448 | type TestCallback = () => TestResult; 449 | type Test = { 450 | description: string; 451 | callback: TestCallback; 452 | }; 453 | 454 | type TestBlock = { 455 | tests: Test[]; 456 | callback: () => Promise; 457 | moduleName: string; 458 | }; 459 | 460 | let currentModuleName: string; 461 | 462 | export async function runTests(env: 'node' | 'deno') { 463 | for (const moduleName in TEST_BLOCKS) { 464 | const block = TEST_BLOCKS[moduleName]; 465 | 466 | // if (env === 'deno' && moduleName === '@elysiajs/swagger') { 467 | // console.log(`⏩ Skipping ${moduleName}`); 468 | // continue; 469 | // } 470 | 471 | console.log((currentModuleName = moduleName)); 472 | 473 | await block.callback(); 474 | 475 | for (const test of block.tests) { 476 | try { 477 | await test.callback(); 478 | console.log(`\t✅ ${test.description}`); 479 | } catch (error) { 480 | console.error(`\t❌ ${test.description}\n\n`, error); 481 | 482 | // @ts-ignore 483 | const process = 'process' in globalThis ? globalThis.process : Deno; 484 | 485 | process.exit(1); 486 | } 487 | } 488 | } 489 | } 490 | 491 | function desc(moduleName: string, callback: () => void | Promise) { 492 | TEST_BLOCKS[moduleName] = { 493 | moduleName, 494 | callback: async () => { 495 | await callback(); 496 | }, 497 | tests: [] 498 | }; 499 | } 500 | 501 | function use( 502 | moduleName: string, 503 | typedImport: () => Promise, 504 | callback: (module: any) => void | Promise 505 | ) { 506 | TEST_BLOCKS[moduleName] = { 507 | moduleName, 508 | callback: async () => { 509 | const module = await typedImport(); 510 | 511 | //? https://github.com/denoland/deno/issues/16088 512 | //! https://github.com/denoland/deno/issues/15826#issuecomment-1324365924 513 | // callback(await import(moduleName)); 514 | 515 | await callback(module); 516 | }, 517 | tests: [] 518 | }; 519 | } 520 | 521 | function it(this: string | void, description: string, callback: TestCallback) { 522 | TEST_BLOCKS[currentModuleName].tests.push({ description, callback }); 523 | } 524 | -------------------------------------------------------------------------------- /tests/modules.d.ts: -------------------------------------------------------------------------------- 1 | type Elysia = import('elysia').Elysia; 2 | 3 | declare module '@elysiajs/cors' { 4 | function cors(): Elysia; 5 | } 6 | 7 | declare module '@elysiajs/html' { 8 | function html(): Elysia; 9 | } 10 | 11 | declare module '@elysiajs/bearer' { 12 | function bearer(...args: any[]): Elysia; 13 | } 14 | 15 | declare module '@elysiajs/cookie' { 16 | function cookie(...agrs: any[]): Elysia; 17 | } 18 | 19 | declare module '@elysiajs/swagger' { 20 | function swagger(...args: any[]): Elysia; 21 | } 22 | 23 | declare module '@elysiajs/static' { 24 | function staticPlugin(...args: any[]): Elysia; 25 | } 26 | -------------------------------------------------------------------------------- /tests/nested/TEST.txt: -------------------------------------------------------------------------------- 1 | TEST -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ESNext" 5 | ], 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "removeComments": true, 11 | "declaration": true, 12 | "types": [ 13 | "bun-types" 14 | ], 15 | "outDir": "dist" 16 | }, 17 | "include": [ 18 | "src" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "dist" 23 | ] 24 | } -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "moduleResolution": "Node", 6 | "outDir": "dist/cjs", 7 | "target": "es2015" 8 | } 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "dist/mjs", 7 | "target": "ESNext" 8 | } 9 | } --------------------------------------------------------------------------------