bun-router 0.7.4-experimental.5 → 0.7.4-experimental.7
Sign up to get free protection for your applications and to get access to all the features.
- package/examples/ssr/index.ts +1 -1
- package/examples/static.ts +2 -1
- package/lib/fs/filetree.ts +6 -7
- package/lib/fs/fsys.ts +10 -11
- package/lib/logger/color.ts +25 -25
- package/lib/logger/logger.d.ts +1 -1
- package/lib/logger/logger.ts +1 -1
- package/lib/router/context.ts +5 -6
- package/lib/router/{tree.ts → routeTree.ts} +5 -6
- package/lib/router/router.ts +29 -26
- package/package.json +1 -1
- package/tsconfig.json +0 -1
- package/lib/resolver/resolver.test.ts +0 -5
- package/lib/resolver/resolver.ts +0 -43
- package/lib/resolver/testdir/foo.tsx +0 -5
- package/lib/util/strings.ts +0 -3
package/examples/ssr/index.ts
CHANGED
package/examples/static.ts
CHANGED
package/lib/fs/filetree.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import { splitFilePath } from '../fs/fsys';
|
2
1
|
import path from 'node:path';
|
3
2
|
|
4
3
|
type File = {
|
@@ -22,8 +21,8 @@ function createFile(name: string): File {
|
|
22
21
|
const FileTree = (dir: string) => {
|
23
22
|
const root = createFile(dir);
|
24
23
|
|
25
|
-
const addFile = (
|
26
|
-
const pathParts =
|
24
|
+
const addFile = (filepath: string) => {
|
25
|
+
const pathParts = filepath.split(path.sep);
|
27
26
|
let current = root;
|
28
27
|
|
29
28
|
for (let i = 0; i < pathParts.length; i++) {
|
@@ -35,8 +34,8 @@ const FileTree = (dir: string) => {
|
|
35
34
|
}
|
36
35
|
|
37
36
|
current.isLast = true;
|
38
|
-
current.path =
|
39
|
-
current.extension = path.extname(
|
37
|
+
current.path = filepath;
|
38
|
+
current.extension = path.extname(filepath);
|
40
39
|
};
|
41
40
|
|
42
41
|
const getFilesByExtension = (extension: string): string[] => {
|
@@ -53,9 +52,9 @@ const FileTree = (dir: string) => {
|
|
53
52
|
return files;
|
54
53
|
};
|
55
54
|
|
56
|
-
const getFileByName = (
|
55
|
+
const getFileByName = (filepath: string): string | undefined => {
|
57
56
|
let current = root;
|
58
|
-
const pathParts =
|
57
|
+
const pathParts = filepath.split(path.sep);
|
59
58
|
for (let i = 0; i < pathParts.length; i++) {
|
60
59
|
const part = pathParts[i];
|
61
60
|
if (current.children.has(part)) {
|
package/lib/fs/fsys.ts
CHANGED
@@ -22,19 +22,18 @@ async function readDir(dirpath: string, handler: (filepath: string, entry: BunFi
|
|
22
22
|
}
|
23
23
|
}
|
24
24
|
|
25
|
-
//
|
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);
|
33
|
-
}
|
34
|
-
|
25
|
+
// resolve module paths relative to the current working directory
|
35
26
|
function resolveModulePath(module: string) {
|
36
27
|
return path.join(process.cwd(), module);
|
37
28
|
}
|
38
29
|
|
30
|
+
function exists(filepath: string): boolean {
|
31
|
+
try {
|
32
|
+
fs.access(filepath);
|
33
|
+
return true;
|
34
|
+
} catch (err) {
|
35
|
+
return false;
|
36
|
+
}
|
37
|
+
}
|
39
38
|
|
40
|
-
export { readDir,
|
39
|
+
export { readDir, resolveModulePath, exists };
|
package/lib/logger/color.ts
CHANGED
@@ -1,36 +1,36 @@
|
|
1
1
|
const Colors: Record<string,string> = {
|
2
|
-
|
2
|
+
reset: '\x1b[0m',
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
4
|
+
// foreground
|
5
|
+
black: '\x1b[30m',
|
6
|
+
red: '\x1b[31m',
|
7
|
+
green: '\x1b[32m',
|
8
|
+
yellow: '\x1b[33m',
|
9
|
+
blue: '\x1b[34m',
|
10
|
+
magenta: '\x1b[35m',
|
11
|
+
cyan: '\x1b[36m',
|
12
|
+
white: '\x1b[37m',
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
14
|
+
// background
|
15
|
+
bgBlack: '\x1b[40m',
|
16
|
+
bgRed: '\x1b[41m',
|
17
|
+
bgGreen: '\x1b[42m',
|
18
|
+
bgYellow: '\x1b[43m',
|
19
|
+
bgBlue: '\x1b[44m',
|
20
|
+
bgMagenta: '\x1b[45m',
|
21
|
+
bgCyan: '\x1b[46m',
|
22
|
+
bgWhite: '\x1b[47m',
|
23
|
+
} as const;
|
24
24
|
|
25
25
|
|
26
26
|
|
27
27
|
function color(foreground: string, background: string, message: string) {
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
const _foreground = Colors[foreground];
|
29
|
+
const _background = Colors[background];
|
30
|
+
const reset = Colors.reset;
|
31
|
+
return `${_foreground}${_background}${message}${reset}`;
|
32
32
|
}
|
33
33
|
|
34
34
|
|
35
35
|
|
36
|
-
export { color }
|
36
|
+
export { color };
|
package/lib/logger/logger.d.ts
CHANGED
package/lib/logger/logger.ts
CHANGED
@@ -9,7 +9,7 @@ _ _
|
|
9
9
|
|___|___|_|_| |_| |___|___|_| |___|_|
|
10
10
|
|
11
11
|
`;
|
12
|
-
const VERSION = '0.7.4-experimental.
|
12
|
+
const VERSION = '0.7.4-experimental.7';
|
13
13
|
const Logger = (): BunLogger => {
|
14
14
|
return {
|
15
15
|
info: async (statusCode: number, routePath: string, method: string, message?: string) => {
|
package/lib/router/context.ts
CHANGED
@@ -6,8 +6,8 @@ import { http } from './router';
|
|
6
6
|
import { ReactNode } from 'react';
|
7
7
|
|
8
8
|
async function createContext(path: string, route: Route, request: Request): Promise<Context> {
|
9
|
-
const params = extractParams(path, route);
|
10
9
|
const query = new URLSearchParams(path);
|
10
|
+
const params = extractParams(path, route);
|
11
11
|
const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
|
12
12
|
|
13
13
|
return Promise.resolve({
|
@@ -21,16 +21,16 @@ async function createContext(path: string, route: Route, request: Request): Prom
|
|
21
21
|
});
|
22
22
|
}
|
23
23
|
|
24
|
-
function extractParams(
|
24
|
+
function extractParams(pattern: string, route: Route): Map<string, string> {
|
25
25
|
const params: Map<string, string> = new Map();
|
26
|
-
const pathSegments =
|
26
|
+
const pathSegments = pattern.split('/');
|
27
27
|
const routeSegments = route.path.split('/');
|
28
28
|
|
29
29
|
if (pathSegments.length !== routeSegments.length) return params;
|
30
30
|
|
31
31
|
for (let i = 0; i < pathSegments.length; i++) {
|
32
32
|
if (routeSegments[i][0] === ':') {
|
33
|
-
const key = routeSegments[i].
|
33
|
+
const key = routeSegments[i].slice(1);
|
34
34
|
const value = pathSegments[i];
|
35
35
|
params.set(key, value);
|
36
36
|
}
|
@@ -41,8 +41,7 @@ function extractParams(path: string, route: Route): Map<string, string> {
|
|
41
41
|
|
42
42
|
function getContentType(headers: Headers): string {
|
43
43
|
const contentType = headers.get('Content-Type');
|
44
|
-
|
45
|
-
return contentType;
|
44
|
+
return contentType ?? '';
|
46
45
|
}
|
47
46
|
|
48
47
|
function isMultiPartForm(headers: Headers): boolean {
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import { HttpHandler, Route } from './router.d';
|
2
2
|
import { http } from '../http/http';
|
3
3
|
import { createContext } from './context';
|
4
|
-
import { splitPath } from '../util/strings';
|
5
4
|
|
6
5
|
const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
|
7
6
|
const route: Route = {
|
@@ -19,8 +18,8 @@ const createRoute = (path: string, method: string, handler: HttpHandler): Route
|
|
19
18
|
const RouteTree = () => {
|
20
19
|
const root = createRoute('', 'GET', () => http.notFound());
|
21
20
|
|
22
|
-
const addRoute = (
|
23
|
-
const pathParts =
|
21
|
+
const addRoute = (pattern: string, method: string, handler: HttpHandler) => {
|
22
|
+
const pathParts = pattern.split('/');
|
24
23
|
let current = root;
|
25
24
|
|
26
25
|
for (let i = 0; i < pathParts.length; i++) {
|
@@ -36,11 +35,11 @@ const RouteTree = () => {
|
|
36
35
|
|
37
36
|
current.handler = handler;
|
38
37
|
current.isLast = true;
|
39
|
-
current.path =
|
38
|
+
current.path = pattern;
|
40
39
|
};
|
41
40
|
|
42
|
-
function findRoute(
|
43
|
-
const pathParts =
|
41
|
+
function findRoute(pathname: string): Route | undefined {
|
42
|
+
const pathParts = pathname.split('/');
|
44
43
|
let current = root;
|
45
44
|
for (let i = 0; i < pathParts.length; i++) {
|
46
45
|
const part = pathParts[i];
|
package/lib/router/router.ts
CHANGED
@@ -2,21 +2,34 @@ import path from 'path';
|
|
2
2
|
import { Database } from 'bun:sqlite';
|
3
3
|
import { Route, BunRouter, RouterOptions, Options, HttpHandler } from './router.d';
|
4
4
|
import { httpStatusCodes } from '../http/status';
|
5
|
-
import { readDir,
|
5
|
+
import { readDir, exists } from '../fs/fsys';
|
6
6
|
import { Logger, startMessage } from '../logger/logger';
|
7
7
|
import { http } from '../http/http';
|
8
|
-
import { RouteTree } from './
|
8
|
+
import { RouteTree } from './routeTree';
|
9
9
|
import { createContext } from './context';
|
10
10
|
|
11
11
|
const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
|
12
12
|
const { addRoute, findRoute } = RouteTree();
|
13
13
|
const logger = Logger();
|
14
14
|
|
15
|
-
async function loadComponent(name: string) {
|
16
|
-
const module = await import(name);
|
15
|
+
async function loadComponent(root: string, name: string) {
|
16
|
+
const module = await import(path.join(process.cwd(), root, name));
|
17
17
|
return module.default;
|
18
18
|
}
|
19
19
|
|
20
|
+
function normalizePath(pattern: string, pathname: string) {
|
21
|
+
const extension = path.extname(pathname);
|
22
|
+
let base = path.basename(pathname);
|
23
|
+
|
24
|
+
if (extension === '.html' || extension === '.tsx') base = base.replace(extension, '');
|
25
|
+
|
26
|
+
let patternPath = [pattern, base].join('/');
|
27
|
+
|
28
|
+
if (base === 'index') patternPath = pattern;
|
29
|
+
|
30
|
+
return { patternPath, extension, base }
|
31
|
+
}
|
32
|
+
|
20
33
|
return {
|
21
34
|
// add a route to the router tree
|
22
35
|
add: (pattern, method, callback) => { addRoute(pattern, method, callback); },
|
@@ -26,35 +39,23 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
26
39
|
delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback); },
|
27
40
|
|
28
41
|
// add static routes to the router tree
|
29
|
-
// .tsx and .html are rendered as components
|
42
|
+
// .tsx and .html are rendered as components, or pages
|
30
43
|
// all other file extensions are served as files
|
31
44
|
// the root directory is traversed recursively
|
32
45
|
static: async (pattern: string, root: string) => {
|
46
|
+
if (!exists(root)) console.log(`Cannot find directory ${root}`);
|
33
47
|
await readDir(root, async (fp) => {
|
34
|
-
const
|
35
|
-
|
36
|
-
let base = path.basename(fp);
|
37
|
-
|
38
|
-
//FIXME: this can be improved
|
39
|
-
if (ext === '.html' || ext === '.tsx') base = base.replace(ext, '');
|
40
|
-
|
41
|
-
if (pattern[0] !== '/') pattern = '/' + pattern;
|
42
|
-
|
43
|
-
let patternPath = pattern + '/' + base;
|
44
|
-
|
45
|
-
if (base === 'index') patternPath = pattern;
|
46
|
-
|
47
|
-
const purePath = path.join(root, patternPath);
|
48
|
+
const { patternPath, extension, base } = normalizePath(pattern, fp);
|
48
49
|
|
49
50
|
const route: Route = {
|
50
51
|
children: new Map(),
|
51
52
|
dynamicPath: '',
|
52
53
|
isLast: true,
|
53
|
-
path: patternPath,
|
54
|
+
path: patternPath.slice(1),
|
54
55
|
method: 'GET',
|
55
56
|
handler: async () => {
|
56
|
-
if (
|
57
|
-
const component = await loadComponent(
|
57
|
+
if (extension === '.tsx') {
|
58
|
+
const component = await loadComponent(root, base);
|
58
59
|
return await http.render(component());
|
59
60
|
} else {
|
60
61
|
return await http.file(200, fp);
|
@@ -62,6 +63,8 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
62
63
|
},
|
63
64
|
};
|
64
65
|
|
66
|
+
console.log(route.path);
|
67
|
+
|
65
68
|
addRoute(route.path, 'GET', route.handler);
|
66
69
|
});
|
67
70
|
},
|
@@ -75,7 +78,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
75
78
|
...options,
|
76
79
|
async fetch(req) {
|
77
80
|
const url = new URL(req.url);
|
78
|
-
const
|
81
|
+
const pathname = url.pathname;
|
79
82
|
|
80
83
|
// set the database
|
81
84
|
if (options) {
|
@@ -83,16 +86,16 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
83
86
|
opts.db = o.db;
|
84
87
|
}
|
85
88
|
|
86
|
-
const route = findRoute(
|
89
|
+
const route = findRoute(pathname);
|
87
90
|
|
88
|
-
// if the route exists,
|
91
|
+
// if the route exists, call the handler
|
89
92
|
if (route) {
|
90
93
|
if (route.method !== req.method) {
|
91
94
|
logger.info(405, url.pathname, req.method, httpStatusCodes[405]);
|
92
95
|
return Promise.resolve(http.methodNotAllowed());
|
93
96
|
}
|
94
97
|
|
95
|
-
const context = await createContext(
|
98
|
+
const context = await createContext(pathname, route, req);
|
96
99
|
context.db = new Database(opts.db);
|
97
100
|
|
98
101
|
const response = await route.handler(context);
|
package/package.json
CHANGED
package/tsconfig.json
CHANGED
package/lib/resolver/resolver.ts
DELETED
@@ -1,43 +0,0 @@
|
|
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
|
-
|
17
|
-
if (!await doesExist(root)) {
|
18
|
-
throw new Error(`Directory ${root} does not exist`);
|
19
|
-
}
|
20
|
-
|
21
|
-
const tree = await createFileTree(root);
|
22
|
-
const files = tree.getFilesByExtension('.tsx');
|
23
|
-
const modules: ComponentType[] = [];
|
24
|
-
|
25
|
-
for (const file of files) {
|
26
|
-
const module = await import(file);
|
27
|
-
modules.push(module);
|
28
|
-
}
|
29
|
-
return modules;
|
30
|
-
}
|
31
|
-
|
32
|
-
async function doesExist(root: string): Promise<boolean> {
|
33
|
-
const file = Bun.file(root);
|
34
|
-
return await file.exists();
|
35
|
-
}
|
36
|
-
|
37
|
-
async function isDir(root: string): Promise<boolean> {
|
38
|
-
const file = Bun.file(root);
|
39
|
-
return await file.isDir();
|
40
|
-
}
|
41
|
-
|
42
|
-
|
43
|
-
export { resolveModules };
|
package/lib/util/strings.ts
DELETED