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 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 configure a GitHub App and configure it:
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
- > [!NOTE]
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
- > ![NOTE]
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "4.2.7",
4
+ "version": "4.2.8",
5
5
  "exports": {
6
6
  ".": {
7
7
  "bun": "./src/api.ts",
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
- const response = await resolve_handler(handler(req, url), status_code, true);
424
- if (response instanceof Response)
425
- return response;
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
- // If the handler returned a status code, use that instead.
428
- status_code = response;
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
  }