@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
@@ -0,0 +1,455 @@
1
+ import { Abort } from "@jsenv/abort";
2
+ import { createDetailedMessage } from "@jsenv/humanize";
3
+ import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
4
+ import { parse } from "node:querystring";
5
+ import {
6
+ colorizeResponseStatus,
7
+ statusToType,
8
+ } from "../internal/colorizeResponseStatus.js";
9
+ import { headersFromObject } from "../internal/headersFromObject.js";
10
+ import { parseSingleHeaderWithAttributes } from "../internal/multiple-header.js";
11
+ import { observableFromNodeStream } from "./observable_from_node_stream.js";
12
+
13
+ export const fromNodeRequest = (
14
+ nodeRequest,
15
+ { serverOrigin, signal, requestBodyLifetime, logger, nagle },
16
+ ) => {
17
+ const requestLogger = createRequestLogger(nodeRequest, (type, value) => {
18
+ const logFunction = logger[type];
19
+ logFunction(value);
20
+ });
21
+ nodeRequest.on("error", (error) => {
22
+ if (error.message === "aborted") {
23
+ requestLogger.debug(
24
+ createDetailedMessage(`request aborted by client`, {
25
+ "error message": error.message,
26
+ }),
27
+ );
28
+ } else {
29
+ // I'm not sure this can happen but it's here in case
30
+ requestLogger.error(
31
+ createDetailedMessage(`"error" event emitted on request`, {
32
+ "error stack": error.stack,
33
+ }),
34
+ );
35
+ }
36
+ });
37
+
38
+ const handleRequestOperation = Abort.startOperation();
39
+ if (signal) {
40
+ handleRequestOperation.addAbortSignal(signal);
41
+ }
42
+ handleRequestOperation.addAbortSource((abort) => {
43
+ nodeRequest.once("close", abort);
44
+ return () => {
45
+ nodeRequest.removeListener("close", abort);
46
+ };
47
+ });
48
+
49
+ const headers = headersFromObject(nodeRequest.headers);
50
+ // pause the request body stream to let a chance for other parts of the code to subscribe to the stream
51
+ // Without this the request body readable stream
52
+ // might be closed when we'll try to attach "data" and "end" listeners to it
53
+ nodeRequest.pause();
54
+ if (!nagle) {
55
+ nodeRequest.connection.setNoDelay(true);
56
+ }
57
+ const body = observableFromNodeStream(nodeRequest, {
58
+ readableLifetime: requestBodyLifetime,
59
+ });
60
+
61
+ let requestOrigin;
62
+ if (nodeRequest.upgrade) {
63
+ requestOrigin = serverOrigin;
64
+ } else if (nodeRequest.authority) {
65
+ requestOrigin = nodeRequest.connection.encrypted
66
+ ? `https://${nodeRequest.authority}`
67
+ : `http://${nodeRequest.authority}`;
68
+ } else if (nodeRequest.headers.host) {
69
+ requestOrigin = nodeRequest.connection.encrypted
70
+ ? `https://${nodeRequest.headers.host}`
71
+ : `http://${nodeRequest.headers.host}`;
72
+ } else {
73
+ requestOrigin = serverOrigin;
74
+ }
75
+
76
+ // check the following parsers if we want to support more request body content types
77
+ // https://github.com/node-formidable/formidable/tree/master/src/parsers
78
+ const buffer = async () => {
79
+ // here we don't really need to warn, one might want to read anything as binary
80
+ // const contentType = headers["content-type"];
81
+ // if (!CONTENT_TYPE.isBinary(contentType)) {
82
+ // console.warn(
83
+ // `buffer() called on a request with content-type: "${contentType}". A binary content-type was expected.`,
84
+ // );
85
+ // }
86
+ const requestBodyBuffer = await readBody(body, { as: "buffer" });
87
+ return requestBodyBuffer;
88
+ };
89
+ // maybe we could use https://github.com/form-data/form-data
90
+ // for now we'll just return { fields, files } it's good enough to work with
91
+ const formData = async () => {
92
+ const contentType = headers["content-type"];
93
+ if (contentType !== "multipart/form-data") {
94
+ console.warn(
95
+ `formData() called on a request with content-type: "${contentType}". multipart/form-data was expected.`,
96
+ );
97
+ }
98
+ const { formidable } = await import("formidable");
99
+ const form = formidable({});
100
+ nodeRequest.resume(); // was paused in line #53
101
+ const [fields, files] = await form.parse(nodeRequest);
102
+ const requestBodyFormData = { fields, files };
103
+ return requestBodyFormData;
104
+ };
105
+ const text = async () => {
106
+ const contentType = headers["content-type"];
107
+ if (!CONTENT_TYPE.isTextual(contentType)) {
108
+ console.warn(
109
+ `text() called on a request with content-type "${contentType}". A textual content-type was expected.`,
110
+ );
111
+ }
112
+ const requestBodyString = await readBody(body, { as: "string" });
113
+ return requestBodyString;
114
+ };
115
+ const json = async () => {
116
+ const contentType = headers["content-type"];
117
+ if (!CONTENT_TYPE.isJson(contentType)) {
118
+ console.warn(
119
+ `json() called on a request with content-type "${contentType}". A json content-type was expected.`,
120
+ );
121
+ }
122
+ const requestBodyString = await readBody(body, { as: "string" });
123
+ const requestBodyJSON = JSON.parse(requestBodyString);
124
+ return requestBodyJSON;
125
+ };
126
+ const queryString = async () => {
127
+ const contentType = headers["content-type"];
128
+ if (contentType !== "application/x-www-form-urlencoded") {
129
+ console.warn(
130
+ `queryString() called on a request with content-type "${contentType}". application/x-www-form-urlencoded was expected.`,
131
+ );
132
+ }
133
+ const requestBodyString = await readBody(body, { as: "string" });
134
+ const requestBodyQueryStringParsed = parse(requestBodyString);
135
+ return requestBodyQueryStringParsed;
136
+ };
137
+
138
+ // request.ip -> request ip as received by the server
139
+ // request.ipForwarded -> ip of the client before proxying, undefined when there is no proxy
140
+ // same applies on request.proto and request.host
141
+ let ip = nodeRequest.socket.remoteAddress;
142
+ let proto = requestOrigin.startsWith("http:") ? "http" : "https";
143
+ let host = headers["host"];
144
+ const forwarded = headers["forwarded"];
145
+ let hostForwarded;
146
+ let ipForwarded;
147
+ let protoForwarded;
148
+ if (forwarded) {
149
+ const forwardedParsed = parseSingleHeaderWithAttributes(forwarded);
150
+ ipForwarded = forwardedParsed.for;
151
+ protoForwarded = forwardedParsed.proto;
152
+ hostForwarded = forwardedParsed.host;
153
+ } else {
154
+ const forwardedFor = headers["x-forwarded-for"];
155
+ const forwardedProto = headers["x-forwarded-proto"];
156
+ const forwardedHost = headers["x-forwarded-host"];
157
+ if (forwardedFor) {
158
+ // format is <client-ip>, <proxy1>, <proxy2>
159
+ ipForwarded = forwardedFor.split(",")[0];
160
+ }
161
+ if (forwardedProto) {
162
+ protoForwarded = forwardedProto;
163
+ }
164
+ if (forwardedHost) {
165
+ hostForwarded = forwardedHost;
166
+ }
167
+ }
168
+
169
+ return Object.freeze({
170
+ logger: requestLogger,
171
+ ip,
172
+ ipForwarded,
173
+ proto,
174
+ protoForwarded,
175
+ host,
176
+ hostForwarded,
177
+ params: {},
178
+ signal: handleRequestOperation.signal,
179
+ http2: Boolean(nodeRequest.stream),
180
+ origin: requestOrigin,
181
+ ...getPropertiesFromResource({
182
+ resource: nodeRequest.url,
183
+ baseUrl: requestOrigin,
184
+ }),
185
+ method: nodeRequest.method,
186
+ headers,
187
+ body,
188
+ buffer,
189
+ formData,
190
+ text,
191
+ json,
192
+ queryString,
193
+ });
194
+ };
195
+
196
+ const createRequestLogger = (nodeRequest, write) => {
197
+ // Handling request is asynchronous, we buffer logs for that request
198
+ // until we know what happens with that request
199
+ // It delays logs until we know of the request will be handled
200
+ // but it's mandatory to make logs readable.
201
+
202
+ const logArray = [];
203
+ const childArray = [];
204
+ const add = ({ type, value }) => {
205
+ logArray.push({ type, value });
206
+ };
207
+
208
+ const requestLogger = {
209
+ logArray,
210
+ childArray,
211
+ hasPushChild: false,
212
+ forPush: () => {
213
+ const childLogBuffer = createRequestLogger(nodeRequest, write);
214
+ childLogBuffer.isChild = true;
215
+ childArray.push(childLogBuffer);
216
+ requestLogger.hasPushChild = true;
217
+ return childLogBuffer;
218
+ },
219
+ debug: (value) => {
220
+ add({
221
+ type: "debug",
222
+ value,
223
+ });
224
+ },
225
+ info: (value) => {
226
+ add({
227
+ type: "info",
228
+ value,
229
+ });
230
+ },
231
+ warn: (value) => {
232
+ add({
233
+ type: "warn",
234
+ value,
235
+ });
236
+ },
237
+ error: (value) => {
238
+ add({
239
+ type: "error",
240
+ value,
241
+ });
242
+ },
243
+ onHeadersSent: ({ status, statusText }) => {
244
+ const statusType = statusToType(status);
245
+ let message = `${colorizeResponseStatus(status)}`;
246
+ if (statusText) {
247
+ message += ` ${statusText}`;
248
+ }
249
+ add({
250
+ type:
251
+ status === 404 && nodeRequest.path === "/favicon.ico"
252
+ ? "debug"
253
+ : {
254
+ information: "info",
255
+ success: "info",
256
+ redirection: "info",
257
+ client_error: "warn",
258
+ server_error: "error",
259
+ }[statusType] || "error",
260
+ value: message,
261
+ });
262
+ },
263
+ ended: false,
264
+ end: () => {
265
+ if (requestLogger.ended) {
266
+ return;
267
+ }
268
+ requestLogger.ended = true;
269
+ if (requestLogger.isChild) {
270
+ // keep buffering until root request write logs for everyone
271
+ return;
272
+ }
273
+ const prefixLines = (string, prefix) => {
274
+ return string.replace(/^(?!\s*$)/gm, prefix);
275
+ };
276
+ const writeLog = (
277
+ { type, value },
278
+ { someLogIsError, someLogIsWarn, depth },
279
+ ) => {
280
+ if (depth > 0) {
281
+ value = prefixLines(value, " ".repeat(depth));
282
+ }
283
+ if (type === "info") {
284
+ if (someLogIsError) {
285
+ type = "error";
286
+ } else if (someLogIsWarn) {
287
+ type = "warn";
288
+ }
289
+ }
290
+ write(type, value);
291
+ };
292
+ const writeLogs = (loggerToWrite, depth) => {
293
+ let someLogIsError = false;
294
+ let someLogIsWarn = false;
295
+ for (const log of loggerToWrite.logArray) {
296
+ if (log.type === "error") {
297
+ someLogIsError = true;
298
+ }
299
+ if (log.type === "warn") {
300
+ someLogIsWarn = true;
301
+ }
302
+ }
303
+ const firstLog = loggerToWrite.logArray.shift();
304
+ const lastLog = loggerToWrite.logArray.pop();
305
+ const middleLogs = loggerToWrite.logArray;
306
+ if (!firstLog) {
307
+ debugger;
308
+ }
309
+ writeLog(firstLog, {
310
+ someLogIsError,
311
+ someLogIsWarn,
312
+ depth,
313
+ });
314
+ for (const middleLog of middleLogs) {
315
+ writeLog(middleLog, {
316
+ someLogIsError,
317
+ someLogIsWarn,
318
+ depth,
319
+ });
320
+ }
321
+ for (const childLoggerToWrite of loggerToWrite.childArray) {
322
+ writeLogs(childLoggerToWrite, depth + 1);
323
+ }
324
+ if (lastLog) {
325
+ writeLog(lastLog, {
326
+ someLogIsError,
327
+ someLogIsWarn,
328
+ depth: depth + 1,
329
+ });
330
+ }
331
+ };
332
+ writeLogs(requestLogger, 0);
333
+ },
334
+ };
335
+
336
+ return requestLogger;
337
+ };
338
+
339
+ const readBody = (body, { as }) => {
340
+ return new Promise((resolve, reject) => {
341
+ const bufferArray = [];
342
+ body.subscribe({
343
+ error: reject,
344
+ next: (buffer) => {
345
+ bufferArray.push(buffer);
346
+ },
347
+ complete: () => {
348
+ const bodyAsBuffer = Buffer.concat(bufferArray);
349
+ if (as === "buffer") {
350
+ resolve(bodyAsBuffer);
351
+ return;
352
+ }
353
+ if (as === "string") {
354
+ const bodyAsString = bodyAsBuffer.toString();
355
+ resolve(bodyAsString);
356
+ return;
357
+ }
358
+ if (as === "json") {
359
+ const bodyAsString = bodyAsBuffer.toString();
360
+ const bodyAsJSON = JSON.parse(bodyAsString);
361
+ resolve(bodyAsJSON);
362
+ return;
363
+ }
364
+ },
365
+ });
366
+ });
367
+ };
368
+ // exported for unit tests
369
+ export const readRequestBody = (request, { as }) => {
370
+ if (as === "string") {
371
+ return request.text();
372
+ }
373
+ if (as === "buffer") {
374
+ return request.buffer();
375
+ }
376
+ if (as === "json") {
377
+ return request.json();
378
+ }
379
+ throw new Error(`unsupported ${as}`);
380
+ };
381
+
382
+ export const applyRedirectionToRequest = (
383
+ request,
384
+ { resource, pathname, ...rest },
385
+ ) => {
386
+ return {
387
+ ...request,
388
+ ...(resource
389
+ ? getPropertiesFromResource({
390
+ resource,
391
+ baseUrl: request.url,
392
+ })
393
+ : pathname
394
+ ? getPropertiesFromPathname({
395
+ pathname,
396
+ baseUrl: request.url,
397
+ })
398
+ : {}),
399
+ ...rest,
400
+ };
401
+ };
402
+ const getPropertiesFromResource = ({ resource, baseUrl }) => {
403
+ const urlObject = new URL(resource, baseUrl);
404
+ let pathname = urlObject.pathname;
405
+
406
+ return {
407
+ url: String(urlObject),
408
+ searchParams: urlObject.searchParams,
409
+ pathname,
410
+ resource,
411
+ };
412
+ };
413
+ const getPropertiesFromPathname = ({ pathname, baseUrl }) => {
414
+ return getPropertiesFromResource({
415
+ resource: `${pathname}${new URL(baseUrl).search}`,
416
+ baseUrl,
417
+ });
418
+ };
419
+
420
+ export const createPushRequest = (
421
+ request,
422
+ { signal, pathname, method, logger },
423
+ ) => {
424
+ const pushRequest = Object.freeze({
425
+ ...request,
426
+ logger,
427
+ parent: request,
428
+ signal,
429
+ http2: true,
430
+ ...(pathname
431
+ ? getPropertiesFromPathname({
432
+ pathname,
433
+ baseUrl: request.url,
434
+ })
435
+ : {}),
436
+ method: method || request.method,
437
+ headers: getHeadersInheritedByPushRequest(request),
438
+ body: undefined,
439
+ });
440
+ return pushRequest;
441
+ };
442
+
443
+ const getHeadersInheritedByPushRequest = (request) => {
444
+ const headersInherited = { ...request.headers };
445
+ // mtime sent by the client in request headers concerns the main request
446
+ // Time remains valid for request to other resources so we keep it
447
+ // in child requests
448
+ // delete childHeaders["if-modified-since"]
449
+
450
+ // eTag sent by the client in request headers concerns the main request
451
+ // A request made to an other resource must not inherit the eTag
452
+ delete headersInherited["if-none-match"];
453
+
454
+ return headersInherited;
455
+ };
@@ -0,0 +1,43 @@
1
+ import { Readable, Stream, Writable } from "node:stream";
2
+ import { isObservable } from "./observable.js";
3
+ import { isFileHandle } from "./observable_from_file_handle.js";
4
+
5
+ export const getObservableValueType = (value) => {
6
+ if (value && typeof value.then === "function") {
7
+ return "promise";
8
+ }
9
+
10
+ if (isObservable(value)) {
11
+ return "observable";
12
+ }
13
+
14
+ if (isFileHandle(value)) {
15
+ return "file_handle";
16
+ }
17
+
18
+ if (isNodeStream(value)) {
19
+ return "node_stream";
20
+ }
21
+
22
+ if (value instanceof ReadableStream) {
23
+ return "node_web_stream";
24
+ }
25
+
26
+ return "js_value";
27
+ };
28
+
29
+ const isNodeStream = (value) => {
30
+ if (value === undefined) {
31
+ return false;
32
+ }
33
+
34
+ if (
35
+ value instanceof Stream ||
36
+ value instanceof Writable ||
37
+ value instanceof Readable
38
+ ) {
39
+ return true;
40
+ }
41
+
42
+ return false;
43
+ };