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.
package/.eslintrc.json ADDED
@@ -0,0 +1,35 @@
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
+ "no-control-regex": "off"
34
+ }
35
+ }
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
@@ -1,13 +1,13 @@
1
1
  import { Router, http } from '..';
2
2
 
3
- const router = Router();
3
+ const router = Router(3000, {enableFileLogging: false});
4
4
 
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,8 @@
1
+ import { Router, http } from '../../';
2
+
3
+ const router = Router();
4
+
5
+ router.add('/', 'GET', () => http.ok());
6
+ router.static('/page', './examples/ssr/pages');
7
+
8
+ 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
+ }
@@ -3,4 +3,5 @@ import { Router } from '..';
3
3
  const r = Router(3001);
4
4
 
5
5
  r.static('/', './examples/pages');
6
+
6
7
  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,7 @@
1
+ import React from 'react';
2
+
3
+ export function User(username: string) {
4
+ return (
5
+ <h1>{ username }</h1>
6
+ );
7
+ }
@@ -0,0 +1,20 @@
1
+ import { Router, http } from '../../';
2
+ import { User } from './components/user';
3
+
4
+ const router = Router();
5
+
6
+ router.get('/', () => {
7
+ return http.ok();
8
+ });
9
+
10
+
11
+ router.get('/u/:username', async ctx => {
12
+ const username = ctx.params.get('username');
13
+
14
+ if (!username) return http.message(400, 'invalid username');
15
+
16
+ return http.render(User(username));
17
+ });
18
+
19
+ router.serve();
20
+
@@ -0,0 +1,75 @@
1
+ import path from 'node:path';
2
+
3
+ type File = {
4
+ name: string;
5
+ path: string;
6
+ extension: string;
7
+ children: Map<string, File>;
8
+ isLast: boolean;
9
+ };
10
+
11
+ function createFile(name: string): File {
12
+ return {
13
+ name,
14
+ path: '',
15
+ extension: '',
16
+ children: new Map(),
17
+ isLast: false,
18
+ };
19
+ }
20
+
21
+ const FileTree = (dir: string) => {
22
+ const root = createFile(dir);
23
+
24
+ const addFile = (filepath: string) => {
25
+ const pathParts = filepath.split(path.sep);
26
+ let current = root;
27
+
28
+ for (let i = 0; i < pathParts.length; i++) {
29
+ const part = pathParts[i];
30
+ if (!current.children.has(part)) {
31
+ current.children.set(part, createFile(part));
32
+ }
33
+ current = current.children.get(part)!;
34
+ }
35
+
36
+ current.isLast = true;
37
+ current.path = filepath;
38
+ current.extension = path.extname(filepath);
39
+ };
40
+
41
+ const getFilesByExtension = (extension: string): string[] => {
42
+ let current = root;
43
+ const files: string[] = [];
44
+
45
+ for (const [name, file] of current.children) {
46
+ if (file.extension === extension) {
47
+ files.push(file.path);
48
+ }
49
+ current = current.children.get(name)!;
50
+ }
51
+
52
+ return files;
53
+ };
54
+
55
+ const getFileByName = (filepath: string): string | undefined => {
56
+ let current = root;
57
+ const pathParts = filepath.split(path.sep);
58
+ for (let i = 0; i < pathParts.length; i++) {
59
+ const part = pathParts[i];
60
+ if (current.children.has(part)) {
61
+ current = current.children.get(part)!;
62
+ } else {
63
+ return;
64
+ }
65
+ }
66
+
67
+ if (!current.isLast) return;
68
+
69
+ return current.path;
70
+ };
71
+
72
+ return { addFile, getFilesByExtension, getFileByName };
73
+ };
74
+
75
+ export { FileTree };
package/lib/fs/fsys.ts CHANGED
@@ -1,25 +1,39 @@
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
 
25
+ // resolve module paths relative to the current working directory
26
+ function resolveModulePath(module: string) {
27
+ return path.join(process.cwd(), module);
28
+ }
29
+
30
+ function exists(filepath: string): boolean {
31
+ try {
32
+ fs.access(filepath);
33
+ return true;
34
+ } catch (err) {
35
+ return false;
36
+ }
37
+ }
24
38
 
25
- export { readDir }
39
+ export { readDir, resolveModulePath, exists };
package/lib/http/http.ts CHANGED
@@ -1,77 +1,91 @@
1
- import { httpStatusCodes } from "./status";
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
 
