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.
@@ -1,100 +1,133 @@
1
1
  import path from 'path';
2
2
  import { Database } from 'bun:sqlite';
3
- import { Route, BunRouter, RouterOptions, Options, HttpHandler } from './router.d';
3
+ import { Route, BunRouter, RouterOptions, Options } from './router.d';
4
4
  import { httpStatusCodes } from '../http/status';
5
5
  import { readDir } from '../fs/fsys';
6
6
  import { Logger, startMessage } from '../logger/logger';
7
7
  import { http } from '../http/http';
8
- import {RouteTree } from './tree';
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
- 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(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
- // 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();
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
- 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 }
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
- "bun-types": "latest"
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.3"
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
  }
@@ -1,35 +1,36 @@
1
1
  import { describe, test, expect } from 'bun:test';
2
2
 
3
3
  describe('Router', () => {
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
- });
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
- const text = await new Response(proc.stdout).text();
11
+ const text = await new Response(proc.stdout).text();
12
12
 
13
- const hasFailed = text.includes('Failed');
13
+ const hasFailed = text.includes('Failed');
14
14
 
15
- if (hasFailed) console.log(text);
15
+ if (hasFailed) console.log(text);
16
16
 
17
- expect(hasFailed).toBe(false);
17
+ expect(hasFailed).toBe(false);
18
18
 
19
- proc.kill(0);
20
- })
19
+ proc.kill(0);
20
+ });
21
21
 
22
- test('Static', async() => {
23
- const proc = Bun.spawn(['./tests/static.test.sh']);
22
+ test('Static', async() => {
23
+ const proc = Bun.spawn(['./tests/static.test.sh']);
24
24
 
25
- const text = await new Response(proc.stdout).text();
25
+ const text = await new Response(proc.stdout).text();
26
26
 
27
- const hasFailed = text.includes('Failed');
28
- if (hasFailed) console.log(text);
27
+ const hasFailed = text.includes('Failed');
28
+ if (hasFailed) console.log(text);
29
29
 
30
- expect(hasFailed).toBe(false);
30
+ expect(hasFailed).toBe(false);
31
31
 
32
- proc.kill(0);
33
- });
32
+ proc.kill(0);
33
+ });
34
34
  });
35
35
 
36
+
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
- ]
22
- }
20
+ ],
21
+ "paths": {
22
+ "/examples/*": ["./examples/*"]
23
+ }
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
-
@@ -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 }
@@ -1,3 +0,0 @@
1
- const splitPath = (path: string): string[] => path.split('/').filter(Boolean);
2
-
3
- export { splitPath }