bun-router 0.3.9 → 0.5.0
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/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
|
+
|