bun-router 0.7.3 → 0.7.4-experimental.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,66 +1,66 @@
1
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
- };
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
65
 
66
- export { httpStatusCodes }
66
+ export { httpStatusCodes };
@@ -1,36 +1,36 @@
1
1
  const Colors: Record<string,string> = {
2
- reset: "\x1b[0m",
2
+ reset: '\x1b[0m',
3
3
 
4
- // foreground
5
- black: "\x1b[30m",
6
- red: "\x1b[31m",
7
- green: "\x1b[32m",
8
- yellow: "\x1b[33m",
9
- blue: "\x1b[34m",
10
- magenta: "\x1b[35m",
11
- cyan: "\x1b[36m",
12
- white: "\x1b[37m",
4
+ // foreground
5
+ black: '\x1b[30m',
6
+ red: '\x1b[31m',
7
+ green: '\x1b[32m',
8
+ yellow: '\x1b[33m',
9
+ blue: '\x1b[34m',
10
+ magenta: '\x1b[35m',
11
+ cyan: '\x1b[36m',
12
+ white: '\x1b[37m',
13
13
 
14
- // background
15
- bgBlack: "\x1b[40m",
16
- bgRed: "\x1b[41m",
17
- bgGreen: "\x1b[42m",
18
- bgYellow: "\x1b[43m",
19
- bgBlue: "\x1b[44m",
20
- bgMagenta: "\x1b[45m",
21
- bgCyan: "\x1b[46m",
22
- bgWhite: "\x1b[47m",
23
- } as const;
14
+ // background
15
+ bgBlack: '\x1b[40m',
16
+ bgRed: '\x1b[41m',
17
+ bgGreen: '\x1b[42m',
18
+ bgYellow: '\x1b[43m',
19
+ bgBlue: '\x1b[44m',
20
+ bgMagenta: '\x1b[45m',
21
+ bgCyan: '\x1b[46m',
22
+ bgWhite: '\x1b[47m',
23
+ } as const;
24
24
 
25
25
 
26
26
 
27
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}`;
28
+ const _foreground = Colors[foreground];
29
+ const _background = Colors[background];
30
+ const reset = Colors.reset;
31
+ return `${_foreground}${_background}${message}${reset}`;
32
32
  }
33
33
 
34
34
 
35
35
 
36
- export { color }
36
+ export { color };
@@ -5,4 +5,4 @@ type BunLogger = {
5
5
  message: (message: string) => void,
6
6
  }
7
7
 
8
- export { BunLogger }
8
+ export { BunLogger };
@@ -1,5 +1,4 @@
1
1
  import { color } from './color';
2
- import { BunLogger } from './logger.d';
3
2
 
4
3
 
5
4
  const TITLE = `
@@ -8,87 +7,104 @@ _ _
8
7
  | . | | | | | _| . | | | _| -_| _|
9
8
  |___|___|_|_| |_| |___|___|_| |___|_|
