bun-router 0.7.4-experimental.0 → 0.7.4-experimental.3

Sign up to get free protection for your applications and to get access to all the features.
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
 
@@ -77,3 +81,4 @@ router.get('/', ctx => ctx.render(Home('Hello World')))
77
81
  router.serve();
78
82
  ```
79
83
 
84
+
package/examples/basic.ts CHANGED
@@ -5,9 +5,9 @@ const router = Router();
5
5
  router.add('/', 'GET', () => http.json(200, 'ok'));
6
6
 
7
7
  router.add('/user/:name', 'GET', (ctx) => {
8
- const name = ctx.params.get('name');
9
- if (!name) return http.json(500, 'no name');
10
- return http.json(200, name);
8
+ const name = ctx.params.get('name');
9
+ if (!name) return http.json(500, 'no name');
10
+ return http.json(200, name);
11
11
  });
12
12
 
13
13
  router.serve();
@@ -4,16 +4,16 @@ import { Context } from '../lib/router/router.d';
4
4
  const home = () => new Response('Welcome Home', { status: 200 });
5
5
 
6
6
  const subreddit = (ctx: Context) => {
7
- const sub = ctx.params.get('subreddit');
8
- if (!sub) return http.json(400, { error: 'no subreddit provided' });
9
- return http.json(200, { subreddit: sub });
10
- }
7
+ const sub = ctx.params.get('subreddit');
8
+ if (!sub) return http.json(400, { error: 'no subreddit provided' });
9
+ return http.json(200, { subreddit: sub });
10
+ };
11
11
 
12
12
  const user = (ctx: Context) => {
13
- const user = ctx.params.get('user');
14
- if (!user) return http.json(400, { error: 'no user provided' });
15
- return http.json(200, { user: user });
16
- }
13
+ const user = ctx.params.get('user');
14
+ if (!user) return http.json(400, { error: 'no user provided' });
15
+ return http.json(200, { user: user });
16
+ };
17
17
 
18
18
  const r = Router();
19
19
 
@@ -3,9 +3,9 @@ import { Router, http } from '..';
3
3
  const r = Router();
4
4
 
5
5
  r.get('/', ctx => {
6
- ctx.logger.debug('hello from home');
6
+ ctx.logger.debug('hello from home');
7
7
 
8
- return http.ok();
8
+ return http.ok();
9
9
  });
10
10
 
11
11
  r.serve();
@@ -0,0 +1,7 @@
1
+ import { Router } from '../../';
2
+
3
+ const router = Router();
4
+
5
+ router.static('/', './examples/ssr/pages');
6
+
7
+ router.serve();
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+
3
+ export default function Foo() {
4
+ return (
5
+ <h1>Foo </h1>
6
+ );
7
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+
3
+ export default function Home() {
4
+ return (
5
+ <h1>Hello World</h1>
6
+ );
7
+ }
@@ -2,5 +2,5 @@ import { Router } from '..';
2
2
 
3
3
  const r = Router(3001);
4
4
 
5
- r.static('/', './examples/pages');
5
+ r.static('/', './pages');
6
6
  r.serve();
package/examples/todo.ts CHANGED
@@ -1,45 +1,45 @@
1
1
  import { Router, http } from '..';
2
2
 
3
3
  const Todo = () => {
4
- const list: Record<string, string> = {};
4
+ const list: Record<string, string> = {};
5
5
 
6
- return {
7
- add: (key: string, value: string) => { list[key] = value },
8
- get: (key: string) => list[key],
9
- remove: (key: string) => { delete list[key] },
10
- size: () => Object.entries(list).length,
11
- export: () => list,
12
- }
13
- }
6
+ return {
7
+ add: (key: string, value: string) => { list[key] = value; },
8
+ get: (key: string) => list[key],
9
+ remove: (key: string) => { delete list[key]; },
10
+ size: () => Object.entries(list).length,
11
+ export: () => list,
12
+ };
13
+ };
14
14
 
15
15
  const todo = Todo();
16
16
 
17
17
  const r = Router();
18
18
 
19
19
  r.add('/api/new', 'POST', ctx => {
20
- const query = new URL(ctx.request.url).searchParams;
21
- const key = query.get('key');
22
- const content = query.get('content');
20
+ const query = new URL(ctx.request.url).searchParams;
21
+ const key = query.get('key');
22
+ const content = query.get('content');
23
23
 
24
- if (!key || !content) return http.message(400, 'invalid query params');
25
- ctx.logger.message(`Adding ${key} with ${content}`);
26
- todo.add(key, content);
24
+ if (!key || !content) return http.message(400, 'invalid query params');
25
+ ctx.logger.message(`Adding ${key} with ${content}`);
26
+ todo.add(key, content);
27
27
 
28
- return ctx.json(200, { message: 'ok' });
28
+ return ctx.json(200, { message: 'ok' });
29
29
  });
30
30
 
31
31
  r.add('/api/todo/:key', 'GET', ctx => {
32
- const key = ctx.params.get('key');
33
- if (!key) return http.message(400, 'invalid params');
32
+ const key = ctx.params.get('key');
33
+ if (!key) return http.message(400, 'invalid params');
34
34
 
35
- const content = todo.get(key);
36
- if (!content) return http.notFound();
35
+ const content = todo.get(key);
36
+ if (!content) return http.notFound();
37
37
 
38
- return ctx.json(200, {key: key, content: content});
38
+ return ctx.json(200, {key: key, content: content});
39
39
  });
40
40
 
41
41
  r.add('/api/get/all', 'GET', ctx => {
42
- return ctx.json(200, todo.export());
42
+ return ctx.json(200, todo.export());
43
43
  });
44
44
 
45
45
  r.serve();
@@ -0,0 +1,76 @@
1
+ import { splitFilePath } from '../fs/fsys';
2
+ import path from 'node:path';
3
+
4
+ type File = {
5
+ name: string;
6
+ path: string;
7
+ extension: string;
8
+ children: Map<string, File>;
9
+ isLast: boolean;
10
+ };
11
+
12
+ function createFile(name: string): File {
13
+ return {
14
+ name,
15
+ path: '',
16
+ extension: '',
17
+ children: new Map(),
18
+ isLast: false,
19
+ };
20
+ }
21
+
22
+ const FileTree = (dir: string) => {
23
+ const root = createFile(dir);
24
+
25
+ const addFile = (_path: string) => {
26
+ const pathParts = splitFilePath(_path);
27
+ let current = root;
28
+
29
+ for (let i = 0; i < pathParts.length; i++) {
30
+ const part = pathParts[i];
31
+ if (!current.children.has(part)) {
32
+ current.children.set(part, createFile(part));
33
+ }
34
+ current = current.children.get(part)!;
35
+ }
36
+
37
+ current.isLast = true;
38
+ current.path = _path;
39
+ current.extension = path.extname(_path);
40
+ };
41
+
42
+ const getFilesByExtension = (extension: string): string[] => {
43
+ let current = root;
44
+ const files: string[] = [];
45
+
46
+ for (const [name, file] of current.children) {
47
+ if (file.extension === extension) {
48
+ files.push(file.path);
49
+ }
50
+ current = current.children.get(name)!;
51
+ }
52
+
53
+ return files;
54
+ };
55
+
56
+ const getFileByName = (path: string): string | undefined => {
57
+ let current = root;
58
+ const pathParts = splitFilePath(path);
59
+ for (let i = 0; i < pathParts.length; i++) {
60
+ const part = pathParts[i];
61
+ if (current.children.has(part)) {
62
+ current = current.children.get(part)!;
63
+ } else {
64
+ return;
65
+ }
66
+ }
67
+
68
+ if (!current.isLast) return;
69
+
70
+ return current.path;
71
+ };
72
+
73
+ return { addFile, getFilesByExtension, getFileByName };
74
+ };
75
+
76
+ export { FileTree };
package/lib/fs/fsys.ts CHANGED
@@ -1,29 +1,36 @@
1
- import { BunFile } from "bun";
1
+ import { BunFile } from 'bun';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'path';
4
4
 
5
5
  // check if the file path is a directory
6
6
  const isDir = async (fp: string): Promise<boolean> => (await fs.lstat(fp)).isDirectory();
7
7
 
8
+ // recursively read a directory and call a handler function on each file
8
9
  async function readDir(dirpath: string, handler: (filepath: string, entry: BunFile) => void) {
9
- const files = await fs.readdir(dirpath);
10
+ const files = await fs.readdir(dirpath);
10
11
 
11
- for (const file of files) {
12
- const bunFile = Bun.file(file);
12
+ for (const file of files) {
13
+ const bunFile = Bun.file(file);
13
14
 
14
- if (typeof bunFile.name === 'undefined') return
15
+ if (typeof bunFile.name === 'undefined') return;
15
16
 
16
- const fp = path.join(dirpath, bunFile.name);
17
- const isdir = await isDir(fp);
17
+ const fp = path.join(dirpath, bunFile.name);
18
+ const isdir = await isDir(fp);
18
19
 
19
- if (isdir) await readDir(fp, handler);
20
- else handler(fp, bunFile);
21
- }
20
+ if (isdir) await readDir(fp, handler);
21
+ else handler(fp, bunFile);
22
+ }
22
23
  }
23
24
 
24
- function ext(path: string): string {
25
- return path.split('.').pop() || '';
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);
26
33
  }
27
34
 
28
35
 
29
- export { readDir, ext }
36
+ export { readDir, ext, splitFilePath };
package/lib/http/http.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { httpStatusCodes } from './status';
3
+ import { ReactNode } from 'react';
4
+ import { renderToReadableStream } from 'react-dom/server';
3
5
 
4
6
  const http = {
5
7
  ok: async (msg?: string): Promise<Response> => {
@@ -43,6 +45,10 @@ const http = {
43
45
  headers: { 'Content-Type': contentType}
44
46
  }));
45
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
+ },
46
52
  noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
47
53
  status: 204,
48
54
  statusText: 'no content',
@@ -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 };
@@ -8,11 +8,15 @@ import { http } from '../http/http';
8
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
12
  const { addRoute, findRoute } = RouteTree();
14
13
  const logger = Logger();
15
14
 
15
+ async function loadComponent(name: string) {
16
+ const module = await import(name);
17
+ return module.default;
18
+ }
19
+
16
20
  return {
17
21
  // add a route to the router tree
18
22
  add: (pattern, method, callback) => { addRoute(pattern, method, callback); },
@@ -21,15 +25,19 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
21
25
  put: (pattern, callback) => { addRoute(pattern, 'PUT', callback);},
22
26
  delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback); },
23
27
 
24
- // add a static route to the router tree
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
25
32
  static: async (pattern: string, root: string) => {
26
33
  await readDir(root, async (fp) => {
27
- const pure = path.join('.', fp);
28
- const ext = path.extname(pure);
34
+ const ext = path.extname(fp);
29
35
 
30
- let base = path.basename(pure);
36
+ let base = path.basename(fp);
31
37
 
38
+ //FIXME: this can be improved
32
39
  if (ext === '.html') base = base.replace(ext, '');
40
+ if (ext === '.tsx') base = base.replace(ext, '');
33
41
 
34
42
  if (pattern[0] !== '/') pattern = '/' + pattern;
35
43
 
@@ -37,13 +45,22 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
37
45
 
38
46
  if (base === 'index') patternPath = pattern;
39
47
 
48
+ const purePath = path.join(root, patternPath);
49
+
40
50
  const route: Route = {
41
51
  children: new Map(),
42
52
  dynamicPath: '',
43
53
  isLast: true,
44
54
  path: patternPath,
45
55
  method: 'GET',
46
- handler: async () => await http.file(200, pure),
56
+ handler: async () => {
57
+ if (ext === '.tsx') {
58
+ const component = await loadComponent(purePath.split('.')[0]);
59
+ return await http.render(component());
60
+ } else {
61
+ return await http.file(200, fp);
62
+ }
63
+ },
47
64
  };
48
65
 
49
66
  addRoute(route.path, 'GET', route.handler);
@@ -90,7 +107,6 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
90
107
 
91
108
  logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
92
109
  return Promise.resolve(http.notFound());
93
-
94
110
  }
95
111
  });
96
112
  },
@@ -59,4 +59,4 @@ const RouteTree = () => {
59
59
 
60
60
  };
61
61
 
62
- export { RouteTree, createContext as createContext };
62
+ export { RouteTree, createContext };
package/package.json CHANGED
@@ -14,8 +14,10 @@
14
14
  "peerDependencies": {
15
15
  "typescript": "^5.0.0"
16
16
  },
17
- "version": "0.7.4-experimental.0",
17
+ "version": "0.7.4-experimental.3",
18
18
  "dependencies": {
19
- "eslint-plugin-react-hooks": "^4.6.0"
19
+ "@types/react": "^18.2.22",
20
+ "eslint-plugin-react-hooks": "^4.6.0",
21
+ "react": "^18.2.0"
20
22
  }
21
23
  }
package/tsconfig.json CHANGED
@@ -17,6 +17,9 @@
17
17
  "noEmit": true,
18
18
  "types": [
19
19
  "bun-types" // add Bun global
20
- ]
20
+ ],
21
+ "paths": {
22
+ "examples/*": ["./examples/*"],
23
+ }
21
24
  }
22
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
-