bun-router 0.7.0 → 0.7.2
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +29 -56
- package/examples/basic.ts +5 -5
- package/examples/db.ts +10 -0
- package/examples/dynamic.ts +3 -3
- package/examples/logger.ts +6 -11
- package/examples/static.ts +2 -2
- package/examples/todo.ts +2 -3
- package/lib/fs/fsys.ts +11 -12
- package/lib/http/{generic-methods.ts → http.ts} +7 -1
- package/lib/logger/color.ts +6 -6
- package/lib/logger/logger.d.ts +4 -5
- package/lib/logger/logger.ts +57 -64
- package/lib/router/context.ts +51 -0
- package/lib/router/router.d.ts +21 -23
- package/lib/router/router.ts +23 -41
- package/lib/router/tree.ts +5 -32
- package/lib/util/strings.ts +3 -0
- package/package.json +1 -1
- package/examples/cookies.ts +0 -15
package/README.md
CHANGED
@@ -1,83 +1,56 @@
|
|
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();
|
4
|
+
`npm i -s bun-router`
|
10
5
|
|
11
|
-
|
12
|
-
|
13
|
-
r.serve();
|
14
|
-
```
|
15
|
-
#### Static Files
|
16
|
-
```typescript
|
17
|
-
import { router } from 'bun-router';
|
6
|
+
or
|
18
7
|
|
19
|
-
|
8
|
+
`bun i bun-router`
|
20
9
|
|
21
|
-
r.static('/', './pages');
|
22
10
|
|
23
|
-
|
24
|
-
|
11
|
+
#### Examples
|
12
|
+
##### URL Parameters
|
13
|
+
```ts
|
14
|
+
import { Router, http } from 'bun-router';
|
25
15
|
|
26
|
-
|
27
|
-
```typescript
|
28
|
-
import {router, html, json } from 'bun-router';
|
16
|
+
const router = Router();
|
29
17
|
|
30
|
-
|
18
|
+
router.add('/', 'GET', () => http.ok());
|
31
19
|
|
32
|
-
|
20
|
+
router.get('/u/:username', ctx => {
|
21
|
+
const username = ctx.params.get('username');
|
33
22
|
|
34
|
-
|
35
|
-
const name = ctx.params.get('name');
|
36
|
-
if (!name) return new Response('invalid url parameters', {status: 400});
|
23
|
+
if (!username) return http.badRequest();
|
37
24
|
|
38
|
-
return
|
25
|
+
return ctx.json({ username: username });
|
39
26
|
});
|
40
27
|
|
41
|
-
|
28
|
+
router.serve();
|
29
|
+
```
|
42
30
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
const user = store.get(id);
|
31
|
+
##### Static
|
32
|
+
```ts
|
33
|
+
import { Router } from 'bun-router';
|
48
34
|
|
49
|
-
|
35
|
+
const router = Router();
|
50
36
|
|
51
|
-
|
52
|
-
});
|
37
|
+
router.static('/assets', 'static');
|
53
38
|
|
54
|
-
|
39
|
+
router.serve();
|
55
40
|
```
|
56
41
|
|
57
|
-
|
42
|
+
##### SQLite
|
58
43
|
```ts
|
59
|
-
import {
|
60
|
-
import { Database } from 'bun:sqlite';
|
61
|
-
|
62
|
-
const r = router(3000, {db: './examples/dbs/test.db'});
|
44
|
+
import { Router } from 'bun-router'
|
63
45
|
|
64
|
-
|
65
|
-
const name = ctx.params.get('name');
|
66
|
-
const rando = Math.floor(Math.random()*1000);
|
46
|
+
const router = Router(3000, { db: 'test.db'});
|
67
47
|
|
68
|
-
|
48
|
+
router.post('/register', ctx => {
|
49
|
+
const query = ctx.db.query("select 'Hello' as message;");
|
69
50
|
|
70
|
-
return
|
51
|
+
return http.ok(query.get());
|
71
52
|
});
|
72
53
|
|
73
|
-
|
74
|
-
const name = ctx.params.get('name');
|
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
|
-
});
|
54
|
+
```
|
80
55
|
|
81
|
-
r.serve();
|
82
56
|
|
83
|
-
```
|
package/examples/basic.ts
CHANGED
@@ -1,13 +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
9
|
if (!name) return http.json(500, 'no name');
|
10
10
|
return http.json(200, name);
|
11
11
|
});
|
12
12
|
|
13
|
-
|
13
|
+
router.serve();
|
package/examples/db.ts
ADDED
package/examples/dynamic.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
import {
|
1
|
+
import { Router, http } from '..';
|
2
2
|
import { Context } from '../lib/router/router.d';
|
3
3
|
|
4
|
-
const home = (
|
4
|
+
const home = () => new Response('Welcome Home', { status: 200 });
|
5
5
|
|
6
6
|
const subreddit = (ctx: Context) => {
|
7
7
|
const sub = ctx.params.get('subreddit');
|
@@ -15,7 +15,7 @@ const user = (ctx: Context) => {
|
|
15
15
|
return http.json(200, { user: user });
|
16
16
|
}
|
17
17
|
|
18
|
-
const r =
|
18
|
+
const r = Router();
|
19
19
|
|
20
20
|
r.add('/', 'GET', home);
|
21
21
|
r.add('/r/:subreddit', 'GET', subreddit);
|
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,5 +1,4 @@
|
|
1
|
-
import {
|
2
|
-
import { Context } from '../lib/router/router.d';
|
1
|
+
import { Router, http } from '..';
|
3
2
|
|
4
3
|
const Todo = () => {
|
5
4
|
const list: Record<string, string> = {};
|
@@ -15,7 +14,7 @@ const Todo = () => {
|
|
15
14
|
|
16
15
|
const todo = Todo();
|
17
16
|
|
18
|
-
const r =
|
17
|
+
const r = Router();
|
19
18
|
|
20
19
|
r.add('/api/new', 'POST', ctx => {
|
21
20
|
const query = new URL(ctx.request.url).searchParams;
|
package/lib/fs/fsys.ts
CHANGED
@@ -5,22 +5,21 @@ import path from 'path';
|
|
5
5
|
// check if the file path is a directory
|
6
6
|
const isDir = async (fp: string): Promise<boolean> => (await fs.lstat(fp)).isDirectory();
|
7
7
|
|
8
|
-
|
9
|
-
const
|
10
|
-
const files = await fs.readdir(dirpath);
|
8
|
+
async function readDir(dirpath: string, handler: (filepath: string, entry: BunFile) => void) {
|
9
|
+
const files = await fs.readdir(dirpath);
|
11
10
|
|
12
|
-
|
13
|
-
|
11
|
+
for (const file of files) {
|
12
|
+
const bunFile = Bun.file(file);
|
14
13
|
|
15
|
-
|
14
|
+
if (typeof bunFile.name === 'undefined') return
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
if (isdir) await readDir(fp, handler);
|
21
|
-
else handler(fp, bunFile);
|
22
|
-
}
|
16
|
+
const fp = path.join(dirpath, bunFile.name);
|
17
|
+
const isdir = await isDir(fp);
|
23
18
|
|
19
|
+
if (isdir) await readDir(fp, handler);
|
20
|
+
else handler(fp, bunFile);
|
21
|
+
}
|
24
22
|
}
|
25
23
|
|
24
|
+
|
26
25
|
export { readDir }
|
@@ -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, {
|
@@ -65,7 +71,7 @@ const http = {
|
|
65
71
|
headers: {'Content-Type': 'text/html; charset-utf-8'},
|
66
72
|
});
|
67
73
|
return Promise.resolve(response)
|
68
|
-
},
|
74
|
+
},
|
69
75
|
}
|
70
76
|
|
71
77
|
export { http }
|
package/lib/logger/color.ts
CHANGED
@@ -23,13 +23,13 @@ const Colors: Record<string,string> = {
|
|
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,7 +1,6 @@
|
|
1
1
|
import { color } from './color';
|
2
|
-
import {
|
2
|
+
import { BunLogger } from './logger.d';
|
3
3
|
|
4
|
-
const pad = (n: number) => String(n).padStart(2, '0');
|
5
4
|
|
6
5
|
const TITLE = `
|
7
6
|
_ _
|
@@ -10,8 +9,50 @@ _ _
|
|
10
9
|
|___|___|_|_| |_| |___|___|_| |___|_|
|
11
10
|
|
12
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`
|
13
21
|
|
14
|
-
|
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`
|
31
|
+
|
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`;
|
49
|
+
|
50
|
+
await Bun.write(Bun.stdout, message);
|
51
|
+
},
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
function timestamp(date: Date) {
|
15
56
|
const month = pad(date.getMonth());
|
16
57
|
const day = pad(date.getDate());
|
17
58
|
const hour = pad(date.getHours());
|
@@ -22,80 +63,32 @@ const timestamp = (date: Date) => {
|
|
22
63
|
return {month, day, hour, minute, stamp};
|
23
64
|
}
|
24
65
|
|
25
|
-
|
26
|
-
const colorCode = (n: number, text?:string): string => {
|
66
|
+
function setColor(n: number, text?: string){
|
27
67
|
const s = ` [${String(n)}${text ?? ''}] `;
|
68
|
+
|
28
69
|
if (n < 100) return color('black', 'bgYellow', s);
|
29
70
|
else if (n >= 100 && n < 200) return color('black', 'bgCyan', s);
|
30
71
|
else if (n >= 200 && n < 300) return color('black', 'bgGreen', s);
|
31
72
|
else if (n >= 300 && n < 400) return color('black', 'bgRed', s);
|
32
73
|
else if (n >= 400 && n < 500) return color('black', 'bgRed', s);
|
33
74
|
else if (n >= 500) return color('white', 'bgRed', s);
|
75
|
+
|
34
76
|
return color('white', 'bgBlack', `[${s}]`).trim();
|
35
77
|
}
|
36
78
|
|
37
|
-
|
38
|
-
const clean = (s: string) => s.replace(/\x1B\[\d{1,2}(;\d{1,2}){0,2}m/g, '');
|
39
|
-
|
40
|
-
const format = (statusCode: number, routePath: string, method: string, message?: string): string => {
|
79
|
+
function startMessage(port: number | string) {
|
41
80
|
const { stamp } = timestamp((new Date(Date.now())));
|
42
|
-
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`)
|
43
|
-
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`);
|
44
85
|
|
45
|
-
|
86
|
+
Bun.write(Bun.stdout, TITLE + '\n' + version);
|
87
|
+
Bun.write(Bun.stdout, msg);
|
46
88
|
}
|
47
89
|
|
48
|
-
|
49
|
-
|
50
|
-
const errors: string[] = [];
|
51
|
-
return {
|
52
|
-
// initial log message
|
53
|
-
start: async (port: number | string) => {
|
54
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
55
|
-
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`)
|
56
|
-
const portColor = color('green', 'bgBlack', String(port));
|
57
|
-
const msg = `${source}: Starting Server on :${portColor}\n`;
|
58
|
-
|
59
|
-
await Bun.write(Bun.stdout, TITLE);
|
60
|
-
await Bun.write(Bun.stdout, msg);
|
61
|
-
},
|
62
|
-
info: async (statusCode: number, routePath: string, method: string, message?: string) => {
|
63
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
64
|
-
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
|
65
|
-
const rp = color('white', 'bgBlack', routePath);
|
66
|
-
const msg = `${source}: ${colorCode(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${method}${' | ' +message ?? ''}\n`
|
67
|
-
|
68
|
-
await Bun.write(Bun.stdout, msg);
|
69
|
-
|
70
|
-
messages.push(clean(msg));
|
71
|
-
},
|
72
|
-
error: async (statusCode: number, routePath: string, method: string, error: Error) => {
|
73
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
74
|
-
const source = color('black', 'bgRed', `[error ${stamp}]`);
|
75
|
-
const rp = color('white', 'bgBlack', routePath);
|
76
|
-
const msg = `${source}: ${colorCode(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${error.message}\n`;
|
77
|
-
|
78
|
-
await Bun.write(Bun.stdout, msg);
|
79
|
-
|
80
|
-
errors.push(clean(msg));
|
81
|
-
},
|
82
|
-
warn: async (msg: string) => {
|
83
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
84
|
-
const source = color('black', 'bgYellow', `[warning ${stamp}]`);
|
85
|
-
const msgColor = color('yellow', 'bgBlack', msg);
|
86
|
-
msg = `${source} : ${msgColor}\n`;
|
87
|
-
await Bun.write(Bun.stdout, msg);
|
88
|
-
},
|
89
|
-
message: async (msg: string) => {
|
90
|
-
const { stamp } = timestamp((new Date(Date.now())));
|
91
|
-
const source = color('black', 'bgCyan', `[message ${stamp}]`);
|
92
|
-
const msgColor = color('yellow', 'bgBlack', msg);
|
93
|
-
msg = `${source}: ${msgColor}\n`;
|
94
|
-
await Bun.write(Bun.stdout, msg);
|
95
|
-
|
96
|
-
messages.push(clean(msg));
|
97
|
-
}
|
98
|
-
}
|
90
|
+
function pad(n: number) {
|
91
|
+
return String(n).padStart(2, '0');
|
99
92
|
}
|
100
93
|
|
101
|
-
export {
|
94
|
+
export { Logger, startMessage }
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import { Route, Context } from "./router.d";
|
2
|
+
import { Logger } from "../..";
|
3
|
+
import { http } from "./router";
|
4
|
+
|
5
|
+
async function createContext(path: string, route: Route, request: Request): Promise<Context> {
|
6
|
+
const params = extractParams(path, route);
|
7
|
+
const query = new URLSearchParams(path);
|
8
|
+
const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
|
9
|
+
|
10
|
+
return Promise.resolve({
|
11
|
+
params,
|
12
|
+
request,
|
13
|
+
query,
|
14
|
+
formData,
|
15
|
+
logger: Logger(),
|
16
|
+
json: (statusCode: number, data: any) => http.json(statusCode, data),
|
17
|
+
});
|
18
|
+
}
|
19
|
+
|
20
|
+
function extractParams(path: string, route: Route): Map<string, string> {
|
21
|
+
const params: Map<string, string> = new Map();
|
22
|
+
const pathSegments = path.split('/');
|
23
|
+
const routeSegments = route.path.split('/');
|
24
|
+
|
25
|
+
if (pathSegments.length !== routeSegments.length) return params;
|
26
|
+
|
27
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
28
|
+
if (routeSegments[i][0] === ':') {
|
29
|
+
const key = routeSegments[i].replace(':', '');
|
30
|
+
const value = pathSegments[i];
|
31
|
+
params.set(key, value);
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
return params;
|
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
|
+
|
50
|
+
|
51
|
+
export { createContext }
|
package/lib/router/router.d.ts
CHANGED
@@ -2,29 +2,36 @@ import { TLSOptions, TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptio
|
|
2
2
|
import { Logger } from '../logger/logger';
|
3
3
|
import { Database } from 'bun:sqlite';
|
4
4
|
|
5
|
-
type
|
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
|
+
}
|
6
23
|
|
7
24
|
type Context = {
|
8
|
-
cookies: Map<string, string>;
|
9
25
|
db?: Database;
|
10
|
-
formData: FormData | Promise<FormData
|
26
|
+
formData: FormData | Promise<FormData>;
|
11
27
|
json: (statusCode: number, data: any) => Response | Promise<Response>;
|
12
28
|
logger: Logger;
|
13
29
|
params: Map<string, string>;
|
14
30
|
query: URLSearchParams;
|
15
31
|
request: Request;
|
16
|
-
token?: string;
|
17
32
|
};
|
18
33
|
|
19
|
-
|
20
|
-
type Route = {
|
21
|
-
children: Map<string, Route>;
|
22
|
-
path: string;
|
23
|
-
dynamicPath: string;
|
24
|
-
method: string;
|
25
|
-
handler: HttpHandler;
|
26
|
-
isLast: boolean;
|
27
|
-
}
|
34
|
+
type HttpHandler = (ctx: Context) => Response | Promise<Response>
|
28
35
|
|
29
36
|
type Options = {
|
30
37
|
db: string,
|
@@ -36,13 +43,4 @@ type RouterOptions<Options> = ServeOptions
|
|
36
43
|
| TLSWebSocketServeOptions<Options>
|
37
44
|
| undefined
|
38
45
|
|
39
|
-
|
40
|
-
type Router = (port?: number | string, options?: RouterOptions) => {
|
41
|
-
add: (pattern: string, method: string, callback: (req: Context) => Response | Promise<Response>) => void;
|
42
|
-
static: (pattern: string, root: string) => void;
|
43
|
-
serve: () => void;
|
44
|
-
}
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
export { Context , Route, Router, RouterOptions, Options, HttpHandler }
|
46
|
+
export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler }
|
package/lib/router/router.ts
CHANGED
@@ -1,42 +1,26 @@
|
|
1
1
|
import path from 'path';
|
2
2
|
import { Database } from 'bun:sqlite';
|
3
|
-
import { Route,
|
3
|
+
import { Route, BunRouter, RouterOptions, Options, HttpHandler } from './router.d';
|
4
4
|
import { httpStatusCodes } from '../http/status';
|
5
5
|
import { readDir } from '../fs/fsys';
|
6
|
-
import {
|
7
|
-
import { http } from '../http/
|
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
|
-
const extract = (path: string, ctx: Context) => {
|
11
|
-
const url = new URL(ctx.request.url);
|
12
|
-
const pathSegments = path.split('/');
|
13
|
-
const urlSegments = url.pathname.split('/');
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
params: () => {
|
19
|
-
for (let i = 0; i < pathSegments.length; i++) {
|
20
|
-
if((pathSegments[i][0] === ':')) {
|
21
|
-
const k = pathSegments[i].replace(':', '');
|
22
|
-
const v = urlSegments[i];
|
23
|
-
ctx.params.set(k,v);
|
24
|
-
}
|
25
|
-
}
|
26
|
-
}
|
27
|
-
}
|
28
|
-
|
29
|
-
}
|
30
|
-
|
31
|
-
const router: Router = (port?: number | string, options?: RouterOptions<Options>) => {
|
32
|
-
const {addRoute, findRoute} = Radix();
|
33
|
-
const lgr = logger();
|
12
|
+
const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
|
13
|
+
const { addRoute, findRoute } = RouteTree();
|
14
|
+
const logger = Logger();
|
34
15
|
|
35
16
|
return {
|
36
17
|
// add a route to the router tree
|
37
|
-
add: (pattern
|
38
|
-
|
39
|
-
},
|
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
|
+
|
40
24
|
// add a static route to the router tree
|
41
25
|
static: async (pattern: string, root: string) => {
|
42
26
|
await readDir(root, async (fp, _) => {
|
@@ -67,10 +51,9 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
|
|
67
51
|
},
|
68
52
|
// start the server
|
69
53
|
serve: () => {
|
70
|
-
|
54
|
+
startMessage(port ?? 3000);
|
71
55
|
let opts: Options = { db: ':memory:' };
|
72
56
|
|
73
|
-
// TODO: add support for TLS and WebSockets
|
74
57
|
Bun.serve({
|
75
58
|
port: port ?? 3000,
|
76
59
|
...options,
|
@@ -89,24 +72,24 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
|
|
89
72
|
// if the route exists, execute the handler
|
90
73
|
if (route) {
|
91
74
|
if (route.method !== req.method) {
|
92
|
-
|
93
|
-
return Promise.resolve(http.
|
75
|
+
logger.info(405, url.pathname, req.method, httpStatusCodes[405]);
|
76
|
+
return Promise.resolve(http.errMethodNotAllowed());
|
94
77
|
}
|
95
78
|
|
96
|
-
const context = createContext(path, route, req);
|
79
|
+
const context = await createContext(path, route, req);
|
97
80
|
context.db = new Database(opts.db);
|
98
81
|
|
99
82
|
const response = await route.handler(context);
|
100
83
|
|
101
|
-
|
84
|
+
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
102
85
|
return Promise.resolve(response);
|
103
86
|
}
|
104
87
|
|
105
88
|
// if no route is found, return 404
|
106
|
-
const response = await http.
|
89
|
+
const response = await http.errNotFound();
|
107
90
|
|
108
|
-
|
109
|
-
return Promise.resolve(http.
|
91
|
+
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
92
|
+
return Promise.resolve(http.errNotFound());
|
110
93
|
|
111
94
|
}
|
112
95
|
});
|
@@ -114,5 +97,4 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
|
|
114
97
|
}
|
115
98
|
}
|
116
99
|
|
117
|
-
|
118
|
-
export { router, extract, http }
|
100
|
+
export { Router, http }
|
package/lib/router/tree.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
import { HttpHandler,
|
2
|
-
import { http } from "../http/
|
1
|
+
import { HttpHandler, Route } from "./router.d";
|
2
|
+
import { http } from "../http/http";
|
3
|
+
import { createContext } from './context';
|
3
4
|
|
4
5
|
const splitPath = (s: string): string[] => s.split('/').filter(x => x !== '');
|
5
6
|
|
@@ -16,35 +17,7 @@ const createRoute = (path: string, method: string, handler: HttpHandler): Route
|
|
16
17
|
return route;
|
17
18
|
};
|
18
19
|
|
19
|
-
const
|
20
|
-
const pathParts = splitPath(path);
|
21
|
-
const routeParts = splitPath(route.path);
|
22
|
-
|
23
|
-
for (let i = 0; i < routeParts.length; i++) {
|
24
|
-
const part = routeParts[i];
|
25
|
-
if (part.startsWith(':')) {
|
26
|
-
params.set(part.slice(1), pathParts[i]);
|
27
|
-
}
|
28
|
-
}
|
29
|
-
};
|
30
|
-
|
31
|
-
const createContext = (path: string, route: Route, req: Request): Context => {
|
32
|
-
const params: Map<string, string> = new Map();
|
33
|
-
|
34
|
-
if (route) extractParams(path, route, params);
|
35
|
-
|
36
|
-
return {
|
37
|
-
params: params,
|
38
|
-
request: req,
|
39
|
-
query: new URLSearchParams(path),
|
40
|
-
cookies: new Map(),
|
41
|
-
formData: undefined,
|
42
|
-
logger: undefined,
|
43
|
-
json: (statusCode: number, data: any) => http.json(statusCode, data),
|
44
|
-
}
|
45
|
-
};
|
46
|
-
|
47
|
-
const Radix = () => {
|
20
|
+
const RouteTree = () => {
|
48
21
|
let root = createRoute('', 'GET', () => http.notFound());
|
49
22
|
|
50
23
|
const addRoute = (path: string, method: string, handler: HttpHandler) => {
|
@@ -87,4 +60,4 @@ const Radix = () => {
|
|
87
60
|
|
88
61
|
};
|
89
62
|
|
90
|
-
export {
|
63
|
+
export { RouteTree, createContext }
|
package/package.json
CHANGED
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();
|