├── .eslintrc ├── .github ├── FUNDING.yaml ├── ISSUE_TEMPLATE │ ├── 2-bug-report.yml │ ├── 3-feature-request.yml │ └── config.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bun.lock ├── example ├── a.ts ├── b.ts ├── client │ ├── .gitignore │ ├── bun.lockb │ ├── index.html │ ├── package.json │ ├── pnpm-lock.yaml │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── fetch.ts │ │ ├── main.ts │ │ ├── style.css │ │ ├── treaty-file.ts │ │ ├── treaty.ts │ │ └── vite-env.d.ts │ └── tsconfig.json ├── fetch.ts ├── grace.webp ├── index.ts ├── regression.ts ├── server.ts └── treaty.ts ├── package.json ├── src ├── errors.ts ├── fetch │ ├── index.ts │ └── types.ts ├── index.ts ├── treaty │ ├── index.ts │ ├── types.ts │ └── utils.ts ├── treaty2 │ ├── index.ts │ ├── types.ts │ └── ws.ts ├── types.ts └── utils │ └── parsingUtils.ts ├── test ├── fetch.test.ts ├── fn.test.ts ├── public │ ├── aris-yuzu.jpg │ ├── kyuukurarin.mp4 │ └── midori.png ├── treaty.test.ts ├── treaty2.test.ts └── types │ └── treaty2.ts ├── tsconfig.json ├── tsconfig.test.json └── tsup.config.ts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/no-namespace": "off", 20 | "@typescript-eslint/ban-ts-comment": "off", 21 | "@typescript-eslint/ban-types": "off", 22 | "@typescript-eslint/no-explicit-any": "off" 23 | }, 24 | "ignorePatterns": ["example/*", "tests/**/*"] 25 | } 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: SaltyAom -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Report an issue that should be fixed 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for submitting a bug report. It helps make Elysia.JS better. 9 | 10 | If you need help or support using Elysia.JS, and are not reporting a bug, please 11 | head over to Q&A discussions [Discussions](https://github.com/elysiajs/elysia/discussions/categories/q-a), where you can ask questions in the Q&A forum. 12 | 13 | Make sure you are running the version of Elysia.JS and Bun.Sh 14 | The bug you are experiencing may already have been fixed. 15 | 16 | Please try to include as much information as possible. 17 | 18 | - type: input 19 | attributes: 20 | label: What version of Elysia is running? 21 | description: Copy the output of `Elysia --revision` 22 | - type: input 23 | attributes: 24 | label: What platform is your computer? 25 | description: | 26 | For MacOS and Linux: copy the output of `uname -mprs` 27 | For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console 28 | - type: textarea 29 | attributes: 30 | label: What steps can reproduce the bug? 31 | description: Explain the bug and provide a code snippet that can reproduce it. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: What is the expected behavior? 37 | description: If possible, please provide text instead of a screenshot. 38 | - type: textarea 39 | attributes: 40 | label: What do you see instead? 41 | description: If possible, please provide text instead of a screenshot. 42 | - type: textarea 43 | attributes: 44 | label: Additional information 45 | description: Is there anything else you think we should know? 46 | - type: input 47 | attributes: 48 | label: Have you try removing the `node_modules` and `bun.lockb` and try again yet? 49 | description: rm -rf node_modules && bun.lockb 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature Request 2 | description: Suggest an idea, feature, or enhancement 3 | labels: [enhancement] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for submitting an idea. It helps make Elysia.JS better. 9 | 10 | If you want to discuss Elysia.JS, or learn how others are using Elysia.JS, please 11 | head to our [Discord](https://discord.com/invite/y7kH46ZE) server, where you can chat among the community. 12 | - type: textarea 13 | attributes: 14 | label: What is the problem this feature would solve? 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: What is the feature you are proposing to solve the problem? 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: What alternatives have you considered? 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 📗 Documentation Issue 4 | url: https://github.com/elysiajs/documentation/issues/new/choose 5 | about: Head over to our Documentation repository! 6 | - name: 💬 Ask a Question 7 | url: https://discord.gg/eaFJ2KDJck 8 | about: Head over to our Discord! 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: './' 5 | schedule: 6 | interval: 'daily' 7 | 8 | - package-ecosystem: 'github-actions' 9 | directory: './' 10 | schedule: 11 | interval: 'daily' 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | name: Build and test code 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup bun 17 | uses: oven-sh/setup-bun@v1 18 | with: 19 | bun-version: latest 20 | 21 | - name: Install packages 22 | run: bun install 23 | 24 | - name: Build code 25 | run: bun run build 26 | 27 | - name: Test 28 | run: bun run test -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | permissions: 12 | id-token: write 13 | 14 | env: 15 | # Enable debug logging for actions 16 | ACTIONS_RUNNER_DEBUG: true 17 | 18 | jobs: 19 | publish-npm: 20 | name: 'Publish: npm Registry' 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: 'Checkout' 24 | uses: actions/checkout@v4 25 | 26 | - name: 'Setup Bun' 27 | uses: oven-sh/setup-bun@v1 28 | with: 29 | bun-version: latest 30 | registry-url: "https://registry.npmjs.org" 31 | 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: '20.x' 35 | registry-url: 'https://registry.npmjs.org' 36 | 37 | - name: Install packages 38 | run: bun install 39 | 40 | - name: Build code 41 | run: bun run build 42 | 43 | - name: Test 44 | run: bun run test 45 | 46 | - name: 'Publish' 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | run: | 50 | npm publish --provenance --access=public 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | .pnpm-debug.log 5 | dist 6 | trace -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .gitignore 4 | .prettierrc 5 | .cjs.swcrc 6 | .es.swcrc 7 | bun.lockb 8 | 9 | node_modules 10 | tsconfig.json 11 | pnpm-lock.yaml 12 | jest.config.js 13 | nodemon.json 14 | 15 | src 16 | trace 17 | example 18 | tests 19 | test 20 | CHANGELOG.md 21 | .eslintrc.js 22 | tsconfig.cjs.json 23 | tsconfig.esm.json 24 | tsconfig.test.json 25 | vite.config.js 26 | .github 27 | .eslintrc 28 | tsup.config.ts 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.2 - 5 May 2025 2 | Bug fix: 3 | - Unwrap FormData 4 | 5 | # 1.3.1 - 5 May 2025 6 | Bug fix: 7 | - [#193](https://github.com/elysiajs/eden/pull/193) t.Files() upload from server side #124 8 | - [#185](https://github.com/elysiajs/eden/pull/185) exclude null-ish values from query encoding by @ShuviSchwarze 9 | 10 | # 1.3.0 - 5 May 2025 11 | Feature: 12 | - support Elysia 1.3 13 | 14 | Breaking Change: 15 | - [Treaty 2] drop the need for `.index()` 16 | 17 | # 1.2.0 - 23 Dec 2024 18 | Feature: 19 | - support Elysia 1.2 20 | - Validation error inference 21 | 22 | # 1.1.3 - 5 Sep 2024 23 | Feature: 24 | - add provenance publish 25 | 26 | # 1.1.2 - 25 Jul 2024 27 | Feature: 28 | - [#115](https://github.com/elysiajs/eden/pull/115) Stringify query params to allow the nested object 29 | 30 | # 1.1.1 - 17 Jul 2024 31 | Feature: 32 | - support conditional value or stream from generator function 33 | 34 | # 1.1.0 - 16 Jul 2024 35 | Feature: 36 | - support Elysia 1.1 37 | - support response streaming with type inference 38 | 39 | # 1.0.15 - 21 Jun 2024 40 | Bug fix: 41 | - [#105](https://github.com/elysiajs/eden/pull/105) Unify parsing into a util and add support for Date inside object 42 | - [#100](https://github.com/elysiajs/eden/issues/100) Duplicated values when passing uppercase values for headers 43 | 44 | # 1.0.14 - 23 May 2024 45 | Feature: 46 | - treaty2: add support for multipart/form-data 47 | - fetch: add support for multipart/form-data 48 | 49 | # 1.0.13 - 8 May 2024 50 | Bug fix: 51 | - [#87](https://github.com/elysiajs/eden/pull/87) serialization/deserialization problems with null, arrays and Date on websocket messages #87 52 | - [#90](https://github.com/elysiajs/eden/pull/90) Auto convert request body to FormData when needed 53 | 54 | # 1.0.12 - 23 Apr 2024 55 | Improvement: 56 | - [#80](https://github.com/elysiajs/eden/pull/80) add package.json to export field 57 | 58 | Bug fix: 59 | - [#84](https://github.com/elysiajs/eden/pull/84) treaty2: adjusts the creation of the query string 60 | - [#76](https://github.com/elysiajs/eden/pull/76) treaty2: keep requestInit for redirect 61 | - [#73](https://github.com/elysiajs/eden/pull/73) treaty2: file get sent as json 62 | 63 | # 1.0.11 - 3 Apr 2024 64 | Improvement: 65 | - treaty2: add dedicated `processHeaders` function 66 | - treaty2: simplify some headers workflow 67 | 68 | Change: 69 | - treaty2: using case-insensitive headers 70 | 71 | # 1.0.10 - 3 Apr 2024 72 | Bug fix: 73 | - treaty2: skip content-type detection if provided 74 | 75 | # 1.0.9 - 3 Apr 2024 76 | Change: 77 | - treaty2: `onRequest` execute before body serialization 78 | 79 | # 1.0.8 - 28 Mar 2024 80 | Bug fix: 81 | - [#72](https://github.com/elysiajs/eden/pulls/72) treaty2: not mutate original paths array 82 | 83 | # 1.0.6 - 21 Mar 2024 84 | Bug fix: 85 | - treaty2: default onResponse to null and mutate when need 86 | 87 | # 1.0.6 - 21 Mar 2024 88 | Change: 89 | - treaty2: use null as default error value instead of undefined 90 | 91 | # 1.0.5 - 20 Mar 2024 92 | Feature: 93 | treaty2: add `keepDomain` option 94 | 95 | Change: 96 | - treaty2: use null as default data value instead of undefined 97 | 98 | Fix: 99 | - treaty2: aligned schema with elysia/ws 100 | - treaty: use uppercase http verbs 101 | 102 | # 1.0.4 - 18 Mar 2024 103 | Bug fix: 104 | - Using regex for date pattern matching 105 | 106 | # 1.0.2 - 18 Mar 2024 107 | Feature: 108 | - support for elysia 1.0.2 109 | 110 | # 1.0.0 - 16 Mar 2024 111 | Feature: 112 | - treaty2 113 | 114 | Bug fix: 115 | - treaty1: fix [object Object] when body is empty 116 | 117 | # 0.8.1 - 8 Jan 2024 118 | Bug fix: 119 | - [#41](https://github.com/elysiajs/eden/pull/41) params now optional for paramless routes in edenFetch 120 | - [#39](https://github.com/elysiajs/eden/pull/39) transform entire object returned by execute 121 | 122 | # 0.8.0 - 23 Dec 2023 123 | Feature: 124 | - Support Elysia 0.8 125 | 126 | # 0.7.7 - 15 Dec 2023 127 | Bug fix: 128 | - treaty: [#36](https://github.com/elysiajs/eden/pull/36) FileArray send [ object Promise ] instead binary 129 | - treaty: [#34](https://github.com/elysiajs/eden/pull/34) Add a way to get the unresolved Response for corner case support 130 | 131 | # 0.7.6 - 6 Dec 2023 132 | Feature: 133 | - treaty: add 2nd optional parameter for sending query, header and fetch 134 | 135 | Bug fix: 136 | - treaty: send array and primitive value [#33](https://github.com/elysiajs/eden/issues/33) 137 | - treaty: fix filename in FormData [#26](https://github.com/elysiajs/eden/pull/26) 138 | - remove 'bun-types' from treaty 139 | 140 | # 0.7.5 - 23 Oct 2023 141 | Bug fix: 142 | - treaty: handle `File[]` 143 | 144 | # 0.7.4 - 29 Sep 2023 145 | Feature: 146 | - [#16](https://github.com/elysiajs/eden/issues/16) add transform 147 | 148 | # 0.7.3 - 27 Sep 2023 149 | Bug fix: 150 | - using uppercase method name because of Cloudflare proxy 151 | 152 | # 0.7.2 - 22 Sep 2023 153 | Bug fix: 154 | - resolve 'FileList' type 155 | - using rimraf to clear previous build 156 | - fix edenTreaty type is undefined when using moduleResolution: bundler 157 | 158 | # 0.7.1 - 21 Sep 2023 159 | Bug fix: 160 | - type panic when `Definitions` is provided 161 | 162 | # 0.7.0 - 20 Sep 2023 163 | Improvement: 164 | - update to Elysia 0.7.0 165 | 166 | Change: 167 | - remove Elysia Fn 168 | 169 | # 0.6.5 - 12 Sep 2023 170 | Bug fix: 171 | - [#15](https://github.com/elysiajs/eden/pull/15) fetch: method inference on route with different methods 172 | - [#17](https://github.com/elysiajs/eden/issues/17) treaty: api.get() maps to request GET /ge instead of / 173 | 174 | # 0.6.4 - 28 Aug 2023 175 | Change: 176 | - use tsup to bundle 177 | 178 | # 0.6.3 - 28 Aug 2023 179 | Feature: 180 | - add query to Eden Fetch thanks to [#10](https://github.com/elysiajs/eden/pull/10) 181 | 182 | # 0.6.2 - 26 Aug 2023 183 | Feature: 184 | - add the `$fetch` parameters to Eden Treaty 185 | - add the following to response: 186 | - status - indicating status code 187 | - raw - Response 188 | - headers - Response's headers 189 | 190 | Improvement: 191 | - rewrite Eden type to New Eden 192 | - LoC reduced by ~35% 193 | - Faster type inference ~26% 194 | 195 | # 0.6.1 - 17 Aug 2023 196 | Feature: 197 | - add support for Elysia 0.6.7 198 | 199 | # 0.6.0 - 6 Aug 2023 200 | Feature: 201 | - add support for Elysia 0.6 202 | 203 | # 0.5.6 - 10 Jun 2023 204 | Improvement: 205 | - treaty: Add custom fetch implementation for treaty 206 | 207 | # 0.5.5 - 10 Jun 2023 208 | Improvement: 209 | - treaty: Automatic unwrap `Promise` response 210 | 211 | Bug fix: 212 | - treaty: query schema is missing 213 | 214 | # 0.5.4 - 9 Jun 2023 215 | Improvement: 216 | - Awaited response data 217 | 218 | # 0.5.3 - 25 May 2023 219 | Improvement: 220 | - treaty: add support for Bun file uploading 221 | 222 | # 0.5.2 - 25 May 2023 223 | Improvement: 224 | - add types declaration to import map 225 | 226 | Bug fix: 227 | - add tsc to generate .d.ts 228 | 229 | # 0.5.1 - 25 May 2023 230 | Bug fix: 231 | - treaty: type not found 232 | - treaty: query not infers type 233 | 234 | # 0.5.0 - 15 May 2023 235 | Improvement: 236 | - Add support for Elysia 0.5 237 | 238 | # 0.4.1 - 1 April 2023 239 | Improvement: 240 | - Sometime query isn't required 241 | 242 | # 0.4.0 - 30 Mar 2023 243 | Improvement: 244 | - Add support for Elysia 0.4 245 | 246 | # 0.3.2 - 20 Mar 2023 247 | Improvement: 248 | - File upload support for Eden Treaty 249 | 250 | # 0.3.1 - 20 Mar 2023 251 | Improvement: 252 | - Path parameter inference 253 | 254 | # 0.3.0 - 17 Mar 2023 255 | Improvement: 256 | - Add support for Elysia 0.3.0 257 | 258 | # 0.3.0-rc.2 - 17 Mar 2023 259 | Breaking Change: 260 | - Eden Fetch error handling use object destructuring, migration as same as Eden Treaty (0.3.0-rc.1) 261 | 262 | # 0.3.0-rc.1 - 16 Mar 2023 263 | Improvement: 264 | - Update @sinclair/typebox to 0.25.24 265 | 266 | Breaking Change: 267 | - Eden Treaty error handling use object destructuring 268 | - To migrate: 269 | ```ts 270 | // to 271 | const anya = await client.products.nendoroid['1902'].put({ 272 | name: 'Anya Forger' 273 | }) 274 | 275 | // From 276 | const { data: anya, error } = await client.products.nendoroid['1902'].put({ 277 | name: 'Anya Forger' 278 | }) 279 | ``` 280 | 281 | # 0.3.0-rc.0 - 7 Mar 2023 282 | Improvement: 283 | - Add support for Elysia 0.3.0-rc.0 284 | 285 | # 0.3.0-beta.4 - 4 Mar 2023 286 | Improvement: 287 | - Separate Eden type 288 | - Rewrite Eden Treaty 289 | 290 | # 0.3.0-beta.3 - 1 Mar 2023 291 | Improvement: 292 | - Add support for Elysia 0.3.0-beta.5 293 | 294 | # 0.3.0-beta.2 - 28 Feb 2023 295 | Improvement: 296 | - Optimize type inference 297 | 298 | # 0.3.0-beta.1 - 27 Feb 2023 299 | Improvement: 300 | - Add TypeBox as peer dependencies 301 | - Minimum support for Elysia >= 0.3.0-beta.3 302 | 303 | # 0.3.0-beta.0 - 25 Feb 2023 304 | Fix: 305 | - Eden doesn't transform path reference 306 | 307 | # 0.2.1 - 27 Jan 2023 308 | Feature: 309 | - Elysia Fn 310 | - Error type inference 311 | 312 | Breaking Change: 313 | - Error type inference 314 | 315 | # 0.2.0-rc.9 - 27 Jan 2023 316 | Improvement: 317 | - Add params name for warning instead of `$params` 318 | - Add warning to install Elysia before using Eden 319 | 320 | # 0.2.0-rc.8 - 24 Jan 2023 321 | Fix: 322 | - Add support for Elysia WS which support Elysia 0.2.0-rc.1 323 | 324 | # 0.2.0-rc.7 - 24 Jan 2023 325 | Fix: 326 | - Resolve Elysia 0.2.0-rc.1 type 327 | 328 | # 0.2.0-rc.6 - 24 Jan 2023 329 | Improvement: 330 | - Set minimum Elysia version to 0.2.0-rc.1 331 | 332 | # 0.2.0-rc.5 - 19 Jan 2023 333 | Improvement: 334 | - Handle application/json with custom encoding 335 | 336 | # 0.2.0-rc.4 - 19 Jan 2023 337 | Change: 338 | - It's now required to specified specific version to use elysia 339 | 340 | # 0.2.0-rc.3 - 7 Jan 2023 341 | Improvement: 342 | - Add `$params` to indicate any string params 343 | 344 | Bug fix: 345 | - Sometime Eden doesn't infer returned type 346 | 347 | # 0.2.0-rc.2 - 5 Jan 2023 348 | Improvement: 349 | - Auto switch between `ws` and `wss` 350 | 351 | # 0.2.0-rc.1 - 4 Jan 2023 352 | Breaking Change: 353 | - Change HTTP verb to lowercase 354 | 355 | Feature: 356 | - Support multiple path parameters 357 | 358 | Bug fix: 359 | - Required query in `subscribe` 360 | - Make `unknown` type optional 361 | - Add support for non-object fetch 362 | 363 | # 0.2.0-rc.0 - 3 Jan 2023 364 | Feature: 365 | - Experimental support for Web Socket 366 | 367 | # 0.1.0-rc.6 - 16 Dec 2022 368 | Feature: 369 | - Auto convert number, boolean on client 370 | 371 | # 0.1.0-rc.4 - 16 Dec 2022 372 | Feature: 373 | - Using `vite` for bundling 374 | 375 | # 0.1.0-rc.3 - 13 Dec 2022 376 | Bug fix: 377 | - Map error to `.catch` 378 | 379 | # 0.1.0-rc.2 - 13 Dec 2022 380 | Feature: 381 | - Add camelCase transformation 382 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 saltyAom 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @elysiajs/eden 2 | Fully type-safe Elysia client refers to the [documentation](https://elysiajs.com/eden/overview) 3 | 4 | ## Installation 5 | ```bash 6 | bun add elysia @elysiajs/eden 7 | ``` 8 | 9 | ## Example 10 | ```typescript 11 | // server.ts 12 | import { Elysia, t } from 'elysia' 13 | 14 | const app = new Elysia() 15 | .get('/', () => 'Hi Elysia') 16 | .get('/id/:id', ({ params: { id } }) => id) 17 | .post('/mirror', ({ body }) => body, { 18 | schema: { 19 | body: t.Object({ 20 | id: t.Number(), 21 | name: t.String() 22 | }) 23 | } 24 | }) 25 | .listen(8080) 26 | 27 | export type App = typeof app 28 | 29 | // client.ts 30 | import { edenTreaty } from '@elysiajs/eden' 31 | import type { App } from './server' 32 | 33 | const app = edenTreaty('http://localhost:8080') 34 | 35 | // data: Hi Elysia (fully type-safe) 36 | const { data: pong } = app.index.get() 37 | 38 | // data: 1895 39 | const { data: id } = client.id.1895.get() 40 | 41 | // data: { id: 1895, name: 'Skadi' } 42 | const { data: nendoroid } = app.mirror.post({ 43 | id: 1895, 44 | name: 'Skadi' 45 | }) 46 | ``` 47 | -------------------------------------------------------------------------------- /example/a.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from 'elysia' 2 | import { treaty } from '../src' 3 | 4 | const app = new Elysia() 5 | .get('/', () => 'hi') 6 | 7 | const api = treaty(app) 8 | 9 | await api.get() 10 | -------------------------------------------------------------------------------- /example/b.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from 'elysia' 2 | import { treaty } from '../src' 3 | 4 | const app = new Elysia().get('/', () => ({ 5 | a: Bun.file('./test/public/kyuukurarin.mp4') 6 | })) 7 | 8 | const api = treaty(app) 9 | const { data, error } = await api.index.get() 10 | 11 | if (error) throw error 12 | 13 | console.log(data.a.size) 14 | -------------------------------------------------------------------------------- /example/client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | trace 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /example/client/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/eden/c1fa86868fd195500bacbb84c25c6d19c2de1349/example/client/bun.lockb -------------------------------------------------------------------------------- /example/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + TS 8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@elysia/eden": "../.." 13 | }, 14 | "devDependencies": { 15 | "typescript": "^4.9.3", 16 | "vite": "^4.0.0" 17 | } 18 | } -------------------------------------------------------------------------------- /example/client/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | '@elysia/eden': ../.. 5 | typescript: ^4.9.3 6 | vite: ^4.0.0 7 | 8 | dependencies: 9 | '@elysia/eden': link:../.. 10 | 11 | devDependencies: 12 | typescript: 4.9.4 13 | vite: 4.0.0 14 | 15 | packages: 16 | 17 | /@esbuild/android-arm/0.16.4: 18 | resolution: {integrity: sha512-rZzb7r22m20S1S7ufIc6DC6W659yxoOrl7sKP1nCYhuvUlnCFHVSbATG4keGUtV8rDz11sRRDbWkvQZpzPaHiw==} 19 | engines: {node: '>=12'} 20 | cpu: [arm] 21 | os: [android] 22 | requiresBuild: true 23 | dev: true 24 | optional: true 25 | 26 | /@esbuild/android-arm64/0.16.4: 27 | resolution: {integrity: sha512-VPuTzXFm/m2fcGfN6CiwZTlLzxrKsWbPkG7ArRFpuxyaHUm/XFHQPD4xNwZT6uUmpIHhnSjcaCmcla8COzmZ5Q==} 28 | engines: {node: '>=12'} 29 | cpu: [arm64] 30 | os: [android] 31 | requiresBuild: true 32 | dev: true 33 | optional: true 34 | 35 | /@esbuild/android-x64/0.16.4: 36 | resolution: {integrity: sha512-MW+B2O++BkcOfMWmuHXB15/l1i7wXhJFqbJhp82IBOais8RBEQv2vQz/jHrDEHaY2X0QY7Wfw86SBL2PbVOr0g==} 37 | engines: {node: '>=12'} 38 | cpu: [x64] 39 | os: [android] 40 | requiresBuild: true 41 | dev: true 42 | optional: true 43 | 44 | /@esbuild/darwin-arm64/0.16.4: 45 | resolution: {integrity: sha512-a28X1O//aOfxwJVZVs7ZfM8Tyih2Za4nKJrBwW5Wm4yKsnwBy9aiS/xwpxiiTRttw3EaTg4Srerhcm6z0bu9Wg==} 46 | engines: {node: '>=12'} 47 | cpu: [arm64] 48 | os: [darwin] 49 | requiresBuild: true 50 | dev: true 51 | optional: true 52 | 53 | /@esbuild/darwin-x64/0.16.4: 54 | resolution: {integrity: sha512-e3doCr6Ecfwd7VzlaQqEPrnbvvPjE9uoTpxG5pyLzr2rI2NMjDHmvY1E5EO81O/e9TUOLLkXA5m6T8lfjK9yAA==} 55 | engines: {node: '>=12'} 56 | cpu: [x64] 57 | os: [darwin] 58 | requiresBuild: true 59 | dev: true 60 | optional: true 61 | 62 | /@esbuild/freebsd-arm64/0.16.4: 63 | resolution: {integrity: sha512-Oup3G/QxBgvvqnXWrBed7xxkFNwAwJVHZcklWyQt7YCAL5bfUkaa6FVWnR78rNQiM8MqqLiT6ZTZSdUFuVIg1w==} 64 | engines: {node: '>=12'} 65 | cpu: [arm64] 66 | os: [freebsd] 67 | requiresBuild: true 68 | dev: true 69 | optional: true 70 | 71 | /@esbuild/freebsd-x64/0.16.4: 72 | resolution: {integrity: sha512-vAP+eYOxlN/Bpo/TZmzEQapNS8W1njECrqkTpNgvXskkkJC2AwOXwZWai/Kc2vEFZUXQttx6UJbj9grqjD/+9Q==} 73 | engines: {node: '>=12'} 74 | cpu: [x64] 75 | os: [freebsd] 76 | requiresBuild: true 77 | dev: true 78 | optional: true 79 | 80 | /@esbuild/linux-arm/0.16.4: 81 | resolution: {integrity: sha512-A47ZmtpIPyERxkSvIv+zLd6kNIOtJH03XA0Hy7jaceRDdQaQVGSDt4mZqpWqJYgDk9rg96aglbF6kCRvPGDSUA==} 82 | engines: {node: '>=12'} 83 | cpu: [arm] 84 | os: [linux] 85 | requiresBuild: true 86 | dev: true 87 | optional: true 88 | 89 | /@esbuild/linux-arm64/0.16.4: 90 | resolution: {integrity: sha512-2zXoBhv4r5pZiyjBKrOdFP4CXOChxXiYD50LRUU+65DkdS5niPFHbboKZd/c81l0ezpw7AQnHeoCy5hFrzzs4g==} 91 | engines: {node: '>=12'} 92 | cpu: [arm64] 93 | os: [linux] 94 | requiresBuild: true 95 | dev: true 96 | optional: true 97 | 98 | /@esbuild/linux-ia32/0.16.4: 99 | resolution: {integrity: sha512-uxdSrpe9wFhz4yBwt2kl2TxS/NWEINYBUFIxQtaEVtglm1eECvsj1vEKI0KX2k2wCe17zDdQ3v+jVxfwVfvvjw==} 100 | engines: {node: '>=12'} 101 | cpu: [ia32] 102 | os: [linux] 103 | requiresBuild: true 104 | dev: true 105 | optional: true 106 | 107 | /@esbuild/linux-loong64/0.16.4: 108 | resolution: {integrity: sha512-peDrrUuxbZ9Jw+DwLCh/9xmZAk0p0K1iY5d2IcwmnN+B87xw7kujOkig6ZRcZqgrXgeRGurRHn0ENMAjjD5DEg==} 109 | engines: {node: '>=12'} 110 | cpu: [loong64] 111 | os: [linux] 112 | requiresBuild: true 113 | dev: true 114 | optional: true 115 | 116 | /@esbuild/linux-mips64el/0.16.4: 117 | resolution: {integrity: sha512-sD9EEUoGtVhFjjsauWjflZklTNr57KdQ6xfloO4yH1u7vNQlOfAlhEzbyBKfgbJlW7rwXYBdl5/NcZ+Mg2XhQA==} 118 | engines: {node: '>=12'} 119 | cpu: [mips64el] 120 | os: [linux] 121 | requiresBuild: true 122 | dev: true 123 | optional: true 124 | 125 | /@esbuild/linux-ppc64/0.16.4: 126 | resolution: {integrity: sha512-X1HSqHUX9D+d0l6/nIh4ZZJ94eQky8d8z6yxAptpZE3FxCWYWvTDd9X9ST84MGZEJx04VYUD/AGgciddwO0b8g==} 127 | engines: {node: '>=12'} 128 | cpu: [ppc64] 129 | os: [linux] 130 | requiresBuild: true 131 | dev: true 132 | optional: true 133 | 134 | /@esbuild/linux-riscv64/0.16.4: 135 | resolution: {integrity: sha512-97ANpzyNp0GTXCt6SRdIx1ngwncpkV/z453ZuxbnBROCJ5p/55UjhbaG23UdHj88fGWLKPFtMoU4CBacz4j9FA==} 136 | engines: {node: '>=12'} 137 | cpu: [riscv64] 138 | os: [linux] 139 | requiresBuild: true 140 | dev: true 141 | optional: true 142 | 143 | /@esbuild/linux-s390x/0.16.4: 144 | resolution: {integrity: sha512-pUvPQLPmbEeJRPjP0DYTC1vjHyhrnCklQmCGYbipkep+oyfTn7GTBJXoPodR7ZS5upmEyc8lzAkn2o29wD786A==} 145 | engines: {node: '>=12'} 146 | cpu: [s390x] 147 | os: [linux] 148 | requiresBuild: true 149 | dev: true 150 | optional: true 151 | 152 | /@esbuild/linux-x64/0.16.4: 153 | resolution: {integrity: sha512-N55Q0mJs3Sl8+utPRPBrL6NLYZKBCLLx0bme/+RbjvMforTGGzFvsRl4xLTZMUBFC1poDzBEPTEu5nxizQ9Nlw==} 154 | engines: {node: '>=12'} 155 | cpu: [x64] 156 | os: [linux] 157 | requiresBuild: true 158 | dev: true 159 | optional: true 160 | 161 | /@esbuild/netbsd-x64/0.16.4: 162 | resolution: {integrity: sha512-LHSJLit8jCObEQNYkgsDYBh2JrJT53oJO2HVdkSYLa6+zuLJh0lAr06brXIkljrlI+N7NNW1IAXGn/6IZPi3YQ==} 163 | engines: {node: '>=12'} 164 | cpu: [x64] 165 | os: [netbsd] 166 | requiresBuild: true 167 | dev: true 168 | optional: true 169 | 170 | /@esbuild/openbsd-x64/0.16.4: 171 | resolution: {integrity: sha512-nLgdc6tWEhcCFg/WVFaUxHcPK3AP/bh+KEwKtl69Ay5IBqUwKDaq/6Xk0E+fh/FGjnLwqFSsarsbPHeKM8t8Sw==} 172 | engines: {node: '>=12'} 173 | cpu: [x64] 174 | os: [openbsd] 175 | requiresBuild: true 176 | dev: true 177 | optional: true 178 | 179 | /@esbuild/sunos-x64/0.16.4: 180 | resolution: {integrity: sha512-08SluG24GjPO3tXKk95/85n9kpyZtXCVwURR2i4myhrOfi3jspClV0xQQ0W0PYWHioJj+LejFMt41q+PG3mlAQ==} 181 | engines: {node: '>=12'} 182 | cpu: [x64] 183 | os: [sunos] 184 | requiresBuild: true 185 | dev: true 186 | optional: true 187 | 188 | /@esbuild/win32-arm64/0.16.4: 189 | resolution: {integrity: sha512-yYiRDQcqLYQSvNQcBKN7XogbrSvBE45FEQdH8fuXPl7cngzkCvpsG2H9Uey39IjQ6gqqc+Q4VXYHsQcKW0OMjQ==} 190 | engines: {node: '>=12'} 191 | cpu: [arm64] 192 | os: [win32] 193 | requiresBuild: true 194 | dev: true 195 | optional: true 196 | 197 | /@esbuild/win32-ia32/0.16.4: 198 | resolution: {integrity: sha512-5rabnGIqexekYkh9zXG5waotq8mrdlRoBqAktjx2W3kb0zsI83mdCwrcAeKYirnUaTGztR5TxXcXmQrEzny83w==} 199 | engines: {node: '>=12'} 200 | cpu: [ia32] 201 | os: [win32] 202 | requiresBuild: true 203 | dev: true 204 | optional: true 205 | 206 | /@esbuild/win32-x64/0.16.4: 207 | resolution: {integrity: sha512-sN/I8FMPtmtT2Yw+Dly8Ur5vQ5a/RmC8hW7jO9PtPSQUPkowxWpcUZnqOggU7VwyT3Xkj6vcXWd3V/qTXwultQ==} 208 | engines: {node: '>=12'} 209 | cpu: [x64] 210 | os: [win32] 211 | requiresBuild: true 212 | dev: true 213 | optional: true 214 | 215 | /esbuild/0.16.4: 216 | resolution: {integrity: sha512-qQrPMQpPTWf8jHugLWHoGqZjApyx3OEm76dlTXobHwh/EBbavbRdjXdYi/GWr43GyN0sfpap14GPkb05NH3ROA==} 217 | engines: {node: '>=12'} 218 | hasBin: true 219 | requiresBuild: true 220 | optionalDependencies: 221 | '@esbuild/android-arm': 0.16.4 222 | '@esbuild/android-arm64': 0.16.4 223 | '@esbuild/android-x64': 0.16.4 224 | '@esbuild/darwin-arm64': 0.16.4 225 | '@esbuild/darwin-x64': 0.16.4 226 | '@esbuild/freebsd-arm64': 0.16.4 227 | '@esbuild/freebsd-x64': 0.16.4 228 | '@esbuild/linux-arm': 0.16.4 229 | '@esbuild/linux-arm64': 0.16.4 230 | '@esbuild/linux-ia32': 0.16.4 231 | '@esbuild/linux-loong64': 0.16.4 232 | '@esbuild/linux-mips64el': 0.16.4 233 | '@esbuild/linux-ppc64': 0.16.4 234 | '@esbuild/linux-riscv64': 0.16.4 235 | '@esbuild/linux-s390x': 0.16.4 236 | '@esbuild/linux-x64': 0.16.4 237 | '@esbuild/netbsd-x64': 0.16.4 238 | '@esbuild/openbsd-x64': 0.16.4 239 | '@esbuild/sunos-x64': 0.16.4 240 | '@esbuild/win32-arm64': 0.16.4 241 | '@esbuild/win32-ia32': 0.16.4 242 | '@esbuild/win32-x64': 0.16.4 243 | dev: true 244 | 245 | /fsevents/2.3.2: 246 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 247 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 248 | os: [darwin] 249 | requiresBuild: true 250 | dev: true 251 | optional: true 252 | 253 | /function-bind/1.1.1: 254 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 255 | dev: true 256 | 257 | /has/1.0.3: 258 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} 259 | engines: {node: '>= 0.4.0'} 260 | dependencies: 261 | function-bind: 1.1.1 262 | dev: true 263 | 264 | /is-core-module/2.11.0: 265 | resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} 266 | dependencies: 267 | has: 1.0.3 268 | dev: true 269 | 270 | /nanoid/3.3.4: 271 | resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} 272 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 273 | hasBin: true 274 | dev: true 275 | 276 | /path-parse/1.0.7: 277 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 278 | dev: true 279 | 280 | /picocolors/1.0.0: 281 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 282 | dev: true 283 | 284 | /postcss/8.4.19: 285 | resolution: {integrity: sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==} 286 | engines: {node: ^10 || ^12 || >=14} 287 | dependencies: 288 | nanoid: 3.3.4 289 | picocolors: 1.0.0 290 | source-map-js: 1.0.2 291 | dev: true 292 | 293 | /resolve/1.22.1: 294 | resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} 295 | hasBin: true 296 | dependencies: 297 | is-core-module: 2.11.0 298 | path-parse: 1.0.7 299 | supports-preserve-symlinks-flag: 1.0.0 300 | dev: true 301 | 302 | /rollup/3.7.3: 303 | resolution: {integrity: sha512-7e68MQbAWCX6mI4/0lG1WHd+NdNAlVamg0Zkd+8LZ/oXojligdGnCNyHlzXqXCZObyjs5FRc3AH0b17iJESGIQ==} 304 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 305 | hasBin: true 306 | optionalDependencies: 307 | fsevents: 2.3.2 308 | dev: true 309 | 310 | /source-map-js/1.0.2: 311 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 312 | engines: {node: '>=0.10.0'} 313 | dev: true 314 | 315 | /supports-preserve-symlinks-flag/1.0.0: 316 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 317 | engines: {node: '>= 0.4'} 318 | dev: true 319 | 320 | /typescript/4.9.4: 321 | resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==} 322 | engines: {node: '>=4.2.0'} 323 | hasBin: true 324 | dev: true 325 | 326 | /vite/4.0.0: 327 | resolution: {integrity: sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA==} 328 | engines: {node: ^14.18.0 || >=16.0.0} 329 | hasBin: true 330 | peerDependencies: 331 | '@types/node': '>= 14' 332 | less: '*' 333 | sass: '*' 334 | stylus: '*' 335 | sugarss: '*' 336 | terser: ^5.4.0 337 | peerDependenciesMeta: 338 | '@types/node': 339 | optional: true 340 | less: 341 | optional: true 342 | sass: 343 | optional: true 344 | stylus: 345 | optional: true 346 | sugarss: 347 | optional: true 348 | terser: 349 | optional: true 350 | dependencies: 351 | esbuild: 0.16.4 352 | postcss: 8.4.19 353 | resolve: 1.22.1 354 | rollup: 3.7.3 355 | optionalDependencies: 356 | fsevents: 2.3.2 357 | dev: true 358 | -------------------------------------------------------------------------------- /example/client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/client/src/fetch.ts: -------------------------------------------------------------------------------- 1 | import { edenFetch } from '@elysia/eden' 2 | import type { Server } from '../../server' 3 | 4 | const fetch = edenFetch('http://localhost:8080') 5 | 6 | const d = await fetch('/', { 7 | method: 'POST' 8 | }) 9 | 10 | console.log(d) 11 | -------------------------------------------------------------------------------- /example/client/src/main.ts: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | 3 | // import './fetch' 4 | // import './fn' 5 | import './treaty' 6 | // import './treaty-file' 7 | -------------------------------------------------------------------------------- /example/client/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | #app { 41 | max-width: 1280px; 42 | margin: 0 auto; 43 | padding: 2rem; 44 | text-align: center; 45 | } 46 | 47 | .logo { 48 | height: 6em; 49 | padding: 1.5em; 50 | will-change: filter; 51 | } 52 | .logo:hover { 53 | filter: drop-shadow(0 0 2em #646cffaa); 54 | } 55 | .logo.vanilla:hover { 56 | filter: drop-shadow(0 0 2em #3178c6aa); 57 | } 58 | 59 | .card { 60 | padding: 2em; 61 | } 62 | 63 | .read-the-docs { 64 | color: #888; 65 | } 66 | 67 | button { 68 | border-radius: 8px; 69 | border: 1px solid transparent; 70 | padding: 0.6em 1.2em; 71 | font-size: 1em; 72 | font-weight: 500; 73 | font-family: inherit; 74 | background-color: #1a1a1a; 75 | cursor: pointer; 76 | transition: border-color 0.25s; 77 | } 78 | button:hover { 79 | border-color: #646cff; 80 | } 81 | button:focus, 82 | button:focus-visible { 83 | outline: 4px auto -webkit-focus-ring-color; 84 | } 85 | 86 | @media (prefers-color-scheme: light) { 87 | :root { 88 | color: #213547; 89 | background-color: #ffffff; 90 | } 91 | a:hover { 92 | color: #747bff; 93 | } 94 | button { 95 | background-color: #f9f9f9; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /example/client/src/treaty-file.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | import { edenTreaty } from '../../../src/treaty' 4 | import type { Server } from '../../server.js' 5 | 6 | export const client = edenTreaty('http://localhost:8080') 7 | 8 | const id = (id: string) => 9 | document.getElementById(id)! as T 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | id('form').addEventListener('submit', async (event) => { 13 | event.preventDefault() 14 | 15 | console.log("POST") 16 | 17 | // FileList upload example 18 | let res = await client.image.post({ 19 | image: id('file').files!, 20 | title: "Hi" 21 | }) 22 | 23 | // FileArray upload example 24 | res = await client.image.post({ 25 | image: Array.from(id('file').files!), 26 | title: "Hi" 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /example/client/src/treaty.ts: -------------------------------------------------------------------------------- 1 | import { treaty, Treaty } from '../../../src/treaty2' 2 | import type { Server } from '../../server' 3 | 4 | export const client = treaty('http://localhost:8080') 5 | 6 | const { data } = await client.products.nendoroid.skadi.get({ 7 | query: { 8 | username: 'A', 9 | filter: { 10 | name: 'A', 11 | address: 'A', 12 | age: 'A' 13 | } 14 | } 15 | }) 16 | 17 | await client['sign-in'].get() 18 | 19 | // const data = await client.products.nendoroid.skadi.post({ 20 | // username: 'A' 21 | // }) 22 | 23 | // const { data, error } = await client.products.nendoroid['1902'].put({ 24 | // name: 'Anya Forger' 25 | // }) 26 | 27 | // if (!error) console.log(data) 28 | 29 | // const mirror = client.ws.mirror.subscribe() 30 | 31 | // mirror.subscribe(({ data }) => { 32 | // mirror.send(data) 33 | // }) 34 | 35 | // const chat = client.chat.subscribe({ 36 | // $query: { 37 | // name: 'A', 38 | // room: 'C' 39 | // } 40 | // }) 41 | // chat.subscribe(({ data }) => { 42 | // chat.send(data.message) 43 | // }) 44 | 45 | // setInterval(() => { 46 | // mirror.send('a') 47 | // }, 200) 48 | -------------------------------------------------------------------------------- /example/client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 7 | "moduleResolution": "Bundler", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "skipLibCheck": true, 17 | "skipDefaultLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /example/fetch.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { edenFetch } from '../src' 3 | import type { Server } from './server' 4 | 5 | const json = { 6 | name: 'Saori', 7 | affiliation: 'Arius', 8 | type: 'Striker' 9 | } 10 | 11 | const app = new Elysia() 12 | .get('/', () => 'hi') 13 | .post('/', () => 'post') 14 | .get('/json', ({ body }) => json) 15 | .get( 16 | '/json-utf8', 17 | ({ set }) => 18 | new Response(JSON.stringify(json), { 19 | headers: { 20 | 'Content-Type': 'application/json; charset=utf-8' 21 | } 22 | }) 23 | ) 24 | .get('/name/:name', ({ params: { name } }) => name) 25 | .post( 26 | '/headers', 27 | ({ request: { headers } }) => headers.get('x-affiliation'), 28 | { 29 | headers: t.Object({ 30 | 'x-affiliation': t.Literal('Arius') 31 | }) 32 | } 33 | ) 34 | .get('/number', () => 1) 35 | .get('/true', () => true) 36 | .get('/false', () => false) 37 | .get('/throw-error', () => { 38 | throw new Error('hare') 39 | 40 | return 'Hi' 41 | }) 42 | .get( 43 | '/direct-error', 44 | ({ set }) => { 45 | set.status = 500 46 | 47 | return 'hare' 48 | }, 49 | { 50 | response: { 51 | 200: t.String(), 52 | 500: t.Literal('hare') 53 | } 54 | } 55 | ) 56 | .listen(8080) 57 | 58 | const fetch = edenFetch('http://localhost:8080') 59 | 60 | const a = await fetch('/headers', { 61 | method: 'POST', 62 | headers: { 63 | 'x-affiliation': 'Arius' 64 | }, 65 | query: {} 66 | }) 67 | 68 | const { data } = await fetch('/name/:name', { 69 | params: { 70 | name: 'elysia' 71 | } 72 | }) 73 | 74 | console.log(data) 75 | -------------------------------------------------------------------------------- /example/grace.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/eden/c1fa86868fd195500bacbb84c25c6d19c2de1349/example/grace.webp -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { join } from 'path' 3 | 4 | Bun.spawn(['bun', 'server.ts'], { 5 | stdout: 'pipe', 6 | cwd: import.meta.dir 7 | }) 8 | 9 | Bun.spawn(['npm', 'run', 'dev'], { 10 | stdout: 'pipe', 11 | cwd: join(import.meta.dir, 'client') 12 | }) 13 | 14 | console.log('If things work properly, see http://localhost:5173') 15 | -------------------------------------------------------------------------------- /example/regression.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { edenTreaty } from '../src' 3 | 4 | class CustomError extends Error { 5 | constructor(public message: string) { 6 | super(message) 7 | } 8 | } 9 | 10 | const errorPlugin = new Elysia().error({ CUSTOM_ERROR: CustomError }) 11 | 12 | const main = new Elysia() 13 | .use(errorPlugin) 14 | .get('/', () => ({ name: 'Elysia' }), { 15 | response: { 16 | 200: t.Object({ name: t.String() }) 17 | } 18 | }) 19 | 20 | type App = typeof main 21 | type B = App['schema']['/'] 22 | 23 | const api = edenTreaty('') 24 | 25 | const res = await api.get() 26 | if (res.error) throw res.error 27 | res.data // unknown 28 | -------------------------------------------------------------------------------- /example/server.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { cors } from '@elysiajs/cors' 3 | 4 | const app = new Elysia() 5 | .use(cors()) 6 | .get('/something/here', () => 'Elysia') 7 | .post('/array', ({ body }) => body, { 8 | body: t.String() 9 | }) 10 | .post('/query', async () => 'There', { 11 | body: t.Object({ 12 | name: t.String() 13 | }), 14 | query: t.Object({ 15 | username: t.String() 16 | }) 17 | }) 18 | .put('/query', async () => 'There', { 19 | body: t.Object({ 20 | namea: t.String() 21 | }), 22 | query: t.Object({ 23 | username: t.String() 24 | }) 25 | }) 26 | .post('/', () => 'A') 27 | .post('/image', ({ body: { image, title } }) => title, { 28 | body: t.Object({ 29 | image: t.Files(), 30 | title: t.String() 31 | }) 32 | }) 33 | .post('/', () => 'Elysia') 34 | .post('/name/:name', () => 1) 35 | .post('/a/bcd/:name/b', () => 1) 36 | .post('/id/here', () => 1) 37 | .post('/id/here/a', () => 1) 38 | .get( 39 | '/error', 40 | ({ set }) => { 41 | set.status = 400 42 | 43 | return { 44 | error: true, 45 | message: 'Something' 46 | } 47 | }, 48 | { 49 | response: { 50 | 200: t.Object({ 51 | myName: t.String() 52 | }), 53 | 400: t.Object({ 54 | error: t.Boolean(), 55 | message: t.String() 56 | }) 57 | } 58 | } 59 | ) 60 | .post('/mirror', ({ body }) => body, { 61 | body: t.Object({ 62 | username: t.String(), 63 | password: t.String() 64 | }) 65 | }) 66 | .get('/sign-in', () => 'ok') 67 | .get( 68 | '/products/nendoroid/skadi', 69 | ({ query }) => { 70 | return { 71 | id: 1, 72 | name: 'Skadi' 73 | } 74 | }, 75 | { 76 | query: t.Partial( 77 | t.Object({ 78 | username: t.String(), 79 | filter: t.Partial( 80 | t.Object({ 81 | name: t.Optional(t.String()), 82 | address: t.Optional(t.String()), 83 | age: t.Optional(t.Number()) 84 | }) 85 | ) 86 | }) 87 | ) 88 | } 89 | ) 90 | .post('/products/nendoroid/skadi', () => 1, { 91 | body: t.Object({ 92 | username: t.String() 93 | }) 94 | }) 95 | .post('/products/nendoroid', ({ body }) => body, { 96 | body: t.Object({ 97 | id: t.Number(), 98 | name: t.String() 99 | }) 100 | }) 101 | .put( 102 | '/products/nendoroid/:id', 103 | ({ body: { name }, params: { id } }) => ({ 104 | name, 105 | id 106 | }), 107 | { 108 | body: t.Object({ 109 | name: t.String() 110 | }), 111 | response: { 112 | 200: t.Object({ 113 | name: t.String(), 114 | id: t.String() 115 | }), 116 | 400: t.Object({ 117 | error: t.String(), 118 | name: t.String(), 119 | id: t.String() 120 | }), 121 | 401: t.Object({ 122 | error: t.String(), 123 | name: t.String(), 124 | id: t.String() 125 | }) 126 | } 127 | } 128 | ) 129 | .group('/group', (app) => app.get('/in', () => 'Hi')) 130 | .ws('/ws/mirror', { 131 | body: t.String(), 132 | response: t.String(), 133 | message(ws, message) { 134 | ws.send(message) 135 | } 136 | }) 137 | .ws('/chat/:room/:name', { 138 | message(ws, message) { 139 | ws.send(message) 140 | }, 141 | body: t.String(), 142 | response: t.String() 143 | }) 144 | .model({ 145 | success: t.Object({ 146 | success: t.Boolean(), 147 | data: t.String() 148 | }), 149 | fail: t.Object({ 150 | success: t.Boolean(), 151 | data: t.Null() 152 | }) 153 | }) 154 | .get( 155 | '/union-type', 156 | () => { 157 | return { 158 | success: true, 159 | data: null 160 | } 161 | }, 162 | { 163 | response: { 164 | 200: 'success', 165 | 400: 'fail' 166 | } 167 | } 168 | ) 169 | .ws('/chat', { 170 | open(ws) { 171 | const { room, name } = ws.data.query 172 | 173 | ws.subscribe(room) 174 | 175 | ws.publish(room, { 176 | message: `${name} has enter the room`, 177 | name: 'notice', 178 | time: Date.now() 179 | }) 180 | }, 181 | message(ws, message) { 182 | const { room, name } = ws.data.query 183 | 184 | ws.publish(room, { 185 | message, 186 | name, 187 | time: Date.now() 188 | }) 189 | }, 190 | close(ws) { 191 | const { room, name } = ws.data.query 192 | 193 | ws.publish(room, { 194 | message: `${name} has leave the room`, 195 | name: 'notice', 196 | time: Date.now() 197 | }) 198 | }, 199 | body: t.String(), 200 | query: t.Object({ 201 | room: t.String(), 202 | name: t.String() 203 | }), 204 | response: t.Object({ 205 | message: t.String(), 206 | name: t.String(), 207 | time: t.Number() 208 | }) 209 | }) 210 | .listen(8080) 211 | 212 | export type Server = typeof app 213 | -------------------------------------------------------------------------------- /example/treaty.ts: -------------------------------------------------------------------------------- 1 | import { treaty } from '../src' 2 | import type { Server } from './server' 3 | 4 | const api = treaty('localhost:8080') 5 | 6 | const { data, error } = await api.products.nendoroid.skadi.get({ 7 | query: { 8 | filter: { name: 'skadi' } 9 | } 10 | }) 11 | 12 | if (error) 13 | switch (error.status) { 14 | case 422: 15 | console.log(error.value.message) 16 | throw error 17 | } 18 | 19 | console.log(data) 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elysiajs/eden", 3 | "version": "1.3.2", 4 | "description": "Fully type-safe Elysia client", 5 | "author": { 6 | "name": "saltyAom", 7 | "url": "https://github.com/SaltyAom", 8 | "email": "saltyaom@gmail.com" 9 | }, 10 | "main": "./dist/index.js", 11 | "module": "./dist/index.mjs", 12 | "exports": { 13 | "./package.json": "./package.json", 14 | ".": { 15 | "require": "./dist/index.js", 16 | "import": "./dist/index.mjs", 17 | "types": "./dist/index.d.ts" 18 | }, 19 | "./treaty": { 20 | "require": "./dist/treaty.js", 21 | "import": "./dist/treaty.mjs", 22 | "types": "./dist/treaty/index.d.ts" 23 | }, 24 | "./treaty2": { 25 | "require": "./dist/2.js", 26 | "import": "./dist/treaty2.mjs", 27 | "types": "./dist/treaty2/index.d.ts" 28 | }, 29 | "./fetch": { 30 | "require": "./dist/fetch.js", 31 | "import": "./dist/fetch.mjs", 32 | "types": "./dist/fetch/index.d.ts" 33 | } 34 | }, 35 | "types": "./dist/index.d.ts", 36 | "keywords": [ 37 | "elysia", 38 | "eden", 39 | "connector" 40 | ], 41 | "homepage": "https://github.com/elysiajs/eden", 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/elysiajs/eden" 45 | }, 46 | "bugs": "https://github.com/elysiajs/eden/issues", 47 | "license": "MIT", 48 | "scripts": { 49 | "dev": "bun run --watch example/index.ts", 50 | "test": "bun test && bun test:types", 51 | "test:types": "tsc --project tsconfig.test.json", 52 | "build": "rimraf dist && tsup", 53 | "release": "npm run build && npm run test && npm publish --access public" 54 | }, 55 | "peerDependencies": { 56 | "elysia": ">= 1.3.0" 57 | }, 58 | "devDependencies": { 59 | "@elysiajs/cors": "1.3.0", 60 | "@types/bun": "^1.2.10", 61 | "@types/node": "^20.17.0", 62 | "elysia": "1.3.0", 63 | "esbuild": "^0.19.3", 64 | "eslint": "^8.26.0", 65 | "expect-type": "^1.1.0", 66 | "rimraf": "^4.4.1", 67 | "tsup": "^8.3.5", 68 | "typescript": "^5.7.2", 69 | "vite": "^6.0.5" 70 | }, 71 | "prettier": { 72 | "semi": false, 73 | "tabWidth": 4, 74 | "singleQuote": true, 75 | "trailingComma": "none" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export class EdenFetchError< 2 | Status extends number = number, 3 | Value = unknown 4 | > extends Error { 5 | constructor(public status: Status, public value: Value) { 6 | super(value + '') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/fetch/index.ts: -------------------------------------------------------------------------------- 1 | import type { Elysia } from 'elysia' 2 | 3 | import { EdenFetchError } from '../errors' 4 | import type { EdenFetch } from './types' 5 | import { parseStringifiedValue } from '../utils/parsingUtils' 6 | 7 | export type { EdenFetch } from './types' 8 | 9 | const parseResponse = async (response: Response) => { 10 | const contentType = response.headers.get('Content-Type')?.split(';')[0] 11 | 12 | switch (contentType) { 13 | case 'application/json': 14 | return response.json() 15 | case 'application/octet-stream': 16 | return response.arrayBuffer() 17 | case 'multipart/form-data': { 18 | const formData = await response.formData() 19 | 20 | const data = {} 21 | formData.forEach((value, key) => { 22 | // @ts-ignore 23 | data[key] = value 24 | }) 25 | 26 | return data 27 | } 28 | } 29 | 30 | return response.text().then(parseStringifiedValue) 31 | } 32 | 33 | const handleResponse = async (response: Response, retry: () => any) => { 34 | const data = await parseResponse(response) 35 | 36 | if (response.status > 300) 37 | return { 38 | data: null, 39 | status: response.status, 40 | headers: response.headers, 41 | retry, 42 | error: new EdenFetchError(response.status, data) 43 | } 44 | 45 | return { 46 | data, 47 | error: null, 48 | status: response.status, 49 | headers: response.headers, 50 | retry 51 | } 52 | } 53 | 54 | export const edenFetch = >( 55 | server: string, 56 | config?: EdenFetch.Config 57 | ): EdenFetch.Create => 58 | // @ts-ignore 59 | (endpoint: string, { query, params, body, ...options } = {}) => { 60 | if (params) 61 | Object.entries(params).forEach(([key, value]) => { 62 | endpoint = endpoint.replace(`:${key}`, value as string) 63 | }) 64 | 65 | // @ts-ignore 66 | const contentType = options.headers?.['Content-Type'] 67 | 68 | if (!contentType || contentType === 'application/json') 69 | try { 70 | body = JSON.stringify(body) 71 | } catch (error) {} 72 | 73 | const fetch = config?.fetcher || globalThis.fetch 74 | 75 | const nonNullishQuery = query 76 | ? Object.fromEntries( 77 | Object.entries(query).filter( 78 | ([_, val]) => val !== undefined && val !== null 79 | ) 80 | ) 81 | : null 82 | 83 | const queryStr = nonNullishQuery 84 | ? `?${new URLSearchParams(nonNullishQuery).toString()}` 85 | : '' 86 | 87 | const requestUrl = `${server}${endpoint}${queryStr}` 88 | const headers = body 89 | ? { 90 | 'content-type': 'application/json', 91 | // @ts-ignore 92 | ...options.headers 93 | } 94 | : // @ts-ignore 95 | options.headers 96 | const init = { 97 | ...options, 98 | // @ts-ignore 99 | method: options.method?.toUpperCase() || 'GET', 100 | headers, 101 | body: body as any 102 | } 103 | 104 | const execute = () => 105 | fetch(requestUrl, init).then((response) => 106 | handleResponse(response, execute) 107 | ) 108 | 109 | return execute() 110 | } 111 | -------------------------------------------------------------------------------- /src/fetch/types.ts: -------------------------------------------------------------------------------- 1 | import type { Elysia } from 'elysia' 2 | import type { EdenFetchError } from '../errors' 3 | import type { 4 | MapError, 5 | IsUnknown, 6 | IsNever, 7 | Prettify, 8 | TreatyToPath 9 | } from '../types' 10 | 11 | export namespace EdenFetch { 12 | export type Create> = 13 | App extends { 14 | '~Routes': infer Schema extends Record 15 | } 16 | ? EdenFetch.Fn< 17 | // @ts-expect-error 18 | TreatyToPath 19 | > 20 | : 'Please install Elysia before using Eden' 21 | 22 | export interface Config extends RequestInit { 23 | fetcher?: typeof globalThis.fetch 24 | } 25 | 26 | export type Fn> = < 27 | Endpoint extends keyof Schema, 28 | Method extends Uppercase>, 29 | Route extends Schema[Endpoint][Lowercase] 30 | >( 31 | endpoint: Endpoint, 32 | options: Omit & 33 | ('GET' extends Method 34 | ? { 35 | method?: Method 36 | } 37 | : { 38 | method: Method 39 | }) & 40 | (IsNever extends true 41 | ? { 42 | params?: Record 43 | } 44 | : { 45 | params: Route['params'] 46 | }) & 47 | (IsNever extends true 48 | ? { 49 | query?: Record 50 | } 51 | : { 52 | query: Route['query'] 53 | }) & 54 | (undefined extends Route['headers'] 55 | ? { 56 | headers?: Record 57 | } 58 | : { 59 | headers: Route['headers'] 60 | }) & 61 | (IsUnknown extends false 62 | ? { body: Route['body'] } 63 | : { body?: unknown }) 64 | ) => Promise< 65 | Prettify< 66 | | { 67 | data: Awaited 68 | error: null 69 | status: number 70 | headers: Record 71 | retry(): Promise 72 | } 73 | | { 74 | data: null 75 | error: MapError extends infer Errors 76 | ? IsNever extends true 77 | ? EdenFetchError 78 | : Errors 79 | : EdenFetchError 80 | status: number 81 | headers: Record 82 | retry(): Promise 83 | } 84 | > 85 | > 86 | } 87 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Treaty } from './treaty2' 2 | 3 | export { treaty } from './treaty2' 4 | export { edenTreaty } from './treaty' 5 | export { edenFetch } from './fetch' 6 | -------------------------------------------------------------------------------- /src/treaty/index.ts: -------------------------------------------------------------------------------- 1 | import type { Elysia, InputSchema } from 'elysia' 2 | import { EdenFetchError } from '../errors' 3 | import { composePath } from './utils' 4 | import type { EdenTreaty } from './types' 5 | import { parseMessageEvent, parseStringifiedValue } from '../utils/parsingUtils' 6 | 7 | export type { EdenTreaty } from './types' 8 | 9 | // @ts-ignore 10 | const isServer = typeof FileList === 'undefined' 11 | 12 | const isFile = (v: any) => { 13 | // @ts-ignore 14 | if (isServer) { 15 | return v instanceof Blob 16 | } else { 17 | // @ts-ignore 18 | return v instanceof FileList || v instanceof File 19 | } 20 | } 21 | 22 | // FormData is 1 level deep 23 | const hasFile = (obj: Record) => { 24 | if (!obj) return false 25 | 26 | for (const key in obj) { 27 | if (isFile(obj[key])) return true 28 | else if ( 29 | Array.isArray(obj[key]) && 30 | (obj[key] as unknown[]).find((x) => isFile(x)) 31 | ) 32 | return true 33 | } 34 | 35 | return false 36 | } 37 | 38 | // @ts-ignore 39 | const createNewFile = (v: File) => 40 | isServer 41 | ? v 42 | : new Promise((resolve) => { 43 | // @ts-ignore 44 | const reader = new FileReader() 45 | 46 | reader.onload = () => { 47 | const file = new File([reader.result!], v.name, { 48 | lastModified: v.lastModified, 49 | type: v.type 50 | }) 51 | resolve(file) 52 | } 53 | 54 | reader.readAsArrayBuffer(v) 55 | }) 56 | 57 | type MaybeArray = T | T[] 58 | 59 | export class EdenWS = InputSchema> { 60 | ws: WebSocket 61 | url: string 62 | 63 | constructor(url: string) { 64 | this.ws = new WebSocket(url) 65 | this.url = url 66 | } 67 | 68 | send(data: MaybeArray) { 69 | if (Array.isArray(data)) { 70 | data.forEach((datum) => this.send(datum)) 71 | 72 | return this 73 | } 74 | 75 | this.ws.send( 76 | typeof data === 'object' ? JSON.stringify(data) : data.toString() 77 | ) 78 | 79 | return this 80 | } 81 | 82 | on( 83 | type: K, 84 | listener: (event: EdenTreaty.WSEvent) => void, 85 | options?: boolean | AddEventListenerOptions 86 | ) { 87 | return this.addEventListener(type, listener, options) 88 | } 89 | 90 | off( 91 | type: K, 92 | listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, 93 | options?: boolean | EventListenerOptions 94 | ) { 95 | this.ws.removeEventListener(type, listener, options) 96 | 97 | return this 98 | } 99 | 100 | subscribe( 101 | onMessage: ( 102 | event: EdenTreaty.WSEvent<'message', Schema['response']> 103 | ) => void, 104 | options?: boolean | AddEventListenerOptions 105 | ) { 106 | return this.addEventListener('message', onMessage, options) 107 | } 108 | 109 | addEventListener( 110 | type: K, 111 | listener: (event: EdenTreaty.WSEvent) => void, 112 | options?: boolean | AddEventListenerOptions 113 | ) { 114 | this.ws.addEventListener( 115 | type, 116 | (ws) => { 117 | if (type === 'message') { 118 | const data = parseMessageEvent(ws as MessageEvent) 119 | 120 | listener({ 121 | ...ws, 122 | data 123 | } as any) 124 | } else listener(ws as any) 125 | }, 126 | options 127 | ) 128 | 129 | return this 130 | } 131 | 132 | removeEventListener( 133 | type: K, 134 | listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, 135 | options?: boolean | EventListenerOptions 136 | ) { 137 | this.off(type, listener, options) 138 | 139 | return this 140 | } 141 | 142 | close() { 143 | this.ws.close() 144 | 145 | return this 146 | } 147 | } 148 | 149 | const createProxy = ( 150 | domain: string, 151 | path = '', 152 | config: EdenTreaty.Config 153 | ): Record => 154 | new Proxy(() => {}, { 155 | get(target, key, value) { 156 | return createProxy(domain, `${path}/${key.toString()}`, config) 157 | }, 158 | // @ts-ignore 159 | apply( 160 | target, 161 | _, 162 | [initialBody, options = {}]: [ 163 | { 164 | [x: string]: any 165 | $fetch?: RequestInit 166 | $headers?: HeadersInit 167 | $query?: Record 168 | getRaw?: boolean 169 | }, 170 | { 171 | fetch?: RequestInit 172 | transform?: EdenTreaty.Transform 173 | headers?: Record 174 | query?: Record 175 | } 176 | ] = [{}, {}] 177 | ) { 178 | let bodyObj: any = 179 | initialBody !== undefined && 180 | (typeof initialBody !== 'object' || Array.isArray(initialBody)) 181 | ? initialBody 182 | : undefined 183 | 184 | const { 185 | $query, 186 | $fetch, 187 | $headers, 188 | $transform, 189 | getRaw, 190 | ...restBody 191 | } = initialBody ?? {} 192 | 193 | bodyObj ??= restBody 194 | 195 | const i = path.lastIndexOf('/'), 196 | method = path.slice(i + 1).toUpperCase(), 197 | url = composePath( 198 | domain, 199 | i === -1 ? '/' : path.slice(0, i), 200 | Object.assign(options.query ?? {}, $query) 201 | ) 202 | 203 | const fetcher = config.fetcher ?? fetch 204 | let transforms = config.transform 205 | ? Array.isArray(config.transform) 206 | ? config.transform 207 | : [config.transform] 208 | : undefined 209 | 210 | const $transforms = $transform 211 | ? Array.isArray($transform) 212 | ? $transform 213 | : [$transform] 214 | : undefined 215 | 216 | if ($transforms) { 217 | if (transforms) transforms = $transforms.concat(transforms) 218 | else transforms = $transforms as any 219 | } 220 | 221 | if (method === 'SUBSCRIBE') 222 | return new EdenWS( 223 | url.replace( 224 | /^([^]+):\/\//, 225 | url.startsWith('https://') ? 'wss://' : 'ws://' 226 | ) 227 | ) 228 | 229 | const execute = async ( 230 | modifiers: T 231 | ): Promise> => { 232 | let body: any 233 | 234 | const headers = { 235 | ...config.$fetch?.headers, 236 | ...$fetch?.headers, 237 | ...options.headers, 238 | ...$headers 239 | } as Record 240 | 241 | if (method !== 'GET' && method !== 'HEAD') { 242 | body = Object.keys(bodyObj).length 243 | ? bodyObj 244 | : Array.isArray(bodyObj) 245 | ? bodyObj 246 | : undefined 247 | 248 | const isObject = 249 | body && 250 | (typeof body === 'object' || Array.isArray(bodyObj)) 251 | const isFormData = isObject && hasFile(body) 252 | 253 | if (isFormData) { 254 | const newBody = new FormData() 255 | 256 | // FormData is 1 level deep 257 | for (const [key, field] of Object.entries(body)) { 258 | if (isServer) { 259 | newBody.append(key, field as any) 260 | } else { 261 | // @ts-ignore 262 | if (field instanceof File) 263 | newBody.append( 264 | key, 265 | await createNewFile(field as any) 266 | ) 267 | // @ts-ignore 268 | else if (field instanceof FileList) { 269 | // @ts-ignore 270 | for (let i = 0; i < field.length; i++) { 271 | newBody.append( 272 | key as any, 273 | await createNewFile( 274 | (field as any)[i] 275 | ) 276 | ) 277 | } 278 | } else if (Array.isArray(field)) { 279 | for (let i = 0; i < field.length; i++) { 280 | const value = (field as any)[i] 281 | 282 | newBody.append( 283 | key as any, 284 | value instanceof File 285 | ? await createNewFile(value) 286 | : value 287 | ) 288 | } 289 | } else newBody.append(key, field as string) 290 | } 291 | } 292 | 293 | body = newBody 294 | } else { 295 | if (body !== null && body !== undefined) { 296 | headers['content-type'] = isObject 297 | ? 'application/json' 298 | : 'text/plain' 299 | 300 | body = isObject ? JSON.stringify(body) : bodyObj 301 | } 302 | } 303 | } 304 | 305 | const response = await fetcher(url, { 306 | method, 307 | body, 308 | ...config.$fetch, 309 | ...options.fetch, 310 | ...$fetch, 311 | headers 312 | }) 313 | 314 | let data 315 | 316 | if (modifiers.getRaw) return response as any 317 | switch (response.headers.get('Content-Type')?.split(';')[0]) { 318 | case 'application/json': 319 | data = await response.json() 320 | break 321 | 322 | default: 323 | data = await response.text().then(parseStringifiedValue) 324 | } 325 | 326 | const error = 327 | response.status >= 300 || response.status < 200 328 | ? new EdenFetchError(response.status, data) 329 | : null 330 | 331 | let executeReturn = { 332 | data, 333 | error, 334 | response, 335 | status: response.status, 336 | headers: response.headers 337 | } 338 | 339 | if (transforms) 340 | for (const transform of transforms) { 341 | let temp = transform(executeReturn) 342 | if (temp instanceof Promise) temp = await temp 343 | if (temp !== undefined && temp !== null) 344 | executeReturn = temp as any 345 | } 346 | 347 | return executeReturn as any 348 | } 349 | 350 | return execute({ getRaw }) 351 | } 352 | }) as unknown as Record 353 | 354 | export const edenTreaty = < 355 | App extends Elysia 356 | >( 357 | domain: string, 358 | config: EdenTreaty.Config = { 359 | fetcher: fetch 360 | } 361 | ): EdenTreaty.Create => 362 | new Proxy( 363 | {}, 364 | { 365 | get(target, key) { 366 | return createProxy(domain, key as string, config) 367 | } 368 | } 369 | ) as any 370 | -------------------------------------------------------------------------------- /src/treaty/types.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Elysia } from 'elysia' 3 | import type { EdenWS } from './index' 4 | import type { IsUnknown, IsNever, MapError, Prettify } from '../types' 5 | import type { EdenFetchError } from '../errors' 6 | 7 | type Files = File | FileList 8 | 9 | type Replace = { 10 | [K in keyof RecordType]: RecordType[K] extends TargetType 11 | ? GenericType 12 | : RecordType[K] 13 | } 14 | 15 | type MaybeArray = T | T[] 16 | 17 | export namespace EdenTreaty { 18 | export type Create< 19 | App extends Elysia 20 | > = App extends { 21 | '~Routes': infer Schema extends Record 22 | } 23 | ? Prettify> 24 | : 'Please install Elysia before using Eden' 25 | 26 | export type Sign> = { 27 | [K in keyof Route as K extends `:${string}` 28 | ? (string & {}) | number | K 29 | : K extends '' | '/' 30 | ? 'index' 31 | : K]: Route[K] extends { 32 | body: infer Body 33 | headers: infer Headers 34 | query: infer Query 35 | params: unknown 36 | response: infer Response 37 | } 38 | ? K extends 'subscribe' 39 | ? // ? Websocket route 40 | undefined extends Route['query'] 41 | ? (params?: { 42 | $query?: Record 43 | }) => EdenWS 44 | : (params: { $query: Route['query'] }) => EdenWS 45 | : // ? HTTP route 46 | (( 47 | params: Prettify< 48 | { 49 | $fetch?: RequestInit 50 | getRaw?: boolean 51 | $transform?: Transform 52 | } & (IsUnknown extends false 53 | ? Replace 54 | : {}) & 55 | (undefined extends Query 56 | ? { 57 | $query?: Record 58 | } 59 | : { 60 | $query: Query 61 | }) & 62 | (undefined extends Headers 63 | ? { 64 | $headers?: Record 65 | } 66 | : { 67 | $headers: Headers 68 | }) 69 | > 70 | ) => Promise< 71 | ( 72 | | { 73 | data: Response extends { 74 | 200: infer ReturnedType 75 | } 76 | ? Awaited 77 | : unknown 78 | error: null 79 | } 80 | | { 81 | data: null 82 | error: Response extends Record 83 | ? MapError extends infer Errors 84 | ? IsNever extends true 85 | ? EdenFetchError 86 | : Errors 87 | : EdenFetchError 88 | : EdenFetchError 89 | } 90 | ) & { 91 | status: number 92 | response: Response 93 | headers: Record 94 | } 95 | >) extends (params: infer Params) => infer Response 96 | ? { 97 | $params: undefined 98 | $headers: undefined 99 | $query: undefined 100 | } extends Params 101 | ? ( 102 | params?: Params, 103 | options?: { 104 | fetch?: RequestInit 105 | transform?: EdenTreaty.Transform 106 | // @ts-ignore 107 | query?: Params['query'] 108 | // @ts-ignore 109 | headers?: Params['headers'] 110 | } 111 | ) => Response 112 | : ( 113 | params: Params, 114 | options?: { 115 | fetch?: RequestInit 116 | transform?: EdenTreaty.Transform 117 | // @ts-ignore 118 | query?: Params['query'] 119 | // @ts-ignore 120 | headers?: Params['headers'] 121 | } 122 | ) => Response 123 | : never 124 | : Prettify> 125 | } 126 | 127 | type UnwrapPromise = T extends Promise ? A : T 128 | 129 | export type Transform = MaybeArray< 130 | ( 131 | response: unknown extends T 132 | ? { 133 | data: any 134 | error: any 135 | response: Response 136 | status: number 137 | headers: Headers 138 | } 139 | : UnwrapPromise 140 | ) => UnwrapPromise | void 141 | > 142 | 143 | export interface Config { 144 | /** 145 | * Default options to pass to fetch 146 | */ 147 | $fetch?: RequestInit 148 | fetcher?: typeof fetch 149 | transform?: Transform 150 | } 151 | 152 | export type DetailedResponse = { 153 | data: any 154 | error: any 155 | response: Response 156 | status: number 157 | headers: Headers 158 | } 159 | 160 | export interface OnMessage extends MessageEvent { 161 | data: Data 162 | rawData: MessageEvent['data'] 163 | } 164 | 165 | export type ExecuteOptions = { 166 | getRaw?: boolean 167 | } 168 | 169 | export type ExecuteReturnType = 170 | T['getRaw'] extends true ? Response : DetailedResponse 171 | 172 | export type WSEvent< 173 | K extends keyof WebSocketEventMap, 174 | Data = unknown 175 | > = K extends 'message' ? OnMessage : WebSocketEventMap[K] 176 | } 177 | -------------------------------------------------------------------------------- /src/treaty/utils.ts: -------------------------------------------------------------------------------- 1 | export const composePath = ( 2 | domain: string, 3 | path: string, 4 | query: Record | undefined 5 | ) => { 6 | if (!domain.endsWith('/')) domain += '/' 7 | if (path === 'index') path = '' 8 | 9 | if (!query || !Object.keys(query).length) return `${domain}${path}` 10 | 11 | let q = '' 12 | for (const [key, value] of Object.entries(query)) q += `${key}=${value}&` 13 | 14 | return `${domain}${path}?${q.slice(0, -1)}` 15 | } 16 | -------------------------------------------------------------------------------- /src/treaty2/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-extra-semi */ 2 | /* eslint-disable no-case-declarations */ 3 | /* eslint-disable prefer-const */ 4 | import type { Elysia } from 'elysia' 5 | import type { Treaty } from './types' 6 | 7 | import { EdenFetchError } from '../errors' 8 | import { EdenWS } from './ws' 9 | import { parseStringifiedValue } from '../utils/parsingUtils' 10 | 11 | const method = [ 12 | 'get', 13 | 'post', 14 | 'put', 15 | 'delete', 16 | 'patch', 17 | 'options', 18 | 'head', 19 | 'connect', 20 | 'subscribe' 21 | ] as const 22 | 23 | const locals = ['localhost', '127.0.0.1', '0.0.0.0'] 24 | 25 | const isServer = typeof FileList === 'undefined' 26 | 27 | const isFile = (v: any) => { 28 | if (isServer) return v instanceof Blob 29 | 30 | return v instanceof FileList || v instanceof File 31 | } 32 | 33 | // FormData is 1 level deep 34 | const hasFile = (obj: Record) => { 35 | if (!obj) return false 36 | 37 | for (const key in obj) { 38 | if (isFile(obj[key])) return true 39 | 40 | if (Array.isArray(obj[key]) && (obj[key] as unknown[]).find(isFile)) 41 | return true 42 | } 43 | 44 | return false 45 | } 46 | 47 | const createNewFile = (v: File) => 48 | isServer 49 | ? v 50 | : new Promise((resolve) => { 51 | const reader = new FileReader() 52 | 53 | reader.onload = () => { 54 | const file = new File([reader.result!], v.name, { 55 | lastModified: v.lastModified, 56 | type: v.type 57 | }) 58 | resolve(file) 59 | } 60 | 61 | reader.readAsArrayBuffer(v) 62 | }) 63 | 64 | const processHeaders = ( 65 | h: Treaty.Config['headers'], 66 | path: string, 67 | options: RequestInit = {}, 68 | headers: Record = {} 69 | ): Record => { 70 | if (Array.isArray(h)) { 71 | for (const value of h) 72 | if (!Array.isArray(value)) 73 | headers = processHeaders(value, path, options, headers) 74 | else { 75 | const key = value[0] 76 | if (typeof key === 'string') 77 | headers[key.toLowerCase()] = value[1] as string 78 | else 79 | for (const [k, value] of key) 80 | headers[k.toLowerCase()] = value as string 81 | } 82 | 83 | return headers 84 | } 85 | 86 | if (!h) return headers 87 | 88 | switch (typeof h) { 89 | case 'function': 90 | if (h instanceof Headers) 91 | return processHeaders(h, path, options, headers) 92 | 93 | const v = h(path, options) 94 | if (v) return processHeaders(v, path, options, headers) 95 | return headers 96 | 97 | case 'object': 98 | if (h instanceof Headers) { 99 | h.forEach((value, key) => { 100 | headers[key.toLowerCase()] = value 101 | }) 102 | return headers 103 | } 104 | 105 | for (const [key, value] of Object.entries(h)) 106 | headers[key.toLowerCase()] = value as string 107 | 108 | return headers 109 | 110 | default: 111 | return headers 112 | } 113 | } 114 | 115 | export async function* streamResponse(response: Response) { 116 | const body = response.body 117 | 118 | if (!body) return 119 | 120 | const reader = body.getReader() 121 | const decoder = new TextDecoder() 122 | 123 | try { 124 | while (true) { 125 | const { done, value } = await reader.read() 126 | if (done) break 127 | 128 | const data = decoder.decode(value) 129 | 130 | yield parseStringifiedValue(data) 131 | } 132 | } finally { 133 | reader.releaseLock() 134 | } 135 | } 136 | 137 | const createProxy = ( 138 | domain: string, 139 | config: Treaty.Config, 140 | paths: string[] = [], 141 | elysia?: Elysia 142 | ): any => 143 | new Proxy(() => {}, { 144 | get(_, param: string): any { 145 | return createProxy( 146 | domain, 147 | config, 148 | param === 'index' ? paths : [...paths, param], 149 | elysia 150 | ) 151 | }, 152 | apply(_, __, [body, options]) { 153 | if ( 154 | !body || 155 | options || 156 | (typeof body === 'object' && Object.keys(body).length !== 1) || 157 | method.includes(paths.at(-1) as any) 158 | ) { 159 | const methodPaths = [...paths] 160 | const method = methodPaths.pop() 161 | const path = '/' + methodPaths.join('/') 162 | 163 | let { 164 | fetcher = fetch, 165 | headers, 166 | onRequest, 167 | onResponse, 168 | fetch: conf 169 | } = config 170 | 171 | const isGetOrHead = 172 | method === 'get' || 173 | method === 'head' || 174 | method === 'subscribe' 175 | 176 | headers = processHeaders(headers, path, options) 177 | 178 | const query = isGetOrHead 179 | ? (body as Record) 180 | ?.query 181 | : options?.query 182 | 183 | let q = '' 184 | if (query) { 185 | const append = (key: string, value: string) => { 186 | q += 187 | (q ? '&' : '?') + 188 | `${encodeURIComponent(key)}=${encodeURIComponent( 189 | value 190 | )}` 191 | } 192 | 193 | for (const [key, value] of Object.entries(query)) { 194 | if (Array.isArray(value)) { 195 | for (const v of value) append(key, v) 196 | continue 197 | } 198 | 199 | // Explicitly exclude null and undefined values from url encoding 200 | // to prevent parsing string "null" / string "undefined" 201 | if (value === undefined || value === null) { 202 | continue 203 | } 204 | 205 | 206 | if (typeof value === 'object') { 207 | append(key, JSON.stringify(value)) 208 | continue 209 | } 210 | append(key, `${value}`) 211 | } 212 | } 213 | 214 | if (method === 'subscribe') { 215 | const url = 216 | domain.replace( 217 | /^([^]+):\/\//, 218 | domain.startsWith('https://') 219 | ? 'wss://' 220 | : domain.startsWith('http://') 221 | ? 'ws://' 222 | : locals.find((v) => 223 | (domain as string).includes(v) 224 | ) 225 | ? 'ws://' 226 | : 'wss://' 227 | ) + 228 | path + 229 | q 230 | 231 | return new EdenWS(url) 232 | } 233 | 234 | return (async () => { 235 | let fetchInit = { 236 | method: method?.toUpperCase(), 237 | body, 238 | ...conf, 239 | headers 240 | } satisfies RequestInit 241 | 242 | fetchInit.headers = { 243 | ...headers, 244 | ...processHeaders( 245 | // For GET and HEAD, options is moved to body (1st param) 246 | isGetOrHead ? body?.headers : options?.headers, 247 | path, 248 | fetchInit 249 | ) 250 | } 251 | 252 | const fetchOpts = 253 | isGetOrHead && typeof body === 'object' 254 | ? body.fetch 255 | : options?.fetch 256 | 257 | fetchInit = { 258 | ...fetchInit, 259 | ...fetchOpts 260 | } 261 | 262 | if (isGetOrHead) delete fetchInit.body 263 | 264 | if (onRequest) { 265 | if (!Array.isArray(onRequest)) onRequest = [onRequest] 266 | 267 | for (const value of onRequest) { 268 | const temp = await value(path, fetchInit) 269 | 270 | if (typeof temp === 'object') 271 | fetchInit = { 272 | ...fetchInit, 273 | ...temp, 274 | headers: { 275 | ...fetchInit.headers, 276 | ...processHeaders( 277 | temp.headers, 278 | path, 279 | fetchInit 280 | ) 281 | } 282 | } 283 | } 284 | } 285 | 286 | // ? Duplicate because end-user might add a body in onRequest 287 | if (isGetOrHead) delete fetchInit.body 288 | 289 | if (hasFile(body)) { 290 | const formData = new FormData() 291 | 292 | // FormData is 1 level deep 293 | for (const [key, field] of Object.entries( 294 | fetchInit.body 295 | )) { 296 | 297 | if (Array.isArray(field)) { 298 | for (let i = 0; i < field.length; i++) { 299 | const value = (field as any)[i] 300 | 301 | formData.append( 302 | key as any, 303 | value instanceof File 304 | ? await createNewFile(value) 305 | : value 306 | ) 307 | } 308 | 309 | continue 310 | } 311 | 312 | if (isServer) { 313 | formData.append(key, field as any) 314 | 315 | continue 316 | } 317 | 318 | if (field instanceof File) { 319 | formData.append( 320 | key, 321 | await createNewFile(field as any) 322 | ) 323 | 324 | continue 325 | } 326 | 327 | if (field instanceof FileList) { 328 | for (let i = 0; i < field.length; i++) 329 | formData.append( 330 | key as any, 331 | await createNewFile((field as any)[i]) 332 | ) 333 | 334 | continue 335 | } 336 | 337 | formData.append(key, field as string) 338 | } 339 | 340 | // We don't do this because we need to let the browser set the content type with the correct boundary 341 | // fetchInit.headers['content-type'] = 'multipart/form-data' 342 | fetchInit.body = formData 343 | } else if (typeof body === 'object') { 344 | ;(fetchInit.headers as Record)[ 345 | 'content-type' 346 | ] = 'application/json' 347 | 348 | fetchInit.body = JSON.stringify(body) 349 | } else if (body !== undefined && body !== null) { 350 | ;(fetchInit.headers as Record)[ 351 | 'content-type' 352 | ] = 'text/plain' 353 | } 354 | 355 | if (isGetOrHead) delete fetchInit.body 356 | 357 | if (onRequest) { 358 | if (!Array.isArray(onRequest)) onRequest = [onRequest] 359 | 360 | for (const value of onRequest) { 361 | const temp = await value(path, fetchInit) 362 | 363 | if (typeof temp === 'object') 364 | fetchInit = { 365 | ...fetchInit, 366 | ...temp, 367 | headers: { 368 | ...fetchInit.headers, 369 | ...processHeaders( 370 | temp.headers, 371 | path, 372 | fetchInit 373 | ) 374 | } as Record 375 | } 376 | } 377 | } 378 | 379 | const url = domain + path + q 380 | const response = await (elysia?.handle( 381 | new Request(url, fetchInit) 382 | ) ?? fetcher!(url, fetchInit)) 383 | 384 | // @ts-ignore 385 | let data = null 386 | let error = null 387 | 388 | if (onResponse) { 389 | if (!Array.isArray(onResponse)) 390 | onResponse = [onResponse] 391 | 392 | for (const value of onResponse) 393 | try { 394 | const temp = await value(response.clone()) 395 | 396 | if (temp !== undefined && temp !== null) { 397 | data = temp 398 | break 399 | } 400 | } catch (err) { 401 | if (err instanceof EdenFetchError) error = err 402 | else error = new EdenFetchError(422, err) 403 | 404 | break 405 | } 406 | } 407 | 408 | if (data !== null) { 409 | return { 410 | data, 411 | error, 412 | response, 413 | status: response.status, 414 | headers: response.headers 415 | } 416 | } 417 | 418 | switch ( 419 | response.headers.get('Content-Type')?.split(';')[0] 420 | ) { 421 | case 'text/event-stream': 422 | data = streamResponse(response) 423 | break 424 | 425 | case 'application/json': 426 | data = await response.json() 427 | break 428 | case 'application/octet-stream': 429 | data = await response.arrayBuffer() 430 | break 431 | 432 | case 'multipart/form-data': 433 | const temp = await response.formData() 434 | 435 | data = {} 436 | temp.forEach((value, key) => { 437 | // @ts-ignore 438 | data[key] = value 439 | }) 440 | 441 | break 442 | 443 | default: 444 | data = await response 445 | .text() 446 | .then(parseStringifiedValue) 447 | } 448 | 449 | if (response.status >= 300 || response.status < 200) { 450 | error = new EdenFetchError(response.status, data) 451 | data = null 452 | } 453 | 454 | return { 455 | data, 456 | error, 457 | response, 458 | status: response.status, 459 | headers: response.headers 460 | } 461 | })() 462 | } 463 | 464 | if (typeof body === 'object') 465 | return createProxy( 466 | domain, 467 | config, 468 | [...paths, Object.values(body)[0] as string], 469 | elysia 470 | ) 471 | 472 | return createProxy(domain, config, paths) 473 | } 474 | }) as any 475 | 476 | export const treaty = < 477 | const App extends Elysia 478 | >( 479 | domain: string | App, 480 | config: Treaty.Config = {} 481 | ): Treaty.Create => { 482 | if (typeof domain === 'string') { 483 | if (!config.keepDomain) { 484 | if (!domain.includes('://')) 485 | domain = 486 | (locals.find((v) => (domain as string).includes(v)) 487 | ? 'http://' 488 | : 'https://') + domain 489 | 490 | if (domain.endsWith('/')) domain = domain.slice(0, -1) 491 | } 492 | 493 | return createProxy(domain, config) 494 | } 495 | 496 | if (typeof window !== 'undefined') 497 | console.warn( 498 | 'Elysia instance server found on client side, this is not recommended for security reason. Use generic type instead.' 499 | ) 500 | 501 | return createProxy('http://e.ly', config, [], domain) 502 | } 503 | 504 | export type { Treaty } 505 | -------------------------------------------------------------------------------- /src/treaty2/types.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import type { Elysia, ELYSIA_FORM_DATA } from 'elysia' 3 | import { EdenWS } from './ws' 4 | import type { IsNever, Not, Prettify } from '../types' 5 | import { ElysiaFormData } from 'elysia/dist/utils' 6 | 7 | type Files = File | FileList 8 | 9 | // type Replace = { 10 | // [K in keyof RecordType]: RecordType[K] extends TargetType 11 | // ? GenericType 12 | // : RecordType[K] 13 | // } 14 | 15 | type ReplaceBlobWithFiles> = { 16 | [K in keyof RecordType]: RecordType[K] extends Blob | Blob[] 17 | ? Files 18 | : RecordType[K] 19 | } & {} 20 | 21 | type And = A extends true 22 | ? B extends true 23 | ? true 24 | : false 25 | : false 26 | 27 | type ReplaceGeneratorWithAsyncGenerator< 28 | in out RecordType extends Record 29 | > = { 30 | [K in keyof RecordType]: IsNever extends true 31 | ? RecordType[K] 32 | : RecordType[K] extends Generator 33 | ? void extends B 34 | ? AsyncGenerator 35 | : And, void extends B ? false : true> extends true 36 | ? B 37 | : AsyncGenerator | B 38 | : RecordType[K] extends AsyncGenerator 39 | ? And>, void extends B ? true : false> extends true 40 | ? AsyncGenerator 41 | : And, void extends B ? false : true> extends true 42 | ? B 43 | : AsyncGenerator | B 44 | : RecordType[K] 45 | } & {} 46 | 47 | type MaybeArray = T | T[] 48 | type MaybePromise = T | Promise 49 | 50 | export namespace Treaty { 51 | interface TreatyParam { 52 | fetch?: RequestInit 53 | } 54 | 55 | export type Create> = 56 | App extends { 57 | '~Routes': infer Schema extends Record 58 | } 59 | ? Prettify> 60 | : 'Please install Elysia before using Eden' 61 | 62 | export type Sign> = { 63 | [K in keyof Route as K extends `:${string}` 64 | ? never 65 | : K]: K extends 'subscribe' // ? Websocket route 66 | ? (undefined extends Route['subscribe']['headers'] 67 | ? { headers?: Record } 68 | : { 69 | headers: Route['subscribe']['headers'] 70 | }) & 71 | (undefined extends Route['subscribe']['query'] 72 | ? { query?: Record } 73 | : { 74 | query: Route['subscribe']['query'] 75 | }) extends infer Param 76 | ? {} extends Param 77 | ? (options?: Param) => EdenWS 78 | : (options?: Param) => EdenWS 79 | : never 80 | : Route[K] extends { 81 | body: infer Body 82 | headers: infer Headers 83 | params: any 84 | query: infer Query 85 | response: infer Response extends Record 86 | } 87 | ? (undefined extends Headers 88 | ? { headers?: Record } 89 | : { 90 | headers: Headers 91 | }) & 92 | (undefined extends Query 93 | ? { query?: Record } 94 | : { query: Query }) extends infer Param 95 | ? {} extends Param 96 | ? undefined extends Body 97 | ? K extends 'get' | 'head' 98 | ? ( 99 | options?: Prettify 100 | ) => Promise< 101 | TreatyResponse< 102 | ReplaceGeneratorWithAsyncGenerator 103 | > 104 | > 105 | : ( 106 | body?: Body, 107 | options?: Prettify 108 | ) => Promise< 109 | TreatyResponse< 110 | ReplaceGeneratorWithAsyncGenerator 111 | > 112 | > 113 | : ( 114 | body: Body extends Record 115 | ? ReplaceBlobWithFiles 116 | : Body, 117 | options?: Prettify 118 | ) => Promise< 119 | TreatyResponse< 120 | ReplaceGeneratorWithAsyncGenerator 121 | > 122 | > 123 | : K extends 'get' | 'head' 124 | ? ( 125 | options: Prettify 126 | ) => Promise< 127 | TreatyResponse< 128 | ReplaceGeneratorWithAsyncGenerator 129 | > 130 | > 131 | : ( 132 | body: Body extends Record 133 | ? ReplaceBlobWithFiles 134 | : Body, 135 | options: Prettify 136 | ) => Promise< 137 | TreatyResponse< 138 | ReplaceGeneratorWithAsyncGenerator 139 | > 140 | > 141 | : never 142 | : CreateParams 143 | } 144 | 145 | type CreateParams> = 146 | Extract extends infer Path extends string 147 | ? IsNever extends true 148 | ? Prettify> 149 | : // ! DO NOT USE PRETTIFY ON THIS LINE, OTHERWISE FUNCTION CALLING WILL BE OMITTED 150 | (((params: { 151 | [param in Path extends `:${infer Param}` 152 | ? Param extends `${infer Param}?` 153 | ? Param 154 | : Param 155 | : never]: string | number 156 | }) => Prettify> & 157 | CreateParams) & 158 | Prettify>) & 159 | (Path extends `:${string}?` 160 | ? CreateParams 161 | : {}) 162 | : never 163 | 164 | export interface Config { 165 | fetch?: Omit 166 | fetcher?: typeof fetch 167 | headers?: MaybeArray< 168 | | RequestInit['headers'] 169 | | (( 170 | path: string, 171 | options: RequestInit 172 | ) => RequestInit['headers'] | void) 173 | > 174 | onRequest?: MaybeArray< 175 | ( 176 | path: string, 177 | options: RequestInit 178 | ) => MaybePromise 179 | > 180 | onResponse?: MaybeArray<(response: Response) => MaybePromise> 181 | keepDomain?: boolean 182 | } 183 | 184 | // type UnwrapAwaited> = { 185 | // [K in keyof T]: Awaited 186 | // } 187 | 188 | export type TreatyResponse> = 189 | | { 190 | data: Res[200] extends { 191 | [ELYSIA_FORM_DATA]: infer Data 192 | } 193 | ? Data 194 | : Res[200] 195 | error: null 196 | response: Response 197 | status: number 198 | headers: RequestInit['headers'] 199 | } 200 | | { 201 | data: null 202 | error: Exclude extends never 203 | ? { 204 | status: unknown 205 | value: unknown 206 | } 207 | : { 208 | [Status in keyof Res]: { 209 | status: Status 210 | value: Res[Status] extends { 211 | [ELYSIA_FORM_DATA]: infer Data 212 | } 213 | ? Data 214 | : Res[Status] 215 | } 216 | }[Exclude] 217 | response: Response 218 | status: number 219 | headers: RequestInit['headers'] 220 | } 221 | 222 | export interface OnMessage extends MessageEvent { 223 | data: Data 224 | rawData: MessageEvent['data'] 225 | } 226 | 227 | export type WSEvent< 228 | K extends keyof WebSocketEventMap, 229 | Data = unknown 230 | > = K extends 'message' ? OnMessage : WebSocketEventMap[K] 231 | } 232 | -------------------------------------------------------------------------------- /src/treaty2/ws.ts: -------------------------------------------------------------------------------- 1 | import type { InputSchema } from 'elysia' 2 | import type { Treaty } from './types' 3 | import { parseMessageEvent } from '../utils/parsingUtils' 4 | 5 | export class EdenWS = {}> { 6 | ws: WebSocket 7 | 8 | constructor(public url: string) { 9 | this.ws = new WebSocket(url) 10 | } 11 | 12 | send(data: Schema['body'] | Schema['body'][]) { 13 | if (Array.isArray(data)) { 14 | data.forEach((datum) => this.send(datum)) 15 | 16 | return this 17 | } 18 | 19 | this.ws.send( 20 | typeof data === 'object' ? JSON.stringify(data) : data.toString() 21 | ) 22 | 23 | return this 24 | } 25 | 26 | on( 27 | type: K, 28 | listener: (event: Treaty.WSEvent) => void, 29 | options?: boolean | AddEventListenerOptions 30 | ) { 31 | return this.addEventListener(type, listener, options) 32 | } 33 | 34 | off( 35 | type: K, 36 | listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, 37 | options?: boolean | EventListenerOptions 38 | ) { 39 | this.ws.removeEventListener(type, listener, options) 40 | 41 | return this 42 | } 43 | 44 | subscribe( 45 | onMessage: ( 46 | event: Treaty.WSEvent<'message', Schema['response']> 47 | ) => void, 48 | options?: boolean | AddEventListenerOptions 49 | ) { 50 | return this.addEventListener('message', onMessage, options) 51 | } 52 | 53 | addEventListener( 54 | type: K, 55 | listener: (event: Treaty.WSEvent) => void, 56 | options?: boolean | AddEventListenerOptions 57 | ) { 58 | this.ws.addEventListener( 59 | type, 60 | (ws) => { 61 | if (type === 'message') { 62 | const data = parseMessageEvent(ws as MessageEvent) 63 | 64 | listener({ 65 | ...ws, 66 | data 67 | } as any) 68 | } else listener(ws as any) 69 | }, 70 | options 71 | ) 72 | 73 | return this 74 | } 75 | 76 | removeEventListener( 77 | type: K, 78 | listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, 79 | options?: boolean | EventListenerOptions 80 | ) { 81 | this.off(type, listener, options) 82 | 83 | return this 84 | } 85 | 86 | close() { 87 | this.ws.close() 88 | 89 | return this 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { EdenFetchError } from './errors' 2 | 3 | // https://stackoverflow.com/a/39495173 4 | type Range = Exclude< 5 | Enumerate, 6 | Enumerate 7 | > 8 | 9 | type Enumerate< 10 | N extends number, 11 | Acc extends number[] = [] 12 | > = Acc['length'] extends N 13 | ? Acc[number] 14 | : Enumerate 15 | 16 | type ErrorRange = Range<300, 599> 17 | 18 | export type MapError> = [ 19 | { 20 | [K in keyof T]-?: K extends ErrorRange ? K : never 21 | }[keyof T] 22 | ] extends [infer A extends number] 23 | ? { 24 | [K in A]: EdenFetchError 25 | }[A] 26 | : false 27 | 28 | export type UnionToIntersect = ( 29 | U extends any ? (arg: U) => any : never 30 | ) extends (arg: infer I) => void 31 | ? I 32 | : never 33 | 34 | export type UnionToTuple = UnionToIntersect< 35 | T extends any ? (t: T) => T : never 36 | > extends (_: any) => infer W 37 | ? [...UnionToTuple>, W] 38 | : [] 39 | 40 | export type IsAny = 0 extends 1 & T ? true : false 41 | 42 | export type IsNever = [T] extends [never] ? true : false 43 | 44 | export type IsUnknown = IsAny extends true 45 | ? false 46 | : unknown extends T 47 | ? true 48 | : false 49 | 50 | export type AnyTypedRoute = { 51 | body?: unknown 52 | headers?: unknown 53 | query?: unknown 54 | params?: unknown 55 | response: Record 56 | } 57 | 58 | export type Prettify = { 59 | [K in keyof T]: T[K] 60 | } & {} 61 | 62 | export type TreatyToPath = UnionToIntersect< 63 | T extends Record 64 | ? { 65 | [K in keyof T]: T[K] extends AnyTypedRoute 66 | ? { [path in Path]: { [method in K]: T[K] } } 67 | : unknown extends T[K] 68 | ? { [path in Path]: { [method in K]: T[K] } } 69 | : TreatyToPath 70 | }[keyof T] 71 | : {} 72 | > 73 | 74 | export type Not = T extends true ? false : true 75 | -------------------------------------------------------------------------------- /src/utils/parsingUtils.ts: -------------------------------------------------------------------------------- 1 | const isISO8601 = 2 | /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/ 3 | const isFormalDate = 4 | /(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)\s(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{2}\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT(?:\+|-)\d{4}\s\([^)]+\)/ 5 | const isShortenDate = 6 | /^(?:(?:(?:(?:0?[1-9]|[12][0-9]|3[01])[/\s-](?:0?[1-9]|1[0-2])[/\s-](?:19|20)\d{2})|(?:(?:19|20)\d{2}[/\s-](?:0?[1-9]|1[0-2])[/\s-](?:0?[1-9]|[12][0-9]|3[01]))))(?:\s(?:1[012]|0?[1-9]):[0-5][0-9](?::[0-5][0-9])?(?:\s[AP]M)?)?$/ 7 | 8 | export const isNumericString = (message: string) => 9 | message.trim().length !== 0 && !Number.isNaN(Number(message)) 10 | 11 | export const parseStringifiedDate = (value: any) => { 12 | if (typeof value !== 'string') { 13 | return null 14 | } 15 | 16 | // Remove quote from stringified date 17 | const temp = value.replace(/"/g, '') 18 | 19 | if ( 20 | isISO8601.test(temp) || 21 | isFormalDate.test(temp) || 22 | isShortenDate.test(temp) 23 | ) { 24 | const date = new Date(temp) 25 | 26 | if (!Number.isNaN(date.getTime())) { 27 | return date 28 | } 29 | } 30 | 31 | return null 32 | } 33 | 34 | export const isStringifiedObject = (value: string) => { 35 | const start = value.charCodeAt(0) 36 | const end = value.charCodeAt(value.length - 1) 37 | 38 | return (start === 123 && end === 125) || (start === 91 && end === 93) 39 | } 40 | 41 | export const parseStringifiedObject = (data: string) => 42 | JSON.parse(data, (_, value) => { 43 | const date = parseStringifiedDate(value) 44 | 45 | if (date) { 46 | return date 47 | } 48 | 49 | return value 50 | }) 51 | 52 | export const parseStringifiedValue = (value: string) => { 53 | if (!value) { 54 | return value 55 | } 56 | 57 | if (isNumericString(value)) { 58 | return +value 59 | } 60 | 61 | if (value === 'true') { 62 | return true 63 | } 64 | 65 | if (value === 'false') { 66 | return false 67 | } 68 | 69 | const date = parseStringifiedDate(value) 70 | 71 | if (date) { 72 | return date 73 | } 74 | 75 | if (isStringifiedObject(value)) { 76 | try { 77 | return parseStringifiedObject(value) 78 | } catch {} 79 | } 80 | 81 | return value 82 | } 83 | 84 | export const parseMessageEvent = (event: MessageEvent) => { 85 | const messageString = event.data.toString() 86 | 87 | return messageString === 'null' 88 | ? null 89 | : parseStringifiedValue(messageString) 90 | } 91 | -------------------------------------------------------------------------------- /test/fetch.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Elysia, t } from 'elysia' 3 | import { edenFetch } from '../src' 4 | 5 | import { describe, expect, it, beforeAll } from 'bun:test' 6 | 7 | const json = { 8 | name: 'Saori', 9 | affiliation: 'Arius', 10 | type: 'Striker' 11 | } 12 | 13 | const app = new Elysia() 14 | .get('/', () => 'hi') 15 | .post('/', () => 'post') 16 | .get('/json', ({ body }) => json) 17 | .get( 18 | '/json-utf8', 19 | ({ set }) => 20 | new Response(JSON.stringify(json), { 21 | headers: { 22 | 'Content-Type': 'application/json; charset=utf-8' 23 | } 24 | }) 25 | ) 26 | .get('/name/:name', ({ params: { name } }) => name) 27 | .post( 28 | '/headers', 29 | ({ request: { headers } }) => headers.get('x-affiliation'), 30 | { 31 | headers: t.Object({ 32 | 'x-affiliation': t.Literal('Arius') 33 | }) 34 | } 35 | ) 36 | .get('/number', () => 1) 37 | .get('/true', () => true) 38 | .get('/false', () => false) 39 | .get('/throw-error', () => { 40 | throw new Error('hare') 41 | 42 | return 'Hi' 43 | }) 44 | .get( 45 | '/direct-error', 46 | ({ set }) => { 47 | set.status = 500 48 | 49 | return 'hare' 50 | }, 51 | { 52 | response: { 53 | 200: t.String(), 54 | 500: t.Literal('hare') 55 | } 56 | } 57 | ) 58 | .get( 59 | '/with-query', 60 | ({ query }) => { 61 | return { 62 | query 63 | } 64 | }, 65 | { 66 | query: t.Object({ 67 | q: t.String() 68 | }) 69 | } 70 | ) 71 | .get( 72 | '/with-query-undefined', 73 | ({ query }) => { 74 | return { 75 | query 76 | } 77 | }, 78 | { 79 | query: t.Object({ 80 | q: t.Undefined(t.String()) 81 | }) 82 | } 83 | ) 84 | .get( 85 | '/with-query-nullish', 86 | ({ query }) => { 87 | return { 88 | query 89 | } 90 | }, 91 | { 92 | query: t.Object({ 93 | q: t.Nullable(t.String()) 94 | }) 95 | } 96 | ) 97 | .listen(8081) 98 | 99 | const fetch = edenFetch('http://localhost:8081') 100 | 101 | describe('Eden Fetch', () => { 102 | it('get by default', async () => { 103 | const { data } = await fetch('/', {}) 104 | 105 | expect(data).toBe('hi') 106 | }) 107 | 108 | it('post', async () => { 109 | const { data } = await fetch('/', { 110 | method: 'POST' 111 | }) 112 | 113 | expect(data).toBe('post') 114 | }) 115 | 116 | it('parse json', async () => { 117 | const { data } = await fetch('/json', {}) 118 | 119 | expect(data).toEqual(json) 120 | }) 121 | 122 | it('parse json with additional parameters', async () => { 123 | const { data } = await fetch('/json-utf8', {}) 124 | 125 | expect(data).toEqual(json) 126 | }) 127 | 128 | it('send parameters', async () => { 129 | const { data } = await fetch('/name/:name', { 130 | params: { 131 | name: 'Elysia' 132 | } 133 | }) 134 | 135 | expect(data).toEqual('Elysia') 136 | }) 137 | 138 | it('send headers', async () => { 139 | const { data } = await fetch('/headers', { 140 | method: 'POST', 141 | headers: { 142 | 'x-affiliation': 'Arius' 143 | }, 144 | query: {} 145 | }) 146 | 147 | expect(data).toEqual('Arius') 148 | }) 149 | 150 | it('parse number', async () => { 151 | const { data } = await fetch('/number', {}) 152 | 153 | expect(data).toEqual(1) 154 | }) 155 | 156 | it('parse true', async () => { 157 | const { data } = await fetch('/true', {}) 158 | 159 | expect(data).toEqual(true) 160 | }) 161 | 162 | it('parse false', async () => { 163 | const { data } = await fetch('/false', {}) 164 | 165 | expect(data).toEqual(false) 166 | }) 167 | 168 | // ! FIX ME 169 | // it('handle throw error', async () => { 170 | // const { data, error } = await fetch('/throw-error', { 171 | // method: 'GET' 172 | // }) 173 | 174 | // expect(error instanceof Error).toEqual(true) 175 | 176 | // expect(error?.value).toEqual('hare') 177 | // }) 178 | 179 | it('scope down error', async () => { 180 | const { data, error } = await fetch('/direct-error', {}) 181 | 182 | expect(error instanceof Error).toEqual(true) 183 | 184 | if (error) 185 | switch (error.status) { 186 | case 500: 187 | expect(error.value).toEqual('hare') 188 | } 189 | }) 190 | 191 | it('send query', async () => { 192 | const { data, error } = await fetch('/with-query', { 193 | query: { 194 | q: 'A' 195 | } 196 | }) 197 | expect(data?.query.q).toBe('A') 198 | }) 199 | 200 | it('send undefined query', async () => { 201 | const { data, error } = await fetch('/with-query-undefined', { 202 | query: { 203 | q: undefined 204 | } 205 | }) 206 | expect(data?.query.q).toBeUndefined() 207 | expect(error).toBeNull() 208 | }) 209 | 210 | // t.Nullable is impossible to represent with query params 211 | // without elysia specifically parsing 'null' 212 | it('send null query', async () => { 213 | const { data, error } = await fetch('/with-query-nullish', { 214 | query: { 215 | q: null 216 | } 217 | }) 218 | expect(data?.query.q).toBeUndefined() 219 | expect(error?.status).toBe(422) 220 | expect(error?.value.type).toBe("validation") 221 | }) 222 | }) 223 | -------------------------------------------------------------------------------- /test/fn.test.ts: -------------------------------------------------------------------------------- 1 | // import { Elysia } from 'elysia' 2 | // import { describe, expect, it } from 'bun:test' 3 | 4 | // import { fn as efn } from '@elysiajs/fn' 5 | // import { edenFn } from '../src' 6 | 7 | // const app = new Elysia() 8 | // .state('version', 1) 9 | // .decorate('getVersion', () => 1) 10 | // .decorate('mirrorDecorator', (v: T) => v) 11 | // .use((app) => 12 | // efn({ 13 | // app, 14 | // value: ({ getVersion, mirrorDecorator, store: { version } }) => ({ 15 | // ping: () => 'pong', 16 | // mirror: async (value: T) => value, 17 | // version: () => version, 18 | // getVersion, 19 | // mirrorDecorator, 20 | // nested: { 21 | // data() { 22 | // return 'a' 23 | // } 24 | // } 25 | // }) 26 | // }) 27 | // ) 28 | // .use((app) => 29 | // efn({ 30 | // app, 31 | // value: ({ permission }) => ({ 32 | // authorized: permission({ 33 | // value: () => 'authorized', 34 | // check({ request: { headers } }) { 35 | // if (!headers.has('Authorization')) 36 | // throw new Error('Authorization is required') 37 | // } 38 | // }), 39 | // prisma: permission({ 40 | // value: { 41 | // user: { 42 | // create(name: T) { 43 | // return name 44 | // }, 45 | // delete(name: T) { 46 | // return name 47 | // } 48 | // } 49 | // }, 50 | // check({ key, params }) { 51 | // if (key === 'user.delete' && params[0] === 'Arona') 52 | // throw new Error('Forbidden') 53 | // } 54 | // }) 55 | // }) 56 | // }) 57 | // ) 58 | // .listen(8080) 59 | 60 | // await app.modules 61 | 62 | // const fn = edenFn('http://localhost:8080') 63 | 64 | // describe('Eden Fn', () => { 65 | // it('ping', async () => { 66 | // expect(await fn.ping()).toBe('pong') 67 | // }) 68 | 69 | // it('extends SuperJSON', async () => { 70 | // const set = new Set([1, 2, 3]) 71 | 72 | // expect(await fn.mirror(set)).toEqual(set) 73 | // }) 74 | 75 | // it('handle falsey value', async () => { 76 | // expect(await fn.mirror(0)).toEqual(0) 77 | // expect(await fn.mirror(false)).toEqual(false) 78 | // expect(await fn.mirror(null)).toEqual(null) 79 | // }) 80 | 81 | // it('accept accept new headers', async () => { 82 | // const fn$ = fn.$clone({ 83 | // fetch: { 84 | // headers: { 85 | // Authorization: 'Ar1s' 86 | // } 87 | // } 88 | // }) 89 | 90 | // expect(await fn$.authorized()).toEqual('authorized') 91 | // }) 92 | 93 | // it('update config', async () => { 94 | // const fn$ = fn.$clone() 95 | 96 | // await fn$.$set({ 97 | // fetch: { 98 | // headers: { 99 | // Authorization: 'Ar1s' 100 | // } 101 | // } 102 | // }) 103 | 104 | // expect(await fn$.authorized()).toEqual('authorized') 105 | // }) 106 | 107 | // // New Error get thrown in Bun 0.6+ 108 | // // it('handle error', async () => { 109 | // // const valid = fn.mirror(1) 110 | // // const invalid = fn.authorized().catch((err) => { 111 | // // return err 112 | // // }) 113 | 114 | // // expect([await valid, await invalid]).toEqual([ 115 | // // 1, 116 | // // new Error('Authorization is required') 117 | // // ]) 118 | // // }) 119 | 120 | // it('handle concurrent request', async () => { 121 | // const arr = new Array(100).fill(null).map((x, i) => i) 122 | 123 | // expect(await Promise.all(arr.map((x) => fn.mirror(x)))).toEqual(arr) 124 | // }) 125 | 126 | // it('multiple batch', async () => { 127 | // expect(await fn.mirror(0)).toEqual(0) 128 | 129 | // await new Promise((resolve) => setTimeout(resolve, 50)) 130 | 131 | // expect(await fn.mirror(0)).toEqual(0) 132 | // }) 133 | 134 | // it('custom path', async () => { 135 | // const app = new Elysia() 136 | // .use((app) => 137 | // efn({ 138 | // app, 139 | // path: '/custom', 140 | // value: { mirror: async (value: T) => value } 141 | // }) 142 | // ) 143 | // .listen(8081) 144 | 145 | // const fn = edenFn('http://localhost:8081', { 146 | // fn: '/custom' 147 | // }) 148 | 149 | // expect(await fn.mirror(0)).toEqual(0) 150 | // }) 151 | // }) 152 | -------------------------------------------------------------------------------- /test/public/aris-yuzu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/eden/c1fa86868fd195500bacbb84c25c6d19c2de1349/test/public/aris-yuzu.jpg -------------------------------------------------------------------------------- /test/public/kyuukurarin.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/eden/c1fa86868fd195500bacbb84c25c6d19c2de1349/test/public/kyuukurarin.mp4 -------------------------------------------------------------------------------- /test/public/midori.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/eden/c1fa86868fd195500bacbb84c25c6d19c2de1349/test/public/midori.png -------------------------------------------------------------------------------- /test/treaty.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { edenTreaty } from '../src' 3 | 4 | import { beforeEach, describe, expect, it, spyOn, mock } from 'bun:test' 5 | 6 | const fetchSpy = spyOn(global, 'fetch') 7 | 8 | const utf8Json = { hello: 'world' } 9 | 10 | const prefix = 11 | (prefix: Prefix) => 12 | (app: Elysia) => 13 | app.get(`${prefix}/prefixed`, () => 'hi') 14 | 15 | const app = new Elysia() 16 | .get('/', () => 'hi') 17 | .use(prefix('/prefix')) 18 | .post('/', () => 'hi') 19 | .delete('/empty', ({ body }) => ({ body: body ?? null })) 20 | .get( 21 | '/json-utf8', 22 | () => 23 | new Response(JSON.stringify(utf8Json), { 24 | headers: { 25 | 'Content-Type': 'application/json;charset=utf-8' 26 | } 27 | }) 28 | ) 29 | .post('/mirror', ({ body }) => body, { 30 | body: t.Object({ 31 | username: t.String(), 32 | password: t.String() 33 | }) 34 | }) 35 | .post('/deep/nested/mirror', ({ body }) => body, { 36 | body: t.Object({ 37 | username: t.String(), 38 | password: t.String() 39 | }) 40 | }) 41 | .get('/query', ({ query }) => query) 42 | .get('/sign-in', ({ query }) => query) 43 | .group('/v2', (app) => app.guard({}, (app) => app.get('/data', () => 'hi'))) 44 | .get('/number', () => 1) 45 | .get('/true', () => true) 46 | .get('/false', () => false) 47 | .patch('/update', () => 1) 48 | .post('/array', ({ body }) => body, { 49 | body: t.Array(t.String()) 50 | }) 51 | .post('/string', ({ body }) => body, { 52 | body: t.String() 53 | }) 54 | .listen(8082) 55 | 56 | const client = edenTreaty('http://localhost:8082') 57 | 58 | beforeEach(() => { 59 | fetchSpy.mockClear() 60 | }) 61 | 62 | describe('Eden Treaty', () => { 63 | it('get index', async () => { 64 | const { data } = await client.get() 65 | 66 | expect(data).toBe('hi') 67 | }) 68 | 69 | it('post index', async () => { 70 | const { data } = await client.get() 71 | 72 | expect(data).toBe('hi') 73 | }) 74 | 75 | it('post mirror', async () => { 76 | const body = { username: 'A', password: 'B' } 77 | 78 | const { data } = await client.mirror.post(body) 79 | 80 | expect(data).toEqual(body) 81 | }) 82 | 83 | it('get query', async () => { 84 | const $query = { username: 'A', password: 'B' } 85 | 86 | const { data } = await client.query.get({ 87 | $query 88 | }) 89 | 90 | expect(data).toEqual($query) 91 | }) 92 | 93 | it('parse number', async () => { 94 | const { data } = await client.number.get() 95 | 96 | expect(data).toEqual(1) 97 | }) 98 | 99 | it('parse true', async () => { 100 | const { data } = await client.true.get() 101 | 102 | expect(data).toEqual(true) 103 | }) 104 | 105 | it('parse false', async () => { 106 | const { data } = await client.false.get() 107 | 108 | expect(data).toEqual(false) 109 | }) 110 | 111 | it('parse json with extra parameters', async () => { 112 | const { data } = await client['json-utf8'].get() 113 | // @ts-ignore 114 | expect(data).toEqual(utf8Json) 115 | }) 116 | 117 | it('send array', async () => { 118 | const { data } = await client.array.post(['a', 'b', 'c']) 119 | 120 | expect(data).toEqual(['a', 'b', 'c']) 121 | }) 122 | 123 | it('send string', async () => { 124 | const { data } = await client.string.post('hello') 125 | 126 | expect(data).toEqual('hello') 127 | }) 128 | 129 | it('Handle single inline transform', async () => { 130 | let thrown = false 131 | 132 | try { 133 | const { data } = await client.mirror.post({ 134 | username: 'saltyaom', 135 | password: '12345678', 136 | $transform: ({ data }) => { 137 | if (data?.password === '12345678') throw new Error('a') 138 | } 139 | }) 140 | } catch { 141 | thrown = true 142 | } 143 | 144 | expect(thrown).toBe(true) 145 | }) 146 | 147 | it('Handle multiple inline transform', async () => { 148 | let thrown = false 149 | 150 | try { 151 | const { data } = await client.mirror.post({ 152 | username: 'saltyaom', 153 | password: '12345678', 154 | $transform: [ 155 | () => {}, 156 | ({ data }) => { 157 | if (data?.password === '12345678') throw new Error('a') 158 | } 159 | ] 160 | }) 161 | } catch { 162 | thrown = true 163 | } 164 | 165 | expect(thrown).toBe(true) 166 | }) 167 | 168 | it('Handle global transform', async () => { 169 | let thrown = false 170 | 171 | const client = edenTreaty('http://localhost:8082', { 172 | transform: ({ data }) => { 173 | if (data?.password === '12345678') throw new Error('a') 174 | } 175 | }) 176 | 177 | try { 178 | const { data } = await client.mirror.post({ 179 | username: 'saltyaom', 180 | password: '12345678' 181 | }) 182 | } catch { 183 | thrown = true 184 | } 185 | 186 | expect(thrown).toBe(true) 187 | }) 188 | 189 | it('Handle multiple global transform', async () => { 190 | let thrown = false 191 | 192 | const client = edenTreaty('http://localhost:8082', { 193 | transform: [ 194 | () => {}, 195 | ({ data }) => { 196 | if (data?.password === '12345678') throw new Error('a') 197 | } 198 | ] 199 | }) 200 | 201 | try { 202 | const { data } = await client.mirror.post({ 203 | username: 'saltyaom', 204 | password: '12345678' 205 | }) 206 | } catch { 207 | thrown = true 208 | } 209 | 210 | expect(thrown).toBe(true) 211 | }) 212 | 213 | it('Merge inline and global transforms', async () => { 214 | let thrown = false 215 | 216 | const client = edenTreaty('http://localhost:8082', { 217 | transform: [ 218 | () => {}, 219 | ({ data }) => { 220 | if (data?.password === '1234567') throw new Error('a') 221 | } 222 | ] 223 | }) 224 | 225 | try { 226 | const { data } = await client.mirror.post({ 227 | username: 'saltyaom', 228 | password: '12345678', 229 | $transform: [ 230 | () => {}, 231 | ({ data }) => { 232 | if (data?.password === '12345678') throw new Error('a') 233 | } 234 | ] 235 | }) 236 | } catch { 237 | thrown = true 238 | } 239 | 240 | expect(thrown).toBe(true) 241 | }) 242 | 243 | // ? Test for type inference 244 | it('handle group and guard', async () => { 245 | const { data } = await client.v2.data.get() 246 | 247 | expect(data).toEqual('hi') 248 | }) 249 | 250 | // ? Test for type inference 251 | it('strictly type plugin prefix', async () => { 252 | const { data } = await client.prefix.prefixed.get() 253 | 254 | expect(data).toBe('hi') 255 | }) 256 | 257 | // ? Test for request method 258 | it('should always send uppercase as final request method', async () => { 259 | const { data } = await client.update.patch() 260 | 261 | expect(data).toBe(1) 262 | expect( 263 | (fetchSpy.mock.calls[0]! as unknown as [unknown, RequestInit])[1] 264 | .method 265 | ).toBe('PATCH') 266 | }) 267 | 268 | it('send empty body', async () => { 269 | const { data } = await client.empty.delete() 270 | 271 | expect(data).toEqual({ body: null }) 272 | }) 273 | }) 274 | -------------------------------------------------------------------------------- /test/treaty2.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, form, t } from 'elysia' 2 | import { treaty } from '../src' 3 | 4 | import { describe, expect, it, beforeAll, afterAll, mock } from 'bun:test' 5 | 6 | const randomObject = { 7 | a: 'a', 8 | b: 2, 9 | c: true, 10 | d: false, 11 | e: null, 12 | f: new Date(0) 13 | } 14 | const randomArray = [ 15 | 'a', 16 | 2, 17 | true, 18 | false, 19 | null, 20 | new Date(0), 21 | { a: 'a', b: 2, c: true, d: false, e: null, f: new Date(0) } 22 | ] 23 | const websocketPayloads = [ 24 | // strings 25 | 'str', 26 | // numbers 27 | 1, 28 | 1.2, 29 | // booleans 30 | true, 31 | false, 32 | // null values 33 | null, 34 | // A date 35 | new Date(0), 36 | // A random object 37 | randomObject, 38 | // A random array 39 | randomArray 40 | ] as const 41 | 42 | const app = new Elysia() 43 | .get('/', 'a') 44 | .post('/', 'a') 45 | .get('/number', () => 1) 46 | .get('/true', () => true) 47 | .get('/false', () => false) 48 | .post('/array', ({ body }) => body, { 49 | body: t.Array(t.String()) 50 | }) 51 | .post('/mirror', ({ body }) => body) 52 | .post('/body', ({ body }) => body, { 53 | body: t.String() 54 | }) 55 | .delete('/empty', ({ body }) => ({ body: body ?? null })) 56 | .post('/deep/nested/mirror', ({ body }) => body, { 57 | body: t.Object({ 58 | username: t.String(), 59 | password: t.String() 60 | }) 61 | }) 62 | .get('/query', ({ query }) => query, { 63 | query: t.Object({ 64 | username: t.String() 65 | }) 66 | }) 67 | .get('/query-optional', ({ query }) => query, { 68 | query: t.Object({ 69 | username: t.Optional(t.String()) 70 | }) 71 | }) 72 | .get('/query-nullable', ({ query }) => query, { 73 | query: t.Object({ 74 | username: t.Nullable(t.String()) 75 | }) 76 | }) 77 | .get('/queries', ({ query }) => query, { 78 | query: t.Object({ 79 | username: t.String(), 80 | alias: t.Literal('Kristen') 81 | }) 82 | }) 83 | .get('/queries-optional', ({ query }) => query, { 84 | query: t.Object({ 85 | username: t.Optional(t.String()), 86 | alias: t.Literal('Kristen') 87 | }) 88 | }) 89 | .get('/queries-nullable', ({ query }) => query, { 90 | query: t.Object({ 91 | username: t.Nullable(t.Number()), 92 | alias: t.Literal('Kristen') 93 | }) 94 | }) 95 | .post('/queries', ({ query }) => query, { 96 | query: t.Object({ 97 | username: t.String(), 98 | alias: t.Literal('Kristen') 99 | }) 100 | }) 101 | .head('/queries', ({ query }) => query, { 102 | query: t.Object({ 103 | username: t.String(), 104 | alias: t.Literal('Kristen') 105 | }) 106 | }) 107 | .group('/nested', (app) => app.guard((app) => app.get('/data', () => 'hi'))) 108 | .get('/error', ({ error }) => error("I'm a teapot", 'Kirifuji Nagisa'), { 109 | response: { 110 | 200: t.Void(), 111 | 418: t.Literal('Kirifuji Nagisa'), 112 | 420: t.Literal('Snoop Dogg') 113 | } 114 | }) 115 | .get( 116 | '/headers', 117 | ({ headers: { username, alias } }) => ({ username, alias }), 118 | { 119 | headers: t.Object({ 120 | username: t.String(), 121 | alias: t.Literal('Kristen') 122 | }) 123 | } 124 | ) 125 | .post( 126 | '/headers', 127 | ({ headers: { username, alias } }) => ({ username, alias }), 128 | { 129 | headers: t.Object({ 130 | username: t.String(), 131 | alias: t.Literal('Kristen') 132 | }) 133 | } 134 | ) 135 | .get( 136 | '/headers-custom', 137 | ({ headers, headers: { username, alias } }) => ({ 138 | username, 139 | alias, 140 | 'x-custom': headers['x-custom'] 141 | }), 142 | { 143 | headers: t.Object({ 144 | username: t.String(), 145 | alias: t.Literal('Kristen'), 146 | 'x-custom': t.Optional(t.Literal('custom')) 147 | }) 148 | } 149 | ) 150 | .post('/date', ({ body: { date } }) => date, { 151 | body: t.Object({ 152 | date: t.Date() 153 | }) 154 | }) 155 | .get('/dateObject', () => ({ date: new Date() })) 156 | .get('/redirect', ({ redirect }) => redirect('http://localhost:8083/true')) 157 | .post( 158 | '/redirect', 159 | ({ redirect }) => redirect('http://localhost:8083/true'), 160 | { 161 | body: t.Object({ 162 | username: t.String() 163 | }) 164 | } 165 | ) 166 | .get('/formdata', () => 167 | form({ 168 | image: Bun.file('./test/public/kyuukurarin.mp4') 169 | }) 170 | ) 171 | .ws('/json-serialization-deserialization', { 172 | open: async (ws) => { 173 | for (const item of websocketPayloads) { 174 | ws.send(item) 175 | } 176 | ws.close() 177 | } 178 | }) 179 | .get('/stream', function* stream() { 180 | yield 'a' 181 | yield 'b' 182 | yield 'c' 183 | }) 184 | .get('/stream-async', async function* stream() { 185 | yield 'a' 186 | yield 'b' 187 | yield 'c' 188 | }) 189 | .get('/stream-return', function* stream() { 190 | return 'a' 191 | }) 192 | .get('/stream-return-async', function* stream() { 193 | return 'a' 194 | }) 195 | .get('/id/:id?', ({ params: { id = 'unknown' } }) => id) 196 | .post('/files', ({ body: { files } }) => files.map((file) => file.name), { 197 | body: t.Object({ 198 | files: t.Files() 199 | }) 200 | }) 201 | .post('/file', ({ body: { file } }) => file.name, { 202 | body: t.Object({ 203 | file: t.File() 204 | }) 205 | }) 206 | 207 | const client = treaty(app) 208 | 209 | describe('Treaty2', () => { 210 | it('get index', async () => { 211 | const { data, error } = await client.get() 212 | 213 | expect(data).toBe('a') 214 | expect(error).toBeNull() 215 | }) 216 | 217 | it('post index', async () => { 218 | const { data, error } = await client.post() 219 | 220 | expect(data).toBe('a') 221 | expect(error).toBeNull() 222 | }) 223 | 224 | it('parse number', async () => { 225 | const { data } = await client.number.get() 226 | 227 | expect(data).toEqual(1) 228 | }) 229 | 230 | it('parse true', async () => { 231 | const { data } = await client.true.get() 232 | 233 | expect(data).toEqual(true) 234 | }) 235 | 236 | it('parse false', async () => { 237 | const { data } = await client.false.get() 238 | 239 | expect(data).toEqual(false) 240 | }) 241 | 242 | it.todo('parse object with date', async () => { 243 | const { data } = await client.dateObject.get() 244 | 245 | expect(data?.date).toBeInstanceOf(Date) 246 | }) 247 | 248 | it('post array', async () => { 249 | const { data } = await client.array.post(['a', 'b']) 250 | 251 | expect(data).toEqual(['a', 'b']) 252 | }) 253 | 254 | it('post body', async () => { 255 | const { data } = await client.body.post('a') 256 | 257 | expect(data).toEqual('a') 258 | }) 259 | 260 | it('post mirror', async () => { 261 | const body = { username: 'A', password: 'B' } 262 | 263 | const { data } = await client.mirror.post(body) 264 | 265 | expect(data).toEqual(body) 266 | }) 267 | 268 | it('delete empty', async () => { 269 | const { data } = await client.empty.delete() 270 | 271 | expect(data).toEqual({ body: null }) 272 | }) 273 | 274 | it('post deep nested mirror', async () => { 275 | const body = { username: 'A', password: 'B' } 276 | 277 | const { data } = await client.deep.nested.mirror.post(body) 278 | 279 | expect(data).toEqual(body) 280 | }) 281 | 282 | it('get query', async () => { 283 | const query = { username: 'A' } 284 | 285 | const { data } = await client.query.get({ 286 | query 287 | }) 288 | 289 | expect(data).toEqual(query) 290 | }) 291 | 292 | // t.Nullable is impossible to represent with query params 293 | // without elysia specifically parsing 'null' 294 | it('get null query', async () => { 295 | const query = { username: null } 296 | 297 | const { data, error } = await client['query-nullable'].get({ 298 | query 299 | }) 300 | 301 | expect(data).toBeNull() 302 | expect(error?.status).toBe(422) 303 | expect(error?.value.type).toBe('validation') 304 | }) 305 | 306 | it('get optional query', async () => { 307 | const query = { username: undefined } 308 | 309 | const { data } = await client['query-optional'].get({ 310 | query 311 | }) 312 | 313 | expect(data).toEqual({ 314 | username: undefined 315 | }) 316 | }) 317 | 318 | it('get queries', async () => { 319 | const query = { username: 'A', alias: 'Kristen' } as const 320 | 321 | const { data } = await client.queries.get({ 322 | query 323 | }) 324 | 325 | expect(data).toEqual(query) 326 | }) 327 | 328 | it('get optional queries', async () => { 329 | const query = { username: undefined, alias: 'Kristen' } as const 330 | 331 | const { data } = await client['queries-optional'].get({ 332 | query 333 | }) 334 | 335 | expect(data).toEqual({ 336 | username: undefined, 337 | alias: 'Kristen' 338 | }) 339 | }) 340 | 341 | // t.Nullable is impossible to represent with query params 342 | // without elysia specifically parsing 'null' 343 | it('get nullable queries', async () => { 344 | const query = { username: null, alias: 'Kristen' } as const 345 | 346 | const { data, error } = await client['queries-nullable'].get({ 347 | query 348 | }) 349 | 350 | expect(data).toBeNull() 351 | expect(error?.status).toBe(422) 352 | expect(error?.value.type).toBe('validation') 353 | }) 354 | 355 | it('post queries', async () => { 356 | const query = { username: 'A', alias: 'Kristen' } as const 357 | 358 | const { data } = await client.queries.post(null, { 359 | query 360 | }) 361 | 362 | expect(data).toEqual(query) 363 | }) 364 | 365 | it('head queries', async () => { 366 | const query = { username: 'A', alias: 'Kristen' } as const 367 | 368 | const { data } = await client.queries.post(null, { 369 | query 370 | }) 371 | 372 | expect(data).toEqual(query) 373 | }) 374 | 375 | it('get nested data', async () => { 376 | const { data } = await client.nested.data.get() 377 | 378 | expect(data).toEqual('hi') 379 | }) 380 | 381 | it('handle error', async () => { 382 | const { data, error } = await client.error.get() 383 | 384 | let value 385 | 386 | if (error) 387 | switch (error.status) { 388 | case 418: 389 | value = error.value 390 | break 391 | 392 | case 420: 393 | value = error.value 394 | break 395 | } 396 | 397 | expect(data).toBeNull() 398 | expect(value).toEqual('Kirifuji Nagisa') 399 | }) 400 | 401 | it('get headers', async () => { 402 | const headers = { username: 'A', alias: 'Kristen' } as const 403 | 404 | const { data } = await client.headers.get({ 405 | headers 406 | }) 407 | 408 | expect(data).toEqual(headers) 409 | }) 410 | 411 | it('post headers', async () => { 412 | const headers = { username: 'A', alias: 'Kristen' } as const 413 | 414 | const { data } = await client.headers.post(null, { 415 | headers 416 | }) 417 | 418 | expect(data).toEqual(headers) 419 | }) 420 | 421 | it('handle interception', async () => { 422 | const client = treaty(app, { 423 | onRequest(path) { 424 | if (path === '/headers-custom') 425 | return { 426 | headers: { 427 | 'x-custom': 'custom' 428 | } 429 | } 430 | }, 431 | async onResponse(response) { 432 | return { intercepted: true, data: await response.json() } 433 | } 434 | }) 435 | 436 | const headers = { username: 'a', alias: 'Kristen' } as const 437 | 438 | const { data } = await client['headers-custom'].get({ 439 | headers 440 | }) 441 | 442 | expect(data).toEqual({ 443 | // @ts-expect-error 444 | intercepted: true, 445 | data: { 446 | ...headers, 447 | 'x-custom': 'custom' 448 | } 449 | }) 450 | }) 451 | 452 | it('handle interception array', async () => { 453 | const client = treaty(app, { 454 | onRequest: [ 455 | () => ({ 456 | headers: { 457 | 'x-custom': 'a' 458 | } 459 | }), 460 | () => ({ 461 | headers: { 462 | 'x-custom': 'custom' 463 | } 464 | }) 465 | ], 466 | onResponse: [ 467 | () => {}, 468 | async (response) => { 469 | return { intercepted: true, data: await response.json() } 470 | } 471 | ] 472 | }) 473 | 474 | const headers = { username: 'a', alias: 'Kristen' } as const 475 | 476 | const { data } = await client['headers-custom'].get({ 477 | headers 478 | }) 479 | 480 | expect(data).toEqual({ 481 | // @ts-expect-error 482 | intercepted: true, 483 | data: { 484 | ...headers, 485 | 'x-custom': 'custom' 486 | } 487 | }) 488 | }) 489 | 490 | it('accept headers configuration', async () => { 491 | const client = treaty(app, { 492 | headers(path) { 493 | if (path === '/headers-custom') 494 | return { 495 | 'x-custom': 'custom' 496 | } 497 | }, 498 | async onResponse(response) { 499 | return { intercepted: true, data: await response.json() } 500 | } 501 | }) 502 | 503 | const headers = { username: 'a', alias: 'Kristen' } as const 504 | 505 | const { data } = await client['headers-custom'].get({ 506 | headers 507 | }) 508 | 509 | expect(data).toEqual({ 510 | // @ts-expect-error 511 | intercepted: true, 512 | data: { 513 | ...headers, 514 | 'x-custom': 'custom' 515 | } 516 | }) 517 | }) 518 | 519 | it('accept headers configuration array', async () => { 520 | const client = treaty(app, { 521 | headers: [ 522 | (path) => { 523 | if (path === '/headers-custom') 524 | return { 525 | 'x-custom': 'custom' 526 | } 527 | } 528 | ], 529 | async onResponse(response) { 530 | return { intercepted: true, data: await response.json() } 531 | } 532 | }) 533 | 534 | const headers = { username: 'a', alias: 'Kristen' } as const 535 | 536 | const { data } = await client['headers-custom'].get({ 537 | headers 538 | }) 539 | 540 | expect(data).toEqual({ 541 | // @ts-expect-error 542 | intercepted: true, 543 | data: { 544 | ...headers, 545 | 'x-custom': 'custom' 546 | } 547 | }) 548 | }) 549 | 550 | it('send date', async () => { 551 | const { data } = await client.date.post({ date: new Date() }) 552 | 553 | expect(data).toBeInstanceOf(Date) 554 | }) 555 | 556 | it('redirect should set location header', async () => { 557 | const { headers, status } = await client['redirect'].get({ 558 | fetch: { 559 | redirect: 'manual' 560 | } 561 | }) 562 | expect(status).toEqual(302) 563 | expect(new Headers(headers).get('location')).toEqual( 564 | 'http://localhost:8083/true' 565 | ) 566 | }) 567 | 568 | it('generator return stream', async () => { 569 | const a = await client.stream.get() 570 | const result = [] 571 | 572 | for await (const chunk of a.data!) result.push(chunk) 573 | 574 | expect(result).toEqual(['a', 'b', 'c']) 575 | }) 576 | 577 | it('generator return async stream', async () => { 578 | const a = await client['stream-async'].get() 579 | const result = [] 580 | 581 | for await (const chunk of a.data!) result.push(chunk) 582 | 583 | expect(result).toEqual(['a', 'b', 'c']) 584 | }) 585 | 586 | it('generator return value', async () => { 587 | const a = await client['stream-return'].get() 588 | 589 | expect(a.data).toBe('a') 590 | }) 591 | 592 | it('generator return async value', async () => { 593 | const a = await client['stream-return-async'].get() 594 | 595 | expect(a.data).toBe('a') 596 | }) 597 | 598 | it('handle optional params', async () => { 599 | const data = await Promise.all([ 600 | client.id.get(), 601 | client.id({ id: 'salty' }).get() 602 | ]) 603 | expect(data.map((x) => x.data)).toEqual(['unknown', 'salty']) 604 | }) 605 | }) 606 | 607 | describe('Treaty2 - Using endpoint URL', () => { 608 | const treatyApp = treaty('http://localhost:8083') 609 | 610 | beforeAll(async () => { 611 | await new Promise((resolve) => { 612 | app.listen(8083, () => { 613 | resolve(null) 614 | }) 615 | }) 616 | }) 617 | 618 | afterAll(() => { 619 | app.stop() 620 | }) 621 | 622 | it('redirect should set location header', async () => { 623 | const { headers, status } = await treatyApp.redirect.get({ 624 | fetch: { 625 | redirect: 'manual' 626 | } 627 | }) 628 | expect(status).toEqual(302) 629 | expect(new Headers(headers).get('location')).toEqual( 630 | 'http://localhost:8083/true' 631 | ) 632 | }) 633 | 634 | it('redirect should set location header with post', async () => { 635 | const { headers, status } = await treatyApp.redirect.post( 636 | { 637 | username: 'a' 638 | }, 639 | { 640 | fetch: { 641 | redirect: 'manual' 642 | } 643 | } 644 | ) 645 | expect(status).toEqual(302) 646 | expect(new Headers(headers).get('location')).toEqual( 647 | 'http://localhost:8083/true' 648 | ) 649 | }) 650 | 651 | it('get formdata', async () => { 652 | const { data } = await treatyApp.formdata.get() 653 | 654 | expect(data!.image.size).toBeGreaterThan(0) 655 | }) 656 | 657 | it("doesn't encode if it doesn't need to", async () => { 658 | const mockedFetch: any = mock((url: string) => { 659 | return new Response(url) 660 | }) 661 | 662 | const client = treaty('localhost', { fetcher: mockedFetch }) 663 | 664 | const { data } = await client.get({ 665 | query: { 666 | hello: 'world' 667 | } 668 | }) 669 | 670 | expect(data).toEqual('http://localhost/?hello=world' as any) 671 | }) 672 | 673 | it('encodes query parameters if it needs to', async () => { 674 | const mockedFetch: any = mock((url: string) => { 675 | return new Response(url) 676 | }) 677 | 678 | const client = treaty('localhost', { fetcher: mockedFetch }) 679 | 680 | const { data } = await client.get({ 681 | query: { 682 | ['1/2']: '1/2' 683 | } 684 | }) 685 | 686 | expect(data).toEqual('http://localhost/?1%2F2=1%2F2' as any) 687 | }) 688 | 689 | it('accepts and serializes several values for the same query parameter', async () => { 690 | const mockedFetch: any = mock((url: string) => { 691 | return new Response(url) 692 | }) 693 | 694 | const client = treaty('localhost', { fetcher: mockedFetch }) 695 | 696 | const { data } = await client.get({ 697 | query: { 698 | ['1/2']: ['1/2', '1 2'] 699 | } 700 | }) 701 | 702 | expect(data).toEqual('http://localhost/?1%2F2=1%2F2&1%2F2=1%202' as any) 703 | }) 704 | 705 | it('Receives the proper objects back from the other end of the websocket', async (done) => { 706 | app.listen(8080, async () => { 707 | const client = treaty('http://localhost:8080') 708 | 709 | const dataOutOfSocket = await new Promise((res) => { 710 | const data: any = [] 711 | // Wait until we've gotten all the data 712 | const socket = 713 | client['json-serialization-deserialization'].subscribe() 714 | socket.subscribe(({ data: dataItem }) => { 715 | data.push(dataItem) 716 | // Only continue when we got all the messages 717 | if (data.length === websocketPayloads.length) { 718 | res(data) 719 | } 720 | }) 721 | }) 722 | 723 | // expect that everything that came out of the socket 724 | // got deserialized into the same thing that we inteded to send 725 | for (let i = 0; i < websocketPayloads.length; i++) { 726 | expect(dataOutOfSocket[i]).toEqual(websocketPayloads[i]) 727 | } 728 | 729 | done() 730 | }) 731 | }) 732 | }) 733 | 734 | describe('Treaty2 - Using t.File() and t.Files() from server', async () => { 735 | const filePath1 = `${import.meta.dir}/public/aris-yuzu.jpg` 736 | const filePath2 = `${import.meta.dir}/public/midori.png` 737 | const filePath3 = `${import.meta.dir}/public/kyuukurarin.mp4` 738 | 739 | const bunFile1 = Bun.file(filePath1) 740 | const bunFile2 = Bun.file(filePath2) 741 | const bunFile3 = Bun.file(filePath3) 742 | 743 | const file1 = new File([await bunFile1.arrayBuffer()], 'cumin.webp', { 744 | type: 'image/webp' 745 | }) 746 | const file2 = new File([await bunFile2.arrayBuffer()], 'curcuma.jpg', { 747 | type: 'image/jpeg' 748 | }) 749 | const file3 = new File([await bunFile3.arrayBuffer()], 'kyuukurarin.mp4', { 750 | type: 'video/mp4' 751 | }) 752 | 753 | const filesForm = new FormData() 754 | filesForm.append('files', file1) 755 | filesForm.append('files', file2) 756 | filesForm.append('files', file3) 757 | 758 | const bunFilesForm = new FormData() 759 | bunFilesForm.append('files', bunFile1) 760 | bunFilesForm.append('files', bunFile2) 761 | bunFilesForm.append('files', bunFile3) 762 | 763 | it('accept a single Bun.file', async () => { 764 | const { data: files } = await client.files.post({ 765 | files: bunFile1 as unknown as FileList 766 | }) 767 | 768 | expect(files).not.toBeNull() 769 | expect(files).not.toBeUndefined() 770 | expect(files).toEqual([bunFile1.name!]) 771 | 772 | const { data: filesbis } = await client.files.post({ 773 | files: [bunFile1] as unknown as FileList 774 | }) 775 | 776 | expect(filesbis).not.toBeNull() 777 | expect(filesbis).not.toBeUndefined() 778 | expect(filesbis).toEqual([bunFile1.name!]) 779 | 780 | const { data: file } = await client.file.post({ 781 | file: bunFile1 as unknown as File 782 | }) 783 | 784 | expect(file).not.toBeNull() 785 | expect(file).not.toBeUndefined() 786 | expect(file).toEqual(bunFile1.name!) 787 | }) 788 | 789 | it('accept a single regular file', async () => { 790 | const { data: files } = await client.files.post({ 791 | files: file1 as unknown as FileList 792 | }) 793 | 794 | expect(files).not.toBeNull() 795 | expect(files).not.toBeUndefined() 796 | expect(files).toEqual([file1.name!]) 797 | 798 | const { data: filesbis } = await client.files.post({ 799 | files: [file1] as unknown as FileList 800 | }) 801 | 802 | expect(filesbis).not.toBeNull() 803 | expect(filesbis).not.toBeUndefined() 804 | expect(filesbis).toEqual([file1.name!]) 805 | 806 | const { data: file } = await client.file.post({ 807 | file: file1 as unknown as File 808 | }) 809 | 810 | expect(file).not.toBeNull() 811 | expect(file).not.toBeUndefined() 812 | expect(file).toEqual(file1.name!) 813 | }) 814 | 815 | it('accept an array of multiple Bun.file', async () => { 816 | const { data: files } = await client.files.post({ 817 | files: [bunFile1, bunFile2, bunFile3] as unknown as FileList 818 | }) 819 | 820 | expect(files).not.toBeNull() 821 | expect(files).not.toBeUndefined() 822 | expect(files).toEqual([bunFile1.name!, bunFile2.name!, bunFile3.name!]) 823 | 824 | const { data: filesbis } = await client.files.post({ 825 | files: bunFilesForm.getAll('files') as unknown as FileList 826 | }) 827 | 828 | expect(filesbis).not.toBeNull() 829 | expect(filesbis).not.toBeUndefined() 830 | expect(filesbis).toEqual([ 831 | bunFile1.name!, 832 | bunFile2.name!, 833 | bunFile3.name! 834 | ]) 835 | }) 836 | 837 | it('accept an array of multiple regular file', async () => { 838 | const { data: files } = await client.files.post({ 839 | files: [file1, file2, file3] as unknown as FileList 840 | }) 841 | 842 | expect(files).not.toBeNull() 843 | expect(files).not.toBeUndefined() 844 | expect(files).toEqual([file1.name!, file2.name!, file3.name!]) 845 | 846 | const { data: filesbis } = await client.files.post({ 847 | files: filesForm.getAll('files') as unknown as FileList 848 | }) 849 | 850 | expect(filesbis).not.toBeNull() 851 | expect(filesbis).not.toBeUndefined() 852 | expect(filesbis).toEqual([file1.name!, file2.name!, file3.name!]) 853 | }) 854 | }) 855 | -------------------------------------------------------------------------------- /test/types/treaty2.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, file, form, t } from 'elysia' 2 | import { treaty } from '../../src' 3 | import { expectTypeOf } from 'expect-type' 4 | 5 | const plugin = new Elysia({ prefix: '/level' }) 6 | .get('/', '2') 7 | .get('/level', '2') 8 | .get('/:id', ({ params: { id } }) => id) 9 | .get('/:id/ok', ({ params: { id } }) => id) 10 | 11 | const app = new Elysia() 12 | .get('/', 'a') 13 | .post('/', 'a') 14 | .get('/number', () => 1 as const) 15 | .get('/true', () => true) 16 | .post('/array', ({ body }) => body, { 17 | body: t.Array(t.String()) 18 | }) 19 | .post('/mirror', ({ body }) => body) 20 | .post('/body', ({ body }) => body, { 21 | body: t.String() 22 | }) 23 | .post('/deep/nested/mirror', ({ body }) => body, { 24 | body: t.Object({ 25 | username: t.String(), 26 | password: t.String() 27 | }) 28 | }) 29 | .get('/query', ({ query }) => query, { 30 | query: t.Object({ 31 | username: t.String() 32 | }) 33 | }) 34 | .get('/queries', ({ query }) => query, { 35 | query: t.Object({ 36 | username: t.String(), 37 | alias: t.Literal('Kristen') 38 | }) 39 | }) 40 | .post('/queries', ({ query }) => query, { 41 | query: t.Object({ 42 | username: t.String(), 43 | alias: t.Literal('Kristen') 44 | }) 45 | }) 46 | .head('/queries', ({ query }) => query, { 47 | query: t.Object({ 48 | username: t.String(), 49 | alias: t.Literal('Kristen') 50 | }) 51 | }) 52 | .group('/nested', (app) => app.guard((app) => app.get('/data', () => 'hi'))) 53 | .get('/error', ({ error }) => error("I'm a teapot", 'Kirifuji Nagisa'), { 54 | response: { 55 | 200: t.Void(), 56 | 418: t.Literal('Kirifuji Nagisa'), 57 | 420: t.Literal('Snoop Dogg') 58 | } 59 | }) 60 | .get( 61 | '/headers', 62 | ({ headers: { username, alias } }) => ({ username, alias }), 63 | { 64 | headers: t.Object({ 65 | username: t.String(), 66 | alias: t.Literal('Kristen') 67 | }) 68 | } 69 | ) 70 | .post( 71 | '/headers', 72 | ({ headers: { username, alias } }) => ({ username, alias }), 73 | { 74 | headers: t.Object({ 75 | username: t.String(), 76 | alias: t.Literal('Kristen') 77 | }) 78 | } 79 | ) 80 | .get( 81 | '/queries-headers', 82 | ({ headers: { username, alias } }) => ({ username, alias }), 83 | { 84 | query: t.Object({ 85 | username: t.String(), 86 | alias: t.Literal('Kristen') 87 | }), 88 | headers: t.Object({ 89 | username: t.String(), 90 | alias: t.Literal('Kristen') 91 | }) 92 | } 93 | ) 94 | .post( 95 | '/queries-headers', 96 | ({ headers: { username, alias } }) => ({ username, alias }), 97 | { 98 | query: t.Object({ 99 | username: t.String(), 100 | alias: t.Literal('Kristen') 101 | }), 102 | headers: t.Object({ 103 | username: t.String(), 104 | alias: t.Literal('Kristen') 105 | }) 106 | } 107 | ) 108 | .post( 109 | '/body-queries-headers', 110 | ({ headers: { username, alias } }) => ({ username, alias }), 111 | { 112 | body: t.Object({ 113 | username: t.String(), 114 | alias: t.Literal('Kristen') 115 | }), 116 | query: t.Object({ 117 | username: t.String(), 118 | alias: t.Literal('Kristen') 119 | }), 120 | headers: t.Object({ 121 | username: t.String(), 122 | alias: t.Literal('Kristen') 123 | }) 124 | } 125 | ) 126 | .get('/async', async ({ error }) => { 127 | if (Math.random() > 0.5) return error(418, 'Nagisa') 128 | if (Math.random() > 0.5) return error(401, 'Himari') 129 | 130 | return 'Hifumi' 131 | }) 132 | .use(plugin) 133 | 134 | const api = treaty(app) 135 | type api = typeof api 136 | 137 | type Result = T extends (...args: any[]) => infer R 138 | ? Awaited 139 | : never 140 | 141 | type ValidationError = { 142 | data: null 143 | error: { 144 | status: 422 145 | value: { 146 | type: 'validation' 147 | on: string 148 | summary?: string 149 | message?: string 150 | found?: unknown 151 | property?: string 152 | expected?: string 153 | } 154 | } 155 | response: Response 156 | status: number 157 | headers: RequestInit['headers'] 158 | } 159 | 160 | // ? Get should have 1 parameter and is optional when no parameter is defined 161 | { 162 | type Route = api['get'] 163 | 164 | expectTypeOf().parameter(0).toEqualTypeOf< 165 | | { 166 | headers?: Record | undefined 167 | query?: Record | undefined 168 | fetch?: RequestInit | undefined 169 | } 170 | | undefined 171 | >() 172 | 173 | expectTypeOf().parameter(1).toBeUndefined() 174 | 175 | type Res = Result 176 | 177 | expectTypeOf().toEqualTypeOf< 178 | | { 179 | data: 'a' 180 | error: null 181 | response: Response 182 | status: number 183 | headers: HeadersInit | undefined 184 | } 185 | | { 186 | data: null 187 | error: { 188 | status: unknown 189 | value: unknown 190 | } 191 | response: Response 192 | status: number 193 | headers: HeadersInit | undefined 194 | } 195 | >() 196 | } 197 | 198 | // ? Non-get should have 2 parameter and is optional when no parameter is defined 199 | { 200 | type Route = api['post'] 201 | 202 | expectTypeOf().parameter(0).toBeUnknown() 203 | 204 | expectTypeOf().parameter(1).toEqualTypeOf< 205 | | { 206 | headers?: Record | undefined 207 | query?: Record | undefined 208 | fetch?: RequestInit | undefined 209 | } 210 | | undefined 211 | >() 212 | 213 | type Res = Result 214 | 215 | expectTypeOf().toEqualTypeOf< 216 | | { 217 | data: 'a' 218 | error: null 219 | response: Response 220 | status: number 221 | headers: HeadersInit | undefined 222 | } 223 | | { 224 | data: null 225 | error: { 226 | status: unknown 227 | value: unknown 228 | } 229 | response: Response 230 | status: number 231 | headers: HeadersInit | undefined 232 | } 233 | >() 234 | } 235 | 236 | // ? Should return literal 237 | { 238 | type Route = api['number']['get'] 239 | 240 | expectTypeOf().parameter(0).toEqualTypeOf< 241 | | { 242 | headers?: Record | undefined 243 | query?: Record | undefined 244 | fetch?: RequestInit | undefined 245 | } 246 | | undefined 247 | >() 248 | 249 | expectTypeOf().parameter(1).toBeUndefined() 250 | 251 | type Res = Result 252 | 253 | expectTypeOf().toEqualTypeOf< 254 | | { 255 | data: 1 256 | error: null 257 | response: Response 258 | status: number 259 | headers: HeadersInit | undefined 260 | } 261 | | { 262 | data: null 263 | error: { 264 | status: unknown 265 | value: unknown 266 | } 267 | response: Response 268 | status: number 269 | headers: HeadersInit | undefined 270 | } 271 | >() 272 | } 273 | 274 | // ? Should return boolean 275 | { 276 | type Route = api['true']['get'] 277 | 278 | expectTypeOf().parameter(0).toEqualTypeOf< 279 | | { 280 | headers?: Record | undefined 281 | query?: Record | undefined 282 | fetch?: RequestInit | undefined 283 | } 284 | | undefined 285 | >() 286 | 287 | expectTypeOf().parameter(1).toBeUndefined() 288 | 289 | type Res = Result 290 | 291 | expectTypeOf().toEqualTypeOf< 292 | | { 293 | data: boolean 294 | error: null 295 | response: Response 296 | status: number 297 | headers: HeadersInit | undefined 298 | } 299 | | { 300 | data: null 301 | error: { 302 | status: unknown 303 | value: unknown 304 | } 305 | response: Response 306 | status: number 307 | headers: HeadersInit | undefined 308 | } 309 | >() 310 | } 311 | 312 | // ? Should return array of string 313 | { 314 | type Route = api['array']['post'] 315 | 316 | expectTypeOf().parameter(0).toEqualTypeOf() 317 | 318 | expectTypeOf().parameter(1).toEqualTypeOf< 319 | | { 320 | headers?: Record | undefined 321 | query?: Record | undefined 322 | fetch?: RequestInit | undefined 323 | } 324 | | undefined 325 | >() 326 | 327 | type Res = Result 328 | 329 | expectTypeOf().toEqualTypeOf< 330 | | { 331 | data: string[] 332 | error: null 333 | response: Response 334 | status: number 335 | headers: RequestInit['headers'] 336 | } 337 | | ValidationError 338 | >() 339 | } 340 | 341 | // ? Should return body 342 | { 343 | type Route = api['mirror']['post'] 344 | 345 | expectTypeOf().parameter(0).toBeUnknown() 346 | 347 | expectTypeOf().parameter(1).toEqualTypeOf< 348 | | { 349 | headers?: Record | undefined 350 | query?: Record | undefined 351 | fetch?: RequestInit | undefined 352 | } 353 | | undefined 354 | >() 355 | type Res = Result 356 | 357 | expectTypeOf().toEqualTypeOf< 358 | | { 359 | data: unknown 360 | error: null 361 | response: Response 362 | status: number 363 | headers: HeadersInit | undefined 364 | } 365 | | { 366 | data: null 367 | error: { 368 | status: unknown 369 | value: unknown 370 | } 371 | response: Response 372 | status: number 373 | headers: HeadersInit | undefined 374 | } 375 | >() 376 | } 377 | 378 | // ? Should return body 379 | { 380 | type Route = api['body']['post'] 381 | 382 | expectTypeOf().parameter(0).toEqualTypeOf() 383 | 384 | expectTypeOf().parameter(1).toEqualTypeOf< 385 | | { 386 | headers?: Record | undefined 387 | query?: Record | undefined 388 | fetch?: RequestInit | undefined 389 | } 390 | | undefined 391 | >() 392 | type Res = Result 393 | 394 | expectTypeOf().toEqualTypeOf< 395 | | { 396 | data: string 397 | error: null 398 | response: Response 399 | status: number 400 | headers: RequestInit['headers'] 401 | } 402 | | { 403 | data: null 404 | error: { 405 | status: 422 406 | value: { 407 | type: 'validation' 408 | on: string 409 | summary?: string 410 | message?: string 411 | found?: unknown 412 | property?: string 413 | expected?: string 414 | } 415 | } 416 | response: Response 417 | status: number 418 | headers: RequestInit['headers'] 419 | } 420 | >() 421 | } 422 | 423 | // ? Should return body 424 | { 425 | type Route = api['deep']['nested']['mirror']['post'] 426 | 427 | expectTypeOf().parameter(0).toEqualTypeOf<{ 428 | username: string 429 | password: string 430 | }>() 431 | 432 | expectTypeOf().parameter(1).toEqualTypeOf< 433 | | { 434 | headers?: Record | undefined 435 | query?: Record | undefined 436 | fetch?: RequestInit | undefined 437 | } 438 | | undefined 439 | >() 440 | 441 | type Res = Result 442 | 443 | expectTypeOf().toEqualTypeOf< 444 | | { 445 | data: { 446 | username: string 447 | password: string 448 | } 449 | error: null 450 | response: Response 451 | status: number 452 | headers: RequestInit['headers'] 453 | } 454 | | ValidationError 455 | >() 456 | } 457 | 458 | // ? Get should have 1 parameter and is required when query is defined 459 | { 460 | type Route = api['query']['get'] 461 | 462 | expectTypeOf().parameter(0).toEqualTypeOf<{ 463 | headers?: Record | undefined 464 | query: { 465 | username: string 466 | } 467 | fetch?: RequestInit | undefined 468 | }>() 469 | 470 | expectTypeOf().parameter(1).toBeUndefined() 471 | 472 | type Res = Result 473 | 474 | expectTypeOf().toEqualTypeOf< 475 | | { 476 | data: { 477 | username: string 478 | } 479 | error: null 480 | response: Response 481 | status: number 482 | headers: HeadersInit | undefined 483 | } 484 | | ValidationError 485 | >() 486 | } 487 | 488 | // ? Get should have 1 parameter and is required when query is defined 489 | { 490 | type Route = api['queries']['get'] 491 | 492 | expectTypeOf().parameter(0).toEqualTypeOf<{ 493 | headers?: Record | undefined 494 | query: { 495 | username: string 496 | alias: 'Kristen' 497 | } 498 | fetch?: RequestInit | undefined 499 | }>() 500 | 501 | expectTypeOf().parameter(1).toBeUndefined() 502 | 503 | type Res = Result 504 | 505 | expectTypeOf().toEqualTypeOf< 506 | | { 507 | data: { 508 | username: string 509 | alias: 'Kristen' 510 | } 511 | error: null 512 | response: Response 513 | status: number 514 | headers: HeadersInit | undefined 515 | } 516 | | ValidationError 517 | >() 518 | } 519 | 520 | // ? Post should have 2 parameter and is required when query is defined 521 | { 522 | type Route = api['queries']['post'] 523 | 524 | expectTypeOf().parameter(0).toBeUnknown() 525 | 526 | expectTypeOf().parameter(1).toEqualTypeOf<{ 527 | headers?: Record | undefined 528 | query: { 529 | username: string 530 | alias: 'Kristen' 531 | } 532 | fetch?: RequestInit | undefined 533 | }>() 534 | 535 | type Res = Result 536 | 537 | expectTypeOf().toEqualTypeOf< 538 | | { 539 | data: { 540 | username: string 541 | alias: 'Kristen' 542 | } 543 | error: null 544 | response: Response 545 | status: number 546 | headers: HeadersInit | undefined 547 | } 548 | | ValidationError 549 | >() 550 | } 551 | 552 | // ? Head should have 1 parameter and is required when query is defined 553 | { 554 | type Route = api['queries']['head'] 555 | 556 | expectTypeOf().parameter(0).toEqualTypeOf<{ 557 | headers?: Record | undefined 558 | query: { 559 | username: string 560 | alias: 'Kristen' 561 | } 562 | fetch?: RequestInit | undefined 563 | }>() 564 | 565 | expectTypeOf().parameter(1).toBeUndefined() 566 | 567 | type Res = Result 568 | 569 | expectTypeOf().toEqualTypeOf< 570 | | { 571 | data: { 572 | username: string 573 | alias: 'Kristen' 574 | } 575 | error: null 576 | response: Response 577 | status: number 578 | headers: HeadersInit | undefined 579 | } 580 | | ValidationError 581 | >() 582 | } 583 | 584 | // ? Should return error 585 | { 586 | type Route = api['error']['get'] 587 | 588 | expectTypeOf().parameter(0).toEqualTypeOf< 589 | | { 590 | headers?: Record | undefined 591 | query?: Record | undefined 592 | fetch?: RequestInit | undefined 593 | } 594 | | undefined 595 | >() 596 | 597 | expectTypeOf().parameter(1).toBeUndefined() 598 | 599 | type Res = Result 600 | 601 | expectTypeOf().toEqualTypeOf< 602 | | { 603 | data: void 604 | error: null 605 | response: Response 606 | status: number 607 | headers: HeadersInit | undefined 608 | } 609 | | { 610 | data: null 611 | error: 612 | | { 613 | status: 418 614 | value: 'Kirifuji Nagisa' 615 | } 616 | | { 617 | status: 420 618 | value: 'Snoop Dogg' 619 | } 620 | | { 621 | status: 422 622 | value: { 623 | type: 'validation' 624 | on: string 625 | summary?: string 626 | message?: string 627 | found?: unknown 628 | property?: string 629 | expected?: string 630 | } 631 | } 632 | response: Response 633 | status: number 634 | headers: HeadersInit | undefined 635 | } 636 | >() 637 | } 638 | 639 | // ? Get should have 1 parameter and is required when headers is defined 640 | { 641 | type Route = api['headers']['get'] 642 | 643 | expectTypeOf().parameter(0).toEqualTypeOf<{ 644 | headers: { 645 | username: string 646 | alias: 'Kristen' 647 | } 648 | query?: Record | undefined 649 | fetch?: RequestInit | undefined 650 | }>() 651 | 652 | expectTypeOf().parameter(1).toBeUndefined() 653 | 654 | type Res = Result 655 | 656 | expectTypeOf().toEqualTypeOf< 657 | | { 658 | data: { 659 | username: string 660 | alias: 'Kristen' 661 | } 662 | error: null 663 | response: Response 664 | status: number 665 | headers: HeadersInit | undefined 666 | } 667 | | ValidationError 668 | >() 669 | } 670 | 671 | // ? Post should have 2 parameter and is required when headers is defined 672 | { 673 | type Route = api['headers']['post'] 674 | 675 | expectTypeOf().parameter(0).toBeUnknown() 676 | 677 | expectTypeOf().parameter(1).toEqualTypeOf<{ 678 | headers: { 679 | username: string 680 | alias: 'Kristen' 681 | } 682 | query?: Record | undefined 683 | fetch?: RequestInit | undefined 684 | }>() 685 | 686 | type Res = Result 687 | 688 | expectTypeOf().toEqualTypeOf< 689 | | { 690 | data: { 691 | username: string 692 | alias: 'Kristen' 693 | } 694 | error: null 695 | response: Response 696 | status: number 697 | headers: HeadersInit | undefined 698 | } 699 | | ValidationError 700 | >() 701 | } 702 | 703 | // ? Get should have 1 parameter and is required when queries and headers is defined 704 | { 705 | type Route = api['queries-headers']['get'] 706 | 707 | expectTypeOf().parameter(0).toEqualTypeOf<{ 708 | headers: { 709 | username: string 710 | alias: 'Kristen' 711 | } 712 | query: { 713 | username: string 714 | alias: 'Kristen' 715 | } 716 | fetch?: RequestInit | undefined 717 | }>() 718 | 719 | expectTypeOf().parameter(1).toBeUndefined() 720 | 721 | type Res = Result 722 | 723 | expectTypeOf().toEqualTypeOf< 724 | | { 725 | data: { 726 | username: string 727 | alias: 'Kristen' 728 | } 729 | error: null 730 | response: Response 731 | status: number 732 | headers: HeadersInit | undefined 733 | } 734 | | ValidationError 735 | >() 736 | } 737 | 738 | // ? Post should have 2 parameter and is required when queries and headers is defined 739 | { 740 | type Route = api['queries-headers']['post'] 741 | 742 | expectTypeOf().parameter(0).toBeUnknown() 743 | 744 | expectTypeOf().parameter(1).toEqualTypeOf<{ 745 | headers: { 746 | username: string 747 | alias: 'Kristen' 748 | } 749 | query: { 750 | username: string 751 | alias: 'Kristen' 752 | } 753 | fetch?: RequestInit | undefined 754 | }>() 755 | 756 | type Res = Result 757 | 758 | expectTypeOf().toEqualTypeOf< 759 | | { 760 | data: { 761 | username: string 762 | alias: 'Kristen' 763 | } 764 | error: null 765 | response: Response 766 | status: number 767 | headers: HeadersInit | undefined 768 | } 769 | | ValidationError 770 | >() 771 | } 772 | 773 | // ? Post should have 2 parameter and is required when queries, headers and body is defined 774 | { 775 | type Route = api['body-queries-headers']['post'] 776 | 777 | expectTypeOf().parameter(0).toEqualTypeOf<{ 778 | username: string 779 | alias: 'Kristen' 780 | }>() 781 | 782 | expectTypeOf().parameter(1).toEqualTypeOf<{ 783 | headers: { 784 | username: string 785 | alias: 'Kristen' 786 | } 787 | query: { 788 | username: string 789 | alias: 'Kristen' 790 | } 791 | fetch?: RequestInit | undefined 792 | }>() 793 | 794 | type Res = Result 795 | 796 | expectTypeOf().toEqualTypeOf< 797 | | { 798 | data: { 799 | username: string 800 | alias: 'Kristen' 801 | } 802 | error: null 803 | response: Response 804 | status: number 805 | headers: HeadersInit | undefined 806 | } 807 | | ValidationError 808 | >() 809 | } 810 | 811 | // ? Should handle async 812 | { 813 | type Route = api['async']['get'] 814 | 815 | expectTypeOf().parameter(0).toEqualTypeOf< 816 | | { 817 | headers?: Record | undefined 818 | query?: Record | undefined 819 | fetch?: RequestInit | undefined 820 | } 821 | | undefined 822 | >() 823 | 824 | expectTypeOf().parameter(1).toBeUndefined() 825 | 826 | type Res = Result 827 | 828 | expectTypeOf().toEqualTypeOf< 829 | | { 830 | data: 'Hifumi' 831 | error: null 832 | response: Response 833 | status: number 834 | headers: HeadersInit | undefined 835 | } 836 | | { 837 | data: null 838 | error: 839 | | { 840 | status: 401 841 | value: 'Himari' 842 | } 843 | | { 844 | status: 418 845 | value: 'Nagisa' 846 | } 847 | response: Response 848 | status: number 849 | headers: HeadersInit | undefined 850 | } 851 | >() 852 | } 853 | 854 | // ? Handle param with nested path 855 | { 856 | type SubModule = api['level'] 857 | 858 | // expectTypeOf().toEqualTypeOf< 859 | // ((params: { id: string | number }) => { 860 | // get: ( 861 | // options?: 862 | // | { 863 | // headers?: Record | undefined 864 | // query?: Record | undefined 865 | // fetch?: RequestInit | undefined 866 | // } 867 | // | undefined 868 | // ) => Promise< 869 | // | { 870 | // data: string 871 | // error: null 872 | // response: Response 873 | // status: number 874 | // headers: HeadersInit | undefined 875 | // } 876 | // | ValidationError 877 | // > 878 | // ok: { 879 | // get: ( 880 | // options?: 881 | // | { 882 | // headers?: Record | undefined 883 | // query?: Record | undefined 884 | // fetch?: RequestInit | undefined 885 | // } 886 | // | undefined 887 | // ) => Promise< 888 | // | { 889 | // data: string 890 | // error: null 891 | // response: Response 892 | // status: number 893 | // headers: HeadersInit | undefined 894 | // } 895 | // | ValidationError 896 | // > 897 | // } 898 | // }) & { 899 | // index: { 900 | // get: ( 901 | // options?: 902 | // | { 903 | // headers?: Record | undefined 904 | // query?: Record | undefined 905 | // fetch?: RequestInit | undefined 906 | // } 907 | // | undefined 908 | // ) => Promise< 909 | // | { 910 | // data: '2' 911 | // error: null 912 | // response: Response 913 | // status: number 914 | // headers: HeadersInit | undefined 915 | // } 916 | // | ValidationError 917 | // > 918 | // } 919 | // level: { 920 | // get: ( 921 | // options?: 922 | // | { 923 | // headers?: Record | undefined 924 | // query?: Record | undefined 925 | // fetch?: RequestInit | undefined 926 | // } 927 | // | undefined 928 | // ) => Promise< 929 | // | { 930 | // data: '2' 931 | // error: null 932 | // response: Response 933 | // status: number 934 | // headers: HeadersInit | undefined 935 | // } 936 | // | ValidationError 937 | // > 938 | // } 939 | // } 940 | // > 941 | } 942 | 943 | // ? Return AsyncGenerator on yield 944 | { 945 | const app = new Elysia().get('', function* () { 946 | yield 1 947 | yield 2 948 | yield 3 949 | }) 950 | 951 | const { data } = await treaty(app).get() 952 | 953 | expectTypeOf().toEqualTypeOf | null>() 958 | } 959 | 960 | // ? Return actual value on generator if not yield 961 | { 962 | const app = new Elysia().get('', function* () { 963 | return 'a' 964 | }) 965 | 966 | const { data } = await treaty(app).get() 967 | 968 | expectTypeOf().toEqualTypeOf() 969 | } 970 | 971 | // ? Return both actual value and generator if yield and return 972 | { 973 | const app = new Elysia().get('', function* () { 974 | if (Math.random() > 0.5) return 'a' 975 | 976 | yield 1 977 | yield 2 978 | yield 3 979 | }) 980 | 981 | const { data } = await treaty(app).get() 982 | 983 | expectTypeOf().toEqualTypeOf< 984 | | 'a' 985 | | AsyncGenerator<1 | 2 | 3, 'a' | undefined, unknown> 986 | | null 987 | | undefined 988 | >() 989 | } 990 | 991 | // ? Return AsyncGenerator on yield 992 | { 993 | const app = new Elysia().get('', async function* () { 994 | yield 1 995 | yield 2 996 | yield 3 997 | }) 998 | 999 | const { data } = await treaty(app).get() 1000 | 1001 | expectTypeOf().toEqualTypeOf | null>() 1006 | } 1007 | 1008 | // ? Return actual value on generator if not yield 1009 | { 1010 | const app = new Elysia().get('', async function* () { 1011 | return 'a' 1012 | }) 1013 | 1014 | const { data } = await treaty(app).get() 1015 | 1016 | expectTypeOf().toEqualTypeOf() 1017 | } 1018 | 1019 | // ? Return both actual value and generator if yield and return 1020 | { 1021 | const app = new Elysia().get('', async function* () { 1022 | if (Math.random() > 0.5) return 'a' 1023 | 1024 | yield 1 1025 | yield 2 1026 | yield 3 1027 | }) 1028 | 1029 | const { data } = await treaty(app).get() 1030 | 1031 | expectTypeOf().toEqualTypeOf< 1032 | | 'a' 1033 | | AsyncGenerator<1 | 2 | 3, 'a' | undefined, unknown> 1034 | | null 1035 | | undefined 1036 | >() 1037 | } 1038 | 1039 | { 1040 | const app = new Elysia().get('/formdata', () => 1041 | form({ 1042 | image: file('/') 1043 | }) 1044 | ) 1045 | 1046 | const { data } = await treaty(app).formdata.get() 1047 | 1048 | expectTypeOf(data!.image).toEqualTypeOf() 1049 | } 1050 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "lib": ["ESNext", "dom"], 5 | "outDir": "./dist", 6 | "sourceMap": true, 7 | "noImplicitAny": true, 8 | "module": "ES2022", 9 | "target": "es2022", 10 | "moduleResolution": "node", 11 | "jsx": "react", 12 | "skipLibCheck": true 13 | }, 14 | "include": ["./src/**/*"], 15 | "exclude": ["test/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "preserveSymlinks": true, 4 | /* Visit https://aka.ms/tsconfig to read more about this file */ 5 | 6 | /* Projects */ 7 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 8 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 9 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 10 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 11 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 12 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 13 | 14 | /* Language and Environment */ 15 | "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 16 | "lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 27 | 28 | /* Modules */ 29 | "module": "ES2022", /* Specify what module code is generated. */ 30 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 31 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 32 | // "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 33 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 35 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 36 | // "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 39 | // "resolveJsonModule": true, /* Enable importing .json files. */ 40 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 41 | 42 | /* JavaScript Support */ 43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 46 | 47 | /* Emit */ 48 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 49 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 50 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 51 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 53 | // "outDir": "./dist", /* Specify an output folder for all emitted files. */ 54 | // "removeComments": true, /* Disable emitting comments. */ 55 | "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // r"sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 63 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 64 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 65 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 66 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 67 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 68 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 69 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 70 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 71 | 72 | /* Interop Constraints */ 73 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 74 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 75 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 76 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 77 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 78 | 79 | /* Type Checking */ 80 | "strict": true, /* Enable all strict type-checking options. */ 81 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 82 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 83 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 84 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 85 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 86 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 87 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 88 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 89 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 90 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 91 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 92 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 93 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 94 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 95 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 96 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 97 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 98 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 99 | 100 | /* Completeness */ 101 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 102 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 103 | }, 104 | "exclude": ["node_modules"], 105 | "include": ["test/types/**/*"] 106 | } 107 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | import { resolve } from 'path' 3 | import { execSync } from 'child_process' 4 | 5 | export default defineConfig({ 6 | entry: { 7 | index: resolve(__dirname, 'src/index.ts'), 8 | treaty: resolve(__dirname, 'src/treaty/index.ts'), 9 | treaty2: resolve(__dirname, 'src/treaty2/index.ts'), 10 | fetch: resolve(__dirname, 'src/fetch/index.ts') 11 | }, 12 | format: ['cjs', 'esm', 'iife'], 13 | globalName: 'Eden', 14 | minify: true, 15 | external: ['elysia'], 16 | dts: true, 17 | async onSuccess() { 18 | execSync('tsc --emitDeclarationOnly --declaration', { 19 | cwd: resolve(__dirname, 'dist') 20 | }) 21 | } 22 | }) 23 | --------------------------------------------------------------------------------