bun-router 0.7.3 → 0.7.4-experimental.11
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 +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