bun-router 0.7.3 → 0.7.4-experimental.2

Sign up to get free protection for your applications and to get access to all the features.
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
-