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 +34 -0
- package/README.md +28 -0
- package/bun.lockb +0 -0
- package/examples/basic.ts +3 -3
- package/examples/dynamic.ts +8 -8
- package/examples/logger.ts +2 -2
- package/examples/ssr/index.ts +7 -0
- package/examples/ssr/pages/foo.tsx +7 -0
- package/examples/ssr/pages/home.tsx +7 -0
- package/examples/todo.ts +22 -22
- package/lib/fs/filetree.ts +76 -0
- package/lib/fs/fsys.ts +22 -11
- package/lib/http/http.ts +74 -67
- package/lib/logger/logger.ts +59 -59
- package/lib/resolver/resolveTSX.ts +28 -0
- package/lib/router/context.ts +41 -34
- package/lib/router/router.d.ts +4 -3
- package/lib/router/router.ts +103 -88
- package/lib/router/tree.ts +53 -54
- package/package.json +11 -2
- package/tsconfig.json +4 -2
- package/examples/db.ts +0 -10
    
        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 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 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();
         | 
    
        package/examples/dynamic.ts
    CHANGED
    
    | @@ -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 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 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 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 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 |  | 
    
        package/examples/logger.ts
    CHANGED
    
    
    
        package/examples/todo.ts
    CHANGED
    
    | @@ -1,45 +1,45 @@ | |
| 1 1 | 
             
            import { Router, http } from '..';
         | 
| 2 2 |  | 
| 3 3 | 
             
            const Todo = () => {
         | 
| 4 | 
            -
             | 
| 4 | 
            +
            	const list: Record<string, string> = {};
         | 
| 5 5 |  | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 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 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 20 | 
            +
            	const query = new URL(ctx.request.url).searchParams;
         | 
| 21 | 
            +
            	const key = query.get('key');
         | 
| 22 | 
            +
            	const content = query.get('content');
         | 
| 23 23 |  | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 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 | 
            -
             | 
| 28 | 
            +
            	return ctx.json(200, { message: 'ok' });
         | 
| 29 29 | 
             
            });
         | 
| 30 30 |  | 
| 31 31 | 
             
            r.add('/api/todo/:key', 'GET', ctx => {
         | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 32 | 
            +
            	const key = ctx.params.get('key');
         | 
| 33 | 
            +
            	if (!key) return http.message(400, 'invalid params');
         | 
| 34 34 |  | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 35 | 
            +
            	const content = todo.get(key);
         | 
| 36 | 
            +
            	if (!content) return http.notFound();
         | 
| 37 37 |  | 
| 38 | 
            -
             | 
| 38 | 
            +
            	return ctx.json(200, {key: key, content: content});
         | 
| 39 39 | 
             
            });
         | 