10
9
 
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`
31
-
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`;
49
-
50
- await Bun.write(Bun.stdout, message);
51
- },
52
- }
53
- }
10
+ `;
11
+ const VERSION = '0.7.4-experimental.11';
12
+ const Logger = (enableFileLogging: boolean) => {
13
+ const file = Bun.file('bun-router.log');
14
+ const writer = enableFileLogging ? file.writer() : null;
15
+
16
+ function stripAnsi(str: string) {
17
+ const ansiRegex = /\u001B\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]/g;
18
+ return str.replace(ansiRegex, '');
19
+ }
20
+
21
+ async function write(message: string) {
22
+ await Bun.write(Bun.stdout, message);
23
+ if (writer) {
24
+ writer.write(stripAnsi(message));
25
+ writer.flush();
26
+ }
27
+ }
28
+
29
+ return {
30
+ info: async (statusCode: number, routePath: string, method: string, message?: string) => {
31
+ const { stamp } = timestamp((new Date(Date.now())));
32
+ const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
33
+ const rp = color('white', 'bgBlack', routePath);
34
+
35
+ message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${ message ?? ''}\n`;
36
+
37
+ await write(message);
38
+
39
+ },
40
+ error: async (statusCode: number, routePath: string, method: string, error: Error) => {
41
+ const { stamp } = timestamp((new Date(Date.now())));
42
+ const source = color('black', 'bgRed', `[error ${stamp}]`);
43
+ const rp = color('white', 'bgBlack', routePath);
44
+
45
+ const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' -> ' : ' <-'} ${error.message}\n`;
46
+
47
+ await write(message);
48
+ },
49
+ warn: async (message: string) => {
50
+ const { stamp } = timestamp((new Date(Date.now())));
51
+ const source = color('black', 'bgYellow', `[warning ${stamp}]`);
52
+ const messageColor = color('yellow', 'bgBlack', message);
53
+
54
+ message = `${source} : ${messageColor}\n`;
55
+
56
+ await write(message);
57
+ },
58
+ message: async (message: string) => {
59
+ const { stamp } = timestamp((new Date(Date.now())));
60
+ const source = color('black', 'bgCyan', `[message ${stamp}]`);
61
+ const messageColor = color('yellow', 'bgBlack', message);
62
+
63
+ message = `${source}: ${messageColor}\n`;
64
+
65
+ await write(message);
66
+ },
67
+
68
+ };
69
+ };
54
70
 
55
71
  function timestamp(date: Date) {
56
- const month = pad(date.getMonth());
57
- const day = pad(date.getDate());
58
- const hour = pad(date.getHours());
59
- const minute = pad(date.getMinutes());
60
- const seconds = pad(date.getSeconds());
61
- const stamp = `${hour}:${minute}:${seconds}`;
62
-
63
- return {month, day, hour, minute, stamp};
72
+ const month = pad(date.getMonth());
73
+ const day = pad(date.getDate());
74
+ const hour = pad(date.getHours());
75
+ const minute = pad(date.getMinutes());
76
+ const seconds = pad(date.getSeconds());
77
+ const stamp = `${hour}:${minute}:${seconds}`;
78
+
79
+ return {month, day, hour, minute, stamp};
64
80
  }
65
81
 
66
82
  function setColor(n: number, text?: string){
67
- const s = ` [${String(n)}${text ?? ''}] `;
83
+ const s = ` [${String(n)}${text ?? ''}] `;
68
84
 
69
- if (n < 100) return color('black', 'bgYellow', s);
70
- else if (n >= 100 && n < 200) return color('black', 'bgCyan', s);
71
- else if (n >= 200 && n < 300) return color('black', 'bgGreen', s);
72
- else if (n >= 300 && n < 400) return color('black', 'bgRed', s);
73
- else if (n >= 400 && n < 500) return color('black', 'bgRed', s);
74
- else if (n >= 500) return color('white', 'bgRed', s);
85
+ if (n < 100) return color('black', 'bgYellow', s);
86
+ else if (n >= 100 && n < 200) return color('black', 'bgCyan', s);
87
+ else if (n >= 200 && n < 300) return color('black', 'bgGreen', s);
88
+ else if (n >= 300 && n < 400) return color('black', 'bgRed', s);
89
+ else if (n >= 400 && n < 500) return color('black', 'bgRed', s);
90
+ else if (n >= 500) return color('white', 'bgRed', s);
75
91
 
76
- return color('white', 'bgBlack', `[${s}]`).trim();
92
+ return color('white', 'bgBlack', `[${s}]`).trim();
77
93
  }
78
94
 
79
95
  function startMessage(port: number | string) {
80
- const { stamp } = timestamp((new Date(Date.now())));
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`);
85
-
86
- Bun.write(Bun.stdout, TITLE + '\n' + version);
87
- Bun.write(Bun.stdout, msg);
96
+ const { stamp } = timestamp((new Date(Date.now())));
97
+ const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
98
+ const portColor = color('green', 'bgBlack', String(port));
99
+ const msg = `${source}: Starting Server on :${portColor}\n`;
100
+ const version = color('red', 'bgBlack', `v${VERSION}\n`);
101
+
102
+ Bun.write(Bun.stdout, TITLE + '\n' + version);
103
+ Bun.write(Bun.stdout, msg);
88
104
  }
