@jsenv/server 12.2.0 → 12.3.2

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/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.2.0",
3
+ "version": "12.3.2",
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,43 +31,40 @@
31
31
  "/main.js"
32
32
  ],
33
33
  "scripts": {
34
- "importmap-generate": "node ./script/importmap/generate_importmap.mjs",
35
- "measure-performances": "node --expose-gc ./script/performance/generate_performance_report.mjs --log",
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",
36
37
  "test": "node ./script/test/test.mjs",
37
38
  "test-with-coverage": "npm run test -- --coverage",
38
39
  "generate-html-snapshots": "node ./test/internal_error/generate_html_snapshot_files.mjs",
39
- "eslint-check": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
40
- "prettier-check": "prettier --check .",
41
- "prettier-write": "prettier --write .",
42
- "certificate-authority-install": "node ./script/certificate/certificate_authority_install.mjs",
43
- "playwright-install": "npx playwright install-deps && npx playwright install",
44
- "prepublishOnly": "node ./script/publish/remove_postinstall.mjs",
45
- "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"
46
43
  },
47
44
  "dependencies": {
48
- "@jsenv/abort": "4.1.1",
45
+ "@jsenv/abort": "4.1.2",
49
46
  "@jsenv/logger": "4.0.1",
50
- "@jsenv/url-meta": "6.0.1",
47
+ "@jsenv/url-meta": "6.0.3",
51
48
  "node-fetch": "2.6.2",
52
49
  "path-to-regexp": "6.2.0"
53
50
  },
54
51
  "devDependencies": {
55
- "@jsenv/assert": "2.3.2",
56
- "@jsenv/core": "23.10.0",
52
+ "@jsenv/assert": "2.4.1",
53
+ "@jsenv/core": "25.1.0",
57
54
  "@jsenv/eslint-config": "16.0.9",
58
- "@jsenv/filesystem": "2.5.1",
55
+ "@jsenv/filesystem": "2.6.1",
59
56
  "@jsenv/github-release-package": "1.2.3",
60
57
  "@jsenv/https-local": "1.0.3",
61
- "@jsenv/importmap-eslint-resolver": "5.1.2",
62
- "@jsenv/importmap-node-module": "2.7.1",
58
+ "@jsenv/importmap-eslint-resolver": "5.2.2",
59
+ "@jsenv/importmap-node-module": "5.1.0",
63
60
  "@jsenv/package-publish": "1.6.2",
64
61
  "@jsenv/performance-impact": "2.2.1",
65
- "eslint": "8.2.0",
66
- "eslint-plugin-import": "2.25.3",
62
+ "eslint": "8.7.0",
63
+ "eslint-plugin-import": "2.25.4",
67
64
  "eventsource": "1.1.0",
68
- "fastify": "3.24.0",
65
+ "fastify": "3.25.3",
69
66
  "fastify-static": "4.5.0",
70
- "playwright": "1.16.3",
71
- "prettier": "2.4.1"
67
+ "playwright": "1.17.2",
68
+ "prettier": "2.5.1"
72
69
  }
73
- }
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
 
@@ -22,8 +23,8 @@ export const populateNodeResponse = async (
22
23
  status,
23
24
  statusText,
24
25
  headers,
26
+ onHeadersSent,
25
27
  })
26
- onHeadersSent()
27
28
 
28
29
  if (!body) {
29
30
  onEnd()
@@ -134,41 +135,48 @@ export const populateNodeResponse = async (
134
135
  })
135
136
  }
136
137
 
137
- const writeHead = (responseStream, { status, statusText, headers }) => {
138
+ const writeHead = (
139
+ responseStream,
140
+ { status, statusText, headers, onHeadersSent },
141
+ ) => {
138
142
  const responseIsHttp2ServerResponse =
139
143
  responseStream instanceof Http2ServerResponse
140
144
  const responseIsServerHttp2Stream =
141
145
  responseStream.constructor.name === "ServerHttp2Stream"
142
-
143
146
  let nodeHeaders = headersToNodeHeaders(headers, {
144
147
  // https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L112
145
148
  ignoreConnectionHeader:
146
149
  responseIsHttp2ServerResponse || responseIsServerHttp2Stream,
147
150
  })
148
-
151
+ if (statusText === undefined) {
152
+ statusText = statusTextFromStatus(status)
153
+ }
149
154
  if (responseIsServerHttp2Stream) {
150
155
  nodeHeaders = {
151
156
  ...nodeHeaders,
152
157
  ":status": status,
153
158
  }
154
159
  responseStream.respond(nodeHeaders)
160
+ onHeadersSent({ nodeHeaders, status, statusText })
155
161
  return
156
162
  }
157
-
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
  ) {
165
169
  responseStream.writeHead(status, nodeHeaders)
170
+ onHeadersSent({ nodeHeaders, status, statusText })
166
171
  return
167
172
  }
168
-
169
173
  responseStream.writeHead(status, statusText, nodeHeaders)
174
+ onHeadersSent({ nodeHeaders, status, statusText })
170
175
  }
