bun-router 0.6.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -1,83 +1,40 @@
1
- # Bun Router
2
-
3
- I needed a router for `Bun`, so I made one. It's simple, naive, and hardly anything is abstracted.
1
+ # Bun router
4
2
 
5
3
  ### Usage
6
- ```typescript
7
- import { router } from 'bun-router';
8
-
9
- const r = router();
10
-
11
- r.add('/', 'GET', (ctx) => new Response('Hello World'));
12
-
13
- r.serve();
14
- ```
15
- #### Static Files
16
- ```typescript
17
- import { router } from 'bun-router';
18
-
19
- const r = router();
20
-
21
- r.static('/', './pages');
22
-
23
- r.serve();
24
- ```
4
+ `npm i -s bun-router`
25
5
 
26
- ##### Example
27
- ```typescript
28
- import {router, html, json } from 'bun-router';
6
+ or
29
7
 
30
- const r = router(3001);
8
+ `bun i bun-router`
31
9
 
32
- r.add('/', (ctx) => html('<h1>Hello World</h1>'));
33
10
 
34
- r.add('/greeting/:name', 'GET', (ctx) => {
35
- const name = ctx.params.get('name');
36
- if (!name) return new Response('invalid url parameters', {status: 400});
11
+ #### Example
12
+ ```ts
13
+ import { Router, http } from 'bun-router';
37
14
 
38
- return html(`<h4>Greetings, ${name}!</h4>`);
39
- });
15
+ const router = Router();
40
16
 
41
- const store: Map<string, string> = new Map();
17
+ router.add('/', 'GET', () => http.ok());
42
18
 
