bun-router 0.7.3 → 0.7.4-experimental.11
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 +35 -0
- package/README.md +28 -0
- package/bun.lockb +0 -0
- package/examples/basic.ts +4 -4
- package/examples/dynamic.ts +8 -8
- package/examples/logger.ts +2 -2
- package/examples/ssr/index.ts +8 -0
- package/examples/ssr/pages/foo.tsx +7 -0
- package/examples/ssr/pages/home.tsx +7 -0
- package/examples/static.ts +1 -0
- package/examples/todo.ts +22 -22
- package/examples/tsx/components/user.tsx +7 -0
- package/examples/tsx/index.ts +20 -0
- package/lib/fs/filetree.ts +75 -0
- package/lib/fs/fsys.ts +25 -11
- package/lib/http/http.ts +81 -67
- package/lib/http/status.ts +64 -64
- package/lib/logger/color.ts +25 -25
- package/lib/logger/logger.d.ts +1 -1
- package/lib/logger/logger.ts +86 -70
- package/lib/router/context.ts +50 -37
- package/lib/router/routeTree.ts +86 -0
- package/lib/router/router.d.ts +6 -4
- package/lib/router/router.ts +122 -89
- package/package.json +13 -2
- package/tests/router.test.ts +21 -20
- package/tsconfig.json +5 -3
- package/examples/db.ts +0 -10
- package/lib/router/tree.ts +0 -63
- package/lib/util/strings.ts +0 -3
    
        package/lib/router/router.ts
    CHANGED
    
    | @@ -1,100 +1,133 @@ | |
| 1 1 | 
             
            import path from 'path';
         | 
| 2 2 | 
             
            import { Database } from 'bun:sqlite';
         | 
| 3 | 
            -
            import { Route, BunRouter, RouterOptions, Options | 
| 3 | 
            +
            import { Route, BunRouter, RouterOptions, Options } from './router.d';
         | 
| 4 4 | 
             
            import { httpStatusCodes } from '../http/status';
         | 
| 5 5 | 
             
            import { readDir } from '../fs/fsys';
         | 
| 6 6 | 
             
            import { Logger, startMessage } from '../logger/logger';
         | 
| 7 7 | 
             
            import { http } from '../http/http';
         | 
| 8 | 
            -
            import {RouteTree } from './ | 
| 8 | 
            +
            import { RouteTree } from './routeTree';
         | 
| 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(options?.enableFileLogging ?? false);
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            	// load a component from the root directory relative to the cwd
         | 
| 16 | 
            +
            	async function loadComponent(root: string, name: string) {
         | 
| 17 | 
            +
            		const module = await import(path.join(process.cwd(), root, name));
         | 
| 18 | 
            +
            		return module.default;
         | 
| 19 | 
            +
            	}
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            	// extract the path, extension, and base name from a file path
         | 
| 22 | 
            +
            	function extractPathExtBase(pattern: string, pathname: string) {
         | 
| 23 | 
            +
            		const extension = path.extname(pathname);
         | 
| 24 | 
            +
            		let base = encodeURIComponent(path.basename(pathname));
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            		if (extension === '.html' || extension === '.tsx') base = base.replace(extension, '');
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            		let patternPath = [pattern, base].join('/');
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            		if (base === 'index') patternPath = pattern;
         | 
| 31 | 
            +
            		
         | 
| 32 | 
            +
            		return { patternPath, extension, base };
         | 
| 33 | 
            +
            	}
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            	// check if a file exists
         | 
| 36 | 
            +
            	async function exists(fp: string) {
         | 
| 37 | 
            +
            		const f = Bun.file(fp);
         | 
| 38 | 
            +
            		return await f.exists();
         | 
| 39 | 
            +
            	}
         | 
| 40 | 
            +
            	
         | 
| 41 | 
            +
            	return {
         | 
| 42 | 
            +
            		// add a route to the router tree 
         | 
| 43 | 
            +
            		add: (pattern, method, callback) => { addRoute(pattern, method, callback); },
         | 
| 44 | 
            +
            		get: (pattern, callback) => { addRoute(pattern, 'GET', callback); },
         | 
| 45 | 
            +
            		post: (pattern, callback) => { addRoute(pattern, 'POST', callback); },
         | 
| 46 | 
            +
            		put: (pattern, callback) => { addRoute(pattern, 'PUT', callback);},
         | 
| 47 | 
            +
            		delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback); },
         | 
| 23 48 |  | 
| 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 | 
            -
             | 
| 49 | 
            +
            		// add static routes to the router tree
         | 
| 50 | 
            +
            		// .tsx and .html are rendered as components, or pages
         | 
| 51 | 
            +
            		// all other file extensions are served as files
         | 
| 52 | 
            +
            		// the root directory is traversed recursively
         | 
