bun-router 0.6.0 → 0.7.1

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
@@ -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();