bun-router 0.4.0 → 0.5.3

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
@@ -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
+