bun-router 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +28 -0
- package/examples/dbs/test.db +0 -0
- package/examples/sqlite.ts +22 -0
- package/index.ts +0 -2
- package/lib/fs/fsys.ts +7 -5
- package/lib/logger/logger.ts +2 -0
- package/lib/router/router.d.ts +12 -7
- package/lib/router/router.ts +59 -23
- package/package.json +1 -1
- package/tests/router.test.ts +3 -7
package/README.md
CHANGED
@@ -52,4 +52,32 @@ r.add('/user/:id', 'GET', (ctx) => {
|
|
52
52
|
});
|
53
53
|
|
54
54
|
r.serve();
|
55
|
+
```
|
56
|
+
|
57
|
+
**SQLite**
|
58
|
+
```ts
|
59
|
+
import { router, json } from '..';
|
60
|
+
import { Database } from 'bun:sqlite';
|
61
|
+
|
62
|
+
const r = router(3000, {db: './examples/dbs/test.db'});
|
63
|
+
|
64
|
+
r.add('/u/new/:name', 'GET', (ctx) => {
|
65
|
+
const name = ctx.params.get('name');
|
66
|
+
const rando = Math.floor(Math.random()*1000);
|
67
|
+
|
68
|
+
ctx.db.run(`INSERT INTO test VALUES(${rando}, "${name}")`);
|
69
|
+
|
70
|
+
return json({message: 'ok'});
|
71
|
+
});
|
72
|
+
|
73
|
+
r.add('/u/:name', 'GET', (ctx) => {
|
74
|
+
const name = ctx.params.get('name');
|
75
|
+
const data = ctx.db.query(`SELECT * FROM test WHERE name = "${name}";`).get();
|
76
|
+
const d = data as {id: number, name: string};
|
77
|
+
|
78
|
+
return d ? json(d) : new Response('not found', {status: 404});
|
79
|
+
});
|
80
|
+
|
81
|
+
r.serve();
|
82
|
+
|
55
83
|
```
|
Binary file
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { router, json } from '..';
|
2
|
+
|
3
|
+
const r = router(3000, {db: './examples/dbs/test.db'});
|
4
|
+
|
5
|
+
r.add('/u/new/:name', 'GET', (ctx) => {
|
6
|
+
const name = ctx.params.get('name');
|
7
|
+
const rando = Math.floor(Math.random()*1000);
|
8
|
+
|
9
|
+
ctx.db.run(`INSERT INTO test VALUES(${rando}, "${name}")`);
|
10
|
+
|
11
|
+
return json({message: 'ok'});
|
12
|
+
});
|
13
|
+
|
14
|
+
r.add('/u/:name', 'GET', (ctx) => {
|
15
|
+
const name = ctx.params.get('name');
|
16
|
+
const data = ctx.db.query(`SELECT * FROM test WHERE name = "${name}";`).get();
|
17
|
+
const d = data as {id: number, name: string};
|
18
|
+
|
19
|
+
return d ? json(d) : new Response('not found', {status: 404});
|
20
|
+
});
|
21
|
+
|
22
|
+
r.serve();
|
package/index.ts
CHANGED
package/lib/fs/fsys.ts
CHANGED
@@ -2,23 +2,25 @@ import { BunFile } from "bun";
|
|
2
2
|
import fs from 'node:fs/promises';
|
3
3
|
import path from 'path';
|
4
4
|
|
5
|
+
// check if the file path is a directory
|
5
6
|
const isDir = async (fp: string): Promise<boolean> => (await fs.lstat(fp)).isDirectory();
|
6
7
|
|
8
|
+
// read a directory recursively and apply the callback to each one
|
7
9
|
const readDir = async (dirpath: string, handler: (filepath: string, entry: BunFile) => void) => {
|
8
|
-
try {
|
9
10
|
const files = await fs.readdir(dirpath);
|
10
11
|
|
11
12
|
for (const file of files) {
|
12
13
|
const bunFile = Bun.file(file);
|
13
|
-
|
14
|
+
|
15
|
+
if (typeof bunFile.name === 'undefined') return
|
16
|
+
|
17
|
+
const fp = path.join(dirpath, bunFile.name);
|
14
18
|
const isdir = await isDir(fp);
|
15
19
|
|
16
20
|
if (isdir) await readDir(fp, handler);
|
17
21
|
else handler(fp, bunFile);
|
18
22
|
}
|
19
|
-
|
20
|
-
console.error(err);
|
21
|
-
}
|
23
|
+
|
22
24
|
}
|
23
25
|
|
24
26
|
export { readDir }
|
package/lib/logger/logger.ts
CHANGED
@@ -14,6 +14,7 @@ const timestamp = (date: Date) => {
|
|
14
14
|
return {month, day, hour, minute, stamp};
|
15
15
|
}
|
16
16
|
|
17
|
+
// append ANSI color escape sequences to a string based on the given HTTP status code.
|
17
18
|
const colorCode = (n: number, text?:string): string => {
|
18
19
|
const s = ` [${String(n)}${text ?? ''}] `;
|
19
20
|
if (n < 100) return color('black', 'bgYellow', s);
|
@@ -39,6 +40,7 @@ const logger = (): Logger => {
|
|
39
40
|
const messages: string[] = [];
|
40
41
|
const errors: string[] = [];
|
41
42
|
return {
|
43
|
+
// initial log message
|
42
44
|
start: async (port: number | string) => {
|
43
45
|
const { stamp } = timestamp((new Date(Date.now())));
|
44
46
|
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`)
|
package/lib/router/router.d.ts
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
import { TLSOptions, TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptions, TLSServeOptions } from 'bun';
|
2
|
+
import { Database } from 'bun:sqlite';
|
2
3
|
|
3
4
|
|
4
5
|
type Context = {
|
5
6
|
request: Request,
|
6
7
|
params: Map<string, string>,
|
7
|
-
fs: Map<string, string>,
|
8
8
|
token?: string,
|
9
|
+
db: Database,
|
9
10
|
}
|
10
11
|
|
11
12
|
type Route = {
|
@@ -14,18 +15,22 @@ type Route = {
|
|
14
15
|
callback: (req: Context) => Response | Promise<Response>
|
15
16
|
}
|
16
17
|
|
17
|
-
type Options =
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
type Options = {
|
19
|
+
db: string,
|
20
|
+
}
|
21
|
+
|
22
|
+
type RouterOptions<Options> = ServeOptions
|
23
|
+
| TLSServeOptions<Options>
|
24
|
+
| WebSocketServeOptions<Options>
|
25
|
+
| TLSWebSocketServeOptions<Options>
|
21
26
|
| undefined
|
22
27
|
|
23
28
|
|
24
|
-
type Router = (port?: number | string, options?:
|
29
|
+
type Router = (port?: number | string, options?: RouterOptions) => {
|
25
30
|
add: (pattern: string, method: string, callback: (req: Context) => Response | Promise<Response>) => void,
|
26
31
|
static: (pattern: string, root: string) => void,
|
27
32
|
serve: () => void,
|
28
33
|
}
|
29
34
|
|
30
35
|
|
31
|
-
export { Context , Route, Router, Options }
|
36
|
+
export { Context , Route, Router, RouterOptions, Options }
|
package/lib/router/router.ts
CHANGED
@@ -1,10 +1,24 @@
|
|
1
|
-
import {
|
1
|
+
import { Database } from 'bun:sqlite';
|
2
|
+
import { Route, Router, Context, RouterOptions, Options } from './router.d';
|
2
3
|
import { readDir } from '../fs/fsys';
|
3
4
|
import { logger } from '../logger/logger';
|
4
5
|
import path from 'path';
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
// create a generic HTTP response
|
8
|
+
const httpMessage = async (status: number, msg?: string): Promise<Response> => {
|
9
|
+
const response = new Response(msg ?? '?', {
|
10
|
+
status: status,
|
11
|
+
statusText: msg ?? '?',
|
12
|
+
headers: {'Content-Type': 'text/html; charset-uft-8'}
|
13
|
+
});
|
14
|
+
return new Promise((resolve) => {
|
15
|
+
resolve(response);
|
16
|
+
});
|
17
|
+
};
|
18
|
+
|
19
|
+
// a generic 'not found' HTTP response
|
20
|
+
const notFound = async (msg?: string): Promise<Response> => {
|
21
|
+
const response = new Response(msg ?? 'not found', {
|
8
22
|
status: 404,
|
9
23
|
statusText: 'not found',
|
10
24
|
headers: { 'Content-Type': 'text/html' },
|
@@ -15,6 +29,7 @@ const notFound = async (): Promise<Response> => {
|
|
15
29
|
});
|
16
30
|
}
|
17
31
|
|
32
|
+
// a generic 'no content' HTTP response
|
18
33
|
const noContent = async (): Promise<Response> => {
|
19
34
|
const response = new Response('no content', {
|
20
35
|
status: 204,
|
@@ -26,47 +41,54 @@ const noContent = async (): Promise<Response> => {
|
|
26
41
|
});
|
27
42
|
}
|
28
43
|
|
44
|
+
// IO handling
|
29
45
|
const file = async (filepath: string): Promise<Response> => {
|
30
46
|
const file = Bun.file(filepath);
|
31
47
|
const exists = await file.exists();
|
32
48
|
|
49
|
+
// check if the file exists, return 'not found' if it doesn't.
|
33
50
|
if (!exists)
|
34
|
-
return notFound();
|
51
|
+
return notFound(`File not found: ${filepath}`);
|
35
52
|
|
53
|
+
// get the content of the file as an ArrayBuffer
|
36
54
|
const content = await file.arrayBuffer();
|
37
55
|
if (!content)
|
38
|
-
return
|
56
|
+
return noContent();
|
39
57
|
|
58
|
+
// default Content-Type + encoding
|
40
59
|
let contentType = 'text/html; charset=utf-8';
|
41
60
|
|
61
|
+
// change the Content-Type if the file type is an image.
|
62
|
+
// file.type provides the necessary Content-Type
|
42
63
|
if (file.type.includes('image')) {
|
43
64
|
contentType = file.type + '; charset=utf-8';
|
44
65
|
}
|
45
66
|
|
67
|
+
// create a new response with the necessary criteria
|
46
68
|
const response = new Response(content, {
|
47
69
|
status: 200,
|
48
70
|
statusText: 'ok',
|
49
71
|
headers: { 'Content-Type': contentType },
|
50
72
|
});
|
51
73
|
|
52
|
-
return
|
53
|
-
resolve(response);
|
54
|
-
});
|
74
|
+
return Promise.resolve(response);
|
55
75
|
}
|
56
76
|
|
77
|
+
// handle strings as HTML
|
57
78
|
const html = async (content: string): Promise<Response> => {
|
58
79
|
const response = new Response(content, {
|
59
80
|
status: 200,
|
60
81
|
statusText: 'ok',
|
61
82
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
62
83
|
});
|
84
|
+
|
85
|
+
// escape the HTML
|
63
86
|
content = Bun.escapeHTML(content);
|
64
87
|
|
65
|
-
return
|
66
|
-
resolve(response);
|
67
|
-
});
|
88
|
+
return Promise.resolve(response);
|
68
89
|
}
|
69
90
|
|
91
|
+
// create a JSON response
|
70
92
|
const json = (data: any): Response => {
|
71
93
|
const jsonString = JSON.stringify(data);
|
72
94
|
|
@@ -76,6 +98,8 @@ const json = (data: any): Response => {
|
|
76
98
|
return res
|
77
99
|
}
|
78
100
|
|
101
|
+
// extract dynamic URL parameters
|
102
|
+
// if the route pattern is /:foo and the request URL is /bar: {foo: 'bar'}
|
79
103
|
const extract = (route: Route, ctx: Context) => {
|
80
104
|
const url = new URL(ctx.request.url);
|
81
105
|
const pathSegments = route.pattern.split('/');
|
@@ -83,7 +107,6 @@ const extract = (route: Route, ctx: Context) => {
|
|
83
107
|
|
84
108
|
if (pathSegments.length !== urlSegments.length) return
|
85
109
|
|
86
|
-
|
87
110
|
return {
|
88
111
|
params: () => {
|
89
112
|
for (let i = 0; i < pathSegments.length; i++) {
|
@@ -98,6 +121,7 @@ const extract = (route: Route, ctx: Context) => {
|
|
98
121
|
|
99
122
|
}
|
100
123
|
|
124
|
+
// ensure the route pattern matches the request URL
|
101
125
|
const match = (route: Route, ctx: Context): boolean => {
|
102
126
|
const url = new URL(ctx.request.url);
|
103
127
|
const patternRegex = new RegExp('^' + route.pattern.replace(/:[^/]+/g, '([^/]+)') + '$');
|
@@ -113,12 +137,15 @@ const match = (route: Route, ctx: Context): boolean => {
|
|
113
137
|
return false;
|
114
138
|
}
|
115
139
|
|
116
|
-
|
140
|
+
|
141
|
+
|
142
|
+
const router: Router = (port?: number | string, options?: RouterOptions<Options>) => {
|
117
143
|
const routes: Array<Route> = new Array();
|
118
|
-
const paths: { [key: string]: string } = {};
|
119
144
|
const lgr = logger();
|
145
|
+
let dbConn = '';
|
120
146
|
|
121
147
|
return {
|
148
|
+
// add a new route
|
122
149
|
add: (pattern: string, method: string, callback: (ctx: Context) => Response | Promise<Response>) => {
|
123
150
|
routes.push({
|
124
151
|
pattern: pattern,
|
@@ -126,6 +153,7 @@ const router: Router = (port?: number | string, options?: Options) => {
|
|
126
153
|
callback: callback,
|
127
154
|
})
|
128
155
|
},
|
156
|
+
// add a route for static files
|
129
157
|
static: async (pattern: string, root: string) => {
|
130
158
|
await readDir(root, async (fp, _) => {
|
131
159
|
const pure = path.join('.', fp);
|
@@ -133,10 +161,7 @@ const router: Router = (port?: number | string, options?: Options) => {
|
|
133
161
|
|
134
162
|
let base = path.basename(pure);
|
135
163
|
|
136
|
-
if (ext === '.html')
|
137
|
-
base = base.replace(ext, '');
|
138
|
-
|
139
|
-
}
|
164
|
+
if (ext === '.html') base = base.replace(ext, '');
|
140
165
|
|
141
166
|
if (pattern[0] !== '/') pattern = '/' + pattern;
|
142
167
|
|
@@ -152,33 +177,44 @@ const router: Router = (port?: number | string, options?: Options) => {
|
|
152
177
|
routes.push(route);
|
153
178
|
});
|
154
179
|
},
|
180
|
+
// start the server
|
155
181
|
serve: () => {
|
156
182
|
lgr.start(port ?? 3000);
|
183
|
+
let opts: Options = {db: ':memory:'};
|
184
|
+
|
157
185
|
Bun.serve({
|
158
186
|
port: port ?? 3000,
|
159
187
|
...options,
|
160
|
-
fetch(req) {
|
188
|
+
async fetch(req) {
|
161
189
|
const url = new URL(req.url);
|
190
|
+
|
191
|
+
//? ????
|
192
|
+
if (options) {
|
193
|
+
let o = options as Options;
|
194
|
+
opts.db = o.db;
|
195
|
+
}
|
162
196
|
for (const route of routes) {
|
163
197
|
const ctx: Context = {
|
164
198
|
request: req,
|
165
199
|
params: new Map(),
|
166
|
-
|
200
|
+
db: new Database(opts.db ?? ':memory:'),
|
167
201
|
};
|
168
202
|
|
169
203
|
if (url.pathname === '/favicon.ico') return noContent();
|
170
204
|
|
171
205
|
if (match(route, ctx)) {
|
172
|
-
|
173
|
-
|
206
|
+
const res = await route.callback(ctx);
|
207
|
+
lgr.info(res.status, url.pathname, route.method);
|
208
|
+
return res;
|
174
209
|
}
|
175
210
|
}
|
176
211
|
lgr.info(404, url.pathname, req.method, 'not found');
|
177
|
-
return
|
212
|
+
return httpMessage(404, 'not found');
|
178
213
|
}
|
179
214
|
});
|
180
215
|
},
|
181
216
|
}
|
182
217
|
}
|
183
218
|
|
219
|
+
|
184
220
|
export { router, json, file, extract, html }
|
package/package.json
CHANGED
package/tests/router.test.ts
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
2
|
-
import {
|
2
|
+
import { extract } from '..';
|
3
3
|
import { Context, Route } from '../lib/router/router.d';
|
4
|
-
import { logger } from '../lib/logger/logger';
|
5
|
-
import { color } from '../lib/logger/color';
|
6
4
|
|
7
5
|
describe('URL Params', () => {
|
8
6
|
test('/user/:name', () => {
|
@@ -15,7 +13,6 @@ describe('URL Params', () => {
|
|
15
13
|
const ctx: Context = {
|
16
14
|
request: new Request('http://localhost:3000/user/foo'),
|
17
15
|
params: new Map(),
|
18
|
-
fs: new Map(),
|
19
16
|
};
|
20
17
|
|
21
18
|
const extractor = extract(route, ctx);
|
@@ -36,7 +33,6 @@ describe('URL Params', () => {
|
|
36
33
|
const ctx: Context = {
|
37
34
|
request: new Request('http://localhost:3000/user/foo/123'),
|
38
35
|
params: new Map(),
|
39
|
-
fs: new Map(),
|
40
36
|
};
|
41
37
|
|
42
38
|
const extractor = extract(route, ctx);
|
@@ -60,7 +56,6 @@ describe('URL Params', () => {
|
|
60
56
|
const ctx: Context = {
|
61
57
|
request: new Request('http://localhost:3000/foo'),
|
62
58
|
params: new Map(),
|
63
|
-
fs: new Map(),
|
64
59
|
}
|
65
60
|
|
66
61
|
const url = new URL(ctx.request.url);
|
@@ -72,7 +67,7 @@ describe('URL Params', () => {
|
|
72
67
|
describe('Router', () => {
|
73
68
|
test('Serve', async () => {
|
74
69
|
const proc = Bun.spawn(['./tests/serve.test.sh'], {
|
75
|
-
onExit: (
|
70
|
+
onExit: (_proc, _exitCode, _signalCode , error) => {
|
76
71
|
if (error) console.error(error);
|
77
72
|
},
|
78
73
|
});
|
@@ -101,3 +96,4 @@ describe('Router', () => {
|
|
101
96
|
proc.kill(0);
|
102
97
|
});
|
103
98
|
});
|
99
|
+
|