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 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.3.9"
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
+