bun-router 0.7.4-experimental.9 → 0.8.1
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/.eslintrc.json +3 -1
- package/bun.lockb +0 -0
- package/examples/basic.ts +2 -2
- 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 -3
package/.eslintrc.json
CHANGED
package/bun.lockb
CHANGED
Binary file
|
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.1';
|
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.1",
|
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
|
+
}
|