89
105
 
90
106
  function pad(n: number) {
91
- return String(n).padStart(2, '0');
107
+ return String(n).padStart(2, '0');
92
108
  }
93
109
 
94
- export { Logger, startMessage }
110
+ export { Logger, startMessage };
@@ -1,51 +1,64 @@
1
- import { Route, Context } from "./router.d";
2
- import { Logger } from "../..";
3
- import { http } from "./router";
4
-
5
- async function createContext(path: string, route: Route, request: Request): Promise<Context> {
6
- const params = extractParams(path, route);
7
- const query = new URLSearchParams(path);
8
- const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
9
-
10
- return Promise.resolve({
11
- params,
12
- request,
13
- query,
14
- formData,
15
- logger: Logger(),
16
- json: (statusCode: number, data: any) => http.json(statusCode, data),
17
- });
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Route, Context } from './router.d';
3
+ import { renderToReadableStream } from 'react-dom/server';
4
+ import { Logger } from '../logger/logger';
5
+ import { http } from './router';
6
+ import { ReactNode } from 'react';
7
+
8
+ // createContext creates a context object
9
+ async function createContext(path: string, route: Route, request: Request, enableFileLogging: boolean): Promise<Context> {
10
+ const query = new URLSearchParams(path);
11
+ const params = extractParams(path, route);
12
+ const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
13
+
14
+ return Promise.resolve({
15
+ params,
16
+ request,
17
+ query,
18
+ formData,
19
+ logger: Logger(enableFileLogging),
20
+ json: (statusCode: number, data: any) => http.json(statusCode, data),
21
+ render: async (component: ReactNode) => await renderStream(component),
22
+ });
18
23
  }
19
24
 
