bun-router 0.7.4-experimental.9 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintrc.json +3 -1
- package/examples/basic.ts +1 -1
- package/examples/ssr/index.ts +2 -1
- package/examples/tsx/components/user.tsx +7 -0
- package/examples/tsx/index.ts +20 -0
- package/index.ts +1 -0
- package/lib/http/http.ts +8 -1
- package/lib/logger/color.ts +3 -3
- package/lib/logger/logger.d.ts +5 -4
- package/lib/logger/logger.ts +35 -9
- package/lib/router/context.ts +9 -2
- package/lib/router/routeTree.ts +1 -0
- package/lib/router/router.d.ts +2 -1
- package/lib/router/router.ts +15 -11
- package/package.json +9 -8
- package/tsconfig.json +2 -0
package/.eslintrc.json
CHANGED
package/examples/basic.ts
CHANGED
package/examples/ssr/index.ts
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
import { Router, http } from '../../';
|
2
|
+
import { User } from './components/user';
|
3
|
+
|
4
|
+
const router = Router();
|
5
|
+
|
6
|
+
router.get('/', () => {
|
7
|
+
return http.ok();
|
8
|
+
});
|
9
|
+
|
10
|
+
|
11
|
+
router.get('/u/:username', async ctx => {
|
12
|
+
const username = ctx.params.get('username');
|
13
|
+
|
14
|
+
if (!username) return http.message(400, 'invalid username');
|
15
|
+
|
16
|
+
return http.render(User(username));
|
17
|
+
});
|
18
|
+
|
19
|
+
router.serve();
|
20
|
+
|
package/index.ts
CHANGED
package/lib/http/http.ts
CHANGED
@@ -3,6 +3,9 @@ import { httpStatusCodes } from './status';
|
|
3
3
|
import { ReactNode } from 'react';
|
4
4
|
import { renderToReadableStream } from 'react-dom/server';
|
5
5
|
|
6
|
+
// http is a collection of functions that return a Response
|
7
|
+
// object with the appropriate status code and content type
|
8
|
+
// e.g. http.ok() returns a 200 response
|
6
9
|
const http = {
|
7
10
|
ok: async (msg?: string): Promise<Response> => {
|
8
11
|
return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
|
@@ -47,7 +50,11 @@ const http = {
|
|
47
50
|
},
|
48
51
|
render: async (component: ReactNode): Promise<Response> => {
|
49
52
|
const stream = await renderToReadableStream(component);
|
50
|
-
return new Response(stream, {
|
53
|
+
return new Response(stream, {
|
54
|
+
status: 200,
|
55
|
+
statusText: httpStatusCodes[200],
|
56
|
+
headers: {'Content-Type': 'text/html; charset=utf-8'}
|
57
|
+
});
|
51
58
|
},
|
52
59
|
noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
|
53
60
|
status: 204,
|
package/lib/logger/color.ts
CHANGED
@@ -23,12 +23,12 @@ const Colors: Record<string,string> = {
|
|
23
23
|
} as const;
|
24
24
|
|
25
25
|
|
26
|
-
|
27
|
-
function color(foreground: string, background: string,
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
27
|
+
function color(foreground: string, background: string, ...args: any[]) {
|
28
28
|
const _foreground = Colors[foreground];
|
29
29
|
const _background = Colors[background];
|
30
30
|
const reset = Colors.reset;
|
31
|
-
return `${_foreground}${_background}${
|
31
|
+
return `${_foreground}${_background}${args.map(String).join('')}${reset}`;
|
32
32
|
}
|
33
33
|
|
34
34
|
|
package/lib/logger/logger.d.ts
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
type BunLogger = {
|
2
|
-
info: (statusCode: number, routePath: string, method: string,
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
info: (statusCode: number, routePath: string, method: string, ...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
3
|
+
|
4
|
+
error: (statusCode: number, routePath: string, method: string, error: Error) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
5
|
+
warn: (...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
6
|
+
message: (...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
6
7
|
}
|
7
8
|
|
8
9
|
export { BunLogger };
|
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 = `
|
@@ -9,17 +8,34 @@ _ _
|
|
9
8
|
|___|___|_|_| |_| |___|___|_| |___|_|
|
10
9
|
|
11
10
|
`;
|
12
|
-
const VERSION = '0.
|
13
|
-
const Logger = (
|
11
|
+
const VERSION = '0.8.0';
|
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
|
+
|
14
29
|
return {
|
15
|
-
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
31
|
+
info: async (statusCode: number, routePath: string, method: string, ...args: any[]) => {
|
16
32
|
const { stamp } = timestamp((new Date(Date.now())));
|
17
33
|
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
|
18
34
|
const rp = color('white', 'bgBlack', routePath);
|
19
35
|
|
20
|
-
message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${
|
36
|
+
const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${args.map(String).join(' ')}\n'}`;
|
21
37
|
|
22
|
-
await
|
38
|
+
await write(message);
|
23
39
|
|
24
40
|
},
|
25
41
|
error: async (statusCode: number, routePath: string, method: string, error: Error) => {
|
@@ -29,7 +45,7 @@ const Logger = (): BunLogger => {
|
|
29
45
|
|
30
46
|
const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' -> ' : ' <-'} ${error.message}\n`;
|
31
47
|
|
32
|
-
await
|
48
|
+
await write(message);
|
33
49
|
},
|
34
50
|
warn: async (message: string) => {
|
35
51
|
const { stamp } = timestamp((new Date(Date.now())));
|
@@ -38,7 +54,7 @@ const Logger = (): BunLogger => {
|
|
38
54
|
|
39
55
|
message = `${source} : ${messageColor}\n`;
|
40
56
|
|
41
|
-
await
|
57
|
+
await write(message);
|
42
58
|
},
|
43
59
|
message: async (message: string) => {
|
44
60
|
const { stamp } = timestamp((new Date(Date.now())));
|
@@ -47,8 +63,18 @@ const Logger = (): BunLogger => {
|
|
47
63
|
|
48
64
|
message = `${source}: ${messageColor}\n`;
|
49
65
|
|
50
|
-
await
|
66
|
+
await write(message);
|
51
67
|
},
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
69
|
+
log: async(args: any[]) => {
|
70
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
71
|
+
const source = color('black', 'bgCyan', `[message ${stamp}]`);
|
72
|
+
const messageColor = color('yellow', 'bgBlack', args);
|
73
|
+
|
74
|
+
const message = `${source}: ${messageColor}\n`;
|
75
|
+
|
76
|
+
await write(message);
|
77
|
+
}
|
52
78
|
};
|
53
79
|
};
|
54
80
|
|
package/lib/router/context.ts
CHANGED
@@ -5,7 +5,8 @@ import { Logger } from '../logger/logger';
|
|
5
5
|
import { http } from './router';
|
6
6
|
import { ReactNode } from 'react';
|
7
7
|
|
8
|
-
|
8
|
+
// createContext creates a context object
|
9
|
+
async function createContext(path: string, route: Route, request: Request, enableFileLogging: boolean): Promise<Context> {
|
9
10
|
const query = new URLSearchParams(path);
|
10
11
|
const params = extractParams(path, route);
|
11
12
|
const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
|
@@ -15,12 +16,15 @@ async function createContext(path: string, route: Route, request: Request): Prom
|
|
15
16
|
request,
|
16
17
|
query,
|
17
18
|
formData,
|
18
|
-
logger: Logger(),
|
19
|
+
logger: Logger(enableFileLogging),
|
19
20
|
json: (statusCode: number, data: any) => http.json(statusCode, data),
|
20
21
|
render: async (component: ReactNode) => await renderStream(component),
|
21
22
|
});
|
22
23
|
}
|
23
24
|
|
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 }
|
24
28
|
function extractParams(pattern: string, route: Route): Map<string, string> {
|
25
29
|
const params: Map<string, string> = new Map();
|
26
30
|
const pathSegments = pattern.split('/');
|
@@ -39,16 +43,19 @@ function extractParams(pattern: string, route: Route): Map<string, string> {
|
|
39
43
|
return params;
|
40
44
|
}
|
41
45
|
|
46
|
+
// getContentType returns the content type from the headers
|
42
47
|
function getContentType(headers: Headers): string {
|
43
48
|
const contentType = headers.get('Content-Type');
|
44
49
|
return contentType ?? '';
|
45
50
|
}
|
46
51
|
|
52
|
+
// isMultiPartForm returns true if the content type is multipart/form-data
|
47
53
|
function isMultiPartForm(headers: Headers): boolean {
|
48
54
|
const contentType = getContentType(headers);
|
49
55
|
return contentType.includes('multipart/form-data');
|
50
56
|
}
|
51
57
|
|
58
|
+
// renderStream renders the component to a readable stream
|
52
59
|
async function renderStream(children: ReactNode) {
|
53
60
|
const stream = await renderToReadableStream(children);
|
54
61
|
return new Response(stream, { headers: { 'Content-Type': 'text/html' } });
|
package/lib/router/routeTree.ts
CHANGED
package/lib/router/router.d.ts
CHANGED
package/lib/router/router.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import path from 'path';
|
2
2
|
import { Database } from 'bun:sqlite';
|
3
|
-
import { Route, BunRouter, RouterOptions, Options
|
3
|
+
import { Route, BunRouter, RouterOptions, Options } from './router.d';
|
4
4
|
import { httpStatusCodes } from '../http/status';
|
5
5
|
import { readDir } from '../fs/fsys';
|
6
6
|
import { Logger, startMessage } from '../logger/logger';
|
@@ -9,14 +9,16 @@ import { RouteTree } from './routeTree';
|
|
9
9
|
import { createContext } from './context';
|
10
10
|
|
11
11
|
const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
|
12
|
-
const { addRoute, findRoute
|
13
|
-
const logger = Logger();
|
12
|
+
const { addRoute, findRoute } = RouteTree();
|
13
|
+
const logger = Logger(options?.enableFileLogging ?? false);
|
14
14
|
|
15
|
+
// load a component from the root directory relative to the cwd
|
15
16
|
async function loadComponent(root: string, name: string) {
|
16
17
|
const module = await import(path.join(process.cwd(), root, name));
|
17
18
|
return module.default;
|
18
19
|
}
|
19
20
|
|
21
|
+
// extract the path, extension, and base name from a file path
|
20
22
|
function extractPathExtBase(pattern: string, pathname: string) {
|
21
23
|
const extension = path.extname(pathname);
|
22
24
|
let base = encodeURIComponent(path.basename(pathname));
|
@@ -30,6 +32,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
30
32
|
return { patternPath, extension, base };
|
31
33
|
}
|
32
34
|
|
35
|
+
// check if a file exists
|
33
36
|
async function exists(fp: string) {
|
34
37
|
const f = Bun.file(fp);
|
35
38
|
return await f.exists();
|
@@ -38,7 +41,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
38
41
|
return {
|
39
42
|
// add a route to the router tree
|
40
43
|
add: (pattern, method, callback) => { addRoute(pattern, method, callback); },
|
41
|
-
get: (pattern
|
44
|
+
get: (pattern, callback) => { addRoute(pattern, 'GET', callback); },
|
42
45
|
post: (pattern, callback) => { addRoute(pattern, 'POST', callback); },
|
43
46
|
put: (pattern, callback) => { addRoute(pattern, 'PUT', callback);},
|
44
47
|
delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback); },
|
@@ -51,10 +54,9 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
51
54
|
if (!exists(root)) return console.error(`Directory not found: ${root}`);
|
52
55
|
await readDir(root, async (fp) => {
|
53
56
|
const { patternPath, extension, base } = extractPathExtBase(pattern, fp);
|
54
|
-
|
55
57
|
const route: Route = {
|
56
58
|
children: new Map(),
|
57
|
-
dynamicPath:
|
59
|
+
dynamicPath: pattern,
|
58
60
|
isLast: true,
|
59
61
|
path: patternPath.startsWith('//') ? patternPath.slice(1) : patternPath, // remove the leading '/' if it exists
|
60
62
|
method: 'GET',
|
@@ -70,14 +72,12 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
70
72
|
|
71
73
|
addRoute(route.path, 'GET', route.handler);
|
72
74
|
});
|
73
|
-
|
74
|
-
console.log(list());
|
75
75
|
|
76
76
|
},
|
77
|
-
// start
|
77
|
+
// start listening for requests
|
78
78
|
serve: () => {
|
79
79
|
startMessage(port ?? 3000);
|
80
|
-
const opts: Options = { db: ':memory:' };
|
80
|
+
const opts: Options = { db: ':memory:', enableFileLogging: false };
|
81
81
|
|
82
82
|
Bun.serve({
|
83
83
|
port: port ?? 3000,
|
@@ -90,6 +90,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
90
90
|
if (options) {
|
91
91
|
const o = options as Options;
|
92
92
|
opts.db = o.db;
|
93
|
+
opts.enableFileLogging = o.enableFileLogging;
|
93
94
|
}
|
94
95
|
|
95
96
|
const route = findRoute(pathname);
|
@@ -101,9 +102,11 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
101
102
|
return Promise.resolve(http.methodNotAllowed());
|
102
103
|
}
|
103
104
|
|
104
|
-
|
105
|
+
// create a context for the handler
|
106
|
+
const context = await createContext(pathname, route, req, opts.enableFileLogging);
|
105
107
|
context.db = new Database(opts.db);
|
106
108
|
|
109
|
+
// call the handler
|
107
110
|
const response = await route.handler(context);
|
108
111
|
|
109
112
|
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
@@ -116,6 +119,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
116
119
|
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
117
120
|
return Promise.resolve(http.notFound());
|
118
121
|
},
|
122
|
+
// if an error occurs, return a 500 response
|
119
123
|
error(error) {
|
120
124
|
return new Response(`<pre>${error}\n${error.stack}</pre>`, {
|
121
125
|
headers: { 'Content-Type': 'text/html' },
|
package/package.json
CHANGED
@@ -3,21 +3,22 @@
|
|
3
3
|
"module": "index.ts",
|
4
4
|
"type": "module",
|
5
5
|
"devDependencies": {
|
6
|
-
"@types/react
|
6
|
+
"@types/react": "^18.2.23",
|
7
|
+
"@types/react-dom": "^18.2.8",
|
7
8
|
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
8
9
|
"@typescript-eslint/parser": "^6.7.0",
|
9
10
|
"bun-types": "latest",
|
10
11
|
"eslint": "^8.49.0",
|
11
|
-
"eslint-plugin-react": "^7.33.2"
|
12
|
-
"react-dom": "^18.2.0"
|
12
|
+
"eslint-plugin-react": "^7.33.2"
|
13
13
|
},
|
14
14
|
"peerDependencies": {
|
15
15
|
"typescript": "^5.0.0"
|
16
16
|
},
|
17
|
-
"version": "0.
|
17
|
+
"version": "0.8.0",
|
18
18
|
"dependencies": {
|
19
|
-
"@types/react": "^18.2.22",
|
20
19
|
"eslint-plugin-react-hooks": "^4.6.0",
|
21
|
-
"react": "^18.2.0"
|
22
|
-
|
23
|
-
}
|
20
|
+
"react": "^18.2.0",
|
21
|
+
"react-dom": "^18.2.0"
|
22
|
+
},
|
23
|
+
"types": "./lib/router.d.ts"
|
24
|
+
}
|
package/tsconfig.json
CHANGED