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 +35 -0
- package/README.md +28 -0
- package/bun.lockb +0 -0
- package/examples/basic.ts +4 -4
- package/examples/dynamic.ts +8 -8
- package/examples/logger.ts +2 -2
- package/examples/ssr/index.ts +8 -0
- package/examples/ssr/pages/foo.tsx +7 -0
- package/examples/ssr/pages/home.tsx +7 -0
- package/examples/static.ts +1 -0
- package/examples/todo.ts +22 -22
- package/examples/tsx/components/user.tsx +7 -0
- package/examples/tsx/index.ts +20 -0
- package/lib/fs/filetree.ts +75 -0
- package/lib/fs/fsys.ts +25 -11
- package/lib/http/http.ts +81 -67
- package/lib/http/status.ts +64 -64
- package/lib/logger/color.ts +25 -25
- package/lib/logger/logger.d.ts +1 -1
- package/lib/logger/logger.ts +86 -70
- package/lib/router/context.ts +50 -37
- package/lib/router/routeTree.ts +86 -0
- package/lib/router/router.d.ts +6 -4
- package/lib/router/router.ts +122 -89
- package/package.json +13 -2
- package/tests/router.test.ts +21 -20
- package/tsconfig.json +5 -3
- package/examples/db.ts +0 -10
- package/lib/router/tree.ts +0 -63
- package/lib/util/strings.ts +0 -3
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
@@ -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
@@ -1,5 +1,4 @@
|
|
1
1
|
import { color } from './color';
|
2
|
-
import { BunLogger } from './logger.d';
|
3
2
|
|
4
3
|
|
5
4
|
const TITLE = `
|
@@ -8,87 +7,104 @@ _ _
|
|
8
7
|
| . | | | | | _| . | | | _| -_| _|
|
9
8
|
|___|___|_|_| |_| |___|___|_| |___|_|
|
10
9
|
|
11
|
-
|
12
|
-
const VERSION = '0.7.
|
13
|
-
const Logger = (
|
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
|
-
|
10
|
+
`;
|
11
|
+
const VERSION = '0.7.4-experimental.11';
|
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
|
+
|
29
|
+
return {
|
30
|
+
info: async (statusCode: number, routePath: string, method: string, message?: string) => {
|
31
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
32
|
+
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
|
33
|
+
const rp = color('white', 'bgBlack', routePath);
|
34
|
+
|
35
|
+
message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' ->' : ' <-'} ${method} ${ message ?? ''}\n`;
|
36
|
+
|
37
|
+
await write(message);
|
38
|
+
|
39
|
+
},
|
40
|
+
error: async (statusCode: number, routePath: string, method: string, error: Error) => {
|
41
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
42
|
+
const source = color('black', 'bgRed', `[error ${stamp}]`);
|
43
|
+
const rp = color('white', 'bgBlack', routePath);
|
44
|
+
|
45
|
+
const message = `${source}: ${setColor(statusCode)}: ${rp} ${(method === 'GET') ? ' -> ' : ' <-'} ${error.message}\n`;
|
46
|
+
|
47
|
+
await write(message);
|
48
|
+
},
|
49
|
+
warn: async (message: string) => {
|
50
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
51
|
+
const source = color('black', 'bgYellow', `[warning ${stamp}]`);
|
52
|
+
const messageColor = color('yellow', 'bgBlack', message);
|
53
|
+
|
54
|
+
message = `${source} : ${messageColor}\n`;
|
55
|
+
|
56
|
+
await write(message);
|
57
|
+
},
|
58
|
+
message: async (message: string) => {
|
59
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
60
|
+
const source = color('black', 'bgCyan', `[message ${stamp}]`);
|
61
|
+
const messageColor = color('yellow', 'bgBlack', message);
|
62
|
+
|
63
|
+
message = `${source}: ${messageColor}\n`;
|
64
|
+
|
65
|
+
await write(message);
|
66
|
+
},
|
67
|
+
|
68
|
+
};
|
69
|
+
};
|
54
70
|
|
55
71
|
function timestamp(date: Date) {
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
72
|
+
const month = pad(date.getMonth());
|
73
|
+
const day = pad(date.getDate());
|
74
|
+
const hour = pad(date.getHours());
|
75
|
+
const minute = pad(date.getMinutes());
|
76
|
+
const seconds = pad(date.getSeconds());
|
77
|
+
const stamp = `${hour}:${minute}:${seconds}`;
|
78
|
+
|
79
|
+
return {month, day, hour, minute, stamp};
|
64
80
|
}
|
65
81
|
|
66
82
|
function setColor(n: number, text?: string){
|
67
|
-
|
83
|
+
const s = ` [${String(n)}${text ?? ''}] `;
|
68
84
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
85
|
+
if (n < 100) return color('black', 'bgYellow', s);
|
86
|
+
else if (n >= 100 && n < 200) return color('black', 'bgCyan', s);
|
87
|
+
else if (n >= 200 && n < 300) return color('black', 'bgGreen', s);
|
88
|
+
else if (n >= 300 && n < 400) return color('black', 'bgRed', s);
|
89
|
+
else if (n >= 400 && n < 500) return color('black', 'bgRed', s);
|
90
|
+
else if (n >= 500) return color('white', 'bgRed', s);
|
75
91
|
|
76
|
-
|
92
|
+
return color('white', 'bgBlack', `[${s}]`).trim();
|
77
93
|
}
|
78
94
|
|
79
95
|
function startMessage(port: number | string) {
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
96
|
+
const { stamp } = timestamp((new Date(Date.now())));
|
97
|
+
const source = color('green', 'bgBlack', `[bun-router ${stamp}]`);
|
98
|
+
const portColor = color('green', 'bgBlack', String(port));
|
99
|
+
const msg = `${source}: Starting Server on :${portColor}\n`;
|
100
|
+
const version = color('red', 'bgBlack', `v${VERSION}\n`);
|
101
|
+
|
102
|
+
Bun.write(Bun.stdout, TITLE + '\n' + version);
|
103
|
+
Bun.write(Bun.stdout, msg);
|
88
104
|
}
|
89
105
|
|
90
106
|
function pad(n: number) {
|
91
|
-
|
107
|
+
return String(n).padStart(2, '0');
|
92
108
|
}
|
93
109
|
|
94
|
-
export { Logger, startMessage }
|
110
|
+
export { Logger, startMessage };
|
package/lib/router/context.ts
CHANGED
@@ -1,51 +1,64 @@
|
|
1
|
-
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
import { Route, Context } from './router.d';
|
3
|
+
import { renderToReadableStream } from 'react-dom/server';
|
4
|
+
import { Logger } from '../logger/logger';
|
5
|
+
import { http } from './router';
|
6
|
+
import { ReactNode } from 'react';
|
7
|
+
|
8
|
+
// createContext creates a context object
|
9
|
+
async function createContext(path: string, route: Route, request: Request, enableFileLogging: boolean): Promise<Context> {
|
10
|
+
const query = new URLSearchParams(path);
|
11
|
+
const params = extractParams(path, route);
|
12
|
+
const formData = isMultiPartForm(request.headers) ? await request.formData() : new FormData();
|
13
|
+
|
14
|
+
return Promise.resolve({
|
15
|
+
params,
|
16
|
+
request,
|
17
|
+
query,
|
18
|
+
formData,
|
19
|
+
logger: Logger(enableFileLogging),
|
20
|
+
json: (statusCode: number, data: any) => http.json(statusCode, data),
|
21
|
+
render: async (component: ReactNode) => await renderStream(component),
|
22
|
+
});
|
18
23
|
}
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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 }
|
28
|
+
function extractParams(pattern: string, route: Route): Map<string, string> {
|
29
|
+
const params: Map<string, string> = new Map();
|
30
|
+
const pathSegments = pattern.split('/');
|
31
|
+
const routeSegments = route.path.split('/');
|
24
32
|
|
25
|
-
|
33
|
+
if (pathSegments.length !== routeSegments.length) return params;
|
26
34
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
35
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
36
|
+
if (routeSegments[i][0] === ':') {
|
37
|
+
const key = routeSegments[i].slice(1);
|
38
|
+
const value = pathSegments[i];
|
39
|
+
params.set(key, value);
|
40
|
+
}
|
41
|
+
}
|
34
42
|
|
35
|
-
|
43
|
+
return params;
|
36
44
|
}
|
37
45
|
|
46
|
+
// getContentType returns the content type from the headers
|
38
47
|
function getContentType(headers: Headers): string {
|
39
|
-
|
40
|
-
|
41
|
-
return contentType;
|
48
|
+
const contentType = headers.get('Content-Type');
|
49
|
+
return contentType ?? '';
|
42
50
|
}
|
43
51
|
|
52
|
+
// isMultiPartForm returns true if the content type is multipart/form-data
|
44
53
|
function isMultiPartForm(headers: Headers): boolean {
|
45
|
-
|
46
|
-
|
54
|
+
const contentType = getContentType(headers);
|
55
|
+
return contentType.includes('multipart/form-data');
|
47
56
|
}
|
48
57
|
|
58
|
+
// renderStream renders the component to a readable stream
|
59
|
+
async function renderStream(children: ReactNode) {
|
60
|
+
const stream = await renderToReadableStream(children);
|
61
|
+
return new Response(stream, { headers: { 'Content-Type': 'text/html' } });
|
62
|
+
}
|
49
63
|
|
50
|
-
|
51
|
-
export { createContext }
|
64
|
+
export { createContext };
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import { HttpHandler, Route } from './router.d';
|
2
|
+
import { http } from '../http/http';
|
3
|
+
import { createContext } from './context';
|
4
|
+
|
5
|
+
const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
|
6
|
+
const route: Route = {
|
7
|
+
children: new Map(),
|
8
|
+
path: path,
|
9
|
+
dynamicPath: '',
|
10
|
+
method: method,
|
11
|
+
handler: handler,
|
12
|
+
isLast: false
|
13
|
+
};
|
14
|
+
|
15
|
+
return route;
|
16
|
+
};
|
17
|
+
|
18
|
+
const RouteTree = () => {
|
19
|
+
const root = createRoute('', 'GET', () => http.notFound());
|
20
|
+
|
21
|
+
function addRoute (pattern: string, method: string, handler: HttpHandler){
|
22
|
+
console.log(pattern);
|
23
|
+
const pathParts = pattern.split('/');
|
24
|
+
let current = root;
|
25
|
+
|
26
|
+
for (let i = 0; i < pathParts.length; i++) {
|
27
|
+
const part = pathParts[i];
|
28
|
+
if (part.startsWith(':')) {
|
29
|
+
current.dynamicPath = part;
|
30
|
+
}
|
31
|
+
if (!current.children.has(part)) {
|
32
|
+
current.children.set(part, createRoute(part, method, handler));
|
33
|
+
}
|
34
|
+
current = current.children.get(part)!;
|
35
|
+
}
|
36
|
+
|
37
|
+
current.handler = handler;
|
38
|
+
current.isLast = true;
|
39
|
+
current.path = pattern;
|
40
|
+
}
|
41
|
+
|
42
|
+
function findRoute(pathname: string): Route | undefined {
|
43
|
+
const pathParts = pathname.split('/');
|
44
|
+
let current = root;
|
45
|
+
for (let i = 0; i < pathParts.length; i++) {
|
46
|
+
const part = pathParts[i];
|
47
|
+
if (current.children.has(part)) {
|
48
|
+
current = current.children.get(part)!;
|
49
|
+
} else if (current.dynamicPath) {
|
50
|
+
current = current.children.get(current.dynamicPath)!;
|
51
|
+
} else {
|
52
|
+
return;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
return current;
|
56
|
+
}
|
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 };
|
83
|
+
|
84
|
+
};
|
85
|
+
|
86
|
+
export { RouteTree, createContext };
|
package/lib/router/router.d.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
import { TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptions, TLSServeOptions } from 'bun';
|
2
3
|
import { Logger } from '../logger/logger';
|
3
4
|
import { Database } from 'bun:sqlite';
|
4
5
|
|
@@ -20,7 +21,6 @@ type Route = {
|
|
20
21
|
handler: HttpHandler;
|
21
22
|
isLast: boolean;
|
22
23
|
}
|
23
|
-
|
24
24
|
type Context = {
|
25
25
|
db?: Database;
|
26
26
|
formData: FormData | Promise<FormData>;
|
@@ -29,12 +29,14 @@ type Context = {
|
|
29
29
|
params: Map<string, string>;
|
30
30
|
query: URLSearchParams;
|
31
31
|
request: Request;
|
32
|
+
render: (component: React.ReactNode) => Response | Promise<Response>;
|
32
33
|
};
|
33
34
|
|
34
35
|
type HttpHandler = (ctx: Context) => Response | Promise<Response>
|
35
36
|
|
36
37
|
type Options = {
|
37
|
-
db: string
|
38
|
+
db: string;
|
39
|
+
enableFileLogging: boolean;
|
38
40
|
}
|
39
41
|
|
40
42
|
type RouterOptions<Options> = ServeOptions
|
@@ -43,4 +45,4 @@ type RouterOptions<Options> = ServeOptions
|
|
43
45
|
| TLSWebSocketServeOptions<Options>
|
44
46
|
| undefined
|
45
47
|
|
46
|
-
export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler }
|
48
|
+
export { Context , Route, BunRouter, RouterOptions, Options, HttpHandler };
|