bun-router 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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();
|