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 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
@@ -1,5 +1,3 @@
1
1
  export * from './lib/router/router';
2
- export * from './lib/router/router.d';
3
2
  export * from './lib/fs/fsys';
4
3
  export * from './lib/logger/logger';
5
- export * from './lib/logger/logger.d';
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
- const fp = path.join(dirpath, bunFile.name!);
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
- } catch (err) {
20
- console.error(err);
21
- }
23
+
22
24
  }
23
25
 
24
26
  export { readDir }
@@ -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}]`)
@@ -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 = ServeOptions
18
- | TLSServeOptions
19
- | WebSocketServeOptions
20
- | TLSWebSocketServeOptions
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?: 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 }
@@ -1,10 +1,24 @@
1
- import { Route, Router, Context, Options } from './router.d';
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
- const notFound = async (): Promise<Response> => {
7
- const response = new Response('not found', {
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 notFound();
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 new Promise<Response>((resolve) => {
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 new Promise<Response>((resolve) => {
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
- const router: Router = (port?: number | string, options?: Options) => {
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
- fs: new Map(),
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
- lgr.info(200, route.pattern, route.method)
173
- return route.callback(ctx);
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 new Response('not found');
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
@@ -8,5 +8,5 @@
8
8
  "peerDependencies": {
9
9
  "typescript": "^5.0.0"
10
10
  },
11
- "version": "0.4.0"
11
+ "version": "0.5.0"
12
12
  }
@@ -1,8 +1,6 @@
1
1
  import { describe, test, expect } from 'bun:test';
2
- import { router, extract } from '..';
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: (proc, exitCode, signalCode , error) => {
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
+