bun-router 0.7.3 → 0.7.4-experimental.2

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 ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "env": {
3
+ "browser": true,
4
+ "es2021": true
5
+ },
6
+ "extends": [
7
+ "eslint:recommended",
8
+ "plugin:@typescript-eslint/recommended",
9
+ "plugin:react/recommended"
10
+ ],
11
+ "parser": "@typescript-eslint/parser",
12
+ "parserOptions": {
13
+ "ecmaVersion": "latest",
14
+ "sourceType": "module"
15
+ },
16
+ "plugins": [
17
+ "@typescript-eslint",
18
+ "react"
19
+ ],
20
+ "rules": {
21
+ "indent": [
22
+ "warn",
23
+ "tab"
24
+ ],
25
+ "quotes": [
26
+ "error",
27
+ "single"
28
+ ],
29
+ "semi": [
30
+ "error",
31
+ "always"
32
+ ]
33
+ }
34
+ }
package/README.md CHANGED
@@ -29,6 +29,10 @@ router.serve();
29
29
  ```
30
30
 
31
31
  ##### Static
32
+ Read a directory and serve it's contents. `.tsx` and `.html` files are rendered by default, everything else is served, including the extension.
33
+
34
+ Ex: `/assets/gopher.png` would serve a `.png` image. `/home` would be `.tsx` or `.html` depending on extension.
35
+
32
36
  ```ts
33
37
  import { Router } from 'bun-router';
34
38
 
