spooder 3.1.0 → 3.2.0
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 +322 -0
- package/package.json +1 -1
- package/src/api.d.ts +26 -0
- package/src/api.ts +204 -0
package/README.md
CHANGED
|
@@ -315,6 +315,328 @@ In addition to the information provided by the developer, `spooder` also include
|
|
|
315
315
|
import { ... } from 'spooder';
|
|
316
316
|
```
|
|
317
317
|
|
|
318
|
+
#### `serve(port: number): Server`
|
|
319
|
+
|
|
320
|
+
The `serve` function simplifies the process of boostrapping a server. Setting up a functioning server is as simple as calling the function and passing a port number to listen on.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const server = serve(8080);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Without any additional configuration, this will create a server which listens on the specified port and responds to all requests with the following response.
|
|
327
|
+
|
|
328
|
+
```http
|
|
329
|
+
HTTP/1.1 404 Not Found
|
|
330
|
+
Content-Length: 9
|
|
331
|
+
Content-Type: text/plain;charset=utf-8
|
|
332
|
+
|
|
333
|
+
Not Found
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
To build functionality on top of this, there are a number of functions that can be called from the `Server` object.
|
|
337
|
+
|
|
338
|
+
#### `server.route(path: string, handler: RequestHandler)`
|
|
339
|
+
|
|
340
|
+
The `route` function allows you to register a handler for a specific path. The handler will be called for all requests that exactly match the given path.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
server.route('/test/route', (req, url) => {
|
|
344
|
+
return new Response('Hello, world!', { status: 200 });
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Additionally, routes also support named parameters. These are defined by prefixing a path segment with a colon. These are added directly to the `searchParams` property of the `URL` object.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
server.route('/test/:param', (req, url) => {
|
|
352
|
+
return new Response(url.searchParams.get('param'), { status: 200 });
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
> Note: Named parameters will overwrite existing search parameters with the same name.
|
|
356
|
+
|
|
357
|
+
By default routes are matched exactly, but you can also use a wildcard to match any path that starts with a given path.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
server.route('/test/*', (req, url) => {
|
|
361
|
+
return new Response('Hello, world!', { status: 200 });
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The above will match any path the starts with `/test`, such as:
|
|
366
|
+
- `/test`
|
|
367
|
+
- `/test/`
|
|
368
|
+
- `/test/route`
|
|
369
|
+
- `/test/route/foo.txt`
|
|
370
|
+
|
|
371
|
+
If you intend to use this for directory serving, you may be better suited looking at the `server.dir()` function.
|
|
372
|
+
|
|
373
|
+
Wildcards can also be placed anywhere in the path, allowing anything to be placed in a given single segment - it does not span multiple segments.
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
server.route('/test/*/route', (req, url) => {
|
|
377
|
+
return new Response('Hello, world!', { status: 200 });
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The above would allow anything to be placed in the middle segment. This behavior is documented for clarity as it is a byproduct of wildcard implementation for directories, but it is recommended you use the named parameters feature instead.
|
|
382
|
+
|
|
383
|
+
Using the standard Web API, the route handler above receives a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object and returns a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object, which is then sent to the client.
|
|
384
|
+
|
|
385
|
+
All handle registration functions in `spooder` support registering async functions which will be awaited before sending the response.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
server.route('/test/route', async (req, url) => {
|
|
389
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
390
|
+
return new Response('Hello, world!', { status: 200 });
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
To streamline this process, `spooder` allows a number of other return types to be used as shortcuts.
|
|
395
|
+
|
|
396
|
+
Returning a `number` type treats the number as a status code and sends a relevant response.
|
|
397
|
+
|
|
398
|
+
By default, this will be a plain text response with the applicable status message as the body. This can be overridden with `server.handle()` or `server.default()`, which will be covered later.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
server.route('/test/route', (req) => {
|
|
402
|
+
return 500;
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
```http
|
|
406
|
+
HTTP/1.1 500 Internal Server Error
|
|
407
|
+
Content-Length: 21
|
|
408
|
+
Content-Type: text/plain;charset=utf-8
|
|
409
|
+
|
|
410
|
+
Internal Server Error
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Returning a `Blob` type, such as the `FileBlob` returned from the `Bun.file()` API, will send the blob as the response body with the appropriate content type and length headers.
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
server.route('test/route', (req) => {
|
|
417
|
+
// Note that calling Bun.file() does not immediately read
|
|
418
|
+
// the file from disk, it will be streamed with the response.
|
|
419
|
+
return Bun.file('test.png');
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
```http
|
|
423
|
+
HTTP/1.1 200 OK
|
|
424
|
+
Content-Length: 12345
|
|
425
|
+
Content-Type: image/png
|
|
426
|
+
|
|
427
|
+
<binary data>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Return an `object` type, such as an array or a plain object, will send the object as JSON with the appropriate content type and length headers.
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
server.route('test/route', (req) => {
|
|
434
|
+
return { message: 'Hello, world!' };
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
```http
|
|
438
|
+
HTTP/1.1 200 OK
|
|
439
|
+
Content-Length: 25
|
|
440
|
+
Content-Type: application/json;charset=utf-8
|
|
441
|
+
|
|
442
|
+
{"message":"Hello, world!"}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Since custom classes are also objects, you can also return a custom class instance and it will be serialized to JSON. To control the serialization process, you can implement the `toJSON()` method on your class.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
class User {
|
|
449
|
+
constructor(public name: string, public age: number) {}
|
|
450
|
+
|
|
451
|
+
toJSON() {
|
|
452
|
+
return {
|
|
453
|
+
name: this.name,
|
|
454
|
+
age: this.age,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
server.route('test/route', (req) => {
|
|
460
|
+
return new User('Bob', 42);
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
```http
|
|
464
|
+
HTTP/1.1 200 OK
|
|
465
|
+
Content-Length: 25
|
|
466
|
+
Content-Type: application/json;charset=utf-8
|
|
467
|
+
|
|
468
|
+
{"name":"Bob","age":42}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Any other type that is returned from a route handler will be converted to a string and sent as the response body with the appropriate length header and the content type `text/plain`.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
server.route('test/route', (req) => {
|
|
475
|
+
return Symbol('foo');
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
```http
|
|
479
|
+
HTTP/1.1 200 OK
|
|
480
|
+
Content-Length: 7
|
|
481
|
+
Content-Type: text/plain;charset=utf-8
|
|
482
|
+
|
|
483
|
+
Symbol(foo)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### `server.default(handler: DefaultHandler)`
|
|
487
|
+
|
|
488
|
+
The server uses a default handler which responds to requests for which there was no handler registered, or the registered handler returned a numeric status code.
|
|
489
|
+
|
|
490
|
+
This default handler sends a simple response to the client with the status code and a body containing the status message.
|
|
491
|
+
|
|
492
|
+
```http
|
|
493
|
+
HTTP/1.1 404 Not Found
|
|
494
|
+
Content-Length: 9
|
|
495
|
+
Content-Type: text/plain;charset=utf-8
|
|
496
|
+
|
|
497
|
+
Not Found
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
To customize the behavior of this handler, you can register a custom default handler using the `default` function.
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
server.default((req, status_code) => {
|
|
504
|
+
return new Response(`Custom error: ${status_code}`, { status: status_code });
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Using your own default handler allows you to provide a custom response for unhandled requests based on the status code.
|
|
509
|
+
|
|
510
|
+
The return type from this handler can be any of the expected return types from a normal route handler with the exception that returning a `number` type will not be treated as a status code and will instead be treated as a plain text response.
|
|
511
|
+
|
|
512
|
+
If is worth noting that if you return a `Response` object from this handler, you must implicitly set the status code. If you do not, the status code will be set to `200` by default.
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
server.default((req, status_code) => {
|
|
516
|
+
return new Response(`Custom error: ${status_code}`);
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
```http
|
|
520
|
+
HTTP/1.1 200 OK
|
|
521
|
+
Content-Length: 18
|
|
522
|
+
Content-Type: text/plain;charset=utf-8
|
|
523
|
+
|
|
524
|
+
Custom error: 404
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Returning anything else, such as a `Blob`, `object` or `string`, the status code will automatically be set to `status_code`. To override this behavior you must provide a `Response` object.
|
|
528
|
+
|
|
529
|
+
#### `server.handle(status_code: number, handler: RequestHandler)`
|
|
530
|
+
|
|
531
|
+
The `handle` function allows you to register a handler for a specific status code. This handler will take priority over the default handler.
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
server.handle(500, (req) => {
|
|
535
|
+
return new Response('Custom Internal Server Error Message', { status: 500 });
|
|
536
|
+
});
|
|
537
|
+
```
|
|
538
|
+
```http
|
|
539
|
+
HTTP/1.1 500 Internal Server Error
|
|
540
|
+
Content-Length: 36
|
|
541
|
+
Content-Type: text/plain;charset=utf-8
|
|
542
|
+
|
|
543
|
+
Custom Internal Server Error Message
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
The return type from this handler can be any of the expected return types from a normal route handler with the exception that returning a `number` type will not be treated as a status code and will instead be treated as a plain text response.
|
|
547
|
+
|
|
548
|
+
If is worth noting that if you return a `Response` object from this handler, you must implicitly set the status code. If you do not, the status code will be set to `200` by default.
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
server.handle(500, (req) => {
|
|
552
|
+
return new Response('Custom Internal Server Error Message');
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
```http
|
|
556
|
+
HTTP/1.1 200 OK
|
|
557
|
+
Content-Length: 36
|
|
558
|
+
Content-Type: text/plain;charset=utf-8
|
|
559
|
+
|
|
560
|
+
Custom Internal Server Error Message
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Returning anything else, such as a `Blob`, `object` or `string`, the status code will automatically be set. To override this behavior you must provide a `Response` object.
|
|
564
|
+
|
|
565
|
+
#### `server.error(handler: ErrorHandler)`
|
|
566
|
+
|
|
567
|
+
The `error` function allows you to register a handler for any uncaught errors that occur during the request handling process.
|
|
568
|
+
|
|
569
|
+
Unlike other handlers, it does not accept asynchronous functions and it must return a `Response` object.
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
server.error((req, err) => {
|
|
573
|
+
return new Response('Custom Internal Server Error Message', { status: 500 });
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
```http
|
|
577
|
+
HTTP/1.1 500 Internal Server Error
|
|
578
|
+
Content-Length: 36
|
|
579
|
+
Content-Type: text/plain;charset=utf-8
|
|
580
|
+
|
|
581
|
+
Custom Internal Server Error Message
|
|
582
|
+
```
|
|
583
|
+
This should be used as a last resort to catch unintended errors and should not be part of your normal request handling process. Generally speaking, this handler should only be called if you have a bug in your code.
|
|
584
|
+
|
|
585
|
+
#### `server.dir(path: string, dir: string)`
|
|
586
|
+
|
|
587
|
+
The `dir` function allows you to serve static files from a directory on your file system.
|
|
588
|
+
|
|
589
|
+
```ts
|
|
590
|
+
server.dir('/content', './public/content');
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
The above example will serve all files from `./public/content` to any requests made to `/content`. For example `/content/test.txt` will serve the file `./public/content/test.txt`.
|
|
594
|
+
|
|
595
|
+
- This function is recursive and will serve all files from the specified directory and any subdirectories.
|
|
596
|
+
- Requesting a directory will return a 401 response (subject to your configured handlers).
|
|
597
|
+
- Requesting a file that does not exist will return a 404 response (subject to your configured handlers).
|
|
598
|
+
- Requesting a file that is not readable will return a 500 response (subject to your configured handlers).
|
|
599
|
+
|
|
600
|
+
By default, hidden files (files prefixed with `.`) will not be served. To serve hidden files, you must set `ignoreHidden` to `false` in the `options` parameter.
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
server.dir('/content', './public/content', { ignoreHidden: false });
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
If `ignoreHidden` is set to `true` (default) then requesting a hidden file will return a 404 response (subject to your configured handlers).
|
|
607
|
+
|
|
608
|
+
Additionally, the `index` property can be set to a filename such as `index.html` to serve a default file when a directory is requested.
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
server.dir('/content', './public/content', { index: 'index.html' });
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
The above will serve `./public/content/index.html` when `/content` is requested.
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
#### `route_location(redirect_url: string)`
|
|
619
|
+
|
|
620
|
+
The `route_location` is a built-in request handler that redirects the client to a specified URL with the status code `301 Moved Permanently`.
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
server.route('test/route', route_location('https://example.com');
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
The above is a much shorter equivalent to the following:
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
server.route('test/route', (req, url) => {
|
|
630
|
+
return new Response(null, {
|
|
631
|
+
status: 301,
|
|
632
|
+
headers: {
|
|
633
|
+
Location: 'https://example.com',
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
---
|
|
639
|
+
|
|
318
640
|
#### `caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>`
|
|
319
641
|
Raise a warning issue on GitHub. This is useful for non-fatal errors which you want to be notified about.
|
|
320
642
|
|
package/package.json
CHANGED
package/src/api.d.ts
CHANGED
|
@@ -1,2 +1,28 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
/// <reference types="node" />
|
|
1
3
|
export declare function panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>;
|
|
2
4
|
export declare function caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>;
|
|
5
|
+
type HandlerReturnType = any;
|
|
6
|
+
type RequestHandler = (req: Request, url: URL) => HandlerReturnType;
|
|
7
|
+
type ErrorHandler = (err: Error) => Response;
|
|
8
|
+
type DefaultHandler = (req: Request, status_code: number) => HandlerReturnType;
|
|
9
|
+
type StatusCodeHandler = (req: Request) => HandlerReturnType;
|
|
10
|
+
type DirOptions = {
|
|
11
|
+
ignoreHidden?: boolean;
|
|
12
|
+
index?: string;
|
|
13
|
+
};
|
|
14
|
+
/** Built-in route handler for redirecting to a different URL. */
|
|
15
|
+
export declare function route_location(redirect_url: string): (req: Request, url: URL) => Response;
|
|
16
|
+
export declare function serve(port: number): {
|
|
17
|
+
/** Register a handler for a specific route. */
|
|
18
|
+
route: (path: string, handler: RequestHandler) => void;
|
|
19
|
+
/** Serve a directory for a specific route. */
|
|
20
|
+
dir: (path: string, dir: string, options?: DirOptions) => void;
|
|
21
|
+
/** Register a default handler for all status codes. */
|
|
22
|
+
default: (handler: DefaultHandler) => void;
|
|
23
|
+
/** Register a handler for a specific status code. */
|
|
24
|
+
handle: (status_code: number, handler: StatusCodeHandler) => void;
|
|
25
|
+
/** Register a handler for uncaught errors. */
|
|
26
|
+
error: (handler: ErrorHandler) => void;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
package/src/api.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { dispatch_report } from './dispatch';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
2
5
|
|
|
3
6
|
async function handle_error(prefix: string, err_message_or_obj: string | object, ...err: unknown[]): Promise<void> {
|
|
4
7
|
let error_message = 'unknown error';
|
|
@@ -36,4 +39,205 @@ export async function panic(err_message_or_obj: string | object, ...err: object[
|
|
|
36
39
|
|
|
37
40
|
export async function caution(err_message_or_obj: string | object, ...err: object[]): Promise<void> {
|
|
38
41
|
await handle_error('caution: ', err_message_or_obj, ...err);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type HandlerReturnType = any;
|
|
45
|
+
type RequestHandler = (req: Request, url: URL) => HandlerReturnType;
|
|
46
|
+
type ErrorHandler = (err: Error) => Response;
|
|
47
|
+
type DefaultHandler = (req: Request, status_code: number) => HandlerReturnType;
|
|
48
|
+
type StatusCodeHandler = (req: Request) => HandlerReturnType;
|
|
49
|
+
|
|
50
|
+
type DirOptions = {
|
|
51
|
+
ignoreHidden?: boolean;
|
|
52
|
+
index?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Built-in route handler for redirecting to a different URL. */
|
|
56
|
+
export function route_location(redirect_url: string) {
|
|
57
|
+
return (req: Request, url: URL) => {
|
|
58
|
+
return new Response(null, {
|
|
59
|
+
status: 301,
|
|
60
|
+
headers: {
|
|
61
|
+
Location: redirect_url
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function route_directory(route_path: string, dir: string, options: DirOptions): RequestHandler {
|
|
68
|
+
const ignore_hidden = options.ignoreHidden ?? true;
|
|
69
|
+
|
|
70
|
+
return async (req: Request, url: URL) => {
|
|
71
|
+
const file_path = path.join(dir, url.pathname.slice(route_path.length));
|
|
72
|
+
|
|
73
|
+
if (ignore_hidden && path.basename(file_path).startsWith('.'))
|
|
74
|
+
return 404;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const file_stat = await fs.stat(file_path);
|
|
78
|
+
|
|
79
|
+
if (file_stat.isDirectory()) {
|
|
80
|
+
if (options.index !== undefined) {
|
|
81
|
+
const index_path = path.join(file_path, options.index);
|
|
82
|
+
const index = Bun.file(index_path);
|
|
83
|
+
|
|
84
|
+
if (index.size !== 0)
|
|
85
|
+
return index;
|
|
86
|
+
}
|
|
87
|
+
return 401;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Bun.file(file_path);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
const err = e as NodeJS.ErrnoException;
|
|
93
|
+
if (err?.code === 'ENOENT')
|
|
94
|
+
return 404;
|
|
95
|
+
|
|
96
|
+
return 500;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function serve(port: number) {
|
|
102
|
+
const routes = new Map<string[], RequestHandler>();
|
|
103
|
+
const handlers = new Map<number, StatusCodeHandler>();
|
|
104
|
+
|
|
105
|
+
let error_handler: ErrorHandler | undefined;
|
|
106
|
+
let default_handler: DefaultHandler | undefined;
|
|
107
|
+
|
|
108
|
+
async function resolve_handler(response: HandlerReturnType | Promise<HandlerReturnType>, status_code: number, return_status_code = false): Promise<Response | number> {
|
|
109
|
+
if (response instanceof Promise)
|
|
110
|
+
response = await response;
|
|
111
|
+
|
|
112
|
+
// Pre-assembled responses are returned as-is.
|
|
113
|
+
if (response instanceof Response)
|
|
114
|
+
return response;
|
|
115
|
+
|
|
116
|
+
// Content-type/content-length are automatically set for blobs.
|
|
117
|
+
if (response instanceof Blob)
|
|
118
|
+
return new Response(response, { status: status_code });
|
|
119
|
+
|
|
120
|
+
// Status codes can be returned from some handlers.
|
|
121
|
+
if (return_status_code && typeof response === 'number')
|
|
122
|
+
return response;
|
|
123
|
+
|
|
124
|
+
// This should cover objects, arrays, etc.
|
|
125
|
+
if (typeof response === 'object')
|
|
126
|
+
return new Response(JSON.stringify(response), { status: status_code, headers: { 'Content-Type': 'application/json' } });
|
|
127
|
+
|
|
128
|
+
return new Response(String(response), { status: status_code })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const server = Bun.serve({
|
|
132
|
+
port,
|
|
133
|
+
development: false,
|
|
134
|
+
|
|
135
|
+
async fetch(req: Request): Promise<Response> {
|
|
136
|
+
const url = new URL(req.url);
|
|
137
|
+
let status_code = 200;
|
|
138
|
+
|
|
139
|
+
console.log(`${req.method} ${url.pathname}`);
|
|
140
|
+
|
|
141
|
+
const route_array = url.pathname.split('/').filter(e => !(e === '..' || e === '.'));
|
|
142
|
+
let handler: RequestHandler | undefined;
|
|
143
|
+
|
|
144
|
+
for (const [path, route_handler] of routes) {
|
|
145
|
+
const is_trailing_wildcard = path[path.length - 1] === '*';
|
|
146
|
+
if (!is_trailing_wildcard && path.length !== route_array.length)
|
|
147
|
+
continue;
|
|
148
|
+
|
|
149
|
+
let match = true;
|
|
150
|
+
for (let i = 0; i < path.length; i++) {
|
|
151
|
+
const path_part = path[i];
|
|
152
|
+
|
|
153
|
+
if (path_part === '*')
|
|
154
|
+
continue;
|
|
155
|
+
|
|
156
|
+
if (path_part.startsWith(':')) {
|
|
157
|
+
url.searchParams.append(path_part.slice(1), route_array[i]);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (path_part !== route_array[i]) {
|
|
162
|
+
match = false;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (match) {
|
|
168
|
+
handler = route_handler;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check for a handler for the route.
|
|
174
|
+
if (handler !== undefined) {
|
|
175
|
+
const response = await resolve_handler(handler(req, url), status_code, true);
|
|
176
|
+
if (response instanceof Response)
|
|
177
|
+
return response;
|
|
178
|
+
|
|
179
|
+
// If the handler returned a status code, use that instead.
|
|
180
|
+
status_code = response;
|
|
181
|
+
} else {
|
|
182
|
+
status_code = 404;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Fallback to checking for a handler for the status code.
|
|
186
|
+
const status_code_handler = handlers.get(status_code);
|
|
187
|
+
if (status_code_handler !== undefined) {
|
|
188
|
+
const response = await resolve_handler(status_code_handler(req), status_code);
|
|
189
|
+
if (response instanceof Response)
|
|
190
|
+
return response;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fallback to the default handler, if any.
|
|
194
|
+
if (default_handler !== undefined) {
|
|
195
|
+
const response = await resolve_handler(default_handler(req, status_code), status_code);
|
|
196
|
+
if (response instanceof Response)
|
|
197
|
+
return response;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Fallback to returning a basic response.
|
|
201
|
+
return new Response(http.STATUS_CODES[status_code], { status: status_code });
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
error(err: Error): Response {
|
|
205
|
+
if (error_handler !== undefined)
|
|
206
|
+
return error_handler(err);
|
|
207
|
+
|
|
208
|
+
return new Response(http.STATUS_CODES[500], { status: 500 });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
console.log(`Server started on port ${port}`);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
/** Register a handler for a specific route. */
|
|
216
|
+
route: (path: string, handler: RequestHandler): void => {
|
|
217
|
+
routes.set(path.split('/'), handler);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/** Serve a directory for a specific route. */
|
|
221
|
+
dir: (path: string, dir: string, options?: DirOptions): void => {
|
|
222
|
+
if (path.endsWith('/'))
|
|
223
|
+
path = path.slice(0, -1);
|
|
224
|
+
|
|
225
|
+
routes.set([...path.split('/'), '*'], route_directory(path, dir, options ?? {}));
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
/** Register a default handler for all status codes. */
|
|
229
|
+
default: (handler: DefaultHandler): void => {
|
|
230
|
+
default_handler = handler;
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
/** Register a handler for a specific status code. */
|
|
234
|
+
handle: (status_code: number, handler: StatusCodeHandler): void => {
|
|
235
|
+
handlers.set(status_code, handler);
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/** Register a handler for uncaught errors. */
|
|
239
|
+
error: (handler: ErrorHandler): void => {
|
|
240
|
+
error_handler = handler;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
39
243
|
}
|