spooder 4.2.7 → 4.2.9
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/README.md +113 -54
- package/package.json +1 -1
- package/src/api.d.ts +7 -3
- package/src/api.ts +40 -21
package/README.md
CHANGED
|
@@ -8,10 +8,68 @@
|
|
|
8
8
|
- It provides streamlined APIs for common server tasks in a minimalistic way, without the overhead of a full-featured web framework.
|
|
9
9
|
- It is opinionated in its design to reduce complexity and overhead.
|
|
10
10
|
|
|
11
|
+
The design goal behind `spooder` is not to provide a full-featured web server, but to expand the Bun runtime with a set of APIs and utilities that make it easy to develop servers with minimal overhead.
|
|
12
|
+
|
|
13
|
+
> [!NOTE]
|
|
14
|
+
> If you think a is missing a feature, consider opening an issue with your use-case. The goal behind `spooder` is to provide APIs that are useful for a wide range of use-cases, not to provide bespoke features better suited for userland.
|
|
15
|
+
|
|
11
16
|
It consists of two components, the `CLI` and the `API`.
|
|
12
17
|
- The `CLI` is responsible for keeping the server process running, applying updates in response to source control changes, and automatically raising issues on GitHub via the canary feature.
|
|
13
18
|
- The `API` provides a minimal building-block style API for developing servers, with a focus on simplicity and performance.
|
|
14
19
|
|
|
20
|
+
> [!WARNING]
|
|
21
|
+
> `spooder` is stable but still in active development. Backwards compatibility between versions is not guaranteed and breaking changes may be introduced. Consider pinning a specific version in your `package.json`.
|
|
22
|
+
|
|
23
|
+
# CLI
|
|
24
|
+
|
|
25
|
+
The `CLI` component of `spooder` is a global command-line tool for running server processes.
|
|
26
|
+
|
|
27
|
+
- [CLI > Usage](#cli-usage)
|
|
28
|
+
- [CLI > Dev Mode](#cli-dev-mode)
|
|
29
|
+
- [CLI > Auto Restart](#cli-auto-restart)
|
|
30
|
+
- [CLI > Auto Update](#cli-auto-update)
|
|
31
|
+
- [CLI > Canary](#cli-canary)
|
|
32
|
+
- [CLI > Canary > Crash](#cli-canary-crash)
|
|
33
|
+
- [CLI > Canary > Sanitization](#cli-canary-sanitization)
|
|
34
|
+
- [CLI > Canary > System Information](#cli-canary-system-information)
|
|
35
|
+
|
|
36
|
+
# API
|
|
37
|
+
|
|
38
|
+
`spooder` exposes a simple yet powerful API for developing servers. The API is designed to be minimal to leave control in the hands of the developer and not add overhead for features you may not need.
|
|
39
|
+
|
|
40
|
+
- [API > Serving](#api-serving)
|
|
41
|
+
- [`serve(port: number): Server`](#api-serving-serve)
|
|
42
|
+
- [API > Routing](#api-routing)
|
|
43
|
+
- [`server.route(path: string, handler: RequestHandler, method: HTTP_METHODS)`](#api-routing-server-route)
|
|
44
|
+
- [HTTP Methods](#api-routing-methods)
|
|
45
|
+
- [Redirection Routes](#api-routing-redirection-routes)
|
|
46
|
+
- [Status Code Text](#api-routing-status-code-text)
|
|
47
|
+
- [API > Routing > RequestHandler](#api-routing-request-handler)
|
|
48
|
+
- [API > Routing > Fallback Handling](#api-routing-fallback-handlers)
|
|
49
|
+
- [`server.handle(status_code: number, handler: RequestHandler)`](#api-routing-server-handle)
|
|
50
|
+
- [`server.default(handler: DefaultHandler)`](#api-routing-server-default)
|
|
51
|
+
- [`server.error(handler: ErrorHandler)`](#api-routing-server-error)
|
|
52
|
+
- [API > Routing > Directory Serving](#api-routing-directory-serving)
|
|
53
|
+
- [`server.dir(path: string, dir: string, handler?: DirHandler, method: HTTP_METHODS)`](#api-routing-server-dir)
|
|
54
|
+
- [API > Routing > Server-Sent Events](#api-routing-server-sent-events)
|
|
55
|
+
- [`server.sse(path: string, handler: ServerSentEventHandler)`](#api-routing-server-sse)
|
|
56
|
+
- [API > Routing > Webhooks](#api-routing-webhooks)
|
|
57
|
+
- [`server.webhook(secret: string, path: string, handler: WebhookHandler)`](#api-routing-server-webhook)
|
|
58
|
+
- [API > Server Control](#api-server-control)
|
|
59
|
+
- [`server.stop(immediate: boolean)`](#api-server-control-server-stop)
|
|
60
|
+
- [API > Error Handling](#api-error-handling)
|
|
61
|
+
- [`ErrorWithMetadata(message: string, metadata: object)`](#api-error-handling-error-with-metadata)
|
|
62
|
+
- [`caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>`](#api-error-handling-caution)
|
|
63
|
+
- [`panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>`](#api-error-handling-panic)
|
|
64
|
+
- [`safe(fn: Callable): Promise<void>`](#api-error-handling-safe)
|
|
65
|
+
- [API > Content](#api-content)
|
|
66
|
+
- [`parse_template(template: string, replacements: Record<string, string>, drop_missing: boolean): string`](#api-content-parse-template)
|
|
67
|
+
- [`generate_hash_subs(length: number, prefix: string): Promise<Record<string, string>>`](#api-content-generate-hash-subs)
|
|
68
|
+
- [`apply_range(file: BunFile, request: Request): HandlerReturnType`](#api-content-apply-range)
|
|
69
|
+
- [API > State Management](#api-state-management)
|
|
70
|
+
- [`set_cookie(res: Response, name: string, value: string, options?: CookieOptions)`](#api-state-management-set-cookie)
|
|
71
|
+
- [`get_cookies(source: Request | Response): Record<string, string>`](#api-state-management-get-cookies)
|
|
72
|
+
|
|
15
73
|
# Installation
|
|
16
74
|
|
|
17
75
|
```bash
|
|
@@ -51,20 +109,6 @@ If there are any issues with the provided configuration, a warning will be print
|
|
|
51
109
|
> [!NOTE]
|
|
52
110
|
> Configuration warnings **do not** raise `caution` events with the `spooder` canary functionality.
|
|
53
111
|
|
|
54
|
-
# CLI
|
|
55
|
-
|
|
56
|
-
The `CLI` component of `spooder` is a global command-line tool for running server processes.
|
|
57
|
-
|
|
58
|
-
- [CLI > Usage](#cli-usage)
|
|
59
|
-
- [CLI > Dev Mode](#cli-dev-mode)
|
|
60
|
-
- [CLI > Auto Restart](#cli-auto-restart)
|
|
61
|
-
- [CLI > Auto Update](#cli-auto-update)
|
|
62
|
-
- [CLI > Canary](#cli-canary)
|
|
63
|
-
- [CLI > Canary > Crash](#cli-canary-crash)
|
|
64
|
-
- [CLI > Canary > Sanitization](#cli-canary-sanitization)
|
|
65
|
-
- [CLI > Canary > System Information](#cli-canary-system-information)
|
|
66
|
-
|
|
67
|
-
|
|
68
112
|
<a id="cli-usage"></a>
|
|
69
113
|
## CLI > Usage
|
|
70
114
|
|
|
@@ -114,7 +158,7 @@ The following differences will be observed when running in development mode:
|
|
|
114
158
|
It is possible to detect in userland if a server is running in development mode by checking the `SPOODER_ENV` environment variable.
|
|
115
159
|
|
|
116
160
|
```ts
|
|
117
|
-
if (process.env.
|
|
161
|
+
if (process.env.SPOODER_ENV === 'dev') {
|
|
118
162
|
// Server is running in development mode.
|
|
119
163
|
}
|
|
120
164
|
```
|
|
@@ -183,7 +227,7 @@ server.webhook(process.env.WEBHOOK_SECRET, '/webhook', payload => {
|
|
|
183
227
|
|
|
184
228
|
`canary` is a feature in `spooder` which allows server problems to be raised as issues in your repository on GitHub.
|
|
185
229
|
|
|
186
|
-
To enable this feature, you will need to
|
|
230
|
+
To enable this feature, you will need to create a GitHub App and configure it:
|
|
187
231
|
|
|
188
232
|
### 1. Create a GitHub App
|
|
189
233
|
|
|
@@ -404,41 +448,6 @@ In addition to the information provided by the developer, `spooder` also include
|
|
|
404
448
|
}
|
|
405
449
|
```
|
|
406
450
|
|
|
407
|
-
# API
|
|
408
|
-
|
|
409
|
-
`spooder` exposes a simple yet powerful API for developing servers. The API is designed to be minimal to leave control in the hands of the developer and not add overhead for features you may not need.
|
|
410
|
-
|
|
411
|
-
- [API > Serving](#api-serving)
|
|
412
|
-
- [`serve(port: number): Server`](#api-serving-serve)
|
|
413
|
-
- [API > Routing](#api-routing)
|
|
414
|
-
- [`server.route(path: string, handler: RequestHandler)`](#api-routing-server-route)
|
|
415
|
-
- [Redirection Routes](#api-routing-redirection-routes)
|
|
416
|
-
- [API > Routing > RequestHandler](#api-routing-request-handler)
|
|
417
|
-
- [API > Routing > Fallback Handling](#api-routing-fallback-handlers)
|
|
418
|
-
- [`server.handle(status_code: number, handler: RequestHandler)`](#api-routing-server-handle)
|
|
419
|
-
- [`server.default(handler: DefaultHandler)`](#api-routing-server-default)
|
|
420
|
-
- [`server.error(handler: ErrorHandler)`](#api-routing-server-error)
|
|
421
|
-
- [API > Routing > Directory Serving](#api-routing-directory-serving)
|
|
422
|
-
- [`server.dir(path: string, dir: string, handler?: DirHandler)`](#api-routing-server-dir)
|
|
423
|
-
- [API > Routing > Server-Sent Events](#api-routing-server-sent-events)
|
|
424
|
-
- [`server.sse(path: string, handler: ServerSentEventHandler)`](#api-routing-server-sse)
|
|
425
|
-
- [API > Routing > Webhooks](#api-routing-webhooks)
|
|
426
|
-
- [`server.webhook(secret: string, path: string, handler: WebhookHandler)`](#api-routing-server-webhook)
|
|
427
|
-
- [API > Server Control](#api-server-control)
|
|
428
|
-
- [`server.stop(immediate: boolean)`](#api-server-control-server-stop)
|
|
429
|
-
- [API > Error Handling](#api-error-handling)
|
|
430
|
-
- [`ErrorWithMetadata(message: string, metadata: object)`](#api-error-handling-error-with-metadata)
|
|
431
|
-
- [`caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>`](#api-error-handling-caution)
|
|
432
|
-
- [`panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>`](#api-error-handling-panic)
|
|
433
|
-
- [`safe(fn: Callable): Promise<void>`](#api-error-handling-safe)
|
|
434
|
-
- [API > Content](#api-content)
|
|
435
|
-
- [`parse_template(template: string, replacements: Record<string, string>, drop_missing: boolean): string`](#api-content-parse-template)
|
|
436
|
-
- [`generate_hash_subs(length: number, prefix: string): Promise<Record<string, string>>`](#api-content-generate-hash-subs)
|
|
437
|
-
- [`apply_range(file: BunFile, request: Request): HandlerReturnType`](#api-content-apply-range)
|
|
438
|
-
- [API > State Management](#api-state-management)
|
|
439
|
-
- [`set_cookie(res: Response, name: string, value: string, options?: CookieOptions)`](#api-state-management-set-cookie)
|
|
440
|
-
- [`get_cookies(source: Request | Response): Record<string, string>`](#api-state-management-get-cookies)
|
|
441
|
-
|
|
442
451
|
<a id="api-serving"></a>
|
|
443
452
|
## API > Serving
|
|
444
453
|
|
|
@@ -477,6 +486,32 @@ server.route('/test/route', (req, url) => {
|
|
|
477
486
|
});
|
|
478
487
|
```
|
|
479
488
|
|
|
489
|
+
<a id="api-routing-methods"></a>
|
|
490
|
+
### HTTP Methods
|
|
491
|
+
|
|
492
|
+
By default, `spooder` will register routes defined with `server.route()` and `server.dir()` as `GET` routes. Requests to these routes with other methods will return `405 Method Not Allowed`.
|
|
493
|
+
|
|
494
|
+
> [!NOTE]
|
|
495
|
+
> spooder does not automatically handle HEAD requests natively.
|
|
496
|
+
|
|
497
|
+
This can be controlled by providing the `method` parameter with a string or array defining one or more of the following methods.
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
GET | HEAD | POST | PUT | DELETE | CONNECT | OPTIONS | TRACE | PATCH
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
server.route('/test/route', (req, url) => {
|
|
505
|
+
if (req.method === 'GET')
|
|
506
|
+
// Handle GET request.
|
|
507
|
+
else if (req.method === 'POST')
|
|
508
|
+
// Handle POST request.
|
|
509
|
+
}, ['GET', 'POST']);
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
> [!NOTE]
|
|
513
|
+
> Routes defined with .sse() or .webhook() are always registered as 'GET' and 'POST' respectively and cannot be configured.
|
|
514
|
+
|
|
480
515
|
<a id="api-routing-redirection-routes"></a>
|
|
481
516
|
### Redirection Routes
|
|
482
517
|
|
|
@@ -486,6 +521,7 @@ server.route('/test/route', (req, url) => {
|
|
|
486
521
|
server.route('/redirect', () => Response.redirect('/redirected', 301));
|
|
487
522
|
```
|
|
488
523
|
|
|
524
|
+
<a id="api-routing-status-code-text"></a>
|
|
489
525
|
### Status Code Text
|
|
490
526
|
|
|
491
527
|
`spooder` exposes `HTTP_STATUS_CODE` to convieniently access status code text.
|
|
@@ -546,7 +582,7 @@ bar
|
|
|
546
582
|
|
|
547
583
|
Named parameters can be used in paths by prefixing a path segment with a colon.
|
|
548
584
|
|
|
549
|
-
> [!
|
|
585
|
+
> [!IMPORTANT]
|
|
550
586
|
> Named parameters will overwrite existing query parameters with the same name.
|
|
551
587
|
|
|
552
588
|
```ts
|
|
@@ -878,7 +914,7 @@ try {
|
|
|
878
914
|
|
|
879
915
|
`safe()` is a utility function that wraps a "callable" and calls `caution()` if it throws an error.
|
|
880
916
|
|
|
881
|
-
> !
|
|
917
|
+
> [!NOTE]
|
|
882
918
|
> This utility is primarily intended to be used to reduce boilerplate for fire-and-forget functions that you want to be notified about if they fail.
|
|
883
919
|
|
|
884
920
|
```ts
|
|
@@ -901,7 +937,7 @@ await safe(() => {
|
|
|
901
937
|
## API > Content
|
|
902
938
|
|
|
903
939
|
<a id="api-content-parse-template"></a>
|
|
904
|
-
### 🔧 `parse_template(template: string, replacements:
|
|
940
|
+
### 🔧 `parse_template(template: string, replacements: Replacements, drop_missing: boolean): string`
|
|
905
941
|
|
|
906
942
|
Replace placeholders in a template string with values from a replacement object.
|
|
907
943
|
|
|
@@ -959,6 +995,29 @@ parse_template(template, replacements, true);
|
|
|
959
995
|
</html>
|
|
960
996
|
```
|
|
961
997
|
|
|
998
|
+
`parse_template` supports passing a function instead of a replacement object. This function will be called for each placeholder and the return value will be used as the replacement.
|
|
999
|
+
|
|
1000
|
+
```ts
|
|
1001
|
+
const replacer = (placeholder: string) => {
|
|
1002
|
+
return placeholder.toUpperCase();
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
parse_template('Hello {$world}', replacer);
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
```html
|
|
1009
|
+
<html>
|
|
1010
|
+
<head>
|
|
1011
|
+
<title>TITLE</title>
|
|
1012
|
+
</head>
|
|
1013
|
+
<body>
|
|
1014
|
+
<h1>TITLE</h1>
|
|
1015
|
+
<p>CONTENT</p>
|
|
1016
|
+
<p>IGNORED</p>
|
|
1017
|
+
</body>
|
|
1018
|
+
</html>
|
|
1019
|
+
```
|
|
1020
|
+
|
|
962
1021
|
`parse_template` supports looping arrays with the following syntax.
|
|
963
1022
|
|
|
964
1023
|
```html
|
package/package.json
CHANGED
package/src/api.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare const HTTP_STATUS_CODE: {
|
|
|
8
8
|
[errorCode: number]: string | undefined;
|
|
9
9
|
[errorCode: string]: string | undefined;
|
|
10
10
|
};
|
|
11
|
+
type HTTP_METHOD = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE';
|
|
12
|
+
type HTTP_METHODS = HTTP_METHOD | HTTP_METHOD[];
|
|
11
13
|
export declare class ErrorWithMetadata extends Error {
|
|
12
14
|
metadata: Record<string, unknown>;
|
|
13
15
|
constructor(message: string, metadata: Record<string, unknown>);
|
|
@@ -18,7 +20,9 @@ export declare function caution(err_message_or_obj: string | object, ...err: obj
|
|
|
18
20
|
type CallableFunction = (...args: any[]) => any;
|
|
19
21
|
type Callable = Promise<any> | CallableFunction;
|
|
20
22
|
export declare function safe(target_fn: Callable): Promise<void>;
|
|
21
|
-
|
|
23
|
+
type ReplacerFn = (key: string) => string | Array<string>;
|
|
24
|
+
type Replacements = Record<string, string | Array<string>> | ReplacerFn;
|
|
25
|
+
export declare function parse_template(template: string, replacements: Replacements, drop_missing?: boolean): string;
|
|
22
26
|
export declare function generate_hash_subs(length?: number, prefix?: string): Promise<Record<string, string>>;
|
|
23
27
|
type CookieOptions = {
|
|
24
28
|
same_site?: 'Strict' | 'Lax' | 'None';
|
|
@@ -60,9 +64,9 @@ type DirStat = PromiseType<ReturnType<typeof fs.stat>>;
|
|
|
60
64
|
type DirHandler = (file_path: string, file: BunFile, stat: DirStat, request: Request, url: URL) => HandlerReturnType;
|
|
61
65
|
export declare function serve(port: number): {
|
|
62
66
|
/** Register a handler for a specific route. */
|
|
63
|
-
route: (path: string, handler: RequestHandler) => void;
|
|
67
|
+
route: (path: string, handler: RequestHandler, method?: HTTP_METHODS) => void;
|
|
64
68
|
/** Serve a directory for a specific route. */
|
|
65
|
-
dir: (path: string, dir: string, handler?: DirHandler) => void;
|
|
69
|
+
dir: (path: string, dir: string, handler?: DirHandler, method?: HTTP_METHODS) => void;
|
|
66
70
|
webhook: (secret: string, path: string, handler: WebhookHandler) => void;
|
|
67
71
|
/** Register a default handler for all status codes. */
|
|
68
72
|
default: (handler: DefaultHandler) => void;
|
package/src/api.ts
CHANGED
|
@@ -8,6 +8,10 @@ import { Blob } from 'node:buffer';
|
|
|
8
8
|
|
|
9
9
|
export const HTTP_STATUS_CODE = http.STATUS_CODES;
|
|
10
10
|
|
|
11
|
+
// Create enum containing HTTP methods
|
|
12
|
+
type HTTP_METHOD = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE';
|
|
13
|
+
type HTTP_METHODS = HTTP_METHOD|HTTP_METHOD[];
|
|
14
|
+
|
|
11
15
|
export class ErrorWithMetadata extends Error {
|
|
12
16
|
constructor(message: string, public metadata: Record<string, unknown>) {
|
|
13
17
|
super(message);
|
|
@@ -24,7 +28,7 @@ export class ErrorWithMetadata extends Error {
|
|
|
24
28
|
if (value instanceof Promise)
|
|
25
29
|
resolved_value = await value;
|
|
26
30
|
else if (typeof value === 'function')
|
|
27
|
-
resolved_value = value();
|
|
31
|
+
resolved_value = await value();
|
|
28
32
|
else if (value instanceof ReadableStream)
|
|
29
33
|
resolved_value = await Bun.readableStreamToText(value);
|
|
30
34
|
|
|
@@ -103,11 +107,16 @@ export async function safe(target_fn: Callable) {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
|
|
110
|
+
type ReplacerFn = (key: string) => string | Array<string>;
|
|
111
|
+
type Replacements = Record<string, string | Array<string>> | ReplacerFn;
|
|
112
|
+
|
|
113
|
+
export function parse_template(template: string, replacements: Replacements, drop_missing = false): string {
|
|
107
114
|
let result = '';
|
|
108
115
|
let buffer = '';
|
|
109
116
|
let buffer_active = false;
|
|
110
117
|
|
|
118
|
+
const is_replacer_fn = typeof replacements === 'function';
|
|
119
|
+
|
|
111
120
|
const template_length = template.length;
|
|
112
121
|
for (let i = 0; i < template_length; i++) {
|
|
113
122
|
const char = template[i];
|
|
@@ -122,7 +131,7 @@ export function parse_template(template: string, replacements: Record<string, st
|
|
|
122
131
|
if (buffer.startsWith('for:')) {
|
|
123
132
|
const loop_key = buffer.substring(4);
|
|
124
133
|
|
|
125
|
-
const loop_entries = replacements[loop_key];
|
|
134
|
+
const loop_entries = is_replacer_fn ? replacements(loop_key) : replacements[loop_key];
|
|
126
135
|
const loop_content_start_index = i + 1;
|
|
127
136
|
const loop_close_index = template.indexOf('{/for}', loop_content_start_index);
|
|
128
137
|
|
|
@@ -143,7 +152,7 @@ export function parse_template(template: string, replacements: Record<string, st
|
|
|
143
152
|
i += loop_content.length + 6;
|
|
144
153
|
}
|
|
145
154
|
} else {
|
|
146
|
-
const replacement = replacements[buffer];
|
|
155
|
+
const replacement = is_replacer_fn ? replacements(buffer) : replacements[buffer];
|
|
147
156
|
if (replacement !== undefined)
|
|
148
157
|
result += replacement;
|
|
149
158
|
else if (!drop_missing)
|
|
@@ -349,8 +358,15 @@ function print_request_info(req: Request, res: Response, url: URL, request_start
|
|
|
349
358
|
return res;
|
|
350
359
|
}
|
|
351
360
|
|
|
361
|
+
function is_valid_method(method: HTTP_METHODS, req: Request): boolean {
|
|
362
|
+
if (Array.isArray(method))
|
|
363
|
+
return method.includes(req.method as HTTP_METHOD);
|
|
364
|
+
|
|
365
|
+
return req.method === method;
|
|
366
|
+
}
|
|
367
|
+
|
|
352
368
|
export function serve(port: number) {
|
|
353
|
-
const routes = new Array<[string[], RequestHandler]>();
|
|
369
|
+
const routes = new Array<[string[], RequestHandler, HTTP_METHODS]>();
|
|
354
370
|
const handlers = new Map<number, StatusCodeHandler>();
|
|
355
371
|
|
|
356
372
|
let error_handler: ErrorHandler | undefined;
|
|
@@ -388,8 +404,9 @@ export function serve(port: number) {
|
|
|
388
404
|
try {
|
|
389
405
|
const route_array = url.pathname.split('/').filter(e => !(e === '..' || e === '.'));
|
|
390
406
|
let handler: RequestHandler | undefined;
|
|
407
|
+
let methods: HTTP_METHODS | undefined;
|
|
391
408
|
|
|
392
|
-
for (const [path, route_handler] of routes) {
|
|
409
|
+
for (const [path, route_handler, route_methods] of routes) {
|
|
393
410
|
const is_trailing_wildcard = path[path.length - 1] === '*';
|
|
394
411
|
if (!is_trailing_wildcard && path.length !== route_array.length)
|
|
395
412
|
continue;
|
|
@@ -414,20 +431,25 @@ export function serve(port: number) {
|
|
|
414
431
|
|
|
415
432
|
if (match) {
|
|
416
433
|
handler = route_handler;
|
|
434
|
+
methods = route_methods;
|
|
417
435
|
break;
|
|
418
436
|
}
|
|
419
437
|
}
|
|
420
438
|
|
|
421
439
|
// Check for a handler for the route.
|
|
422
440
|
if (handler !== undefined) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
441
|
+
if (is_valid_method(methods!, req)) {
|
|
442
|
+
const response = await resolve_handler(handler(req, url), status_code, true);
|
|
443
|
+
if (response instanceof Response)
|
|
444
|
+
return response;
|
|
426
445
|
|
|
427
|
-
|
|
428
|
-
|
|
446
|
+
// If the handler returned a status code, use that instead.
|
|
447
|
+
status_code = response;
|
|
448
|
+
} else {
|
|
449
|
+
status_code = 405; // Method Not Allowed
|
|
450
|
+
}
|
|
429
451
|
} else {
|
|
430
|
-
status_code = 404;
|
|
452
|
+
status_code = 404; // Not Found
|
|
431
453
|
}
|
|
432
454
|
|
|
433
455
|
// Fallback to checking for a handler for the status code.
|
|
@@ -472,23 +494,20 @@ export function serve(port: number) {
|
|
|
472
494
|
|
|
473
495
|
return {
|
|
474
496
|
/** Register a handler for a specific route. */
|
|
475
|
-
route: (path: string, handler: RequestHandler): void => {
|
|
476
|
-
routes.push([path.split('/'), handler]);
|
|
497
|
+
route: (path: string, handler: RequestHandler, method: HTTP_METHODS = 'GET'): void => {
|
|
498
|
+
routes.push([path.split('/'), handler, method]);
|
|
477
499
|
},
|
|
478
500
|
|
|
479
501
|
/** Serve a directory for a specific route. */
|
|
480
|
-
dir: (path: string, dir: string, handler?: DirHandler): void => {
|
|
502
|
+
dir: (path: string, dir: string, handler?: DirHandler, method: HTTP_METHODS = 'GET'): void => {
|
|
481
503
|
if (path.endsWith('/'))
|
|
482
504
|
path = path.slice(0, -1);
|
|
483
505
|
|
|
484
|
-
routes.push([[...path.split('/'), '*'], route_directory(path, dir, handler ?? default_directory_handler)]);
|
|
506
|
+
routes.push([[...path.split('/'), '*'], route_directory(path, dir, handler ?? default_directory_handler), method]);
|
|
485
507
|
},
|
|
486
508
|
|
|
487
509
|
webhook: (secret: string, path: string, handler: WebhookHandler): void => {
|
|
488
510
|
routes.push([path.split('/'), async (req: Request, url: URL) => {
|
|
489
|
-
if (req.method !== 'POST')
|
|
490
|
-
return 405; // Method Not Allowed
|
|
491
|
-
|
|
492
511
|
if (req.headers.get('Content-Type') !== 'application/json')
|
|
493
512
|
return 400; // Bad Request
|
|
494
513
|
|
|
@@ -504,7 +523,7 @@ export function serve(port: number) {
|
|
|
504
523
|
return 401; // Unauthorized
|
|
505
524
|
|
|
506
525
|
return handler(body);
|
|
507
|
-
}]);
|
|
526
|
+
}, 'POST']);
|
|
508
527
|
},
|
|
509
528
|
|
|
510
529
|
/** Register a default handler for all status codes. */
|
|
@@ -576,7 +595,7 @@ export function serve(port: number) {
|
|
|
576
595
|
'Cache-Control': 'no-cache',
|
|
577
596
|
'Connection': 'keep-alive'
|
|
578
597
|
}});
|
|
579
|
-
}]);
|
|
598
|
+
}, 'GET']);
|
|
580
599
|
}
|
|
581
600
|
}
|
|
582
601
|
}
|