bun-router 0.4.0 → 0.5.3

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 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
@@ -1,4 +1,5 @@
1
- import { router, html, Context } from '..';
1
+ import { router, html } from '..';
2
+ import { Context } from '../lib/router/router.d';
2
3
 
3
4
  const handler = (ctx: Context) => {
4
5
  const name = ctx.params.get('name');
@@ -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 }
@@ -0,0 +1,66 @@
1
+ const httpStatusCodes: { [key: number]: string } = {
2
+ 100: 'Continue',
3
+ 101: 'Switching Protocols',
4
+ 102: 'Processing',
5
+ 103: 'Early Hints',
6
+ 200: 'OK',
7
+ 201: 'Created',
8
+ 202: 'Accepted',
9
+ 203: 'Non-Authoritative Information',
10
+ 204: 'No Content',
11
+ 205: 'Reset Content',
12
+ 206: 'Partial Content',
13
+ 207: 'Multi-Status',
14
+ 208: 'Already Reported',
15
+ 226: 'IM Used',
16
+ 300: 'Multiple Choices',
17
+ 301: 'Moved Permanently',
18
+ 302: 'Found',
19
+ 303: 'See Other',
20
+ 304: 'Not Modified',
21
+ 305: 'Use Proxy',
22
+ 307: 'Temporary Redirect',
23
+ 308: 'Permanent Redirect',
24
+ 400: 'Bad Request',
25
+ 401: 'Unauthorized',
26
+ 402: 'Payment Required',
27
+ 403: 'Forbidden',
28
+ 404: 'Not Found',
29
+ 405: 'Method Not Allowed',
30
+ 406: 'Not Acceptable',
31
+ 407: 'Proxy Authentication Required',
32
+ 408: 'Request Timeout',
33
+ 409: 'Conflict',
34
+ 410: 'Gone',
35
+ 411: 'Length Required',
36
+ 412: 'Precondition Failed',
37
+ 413: 'Payload Too Large',
38
+ 414: 'URI Too Long',
39
+ 415: 'Unsupported Media Type',
40
+ 416: 'Range Not Satisfiable',
41
+ 417: 'Expectation Failed',
42
+ 418: "I'm a Teapot",
43
+ 421: 'Misdirected Request',
44
+ 422: 'Unprocessable Entity',
45
+ 423: 'Locked',
46
+ 424: 'Failed Dependency',
47
+ 425: 'Too Early',
48
+ 426: 'Upgrade Required',
49
+ 428: 'Precondition Required',
50
+ 429: 'Too Many Requests',
51
+ 431: 'Request Header Fields Too Large',
52
+ 451: 'Unavailable For Legal Reasons',
53
+ 500: 'Internal Server Error',
54
+ 501: 'Not Implemented',
55
+ 502: 'Bad Gateway',
56
+ 503: 'Service Unavailable',
57
+ 504: 'Gateway Timeout',
58
+ 505: 'HTTP Version Not Supported',
59
+ 506: 'Variant Also Negotiates',
60
+ 507: 'Insufficient Storage',
61
+ 508: 'Loop Detected',
62
+ 510: 'Not Extended',
63
+ 511: 'Network Authentication Required',
64
+ };
65
+
66
+ export { httpStatusCodes }
@@ -3,6 +3,7 @@ type Logger = {
3
3
  info: (statusCode: number, routePath: string, method: string, message?: string) => void,
4
4
  error: (statusCode: number, routePath: string, method: string, error: Error) => void,
5
5
  warn: (msg: string) => void,
6
+ message: (msg: string) => void,
6
7
  }
7
8
 
8
9
  export { Logger }
@@ -14,17 +14,23 @@ const timestamp = (date: Date) => {
14
14
  return {month, day, hour, minute, stamp};
15
15
  }
16
16
 
