bun-router 0.7.3 → 0.7.4-experimental.11

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