20
- function extractParams(path: string, route: Route): Map<string, string> {
21
- const params: Map<string, string> = new Map();
22
- const pathSegments = path.split('/');
23
- const routeSegments = route.path.split('/');
25
+ // extractParams extracts the parameters from the path
26
+ // and returns a map of key/value pairs
27
+ // e.g. /users/:id => /users/123 => { id: 123 }
28
+ function extractParams(pattern: string, route: Route): Map<string, string> {
29
+ const params: Map<string, string> = new Map();
30
+ const pathSegments = pattern.split('/');
31
+ const routeSegments = route.path.split('/');
24
32
 
25
- if (pathSegments.length !== routeSegments.length) return params;
33
+ if (pathSegments.length !== routeSegments.length) return params;
26
34
 
27
- for (let i = 0; i < pathSegments.length; i++) {
28
- if (routeSegments[i][0] === ':') {
29
- const key = routeSegments[i].replace(':', '');
30
- const value = pathSegments[i];
31
- params.set(key, value);
32
- }
33
- }
35
+ for (let i = 0; i < pathSegments.length; i++) {
36
+ if (routeSegments[i][0] === ':') {
37
+ const key = routeSegments[i].slice(1);
38
+ const value = pathSegments[i];
39
+ params.set(key, value);
40
+ }
41
+ }
34
42
 
35
- return params;
43
+ return params;
36
44
  }
37
45
 
46
+ // getContentType returns the content type from the headers
38
47
  function getContentType(headers: Headers): string {
39
- const contentType = headers.get('Content-Type');
40
- if (!contentType) return '';
41
- return contentType;
48
+ const contentType = headers.get('Content-Type');
49
+ return contentType ?? '';
42
50
  }
43
51
 
52
+ // isMultiPartForm returns true if the content type is multipart/form-data
44
53
  function isMultiPartForm(headers: Headers): boolean {
45
- const contentType = getContentType(headers);
46
- return contentType.includes('multipart/form-data');
54
+ const contentType = getContentType(headers);
55
+ return contentType.includes('multipart/form-data');
47
56
  }
48
57
 
58
+ // renderStream renders the component to a readable stream
59
+ async function renderStream(children: ReactNode) {
60
+ const stream = await renderToReadableStream(children);
61
+ return new Response(stream, { headers: { 'Content-Type': 'text/html' } });
62
+ }
49
63
 
50
-
51
- export { createContext }
64
+ export { createContext };
@@ -0,0 +1,86 @@
1
+ import { HttpHandler, Route } from './router.d';
2
+ import { http } from '../http/http';
3
+ import { createContext } from './context';
4
+
5
+ const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
6
+ const route: Route = {
7
+ children: new Map(),
8
+ path: path,
9
+ dynamicPath: '',
10
+ method: method,
11
+ handler: handler,
12
+ isLast: false
13
+ };
14
+
15
+ return route;
16
+ };
17
+
18
+ const RouteTree = () => {
19
+ const root = createRoute('', 'GET', () => http.notFound());
20
+
21
+ function addRoute (pattern: string, method: string, handler: HttpHandler){
22
+ console.log(pattern);
23
+ const pathParts = pattern.split('/');
24
+ let current = root;
25
+
26
+ for (let i = 0; i < pathParts.length; i++) {
27
+ const part = pathParts[i];
28
+ if (part.startsWith(':')) {
29
+ current.dynamicPath = part;
30
+ }
31
+ if (!current.children.has(part)) {
32
+ current.children.set(part, createRoute(part, method, handler));
33
+ }
34
+ current = current.children.get(part)!;
35
+ }
36
+
37
+ current.handler = handler;
38
+ current.isLast = true;
39
+ current.path = pattern;
40
+ }
41
+
42
+ function findRoute(pathname: string): Route | undefined {
43
+ const pathParts = pathname.split('/');
44
+ let current = root;
45
+ for (let i = 0; i < pathParts.length; i++) {
46
+ const part = pathParts[i];
47
+ if (current.children.has(part)) {
48
+ current = current.children.get(part)!;
49
+ } else if (current.dynamicPath) {
50
+ current = current.children.get(current.dynamicPath)!;
51
+ } else {
52
+ return;
53
+ }
54
+ }
55
+ return current;
56
+ }
57
+
58
+ function size() {
59
+ let count = 0;
60
+ function traverse(route: Route) {
61
+ count++;
62
+ for (const child of route.children.values()) {
63
+ traverse(child);
64
+ }
65
+ }
66
+ traverse(root);
67
+ return count;
68
+ }
69
+
70
+ function list() {
71
+ const routes: Route[] = [];
72
+ function traverse(route: Route) {
73
+ routes.push(route);
74
+ for (const child of route.children.values()) {
75
+ traverse(child);
76
+ }
77
+ }
78
+ traverse(root);
79
+ return routes;
80
+ }
81
+
82
+ return { addRoute, findRoute, size, list };
83
+
84
+ };
85
+
86
+ export { RouteTree, createContext };
@@ -1,4 +1,5 @@
1
- import { TLSOptions, TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptions, TLSServeOptions } from 'bun';
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptions, TLSServeOptions } from 'bun';
2
3
  import { Logger } from '../logger/logger';
3
4
  import { Database } from 'bun:sqlite';
4
5
 
@@ -20,7 +21,6 @@ type Route = {
20
21
  handler: HttpHandler;
21
22
  isLast: boolean;
22
23
  }
23
-
24
24
  type Context = {
25
25
  db?: Database;
26
26
  formData: FormData | Promise<FormData>;
@@ -29,12 +29,14 @@ type Context = {
29
29
  params: Map<string, string>;
30
30
  query: URLSearchParams;
31
31
  request: Request;
32
+ render: (component: React.ReactNode) => Response | Promise<Response>;
32
33
  };
33
34
 
34
35
  type HttpHandler = (ctx: Context) => Response | Promise<Response>
35
36
 
36
37
  type Options = {
37
- db: string,
38
+ db: string;
39
+ enableFileLogging: boolean;
38
40
  }
39
41
 
40
42
  type RouterOptions<Options> = ServeOptions
@@ -43,4 +45,4 @@ type RouterOptions<Options> = ServeOptions
43
45
  | TLSWebSocketServeOptions<Options>
44
46
  | undefined
45
47
 
46
- export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler }
48
+ export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler };