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 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 };