| 40 40 |  | 
| 41 41 | 
             
            r.add('/api/get/all', 'GET', ctx => {
         | 
| 42 | 
            -
             | 
| 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  | 
| 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 | 
            -
             | 
| 10 | 
            +
            	const files = await fs.readdir(dirpath);
         | 
| 10 11 |  | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 12 | 
            +
            	for (const file of files) {
         | 
| 13 | 
            +
            		const bunFile = Bun.file(file);
         | 
| 13 14 |  | 
| 14 | 
            -
             | 
| 15 | 
            +
            		if (typeof bunFile.name === 'undefined') return;
         | 
| 15 16 |  | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 17 | 
            +
            		const fp = path.join(dirpath, bunFile.name);
         | 
| 18 | 
            +
            		const isdir = await isDir(fp);
         | 
| 18 19 |  | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 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 | 
            -
             | 
| 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 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 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 | 
            -
             | 
| 32 | 
            +
            		if (!exists) return http.notFound(`File not found: ${fp}`);
         | 
| 30 33 |  | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 34 | 
            +
            		const content = await file.arrayBuffer();
         | 
| 35 | 
            +
            		if (!content) return http.noContent();
         | 
| 33 36 |  | 
| 34 | 
            -
             | 
| 37 | 
            +
            		let contentType = 'text/html; charset=utf-9';
         | 
| 35 38 |  | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 39 | 
            +
            		if (file.type.includes('image'))
         | 
| 40 | 
            +
            			contentType = file.type + '; charset=utf-8';
         | 
| 38 41 |  | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 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 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 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 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 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 };
         | 
    
        package/lib/logger/logger.ts
    CHANGED
    
    | @@ -8,87 +8,87 @@ _                          _ | |
| 8 8 | 
             
            | . | | |   |  |  _| . | | |  _| -_|  _|
         | 
| 9 9 | 
             
            |___|___|_|_|  |_| |___|___|_| |___|_|  
         | 
| 10 10 |  | 
| 11 | 
            -
             | 
| 12 | 
            -
            const VERSION = '0.7. | 
| 11 | 
            +
            `;
         | 
| 12 | 
            +
            const VERSION = '0.7.4-experimental';
         | 
| 13 13 | 
             
            const Logger = (): BunLogger => {
         | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 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 | 
            -
             | 
| 20 | 
            +
            			message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${ message ?? ''}\n`;
         | 
| 21 21 |  | 
| 22 | 
            -
             | 
| 22 | 
            +
            			await Bun.write(Bun.stdout, message);
         | 
| 23 23 |  | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 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 | 
            -
             | 
| 30 | 
            +
            			const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' -> ' : ' <-'} ${error.message}\n`;
         | 
| 31 31 |  | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 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 | 
            -
             | 
| 39 | 
            +
            			message = `${source} : ${messageColor}\n`;
         | 
| 40 40 |  | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 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 | 
            -
             | 
| 48 | 
            +
            			message = `${source}: ${messageColor}\n`;
         | 
| 49 49 |  | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
            }
         | 
| 50 | 
            +
            			await Bun.write(Bun.stdout, message);
         | 
| 51 | 
            +
            		},
         | 
| 52 | 
            +
            	};
         | 
| 53 | 
            +
            };
         | 
| 54 54 |  | 
| 55 55 | 
             
            function timestamp(date: Date) {
         | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 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 | 
            -
             | 
| 67 | 
            +
            	const s = ` [${String(n)}${text ?? ''}] `;
         | 
| 68 68 |  | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 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 | 
            -
             | 
| 76 | 
            +
            	return color('white', 'bgBlack', `[${s}]`).trim();
         | 
| 77 77 | 
             
            }
         | 
| 78 78 |  | 
| 79 79 | 
             
            function startMessage(port: number | string) {
         | 
| 80 | 
            -
             | 
| 81 | 
            -
             | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 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 | 
            -
             | 
| 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 };
         | 
    
        package/lib/router/context.ts
    CHANGED
    
    | @@ -1,51 +1,58 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
            import {  | 
| 3 | 
            -
            import {  | 
| 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 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 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 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 25 | 
            +
            	const params: Map<string, string> = new Map();
         | 
| 26 | 
            +
            	const pathSegments = path.split('/');
         | 
| 27 | 
            +
            	const routeSegments = route.path.split('/');
         | 
| 24 28 |  | 
| 25 | 
            -
             | 
| 29 | 
            +
            	if (pathSegments.length !== routeSegments.length) return params;
         | 
| 26 30 |  | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 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 | 
            -
             | 
| 39 | 
            +
            	return params;
         | 
| 36 40 | 
             
            }
         | 
| 37 41 |  | 
| 38 42 | 
             
            function getContentType(headers: Headers): string {
         | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 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 | 
            -
             | 
| 46 | 
            -
             | 
| 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 };
         | 
    
        package/lib/router/router.d.ts
    CHANGED
    
    | @@ -1,4 +1,5 @@ | |
| 1 | 
            -
             | 
| 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 };
         | 
    
        package/lib/router/router.ts
    CHANGED
    
    | @@ -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 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 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 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 76 | 
            -
             | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 81 | 
            -
             | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 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 | 
            -
             | 
| 92 | 
            -
             | 
| 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 };
         | 
    
        package/lib/router/tree.ts
    CHANGED
    
    | @@ -1,63 +1,62 @@ | |
| 1 | 
            -
            import { HttpHandler, Route } from  | 
| 2 | 
            -
            import { http } from  | 
| 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 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 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 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 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 | 
            -
                " | 
| 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. | 
| 17 | 
            +
              "version": "0.7.4-experimental.2",
         | 
| 18 | 
            +
              "dependencies": {
         | 
| 19 | 
            +
                "eslint-plugin-react-hooks": "^4.6.0"
         | 
| 20 | 
            +
              }
         | 
| 12 21 | 
             
            }
         | 
    
        package/tsconfig.json
    CHANGED
    
    
    
        package/examples/db.ts
    DELETED