bun-router 0.7.4-experimental.9 → 0.8.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/.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
  }