lightpress 1.1.0 → 2.0.0-beta.1

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/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -1,196 +1,132 @@
1
1
  # lightpress
2
2
 
3
- Lightpress is a thin wrapper around node's HTTP handler interface, that enables
4
- you to
5
-
6
- - compose a handler tree without overhead
7
- - write reusable and easy-to-test handler functions
8
-
9
- Although you can use lightpress for any kind of application, it was designed
10
- with modern API driven web applications in mind. These usually require a single
11
- handler for serving the (SSR) HTML content, another one for static assets, and
12
- one or more handlers for data.
3
+ Lightpress is a thin wrapper around Node's HTTP request event, providing a composable HTTP handler interface.
13
4
 
14
5
  ## Installation
15
6
 
16
- You can install lightpress from [npmjs.com](https://www.npmjs.com) using your
17
- favorite package manager, e.g.
18
-
19
7
  ```bash
20
- $ npm install --save lightpress
8
+ npm add lightpress
21
9
  ```
22
10
 
23
- ## Getting Started
24
-
25
- In lightpress a request handler is a plain function that takes a context object
26
- as single argument and returns a result or a promise that resolves to a result.
27
-
28
- By default, the context object only contains a reference to the incoming
29
- request, but can be augmented to your application's needs.
11
+ ## Basic Usage
30
12
 
31
- The handler's outcome, if any, has to be an object that might contain a
32
- `statusCode`, `headers` and a `body`.
13
+ Create an HTTP handler function that receives a Node.js `IncomingMessage` and returns a result:
33
14
 
34
15
  ```js
35
16
  import { createServer } from "http";
36
- import lightpress from "lightpress";
17
+ import { createRequestListener } from "lightpress";
37
18
 
38
- function hello(context) {
19
+ function greet(request) {
39
20
  return {
40
21
  statusCode: 200,
41
- headers: {
42
- "Content-Type": "text/plain",
43
- },
44
- body: `Hello from '${context.request.url}'.`,
22
+ headers: { "Content-Type": "text/plain" },
23
+ body: `Hello from '${request.url}'.`,
45
24
  };
46
25
  }
47
26
 
48
- const server = createServer(lightpress(hello));
49
- server.listen(8080);
27
+ createServer(
28
+ createRequestListener(greet)
29
+ ).listen(8080);
50
30
  ```
51
31
 
52
32
  ## Composing Handlers
53
33
 
54
- Putting everything into a single handler isn't sufficient. And the way
55
- lightpress solves this circumstance is by composing its handlers.
56
-
57
- Lets imagine, the `hello` handler from above must only be called for `GET`
58
- requests. To achieve this we could simply check the request method inside our
59
- `hello` handler. However, a better approach is to create a separate
60
- handler which only cares about request methods.
34
+ You can compose handlers for more control. For example, you can restrict allowed HTTP methods.
61
35
 
62
36
  ```js
63
- import lightpress, { HttpError } from "lightpress";
64
-
65
- // ...
37
+ import { HttpError } from "lightpress";
66
38
 
67
39
  function allowedMethods(methods, handler) {
68
- return (context) => {
69
- if (methods.includes(context.request.method)) {
70
- return handler(context);
40
+ return (request) => {
41
+ if (methods.includes(request.method)) {
42
+ return handler(request);
71
43
  }
72
-
73
44
  throw new HttpError(405);
74
45
  };
75
46
  }
76
47
 
77
- // ...
78
-
79
- const server = createServer(lightpress(allowedMethods(["GET"], hello)));
48
+ createServer(
49
+ createRequestListener(
50
+ allowedMethods(["GET"], greet)
51
+ )
52
+ ).listen(8080);
80
53
  ```
81
54
 
82
- The `allowedMethods` function is a factory that takes an array of allowed HTTP
83
- methods and a handler. It creates a new handler that will invoke the given one
84
- only if the method of the incoming request is included in the array of allowed
85
- methods. Otherwise, a `Method Not Allowed` error is thrown.
86
-
87
55
  ## Error Handling
88
56
 
89
- In lightpress, errors are handled using guards. A guard itself is just another
90
- handler that catches the error that was thrown from the inner handler and
91
- converts it to a result. As with any other handler, guards can be nested, giving
92
- you fine grained control on how the error flows.
57
+ Lightpress supports flexible error handling at multiple levels. You can create special HTTP handlers that act as error guards. These guards allow you to control how specific parts of your handler tree respond to errors. For example, a guard around your rendering code could send errors as HTML, while a guard around your API could return JSON responses.
93
58
 
94
59
  ```js
95
- // ...
96
-
97
- function catchError(handler) {
98
- return (context) =>
99
- new Promise((resolve) => resolve(handler(context))).catch((error) => {
100
- const statusCode = error instanceof HttpError ? error.statusCode : 500;
101
- const message =
102
- statusCode === 405 ? "Better watch your verbs." : "My bad.";
103
- const body = Buffer.from(message);
104
-
105
- return {
106
- headers: {
107
- "Content-Type": "text/plain",
108
- "Content-Length": body.length,
109
- },
110
- statusCode,
111
- body,
112
- };
113
- });
60
+ import { HttpError } from "lightpress";
61
+
62
+ async function errorGuard(handler: HttpHandler) {
63
+ try {
64
+ return await handler(request);
65
+ } catch (error) {
66
+ // Handle the error and return a result or re-throw the error
67
+ // to be handled by an upper guard.
68
+ }
114
69
  }
115
-
116
- // ...
117
-
118
- const server = createServer(
119
- lightpress(catchError(allowedMethods(["GET"], hello)))
120
- );
121
70
  ```
122
71
 
123
- If an error is not handled, lightpress will catch it and send a basic error
124
- response without content.
125
-
126
- ## Custom Data
127
-
128
- The context object that is passed to a handler can be augmented with custom
129
- data. Although it is technically possible to create a new copy of that context
130
- object whenever you pass it on to the next handler, you most likely won't need
131
- that. In fact, some 3rd-party packages might rely on using the same reference
132
- and could break when creating a copy.
133
-
134
- The recommended way to augement the context object, is by providing a handler
135
- function that manipulates the context object. And another function that savely
136
- returns the desired data from the context object. Or provides a fallback.
137
-
138
- The following function adds a simple `log` function to the context object.
72
+ Additionally, any `HttpError` that reaches Lightpress’s root handler is considered a handled error and will be sent as an HTTP response. The `HttpError` constructor can receive either a status code or a full `HttpResult` object.
139
73
 
140
74
  ```js
141
- function injectLogger(handler) {
142
- return (context) => {
143
- const { method, url } = context.request;
144
-
145
- context.log = (message) => `${new Date()} [${method} ${url}]: ${message}`;
146
-
147
- return handler(context);
148
- };
149
- }
75
+ // Only status code
76
+ throw new HttpError(404);
77
+
78
+ // With full HTTP result
79
+ throw new HttpError({
80
+ statusCode: 404,
81
+ headers: { "Content-Type": "text/plain" },
82
+ body: "Not found",
83
+ });
150
84
  ```
151
85
 
152
- The `log` function can be retrieved from the context using the following
153
- function.
86
+ Any other error is considered unexpected, and Lightpress will therefore respond with a generic `500` error. However, you can pass a `recover` function to `createRequestListener` as a second argument for global error handling.
154
87
 
155
88
  ```js
156
- function extractLogger(context) {
157
- if (context.log) {
158
- return context.log;
159
- }
160
-
161
- console.warn("Trying to access logger, but was not injected.");
89
+ function recover(request, error) {
90
+ // Use this to run some cleanup code or do some logging.
162
91
 
163
- return () => void 0;
92
+ return {
93
+ statusCode: 500,
94
+ headers: { "Content-Type": "text/plain" },
95
+ body: "Internal Server Error",
96
+ };
164
97
  }
165
- ```
166
-
167
- If no `log` function was injected into the context object, a warning is
168
- printed and a `noop`-fallback is return instead.
169
98
 
170
- ```js
171
- // ...
99
+ createServer(
100
+ createRequestListener(greet, recover)
101
+ ).listen(8080);
102
+ ```
172
103
 
173
- function hello(context) {
174
- const log = extractLogger(context);
104
+ ## Handler Factories
175
105
 
176
- log("Serving request from hello handler.");
106
+ In real-world applications, it’s common to provide an HTTP handler by using a configurable factory. A factory can receive options, such as a database connection or other configuration, and returns an HTTP handler. This helps to decouple infrastructure from business logic and allows for simpler code reuse.
177
107
 
178
- return {
179
- statusCode: 200,
180
- headers: {
181
- "Content-Type": "text/plain",
182
- },
183
- body: `Hello from '${context.request.url}'.`,
108
+ ```js
109
+ function createApiHandler({ db }) {
110
+ return async (request) => {
111
+ const data = await db.getSomeData();
112
+
113
+ return {
114
+ statusCode: 200,
115
+ headers: { "Content-Type": "application/json" },
116
+ body: JSON.stringify(data),
117
+ };
184
118
  };
185
119
  }
186
120
 
187
- // ...
188
-
189
- const server = createServer(
190
- lightpress(injectLogger(catchError(allowedMethods(["GET"], hello))))
191
- );
121
+ createServer(
122
+ createRequestListener(
123
+ createApiHandler({ db })
124
+ )
125
+ ).listen(8080);
192
126
  ```
193
127
 
194
- Just like with error handlers, you have the exact same control when to extend
195
- the context object. This lets you for example inject a `user` right before your
196
- API handler is called, but ignore it for all sibling handlers.
128
+ ## Custom Handler Types and Context
129
+
130
+ Lightpress’s handler type is intentionally simple: it expects a function that receives a Node.js `IncomingMessage` and returns a result. Depending on your application, your HTTP handler may need additional request-related context, such as a timestamp, a user, or data that is expensive to retrieve. In this case, you will likely want to define your own handler type.
131
+
132
+ _TODO: add example_
@@ -0,0 +1,16 @@
1
+ import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from "node:http";
2
+ /** An object that is used to be send as HTTP response. */
3
+ export type HttpResult = null | undefined | {
4
+ /** Optional response status code (defaults to `200`). */
5
+ statusCode?: null | number;
6
+ /** Optional response body. */
7
+ body?: null | string | Buffer | NodeJS.ReadableStream;
8
+ /** Optional HTTP response headers. */
9
+ headers?: null | OutgoingHttpHeaders;
10
+ };
11
+ /** An HTTP request handler that creates an {@link HttpResult}. */
12
+ export type HttpHandler = (request: IncomingMessage) => HttpResult | Promise<HttpResult>;
13
+ /** A handler to recover from unhandled errors by the {@link HttpHandler}. */
14
+ export type RecoverHandler = (request: IncomingMessage, error: unknown) => HttpResult | Promise<HttpResult>;
15
+ /** Wraps an {@link HttpHandler} and returns a NodeJS request listener. */
16
+ export declare function createRequestListener(handler: HttpHandler, recover?: RecoverHandler): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { sendResult } from "./send-result";
2
+ import { HttpError } from "./http-error";
3
+ /** Wraps an {@link HttpHandler} and returns a NodeJS request listener. */
4
+ export function createRequestListener(handler, recover) {
5
+ if (typeof handler !== "function") {
6
+ throw new TypeError("request handler must be a function");
7
+ }
8
+ return (request, response) => new Promise((resolve) => resolve(handler(request)))
9
+ .catch((error) => {
10
+ // Instances of `HttpError` are send as response, all other error types
11
+ // are considered unhandled.
12
+ if (error instanceof HttpError) {
13
+ return error;
14
+ }
15
+ if (recover) {
16
+ return recover(request, error);
17
+ }
18
+ throw error;
19
+ })
20
+ .catch((error) => {
21
+ // Log unhandled error if no recover handler is defined or an error was
22
+ // thrown from the recover handler itself.
23
+ console.error(error);
24
+ return { statusCode: 500 };
25
+ })
26
+ .then((result) => sendResult(response, result));
27
+ }
@@ -1,14 +1,11 @@
1
- import { LightpressError } from "./types/lightpress-error";
2
- import { LightpressResult } from "./types/lightpress-result";
3
- /** Basic error implementation to signal HTTP errors. */
4
- export declare class HttpError extends Error implements LightpressError {
5
- readonly name: string;
6
- readonly statusCode: number;
7
- /**
8
- * The HTTP error represents an error based on the HTTP error codes.
9
- * @param statusCode An HTTP status code
10
- */
11
- constructor(statusCode: number);
12
- /** Converts the error to an HTTP result object. */
13
- toResult(): LightpressResult;
1
+ import type { OutgoingHttpHeaders } from "node:http";
2
+ import type { HttpResult } from "./create-request-listener";
3
+ /** An error that can be send as an HTTP result. */
4
+ export declare class HttpError extends Error implements NonNullable<HttpResult> {
5
+ name: string;
6
+ statusCode: number;
7
+ body?: null | string | Buffer | NodeJS.ReadableStream;
8
+ headers?: null | OutgoingHttpHeaders;
9
+ constructor(result: HttpResult, options?: ErrorOptions);
10
+ constructor(statusCode: number, options?: ErrorOptions);
14
11
  }
@@ -0,0 +1,21 @@
1
+ import { STATUS_CODES } from "node:http";
2
+ /** An error that can be send as an HTTP result. */
3
+ export class HttpError extends Error {
4
+ name = "HttpError";
5
+ statusCode;
6
+ body;
7
+ headers;
8
+ constructor(resultOrStatusCode, options) {
9
+ const [statusCode, result] = typeof resultOrStatusCode === "number"
10
+ ? [resultOrStatusCode, null]
11
+ : [resultOrStatusCode?.statusCode ?? 500, resultOrStatusCode];
12
+ super(STATUS_CODES[statusCode], options);
13
+ // TODO: investigate if this is really needed
14
+ if (Error.captureStackTrace) {
15
+ Error.captureStackTrace(this, this.constructor);
16
+ }
17
+ this.statusCode = statusCode;
18
+ this.body = result?.body;
19
+ this.headers = result?.headers;
20
+ }
21
+ }
package/lib/index.d.ts CHANGED
@@ -1,10 +1,3 @@
1
- export * from "./types/lightpress-context";
2
- export * from "./types/lightpress-error";
3
- export * from "./types/lightpress-handler";
4
- export * from "./types/lightpress-recovery-handler";
5
- export * from "./types/lightpress-result";
6
- export * from "./from-lightpress-error";
1
+ export * from "./create-request-listener";
7
2
  export * from "./http-error";
8
- export * from "./is-lightpress-error";
9
- export * from "./lightpress";
10
- export { lightpress as default } from "./lightpress";
3
+ export { createRequestListener as default } from "./create-request-listener";
package/lib/index.js CHANGED
@@ -1,143 +1,3 @@
1
- 'use strict';
2
-
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- var http = require('http');
6
-
7
- /**
8
- * Typeguard to test if the given object implements the `LightpressError`
9
- * interface.
10
- */
11
- function isLightpressError(error) {
12
- return Boolean(error && typeof error.toResult === "function");
13
- }
14
-
15
- /**
16
- * Wraps a recover function to automatically recover from `LightpressError`s by
17
- * calling `toResult()` on it. All other errors are passed on to the given
18
- * recover function.
19
- * **WARNING:** does not catch errors from the recover function itself!
20
- */
21
- function fromLightpressError(
22
- recoverUnhandled
23
- ) {
24
- return (request, error) => {
25
- if (isLightpressError(error)) {
26
- try {
27
- return error.toResult();
28
- } catch (exception) {
29
- return recoverUnhandled(request, exception);
30
- }
31
- } else {
32
- return recoverUnhandled(request, error);
33
- }
34
- };
35
- }
36
-
37
- /** Basic error implementation to signal HTTP errors. */
38
- class HttpError extends Error {
39
- __init() {this.name = "HttpError";}
40
-
41
-
42
- /**
43
- * The HTTP error represents an error based on the HTTP error codes.
44
- * @param statusCode An HTTP status code
45
- */
46
- constructor(statusCode) {
47
- super(http.STATUS_CODES[statusCode]);HttpError.prototype.__init.call(this);
48
- if (Error.captureStackTrace) {
49
- Error.captureStackTrace(this, this.constructor);
50
- }
51
-
52
- this.statusCode = statusCode;
53
- }
54
-
55
- /** Converts the error to an HTTP result object. */
56
- toResult() {
57
- return { statusCode: this.statusCode };
58
- }
59
- }
60
-
61
- /** Typeguard to test if the given object is a readable stream. */
62
- function isReadableStream(data) {
63
- return Boolean(
64
- data &&
65
- data.readable === true &&
66
- typeof data.pipe === "function" &&
67
- typeof data._read === "function"
68
- );
69
- }
70
-
71
- /** Takes a `LightpressResult` and sends it as HTTP response. */
72
- function sendResult(
73
- response,
74
- result
75
- ) {
76
- const statusCode = result && result.statusCode ? result.statusCode : 200;
77
- const headers = result && result.headers ? result.headers : null;
78
- const body = result && result.body ? result.body : null;
79
-
80
- if (headers) {
81
- response.writeHead(statusCode, headers);
82
- } else {
83
- response.statusCode = statusCode;
84
- }
85
-
86
- if (isReadableStream(body)) {
87
- body.pipe(response);
88
- } else {
89
- response.end(body);
90
- }
91
- }
92
-
93
- /**
94
- * Wraps a `LightpressHandler` into a function that directly be bound as handler
95
- * for an HTTP server's incoming `request` events.
96
- */
97
- function lightpress(
98
- handler,
99
- recover
100
- ) {
101
- if (typeof handler !== "function") {
102
- throw new TypeError("request handler must be a function");
103
- }
104
- if (recover && typeof recover !== "function") {
105
- throw new TypeError("recovery handler must be a function");
106
- }
107
-
108
- // Although it requires a little bit more boilerplate code, it is expected
109
- // that the given recover handler cares about `LightpressError`s itself. In
110
- // total, this provides more control over the error recovery.
111
- const innerRecover =
112
- recover || fromLightpressError(() => ({ statusCode: 500 }));
113
-
114
- return (request, response) =>
115
- // Directly return the promise so that it's resolution can be tracked
116
- // outside, e.g. in unit tests.
117
- new Promise((resolve) => resolve(handler({ request })))
118
- // Instead of the context object, the request is passed to the recovery
119
- // handler as we cannot know if the reference to the context has changed
120
- // during handler invokation. Assumably, it would lead to more confussion
121
- // about possibly missing properties than it would help.
122
- .catch((error) => innerRecover(request, error))
123
- .then((result) => sendResult(response, result))
124
- // Fallback guard to prevent the application to crash if error recovery
125
- // or sending the response have failed.
126
- .catch((error) => {
127
- console.error(error);
128
-
129
- try {
130
- // TODO: investigate if closing the connection is a better option
131
- request.pause();
132
- response.end();
133
- } catch (exception) {
134
- console.error(exception);
135
- }
136
- });
137
- }
138
-
139
- exports.HttpError = HttpError;
140
- exports.default = lightpress;
141
- exports.fromLightpressError = fromLightpressError;
142
- exports.isLightpressError = isLightpressError;
143
- exports.lightpress = lightpress;
1
+ export * from "./create-request-listener";
2
+ export * from "./http-error";
3
+ export { createRequestListener as default } from "./create-request-listener";
@@ -1,3 +1,2 @@
1
- /// <reference types="node" />
2
1
  /** Typeguard to test if the given object is a readable stream. */
3
2
  export declare function isReadableStream(data: any): data is NodeJS.ReadableStream;
@@ -0,0 +1,6 @@
1
+ /** Typeguard to test if the given object is a readable stream. */
2
+ export function isReadableStream(data) {
3
+ return Boolean(data?.readable === true &&
4
+ typeof data?.pipe === "function" &&
5
+ typeof data?._read === "function");
6
+ }
@@ -1,5 +1,4 @@
1
- /// <reference types="node" />
2
- import { ServerResponse } from "http";
3
- import { LightpressResult } from "./types/lightpress-result";
4
- /** Takes a `LightpressResult` and sends it as HTTP response. */
5
- export declare function sendResult(response: ServerResponse, result: LightpressResult): void;
1
+ import type { ServerResponse } from "node:http";
2
+ import type { HttpResult } from "./create-request-listener";
3
+ /** Passes the given {@link HttpResult} to the given {@link ServerResponse}. */
4
+ export declare function sendResult(response: ServerResponse, result: HttpResult): void;
@@ -0,0 +1,17 @@
1
+ import { isReadableStream } from "./is-readable-stream";
2
+ /** Passes the given {@link HttpResult} to the given {@link ServerResponse}. */
3
+ export function sendResult(response, result) {
4
+ const statusCode = result?.statusCode ?? 200;
5
+ if (result?.headers) {
6
+ response.writeHead(statusCode, result.headers);
7
+ }
8
+ else {
9
+ response.statusCode = statusCode;
10
+ }
11
+ if (isReadableStream(result?.body)) {
12
+ result.body.pipe(response);
13
+ }
14
+ else {
15
+ response.end(result?.body ?? null);
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
2
+ export declare function assertContentType<const TContentType extends string>(request: IncomingMessage, contentType: TContentType): asserts request is IncomingMessage & {
3
+ headers: IncomingHttpHeaders & {
4
+ ["content-type"]: TContentType;
5
+ };
6
+ };
@@ -0,0 +1,6 @@
1
+ import { HttpError } from "../http-error";
2
+ export function assertContentType(request, contentType) {
3
+ if (request.headers["content-type"] !== contentType) {
4
+ throw new HttpError(415);
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ export declare function assertMethod<const TMethod extends string>(request: IncomingMessage, method: TMethod): asserts request is IncomingMessage & {
3
+ method: TMethod;
4
+ };
@@ -0,0 +1,6 @@
1
+ import { HttpError } from "../http-error";
2
+ export function assertMethod(request, method) {
3
+ if (request.method !== method) {
4
+ throw new HttpError(405);
5
+ }
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ export declare function consumeBody(request: IncomingMessage, maxByteLength?: number): Promise<Buffer<ArrayBuffer>>;
@@ -0,0 +1,15 @@
1
+ import { HttpError } from "../http-error";
2
+ export async function consumeBody(request, maxByteLength) {
3
+ const chunks = [];
4
+ let chunksBytes = 0;
5
+ for await (const chunk of request.iterator()) {
6
+ chunks.push(chunk);
7
+ chunksBytes = chunksBytes + chunk.byteLength;
8
+ if (maxByteLength && chunksBytes > maxByteLength) {
9
+ chunks.length = 0;
10
+ chunksBytes = 0;
11
+ throw new HttpError(413);
12
+ }
13
+ }
14
+ return Buffer.concat(chunks, chunksBytes);
15
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ export declare function consumeJsonBody(request: IncomingMessage, maxByteLength?: number): Promise<any>;
@@ -0,0 +1,13 @@
1
+ import { HttpError } from "../http-error";
2
+ import { consumeBody } from "./consume-body";
3
+ export async function consumeJsonBody(request, maxByteLength) {
4
+ const buffer = await consumeBody(request, maxByteLength);
5
+ try {
6
+ return JSON.parse(buffer.toString("utf8"));
7
+ }
8
+ catch (error) {
9
+ throw new HttpError(400, {
10
+ cause: error,
11
+ });
12
+ }
13
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./assert-content-type";
2
+ export * from "./assert-method";
3
+ export * from "./consume-body";
4
+ export * from "./consume-json-body";
@@ -0,0 +1,4 @@
1
+ export * from "./assert-content-type";
2
+ export * from "./assert-method";
3
+ export * from "./consume-body";
4
+ export * from "./consume-json-body";
package/package.json CHANGED
@@ -1,54 +1,54 @@
1
1
  {
2
- "name": "lightpress",
3
- "version": "1.1.0",
4
- "description": "Your composable HTTP server.",
5
- "main": "lib/index.js",
6
- "module": "lib/index.mjs",
7
- "typings": "lib/index.d.ts",
8
- "engines": {
9
- "node": ">=12.0.0"
10
- },
11
- "scripts": {
12
- "build": "rimraf $npm_package_directories_lib && rollup -c",
13
- "develop": "rollup -c -w",
14
- "format": "prettier --ignore-path .gitignore --write .",
15
- "prepublishOnly": "npm run test && npm run build && npm run types",
16
- "test": "jest",
17
- "types": "tsc --emitDeclarationOnly"
18
- },
19
- "files": [
20
- "lib/**/*"
21
- ],
22
- "directories": {
23
- "lib": "lib"
24
- },
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/lunsdorf/lightpress.git"
28
- },
29
- "keywords": [
30
- "composable",
31
- "express",
32
- "http",
33
- "lightpress",
34
- "server"
35
- ],
36
- "license": "MIT",
37
- "author": "Sören Lünsdorf <code@lunsdorf.com>",
38
- "homepage": "https://github.com/lunsdorf/lightpress#readme",
39
- "bugs": {
40
- "url": "https://github.com/lunsdorf/lightpress/issues"
41
- },
42
- "devDependencies": {
43
- "@rollup/plugin-sucrase": "3.1.0",
44
- "@sucrase/jest-plugin": "2.0.0",
45
- "@types/jest": "26.0.20",
46
- "@types/node": "14.14.21",
47
- "jest": "26.6.3",
48
- "prettier": "2.2.1",
49
- "rimraf": "3.0.2",
50
- "rollup": "2.36.2",
51
- "rollup-plugin-import-resolver": "1.0.5",
52
- "typescript": "4.1.3"
53
- }
2
+ "name": "lightpress",
3
+ "version": "2.0.0-beta.1",
4
+ "description": "A thin composable HTTP handler interface around NodeJS native server listener.",
5
+ "type": "module",
6
+ "typings": "lib/index.d.ts",
7
+ "main": "lib/index.js",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./utility": "./lib/utility/index.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "engines": {
14
+ "node": ">=22.0.0"
15
+ },
16
+ "scripts": {
17
+ "build": "rimraf ./lib && tsc",
18
+ "check-format": "biome check .",
19
+ "check-types": "tsc --noEmit",
20
+ "format": "biome check --write .",
21
+ "prepublishOnly": "npm run check-format && npm run test && npm run build",
22
+ "test": "jest"
23
+ },
24
+ "files": [
25
+ "lib/**/*"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/lunsdorf/lightpress.git"
30
+ },
31
+ "keywords": [
32
+ "composable",
33
+ "express",
34
+ "http",
35
+ "lightpress",
36
+ "server"
37
+ ],
38
+ "license": "MIT",
39
+ "author": "Lunsdorf <lunsdorf@users.noreply.github.com>",
40
+ "homepage": "https://github.com/lunsdorf/lightpress#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/lunsdorf/lightpress/issues"
43
+ },
44
+ "devDependencies": {
45
+ "@biomejs/biome": "^2.3.11",
46
+ "@swc/core": "^1.15.8",
47
+ "@swc/jest": "^0.2.39",
48
+ "@types/jest": "^30.0.0",
49
+ "@types/node": "^22.0.0",
50
+ "jest": "^30.2.0",
51
+ "rimraf": "^6.1.2",
52
+ "typescript": "^5.9.3"
53
+ }
54
54
  }
@@ -1,8 +0,0 @@
1
- import { LightpressRecoveryHandler } from "./types/lightpress-recovery-handler";
2
- /**
3
- * Wraps a recover function to automatically recover from `LightpressError`s by
4
- * calling `toResult()` on it. All other errors are passed on to the given
5
- * recover function.
6
- * **WARNING:** does not catch errors from the recover function itself!
7
- */
8
- export declare function fromLightpressError(recoverUnhandled: LightpressRecoveryHandler): LightpressRecoveryHandler;
package/lib/index.mjs DELETED
@@ -1,136 +0,0 @@
1
- import { STATUS_CODES } from 'http';
2
-
3
- /**
4
- * Typeguard to test if the given object implements the `LightpressError`
5
- * interface.
6
- */
7
- function isLightpressError(error) {
8
- return Boolean(error && typeof error.toResult === "function");
9
- }
10
-
11
- /**
12
- * Wraps a recover function to automatically recover from `LightpressError`s by
13
- * calling `toResult()` on it. All other errors are passed on to the given
14
- * recover function.
15
- * **WARNING:** does not catch errors from the recover function itself!
16
- */
17
- function fromLightpressError(
18
- recoverUnhandled
19
- ) {
20
- return (request, error) => {
21
- if (isLightpressError(error)) {
22
- try {
23
- return error.toResult();
24
- } catch (exception) {
25
- return recoverUnhandled(request, exception);
26
- }
27
- } else {
28
- return recoverUnhandled(request, error);
29
- }
30
- };
31
- }
32
-
33
- /** Basic error implementation to signal HTTP errors. */
34
- class HttpError extends Error {
35
- __init() {this.name = "HttpError";}
36
-
37
-
38
- /**
39
- * The HTTP error represents an error based on the HTTP error codes.
40
- * @param statusCode An HTTP status code
41
- */
42
- constructor(statusCode) {
43
- super(STATUS_CODES[statusCode]);HttpError.prototype.__init.call(this);
44
- if (Error.captureStackTrace) {
45
- Error.captureStackTrace(this, this.constructor);
46
- }
47
-
48
- this.statusCode = statusCode;
49
- }
50
-
51
- /** Converts the error to an HTTP result object. */
52
- toResult() {
53
- return { statusCode: this.statusCode };
54
- }
55
- }
56
-
57
- /** Typeguard to test if the given object is a readable stream. */
58
- function isReadableStream(data) {
59
- return Boolean(
60
- data &&
61
- data.readable === true &&
62
- typeof data.pipe === "function" &&
63
- typeof data._read === "function"
64
- );
65
- }
66
-
67
- /** Takes a `LightpressResult` and sends it as HTTP response. */
68
- function sendResult(
69
- response,
70
- result
71
- ) {
72
- const statusCode = result && result.statusCode ? result.statusCode : 200;
73
- const headers = result && result.headers ? result.headers : null;
74
- const body = result && result.body ? result.body : null;
75
-
76
- if (headers) {
77
- response.writeHead(statusCode, headers);
78
- } else {
79
- response.statusCode = statusCode;
80
- }
81
-
82
- if (isReadableStream(body)) {
83
- body.pipe(response);
84
- } else {
85
- response.end(body);
86
- }
87
- }
88
-
89
- /**
90
- * Wraps a `LightpressHandler` into a function that directly be bound as handler
91
- * for an HTTP server's incoming `request` events.
92
- */
93
- function lightpress(
94
- handler,
95
- recover
96
- ) {
97
- if (typeof handler !== "function") {
98
- throw new TypeError("request handler must be a function");
99
- }
100
- if (recover && typeof recover !== "function") {
101
- throw new TypeError("recovery handler must be a function");
102
- }
103
-
104
- // Although it requires a little bit more boilerplate code, it is expected
105
- // that the given recover handler cares about `LightpressError`s itself. In
106
- // total, this provides more control over the error recovery.
107
- const innerRecover =
108
- recover || fromLightpressError(() => ({ statusCode: 500 }));
109
-
110
- return (request, response) =>
111
- // Directly return the promise so that it's resolution can be tracked
112
- // outside, e.g. in unit tests.
113
- new Promise((resolve) => resolve(handler({ request })))
114
- // Instead of the context object, the request is passed to the recovery
115
- // handler as we cannot know if the reference to the context has changed
116
- // during handler invokation. Assumably, it would lead to more confussion
117
- // about possibly missing properties than it would help.
118
- .catch((error) => innerRecover(request, error))
119
- .then((result) => sendResult(response, result))
120
- // Fallback guard to prevent the application to crash if error recovery
121
- // or sending the response have failed.
122
- .catch((error) => {
123
- console.error(error);
124
-
125
- try {
126
- // TODO: investigate if closing the connection is a better option
127
- request.pause();
128
- response.end();
129
- } catch (exception) {
130
- console.error(exception);
131
- }
132
- });
133
- }
134
-
135
- export default lightpress;
136
- export { HttpError, fromLightpressError, isLightpressError, lightpress };
@@ -1,6 +0,0 @@
1
- import { LightpressError } from "./types/lightpress-error";
2
- /**
3
- * Typeguard to test if the given object implements the `LightpressError`
4
- * interface.
5
- */
6
- export declare function isLightpressError(error: any): error is LightpressError;
@@ -1,10 +0,0 @@
1
- /// <reference types="node" />
2
- import { IncomingMessage, ServerResponse } from "http";
3
- import { LightpressContext } from "./types/lightpress-context";
4
- import { LightpressHandler } from "./types/lightpress-handler";
5
- import { LightpressRecoveryHandler } from "./types/lightpress-recovery-handler";
6
- /**
7
- * Wraps a `LightpressHandler` into a function that directly be bound as handler
8
- * for an HTTP server's incoming `request` events.
9
- */
10
- export declare function lightpress(handler: LightpressHandler<LightpressContext>, recover?: LightpressRecoveryHandler): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
@@ -1,7 +0,0 @@
1
- /// <reference types="node" />
2
- import { IncomingMessage } from "http";
3
- /** Basic context object required by an HTTP handler to create a result. */
4
- export declare type LightpressContext = {
5
- /** The incoming server request. */
6
- request: IncomingMessage;
7
- };
@@ -1,11 +0,0 @@
1
- import { LightpressResult } from "./lightpress-result";
2
- /**
3
- * Represents a handled error result that fits the dataflow of promises and can
4
- * be recoverd from.
5
- * NOTE: Intentionally, this is not designed as a common result factory as it
6
- * would make dataflow overly complicated and can already be archieved through
7
- * the use of promises itself.
8
- */
9
- export interface LightpressError extends Error {
10
- toResult(): LightpressResult;
11
- }
@@ -1,7 +0,0 @@
1
- import { LightpressContext } from "./lightpress-context";
2
- import { LightpressResult } from "./lightpress-result";
3
- /**
4
- * Describes a function that creates a `LightpressResult` for incoming HTTP
5
- * requests.
6
- */
7
- export declare type LightpressHandler<T extends LightpressContext> = (context: T) => LightpressResult | Promise<LightpressResult>;
@@ -1,5 +0,0 @@
1
- /// <reference types="node" />
2
- import { IncomingMessage } from "http";
3
- import { LightpressResult } from "./lightpress-result";
4
- /** Describes a function to convert an error to a `LightpressResult`. */
5
- export declare type LightpressRecoveryHandler = (request: IncomingMessage, error: Error) => LightpressResult | Promise<LightpressResult>;
@@ -1,11 +0,0 @@
1
- /// <reference types="node" />
2
- import { OutgoingHttpHeaders } from "http";
3
- /** An object that is used to be send as HTTP response. */
4
- export declare type LightpressResult = void | null | {
5
- /** Optional response status code (defaults to `200`). */
6
- statusCode?: null | number;
7
- /** Optional response payload. */
8
- body?: null | string | Buffer | NodeJS.ReadableStream;
9
- /** Optional HTTP response headers. */
10
- headers?: null | OutgoingHttpHeaders;
11
- };