6
+ // http is a collection of functions that return a Response
7
+ // object with the appropriate status code and content type
8
+ // e.g. http.ok() returns a 200 response
3
9
  const http = {
4
- ok: async (msg?: string): Promise<Response> => {
5
- return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
6
- status: 200,
7
- statusText: httpStatusCodes[200],
8
- }));
9
- },
10
- json: async (statusCode: number, data: any): Promise<Response> => {
11
- const jsonString = JSON.stringify(data);
12
- return Promise.resolve(new Response(jsonString, {
13
- status: statusCode,
14
- statusText: httpStatusCodes[statusCode],
15
- headers: {'Content-Type': 'application/json'},
16
- }));
17
- },
18
- html: async (statusCode: number, content: string): Promise<Response> => {
19
- return Promise.resolve(new Response(content, {
20
- status: statusCode,
21
- statusText: httpStatusCodes[statusCode],
22
- headers: {'Content-Type': 'text/html; charset=utf-8'}
23
- }));
24
- },
25
- file: async (statusCode: number, fp: string): Promise<Response> => {
26
- const file = Bun.file(fp);
27
- const exists = await file.exists();
10
+ ok: async (msg?: string): Promise<Response> => {
11
+ return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
12
+ status: 200,
13
+ statusText: httpStatusCodes[200],
14
+ }));
15
+ },
16
+ json: async (statusCode: number, data: any): Promise<Response> => {
17
+ const jsonString = JSON.stringify(data);
18
+ return Promise.resolve(new Response(jsonString, {
19
+ status: statusCode,
20
+ statusText: httpStatusCodes[statusCode],
21
+ headers: {'Content-Type': 'application/json'},
22
+ }));
23
+ },
24
+ html: async (statusCode: number, content: string): Promise<Response> => {
25
+ return Promise.resolve(new Response(content, {
26
+ status: statusCode,
27
+ statusText: httpStatusCodes[statusCode],
28
+ headers: {'Content-Type': 'text/html; charset=utf-8'}
29
+ }));
30
+ },
31
+ file: async (statusCode: number, fp: string): Promise<Response> => {
32
+ const file = Bun.file(fp);
33
+ const exists = await file.exists();
28
34
 
29
- if (!exists) return http.notFound(`File not found: ${fp}`);
35
+ if (!exists) return http.notFound(`File not found: ${fp}`);
30
36
 
31
- const content = await file.arrayBuffer();
32
- if (!content) return http.noContent();
37
+ const content = await file.arrayBuffer();
38
+ if (!content) return http.noContent();
33
39
 
34
- let contentType = 'text/html; charset=utf-9';
40
+ let contentType = 'text/html; charset=utf-9';
35
41
 
36
- if (file.type.includes('image'))
37
- contentType = file.type + '; charset=utf-8';
42
+ if (file.type.includes('image'))
43
+ contentType = file.type + '; charset=utf-8';
38
44
 
39
- return Promise.resolve(new Response(content, {
40
- status: statusCode,
41
- statusText: httpStatusCodes[statusCode],
42
- headers: { 'Content-Type': contentType}
43
- }));
44
- },
45
- noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
46
- status: 204,
47
- statusText: 'no content',
48
- })),
49
- notFound: async(msg?: string): Promise<Response> => {
50
- const response = new Response(msg ?? 'not found', {
51
- status: 404,
52
- statusText: httpStatusCodes[404],
53
- headers: {'Content-Type': 'text/html'},
54
- });
45
+ return Promise.resolve(new Response(content, {
46
+ status: statusCode,
47
+ statusText: httpStatusCodes[statusCode],
48
+ headers: { 'Content-Type': contentType}
49
+ }));
50
+ },
51
+ render: async (component: ReactNode): Promise<Response> => {
52
+ const stream = await renderToReadableStream(component);
53
+ return new Response(stream, {
54
+ status: 200,
55
+ statusText: httpStatusCodes[200],
56
+ headers: {'Content-Type': 'text/html; charset=utf-8'}
57
+ });
58
+ },
59
+ noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
60
+ status: 204,
61
+ statusText: 'no content',
62
+ })),
63
+ notFound: async(msg?: string): Promise<Response> => {
64
+ const response = new Response(msg ?? 'not found', {
65
+ status: 404,
66
+ statusText: httpStatusCodes[404],
67
+ headers: {'Content-Type': 'text/html'},
68
+ });
55
69
 
56
- return Promise.resolve(response);
57
- },
58
- methodNotAllowed: async (msg?: string): Promise<Response> => {
59
- const response = new Response(msg ?? 'method not allowed', {
60
- status: 405,
61
- statusText: httpStatusCodes[405],
62
- headers: {'Content-Type': 'text/html'},
63
- });
70
+ return Promise.resolve(response);
71
+ },
72
+ methodNotAllowed: async (msg?: string): Promise<Response> => {
73
+ const response = new Response(msg ?? 'method not allowed', {
74
+ status: 405,
75
+ statusText: httpStatusCodes[405],
76
+ headers: {'Content-Type': 'text/html'},
77
+ });
64
78
 
65
- return Promise.resolve(response);
66
- },
67
- message: async (status: number, msg?: string): Promise<Response> => {
68
- const response = new Response(msg ?? '?', {
69
- status: status,
70
- statusText: httpStatusCodes[status],
71
- headers: {'Content-Type': 'text/html; charset-utf-8'},
72
- });
73
- return Promise.resolve(response)
74
- },
75
- }
79
+ return Promise.resolve(response);
80
+ },
81
+ message: async (status: number, msg?: string): Promise<Response> => {
82
+ const response = new Response(msg ?? '?', {
83
+ status: status,
84
+ statusText: httpStatusCodes[status],
85
+ headers: {'Content-Type': 'text/html; charset-utf-8'},
86
+ });
87
+ return Promise.resolve(response);
88
+ },
89
+ };
76
90
 
77
- export { http }
91
+ export { http };