lightpress 1.0.2 → 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,205 +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
- }
89
+ function recover(request, error) {
90
+ // Use this to run some cleanup code or do some logging.
160
91
 
161
- console.warn("Trying to access logger, but was not injected.");
162
-
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
98
 
167
- If no `log` function was injected into the context object, a warning is
168
- printed and a `noop`-fallback is return instead.
169
-
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.
197
-
198
- ## Environment Variables
128
+ ## Custom Handler Types and Context
199
129
 
200
- Lightpress reacts to certain environment variables that can be used to control
201
- the internal behaviour.
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.
202
131
 
203
- | Variable Name | Variable Value | Description |
204
- | :----------------- | :------------- | :------------------------------------------ |
205
- | `LIGHTPRESS_ERROR` | `verbose` | Writes unhandled errors to `console.error`. |
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,13 +1,11 @@
1
- import { LightpressError } from "./types/lightpress-error";
2
- import { LightpressResult } from "./types/lightpress-result";
3
- export declare class HttpError extends Error implements LightpressError {
4
- readonly name: string;
5
- readonly statusCode: number;
6
- /**
7
- * The HTTP error represents an error based on the HTTP error codes.
8
- * @param statusCode An HTTP status code
9
- */
10
- constructor(statusCode: number);
11
- /** Converts the error to an HTTP result object. */
12
- 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);
13
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,8 +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-result";
1
+ export * from "./create-request-listener";
5
2
  export * from "./http-error";
6
- export * from "./is-lightpress-error";
7
- export * from "./lightpress";
8
- export { lightpress as default } from "./lightpress";
3
+ export { createRequestListener as default } from "./create-request-listener";
package/lib/index.js CHANGED
@@ -1,100 +1,3 @@
1
- 'use strict';
2
-
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- var http = require('http');
6
-
7
- class HttpError extends Error {
8
- __init() {this.name = "HttpError";}
9
-
10
-
11
- /**
12
- * The HTTP error represents an error based on the HTTP error codes.
13
- * @param statusCode An HTTP status code
14
- */
15
- constructor(statusCode) {
16
- super(http.STATUS_CODES[statusCode]);HttpError.prototype.__init.call(this);
17
- if (Error.captureStackTrace) {
18
- Error.captureStackTrace(this, this.constructor);
19
- }
20
-
21
- this.statusCode = statusCode;
22
- }
23
-
24
- /** Converts the error to an HTTP result object. */
25
- toResult() {
26
- return { statusCode: this.statusCode };
27
- }
28
- }
29
-
30
- function isLightpressError(error) {
31
- return Boolean(error && typeof error.toResult === "function");
32
- }
33
-
34
- function isReadableStream(data) {
35
- return Boolean(
36
- data &&
37
- data.readable === true &&
38
- typeof data.pipe === "function" &&
39
- typeof data._read === "function"
40
- );
41
- }
42
-
43
- function sendResult(
44
- response,
45
- result
46
- ) {
47
- const statusCode = result && result.statusCode ? result.statusCode : 200;
48
- const headers = result && result.headers ? result.headers : null;
49
- const body = result && result.body ? result.body : null;
50
-
51
- if (headers) {
52
- response.writeHead(statusCode, headers);
53
- } else {
54
- response.statusCode = statusCode;
55
- }
56
-
57
- if (isReadableStream(body)) {
58
- body.pipe(response);
59
- } else {
60
- response.end(body);
61
- }
62
- }
63
-
64
- function sendError(response, error) {
65
- // TODO: investigate alternatives for application specific debugging/logging,
66
- // maybe by passing in an `unhandledError` callback function from lightpress
67
- // options object.
68
- // Consider if a `LightpressError` should be excluded here.
69
- if (process.env.LIGHTPRESS_ERROR === "verbose") {
70
- console.error(error);
71
- }
72
-
73
- if (isLightpressError(error)) {
74
- sendResult(response, error.toResult());
75
- } else {
76
- sendResult(response, { statusCode: 500 });
77
- }
78
- }
79
-
80
- function lightpress(
81
- handler
82
- ) {
83
- if (typeof handler !== "function") {
84
- throw new TypeError("request handler must be a function");
85
- }
86
-
87
- return (request, response) => {
88
- // Directly return the promise so that it's resolution can be tracked
89
- // outside, e.g. in unit tests.
90
- return Promise.resolve({ request })
91
- .then((context) => handler(context))
92
- .then((result) => sendResult(response, result))
93
- .catch((error) => sendError(response, error));
94
- };
95
- }
96
-
97
- exports.HttpError = HttpError;
98
- exports.default = lightpress;
99
- exports.isLightpressError = isLightpressError;
100
- exports.lightpress = lightpress;
1
+ export * from "./create-request-listener";
2
+ export * from "./http-error";
3
+ export { createRequestListener as default } from "./create-request-listener";
@@ -1,2 +1,2 @@
1
- /// <reference types="node" />
1
+ /** Typeguard to test if the given object is a readable stream. */
2
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,4 +1,4 @@
1
- /// <reference types="node" />
2
- import { ServerResponse } from "http";
3
- import { LightpressResult } from "./types/lightpress-result";
4
- 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.0.2",
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 --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.9",
46
- "@types/node": "14.0.27",
47
- "jest": "26.4.0",
48
- "prettier": "2.0.5",
49
- "rimraf": "3.0.2",
50
- "rollup": "2.24.0",
51
- "rollup-plugin-import-resolver": "1.0.4",
52
- "typescript": "3.9.7"
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
  }
