bun-router 0.7.4-experimental.8 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintrc.json +3 -1
- package/examples/basic.ts +1 -1
- package/examples/ssr/index.ts +2 -1
- package/examples/static.ts +1 -1
- package/examples/tsx/components/user.tsx +7 -0
- package/examples/tsx/index.ts +20 -0
- package/index.ts +1 -0
- package/lib/http/http.ts +8 -1
- package/lib/http/status.ts +64 -64
- package/lib/logger/color.ts +3 -3
- package/lib/logger/logger.d.ts +5 -4
- package/lib/logger/logger.ts +35 -9
- package/lib/router/context.ts +9 -2
- package/lib/router/routeTree.ts +28 -3
- package/lib/router/router.d.ts +2 -1
- package/lib/router/router.ts +33 -16
- package/package.json +9 -8
- package/tests/router.test.ts +21 -20
- package/tsconfig.json +2 -0
package/.eslintrc.json
CHANGED
package/examples/basic.ts
CHANGED
package/examples/ssr/index.ts
CHANGED
package/examples/static.ts
CHANGED
@@ -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
|
+
|
package/index.ts
CHANGED
package/lib/http/http.ts
CHANGED
@@ -3,6 +3,9 @@ import { httpStatusCodes } from './status';
|
|
3
3
|
import { ReactNode } from 'react';
|
4
4
|
import { renderToReadableStream } from 'react-dom/server';
|
5
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
|
6
9
|
const http = {
|
7
10
|
ok: async (msg?: string): Promise<Response> => {
|
8
11
|
return Promise.resolve(new Response(msg ?? httpStatusCodes[200], {
|
@@ -47,7 +50,11 @@ const http = {
|
|
47
50
|
},
|
48
51
|
render: async (component: ReactNode): Promise<Response> => {
|
49
52
|
const stream = await renderToReadableStream(component);
|
50
|
-
return new Response(stream, {
|
53
|
+
return new Response(stream, {
|
54
|
+
status: 200,
|
55
|
+
statusText: httpStatusCodes[200],
|
56
|
+
headers: {'Content-Type': 'text/html; charset=utf-8'}
|
57
|
+
});
|
51
58
|
},
|
52
59
|
noContent: async (): Promise<Response> => Promise.resolve(new Response('no content', {
|
53
60
|
status: 204,
|
package/lib/http/status.ts
CHANGED
@@ -1,66 +1,66 @@
|
|
1
1
|
const httpStatusCodes: { [key: number]: string } = {
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
2
|
+
100: 'Continue',
|
3
|
+
101: 'Switching Protocols',
|
4
|
+
102: 'Processing',
|
5
|
+
103: 'Early Hints',
|
6
|
+
200: 'OK',
|
7
|
+
201: 'Created',
|
8
|
+
202: 'Accepted',
|
9
|
+
203: 'Non-Authoritative Information',
|
10
|
+
204: 'No Content',
|
11
|
+
205: 'Reset Content',
|
12
|
+
206: 'Partial Content',
|
13
|
+
207: 'Multi-Status',
|
14
|
+
208: 'Already Reported',
|
15
|
+
226: 'IM Used',
|
16
|
+
300: 'Multiple Choices',
|
17
|
+
301: 'Moved Permanently',
|
18
|
+
302: 'Found',
|
19
|
+
303: 'See Other',
|
20
|
+
304: 'Not Modified',
|
21
|
+
305: 'Use Proxy',
|
22
|
+
307: 'Temporary Redirect',
|
23
|
+
308: 'Permanent Redirect',
|
24
|
+
400: 'Bad Request',
|
25
|
+
401: 'Unauthorized',
|
26
|
+
402: 'Payment Required',
|
27
|
+
403: 'Forbidden',
|
28
|
+
404: 'Not Found',
|
29
|
+
405: 'Method Not Allowed',
|
30
|
+
406: 'Not Acceptable',
|
31
|
+
407: 'Proxy Authentication Required',
|
32
|
+
408: 'Request Timeout',
|
33
|
+
409: 'Conflict',
|
34
|
+
410: 'Gone',
|
35
|
+
411: 'Length Required',
|
36
|
+
412: 'Precondition Failed',
|
37
|
+
413: 'Payload Too Large',
|
38
|
+
414: 'URI Too Long',
|
39
|
+
415: 'Unsupported Media Type',
|
40
|
+
416: 'Range Not Satisfiable',
|
41
|
+
417: 'Expectation Failed',
|
42
|
+
418: 'I\'m a Teapot',
|
43
|
+
421: 'Misdirected Request',
|
44
|
+
422: 'Unprocessable Entity',
|
45
|
+
423: 'Locked',
|
46
|
+
424: 'Failed Dependency',
|
47
|
+
425: 'Too Early',
|
48
|
+
426: 'Upgrade Required',
|
49
|
+
428: 'Precondition Required',
|
50
|
+
429: 'Too Many Requests',
|
51
|
+
431: 'Request Header Fields Too Large',
|
52
|
+
451: 'Unavailable For Legal Reasons',
|
53
|
+
500: 'Internal Server Error',
|
54
|
+
501: 'Not Implemented',
|
55
|
+
502: 'Bad Gateway',
|
56
|
+
503: 'Service Unavailable',
|
57
|
+
504: 'Gateway Timeout',
|
58
|
+
505: 'HTTP Version Not Supported',
|
59
|
+
506: 'Variant Also Negotiates',
|
60
|
+
507: 'Insufficient Storage',
|
61
|
+
508: 'Loop Detected',
|
62
|
+
510: 'Not Extended',
|
63
|
+
511: 'Network Authentication Required',
|
64
|
+
};
|
65
65
|
|
66
|
-
|
66
|
+
export { httpStatusCodes };
|
package/lib/logger/color.ts
CHANGED
@@ -23,12 +23,12 @@ const Colors: Record<string,string> = {
|
|
23
23
|
} as const;
|
24
24
|
|
25
25
|
|
26
|
-
|
27
|
-
function color(foreground: string, background: string,
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
27
|
+
function color(foreground: string, background: string, ...args: any[]) {
|
28
28
|
const _foreground = Colors[foreground];
|
29
29
|
const _background = Colors[background];
|
30
30
|
const reset = Colors.reset;
|
31
|
-
return `${_foreground}${_background}${
|
31
|
+
return `${_foreground}${_background}${args.map(String).join('')}${reset}`;
|
32
32
|
}
|
33
33
|
|
34
34
|
|
package/lib/logger/logger.d.ts
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
type BunLogger = {
|
2
|
-
info: (statusCode: number, routePath: string, method: string,
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
info: (statusCode: number, routePath: string, method: string, ...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
3
|
+
|
4
|
+
error: (statusCode: number, routePath: string, method: string, error: Error) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
5
|
+
warn: (...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
6
|
+
message: (...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
6
7
|
}
|
7
8
|
|
8
9
|
export { BunLogger };
|
package/lib/logger/logger.ts
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import { color } from './color';
|
2
|
-
import { BunLogger } from './logger.d';
|
3
2
|
|
4
3
|
|
5
4
|
const TITLE = `
|
@@ -9,17 +8,34 @@ _ _
|
|
9
8
|
|___|___|_|_| |_| |___|___|_| |___|_|
|
10
9
|
|
11
10
|
`;
|
12
|
-
const VERSION = '0.
|
13
|
-
const Logger = (
|
11
|
+
const VERSION = '0.8.0';
|
12
|
+
const Logger = (enableFileLogging: boolean) => {
|
13
|
+
const file = Bun.file('bun-router.log');
|
14
|
+
const writer = enableFileLogging ? file.writer() : null;
|
15
|
+
|
16
|
+
function stripAnsi(str: string) {
|
17
|
+
const ansiRegex = /\u001B\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]/g;
|
18
|
+
return str.replace(ansiRegex, '');
|
19
|
+
}
|
20
|
+
|
21
|
+
async function write(message: string) {
|
22
|
+
await Bun.write(Bun.stdout, message);
|
23
|
+
if (writer) {
|
24
|
+
writer.write(stripAnsi(message));
|
25
|
+
writer.flush();
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
14
29
|
return {
|
15
|
-
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
31
|
+
info: async (statusCode: number, routePath: string, method: string, ...args: any[]) => {
|
16
32
|
const { stamp } = timestamp((new Date(Date.now())));
|
17
33
|
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
|
18
34
|
const rp = color('white', 'bgBlack', routePath);
|
19
35
|
|
20
|
-
message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${
|
36
|
+
const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${args.map(String).join(' ')}\n'}`;
|
21
37
|
|
22
|
-
await
|
38
|
+
await write(message);
|
23
39
|
|
24
40
|
},
|
25
41
|
error: async (statusCode: number, routePath: string, method: string, error: Error) => {
|
@@ -29,7 +45,7 @@ const Logger = (): BunLogger => {
|
|
29
45
|
|
30
46
|
const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' -> ' : ' <-'} ${error.message}\n`;
|
31
47
|
|
32
|
-
await
|
48
|
+
await write(message);
|
33
49
|
},
|
34
50
|
warn: async (message: string) => {
|
35
51
|
const { stamp } = timestamp((new Date(Date.now())));
|
@@ -38,7 +54,7 @@ const Logger = (): BunLogger => {
|
|
38
54
|
|
39
55
|
message = `${source} : ${messageColor}\n`;
|
40
56
|
|
41
|
-
await
|
57
|
+
await write(message);
|
42
58
|
},
|
43
59
|
message: async (message: string) => {
|
44
60
|
const { stamp } = timestamp((new Date(Date.now())));
|
@@ -47,8 +63,18 @@ const Logger = (): BunLogger => {
|
|
47
63
|
|
48
64
|
message = `${source}: ${messageColor}\n`;
|
49
65
|
|
50
|
-
await
|
66
|
+
await write(message);
|
51
67
|
},
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
69
|
+
log: async(args: any[]) => {
|
70
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
71
|
+
const source = color('black', 'bgCyan', `[message ${stamp}]`);
|
72
|
+
const messageColor = color('yellow', 'bgBlack', args);
|
73
|
+
|
74
|
+
const message = `${source}: ${messageColor}\n`;
|
75
|
+
|
76
|
+
await write(message);
|
77
|
+
}
|
52
78
|
};
|
53
79
|
};
|
54
80
|
|
package/lib/router/context.ts
CHANGED
@@ -5,7 +5,8 @@ import { Logger } from '../logger/logger';
|
|
5
5
|
import { http } from './router';
|
6
6
|
import { ReactNode } from 'react';
|
7
7
|
|
8
|
-
|
8
|
+
// createContext creates a context object
|
9
|
+
async function createContext(path: string, route: Route, request: Request, enableFileLogging: boolean): Promise<Context> {
|
9
10
|
const query = new URLSearchParams(path);
|
10
11
|
const params = extractParams(path, route);
|
11
12
|
const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
|
@@ -15,12 +16,15 @@ async function createContext(path: string, route: Route, request: Request): Prom
|
|
15
16
|
request,
|
16
17
|
query,
|
17
18
|
formData,
|
18
|
-
logger: Logger(),
|
19
|
+
logger: Logger(enableFileLogging),
|
19
20
|
json: (statusCode: number, data: any) => http.json(statusCode, data),
|
20
21
|
render: async (component: ReactNode) => await renderStream(component),
|
21
22
|
});
|
22
23
|
}
|
23
24
|
|
25
|
+
// extractParams extracts the parameters from the path
|
26
|
+
// and returns a map of key/value pairs
|
27
|
+
// e.g. /users/:id => /users/123 => { id: 123 }
|
24
28
|
function extractParams(pattern: string, route: Route): Map<string, string> {
|
25
29
|
const params: Map<string, string> = new Map();
|
26
30
|
const pathSegments = pattern.split('/');
|
@@ -39,16 +43,19 @@ function extractParams(pattern: string, route: Route): Map<string, string> {
|
|
39
43
|
return params;
|
40
44
|
}
|
41
45
|
|
46
|
+
// getContentType returns the content type from the headers
|
42
47
|
function getContentType(headers: Headers): string {
|
43
48
|
const contentType = headers.get('Content-Type');
|
44
49
|
return contentType ?? '';
|
45
50
|
}
|
46
51
|
|
52
|
+
// isMultiPartForm returns true if the content type is multipart/form-data
|
47
53
|
function isMultiPartForm(headers: Headers): boolean {
|
48
54
|
const contentType = getContentType(headers);
|
49
55
|
return contentType.includes('multipart/form-data');
|
50
56
|
}
|
51
57
|
|
58
|
+
// renderStream renders the component to a readable stream
|
52
59
|
async function renderStream(children: ReactNode) {
|
53
60
|
const stream = await renderToReadableStream(children);
|
54
61
|
return new Response(stream, { headers: { 'Content-Type': 'text/html' } });
|
package/lib/router/routeTree.ts
CHANGED
@@ -18,7 +18,8 @@ const createRoute = (path: string, method: string, handler: HttpHandler): Route
|
|
18
18
|
const RouteTree = () => {
|
19
19
|
const root = createRoute('', 'GET', () => http.notFound());
|
20
20
|
|
21
|
-
|
21
|
+
function addRoute (pattern: string, method: string, handler: HttpHandler){
|
22
|
+
console.log(pattern);
|
22
23
|
const pathParts = pattern.split('/');
|
23
24
|
let current = root;
|
24
25
|
|
@@ -36,7 +37,7 @@ const RouteTree = () => {
|
|
36
37
|
current.handler = handler;
|
37
38
|
current.isLast = true;
|
38
39
|
current.path = pattern;
|
39
|
-
}
|
40
|
+
}
|
40
41
|
|
41
42
|
function findRoute(pathname: string): Route | undefined {
|
42
43
|
const pathParts = pathname.split('/');
|
@@ -54,7 +55,31 @@ const RouteTree = () => {
|
|
54
55
|
return current;
|
55
56
|
}
|
56
57
|
|
57
|
-
|
58
|
+
function size() {
|
59
|
+
let count = 0;
|
60
|
+
function traverse(route: Route) {
|
61
|
+
count++;
|
62
|
+
for (const child of route.children.values()) {
|
63
|
+
traverse(child);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
traverse(root);
|
67
|
+
return count;
|
68
|
+
}
|
69
|
+
|
70
|
+
function list() {
|
71
|
+
const routes: Route[] = [];
|
72
|
+
function traverse(route: Route) {
|
73
|
+
routes.push(route);
|
74
|
+
for (const child of route.children.values()) {
|
75
|
+
traverse(child);
|
76
|
+
}
|
77
|
+
}
|
78
|
+
traverse(root);
|
79
|
+
return routes;
|
80
|
+
}
|
81
|
+
|
82
|
+
return { addRoute, findRoute, size, list };
|
58
83
|
|
59
84
|
};
|
60
85
|
|
package/lib/router/router.d.ts
CHANGED
package/lib/router/router.ts
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
import path from 'path';
|
2
2
|
import { Database } from 'bun:sqlite';
|
3
|
-
import { Route, BunRouter, RouterOptions, Options
|
3
|
+
import { Route, BunRouter, RouterOptions, Options } from './router.d';
|
4
4
|
import { httpStatusCodes } from '../http/status';
|
5
|
-
import { readDir
|
5
|
+
import { readDir } from '../fs/fsys';
|
6
6
|
import { Logger, startMessage } from '../logger/logger';
|
7
7
|
import { http } from '../http/http';
|
8
8
|
import { RouteTree } from './routeTree';
|
@@ -10,16 +10,18 @@ import { createContext } from './context';
|
|
10
10
|
|
11
11
|
const Router: BunRouter = (port?: number | string, options?: RouterOptions<Options>) => {
|
12
12
|
const { addRoute, findRoute } = RouteTree();
|
13
|
-
const logger = Logger();
|
13
|
+
const logger = Logger(options?.enableFileLogging ?? false);
|
14
14
|
|
15
|
+
// load a component from the root directory relative to the cwd
|
15
16
|
async function loadComponent(root: string, name: string) {
|
16
17
|
const module = await import(path.join(process.cwd(), root, name));
|
17
18
|
return module.default;
|
18
19
|
}
|
19
20
|
|
20
|
-
|
21
|
+
// extract the path, extension, and base name from a file path
|
22
|
+
function extractPathExtBase(pattern: string, pathname: string) {
|
21
23
|
const extension = path.extname(pathname);
|
22
|
-
let base = path.basename(pathname);
|
24
|
+
let base = encodeURIComponent(path.basename(pathname));
|
23
25
|
|
24
26
|
if (extension === '.html' || extension === '.tsx') base = base.replace(extension, '');
|
25
27
|
|
@@ -27,13 +29,19 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
27
29
|
|
28
30
|
if (base === 'index') patternPath = pattern;
|
29
31
|
|
30
|
-
return { patternPath, extension, base }
|
32
|
+
return { patternPath, extension, base };
|
31
33
|
}
|
32
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
|
+
|
33
41
|
return {
|
34
42
|
// add a route to the router tree
|
35
43
|
add: (pattern, method, callback) => { addRoute(pattern, method, callback); },
|
36
|
-
get: (pattern
|
44
|
+
get: (pattern, callback) => { addRoute(pattern, 'GET', callback); },
|
37
45
|
post: (pattern, callback) => { addRoute(pattern, 'POST', callback); },
|
38
46
|
put: (pattern, callback) => { addRoute(pattern, 'PUT', callback);},
|
39
47
|
delete: (pattern, callback) => { addRoute(pattern, 'DELETE', callback); },
|
@@ -43,15 +51,14 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
43
51
|
// all other file extensions are served as files
|
44
52
|
// the root directory is traversed recursively
|
45
53
|
static: async (pattern: string, root: string) => {
|
46
|
-
if (!exists(root)) console.
|
54
|
+
if (!exists(root)) return console.error(`Directory not found: ${root}`);
|
47
55
|
await readDir(root, async (fp) => {
|
48
|
-
const { patternPath, extension, base } =
|
49
|
-
|
56
|
+
const { patternPath, extension, base } = extractPathExtBase(pattern, fp);
|
50
57
|
const route: Route = {
|
51
58
|
children: new Map(),
|
52
|
-
dynamicPath:
|
59
|
+
dynamicPath: pattern,
|
53
60
|
isLast: true,
|
54
|
-
path: patternPath.slice(1), // remove the leading '/'
|
61
|
+
path: patternPath.startsWith('//') ? patternPath.slice(1) : patternPath, // remove the leading '/' if it exists
|
55
62
|
method: 'GET',
|
56
63
|
handler: async () => {
|
57
64
|
if (extension === '.tsx') {
|
@@ -65,11 +72,12 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
65
72
|
|
66
73
|
addRoute(route.path, 'GET', route.handler);
|
67
74
|
});
|
75
|
+
|
68
76
|
},
|
69
|
-
// start
|
77
|
+
// start listening for requests
|
70
78
|
serve: () => {
|
71
79
|
startMessage(port ?? 3000);
|
72
|
-
const opts: Options = { db: ':memory:' };
|
80
|
+
const opts: Options = { db: ':memory:', enableFileLogging: false };
|
73
81
|
|
74
82
|
Bun.serve({
|
75
83
|
port: port ?? 3000,
|
@@ -82,6 +90,7 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
82
90
|
if (options) {
|
83
91
|
const o = options as Options;
|
84
92
|
opts.db = o.db;
|
93
|
+
opts.enableFileLogging = o.enableFileLogging;
|
85
94
|
}
|
86
95
|
|
87
96
|
const route = findRoute(pathname);
|
@@ -93,20 +102,28 @@ const Router: BunRouter = (port?: number | string, options?: RouterOptions<Optio
|
|
93
102
|
return Promise.resolve(http.methodNotAllowed());
|
94
103
|
}
|
95
104
|
|
96
|
-
|
105
|
+
// create a context for the handler
|
106
|
+
const context = await createContext(pathname, route, req, opts.enableFileLogging);
|
97
107
|
context.db = new Database(opts.db);
|
98
108
|
|
109
|
+
// call the handler
|
99
110
|
const response = await route.handler(context);
|
100
111
|
|
101
112
|
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
102
113
|
return Promise.resolve(response);
|
103
114
|
}
|
104
115
|
|
105
|
-
// if no route is found, return 404
|
116
|
+
// if no route is found, return 404
|
106
117
|
const response = await http.notFound();
|
107
118
|
|
108
119
|
logger.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
|
109
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
|
+
});
|
110
127
|
}
|
111
128
|
});
|
112
129
|
},
|
package/package.json
CHANGED
@@ -3,21 +3,22 @@
|
|
3
3
|
"module": "index.ts",
|
4
4
|
"type": "module",
|
5
5
|
"devDependencies": {
|
6
|
-
"@types/react
|
6
|
+
"@types/react": "^18.2.23",
|
7
|
+
"@types/react-dom": "^18.2.8",
|
7
8
|
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
8
9
|
"@typescript-eslint/parser": "^6.7.0",
|
9
10
|
"bun-types": "latest",
|
10
11
|
"eslint": "^8.49.0",
|
11
|
-
"eslint-plugin-react": "^7.33.2"
|
12
|
-
"react-dom": "^18.2.0"
|
12
|
+
"eslint-plugin-react": "^7.33.2"
|
13
13
|
},
|
14
14
|
"peerDependencies": {
|
15
15
|
"typescript": "^5.0.0"
|
16
16
|
},
|
17
|
-
"version": "0.
|
17
|
+
"version": "0.8.0",
|
18
18
|
"dependencies": {
|
19
|
-
"@types/react": "^18.2.22",
|
20
19
|
"eslint-plugin-react-hooks": "^4.6.0",
|
21
|
-
"react": "^18.2.0"
|
22
|
-
|
23
|
-
}
|
20
|
+
"react": "^18.2.0",
|
21
|
+
"react-dom": "^18.2.0"
|
22
|
+
},
|
23
|
+
"types": "./lib/router.d.ts"
|
24
|
+
}
|
package/tests/router.test.ts
CHANGED
@@ -1,35 +1,36 @@
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
2
2
|
|
3
3
|
describe('Router', () => {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
11
|
+
const text = await new Response(proc.stdout).text();
|
12
12
|
|
13
|
-
|
13
|
+
const hasFailed = text.includes('Failed');
|
14
14
|
|
15
|
-
|
15
|
+
if (hasFailed) console.log(text);
|
16
16
|
|
17
|
-
|
17
|
+
expect(hasFailed).toBe(false);
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
proc.kill(0);
|
20
|
+
});
|
21
21
|
|
22
|
-
|
23
|
-
|
22
|
+
test('Static', async() => {
|
23
|
+
const proc = Bun.spawn(['./tests/static.test.sh']);
|
24
24
|
|
25
|
-
|
25
|
+
const text = await new Response(proc.stdout).text();
|
26
26
|
|
27
|
-
|
28
|
-
|
27
|
+
const hasFailed = text.includes('Failed');
|
28
|
+
if (hasFailed) console.log(text);
|
29
29
|
|
30
|
-
|
30
|
+
expect(hasFailed).toBe(false);
|
31
31
|
|
32
|
-
|
33
|
-
|
32
|
+
proc.kill(0);
|
33
|
+
});
|
34
34
|
});
|
35
35
|
|
36
|
+
|
package/tsconfig.json
CHANGED