bun-router 0.7.4-experimental.9 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
package/.eslintrc.json CHANGED
@@ -18,6 +18,7 @@
18
18
  "react"
19
19
  ],
20
20
  "rules": {
21
+ "no-eplicit-any": "off",
21
22
  "indent": [
22
23
  "warn",
23
24
  "tab"
@@ -29,6 +30,7 @@
29
30
  "semi": [
30
31
  "error",
31
32
  "always"
32
- ]
33
+ ],
34
+ "no-control-regex": "off"
33
35
  }
34
36
  }
package/bun.lockb CHANGED
Binary file
package/examples/basic.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Router, http } from '..';
1
+ import { Router, http } from '../index';
2
2
 
3
- const router = Router();
3
+ const router = Router(3000, {enableFileLogging: false});
4
4
 
5
5
  router.add('/', 'GET', () => http.json(200, 'ok'));
6
6
 
@@ -1,7 +1,8 @@
1
- import { Router } from '../../';
1
+ import { Router, http } from '../../';
2
2
 
3
3
  const router = Router();
4
4
 
5
+ router.add('/', 'GET', () => http.ok());
5
6
  router.static('/page', './examples/ssr/pages');
6
7
 
7
8
  router.serve();
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+
3
+ export function User(username: string) {
4
+ return (
5
+ <h1>{ username }</h1>
6
+ );
7
+ }
@@ -0,0 +1,20 @@
1
+ import { Router, http } from '../../';
2
+ import { User } from './components/user';
3
+
4
+ const router = Router();
5
+
6
+ router.get('/', () => {
7
+ return http.ok();
8
+ });
9
+
10
+
11
+ router.get('/u/:username', async ctx => {
12
+ const username = ctx.params.get('username');
13
+
14
+ if (!username) return http.message(400, 'invalid username');
15
+
16
+ return http.render(User(username));
17
+ });
18
+
19
+ router.serve();
20
+
package/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './lib/router/router';
2
2
  export * from './lib/fs/fsys';
3
3
  export * from './lib/logger/logger';
4
+ export * from './lib/router/router.d';
package/lib/http/http.ts CHANGED
@@ -3,6 +3,9 @@ import { httpStatusCodes } from './status';
3
3
  import { ReactNode } from 'react';
4
4
  import { renderToReadableStream } from 'react-dom/server';
5
5
 
