├── .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 │ ├── 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.4.1 - 14 Sep 2025 2 | Bug fix: 3 | - inline object value / Elysia file cause type error 4 | - macro should not mark property as required 5 | 6 | # 1.4.0 - 13 Sep 2025 7 | Improvement: 8 | - support Elysia 1.4 9 | 10 | # 1.3.3 - 24 Aug 2025 11 | Feature: 12 | - treaty2: support type safe Server-Sent Events (SSE) 13 | - treaty2: add utility type `Treaty.Data`, `Treaty.Error` to extract data and error type from a route 14 | 15 | Bug fix: 16 | - [elysia#823](https://github.com/elysiajs/elysia/issues/823) treaty2: not generating for dynamic params at root 17 | - treaty2: parse Date in object 18 | - treaty2: [#196](https://github.com/elysiajs/eden/issues/196) allow custom content-type 19 | 20 | Change: 21 | - minimum Elysia version is set to 1.3.18 22 | 23 | # 1.3.2 - 5 May 2025 24 | Bug fix: 25 | - Unwrap FormData 26 | 27 | # 1.3.1 - 5 May 2025 28 | Bug fix: 29 | - [#193](https://github.com/elysiajs/eden/pull/193) t.Files() upload from server side #124 30 | - [#185](https://github.com/elysiajs/eden/pull/185) exclude null-ish values from query encoding by @ShuviSchwarze 31 | 32 | # 1.3.0 - 5 May 2025 33 | Feature: 34 | - support Elysia 1.3 35 | 36 | Breaking Change: 37 | - treaty2: drop the need for `.index()` 38 | 39 | # 1.2.0 - 23 Dec 2024 40 | Feature: 41 | - support Elysia 1.2 42 | - Validation error inference 43 | 44 | # 1.1.3 - 5 Sep 2024 45 | Feature: 46 | - add provenance publish 47 | 48 | # 1.1.2 - 25 Jul 2024 49 | Feature: 50 | - [#115](https://github.com/elysiajs/eden/pull/115) Stringify query params to allow the nested object 51 | 52 | # 1.1.1 - 17 Jul 2024 53 | Feature: 54 | - support conditional value or stream from generator function 55 | 56 | # 1.1.0 - 16 Jul 2024 57 | Feature: 58 | - support Elysia 1.1 59 | - support response streaming with type inference 60 | 61 | # 1.0.15 - 21 Jun 2024 62 | Bug fix: 63 | - [#105](https://github.com/elysiajs/eden/pull/105) Unify parsing into a util and add support for Date inside object 64 | - [#100](https://github.com/elysiajs/eden/issues/100) Duplicated values when passing uppercase values for headers 65 | 66 | # 1.0.14 - 23 May 2024 67 | Feature: 68 | - treaty2: add support for multipart/form-data 69 | - fetch: add support for multipart/form-data 70 | 71 | # 1.0.13 - 8 May 2024 72 | Bug fix: 73 | - [#87](https://github.com/elysiajs/eden/pull/87) serialization/deserialization problems with null, arrays and Date on websocket messages #87 74 | - [#90](https://github.com/elysiajs/eden/pull/90) Auto convert request body to FormData when needed 75 | 76 | # 1.0.12 - 23 Apr 2024 77 | Improvement: 78 | - [#80](https://github.com/elysiajs/eden/pull/80) add package.json to export field 79 | 80 | Bug fix: 81 | - [#84](https://github.com/elysiajs/eden/pull/84) treaty2: adjusts the creation of the query string 82 | - [#76](https://github.com/elysiajs/eden/pull/76) treaty2: keep requestInit for redirect 83 | - [#73](https://github.com/elysiajs/eden/pull/73) treaty2: file get sent as json 84 | 85 | # 1.0.11 - 3 Apr 2024 86 | Improvement: 87 | - treaty2: add dedicated `processHeaders` function 88 | - treaty2: simplify some headers workflow 89 | 90 | Change: 91 | - treaty2: using case-insensitive headers 92 | 93 | # 1.0.10 - 3 Apr 2024 94 | Bug fix: 95 | - treaty2: skip content-type detection if provided 96 | 97 | # 1.0.9 - 3 Apr 2024 98 | Change: 99 | - treaty2: `onRequest` execute before body serialization 100 | 101 | # 1.0.8 - 28 Mar 2024 102 | Bug fix: 103 | - [#72](https://github.com/elysiajs/eden/pulls/72) treaty2: not mutate original paths array 104 | 105 | # 1.0.6 - 21 Mar 2024 106 | Bug fix: 107 | - treaty2: default onResponse to null and mutate when need 108 | 109 | # 1.0.6 - 21 Mar 2024 110 | Change: 111 | - treaty2: use null as default error value instead of undefined 112 | 113 | # 1.0.5 - 20 Mar 2024 114 | Feature: 115 | treaty2: add `keepDomain` option 116 | 117 | Change: 118 | - treaty2: use null as default data value instead of undefined 119 | 120 | Fix: 121 | - treaty2: aligned schema with elysia/ws 122 | - treaty: use uppercase http verbs 123 | 124 | # 1.0.4 - 18 Mar 2024 125 | Bug fix: 126 | - Using regex for date pattern matching 127 | 128 | # 1.0.2 - 18 Mar 2024 129 | Feature: 130 | - support for elysia 1.0.2 131 | 132 | # 1.0.0 - 16 Mar 2024 133 | Feature: 134 | - treaty2 135 | 136 | Bug fix: 137 | - treaty1: fix [object Object] when body is empty 138 | 139 | # 0.8.1 - 8 Jan 2024 140 | Bug fix: 141 | - [#41](https://github.com/elysiajs/eden/pull/41) params now optional for paramless routes in edenFetch 142 | - [#39](https://github.com/elysiajs/eden/pull/39) transform entire object returned by execute 143 | 144 | # 0.8.0 - 23 Dec 2023 145 | Feature: 146 | - Support Elysia 0.8 147 | 148 | # 0.7.7 - 15 Dec 2023 149 | Bug fix: 150 | - treaty: [#36](https://github.com/elysiajs/eden/pull/36) FileArray send [ object Promise ] instead binary 151 | - treaty: [#34](https://github.com/elysiajs/eden/pull/34) Add a way to get the unresolved Response for corner case support 152 | 153 | # 0.7.6 - 6 Dec 2023 154 | Feature: 155 | - treaty: add 2nd optional parameter for sending query, header and fetch 156 | 157 | Bug fix: 158 | - treaty: send array and primitive value [#33](https://github.com/elysiajs/eden/issues/33) 159 | - treaty: fix filename in FormData [#26](https://github.com/elysiajs/eden/pull/26) 160 | - remove 'bun-types' from treaty 161 | 162 | # 0.7.5 - 23 Oct 2023 163 | Bug fix: 164 | - treaty: handle `File[]` 165 | 166 | # 0.7.4 - 29 Sep 2023 167 | Feature: 168 | - [#16](https://github.com/elysiajs/eden/issues/16) add transform 169 | 170 | # 0.7.3 - 27 Sep 2023 171 | Bug fix: 172 | - using uppercase method name because of Cloudflare proxy 173 | 174 | # 0.7.2 - 22 Sep 2023 175 | Bug fix: 176 | - resolve 'FileList' type 177 | - using rimraf to clear previous build 178 | - fix edenTreaty type is undefined when using moduleResolution: bundler 179 | 180 | # 0.7.1 - 21 Sep 2023 181 | Bug fix: 182 | - type panic when `Definitions` is provided 183 | 184 | # 0.7.0 - 20 Sep 2023 185 | Improvement: 186 | - update to Elysia 0.7.0 187 | 188 | Change: 189 | - remove Elysia Fn 190 | 191 | # 0.6.5 - 12 Sep 2023 192 | Bug fix: 193 | - [#15](https://github.com/elysiajs/eden/pull/15) fetch: method inference on route with different methods 194 | - [#17](https://github.com/elysiajs/eden/issues/17) treaty: api.get() maps to request GET /ge instead of / 195 | 196 | # 0.6.4 - 28 Aug 2023 197 | Change: 198 | - use tsup to bundle 199 | 200 | # 0.6.3 - 28 Aug 2023 201 | Feature: 202 | - add query to Eden Fetch thanks to [#10](https://github.com/elysiajs/eden/pull/10) 203 | 204 | # 0.6.2 - 26 Aug 2023 205 | Feature: 206 | - add the `$fetch` parameters to Eden Treaty 207 | - add the following to response: 208 | - status - indicating status code 209 | - raw - Response 210 | - headers - Response's headers 211 | 212 | Improvement: 213 | - rewrite Eden type to New Eden 214 | - LoC reduced by ~35% 215 | - Faster type inference ~26% 216 | 217 | # 0.6.1 - 17 Aug 2023 218 | Feature: 219 | - add support for Elysia 0.6.7 220 | 221 | # 0.6.0 - 6 Aug 2023 222 | Feature: 223 | - add support for Elysia 0.6 224 | 225 | # 0.5.6 - 10 Jun 2023 226 | Improvement: 227 | - treaty: Add custom fetch implementation for treaty 228 | 229 | # 0.5.5 - 10 Jun 2023 230 | Improvement: 231 | - treaty: Automatic unwrap `Promise` response 232 | 233 | Bug fix: 234 | - treaty: query schema is missing 235 | 236 | # 0.5.4 - 9 Jun 2023 237 | Improvement: 238 | - Awaited response data 239 | 240 | # 0.5.3 - 25 May 2023 241 | Improvement: 242 | - treaty: add support for Bun file uploading 243 | 244 | # 0.5.2 - 25 May 2023 245 | Improvement: 246 | - add types declaration to import map 247 | 248 | Bug fix: 249 | - add tsc to generate .d.ts 250 | 251 | # 0.5.1 - 25 May 2023 252 | Bug fix: 253 | - treaty: type not found 254 | - treaty: query not infers type 255 | 256 | # 0.5.0 - 15 May 2023 257 | Improvement: 258 | - Add support for Elysia 0.5 259 | 260 | # 0.4.1 - 1 April 2023 261 | Improvement: 262 | - Sometime query isn't required 263 | 264 | # 0.4.0 - 30 Mar 2023 265 | Improvement: 266 | - Add support for Elysia 0.4 267 | 268 | # 0.3.2 - 20 Mar 2023 269 | Improvement: 270 | - File upload support for Eden Treaty 271 | 272 | # 0.3.1 - 20 Mar 2023 273 | Improvement: 274 | - Path parameter inference 275 | 276 | # 0.3.0 - 17 Mar 2023 277 | Improvement: 278 | - Add support for Elysia 0.3.0 279 | 280 | # 0.3.0-rc.2 - 17 Mar 2023 281 | Breaking Change: 282 | - Eden Fetch error handling use object destructuring, migration as same as Eden Treaty (0.3.0-rc.1) 283 | 284 | # 0.3.0-rc.1 - 16 Mar 2023 285 | Improvement: 286 | - Update @sinclair/typebox to 0.25.24 287 | 288 | Breaking Change: 289 | - Eden Treaty error handling use object destructuring 290 | - To migrate: 291 | ```ts 292 | // to 293 | const anya = await client.products.nendoroid['1902'].put({ 294 | name: 'Anya Forger' 295 | }) 296 | 297 | // From 298 | const { data: anya, error } = await client.products.nendoroid['1902'].put({ 299 | name: 'Anya Forger' 300 | }) 301 | ``` 302 | 303 | # 0.3.0-rc.0 - 7 Mar 2023 304 | Improvement: 305 | - Add support for Elysia 0.3.0-rc.0 306 | 307 | # 0.3.0-beta.4 - 4 Mar 2023 308 | Improvement: 309 | - Separate Eden type 310 | - Rewrite Eden Treaty 311 | 312 | # 0.3.0-beta.3 - 1 Mar 2023 313 | Improvement: 314 | - Add support for Elysia 0.3.0-beta.5 315 | 316 | # 0.3.0-beta.2 - 28 Feb 2023 317 | Improvement: 318 | - Optimize type inference 319 | 320 | # 0.3.0-beta.1 - 27 Feb 2023 321 | Improvement: 322 | - Add TypeBox as peer dependencies 323 | - Minimum support for Elysia >= 0.3.0-beta.3 324 | 325 | # 0.3.0-beta.0 - 25 Feb 2023 326 | Fix: 327 | - Eden doesn't transform path reference 328 | 329 | # 0.2.1 - 27 Jan 2023 330 | Feature: 331 | - Elysia Fn 332 | - Error type inference 333 | 334 | Breaking Change: 335 | - Error type inference 336 | 337 | # 0.2.0-rc.9 - 27 Jan 2023 338 | Improvement: 339 | - Add params name for warning instead of `$params` 340 | - Add warning to install Elysia before using Eden 341 | 342 | # 0.2.0-rc.8 - 24 Jan 2023 343 | Fix: 344 | - Add support for Elysia WS which support Elysia 0.2.0-rc.1 345 | 346 | # 0.2.0-rc.7 - 24 Jan 2023 347 | Fix: 348 | - Resolve Elysia 0.2.0-rc.1 type 349 | 350 | # 0.2.0-rc.6 - 24 Jan 2023 351 | Improvement: 352 | - Set minimum Elysia version to 0.2.0-rc.1 353 | 354 | # 0.2.0-rc.5 - 19 Jan 2023 355 | Improvement: 356 | - Handle application/json with custom encoding 357 | 358 | # 0.2.0-rc.4 - 19 Jan 2023 359 | Change: 360 | - It's now required to specified specific version to use elysia 361 | 362 | # 0.2.0-rc.3 - 7 Jan 2023 363 | Improvement: 364 | - Add `$params` to indicate any string params 365 | 366 | Bug fix: 367 | - Sometime Eden doesn't infer returned type 368 | 369 | # 0.2.0-rc.2 - 5 Jan 2023 370 | Improvement: 371 | - Auto switch between `ws` and `wss` 372 | 373 | # 0.2.0-rc.1 - 4 Jan 2023 374 | Breaking Change: 375 | - Change HTTP verb to lowercase 376 | 377 | Feature: 378 | - Support multiple path parameters 379 | 380 | Bug fix: 381 | - Required query in `subscribe` 382 | - Make `unknown` type optional 383 | - Add support for non-object fetch 384 | 385 | # 0.2.0-rc.0 - 3 Jan 2023 386 | Feature: 387 | - Experimental support for Web Socket 388 | 389 | # 0.1.0-rc.6 - 16 Dec 2022 390 | Feature: 391 | - Auto convert number, boolean on client 392 | 393 | # 0.1.0-rc.4 - 16 Dec 2022 394 | Feature: 395 | - Using `vite` for bundling 396 | 397 | # 0.1.0-rc.3 - 13 Dec 2022 398 | Bug fix: 399 | - Map error to `.catch` 400 | 401 | # 0.1.0-rc.2 - 13 Dec 2022 402 | Feature: 403 | - Add camelCase transformation 404 | -------------------------------------------------------------------------------- /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 authMacro = new Elysia().macro({ 5 | auth: { 6 | async resolve() { 7 | return { newProperty: 'Macro added property' } 8 | } 9 | } 10 | }) 11 | 12 | const routerWithMacro = new Elysia() 13 | .use(authMacro) 14 | .get('/bug', 'Problem', { auth: true }) 15 | 16 | const routerWithoutMacro = new Elysia().get('/noBug', 'No Problem') 17 | 18 | const app = new Elysia().use(routerWithMacro).use(routerWithoutMacro) 19 | 20 | const api = treaty('localhost:3000') 21 | 22 | api.noBug.get() 23 | 24 | api.bug.get() 25 | -------------------------------------------------------------------------------- /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/ed8d22a947f1635d2df5b6d2916abd40bc6f8c56/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": "^5.9.2", 16 | "vite": "^7.1.3" 17 | } 18 | } -------------------------------------------------------------------------------- /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/ed8d22a947f1635d2df5b6d2916abd40bc6f8c56/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.4.1", 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.4.0-exp.0" 57 | }, 58 | "devDependencies": { 59 | "@elysiajs/cors": "1.3.3", 60 | "@types/bun": "^1.2.20", 61 | "@types/node": "^24.3.0", 62 | "elysia": "1.4.0-exp.18", 63 | "esbuild": "^0.25.9", 64 | "eslint": "^9.34.0", 65 | "expect-type": "^1.2.2", 66 | "rimraf": "^6.0.1", 67 | "tsup": "^8.5.0", 68 | "typescript": "^5.9.2", 69 | "vite": "^7.1.3" 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 { 10 | parseStringifiedDate, 11 | parseStringifiedValue 12 | } from '../utils/parsingUtils' 13 | 14 | const method = [ 15 | 'get', 16 | 'post', 17 | 'put', 18 | 'delete', 19 | 'patch', 20 | 'options', 21 | 'head', 22 | 'connect', 23 | 'subscribe' 24 | ] as const 25 | 26 | const locals = ['localhost', '127.0.0.1', '0.0.0.0'] 27 | 28 | const isServer = typeof FileList === 'undefined' 29 | 30 | const isFile = (v: any) => { 31 | if (isServer) return v instanceof Blob 32 | 33 | return v instanceof FileList || v instanceof File 34 | } 35 | 36 | // FormData is 1 level deep 37 | const hasFile = (obj: Record) => { 38 | if (!obj) return false 39 | 40 | for (const key in obj) { 41 | if (isFile(obj[key])) return true 42 | 43 | if (Array.isArray(obj[key]) && (obj[key] as unknown[]).find(isFile)) 44 | return true 45 | } 46 | 47 | return false 48 | } 49 | 50 | const createNewFile = (v: File) => 51 | isServer 52 | ? v 53 | : new Promise((resolve) => { 54 | const reader = new FileReader() 55 | 56 | reader.onload = () => { 57 | const file = new File([reader.result!], v.name, { 58 | lastModified: v.lastModified, 59 | type: v.type 60 | }) 61 | resolve(file) 62 | } 63 | 64 | reader.readAsArrayBuffer(v) 65 | }) 66 | 67 | const processHeaders = ( 68 | h: Treaty.Config['headers'], 69 | path: string, 70 | options: RequestInit = {}, 71 | headers: Record = {} 72 | ): Record => { 73 | if (Array.isArray(h)) { 74 | for (const value of h) 75 | if (!Array.isArray(value)) 76 | headers = processHeaders(value, path, options, headers) 77 | else { 78 | const key = value[0] 79 | if (typeof key === 'string') 80 | headers[key.toLowerCase()] = value[1] as string 81 | else 82 | for (const [k, value] of key) 83 | headers[k.toLowerCase()] = value as string 84 | } 85 | 86 | return headers 87 | } 88 | 89 | if (!h) return headers 90 | 91 | switch (typeof h) { 92 | case 'function': 93 | if (h instanceof Headers) 94 | return processHeaders(h, path, options, headers) 95 | 96 | const v = h(path, options) 97 | if (v) return processHeaders(v, path, options, headers) 98 | return headers 99 | 100 | case 'object': 101 | if (h instanceof Headers) { 102 | h.forEach((value, key) => { 103 | headers[key.toLowerCase()] = value 104 | }) 105 | return headers 106 | } 107 | 108 | for (const [key, value] of Object.entries(h)) 109 | headers[key.toLowerCase()] = value as string 110 | 111 | return headers 112 | 113 | default: 114 | return headers 115 | } 116 | } 117 | 118 | export async function* streamResponse(response: Response) { 119 | const body = response.body 120 | 121 | if (!body) return 122 | 123 | const reader = body.getReader() 124 | const decoder = new TextDecoder() 125 | 126 | try { 127 | while (true) { 128 | const { done, value } = await reader.read() 129 | if (done) break 130 | 131 | const data = 132 | typeof value === 'string' ? value : decoder.decode(value) 133 | 134 | if (data.endsWith('\n\n')) yield parseServerSentEvent(data) 135 | else yield parseStringifiedValue(data) 136 | } 137 | } finally { 138 | reader.releaseLock() 139 | } 140 | } 141 | 142 | const parseServerSentEvent = (data: string) => { 143 | const lines = data.split('\n') 144 | const event: Record = {} 145 | 146 | for (const line of lines) { 147 | const index = line.indexOf(':') 148 | if (index > 0) { 149 | const key = line.slice(0, index).trim() 150 | const value = line.slice(index + 1).trim() 151 | event[key] = parseStringifiedValue(value) 152 | } 153 | } 154 | 155 | return event 156 | } 157 | 158 | const createProxy = ( 159 | domain: string, 160 | config: Treaty.Config, 161 | paths: string[] = [], 162 | elysia?: Elysia 163 | ): any => 164 | new Proxy(() => {}, { 165 | get(_, param: string): any { 166 | return createProxy( 167 | domain, 168 | config, 169 | param === 'index' ? paths : [...paths, param], 170 | elysia 171 | ) 172 | }, 173 | apply(_, __, [body, options]) { 174 | if ( 175 | !body || 176 | options || 177 | (typeof body === 'object' && Object.keys(body).length !== 1) || 178 | method.includes(paths.at(-1) as any) 179 | ) { 180 | const methodPaths = [...paths] 181 | const method = methodPaths.pop() 182 | const path = '/' + methodPaths.join('/') 183 | 184 | let { 185 | fetcher = fetch, 186 | headers, 187 | onRequest, 188 | onResponse, 189 | fetch: conf 190 | } = config 191 | 192 | const isGetOrHead = 193 | method === 'get' || 194 | method === 'head' || 195 | method === 'subscribe' 196 | 197 | headers = processHeaders(headers, path, options) 198 | 199 | const query = isGetOrHead 200 | ? (body as Record) 201 | ?.query 202 | : options?.query 203 | 204 | let q = '' 205 | if (query) { 206 | const append = (key: string, value: string) => { 207 | q += 208 | (q ? '&' : '?') + 209 | `${encodeURIComponent(key)}=${encodeURIComponent( 210 | value 211 | )}` 212 | } 213 | 214 | for (const [key, value] of Object.entries(query)) { 215 | if (Array.isArray(value)) { 216 | for (const v of value) append(key, v) 217 | continue 218 | } 219 | 220 | // Explicitly exclude null and undefined values from url encoding 221 | // to prevent parsing string "null" / string "undefined" 222 | if (value === undefined || value === null) continue 223 | 224 | if (typeof value === 'object') { 225 | append(key, JSON.stringify(value)) 226 | continue 227 | } 228 | append(key, `${value}`) 229 | } 230 | } 231 | 232 | if (method === 'subscribe') { 233 | const url = 234 | domain.replace( 235 | /^([^]+):\/\//, 236 | domain.startsWith('https://') 237 | ? 'wss://' 238 | : domain.startsWith('http://') 239 | ? 'ws://' 240 | : locals.find((v) => 241 | (domain as string).includes(v) 242 | ) 243 | ? 'ws://' 244 | : 'wss://' 245 | ) + 246 | path + 247 | q 248 | 249 | return new EdenWS(url) 250 | } 251 | 252 | return (async () => { 253 | let fetchInit = { 254 | method: method?.toUpperCase(), 255 | body, 256 | ...conf, 257 | headers 258 | } satisfies RequestInit 259 | 260 | fetchInit.headers = { 261 | ...headers, 262 | ...processHeaders( 263 | // For GET and HEAD, options is moved to body (1st param) 264 | isGetOrHead ? body?.headers : options?.headers, 265 | path, 266 | fetchInit 267 | ) 268 | } 269 | 270 | const fetchOpts = 271 | isGetOrHead && typeof body === 'object' 272 | ? body.fetch 273 | : options?.fetch 274 | 275 | fetchInit = { 276 | ...fetchInit, 277 | ...fetchOpts 278 | } 279 | 280 | if (isGetOrHead) delete fetchInit.body 281 | 282 | if (onRequest) { 283 | if (!Array.isArray(onRequest)) onRequest = [onRequest] 284 | 285 | for (const value of onRequest) { 286 | const temp = await value(path, fetchInit) 287 | 288 | if (typeof temp === 'object') 289 | fetchInit = { 290 | ...fetchInit, 291 | ...temp, 292 | headers: { 293 | ...fetchInit.headers, 294 | ...processHeaders( 295 | temp.headers, 296 | path, 297 | fetchInit 298 | ) 299 | } 300 | } 301 | } 302 | } 303 | 304 | // ? Duplicate because end-user might add a body in onRequest 305 | if (isGetOrHead) delete fetchInit.body 306 | 307 | if (hasFile(body)) { 308 | const formData = new FormData() 309 | 310 | // FormData is 1 level deep 311 | for (const [key, field] of Object.entries( 312 | fetchInit.body 313 | )) { 314 | if (Array.isArray(field)) { 315 | for (let i = 0; i < field.length; i++) { 316 | const value = (field as any)[i] 317 | 318 | formData.append( 319 | key as any, 320 | value instanceof File 321 | ? await createNewFile(value) 322 | : value 323 | ) 324 | } 325 | 326 | continue 327 | } 328 | 329 | if (isServer) { 330 | formData.append(key, field as any) 331 | 332 | continue 333 | } 334 | 335 | if (field instanceof File) { 336 | formData.append( 337 | key, 338 | await createNewFile(field as any) 339 | ) 340 | 341 | continue 342 | } 343 | 344 | if (field instanceof FileList) { 345 | for (let i = 0; i < field.length; i++) 346 | formData.append( 347 | key as any, 348 | await createNewFile((field as any)[i]) 349 | ) 350 | 351 | continue 352 | } 353 | 354 | formData.append(key, field as string) 355 | } 356 | 357 | // We don't do this because we need to let the browser set the content type with the correct boundary 358 | // fetchInit.headers['content-type'] = 'multipart/form-data' 359 | fetchInit.body = formData 360 | } else if (typeof body === 'object') { 361 | ;(fetchInit.headers as Record)[ 362 | 'content-type' 363 | ] = 'application/json' 364 | 365 | fetchInit.body = JSON.stringify(body) 366 | } else if (body !== undefined && body !== null) { 367 | ;(fetchInit.headers as Record)[ 368 | 'content-type' 369 | ] = 'text/plain' 370 | } 371 | 372 | if (isGetOrHead) delete fetchInit.body 373 | 374 | if (onRequest) { 375 | if (!Array.isArray(onRequest)) onRequest = [onRequest] 376 | 377 | for (const value of onRequest) { 378 | const temp = await value(path, fetchInit) 379 | 380 | if (typeof temp === 'object') 381 | fetchInit = { 382 | ...fetchInit, 383 | ...temp, 384 | headers: { 385 | ...fetchInit.headers, 386 | ...processHeaders( 387 | temp.headers, 388 | path, 389 | fetchInit 390 | ) 391 | } as Record 392 | } 393 | } 394 | } 395 | 396 | if (options?.headers?.['content-type']) 397 | fetchInit.headers['content-type'] = 398 | options?.headers['content-type'] 399 | 400 | const url = domain + path + q 401 | const response = await (elysia?.handle( 402 | new Request(url, fetchInit) 403 | ) ?? fetcher!(url, fetchInit)) 404 | 405 | // @ts-ignore 406 | let data = null 407 | let error = null 408 | 409 | if (onResponse) { 410 | if (!Array.isArray(onResponse)) 411 | onResponse = [onResponse] 412 | 413 | for (const value of onResponse) 414 | try { 415 | const temp = await value(response.clone()) 416 | 417 | if (temp !== undefined && temp !== null) { 418 | data = temp 419 | break 420 | } 421 | } catch (err) { 422 | if (err instanceof EdenFetchError) error = err 423 | else error = new EdenFetchError(422, err) 424 | 425 | break 426 | } 427 | } 428 | 429 | if (data !== null) { 430 | return { 431 | data, 432 | error, 433 | response, 434 | status: response.status, 435 | headers: response.headers 436 | } 437 | } 438 | 439 | switch ( 440 | response.headers.get('Content-Type')?.split(';')[0] 441 | ) { 442 | case 'text/event-stream': 443 | data = streamResponse(response) 444 | break 445 | 446 | case 'application/json': 447 | data = JSON.parse(await response.text(), (k, v) => { 448 | if (typeof v !== 'string') return v 449 | 450 | const date = parseStringifiedDate(v) 451 | if (date) return date 452 | 453 | return v 454 | }) 455 | break 456 | case 'application/octet-stream': 457 | data = await response.arrayBuffer() 458 | break 459 | 460 | case 'multipart/form-data': 461 | const temp = await response.formData() 462 | 463 | data = {} 464 | temp.forEach((value, key) => { 465 | // @ts-ignore 466 | data[key] = value 467 | }) 468 | 469 | break 470 | 471 | default: 472 | data = await response 473 | .text() 474 | .then(parseStringifiedValue) 475 | } 476 | 477 | if (response.status >= 300 || response.status < 200) { 478 | error = new EdenFetchError(response.status, data) 479 | data = null 480 | } 481 | 482 | return { 483 | data, 484 | error, 485 | response, 486 | status: response.status, 487 | headers: response.headers 488 | } 489 | })() 490 | } 491 | 492 | if (typeof body === 'object') 493 | return createProxy( 494 | domain, 495 | config, 496 | [...paths, Object.values(body)[0] as string], 497 | elysia 498 | ) 499 | 500 | return createProxy(domain, config, paths) 501 | } 502 | }) as any 503 | 504 | export const treaty = < 505 | const App extends Elysia 506 | >( 507 | domain: string | App, 508 | config: Treaty.Config = {} 509 | ): Treaty.Create => { 510 | if (typeof domain === 'string') { 511 | if (!config.keepDomain) { 512 | if (!domain.includes('://')) 513 | domain = 514 | (locals.find((v) => (domain as string).includes(v)) 515 | ? 'http://' 516 | : 'https://') + domain 517 | 518 | if (domain.endsWith('/')) domain = domain.slice(0, -1) 519 | } 520 | 521 | return createProxy(domain, config) 522 | } 523 | 524 | if (typeof window !== 'undefined') 525 | console.warn( 526 | 'Elysia instance server found on client side, this is not recommended for security reason. Use generic type instead.' 527 | ) 528 | 529 | return createProxy('http://e.ly', config, [], domain) 530 | } 531 | 532 | export type { Treaty } 533 | -------------------------------------------------------------------------------- /src/treaty2/types.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import type { Elysia, ELYSIA_FORM_DATA } from 'elysia' 3 | 4 | import { EdenWS } from './ws' 5 | import type { IsNever, Not, Prettify } from '../types' 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 And = A extends true 16 | ? B extends true 17 | ? true 18 | : false 19 | : false 20 | 21 | type ReplaceGeneratorWithAsyncGenerator< 22 | in out RecordType extends Record 23 | > = { 24 | [K in keyof RecordType]: IsNever extends true 25 | ? RecordType[K] 26 | : RecordType[K] extends Generator 27 | ? void extends B 28 | ? AsyncGenerator 29 | : And, void extends B ? false : true> extends true 30 | ? B 31 | : AsyncGenerator | B 32 | : RecordType[K] extends AsyncGenerator 33 | ? And>, void extends B ? true : false> extends true 34 | ? AsyncGenerator 35 | : And, void extends B ? false : true> extends true 36 | ? B 37 | : AsyncGenerator | B 38 | : RecordType[K] extends ReadableStream 39 | ? AsyncGenerator 40 | : RecordType[K] 41 | } & {} 42 | 43 | type MaybeArray = T | T[] 44 | type MaybePromise = T | Promise 45 | 46 | export namespace Treaty { 47 | interface TreatyParam { 48 | fetch?: RequestInit 49 | } 50 | 51 | export type Create> = 52 | App extends { 53 | '~Routes': infer Schema extends Record 54 | } 55 | ? Prettify> & CreateParams 56 | : 'Please install Elysia before using Eden' 57 | 58 | export type Sign> = { 59 | [K in keyof Route as K extends `:${string}` 60 | ? never 61 | : K]: K extends 'subscribe' // ? Websocket route 62 | ? ({} extends Route['subscribe']['headers'] 63 | ? { headers?: Record } 64 | : undefined extends Route['subscribe']['headers'] 65 | ? { headers?: Record } 66 | : { 67 | headers: Route['subscribe']['headers'] 68 | }) & 69 | ({} extends Route['subscribe']['query'] 70 | ? { query?: Record } 71 | : undefined extends Route['subscribe']['query'] 72 | ? { query?: Record } 73 | : { 74 | query: Route['subscribe']['query'] 75 | }) extends infer Param 76 | ? (options?: Param) => EdenWS 77 | : never 78 | : Route[K] extends { 79 | body: infer Body 80 | headers: infer Headers 81 | params: any 82 | query: infer Query 83 | response: infer Res extends Record 84 | } 85 | ? ({} extends Headers 86 | ? { 87 | headers?: Record 88 | } 89 | : undefined extends Headers 90 | ? { headers?: Record } 91 | : { 92 | headers: Headers 93 | }) & 94 | ({} extends Query 95 | ? { 96 | query?: Record 97 | } 98 | : undefined extends Query 99 | ? { query?: Record } 100 | : { query: Query }) extends infer Param 101 | ? {} extends Param 102 | ? undefined extends Body 103 | ? K extends 'get' | 'head' 104 | ? ( 105 | options?: Prettify 106 | ) => Promise< 107 | TreatyResponse< 108 | ReplaceGeneratorWithAsyncGenerator 109 | > 110 | > 111 | : ( 112 | body?: Body, 113 | options?: Prettify 114 | ) => Promise< 115 | TreatyResponse< 116 | ReplaceGeneratorWithAsyncGenerator 117 | > 118 | > 119 | : {} extends Body 120 | ? ( 121 | body?: Body, 122 | options?: Prettify 123 | ) => Promise< 124 | TreatyResponse< 125 | ReplaceGeneratorWithAsyncGenerator 126 | > 127 | > 128 | : ( 129 | body: Body, 130 | options?: Prettify 131 | ) => Promise< 132 | TreatyResponse< 133 | ReplaceGeneratorWithAsyncGenerator 134 | > 135 | > 136 | : K extends 'get' | 'head' 137 | ? ( 138 | options: Prettify 139 | ) => Promise< 140 | TreatyResponse< 141 | ReplaceGeneratorWithAsyncGenerator 142 | > 143 | > 144 | : ( 145 | body: Body, 146 | options: Prettify 147 | ) => Promise< 148 | TreatyResponse< 149 | ReplaceGeneratorWithAsyncGenerator 150 | > 151 | > 152 | : never 153 | : CreateParams 154 | } 155 | 156 | type CreateParams> = 157 | Extract extends infer Path extends string 158 | ? IsNever extends true 159 | ? Prettify> 160 | : // ! DO NOT USE PRETTIFY ON THIS LINE, OTHERWISE FUNCTION CALLING WILL BE OMITTED 161 | (((params: { 162 | [param in Path extends `:${infer Param}` 163 | ? Param extends `${infer Param}?` 164 | ? Param 165 | : Param 166 | : never]: string | number 167 | }) => Prettify> & 168 | CreateParams) & 169 | Prettify>) & 170 | (Path extends `:${string}?` 171 | ? CreateParams 172 | : {}) 173 | : never 174 | 175 | export interface Config { 176 | fetch?: Omit 177 | fetcher?: typeof fetch 178 | headers?: MaybeArray< 179 | | RequestInit['headers'] 180 | | (( 181 | path: string, 182 | options: RequestInit 183 | ) => RequestInit['headers'] | void) 184 | > 185 | onRequest?: MaybeArray< 186 | ( 187 | path: string, 188 | options: RequestInit 189 | ) => MaybePromise 190 | > 191 | onResponse?: MaybeArray<(response: Response) => MaybePromise> 192 | keepDomain?: boolean 193 | } 194 | 195 | // type UnwrapAwaited> = { 196 | // [K in keyof T]: Awaited 197 | // } 198 | 199 | export type TreatyResponse> = 200 | | { 201 | data: Res[200] extends { 202 | [ELYSIA_FORM_DATA]: infer Data 203 | } 204 | ? Data 205 | : Res[200] 206 | error: null 207 | response: Response 208 | status: number 209 | headers: RequestInit['headers'] 210 | } 211 | | { 212 | data: null 213 | error: Exclude extends never 214 | ? { 215 | status: unknown 216 | value: unknown 217 | } 218 | : { 219 | [Status in keyof Res]: { 220 | status: Status 221 | value: Res[Status] extends { 222 | [ELYSIA_FORM_DATA]: infer Data 223 | } 224 | ? Data 225 | : Res[Status] 226 | } 227 | }[Exclude] 228 | response: Response 229 | status: number 230 | headers: RequestInit['headers'] 231 | } 232 | 233 | export interface OnMessage extends MessageEvent { 234 | data: Data 235 | rawData: MessageEvent['data'] 236 | } 237 | 238 | export type WSEvent< 239 | K extends keyof WebSocketEventMap, 240 | Data = unknown 241 | > = K extends 'message' ? OnMessage : WebSocketEventMap[K] 242 | 243 | type MaybeFunction = T | ((...a: any) => T) 244 | type UnwrapMaybeFunction = T extends (...a: any) => infer R ? R : T 245 | 246 | type MaybePromise = T | Promise 247 | 248 | export type Data< 249 | Response extends MaybeFunction>> 250 | > = NonNullable>['data']> 251 | 252 | export type Error< 253 | Response extends MaybeFunction>> 254 | > = NonNullable>['error']> 255 | } 256 | -------------------------------------------------------------------------------- /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') return null 13 | 14 | // Remove quote from stringified date 15 | const temp = value.replace(/"/g, '') 16 | 17 | if ( 18 | isISO8601.test(temp) || 19 | isFormalDate.test(temp) || 20 | isShortenDate.test(temp) 21 | ) { 22 | const date = new Date(temp) 23 | 24 | if (!Number.isNaN(date.getTime())) return date 25 | } 26 | 27 | return null 28 | } 29 | 30 | export const isStringifiedObject = (value: string) => { 31 | const start = value.charCodeAt(0) 32 | const end = value.charCodeAt(value.length - 1) 33 | 34 | return (start === 123 && end === 125) || (start === 91 && end === 93) 35 | } 36 | 37 | export const parseStringifiedObject = (data: string) => 38 | JSON.parse(data, (_, value) => { 39 | const date = parseStringifiedDate(value) 40 | 41 | if (date) { 42 | return date 43 | } 44 | 45 | return value 46 | }) 47 | 48 | export const parseStringifiedValue = (value: string) => { 49 | if (!value) return value 50 | if (isNumericString(value)) return +value 51 | if (value === 'true') return true 52 | if (value === 'false') return false 53 | 54 | const date = parseStringifiedDate(value) 55 | if (date) return date 56 | 57 | if (isStringifiedObject(value)) { 58 | try { 59 | return parseStringifiedObject(value) 60 | } catch {} 61 | } 62 | 63 | return value 64 | } 65 | 66 | export const parseMessageEvent = (event: MessageEvent) => { 67 | const messageString = event.data.toString() 68 | 69 | return messageString === 'null' 70 | ? null 71 | : parseStringifiedValue(messageString) 72 | } 73 | -------------------------------------------------------------------------------- /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/ed8d22a947f1635d2df5b6d2916abd40bc6f8c56/test/public/aris-yuzu.jpg -------------------------------------------------------------------------------- /test/public/kyuukurarin.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/eden/ed8d22a947f1635d2df5b6d2916abd40bc6f8c56/test/public/kyuukurarin.mp4 -------------------------------------------------------------------------------- /test/public/midori.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/eden/ed8d22a947f1635d2df5b6d2916abd40bc6f8c56/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, sse, t } from 'elysia' 2 | import { Treaty, 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) => 108 | app.guard({}, (app) => app.get('/data', () => 'hi')) 109 | ) 110 | .get('/error', ({ status }) => status("I'm a teapot", 'Kirifuji Nagisa'), { 111 | response: { 112 | 200: t.Void(), 113 | 418: t.Literal('Kirifuji Nagisa'), 114 | 420: t.Literal('Snoop Dogg') 115 | } 116 | }) 117 | .get( 118 | '/headers', 119 | ({ headers: { username, alias } }) => ({ username, alias }), 120 | { 121 | headers: t.Object({ 122 | username: t.String(), 123 | alias: t.Literal('Kristen') 124 | }) 125 | } 126 | ) 127 | .post( 128 | '/headers', 129 | ({ headers: { username, alias } }) => ({ username, alias }), 130 | { 131 | headers: t.Object({ 132 | username: t.String(), 133 | alias: t.Literal('Kristen') 134 | }) 135 | } 136 | ) 137 | .get( 138 | '/headers-custom', 139 | ({ headers, headers: { username, alias } }) => ({ 140 | username, 141 | alias, 142 | 'x-custom': headers['x-custom'] 143 | }), 144 | { 145 | headers: t.Object({ 146 | username: t.String(), 147 | alias: t.Literal('Kristen'), 148 | 'x-custom': t.Optional(t.Literal('custom')) 149 | }) 150 | } 151 | ) 152 | .post('/date', ({ body: { date } }) => date, { 153 | body: t.Object({ 154 | date: t.Date() 155 | }) 156 | }) 157 | .get('/dateObject', () => ({ date: new Date() })) 158 | .get('/redirect', ({ redirect }) => redirect('http://localhost:8083/true')) 159 | .post( 160 | '/redirect', 161 | ({ redirect }) => redirect('http://localhost:8083/true'), 162 | { 163 | body: t.Object({ 164 | username: t.String() 165 | }) 166 | } 167 | ) 168 | .get('/formdata', () => 169 | form({ 170 | image: Bun.file('./test/public/kyuukurarin.mp4') 171 | }) 172 | ) 173 | .ws('/json-serialization-deserialization', { 174 | open: async (ws) => { 175 | for (const item of websocketPayloads) { 176 | ws.send(item) 177 | } 178 | ws.close() 179 | } 180 | }) 181 | .get('/stream', function* stream() { 182 | yield 'a' 183 | yield 'b' 184 | yield 'c' 185 | }) 186 | .get('/stream-async', async function* stream() { 187 | yield 'a' 188 | yield 'b' 189 | yield 'c' 190 | }) 191 | .get('/stream-return', function* stream() { 192 | return 'a' 193 | }) 194 | .get('/stream-return-async', function* stream() { 195 | return 'a' 196 | }) 197 | .get('/id/:id?', ({ params: { id = 'unknown' } }) => id) 198 | .post('/files', ({ body: { files } }) => files.map((file) => file.name), { 199 | body: t.Object({ 200 | files: t.Files() 201 | }) 202 | }) 203 | .post('/file', ({ body: { file } }) => file.name, { 204 | body: t.Object({ 205 | file: t.File() 206 | }) 207 | }) 208 | 209 | const client = treaty(app) 210 | 211 | describe('Treaty2', () => { 212 | it('get index', async () => { 213 | const { data, error } = await client.get() 214 | 215 | expect(data).toBe('a') 216 | expect(error).toBeNull() 217 | }) 218 | 219 | it('post index', async () => { 220 | const { data, error } = await client.post() 221 | 222 | expect(data).toBe('a') 223 | expect(error).toBeNull() 224 | }) 225 | 226 | it('parse number', async () => { 227 | const { data } = await client.number.get() 228 | 229 | expect(data).toEqual(1) 230 | }) 231 | 232 | it('parse true', async () => { 233 | const { data } = await client.true.get() 234 | 235 | expect(data).toEqual(true) 236 | }) 237 | 238 | it('parse false', async () => { 239 | const { data } = await client.false.get() 240 | 241 | expect(data).toEqual(false) 242 | }) 243 | 244 | it('parse object with date', async () => { 245 | const { data } = await client.dateObject.get() 246 | 247 | expect(data?.date).toBeInstanceOf(Date) 248 | }) 249 | 250 | it('post array', async () => { 251 | const { data } = await client.array.post(['a', 'b']) 252 | 253 | expect(data).toEqual(['a', 'b']) 254 | }) 255 | 256 | it('post body', async () => { 257 | const { data } = await client.body.post('a') 258 | 259 | expect(data).toEqual('a') 260 | }) 261 | 262 | it('post mirror', async () => { 263 | const body = { username: 'A', password: 'B' } 264 | 265 | const { data } = await client.mirror.post(body) 266 | 267 | expect(data).toEqual(body) 268 | }) 269 | 270 | it('delete empty', async () => { 271 | const { data } = await client.empty.delete() 272 | 273 | expect(data).toEqual({ body: null }) 274 | }) 275 | 276 | it('post deep nested mirror', async () => { 277 | const body = { username: 'A', password: 'B' } 278 | 279 | const { data } = await client.deep.nested.mirror.post(body) 280 | 281 | expect(data).toEqual(body) 282 | }) 283 | 284 | it('get query', async () => { 285 | const query = { username: 'A' } 286 | 287 | const { data } = await client.query.get({ 288 | query 289 | }) 290 | 291 | expect(data).toEqual(query) 292 | }) 293 | 294 | // t.Nullable is impossible to represent with query params 295 | // without elysia specifically parsing 'null' 296 | it('get null query', async () => { 297 | const query = { username: null } 298 | 299 | const { data, error } = await client['query-nullable'].get({ 300 | query 301 | }) 302 | 303 | expect(data).toBeNull() 304 | expect(error?.status).toBe(422) 305 | expect(error?.value.type).toBe('validation') 306 | }) 307 | 308 | it('get optional query', async () => { 309 | const query = { username: undefined } 310 | 311 | const { data } = await client['query-optional'].get({ 312 | query 313 | }) 314 | 315 | expect(data).toEqual({ 316 | username: undefined 317 | }) 318 | }) 319 | 320 | it('get queries', async () => { 321 | const query = { username: 'A', alias: 'Kristen' } as const 322 | 323 | const { data } = await client.queries.get({ 324 | query 325 | }) 326 | 327 | expect(data).toEqual(query) 328 | }) 329 | 330 | it('get optional queries', async () => { 331 | const query = { username: undefined, alias: 'Kristen' } as const 332 | 333 | const { data } = await client['queries-optional'].get({ 334 | query 335 | }) 336 | 337 | expect(data).toEqual({ 338 | username: undefined, 339 | alias: 'Kristen' 340 | }) 341 | }) 342 | 343 | // t.Nullable is impossible to represent with query params 344 | // without elysia specifically parsing 'null' 345 | it('get nullable queries', async () => { 346 | const query = { username: null, alias: 'Kristen' } as const 347 | 348 | const { data, error } = await client['queries-nullable'].get({ 349 | query 350 | }) 351 | 352 | expect(data).toBeNull() 353 | expect(error?.status).toBe(422) 354 | expect(error?.value.type).toBe('validation') 355 | }) 356 | 357 | it('post queries', async () => { 358 | const query = { username: 'A', alias: 'Kristen' } as const 359 | 360 | const { data } = await client.queries.post(null, { 361 | query 362 | }) 363 | 364 | expect(data).toEqual(query) 365 | }) 366 | 367 | it('head queries', async () => { 368 | const query = { username: 'A', alias: 'Kristen' } as const 369 | 370 | const { data } = await client.queries.post(null, { 371 | query 372 | }) 373 | 374 | expect(data).toEqual(query) 375 | }) 376 | 377 | it('get nested data', async () => { 378 | const { data } = await client.nested.data.get() 379 | 380 | expect(data).toEqual('hi') 381 | }) 382 | 383 | it('handle error', async () => { 384 | const { data, error } = await client.error.get() 385 | 386 | let value 387 | 388 | if (error) 389 | switch (error.status) { 390 | case 418: 391 | value = error.value 392 | break 393 | 394 | case 420: 395 | value = error.value 396 | break 397 | } 398 | 399 | expect(data).toBeNull() 400 | expect(value).toEqual('Kirifuji Nagisa') 401 | }) 402 | 403 | it('get headers', async () => { 404 | const headers = { username: 'A', alias: 'Kristen' } as const 405 | 406 | const { data } = await client.headers.get({ 407 | headers 408 | }) 409 | 410 | expect(data).toEqual(headers) 411 | }) 412 | 413 | it('post headers', async () => { 414 | const headers = { username: 'A', alias: 'Kristen' } as const 415 | 416 | const { data } = await client.headers.post(null, { 417 | headers 418 | }) 419 | 420 | expect(data).toEqual(headers) 421 | }) 422 | 423 | it('handle interception', async () => { 424 | const client = treaty(app, { 425 | onRequest(path) { 426 | if (path === '/headers-custom') 427 | return { 428 | headers: { 429 | 'x-custom': 'custom' 430 | } 431 | } 432 | }, 433 | async onResponse(response) { 434 | return { intercepted: true, data: await response.json() } 435 | } 436 | }) 437 | 438 | const headers = { username: 'a', alias: 'Kristen' } as const 439 | 440 | const { data } = await client['headers-custom'].get({ 441 | headers 442 | }) 443 | 444 | expect(data).toEqual({ 445 | // @ts-expect-error 446 | intercepted: true, 447 | data: { 448 | ...headers, 449 | 'x-custom': 'custom' 450 | } 451 | }) 452 | }) 453 | 454 | it('handle interception array', async () => { 455 | const client = treaty(app, { 456 | onRequest: [ 457 | () => ({ 458 | headers: { 459 | 'x-custom': 'a' 460 | } 461 | }), 462 | () => ({ 463 | headers: { 464 | 'x-custom': 'custom' 465 | } 466 | }) 467 | ], 468 | onResponse: [ 469 | () => {}, 470 | async (response) => { 471 | return { intercepted: true, data: await response.json() } 472 | } 473 | ] 474 | }) 475 | 476 | const headers = { username: 'a', alias: 'Kristen' } as const 477 | 478 | const { data } = await client['headers-custom'].get({ 479 | headers 480 | }) 481 | 482 | expect(data).toEqual({ 483 | // @ts-expect-error 484 | intercepted: true, 485 | data: { 486 | ...headers, 487 | 'x-custom': 'custom' 488 | } 489 | }) 490 | }) 491 | 492 | it('accept headers configuration', async () => { 493 | const client = treaty(app, { 494 | headers(path) { 495 | if (path === '/headers-custom') 496 | return { 497 | 'x-custom': 'custom' 498 | } 499 | }, 500 | async onResponse(response) { 501 | return { intercepted: true, data: await response.json() } 502 | } 503 | }) 504 | 505 | const headers = { username: 'a', alias: 'Kristen' } as const 506 | 507 | const { data } = await client['headers-custom'].get({ 508 | headers 509 | }) 510 | 511 | expect(data).toEqual({ 512 | // @ts-expect-error 513 | intercepted: true, 514 | data: { 515 | ...headers, 516 | 'x-custom': 'custom' 517 | } 518 | }) 519 | }) 520 | 521 | it('accept headers configuration array', async () => { 522 | const client = treaty(app, { 523 | headers: [ 524 | (path) => { 525 | if (path === '/headers-custom') 526 | return { 527 | 'x-custom': 'custom' 528 | } 529 | } 530 | ], 531 | async onResponse(response) { 532 | return { intercepted: true, data: await response.json() } 533 | } 534 | }) 535 | 536 | const headers = { username: 'a', alias: 'Kristen' } as const 537 | 538 | const { data } = await client['headers-custom'].get({ 539 | headers 540 | }) 541 | 542 | expect(data).toEqual({ 543 | // @ts-expect-error 544 | intercepted: true, 545 | data: { 546 | ...headers, 547 | 'x-custom': 'custom' 548 | } 549 | }) 550 | }) 551 | 552 | it('send date', async () => { 553 | const { data } = await client.date.post({ date: new Date() }) 554 | 555 | expect(data).toBeInstanceOf(Date) 556 | }) 557 | 558 | it('redirect should set location header', async () => { 559 | const { headers, status } = await client['redirect'].get({ 560 | fetch: { 561 | redirect: 'manual' 562 | } 563 | }) 564 | expect(status).toEqual(302) 565 | expect(new Headers(headers).get('location')).toEqual( 566 | 'http://localhost:8083/true' 567 | ) 568 | }) 569 | 570 | it('generator return stream', async () => { 571 | const a = await client.stream.get() 572 | const result = [] 573 | 574 | for await (const chunk of a.data!) result.push(chunk) 575 | 576 | expect(result).toEqual(['a', 'b', 'c']) 577 | }) 578 | 579 | it('generator return async stream', async () => { 580 | const a = await client['stream-async'].get() 581 | const result = [] 582 | 583 | for await (const chunk of a.data!) result.push(chunk) 584 | 585 | expect(result).toEqual(['a', 'b', 'c']) 586 | }) 587 | 588 | it('generator return value', async () => { 589 | const a = await client['stream-return'].get() 590 | 591 | expect(a.data).toBe('a') 592 | }) 593 | 594 | it('generator return async value', async () => { 595 | const a = await client['stream-return-async'].get() 596 | 597 | expect(a.data).toBe('a') 598 | }) 599 | 600 | it('handle optional params', async () => { 601 | const data = await Promise.all([ 602 | client.id.get(), 603 | client.id({ id: 'salty' }).get() 604 | ]) 605 | expect(data.map((x) => x.data)).toEqual(['unknown', 'salty']) 606 | }) 607 | }) 608 | 609 | describe('Treaty2 - Using endpoint URL', () => { 610 | const treatyApp = treaty('http://localhost:8083') 611 | 612 | beforeAll(async () => { 613 | await new Promise((resolve) => { 614 | app.listen(8083, () => { 615 | resolve(null) 616 | }) 617 | }) 618 | }) 619 | 620 | afterAll(() => { 621 | app.stop() 622 | }) 623 | 624 | it('redirect should set location header', async () => { 625 | const { headers, status } = await treatyApp.redirect.get({ 626 | fetch: { 627 | redirect: 'manual' 628 | } 629 | }) 630 | expect(status).toEqual(302) 631 | expect(new Headers(headers).get('location')).toEqual( 632 | 'http://localhost:8083/true' 633 | ) 634 | }) 635 | 636 | it('redirect should set location header with post', async () => { 637 | const { headers, status } = await treatyApp.redirect.post( 638 | { 639 | username: 'a' 640 | }, 641 | { 642 | fetch: { 643 | redirect: 'manual' 644 | } 645 | } 646 | ) 647 | expect(status).toEqual(302) 648 | expect(new Headers(headers).get('location')).toEqual( 649 | 'http://localhost:8083/true' 650 | ) 651 | }) 652 | 653 | it('get formdata', async () => { 654 | const { data } = await treatyApp.formdata.get() 655 | 656 | expect(data!.image.size).toBeGreaterThan(0) 657 | }) 658 | 659 | it("doesn't encode if it doesn't need to", async () => { 660 | const mockedFetch: any = mock((url: string) => { 661 | return new Response(url) 662 | }) 663 | 664 | const client = treaty('localhost', { fetcher: mockedFetch }) 665 | 666 | const { data } = await client.get({ 667 | query: { 668 | hello: 'world' 669 | } 670 | }) 671 | 672 | expect(data).toEqual('http://localhost/?hello=world' as any) 673 | }) 674 | 675 | it('encodes query parameters if it needs to', async () => { 676 | const mockedFetch: any = mock((url: string) => { 677 | return new Response(url) 678 | }) 679 | 680 | const client = treaty('localhost', { fetcher: mockedFetch }) 681 | 682 | const { data } = await client.get({ 683 | query: { 684 | ['1/2']: '1/2' 685 | } 686 | }) 687 | 688 | expect(data).toEqual('http://localhost/?1%2F2=1%2F2' as any) 689 | }) 690 | 691 | it('accepts and serializes several values for the same query parameter', async () => { 692 | const mockedFetch: any = mock((url: string) => { 693 | return new Response(url) 694 | }) 695 | 696 | const client = treaty('localhost', { fetcher: mockedFetch }) 697 | 698 | const { data } = await client.get({ 699 | query: { 700 | ['1/2']: ['1/2', '1 2'] 701 | } 702 | }) 703 | 704 | expect(data).toEqual('http://localhost/?1%2F2=1%2F2&1%2F2=1%202' as any) 705 | }) 706 | 707 | it('Receives the proper objects back from the other end of the websocket', async (done) => { 708 | app.listen(8080, async () => { 709 | const client = treaty('http://localhost:8080') 710 | 711 | const dataOutOfSocket = await new Promise((res) => { 712 | const data: any = [] 713 | // Wait until we've gotten all the data 714 | const socket = 715 | client['json-serialization-deserialization'].subscribe() 716 | socket.subscribe(({ data: dataItem }) => { 717 | data.push(dataItem) 718 | // Only continue when we got all the messages 719 | if (data.length === websocketPayloads.length) { 720 | res(data) 721 | } 722 | }) 723 | }) 724 | 725 | // expect that everything that came out of the socket 726 | // got deserialized into the same thing that we inteded to send 727 | for (let i = 0; i < websocketPayloads.length; i++) { 728 | expect(dataOutOfSocket[i]).toEqual(websocketPayloads[i]) 729 | } 730 | 731 | done() 732 | }) 733 | }) 734 | 735 | it('handle Server-Sent Event', async () => { 736 | const app = new Elysia().get('/test', function* () { 737 | yield sse({ 738 | event: 'start' 739 | }) 740 | yield sse({ 741 | event: 'message', 742 | data: 'Hi, this is Yae Miko from Grand Narukami Shrine' 743 | }) 744 | yield sse({ 745 | event: 'message', 746 | data: 'Would you interested in some novels about Raiden Shogun?' 747 | }) 748 | yield sse({ 749 | event: 'end' 750 | }) 751 | }) 752 | 753 | const client = treaty(app) 754 | 755 | const response = await client.test.get() 756 | 757 | const events = [] 758 | 759 | type A = typeof client.test.get 760 | 761 | for await (const a of response.data!) events.push(a) 762 | 763 | expect(events).toEqual([ 764 | { event: 'start' }, 765 | { 766 | event: 'message', 767 | data: 'Hi, this is Yae Miko from Grand Narukami Shrine' 768 | }, 769 | { 770 | event: 'message', 771 | data: 'Would you interested in some novels about Raiden Shogun?' 772 | }, 773 | { event: 'end' } 774 | ]) 775 | }) 776 | 777 | it('use custom content-type', async () => { 778 | const app = new Elysia().post( 779 | '/', 780 | ({ headers }) => headers['content-type'] 781 | ) 782 | 783 | const client = treaty(app) 784 | 785 | const { data } = await client.post( 786 | {}, 787 | { 788 | headers: { 789 | 'content-type': 'application/json!' 790 | } 791 | } 792 | ) 793 | 794 | expect(data).toBe('application/json!' as any) 795 | }) 796 | }) 797 | 798 | describe('Treaty2 - Using t.File() and t.Files() from server', async () => { 799 | const filePath1 = `${import.meta.dir}/public/aris-yuzu.jpg` 800 | const filePath2 = `${import.meta.dir}/public/midori.png` 801 | const filePath3 = `${import.meta.dir}/public/kyuukurarin.mp4` 802 | 803 | const bunFile1 = Bun.file(filePath1) 804 | const bunFile2 = Bun.file(filePath2) 805 | const bunFile3 = Bun.file(filePath3) 806 | 807 | const file1 = new File([await bunFile1.arrayBuffer()], 'cumin.webp', { 808 | type: 'image/webp' 809 | }) 810 | const file2 = new File([await bunFile2.arrayBuffer()], 'curcuma.jpg', { 811 | type: 'image/jpeg' 812 | }) 813 | const file3 = new File([await bunFile3.arrayBuffer()], 'kyuukurarin.mp4', { 814 | type: 'video/mp4' 815 | }) 816 | 817 | const filesForm = new FormData() 818 | filesForm.append('files', file1) 819 | filesForm.append('files', file2) 820 | filesForm.append('files', file3) 821 | 822 | const bunFilesForm = new FormData() 823 | bunFilesForm.append('files', bunFile1) 824 | bunFilesForm.append('files', bunFile2) 825 | bunFilesForm.append('files', bunFile3) 826 | 827 | it('accept a single Bun.file', async () => { 828 | const { data: files } = await client.files.post({ 829 | files: bunFile1 as unknown as File[] 830 | }) 831 | 832 | expect(files).not.toBeNull() 833 | expect(files).not.toBeUndefined() 834 | expect(files).toEqual([bunFile1.name!]) 835 | 836 | const { data: filesbis } = await client.files.post({ 837 | files: [bunFile1] as unknown as File[] 838 | }) 839 | 840 | expect(filesbis).not.toBeNull() 841 | expect(filesbis).not.toBeUndefined() 842 | expect(filesbis).toEqual([bunFile1.name!]) 843 | 844 | const { data: file } = await client.file.post({ 845 | file: bunFile1 as unknown as File 846 | }) 847 | 848 | expect(file).not.toBeNull() 849 | expect(file).not.toBeUndefined() 850 | expect(file).toEqual(bunFile1.name!) 851 | }) 852 | 853 | it('accept a single regular file', async () => { 854 | const { data: files } = await client.files.post({ 855 | files: file1 as unknown as File[] 856 | }) 857 | 858 | expect(files).not.toBeNull() 859 | expect(files).not.toBeUndefined() 860 | expect(files).toEqual([file1.name!]) 861 | 862 | const { data: filesbis } = await client.files.post({ 863 | files: [file1] as unknown as File[] 864 | }) 865 | 866 | expect(filesbis).not.toBeNull() 867 | expect(filesbis).not.toBeUndefined() 868 | expect(filesbis).toEqual([file1.name!]) 869 | 870 | const { data: file } = await client.file.post({ 871 | file: file1 as unknown as File 872 | }) 873 | 874 | expect(file).not.toBeNull() 875 | expect(file).not.toBeUndefined() 876 | expect(file).toEqual(file1.name!) 877 | }) 878 | 879 | it('accept an array of multiple Bun.file', async () => { 880 | const { data: files } = await client.files.post({ 881 | files: [bunFile1, bunFile2, bunFile3] as unknown as File[] 882 | }) 883 | 884 | expect(files).not.toBeNull() 885 | expect(files).not.toBeUndefined() 886 | expect(files).toEqual([bunFile1.name!, bunFile2.name!, bunFile3.name!]) 887 | 888 | const { data: filesbis } = await client.files.post({ 889 | files: bunFilesForm.getAll('files') as unknown as File[] 890 | }) 891 | 892 | expect(filesbis).not.toBeNull() 893 | expect(filesbis).not.toBeUndefined() 894 | expect(filesbis).toEqual([ 895 | bunFile1.name!, 896 | bunFile2.name!, 897 | bunFile3.name! 898 | ]) 899 | }) 900 | 901 | it('accept an array of multiple regular file', async () => { 902 | const { data: files } = await client.files.post({ 903 | files: [file1, file2, file3] as unknown as File[] 904 | }) 905 | 906 | expect(files).not.toBeNull() 907 | expect(files).not.toBeUndefined() 908 | expect(files).toEqual([file1.name!, file2.name!, file3.name!]) 909 | 910 | const { data: filesbis } = await client.files.post({ 911 | files: filesForm.getAll('files') as unknown as File[] 912 | }) 913 | 914 | expect(filesbis).not.toBeNull() 915 | expect(filesbis).not.toBeUndefined() 916 | expect(filesbis).toEqual([file1.name!, file2.name!, file3.name!]) 917 | }) 918 | 919 | it('handle root dynamic parameter', async () => { 920 | const app = new Elysia().get('/:id', ({ params: { id } }) => id, { 921 | params: t.Object({ 922 | id: t.Number() 923 | }) 924 | }) 925 | 926 | const api = treaty(app) 927 | const { data } = await api({ id: '1' }).get() 928 | 929 | expect(data).toBe(1) 930 | }) 931 | }) 932 | -------------------------------------------------------------------------------- /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) => 53 | app.guard({}, (app) => app.get('/data', () => 'hi')) 54 | ) 55 | .get('/error', ({ status }) => status("I'm a teapot", 'Kirifuji Nagisa'), { 56 | response: { 57 | 200: t.Void(), 58 | 418: t.Literal('Kirifuji Nagisa'), 59 | 420: t.Literal('Snoop Dogg') 60 | } 61 | }) 62 | .get( 63 | '/headers', 64 | ({ headers: { username, alias } }) => ({ username, alias }), 65 | { 66 | headers: t.Object({ 67 | username: t.String(), 68 | alias: t.Literal('Kristen') 69 | }) 70 | } 71 | ) 72 | .post( 73 | '/headers', 74 | ({ headers: { username, alias } }) => ({ username, alias }), 75 | { 76 | headers: t.Object({ 77 | username: t.String(), 78 | alias: t.Literal('Kristen') 79 | }) 80 | } 81 | ) 82 | .get( 83 | '/queries-headers', 84 | ({ headers: { username, alias } }) => ({ username, alias }), 85 | { 86 | query: t.Object({ 87 | username: t.String(), 88 | alias: t.Literal('Kristen') 89 | }), 90 | headers: t.Object({ 91 | username: t.String(), 92 | alias: t.Literal('Kristen') 93 | }) 94 | } 95 | ) 96 | .post( 97 | '/queries-headers', 98 | ({ headers: { username, alias } }) => ({ username, alias }), 99 | { 100 | query: t.Object({ 101 | username: t.String(), 102 | alias: t.Literal('Kristen') 103 | }), 104 | headers: t.Object({ 105 | username: t.String(), 106 | alias: t.Literal('Kristen') 107 | }) 108 | } 109 | ) 110 | .post( 111 | '/body-queries-headers', 112 | ({ headers: { username, alias } }) => ({ username, alias }), 113 | { 114 | body: t.Object({ 115 | username: t.String(), 116 | alias: t.Literal('Kristen') 117 | }), 118 | query: t.Object({ 119 | username: t.String(), 120 | alias: t.Literal('Kristen') 121 | }), 122 | headers: t.Object({ 123 | username: t.String(), 124 | alias: t.Literal('Kristen') 125 | }) 126 | } 127 | ) 128 | .get('/async', async ({ status }) => { 129 | if (Math.random() > 0.5) return status(418, 'Nagisa') 130 | if (Math.random() > 0.5) return status(401, 'Himari') 131 | 132 | return 'Hifumi' 133 | }) 134 | .use(plugin) 135 | 136 | const api = treaty(app) 137 | type api = typeof api 138 | 139 | type Result = T extends (...args: any[]) => infer R 140 | ? Awaited 141 | : never 142 | 143 | type ValidationError = { 144 | data: null 145 | error: { 146 | status: 422 147 | value: { 148 | type: 'validation' 149 | on: string 150 | summary?: string 151 | message?: string 152 | found?: unknown 153 | property?: string 154 | expected?: string 155 | } 156 | } 157 | response: Response 158 | status: number 159 | headers: RequestInit['headers'] 160 | } 161 | 162 | // ? Get should have 1 parameter and is optional when no parameter is defined 163 | { 164 | type Route = api['get'] 165 | 166 | expectTypeOf().parameter(0).toEqualTypeOf< 167 | | { 168 | headers?: Record | undefined 169 | query?: Record | undefined 170 | fetch?: RequestInit | undefined 171 | } 172 | | undefined 173 | >() 174 | 175 | expectTypeOf().parameter(1).toBeUndefined() 176 | 177 | type Res = Result 178 | 179 | expectTypeOf().toEqualTypeOf< 180 | | { 181 | data: 'a' 182 | error: null 183 | response: Response 184 | status: number 185 | headers: HeadersInit | undefined 186 | } 187 | | { 188 | data: null 189 | error: { 190 | status: unknown 191 | value: unknown 192 | } 193 | response: Response 194 | status: number 195 | headers: HeadersInit | undefined 196 | } 197 | >() 198 | } 199 | 200 | // ? Non-get should have 2 parameter and is optional when no parameter is defined 201 | { 202 | type Route = api['post'] 203 | 204 | expectTypeOf().parameter(0).toBeUnknown() 205 | 206 | expectTypeOf().parameter(1).toEqualTypeOf< 207 | | { 208 | headers?: Record | undefined 209 | query?: Record | undefined 210 | fetch?: RequestInit | undefined 211 | } 212 | | undefined 213 | >() 214 | 215 | type Res = Result 216 | 217 | expectTypeOf().toEqualTypeOf< 218 | | { 219 | data: 'a' 220 | error: null 221 | response: Response 222 | status: number 223 | headers: HeadersInit | undefined 224 | } 225 | | { 226 | data: null 227 | error: { 228 | status: unknown 229 | value: unknown 230 | } 231 | response: Response 232 | status: number 233 | headers: HeadersInit | undefined 234 | } 235 | >() 236 | } 237 | 238 | // ? Should return literal 239 | { 240 | type Route = api['number']['get'] 241 | 242 | expectTypeOf().parameter(0).toEqualTypeOf< 243 | | { 244 | headers?: Record | undefined 245 | query?: Record | undefined 246 | fetch?: RequestInit | undefined 247 | } 248 | | undefined 249 | >() 250 | 251 | expectTypeOf().parameter(1).toBeUndefined() 252 | 253 | type Res = Result 254 | 255 | expectTypeOf().toEqualTypeOf< 256 | | { 257 | data: 1 258 | error: null 259 | response: Response 260 | status: number 261 | headers: HeadersInit | undefined 262 | } 263 | | { 264 | data: null 265 | error: { 266 | status: unknown 267 | value: unknown 268 | } 269 | response: Response 270 | status: number 271 | headers: HeadersInit | undefined 272 | } 273 | >() 274 | } 275 | 276 | // ? Should return boolean 277 | { 278 | type Route = api['true']['get'] 279 | 280 | expectTypeOf().parameter(0).toEqualTypeOf< 281 | | { 282 | headers?: Record | undefined 283 | query?: Record | undefined 284 | fetch?: RequestInit | undefined 285 | } 286 | | undefined 287 | >() 288 | 289 | expectTypeOf().parameter(1).toBeUndefined() 290 | 291 | type Res = Result 292 | 293 | expectTypeOf().toEqualTypeOf< 294 | | { 295 | data: boolean 296 | error: null 297 | response: Response 298 | status: number 299 | headers: HeadersInit | undefined 300 | } 301 | | { 302 | data: null 303 | error: { 304 | status: unknown 305 | value: unknown 306 | } 307 | response: Response 308 | status: number 309 | headers: HeadersInit | undefined 310 | } 311 | >() 312 | } 313 | 314 | // ? Should return array of string 315 | { 316 | type Route = api['array']['post'] 317 | 318 | expectTypeOf().parameter(0).toEqualTypeOf() 319 | 320 | expectTypeOf().parameter(1).toEqualTypeOf< 321 | | { 322 | headers?: Record | undefined 323 | query?: Record | undefined 324 | fetch?: RequestInit | undefined 325 | } 326 | | undefined 327 | >() 328 | 329 | type Res = Result 330 | 331 | expectTypeOf().toEqualTypeOf< 332 | | { 333 | data: string[] 334 | error: null 335 | response: Response 336 | status: number 337 | headers: RequestInit['headers'] 338 | } 339 | | ValidationError 340 | >() 341 | } 342 | 343 | // ? Should return body 344 | { 345 | type Route = api['mirror']['post'] 346 | 347 | expectTypeOf().parameter(0).toBeUnknown() 348 | 349 | expectTypeOf().parameter(1).toEqualTypeOf< 350 | | { 351 | headers?: Record | undefined 352 | query?: Record | undefined 353 | fetch?: RequestInit | undefined 354 | } 355 | | undefined 356 | >() 357 | type Res = Result 358 | 359 | expectTypeOf().toEqualTypeOf< 360 | | { 361 | data: unknown 362 | error: null 363 | response: Response 364 | status: number 365 | headers: HeadersInit | undefined 366 | } 367 | | { 368 | data: null 369 | error: { 370 | status: unknown 371 | value: unknown 372 | } 373 | response: Response 374 | status: number 375 | headers: HeadersInit | undefined 376 | } 377 | >() 378 | } 379 | 380 | // ? Should return body 381 | { 382 | type Route = api['body']['post'] 383 | 384 | expectTypeOf().parameter(0).toEqualTypeOf() 385 | 386 | expectTypeOf().parameter(1).toEqualTypeOf< 387 | | { 388 | headers?: Record | undefined 389 | query?: Record | undefined 390 | fetch?: RequestInit | undefined 391 | } 392 | | undefined 393 | >() 394 | type Res = Result 395 | 396 | expectTypeOf().toEqualTypeOf< 397 | | { 398 | data: string 399 | error: null 400 | response: Response 401 | status: number 402 | headers: RequestInit['headers'] 403 | } 404 | | { 405 | data: null 406 | error: { 407 | status: 422 408 | value: { 409 | type: 'validation' 410 | on: string 411 | summary?: string 412 | message?: string 413 | found?: unknown 414 | property?: string 415 | expected?: string 416 | } 417 | } 418 | response: Response 419 | status: number 420 | headers: RequestInit['headers'] 421 | } 422 | >() 423 | } 424 | 425 | // ? Should return body 426 | { 427 | type Route = api['deep']['nested']['mirror']['post'] 428 | 429 | expectTypeOf().parameter(0).toEqualTypeOf<{ 430 | username: string 431 | password: string 432 | }>() 433 | 434 | expectTypeOf().parameter(1).toEqualTypeOf< 435 | | { 436 | headers?: Record | undefined 437 | query?: Record | undefined 438 | fetch?: RequestInit | undefined 439 | } 440 | | undefined 441 | >() 442 | 443 | type Res = Result 444 | 445 | expectTypeOf().toEqualTypeOf< 446 | | { 447 | data: { 448 | username: string 449 | password: string 450 | } 451 | error: null 452 | response: Response 453 | status: number 454 | headers: RequestInit['headers'] 455 | } 456 | | ValidationError 457 | >() 458 | } 459 | 460 | // ? Get should have 1 parameter and is required when query is defined 461 | { 462 | type Route = api['query']['get'] 463 | 464 | expectTypeOf().parameter(0).toEqualTypeOf<{ 465 | headers?: Record | undefined 466 | query: { 467 | username: string 468 | } 469 | fetch?: RequestInit | undefined 470 | }>() 471 | 472 | expectTypeOf().parameter(1).toBeUndefined() 473 | 474 | type Res = Result 475 | 476 | expectTypeOf().toEqualTypeOf< 477 | | { 478 | data: { 479 | username: string 480 | } 481 | error: null 482 | response: Response 483 | status: number 484 | headers: HeadersInit | undefined 485 | } 486 | | ValidationError 487 | >() 488 | } 489 | 490 | // ? Get should have 1 parameter and is required when query is defined 491 | { 492 | type Route = api['queries']['get'] 493 | 494 | expectTypeOf().parameter(0).toEqualTypeOf<{ 495 | headers?: Record | undefined 496 | query: { 497 | username: string 498 | alias: 'Kristen' 499 | } 500 | fetch?: RequestInit | undefined 501 | }>() 502 | 503 | expectTypeOf().parameter(1).toBeUndefined() 504 | 505 | type Res = Result 506 | 507 | expectTypeOf().toEqualTypeOf< 508 | | { 509 | data: { 510 | username: string 511 | alias: 'Kristen' 512 | } 513 | error: null 514 | response: Response 515 | status: number 516 | headers: HeadersInit | undefined 517 | } 518 | | ValidationError 519 | >() 520 | } 521 | 522 | // ? Post should have 2 parameter and is required when query is defined 523 | { 524 | type Route = api['queries']['post'] 525 | 526 | expectTypeOf().parameter(0).toBeUnknown() 527 | 528 | expectTypeOf().parameter(1).toEqualTypeOf<{ 529 | headers?: Record | undefined 530 | query: { 531 | username: string 532 | alias: 'Kristen' 533 | } 534 | fetch?: RequestInit | undefined 535 | }>() 536 | 537 | type Res = Result 538 | 539 | expectTypeOf().toEqualTypeOf< 540 | | { 541 | data: { 542 | username: string 543 | alias: 'Kristen' 544 | } 545 | error: null 546 | response: Response 547 | status: number 548 | headers: HeadersInit | undefined 549 | } 550 | | ValidationError 551 | >() 552 | } 553 | 554 | // ? Head should have 1 parameter and is required when query is defined 555 | { 556 | type Route = api['queries']['head'] 557 | 558 | expectTypeOf().parameter(0).toEqualTypeOf<{ 559 | headers?: Record | undefined 560 | query: { 561 | username: string 562 | alias: 'Kristen' 563 | } 564 | fetch?: RequestInit | undefined 565 | }>() 566 | 567 | expectTypeOf().parameter(1).toBeUndefined() 568 | 569 | type Res = Result 570 | 571 | expectTypeOf().toEqualTypeOf< 572 | | { 573 | data: { 574 | username: string 575 | alias: 'Kristen' 576 | } 577 | error: null 578 | response: Response 579 | status: number 580 | headers: HeadersInit | undefined 581 | } 582 | | ValidationError 583 | >() 584 | } 585 | 586 | // ? Should return error 587 | { 588 | type Route = api['error']['get'] 589 | 590 | expectTypeOf().parameter(0).toEqualTypeOf< 591 | | { 592 | headers?: Record | undefined 593 | query?: Record | undefined 594 | fetch?: RequestInit | undefined 595 | } 596 | | undefined 597 | >() 598 | 599 | expectTypeOf().parameter(1).toBeUndefined() 600 | 601 | type Res = Result 602 | 603 | expectTypeOf().toEqualTypeOf< 604 | | { 605 | data: void 606 | error: null 607 | response: Response 608 | status: number 609 | headers: HeadersInit | undefined 610 | } 611 | | { 612 | data: null 613 | error: 614 | | { 615 | status: 418 616 | value: 'Kirifuji Nagisa' 617 | } 618 | | { 619 | status: 420 620 | value: 'Snoop Dogg' 621 | } 622 | | { 623 | status: 422 624 | value: { 625 | type: 'validation' 626 | on: string 627 | summary?: string 628 | message?: string 629 | found?: unknown 630 | property?: string 631 | expected?: string 632 | } 633 | } 634 | response: Response 635 | status: number 636 | headers: HeadersInit | undefined 637 | } 638 | >() 639 | } 640 | 641 | // ? Get should have 1 parameter and is required when headers is defined 642 | { 643 | type Route = api['headers']['get'] 644 | 645 | expectTypeOf().parameter(0).toEqualTypeOf<{ 646 | headers: { 647 | username: string 648 | alias: 'Kristen' 649 | } 650 | query?: Record | undefined 651 | fetch?: RequestInit | undefined 652 | }>() 653 | 654 | expectTypeOf().parameter(1).toBeUndefined() 655 | 656 | type Res = Result 657 | 658 | expectTypeOf().toEqualTypeOf< 659 | | { 660 | data: { 661 | username: string 662 | alias: 'Kristen' 663 | } 664 | error: null 665 | response: Response 666 | status: number 667 | headers: HeadersInit | undefined 668 | } 669 | | ValidationError 670 | >() 671 | } 672 | 673 | // ? Post should have 2 parameter and is required when headers is defined 674 | { 675 | type Route = api['headers']['post'] 676 | 677 | expectTypeOf().parameter(0).toBeUnknown() 678 | 679 | expectTypeOf().parameter(1).toEqualTypeOf<{ 680 | headers: { 681 | username: string 682 | alias: 'Kristen' 683 | } 684 | query?: Record | undefined 685 | fetch?: RequestInit | undefined 686 | }>() 687 | 688 | type Res = Result 689 | 690 | expectTypeOf().toEqualTypeOf< 691 | | { 692 | data: { 693 | username: string 694 | alias: 'Kristen' 695 | } 696 | error: null 697 | response: Response 698 | status: number 699 | headers: HeadersInit | undefined 700 | } 701 | | ValidationError 702 | >() 703 | } 704 | 705 | // ? Get should have 1 parameter and is required when queries and headers is defined 706 | { 707 | type Route = api['queries-headers']['get'] 708 | 709 | expectTypeOf().parameter(0).toEqualTypeOf<{ 710 | headers: { 711 | username: string 712 | alias: 'Kristen' 713 | } 714 | query: { 715 | username: string 716 | alias: 'Kristen' 717 | } 718 | fetch?: RequestInit | undefined 719 | }>() 720 | 721 | expectTypeOf().parameter(1).toBeUndefined() 722 | 723 | type Res = Result 724 | 725 | expectTypeOf().toEqualTypeOf< 726 | | { 727 | data: { 728 | username: string 729 | alias: 'Kristen' 730 | } 731 | error: null 732 | response: Response 733 | status: number 734 | headers: HeadersInit | undefined 735 | } 736 | | ValidationError 737 | >() 738 | } 739 | 740 | // ? Post should have 2 parameter and is required when queries and headers is defined 741 | { 742 | type Route = api['queries-headers']['post'] 743 | 744 | expectTypeOf().parameter(0).toBeUnknown() 745 | 746 | expectTypeOf().parameter(1).toEqualTypeOf<{ 747 | headers: { 748 | username: string 749 | alias: 'Kristen' 750 | } 751 | query: { 752 | username: string 753 | alias: 'Kristen' 754 | } 755 | fetch?: RequestInit | undefined 756 | }>() 757 | 758 | type Res = Result 759 | 760 | expectTypeOf().toEqualTypeOf< 761 | | { 762 | data: { 763 | username: string 764 | alias: 'Kristen' 765 | } 766 | error: null 767 | response: Response 768 | status: number 769 | headers: HeadersInit | undefined 770 | } 771 | | ValidationError 772 | >() 773 | } 774 | 775 | // ? Post should have 2 parameter and is required when queries, headers and body is defined 776 | { 777 | type Route = api['body-queries-headers']['post'] 778 | 779 | expectTypeOf().parameter(0).toEqualTypeOf<{ 780 | username: string 781 | alias: 'Kristen' 782 | }>() 783 | 784 | expectTypeOf().parameter(1).toEqualTypeOf<{ 785 | headers: { 786 | username: string 787 | alias: 'Kristen' 788 | } 789 | query: { 790 | username: string 791 | alias: 'Kristen' 792 | } 793 | fetch?: RequestInit | undefined 794 | }>() 795 | 796 | type Res = Result 797 | 798 | expectTypeOf().toEqualTypeOf< 799 | | { 800 | data: { 801 | username: string 802 | alias: 'Kristen' 803 | } 804 | error: null 805 | response: Response 806 | status: number 807 | headers: HeadersInit | undefined 808 | } 809 | | ValidationError 810 | >() 811 | } 812 | 813 | // ? Should handle async 814 | { 815 | type Route = api['async']['get'] 816 | 817 | expectTypeOf().parameter(0).toEqualTypeOf< 818 | | { 819 | headers?: Record | undefined 820 | query?: Record | undefined 821 | fetch?: RequestInit | undefined 822 | } 823 | | undefined 824 | >() 825 | 826 | expectTypeOf().parameter(1).toBeUndefined() 827 | 828 | type Res = Result 829 | 830 | expectTypeOf().toEqualTypeOf< 831 | | { 832 | data: 'Hifumi' 833 | error: null 834 | response: Response 835 | status: number 836 | headers: HeadersInit | undefined 837 | } 838 | | { 839 | data: null 840 | error: 841 | | { 842 | status: 401 843 | value: 'Himari' 844 | } 845 | | { 846 | status: 418 847 | value: 'Nagisa' 848 | } 849 | response: Response 850 | status: number 851 | headers: HeadersInit | undefined 852 | } 853 | >() 854 | } 855 | 856 | // ? Handle param with nested path 857 | { 858 | type SubModule = api['level'] 859 | 860 | // expectTypeOf().toEqualTypeOf< 861 | // ((params: { id: string | number }) => { 862 | // get: ( 863 | // options?: 864 | // | { 865 | // headers?: Record | undefined 866 | // query?: Record | undefined 867 | // fetch?: RequestInit | undefined 868 | // } 869 | // | undefined 870 | // ) => Promise< 871 | // | { 872 | // data: string 873 | // error: null 874 | // response: Response 875 | // status: number 876 | // headers: HeadersInit | undefined 877 | // } 878 | // | ValidationError 879 | // > 880 | // ok: { 881 | // get: ( 882 | // options?: 883 | // | { 884 | // headers?: Record | undefined 885 | // query?: Record | undefined 886 | // fetch?: RequestInit | undefined 887 | // } 888 | // | undefined 889 | // ) => Promise< 890 | // | { 891 | // data: string 892 | // error: null 893 | // response: Response 894 | // status: number 895 | // headers: HeadersInit | undefined 896 | // } 897 | // | ValidationError 898 | // > 899 | // } 900 | // }) & { 901 | // index: { 902 | // get: ( 903 | // options?: 904 | // | { 905 | // headers?: Record | undefined 906 | // query?: Record | undefined 907 | // fetch?: RequestInit | undefined 908 | // } 909 | // | undefined 910 | // ) => Promise< 911 | // | { 912 | // data: '2' 913 | // error: null 914 | // response: Response 915 | // status: number 916 | // headers: HeadersInit | undefined 917 | // } 918 | // | ValidationError 919 | // > 920 | // } 921 | // level: { 922 | // get: ( 923 | // options?: 924 | // | { 925 | // headers?: Record | undefined 926 | // query?: Record | undefined 927 | // fetch?: RequestInit | undefined 928 | // } 929 | // | undefined 930 | // ) => Promise< 931 | // | { 932 | // data: '2' 933 | // error: null 934 | // response: Response 935 | // status: number 936 | // headers: HeadersInit | undefined 937 | // } 938 | // | ValidationError 939 | // > 940 | // } 941 | // } 942 | // > 943 | } 944 | 945 | // ? Return AsyncGenerator on yield 946 | { 947 | const app = new Elysia().get('', function* () { 948 | yield 1 949 | yield 2 950 | yield 3 951 | }) 952 | 953 | const { data } = await treaty(app).get() 954 | 955 | expectTypeOf().toEqualTypeOf | null>() 960 | } 961 | 962 | // ? Return actual value on generator if not yield 963 | { 964 | const app = new Elysia().get('', function* () { 965 | return 'a' 966 | }) 967 | 968 | const { data } = await treaty(app).get() 969 | 970 | expectTypeOf().toEqualTypeOf() 971 | } 972 | 973 | // ? Return both actual value and generator if yield and return 974 | { 975 | const app = new Elysia().get('', function* () { 976 | if (Math.random() > 0.5) return 'a' 977 | 978 | yield 1 979 | yield 2 980 | yield 3 981 | }) 982 | 983 | const { data } = await treaty(app).get() 984 | 985 | expectTypeOf().toEqualTypeOf< 986 | | 'a' 987 | | AsyncGenerator<1 | 2 | 3, 'a' | undefined, unknown> 988 | | null 989 | | undefined 990 | >() 991 | } 992 | 993 | // ? Return AsyncGenerator on yield 994 | { 995 | const app = new Elysia().get('', async function* () { 996 | yield 1 997 | yield 2 998 | yield 3 999 | }) 1000 | 1001 | const { data } = await treaty(app).get() 1002 | 1003 | expectTypeOf().toEqualTypeOf | null>() 1008 | } 1009 | 1010 | { 1011 | const app = new Elysia().get('/formdata', () => 1012 | form({ 1013 | image: file('/') 1014 | }) 1015 | ) 1016 | 1017 | const { data } = await treaty(app).formdata.get() 1018 | 1019 | expectTypeOf(data!.image).toEqualTypeOf() 1020 | } 1021 | 1022 | // Handle dynamic parameter at root 1023 | { 1024 | const app = new Elysia().get('/:id', () => null) 1025 | 1026 | type App = typeof app 1027 | 1028 | const edenClient = treaty('http://localhost:3000') 1029 | 1030 | edenClient({ id: '1' }).get() 1031 | } 1032 | 1033 | // Turn Generator to AsyncGenerator 1034 | { 1035 | const app = new Elysia().get('/test', function* a() { 1036 | yield 'a' 1037 | yield 'b' 1038 | }) 1039 | 1040 | const client = treaty(app) 1041 | 1042 | const { data } = await client.test.get() 1043 | 1044 | expectTypeOf(data).toEqualTypeOf | null>() 1049 | } 1050 | 1051 | // Turn ReadableStream to AsyncGenerator 1052 | { 1053 | const app = new Elysia().get( 1054 | '/test', 1055 | () => 1056 | new ReadableStream<'a' | 'b'>({ 1057 | start(controller) { 1058 | controller.enqueue('a') 1059 | controller.enqueue('b') 1060 | } 1061 | }) 1062 | ) 1063 | 1064 | const client = treaty(app) 1065 | 1066 | const { data } = await client.test.get() 1067 | 1068 | expectTypeOf(data).toEqualTypeOf | null>() 1073 | } 1074 | 1075 | // macro should not mark property as required 1076 | { 1077 | const authMacro = new Elysia().macro({ 1078 | auth: { 1079 | async resolve() { 1080 | return { newProperty: 'Macro added property' } 1081 | } 1082 | } 1083 | }) 1084 | 1085 | const routerWithMacro = new Elysia() 1086 | .use(authMacro) 1087 | .get('/bug', 'Problem', { auth: true }) 1088 | 1089 | const routerWithoutMacro = new Elysia().get('/noBug', 'No Problem') 1090 | 1091 | const app = new Elysia().use(routerWithMacro).use(routerWithoutMacro) 1092 | const api = treaty('localhost:3000') 1093 | 1094 | api.noBug.get() 1095 | api.bug.get() 1096 | } 1097 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------