bun-router 0.7.3 → 0.7.4-experimental.11
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintrc.json +35 -0
- package/README.md +28 -0
- package/bun.lockb +0 -0
- package/examples/basic.ts +4 -4
- package/examples/dynamic.ts +8 -8
- package/examples/logger.ts +2 -2
- package/examples/ssr/index.ts +8 -0
- package/examples/ssr/pages/foo.tsx +7 -0
- package/examples/ssr/pages/home.tsx +7 -0
- package/examples/static.ts +1 -0
- package/examples/todo.ts +22 -22
- package/examples/tsx/components/user.tsx +7 -0
- package/examples/tsx/index.ts +20 -0
- package/lib/fs/filetree.ts +75 -0
- package/lib/fs/fsys.ts +25 -11
- package/lib/http/http.ts +81 -67
- package/lib/http/status.ts +64 -64
- package/lib/logger/color.ts +25 -25
- package/lib/logger/logger.d.ts +1 -1
- package/lib/logger/logger.ts +86 -70
- package/lib/router/context.ts +50 -37
- package/lib/router/routeTree.ts +86 -0
- package/lib/router/router.d.ts +6 -4
- package/lib/router/router.ts +122 -89
- package/package.json +13 -2
- package/tests/router.test.ts +21 -20
- package/tsconfig.json +5 -3
- package/examples/db.ts +0 -10
- package/lib/router/tree.ts +0 -63
- package/lib/util/strings.ts +0 -3
package/lib/http/status.ts
CHANGED
@@ -1,66 +1,66 @@
|
|
1
1
|
const httpStatusCodes: { [key: number]: string } = {
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
2
|
+
100: 'Continue',
|
3
|
+
101: 'Switching Protocols',
|
4
|
+
102: 'Processing',
|
5
|
+
103: 'Early Hints',
|
6
|
+
200: 'OK',
|
7
|
+
201: 'Created',
|
8
|
+
202: 'Accepted',
|
9
|
+
203: 'Non-Authoritative Information',
|
10
|
+
204: 'No Content',
|
11
|
+
205: 'Reset Content',
|
12
|
+
206: 'Partial Content',
|
13
|
+
207: 'Multi-Status',
|
14
|
+
208: 'Already Reported',
|
15
|
+
226: 'IM Used',
|
16
|
+
300: 'Multiple Choices',
|
17
|
+
301: 'Moved Permanently',
|
18
|
+
302: 'Found',
|
19
|
+
303: 'See Other',
|
20
|
+
304: 'Not Modified',
|
21
|
+
305: 'Use Proxy',
|
22
|
+
307: 'Temporary Redirect',
|
23
|
+
308: 'Permanent Redirect',
|
24
|
+
400: 'Bad Request',
|
25
|
+
401: 'Unauthorized',
|
26
|
+
402: 'Payment Required',
|
27
|
+
403: 'Forbidden',
|
28
|
+
404: 'Not Found',
|
29
|
+
405: 'Method Not Allowed',
|
30
|
+
406: 'Not Acceptable',
|
31
|
+
407: 'Proxy Authentication Required',
|
32
|
+
408: 'Request Timeout',
|
33
|
+
409: 'Conflict',
|
34
|
+
410: 'Gone',
|
35
|
+
411: 'Length Required',
|
36
|
+
412: 'Precondition Failed',
|
37
|
+
413: 'Payload Too Large',
|
38
|
+
414: 'URI Too Long',
|
39
|
+
415: 'Unsupported Media Type',
|
40
|
+
416: 'Range Not Satisfiable',
|
41
|
+
417: 'Expectation Failed',
|
42
|
+
418: 'I\'m a Teapot',
|
43
|
+
421: 'Misdirected Request',
|
44
|
+
422: 'Unprocessable Entity',
|
45
|
+
423: 'Locked',
|
46
|
+
424: 'Failed Dependency',
|
47
|
+
425: 'Too Early',
|
48
|
+
426: 'Upgrade Required',
|
49
|
+
428: 'Precondition Required',
|
50
|
+
429: 'Too Many Requests',
|
51
|
+
431: 'Request Header Fields Too Large',
|
52
|
+
451: 'Unavailable For Legal Reasons',
|
53
|
+
500: 'Internal Server Error',
|
54
|
+
501: 'Not Implemented',
|
55
|
+
502: 'Bad Gateway',
|
56
|
+
503: 'Service Unavailable',
|
57
|
+
504: 'Gateway Timeout',
|
58
|
+
505: 'HTTP Version Not Supported',
|
59
|
+
506: 'Variant Also Negotiates',
|
60
|
+
507: 'Insufficient Storage',
|
61
|
+
508: 'Loop Detected',
|
62
|
+
510: 'Not Extended',
|
63
|
+
511: 'Network Authentication Required',
|
64
|
+
};
|
65
65
|
|
66
|
-
|
66
|
+
export { httpStatusCodes };
|
package/lib/logger/color.ts
CHANGED
@@ -1,36 +1,36 @@
|
|
1
1
|
const Colors: Record<string,string> = {
|
2
|
-
|
2
|
+
reset: '\x1b[0m',
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
4
|
+
// foreground
|
5
|
+
black: '\x1b[30m',
|
6
|
+
red: '\x1b[31m',
|
7
|
+
green: '\x1b[32m',
|
8
|
+
yellow: '\x1b[33m',
|
9
|
+
blue: '\x1b[34m',
|
10
|
+
magenta: '\x1b[35m',
|
11
|
+
cyan: '\x1b[36m',
|
12
|
+
white: '\x1b[37m',
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
14
|
+
// background
|
15
|
+
bgBlack: '\x1b[40m',
|
16
|
+
bgRed: '\x1b[41m',
|
17
|
+
bgGreen: '\x1b[42m',
|
18
|
+
bgYellow: '\x1b[43m',
|
19
|
+
bgBlue: '\x1b[44m',
|
20
|
+
bgMagenta: '\x1b[45m',
|
21
|
+
bgCyan: '\x1b[46m',
|
22
|
+
bgWhite: '\x1b[47m',
|
23
|
+
} as const;
|
24
24
|
|
25
25
|
|
26
26
|
|
27
27
|
function color(foreground: string, background: string, message: string) {
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
const _foreground = Colors[foreground];
|
29
|
+
const _background = Colors[background];
|
30
|
+
const reset = Colors.reset;
|
31
|
+
return `${_foreground}${_background}${message}${reset}`;
|
32
32
|
}
|
33
33
|
|
34
34
|
|
35
35
|
|
36
|
-
export { color }
|
36
|
+
export { color };
|
package/lib/logger/logger.d.ts
CHANGED
package/lib/logger/logger.ts
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import { color } from './color';
|
2
|
-
import { BunLogger } from './logger.d';
|
3
2
|
|
4
3
|
|
5
4
|
const TITLE = `
|
@@ -8,87 +7,104 @@ _ _
|
|
8
7
|
| . | | | | | _| . | | | _| -_| _|
|
9
8
|
|___|___|_|_| |_| |___|___|_| |___|_|
|
10
9
|
|
11
|
-
|
12
|
-
const VERSION = '0.7.
|
13
|
-
const Logger = (
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
10
|
+
`;
|
11
|
+
const VERSION = '0.7.4-experimental.11';
|
12
|
+
const Logger = (enableFileLogging: boolean) => {
|
13
|
+
const file = Bun.file('bun-router.log');
|
14
|
+
const writer = enableFileLogging ? file.writer() : null;
|
15
|
+
|
16
|
+
function stripAnsi(str: string) {
|
17
|
+
const ansiRegex = /\u001B\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]/g;
|
18
|
+
return str.replace(ansiRegex, '');
|
19
|
+
}
|
20
|
+
|
21
|
+
async function write(message: string) {
|
22
|
+
await Bun.write(Bun.stdout, message);
|
23
|
+
if (writer) {
|
24
|
+
writer.write(stripAnsi(message));
|
25
|
+
writer.flush();
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
return {
|
30
|
+
info: async (statusCode: number, routePath: string, method: string, message?: string) => {
|
31
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
32
|
+
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
|
33
|
+
const rp = color('white', 'bgBlack', routePath);
|
34
|
+
|
35
|
+
message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${ message ?? ''}\n`;
|
36
|
+
|
37
|
+
await write(message);
|
38
|
+
|
39
|
+
},
|
40
|
+
error: async (statusCode: number, routePath: string, method: string, error: Error) => {
|
41
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
42
|
+
const source = color('black', 'bgRed', `[error ${stamp}]`);
|
43
|
+
const rp = color('white', 'bgBlack', routePath);
|
44
|
+
|
45
|
+
const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' -> ' : ' <-'} ${error.message}\n`;
|
46
|
+
|
47
|
+
await write(message);
|
48
|
+
},
|
49
|
+
warn: async (message: string) => {
|
50
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
51
|
+
const source = color('black', 'bgYellow', `[warning ${stamp}]`);
|
52
|
+
const messageColor = color('yellow', 'bgBlack', message);
|
53
|
+
|
54
|
+
message = `${source} : ${messageColor}\n`;
|
55
|
+
|
56
|
+
await write(message);
|
57
|
+
},
|
58
|
+
message: async (message: string) => {
|
59
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
60
|
+
const source = color('black', 'bgCyan', `[message ${stamp}]`);
|
61
|
+
const messageColor = color('yellow', 'bgBlack', message);
|
62
|
+
|
63
|
+
message = `${source}: ${messageColor}\n`;
|
64
|
+
|
65
|
+
await write(message);
|
66
|
+
},
|
67
|
+
|
68
|
+
};
|
69
|
+
};
|
54
70
|
|
55
71
|
function timestamp(date: Date) {
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
72
|
+
const month = pad(date.getMonth());
|
73
|
+
const day = pad(date.getDate());
|
74
|
+
const hour = pad(date.getHours());
|
75
|
+
const minute = pad(date.getMinutes());
|
76
|
+
const seconds = pad(date.getSeconds());
|
77
|
+
const stamp = `${hour}:${minute}:${seconds}`;
|
78
|
+
|
79
|
+
return {month, day, hour, minute, stamp};
|
64
80
|
}
|
65
81
|
|
66
82
|
function setColor(n: number, text?: string){
|
67
|
-
|
83
|
+
const s = ` [${String(n)}${text ?? ''}] `;
|
68
84
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
85
|
+
if (n < 100) return color('black', 'bgYellow', s);
|
86
|
+
else if (n >= 100 && n < 200) return color('black', 'bgCyan', s);
|
87
|
+
else if (n >= 200 && n < 300) return color('black', 'bgGreen', s);
|
88
|
+
else if (n >= 300 && n < 400) return color('black', 'bgRed', s);
|
89
|
+
else if (n >= 400 && n < 500) return color('black', 'bgRed', s);
|
90
|
+
else if (n >= 500) return color('white', 'bgRed', s);
|
75
91
|
|
76
|
-
|
92
|
+
return color('white', 'bgBlack', `[${s}]`).trim();
|
77
93
|
}
|
78
94
|
|
79
95
|
function startMessage(port: number | string) {
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
96
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
97
|
+
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
|
98
|
+
const portColor = color('green', 'bgBlack', String(port));
|
99
|
+
const msg = `${source}: Starting Server on :${portColor}\n`;
|
100
|
+
const version = color('red', 'bgBlack', `v${VERSION}\n`);
|
101
|
+
|
102
|
+
Bun.write(Bun.stdout, TITLE + '\n' + version);
|
103
|
+
Bun.write(Bun.stdout, msg);
|
88
104
|
}
|
89
105
|
|
90
106
|
function pad(n: number) {
|
91
|
-
|
107
|
+
return String(n).padStart(2, '0');
|
92
108
|
}
|
93
109
|
|
94
|
-
export { Logger, startMessage }
|
110
|
+
export { Logger, startMessage };
|
package/lib/router/context.ts
CHANGED
@@ -1,51 +1,64 @@
|
|
1
|
-
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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';
|
7
|
+
|
8
|
+
// createContext creates a context object
|
9
|
+
async function createContext(path: string, route: Route, request: Request, enableFileLogging: boolean): Promise<Context> {
|
10
|
+
const query = new URLSearchParams(path);
|
11
|
+
const params = extractParams(path, route);
|
12
|
+
const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
|
13
|
+
|
14
|
+
return Promise.resolve({
|
15
|
+
params,
|
16
|
+
request,
|
17
|
+
query,
|
18
|
+
formData,
|
19
|
+
logger: Logger(enableFileLogging),
|
20
|
+
json: (statusCode: number, data: any) => http.json(statusCode, data),
|
21
|
+
render: async (component: ReactNode) => await renderStream(component),
|
22
|
+
});
|
18
23
|
}
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
// extractParams extracts the parameters from the path
|
26
|
+
// and returns a map of key/value pairs
|
27
|
+
// e.g. /users/:id => /users/123 => { id: 123 }
|
28
|
+
function extractParams(pattern: string, route: Route): Map<string, string> {
|
29
|
+
const params: Map<string, string> = new Map();
|
30
|
+
const pathSegments = pattern.split('/');
|
31
|
+
const routeSegments = route.path.split('/');
|
24
32
|
|
25
|
-
|
33
|
+
if (pathSegments.length !== routeSegments.length) return params;
|
26
34
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
35
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
36
|
+
if (routeSegments[i][0] === ':') {
|
37
|
+
const key = routeSegments[i].slice(1);
|
38
|
+
const value = pathSegments[i];
|
39
|
+
params.set(key, value);
|
40
|
+
}
|
41
|
+
}
|
34
42
|
|
35
|
-
|
43
|
+
return params;
|
36
44
|
}
|
37
45
|
|
46
|
+
// getContentType returns the content type from the headers
|
38
47
|
function getContentType(headers: Headers): string {
|
39
|
-
|
40
|
-
|
41
|
-
return contentType;
|
48
|
+
const contentType = headers.get('Content-Type');
|
49
|
+
return contentType ?? '';
|
42
50
|
}
|
43
51
|
|
52
|
+
// isMultiPartForm returns true if the content type is multipart/form-data
|
44
53
|
function isMultiPartForm(headers: Headers): boolean {
|
45
|
-
|
46
|
-
|
54
|
+
const contentType = getContentType(headers);
|
55
|
+
return contentType.includes('multipart/form-data');
|
47
56
|
}
|
48
57
|
|
58
|
+
// renderStream renders the component to a readable stream
|
59
|
+
async function renderStream(children: ReactNode) {
|
60
|
+
const stream = await renderToReadableStream(children);
|
61
|
+
return new Response(stream, { headers: { 'Content-Type': 'text/html' } });
|
62
|
+
}
|
49
63
|
|
50
|
-
|
51
|
-
export { createContext }
|
64
|
+
export { createContext };
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import { HttpHandler, Route } from './router.d';
|
2
|
+
import { http } from '../http/http';
|
3
|
+
import { createContext } from './context';
|
4
|
+
|
5
|
+
const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
|
6
|
+
const route: Route = {
|
7
|
+
children: new Map(),
|
8
|
+
path: path,
|
9
|
+
dynamicPath: '',
|
10
|
+
method: method,
|
11
|
+
handler: handler,
|
12
|
+
isLast: false
|
13
|
+
};
|
14
|
+
|
15
|
+
return route;
|
16
|
+
};
|
17
|
+
|
18
|
+
const RouteTree = () => {
|
19
|
+
const root = createRoute('', 'GET', () => http.notFound());
|
20
|
+
|
21
|
+
function addRoute (pattern: string, method: string, handler: HttpHandler){
|
22
|
+
console.log(pattern);
|
23
|
+
const pathParts = pattern.split('/');
|
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 = pattern;
|
40
|
+
}
|
41
|
+
|
42
|
+
function findRoute(pathname: string): Route | undefined {
|
43
|
+
const pathParts = pathname.split('/');
|
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
|
+
function size() {
|
59
|
+
let count = 0;
|
60
|
+
function traverse(route: Route) {
|
61
|
+
count++;
|
62
|
+
for (const child of route.children.values()) {
|
63
|
+
traverse(child);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
traverse(root);
|
67
|
+
return count;
|
68
|
+
}
|
69
|
+
|
70
|
+
function list() {
|
71
|
+
const routes: Route[] = [];
|
72
|
+
function traverse(route: Route) {
|
73
|
+
routes.push(route);
|
74
|
+
for (const child of route.children.values()) {
|
75
|
+
traverse(child);
|
76
|
+
}
|
77
|
+
}
|
78
|
+
traverse(root);
|
79
|
+
return routes;
|
80
|
+
}
|
81
|
+
|
82
|
+
return { addRoute, findRoute, size, list };
|
83
|
+
|
84
|
+
};
|
85
|
+
|
86
|
+
export { RouteTree, 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,12 +29,14 @@ 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>
|
35
36
|
|
36
37
|
type Options = {
|
37
|
-
db: string
|
38
|
+
db: string;
|
39
|
+
enableFileLogging: boolean;
|
38
40
|
}
|
39
41
|
|
40
42
|
type RouterOptions<Options> = ServeOptions
|
@@ -43,4 +45,4 @@ type RouterOptions<Options> = ServeOptions
|
|
43
45
|
| TLSWebSocketServeOptions<Options>
|
44
46
|
| undefined
|
45
47
|
|
46
|
-
export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler }
|
48
|
+
export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler };
|