@jsenv/server 12.1.0 → 12.3.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 CHANGED
@@ -92,8 +92,8 @@ import { startServer } from "@jsenv/server"
92
92
 
93
93
  await startServer({
94
94
  protocol: "https",
95
- serverCertificate: readFileSyncAsString("./server.crt"),
96
- serverCertificatePrivateKey: readFileSyncAsString("./server.key"),
95
+ certificate: readFileSyncAsString("./server.crt"),
96
+ privateKey: readFileSyncAsString("./server.key"),
97
97
  allowHttpRequestOnHttps: true,
98
98
  requestToResponse: (request) => {
99
99
  const clientUsesHttp = request.origin.startsWith("http:")
package/main.js CHANGED
@@ -42,6 +42,7 @@ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_
42
42
 
43
43
  // others
44
44
  export { fetchUrl } from "./src/fetch_url.js"
45
+ export { fromFetchResponse } from "./src/from_fetch_response.js"
45
46
  export { composeTwoResponses } from "./src/internal/response_composition.js"
46
47
  export { pluginRessourceAliases } from "./src/ressource_aliases/plugin_ressource_aliases.js"
47
48
  export { pluginRequestWaitingCheck } from "./src/plugin_request_waiting_check.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "12.1.0",
3
+ "version": "12.3.1",
4
4
  "description": "Write your Node.js server using pure functions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -13,7 +13,7 @@
13
13
  "url": "https://github.com/jsenv/server"
14
14
  },
15
15
  "engines": {
16
- "node": ">=16.12.0"
16
+ "node": ">=16.13.0"
17
17
  },
18
18
  "publishConfig": {
19
19
  "access": "public"
@@ -31,45 +31,40 @@
31
31
  "/main.js"
32
32
  ],
33
33
  "scripts": {
34
- "eslint-check": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
35
- "importmap-generate": "node ./script/importmap/generate_importmap.mjs",
36
- "measure-performances": "node --expose-gc ./script/performance/generate_performance_report.mjs --log",
37
- "test": "node --unhandled-rejections=strict ./script/test/test.mjs",
34
+ "importmap": "node ./script/importmap/importmap.mjs",
35
+ "eslint": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
36
+ "performance": "node --expose-gc ./script/performance/performance.mjs --log",
37
+ "test": "node ./script/test/test.mjs",
38
38
  "test-with-coverage": "npm run test -- --coverage",
39
39
  "generate-html-snapshots": "node ./test/internal_error/generate_html_snapshot_files.mjs",
40
- "prettier-format": "node ./script/prettier/prettier_format.mjs",
41
- "prettier-format-stage": "npm run prettier-format -- --staged",
42
- "prettier-check": "npm run prettier-format -- --dry-run",
43
- "certificate-authority-install": "node ./script/certificate/certificate_authority_install.mjs",
44
- "playwright-install": "npx playwright install-deps && npx playwright install",
45
- "prepublishOnly": "node ./script/publish/remove_postinstall.mjs",
46
- "postpublish": "node ./script/publish/restore_postinstall.mjs"
40
+ "prettier": "prettier --write .",
41
+ "certificate-install": "node ./script/certificate/certificate_install.mjs",
42
+ "playwright-install": "npx playwright install-deps && npx playwright install"
47
43
  },
48
44
  "dependencies": {
49
- "@jsenv/abort": "4.1.1",
45
+ "@jsenv/abort": "4.1.2",
50
46
  "@jsenv/logger": "4.0.1",
51
- "@jsenv/url-meta": "6.0.1",
47
+ "@jsenv/url-meta": "6.0.3",
52
48
  "node-fetch": "2.6.2",
53
49
  "path-to-regexp": "6.2.0"
54
50
  },
55
51
  "devDependencies": {
56
- "@jsenv/assert": "2.3.2",
57
- "@jsenv/core": "23.10.0",
52
+ "@jsenv/assert": "2.4.1",
53
+ "@jsenv/core": "25.1.0",
58
54
  "@jsenv/eslint-config": "16.0.9",
59
- "@jsenv/filesystem": "2.5.1",
55
+ "@jsenv/filesystem": "2.6.1",
60
56
  "@jsenv/github-release-package": "1.2.3",
61
57
  "@jsenv/https-local": "1.0.3",
62
- "@jsenv/importmap-eslint-resolver": "5.1.2",
63
- "@jsenv/importmap-node-module": "2.7.1",
58
+ "@jsenv/importmap-eslint-resolver": "5.2.2",
59
+ "@jsenv/importmap-node-module": "5.1.0",
64
60
  "@jsenv/package-publish": "1.6.2",
65
61
  "@jsenv/performance-impact": "2.2.1",
66
- "@jsenv/prettier-check-project": "5.6.1",
67
- "eslint": "8.2.0",
68
- "eslint-plugin-import": "2.25.3",
62
+ "eslint": "8.7.0",
63
+ "eslint-plugin-import": "2.25.4",
69
64
  "eventsource": "1.1.0",
70
- "fastify": "3.24.0",
65
+ "fastify": "3.25.3",
71
66
  "fastify-static": "4.5.0",
72
- "playwright": "1.16.3",
73
- "prettier": "2.4.1"
67
+ "playwright": "1.17.2",
68
+ "prettier": "2.5.1"
74
69
  }
75
- }
70
+ }
@@ -1,5 +1,3 @@
1
- import { urlToOrigin } from "@jsenv/filesystem"
2
-
3
1
  export const jsenvAccessControlAllowedHeaders = ["x-requested-with"]
