├── .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 | }
--------------------------------------------------------------------------------