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/.eslintrc.json
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
{
|
2
|
+
"env": {
|
3
|
+
"browser": true,
|
4
|
+
"es2021": true
|
5
|
+
},
|
6
|
+
"extends": [
|
7
|
+
"eslint:recommended",
|
8
|
+
"plugin:@typescript-eslint/recommended",
|
9
|
+
"plugin:react/recommended"
|
10
|
+
],
|
11
|
+
"parser": "@typescript-eslint/parser",
|
12
|
+
"parserOptions": {
|
13
|
+
"ecmaVersion": "latest",
|
14
|
+
"sourceType": "module"
|
15
|
+
},
|
16
|
+
"plugins": [
|
17
|
+
"@typescript-eslint",
|
18
|
+
"react"
|
19
|
+
],
|
20
|
+
"rules": {
|
21
|
+
"indent": [
|
22
|
+
"warn",
|
23
|
+
"tab"
|
24
|
+
],
|
25
|
+
"quotes": [
|
26
|
+
"error",
|
27
|
+
"single"
|
28
|
+
],
|
29
|
+
"semi": [
|
30
|
+
"error",
|
31
|
+
"always"
|
32
|
+
],
|
33
|
+
"no-control-regex": "off"
|
34
|
+
}
|
35
|
+
}
|
package/README.md
CHANGED
@@ -29,6 +29,10 @@ router.serve();
|
|
29
29
|
```
|
30
30
|
|
31
31
|
##### Static
|
32
|
+
Read a directory and serve it's contents. `.tsx` and `.html` files are rendered by default, everything else is served, including the extension.
|
33
|
+
|
34
|
+
Ex: `/assets/gopher.png` would serve a `.png` image. `/home` would be `.tsx` or `.html` depending on extension.
|
35
|
+
|
32
36
|
```ts
|
33
37
|
import { Router } from 'bun-router';
|
34
38
|
|
@@ -53,4 +57,28 @@ router.post('/register', ctx => {
|
|
53
57
|
|
54
58
|
```
|
55
59
|
|
60
|
+
##### JSX
|
61
|
+
```tsx
|
62
|
+
// ./pages/home.tsx
|
63
|
+
export default const Home = (title: string) => {
|
64
|
+
return (
|
65
|
+
<main>
|
66
|
+
<h1>{ title }</h1>
|
67
|
+
</main>
|
68
|
+
);
|
69
|
+
};
|
70
|
+
```
|
71
|
+
|
72
|
+
```ts
|
73
|
+
// ./index.ts
|
74
|
+
import { Router } from 'bun-router';
|
75
|
+
import Home from './pages/home';
|
76
|
+
|
77
|
+
const router = Router();
|
78
|
+
|
79
|
+
router.get('/', ctx => ctx.render(Home('Hello World')))
|
80
|
+
|
81
|
+
router.serve();
|
82
|
+
```
|
83
|
+
|
56
84
|
|
package/bun.lockb
CHANGED
Binary file
|
package/examples/basic.ts
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
import { Router, http } from '..';
|
2
2
|
|
3
|
-
const router = Router();
|
3
|
+
const router = Router(3000, {enableFileLogging: false});
|
4
4
|
|
5
5
|
router.add('/', 'GET', () => http.json(200, 'ok'));
|
6
6
|
|
7
7
|
router.add('/user/:name', 'GET', (ctx) => {
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
const name = ctx.params.get('name');
|
9
|
+
if (!name) return http.json(500, 'no name');
|
10
|
+
return http.json(200, name);
|
11
11
|
});
|
12
12
|
|
13
13
|
router.serve();
|
package/examples/dynamic.ts
CHANGED
@@ -4,16 +4,16 @@ import { Context } from '../lib/router/router.d';
|
|
4
4
|
const home = () => new Response('Welcome Home', { status: 200 });
|
5
5
|
|
6
6
|
const subreddit = (ctx: Context) => {
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
}
|
7
|
+
const sub = ctx.params.get('subreddit');
|
8
|
+
if (!sub) return http.json(400, { error: 'no subreddit provided' });
|
9
|
+
return http.json(200, { subreddit: sub });
|
10
|
+
};
|
11
11
|
|
12
12
|
const user = (ctx: Context) => {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
}
|
13
|
+
const user = ctx.params.get('user');
|
14
|
+
if (!user) return http.json(400, { error: 'no user provided' });
|
15
|
+
return http.json(200, { user: user });
|
16
|
+
};
|
17
17
|
|
18
18
|
const r = Router();
|
19
19
|
|
package/examples/logger.ts
CHANGED
package/examples/static.ts
CHANGED
package/examples/todo.ts
CHANGED
@@ -1,45 +1,45 @@
|
|
1
1
|
import { Router, http } from '..';
|
2
2
|
|
3
3
|
const Todo = () => {
|
4
|
-
|
4
|
+
const list: Record<string, string> = {};
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
}
|
6
|
+
return {
|
7
|
+
add: (key: string, value: string) => { list[key] = value; },
|
8
|
+
get: (key: string) => list[key],
|
9
|
+
remove: (key: string) => { delete list[key]; },
|
10
|
+
size: () => Object.entries(list).length,
|
11
|
+
export: () => list,
|
12
|
+
};
|
13
|
+
};
|
14
14
|
|
15
15
|
const todo = Todo();
|
16
16
|
|
17
17
|
const r = Router();
|
18
18
|
|
19
19
|
r.add('/api/new', 'POST', ctx => {
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
const query = new URL(ctx.request.url).searchParams;
|
21
|
+
const key = query.get('key');
|
22
|
+
const content = query.get('content');
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
if (!key || !content) return http.message(400, 'invalid query params');
|
25
|
+
ctx.logger.message(`Adding ${key} with ${content}`);
|
26
|
+
todo.add(key, content);
|
27
27
|
|
28
|
-
|
28
|
+
return ctx.json(200, { message: 'ok' });
|
29
29
|
});
|
30
30
|
|
31
31
|
r.add('/api/todo/:key', 'GET', ctx => {
|
32
|
-
|
33
|
-
|
32
|
+
const key = ctx.params.get('key');
|
33
|
+
if (!key) return http.message(400, 'invalid params');
|
34
34
|
|
35
|
-
|
36
|
-
|
35
|
+
const content = todo.get(key);
|
36
|
+
if (!content) return http.notFound();
|
37
37
|
|
38
|
-
|
38
|
+
return ctx.json(200, {key: key, content: content});
|
39
39
|
});
|
40
40
|
|
41
41
|
r.add('/api/get/all', 'GET', ctx => {
|
42
|
-
|
42
|
+
return ctx.json(200, todo.export());
|
43
43
|
});
|
44
44
|
|
45
45
|
r.serve();
|
@@ -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
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import path from 'node:path';
|
2
|
+
|
3
|
+
type File = {
|
4
|
+
name: string;
|
5
|
+
path: string;
|
6
|
+
extension: string;
|
7
|
+
children: Map<string, File>;
|
8
|
+
isLast: boolean;
|
9
|
+
};
|
10
|
+
|
11
|
+
function createFile(name: string): File {
|
12
|
+
return {
|
13
|
+
name,
|
14
|
+
path: '',
|
15
|
+
extension: '',
|
16
|
+
children: new Map(),
|
17
|
+
isLast: false,
|
18
|
+
};
|
19
|
+
}
|
20
|
+
|
21
|
+
const FileTree = (dir: string) => {
|
22
|
+
const root = createFile(dir);
|
23
|
+
|
24
|
+
const addFile = (filepath: string) => {
|
25
|
+
const pathParts = filepath.split(path.sep);
|
26
|
+
let current = root;
|
27
|
+
|
28
|
+
for (let i = 0; i < pathParts.length; i++) {
|
29
|
+
const part = pathParts[i];
|
30
|
+
if (!current.children.has(part)) {
|
31
|
+
current.children.set(part, createFile(part));
|
32
|
+
}
|
33
|
+
current = current.children.get(part)!;
|
34
|
+
}
|
35
|
+
|
36
|
+
current.isLast = true;
|
37
|
+
current.path = filepath;
|
38
|
+
current.extension = path.extname(filepath);
|
39
|
+
};
|
40
|
+
|
41
|
+
const getFilesByExtension = (extension: string): string[] => {
|
42
|
+
let current = root;
|
43
|
+
const files: string[] = [];
|
44
|
+
|
45
|
+
for (const [name, file] of current.children) {
|
46
|
+
if (file.extension === extension) {
|
47
|
+
files.push(file.path);
|
48
|
+
}
|
49
|
+
current = current.children.get(name)!;
|
50
|
+
}
|
51
|
+
|
52
|
+
return files;
|
53
|
+
};
|
54
|
+
|
55
|
+
const getFileByName = (filepath: string): string | undefined => {
|
56
|
+
let current = root;
|
57
|
+
const pathParts = filepath.split(path.sep);
|
58
|
+
for (let i = 0; i < pathParts.length; i++) {
|
59
|
+
const part = pathParts[i];
|
60
|
+
if (current.children.has(part)) {
|
61
|
+
current = current.children.get(part)!;
|
62
|
+
} else {
|
63
|
+
return;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
if (!current.isLast) return;
|
68
|
+
|
69
|
+
return current.path;
|
70
|
+
};
|
71
|
+
|
72
|
+
return { addFile, getFilesByExtension, getFileByName };
|
73
|
+
};
|
74
|
+
|
75
|
+
export { FileTree };
|
package/lib/fs/fsys.ts
CHANGED
@@ -1,25 +1,39 @@
|
|
1
|
-
import { BunFile } from
|
1
|
+
import { BunFile } from 'bun';
|
2
2
|
import fs from 'node:fs/promises';
|
3
3
|
import path from 'path';
|
4
4
|
|
5
5
|
// check if the file path is a directory
|
6
6
|
const isDir = async (fp: string): Promise<boolean> => (await fs.lstat(fp)).isDirectory();
|
7
7
|
|
8
|
+
// recursively read a directory and call a handler function on each file
|
8
9
|
async function readDir(dirpath: string, handler: (filepath: string, entry: BunFile) => void) {
|
9
|
-
|
10
|
+
const files = await fs.readdir(dirpath);
|
10
11
|
|
11
|
-
|
12
|
-
|
12
|
+
for (const file of files) {
|
13
|
+
const bunFile = Bun.file(file);
|
13
14
|
|
14
|
-
|
15
|
+
if (typeof bunFile.name === 'undefined') return;
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
const fp = path.join(dirpath, bunFile.name);
|
18
|
+
const isdir = await isDir(fp);
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
if (isdir) await readDir(fp, handler);
|
21
|
+
else handler(fp, bunFile);
|
22
|
+
}
|
22
23
|
}
|
23
24
|
|
25
|
+
// resolve module paths relative to the current working directory
|
26
|
+
function resolveModulePath(module: string) {
|
27
|
+
return path.join(process.cwd(), module);
|
28
|
+
}
|
29
|
+
|
30
|
+
function exists(filepath: string): boolean {
|
31
|
+
try {
|
32
|
+
fs.access(filepath);
|
33
|
+
return true;
|
34
|
+
} catch (err) {
|
35
|
+
return false;
|
36
|
+
}
|
37
|
+
}
|
24
38
|
|
25
|
-
export { readDir }
|
39
|
+
export { readDir, resolveModulePath, exists };
|
package/lib/http/http.ts
CHANGED
@@ -1,77 +1,91 @@
|
|
1
|
-
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
import { httpStatusCodes } from './status';
|
3
|
+
import { ReactNode } from 'react';
|
4
|
+
import { renderToReadableStream } from 'react-dom/server';
|
2
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
|
3
9
|
const http = {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
10
|
+
ok: async (msg?: string): Promise<Response> => {
|
11
|
+
return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
|
12
|
+
status: 200,
|
13
|
+
statusText: httpStatusCodes[200],
|
14
|
+
}));
|
15
|
+
},
|
16
|
+
json: async (statusCode: number, data: any): Promise<Response> => {
|
17
|
+
const jsonString = JSON.stringify(data);
|
18
|
+
return Promise.resolve(new Response(jsonString, {
|
19
|
+
status: statusCode,
|
20
|
+
statusText: httpStatusCodes[statusCode],
|
21
|
+
headers: {'Content-Type': 'application/json'},
|
22
|
+
}));
|
23
|
+
},
|
24
|
+
html: async (statusCode: number, content: string): Promise<Response> => {
|
25
|
+
return Promise.resolve(new Response(content, {
|
26
|
+
status: statusCode,
|
27
|
+
statusText: httpStatusCodes[statusCode],
|
28
|
+
headers: {'Content-Type': 'text/html; charset=utf-8'}
|
29
|
+
}));
|
30
|
+
},
|
31
|
+
file: async (statusCode: number, fp: string): Promise<Response> => {
|
32
|
+
const file = Bun.file(fp);
|
33
|
+
const exists = await file.exists();
|
28
34
|
|
29
|
-
|
35
|
+
if (!exists) return http.notFound(`File not found: ${fp}`);
|
30
36
|
|
31
|
-
|
32
|
-
|
37
|
+
const content = await file.arrayBuffer();
|
38
|
+
if (!content) return http.noContent();
|
33
39
|
|
34
|
-
|
40
|
+
let contentType = 'text/html; charset=utf-9';
|
35
41
|
|
36
|
-
|
37
|
-
|
42
|
+
if (file.type.includes('image'))
|
43
|
+
contentType = file.type + '; charset=utf-8';
|
38
44
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
45
|
+
return Promise.resolve(new Response(content, {
|
46
|
+
status: statusCode,
|
47
|
+
statusText: httpStatusCodes[statusCode],
|
48
|
+
headers: { 'Content-Type': contentType}
|
49
|
+
}));
|
50
|
+
},
|
51
|
+
render: async (component: ReactNode): Promise<Response> => {
|
52
|
+
const stream = await renderToReadableStream(component);
|
53
|
+
return new Response(stream, {
|
54
|
+
status: 200,
|
55
|
+
statusText: httpStatusCodes[200],
|
56
|
+
headers: {'Content-Type': 'text/html; charset=utf-8'}
|
57
|
+
});
|
58
|
+
},
|
59
|
+
noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
|
60
|
+
status: 204,
|
61
|
+
statusText: 'no content',
|
62
|
+
})),
|
63
|
+
notFound: async(msg?: string): Promise<Response> => {
|
64
|
+
const response = new Response(msg ?? 'not found', {
|
65
|
+
status: 404,
|
66
|
+
statusText: httpStatusCodes[404],
|
67
|
+
headers: {'Content-Type': 'text/html'},
|
68
|
+
});
|
55
69
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
70
|
+
return Promise.resolve(response);
|
71
|
+
},
|
72
|
+
methodNotAllowed: async (msg?: string): Promise<Response> => {
|
73
|
+
const response = new Response(msg ?? 'method not allowed', {
|
74
|
+
status: 405,
|
75
|
+
statusText: httpStatusCodes[405],
|
76
|
+
headers: {'Content-Type': 'text/html'},
|
77
|
+
});
|
64
78
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
}
|
79
|
+
return Promise.resolve(response);
|
80
|
+
},
|
81
|
+
message: async (status: number, msg?: string): Promise<Response> => {
|
82
|
+
const response = new Response(msg ?? '?', {
|
83
|
+
status: status,
|
84
|
+
statusText: httpStatusCodes[status],
|
85
|
+
headers: {'Content-Type': 'text/html; charset-utf-8'},
|
86
|
+
});
|
87
|
+
return Promise.resolve(response);
|
88
|
+
},
|
89
|
+
};
|
76
90
|
|
77
|
-
export { http }
|
91
|
+
export { http };
|