spooder 4.2.7 → 4.2.8
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 +88 -52
- package/package.json +1 -1
- package/src/api.d.ts +4 -2
- package/src/api.ts +32 -18
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
|
|
|
@@ -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
|
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>);
|
|
@@ -60,9 +62,9 @@ type DirStat = PromiseType<ReturnType<typeof fs.stat>>;
|
|
|
60
62
|
type DirHandler = (file_path: string, file: BunFile, stat: DirStat, request: Request, url: URL) => HandlerReturnType;
|
|
61
63
|
export declare function serve(port: number): {
|
|
62
64
|
/** Register a handler for a specific route. */
|
|
63
|
-
route: (path: string, handler: RequestHandler) => void;
|
|
65
|
+
route: (path: string, handler: RequestHandler, method?: HTTP_METHODS) => void;
|
|
64
66
|
/** Serve a directory for a specific route. */
|
|
65
|
-
dir: (path: string, dir: string, handler?: DirHandler) => void;
|
|
67
|
+
dir: (path: string, dir: string, handler?: DirHandler, method?: HTTP_METHODS) => void;
|
|
66
68
|
webhook: (secret: string, path: string, handler: WebhookHandler) => void;
|
|
67
69
|
/** Register a default handler for all status codes. */
|
|
68
70
|
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
|
|
|
@@ -349,8 +353,15 @@ function print_request_info(req: Request, res: Response, url: URL, request_start
|
|
|
349
353
|
return res;
|
|
350
354
|
}
|
|
351
355
|
|
|
356
|
+
function is_valid_method(method: HTTP_METHODS, req: Request): boolean {
|
|
357
|
+
if (Array.isArray(method))
|
|
358
|
+
return method.includes(req.method as HTTP_METHOD);
|
|
359
|
+
|
|
360
|
+
return req.method === method;
|
|
361
|
+
}
|
|
362
|
+
|
|
352
363
|
export function serve(port: number) {
|
|
353
|
-
const routes = new Array<[string[], RequestHandler]>();
|
|
364
|
+
const routes = new Array<[string[], RequestHandler, HTTP_METHODS]>();
|
|
354
365
|
const handlers = new Map<number, StatusCodeHandler>();
|
|
355
366
|
|
|
356
367
|
let error_handler: ErrorHandler | undefined;
|
|
@@ -388,8 +399,9 @@ export function serve(port: number) {
|
|
|
388
399
|
try {
|
|
389
400
|
const route_array = url.pathname.split('/').filter(e => !(e === '..' || e === '.'));
|
|
390
401
|
let handler: RequestHandler | undefined;
|
|
402
|
+
let methods: HTTP_METHODS | undefined;
|
|
391
403
|
|
|
392
|
-
for (const [path, route_handler] of routes) {
|
|
404
|
+
for (const [path, route_handler, route_methods] of routes) {
|
|
393
405
|
const is_trailing_wildcard = path[path.length - 1] === '*';
|
|
394
406
|
if (!is_trailing_wildcard && path.length !== route_array.length)
|
|
395
407
|
continue;
|
|
@@ -414,20 +426,25 @@ export function serve(port: number) {
|
|
|
414
426
|
|
|
415
427
|
if (match) {
|
|
416
428
|
handler = route_handler;
|
|
429
|
+
methods = route_methods;
|
|
417
430
|
break;
|
|
418
431
|
}
|
|
419
432
|
}
|
|
420
433
|
|
|
421
434
|
// Check for a handler for the route.
|
|
422
435
|
if (handler !== undefined) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
436
|
+
if (is_valid_method(methods!, req)) {
|
|
437
|
+
const response = await resolve_handler(handler(req, url), status_code, true);
|
|
438
|
+
if (response instanceof Response)
|
|
439
|
+
return response;
|
|
426
440
|
|
|
427
|
-
|
|
428
|
-
|
|
441
|
+
// If the handler returned a status code, use that instead.
|
|
442
|
+
status_code = response;
|
|
443
|
+
} else {
|
|
444
|
+
status_code = 405; // Method Not Allowed
|
|
445
|
+
}
|
|
429
446
|
} else {
|
|
430
|
-
status_code = 404;
|
|
447
|
+
status_code = 404; // Not Found
|
|
431
448
|
}
|
|
432
449
|
|
|
433
450
|
// Fallback to checking for a handler for the status code.
|
|
@@ -472,23 +489,20 @@ export function serve(port: number) {
|
|
|
472
489
|
|
|
473
490
|
return {
|
|
474
491
|
/** Register a handler for a specific route. */
|
|
475
|
-
route: (path: string, handler: RequestHandler): void => {
|
|
476
|
-
routes.push([path.split('/'), handler]);
|
|
492
|
+
route: (path: string, handler: RequestHandler, method: HTTP_METHODS = 'GET'): void => {
|
|
493
|
+
routes.push([path.split('/'), handler, method]);
|
|
477
494
|
},
|
|
478
495
|
|
|
479
496
|
/** Serve a directory for a specific route. */
|
|
480
|
-
dir: (path: string, dir: string, handler?: DirHandler): void => {
|
|
497
|
+
dir: (path: string, dir: string, handler?: DirHandler, method: HTTP_METHODS = 'GET'): void => {
|
|
481
498
|
if (path.endsWith('/'))
|
|
482
499
|
path = path.slice(0, -1);
|
|
483
500
|
|
|
484
|
-
routes.push([[...path.split('/'), '*'], route_directory(path, dir, handler ?? default_directory_handler)]);
|
|
501
|
+
routes.push([[...path.split('/'), '*'], route_directory(path, dir, handler ?? default_directory_handler), method]);
|
|
485
502
|
},
|
|
486
503
|
|
|
487
504
|
webhook: (secret: string, path: string, handler: WebhookHandler): void => {
|
|
488
505
|
routes.push([path.split('/'), async (req: Request, url: URL) => {
|
|
489
|
-
if (req.method !== 'POST')
|
|
490
|
-
return 405; // Method Not Allowed
|
|
491
|
-
|
|
492
506
|
if (req.headers.get('Content-Type') !== 'application/json')
|
|
493
507
|
return 400; // Bad Request
|
|
494
508
|
|
|
@@ -504,7 +518,7 @@ export function serve(port: number) {
|
|
|
504
518
|
return 401; // Unauthorized
|
|
505
519
|
|
|
506
520
|
return handler(body);
|
|
507
|
-
}]);
|
|
521
|
+
}, 'POST']);
|
|
508
522
|
},
|
|
509
523
|
|
|
510
524
|
/** Register a default handler for all status codes. */
|
|
@@ -576,7 +590,7 @@ export function serve(port: number) {
|
|
|
576
590
|
'Cache-Control': 'no-cache',
|
|
577
591
|
'Connection': 'keep-alive'
|
|
578
592
|
}});
|
|
579
|
-
}]);
|
|
593
|
+
}, 'GET']);
|
|
580
594
|
}
|
|
581
595
|
}
|
|
582
596
|
}
|