6
+ // http is a collection of functions that return a Response
7
+ // object with the appropriate status code and content type
8
+ // e.g. http.ok() returns a 200 response
6
9
  const http = {
7
10
  ok: async (msg?: string): Promise<Response> => {
8
11
  return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
@@ -47,7 +50,11 @@ const http = {
47
50
  },
48
51
  render: async (component: ReactNode): Promise<Response> => {
49
52
  const stream = await renderToReadableStream(component);
50
- return new Response(stream, { status: 200, statusText: httpStatusCodes[200]});
53
+ return new Response(stream, {
54
+ status: 200,
55
+ statusText: httpStatusCodes[200],
56
+ headers: {'Content-Type': 'text/html; charset=utf-8'}
57
+ });
51
58
  },
52
59
  noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
53
60
  status: 204,
@@ -23,12 +23,12 @@ const Colors: Record<string,string> = {
23
23
  } as const;
24
24
 
25
25
 
26
-
27
- function color(foreground: string, background: string, message: string) {
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ function color(foreground: string, background: string, ...args: any[]) {
28
28
  const _foreground = Colors[foreground];
29
29
  const _background = Colors[background];
30
30
  const reset = Colors.reset;
31
- return `${_foreground}${_background}${message}${reset}`;
31
+ return `${_foreground}${_background}${args.map(String).join('')}${reset}`;
32
32
  }
33
33
 
34
34
 
@@ -1,8 +1,9 @@
1
1
  type BunLogger = {
2
- info: (statusCode: number, routePath: string, method: string, message?: string) => void,
3
- error: (statusCode: number, routePath: string, method: string, error: Error) => void,
4
- warn: (message: string) => void,
5
- message: (message: string) => void,
2
+ info: (statusCode: number, routePath: string, method: string, ...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
3
+
4
+ error: (statusCode: number, routePath: string, method: string, error: Error) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
5
+ warn: (...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
6
+ message: (...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
6
7
  }
7
8
 
8
9
  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 = `
@@ -9,17 +8,34 @@ _ _
9
8
  |___|___|_|_| |_| |___|___|_| |___|_|
10
9
 
11
10
  `;
12
- const VERSION = '0.7.4-experimental.9';
13
- const Logger = (): BunLogger => {
11
+ const VERSION = '0.8.1';
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
+
14
29
  return {
15
- info: async (statusCode: number, routePath: string, method: string, message?: string) => {
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ info: async (statusCode: number, routePath: string, method: string, ...args: any[]) => {
16
32
  const { stamp } = timestamp((new Date(Date.now())));
17
33
  const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
18
34
  const rp = color('white', 'bgBlack', routePath);
19
35
 
20
- message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${ message ?? ''}\n`;
36
+ const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${args.map(String).join(' ')}\n'}`;
21
37
 
22
- await Bun.write(Bun.stdout, message);
38
+ await write(message);
23
39
 
24
40
  },
25
41
  error: async (statusCode: number, routePath: string, method: string, error: Error) => {
@@ -29,7 +45,7 @@ const Logger = (): BunLogger => {
29
45
 
30
46
  const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' -> ' : ' <-'} ${error.message}\n`;
31
47
 
32
- await Bun.write(Bun.stdout, message);
48
+ await write(message);
33
49
  },
34
50
  warn: async (message: string) => {
35
51
  const { stamp } = timestamp((new Date(Date.now())));
@@ -38,7 +54,7 @@ const Logger = (): BunLogger => {
38
54
 
39
55
  message = `${source} : ${messageColor}\n`;
40
56
 
41
- await Bun.write(Bun.stdout, message);
57
+ await write(message);
42
58
  },
43
59
  message: async (message: string) => {
44
60
  const { stamp } = timestamp((new Date(Date.now())));
@@ -47,8 +63,18 @@ const Logger = (): BunLogger => {
47
63
 
48
64
  message = `${source}: ${messageColor}\n`;
49
65
 
50
- await Bun.write(Bun.stdout, message);
66
+ await write(message);
51
67
  },
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ log: async(args: any[]) => {
70
+ const { stamp } = timestamp((new Date(Date.now())));
71
+ const source = color('black', 'bgCyan', `[message ${stamp}]`);
72
+ const messageColor = color('yellow', 'bgBlack', args);
73
+
74
+ const message = `${source}: ${messageColor}\n`;
75
+
76
+ await write(message);
77
+ }
52
78
  };
53
79
  };
54
80
 
@@ -5,7 +5,8 @@ import { Logger } from '../logger/logger';
5
5
  import { http } from './router';
6
6
  import { ReactNode } from 'react';
7
7
 
8
- async function createContext(path: string, route: Route, request: Request): Promise<Context> {
8
+ // createContext creates a context object
9
+ async function createContext(path: string, route: Route, request: Request, enableFileLogging: boolean): Promise<Context> {
9
10
  const query = new URLSearchParams(path);
10
11
  const params = extractParams(path, route);
11
12
  const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
@@ -15,12 +16,15 @@ async function createContext(path: string, route: Route, request: Request): Prom
15
16
  request,
16
17
  query,
17
18
  formData,
18
- logger: Logger(),
19
+ logger: Logger(enableFileLogging),
19
20
  json: (statusCode: number, data: any) => http.json(statusCode, data),
20
21
  render: async (component: ReactNode) => await renderStream(component),
21
22
  });
22
23
  }
23
24
 
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 }
24
28
  function extractParams(pattern: string, route: Route): Map<string, string> {
25
29
  const params: Map<string, string> = new Map();
26
30
  const pathSegments = pattern.split('/');
@@ -39,16 +43,19 @@ function extractParams(pattern: string, route: Route): Map<string, string> {
39
43
  return params;
40
44
  }
41
45
 
46
+ // getContentType returns the content type from the headers
42
47
  function getContentType(headers: Headers): string {
43
48
  const contentType = headers.get('Content-Type');
44
49
  return contentType ?? '';
45
50
  }
46
51
 
52
+ // isMultiPartForm returns true if the content type is multipart/form-data
47
53
  function isMultiPartForm(headers: Headers): boolean {
48
54
  const contentType = getContentType(headers);
49
55
  return contentType.includes('multipart/form-data');
50
56
  }
51
57
 
58
+ // renderStream renders the component to a readable stream
52
59
  async function renderStream(children: ReactNode) {
53
60
  const stream = await renderToReadableStream(children);
54
61
  return new Response(stream, { headers: { 'Content-Type': 'text/html' } });
@@ -19,6 +19,7 @@ const RouteTree = () => {
19
19
  const root = createRoute('', 'GET', () => http.notFound());
20
20
 
21
21
  function addRoute (pattern: string, method: string, handler: HttpHandler){
22
+ console.log(pattern);
22
23
  const pathParts = pattern.split('/');
23
24
  let current = root;
24
25
 
@@ -35,7 +35,8 @@ type Context = {
35
35
  type HttpHandler = (ctx: Context) => Response | Promise<Response>
36
36
 
37
37
  type Options = {
38
- db: string,
38
+ db: string;
39
+ enableFileLogging: boolean;
39
40
  }
40
41
 
41
42
  type RouterOptions<Options> = ServeOptions
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import { Database } from 'bun:sqlite';
3
- import { Route, BunRouter, RouterOptions, Options, HttpHandler } from './router.d';
3
+ import { Route, BunRouter, RouterOptions, Options } from './router.d';
4
4
  import { httpStatusCodes } from '../http/status';
5
5
  import { readDir } from '../fs/fsys';
6
6
  import { Logger, startMessage } from '../logger/logger';
@@ -9,14 +9,16 @@ import { RouteTree } from './routeTree';
9
9
  import { createContext } from './context';
10
10
 
11
11
  const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
12
- const { addRoute, findRoute, list } = RouteTree();
13
- const logger = Logger();
12
+ const { addRoute, findRoute } = RouteTree();
13
+ const logger = Logger(options?.enableFileLogging ?? false);
14
14
 
15
+ // load a component from the root directory relative to the cwd
15
16
  async function loadComponent(root: string, name: string) {
16
17
  const module = await import(path.join(process.cwd(), root, name));
17
18
  return module.default;
18
19
  }
19
20
 
21
+ // extract the path, extension, and base name from a file path
20
22
  function extractPathExtBase(pattern: string, pathname: string) {
21
23
  const extension = path.extname(pathname);
22
24
  let base = encodeURIComponent(path.basename(pathname));
@@ -30,6 +32,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
30
32
  return { patternPath, extension, base };
31
33
  }
32
34
 
35
+ // check if a file exists
33
36
  async function exists(fp: string) {
34
37
  const f = Bun.file(fp);
35
38
  return await f.exists();
@@ -38,7 +41,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
38
41
  return {
39
42
  // add a route to the router tree
40
43
  add: (pattern, method, callback) => { addRoute(pattern, method, callback); },
41
- get: (pattern: string, callback: HttpHandler) => { addRoute(pattern, 'GET', callback); },
44
+ get: (pattern, callback) => { addRoute(pattern, 'GET', callback); },
42
45
  post: (pattern, callback) => { addRoute(pattern, 'POST', callback); },
43
46
  put: (pattern, callback) => { addRoute(pattern, 'PUT', callback);},
44
47
  delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback); },
@@ -51,10 +54,9 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
51
54
  if (!exists(root)) return console.error(`Directory not found: ${root}`);
52
55
  await readDir(root, async (fp) => {
53
56
  const { patternPath, extension, base } = extractPathExtBase(pattern, fp);
54
-
55
57
  const route: Route = {
56
58
  children: new Map(),
57
- dynamicPath: '',
59
+ dynamicPath: pattern,
58
60
  isLast: true,
59
61
  path: patternPath.startsWith('//') ? patternPath.slice(1) : patternPath, // remove the leading '/' if it exists
60
62
  method: 'GET',
@@ -70,14 +72,12 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
70
72
 
71
73
  addRoute(route.path, 'GET', route.handler);
72
74
  });
73
-
74
- console.log(list());
75
75
 
76
76
  },
77
- // start the server
77
+ // start listening for requests
78
78
  serve: () => {
79
79
  startMessage(port ?? 3000);
80
- const opts: Options = { db: ':memory:' };
80
+ const opts: Options = { db: ':memory:', enableFileLogging: false };
81
81
 
82
82
  Bun.serve({
83
83
  port: port ?? 3000,
@@ -90,6 +90,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
90
90
  if (options) {
91
91
  const o = options as Options;
92
92
  opts.db = o.db;
93
+ opts.enableFileLogging = o.enableFileLogging;
93
94
  }
94
95
 
95
96
  const route = findRoute(pathname);
@@ -101,9 +102,11 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
101
102
  return Promise.resolve(http.methodNotAllowed());
102
103
  }
103
104
 
104
- const context = await createContext(pathname, route, req);
105
+ // create a context for the handler
106
+ const context = await createContext(pathname, route, req, opts.enableFileLogging);
105
107
  context.db = new Database(opts.db);
106
108
 
109
+ // call the handler
107
110
  const response = await route.handler(context);
108
111
 
109
112
  logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
@@ -116,6 +119,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
116
119
  logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
117
120
  return Promise.resolve(http.notFound());
118
121
  },
122
+ // if an error occurs, return a 500 response
119
123
  error(error) {
120
124
  return new Response(`<pre>${error}\n${error.stack}</pre>`, {
121
125
  headers: { 'Content-Type': 'text/html' },
package/package.json CHANGED
@@ -3,21 +3,22 @@
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
5
  "devDependencies": {
6
- "@types/react-dom": "^18.2.7",
6
+ "@types/react": "^18.2.23",
7
+ "@types/react-dom": "^18.2.8",
7
8
  "@typescript-eslint/eslint-plugin": "^6.7.0",
8
9
  "@typescript-eslint/parser": "^6.7.0",
9
10
  "bun-types": "latest",
10
11
  "eslint": "^8.49.0",
11
- "eslint-plugin-react": "^7.33.2",
12
- "react-dom": "^18.2.0"
12
+ "eslint-plugin-react": "^7.33.2"
13
13
  },
14
14
  "peerDependencies": {
15
15
  "typescript": "^5.0.0"
16
16
  },
17
- "version": "0.7.4-experimental.9",
17
+ "version": "0.8.1",
18
18
  "dependencies": {
19
- "@types/react": "^18.2.22",
20
19
  "eslint-plugin-react-hooks": "^4.6.0",
21
- "react": "^18.2.0"
22
- }
23
- }
20
+ "react": "^18.2.0",
21
+ "react-dom": "^18.2.0"
22
+ },
23
+ "types": "./lib/router.d.ts"
24
+ }
package/tsconfig.json CHANGED
@@ -15,10 +15,9 @@
15
15
  "forceConsistentCasingInFileNames": true,
16
16
  "allowJs": true,
17
17
  "noEmit": true,
18
+ "noImplicitAny": false,
18
19
  "types": [
19
20
  "bun-types" // add Bun global
20
- ],
21
- "paths": {
22
- }
21
+ ]
23
22
  },
24
23
  }