@jsenv/server 15.5.3 → 16.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/README.md +79 -86
- package/dist/html/4xx.html +196 -0
- package/dist/html/500.html +17 -0
- package/dist/html/route_inspector.html +2435 -0
- package/dist/js/formidable.js +17 -0
- package/dist/js/vendors.js +2001 -0
- package/dist/js/ws.js +6863 -0
- package/dist/jsenv_server.js +8829 -0
- package/package.json +20 -9
- package/src/content_negotiation/pick_content_version.js +63 -0
- package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
- package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
- package/src/interfacing_with_node/from_node_request.js +455 -0
- package/src/interfacing_with_node/get_observable_value_type.js +43 -0
- package/src/interfacing_with_node/observable.js +91 -75
- package/src/interfacing_with_node/observable_from.js +34 -0
- package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
- package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
- package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
- package/src/interfacing_with_node/observable_from_promise.js +13 -0
- package/src/interfacing_with_node/write_node_response.js +80 -35
- package/src/internal/colorizeResponseStatus.js +2 -0
- package/src/internal/headers_composition.js +42 -15
- package/src/internal/multiple-header.js +27 -12
- package/src/internal/object_composition.js +2 -2
- package/src/internal/response_composition.js +16 -3
- package/src/main.js +15 -11
- package/src/progressive_response.js +38 -0
- package/src/replace_placeholder_in_html.js +12 -0
- package/src/request_body_handling.js +22 -0
- package/src/router/router.js +878 -0
- package/src/server_events.js +431 -0
- package/src/service_controller.js +79 -73
- package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
- package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
- package/src/services/cors/jsenv_service_cors.js +10 -18
- package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
- package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
- package/src/services/error_handler/client/500.html +17 -0
- package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
- package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
- package/src/services/open_file/jsenv_service_open_file.js +37 -0
- package/src/services/route_inspector/client/code_block.js +215 -0
- package/src/services/route_inspector/client/route_inspector.html +1363 -0
- package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
- package/src/start_server.js +575 -607
- package/src/web_socket_response.js +53 -0
- package/src/from_fetch_response.js +0 -15
- package/src/interfacing_with_node/body.js +0 -81
- package/src/interfacing_with_node/request_factory.js +0 -125
- package/src/readRequestBody.js +0 -32
- package/src/server_timing/timing_measure.js +0 -26
- package/src/service_composition/path_to_regexp.js +0 -436
- package/src/service_composition/routing.js +0 -31
- package/src/sse/sse_room.js +0 -313
package/README.md
CHANGED
|
@@ -1,112 +1,109 @@
|
|
|
1
1
|
# server [](https://www.npmjs.com/package/@jsenv/server)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> A modern, flexible Node.js HTTP server with declarative routing, content negotiation, and WebSocket support.
|
|
4
|
+
|
|
5
|
+
`@jsenv/server` simplifies server development with a declarative API that handles common web server needs like routing, content negotiation, file serving, and real-time communication.
|
|
4
6
|
|
|
5
7
|
```js
|
|
6
8
|
import { startServer } from "@jsenv/server";
|
|
7
9
|
|
|
8
10
|
await startServer({
|
|
9
11
|
port: 8080,
|
|
10
|
-
|
|
12
|
+
routes: [
|
|
11
13
|
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
status: 200,
|
|
15
|
-
headers: {
|
|
16
|
-
"content-type": "text/plain",
|
|
17
|
-
},
|
|
18
|
-
body: "Hello world",
|
|
19
|
-
};
|
|
20
|
-
},
|
|
14
|
+
endpoint: "GET *",
|
|
15
|
+
response: () => new Response("Hello world"),
|
|
21
16
|
},
|
|
22
17
|
],
|
|
23
18
|
});
|
|
24
19
|
```
|
|
25
20
|
|
|
26
|
-
#
|
|
21
|
+
# Features
|
|
22
|
+
|
|
23
|
+
- [*] Declarative routing with path parameters and pattern matching
|
|
24
|
+
- [*] Content negotiation for type, language, version and encoding
|
|
25
|
+
- [*] Real-time communication via WebSockets and Server-Sent Events
|
|
26
|
+
- [*] File serving with ETags, conditional requests, and compression
|
|
27
|
+
- [*] Security with HTTPS and automatic HTTP-to-HTTPS redirection
|
|
28
|
+
- [*] HTTP/2 support including server push
|
|
29
|
+
- [*] CORS handling built-in
|
|
30
|
+
- [*] Performance monitoring with server timing
|
|
31
|
+
- [*] Sacalability through cluster mode for multi-core utilization
|
|
32
|
+
- [ ] Request authentification with JWT, OAuth etc
|
|
33
|
+
|
|
34
|
+
# Installation
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
npm install @jsenv/server
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Requirements:**
|
|
41
|
+
|
|
42
|
+
- Node.js 22.13.1 or higher
|
|
43
|
+
- ES modules support
|
|
27
44
|
|
|
28
|
-
|
|
45
|
+
# Quick Examples
|
|
46
|
+
|
|
47
|
+
**Basic API Server**
|
|
29
48
|
|
|
30
49
|
```js
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* -> respond with 404
|
|
37
|
-
*/
|
|
38
|
-
import { startServer, composeServices } from "@jsenv/server";
|
|
39
|
-
|
|
40
|
-
const server = await startServer({
|
|
41
|
-
services: [
|
|
50
|
+
import { startServer } from "@jsenv/server";
|
|
51
|
+
|
|
52
|
+
await startServer({
|
|
53
|
+
port: 3000,
|
|
54
|
+
routes: [
|
|
42
55
|
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (request.resource === "/") {
|
|
46
|
-
return { status: 200 };
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
},
|
|
56
|
+
endpoint: "GET /api/users",
|
|
57
|
+
response: () => Response.json([{ id: 1, name: "John" }]),
|
|
50
58
|
},
|
|
51
59
|
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
endpoint: "GET /api/users/:id",
|
|
61
|
+
response: (request) =>
|
|
62
|
+
Response.json({ id: request.params.id, name: "John" }),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
endpoint: "GET *",
|
|
66
|
+
response: () => new Response("Not found", { status: 404 }),
|
|
56
67
|
},
|
|
57
68
|
],
|
|
58
69
|
});
|
|
59
|
-
|
|
60
|
-
const fetch = await import("node-fetch");
|
|
61
|
-
const responseForOrigin = await fetch(server.origin);
|
|
62
|
-
responseForOrigin.status; // 200
|
|
63
|
-
|
|
64
|
-
const responseForFoo = await fetch(`${server.origin}/foo`);
|
|
65
|
-
responseForFoo.status; // 404
|
|
66
70
|
```
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
**Static File Server**
|
|
69
73
|
|
|
70
74
|
```js
|
|
71
|
-
import {
|
|
72
|
-
import { startServer } from "@jsenv/server";
|
|
75
|
+
import { startServer, createFileSystemFetch } from "@jsenv/server";
|
|
73
76
|
|
|
74
77
|
await startServer({
|
|
75
|
-
|
|
76
|
-
certificate: readFileSync(new URL("./server.crt", import.meta.url), "utf8"),
|
|
77
|
-
privateKey: readFileSync(new URL("./server.key", import.meta.url), "utf8"),
|
|
78
|
-
},
|
|
79
|
-
allowHttpRequestOnHttps: true,
|
|
80
|
-
services: [
|
|
78
|
+
routes: [
|
|
81
79
|
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
status: 200,
|
|
87
|
-
headers: {
|
|
88
|
-
"content-type": "text/plain",
|
|
89
|
-
},
|
|
90
|
-
body: clientUsesHttp ? `Welcome http user` : `Welcome https user`,
|
|
91
|
-
};
|
|
92
|
-
},
|
|
80
|
+
endpoint: "GET *",
|
|
81
|
+
response: createFileSystemFetch(import.meta.resolve("./")),
|
|
93
82
|
},
|
|
94
83
|
],
|
|
95
84
|
});
|
|
96
85
|
```
|
|
97
86
|
|
|
98
|
-
|
|
87
|
+
**HTTPS Server**
|
|
99
88
|
|
|
100
89
|
```js
|
|
101
|
-
import {
|
|
90
|
+
import { readFileSync } from "node:fs";
|
|
91
|
+
import { startServer } from "@jsenv/server";
|
|
102
92
|
|
|
103
93
|
await startServer({
|
|
104
|
-
|
|
94
|
+
https: {
|
|
95
|
+
certificate: readFileSync(new URL("./server.crt", import.meta.url), "utf8"),
|
|
96
|
+
privateKey: readFileSync(new URL("./server.key", import.meta.url), "utf8"),
|
|
97
|
+
},
|
|
98
|
+
allowHttpRequestOnHttps: true, // will disable https redirection and let you handle http request
|
|
99
|
+
routes: [
|
|
105
100
|
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
return
|
|
101
|
+
endpoint: "GET *",
|
|
102
|
+
response: (request) => {
|
|
103
|
+
const clientUsesHttp = request.origin.startsWith("http:");
|
|
104
|
+
return new Response(
|
|
105
|
+
clientUsesHttp ? `Welcome http user` : `Welcome https user`,
|
|
106
|
+
);
|
|
110
107
|
},
|
|
111
108
|
},
|
|
112
109
|
],
|
|
@@ -115,20 +112,16 @@ await startServer({
|
|
|
115
112
|
|
|
116
113
|
# Documentation
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
```console
|
|
133
|
-
npm install @jsenv/server
|
|
134
|
-
```
|
|
115
|
+
| Topic | Description |
|
|
116
|
+
| ---------------------------------------------------- | ------------------------------------------------ |
|
|
117
|
+
| [Handling requests](./docs/handling_requests.md) | Process HTTP requests and generate responses |
|
|
118
|
+
| [Handling errors](./docs/handling_errors.md) | Error handling strategies and custom responses |
|
|
119
|
+
| [Server timing](./docs/server_timing.md) | Measure and report server performance metrics |
|
|
120
|
+
| [CORS](./docs/cors.md) | Configure Cross-Origin Resource Sharing |
|
|
121
|
+
| [HTTPS](./docs/https.md) | Set up secure HTTPS connections |
|
|
122
|
+
| [Serving files](./docs/serving_files.md) | Static file serving with caching and compression |
|
|
123
|
+
| [Content negotiation](./docs/content_negotiation.md) | Content type, language and encoding negotiation |
|
|
124
|
+
| [Websocket](./docs/websocket.md) | Bi-directional real-time communication |
|
|
125
|
+
| [Server Sent Events](./docs/server_sent_events.md) | Push updates to clients over HTTP |
|
|
126
|
+
| [Cluster](./docs/cluster.md) | Scale your server across multiple CPU cores |
|
|
127
|
+
| [HTTP/2 Push](./docs/http2_push.md) | Optimize loading with server push |
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>${status} ${statusText}</title>
|
|
7
|
+
<style>body {
|
|
8
|
+
color: #333;
|
|
9
|
+
background-color: #f8f9fa;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
align-items: center;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
margin: 0;
|
|
14
|
+
font-family: Arial, sans-serif;
|
|
15
|
+
display: flex;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.error_container {
|
|
19
|
+
text-align: center;
|
|
20
|
+
background: #fff;
|
|
21
|
+
border-radius: 8px;
|
|
22
|
+
padding: 20px;
|
|
23
|
+
box-shadow: 0 4px 6px #0000001a;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.error_status {
|
|
27
|
+
color: #dc3545;
|
|
28
|
+
margin: 0;
|
|
29
|
+
font-size: 5rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.error_status_text {
|
|
33
|
+
word-break: break-word;
|
|
34
|
+
text-align: left;
|
|
35
|
+
margin: 10px 0;
|
|
36
|
+
font-size: 1.5rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.error_message {
|
|
40
|
+
color: #555;
|
|
41
|
+
word-break: break-word;
|
|
42
|
+
text-align: left;
|
|
43
|
+
font-size: 1rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.home_link {
|
|
47
|
+
color: #fff;
|
|
48
|
+
background-color: #007bff;
|
|
49
|
+
border-radius: 5px;
|
|
50
|
+
margin-top: 15px;
|
|
51
|
+
padding: 10px 15px;
|
|
52
|
+
text-decoration: none;
|
|
53
|
+
display: inline-block;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.home_link:hover {
|
|
57
|
+
background-color: #0056b3;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<div class="error_container">
|
|
63
|
+
<p class="error_status">${status}</p>
|
|
64
|
+
<p class="error_status_text">${statusText}</p>
|
|
65
|
+
<p class="error_message">${statusMessage}</p>
|
|
66
|
+
<a href="/" jsenv-ignore="" class="home_link">Return to Home</a>
|
|
67
|
+
</div>
|
|
68
|
+
<script>
|
|
69
|
+
const css = /* css */ `
|
|
70
|
+
dialog {
|
|
71
|
+
opacity: 0;
|
|
72
|
+
transition: all 0.3s allow-discrete;
|
|
73
|
+
}
|
|
74
|
+
dialog::backdrop {
|
|
75
|
+
background-image: linear-gradient(
|
|
76
|
+
45deg,
|
|
77
|
+
magenta,
|
|
78
|
+
rebeccapurple,
|
|
79
|
+
dodgerblue,
|
|
80
|
+
green
|
|
81
|
+
);
|
|
82
|
+
opacity: 0;
|
|
83
|
+
transition: all 0.3s allow-discrete;
|
|
84
|
+
}
|
|
85
|
+
dialog:open {
|
|
86
|
+
opacity: 1;
|
|
87
|
+
}
|
|
88
|
+
dialog:open::backdrop {
|
|
89
|
+
opacity: 0.75;
|
|
90
|
+
}`;
|
|
91
|
+
|
|
92
|
+
const html = /* html */ `<style>
|
|
93
|
+
${css}
|
|
94
|
+
</style>
|
|
95
|
+
<dialog>
|
|
96
|
+
<p>Connection with server is lost</p>
|
|
97
|
+
<button autofocus name="reconnect">Retry</button>
|
|
98
|
+
</dialog>`;
|
|
99
|
+
|
|
100
|
+
class JsenvAutoreloadOnServerRestart extends HTMLElement {
|
|
101
|
+
constructor({ url } = {}) {
|
|
102
|
+
super();
|
|
103
|
+
const root = this.attachShadow({ mode: "open" });
|
|
104
|
+
root.innerHTML = html;
|
|
105
|
+
|
|
106
|
+
const dialog = root.querySelector("dialog");
|
|
107
|
+
const reconnectButton = dialog.querySelector('button[name="reconnect"]');
|
|
108
|
+
|
|
109
|
+
(async () => {
|
|
110
|
+
const connect = () => {
|
|
111
|
+
const websocket = new WebSocket(url);
|
|
112
|
+
const connectedPromise = new Promise((resolve) => {
|
|
113
|
+
websocket.onerror = () => {
|
|
114
|
+
resolve(null);
|
|
115
|
+
};
|
|
116
|
+
websocket.onopen = () => {
|
|
117
|
+
resolve(websocket);
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return connectedPromise;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const initialWebSocket = await connect();
|
|
124
|
+
initialWebSocket.onclose = async () => {
|
|
125
|
+
console.info("connection to server lost, trying to reconnect");
|
|
126
|
+
const retry = async () => {
|
|
127
|
+
let attemptCount = 0;
|
|
128
|
+
let timeToWait = 100;
|
|
129
|
+
const tryToReconnect = async () => {
|
|
130
|
+
const newWebsocket = await connect();
|
|
131
|
+
if (newWebsocket) {
|
|
132
|
+
dialog.close();
|
|
133
|
+
console.info("reconnected to server, reloading the page...");
|
|
134
|
+
window.location.reload(true);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
attemptCount++;
|
|
138
|
+
if (attemptCount === 10) {
|
|
139
|
+
dialog.showModal();
|
|
140
|
+
reconnectButton.onclick = async () => {
|
|
141
|
+
reconnectButton.disabled = true;
|
|
142
|
+
const previousContext = reconnectButton.textContent;
|
|
143
|
+
reconnectButton.textContent = "Retrying...";
|
|
144
|
+
await retry();
|
|
145
|
+
reconnectButton.textContent = previousContext;
|
|
146
|
+
reconnectButton.disabled = false;
|
|
147
|
+
};
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
timeToWait += 100;
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, timeToWait));
|
|
152
|
+
await tryToReconnect();
|
|
153
|
+
};
|
|
154
|
+
await tryToReconnect();
|
|
155
|
+
};
|
|
156
|
+
retry();
|
|
157
|
+
};
|
|
158
|
+
})();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!customElements.get("jsenv-autoreload-on-server-restart")) {
|
|
162
|
+
customElements.define(
|
|
163
|
+
"jsenv-autoreload-on-server-restart",
|
|
164
|
+
JsenvAutoreloadOnServerRestart,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const jsenvAutoreloadOnServerRestartElement =
|
|
169
|
+
new JsenvAutoreloadOnServerRestart({
|
|
170
|
+
url: (() => {
|
|
171
|
+
const websocketScheme =
|
|
172
|
+
self.location.protocol === "https:" ? "wss" : "ws";
|
|
173
|
+
const websocketUrl = `${websocketScheme}://${self.location.host}/.internal/alive.websocket`;
|
|
174
|
+
return websocketUrl;
|
|
175
|
+
})(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// script is injected into document.body
|
|
179
|
+
if (document.body) {
|
|
180
|
+
document.currentScript.parentNode.replaceChild(
|
|
181
|
+
jsenvAutoreloadOnServerRestartElement,
|
|
182
|
+
document.currentScript,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
// script injected outisde document.body (likely in head)
|
|
186
|
+
else {
|
|
187
|
+
document.currentScript.parentNode.removeChild(document.currentScript);
|
|
188
|
+
document.onreadystatechange = () => {
|
|
189
|
+
if (document.readyState === "interactive") {
|
|
190
|
+
document.body.appendChild(jsenvAutoreloadOnServerRestartElement);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
</script>
|
|
195
|
+
</body>
|
|
196
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Internal server error</title>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<link rel="icon" href="data:,">
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Internal server error</h1>
|
|
11
|
+
<p>${errorMessage}</p>
|
|
12
|
+
<details>
|
|
13
|
+
<summary>See internal error details</summary>
|
|
14
|
+
${errorDetailsContent}
|
|
15
|
+
</details>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|