| 53 | 
            +
            		static: async (pattern: string, root: string) => {
         | 
| 54 | 
            +
            			if (!exists(root)) return console.error(`Directory not found: ${root}`);
         | 
| 55 | 
            +
            			await readDir(root, async (fp) => {
         | 
| 56 | 
            +
            				const { patternPath, extension, base } = extractPathExtBase(pattern, fp);
         | 
| 57 | 
            +
            				const route: Route = {
         | 
| 58 | 
            +
            					children: new Map(),
         | 
| 59 | 
            +
            					dynamicPath: pattern,
         | 
| 60 | 
            +
            					isLast: true,
         | 
| 61 | 
            +
            					path: patternPath.startsWith('//') ? patternPath.slice(1) : patternPath, // remove the leading '/' if it exists
         | 
| 62 | 
            +
            					method: 'GET',
         | 
| 63 | 
            +
            					handler: async () => {
         | 
| 64 | 
            +
            						if (extension === '.tsx') {
         | 
| 65 | 
            +
            							const component = await loadComponent(root, base);
         | 
| 66 | 
            +
            							return await http.render(component());
         | 
| 67 | 
            +
            						} else {
         | 
| 68 | 
            +
            							return await http.file(200, fp);
         | 
| 69 | 
            +
            						}
         | 
| 70 | 
            +
            					},
         | 
| 71 | 
            +
            				};
         | 
| 72 | 
            +
             | 
| 73 | 
            +
            				addRoute(route.path, 'GET', route.handler);
         | 
| 74 | 
            +
            			});
         | 
| 75 | 
            +
            			
         | 
| 76 | 
            +
            		},
         | 
| 77 | 
            +
            		// start listening for requests
         | 
| 78 | 
            +
            		serve: () => {
         | 
| 79 | 
            +
            			startMessage(port ?? 3000);
         | 
| 80 | 
            +
            			const opts: Options = { db: ':memory:', enableFileLogging: false };
         | 
| 81 | 
            +
             | 
| 82 | 
            +
            			Bun.serve({
         | 
| 83 | 
            +
            				port: port ?? 3000,
         | 
| 84 | 
            +
            				...options,
         | 
| 85 | 
            +
            				async fetch(req) {
         | 
| 86 | 
            +
            					const url = new URL(req.url);
         | 
| 87 | 
            +
            					const pathname = url.pathname;
         | 
| 88 | 
            +
             | 
| 89 | 
            +
            					// set the database
         | 
| 90 | 
            +
            					if (options) {
         | 
| 91 | 
            +
            						const o = options as Options;
         | 
| 92 | 
            +
            						opts.db = o.db;
         | 
| 93 | 
            +
            						opts.enableFileLogging = o.enableFileLogging;
         | 
| 94 | 
            +
            					}
         | 
| 95 | 
            +
             | 
| 96 | 
            +
            					const route = findRoute(pathname);
         | 
| 97 | 
            +
             | 
| 98 | 
            +
            					// if the route exists, call the handler
         | 
| 99 | 
            +
            					if (route) {
         | 
| 100 | 
            +
            						if (route.method !== req.method) {
         | 
| 101 | 
            +
            							logger.info(405, url.pathname, req.method, httpStatusCodes[405]);
         | 
| 102 | 
            +
            							return Promise.resolve(http.methodNotAllowed());
         | 
| 103 | 
            +
            						}
         | 
| 104 | 
            +
             | 
| 105 | 
            +
            						// create a context for the handler
         | 
| 106 | 
            +
            						const context = await createContext(pathname, route, req, opts.enableFileLogging);
         | 
| 107 | 
            +
            						context.db = new Database(opts.db);
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            						// call the handler
         | 
| 110 | 
            +
            						const response = await route.handler(context);
         | 
| 111 | 
            +
             | 
| 112 | 
            +
            						logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
         | 
| 113 | 
            +
            						return Promise.resolve(response);
         | 
| 114 | 
            +
            					} 
         | 
| 115 | 
            +
             | 
| 116 | 
            +
            					// if no route is found, return 404 
         | 
| 117 | 
            +
            					const response = await http.notFound();
         | 
| 90 118 |  | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
            }
         | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 119 | 
            +
            					logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
         | 
| 120 | 
            +
            					return Promise.resolve(http.notFound());
         | 
| 121 | 
            +
            				},
         | 
| 122 | 
            +
            				// if an error occurs, return a 500 response
         | 
| 123 | 
            +
            				error(error) {
         | 
| 124 | 
            +
            					return new Response(`<pre>${error}\n${error.stack}</pre>`, {
         | 
| 125 | 
            +
            						headers: { 'Content-Type': 'text/html' },
         | 
| 126 | 
            +
            					});
         | 
| 127 | 
            +
            				}
         | 
| 128 | 
            +
            			});
         | 
| 129 | 
            +
            		},
         | 
| 130 | 
            +
            	};
         | 
| 131 | 
            +
            };
         | 
| 132 | 
            +
             | 
| 133 | 
            +
            export { Router, http };
         | 
    
        package/package.json
    CHANGED
    
    | @@ -3,10 +3,21 @@ | |
