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/router/router.ts
CHANGED
@@ -1,100 +1,133 @@
|
|
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';
|
7
7
|
import { http } from '../http/http';
|
8
|
-
import {RouteTree } from './
|
8
|
+
import { RouteTree } from './routeTree';
|
9
9
|
import { createContext } from './context';
|
10
10
|
|
11
|
-
|
12
11
|
const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
12
|
+
const { addRoute, findRoute } = RouteTree();
|
13
|
+
const logger = Logger(options?.enableFileLogging ?? false);
|
14
|
+
|
15
|
+
// load a component from the root directory relative to the cwd
|
16
|
+
async function loadComponent(root: string, name: string) {
|
17
|
+
const module = await import(path.join(process.cwd(), root, name));
|
18
|
+
return module.default;
|
19
|
+
}
|
20
|
+
|
21
|
+
// extract the path, extension, and base name from a file path
|
22
|
+
function extractPathExtBase(pattern: string, pathname: string) {
|
23
|
+
const extension = path.extname(pathname);
|
24
|
+
let base = encodeURIComponent(path.basename(pathname));
|
25
|
+
|
26
|
+
if (extension === '.html' || extension === '.tsx') base = base.replace(extension, '');
|
27
|
+
|
28
|
+
let patternPath = [pattern, base].join('/');
|
29
|
+
|
30
|
+
if (base === 'index') patternPath = pattern;
|
31
|
+
|
32
|
+
return { patternPath, extension, base };
|
33
|
+
}
|
34
|
+
|
35
|
+
// check if a file exists
|
36
|
+
async function exists(fp: string) {
|
37
|
+
const f = Bun.file(fp);
|
38
|
+
return await f.exists();
|
39
|
+
}
|
40
|
+
|
41
|
+
return {
|
42
|
+
// add a route to the router tree
|
43
|
+
add: (pattern, method, callback) => { addRoute(pattern, method, callback); },
|
44
|
+
get: (pattern, callback) => { addRoute(pattern, 'GET', callback); },
|
45
|
+
post: (pattern, callback) => { addRoute(pattern, 'POST', callback); },
|
46
|
+
put: (pattern, callback) => { addRoute(pattern, 'PUT', callback);},
|
47
|
+
delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback); },
|
23
48
|
|
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
|
-
|
49
|
+
// add static routes to the router tree
|
50
|
+
// .tsx and .html are rendered as components, or pages
|
51
|
+
// all other file extensions are served as files
|
52
|
+
// the root directory is traversed recursively
|
53
|
+
static: async (pattern: string, root: string) => {
|
54
|
+
if (!exists(root)) return console.error(`Directory not found: ${root}`);
|
55
|
+
await readDir(root, async (fp) => {
|
56
|
+
const { patternPath, extension, base } = extractPathExtBase(pattern, fp);
|
57
|
+
const route: Route = {
|
58
|
+
children: new Map(),
|
59
|
+
dynamicPath: pattern,
|
60
|
+
isLast: true,
|
61
|
+
path: patternPath.startsWith('//') ? patternPath.slice(1) : patternPath, // remove the leading '/' if it exists
|
62
|
+
method: 'GET',
|
63
|
+
handler: async () => {
|
64
|
+
if (extension === '.tsx') {
|
65
|
+
const component = await loadComponent(root, base);
|
66
|
+
return await http.render(component());
|
67
|
+
} else {
|
68
|
+
return await http.file(200, fp);
|
69
|
+
}
|
70
|
+
},
|
71
|
+
};
|
72
|
+
|
73
|
+
addRoute(route.path, 'GET', route.handler);
|
74
|
+
});
|
75
|
+
|
76
|
+
},
|
77
|
+
// start listening for requests
|
78
|
+
serve: () => {
|
79
|
+
startMessage(port ?? 3000);
|
80
|
+
const opts: Options = { db: ':memory:', enableFileLogging: false };
|
81
|
+
|
82
|
+
Bun.serve({
|
83
|
+
port: port ?? 3000,
|
84
|
+
...options,
|
85
|
+
async fetch(req) {
|
86
|
+
const url = new URL(req.url);
|
87
|
+
const pathname = url.pathname;
|
88
|
+
|
89
|
+
// set the database
|
90
|
+
if (options) {
|
91
|
+
const o = options as Options;
|
92
|
+
opts.db = o.db;
|
93
|
+
opts.enableFileLogging = o.enableFileLogging;
|
94
|
+
}
|
95
|
+
|
96
|
+
const route = findRoute(pathname);
|
97
|
+
|
98
|
+
// if the route exists, call the handler
|
99
|
+
if (route) {
|
100
|
+
if (route.method !== req.method) {
|
101
|
+
logger.info(405, url.pathname, req.method, httpStatusCodes[405]);
|
102
|
+
return Promise.resolve(http.methodNotAllowed());
|
103
|
+
}
|
104
|
+
|
105
|
+
// create a context for the handler
|
106
|
+
const context = await createContext(pathname, route, req, opts.enableFileLogging);
|
107
|
+
context.db = new Database(opts.db);
|
108
|
+
|
109
|
+
// call the handler
|
110
|
+
const response = await route.handler(context);
|
111
|
+
|
112
|
+
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
113
|
+
return Promise.resolve(response);
|
114
|
+
}
|
115
|
+
|
116
|
+
// if no route is found, return 404
|
117
|
+
const response = await http.notFound();
|
90
118
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
}
|
99
|
-
|
100
|
-
|
119
|
+
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
120
|
+
return Promise.resolve(http.notFound());
|
121
|
+
},
|
122
|
+
// if an error occurs, return a 500 response
|
123
|
+
error(error) {
|
124
|
+
return new Response(`<pre>${error}\n${error.stack}</pre>`, {
|
125
|
+
headers: { 'Content-Type': 'text/html' },
|
126
|
+
});
|
127
|
+
}
|
128
|
+
});
|
129
|
+
},
|
130
|
+
};
|
131
|
+
};
|
132
|
+
|
133
|
+
export { Router, http };
|
package/package.json
CHANGED
@@ -3,10 +3,21 @@
|
|
3
3
|
"module": "index.ts",
|
4
4
|
"type": "module",
|
5
5
|
"devDependencies": {
|
6
|
-
"
|
6
|
+
"@types/react": "^18.2.23",
|
7
|
+
"@types/react-dom": "^18.2.8",
|
8
|
+
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
9
|
+
"@typescript-eslint/parser": "^6.7.0",
|
10
|
+
"bun-types": "latest",
|
11
|
+
"eslint": "^8.49.0",
|
12
|
+
"eslint-plugin-react": "^7.33.2"
|
7
13
|
},
|
8
14
|
"peerDependencies": {
|
9
15
|
"typescript": "^5.0.0"
|
10
16
|
},
|
11
|
-
"version": "0.7.
|
17
|
+
"version": "0.7.4-experimental.11",
|
18
|
+
"dependencies": {
|
19
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
20
|
+
"react": "^18.2.0",
|
21
|
+
"react-dom": "^18.2.0"
|
22
|
+
}
|
12
23
|
}
|
package/tests/router.test.ts
CHANGED
@@ -1,35 +1,36 @@
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
2
2
|
|
3
3
|
describe('Router', () => {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
test('Serve', async () => {
|
5
|
+
const proc = Bun.spawn(['./tests/serve.test.sh'], {
|
6
|
+
onExit: (_proc, _exitCode, _signalCode , error) => {
|
7
|
+
if (error) console.error(error);
|
8
|
+
},
|
9
|
+
});
|
10
10
|
|
11
|
-
|
11
|
+
const text = await new Response(proc.stdout).text();
|
12
12
|
|
13
|
-
|
13
|
+
const hasFailed = text.includes('Failed');
|
14
14
|
|
15
|
-
|
15
|
+
if (hasFailed) console.log(text);
|
16
16
|
|
17
|
-
|
17
|
+
expect(hasFailed).toBe(false);
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
proc.kill(0);
|
20
|
+
});
|
21
21
|
|
22
|
-
|
23
|
-
|
22
|
+
test('Static', async() => {
|
23
|
+
const proc = Bun.spawn(['./tests/static.test.sh']);
|
24
24
|
|
25
|
-
|
25
|
+
const text = await new Response(proc.stdout).text();
|
26
26
|
|
27
|
-
|
28
|
-
|
27
|
+
const hasFailed = text.includes('Failed');
|
28
|
+
if (hasFailed) console.log(text);
|
29
29
|
|
30
|
-
|
30
|
+
expect(hasFailed).toBe(false);
|
31
31
|
|
32
|
-
|
33
|
-
|
32
|
+
proc.kill(0);
|
33
|
+
});
|
34
34
|
});
|
35
35
|
|
36
|
+
|
package/tsconfig.json
CHANGED
package/examples/db.ts
DELETED
package/lib/router/tree.ts
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
import { HttpHandler, Route } from "./router.d";
|
2
|
-
import { http } from "../http/http";
|
3
|
-
import { createContext } from './context';
|
4
|
-
|
5
|
-
const splitPath = (s: string): string[] => s.split('/').filter(x => x !== '');
|
6
|
-
|
7
|
-
const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
|
8
|
-
const route: Route = {
|
9
|
-
children: new Map(),
|
10
|
-
path: path,
|
11
|
-
dynamicPath: '',
|
12
|
-
method: method,
|
13
|
-
handler: handler,
|
14
|
-
isLast: false
|
15
|
-
};
|
16
|
-
|
17
|
-
return route;
|
18
|
-
};
|
19
|
-
|
20
|
-
const RouteTree = () => {
|
21
|
-
let root = createRoute('', 'GET', () => http.notFound());
|
22
|
-
|
23
|
-
const addRoute = (path: string, method: string, handler: HttpHandler) => {
|
24
|
-
const pathParts = splitPath(path);
|
25
|
-
let current = root;
|
26
|
-
|
27
|
-
for (let i = 0; i < pathParts.length; i++) {
|
28
|
-
const part = pathParts[i];
|
29
|
-
if (part.startsWith(':')) {
|
30
|
-
current.dynamicPath = part;
|
31
|
-
}
|
32
|
-
if (!current.children.has(part)) {
|
33
|
-
current.children.set(part, createRoute(part, method, handler));
|
34
|
-
}
|
35
|
-
current = current.children.get(part)!;
|
36
|
-
}
|
37
|
-
|
38
|
-
current.handler = handler;
|
39
|
-
current.isLast = true;
|
40
|
-
current.path = path;
|
41
|
-
};
|
42
|
-
|
43
|
-
const findRoute = (path: string): Route | undefined => {
|
44
|
-
const pathParts = splitPath(path);
|
45
|
-
let current = root;
|
46
|
-
for (let i = 0; i < pathParts.length; i++) {
|
47
|
-
const part = pathParts[i];
|
48
|
-
if (current.children.has(part)) {
|
49
|
-
current = current.children.get(part)!;
|
50
|
-
} else if (current.dynamicPath) {
|
51
|
-
current = current.children.get(current.dynamicPath)!;
|
52
|
-
} else {
|
53
|
-
return;
|
54
|
-
}
|
55
|
-
}
|
56
|
-
return current;
|
57
|
-
}
|
58
|
-
|
59
|
-
return { addRoute, findRoute }
|
60
|
-
|
61
|
-
};
|
62
|
-
|
63
|
-
export { RouteTree, createContext }
|
package/lib/util/strings.ts
DELETED