43
- r.add('/user/:id', 'GET', (ctx) => {
44
- const id = ctx.params.get('id');
45
- if (!id) return new Response('user not found', {status: 404});
46
-
47
- const user = store.get(id);
19
+ router.get('/u/:username', ctx => {
20
+ const username = ctx.params.get('username');
48
21
 
49
- if (!user) return new Response('user not found', { status: 404 });
22
+ if (!username) return http.badRequest();
50
23
 
51
- return json(user);
24
+ return ctx.json({ username: username });
52
25
  });
53
26
 
54
- r.serve();
27
+ router.serve();
55
28
  ```
56
29
 
57
- **SQLite**
30
+ ##### Static
58
31
  ```ts
59
- import { router, json } from '..';
60
- import { Database } from 'bun:sqlite';
61
-
62
- const r = router(3000, {db: './examples/dbs/test.db'});
32
+ import { Router } from 'bun-router';
63
33
 
64
- r.add('/u/new/:name', 'GET', (ctx) => {
65
- const name = ctx.params.get('name');
66
- const rando = Math.floor(Math.random()*1000);
34
+ const router = Router();
67
35
 
68
- ctx.db.run(`INSERT INTO test VALUES(${rando}, "${name}")`);
69
-
70
- return json({message: 'ok'});
71
- });
36
+ router.static('/assets', 'static');
72
37
 
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();
38
+ router.serve();
39
+ ```
82
40
 
83
- ```
package/examples/basic.ts CHANGED
@@ -1,18 +1,13 @@
1
- import { router, http } from '..';
1
+ import { Router, http } from '..';
2
2
 
3
- const r = router();
3
+ const router = Router();
4
4
 
5
- r.add('/', 'GET', () => http.json(200, 'ok'));
5
+ router.add('/', 'GET', () => http.json(200, 'ok'));
6
6
 
7
- r.add('/user/:name', 'GET', (ctx) => {
7
+ router.add('/user/:name', 'GET', (ctx) => {
8
8
  const name = ctx.params.get('name');
9
+ if (!name) return http.json(500, 'no name');
9
10
  return http.json(200, name);
10
11
  });
11
12
 
12
- r.add('/user/:name/:id', 'GET', (ctx) => {
13
- const name = ctx.params.get('name');
14
- const id = ctx.params.get('id');
15
- return http.json(200, {name: name, id: id});
16
- });
17
-
18
- r.serve();
13
+ router.serve();
package/examples/db.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { Router, http } from '..';
2
+
3
+ const router = Router(3000, {db: './dbs/test.db'});
4
+
5
+ router.add('/', 'GET', async (ctx) => {
6
+ const formData = await ctx.formData;
7
+ const name = formData?.get('name');
8
+ return http.ok();
9
+ });
10
+
@@ -1,29 +1,24 @@
1
- import { router, http } from '..';
1
+ import { Router, http } from '..';
2
2
  import { Context } from '../lib/router/router.d';
3
3
 
4
- const handler = (ctx: Context) => {
5
- const name = ctx.params.get('name');
6
- if (typeof name === 'undefined' || name === '') return http.html(500, '<h6 style="color: red">User Undefined</h6>');
7
- return http.html(200, `<h4>Hello, ${name}!</h4>`);
8
- }
4
+ const home = () => new Response('Welcome Home', { status: 200 });
9
5
 
10
- const space = (ctx: Context) => {
11
- const name = ctx.params.get('name');
12
- if (typeof name === 'undefined' || name === '') return http.html(404, `<h6 style="color: red">Space [${name}] Not Found</h6>`);
13
- return http.html(200, `<h4>Welcome to ${name}!`)
6
+ const subreddit = (ctx: Context) => {
7
+ const sub = ctx.params.get('subreddit');
8
+ if (!sub) return http.json(400, { error: 'no subreddit provided' });
9
+ return http.json(200, { subreddit: sub });
14
10
  }
15
11
 
16
- const handleSettings = (ctx: Context) => {
17
- const name = ctx.params.get('name');
18
- if (typeof name === 'undefined' || name === '') return http.html(404, `<h6 style="color: red">User Not Found</h6>`);
19
- return http.html(200, `<h4>Settings for ${name}</h4>`)
12
+ const user = (ctx: Context) => {
13
+ const user = ctx.params.get('user');
14
+ if (!user) return http.json(400, { error: 'no user provided' });
15
+ return http.json(200, { user: user });
20
16
  }
21
17
 
22
- const r = router();
23
-
24
- r.add('/u/:name', 'GET', handler);
25
- r.add('/s/:name', 'GET', space);
26
- r.add('/u/:name/settings', 'GET', handleSettings);
18
+ const r = Router();
27
19
 
20
+ r.add('/', 'GET', home);
21
+ r.add('/r/:subreddit', 'GET', subreddit);
22
+ r.add('/u/:user', 'GET', user);
28
23
 
29
24
  r.serve();
@@ -1,16 +1,11 @@
1
- import { router, logger, http } from '..';
1
+ import { Router, http } from '..';
2
2
 
3
- const r = router();
4
- const log = logger();
3
+ const r = Router();
5
4
 
6
- r.add('/:foo', 'GET', (ctx) => {
7
- const url = new URL(ctx.request.url);
8
- const foo = ctx.params.get('foo');
9
- if (!foo) {
10
- log.error(500, url.pathname, ctx.request.method, new Error('undefined'));
11
- return http.json(500,{text: 'Foo is undefined'});
12
- }
13
- return http.html(200, `<h4 style='font-family: sans-serif;'>Oh hello, ${foo}</h4>`)
5
+ r.get('/', ctx => {
6
+ ctx.logger.debug('hello from home');
7
+
8
+ return http.ok();
14
9
  });
15
10
 
16
11
  r.serve();
@@ -1,6 +1,6 @@
1
- import { router } from '..';
1
+ import { Router } from '..';
2
2
 
3
- const r = router(3001);
3
+ const r = Router(3001);
4
4
 
5
5
  r.static('/', './examples/pages');
6
6
  r.serve();
package/examples/todo.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { router, http } from '..';
1
+ import { Router, http } from '..';
2
2
  import { Context } from '../lib/router/router.d';
3
3
 
4
4
  const Todo = () => {
@@ -15,7 +15,7 @@ const Todo = () => {
15
15
 
16
16
  const todo = Todo();
17
17
 
18
- const r = router();
18
+ const r = Router();
19
19
 
20
20
  r.add('/api/new', 'POST', ctx => {
21
21
  const query = new URL(ctx.request.url).searchParams;
package/lib/fs/fsys.ts CHANGED
@@ -20,7 +20,7 @@ const readDir = async (dirpath: string, handler: (filepath: string, entry: BunFi
20
20
  if (isdir) await readDir(fp, handler);
21
21
  else handler(fp, bunFile);
22
22
  }
23
-
24
23
  }
25
24
 
25
+
26
26
  export { readDir }
@@ -1,6 +1,12 @@
1
1
  import { httpStatusCodes } from "./status";
2
2
 
3
3
  const http = {
4
+ ok: async (msg?: string): Promise<Response> => {
5
+ return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
6
+ status: 200,
7
+ statusText: httpStatusCodes[200],
8
+ }));
9
+ },
4
10
  json: async (statusCode: number, data: any): Promise<Response> => {
5
11
  const jsonString = JSON.stringify(data);
6
12
  return Promise.resolve(new Response(jsonString, {
@@ -10,8 +16,7 @@ const http = {
10
16
  }));
11
17
  },
12
18
  html: async (statusCode: number, content: string): Promise<Response> => {
13
- content = Bun.escapeHTML(content);
14
- return Promise.resolve(new Response(Bun.escapeHTML(content), {
19
+ return Promise.resolve(new Response(content, {
15
20
  status: statusCode,
16
21
  statusText: httpStatusCodes[statusCode],
17
22
  headers: {'Content-Type': 'text/html; charset=utf-8'}
@@ -50,6 +55,15 @@ const http = {
50
55
 
51
56
  return Promise.resolve(response);
52
57
  },
58
+ methodNotAllowed: async (msg?: string): Promise<Response> => {
59
+ const response = new Response(msg ?? 'method not allowed', {
60
+ status: 405,
61
+ statusText: httpStatusCodes[405],
62
+ headers: {'Content-Type': 'text/html'},
63
+ });
64
+
65
+ return Promise.resolve(response);
66
+ },
53
67
  message: async (status: number, msg?: string): Promise<Response> => {
54
68
  const response = new Response(msg ?? '?', {
55
69
  status: status,
@@ -57,7 +71,7 @@ const http = {
57
71
  headers: {'Content-Type': 'text/html; charset-utf-8'},
58
72
  });
59
73
  return Promise.resolve(response)
60
- },
74
+ },
61
75
  }
62
76
 
63
77
  export { http }
@@ -20,16 +20,16 @@ const Colors: Record<string,string> = {
20
20
  bgMagenta: "\x1b[45m",
21
21
  bgCyan: "\x1b[46m",
22
22
  bgWhite: "\x1b[47m",
23
- };
23
+ } as const;
24
24
 
25
25
 
26
- const color = (c: string, bkg: string, msg: string) => {
27
- const foreground = Colors[c];
28
- const background = Colors[bkg];
29
- const reset = Colors.reset;
30
26
 
31
- return `${foreground}${background}${msg}${reset}`;
32
- };
27
+ function color(foreground: string, background: string, message: string) {
28
+ const _foreground = Colors[foreground];
29
+ const _background = Colors[background];
30
+ const reset = Colors.reset;
31
+ return `${_foreground}${_background}${message}${reset}`;
32
+ }
33
33
 
34
34
 
35
35
 
@@ -1,9 +1,8 @@
1
- type Logger = {
2
- start: (port: number | string) => void,
1
+ type BunLogger = {
3
2
  info: (statusCode: number, routePath: string, method: string, message?: string) => void,
4
3
  error: (statusCode: number, routePath: string, method: string, error: Error) => void,
5
- warn: (msg: string) => void,
6
- message: (msg: string) => void,
4
+ warn: (message: string) => void,
5
+ message: (message: string) => void,
7
6
  }
8
7
 
9
- export { Logger }
8
+ export { BunLogger }
@@ -1,9 +1,58 @@
1
1
  import { color } from './color';
2
- import {Logger} from './logger.d';
2
+ import { BunLogger } from './logger.d';
3
+
4
+
5
+ const TITLE = `
6
+ _ _
7
+ | |_ _ _ ___ ___ ___ _ _| |_ ___ ___
8
+ | . | | | | | _| . | | | _| -_| _|
9
+ |___|___|_|_| |_| |___|___|_| |___|_|
10
+
11
+ `
12
+ const VERSION = '0.7.1';
13
+ const Logger = (): BunLogger => {
14
+ return {
15
+ info: async (statusCode: number, routePath: string, method: string, message?: string) => {
16
+ const { stamp } = timestamp((new Date(Date.now())));
17
+ const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
18
+ const rp = color('white', 'bgBlack', routePath);
19
+
20
+ message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${method}${ message ?? ''}\n`
21
+
22
+ await Bun.write(Bun.stdout, message);
23
+
24
+ },
25
+ error: async (statusCode: number, routePath: string, method: string, error: Error) => {
26
+ const { stamp } = timestamp((new Date(Date.now())));
27
+ const source = color('black', 'bgRed', `[error ${stamp}]`);
28
+ const rp = color('white', 'bgBlack', routePath);
29
+
30
+ const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${error.message}\n`
3
31
 
4
- const pad = (n: number) => String(n).padStart(2, '0');
32
+ await Bun.write(Bun.stdout, message);
33
+ },
34
+ warn: async (message: string) => {
35
+ const { stamp } = timestamp((new Date(Date.now())));
36
+ const source = color('black', 'bgYellow', `[warning ${stamp}]`);
37
+ const messageColor = color('yellow', 'bgBlack', message);
38
+
39
+ message = `${source} : ${messageColor}\n`;
40
+
41
+ await Bun.write(Bun.stdout, message);
42
+ },
43
+ message: async (message: string) => {
44
+ const { stamp } = timestamp((new Date(Date.now())));
45
+ const source = color('black', 'bgCyan', `[message ${stamp}]`);
46
+ const messageColor = color('yellow', 'bgBlack', message);
47
+
48
+ message = `${source}: ${messageColor}\n`;
5
49
 
6
- const timestamp = (date: Date) => {
50
+ await Bun.write(Bun.stdout, message);
51
+ },
52
+ }
53
+ }
54
+
55
+ function timestamp(date: Date) {
7
56
  const month = pad(date.getMonth());
8
57
  const day = pad(date.getDate());
9
58
  const hour = pad(date.getHours());
@@ -14,79 +63,32 @@ const timestamp = (date: Date) => {
14
63
  return {month, day, hour, minute, stamp};
15
64
  }
16
65
 
17
- // append ANSI color escape sequences to a string based on the given HTTP status code.
18
- const colorCode = (n: number, text?:string): string => {
66
+ function setColor(n: number, text?: string){
19
67
  const s = ` [${String(n)}${text ?? ''}] `;
68
+
20
69
  if (n < 100) return color('black', 'bgYellow', s);
21
70
  else if (n >= 100 && n < 200) return color('black', 'bgCyan', s);
22
71
  else if (n >= 200 && n < 300) return color('black', 'bgGreen', s);
23
72
  else if (n >= 300 && n < 400) return color('black', 'bgRed', s);
24
73
  else if (n >= 400 && n < 500) return color('black', 'bgRed', s);
25
74
  else if (n >= 500) return color('white', 'bgRed', s);
75
+
26
76
  return color('white', 'bgBlack', `[${s}]`).trim();
27
77
  }
28
78
 
29
-
30
- const clean = (s: string) => s.replace(/\x1B\[\d{1,2}(;\d{1,2}){0,2}m/g, '');
31
-
32
- const format = (statusCode: number, routePath: string, method: string, message?: string): string => {
79
+ function startMessage(port: number | string) {
33
80
  const { stamp } = timestamp((new Date(Date.now())));
34
- const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
35
- const rp = color('white', 'bgBlack', routePath);
81
+ const source = color('green', 'bgBlack', `[bun-router ${stamp}]`)
82
+ const portColor = color('green', 'bgBlack', String(port));
83
+ const msg = `${source}: Starting Server on :${portColor}\n`;
84
+ const version = color('red', 'bgBlack', `v${VERSION}\n`);
36
85
 
37
- return `${source} : ${colorCode(statusCode)} : ${rp} ${(method === 'GET') ? '->' : '<-'} ${method}\n`
86
+ Bun.write(Bun.stdout, TITLE + '\n' + version);
87
+ Bun.write(Bun.stdout, msg);
38
88
  }
39
89
 
40
- const logger = (): Logger => {
41
- const messages: string[] = [];
42
- const errors: string[] = [];
43
- return {
44
- // initial log message
45
- start: async (port: number | string) => {
46
- const { stamp } = timestamp((new Date(Date.now())));
47
- const source = color('green', 'bgBlack', `[bun-router ${stamp}]`)
48
- const portColor = color('green', 'bgBlack', String(port));
49
- const msg = `${source}: Starting Server on :${portColor}\n`;
50
-
51
- await Bun.write(Bun.stdout, msg);
52
- },
53
- info: async (statusCode: number, routePath: string, method: string, message?: string) => {
54
- const { stamp } = timestamp((new Date(Date.now())));
55
- const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
56
- const rp = color('white', 'bgBlack', routePath);
57
- const msg = `${source}: ${colorCode(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${method}${' | ' +message ?? ''}\n`
58
-
59
- await Bun.write(Bun.stdout, msg);
60
-
61
- messages.push(clean(msg));
62
- },
63
- error: async (statusCode: number, routePath: string, method: string, error: Error) => {
64
- const { stamp } = timestamp((new Date(Date.now())));
65
- const source = color('black', 'bgRed', `[error ${stamp}]`);
66
- const rp = color('white', 'bgBlack', routePath);
67
- const msg = `${source}: ${colorCode(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${error.message}\n`;
68
-
69
- await Bun.write(Bun.stdout, msg);
70
-
71
- errors.push(clean(msg));
72
- },
73
- warn: async (msg: string) => {
74
- const { stamp } = timestamp((new Date(Date.now())));
75
- const source = color('black', 'bgYellow', `[warning ${stamp}]`);
76
- const msgColor = color('yellow', 'bgBlack', msg);
77
- msg = `${source} : ${msgColor}\n`;
78
- await Bun.write(Bun.stdout, msg);
79
- },
80
- message: async (msg: string) => {
81
- const { stamp } = timestamp((new Date(Date.now())));
82
- const source = color('black', 'bgCyan', `[message ${stamp}]`);
83
- const msgColor = color('yellow', 'bgBlack', msg);
84
- msg = `${source}: ${msgColor}\n`;
85
- await Bun.write(Bun.stdout, msg);
86
-
87
- messages.push(clean(msg));
88
- }
89
- }
90
+ function pad(n: number) {
91
+ return String(n).padStart(2, '0');
90
92
  }
91
93
 
92
- export { logger }
94
+ export { Logger, startMessage }
@@ -0,0 +1,49 @@
1
+ import { Route, Context } from "./router.d";
2
+ import { Logger } from "../..";
3
+ import { http } from "./router";
4
+
5
+ function extractParams(path: string, route: Route): Map<string, string> {
6
+ const params: Map<string, string> = new Map();
7
+ const pathSegments = path.split('/');
8
+ const routeSegments = route.path.split('/');
9
+
10
+ if (pathSegments.length !== routeSegments.length) return params;
11
+
12
+ for (let i = 0; i < pathSegments.length; i++) {
13
+ if (routeSegments[i][0] === ':') {
14
+ const key = routeSegments[i].replace(':', '');
15
+ const value = pathSegments[i];
16
+ params.set(key, value);
17
+ }
18
+ }
19
+
20
+ return params;
21
+ }
22
+
23
+ async function createContext(path: string, route: Route, request: Request): Promise<Context> {
24
+ const params = extractParams(path, route);
25
+ const query = new URLSearchParams(path);
26
+ const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
27
+
28
+ return Promise.resolve({
29
+ params,
30
+ request,
31
+ query,
32
+ formData,
33
+ logger: Logger(),
34
+ json: (statusCode: number, data: any) => http.json(statusCode, data),
35
+ });
36
+ }
37
+
38
+ function getContentType(headers: Headers): string {
39
+ const contentType = headers.get('Content-Type');
40
+ if (!contentType) return '';
41
+ return contentType;
42
+ }
43
+
44
+ function isMultiPartForm(headers: Headers): boolean {
45
+ const contentType = getContentType(headers);
46
+ return contentType.includes('multipart/form-data');
47
+ }
48
+
49
+ export { createContext }
@@ -2,26 +2,37 @@ import { TLSOptions, TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptio
2
2
  import { Logger } from '../logger/logger';
3
3
  import { Database } from 'bun:sqlite';
4
4
 
5
+ type BunRouter = (port?: number | string, options?: RouterOptions) => {
6
+ add: (pattern: string, method: string, callback: HttpHandler) => void;
7
+ get: (pattern: string, callback: HttpHandler) => void;
8
+ post: (pattern: string, callback: HttpHandler) => void;
9
+ put: (pattern: string, callback: HttpHandler) => void;
10
+ delete: (pattern: string, callback: HttpHandler) => void;
11
+ static: (pattern: string, root: string) => void;
12
+ serve: () => void;
13
+ }
14
+
15
+ type Route = {
16
+ children: Map<string, Route>;
17
+ path: string;
18
+ dynamicPath: string;
19
+ method: string;
20
+ handler: HttpHandler;
21
+ isLast: boolean;
22
+ }
5
23
 
6
24
  type Context = {
7
- cookies: Map<string, string>,
8
- db: Database,
9
- formData: FormData | Promise<FormData> | undefined,
10
- json: (statusCode: number, data: any) => Response | Promise<Response>,
11
- logger: Logger,
12
- params: Map<string, string>,
13
- query: URLSearchParams,
14
- request: Request,
15
- route: Route,
16
- token?: string,
25
+ db?: Database;
26
+ formData: FormData | Promise<FormData>;
27
+ json: (statusCode: number, data: any) => Response | Promise<Response>;
28
+ logger: Logger;
29
+ params: Map<string, string>;
30
+ query: URLSearchParams;
31
+ request: Request;
32
+ token?: string;
17
33
  };
18
34
 
19
-
20
- type Route = {
21
- pattern: string,
22
- method: string,
23
- callback: (req: Context) => Response | Promise<Response>
24
- }
35
+ type HttpHandler = (ctx: Context) => Response | Promise<Response>
25
36
 
26
37
  type Options = {
27
38
  db: string,
@@ -33,15 +44,4 @@ type RouterOptions<Options> = ServeOptions
33
44
  | TLSWebSocketServeOptions<Options>
34
45
  | undefined
35
46
 
36
-
37
- type Router = (port?: number | string, options?: RouterOptions) => {
38
- add: (pattern: string, method: string, callback: (req: Context) => Response | Promise<Response>) => void,
39
- GET: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => void,
40
- POST: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => void,
41
- static: (pattern: string, root: string) => void,
42
- serve: () => void,
43
- }
44
-
45
-
46
-
47
- export { Context , Route, Router, RouterOptions, Options }
47
+ export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler }
@@ -1,96 +1,27 @@
1
1
  import path from 'path';
2
2
  import { Database } from 'bun:sqlite';
3
- import { Route, Router, Context, RouterOptions, Options } from './router.d';
3
+ import { Route, BunRouter, Context, RouterOptions, Options, HttpHandler } from './router.d';
4
4
  import { httpStatusCodes } from '../http/status';
5
5
  import { readDir } from '../fs/fsys';
6
- import { logger } from '../logger/logger';
7
- import { Logger } from '../logger/logger.d';
8
- import { http } from '../http/generic-methods';
6
+ import { Logger, startMessage } from '../logger/logger';
7
+ import { http } from '../http/http';
8
+ import {RouteTree } from './tree';
9
+ import { createContext } from './context';
9
10
 
10
- // extract dynamic URL parameters
11
- // if the route pattern is /:foo and the request URL is /bar: {foo: 'bar'}
12
- const extract = (route: Route, ctx: Context) => {
13
- const url = new URL(ctx.request.url);
14
- const pathSegments = route.pattern.split('/');
15
- const urlSegments = url.pathname.split('/');
16
11
 
17
- if (pathSegments.length !== urlSegments.length) return
12
+ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
13
+ const { addRoute, findRoute } = RouteTree();
14
+ const logger = Logger();
18
15
 
19
16
  return {
20
- params: () => {
21
- for (let i = 0; i < pathSegments.length; i++) {
22
- if ((pathSegments[i][0] === ':')) {
23
- const k = pathSegments[i].replace(':', '');
24
- const v = urlSegments[i];
25
- ctx.params.set(k, v);
26
- }
27
- }
28
- }
29
- }
30
-
31
- }
32
-
33
- // ensure the route pattern matches the request URL
34
- const match = (route: Route, ctx: Context): boolean => {
35
- const url = new URL(ctx.request.url);
36
- const patternRegex = new RegExp('^' + route.pattern.replace(/:[^/]+/g, '([^/]+)') + '$');
37
- const matches = url.pathname.match(patternRegex);
38
-
39
- if (matches && route.method === ctx.request.method) {
40
- const extractor = extract(route, ctx);
41
- extractor?.params();
42
-
43
- return true;
44
- }
45
-
46
- return false;
47
- }
48
-
49
- // set the context for the reuest
50
- const setContext = (req: Request, lgr: Logger, opts: Options, route: Route): Context => {
51
- const token = req.headers.get('Authorization');
52
- return {
53
- token: token ?? '',
54
- cookies: new Map(),
55
- formData: req.formData(),
56
- request: req,
57
- params: new Map(),
58
- query: new URL(req.url).searchParams,
59
- db: new Database(opts.db ?? ':memory:'),
60
- logger: lgr,
61
- route: route,
62
- json: (statusCode: number, data: any) => http.json(statusCode, data),
63
- }
64
- }
65
-
66
- const router: Router = (port?: number | string, options?: RouterOptions<Options>) => {
67
- const routes: Array<Route> = new Array();
68
- const lgr = logger();
69
-
70
- return {
71
- // add a new route
72
- add: (pattern: string, method: string, callback: (ctx: Context) => Response | Promise<Response>) => {
73
- routes.push({
74
- pattern: pattern,
75
- method: method,
76
- callback: callback,
77
- })
78
- },
79
- GET: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => {
80
- routes.push({
81
- pattern: pattern,
82
- method: 'GET',
83
- callback: callback,
84
- });
85
- },
86
- POST: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => {
87
- routes.push({
88
- pattern: pattern,
89
- method: 'POST',
90
- callback: callback,
91
- });
92
- },
93
- // add a route for static files
17
+ // add a route to the router tree
18
+ add: (pattern, method, callback) => { addRoute(pattern, method, callback) },
19
+ get: (pattern: string, callback: HttpHandler) => { addRoute(pattern, 'GET', callback) },
20
+ post: (pattern, callback) => { addRoute(pattern, 'POST', callback) },
21
+ put: (pattern, callback) => { addRoute(pattern, 'PUT', callback)},
22
+ delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback) },
23
+
24
+ // add a static route to the router tree
94
25
  static: async (pattern: string, root: string) => {
95
26
  await readDir(root, async (fp, _) => {
96
27
  const pure = path.join('.', fp);
@@ -107,17 +38,20 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
107
38
  if (base === 'index') patternPath = pattern;
108
39
 
109
40
  const route: Route = {
110
- pattern: patternPath,
41
+ children: new Map(),
42
+ dynamicPath: '',
43
+ isLast: true,
44
+ path: patternPath,
111
45
  method: 'GET',
112
- callback: async () => await http.file(200, pure),
46
+ handler: async () => await http.file(200, pure),
113
47
  };
114
48
 
115
- routes.push(route);
49
+ addRoute(route.path, 'GET', route.handler);
116
50
  });
117
51
  },
118
52
  // start the server
119
53
  serve: () => {
120
- lgr.start(port ?? 3000);
54
+ startMessage(port ?? 3000);
121
55
  let opts: Options = { db: ':memory:' };
122
56
 
123
57
  Bun.serve({
@@ -125,52 +59,42 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
125
59
  ...options,
126
60
  async fetch(req) {
127
61
  const url = new URL(req.url);
62
+ let path = url.pathname;
128
63
 
64
+ // set the database
129
65
  if (options) {
130
66
  let o = options as Options;
131
67
  opts.db = o.db;
132
68
  }
133
69
 
134
- let statusCode = 404;
135
-
136
- for (const route of routes) {
137
- const ctx = setContext(req, lgr, opts, route);
70
+ const route = findRoute(path);
138
71
 
139
- if (match(route, ctx) || route.pattern === url.pathname) {
140
- if (route.method === ctx.request.method) {
141
- const res = await route.callback(ctx);
72
+ // if the route exists, execute the handler
73
+ if (route) {
74
+ if (route.method !== req.method) {
75
+ logger.info(405, url.pathname, req.method, httpStatusCodes[405]);
76
+ return Promise.resolve(http.methodNotAllowed());
77
+ }
142
78
 
143
- let cookieValue: string[] = [];
144
- if (ctx.cookies.size !== 0) {
145
- for (const [key, value] of ctx.cookies) {
146
- cookieValue.push(`${key}=${value}`);
147
- }
148
- }
79
+ const context = await createContext(path, route, req);
80
+ context.db = new Database(opts.db);
149
81
 
150
- res.headers.set('Set-Cookie', cookieValue.join('; '));
82
+ const response = await route.handler(context);
151
83
 
152
- statusCode = res.status;
84
+ logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
85
+ return Promise.resolve(response);
86
+ }
153
87
 
154
- lgr.info(res.status, route.pattern, req.method, httpStatusCodes[res.status]);
155
- return Promise.resolve(res);
156
- } else {
157
- const res = new Response(httpStatusCodes[405], {
158
- status: 405,
159
- statusText: httpStatusCodes[405]
160
- });
161
- lgr.info(405, route.pattern, req.method, httpStatusCodes[405])
162
- return Promise.resolve(res);
163
- }
164
- }
165
- }
88
+ // if no route is found, return 404
89
+ const response = await http.notFound();
90
+
91
+ logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
92
+ return Promise.resolve(http.notFound());
166
93
 
167
- lgr.info(statusCode, url.pathname, req.method, httpStatusCodes[statusCode]);
168
- return Promise.resolve(http.message(statusCode, httpStatusCodes[statusCode]));
169
94
  }
170
95
  });
171
96
  },
172
97
  }
173
98
  }
174
99
 
175
-
176
- export { router, extract, http }
100
+ export { Router, http }
@@ -0,0 +1,63 @@
1
+ import { HttpHandler, Context, Route } from "./router.d";
2
+ import { http } from "../http/http";
3
+ import { createContext } from './context';
4
+
5
+ const splitPath = (s: string): string[] => s.split('/').filter(x => x !== '');
6
+
7
+ const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
8
+ const route: Route = {
9
+ children: new Map(),
10
+ path: path,
11
+ dynamicPath: '',
12
+ method: method,
13
+ handler: handler,
14
+ isLast: false
15
+ };
16
+
17
+ return route;
18
+ };
19
+
20
+ const RouteTree = () => {
21
+ let root = createRoute('', 'GET', () => http.notFound());
22
+
23
+ const addRoute = (path: string, method: string, handler: HttpHandler) => {
24
+ const pathParts = splitPath(path);
25
+ let current = root;
26
+
27
+ for (let i = 0; i < pathParts.length; i++) {
28
+ const part = pathParts[i];
29
+ if (part.startsWith(':')) {
30
+ current.dynamicPath = part;
31
+ }
32
+ if (!current.children.has(part)) {
33
+ current.children.set(part, createRoute(part, method, handler));
34
+ }
35
+ current = current.children.get(part)!;
36
+ }
37
+
38
+ current.handler = handler;
39
+ current.isLast = true;
40
+ current.path = path;
41
+ };
42
+
43
+ const findRoute = (path: string): Route | undefined => {
44
+ const pathParts = splitPath(path);
45
+ let current = root;
46
+ for (let i = 0; i < pathParts.length; i++) {
47
+ const part = pathParts[i];
48
+ if (current.children.has(part)) {
49
+ current = current.children.get(part)!;
50
+ } else if (current.dynamicPath) {
51
+ current = current.children.get(current.dynamicPath)!;
52
+ } else {
53
+ return;
54
+ }
55
+ }
56
+ return current;
57
+ }
58
+
59
+ return { addRoute, findRoute }
60
+
61
+ };
62
+
63
+ export { RouteTree, createContext }
@@ -0,0 +1,3 @@
1
+ const splitPath = (path: string): string[] => path.split('/').filter(Boolean);
2
+
3
+ export { splitPath }
package/package.json CHANGED
@@ -8,5 +8,5 @@
8
8
  "peerDependencies": {
9
9
  "typescript": "^5.0.0"
10
10
  },
11
- "version": "0.6.0"
11
+ "version": "0.7.1"
12
12
  }
@@ -1,68 +1,4 @@
1
1
  import { describe, test, expect } from 'bun:test';
2
- import { extract } from '..';
3
- import { Context, Route } from '../lib/router/router.d';
4
-
5
- describe('URL Params', () => {
6
- test('/user/:name', () => {
7
- const route: Route = {
8
- pattern: '/user/:name',
9
- method: 'GET',
10
- callback: () => new Response('ok'),
11
- };
12
-
13
- const ctx: Context = {
14
- request: new Request('http://localhost:3000/user/foo'),
15
- params: new Map(),
16
- };
17
-
18
- const extractor = extract(route, ctx);
19
-
20
- extractor?.params();
21
-
22
- const name = ctx.params.get('name');
23
- expect(name).toBe('foo');
24
- });
25
-
26
- test('/user/:name/:id', () => {
27
- const route: Route = {
28
- pattern: '/user/:name/:id',
29
- method: 'GET',
30
- callback: () => new Response('ok'),
31
- };
32
-
33
- const ctx: Context = {
34
- request: new Request('http://localhost:3000/user/foo/123'),
35
- params: new Map(),
36
- };
37
-
38
- const extractor = extract(route, ctx);
39
-
40
- extractor?.params();
41
-
42
- const name = ctx.params.get('name');
43
- const id = ctx.params.get('id');
44
-
45
- expect(name).toBe('foo');
46
- expect(id).toBe('123');
47
- });
48
-
49
- test('/foo', () => {
50
- const route: Route = {
51
- pattern: '/foo',
52
- method: 'GET',
53
- callback: () => new Response('ok'),
54
- }
55
-
56
- const ctx: Context = {
57
- request: new Request('http://localhost:3000/foo'),
58
- params: new Map(),
59
- }
60
-
61
- const url = new URL(ctx.request.url);
62
-
63
- expect(url.pathname).toBe(route.pattern);
64
- });
65
- });
66
2
 
67
3
  describe('Router', () => {
68
4
  test('Serve', async () => {
@@ -1,15 +0,0 @@
1
- import { router } from '..';
2
-
3
- const r = router();
4
-
5
- r.add('/set-cookie', 'GET', ctx => {
6
- ctx.cookies.set('domain', 'localhost');
7
- ctx.cookies.set('path', '/set-cookie');
8
-
9
- ctx.logger.message(ctx.token ?? 'no token provided');
10
-
11
- return ctx.json( 200, {message: 'cookie stored'});
12
- });
13
-
14
-
15
- r.serve();
@@ -1,22 +0,0 @@
1
- import { router, http } 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 http.json(200, {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 ? http.json(200, d) : new Response('not found', {status: 404});
20
- });
21
-
22
- r.serve();