4
2
 
5
3
  export const jsenvAccessControlAllowedMethods = [
@@ -107,7 +105,7 @@ const generateAccessControlHeaders = ({
107
105
  allowedOriginArray.push(headers.origin)
108
106
  vary.push("origin")
109
107
  } else if ("referer" in headers) {
110
- allowedOriginArray.push(urlToOrigin(headers.referer))
108
+ allowedOriginArray.push(new URL(headers.referer).origin)
111
109
  vary.push("referer")
112
110
  } else {
113
111
  allowedOriginArray.push("*")
@@ -0,0 +1,15 @@
1
+ export const fromFetchResponse = (fetchResponse) => {
2
+ const responseHeaders = {}
3
+ const headersToIgnore = ["connection"]
4
+ fetchResponse.headers.forEach((value, name) => {
5
+ if (!headersToIgnore.includes(name)) {
6
+ responseHeaders[name] = value
7
+ }
8
+ })
9
+ return {
10
+ status: fetchResponse.status,
11
+ statusText: fetchResponse.statusText,
12
+ headers: responseHeaders,
13
+ body: fetchResponse.body, // node-fetch assumed
14
+ }
15
+ }
@@ -0,0 +1,24 @@
1
+ export const memoize = (compute) => {
2
+ let memoized = false
3
+ let memoizedValue
4
+
5
+ const fnWithMemoization = (...args) => {
6
+ if (memoized) {
7
+ return memoizedValue
8
+ }
9
+ // if compute is recursive wait for it to be fully done before storing the value
10
+ // so set memoized boolean after the call
11
+ memoizedValue = compute(...args)
12
+ memoized = true
13
+ return memoizedValue
14
+ }
15
+
16
+ fnWithMemoization.forget = () => {
17
+ const value = memoizedValue
18
+ memoized = false
19
+ memoizedValue = undefined
20
+ return value
21
+ }
22
+
23
+ return fnWithMemoization
24
+ }
@@ -1,3 +1,4 @@
1
+ import http from "node:http"
1
2
  import { Http2ServerResponse } from "node:http2"
2
3
  import { raceCallbacks } from "@jsenv/abort"
3
4
 
@@ -155,10 +156,13 @@ const writeHead = (responseStream, { status, statusText, headers }) => {
155
156
  return
156
157
  }
157
158
 
159
+ if (statusText === undefined) {
160
+ statusText = statusTextFromStatus(status)
161
+ }
162
+
158
163
  // nodejs strange signature for writeHead force this
159
164
  // https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers
160
165
  if (
161
- statusText === undefined ||
162
166
  // https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L97
163
167
  responseIsHttp2ServerResponse
164
168
  ) {
@@ -169,6 +173,9 @@ const writeHead = (responseStream, { status, statusText, headers }) => {
169
173
  responseStream.writeHead(status, statusText, nodeHeaders)
170
174
  }
171
175
 
176
+ const statusTextFromStatus = (status) =>
177
+ http.STATUS_CODES[status] || "not specified"
178
+
172
179
  const headersToNodeHeaders = (headers, { ignoreConnectionHeader }) => {
173
180
  const nodeHeaders = {}
174
181
 
@@ -11,13 +11,13 @@ import { listenEvent } from "./listenEvent.js"
11
11
  export const createPolyglotServer = async ({
12
12
  http2 = false,
13
13
  http1Allowed = true,
14
- serverCertificate,
15
- serverCertificatePrivateKey,
14
+ certificate,
15
+ privateKey,
16
16
  }) => {
17
17
  const httpServer = http.createServer()
18
18
  const tlsServer = await createSecureServer({
19
- serverCertificate,
20
- serverCertificatePrivateKey,
19
+ certificate,
20
+ privateKey,
21
21
  http2,
22
22
  http1Allowed,
23
23
  })
@@ -63,24 +63,24 @@ export const createPolyglotServer = async ({
63
63
  // The async part is just to lazyly import "http2" or "https"
64
64
  // so that these module are parsed only if used.
65
65
  const createSecureServer = async ({
66
- serverCertificate,
67
- serverCertificatePrivateKey,
66
+ certificate,
67
+ privateKey,
68
68
  http2,
69
69
  http1Allowed,
70
70
  }) => {
71
71
  if (http2) {
72
72
  const { createSecureServer } = await import("http2")
73
73
  return createSecureServer({
74
- cert: serverCertificate,
75
- key: serverCertificatePrivateKey,
74
+ cert: certificate,
75
+ key: privateKey,
76
76
  allowHTTP1: http1Allowed,
77
77
  })
78
78
  }
79
79
 
80
80
  const { createServer } = await import("https")
81
81
  return createServer({
82
- cert: serverCertificate,
83
- key: serverCertificatePrivateKey,
82
+ cert: certificate,
83
+ key: privateKey,
84
84
  })
85
85
  }
86
86
 
@@ -1,6 +1,5 @@
1
1
  import http from "node:http"
2
2
  import cluster from "node:cluster"
3
- import { memoize } from "@jsenv/filesystem"
4
3
  import { createDetailedMessage, createLogger } from "@jsenv/logger"
5
4
  import {
6
5
  Abort,
@@ -23,6 +22,7 @@ import { listen, stopListening } from "./internal/listen.js"
23
22
  import { composeTwoResponses } from "./internal/response_composition.js"
24
23
  import { listenRequest } from "./internal/listenRequest.js"
25
24
  import { listenServerConnectionError } from "./internal/listenServerConnectionError.js"
25
+ import { memoize } from "./internal/memoize.js"
26
26
  import { jsenvServerInternalErrorToResponse } from "./jsenvServerInternalErrorToResponse.js"
27
27
  import {
28
28
  STOP_REASON_INTERNAL_ERROR,
@@ -47,8 +47,8 @@ export const startServer = async ({
47
47
  ip = "0.0.0.0", // will it work on windows ? https://github.com/nodejs/node/issues/14900
48
48
  port = 0, // assign a random available port
49
49
  portHint,
50
- serverCertificatePrivateKey,
51
- serverCertificate,
50
+ privateKey,
51
+ certificate,
52
52
 
53
53
  // when inside a worker, we should not try to stop server on SIGINT
54
54
  // otherwise it can create an EPIPE error while primary process tries
@@ -74,11 +74,11 @@ export const startServer = async ({
74
74
  throw new Error(`protocol must be http or https, got ${protocol}`)
75
75
  }
76
76
  if (protocol === "https") {
77
- if (!serverCertificate) {
78
- throw new Error(`missing serverCertificate for https server`)
77
+ if (!certificate) {
78
+ throw new Error(`missing certificate for https server`)
79
79
  }
80
- if (!serverCertificatePrivateKey) {
81
- throw new Error(`missing serverCertificatePrivateKey for https server`)
80
+ if (!privateKey) {
81
+ throw new Error(`missing privateKey for https server`)
82
82
  }
83
83
  }
84
84
  if (http2 && protocol !== "https") {
@@ -134,8 +134,8 @@ export const startServer = async ({
134
134
  protocol,
135
135
  redirectHttpToHttps,
136
136
  allowHttpRequestOnHttps,
137
- serverCertificate,
138
- serverCertificatePrivateKey,
137
+ certificate,
138
+ privateKey,
139
139
  http2,
140
140
  http1Allowed,
141
141
  })
@@ -332,29 +332,6 @@ export const startServer = async ({
332
332
  const pushResponse = async ({ path, method }, { requestNode }) => {
333
333
  const http2Stream = nodeResponse.stream
334
334
 
335
- if (typeof path !== "string" || path[0] !== "/") {
336
- addRequestLog(requestNode, {
337
- type: "warn",
338
- value: `pushResponse call ignored because path is invalid (must be a string starting with "/", found ${path})`,
339
- })
340
- return
341
- }
342
- if (!request.http2) {
343
- addRequestLog(requestNode, {
344
- type: "warn",
345
- value: `pushResponse call ignored because request is not http2`,
346
- })
347
- return
348
- }
349
- const canPushStream = testCanPushStream(http2Stream)
350
- if (!canPushStream.can) {
351
- addRequestLog(requestNode, {
352
- type: "debug",
353
- value: `Ignoring call to pushResponse because ${canPushStream.reason}`,
354
- })
355
- return
356
- }
357
-
358
335
  // being able to push a stream is nice to have
359
336
  // so when it fails it's not critical
360
337
  const onPushStreamError = (e) => {
@@ -488,19 +465,65 @@ export const startServer = async ({
488
465
  redirectRequest,
489
466
  })
490
467
  if (returnValue) {
491
- requestPlugins.push(returnValue)
468
+ requestPlugins.push({
469
+ pluginName,
470
+ ...returnValue,
471
+ })
492
472
  }
493
473
  }
494
474
  })
495
- let { aborted, error, responseProperties } = responseFromShortcircuit
496
- ? { responseProperties: responseFromShortcircuit }
475
+ const result = responseFromShortcircuit
476
+ ? { returnValue: responseFromShortcircuit }
497
477
  : await generateResponseDescription({
498
478
  requestToResponse,
499
479
  request,
500
- pushResponse: ({ path, method }) => {
480
+ pushResponse: async ({ path, method }) => {
481
+ if (typeof path !== "string" || path[0] !== "/") {
482
+ addRequestLog(requestNode, {
483
+ type: "warn",
484
+ value: `response push ignored because path is invalid (must be a string starting with "/", found ${path})`,
485
+ })
486
+ return
487
+ }
488
+ if (!request.http2) {
489
+ addRequestLog(requestNode, {
490
+ type: "warn",
491
+ value: `response push ignored because request is not http2`,
492
+ })
493
+ return
494
+ }
495
+ const canPushStream = testCanPushStream(nodeResponse.stream)
496
+ if (!canPushStream.can) {
497
+ addRequestLog(requestNode, {
498
+ type: "debug",
499
+ value: `response push ignored because ${canPushStream.reason}`,
500
+ })
501
+ return
502
+ }
503
+
504
+ let prevented = false
505
+ const prevent = () => {
506
+ prevented = true
507
+ }
508
+ const preventedByPlugin = requestPlugins.find((requestPlugin) => {
509
+ const { onPushResponse } = requestPlugin
510
+ if (!onPushResponse) {
511
+ return false
512
+ }
513
+ onPushResponse({ path, method }, { prevent })
514
+ return prevented
515
+ })
516
+ if (prevented) {
517
+ addRequestLog(requestNode, {
518
+ type: "debug",
519
+ value: `response push prevented by "${preventedByPlugin.pluginName}" plugin`,
520
+ })
521
+ return
522
+ }
523
+
501
524
  const requestChildNode = { logs: [], children: [] }
502
525
  requestNode.children.push(requestChildNode)
503
- return pushResponse(
526
+ await pushResponse(
504
527
  { path, method },
505
528
  {
506
529
  requestNode: requestChildNode,
@@ -510,65 +533,72 @@ export const startServer = async ({
510
533
  },
511
534
  })
512
535
 
536
+ const { aborted } = result
513
537
  if (aborted) {
514
538
  return ABORTED_RESPONSE_PROPERTIES
515
539
  }
516
-
517
- // internal error, create 500 response
518
- if (error) {
519
- if (
520
- // stopOnInternalError stops server only if requestToResponse generated
521
- // a non controlled error (internal error).
522
- // if requestToResponse gracefully produced a 500 response (it did not throw)
523
- // then we can assume we are still in control of what we are doing
524
- stopOnInternalError
525
- ) {
526
- // il faudrais pouvoir stop que les autres response ?
527
- stop(STOP_REASON_INTERNAL_ERROR)
528
- }
529
-
530
- const propertiesFromError = await serverInternalErrorToResponse(error, {
531
- request,
532
- sendServerInternalErrorDetails,
533
- })
534
- responseProperties = composeTwoResponses(
535
- {
536
- status: 500,
537
- statusText: statusToStatusText(500),
538
- headers: {
539
- // ensure error are not cached
540
- "cache-control": "no-store",
541
- "content-type": "text/plain",
540
+ const readResponseProperties = async () => {
541
+ const { error } = result
542
+ // internal error, create 500 response
543
+ if (error) {
544
+ if (
545
+ // stopOnInternalError stops server only if requestToResponse generated
546
+ // a non controlled error (internal error).
547
+ // if requestToResponse gracefully produced a 500 response (it did not throw)
548
+ // then we can assume we are still in control of what we are doing
549
+ stopOnInternalError
550
+ ) {
551
+ // il faudrais pouvoir stop que les autres response ?
552
+ stop(STOP_REASON_INTERNAL_ERROR)
553
+ }
554
+ const propertiesFromError = await serverInternalErrorToResponse(
555
+ error,
556
+ {
557
+ request,
558
+ sendServerInternalErrorDetails,
542
559
  },
543
- },
544
- propertiesFromError,
545
- )
546
- addRequestLog(requestNode, {
547
- type: "error",
548
- value: createDetailedMessage(
549
- `internal error while handling request`,
560
+ )
561
+ addRequestLog(requestNode, {
562
+ type: "error",
563
+ value: createDetailedMessage(
564
+ `internal error while handling request`,
565
+ {
566
+ "error stack": error.stack,
567
+ },
568
+ ),
569
+ })
570
+ return composeTwoResponses(
550
571
  {
551
- "error stack": error.stack,
572
+ status: 500,
573
+ statusText: "Internal Server Error",
574
+ headers: {
575
+ // ensure error are not cached
576
+ "cache-control": "no-store",
577
+ "content-type": "text/plain",
578
+ },
552
579
  },
553
- ),
554
- })
555
- } else {
580
+ propertiesFromError,
581
+ )
582
+ }
583
+
556
584
  const {
557
585
  status = 501,
558
- statusText = statusToStatusText(status),
586
+ statusText,
559
587
  headers = {},
560
588
  body,
561
589
  ...rest
562
- } = responseProperties || {}
563
- responseProperties = {
590
+ } = result.returnValue || {}
591
+ const responseProperties = {
564
592
  status,
565
593
  statusText,
566
594
  headers,
567
595
  body,
568
596
  ...rest,
569
597
  }
598
+ return responseProperties
570
599
  }
571
600
 
601
+ let responseProperties = await readResponseProperties()
572
602
  // call every plugin "onResponse" hook
573
603
  requestPlugins.forEach((requestPlugin) => {
574
604
  const { onResponse } = requestPlugin
@@ -582,7 +612,6 @@ export const startServer = async ({
582
612
  }
583
613
  }
584
614
  })
585
-
586
615
  if (
587
616
  request.method !== "HEAD" &&
588
617
  responseProperties.headers["content-length"] > 0 &&
@@ -593,7 +622,6 @@ export const startServer = async ({
593
622
  value: `content-length header is ${responseProperties.headers["content-length"]} but body is empty`,
594
623
  })
595
624
  }
596
-
597
625
  return responseProperties
598
626
  }
599
627
 
@@ -701,8 +729,8 @@ const createNodeServer = async ({
701
729
  protocol,
702
730
  redirectHttpToHttps,
703
731
  allowHttpRequestOnHttps,
704
- serverCertificate,
705
- serverCertificatePrivateKey,
732
+ certificate,
733
+ privateKey,
706
734
  http2,
707
735
  http1Allowed,
708
736
  }) => {
@@ -712,8 +740,8 @@ const createNodeServer = async ({
712
740
 
713
741
  if (redirectHttpToHttps || allowHttpRequestOnHttps) {
714
742
  return createPolyglotServer({
715
- serverCertificate,
716
- serverCertificatePrivateKey,
743
+ certificate,
744
+ privateKey,
717
745
  http2,
718
746
  http1Allowed,
719
747
  })
@@ -721,8 +749,8 @@ const createNodeServer = async ({
721
749
 
722
750
  const { createServer } = await import("https")
723
751
  return createServer({
724
- cert: serverCertificate,
725
- key: serverCertificatePrivateKey,
752
+ cert: certificate,
753
+ key: privateKey,
726
754
  })
727
755
  }
728
756
 
@@ -736,7 +764,7 @@ const generateResponseDescription = async ({
736
764
  pushResponse,
737
765
  })
738
766
  return {
739
- responseProperties: returnValue,
767
+ returnValue,
740
768
  }
741
769
  } catch (error) {
742
770
  if (error.name === "AbortError" && request.signal.aborted) {
@@ -749,10 +777,6 @@ const generateResponseDescription = async ({
749
777
  }
750
778
  }
751
779
  }
752
-
753
- const statusToStatusText = (status) =>
754
- http.STATUS_CODES[status] || "not specified"
755
-
756
780
  const testCanPushStream = (http2Stream) => {
757
781
  if (!http2Stream.pushAllowed) {
758
782
  return {