@bjoernboss/mws 1.0.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/LICENSE.txt +29 -0
- package/README.md +301 -0
- package/dist/base.d.ts +198 -0
- package/dist/base.js +73 -0
- package/dist/builder.d.ts +58 -0
- package/dist/builder.js +146 -0
- package/dist/cache.d.ts +66 -0
- package/dist/cache.js +708 -0
- package/dist/client.d.ts +228 -0
- package/dist/client.js +1646 -0
- package/dist/handler.d.ts +119 -0
- package/dist/handler.js +542 -0
- package/dist/helper.d.ts +33 -0
- package/dist/helper.js +363 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +12 -0
- package/dist/log.d.ts +52 -0
- package/dist/log.js +288 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +257 -0
- package/package.json +46 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Bjoern Boss Henrichsen
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# MWS - Modular Web Server
|
|
2
|
+

|
|
3
|
+
[](LICENSE.txt)
|
|
4
|
+
|
|
5
|
+
A lightweight TypeScript framework for hosting isolated modules behind HTTP/HTTPS and WebSocket endpoints. Each module owns a subtree in the URL space and is isolated from its siblings. Modules compose into trees for path-based routing, hostname routing, and request interception.
|
|
6
|
+
|
|
7
|
+
The server integrates various automation features, such as error handling, validations, caching... Further, it contains integrated logging for proper connection tracing and logging.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
Only depends on [`ws`](https://github.com/websockets/ws) at runtime.
|
|
11
|
+
|
|
12
|
+
$ npm install @bjoernboss/mws
|
|
13
|
+
|
|
14
|
+
Requires Node.js 22 or later.
|
|
15
|
+
|
|
16
|
+
Note: Look into the source `TypeScript` code on [`GitHub`](https://github.com/BjoernBoss/mws).
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { Server, ModuleHandler, ClientRequest, Media, addLogger, createConsoleLogger } from "@bjoernboss/mws";
|
|
22
|
+
|
|
23
|
+
addLogger(createConsoleLogger());
|
|
24
|
+
|
|
25
|
+
class HelloModule extends ModuleHandler {
|
|
26
|
+
constructor() {
|
|
27
|
+
super('hello');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected override async handleRequest(client: ClientRequest): Promise<void> {
|
|
31
|
+
client.respond('Hello, World!', { media: Media.Text });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const server = new Server();
|
|
36
|
+
server.listen(new HelloModule(), { port: 8080 });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For HTTPS, pass a TLS configuration:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
server.listen(new HelloModule(), {
|
|
43
|
+
port: 443,
|
|
44
|
+
tls: { key: './privkey.pem', cert: './fullchain.pem' }
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Writing Modules
|
|
49
|
+
|
|
50
|
+
A module extends `ModuleHandler` and implements up to three lifecycle hooks:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { ModuleHandler, ClientRequest, Server } from "@bjoernboss/mws";
|
|
54
|
+
|
|
55
|
+
export class MyModule extends ModuleHandler {
|
|
56
|
+
constructor() {
|
|
57
|
+
super('my-module');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Called once when first attached to a server (directly or through a parent module) */
|
|
61
|
+
protected override async handleInitialize(server: Server): Promise<void> {
|
|
62
|
+
/* allocate resources, start timers */
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Called for every incoming request routed to this module */
|
|
66
|
+
protected override async handleRequest(client: ClientRequest, params?: object): Promise<void> {
|
|
67
|
+
/* respond to the client */
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Called once after the module has stopped and all clients have left;
|
|
71
|
+
accepted WebSockets are still open and must be closed manually */
|
|
72
|
+
protected override async handleStop(): Promise<void> {
|
|
73
|
+
/* release resources, close WebSockets */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Only `handleRequest` is required. Unhandled requests can be handled by parent modules or receive an automatic `404 Not Found`.
|
|
79
|
+
|
|
80
|
+
Modules form a tree via `linkModule()`. They only ever see requests relative to their `root`. Path translation happens automatically when dispatching to children — client paths are rebased relative to each child module's position in the tree. A module can be linked to multiple parents and will only be initialized once, on first attachment to a server.
|
|
81
|
+
|
|
82
|
+
### Header and HTML Patching
|
|
83
|
+
|
|
84
|
+
Parent modules can register patches that modify outgoing headers or HTML pages before they are sent to the client:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
/* called just before headers are sent (in reverse registration order) */
|
|
88
|
+
client.patchHeaders((status, headers) => {
|
|
89
|
+
headers['X-Custom'] = 'value';
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/* called just before an HTML page is finalized (in reverse registration order) */
|
|
93
|
+
client.patchHtmlPage(async (page, status, headers) => {
|
|
94
|
+
page.head.push(build.LoadScript('/analytics.js'));
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Patches are scoped to the current handler context and automatically removed when the handler returns.
|
|
99
|
+
|
|
100
|
+
### Shutdown
|
|
101
|
+
|
|
102
|
+
`server.stop()` waits for all active request handlers to complete before shutting down. Long-running handlers **must** check `client.claimed` or await `client.responded` to detect when the connection has been broken, and exit promptly.
|
|
103
|
+
|
|
104
|
+
### Stopping Individual Modules
|
|
105
|
+
|
|
106
|
+
Calling `module.stop()` detaches the module and waits for its active clients to drain. By default, a module stops automatically when all its parents unlink it; this can be disabled with `module.stopOnDetach(false)`.
|
|
107
|
+
|
|
108
|
+
## Helper Modules
|
|
109
|
+
|
|
110
|
+
Factory functions create common module patterns without subclassing:
|
|
111
|
+
|
|
112
|
+
### dispatch — Path Routing
|
|
113
|
+
|
|
114
|
+
Routes requests to children by longest URL path match. Stops itself once all children have been unlinked.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { Server, dispatch } from "@bjoernboss/mws";
|
|
118
|
+
|
|
119
|
+
const server = new Server();
|
|
120
|
+
server.listen(dispatch({
|
|
121
|
+
'/api': apiModule,
|
|
122
|
+
'/static': staticModule
|
|
123
|
+
}), { port: 8080 });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### host — Hostname Routing
|
|
127
|
+
|
|
128
|
+
Routes requests to children by longest hostname match (supports sub-domain matching).
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { Server, host } from "@bjoernboss/mws";
|
|
132
|
+
|
|
133
|
+
server.listen(host({
|
|
134
|
+
'api.example.com': apiModule,
|
|
135
|
+
'example.com': mainModule
|
|
136
|
+
}), { port: 8080 });
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### bind — Parameter and Translation Binding
|
|
140
|
+
|
|
141
|
+
Forwards all requests to a single child handler, optionally injecting `params` and a path `translate` map.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { bind } from "@bjoernboss/mws";
|
|
145
|
+
|
|
146
|
+
const bound = bind(myModule, {
|
|
147
|
+
params: { role: 'admin' },
|
|
148
|
+
translate: { '/v2': '/' }
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### check — Host and Port Validation
|
|
153
|
+
|
|
154
|
+
Validates the request hostname and port before forwarding. Responds `404` and kills the connection on mismatch.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { check } from "@bjoernboss/mws";
|
|
158
|
+
|
|
159
|
+
const checked = check(myModule, ['localhost', '127.0.0.1'], { port: 8080 });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### lambda — Callback-Based Handler
|
|
163
|
+
|
|
164
|
+
Handles requests via callbacks instead of subclassing, with optional attached child modules.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { lambda, ClientRequest, Server, AttachedModule } from "@bjoernboss/mws";
|
|
168
|
+
|
|
169
|
+
const handler = lambda({
|
|
170
|
+
attach: { api: apiModule },
|
|
171
|
+
setup: async function (server: Server, links: Record<string, AttachedModule>) {
|
|
172
|
+
/* runs on first attachment */
|
|
173
|
+
},
|
|
174
|
+
handle: async function (client: ClientRequest, params, links) {
|
|
175
|
+
if (client.isSubPathOf('/api'))
|
|
176
|
+
await links.api.handle(client, { translate: { '/api': '/' } });
|
|
177
|
+
else
|
|
178
|
+
client.respondNotFound();
|
|
179
|
+
},
|
|
180
|
+
stop: async function (links) {
|
|
181
|
+
/* cleanup */
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Request Handling
|
|
187
|
+
|
|
188
|
+
`ClientRequest` automatically manages requests to handle errors, prevent double-responses, and ensure expected HTTP behavior.
|
|
189
|
+
|
|
190
|
+
### Receiving Data
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
/* as a complete buffer */
|
|
194
|
+
const data = await client.receiveAllBuffer(1_000_000);
|
|
195
|
+
|
|
196
|
+
/* as a decoded string */
|
|
197
|
+
const text = await client.receiveAllText('utf-8', 1_000_000);
|
|
198
|
+
|
|
199
|
+
/* as a readable stream */
|
|
200
|
+
const stream = client.receiveData(1_000_000);
|
|
201
|
+
|
|
202
|
+
/* directly to a file (fails if the file already exists) */
|
|
203
|
+
await client.receiveToFile('/uploads/file.bin', 10_000_000);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Responding
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
/* simple text or buffer */
|
|
210
|
+
client.respond('OK', { media: Media.Text, status: Status.Ok });
|
|
211
|
+
|
|
212
|
+
/* streaming response */
|
|
213
|
+
const writer = client.respondData({ media: Media.Json, dynamicEncode: true });
|
|
214
|
+
writer.end(JSON.stringify(data));
|
|
215
|
+
|
|
216
|
+
/* file with automatic range requests, etag, last-modified, encoding, and caching */
|
|
217
|
+
if (!await client.tryRespondFile('/var/www/index.html'))
|
|
218
|
+
client.respondNotFound();
|
|
219
|
+
|
|
220
|
+
/* HTML page */
|
|
221
|
+
await client.respondHtml(page, { status: Status.Ok });
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Convenience methods: `respondOk`, `respondNotFound`, `respondBadRequest`, `respondForbidden`, `respondInternalError`, `respondConflict`, `respondSeeOther`, `respondTemporaryRedirect`, `respondPermanentRedirect`, `respondCreated`, and others.
|
|
225
|
+
|
|
226
|
+
## WebSocket
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
protected override async handleRequest(client: ClientRequest): Promise<void> {
|
|
230
|
+
const ws = await client.acceptWebSocket();
|
|
231
|
+
if (ws == null) return;
|
|
232
|
+
|
|
233
|
+
ws.on('data', (data: Buffer) => {
|
|
234
|
+
ws.send(data);
|
|
235
|
+
});
|
|
236
|
+
ws.on('close', () => {
|
|
237
|
+
/* cleanup */
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`ClientSocket` handles alive checks automatically via configurable ping/pong intervals (`ClientConfig.webSocketTimeout`, `ClientConfig.webSocketAliveTimeout`).
|
|
243
|
+
|
|
244
|
+
Non-upgrade requests that reach `acceptWebSocket()` receive an automatic `426 Upgrade Required` response.
|
|
245
|
+
|
|
246
|
+
## Caching
|
|
247
|
+
|
|
248
|
+
`Server` provides integrated caching with a two-layer system:
|
|
249
|
+
|
|
250
|
+
### In-Memory File Cache
|
|
251
|
+
|
|
252
|
+
An LRU cache for frequently served files. Encoded variants (gzip, brotli, ...) are cached alongside the original.
|
|
253
|
+
|
|
254
|
+
`client.tryRespondFile()` reads through this cache automatically.
|
|
255
|
+
|
|
256
|
+
### Immutable Versioned Paths
|
|
257
|
+
|
|
258
|
+
File paths can be tagged with a unique version identifier so clients can cache them immutably:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
/* style.css becomes style.<id>.css — the id changes when the file changes */
|
|
262
|
+
const versionedPath = server.cache.immutable('my-handler', '/static/style.css');
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
When a request arrives for an immutable path, the cache strips the version tag, serves the underlying file, and redirects stale version tags to the current one. Set `CacheConfig.immutableStatePath` to persist version mappings across restarts.
|
|
266
|
+
|
|
267
|
+
### Direct Cache Access
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
const buffer = await server.cache.read('/path/to/data.json');
|
|
271
|
+
await server.cache.write('/path/to/output.json', JSON.stringify(data));
|
|
272
|
+
server.cache.flush();
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## HTML Building
|
|
276
|
+
|
|
277
|
+
The `build` namespace provides programmatic HTML construction with automatic escaping:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import { build } from "@bjoernboss/mws";
|
|
281
|
+
|
|
282
|
+
const page = new build.HtmlPage({
|
|
283
|
+
head: [
|
|
284
|
+
build.Title('My Page'),
|
|
285
|
+
build.Meta('viewport', 'width=device-width, initial-scale=1'),
|
|
286
|
+
build.LoadStyle('/style.css'),
|
|
287
|
+
build.LoadScript('/app.js', { defer: '' })
|
|
288
|
+
],
|
|
289
|
+
body: [
|
|
290
|
+
build.Div({ id: 'root' }, [
|
|
291
|
+
build.Text('Hello, World!')
|
|
292
|
+
])
|
|
293
|
+
]
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Plain strings passed as `HtmlString` values are automatically HTML-escaped. Use `build.Safe(content)` to mark trusted content that should not be escaped.
|
|
298
|
+
|
|
299
|
+
## Internal Methods
|
|
300
|
+
|
|
301
|
+
Methods prefixed with `_` (e.g. `_rootAttachToServer`, `_pushTranslation`, `_restoreSnapshot`) are framework-internal and **must not** be called by module implementations. They are `public` for cross-class access within the framework, but are not part of the public API and may change without notice.
|
package/dist/base.d.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as libZlib from "zlib";
|
|
2
|
+
import * as libStream from "stream";
|
|
3
|
+
export interface StatusType {
|
|
4
|
+
code: number;
|
|
5
|
+
msg: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const Status: {
|
|
8
|
+
readonly Ok: {
|
|
9
|
+
readonly code: 200;
|
|
10
|
+
readonly msg: "OK";
|
|
11
|
+
};
|
|
12
|
+
readonly Created: {
|
|
13
|
+
readonly code: 201;
|
|
14
|
+
readonly msg: "Created";
|
|
15
|
+
};
|
|
16
|
+
readonly PartialContent: {
|
|
17
|
+
readonly code: 206;
|
|
18
|
+
readonly msg: "Partial Content";
|
|
19
|
+
};
|
|
20
|
+
readonly SeeOther: {
|
|
21
|
+
readonly code: 303;
|
|
22
|
+
readonly msg: "See Other";
|
|
23
|
+
};
|
|
24
|
+
readonly NotModified: {
|
|
25
|
+
readonly code: 304;
|
|
26
|
+
readonly msg: "Not Modified";
|
|
27
|
+
};
|
|
28
|
+
readonly TemporaryRedirect: {
|
|
29
|
+
readonly code: 307;
|
|
30
|
+
readonly msg: "Temporary Redirect";
|
|
31
|
+
};
|
|
32
|
+
readonly PermanentRedirect: {
|
|
33
|
+
readonly code: 308;
|
|
34
|
+
readonly msg: "Permanent Redirect";
|
|
35
|
+
};
|
|
36
|
+
readonly BadRequest: {
|
|
37
|
+
readonly code: 400;
|
|
38
|
+
readonly msg: "Bad Request";
|
|
39
|
+
};
|
|
40
|
+
readonly Forbidden: {
|
|
41
|
+
readonly code: 403;
|
|
42
|
+
readonly msg: "Forbidden";
|
|
43
|
+
};
|
|
44
|
+
readonly NotFound: {
|
|
45
|
+
readonly code: 404;
|
|
46
|
+
readonly msg: "Not Found";
|
|
47
|
+
};
|
|
48
|
+
readonly MethodNotAllowed: {
|
|
49
|
+
readonly code: 405;
|
|
50
|
+
readonly msg: "Method Not Allowed";
|
|
51
|
+
};
|
|
52
|
+
readonly RequestTimeout: {
|
|
53
|
+
readonly code: 408;
|
|
54
|
+
readonly msg: "Request Timeout";
|
|
55
|
+
};
|
|
56
|
+
readonly Conflict: {
|
|
57
|
+
readonly code: 409;
|
|
58
|
+
readonly msg: "Conflict";
|
|
59
|
+
};
|
|
60
|
+
readonly PreconditionFailed: {
|
|
61
|
+
readonly code: 412;
|
|
62
|
+
readonly msg: "Precondition Failed";
|
|
63
|
+
};
|
|
64
|
+
readonly ContentTooLarge: {
|
|
65
|
+
readonly code: 413;
|
|
66
|
+
readonly msg: "Content Too Large";
|
|
67
|
+
};
|
|
68
|
+
readonly UnsupportedMediaType: {
|
|
69
|
+
readonly code: 415;
|
|
70
|
+
readonly msg: "Unsupported Media Type";
|
|
71
|
+
};
|
|
72
|
+
readonly RangeIssue: {
|
|
73
|
+
readonly code: 416;
|
|
74
|
+
readonly msg: "Range Not Satisfiable";
|
|
75
|
+
};
|
|
76
|
+
readonly UpgradeRequired: {
|
|
77
|
+
readonly code: 426;
|
|
78
|
+
readonly msg: "Upgrade Required";
|
|
79
|
+
};
|
|
80
|
+
readonly InternalError: {
|
|
81
|
+
readonly code: 500;
|
|
82
|
+
readonly msg: "Internal Server Error";
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
export interface MediaType {
|
|
86
|
+
fileEnding: string[];
|
|
87
|
+
mediaType: string;
|
|
88
|
+
encoding: string;
|
|
89
|
+
compressible: boolean;
|
|
90
|
+
}
|
|
91
|
+
export declare const Media: {
|
|
92
|
+
readonly Html: {
|
|
93
|
+
readonly fileEnding: ["html"];
|
|
94
|
+
readonly mediaType: "text/html";
|
|
95
|
+
readonly encoding: "charset=utf-8";
|
|
96
|
+
readonly compressible: true;
|
|
97
|
+
};
|
|
98
|
+
readonly Css: {
|
|
99
|
+
readonly fileEnding: ["css"];
|
|
100
|
+
readonly mediaType: "text/css";
|
|
101
|
+
readonly encoding: "charset=utf-8";
|
|
102
|
+
readonly compressible: true;
|
|
103
|
+
};
|
|
104
|
+
readonly JavaScript: {
|
|
105
|
+
readonly fileEnding: ["js"];
|
|
106
|
+
readonly mediaType: "text/javascript";
|
|
107
|
+
readonly encoding: "charset=utf-8";
|
|
108
|
+
readonly compressible: true;
|
|
109
|
+
};
|
|
110
|
+
readonly Text: {
|
|
111
|
+
readonly fileEnding: ["txt", "text"];
|
|
112
|
+
readonly mediaType: "text/plain";
|
|
113
|
+
readonly encoding: "charset=utf-8";
|
|
114
|
+
readonly compressible: true;
|
|
115
|
+
};
|
|
116
|
+
readonly Json: {
|
|
117
|
+
readonly fileEnding: ["json"];
|
|
118
|
+
readonly mediaType: "application/json";
|
|
119
|
+
readonly encoding: "charset=utf-8";
|
|
120
|
+
readonly compressible: true;
|
|
121
|
+
};
|
|
122
|
+
readonly Mp4: {
|
|
123
|
+
readonly fileEnding: ["mp4"];
|
|
124
|
+
readonly mediaType: "video/mp4";
|
|
125
|
+
readonly encoding: "";
|
|
126
|
+
readonly compressible: false;
|
|
127
|
+
};
|
|
128
|
+
readonly Png: {
|
|
129
|
+
readonly fileEnding: ["png"];
|
|
130
|
+
readonly mediaType: "image/png";
|
|
131
|
+
readonly encoding: "";
|
|
132
|
+
readonly compressible: false;
|
|
133
|
+
};
|
|
134
|
+
readonly Gif: {
|
|
135
|
+
readonly fileEnding: ["gif"];
|
|
136
|
+
readonly mediaType: "image/gif";
|
|
137
|
+
readonly encoding: "";
|
|
138
|
+
readonly compressible: false;
|
|
139
|
+
};
|
|
140
|
+
readonly Jpg: {
|
|
141
|
+
readonly fileEnding: ["jpg", "jpeg"];
|
|
142
|
+
readonly mediaType: "image/jpeg";
|
|
143
|
+
readonly encoding: "";
|
|
144
|
+
readonly compressible: false;
|
|
145
|
+
};
|
|
146
|
+
readonly Svg: {
|
|
147
|
+
readonly fileEnding: ["svg"];
|
|
148
|
+
readonly mediaType: "image/svg+xml";
|
|
149
|
+
readonly encoding: "";
|
|
150
|
+
readonly compressible: true;
|
|
151
|
+
};
|
|
152
|
+
readonly Unknown: {
|
|
153
|
+
readonly fileEnding: [];
|
|
154
|
+
readonly mediaType: "application/octet-stream";
|
|
155
|
+
readonly encoding: "";
|
|
156
|
+
readonly compressible: false;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
export interface EncodingType {
|
|
160
|
+
name: string;
|
|
161
|
+
makeDecode(): libStream.Transform;
|
|
162
|
+
makeEncode(): libStream.Transform;
|
|
163
|
+
encodeBuffer(buffer: Buffer): Buffer;
|
|
164
|
+
}
|
|
165
|
+
export declare const Encoding: {
|
|
166
|
+
readonly Br: {
|
|
167
|
+
readonly name: "br";
|
|
168
|
+
readonly makeDecode: () => libZlib.BrotliDecompress;
|
|
169
|
+
readonly makeEncode: () => libZlib.BrotliCompress;
|
|
170
|
+
readonly encodeBuffer: (buffer: Buffer) => NonSharedBuffer;
|
|
171
|
+
};
|
|
172
|
+
readonly Zstd: {
|
|
173
|
+
readonly name: "zstd";
|
|
174
|
+
readonly makeDecode: () => libZlib.ZstdDecompress;
|
|
175
|
+
readonly makeEncode: () => libZlib.ZstdCompress;
|
|
176
|
+
readonly encodeBuffer: (buffer: Buffer) => NonSharedBuffer;
|
|
177
|
+
};
|
|
178
|
+
readonly Gzip: {
|
|
179
|
+
readonly name: "gzip";
|
|
180
|
+
readonly makeDecode: () => libZlib.Gunzip;
|
|
181
|
+
readonly makeEncode: () => libZlib.Gzip;
|
|
182
|
+
readonly encodeBuffer: (buffer: Buffer) => NonSharedBuffer;
|
|
183
|
+
};
|
|
184
|
+
readonly Deflate: {
|
|
185
|
+
readonly name: "deflate";
|
|
186
|
+
readonly makeDecode: () => libZlib.Inflate;
|
|
187
|
+
readonly makeEncode: () => libZlib.Deflate;
|
|
188
|
+
readonly encodeBuffer: (buffer: Buffer) => NonSharedBuffer;
|
|
189
|
+
};
|
|
190
|
+
readonly Identity: {
|
|
191
|
+
readonly name: "identity";
|
|
192
|
+
readonly makeDecode: () => libStream.PassThrough;
|
|
193
|
+
readonly makeEncode: () => libStream.PassThrough;
|
|
194
|
+
readonly encodeBuffer: (buffer: Buffer) => Buffer<ArrayBufferLike>;
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
export declare const MIN_ENCODING_SIZE: number;
|
|
198
|
+
//# sourceMappingURL=base.d.ts.map
|
package/dist/base.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: BSD-3-Clause */
|
|
2
|
+
/* Copyright (c) 2026 Bjoern Boss Henrichsen */
|
|
3
|
+
import * as libZlib from "zlib";
|
|
4
|
+
import * as libStream from "stream";
|
|
5
|
+
export const Status = {
|
|
6
|
+
Ok: { code: 200, msg: 'OK' },
|
|
7
|
+
Created: { code: 201, msg: 'Created' },
|
|
8
|
+
PartialContent: { code: 206, msg: 'Partial Content' },
|
|
9
|
+
SeeOther: { code: 303, msg: 'See Other' },
|
|
10
|
+
NotModified: { code: 304, msg: 'Not Modified' },
|
|
11
|
+
TemporaryRedirect: { code: 307, msg: 'Temporary Redirect' },
|
|
12
|
+
PermanentRedirect: { code: 308, msg: 'Permanent Redirect' },
|
|
13
|
+
BadRequest: { code: 400, msg: 'Bad Request' },
|
|
14
|
+
Forbidden: { code: 403, msg: 'Forbidden' },
|
|
15
|
+
NotFound: { code: 404, msg: 'Not Found' },
|
|
16
|
+
MethodNotAllowed: { code: 405, msg: 'Method Not Allowed' },
|
|
17
|
+
RequestTimeout: { code: 408, msg: 'Request Timeout' },
|
|
18
|
+
Conflict: { code: 409, msg: 'Conflict' },
|
|
19
|
+
PreconditionFailed: { code: 412, msg: 'Precondition Failed' },
|
|
20
|
+
ContentTooLarge: { code: 413, msg: 'Content Too Large' },
|
|
21
|
+
UnsupportedMediaType: { code: 415, msg: 'Unsupported Media Type' },
|
|
22
|
+
RangeIssue: { code: 416, msg: 'Range Not Satisfiable' },
|
|
23
|
+
UpgradeRequired: { code: 426, msg: 'Upgrade Required' },
|
|
24
|
+
InternalError: { code: 500, msg: 'Internal Server Error' }
|
|
25
|
+
};
|
|
26
|
+
export const Media = {
|
|
27
|
+
Html: { fileEnding: ['html'], mediaType: 'text/html', encoding: 'charset=utf-8', compressible: true },
|
|
28
|
+
Css: { fileEnding: ['css'], mediaType: 'text/css', encoding: 'charset=utf-8', compressible: true },
|
|
29
|
+
JavaScript: { fileEnding: ['js'], mediaType: 'text/javascript', encoding: 'charset=utf-8', compressible: true },
|
|
30
|
+
Text: { fileEnding: ['txt', 'text'], mediaType: 'text/plain', encoding: 'charset=utf-8', compressible: true },
|
|
31
|
+
Json: { fileEnding: ['json'], mediaType: 'application/json', encoding: 'charset=utf-8', compressible: true },
|
|
32
|
+
Mp4: { fileEnding: ['mp4'], mediaType: 'video/mp4', encoding: '', compressible: false },
|
|
33
|
+
Png: { fileEnding: ['png'], mediaType: 'image/png', encoding: '', compressible: false },
|
|
34
|
+
Gif: { fileEnding: ['gif'], mediaType: 'image/gif', encoding: '', compressible: false },
|
|
35
|
+
Jpg: { fileEnding: ['jpg', 'jpeg'], mediaType: 'image/jpeg', encoding: '', compressible: false },
|
|
36
|
+
Svg: { fileEnding: ['svg'], mediaType: 'image/svg+xml', encoding: '', compressible: true },
|
|
37
|
+
Unknown: { fileEnding: [], mediaType: 'application/octet-stream', encoding: '', compressible: false }
|
|
38
|
+
};
|
|
39
|
+
export const Encoding = {
|
|
40
|
+
Br: {
|
|
41
|
+
name: 'br',
|
|
42
|
+
makeDecode: () => libZlib.createBrotliDecompress(),
|
|
43
|
+
makeEncode: () => libZlib.createBrotliCompress(),
|
|
44
|
+
encodeBuffer: (buffer) => libZlib.brotliCompressSync(buffer)
|
|
45
|
+
},
|
|
46
|
+
Zstd: {
|
|
47
|
+
name: 'zstd',
|
|
48
|
+
makeDecode: () => libZlib.createZstdDecompress(),
|
|
49
|
+
makeEncode: () => libZlib.createZstdCompress(),
|
|
50
|
+
encodeBuffer: (buffer) => libZlib.zstdCompressSync(buffer)
|
|
51
|
+
},
|
|
52
|
+
Gzip: {
|
|
53
|
+
name: 'gzip',
|
|
54
|
+
makeDecode: () => libZlib.createGunzip(),
|
|
55
|
+
makeEncode: () => libZlib.createGzip(),
|
|
56
|
+
encodeBuffer: (buffer) => libZlib.gzipSync(buffer)
|
|
57
|
+
},
|
|
58
|
+
Deflate: {
|
|
59
|
+
name: 'deflate',
|
|
60
|
+
makeDecode: () => libZlib.createInflate(),
|
|
61
|
+
makeEncode: () => libZlib.createDeflate(),
|
|
62
|
+
encodeBuffer: (buffer) => libZlib.deflateSync(buffer)
|
|
63
|
+
},
|
|
64
|
+
Identity: {
|
|
65
|
+
name: 'identity',
|
|
66
|
+
makeDecode: () => new libStream.PassThrough(),
|
|
67
|
+
makeEncode: () => new libStream.PassThrough(),
|
|
68
|
+
encodeBuffer: (buffer) => buffer
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/* minimum size for content is considered encodable */
|
|
72
|
+
export const MIN_ENCODING_SIZE = 1_000;
|
|
73
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type HtmlString = HtmlGuard | string;
|
|
2
|
+
export declare class HtmlGuard {
|
|
3
|
+
content: string;
|
|
4
|
+
private constructor();
|
|
5
|
+
static get(str: HtmlString): HtmlGuard;
|
|
6
|
+
static make(str: string, safe: boolean): HtmlGuard;
|
|
7
|
+
}
|
|
8
|
+
export declare function Safe(content: string, safe?: boolean): HtmlGuard;
|
|
9
|
+
export interface HtmlComponent {
|
|
10
|
+
finalize(indent: string): string;
|
|
11
|
+
simple(): boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class EmbeddedContent implements HtmlComponent {
|
|
14
|
+
private content;
|
|
15
|
+
constructor(content: string, safe: boolean);
|
|
16
|
+
simple(): boolean;
|
|
17
|
+
finalize(_: string): string;
|
|
18
|
+
}
|
|
19
|
+
export declare class SingleTag implements HtmlComponent {
|
|
20
|
+
private content;
|
|
21
|
+
constructor(name: string, properties: Record<string, HtmlString>);
|
|
22
|
+
simple(): boolean;
|
|
23
|
+
finalize(indent: string): string;
|
|
24
|
+
}
|
|
25
|
+
export declare class DualTag implements HtmlComponent {
|
|
26
|
+
private openTag;
|
|
27
|
+
private closeTag;
|
|
28
|
+
private children;
|
|
29
|
+
constructor(name: string, properties: Record<string, HtmlString>, children: HtmlComponent[] | HtmlComponent);
|
|
30
|
+
simple(): boolean;
|
|
31
|
+
finalize(indent: string): string;
|
|
32
|
+
}
|
|
33
|
+
export declare class HtmlPage {
|
|
34
|
+
private _head;
|
|
35
|
+
private _body;
|
|
36
|
+
language: HtmlString;
|
|
37
|
+
constructor(options?: {
|
|
38
|
+
language?: HtmlString;
|
|
39
|
+
head?: HtmlComponent[] | HtmlComponent;
|
|
40
|
+
body?: HtmlComponent[] | HtmlComponent;
|
|
41
|
+
});
|
|
42
|
+
get head(): HtmlComponent[];
|
|
43
|
+
set head(value: HtmlComponent | HtmlComponent[]);
|
|
44
|
+
get body(): HtmlComponent[];
|
|
45
|
+
set body(value: HtmlComponent | HtmlComponent[]);
|
|
46
|
+
finalize(): string;
|
|
47
|
+
}
|
|
48
|
+
export declare function Embed(content: string, safe: boolean): HtmlComponent;
|
|
49
|
+
export declare function Meta(name: HtmlString, content: HtmlString): HtmlComponent;
|
|
50
|
+
export declare function Title(name: HtmlString): HtmlComponent;
|
|
51
|
+
export declare function LoadStyle(path: HtmlString): HtmlComponent;
|
|
52
|
+
export declare function LoadScript(path: HtmlString, properties?: Record<string, HtmlString>): HtmlComponent;
|
|
53
|
+
export declare function AddStyle(content: string, properties?: Record<string, HtmlString>): HtmlComponent;
|
|
54
|
+
export declare function AddScript(content: string, properties?: Record<string, HtmlString>): HtmlComponent;
|
|
55
|
+
export declare function Text(text: HtmlString, properties?: Record<string, HtmlString>): HtmlComponent;
|
|
56
|
+
export declare function Div(properties?: Record<string, HtmlString>, children?: HtmlComponent[] | HtmlComponent): HtmlComponent;
|
|
57
|
+
export declare function LoadingError(): HtmlComponent;
|
|
58
|
+
//# sourceMappingURL=builder.d.ts.map
|