bun-router 0.7.3 → 0.7.4-experimental.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintrc.json +34 -0
- package/README.md +23 -0
- package/bun.lockb +0 -0
- package/lib/fs/fsys.ts +5 -1
- package/lib/http/http.ts +68 -67
- package/lib/logger/logger.ts +59 -59
- package/lib/router/context.ts +41 -34
- package/lib/router/router.d.ts +4 -3
- package/lib/router/router.ts +85 -85
- package/lib/router/tree.ts +53 -54
- package/package.json +11 -2
- package/tsconfig.json +0 -1
package/.eslintrc.json
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
{
|
2
|
+
"env": {
|
3
|
+
"browser": true,
|
4
|
+
"es2021": true
|
5
|
+
},
|
6
|
+
"extends": [
|
7
|
+
"eslint:recommended",
|
8
|
+
"plugin:@typescript-eslint/recommended",
|
9
|
+
"plugin:react/recommended"
|
10
|
+
],
|
11
|
+
"parser": "@typescript-eslint/parser",
|
12
|
+
"parserOptions": {
|
13
|
+
"ecmaVersion": "latest",
|
14
|
+
"sourceType": "module"
|
15
|
+
},
|
16
|
+
"plugins": [
|
17
|
+
"@typescript-eslint",
|
18
|
+
"react"
|
19
|
+
],
|
20
|
+
"rules": {
|
21
|
+
"indent": [
|
22
|
+
"warn",
|
23
|
+
"tab"
|
24
|
+
],
|
25
|
+
"quotes": [
|
26
|
+
"error",
|
27
|
+
"single"
|
28
|
+
],
|
29
|
+
"semi": [
|
30
|
+
"error",
|
31
|
+
"always"
|
32
|
+
]
|
33
|
+
}
|
34
|
+
}
|
package/README.md
CHANGED
@@ -53,4 +53,27 @@ router.post('/register', ctx => {
|
|
53
53
|
|
54
54
|
```
|
55
55
|
|
56
|
+
##### JSX
|
57
|
+
```tsx
|
58
|
+
// ./pages/home.tsx
|
59
|
+
export default const Home = (title: string) => {
|
60
|
+
return (
|
61
|
+
<main>
|
62
|
+
<h1>{ title }</h1>
|
63
|
+
</main>
|
64
|
+
);
|
65
|
+
};
|
66
|
+
```
|
67
|
+
|
68
|
+
```ts
|
69
|
+
// ./index.ts
|
70
|
+
import { Router } from 'bun-router';
|
71
|
+
import Home from './pages/home';
|
72
|
+
|
73
|
+
const router = Router();
|
74
|
+
|
75
|
+
router.get('/', ctx => ctx.render(Home('Hello World')))
|
76
|
+
|
77
|
+
router.serve();
|
78
|
+
```
|
56
79
|
|
package/bun.lockb
CHANGED
Binary file
|
package/lib/fs/fsys.ts
CHANGED
package/lib/http/http.ts
CHANGED
@@ -1,77 +1,78 @@
|
|
1
|
-
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
import { httpStatusCodes } from './status';
|
2
3
|
|
3
4
|
const http = {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
5
|
+
ok: async (msg?: string): Promise<Response> => {
|
6
|
+
return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
|
7
|
+
status: 200,
|
8
|
+
statusText: httpStatusCodes[200],
|
9
|
+
}));
|
10
|
+
},
|
11
|
+
json: async (statusCode: number, data: any): Promise<Response> => {
|
12
|
+
const jsonString = JSON.stringify(data);
|
13
|
+
return Promise.resolve(new Response(jsonString, {
|
14
|
+
status: statusCode,
|
15
|
+
statusText: httpStatusCodes[statusCode],
|
16
|
+
headers: {'Content-Type': 'application/json'},
|
17
|
+
}));
|
18
|
+
},
|
19
|
+
html: async (statusCode: number, content: string): Promise<Response> => {
|
20
|
+
return Promise.resolve(new Response(content, {
|
21
|
+
status: statusCode,
|
22
|
+
statusText: httpStatusCodes[statusCode],
|
23
|
+
headers: {'Content-Type': 'text/html; charset=utf-8'}
|
24
|
+
}));
|
25
|
+
},
|
26
|
+
file: async (statusCode: number, fp: string): Promise<Response> => {
|
27
|
+
const file = Bun.file(fp);
|
28
|
+
const exists = await file.exists();
|
28
29
|
|
29
|
-
|
30
|
+
if (!exists) return http.notFound(`File not found: ${fp}`);
|
30
31
|
|
31
|
-
|
32
|
-
|
32
|
+
const content = await file.arrayBuffer();
|
33
|
+
if (!content) return http.noContent();
|
33
34
|
|
34
|
-
|
35
|
+
let contentType = 'text/html; charset=utf-9';
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
if (file.type.includes('image'))
|
38
|
+
contentType = file.type + '; charset=utf-8';
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
40
|
+
return Promise.resolve(new Response(content, {
|
41
|
+
status: statusCode,
|
42
|
+
statusText: httpStatusCodes[statusCode],
|
43
|
+
headers: { 'Content-Type': contentType}
|
44
|
+
}));
|
45
|
+
},
|
46
|
+
noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
|
47
|
+
status: 204,
|
48
|
+
statusText: 'no content',
|
49
|
+
})),
|
50
|
+
notFound: async(msg?: string): Promise<Response> => {
|
51
|
+
const response = new Response(msg ?? 'not found', {
|
52
|
+
status: 404,
|
53
|
+
statusText: httpStatusCodes[404],
|
54
|
+
headers: {'Content-Type': 'text/html'},
|
55
|
+
});
|
55
56
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
57
|
+
return Promise.resolve(response);
|
58
|
+
},
|
59
|
+
methodNotAllowed: async (msg?: string): Promise<Response> => {
|
60
|
+
const response = new Response(msg ?? 'method not allowed', {
|
61
|
+
status: 405,
|
62
|
+
statusText: httpStatusCodes[405],
|
63
|
+
headers: {'Content-Type': 'text/html'},
|
64
|
+
});
|
64
65
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
}
|
66
|
+
return Promise.resolve(response);
|
67
|
+
},
|
68
|
+
message: async (status: number, msg?: string): Promise<Response> => {
|
69
|
+
const response = new Response(msg ?? '?', {
|
70
|
+
status: status,
|
71
|
+
statusText: httpStatusCodes[status],
|
72
|
+
headers: {'Content-Type': 'text/html; charset-utf-8'},
|
73
|
+
});
|
74
|
+
return Promise.resolve(response);
|
75
|
+
},
|
76
|
+
};
|
76
77
|
|
77
|
-
export { http }
|
78
|
+
export { http };
|
package/lib/logger/logger.ts
CHANGED
@@ -8,87 +8,87 @@ _ _
|
|
8
8
|
| . | | | | | _| . | | | _| -_| _|
|
9
9
|
|___|___|_|_| |_| |___|___|_| |___|_|
|
10
10
|
|
11
|
-
|
12
|
-
const VERSION = '0.7.
|
11
|
+
`;
|
12
|
+
const VERSION = '0.7.4-experimental';
|
13
13
|
const Logger = (): BunLogger => {
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
19
|
|
20
|
-
|
20
|
+
message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${ message ?? ''}\n`;
|
21
21
|
|
22
|
-
|
22
|
+
await Bun.write(Bun.stdout, message);
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
29
|
|
30
|
-
|
30
|
+
const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' -> ' : ' <-'} ${error.message}\n`;
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
38
|
|
39
|
-
|
39
|
+
message = `${source} : ${messageColor}\n`;
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
47
|
|
48
|
-
|
48
|
+
message = `${source}: ${messageColor}\n`;
|
49
49
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
}
|
50
|
+
await Bun.write(Bun.stdout, message);
|
51
|
+
},
|
52
|
+
};
|
53
|
+
};
|
54
54
|
|
55
55
|
function timestamp(date: Date) {
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
56
|
+
const month = pad(date.getMonth());
|
57
|
+
const day = pad(date.getDate());
|
58
|
+
const hour = pad(date.getHours());
|
59
|
+
const minute = pad(date.getMinutes());
|
60
|
+
const seconds = pad(date.getSeconds());
|
61
|
+
const stamp = `${hour}:${minute}:${seconds}`;
|
62
|
+
|
63
|
+
return {month, day, hour, minute, stamp};
|
64
64
|
}
|
65
65
|
|
66
66
|
function setColor(n: number, text?: string){
|
67
|
-
|
67
|
+
const s = ` [${String(n)}${text ?? ''}] `;
|
68
68
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
69
|
+
if (n < 100) return color('black', 'bgYellow', s);
|
70
|
+
else if (n >= 100 && n < 200) return color('black', 'bgCyan', s);
|
71
|
+
else if (n >= 200 && n < 300) return color('black', 'bgGreen', s);
|
72
|
+
else if (n >= 300 && n < 400) return color('black', 'bgRed', s);
|
73
|
+
else if (n >= 400 && n < 500) return color('black', 'bgRed', s);
|
74
|
+
else if (n >= 500) return color('white', 'bgRed', s);
|
75
75
|
|
76
|
-
|
76
|
+
return color('white', 'bgBlack', `[${s}]`).trim();
|
77
77
|
}
|
78
78
|
|
79
79
|
function startMessage(port: number | string) {
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
80
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
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`);
|
85
|
+
|
86
|
+
Bun.write(Bun.stdout, TITLE + '\n' + version);
|
87
|
+
Bun.write(Bun.stdout, msg);
|
88
88
|
}
|
89
89
|
|
90
90
|
function pad(n: number) {
|
91
|
-
|
91
|
+
return String(n).padStart(2, '0');
|
92
92
|
}
|
93
93
|
|
94
|
-
export { Logger, startMessage }
|
94
|
+
export { Logger, startMessage };
|
package/lib/router/context.ts
CHANGED
@@ -1,51 +1,58 @@
|
|
1
|
-
|
2
|
-
import {
|
3
|
-
import {
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
import { Route, Context } from './router.d';
|
3
|
+
import { renderToReadableStream } from 'react-dom/server';
|
4
|
+
import { Logger } from '../logger/logger';
|
5
|
+
import { http } from './router';
|
6
|
+
import { ReactNode } from 'react';
|
4
7
|
|
5
8
|
async function createContext(path: string, route: Route, request: Request): Promise<Context> {
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
9
|
+
const params = extractParams(path, route);
|
10
|
+
const query = new URLSearchParams(path);
|
11
|
+
const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
|
12
|
+
|
13
|
+
return Promise.resolve({
|
14
|
+
params,
|
15
|
+
request,
|
16
|
+
query,
|
17
|
+
formData,
|
18
|
+
logger: Logger(),
|
19
|
+
json: (statusCode: number, data: any) => http.json(statusCode, data),
|
20
|
+
render: async (component: ReactNode) => await renderStream(component),
|
21
|
+
});
|
18
22
|
}
|
19
23
|
|
20
24
|
function extractParams(path: string, route: Route): Map<string, string> {
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
const params: Map<string, string> = new Map();
|
26
|
+
const pathSegments = path.split('/');
|
27
|
+
const routeSegments = route.path.split('/');
|
24
28
|
|
25
|
-
|
29
|
+
if (pathSegments.length !== routeSegments.length) return params;
|
26
30
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
32
|
+
if (routeSegments[i][0] === ':') {
|
33
|
+
const key = routeSegments[i].replace(':', '');
|
34
|
+
const value = pathSegments[i];
|
35
|
+
params.set(key, value);
|
36
|
+
}
|
37
|
+
}
|
34
38
|
|
35
|
-
|
39
|
+
return params;
|
36
40
|
}
|
37
41
|
|
38
42
|
function getContentType(headers: Headers): string {
|
39
|
-
|
40
|
-
|
41
|
-
|
43
|
+
const contentType = headers.get('Content-Type');
|
44
|
+
if (!contentType) return '';
|
45
|
+
return contentType;
|
42
46
|
}
|
43
47
|
|
44
48
|
function isMultiPartForm(headers: Headers): boolean {
|
45
|
-
|
46
|
-
|
49
|
+
const contentType = getContentType(headers);
|
50
|
+
return contentType.includes('multipart/form-data');
|
47
51
|
}
|
48
52
|
|
53
|
+
async function renderStream(children: ReactNode) {
|
54
|
+
const stream = await renderToReadableStream(children);
|
55
|
+
return new Response(stream, { headers: { 'Content-Type': 'text/html' } });
|
56
|
+
}
|
49
57
|
|
50
|
-
|
51
|
-
export { createContext }
|
58
|
+
export { createContext };
|
package/lib/router/router.d.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
import { TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptions, TLSServeOptions } from 'bun';
|
2
3
|
import { Logger } from '../logger/logger';
|
3
4
|
import { Database } from 'bun:sqlite';
|
4
5
|
|
@@ -20,7 +21,6 @@ type Route = {
|
|
20
21
|
handler: HttpHandler;
|
21
22
|
isLast: boolean;
|
22
23
|
}
|
23
|
-
|
24
24
|
type Context = {
|
25
25
|
db?: Database;
|
26
26
|
formData: FormData | Promise<FormData>;
|
@@ -29,6 +29,7 @@ type Context = {
|
|
29
29
|
params: Map<string, string>;
|
30
30
|
query: URLSearchParams;
|
31
31
|
request: Request;
|
32
|
+
render: (component: React.ReactNode) => Response | Promise<Response>;
|
32
33
|
};
|
33
34
|
|
34
35
|
type HttpHandler = (ctx: Context) => Response | Promise<Response>
|
@@ -43,4 +44,4 @@ type RouterOptions<Options> = ServeOptions
|
|
43
44
|
| TLSWebSocketServeOptions<Options>
|
44
45
|
| undefined
|
45
46
|
|
46
|
-
export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler }
|
47
|
+
export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler };
|
package/lib/router/router.ts
CHANGED
@@ -5,96 +5,96 @@ import { httpStatusCodes } from '../http/status';
|
|
5
5
|
import { readDir } from '../fs/fsys';
|
6
6
|
import { Logger, startMessage } from '../logger/logger';
|
7
7
|
import { http } from '../http/http';
|
8
|
-
import {RouteTree } from './tree';
|
8
|
+
import { RouteTree } from './tree';
|
9
9
|
import { createContext } from './context';
|
10
10
|
|
11
11
|
|
12
12
|
const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
13
|
+
const { addRoute, findRoute } = RouteTree();
|
14
|
+
const logger = Logger();
|
15
|
+
|
16
|
+
return {
|
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
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
24
|
+
// add a static route to the router tree
|
25
|
+
static: async (pattern: string, root: string) => {
|
26
|
+
await readDir(root, async (fp) => {
|
27
|
+
const pure = path.join('.', fp);
|
28
|
+
const ext = path.extname(pure);
|
29
|
+
|
30
|
+
let base = path.basename(pure);
|
31
|
+
|
32
|
+
if (ext === '.html') base = base.replace(ext, '');
|
33
|
+
|
34
|
+
if (pattern[0] !== '/') pattern = '/' + pattern;
|
35
|
+
|
36
|
+
let patternPath = pattern + base;
|
37
|
+
|
38
|
+
if (base === 'index') patternPath = pattern;
|
39
|
+
|
40
|
+
const route: Route = {
|
41
|
+
children: new Map(),
|
42
|
+
dynamicPath: '',
|
43
|
+
isLast: true,
|
44
|
+
path: patternPath,
|
45
|
+
method: 'GET',
|
46
|
+
handler: async () => await http.file(200, pure),
|
47
|
+
};
|
48
|
+
|
49
|
+
addRoute(route.path, 'GET', route.handler);
|
50
|
+
});
|
51
|
+
},
|
52
|
+
// start the server
|
53
|
+
serve: () => {
|
54
|
+
startMessage(port ?? 3000);
|
55
|
+
const opts: Options = { db: ':memory:' };
|
56
|
+
|
57
|
+
Bun.serve({
|
58
|
+
port: port ?? 3000,
|
59
|
+
...options,
|
60
|
+
async fetch(req) {
|
61
|
+
const url = new URL(req.url);
|
62
|
+
const path = url.pathname;
|
63
|
+
|
64
|
+
// set the database
|
65
|
+
if (options) {
|
66
|
+
const o = options as Options;
|
67
|
+
opts.db = o.db;
|
68
|
+
}
|
69
|
+
|
70
|
+
const route = findRoute(path);
|
71
|
+
|
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
|
+
}
|
78
|
+
|
79
|
+
const context = await createContext(path, route, req);
|
80
|
+
context.db = new Database(opts.db);
|
81
|
+
|
82
|
+
const response = await route.handler(context);
|
83
|
+
|
84
|
+
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
85
|
+
return Promise.resolve(response);
|
86
|
+
}
|
87
|
+
|
88
|
+
// if no route is found, return 404
|
89
|
+
const response = await http.notFound();
|
90
90
|
|
91
|
-
|
92
|
-
|
91
|
+
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
92
|
+
return Promise.resolve(http.notFound());
|
93
93
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
}
|
94
|
+
}
|
95
|
+
});
|
96
|
+
},
|
97
|
+
};
|
98
|
+
};
|
99
99
|
|
100
|
-
export { Router, http }
|
100
|
+
export { Router, http };
|
package/lib/router/tree.ts
CHANGED
@@ -1,63 +1,62 @@
|
|
1
|
-
import { HttpHandler, Route } from
|
2
|
-
import { http } from
|
1
|
+
import { HttpHandler, Route } from './router.d';
|
2
|
+
import { http } from '../http/http';
|
3
3
|
import { createContext } from './context';
|
4
|
-
|
5
|
-
const splitPath = (s: string): string[] => s.split('/').filter(x => x !== '');
|
4
|
+
import { splitPath } from '../util/strings';
|
6
5
|
|
7
6
|
const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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;
|
18
17
|
};
|
19
18
|
|
20
19
|
const RouteTree = () => {
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
20
|
+
const root = createRoute('', 'GET', () => http.notFound());
|
21
|
+
|
22
|
+
const addRoute = (path: string, method: string, handler: HttpHandler) => {
|
23
|
+
const pathParts = splitPath(path);
|
24
|
+
let current = root;
|
25
|
+
|
26
|
+
for (let i = 0; i < pathParts.length; i++) {
|
27
|
+
const part = pathParts[i];
|
28
|
+
if (part.startsWith(':')) {
|
29
|
+
current.dynamicPath = part;
|
30
|
+
}
|
31
|
+
if (!current.children.has(part)) {
|
32
|
+
current.children.set(part, createRoute(part, method, handler));
|
33
|
+
}
|
34
|
+
current = current.children.get(part)!;
|
35
|
+
}
|
36
|
+
|
37
|
+
current.handler = handler;
|
38
|
+
current.isLast = true;
|
39
|
+
current.path = path;
|
40
|
+
};
|
41
|
+
|
42
|
+
function findRoute(path: string): Route | undefined {
|
43
|
+
const pathParts = splitPath(path);
|
44
|
+
let current = root;
|
45
|
+
for (let i = 0; i < pathParts.length; i++) {
|
46
|
+
const part = pathParts[i];
|
47
|
+
if (current.children.has(part)) {
|
48
|
+
current = current.children.get(part)!;
|
49
|
+
} else if (current.dynamicPath) {
|
50
|
+
current = current.children.get(current.dynamicPath)!;
|
51
|
+
} else {
|
52
|
+
return;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
return current;
|
56
|
+
}
|
57
|
+
|
58
|
+
return { addRoute, findRoute };
|
60
59
|
|
61
60
|
};
|
62
61
|
|
63
|
-
export { RouteTree, createContext }
|
62
|
+
export { RouteTree, createContext as createContext };
|
package/package.json
CHANGED
@@ -3,10 +3,19 @@
|
|
3
3
|
"module": "index.ts",
|
4
4
|
"type": "module",
|
5
5
|
"devDependencies": {
|
6
|
-
"
|
6
|
+
"@types/react-dom": "^18.2.7",
|
7
|
+
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
8
|
+
"@typescript-eslint/parser": "^6.7.0",
|
9
|
+
"bun-types": "latest",
|
10
|
+
"eslint": "^8.49.0",
|
11
|
+
"eslint-plugin-react": "^7.33.2",
|
12
|
+
"react-dom": "^18.2.0"
|
7
13
|
},
|
8
14
|
"peerDependencies": {
|
9
15
|
"typescript": "^5.0.0"
|
10
16
|
},
|
11
|
-
"version": "0.7.
|
17
|
+
"version": "0.7.4-experimental.0",
|
18
|
+
"dependencies": {
|
19
|
+
"eslint-plugin-react-hooks": "^4.6.0"
|
20
|
+
}
|
12
21
|
}
|