bun-router 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # Bun Router
2
+
3
+ I needed a router for `Bun`, so I made one. It's simple, naive, and hardly anything is abstracted.
4
+
5
+ ### Usage
6
+ Import the `router`.
7
+ ```typescript
8
+ import { router } from 'bun-router';
9
+ ```
10
+
11
+ Create the `router`.
12
+ ```typescript
13
+ const r = router(3000)
14
+ ```
15
+
16
+ Add routes to the `router`.
17
+ ```typescript
18
+ r.add('/', 'GET', req => new Response('Hello World'));
19
+ ```
20
+
21
+ The `req` parameter is of type `HttpRequest` which is just a type that contains both `Response` and `Params` for URL parameters.
22
+
23
+ Start the server.
24
+ ```typescript
25
+ r.serve()
26
+ ```
27
+
28
+ Some overly-simple examples:
29
+ ```typescript
30
+ import { router, json } from '..';
31
+
32
+ const r = router();
33
+
34
+ const pets = {
35
+ dogs: ['Joey', 'Benny', 'Max'],
36
+ cats: ['Charles', 'Arya', 'Binx'],
37
+ }
38
+
39
+ const foods = {
40
+ apple: '🍎', banana: '🍌', strawberry: '🍓', pear: '🍐',
41
+ }
42
+
43
+ r.add('/pets/:type', 'GET', req => {
44
+ const petType = req.params.get('type') as keyof typeof pets;
45
+ return json(pets[petType] ?? 'not found');
46
+ });
47
+ r.add('/grocery/:food', 'GET', req => {
48
+ const food = req.params.get('food') as keyof typeof foods
49
+ return json(foods[food] ?? 'not found')
50
+ });
51
+
52
+ r.serve();
53
+ ```
54
+
55
+ ```typescript
56
+ import { router, json } from "..";
57
+
58
+ const r = router();
59
+
60
+
61
+ const cacher = () => {
62
+ const cache = new Map();
63
+ return {
64
+ set: (key: string, value: string) => {
65
+ cache.set(key, value);
66
+ },
67
+ get: (key: string) => cache.get(key)!,
68
+ }
69
+ }
70
+
71
+ const cache = cacher();
72
+
73
+ const rand = (max: number) => Math.floor(Math.random() * max);
74
+
75
+ r.add('/set', 'POST', req => {
76
+ const url = new URL(req.request.url);
77
+ const query = url.searchParams;
78
+
79
+ const name = query.get('name')!;
80
+
81
+ cache.set(name, `${rand(1000)}`)
82
+
83
+ return new Response('thank you for joining\n');
84
+ });
85
+
86
+ r.add('/get/:key', 'GET', req => {
87
+ const name = req.params.get('key')
88
+ const result = cache.get(name ?? '');
89
+
90
+ if (!result) return new Response('not found')
91
+
92
+ return json(`Welcome user ID: ${result}`)
93
+
94
+ });
95
+
96
+ r.serve();
97
+ ```
98
+
package/bun.lockb ADDED
Binary file
@@ -0,0 +1,41 @@
1
+ import { router, json } from "..";
2
+
3
+ const r = router();
4
+
5
+
6
+ const cacher = () => {
7
+ const cache = new Map();
8
+ return {
9
+ set: (key: string, value: string) => {
10
+ cache.set(key, value);
11
+ },
12
+ get: (key: string) => cache.get(key)!,
13
+ }
14
+ }
15
+
16
+ const cache = cacher();
17
+
18
+ const rand = (max: number) => Math.floor(Math.random() * max);
19
+
20
+ r.add('/set', 'POST', req => {
21
+ const url = new URL(req.request.url);
22
+ const query = url.searchParams;
23
+
24
+ const name = query.get('name')!;
25
+
26
+ cache.set(name, `${rand(1000)}`)
27
+
28
+ return new Response('thank you for joining\n');
29
+ });
30
+
31
+ r.add('/get/:key', 'GET', req => {
32
+ const name = req.params.get('key')
33
+ const result = cache.get(name ?? '');
34
+
35
+ if (!result) return new Response('not found')
36
+
37
+ return json(`Welcome user ID: ${result}`)
38
+
39
+ });
40
+
41
+ r.serve();
@@ -0,0 +1,12 @@
1
+ import { router, html } from '..';
2
+ import path from 'path';
3
+
4
+ const r = router();
5
+
6
+ r.add('/page/:name', 'GET', async req => {
7
+ const pageName = req.params.get('name')!;
8
+ const fullpath = path.join('.', 'examples', 'pages', pageName+'.html');
9
+ return html(fullpath);
10
+ });
11
+
12
+ r.serve();
@@ -0,0 +1,9 @@
1
+ import { router, html } from '..';
2
+
3
+ Bun.serve
4
+
5
+ const r = router(3000);
6
+
7
+ r.add('/', 'GET', () => html('./examples/pages/index.html'));
8
+
9
+ r.serve();
@@ -0,0 +1,37 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=>, initial-scale=1.0">
6
+ <title>Document</title>
7
+ </head>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ font-family: sans-serif;
14
+ }
15
+
16
+ body {
17
+ min-width: 100vw;
18
+ min-height: 100vh;
19
+ background: white;
20
+ }
21
+
22
+ main {
23
+ width: 75%;
24
+ line-height: 2;
25
+ padding: 0.5em;
26
+ margin: 0.125em;
27
+ box-shadow: 2px 2px 5px rgba(0,0,0,0.7);
28
+ }
29
+ </style>
30
+ <body>
31
+ <main>
32
+ <h1>Foobar</h1>
33
+ <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Dicta a atque perspiciatis eos ipsa facere ut laborum commodi, quibusdam doloribus quia numquam iusto fugit. Mollitia quam at voluptas ipsam blanditiis.</p>
34
+ <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolor unde tempora illo vitae dignissimos, sed blanditiis velit ipsa aut, veritatis voluptates! Exercitationem excepturi dolorum nesciunt numquam assumenda similique aliquam dicta veritatis magnam tenetur dolor, harum quos? Accusamus autem in culpa velit earum. Error repudiandae dolore pariatur reprehenderit molestiae quam suscipit.</p>
35
+ </main>
36
+ </body>
37
+ </html>
@@ -0,0 +1,79 @@
1
+ <!DOCTYPE html>
2
+ <meta charset=UTF-8>
3
+ <meta name=viewport content=width=device-width, initial-scale=1.0>
4
+ <title>Bun Router</title>
5
+
6
+ <style>
7
+ * {
8
+ margin: 0;
9
+ padding: 0;
10
+ box-sizing: border-box;
11
+ font-family: sans-serif;
12
+ }
13
+
14
+
15
+ main div {
16
+ width: 85%;
17
+ border-radius: 8px;
18
+ box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2);
19
+ padding: 4em;
20
+ background: #18191a;
21
+ color: white;
22
+ }
23
+
24
+ main div a {
25
+ background: none;
26
+ color: dodgerblue;
27
+ text-decoration: none;
28
+ font-weight: 200;
29
+ }
30
+
31
+
32
+ a:not(div a) {
33
+ text-decoration: none;
34
+ min-width: 128px;
35
+ text-align: center;
36
+ color: dodgerblue;
37
+ display: block;
38
+ padding: 0.85em;
39
+ margin: 0.25em;
40
+ background: #18191a;
41
+ border-radius: 5px;
42
+ }
43
+
44
+ main a:hover {
45
+ color: skyblue;
46
+ transition: color 100ms ease;
47
+ }
48
+
49
+ main {
50
+ display: flex;
51
+ background: #0d0d0d;
52
+ flex-flow: column;
53
+ justify-content: center;
54
+ align-items: center;
55
+ width: 100vw;
56
+ height: 100vh;
57
+ padding: 1.25em;
58
+ }
59
+
60
+ main section {
61
+ display: flex;
62
+ }
63
+
64
+
65
+ </style>
66
+
67
+
68
+
69
+ <main>
70
+ <div>
71
+ <h1>Bun Router</h1>
72
+ <p>A simple router, for <a href=https://bun.sh>Bun.sh.</a></p>
73
+ </div>
74
+ <section>
75
+ <a href=https://github.com/aboxofsox/bun-router/docs>Docs</a>
76
+ <a href=https://github.com/aboxofsox/bun-router/examples>Examples</a>
77
+ <a href=https://google.com>Why</a>
78
+ </section>
79
+ </main>
@@ -0,0 +1,24 @@
1
+ import { router, json } from '..';
2
+
3
+ const r = router();
4
+
5
+ const pets = {
6
+ dogs: ['Joey', 'Benny', 'Max'],
7
+ cats: ['Charles', 'Arya', 'Binx'],
8
+ }
9
+
10
+ const foods = {
11
+ apple: '🍎', banana: '🍌', strawberry: '🍓', pear: '🍐',
12
+ }
13
+
14
+ r.add('/pets/:type', 'GET', req => {
15
+ const petType = req.params.get('type') as keyof typeof pets;
16
+ return json(pets[petType] ?? 'not found');
17
+ });
18
+ r.add('/grocery/:food', 'GET', req => {
19
+ const food = req.params.get('food') as keyof typeof foods
20
+ return json(foods[food] ?? 'not found')
21
+ });
22
+
23
+ r.serve();
24
+
@@ -0,0 +1,38 @@
1
+ import { router, json, } from '..';
2
+
3
+ const r = router(3030);
4
+
5
+ const userStore = new Map();
6
+ const rando = (max: number) => `${Math.floor(Math.random() * max)}`
7
+
8
+ r.add('/new', 'POST', req => {
9
+ const url = new URL(req.request.url);
10
+ const query = url.searchParams;
11
+
12
+ const name = query.get('name');
13
+ const email = query.get('email');
14
+
15
+ if (typeof name === 'undefined' || typeof email === 'undefined')
16
+ return new Response('invalid query parameters');
17
+
18
+
19
+ const id = rando(2_000_000);
20
+ userStore.set(id, {name: name, email: email});
21
+
22
+ const message = `Thank you, ${name} for registering your email: ${email}.\nYour ID is ${id}`
23
+ return new Response(message);
24
+ });
25
+
26
+ r.add('/user/:id', 'GET', req => {
27
+ const id = req.params.get('id');
28
+ if (typeof id === 'undefined')
29
+ return new Response('invalid id');
30
+
31
+ const user = userStore.get(id);
32
+ if (typeof user === 'undefined')
33
+ return new Response('not found');
34
+
35
+ return json(user);
36
+ });
37
+
38
+ r.serve();
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export type HttpHandler = (r: Request) => Response
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/router/router';
@@ -0,0 +1,28 @@
1
+ import { TLSOptions, TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptions, TLSServeOptions } from 'bun';
2
+
3
+
4
+ type HttpRequest = {
5
+ request: Request,
6
+ params: Map<string, string>,
7
+ }
8
+
9
+ type Route = {
10
+ pattern: string,
11
+ method: string,
12
+ callback: (req: HttpRequest) => Response | Promise<Response>
13
+ }
14
+
15
+ type Options = ServeOptions
16
+ | TLSServeOptions
17
+ | WebSocketServeOptions
18
+ | TLSWebSocketServeOptions
19
+ | undefined
20
+
21
+
22
+ type Router = (port?: number | string, options?: Options) => {
23
+ add: (pattern: string, method: string, callback: (req: HttpRequest) => Response | Promise<Response>) => void,
24
+ serve: () => void,
25
+ }
26
+
27
+
28
+ export { HttpRequest, Route, Router, Options }
@@ -0,0 +1,101 @@
1
+ import { Route, Router, HttpRequest, Options } from './router.d';
2
+
3
+ const notFound = async (): Promise<Response> => {
4
+ const response = new Response('not found', {
5
+ status: 404,
6
+ statusText: 'not found',
7
+ headers: { 'Content-Type': 'text/html' },
8
+ });
9
+
10
+ return new Promise((resolve) => {
11
+ resolve(response);
12
+ });
13
+ }
14
+
15
+ const html = async (filepath: string): Promise<Response> => {
16
+ const file = Bun.file(filepath);
17
+ const exists = await file.exists();
18
+
19
+ if (!exists)
20
+ return notFound();
21
+
22
+ const content = await file.text();
23
+ if (content === '')
24
+ return notFound();
25
+
26
+ const response = new Response(content, {
27
+ status: 200,
28
+ statusText: 'ok',
29
+ headers: { 'Content-Type': 'text/html' },
30
+ });
31
+
32
+ return new Promise<Response>((resolve) => {
33
+ resolve(response);
34
+ });
35
+ }
36
+
37
+ const json = (data: any): Response => {
38
+ const jsonString = JSON.stringify(data);
39
+
40
+ const res = new Response(jsonString);
41
+ res.headers.set('Content-Type', 'application/json');
42
+
43
+ return res
44
+ }
45
+
46
+ const extractParams = (route: Route, req: HttpRequest) => {
47
+ const url = new URL(req.request.url);
48
+ const pathSegments = route.pattern.split('/');
49
+ const urlSegments = url.pathname.split('/');
50
+
51
+ if (pathSegments.length !== urlSegments.length) return
52
+
53
+ for (let i = 0; i < pathSegments.length; i++) {
54
+ if ((pathSegments[i][0] === ':') && (pathSegments[i - 1] === urlSegments[i - 1])) {
55
+ const k = pathSegments[i].replace(':', '');
56
+ const v = urlSegments[i];
57
+ req.params.set(k, v);
58
+ }
59
+ }
60
+ }
61
+
62
+ const match = (route: Route, req: HttpRequest): boolean => {
63
+ return req.params.size !== 0 && route.method === req.request.method
64
+ }
65
+
66
+ const router: Router = (port?: number | string, options?: Options) => {
67
+ const routes: Array<Route> = new Array();
68
+
69
+ return {
70
+ add: (pattern: string, method: string, callback: (req: HttpRequest) => Response | Promise<Response>) => {
71
+ routes.push({
72
+ pattern: pattern,
73
+ method: method,
74
+ callback: callback,
75
+ })
76
+ },
77
+ serve: () => {
78
+ console.log(`[bun-router]: Listening on port -> :${ port ?? 3000 }`)
79
+ Bun.serve({
80
+ port: port ?? 3000,
81
+ ...options,
82
+ fetch(req) {
83
+ const url = new URL(req.url);
84
+ for (const route of routes) {
85
+ const httpRequest: HttpRequest = {
86
+ request: req,
87
+ params: new Map(),
88
+ };
89
+
90
+ extractParams(route, httpRequest);
91
+
92
+ if (match(route, httpRequest)) return route.callback(httpRequest);
93
+ }
94
+ return new Response('not found');
95
+ }
96
+ });
97
+ },
98
+ }
99
+ }
100
+
101
+ export { router, json, html, extractParams }
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "bun-router",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "devDependencies": {
6
+ "bun-types": "latest"
7
+ },
8
+ "peerDependencies": {
9
+ "typescript": "^5.0.0"
10
+ },
11
+ "version": "0.2.0"
12
+ }
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+
3
+ PARAMS_TEST=(
4
+ "http://localhost:3000/pets/dogs"
5
+ "http://localhost:3000/grocery/strawberry"
6
+ )
7
+
8
+ echo "Starting test server"
9
+ bun run ./examples/params.ts &
10
+
11
+ SERVER_PID=$!
12
+
13
+ function stop_server {
14
+ echo "Stopping the server $SERVER_PID"
15
+ kill $SERVER_PID
16
+ }
17
+
18
+ trap stop_server EXIT
19
+
20
+ sleep .1
21
+
22
+ expected='["Joey","Benny","Max"]'
23
+ response=$(curl -sS "${PARAMS_TEST[0]}")
24
+ response_trimmed=$(echo "$response" | xargs -0)
25
+ expected_trimmed=$(echo "$expected" | xargs -0)
26
+
27
+ if [ "$response_trimmed" = "$expected_trimmed" ]; then
28
+ echo "Passed."
29
+ else
30
+ echo "$response_trimmed"
31
+ echo "$expected_trimmed"
32
+ echo "Failed."
33
+ fi
34
+
35
+ expected="🍓"
36
+ response=$(curl -s "${PARAMS_TEST[1]}")
37
+ if [ "$response" = "$expected" ]; then
38
+ echo "Failed: ${PARAMS_TEST[1]}"
39
+ else
40
+ echo "Passed."
41
+ fi
42
+
@@ -0,0 +1,47 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { html, json, extractParams } from '..';
3
+ import { HttpRequest, Route } from '../lib/router/router.d';
4
+
5
+ describe('Helpers', async () => {
6
+ test('html', async () => {
7
+ const fp = './examples/pages/index.html';
8
+ const res = await html(fp);
9
+ const contentType = res.headers.get('Content-Type');
10
+
11
+ expect(contentType).toBe('text/html');
12
+ expect(res.status).toBe(200);
13
+ });
14
+
15
+ test('json', async () => {
16
+ const test = { message: 'ok' }
17
+ const res = await json(test);
18
+ const jsn = await res.json();
19
+
20
+ expect(jsn).toStrictEqual({ message: 'ok' })
21
+
22
+ });
23
+
24
+ test('extract params 1', () => {
25
+ const route: Route = {pattern: '/:name', method: 'GET', callback: (req) => new Response('ok')};
26
+ const httpRequest: HttpRequest = {
27
+ request: new Request('http://localhost:3000/foo'),
28
+ params: new Map(),
29
+ }
30
+
31
+ extractParams(route, httpRequest);
32
+ const name = httpRequest.params.get('name');
33
+ expect(name).toBe('foo');
34
+ });
35
+
36
+ test('extract params 2', () =>{
37
+ const route: Route = {pattern: '/foo/:name', method: 'GET', callback: (req) => new Response('ok')};
38
+ const httpRequest: HttpRequest = {
39
+ request: new Request('http://localhost:3000/foo/bar'),
40
+ params: new Map(),
41
+ }
42
+
43
+ extractParams(route, httpRequest);
44
+ const name = httpRequest.params.get('name');
45
+ expect(name).toBe('bar');
46
+ });
47
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ESNext"],
4
+ "module": "esnext",
5
+ "target": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "moduleDetection": "force",
8
+ "allowImportingTsExtensions": true,
9
+ "strict": true,
10
+ "downlevelIteration": true,
11
+ "skipLibCheck": true,
12
+ "jsx": "preserve",
13
+ "allowSyntheticDefaultImports": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "allowJs": true,
16
+ "noEmit": true,
17
+ "types": [
18
+ "bun-types" // add Bun global
19
+ ]
20
+ }
21
+ }