17
- const colorCode = (n: number, text?:string): string => {
17
+ // append ANSI color escape sequences to a string based on the given HTTP status code.
18
+ const colorCode = (n: number, text?: string): string => {
18
19
  const s = ` [${String(n)}${text ?? ''}] `;
19
- if (n < 100) return color('black', 'bgYellow', s);
20
- else if (n >= 100 && n < 200) return color('black', 'bgCyan', s);
21
- else if (n >= 200 && n < 300) return color('black', 'bgGreen', s);
22
- else if (n >= 300 && n < 400) return color('black', 'bgRed', s);
23
- else if (n >= 400 && n < 500) return color('black', 'bgRed', s);
24
- else if (n >= 500) return color('white', 'bgRed', s);
25
- return color('white', 'bgBlack', `[${s}]`).trim();
20
+ let backgroundColor = 'bgBlack';
21
+ let foregroundColor = 'white';
22
+
23
+ if (n < 100) backgroundColor = 'bgYellow';
24
+ else if (n < 200) backgroundColor = 'bgCyan';
25
+ else if (n < 300) backgroundColor = 'bgGreen';
26
+ else if (n < 400) backgroundColor = 'bgRed';
27
+ else if (n < 500) backgroundColor = 'bgRed';
28
+ else foregroundColor = 'white';
29
+
30
+ return color(foregroundColor, backgroundColor, s).trim();
26
31
  }
27
32
 
33
+
28
34
  const clean = (s: string) => s.replace(/\x1B\[\d{1,2}(;\d{1,2}){0,2}m/g, '');
29
35
 
30
36
  const format = (statusCode: number, routePath: string, method: string, message?: string): string => {
@@ -39,6 +45,7 @@ const logger = (): Logger => {
39
45
  const messages: string[] = [];
40
46
  const errors: string[] = [];
41
47
  return {
48
+ // initial log message
42
49
  start: async (port: number | string) => {
43
50
  const { stamp } = timestamp((new Date(Date.now())));
44
51
  const source = color('green', 'bgBlack', `[bun-router ${stamp}]`)
@@ -51,7 +58,7 @@ const logger = (): Logger => {
51
58
  const { stamp } = timestamp((new Date(Date.now())));
52
59
  const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
53
60
  const rp = color('white', 'bgBlack', routePath);
54
- const msg = `${source}: ${colorCode(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${method}\n`
61
+ const msg = `${source}: ${colorCode(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${method}${' | ' +message ?? ''}\n`
55
62
 
56
63
  await Bun.write(Bun.stdout, msg);
57
64
 
@@ -73,6 +80,15 @@ const logger = (): Logger => {
73
80
  const msgColor = color('yellow', 'bgBlack', msg);
74
81
  msg = `${source} : ${msgColor}\n`;
75
82
  await Bun.write(Bun.stdout, msg);
83
+ },
84
+ message: async (msg: string) => {
85
+ const { stamp } = timestamp((new Date(Date.now())));
86
+ const source = color('black', 'bgCyan', `[message ${stamp}]`);
87
+ const msgColor = color('yellow', 'bgBlack', msg);
88
+ msg = `${source}: ${msgColor}\n`;
89
+ await Bun.write(Bun.stdout, msg);
90
+
91
+ messages.push(clean(msg));
76
92
  }
77
93
  }
78
94
  }
@@ -1,11 +1,14 @@
1
1
  import { TLSOptions, TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptions, TLSServeOptions } from 'bun';
2
+ import { Logger } from '../logger/logger';
3
+ import { Database } from 'bun:sqlite';
2
4
 
3
5
 
4
6
  type Context = {
5
7
  request: Request,
6
8
  params: Map<string, string>,
7
- fs: Map<string, string>,
8
9
  token?: string,
10
+ db: Database,
11
+ logger: Logger,
9
12
  }
10
13
 
11
14
  type Route = {
@@ -14,18 +17,24 @@ type Route = {
14
17
  callback: (req: Context) => Response | Promise<Response>
15
18
  }
16
19
 
17
- type Options = ServeOptions
18
- | TLSServeOptions
19
- | WebSocketServeOptions
20
- | TLSWebSocketServeOptions
20
+ type Options = {
21
+ db: string,
22
+ }
23
+
24
+ type RouterOptions<Options> = ServeOptions
25
+ | TLSServeOptions<Options>
26
+ | WebSocketServeOptions<Options>
27
+ | TLSWebSocketServeOptions<Options>
21
28
  | undefined
22
29
 
23
30
 
24
- type Router = (port?: number | string, options?: Options) => {
31
+ type Router = (port?: number | string, options?: RouterOptions) => {
25
32
  add: (pattern: string, method: string, callback: (req: Context) => Response | Promise<Response>) => void,
33
+ GET: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => void,
34
+ POST: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => void,
26
35
  static: (pattern: string, root: string) => void,
27
36
  serve: () => void,
28
37
  }
29
38
 
30
39
 
31
- export { Context , Route, Router, Options }
40
+ export { Context , Route, Router, RouterOptions, Options }
@@ -1,10 +1,26 @@
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';
3
+ import { httpStatusCodes } from '../http/status';
2
4
  import { readDir } from '../fs/fsys';
3
5
  import { logger } from '../logger/logger';
4
6
  import path from 'path';
7
+ import { Logger } from '../logger/logger.d';
8
+
9
+ // create a generic HTTP response
10
+ const httpMessage = async (status: number, msg?: string): Promise<Response> => {
11
+ const response = new Response(msg ?? '?', {
12
+ status: status,
13
+ statusText: msg ?? '?',
14
+ headers: { 'Content-Type': 'text/html; charset-uft-8' }
15
+ });
16
+ return new Promise((resolve) => {
17
+ resolve(response);
18
+ });
19
+ };
5
20
 
6
- const notFound = async (): Promise<Response> => {
7
- const response = new Response('not found', {
21
+ // a generic 'not found' HTTP response
22
+ const notFound = async (msg?: string): Promise<Response> => {
23
+ const response = new Response(msg ?? 'not found', {
8
24
  status: 404,
9
25
  statusText: 'not found',
10
26
  headers: { 'Content-Type': 'text/html' },
@@ -15,6 +31,7 @@ const notFound = async (): Promise<Response> => {
15
31
  });
16
32
  }
17
33
 
34
+ // a generic 'no content' HTTP response
18
35
  const noContent = async (): Promise<Response> => {
19
36
  const response = new Response('no content', {
20
37
  status: 204,
@@ -26,47 +43,54 @@ const noContent = async (): Promise<Response> => {
26
43
  });
27
44
  }
28
45
 
46
+ // IO handling
29
47
  const file = async (filepath: string): Promise<Response> => {
30
48
  const file = Bun.file(filepath);
31
49
  const exists = await file.exists();
32
50
 
51
+ // check if the file exists, return 'not found' if it doesn't.
33
52
  if (!exists)
34
- return notFound();
53
+ return notFound(`File not found: ${filepath}`);
35
54
 
55
+ // get the content of the file as an ArrayBuffer
36
56
  const content = await file.arrayBuffer();
37
57
  if (!content)
38
- return notFound();
58
+ return noContent();
39
59
 
60
+ // default Content-Type + encoding
40
61
  let contentType = 'text/html; charset=utf-8';
41
62
 
63
+ // change the Content-Type if the file type is an image.
64
+ // file.type provides the necessary Content-Type
42
65
  if (file.type.includes('image')) {
43
66
  contentType = file.type + '; charset=utf-8';
44
67
  }
45
68
 
69
+ // create a new response with the necessary criteria
46
70
  const response = new Response(content, {
47
71
  status: 200,
48
72
  statusText: 'ok',
49
73
  headers: { 'Content-Type': contentType },
50
74
  });
51
75
 
52
- return new Promise<Response>((resolve) => {
53
- resolve(response);
54
- });
76
+ return Promise.resolve(response);
55
77
  }
56
78
 
79
+ // handle strings as HTML
57
80
  const html = async (content: string): Promise<Response> => {
58
81
  const response = new Response(content, {
59
82
  status: 200,
60
83
  statusText: 'ok',
61
84
  headers: { 'Content-Type': 'text/html; charset=utf-8' },
62
85
  });
86
+
87
+ // escape the HTML
63
88
  content = Bun.escapeHTML(content);
64
89
 
65
- return new Promise<Response>((resolve) => {
66
- resolve(response);
67
- });
90
+ return Promise.resolve(response);
68
91
  }
69
92
 
93
+ // create a JSON response
70
94
  const json = (data: any): Response => {
71
95
  const jsonString = JSON.stringify(data);
72
96
 
@@ -76,6 +100,8 @@ const json = (data: any): Response => {
76
100
  return res
77
101
  }
78
102
 
103
+ // extract dynamic URL parameters
104
+ // if the route pattern is /:foo and the request URL is /bar: {foo: 'bar'}
79
105
  const extract = (route: Route, ctx: Context) => {
80
106
  const url = new URL(ctx.request.url);
81
107
  const pathSegments = route.pattern.split('/');
@@ -83,7 +109,6 @@ const extract = (route: Route, ctx: Context) => {
83
109
 
84
110
  if (pathSegments.length !== urlSegments.length) return
85
111
 
86
-
87
112
  return {
88
113
  params: () => {
89
114
  for (let i = 0; i < pathSegments.length; i++) {
@@ -98,12 +123,13 @@ const extract = (route: Route, ctx: Context) => {
98
123
 
99
124
  }
100
125
 
126
+ // ensure the route pattern matches the request URL
101
127
  const match = (route: Route, ctx: Context): boolean => {
102
128
  const url = new URL(ctx.request.url);
103
129
  const patternRegex = new RegExp('^' + route.pattern.replace(/:[^/]+/g, '([^/]+)') + '$');
104
130
  const matches = url.pathname.match(patternRegex);
105
131
 
106
- if (matches) {
132
+ if (matches && route.method === ctx.request.method) {
107
133
  const extractor = extract(route, ctx);
108
134
  extractor?.params();
109
135
 
@@ -113,12 +139,24 @@ const match = (route: Route, ctx: Context): boolean => {
113
139
  return false;
114
140
  }
115
141
 
116
- const router: Router = (port?: number | string, options?: Options) => {
142
+ const setContext = (req: Request, lgr: Logger, opts: Options): Context => {
143
+ return {
144
+ request: req,
145
+ params: new Map(),
146
+ db: new Database(opts.db ?? ':memory:'),
147
+ logger: lgr,
148
+ }
149
+ }
150
+
151
+
152
+
153
+ const router: Router = (port?: number | string, options?: RouterOptions<Options>) => {
117
154
  const routes: Array<Route> = new Array();
118
- const paths: { [key: string]: string } = {};
119
155
  const lgr = logger();
156
+ let dbConn = '';
120
157
 
121
158
  return {
159
+ // add a new route
122
160
  add: (pattern: string, method: string, callback: (ctx: Context) => Response | Promise<Response>) => {
123
161
  routes.push({
124
162
  pattern: pattern,
@@ -126,6 +164,21 @@ const router: Router = (port?: number | string, options?: Options) => {
126
164
  callback: callback,
127
165
  })
128
166
  },
167
+ GET: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => {
168
+ routes.push({
169
+ pattern: pattern,
170
+ method: 'GET',
171
+ callback: callback,
172
+ });
173
+ },
174
+ POST: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => {
175
+ routes.push({
176
+ pattern: pattern,
177
+ method: 'POST',
178
+ callback: callback,
179
+ });
180
+ },
181
+ // add a route for static files
129
182
  static: async (pattern: string, root: string) => {
130
183
  await readDir(root, async (fp, _) => {
131
184
  const pure = path.join('.', fp);
@@ -133,10 +186,7 @@ const router: Router = (port?: number | string, options?: Options) => {
133
186
 
134
187
  let base = path.basename(pure);
135
188
 
136
- if (ext === '.html') {
137
- base = base.replace(ext, '');
138
-
139
- }
189
+ if (ext === '.html') base = base.replace(ext, '');
140
190
 
141
191
  if (pattern[0] !== '/') pattern = '/' + pattern;
142
192
 
@@ -152,33 +202,62 @@ const router: Router = (port?: number | string, options?: Options) => {
152
202
  routes.push(route);
153
203
  });
154
204
  },
205
+ // start the server
155
206
  serve: () => {
156
207
  lgr.start(port ?? 3000);
208
+ let opts: Options = { db: ':memory:' };
209
+
157
210
  Bun.serve({
158
211
  port: port ?? 3000,
159
212
  ...options,
160
- fetch(req) {
213
+ async fetch(req) {
161
214
  const url = new URL(req.url);
215
+
216
+ if (options) {
217
+ let o = options as Options;
218
+ opts.db = o.db;
219
+ }
220
+
221
+ let statusCode = 404; // Default status code for route not found
222
+
162
223
  for (const route of routes) {
163
- const ctx: Context = {
164
- request: req,
165
- params: new Map(),
166
- fs: new Map(),
167
- };
224
+ const ctx = setContext(req, lgr, opts);
168
225
 
169
- if (url.pathname === '/favicon.ico') return noContent();
226
+ if (url.pathname === '/favicon.ico') {
227
+ return noContent();
228
+ }
229
+
230
+ if (route.method !== req.method) {
231
+ statusCode = 405;
232
+ continue;
233
+ }
170
234
 
171
235
  if (match(route, ctx)) {
172
- lgr.info(200, route.pattern, route.method)
173
- return route.callback(ctx);
236
+ const res = await route.callback(ctx);
237
+ if (res) {
238
+ statusCode = 200;
239
+ lgr.info(statusCode, url.pathname, req.method, httpStatusCodes[statusCode]);
240
+ return res
241
+ } else {
242
+ statusCode = 500;
243
+ break;
244
+ }
174
245
  }
175
246
  }
176
- lgr.info(404, url.pathname, req.method, 'not found');
177
- return new Response('not found');
247
+
248
+ if (statusCode === 405) {
249
+ lgr.info(statusCode, url.pathname, req.method, httpStatusCodes[statusCode]);
250
+ return httpMessage(statusCode, httpStatusCodes[statusCode]);
251
+ }
252
+
253
+ lgr.info(statusCode, url.pathname, req.method, httpStatusCodes[statusCode]);
254
+ return httpMessage(statusCode, httpStatusCodes[statusCode]);
255
+
178
256
  }
179
257
  });
180
258
  },
181
259
  }
182
260
  }
183
261
 
262
+
184
263
  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.3"
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
+