| 3 3 | 
             
              "module": "index.ts",
         | 
| 4 4 | 
             
              "type": "module",
         | 
| 5 5 | 
             
              "devDependencies": {
         | 
| 6 | 
            -
                " | 
| 6 | 
            +
                "@types/react": "^18.2.23",
         | 
| 7 | 
            +
                "@types/react-dom": "^18.2.8",
         | 
| 8 | 
            +
                "@typescript-eslint/eslint-plugin": "^6.7.0",
         | 
| 9 | 
            +
                "@typescript-eslint/parser": "^6.7.0",
         | 
| 10 | 
            +
                "bun-types": "latest",
         | 
| 11 | 
            +
                "eslint": "^8.49.0",
         | 
| 12 | 
            +
                "eslint-plugin-react": "^7.33.2"
         | 
| 7 13 | 
             
              },
         | 
| 8 14 | 
             
              "peerDependencies": {
         | 
| 9 15 | 
             
                "typescript": "^5.0.0"
         | 
| 10 16 | 
             
              },
         | 
| 11 | 
            -
              "version": "0.7. | 
| 17 | 
            +
              "version": "0.7.4-experimental.11",
         | 
| 18 | 
            +
              "dependencies": {
         | 
| 19 | 
            +
                "eslint-plugin-react-hooks": "^4.6.0",
         | 
| 20 | 
            +
                "react": "^18.2.0",
         | 
| 21 | 
            +
                "react-dom": "^18.2.0"
         | 
| 22 | 
            +
              }
         | 
| 12 23 | 
             
            }
         | 
    
        package/tests/router.test.ts
    CHANGED
    
    | @@ -1,35 +1,36 @@ | |
| 1 1 | 
             
            import { describe, test, expect } from 'bun:test';
         | 
| 2 2 |  | 
| 3 3 | 
             
            describe('Router', () => {
         | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 4 | 
            +
            	test('Serve', async () => {
         | 
| 5 | 
            +
            		const proc = Bun.spawn(['./tests/serve.test.sh'], {
         | 
| 6 | 
            +
            			onExit: (_proc, _exitCode, _signalCode , error) => {
         | 
| 7 | 
            +
            				if (error) console.error(error);     
         | 
| 8 | 
            +
            			},
         | 
| 9 | 
            +
            		});
         | 
| 10 10 |  | 
| 11 | 
            -
             | 
| 11 | 
            +
            		const text = await new Response(proc.stdout).text();
         | 
| 12 12 |  | 
| 13 | 
            -
             | 
| 13 | 
            +
            		const hasFailed = text.includes('Failed');
         | 
| 14 14 |  | 
| 15 | 
            -
             | 
| 15 | 
            +
            		if (hasFailed) console.log(text);
         | 
| 16 16 |  | 
| 17 | 
            -
             | 
| 17 | 
            +
            		expect(hasFailed).toBe(false);
         | 
| 18 18 |  | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 19 | 
            +
            		proc.kill(0);
         | 
| 20 | 
            +
            	});
         | 
| 21 21 |  | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 22 | 
            +
            	test('Static', async() => {
         | 
| 23 | 
            +
            		const proc = Bun.spawn(['./tests/static.test.sh']);
         | 
| 24 24 |  | 
| 25 | 
            -
             | 
| 25 | 
            +
            		const text = await new Response(proc.stdout).text();
         | 
| 26 26 |  | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 27 | 
            +
            		const hasFailed = text.includes('Failed');
         | 
| 28 | 
            +
            		if (hasFailed) console.log(text);
         | 
| 29 29 |  | 
| 30 | 
            -
             | 
| 30 | 
            +
            		expect(hasFailed).toBe(false);
         | 
| 31 31 |  | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 32 | 
            +
            		proc.kill(0);
         | 
| 33 | 
            +
            	});
         | 
| 34 34 | 
             
            });
         | 
| 35 35 |  | 
| 36 | 
            +
             | 
    
        package/tsconfig.json
    CHANGED
    
    
    
        package/examples/db.ts
    DELETED
    
    
    
        package/lib/router/tree.ts
    DELETED
    
    | @@ -1,63 +0,0 @@ | |
| 1 | 
            -
            import { HttpHandler, Route } from "./router.d";
         | 
| 2 | 
            -
            import { http } from "../http/http";
         | 
| 3 | 
            -
            import { createContext } from './context';
         | 
| 4 | 
            -
             | 
| 5 | 
            -
            const splitPath = (s: string): string[] => s.split('/').filter(x => x !== '');
         | 
| 6 | 
            -
             | 
| 7 | 
            -
            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;
         | 
| 18 | 
            -
            };
         | 
| 19 | 
            -
             | 
| 20 | 
            -
            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 }
         | 
| 60 | 
            -
             | 
| 61 | 
            -
            };
         | 
| 62 | 
            -
             | 
| 63 | 
            -
            export { RouteTree, createContext }
         | 
    
        package/lib/util/strings.ts
    DELETED