@@ -53,4 +57,28 @@ router.post('/register', ctx => {
53
57
 
54
58
  ```
55
59
 
60
+ ##### JSX
61
+ ```tsx
62
+ // ./pages/home.tsx
63
+ export default const Home = (title: string) => {
64
+ return (
65
+ <main>
66
+ <h1>{ title }</h1>
67
+ </main>
68
+ );
69
+ };
70
+ ```
71
+
72
+ ```ts
73
+ // ./index.ts
74
+ import { Router } from 'bun-router';
75
+ import Home from './pages/home';
76
+
77
+ const router = Router();
78
+
79
+ router.get('/', ctx => ctx.render(Home('Hello World')))
80
+
81
+ router.serve();
82
+ ```
83
+
56
84
 
package/bun.lockb CHANGED
Binary file
package/examples/basic.ts CHANGED
@@ -5,9 +5,9 @@ const router = Router();
5
5
  router.add('/', 'GET', () => http.json(200, 'ok'));
6
6
 
7
7
  router.add('/user/:name', 'GET', (ctx) => {
8
- const name = ctx.params.get('name');
9
- if (!name) return http.json(500, 'no name');
10
- return http.json(200, name);
8
+ const name = ctx.params.get('name');
9
+ if (!name) return http.json(500, 'no name');
10
+ return http.json(200, name);
11
11
  });
12
12
 
13
13
  router.serve();
@@ -4,16 +4,16 @@ import { Context } from '../lib/router/router.d';
4
4
  const home = () => new Response('Welcome Home', { status: 200 });
5
5
 
6
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 });
10
- }
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 });
10
+ };
11
11
 
12
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 });
16
- }
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 });
16
+ };
17
17
 
18
18
  const r = Router();
19
19
 
@@ -3,9 +3,9 @@ import { Router, http } from '..';
3
3
  const r = Router();
4
4
 
5
5
  r.get('/', ctx => {
6
- ctx.logger.debug('hello from home');
6
+ ctx.logger.debug('hello from home');
7
7
 
8
- return http.ok();
8
+ return http.ok();
9
9
  });
10
10
 
11
11
  r.serve();
@@ -0,0 +1,7 @@
1
+ import { Router } from '../../';
2
+
3
+ const router = Router();
4
+
5
+ router.static('/', './examples/ssr/pages');
6
+
7
+ router.serve();
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+
3
+ export default function Foo() {
4
+ return (
5
+ <h1>Foo </h1>
6
+ );
7
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+
3
+ export default function Home() {
4
+ return (
5
+ <h1>Hello World</h1>
6
+ );
7
+ }
package/examples/todo.ts CHANGED
@@ -1,45 +1,45 @@
1
1
  import { Router, http } from '..';
2
2
 
3
3
  const Todo = () => {
4
- const list: Record<string, string> = {};
4
+ const list: Record<string, string> = {};
5
5
 
6
- return {
7
- add: (key: string, value: string) => { list[key] = value },
8
- get: (key: string) => list[key],
9
- remove: (key: string) => { delete list[key] },
10
- size: () => Object.entries(list).length,
11
- export: () => list,
12
- }
13
- }
6
+ return {
7
+ add: (key: string, value: string) => { list[key] = value; },
8
+ get: (key: string) => list[key],
9
+ remove: (key: string) => { delete list[key]; },
10
+ size: () => Object.entries(list).length,
11
+ export: () => list,
12
+ };
13
+ };
14
14
 
15
15
  const todo = Todo();
16
16
 
17
17
  const r = Router();
18
18
 
19
19
  r.add('/api/new', 'POST', ctx => {
20
- const query = new URL(ctx.request.url).searchParams;
21
- const key = query.get('key');
22
- const content = query.get('content');
20
+ const query = new URL(ctx.request.url).searchParams;
21
+ const key = query.get('key');
22
+ const content = query.get('content');
23
23
 
24
- if (!key || !content) return http.message(400, 'invalid query params');
25
- ctx.logger.message(`Adding ${key} with ${content}`);
26
- todo.add(key, content);
24
+ if (!key || !content) return http.message(400, 'invalid query params');
25
+ ctx.logger.message(`Adding ${key} with ${content}`);
26
+ todo.add(key, content);
27
27
 
28
- return ctx.json(200, { message: 'ok' });
28
+ return ctx.json(200, { message: 'ok' });
29
29
  });
30
30
 
31
31
  r.add('/api/todo/:key', 'GET', ctx => {
32
- const key = ctx.params.get('key');
33
- if (!key) return http.message(400, 'invalid params');
32
+ const key = ctx.params.get('key');
33
+ if (!key) return http.message(400, 'invalid params');
34
34
 
35
- const content = todo.get(key);
36
- if (!content) return http.notFound();
35
+ const content = todo.get(key);
36
+ if (!content) return http.notFound();
37
37
 
38
- return ctx.json(200, {key: key, content: content});
38
+ return ctx.json(200, {key: key, content: content});
39
39
  });
40
40
 
41
41
  r.add('/api/get/all', 'GET', ctx => {
42
- return ctx.json(200, todo.export());
42
+ return ctx.json(200, todo.export());
43
43
  });
44
44
 
45
45
  r.serve();
@@ -0,0 +1,76 @@
1
+ import { splitFilePath } from '../fs/fsys';
2
+ import path from 'node:path';
3
+
4
+ type File = {
5
+ name: string;
6
+ path: string;
7
+ extension: string;
8
+ children: Map<string, File>;
9
+ isLast: boolean;
10
+ };
11
+
12
+ function createFile(name: string): File {
13
+ return {
14
+ name,
15
+ path: '',
16
+ extension: '',
17
+ children: new Map(),
18
+ isLast: false,
19
+ };
20
+ }
21
+
22
+ const FileTree = (dir: string) => {
23
+ const root = createFile(dir);
24
+
25
+ const addFile = (_path: string) => {
26
+ const pathParts = splitFilePath(_path);
27
+ let current = root;
28
+
29
+ for (let i = 0; i < pathParts.length; i++) {
30
+ const part = pathParts[i];
31
+ if (!current.children.has(part)) {
32
+ current.children.set(part, createFile(part));
33
+ }
34
+ current = current.children.get(part)!;
35
+ }
36
+
37
+ current.isLast = true;
38
+ current.path = _path;
39
+ current.extension = path.extname(_path);
40
+ };
41
+
42
+ const getFilesByExtension = (extension: string): string[] => {
43
+ let current = root;
44
+ const files: string[] = [];
45
+
46
+ for (const [name, file] of current.children) {
47
+ if (file.extension === extension) {
48
+ files.push(file.path);
49
+ }
50
+ current = current.children.get(name)!;
51
+ }
52
+
53
+ return files;
54
+ };
55
+
56
+ const getFileByName = (path: string): string | undefined => {
57
+ let current = root;
58
+ const pathParts = splitFilePath(path);
59
+ for (let i = 0; i < pathParts.length; i++) {
60
+ const part = pathParts[i];
61
+ if (current.children.has(part)) {
62
+ current = current.children.get(part)!;
63
+ } else {
64
+ return;
65
+ }
66
+ }
67
+
68
+ if (!current.isLast) return;
69
+
70
+ return current.path;
71
+ };
72
+
73
+ return { addFile, getFilesByExtension, getFileByName };
74
+ };
75
+
76
+ export { FileTree };
package/lib/fs/fsys.ts CHANGED
@@ -1,25 +1,36 @@
1
- import { BunFile } from "bun";
1
+ import { BunFile } from 'bun';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'path';
4
4
 
5
5
  // check if the file path is a directory
6
6
  const isDir = async (fp: string): Promise<boolean> => (await fs.lstat(fp)).isDirectory();
7
7
 
8
+ // recursively read a directory and call a handler function on each file
8
9
  async function readDir(dirpath: string, handler: (filepath: string, entry: BunFile) => void) {
9
- const files = await fs.readdir(dirpath);
10
+ const files = await fs.readdir(dirpath);
10
11
 
11
- for (const file of files) {
12
- const bunFile = Bun.file(file);
12
+ for (const file of files) {
13
+ const bunFile = Bun.file(file);
13
14
 
14
- if (typeof bunFile.name === 'undefined') return
15
+ if (typeof bunFile.name === 'undefined') return;
15
16
 
16
- const fp = path.join(dirpath, bunFile.name);
17
- const isdir = await isDir(fp);
17
+ const fp = path.join(dirpath, bunFile.name);
18
+ const isdir = await isDir(fp);
18
19
 
19
- if (isdir) await readDir(fp, handler);
20
- else handler(fp, bunFile);
21
- }
20
+ if (isdir) await readDir(fp, handler);
21
+ else handler(fp, bunFile);
22
+ }
23
+ }
24
+
25
+ // get the extension of a file (unnecessary)
26
+ function ext(p: string): string {
27
+ return path.extname(p);
28
+ }
29
+
30
+ // split a file path into an array of strings (unnecessary)
31
+ function splitFilePath(p: string): string[] {
32
+ return p.split(path.sep);
22
33
  }
23
34
 
24
35
 
25
- export { readDir }
36
+ export { readDir, ext, splitFilePath };
package/lib/http/http.ts CHANGED
@@ -1,77 +1,84 @@
1
- import { httpStatusCodes } from "./status";
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { httpStatusCodes } from './status';
3
+ import { ReactNode } from 'react';
4
+ import { renderToReadableStream } from 'react-dom/server';
2
5
 
3
6
  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
- },
10
- json: async (statusCode: number, data: any): Promise<Response> => {
11
- const jsonString = JSON.stringify(data);
12
- return Promise.resolve(new Response(jsonString, {
13
- status: statusCode,
14
- statusText: httpStatusCodes[statusCode],
15
- headers: {'Content-Type': 'application/json'},
16
- }));
17
- },
18
- html: async (statusCode: number, content: string): Promise<Response> => {
19
- return Promise.resolve(new Response(content, {
20
- status: statusCode,
21
- statusText: httpStatusCodes[statusCode],
22
- headers: {'Content-Type': 'text/html; charset=utf-8'}
23
- }));
24
- },
25
- file: async (statusCode: number, fp: string): Promise<Response> => {
26
- const file = Bun.file(fp);
27
- const exists = await file.exists();
7
+ ok: async (msg?: string): Promise<Response> => {
8
+ return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
9
+ status: 200,
10
+ statusText: httpStatusCodes[200],
11
+ }));
12
+ },
13
+ json: async (statusCode: number, data: any): Promise<Response> => {
14
+ const jsonString = JSON.stringify(data);
15
+ return Promise.resolve(new Response(jsonString, {
16
+ status: statusCode,
17
+ statusText: httpStatusCodes[statusCode],
18
+ headers: {'Content-Type': 'application/json'},
19
+ }));
20
+ },
21
+ html: async (statusCode: number, content: string): Promise<Response> => {
22
+ return Promise.resolve(new Response(content, {
23
+ status: statusCode,
24
+ statusText: httpStatusCodes[statusCode],
25
+ headers: {'Content-Type': 'text/html; charset=utf-8'}
26
+ }));
27
+ },
28
+ file: async (statusCode: number, fp: string): Promise<Response> => {
29
+ const file = Bun.file(fp);
30
+ const exists = await file.exists();
28
31
 
29
- if (!exists) return http.notFound(`File not found: ${fp}`);
32
+ if (!exists) return http.notFound(`File not found: ${fp}`);
30
33
 
31
- const content = await file.arrayBuffer();
32
- if (!content) return http.noContent();
34
+ const content = await file.arrayBuffer();
35
+ if (!content) return http.noContent();
33
36
 
34
- let contentType = 'text/html; charset=utf-9';
37
+ let contentType = 'text/html; charset=utf-9';
35
38
 
36
- if (file.type.includes('image'))
37
- contentType = file.type + '; charset=utf-8';
39
+ if (file.type.includes('image'))
40
+ contentType = file.type + '; charset=utf-8';
38
41
 
39
- return Promise.resolve(new Response(content, {
40
- status: statusCode,
41
- statusText: httpStatusCodes[statusCode],
42
- headers: { 'Content-Type': contentType}
43
- }));
44
- },
45
- noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
46
- status: 204,
47
- statusText: 'no content',
48
- })),
49
- notFound: async(msg?: string): Promise<Response> => {
50
- const response = new Response(msg ?? 'not found', {
51
- status: 404,
52
- statusText: httpStatusCodes[404],
53
- headers: {'Content-Type': 'text/html'},
54
- });
42
+ return Promise.resolve(new Response(content, {
43
+ status: statusCode,
44
+ statusText: httpStatusCodes[statusCode],
45
+ headers: { 'Content-Type': contentType}
46
+ }));
47
+ },
48
+ render: async (component: ReactNode): Promise<Response> => {
49
+ const stream = await renderToReadableStream(component);
50
+ return new Response(stream, { status: 200, statusText: httpStatusCodes[200]});
51
+ },
52
+ noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
53
+ status: 204,
54
+ statusText: 'no content',
55
+ })),
56
+ notFound: async(msg?: string): Promise<Response> => {
57
+ const response = new Response(msg ?? 'not found', {
58
+ status: 404,
59
+ statusText: httpStatusCodes[404],
60
+ headers: {'Content-Type': 'text/html'},
61
+ });
55
62
 
56
- return Promise.resolve(response);
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
- });
63
+ return Promise.resolve(response);
64
+ },
65
+ methodNotAllowed: async (msg?: string): Promise<Response> => {
66
+ const response = new Response(msg ?? 'method not allowed', {
67
+ status: 405,
68
+ statusText: httpStatusCodes[405],
69
+ headers: {'Content-Type': 'text/html'},
70
+ });
64
71
 
65
- return Promise.resolve(response);
66
- },
67
- message: async (status: number, msg?: string): Promise<Response> => {
68
- const response = new Response(msg ?? '?', {
69
- status: status,
70
- statusText: httpStatusCodes[status],
71
- headers: {'Content-Type': 'text/html; charset-utf-8'},
72
- });
73
- return Promise.resolve(response)
74
- },
75
- }
72
+ return Promise.resolve(response);
73
+ },
74
+ message: async (status: number, msg?: string): Promise<Response> => {
75
+ const response = new Response(msg ?? '?', {
76
+ status: status,
77
+ statusText: httpStatusCodes[status],
78
+ headers: {'Content-Type': 'text/html; charset-utf-8'},
79
+ });
80
+ return Promise.resolve(response);
81
+ },
82
+ };
76
83
 
77
- export { http }
84
+ export { http };
@@ -8,87 +8,87 @@ _ _
8
8
  | . | | | | | _| . | | | _| -_| _|
9
9
  |___|___|_|_| |_| |___|___|_| |___|_|
10
10
 
11
- `
12
- const VERSION = '0.7.1';
11
+ `;
12
+ const VERSION = '0.7.4-experimental';
13
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);
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
19
 
20
- message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${method}${ message ?? ''}\n`
20
+ message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${ message ?? ''}\n`;
21
21
 
22
- await Bun.write(Bun.stdout, message);
22
+ await Bun.write(Bun.stdout, message);
23
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);
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
29
 
30
- const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? '->' : '<-'} ${error.message}\n`
30
+ const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' -> ' : ' <-'} ${error.message}\n`;
31
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);
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
38
 
39
- message = `${source} : ${messageColor}\n`;
39
+ message = `${source} : ${messageColor}\n`;
40
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);
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
47
 
48
- message = `${source}: ${messageColor}\n`;
48
+ message = `${source}: ${messageColor}\n`;
49
49
 
50
- await Bun.write(Bun.stdout, message);
51
- },
52
- }
53
- }
50
+ await Bun.write(Bun.stdout, message);
51
+ },
52
+ };
53
+ };
54
54
 
55
55
  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};
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};
64
64
  }
65
65
 
66
66
  function setColor(n: number, text?: string){
67
- const s = ` [${String(n)}${text ?? ''}] `;
67
+ const s = ` [${String(n)}${text ?? ''}] `;
68
68
 
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);
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);
75
75
 
76
- return color('white', 'bgBlack', `[${s}]`).trim();
76
+ return color('white', 'bgBlack', `[${s}]`).trim();
77
77
  }
78
78
 
79
79
  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);
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);
88
88
  }
89
89
 
90
90
  function pad(n: number) {
91
- return String(n).padStart(2, '0');
91
+ return String(n).padStart(2, '0');
92
92
  }
93
93
 
94
- export { Logger, startMessage }
94
+ export { Logger, startMessage };
@@ -0,0 +1,28 @@
1
+ import { FileTree } from '../fs/filetree';
2
+ import { readDir } from '../fs/fsys';
3
+ import { ComponentType } from 'react';
4
+
5
+ async function createFileTree(root: string) {
6
+ const tree = FileTree(root);
7
+
8
+ await readDir(root, (fp) => {
9
+ tree.addFile(fp);
10
+ });
11
+
12
+ return tree;
13
+ }
14
+
15
+ async function resolveModules(root: string) {
16
+ const tree = await createFileTree(root);
17
+ const files = tree.getFilesByExtension('.tsx');
18
+ const modules: ComponentType[] = [];
19
+
20
+ for (const file of files) {
21
+ const module = await import(file) as ComponentType;
22
+ modules.push(module);
23
+ }
24
+
25
+ return modules;
26
+ }
27
+
28
+ export { resolveModules };
@@ -1,51 +1,58 @@
1
- import { Route, Context } from "./router.d";
2
- import { Logger } from "../..";
3
- import { http } from "./router";
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';
4
7
 
5
8
  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
- });
9
+ const params = extractParams(path, route);
10
+ const query = new URLSearchParams(path);
11
+ const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
12
+
13
+ return Promise.resolve({
14
+ params,
15
+ request,
16
+ query,
17
+ formData,
18
+ logger: Logger(),
19
+ json: (statusCode: number, data: any) => http.json(statusCode, data),
20
+ render: async (component: ReactNode) => await renderStream(component),
21
+ });
18
22
  }
19
23
 
20
24
  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
+ const params: Map<string, string> = new Map();
26
+ const pathSegments = path.split('/');
27
+ const routeSegments = route.path.split('/');
24
28
 
25
- if (pathSegments.length !== routeSegments.length) return params;
29
+ if (pathSegments.length !== routeSegments.length) return params;
26
30
 
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
- }
31
+ for (let i = 0; i < pathSegments.length; i++) {
32
+ if (routeSegments[i][0] === ':') {
33
+ const key = routeSegments[i].replace(':', '');
34
+ const value = pathSegments[i];
35
+ params.set(key, value);
36
+ }
37
+ }
34
38
 
35
- return params;
39
+ return params;
36
40
  }
37
41
 
38
42
  function getContentType(headers: Headers): string {
39
- const contentType = headers.get('Content-Type');
40
- if (!contentType) return '';
41
- return contentType;
43
+ const contentType = headers.get('Content-Type');
44
+ if (!contentType) return '';
45
+ return contentType;
42
46
  }
43
47
 
44
48
  function isMultiPartForm(headers: Headers): boolean {
45
- const contentType = getContentType(headers);
46
- return contentType.includes('multipart/form-data');
49
+ const contentType = getContentType(headers);
50
+ return contentType.includes('multipart/form-data');
47
51
  }
48
52
 
53
+ async function renderStream(children: ReactNode) {
54
+ const stream = await renderToReadableStream(children);
55
+ return new Response(stream, { headers: { 'Content-Type': 'text/html' } });
56
+ }
49
57
 
50
-
51
- export { createContext }
58
+ export { 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,6 +29,7 @@ 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>
@@ -43,4 +44,4 @@ type RouterOptions<Options> = ServeOptions
43
44
  | TLSWebSocketServeOptions<Options>
44
45
  | undefined
45
46
 
46
- export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler }
47
+ export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler };
@@ -5,96 +5,111 @@ import { httpStatusCodes } from '../http/status';
5
5
  import { readDir } from '../fs/fsys';
6
6
  import { Logger, startMessage } from '../logger/logger';
7
7
  import { http } from '../http/http';
8
- import {RouteTree } from './tree';
8
+ import { RouteTree } from './tree';
9
9
  import { createContext } from './context';
10
10
 
11
-
12
11
  const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
13
- const { addRoute, findRoute } = RouteTree();
14
- const logger = Logger();
15
-
16
- return {
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) },
12
+ const { addRoute, findRoute } = RouteTree();
13
+ const logger = Logger();
14
+
15
+ async function loadComponent(name: string) {
16
+ const module = await import(name);
17
+ return module.default;
18
+ }
19
+
20
+ return {
21
+ // add a route to the router tree
22
+ add: (pattern, method, callback) => { addRoute(pattern, method, callback); },
23
+ get: (pattern: string, callback: HttpHandler) => { addRoute(pattern, 'GET', callback); },
24
+ post: (pattern, callback) => { addRoute(pattern, 'POST', callback); },
25
+ put: (pattern, callback) => { addRoute(pattern, 'PUT', callback);},
26
+ delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback); },
23
27
 
24
- // add a static route to the router tree
25
- static: async (pattern: string, root: string) => {
26
- await readDir(root, async (fp, _) => {
27
- const pure = path.join('.', fp);
28
- const ext = path.extname(pure);
29
-
30
- let base = path.basename(pure);
31
-
32
- if (ext === '.html') base = base.replace(ext, '');
33
-
34
- if (pattern[0] !== '/') pattern = '/' + pattern;
35
-
36
- let patternPath = pattern + base;
37
-
38
- if (base === 'index') patternPath = pattern;
39
-
40
- const route: Route = {
41
- children: new Map(),
42
- dynamicPath: '',
43
- isLast: true,
44
- path: patternPath,
45
- method: 'GET',
46
- handler: async () => await http.file(200, pure),
47
- };
48
-
49
- addRoute(route.path, 'GET', route.handler);
50
- });
51
- },
52
- // start the server
53
- serve: () => {
54
- startMessage(port ?? 3000);
55
- let opts: Options = { db: ':memory:' };
56
-
57
- Bun.serve({
58
- port: port ?? 3000,
59
- ...options,
60
- async fetch(req) {
61
- const url = new URL(req.url);
62
- let path = url.pathname;
63
-
64
- // set the database
65
- if (options) {
66
- let o = options as Options;
67
- opts.db = o.db;
68
- }
69
-
70
- const route = findRoute(path);
71
-
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
- }
78
-
79
- const context = await createContext(path, route, req);
80
- context.db = new Database(opts.db);
81
-
82
- const response = await route.handler(context);
83
-
84
- logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
85
- return Promise.resolve(response);
86
- }
87
-
88
- // if no route is found, return 404
89
- const response = await http.notFound();
28
+ // add static routes to the router tree
29
+ // .tsx and .html are rendered as components
30
+ // all other file extensions are served as files
31
+ // the root directory is traversed recursively
32
+ static: async (pattern: string, root: string) => {
33
+ await readDir(root, async (fp) => {
34
+ const pure = path.join('.', fp);
35
+ const ext = path.extname(pure);
36
+
37
+ let base = path.basename(pure);
38
+
39
+ //FIXME: this can be improved
40
+ if (ext === '.html') base = base.replace(ext, '');
41
+ if (ext === '.tsx') base = base.replace(ext, '');
42
+
43
+ if (pattern[0] !== '/') pattern = '/' + pattern;
44
+
45
+ let patternPath = pattern + base;
46
+
47
+ if (base === 'index') patternPath = pattern;
48
+
49
+ const route: Route = {
50
+ children: new Map(),
51
+ dynamicPath: '',
52
+ isLast: true,
53
+ path: patternPath,
54
+ method: 'GET',
55
+ handler: async () => {
56
+ if (ext === '.tsx') {
57
+ const component = await loadComponent(path.join(root, patternPath));
58
+ return await http.render(component());
59
+ } else {
60
+ return await http.file(200, pure);
61
+ }
62
+ },
63
+ };
64
+
65
+ addRoute(route.path, 'GET', route.handler);
66
+ });
67
+ },
68
+ // start the server
69
+ serve: () => {
70
+ startMessage(port ?? 3000);
71
+ const opts: Options = { db: ':memory:' };
72
+
73
+ Bun.serve({
74
+ port: port ?? 3000,
75
+ ...options,
76
+ async fetch(req) {
77
+ const url = new URL(req.url);
78
+ const path = url.pathname;
79
+
80
+ // set the database
81
+ if (options) {
82
+ const o = options as Options;
83
+ opts.db = o.db;
84
+ }
85
+
86
+ const route = findRoute(path);
87
+
88
+ // if the route exists, execute the handler
89
+ if (route) {
90
+ if (route.method !== req.method) {
91
+ logger.info(405, url.pathname, req.method, httpStatusCodes[405]);
92
+ return Promise.resolve(http.methodNotAllowed());
93
+ }
94
+
95
+ const context = await createContext(path, route, req);
96
+ context.db = new Database(opts.db);
97
+
98
+ const response = await route.handler(context);
99
+
100
+ logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
101
+ return Promise.resolve(response);
102
+ }
103
+
104
+ // if no route is found, return 404
105
+ const response = await http.notFound();
90
106
 
91
- logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
92
- return Promise.resolve(http.notFound());
93
-
94
- }
95
- });
96
- },
97
- }
98
- }
99
-
100
- export { Router, http }
107
+ logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
108
+ return Promise.resolve(http.notFound());
109
+ }
110
+ });
111
+ },
112
+ };
113
+ };
114
+
115
+ export { Router, http };
@@ -1,63 +1,62 @@
1
- import { HttpHandler, Route } from "./router.d";
2
- import { http } from "../http/http";
1
+ import { HttpHandler, Route } from './router.d';
2
+ import { http } from '../http/http';
3
3
  import { createContext } from './context';
4
-
5
- const splitPath = (s: string): string[] => s.split('/').filter(x => x !== '');
4
+ import { splitPath } from '../util/strings';
6
5
 
7
6
  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;
7
+ const route: Route = {
8
+ children: new Map(),
9
+ path: path,
10
+ dynamicPath: '',
11
+ method: method,
12
+ handler: handler,
13
+ isLast: false
14
+ };
15
+
16
+ return route;
18
17
  };
19
18
 
20
19
  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 }
20
+ const root = createRoute('', 'GET', () => http.notFound());
21
+
22
+ const addRoute = (path: string, method: string, handler: HttpHandler) => {
23
+ const pathParts = splitPath(path);
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 = path;
40
+ };
41
+
42
+ function findRoute(path: string): Route | undefined {
43
+ const pathParts = splitPath(path);
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
+ return { addRoute, findRoute };
60
59
 
61
60
  };
62
61
 
63
- export { RouteTree, createContext }
62
+ export { RouteTree, createContext };
package/package.json CHANGED
@@ -3,10 +3,19 @@
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
5
  "devDependencies": {
6
- "bun-types": "latest"
6
+ "@types/react-dom": "^18.2.7",
7
+ "@typescript-eslint/eslint-plugin": "^6.7.0",
8
+ "@typescript-eslint/parser": "^6.7.0",
9
+ "bun-types": "latest",
10
+ "eslint": "^8.49.0",
11
+ "eslint-plugin-react": "^7.33.2",
12
+ "react-dom": "^18.2.0"
7
13
  },
8
14
  "peerDependencies": {
9
15
  "typescript": "^5.0.0"
10
16
  },
11
- "version": "0.7.3"
17
+ "version": "0.7.4-experimental.2",
18
+ "dependencies": {
19
+ "eslint-plugin-react-hooks": "^4.6.0"
20
+ }
12
21
  }
package/tsconfig.json CHANGED
@@ -16,8 +16,10 @@
16
16
  "allowJs": true,
17
17
  "noEmit": true,
18
18
  "types": [
19
- "./index.d.ts",
20
19
  "bun-types" // add Bun global
21
- ]
20
+ ],
21
+ "paths": {
22
+ "examples/*": ["./examples/*"]
23
+ }
22
24
  }
23
25
  }
package/examples/db.ts DELETED
@@ -1,10 +0,0 @@
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
-