171
176
 
177
+ const statusTextFromStatus = (status) =>
178
+ http.STATUS_CODES[status] || "not specified"
179
+
172
180
  const headersToNodeHeaders = (headers, { ignoreConnectionHeader }) => {
173
181
  const nodeHeaders = {}
174
182
 
@@ -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,
@@ -472,8 +472,8 @@ export const startServer = async ({
472
472
  }
473
473
  }
474
474
  })
475
- let { aborted, error, responseProperties } = responseFromShortcircuit
476
- ? { responseProperties: responseFromShortcircuit }
475
+ const result = responseFromShortcircuit
476
+ ? { returnValue: responseFromShortcircuit }
477
477
  : await generateResponseDescription({
478
478
  requestToResponse,
479
479
  request,
@@ -533,65 +533,72 @@ export const startServer = async ({
533
533
  },
534
534
  })
535
535
 
536
+ const { aborted } = result
536
537
  if (aborted) {
537
538
  return ABORTED_RESPONSE_PROPERTIES
538
539
  }
539
-
540
- // internal error, create 500 response
541
- if (error) {
542
- if (
543
- // stopOnInternalError stops server only if requestToResponse generated
544
- // a non controlled error (internal error).
545
- // if requestToResponse gracefully produced a 500 response (it did not throw)
546
- // then we can assume we are still in control of what we are doing
547
- stopOnInternalError
548
- ) {
549
- // il faudrais pouvoir stop que les autres response ?
550
- stop(STOP_REASON_INTERNAL_ERROR)
551
- }
552
-
553
- const propertiesFromError = await serverInternalErrorToResponse(error, {
554
- request,
555
- sendServerInternalErrorDetails,
556
- })
557
- responseProperties = composeTwoResponses(
558
- {
559
- status: 500,
560
- statusText: statusToStatusText(500),
561
- headers: {
562
- // ensure error are not cached
563
- "cache-control": "no-store",
564
- "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,
565
559
  },
566
- },
567
- propertiesFromError,
568
- )
569
- addRequestLog(requestNode, {
570
- type: "error",
571
- value: createDetailedMessage(
572
- `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(
573
571
  {
574
- "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
+ },
575
579
  },
576
- ),
577
- })
578
- } else {
580
+ propertiesFromError,
581
+ )
582
+ }
583
+
579
584
  const {
580
585
  status = 501,
581
- statusText = statusToStatusText(status),
586
+ statusText,
582
587
  headers = {},
583
588
  body,
584
589
  ...rest
585
- } = responseProperties || {}
586
- responseProperties = {
590
+ } = result.returnValue || {}
591
+ const responseProperties = {
587
592
  status,
588
593
  statusText,
589
594
  headers,
590
595
  body,
591
596
  ...rest,
592
597
  }
598
+ return responseProperties
593
599
  }
594
600
 
601
+ let responseProperties = await readResponseProperties()
595
602
  // call every plugin "onResponse" hook
596
603
  requestPlugins.forEach((requestPlugin) => {
597
604
  const { onResponse } = requestPlugin
@@ -605,7 +612,6 @@ export const startServer = async ({
605
612
  }
606
613
  }
607
614
  })
608
-
609
615
  if (
610
616
  request.method !== "HEAD" &&
611
617
  responseProperties.headers["content-length"] > 0 &&
@@ -616,7 +622,6 @@ export const startServer = async ({
616
622
  value: `content-length header is ${responseProperties.headers["content-length"]} but body is empty`,
617
623
  })
618
624
  }
619
-
620
625
  return responseProperties
621
626
  }
622
627
 
@@ -659,12 +664,10 @@ export const startServer = async ({
659
664
  })
660
665
  onRequestHandled(requestNode)
661
666
  },
662
- onHeadersSent: () => {
667
+ onHeadersSent: ({ status, statusText }) => {
663
668
  addRequestLog(requestNode, {
664
669
  type: "info",
665
- value: `${colorizeResponseStatus(responseProperties.status)} ${
666
- responseProperties.statusText
667
- }`,
670
+ value: `${colorizeResponseStatus(status)} ${statusText}`,
668
671
  })
669
672
  },
670
673
  onEnd: () => {
@@ -759,7 +762,7 @@ const generateResponseDescription = async ({
759
762
  pushResponse,
760
763
  })
761
764
  return {
762
- responseProperties: returnValue,
765
+ returnValue,
763
766
  }
764
767
  } catch (error) {
765
768
  if (error.name === "AbortError" && request.signal.aborted) {
@@ -772,10 +775,6 @@ const generateResponseDescription = async ({
772
775
  }
773
776
  }
774
777
  }
775
-
776
- const statusToStatusText = (status) =>
777
- http.STATUS_CODES[status] || "not specified"
778
-
779
778
  const testCanPushStream = (http2Stream) => {
780
779
  if (!http2Stream.pushAllowed) {
781
780
  return {