bun-router 0.6.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +20 -63
- package/examples/basic.ts +6 -11
- package/examples/db.ts +10 -0
- package/examples/dynamic.ts +14 -19
- package/examples/logger.ts +6 -11
- package/examples/static.ts +2 -2
- package/examples/todo.ts +2 -2
- package/lib/fs/fsys.ts +1 -1
- package/lib/http/{generic-methods.ts → http.ts} +17 -3
- package/lib/logger/color.ts +7 -7
- package/lib/logger/logger.d.ts +4 -5
- package/lib/logger/logger.ts +65 -63
- package/lib/router/context.ts +49 -0
- package/lib/router/router.d.ts +28 -28
- package/lib/router/router.ts +44 -120
- package/lib/router/tree.ts +63 -0
- package/lib/util/strings.ts +3 -0
- package/package.json +1 -1
- package/tests/router.test.ts +0 -64
- package/examples/cookies.ts +0 -15
- package/examples/sqlite.ts +0 -22
package/README.md
CHANGED
@@ -1,83 +1,40 @@
|
|
1
|
-
# Bun
|
2
|
-
|
3
|
-
I needed a router for `Bun`, so I made one. It's simple, naive, and hardly anything is abstracted.
|
1
|
+
# Bun router
|
4
2
|
|
5
3
|
### Usage
|
6
|
-
|
7
|
-
import { router } from 'bun-router';
|
8
|
-
|
9
|
-
const r = router();
|
10
|
-
|
11
|
-
r.add('/', 'GET', (ctx) => new Response('Hello World'));
|
12
|
-
|
13
|
-
r.serve();
|
14
|
-
```
|
15
|
-
#### Static Files
|
16
|
-
```typescript
|
17
|
-
import { router } from 'bun-router';
|
18
|
-
|
19
|
-
const r = router();
|
20
|
-
|
21
|
-
r.static('/', './pages');
|
22
|
-
|
23
|
-
r.serve();
|
24
|
-
```
|
4
|
+
`npm i -s bun-router`
|
25
5
|
|
26
|
-
|
27
|
-
```typescript
|
28
|
-
import {router, html, json } from 'bun-router';
|
6
|
+
or
|
29
7
|
|
30
|
-
|
8
|
+
`bun i bun-router`
|
31
9
|
|
32
|
-
r.add('/', (ctx) => html('<h1>Hello World</h1>'));
|
33
10
|
|
34
|
-
|
35
|
-
|
36
|
-
|
11
|
+
#### Example
|
12
|
+
```ts
|
13
|
+
import { Router, http } from 'bun-router';
|
37
14
|
|
38
|
-
|
39
|
-
});
|
15
|
+
const router = Router();
|
40
16
|
|
41
|
-
|
17
|
+
router.add('/', 'GET', () => http.ok());
|
42
18
|
|
43
|
-
|
44
|
-
const
|
45
|
-
if (!id) return new Response('user not found', {status: 404});
|
46
|
-
|
47
|
-
const user = store.get(id);
|
19
|
+
router.get('/u/:username', ctx => {
|
20
|
+
const username = ctx.params.get('username');
|
48
21
|
|
49
|
-
if (!
|
22
|
+
if (!username) return http.badRequest();
|
50
23
|
|
51
|
-
return json(
|
24
|
+
return ctx.json({ username: username });
|
52
25
|
});
|
53
26
|
|
54
|
-
|
27
|
+
router.serve();
|
55
28
|
```
|
56
29
|
|
57
|
-
|
30
|
+
##### Static
|
58
31
|
```ts
|
59
|
-
import {
|
60
|
-
import { Database } from 'bun:sqlite';
|
61
|
-
|
62
|
-
const r = router(3000, {db: './examples/dbs/test.db'});
|
32
|
+
import { Router } from 'bun-router';
|
63
33
|
|
64
|
-
|
65
|
-
const name = ctx.params.get('name');
|
66
|
-
const rando = Math.floor(Math.random()*1000);
|
34
|
+
const router = Router();
|
67
35
|
|
68
|
-
|
69
|
-
|
70
|
-
return json({message: 'ok'});
|
71
|
-
});
|
36
|
+
router.static('/assets', 'static');
|
72
37
|
|
73
|
-
|
74
|
-
|
75
|
-
const data = ctx.db.query(`SELECT * FROM test WHERE name = "${name}";`).get();
|
76
|
-
const d = data as {id: number, name: string};
|
77
|
-
|
78
|
-
return d ? json(d) : new Response('not found', {status: 404});
|
79
|
-
});
|
80
|
-
|
81
|
-
r.serve();
|
38
|
+
router.serve();
|
39
|
+
```
|
82
40
|
|
83
|
-
```
|
package/examples/basic.ts
CHANGED
@@ -1,18 +1,13 @@
|
|
1
|
-
import {
|
1
|
+
import { Router, http } from '..';
|
2
2
|
|
3
|
-
const
|
3
|
+
const router = Router();
|
4
4
|
|
5
|
-
|
5
|
+
router.add('/', 'GET', () => http.json(200, 'ok'));
|
6
6
|
|
7
|
-
|
7
|
+
router.add('/user/:name', 'GET', (ctx) => {
|
8
8
|
const name = ctx.params.get('name');
|
9
|
+
if (!name) return http.json(500, 'no name');
|
9
10
|
return http.json(200, name);
|
10
11
|
});
|
11
12
|
|
12
|
-
|
13
|
-
const name = ctx.params.get('name');
|
14
|
-
const id = ctx.params.get('id');
|
15
|
-
return http.json(200, {name: name, id: id});
|
16
|
-
});
|
17
|
-
|
18
|
-
r.serve();
|
13
|
+
router.serve();
|
package/examples/db.ts
ADDED
package/examples/dynamic.ts
CHANGED
@@ -1,29 +1,24 @@
|
|
1
|
-
import {
|
1
|
+
import { Router, http } from '..';
|
2
2
|
import { Context } from '../lib/router/router.d';
|
3
3
|
|
4
|
-
const
|
5
|
-
const name = ctx.params.get('name');
|
6
|
-
if (typeof name === 'undefined' || name === '') return http.html(500, '<h6 style="color: red">User Undefined</h6>');
|
7
|
-
return http.html(200, `<h4>Hello, ${name}!</h4>`);
|
8
|
-
}
|
4
|
+
const home = () => new Response('Welcome Home', { status: 200 });
|
9
5
|
|
10
|
-
const
|
11
|
-
const
|
12
|
-
if (
|
13
|
-
return http.
|
6
|
+
const subreddit = (ctx: Context) => {
|
7
|
+
const sub = ctx.params.get('subreddit');
|
8
|
+
if (!sub) return http.json(400, { error: 'no subreddit provided' });
|
9
|
+
return http.json(200, { subreddit: sub });
|
14
10
|
}
|
15
11
|
|
16
|
-
const
|
17
|
-
const
|
18
|
-
if (
|
19
|
-
return http.
|
12
|
+
const user = (ctx: Context) => {
|
13
|
+
const user = ctx.params.get('user');
|
14
|
+
if (!user) return http.json(400, { error: 'no user provided' });
|
15
|
+
return http.json(200, { user: user });
|
20
16
|
}
|
21
17
|
|
22
|
-
const r =
|
23
|
-
|
24
|
-
r.add('/u/:name', 'GET', handler);
|
25
|
-
r.add('/s/:name', 'GET', space);
|
26
|
-
r.add('/u/:name/settings', 'GET', handleSettings);
|
18
|
+
const r = Router();
|
27
19
|
|
20
|
+
r.add('/', 'GET', home);
|
21
|
+
r.add('/r/:subreddit', 'GET', subreddit);
|
22
|
+
r.add('/u/:user', 'GET', user);
|
28
23
|
|
29
24
|
r.serve();
|
package/examples/logger.ts
CHANGED
@@ -1,16 +1,11 @@
|
|
1
|
-
import {
|
1
|
+
import { Router, http } from '..';
|
2
2
|
|
3
|
-
const r =
|
4
|
-
const log = logger();
|
3
|
+
const r = Router();
|
5
4
|
|
6
|
-
r.
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
log.error(500, url.pathname, ctx.request.method, new Error('undefined'));
|
11
|
-
return http.json(500,{text: 'Foo is undefined'});
|
12
|
-
}
|
13
|
-
return http.html(200, `<h4 style='font-family: sans-serif;'>Oh hello, ${foo}</h4>`)
|
5
|
+
r.get('/', ctx => {
|
6
|
+
ctx.logger.debug('hello from home');
|
7
|
+
|
8
|
+
return http.ok();
|
14
9
|
});
|
15
10
|
|
16
11
|
r.serve();
|
package/examples/static.ts
CHANGED
package/examples/todo.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { Router, http } from '..';
|
2
2
|
import { Context } from '../lib/router/router.d';
|
3
3
|
|
4
4
|
const Todo = () => {
|
@@ -15,7 +15,7 @@ const Todo = () => {
|
|
15
15
|
|
16
16
|
const todo = Todo();
|
17
17
|
|
18
|
-
const r =
|
18
|
+
const r = Router();
|
19
19
|
|
20
20
|
r.add('/api/new', 'POST', ctx => {
|
21
21
|
const query = new URL(ctx.request.url).searchParams;
|
package/lib/fs/fsys.ts
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
import { httpStatusCodes } from "./status";
|
2
2
|
|
3
3
|
const http = {
|
4
|
+
ok: async (msg?: string): Promise<Response> => {
|
5
|
+
return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
|
6
|
+
status: 200,
|
7
|
+
statusText: httpStatusCodes[200],
|
8
|
+
}));
|
9
|
+
},
|
4
10
|
json: async (statusCode: number, data: any): Promise<Response> => {
|
5
11
|
const jsonString = JSON.stringify(data);
|
6
12
|
return Promise.resolve(new Response(jsonString, {
|
@@ -10,8 +16,7 @@ const http = {
|
|
10
16
|
}));
|
11
17
|
},
|
12
18
|
html: async (statusCode: number, content: string): Promise<Response> => {
|
13
|
-
|
14
|
-
return Promise.resolve(new Response(Bun.escapeHTML(content), {
|
19
|
+
return Promise.resolve(new Response(content, {
|
15
20
|
status: statusCode,
|
16
21
|
statusText: httpStatusCodes[statusCode],
|
17
22
|
headers: {'Content-Type': 'text/html; charset=utf-8'}
|
@@ -50,6 +55,15 @@ const http = {
|
|
50
55
|
|
51
56
|
return Promise.resolve(response);
|
52
57
|
},
|
58
|
+
methodNotAllowed: async (msg?: string): Promise<Response> => {
|
59
|
+
const response = new Response(msg ?? 'method not allowed', {
|
60
|
+
status: 405,
|
61
|
+
statusText: httpStatusCodes[405],
|
62
|
+
headers: {'Content-Type': 'text/html'},
|
63
|
+
});
|
64
|
+
|
65
|
+
return Promise.resolve(response);
|
66
|
+
},
|
53
67
|
message: async (status: number, msg?: string): Promise<Response> => {
|
54
68
|
const response = new Response(msg ?? '?', {
|
55
69
|
status: status,
|
@@ -57,7 +71,7 @@ const http = {
|
|
57
71
|
headers: {'Content-Type': 'text/html; charset-utf-8'},
|
58
72
|
});
|
59
73
|
return Promise.resolve(response)
|
60
|
-
},
|
74
|
+
},
|
61
75
|
}
|
62
76
|
|
63
77
|
export { http }
|
package/lib/logger/color.ts
CHANGED
@@ -20,16 +20,16 @@ const Colors: Record<string,string> = {
|
|
20
20
|
bgMagenta: "\x1b[45m",
|
21
21
|
bgCyan: "\x1b[46m",
|
22
22
|
bgWhite: "\x1b[47m",
|
23
|
-
};
|
23
|
+
} as const;
|
24
24
|
|
25
25
|
|
26
|
-
const color = (c: string, bkg: string, msg: string) => {
|
27
|
-
const foreground = Colors[c];
|
28
|
-
const background = Colors[bkg];
|
29
|
-
const reset = Colors.reset;
|
30
26
|
|
31
|
-
|
32
|
-
|
27
|
+
function color(foreground: string, background: string, message: string) {
|
28
|
+
const _foreground = Colors[foreground];
|
29
|
+
const _background = Colors[background];
|
30
|
+
const reset = Colors.reset;
|
31
|
+
return `${_foreground}${_background}${message}${reset}`;
|
32
|
+
}
|
33
33
|
|
34
34
|
|
35
35
|
|
package/lib/logger/logger.d.ts
CHANGED
@@ -1,9 +1,8 @@
|
|
1
|
-
type
|
2
|
-
start: (port: number | string) => void,
|
1
|
+
type BunLogger = {
|
3
2
|
info: (statusCode: number, routePath: string, method: string, message?: string) => void,
|
4
3
|
error: (statusCode: number, routePath: string, method: string, error: Error) => void,
|
5
|
-
warn: (
|
6
|
-
message: (
|
4
|
+
warn: (message: string) => void,
|
5
|
+
message: (message: string) => void,
|
7
6
|
}
|
8
7
|
|
9
|
-
export {
|
8
|
+
export { BunLogger }
|
package/lib/logger/logger.ts
CHANGED
@@ -1,9 +1,58 @@
|
|
1
1
|
import { color } from './color';
|
2
|
-
import {
|
2
|
+
import { BunLogger } from './logger.d';
|
3
|
+
|
4
|
+
|
5
|
+
const TITLE = `
|
6
|
+
_ _
|
7
|
+
| |_ _ _ ___ ___ ___ _ _| |_ ___ ___
|
8
|
+
| . | | | | | _| . | | | _| -_| _|
|
9
|
+
|___|___|_|_| |_| |___|___|_| |___|_|
|
10
|
+
|
11
|
+
`
|
12
|
+
const VERSION = '0.7.1';
|
13
|
+
const Logger = (): BunLogger => {
|
14
|
+
return {
|
15
|
+
info: async (statusCode: number, routePath: string, method: string, message?: string) => {
|
16
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
17
|
+
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
|
18
|
+
const rp = color('white', 'bgBlack', routePath);
|
19
|
+
|
20
|
+
message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${method}${ message ?? ''}\n`
|
21
|
+
|
22
|
+
await Bun.write(Bun.stdout, message);
|
23
|
+
|
24
|
+
},
|
25
|
+
error: async (statusCode: number, routePath: string, method: string, error: Error) => {
|
26
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
27
|
+
const source = color('black', 'bgRed', `[error ${stamp}]`);
|
28
|
+
const rp = color('white', 'bgBlack', routePath);
|
29
|
+
|
30
|
+
const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${error.message}\n`
|
3
31
|
|
4
|
-
|
32
|
+
await Bun.write(Bun.stdout, message);
|
33
|
+
},
|
34
|
+
warn: async (message: string) => {
|
35
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
36
|
+
const source = color('black', 'bgYellow', `[warning ${stamp}]`);
|
37
|
+
const messageColor = color('yellow', 'bgBlack', message);
|
38
|
+
|
39
|
+
message = `${source} : ${messageColor}\n`;
|
40
|
+
|
41
|
+
await Bun.write(Bun.stdout, message);
|
42
|
+
},
|
43
|
+
message: async (message: string) => {
|
44
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
45
|
+
const source = color('black', 'bgCyan', `[message ${stamp}]`);
|
46
|
+
const messageColor = color('yellow', 'bgBlack', message);
|
47
|
+
|
48
|
+
message = `${source}: ${messageColor}\n`;
|
5
49
|
|
6
|
-
|
50
|
+
await Bun.write(Bun.stdout, message);
|
51
|
+
},
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
function timestamp(date: Date) {
|
7
56
|
const month = pad(date.getMonth());
|
8
57
|
const day = pad(date.getDate());
|
9
58
|
const hour = pad(date.getHours());
|
@@ -14,79 +63,32 @@ const timestamp = (date: Date) => {
|
|
14
63
|
return {month, day, hour, minute, stamp};
|
15
64
|
}
|
16
65
|
|
17
|
-
|
18
|
-
const colorCode = (n: number, text?:string): string => {
|
66
|
+
function setColor(n: number, text?: string){
|
19
67
|
const s = ` [${String(n)}${text ?? ''}] `;
|
68
|
+
|
20
69
|
if (n < 100) return color('black', 'bgYellow', s);
|
21
70
|
else if (n >= 100 && n < 200) return color('black', 'bgCyan', s);
|
22
71
|
else if (n >= 200 && n < 300) return color('black', 'bgGreen', s);
|
23
72
|
else if (n >= 300 && n < 400) return color('black', 'bgRed', s);
|
24
73
|
else if (n >= 400 && n < 500) return color('black', 'bgRed', s);
|
25
74
|
else if (n >= 500) return color('white', 'bgRed', s);
|
75
|
+
|
26
76
|
return color('white', 'bgBlack', `[${s}]`).trim();
|
27
77
|
}
|
28
78
|
|
29
|
-
|
30
|
-
const clean = (s: string) => s.replace(/\x1B\[\d{1,2}(;\d{1,2}){0,2}m/g, '');
|
31
|
-
|
32
|
-
const format = (statusCode: number, routePath: string, method: string, message?: string): string => {
|
79
|
+
function startMessage(port: number | string) {
|
33
80
|
const { stamp } = timestamp((new Date(Date.now())));
|
34
|
-
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`)
|
35
|
-
const
|
81
|
+
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`)
|
82
|
+
const portColor = color('green', 'bgBlack', String(port));
|
83
|
+
const msg = `${source}: Starting Server on :${portColor}\n`;
|
84
|
+
const version = color('red', 'bgBlack', `v${VERSION}\n`);
|
36
85
|
|
37
|
-
|
86
|
+
Bun.write(Bun.stdout, TITLE + '\n' + version);
|
87
|
+
Bun.write(Bun.stdout, msg);
|
38
88
|
}
|
39
89
|
|
40
|
-
|
41
|
-
|
42
|
-
const errors: string[] = [];
|
43
|
-
return {
|
44
|
-
// initial log message
|
45
|
-
start: async (port: number | string) => {
|
46
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
47
|
-
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`)
|
48
|
-
const portColor = color('green', 'bgBlack', String(port));
|
49
|
-
const msg = `${source}: Starting Server on :${portColor}\n`;
|
50
|
-
|
51
|
-
await Bun.write(Bun.stdout, msg);
|
52
|
-
},
|
53
|
-
info: async (statusCode: number, routePath: string, method: string, message?: string) => {
|
54
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
55
|
-
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
|
56
|
-
const rp = color('white', 'bgBlack', routePath);
|
57
|
-
const msg = `${source}: ${colorCode(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${method}${' | ' +message ?? ''}\n`
|
58
|
-
|
59
|
-
await Bun.write(Bun.stdout, msg);
|
60
|
-
|
61
|
-
messages.push(clean(msg));
|
62
|
-
},
|
63
|
-
error: async (statusCode: number, routePath: string, method: string, error: Error) => {
|
64
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
65
|
-
const source = color('black', 'bgRed', `[error ${stamp}]`);
|
66
|
-
const rp = color('white', 'bgBlack', routePath);
|
67
|
-
const msg = `${source}: ${colorCode(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${error.message}\n`;
|
68
|
-
|
69
|
-
await Bun.write(Bun.stdout, msg);
|
70
|
-
|
71
|
-
errors.push(clean(msg));
|
72
|
-
},
|
73
|
-
warn: async (msg: string) => {
|
74
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
75
|
-
const source = color('black', 'bgYellow', `[warning ${stamp}]`);
|
76
|
-
const msgColor = color('yellow', 'bgBlack', msg);
|
77
|
-
msg = `${source} : ${msgColor}\n`;
|
78
|
-
await Bun.write(Bun.stdout, msg);
|
79
|
-
},
|
80
|
-
message: async (msg: string) => {
|
81
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
82
|
-
const source = color('black', 'bgCyan', `[message ${stamp}]`);
|
83
|
-
const msgColor = color('yellow', 'bgBlack', msg);
|
84
|
-
msg = `${source}: ${msgColor}\n`;
|
85
|
-
await Bun.write(Bun.stdout, msg);
|
86
|
-
|
87
|
-
messages.push(clean(msg));
|
88
|
-
}
|
89
|
-
}
|
90
|
+
function pad(n: number) {
|
91
|
+
return String(n).padStart(2, '0');
|
90
92
|
}
|
91
93
|
|
92
|
-
export {
|
94
|
+
export { Logger, startMessage }
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import { Route, Context } from "./router.d";
|
2
|
+
import { Logger } from "../..";
|
3
|
+
import { http } from "./router";
|
4
|
+
|
5
|
+
function extractParams(path: string, route: Route): Map<string, string> {
|
6
|
+
const params: Map<string, string> = new Map();
|
7
|
+
const pathSegments = path.split('/');
|
8
|
+
const routeSegments = route.path.split('/');
|
9
|
+
|
10
|
+
if (pathSegments.length !== routeSegments.length) return params;
|
11
|
+
|
12
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
13
|
+
if (routeSegments[i][0] === ':') {
|
14
|
+
const key = routeSegments[i].replace(':', '');
|
15
|
+
const value = pathSegments[i];
|
16
|
+
params.set(key, value);
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
return params;
|
21
|
+
}
|
22
|
+
|
23
|
+
async function createContext(path: string, route: Route, request: Request): Promise<Context> {
|
24
|
+
const params = extractParams(path, route);
|
25
|
+
const query = new URLSearchParams(path);
|
26
|
+
const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
|
27
|
+
|
28
|
+
return Promise.resolve({
|
29
|
+
params,
|
30
|
+
request,
|
31
|
+
query,
|
32
|
+
formData,
|
33
|
+
logger: Logger(),
|
34
|
+
json: (statusCode: number, data: any) => http.json(statusCode, data),
|
35
|
+
});
|
36
|
+
}
|
37
|
+
|
38
|
+
function getContentType(headers: Headers): string {
|
39
|
+
const contentType = headers.get('Content-Type');
|
40
|
+
if (!contentType) return '';
|
41
|
+
return contentType;
|
42
|
+
}
|
43
|
+
|
44
|
+
function isMultiPartForm(headers: Headers): boolean {
|
45
|
+
const contentType = getContentType(headers);
|
46
|
+
return contentType.includes('multipart/form-data');
|
47
|
+
}
|
48
|
+
|
49
|
+
export { createContext }
|
package/lib/router/router.d.ts
CHANGED
@@ -2,26 +2,37 @@ import { TLSOptions, TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptio
|
|
2
2
|
import { Logger } from '../logger/logger';
|
3
3
|
import { Database } from 'bun:sqlite';
|
4
4
|
|
5
|
+
type BunRouter = (port?: number | string, options?: RouterOptions) => {
|
6
|
+
add: (pattern: string, method: string, callback: HttpHandler) => void;
|
7
|
+
get: (pattern: string, callback: HttpHandler) => void;
|
8
|
+
post: (pattern: string, callback: HttpHandler) => void;
|
9
|
+
put: (pattern: string, callback: HttpHandler) => void;
|
10
|
+
delete: (pattern: string, callback: HttpHandler) => void;
|
11
|
+
static: (pattern: string, root: string) => void;
|
12
|
+
serve: () => void;
|
13
|
+
}
|
14
|
+
|
15
|
+
type Route = {
|
16
|
+
children: Map<string, Route>;
|
17
|
+
path: string;
|
18
|
+
dynamicPath: string;
|
19
|
+
method: string;
|
20
|
+
handler: HttpHandler;
|
21
|
+
isLast: boolean;
|
22
|
+
}
|
5
23
|
|
6
24
|
type Context = {
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
route: Route,
|
16
|
-
token?: string,
|
25
|
+
db?: Database;
|
26
|
+
formData: FormData | Promise<FormData>;
|
27
|
+
json: (statusCode: number, data: any) => Response | Promise<Response>;
|
28
|
+
logger: Logger;
|
29
|
+
params: Map<string, string>;
|
30
|
+
query: URLSearchParams;
|
31
|
+
request: Request;
|
32
|
+
token?: string;
|
17
33
|
};
|
18
34
|
|
19
|
-
|
20
|
-
type Route = {
|
21
|
-
pattern: string,
|
22
|
-
method: string,
|
23
|
-
callback: (req: Context) => Response | Promise<Response>
|
24
|
-
}
|
35
|
+
type HttpHandler = (ctx: Context) => Response | Promise<Response>
|
25
36
|
|
26
37
|
type Options = {
|
27
38
|
db: string,
|
@@ -33,15 +44,4 @@ type RouterOptions<Options> = ServeOptions
|
|
33
44
|
| TLSWebSocketServeOptions<Options>
|
34
45
|
| undefined
|
35
46
|
|
36
|
-
|
37
|
-
type Router = (port?: number | string, options?: RouterOptions) => {
|
38
|
-
add: (pattern: string, method: string, callback: (req: Context) => Response | Promise<Response>) => void,
|
39
|
-
GET: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => void,
|
40
|
-
POST: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => void,
|
41
|
-
static: (pattern: string, root: string) => void,
|
42
|
-
serve: () => void,
|
43
|
-
}
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
export { Context , Route, Router, RouterOptions, Options }
|
47
|
+
export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler }
|
package/lib/router/router.ts
CHANGED
@@ -1,96 +1,27 @@
|
|
1
1
|
import path from 'path';
|
2
2
|
import { Database } from 'bun:sqlite';
|
3
|
-
import { Route,
|
3
|
+
import { Route, BunRouter, Context, RouterOptions, Options, HttpHandler } from './router.d';
|
4
4
|
import { httpStatusCodes } from '../http/status';
|
5
5
|
import { readDir } from '../fs/fsys';
|
6
|
-
import {
|
7
|
-
import {
|
8
|
-
import {
|
6
|
+
import { Logger, startMessage } from '../logger/logger';
|
7
|
+
import { http } from '../http/http';
|
8
|
+
import {RouteTree } from './tree';
|
9
|
+
import { createContext } from './context';
|
9
10
|
|
10
|
-
// extract dynamic URL parameters
|
11
|
-
// if the route pattern is /:foo and the request URL is /bar: {foo: 'bar'}
|
12
|
-
const extract = (route: Route, ctx: Context) => {
|
13
|
-
const url = new URL(ctx.request.url);
|
14
|
-
const pathSegments = route.pattern.split('/');
|
15
|
-
const urlSegments = url.pathname.split('/');
|
16
11
|
|
17
|
-
|
12
|
+
const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
|
13
|
+
const { addRoute, findRoute } = RouteTree();
|
14
|
+
const logger = Logger();
|
18
15
|
|
19
16
|
return {
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
}
|
29
|
-
}
|
30
|
-
|
31
|
-
}
|
32
|
-
|
33
|
-
// ensure the route pattern matches the request URL
|
34
|
-
const match = (route: Route, ctx: Context): boolean => {
|
35
|
-
const url = new URL(ctx.request.url);
|
36
|
-
const patternRegex = new RegExp('^' + route.pattern.replace(/:[^/]+/g, '([^/]+)') + '$');
|
37
|
-
const matches = url.pathname.match(patternRegex);
|
38
|
-
|
39
|
-
if (matches && route.method === ctx.request.method) {
|
40
|
-
const extractor = extract(route, ctx);
|
41
|
-
extractor?.params();
|
42
|
-
|
43
|
-
return true;
|
44
|
-
}
|
45
|
-
|
46
|
-
return false;
|
47
|
-
}
|
48
|
-
|
49
|
-
// set the context for the reuest
|
50
|
-
const setContext = (req: Request, lgr: Logger, opts: Options, route: Route): Context => {
|
51
|
-
const token = req.headers.get('Authorization');
|
52
|
-
return {
|
53
|
-
token: token ?? '',
|
54
|
-
cookies: new Map(),
|
55
|
-
formData: req.formData(),
|
56
|
-
request: req,
|
57
|
-
params: new Map(),
|
58
|
-
query: new URL(req.url).searchParams,
|
59
|
-
db: new Database(opts.db ?? ':memory:'),
|
60
|
-
logger: lgr,
|
61
|
-
route: route,
|
62
|
-
json: (statusCode: number, data: any) => http.json(statusCode, data),
|
63
|
-
}
|
64
|
-
}
|
65
|
-
|
66
|
-
const router: Router = (port?: number | string, options?: RouterOptions<Options>) => {
|
67
|
-
const routes: Array<Route> = new Array();
|
68
|
-
const lgr = logger();
|
69
|
-
|
70
|
-
return {
|
71
|
-
// add a new route
|
72
|
-
add: (pattern: string, method: string, callback: (ctx: Context) => Response | Promise<Response>) => {
|
73
|
-
routes.push({
|
74
|
-
pattern: pattern,
|
75
|
-
method: method,
|
76
|
-
callback: callback,
|
77
|
-
})
|
78
|
-
},
|
79
|
-
GET: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => {
|
80
|
-
routes.push({
|
81
|
-
pattern: pattern,
|
82
|
-
method: 'GET',
|
83
|
-
callback: callback,
|
84
|
-
});
|
85
|
-
},
|
86
|
-
POST: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => {
|
87
|
-
routes.push({
|
88
|
-
pattern: pattern,
|
89
|
-
method: 'POST',
|
90
|
-
callback: callback,
|
91
|
-
});
|
92
|
-
},
|
93
|
-
// add a route for static files
|
17
|
+
// add a route to the router tree
|
18
|
+
add: (pattern, method, callback) => { addRoute(pattern, method, callback) },
|
19
|
+
get: (pattern: string, callback: HttpHandler) => { addRoute(pattern, 'GET', callback) },
|
20
|
+
post: (pattern, callback) => { addRoute(pattern, 'POST', callback) },
|
21
|
+
put: (pattern, callback) => { addRoute(pattern, 'PUT', callback)},
|
22
|
+
delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback) },
|
23
|
+
|
24
|
+
// add a static route to the router tree
|
94
25
|
static: async (pattern: string, root: string) => {
|
95
26
|
await readDir(root, async (fp, _) => {
|
96
27
|
const pure = path.join('.', fp);
|
@@ -107,17 +38,20 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
|
|
107
38
|
if (base === 'index') patternPath = pattern;
|
108
39
|
|
109
40
|
const route: Route = {
|
110
|
-
|
41
|
+
children: new Map(),
|
42
|
+
dynamicPath: '',
|
43
|
+
isLast: true,
|
44
|
+
path: patternPath,
|
111
45
|
method: 'GET',
|
112
|
-
|
46
|
+
handler: async () => await http.file(200, pure),
|
113
47
|
};
|
114
48
|
|
115
|
-
|
49
|
+
addRoute(route.path, 'GET', route.handler);
|
116
50
|
});
|
117
51
|
},
|
118
52
|
// start the server
|
119
53
|
serve: () => {
|
120
|
-
|
54
|
+
startMessage(port ?? 3000);
|
121
55
|
let opts: Options = { db: ':memory:' };
|
122
56
|
|
123
57
|
Bun.serve({
|
@@ -125,52 +59,42 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
|
|
125
59
|
...options,
|
126
60
|
async fetch(req) {
|
127
61
|
const url = new URL(req.url);
|
62
|
+
let path = url.pathname;
|
128
63
|
|
64
|
+
// set the database
|
129
65
|
if (options) {
|
130
66
|
let o = options as Options;
|
131
67
|
opts.db = o.db;
|
132
68
|
}
|
133
69
|
|
134
|
-
|
135
|
-
|
136
|
-
for (const route of routes) {
|
137
|
-
const ctx = setContext(req, lgr, opts, route);
|
70
|
+
const route = findRoute(path);
|
138
71
|
|
139
|
-
|
140
|
-
|
141
|
-
|
72
|
+
// if the route exists, execute the handler
|
73
|
+
if (route) {
|
74
|
+
if (route.method !== req.method) {
|
75
|
+
logger.info(405, url.pathname, req.method, httpStatusCodes[405]);
|
76
|
+
return Promise.resolve(http.methodNotAllowed());
|
77
|
+
}
|
142
78
|
|
143
|
-
|
144
|
-
|
145
|
-
for (const [key, value] of ctx.cookies) {
|
146
|
-
cookieValue.push(`${key}=${value}`);
|
147
|
-
}
|
148
|
-
}
|
79
|
+
const context = await createContext(path, route, req);
|
80
|
+
context.db = new Database(opts.db);
|
149
81
|
|
150
|
-
|
82
|
+
const response = await route.handler(context);
|
151
83
|
|
152
|
-
|
84
|
+
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
85
|
+
return Promise.resolve(response);
|
86
|
+
}
|
153
87
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
statusText: httpStatusCodes[405]
|
160
|
-
});
|
161
|
-
lgr.info(405, route.pattern, req.method, httpStatusCodes[405])
|
162
|
-
return Promise.resolve(res);
|
163
|
-
}
|
164
|
-
}
|
165
|
-
}
|
88
|
+
// if no route is found, return 404
|
89
|
+
const response = await http.notFound();
|
90
|
+
|
91
|
+
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
92
|
+
return Promise.resolve(http.notFound());
|
166
93
|
|
167
|
-
lgr.info(statusCode, url.pathname, req.method, httpStatusCodes[statusCode]);
|
168
|
-
return Promise.resolve(http.message(statusCode, httpStatusCodes[statusCode]));
|
169
94
|
}
|
170
95
|
});
|
171
96
|
},
|
172
97
|
}
|
173
98
|
}
|
174
99
|
|
175
|
-
|
176
|
-
export { router, extract, http }
|
100
|
+
export { Router, http }
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import { HttpHandler, Context, Route } from "./router.d";
|
2
|
+
import { http } from "../http/http";
|
3
|
+
import { createContext } from './context';
|
4
|
+
|
5
|
+
const splitPath = (s: string): string[] => s.split('/').filter(x => x !== '');
|
6
|
+
|
7
|
+
const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
|
8
|
+
const route: Route = {
|
9
|
+
children: new Map(),
|
10
|
+
path: path,
|
11
|
+
dynamicPath: '',
|
12
|
+
method: method,
|
13
|
+
handler: handler,
|
14
|
+
isLast: false
|
15
|
+
};
|
16
|
+
|
17
|
+
return route;
|
18
|
+
};
|
19
|
+
|
20
|
+
const RouteTree = () => {
|
21
|
+
let root = createRoute('', 'GET', () => http.notFound());
|
22
|
+
|
23
|
+
const addRoute = (path: string, method: string, handler: HttpHandler) => {
|
24
|
+
const pathParts = splitPath(path);
|
25
|
+
let current = root;
|
26
|
+
|
27
|
+
for (let i = 0; i < pathParts.length; i++) {
|
28
|
+
const part = pathParts[i];
|
29
|
+
if (part.startsWith(':')) {
|
30
|
+
current.dynamicPath = part;
|
31
|
+
}
|
32
|
+
if (!current.children.has(part)) {
|
33
|
+
current.children.set(part, createRoute(part, method, handler));
|
34
|
+
}
|
35
|
+
current = current.children.get(part)!;
|
36
|
+
}
|
37
|
+
|
38
|
+
current.handler = handler;
|
39
|
+
current.isLast = true;
|
40
|
+
current.path = path;
|
41
|
+
};
|
42
|
+
|
43
|
+
const findRoute = (path: string): Route | undefined => {
|
44
|
+
const pathParts = splitPath(path);
|
45
|
+
let current = root;
|
46
|
+
for (let i = 0; i < pathParts.length; i++) {
|
47
|
+
const part = pathParts[i];
|
48
|
+
if (current.children.has(part)) {
|
49
|
+
current = current.children.get(part)!;
|
50
|
+
} else if (current.dynamicPath) {
|
51
|
+
current = current.children.get(current.dynamicPath)!;
|
52
|
+
} else {
|
53
|
+
return;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
return current;
|
57
|
+
}
|
58
|
+
|
59
|
+
return { addRoute, findRoute }
|
60
|
+
|
61
|
+
};
|
62
|
+
|
63
|
+
export { RouteTree, createContext }
|
package/package.json
CHANGED
package/tests/router.test.ts
CHANGED
@@ -1,68 +1,4 @@
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
2
|
-
import { extract } from '..';
|
3
|
-
import { Context, Route } from '../lib/router/router.d';
|
4
|
-
|
5
|
-
describe('URL Params', () => {
|
6
|
-
test('/user/:name', () => {
|
7
|
-
const route: Route = {
|
8
|
-
pattern: '/user/:name',
|
9
|
-
method: 'GET',
|
10
|
-
callback: () => new Response('ok'),
|
11
|
-
};
|
12
|
-
|
13
|
-
const ctx: Context = {
|
14
|
-
request: new Request('http://localhost:3000/user/foo'),
|
15
|
-
params: new Map(),
|
16
|
-
};
|
17
|
-
|
18
|
-
const extractor = extract(route, ctx);
|
19
|
-
|
20
|
-
extractor?.params();
|
21
|
-
|
22
|
-
const name = ctx.params.get('name');
|
23
|
-
expect(name).toBe('foo');
|
24
|
-
});
|
25
|
-
|
26
|
-
test('/user/:name/:id', () => {
|
27
|
-
const route: Route = {
|
28
|
-
pattern: '/user/:name/:id',
|
29
|
-
method: 'GET',
|
30
|
-
callback: () => new Response('ok'),
|
31
|
-
};
|
32
|
-
|
33
|
-
const ctx: Context = {
|
34
|
-
request: new Request('http://localhost:3000/user/foo/123'),
|
35
|
-
params: new Map(),
|
36
|
-
};
|
37
|
-
|
38
|
-
const extractor = extract(route, ctx);
|
39
|
-
|
40
|
-
extractor?.params();
|
41
|
-
|
42
|
-
const name = ctx.params.get('name');
|
43
|
-
const id = ctx.params.get('id');
|
44
|
-
|
45
|
-
expect(name).toBe('foo');
|
46
|
-
expect(id).toBe('123');
|
47
|
-
});
|
48
|
-
|
49
|
-
test('/foo', () => {
|
50
|
-
const route: Route = {
|
51
|
-
pattern: '/foo',
|
52
|
-
method: 'GET',
|
53
|
-
callback: () => new Response('ok'),
|
54
|
-
}
|
55
|
-
|
56
|
-
const ctx: Context = {
|
57
|
-
request: new Request('http://localhost:3000/foo'),
|
58
|
-
params: new Map(),
|
59
|
-
}
|
60
|
-
|
61
|
-
const url = new URL(ctx.request.url);
|
62
|
-
|
63
|
-
expect(url.pathname).toBe(route.pattern);
|
64
|
-
});
|
65
|
-
});
|
66
2
|
|
67
3
|
describe('Router', () => {
|
68
4
|
test('Serve', async () => {
|
package/examples/cookies.ts
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
import { router } from '..';
|
2
|
-
|
3
|
-
const r = router();
|
4
|
-
|
5
|
-
r.add('/set-cookie', 'GET', ctx => {
|
6
|
-
ctx.cookies.set('domain', 'localhost');
|
7
|
-
ctx.cookies.set('path', '/set-cookie');
|
8
|
-
|
9
|
-
ctx.logger.message(ctx.token ?? 'no token provided');
|
10
|
-
|
11
|
-
return ctx.json( 200, {message: 'cookie stored'});
|
12
|
-
});
|
13
|
-
|
14
|
-
|
15
|
-
r.serve();
|
package/examples/sqlite.ts
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
import { router, http } from '..';
|
2
|
-
|
3
|
-
const r = router(3000, {db: './examples/dbs/test.db'});
|
4
|
-
|
5
|
-
r.add('/u/new/:name', 'GET', (ctx) => {
|
6
|
-
const name = ctx.params.get('name');
|
7
|
-
const rando = Math.floor(Math.random()*1000);
|
8
|
-
|
9
|
-
ctx.db.run(`INSERT INTO test VALUES(${rando}, "${name}")`);
|
10
|
-
|
11
|
-
return http.json(200, {message: 'ok'});
|
12
|
-
});
|
13
|
-
|
14
|
-
r.add('/u/:name', 'GET', (ctx) => {
|
15
|
-
const name = ctx.params.get('name');
|
16
|
-
const data = ctx.db.query(`SELECT * FROM test WHERE name = "${name}";`).get();
|
17
|
-
const d = data as {id: number, name: string};
|
18
|
-
|
19
|
-
return d ? http.json(200, d) : new Response('not found', {status: 404});
|
20
|
-
});
|
21
|
-
|
22
|
-
r.serve();
|