@jsenv/server 15.5.3 → 16.0.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.
Files changed (55) hide show
  1. package/README.md +79 -86
  2. package/dist/html/4xx.html +196 -0
  3. package/dist/html/500.html +17 -0
  4. package/dist/html/route_inspector.html +2435 -0
  5. package/dist/js/formidable.js +17 -0
  6. package/dist/js/vendors.js +2001 -0
  7. package/dist/js/ws.js +6863 -0
  8. package/dist/jsenv_server.js +8829 -0
  9. package/package.json +19 -9
  10. package/src/content_negotiation/pick_content_version.js +63 -0
  11. package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
  12. package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
  13. package/src/interfacing_with_node/from_node_request.js +455 -0
  14. package/src/interfacing_with_node/get_observable_value_type.js +43 -0
  15. package/src/interfacing_with_node/observable.js +91 -75
  16. package/src/interfacing_with_node/observable_from.js +34 -0
  17. package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
  18. package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
  19. package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
  20. package/src/interfacing_with_node/observable_from_promise.js +13 -0
  21. package/src/interfacing_with_node/write_node_response.js +80 -35
  22. package/src/internal/colorizeResponseStatus.js +2 -0
  23. package/src/internal/headers_composition.js +42 -15
  24. package/src/internal/multiple-header.js +27 -12
  25. package/src/internal/object_composition.js +2 -2
  26. package/src/internal/response_composition.js +16 -3
  27. package/src/main.js +15 -11
  28. package/src/progressive_response.js +38 -0
  29. package/src/replace_placeholder_in_html.js +12 -0
  30. package/src/request_body_handling.js +22 -0
  31. package/src/router/router.js +878 -0
  32. package/src/server_events.js +431 -0
  33. package/src/service_controller.js +79 -73
  34. package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
  35. package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
  36. package/src/services/cors/jsenv_service_cors.js +10 -18
  37. package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
  38. package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
  39. package/src/services/error_handler/client/500.html +17 -0
  40. package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
  41. package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
  42. package/src/services/open_file/jsenv_service_open_file.js +37 -0
  43. package/src/services/route_inspector/client/code_block.js +215 -0
  44. package/src/services/route_inspector/client/route_inspector.html +1363 -0
  45. package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
  46. package/src/start_server.js +575 -607
  47. package/src/web_socket_response.js +53 -0
  48. package/src/from_fetch_response.js +0 -15
  49. package/src/interfacing_with_node/body.js +0 -81
  50. package/src/interfacing_with_node/request_factory.js +0 -125
  51. package/src/readRequestBody.js +0 -32
  52. package/src/server_timing/timing_measure.js +0 -26
  53. package/src/service_composition/path_to_regexp.js +0 -436
  54. package/src/service_composition/routing.js +0 -31
  55. package/src/sse/sse_room.js +0 -313
package/README.md CHANGED
@@ -1,112 +1,109 @@
1
1
  # server [![npm package](https://img.shields.io/npm/v/@jsenv/server.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/server)
2
2
 
3
- `@jsenv/server` helps to write flexible server code with a declarative API.
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
- services: [
12
+ routes: [
11
13
  {
12
- handleRequest: () => {
13
- return {
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
- # Examples
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
- _Code starting a server with 2 request handlers:_
45
+ # Quick Examples
46
+
47
+ **Basic API Server**
29
48
 
30
49
  ```js
31
- /*
32
- * starts a server which:
33
- * - when requested at "/"
34
- * -> respond with 200
35
- * - otherwise
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
- name: "index",
44
- handleRequest: (request) => {
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
- name: "otherwise",
53
- handleRequest: () => {
54
- return { status: 404 };
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
- _Code starting a server in https:_
72
+ **Static File Server**
69
73
 
70
74
  ```js
71
- import { readFileSync } from "node:fs";
72
- import { startServer } from "@jsenv/server";
75
+ import { startServer, createFileSystemFetch } from "@jsenv/server";
73
76
 
74
77
  await startServer({
75
- https: {
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
- handleRequest: (request) => {
83
- const clientUsesHttp = request.origin.startsWith("http:");
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
- _Code starting a server for static files:_
87
+ **HTTPS Server**
99
88
 
100
89
  ```js
101
- import { startServer, fetchFileSystem } from "@jsenv/server";
90
+ import { readFileSync } from "node:fs";
91
+ import { startServer } from "@jsenv/server";
102
92
 
103
93
  await startServer({
104
- services: [
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
- handleRequest: async (request) => {
107
- const fileUrl = new URL(request.resource.slice(1), import.meta.url);
108
- const response = await fetchFileSystem(fileUrl, request);
109
- return response;
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
- - [Handling requests](./docs/handling_requests.md)
119
- - [Handling errors](./docs/handling_errors.md)
120
- - [Server timing](./docs/server_timing.md)
121
- - [CORS](./docs/cors.md)
122
- - [https](./docs/https.md)
123
- - [Serving files](./docs/serving_files.md)
124
- - [Content negotiation](./docs/content_negotiation.md)
125
- - [Websocket](./docs/websocket.md)
126
- - [Server Sent Events](./docs/server_sent_events.md)
127
- - [Cluster](./docs/cluster.md)
128
- - [Http2 push](./docs/http2_push.md)
129
-
130
- # Installation
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>