spooder 4.2.6 → 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
@@ -962,12 +998,12 @@ parse_template(template, replacements, true);
962
998
  `parse_template` supports looping arrays with the following syntax.
963
999
 
964
1000
  ```html
965
- {$for:foo}My colour is {$entry}{/for}
1001
+ {$for:foo}My colour is %s{/for}
966
1002
  ```
967
1003
  ```ts
968
1004
  const template = `
969
1005
  <ul>
970
- {$for:foo}<li>{$entry}</li>{/for}
1006
+ {$for:foo}<li>%s</li>{/for}
971
1007
  </ul>
972
1008
  `;
973
1009
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "4.2.6",
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
 
@@ -132,14 +136,9 @@ export function parse_template(template: string, replacements: Record<string, st
132
136
  } else {
133
137
  const loop_content = template.substring(loop_content_start_index, loop_close_index);
134
138
  if (loop_entries !== undefined) {
135
- const inner_replacements = {
136
- ...replacements,
137
- entry: ''
138
- };
139
-
140
139
  for (const loop_entry of loop_entries) {
141
- inner_replacements.entry = loop_entry;
142
- result += parse_template(loop_content, inner_replacements, drop_missing);
140
+ const inner_content = loop_content.replaceAll('%s', loop_entry);
141
+ result += parse_template(inner_content, replacements, drop_missing);
143
142
  }
144
143
  } else {
145
144
  if (!drop_missing)
@@ -354,8 +353,15 @@ function print_request_info(req: Request, res: Response, url: URL, request_start
354
353
  return res;
355
354
  }
356
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
+
357
363
  export function serve(port: number) {
358
- const routes = new Array<[string[], RequestHandler]>();
364
+ const routes = new Array<[string[], RequestHandler, HTTP_METHODS]>();
359
365
  const handlers = new Map<number, StatusCodeHandler>();
360
366
 
361
367
  let error_handler: ErrorHandler | undefined;
@@ -393,8 +399,9 @@ export function serve(port: number) {
393
399
  try {
394
400
  const route_array = url.pathname.split('/').filter(e => !(e === '..' || e === '.'));
395
401
  let handler: RequestHandler | undefined;
402
+ let methods: HTTP_METHODS | undefined;
396
403
 
397
- for (const [path, route_handler] of routes) {
404
+ for (const [path, route_handler, route_methods] of routes) {
398
405
  const is_trailing_wildcard = path[path.length - 1] === '*';
399
406
  if (!is_trailing_wildcard && path.length !== route_array.length)
400
407
  continue;
@@ -419,20 +426,25 @@ export function serve(port: number) {
419
426
 
420
427
  if (match) {
421
428
  handler = route_handler;
429
+ methods = route_methods;
422
430
  break;
423
431
  }
424
432
  }
425
433
 
426
434
  // Check for a handler for the route.
427
435
  if (handler !== undefined) {
428
- const response = await resolve_handler(handler(req, url), status_code, true);
429
- if (response instanceof Response)
430
- 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;
431
440
 
432
- // If the handler returned a status code, use that instead.
433
- 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
+ }
434
446
  } else {
435
- status_code = 404;
447
+ status_code = 404; // Not Found
436
448
  }
437
449
 
438
450
  // Fallback to checking for a handler for the status code.
@@ -477,23 +489,20 @@ export function serve(port: number) {
477
489
 
478
490
  return {
479
491
  /** Register a handler for a specific route. */
480
- route: (path: string, handler: RequestHandler): void => {
481
- routes.push([path.split('/'), handler]);
492
+ route: (path: string, handler: RequestHandler, method: HTTP_METHODS = 'GET'): void => {
493
+ routes.push([path.split('/'), handler, method]);
482
494
  },
483
495
 
484
496
  /** Serve a directory for a specific route. */
485
- dir: (path: string, dir: string, handler?: DirHandler): void => {
497
+ dir: (path: string, dir: string, handler?: DirHandler, method: HTTP_METHODS = 'GET'): void => {
486
498
  if (path.endsWith('/'))
487
499
  path = path.slice(0, -1);
488
500
 
489
- 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]);
490
502
  },
491
503
 
492
504
  webhook: (secret: string, path: string, handler: WebhookHandler): void => {
493
505
  routes.push([path.split('/'), async (req: Request, url: URL) => {
494
- if (req.method !== 'POST')
495
- return 405; // Method Not Allowed
496
-
497
506
  if (req.headers.get('Content-Type') !== 'application/json')
498
507
  return 400; // Bad Request
499
508
 
@@ -509,7 +518,7 @@ export function serve(port: number) {
509
518
  return 401; // Unauthorized
510
519
 
511
520
  return handler(body);
512
- }]);
521
+ }, 'POST']);
513
522
  },
514
523
 
515
524
  /** Register a default handler for all status codes. */
@@ -581,7 +590,7 @@ export function serve(port: number) {
581
590
  'Cache-Control': 'no-cache',
582
591
  'Connection': 'keep-alive'
583
592
  }});
584
- }]);
593
+ }, 'GET']);
585
594
  }
586
595
  }
587
596
  }