bun-router 0.5.8 → 0.7.0
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 +1 -1
- package/examples/basic.ts +3 -8
- package/examples/cookies.ts +15 -0
- package/examples/dynamic.ts +12 -17
- package/examples/logger.ts +2 -2
- package/examples/todo.ts +3 -3
- package/lib/http/generic-methods.ts +20 -13
- package/lib/logger/color.ts +1 -1
- package/lib/logger/logger.ts +9 -0
- package/lib/router/router.d.ts +23 -17
- package/lib/router/router.ts +43 -86
- package/lib/router/tree.ts +90 -0
- package/package.json +1 -1
- package/tests/router.test.ts +0 -64
- package/examples/sqlite.ts +0 -22
package/README.md
CHANGED
package/examples/basic.ts
CHANGED
|
@@ -2,17 +2,12 @@ import { router, http } from '..';
|
|
|
2
2
|
|
|
3
3
|
const r = router();
|
|
4
4
|
|
|
5
|
-
r.add('/', 'GET', () => http.json('ok'));
|
|
5
|
+
r.add('/', 'GET', () => http.json(200, 'ok'));
|
|
6
6
|
|
|
7
7
|
r.add('/user/:name', 'GET', (ctx) => {
|
|
8
8
|
const name = ctx.params.get('name');
|
|
9
|
-
return http.json(name);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
r.add('/user/:name/:id', 'GET', (ctx) => {
|
|
13
|
-
const name = ctx.params.get('name');
|
|
14
|
-
const id = ctx.params.get('id');
|
|
15
|
-
return http.json({name: name, id: id});
|
|
9
|
+
if (!name) return http.json(500, 'no name');
|
|
10
|
+
return http.json(200, name);
|
|
16
11
|
});
|
|
17
12
|
|
|
18
13
|
r.serve();
|
|
@@ -0,0 +1,15 @@
|
|
|
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/dynamic.ts
CHANGED
|
@@ -1,29 +1,24 @@
|
|
|
1
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('<h6 style="color: red">User Undefined</h6>');
|
|
7
|
-
return http.html(`<h4>Hello, ${name}!</h4>`);
|
|
8
|
-
}
|
|
4
|
+
const home = (ctx: Context) => 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
18
|
const r = router();
|
|
23
19
|
|
|
24
|
-
r.add('/
|
|
25
|
-
r.add('/
|
|
26
|
-
r.add('/u/:
|
|
27
|
-
|
|
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
|
@@ -8,9 +8,9 @@ r.add('/:foo', 'GET', (ctx) => {
|
|
|
8
8
|
const foo = ctx.params.get('foo');
|
|
9
9
|
if (!foo) {
|
|
10
10
|
log.error(500, url.pathname, ctx.request.method, new Error('undefined'));
|
|
11
|
-
return http.json(
|
|
11
|
+
return http.json(500,{text: 'Foo is undefined'});
|
|
12
12
|
}
|
|
13
|
-
return http.html(`<h4 style='font-family: sans-serif;'>Oh hello, ${foo}</h4>`)
|
|
13
|
+
return http.html(200, `<h4 style='font-family: sans-serif;'>Oh hello, ${foo}</h4>`)
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
r.serve();
|
package/examples/todo.ts
CHANGED
|
@@ -26,7 +26,7 @@ r.add('/api/new', 'POST', ctx => {
|
|
|
26
26
|
ctx.logger.message(`Adding ${key} with ${content}`);
|
|
27
27
|
todo.add(key, content);
|
|
28
28
|
|
|
29
|
-
return ctx.json({ message: 'ok' });
|
|
29
|
+
return ctx.json(200, { message: 'ok' });
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
r.add('/api/todo/:key', 'GET', ctx => {
|
|
@@ -36,11 +36,11 @@ r.add('/api/todo/:key', 'GET', ctx => {
|
|
|
36
36
|
const content = todo.get(key);
|
|
37
37
|
if (!content) return http.notFound();
|
|
38
38
|
|
|
39
|
-
return ctx.json({key: key, content: content});
|
|
39
|
+
return ctx.json(200, {key: key, content: content});
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
r.add('/api/get/all', 'GET', ctx => {
|
|
43
|
-
return ctx.json(todo.export());
|
|
43
|
+
return ctx.json(200, todo.export());
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
r.serve();
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { httpStatusCodes } from "./status";
|
|
2
2
|
|
|
3
3
|
const http = {
|
|
4
|
-
json: async (data: any): Promise<Response> => {
|
|
4
|
+
json: async (statusCode: number, data: any): Promise<Response> => {
|
|
5
5
|
const jsonString = JSON.stringify(data);
|
|
6
6
|
return Promise.resolve(new Response(jsonString, {
|
|
7
|
-
status:
|
|
8
|
-
statusText: httpStatusCodes[
|
|
7
|
+
status: statusCode,
|
|
8
|
+
statusText: httpStatusCodes[statusCode],
|
|
9
9
|
headers: {'Content-Type': 'application/json'},
|
|
10
10
|
}));
|
|
11
11
|
},
|
|
12
|
-
html: async (content: string): Promise<Response> => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
statusText: httpStatusCodes[200],
|
|
12
|
+
html: async (statusCode: number, content: string): Promise<Response> => {
|
|
13
|
+
return Promise.resolve(new Response(content, {
|
|
14
|
+
status: statusCode,
|
|
15
|
+
statusText: httpStatusCodes[statusCode],
|
|
17
16
|
headers: {'Content-Type': 'text/html; charset=utf-8'}
|
|
18
17
|
}));
|
|
19
18
|
},
|
|
20
|
-
file: async (fp: string): Promise<Response> => {
|
|
19
|
+
file: async (statusCode: number, fp: string): Promise<Response> => {
|
|
21
20
|
const file = Bun.file(fp);
|
|
22
21
|
const exists = await file.exists();
|
|
23
22
|
|
|
@@ -32,8 +31,8 @@ const http = {
|
|
|
32
31
|
contentType = file.type + '; charset=utf-8';
|
|
33
32
|
|
|
34
33
|
return Promise.resolve(new Response(content, {
|
|
35
|
-
status:
|
|
36
|
-
statusText: httpStatusCodes[
|
|
34
|
+
status: statusCode,
|
|
35
|
+
statusText: httpStatusCodes[statusCode],
|
|
37
36
|
headers: { 'Content-Type': contentType}
|
|
38
37
|
}));
|
|
39
38
|
},
|
|
@@ -50,6 +49,15 @@ const http = {
|
|
|
50
49
|
|
|
51
50
|
return Promise.resolve(response);
|
|
52
51
|
},
|
|
52
|
+
methodNotAllowed: async (msg?: string): Promise<Response> => {
|
|
53
|
+
const response = new Response(msg ?? 'method not allowed', {
|
|
54
|
+
status: 405,
|
|
55
|
+
statusText: httpStatusCodes[405],
|
|
56
|
+
headers: {'Content-Type': 'text/html'},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return Promise.resolve(response);
|
|
60
|
+
},
|
|
53
61
|
message: async (status: number, msg?: string): Promise<Response> => {
|
|
54
62
|
const response = new Response(msg ?? '?', {
|
|
55
63
|
status: status,
|
|
@@ -57,8 +65,7 @@ const http = {
|
|
|
57
65
|
headers: {'Content-Type': 'text/html; charset-utf-8'},
|
|
58
66
|
});
|
|
59
67
|
return Promise.resolve(response)
|
|
60
|
-
}
|
|
61
|
-
|
|
68
|
+
},
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
export { http }
|
package/lib/logger/color.ts
CHANGED
package/lib/logger/logger.ts
CHANGED
|
@@ -3,6 +3,14 @@ import {Logger} from './logger.d';
|
|
|
3
3
|
|
|
4
4
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
5
5
|
|
|
6
|
+
const TITLE = `
|
|
7
|
+
_ _
|
|
8
|
+
| |_ _ _ ___ ___ ___ _ _| |_ ___ ___
|
|
9
|
+
| . | | | | | _| . | | | _| -_| _|
|
|
10
|
+
|___|___|_|_| |_| |___|___|_| |___|_|
|
|
11
|
+
|
|
12
|
+
`
|
|
13
|
+
|
|
6
14
|
const timestamp = (date: Date) => {
|
|
7
15
|
const month = pad(date.getMonth());
|
|
8
16
|
const day = pad(date.getDate());
|
|
@@ -48,6 +56,7 @@ const logger = (): Logger => {
|
|
|
48
56
|
const portColor = color('green', 'bgBlack', String(port));
|
|
49
57
|
const msg = `${source}: Starting Server on :${portColor}\n`;
|
|
50
58
|
|
|
59
|
+
await Bun.write(Bun.stdout, TITLE);
|
|
51
60
|
await Bun.write(Bun.stdout, msg);
|
|
52
61
|
},
|
|
53
62
|
info: async (statusCode: number, routePath: string, method: string, message?: string) => {
|
package/lib/router/router.d.ts
CHANGED
|
@@ -2,21 +2,28 @@ import { TLSOptions, TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptio
|
|
|
2
2
|
import { Logger } from '../logger/logger';
|
|
3
3
|
import { Database } from 'bun:sqlite';
|
|
4
4
|
|
|
5
|
+
type HttpHandler = (ctx: Context) => Response | Promise<Response>
|
|
5
6
|
|
|
6
7
|
type Context = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
cookies: Map<string, string>;
|
|
9
|
+
db?: Database;
|
|
10
|
+
formData: FormData | Promise<FormData> | undefined;
|
|
11
|
+
json: (statusCode: number, data: any) => Response | Promise<Response>;
|
|
12
|
+
logger: Logger;
|
|
13
|
+
params: Map<string, string>;
|
|
14
|
+
query: URLSearchParams;
|
|
15
|
+
request: Request;
|
|
16
|
+
token?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
type Route = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
children: Map<string, Route>;
|
|
22
|
+
path: string;
|
|
23
|
+
dynamicPath: string;
|
|
24
|
+
method: string;
|
|
25
|
+
handler: HttpHandler;
|
|
26
|
+
isLast: boolean;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
type Options = {
|
|
@@ -31,12 +38,11 @@ type RouterOptions<Options> = ServeOptions
|
|
|
31
38
|
|
|
32
39
|
|
|
33
40
|
type Router = (port?: number | string, options?: RouterOptions) => {
|
|
34
|
-
add: (pattern: string, method: string, callback: (req: Context) => Response | Promise<Response>) => void
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
static: (pattern: string, root: string) => void,
|
|
38
|
-
serve: () => void,
|
|
41
|
+
add: (pattern: string, method: string, callback: (req: Context) => Response | Promise<Response>) => void;
|
|
42
|
+
static: (pattern: string, root: string) => void;
|
|
43
|
+
serve: () => void;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
|
|
48
|
+
export { Context , Route, Router, RouterOptions, Options, HttpHandler }
|
package/lib/router/router.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { Database } from 'bun:sqlite';
|
|
3
|
-
import { Route, Router, Context, RouterOptions, Options } from './router.d';
|
|
3
|
+
import { Route, Router, Context, RouterOptions, Options, HttpHandler } from './router.d';
|
|
4
4
|
import { httpStatusCodes } from '../http/status';
|
|
5
5
|
import { readDir } from '../fs/fsys';
|
|
6
6
|
import { logger } from '../logger/logger';
|
|
7
|
-
import { Logger } from '../logger/logger.d';
|
|
8
7
|
import { http } from '../http/generic-methods';
|
|
8
|
+
import {Radix, createContext} from './tree';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
// if the route pattern is /:foo and the request URL is /bar: {foo: 'bar'}
|
|
12
|
-
const extract = (route: Route, ctx: Context) => {
|
|
10
|
+
const extract = (path: string, ctx: Context) => {
|
|
13
11
|
const url = new URL(ctx.request.url);
|
|
14
|
-
const pathSegments =
|
|
12
|
+
const pathSegments = path.split('/');
|
|
15
13
|
const urlSegments = url.pathname.split('/');
|
|
16
14
|
|
|
17
15
|
if (pathSegments.length !== urlSegments.length) return
|
|
@@ -19,10 +17,10 @@ const extract = (route: Route, ctx: Context) => {
|
|
|
19
17
|
return {
|
|
20
18
|
params: () => {
|
|
21
19
|
for (let i = 0; i < pathSegments.length; i++) {
|
|
22
|
-
if
|
|
20
|
+
if((pathSegments[i][0] === ':')) {
|
|
23
21
|
const k = pathSegments[i].replace(':', '');
|
|
24
22
|
const v = urlSegments[i];
|
|
25
|
-
ctx.params.set(k,
|
|
23
|
+
ctx.params.set(k,v);
|
|
26
24
|
}
|
|
27
25
|
}
|
|
28
26
|
}
|
|
@@ -30,63 +28,16 @@ const extract = (route: Route, ctx: Context) => {
|
|
|
30
28
|
|
|
31
29
|
}
|
|
32
30
|
|
|
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
|
-
return {
|
|
52
|
-
request: req,
|
|
53
|
-
params: new Map(),
|
|
54
|
-
db: new Database(opts.db ?? ':memory:'),
|
|
55
|
-
logger: lgr,
|
|
56
|
-
route: route,
|
|
57
|
-
json: (data: any) => http.json(data),
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
31
|
const router: Router = (port?: number | string, options?: RouterOptions<Options>) => {
|
|
62
|
-
const
|
|
32
|
+
const {addRoute, findRoute} = Radix();
|
|
63
33
|
const lgr = logger();
|
|
64
|
-
let dbConn = '';
|
|
65
34
|
|
|
66
35
|
return {
|
|
67
|
-
// add a
|
|
68
|
-
add: (pattern: string, method: string, callback:
|
|
69
|
-
|
|
70
|
-
pattern: pattern,
|
|
71
|
-
method: method,
|
|
72
|
-
callback: callback,
|
|
73
|
-
})
|
|
36
|
+
// add a route to the router tree
|
|
37
|
+
add: (pattern: string, method: string, callback: HttpHandler) => {
|
|
38
|
+
addRoute(pattern, method, callback);
|
|
74
39
|
},
|
|
75
|
-
|
|
76
|
-
routes.push({
|
|
77
|
-
pattern: pattern,
|
|
78
|
-
method: 'GET',
|
|
79
|
-
callback: callback,
|
|
80
|
-
});
|
|
81
|
-
},
|
|
82
|
-
POST: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => {
|
|
83
|
-
routes.push({
|
|
84
|
-
pattern: pattern,
|
|
85
|
-
method: 'POST',
|
|
86
|
-
callback: callback,
|
|
87
|
-
});
|
|
88
|
-
},
|
|
89
|
-
// add a route for static files
|
|
40
|
+
// add a static route to the router tree
|
|
90
41
|
static: async (pattern: string, root: string) => {
|
|
91
42
|
await readDir(root, async (fp, _) => {
|
|
92
43
|
const pure = path.join('.', fp);
|
|
@@ -103,12 +54,15 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
|
|
|
103
54
|
if (base === 'index') patternPath = pattern;
|
|
104
55
|
|
|
105
56
|
const route: Route = {
|
|
106
|
-
|
|
57
|
+
children: new Map(),
|
|
58
|
+
dynamicPath: '',
|
|
59
|
+
isLast: true,
|
|
60
|
+
path: patternPath,
|
|
107
61
|
method: 'GET',
|
|
108
|
-
|
|
62
|
+
handler: async () => await http.file(200, pure),
|
|
109
63
|
};
|
|
110
64
|
|
|
111
|
-
|
|
65
|
+
addRoute(route.path, 'GET', route.handler);
|
|
112
66
|
});
|
|
113
67
|
},
|
|
114
68
|
// start the server
|
|
@@ -116,41 +70,44 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
|
|
|
116
70
|
lgr.start(port ?? 3000);
|
|
117
71
|
let opts: Options = { db: ':memory:' };
|
|
118
72
|
|
|
73
|
+
// TODO: add support for TLS and WebSockets
|
|
119
74
|
Bun.serve({
|
|
120
75
|
port: port ?? 3000,
|
|
121
76
|
...options,
|
|
122
77
|
async fetch(req) {
|
|
123
78
|
const url = new URL(req.url);
|
|
79
|
+
let path = url.pathname;
|
|
124
80
|
|
|
81
|
+
// set the database
|
|
125
82
|
if (options) {
|
|
126
83
|
let o = options as Options;
|
|
127
84
|
opts.db = o.db;
|
|
128
85
|
}
|
|
129
86
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
87
|
+
const route = findRoute(path);
|
|
88
|
+
|
|
89
|
+
// if the route exists, execute the handler
|
|
90
|
+
if (route) {
|
|
91
|
+
if (route.method !== req.method) {
|
|
92
|
+
lgr.info(405, url.pathname, req.method, httpStatusCodes[405]);
|
|
93
|
+
return Promise.resolve(http.methodNotAllowed());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const context = createContext(path, route, req);
|
|
97
|
+
context.db = new Database(opts.db);
|
|
98
|
+
|
|
99
|
+
const response = await route.handler(context);
|
|
100
|
+
|
|
101
|
+
lgr.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
|
102
|
+
return Promise.resolve(response);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// if no route is found, return 404
|
|
106
|
+
const response = await http.notFound();
|
|
107
|
+
|
|
108
|
+
lgr.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
|
109
|
+
return Promise.resolve(http.notFound());
|
|
151
110
|
|
|
152
|
-
lgr.info(statusCode, url.pathname, req.method, httpStatusCodes[statusCode]);
|
|
153
|
-
return Promise.resolve(http.message(statusCode, httpStatusCodes[statusCode]));
|
|
154
111
|
}
|
|
155
112
|
});
|
|
156
113
|
},
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { HttpHandler, Context, Route } from "./router.d";
|
|
2
|
+
import { http } from "../http/generic-methods";
|
|
3
|
+
|
|
4
|
+
const splitPath = (s: string): string[] => s.split('/').filter(x => x !== '');
|
|
5
|
+
|
|
6
|
+
const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
|
|
7
|
+
const route: Route = {
|
|
8
|
+
children: new Map(),
|
|
9
|
+
path: path,
|
|
10
|
+
dynamicPath: '',
|
|
11
|
+
method: method,
|
|
12
|
+
handler: handler,
|
|
13
|
+
isLast: false
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return route;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const extractParams = (path: string, route: Route, params: Map<string, string>) => {
|
|
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 = () => {
|
|
48
|
+
let root = createRoute('', 'GET', () => http.notFound());
|
|
49
|
+
|
|
50
|
+
const addRoute = (path: string, method: string, handler: HttpHandler) => {
|
|
51
|
+
const pathParts = splitPath(path);
|
|
52
|
+
let current = root;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
55
|
+
const part = pathParts[i];
|
|
56
|
+
if (part.startsWith(':')) {
|
|
57
|
+
current.dynamicPath = part;
|
|
58
|
+
}
|
|
59
|
+
if (!current.children.has(part)) {
|
|
60
|
+
current.children.set(part, createRoute(part, method, handler));
|
|
61
|
+
}
|
|
62
|
+
current = current.children.get(part)!;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
current.handler = handler;
|
|
66
|
+
current.isLast = true;
|
|
67
|
+
current.path = path;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const findRoute = (path: string): Route | undefined => {
|
|
71
|
+
const pathParts = splitPath(path);
|
|
72
|
+
let current = root;
|
|
73
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
74
|
+
const part = pathParts[i];
|
|
75
|
+
if (current.children.has(part)) {
|
|
76
|
+
current = current.children.get(part)!;
|
|
77
|
+
} else if (current.dynamicPath) {
|
|
78
|
+
current = current.children.get(current.dynamicPath)!;
|
|
79
|
+
} else {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return current;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { addRoute, findRoute }
|
|
87
|
+
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export { Radix, 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/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({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(d) : new Response('not found', {status: 404});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
r.serve();
|