@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.
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 +20 -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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "15.5.3",
4
- "description": "Write your Node.js server using pure functions",
3
+ "version": "16.0.0",
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.13.0"
16
+ "node": ">=22.13.1"
17
17
  },
18
18
  "publishConfig": {
19
19
  "access": "public"
@@ -21,20 +21,31 @@
21
21
  "type": "module",
22
22
  "exports": {
23
23
  ".": {
24
- "import": "./src/main.js"
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
- "@jsenv/abort": "4.3.0",
34
- "@jsenv/humanize": "1.2.8",
35
- "@jsenv/url-meta": "8.5.3",
36
- "@jsenv/utils": "2.1.3",
37
- "ws": "8.18.0"
41
+ "@jsenv/router": "0.0.8",
42
+ "launch-editor": "2.10.0",
43
+ "@jsenv/abort": "4.3.1",
44
+ "@jsenv/humanize": "1.3.0",
45
+ "@jsenv/url-meta": "8.5.4",
46
+ "@jsenv/utils": "2.2.0",
47
+ "ws": "8.18.0",
48
+ "formidable": "3.5.2"
38
49
  },
39
50
  "devDependencies": {
40
51
  "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 "./content_negotiation/pick_content_encoding.js";
10
- import { convertFileSystemErrorToResponseProperties } from "./internal/convertFileSystemErrorToResponseProperties.js";
11
- import { bufferToEtag } from "./internal/etag.js";
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 "./internal/filesystem.js";
16
- import { composeTwoResponses } from "./internal/response_composition.js";
15
+ } from "../internal/filesystem.js";
16
+ import { composeTwoResponses } from "../internal/response_composition.js";
17
17
  import { serveDirectory } from "./serve_directory.js";
18
- import { timeFunction } from "./server_timing/timing_measure.js";
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
- filesystemUrl,
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
- const urlString = asUrlString(filesystemUrl);
41
- if (!urlString) {
42
- return create500Response(
43
- `fetchFileSystem first parameter must be a file url, got ${filesystemUrl}`,
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 (!urlString.startsWith("file://")) {
47
- return create500Response(
48
- `fetchFileSystem url must use "file://" scheme, got ${filesystemUrl}`,
49
+ if (!directoryUrlString.startsWith("file://")) {
50
+ throw new Error(
51
+ `directoryUrl must start with "file://", got ${directoryUrlString}`,
49
52
  );
50
53
  }
51
- if (rootDirectoryUrl) {
52
- let rootDirectoryUrlString = asUrlString(rootDirectoryUrl);
53
- if (!rootDirectoryUrlString) {
54
- return create500Response(
55
- `rootDirectoryUrl must be a string or an url, got ${rootDirectoryUrl}`,
56
- );
57
- }
58
- if (!rootDirectoryUrlString.endsWith("/")) {
59
- rootDirectoryUrlString = `${rootDirectoryUrlString}/`;
60
- }
61
- if (!urlString.startsWith(rootDirectoryUrlString)) {
62
- return create500Response(
63
- `fetchFileSystem url must be inside root directory, got ${urlString}`,
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 [readStatTiming, fileStat] = timeFunction(
98
- "file service>read file stat",
99
- () => statSync(new URL(fileUrl)),
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 [computeEtagTiming, fileContentEtag] = await timeFunction(
260
- "file service>generate file etag",
261
- () =>
262
- computeEtag({
263
- etagMemory,
264
- etagMemoryMaxSize,
265
- fileUrl,
266
- fileStat,
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
 
@@ -1,6 +1,5 @@
1
1
  import { lstatSync, readdirSync } from "node:fs";
2
-
3
- import { pickContentType } from "./content_negotiation/pick_content_type.js";
2
+ import { pickContentType } from "../content_negotiation/pick_content_type.js";
4
3
 
5
4
  export const serveDirectory = (
6
5
  url,