├── .github ├── FUNDING.yaml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── README.md ├── bun.lockb ├── example ├── a.ts ├── b.ts └── index.ts ├── package.json ├── src └── index.ts ├── test ├── client.test.ts └── server.test.ts ├── tsconfig.cjs.json └── tsconfig.json /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: SaltyAom -------------------------------------------------------------------------------- /.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 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .github 4 | .prettierrc 5 | .eslintrc.js 6 | .swc.cjs.swcrc 7 | .swc.esm.swcrc 8 | .swcrc 9 | .husky 10 | 11 | bun.lockb 12 | node_modules 13 | tsconfig.json 14 | CHANGELOG.md 15 | 16 | example 17 | tests 18 | test 19 | docs 20 | src 21 | .DS_Store 22 | test 23 | tsconfig.cjs.json 24 | tsconfig.esm.json 25 | tsconfig.test.json 26 | 27 | CONTRIBUTING.md 28 | CODE_OF_CONDUCT.md 29 | 30 | build.ts -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | registry=https://registry.npmjs.org 3 | always-auth=true 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sirine 2 | 3 | Export function as endpoint. 4 | 5 | Installation: 6 | 7 | ```bash 8 | bun add sirine 9 | ``` 10 | 11 | Suppose you have an existing function: 12 | 13 | ```typescript 14 | // utils.ts 15 | export function hello(person: string) { 16 | return `hello ${person}` 17 | } 18 | ``` 19 | 20 | We can turns the function into an endpoint: 21 | 22 | ```typescript 23 | import { sirine } from 'sirine' 24 | import * as utils from './utils' 25 | 26 | const server = sirine(utils).listen(3000) 27 | ``` 28 | 29 | The function name will turn into an endpoint, in this case as **/hello** 30 | 31 | ```typescript 32 | // server.ts 33 | import { sirine } from 'sirine' 34 | import * as utils from './utils' 35 | 36 | const server = sirine(utils).listen(3000) 37 | export type server = typeof server 38 | ``` 39 | 40 | Then import Sirine client and server type on client. 41 | 42 | ```typescript 43 | // client.ts 44 | import { client } from 'sirine' 45 | 46 | const app = client('http://localhost:3000') 47 | 48 | app.hello().then(console.log) 49 | ``` 50 | 51 | ## Why 52 | 53 | To export function into an endpoint in the most easiest/convenient way possible. 54 | 55 | Sirine is intent **NOT TO** to replace or compete with existing HTTP-based framework nor being more than just exposing function to network. 56 | 57 | Sirine is an RPC-like client-server with end-to-end type safety for TypeScript developer who just wants to run functions on edge. 58 | 59 | Sirine **do not** do the following: 60 | 61 | - Validate input/output 62 | - Interact with how HTTP work 63 | 64 | ## How it works 65 | 66 | sirine is a function that may accept multiple 1 level-deep object with a value of function. 67 | 68 | Each function will be register as HTTP by the following condition: 69 | 70 | - **GET** - function accept **DO NOT** paramter 71 | - **POST** - function accept paramter 72 | 73 | The method should list as follows: 74 | 75 | | Function | Method | 76 | | ------------ | ------ | 77 | | say() {} | GET | 78 | | say(word) {} | POST | 79 | 80 | Each function name will turn into an endpoint name separated by `/` for each camelCase separation. 81 | 82 | The path should list as follows: 83 | 84 | | Function | Path | 85 | | ------------- | ---------- | 86 | | say() {} | /say | 87 | | sayHello() {} | /say/hello | 88 | | index() {} | / | 89 | | default() {} | / | 90 | 91 | ## Handler 92 | 93 | Each function may or may not accept one parameters which will always be a value of HTTP body. 94 | 95 | ```typescript 96 | export function hello(word: string) { 97 | return word 98 | } 99 | ``` 100 | 101 | We may interact with HTTP `Request` by accessing `this`. 102 | 103 | ```typescript 104 | export function hello(this: Response) { 105 | return this.url 106 | } 107 | ``` 108 | 109 | 110 | ## Validation 111 | 112 | Sirine doesn't provide validation but we recommend using Zod for easy integration. 113 | 114 | ```typescript 115 | import { z } from 'zod' 116 | 117 | const Word = z.object({ 118 | word: z.string() 119 | }) 120 | export function say(this: Response, word: z.infer) { 121 | word = Word.parse(word) 122 | 123 | return word 124 | } 125 | ``` 126 | 127 | ## Types 128 | We may use infers the type of the server by the following: 129 | 130 | ### Server to client 131 | Accessing `Sirine.types`, and pass to `client` 132 | 133 | On the server, we create a new Sirine instance. 134 | ```typescript 135 | // server.ts 136 | import { sirine } from 'sirine' 137 | import * as utils from './utils' 138 | 139 | const server = sirine(utils).listen(3000) 140 | export type server = typeof server 141 | ``` 142 | 143 | Then import Sirine client and server type on client. 144 | 145 | ```typescript 146 | // client.ts 147 | import { client } from 'sirine' 148 | 149 | const app = client('http://localhost:3000') 150 | 151 | app.hello().then(console.log) 152 | ``` 153 | 154 | This will create the order of type as the following: 155 | ``` 156 | utils ---> server ---> client 157 | ``` 158 | 159 | ### Shared to client and server 160 | 161 | On monorepo, if we have access shared utilities on both client and server, we may pass the types to client directly. 162 | 163 | ```typescript 164 | // server.ts 165 | import { sirine } from 'sirine' 166 | import * as utils from '@workspace/utils' 167 | 168 | const server = sirine(utils).listen(3000) 169 | ``` 170 | 171 | Then on the client, we import the same shared utils. 172 | 173 | ```typescript 174 | // client.ts 175 | import { client } from 'sirine' 176 | import * as utils from '@workspace/utils' 177 | 178 | const app = client('http://localhost:3000') 179 | 180 | app.hello().then(console.log) 181 | ``` 182 | 183 | This will create the order of type as the following: 184 | ``` 185 | utils ---> server 186 | utils ---> client 187 | ``` 188 | 189 | ## Server Specification 190 | Sirine server may only introduce either **GET** or **POST** method. 191 | 192 | The content-type may only be **application/json** or **multipart/formdata**. 193 | 194 | If content-type is provide as **multipart/form-data**, the parameter may only be a singular in constrast to **application/json** 195 | 196 | ## Runtime 197 | By default Sirine the following runtime using `Sirine.listen` out of the box: 198 | - Bun 199 | - Deno 200 | - Node (via [@whatwg-node/server](https://npmjs.com/package/@whatwg-node/server)) 201 | 202 | Sirine is using Web Standard Request / Response allowing compatible with WinterCG compliance runtime, for example: 203 | - Cloudflare 204 | - Vercel Edge Function 205 | - Lagon 206 | 207 | We may access `Sirine.handle` which accepts Web Standard Request / Response to integrate with the runtime above. 208 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/sirine/799797a1081e7f8284c463a16a3863f6696f5fea/bun.lockb -------------------------------------------------------------------------------- /example/a.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export function a(this: Response) { 4 | return this.url 5 | } 6 | 7 | export function helloWorld() { 8 | return 'hello world' 9 | } 10 | 11 | const Word = z.object({ 12 | word: z.string() 13 | }) 14 | export function say(this: Response, word: z.infer) { 15 | word = Word.parse(word) 16 | 17 | return word 18 | } 19 | 20 | export default function () { 21 | return 'hello' 22 | } 23 | -------------------------------------------------------------------------------- /example/b.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const b = () => 'hello' 4 | 5 | export const many = (a: string, b: string) => { 6 | return { 7 | word: a + ' ' + b 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { sirine, client } from '../src' 2 | import * as a from './a' 3 | import * as b from './b' 4 | 5 | const server = sirine(a, b, { 6 | hello: async () => 'Sirin' 7 | }).listen(3000) 8 | 9 | type server = typeof server 10 | 11 | const app = client('http://localhost:3000') 12 | 13 | const response = await app.many('hello', 'world') 14 | 15 | console.log(response) 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sirine", 3 | "description": "Export function as endpoint", 4 | "version": "0.1.1", 5 | "license": "MIT", 6 | "author": { 7 | "name": "saltyAom", 8 | "url": "https://github.com/SaltyAom", 9 | "email": "saltyaom@gmail.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/elysiajs/sirine" 14 | }, 15 | "homepage": "https://github.com/elysiajs/sirine", 16 | "bugs": "https://github.com/elysiajs/sirine/issues", 17 | "keywords": [ 18 | "sirine", 19 | "elysia", 20 | "web server" 21 | ], 22 | "scripts": { 23 | "dev": "bun --watch example/index.ts", 24 | "test": "bun test", 25 | "build": "rimraf dist && bun run build:cjs && bun run build:bun", 26 | "build:bun": "bun build src/index.ts --minify --outfile=dist/index.mjs", 27 | "build:cjs": "tsc --project tsconfig.cjs.json", 28 | "release": "npm run build && npm run test && npm publish" 29 | }, 30 | "main": "dist/index.js", 31 | "module": "dist/index.mjs", 32 | "types": "dist/index.d.ts", 33 | "exports": { 34 | ".": { 35 | "bun": "./dist/index.mjs", 36 | "node": "./dist/index.js", 37 | "require": "./dist/index.js", 38 | "import": "./dist/index.mjs", 39 | "default": "./dist/index.js", 40 | "types": "./dist/index.d.ts" 41 | } 42 | }, 43 | "dependencies": { 44 | "memoirist": "0.1.4" 45 | }, 46 | "peerDependencies": { 47 | "typescript": ">= 5.0.0", 48 | "@whatwg-node/server": ">= 0.9.0" 49 | }, 50 | "optionalDependencies": { 51 | "@whatwg-node/server": ">= 0.9.0" 52 | }, 53 | "peerDependenciesMeta": { 54 | "@whatwg-node/server": { 55 | "optional": true 56 | } 57 | }, 58 | "devDependencies": { 59 | "@whatwg-node/server": "^0.9.22", 60 | "bun-types": "^1.0.15", 61 | "rimraf": "4.4.1", 62 | "zod": "^3.22.4" 63 | } 64 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Memoirist } from 'memoirist' 2 | import { literal } from 'zod' 3 | 4 | const parseBodyError = (error: Error) => 5 | Response.json(error, { 6 | status: 400 7 | }) 8 | 9 | export const mapResponse = (response: unknown): Response => { 10 | switch (response?.constructor?.name) { 11 | case 'String': 12 | return new Response(response as string) 13 | 14 | case 'Blob': 15 | const size = (response as File).size 16 | if (size) 17 | return new Response(response as Blob, { 18 | headers: { 19 | 'accept-ranges': 'bytes', 20 | 'content-range': `bytes 0-${size - 1}/${size}` 21 | } 22 | }) 23 | else return new Response(response as Blob) 24 | 25 | case 'Object': 26 | case 'Array': 27 | return Response.json(response as Record) 28 | 29 | case 'ReadableStream': 30 | return new Response(response as ReadableStream, { 31 | headers: { 32 | 'Content-Type': 'text/event-stream; charset=utf-8' 33 | } 34 | }) 35 | 36 | case undefined: 37 | if (!response) return new Response('') 38 | 39 | return new Response(JSON.stringify(response), { 40 | headers: { 41 | 'content-type': 'application/json' 42 | } 43 | }) 44 | 45 | case 'Response': 46 | return response as Response 47 | 48 | case 'Error': 49 | return Response.json(response as Error, { 50 | status: 500 51 | }) 52 | 53 | case 'Promise': 54 | // @ts-ignore 55 | return (response as any as Promise).then((x) => { 56 | const r = mapResponse(x) 57 | if (r !== undefined) return r 58 | 59 | return new Response('') 60 | }) 61 | 62 | // ? Maybe response or Blob 63 | case 'Function': 64 | return mapResponse((response as Function)()) 65 | 66 | case 'Number': 67 | case 'Boolean': 68 | return new Response((response as number | boolean).toString()) 69 | 70 | default: 71 | const r = JSON.stringify(response) 72 | if (r.charCodeAt(0) === 123) 73 | return new Response(JSON.stringify(response), { 74 | headers: { 75 | 'Content-Type': 'application/json' 76 | } 77 | }) as any 78 | 79 | return new Response(r) 80 | } 81 | } 82 | 83 | const camelCaseToPath = (str: string) => 84 | str 85 | .replace(/([A-Z])/g, '/$1') 86 | .trim() 87 | .toLowerCase() 88 | 89 | const getPath = (url: string) => { 90 | const s = url.indexOf('/', 11) 91 | const qi = url.indexOf('?', s + 1) 92 | return url.substring(s, qi === -1 ? undefined : qi) 93 | } 94 | 95 | type Prettify = { 96 | [K in keyof T]: T[K] 97 | } & {} 98 | 99 | type UnionToIntersection = (U extends any ? (x: U) => void : never) extends ( 100 | x: infer I 101 | ) => void 102 | ? I 103 | : never 104 | 105 | export type Merge[]> = Prettify< 106 | UnionToIntersection 107 | > 108 | 109 | export const sirine = []>( 110 | ...modules: Modules 111 | ) => { 112 | const router = new Memoirist<{ handle: Function; useRequest: boolean }>() 113 | 114 | for (const module of modules) 115 | for (const [key, value] of Object.entries(module)) { 116 | if (typeof value === 'function') { 117 | const literal = (value as Function).toString() 118 | const parameter = literal.split('\n', 1)[0] 119 | const noParameter = parameter.includes('()') 120 | 121 | router.add( 122 | noParameter ? 'GET' : 'POST', 123 | key === 'index' || key === 'default' 124 | ? '/' 125 | : `/${camelCaseToPath(key)}`, 126 | { 127 | handle: value, 128 | useRequest: literal.includes('this') 129 | } 130 | ) 131 | } 132 | } 133 | 134 | const handle = (request: Request) => { 135 | const route = router.find(request.method, getPath(request.url)) 136 | 137 | if (route) { 138 | const handle = route.store.useRequest 139 | ? route.store.handle.bind(request) 140 | : route.store.handle 141 | 142 | if (request.method === 'GET') return mapResponse(handle()) 143 | else { 144 | const contentType = request.headers.get('content-type') 145 | 146 | if (contentType) 147 | return (async () => { 148 | const body = contentType?.startsWith( 149 | 'multipart/form-data' 150 | ) 151 | ? await request.formData().catch(parseBodyError) 152 | : await request.json().catch(parseBodyError) 153 | 154 | if (body instanceof Response) return body 155 | 156 | try { 157 | return mapResponse(handle(...body)) 158 | } catch (error) { 159 | if (error instanceof Error) 160 | return Response.json(error, { 161 | status: 500 162 | }) 163 | 164 | return Response.json( 165 | { 166 | name: 'error', 167 | message: 'unknown error' 168 | }, 169 | { 170 | status: 500 171 | } 172 | ) 173 | } 174 | })() 175 | 176 | return mapResponse(handle()) 177 | } 178 | } 179 | 180 | return new Response('NOT_FOUND') 181 | } 182 | 183 | let server: unknown 184 | 185 | return { 186 | router, 187 | handle, 188 | server, 189 | listen(port: number) { 190 | // @ts-ignore 191 | if (typeof Bun !== 'undefined') { 192 | // @ts-ignore 193 | server = Bun.serve({ 194 | port, 195 | reusePort: true, 196 | fetch: handle 197 | }) 198 | 199 | return this 200 | } 201 | 202 | // @ts-ignore 203 | if (typeof Deno !== 'undefined') { 204 | // @ts-ignore 205 | server = Deno.serve({ port }, handle) 206 | 207 | return this 208 | } 209 | 210 | if (typeof process?.versions?.node !== 'undefined') { 211 | ;(async () => { 212 | const { createServer } = await import('node:http') 213 | const polyfill = await import('@whatwg-node/server') 214 | 215 | if (!polyfill.createServerAdapter) 216 | throw new Error( 217 | "Please install '@whatwg-node/server' to use Sirine on Node.js" 218 | ) 219 | 220 | return (server = createServer( 221 | polyfill.createServerAdapter(handle) 222 | ).listen(port)) 223 | })() 224 | 225 | return this 226 | } 227 | 228 | throw new Error('Unknown environment') 229 | 230 | return this 231 | }, 232 | types: undefined as unknown as Merge 233 | } as const 234 | } 235 | 236 | const deserialize = (response: Response) => { 237 | if (response.headers.get('content-type')?.startsWith('application/json')) 238 | return response.json() 239 | 240 | return response.text().then((value) => { 241 | if (!Number.isNaN(+value)) return +value 242 | if (value === 'true') return true 243 | if (value === 'false') return false 244 | return value 245 | }) 246 | } 247 | 248 | export const client = any>>( 249 | url: string 250 | ): { 251 | [K in keyof Module]: ( 252 | ...body: Parameters 253 | ) => Promise> 254 | } => { 255 | if (url.endsWith('/')) url = url.slice(0, -1) 256 | 257 | return new Proxy( 258 | {}, 259 | { 260 | get: (_, key) => { 261 | return (...body: unknown[]) => { 262 | const path = 263 | url + 264 | (key === 'index' || key === 'default' 265 | ? '/' 266 | : `/${camelCaseToPath(key as string)}`) 267 | 268 | if (body === undefined || body.length === 0) 269 | return fetch(path).then(deserialize) 270 | 271 | if (body === null) 272 | return fetch(path, { 273 | method: 'POST', 274 | body: 'null', 275 | headers: { 276 | 'content-type': 'application/json' 277 | } 278 | }).then(deserialize) 279 | 280 | if ( 281 | body.length === 1 && 282 | body[0] !== null && 283 | typeof body[0] === 'object' 284 | ) { 285 | const formData = new FormData() 286 | let isFormData = false 287 | 288 | for (const [key, value] of Object.entries(body[0])) { 289 | if (value instanceof File || value instanceof Blob) 290 | isFormData = true 291 | 292 | // @ts-ignore 293 | formData.append(key, value) 294 | } 295 | 296 | if (isFormData) 297 | return fetch(path, { 298 | method: 'POST', 299 | body: formData 300 | }).then(deserialize) 301 | } 302 | 303 | return fetch(path, { 304 | method: 'POST', 305 | body: JSON.stringify(body), 306 | headers: { 307 | 'content-type': 'application/json' 308 | } 309 | }).then(deserialize) 310 | } 311 | } 312 | } 313 | ) as any 314 | } 315 | -------------------------------------------------------------------------------- /test/client.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { sirine, client } from '../src' 3 | 4 | const server = sirine({ 5 | get: () => 'Sirin', 6 | say: (word: string) => word, 7 | sum: (a: number, b: number) => a + b, 8 | json: (value: Object) => value 9 | }).listen(3000) 10 | 11 | type server = typeof server 12 | 13 | const app = client('http://localhost:3000') 14 | 15 | describe('Sirine', () => { 16 | it('get request', async () => { 17 | expect(await app.get()).toBe('Sirin') 18 | }) 19 | 20 | it('send paramter', async () => { 21 | expect(await app.say('Bella')).toBe('Bella') 22 | }) 23 | 24 | it('multiple paramters', async () => { 25 | expect(await app.sum(1, 2)).toBe(3) 26 | }) 27 | 28 | it('deserialize JSON', async () => { 29 | expect( 30 | await app.json({ 31 | hello: 'world' 32 | }) 33 | ).toEqual({ 34 | hello: 'world' 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/server.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { sirine } from '../src' 3 | 4 | const get = (path: string) => new Request(`http://localhost${path}`) 5 | 6 | const json = (path: string, body: Object) => 7 | new Request(`http://localhost${path}`, { 8 | method: 'POST', 9 | body: JSON.stringify(body), 10 | headers: { 11 | 'content-type': 'application/json' 12 | } 13 | }) 14 | 15 | describe('Sirine', () => { 16 | it('handle request', async () => { 17 | const { handle } = sirine({ 18 | hello: () => 'Sirin' 19 | }) 20 | 21 | const response = await handle(get('/hello')) 22 | const result = await response.text() 23 | 24 | expect(result).toBe('Sirin') 25 | }) 26 | 27 | it('handle async', async () => { 28 | const { handle } = sirine({ 29 | hello: async () => 'Sirin' 30 | }) 31 | 32 | const response = await handle(get('/hello')) 33 | const result = await response.text() 34 | 35 | expect(result).toBe('Sirin') 36 | }) 37 | 38 | it('handle index', async () => { 39 | const { handle } = sirine({ 40 | index: () => 'Sirin' 41 | }) 42 | 43 | const response = await handle(get('/')) 44 | const result = await response.text() 45 | 46 | expect(result).toBe('Sirin') 47 | }) 48 | 49 | it('accept text', async () => { 50 | const { handle } = sirine({ 51 | hello: (word: string) => word 52 | }) 53 | 54 | const response = await handle(json('/hello', ['Bella'])) 55 | const result = await response.text() 56 | 57 | expect(result).toBe('Bella') 58 | }) 59 | 60 | it('accept JSON', async () => { 61 | const { handle } = sirine({ 62 | hello: (body: { name: string }) => body 63 | }) 64 | 65 | const response = await handle( 66 | json('/hello', [ 67 | { 68 | name: 'Bella' 69 | } 70 | ]) 71 | ) 72 | const result = await response.json() 73 | 74 | expect(result).toEqual({ 75 | name: 'Bella' 76 | }) 77 | }) 78 | 79 | it('throw error', async () => { 80 | const { handle } = sirine({ 81 | hello: () => { 82 | return new Error('Sirin') 83 | } 84 | }) 85 | 86 | const response = await handle(get('/hello')) 87 | const result = response.status 88 | 89 | expect(result).toEqual(500) 90 | }) 91 | 92 | it('map camelCase to path', async () => { 93 | const { handle } = sirine({ 94 | goodMorning: () => 'Nice' 95 | }) 96 | 97 | const response = await handle(get('/good/morning')) 98 | const result = await response.text() 99 | 100 | expect(result).toEqual('Nice') 101 | }) 102 | 103 | it('accept request', async () => { 104 | const { handle } = sirine({ 105 | hi(this: Request) { 106 | return this.url 107 | } 108 | }) 109 | 110 | const response = await handle(get('/hi')) 111 | const result = await response.text() 112 | 113 | expect(result).toEqual('http://localhost/hi') 114 | }) 115 | 116 | it('multiple instance', async () => { 117 | const { handle } = sirine( 118 | {}, 119 | { 120 | a: () => 'Sirin' 121 | }, 122 | { 123 | b: () => 'Bella' 124 | } 125 | ) 126 | 127 | { 128 | const response = await handle(get('/a')) 129 | const result = await response.text() 130 | 131 | expect(result).toBe('Sirin') 132 | } 133 | 134 | { 135 | const response = await handle(get('/b')) 136 | const result = await response.text() 137 | 138 | expect(result).toBe('Bella') 139 | } 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ESNext", "DOM", "ScriptHost"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "CommonJS", /* Specify what module code is generated. */ 29 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "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. */ 52 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 102 | }, 103 | "include": ["src/**/*"], 104 | "exclude": ["node_modules"] 105 | } 106 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "react-jsx", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | } 22 | } 23 | --------------------------------------------------------------------------------