package/lib/index.mjs DELETED
@@ -1,94 +0,0 @@
1
- import { STATUS_CODES } from 'http';
2
-
3
- class HttpError extends Error {
4
- __init() {this.name = "HttpError";}
5
-
6
-
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) {
12
- super(STATUS_CODES[statusCode]);HttpError.prototype.__init.call(this);
13
- if (Error.captureStackTrace) {
14
- Error.captureStackTrace(this, this.constructor);
15
- }
16
-
17
- this.statusCode = statusCode;
18
- }
19
-
20
- /** Converts the error to an HTTP result object. */
21
- toResult() {
22
- return { statusCode: this.statusCode };
23
- }
24
- }
25
-
26
- function isLightpressError(error) {
27
- return Boolean(error && typeof error.toResult === "function");
28
- }
29
-
30
- function isReadableStream(data) {
31
- return Boolean(
32
- data &&
33
- data.readable === true &&
34
- typeof data.pipe === "function" &&
35
- typeof data._read === "function"
36
- );
37
- }
38
-
39
- function sendResult(
40
- response,
41
- result
42
- ) {
43
- const statusCode = result && result.statusCode ? result.statusCode : 200;
44
- const headers = result && result.headers ? result.headers : null;
45
- const body = result && result.body ? result.body : null;
46
-
47
- if (headers) {
48
- response.writeHead(statusCode, headers);
49
- } else {
50
- response.statusCode = statusCode;
51
- }
52
-
53
- if (isReadableStream(body)) {
54
- body.pipe(response);
55
- } else {
56
- response.end(body);
57
- }
58
- }
59
-
60
- function sendError(response, error) {
61
- // TODO: investigate alternatives for application specific debugging/logging,
62
- // maybe by passing in an `unhandledError` callback function from lightpress
63
- // options object.
64
- // Consider if a `LightpressError` should be excluded here.
65
- if (process.env.LIGHTPRESS_ERROR === "verbose") {
66
- console.error(error);
67
- }
68
-
69
- if (isLightpressError(error)) {
70
- sendResult(response, error.toResult());
71
- } else {
72
- sendResult(response, { statusCode: 500 });
73
- }
74
- }
75
-
76
- function lightpress(
77
- handler
78
- ) {
79
- if (typeof handler !== "function") {
80
- throw new TypeError("request handler must be a function");
81
- }
82
-
83
- return (request, response) => {
84
- // Directly return the promise so that it's resolution can be tracked
85
- // outside, e.g. in unit tests.
86
- return Promise.resolve({ request })
87
- .then((context) => handler(context))
88
- .then((result) => sendResult(response, result))
89
- .catch((error) => sendError(response, error));
90
- };
91
- }
92
-
93
- export default lightpress;
94
- export { HttpError, isLightpressError, lightpress };
@@ -1,2 +0,0 @@
1
- import { LightpressError } from "./types/lightpress-error";
2
- export declare function isLightpressError(error: any): error is LightpressError;
@@ -1,5 +0,0 @@
1
- /// <reference types="node" />
2
- import { IncomingMessage, ServerResponse } from "http";
3
- import { LightpressHandler } from "./types/lightpress-handler";
4
- import { LightpressContext } from "./types/lightpress-context";
5
- export declare function lightpress(handler: LightpressHandler<LightpressContext>): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
@@ -1,3 +0,0 @@
1
- /// <reference types="node" />
2
- import { ServerResponse } from "http";
3
- export declare function sendError(response: ServerResponse, error: Error): void;
@@ -1,6 +0,0 @@
1
- /// <reference types="node" />
2
- import { IncomingMessage } from "http";
3
- export declare type LightpressContext = {
4
- /** The incoming server request. */
5
- request: IncomingMessage;
6
- };
@@ -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 within `sendError()`.
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 {
10
- toResult(): LightpressResult;
11
- }
@@ -1,3 +0,0 @@
1
- import { LightpressContext } from "./lightpress-context";
2
- import { LightpressResult } from "./lightpress-result";
3
- export declare type LightpressHandler<T extends LightpressContext> = (context: T) => LightpressResult | Promise<LightpressResult>;
@@ -1,10 +0,0 @@
1
- /// <reference types="node" />
2
- import { OutgoingHttpHeaders } from "http";
3
- export declare type LightpressResult = void | null | {
4
- /** Optional response status code (defaults to `200`). */
5
- statusCode?: null | number;
6
- /** Optional response payload. */
7
- body?: null | string | Buffer | NodeJS.ReadableStream;
8
- /** Optional HTTP response headers. */
9
- headers?: null | OutgoingHttpHeaders;
10
- };