@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.
- 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 +19 -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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "16.0.1",
|
|
4
|
+
"description": "A modern Node.js HTTP server with declarative routing, content negotiation, and WebSocket support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "dmail",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"directory": "packages/independent/backend/server"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
16
|
+
"node": ">=22.13.1"
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
@@ -21,20 +21,30 @@
|
|
|
21
21
|
"type": "module",
|
|
22
22
|
"exports": {
|
|
23
23
|
".": {
|
|
24
|
-
"import":
|
|
24
|
+
"import": {
|
|
25
|
+
"development": "./src/main.js",
|
|
26
|
+
"default": "./dist/jsenv_server.js"
|
|
27
|
+
}
|
|
25
28
|
},
|
|
26
29
|
"./*": "./*"
|
|
27
30
|
},
|
|
28
31
|
"main": "./src/main.js",
|
|
29
32
|
"files": [
|
|
33
|
+
"/dist/",
|
|
30
34
|
"/src/"
|
|
31
35
|
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "node --experimental-import-meta-resolve ./scripts/build.mjs",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
32
40
|
"dependencies": {
|
|
33
|
-
"
|
|
34
|
-
"@jsenv/
|
|
35
|
-
"@jsenv/
|
|
36
|
-
"@jsenv/
|
|
37
|
-
"
|
|
41
|
+
"launch-editor": "2.10.0",
|
|
42
|
+
"@jsenv/abort": "4.3.1",
|
|
43
|
+
"@jsenv/humanize": "1.3.0",
|
|
44
|
+
"@jsenv/url-meta": "8.5.4",
|
|
45
|
+
"@jsenv/utils": "2.2.0",
|
|
46
|
+
"ws": "8.18.0",
|
|
47
|
+
"formidable": "3.5.2"
|
|
38
48
|
},
|
|
39
49
|
"devDependencies": {
|
|
40
50
|
"eventsource": "3.0.2",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { parseMultipleHeader } from "../internal/multiple-header.js";
|
|
2
|
+
import { pickAcceptedContent } from "./pick_accepted_content.js";
|
|
3
|
+
|
|
4
|
+
export const pickContentVersion = (request, availableVersions) => {
|
|
5
|
+
const { headers = {} } = request;
|
|
6
|
+
const requestAcceptVersionHeader = headers["accept-version"];
|
|
7
|
+
if (!requestAcceptVersionHeader) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const versionsAccepted = parseAcceptVersionHeader(requestAcceptVersionHeader);
|
|
12
|
+
return pickAcceptedContent({
|
|
13
|
+
accepteds: versionsAccepted,
|
|
14
|
+
availables: availableVersions,
|
|
15
|
+
getAcceptanceScore: getVersionAcceptanceScore,
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const parseAcceptVersionHeader = (acceptVersionHeaderString) => {
|
|
20
|
+
const acceptVersionHeader = parseMultipleHeader(acceptVersionHeaderString, {
|
|
21
|
+
validateProperty: ({ name }) => {
|
|
22
|
+
// read only q, anything else is ignored
|
|
23
|
+
return name === "q";
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const versionsAccepted = [];
|
|
28
|
+
for (const key of Object.keys(acceptVersionHeader)) {
|
|
29
|
+
const { q = 1 } = acceptVersionHeader[key];
|
|
30
|
+
const value = key;
|
|
31
|
+
versionsAccepted.push({
|
|
32
|
+
value,
|
|
33
|
+
quality: q,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
versionsAccepted.sort((a, b) => {
|
|
37
|
+
return b.quality - a.quality;
|
|
38
|
+
});
|
|
39
|
+
return versionsAccepted;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const getVersionAcceptanceScore = ({ value, quality }, availableVersion) => {
|
|
43
|
+
if (value === "*") {
|
|
44
|
+
return quality;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof availableVersion === "function") {
|
|
48
|
+
if (availableVersion(value)) {
|
|
49
|
+
return quality;
|
|
50
|
+
}
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof availableVersion === "number") {
|
|
55
|
+
availableVersion = String(availableVersion);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (value === availableVersion) {
|
|
59
|
+
return quality;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return -1;
|
|
63
|
+
};
|
|
@@ -6,23 +6,27 @@
|
|
|
6
6
|
|
|
7
7
|
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
8
8
|
import { createReadStream, readFile, statSync } from "node:fs";
|
|
9
|
-
import { pickContentEncoding } from "
|
|
10
|
-
import { convertFileSystemErrorToResponseProperties } from "
|
|
11
|
-
import { bufferToEtag } from "
|
|
9
|
+
import { pickContentEncoding } from "../content_negotiation/pick_content_encoding.js";
|
|
10
|
+
import { convertFileSystemErrorToResponseProperties } from "../internal/convertFileSystemErrorToResponseProperties.js";
|
|
11
|
+
import { bufferToEtag } from "../internal/etag.js";
|
|
12
12
|
import {
|
|
13
13
|
fileSystemPathToUrl,
|
|
14
14
|
isFileSystemPath,
|
|
15
|
-
} from "
|
|
16
|
-
import { composeTwoResponses } from "
|
|
15
|
+
} from "../internal/filesystem.js";
|
|
16
|
+
import { composeTwoResponses } from "../internal/response_composition.js";
|
|
17
17
|
import { serveDirectory } from "./serve_directory.js";
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
export const createFileSystemFetch = (directoryUrl, options) => {
|
|
20
|
+
return (request, helpers) => {
|
|
21
|
+
return fetchFileSystem(request, helpers, directoryUrl, options);
|
|
22
|
+
};
|
|
23
|
+
};
|
|
19
24
|
|
|
20
25
|
export const fetchFileSystem = async (
|
|
21
|
-
|
|
26
|
+
request,
|
|
27
|
+
helpers,
|
|
28
|
+
directoryUrl,
|
|
22
29
|
{
|
|
23
|
-
// signal,
|
|
24
|
-
method = "GET",
|
|
25
|
-
headers = {},
|
|
26
30
|
etagEnabled = false,
|
|
27
31
|
etagMemory = true,
|
|
28
32
|
etagMemoryMaxSize = 1000,
|
|
@@ -33,37 +37,34 @@ export const fetchFileSystem = async (
|
|
|
33
37
|
? "private,max-age=0,must-revalidate"
|
|
34
38
|
: "no-store",
|
|
35
39
|
canReadDirectory = false,
|
|
36
|
-
rootDirectoryUrl, // = `${pathToFileURL(process.cwd())}/`,
|
|
37
40
|
ENOENTFallback = () => {},
|
|
38
41
|
} = {},
|
|
39
42
|
) => {
|
|
40
|
-
|
|
41
|
-
if (!
|
|
42
|
-
|
|
43
|
-
`
|
|
43
|
+
let directoryUrlString = asUrlString(directoryUrl);
|
|
44
|
+
if (!directoryUrlString) {
|
|
45
|
+
throw new TypeError(
|
|
46
|
+
`directoryUrl must be a string or an url, got ${directoryUrl}`,
|
|
44
47
|
);
|
|
45
48
|
}
|
|
46
|
-
if (!
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
+
if (!directoryUrlString.startsWith("file://")) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`directoryUrl must start with "file://", got ${directoryUrlString}`,
|
|
49
52
|
);
|
|
50
53
|
}
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
rootDirectoryUrl = rootDirectoryUrlString;
|
|
54
|
+
if (!directoryUrlString.endsWith("/")) {
|
|
55
|
+
directoryUrlString = `${directoryUrlString}/`;
|
|
56
|
+
}
|
|
57
|
+
let resource;
|
|
58
|
+
if (request.params && "0" in request.params) {
|
|
59
|
+
resource = request.params["0"];
|
|
60
|
+
} else {
|
|
61
|
+
resource = request.resource.slice(1);
|
|
62
|
+
}
|
|
63
|
+
const filesystemUrl = new URL(resource, directoryUrl);
|
|
64
|
+
const urlString = asUrlString(filesystemUrl);
|
|
65
|
+
|
|
66
|
+
if (typeof cacheControl === "function") {
|
|
67
|
+
cacheControl = cacheControl(request);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
// here you might be tempted to add || cacheControl === 'no-cache'
|
|
@@ -86,24 +87,22 @@ export const fetchFileSystem = async (
|
|
|
86
87
|
mtimeEnabled = false;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
90
|
-
return
|
|
91
|
-
status: 501,
|
|
92
|
-
};
|
|
90
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
91
|
+
return null;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
const serveFile = async (fileUrl) => {
|
|
96
95
|
try {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
const readStatTiming = helpers?.timing("file service>read file stat");
|
|
97
|
+
const fileStat = statSync(new URL(fileUrl));
|
|
98
|
+
readStatTiming?.end();
|
|
99
|
+
|
|
101
100
|
if (fileStat.isDirectory()) {
|
|
102
101
|
if (canReadDirectory) {
|
|
103
102
|
return serveDirectory(fileUrl, {
|
|
104
|
-
headers,
|
|
103
|
+
headers: request.headers,
|
|
105
104
|
canReadDirectory,
|
|
106
|
-
rootDirectoryUrl,
|
|
105
|
+
rootDirectoryUrl: directoryUrl,
|
|
107
106
|
});
|
|
108
107
|
}
|
|
109
108
|
return {
|
|
@@ -115,12 +114,12 @@ export const fetchFileSystem = async (
|
|
|
115
114
|
if (!fileStat.isFile()) {
|
|
116
115
|
return {
|
|
117
116
|
status: 404,
|
|
118
|
-
timing: readStatTiming,
|
|
119
117
|
};
|
|
120
118
|
}
|
|
121
119
|
|
|
122
120
|
const clientCacheResponse = await getClientCacheResponse({
|
|
123
|
-
headers,
|
|
121
|
+
headers: request.headers,
|
|
122
|
+
helpers,
|
|
124
123
|
etagEnabled,
|
|
125
124
|
etagMemory,
|
|
126
125
|
etagMemoryMaxSize,
|
|
@@ -134,7 +133,6 @@ export const fetchFileSystem = async (
|
|
|
134
133
|
if (clientCacheResponse.status === 304) {
|
|
135
134
|
return composeTwoResponses(
|
|
136
135
|
{
|
|
137
|
-
timing: readStatTiming,
|
|
138
136
|
headers: {
|
|
139
137
|
...(cacheControl ? { "cache-control": cacheControl } : {}),
|
|
140
138
|
},
|
|
@@ -146,7 +144,7 @@ export const fetchFileSystem = async (
|
|
|
146
144
|
let response;
|
|
147
145
|
if (compressionEnabled && fileStat.size >= compressionSizeThreshold) {
|
|
148
146
|
const compressedResponse = await getCompressedResponse({
|
|
149
|
-
headers,
|
|
147
|
+
headers: request.headers,
|
|
150
148
|
fileUrl,
|
|
151
149
|
});
|
|
152
150
|
if (compressedResponse) {
|
|
@@ -162,7 +160,6 @@ export const fetchFileSystem = async (
|
|
|
162
160
|
|
|
163
161
|
const intermediateResponse = composeTwoResponses(
|
|
164
162
|
{
|
|
165
|
-
timing: readStatTiming,
|
|
166
163
|
headers: {
|
|
167
164
|
...(cacheControl ? { "cache-control": cacheControl } : {}),
|
|
168
165
|
// even if client cache is disabled, server can still
|
|
@@ -197,19 +194,9 @@ export const fetchFileSystem = async (
|
|
|
197
194
|
return serveFile(`file://${new URL(urlString).pathname}`);
|
|
198
195
|
};
|
|
199
196
|
|
|
200
|
-
const create500Response = (message) => {
|
|
201
|
-
return {
|
|
202
|
-
status: 500,
|
|
203
|
-
headers: {
|
|
204
|
-
"content-type": "text/plain",
|
|
205
|
-
"content-length": Buffer.byteLength(message),
|
|
206
|
-
},
|
|
207
|
-
body: message,
|
|
208
|
-
};
|
|
209
|
-
};
|
|
210
|
-
|
|
211
197
|
const getClientCacheResponse = async ({
|
|
212
198
|
headers,
|
|
199
|
+
helpers,
|
|
213
200
|
etagEnabled,
|
|
214
201
|
etagMemory,
|
|
215
202
|
etagMemoryMaxSize,
|
|
@@ -232,6 +219,7 @@ const getClientCacheResponse = async ({
|
|
|
232
219
|
if (etagEnabled) {
|
|
233
220
|
return getEtagResponse({
|
|
234
221
|
headers,
|
|
222
|
+
helpers,
|
|
235
223
|
etagMemory,
|
|
236
224
|
etagMemoryMaxSize,
|
|
237
225
|
fileStat,
|
|
@@ -251,21 +239,20 @@ const getClientCacheResponse = async ({
|
|
|
251
239
|
|
|
252
240
|
const getEtagResponse = async ({
|
|
253
241
|
headers,
|
|
242
|
+
helpers,
|
|
254
243
|
etagMemory,
|
|
255
244
|
etagMemoryMaxSize,
|
|
256
245
|
fileUrl,
|
|
257
246
|
fileStat,
|
|
258
247
|
}) => {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}),
|
|
268
|
-
);
|
|
248
|
+
const etagTiming = helpers?.timing("file service>generate file etag");
|
|
249
|
+
const fileContentEtag = await computeEtag({
|
|
250
|
+
etagMemory,
|
|
251
|
+
etagMemoryMaxSize,
|
|
252
|
+
fileUrl,
|
|
253
|
+
fileStat,
|
|
254
|
+
});
|
|
255
|
+
etagTiming?.end();
|
|
269
256
|
|
|
270
257
|
const requestHasIfNoneMatchHeader = "if-none-match" in headers;
|
|
271
258
|
if (
|
|
@@ -274,7 +261,6 @@ const getEtagResponse = async ({
|
|
|
274
261
|
) {
|
|
275
262
|
return {
|
|
276
263
|
status: 304,
|
|
277
|
-
timing: computeEtagTiming,
|
|
278
264
|
};
|
|
279
265
|
}
|
|
280
266
|
|
|
@@ -283,7 +269,6 @@ const getEtagResponse = async ({
|
|
|
283
269
|
headers: {
|
|
284
270
|
etag: fileContentEtag,
|
|
285
271
|
},
|
|
286
|
-
timing: computeEtagTiming,
|
|
287
272
|
};
|
|
288
273
|
};
|
|
289
274
|
|