@jsenv/server 7.1.0 → 8.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 (44) hide show
  1. package/README.md +63 -23
  2. package/main.js +32 -17
  3. package/package.json +20 -15
  4. package/src/{internal → content_negotiation}/applyContentNegotiation.js +5 -1
  5. package/src/{internal → content_negotiation}/checkContentNegotiation.js +3 -3
  6. package/src/{negotiateContentEncoding.js → content_negotiation/negotiateContentEncoding.js} +5 -3
  7. package/src/{negotiateContentLanguage.js → content_negotiation/negotiateContentLanguage.js} +9 -5
  8. package/src/{negotiateContentType.js → content_negotiation/negotiateContentType.js} +10 -5
  9. package/src/content_negotiation/plugin_content_negotiation_check.js +15 -0
  10. package/src/cors/plugin_cors.js +156 -0
  11. package/src/fetchUrl.js +12 -7
  12. package/src/findFreePort.js +2 -1
  13. package/src/{composeResponse.js → internal/composeResponse.js} +5 -3
  14. package/src/internal/compositionMappingToCompose.js +26 -6
  15. package/src/{convertFileSystemErrorToResponseProperties.js → internal/convertFileSystemErrorToResponseProperties.js} +0 -0
  16. package/src/internal/getServerOrigins.js +5 -1
  17. package/src/internal/headersFromObject.js +3 -1
  18. package/src/{headersToObject.js → internal/headersToObject.js} +0 -0
  19. package/src/internal/listenClientError.js +5 -1
  20. package/src/internal/listenEvent.js +6 -1
  21. package/src/internal/listenRequest.js +5 -1
  22. package/src/internal/listenServerConnectionError.js +31 -23
  23. package/src/internal/multiple-header.js +11 -5
  24. package/src/internal/nodeRequestToRequest.js +10 -7
  25. package/src/internal/observable.js +5 -1
  26. package/src/internal/populateNodeResponse.js +11 -2
  27. package/src/internal/server-polyglot.js +11 -2
  28. package/src/internal/trackServerPendingConnections.js +15 -11
  29. package/src/internal/trackServerPendingRequests.js +10 -7
  30. package/src/jsenvServerInternalErrorToResponse.js +7 -3
  31. package/src/plugin_request_waiting_check.js +34 -0
  32. package/src/serveFile.js +88 -41
  33. package/src/server_timing/plugin_server_timing.js +28 -0
  34. package/src/{serverTiming.js → server_timing/timing_header.js} +43 -62
  35. package/src/server_timing/timing_measure.js +26 -0
  36. package/src/service_composition/service_composition.js +65 -0
  37. package/src/{createSSERoom.js → sse/createSSERoom.js} +22 -6
  38. package/src/startServer.js +197 -252
  39. package/src/stopReasons.js +3 -1
  40. package/src/urlToContentType.js +13 -6
  41. package/src/jsenvAccessControlAllowedHeaders.js +0 -1
  42. package/src/jsenvAccessControlAllowedMethods.js +0 -1
  43. package/src/service-composition.js +0 -41
  44. package/src/urlToSearchParamValue.js +0 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # server · [![npm package](https://img.shields.io/npm/v/@jsenv/server.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/server) [![github workflow](https://github.com/jsenv/server/workflows/main/badge.svg)](https://github.com/jsenv/server/actions?workflow=main) [![codecov coverage](https://codecov.io/gh/jsenv/server/branch/main/graph/badge.svg)](https://codecov.io/gh/jsenv/server)
1
+ # server [![npm package](https://img.shields.io/npm/v/@jsenv/server.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/server) [![github workflow](https://github.com/jsenv/server/workflows/main/badge.svg)](https://github.com/jsenv/server/actions?workflow=main) [![codecov coverage](https://codecov.io/gh/jsenv/server/branch/main/graph/badge.svg)](https://codecov.io/gh/jsenv/server)
2
2
 
3
- `@jsenv/server` api helps to write flexible server code with a declarative API.
3
+ `@jsenv/server` helps to write flexible server code with a declarative API.
4
4
 
5
5
  ```js
6
6
  import { startServer } from "@jsenv/server"
@@ -23,7 +23,7 @@ await startServer({
23
23
 
24
24
  # Example
25
25
 
26
- _Code starting a server and using [node-fetch](https://github.com/node-fetch/node-fetch) to send to request on that server:_
26
+ _Code starting a server and using [node-fetch](https://github.com/node-fetch/node-fetch) to send a request on that server:_
27
27
 
28
28
  ```js
29
29
  import fetch from "node-fetch"
@@ -46,28 +46,39 @@ const responseBodyAsText = await response.text()
46
46
  console.log(responseBodyAsText) // "Hello world"
47
47
  ```
48
48
 
49
- _Code starting a server serving files from a directory:_
49
+ _Code starting a server with 2 request handlers:_
50
50
 
51
51
  ```js
52
- import { startServer, serveFile } from "@jsenv/server"
53
-
54
- const buildDirectoryUrl = new URL("./dist/", import.meta.url)
55
- const LONG_TERM_CACHE_VALIDITY_IN_SECONDS = 60 * 60 * 24 * 30 // 30 days
52
+ /*
53
+ * starts a server which:
54
+ * - when requested at "/"
55
+ * -> respond with 200
56
+ * - otherwise
57
+ * -> respond with 404
58
+ */
59
+ import { startServer, composeServices } from "@jsenv/server"
60
+
61
+ const indexService = (request) => {
62
+ if (request.ressource === "/") {
63
+ return { status: 200 }
64
+ }
65
+ return null // means "I don't handle that request"
66
+ }
67
+
68
+ const notFoundService = (request) => {
69
+ return { status: 404 }
70
+ }
56
71
 
57
- await startServer({
58
- requestToResponse: async (request) => {
59
- const longTermCacheDisabled = request.ressource === "/index.html"
60
- const response = await serveFile(request, {
61
- rootDirectoryUrl: buildDirectoryUrl,
62
- cacheControl: longTermCacheDisabled
63
- ? `private,max-age=0,must-revalidate`
64
- : `private,max-age=${LONG_TERM_CACHE_VALIDITY_IN_SECONDS},immutable`,
65
- etagEnabled: true,
66
- compressionEnabled: true,
67
- })
68
- return response
69
- },
72
+ const server = await startServer({
73
+ requestToResponse: composeServices(indexService, notFoundService),
70
74
  })
75
+
76
+ const fetch = await import("node-fetch")
77
+ const responseForOrigin = await fetch(server.origin)
78
+ responseForOrigin.status // 200
79
+
80
+ const responseForFoo = await fetch(`${server.origin}/foo`)
81
+ responseForFoo.status // 404
71
82
  ```
72
83
 
73
84
  _Code starting a server in https:_
@@ -78,8 +89,8 @@ import { startServer } from "@jsenv/server"
78
89
 
79
90
  await startServer({
80
91
  protocol: "https",
81
- serverCertificate: String(readFileSync(new URL("./server.crt", import.meta.url)),
82
- serverCertificatePrivateKey: String(readFileSync(new URL("./server.key", import.meta.url)),
92
+ serverCertificate: readFileSyncAsString("./server.crt"),
93
+ serverCertificatePrivateKey: readFileSyncAsString("./server.key"),
83
94
  allowHttpRequestOnHttps: true,
84
95
  requestToResponse: (request) => {
85
96
  const clientUsesHttp = request.origin.startsWith("http:")
@@ -93,6 +104,35 @@ await startServer({
93
104
  }
94
105
  },
95
106
  })
107
+
108
+ function readFileSyncAsString(relativeUrl) {
109
+ const fileUrl = new URL(relativeUrl, import.meta.url)
110
+ return String(readFileSync(fileUrl))
111
+ }
112
+ ```
113
+
114
+ _Code starting a server serving files from a directory:_
115
+
116
+ ```js
117
+ import { startServer, serveFile } from "@jsenv/server"
118
+
119
+ const distDirectoryUrl = new URL("./dist/", import.meta.url)
120
+ const LONG_TERM_CACHE_VALIDITY_IN_SECONDS = 60 * 60 * 24 * 30 // 30 days
121
+
122
+ await startServer({
123
+ requestToResponse: async (request) => {
124
+ const longTermCacheDisabled = request.ressource === "/index.html"
125
+ const response = await serveFile(request, {
126
+ rootDirectoryUrl: distDirectoryUrl,
127
+ cacheControl: longTermCacheDisabled
128
+ ? `private,max-age=0,must-revalidate`
129
+ : `private,max-age=${LONG_TERM_CACHE_VALIDITY_IN_SECONDS},immutable`,
130
+ etagEnabled: true,
131
+ compressionEnabled: true,
132
+ })
133
+ return response
134
+ },
135
+ })
96
136
  ```
97
137
 
98
138
  # Documentation
package/main.js CHANGED
@@ -3,27 +3,16 @@
3
3
  * Its responsability is to export what is documented
4
4
  * Ideally this file should be kept simple to help discovering codebase progressively.
5
5
  */
6
- /* eslint-disable import/max-dependencies */
7
6
 
8
- export { composeResponse } from "./src/composeResponse.js"
9
- export { convertFileSystemErrorToResponseProperties } from "./src/convertFileSystemErrorToResponseProperties.js"
10
- export { createSSERoom } from "./src/createSSERoom.js"
11
- export { fetchUrl } from "./src/fetchUrl.js"
12
- export { findFreePort } from "./src/findFreePort.js"
13
- export { headersToObject } from "./src/headersToObject.js"
14
- export { composeServices, composeServicesWithTiming } from "./src/service-composition.js"
15
- export { jsenvAccessControlAllowedHeaders } from "./src/jsenvAccessControlAllowedHeaders.js"
16
- export { jsenvAccessControlAllowedMethods } from "./src/jsenvAccessControlAllowedMethods.js"
7
+ export { startServer } from "./src/startServer.js"
8
+ export {
9
+ composeServices,
10
+ composeServicesWithTiming,
11
+ nextService,
12
+ } from "./src/service_composition/service_composition.js"
17
13
  export { jsenvServerInternalErrorToResponse } from "./src/jsenvServerInternalErrorToResponse.js"
18
- export { negotiateContentEncoding } from "./src/negotiateContentEncoding.js"
19
- export { negotiateContentLanguage } from "./src/negotiateContentLanguage.js"
20
- export { negotiateContentType } from "./src/negotiateContentType.js"
21
- export { urlToContentType } from "./src/urlToContentType.js"
22
- export { urlToSearchParamValue } from "./src/urlToSearchParamValue.js"
23
14
  export { readRequestBody } from "./src/readRequestBody.js"
24
15
  export { serveFile } from "./src/serveFile.js"
25
- export { timeFunction, timeStart } from "./src/serverTiming.js"
26
- export { startServer } from "./src/startServer.js"
27
16
  export {
28
17
  STOP_REASON_INTERNAL_ERROR,
29
18
  STOP_REASON_PROCESS_SIGHUP,
@@ -33,3 +22,29 @@ export {
33
22
  STOP_REASON_PROCESS_EXIT,
34
23
  STOP_REASON_NOT_SPECIFIED,
35
24
  } from "./src/stopReasons.js"
25
+
26
+ // CORS
27
+ export {
28
+ pluginCORS,
29
+ jsenvAccessControlAllowedHeaders,
30
+ jsenvAccessControlAllowedMethods,
31
+ } from "./src/cors/plugin_cors.js"
32
+
33
+ // server timings
34
+ export { pluginServerTiming } from "./src/server_timing/plugin_server_timing.js"
35
+ export { timeFunction, timeStart } from "./src/server_timing/timing_measure.js"
36
+
37
+ // SSE
38
+ export { createSSERoom } from "./src/sse/createSSERoom.js"
39
+
40
+ // content-negotiation
41
+ export { negotiateContentType } from "./src/content_negotiation/negotiateContentType.js"
42
+ export { negotiateContentEncoding } from "./src/content_negotiation/negotiateContentEncoding.js"
43
+ export { negotiateContentLanguage } from "./src/content_negotiation/negotiateContentLanguage.js"
44
+ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_content_negotiation_check.js"
45
+
46
+ // others
47
+ export { pluginRequestWaitingCheck } from "./src/plugin_request_waiting_check.js"
48
+ export { findFreePort } from "./src/findFreePort.js"
49
+ export { fetchUrl } from "./src/fetchUrl.js"
50
+ export { urlToContentType } from "./src/urlToContentType.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "7.1.0",
3
+ "version": "8.0.0",
4
4
  "description": "Write your Node.js server using pure functions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -33,41 +33,46 @@
33
33
  "scripts": {
34
34
  "eslint-check": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
35
35
  "generate-importmap": "node ./script/importmap/generate_importmap.mjs",
36
- "measure-performances": "node ./script/performance/generate_performance_report.mjs --local",
37
- "test": "node ./script/test/test.mjs",
36
+ "measure-performances": "node --expose-gc ./script/performance/generate_performance_report.mjs --log",
37
+ "test": "node --unhandled-rejections=strict ./script/test/test.mjs",
38
38
  "test-with-coverage": "npm run test -- --coverage",
39
+ "generate-html-snapshots": "node ./test/internal_error/generate_html_snapshot_files.mjs",
39
40
  "prettier-format": "node ./script/prettier/prettier_format.mjs",
40
41
  "prettier-format-stage": "npm run prettier-format -- --staged",
41
42
  "prettier-check": "npm run prettier-format -- --dry-run",
42
- "install-certificate-authority": "node ./script/certificate/install-certificate-authority.mjs",
43
+ "certificate-authority-install": "node ./script/certificate/certificate_authority_install.mjs",
44
+ "playwright-install": "npx playwright install-deps && npx playwright install",
43
45
  "prepublishOnly": "node ./script/publish/remove_postinstall.mjs",
44
46
  "postpublish": "node ./script/publish/restore_postinstall.mjs"
45
47
  },
46
48
  "dependencies": {
47
49
  "@jsenv/cancellation": "3.0.0",
48
- "@jsenv/filesystem": "2.1.1",
50
+ "@jsenv/filesystem": "2.3.1",
49
51
  "@jsenv/logger": "4.0.1",
50
52
  "@jsenv/node-signals": "2.0.1",
51
53
  "abort-controller": "3.0.0",
52
54
  "node-fetch": "3.0.0"
53
55
  },
54
56
  "devDependencies": {
55
- "@babel/core": "7.15.5",
56
- "@babel/eslint-parser": "7.15.4",
57
- "@jsenv/assert": "2.3.1",
57
+ "@babel/core": "7.15.8",
58
+ "@babel/eslint-parser": "7.15.8",
59
+ "@jsenv/assert": "2.3.2",
58
60
  "@jsenv/codecov-upload": "3.5.0",
59
- "@jsenv/core": "19.6.2",
60
- "@jsenv/eslint-config": "16.0.1",
61
+ "@jsenv/core": "23.1.3",
62
+ "@jsenv/eslint-config": "16.0.8",
61
63
  "@jsenv/github-release-package": "1.2.3",
62
64
  "@jsenv/https-local": "1.0.2",
63
- "@jsenv/importmap-eslint-resolver": "5.1.0",
64
- "@jsenv/importmap-node-module": "2.2.0",
65
+ "@jsenv/importmap-eslint-resolver": "5.1.2",
66
+ "@jsenv/importmap-node-module": "2.4.1",
65
67
  "@jsenv/package-publish": "1.6.2",
66
- "@jsenv/performance-impact": "1.7.0",
68
+ "@jsenv/performance-impact": "2.2.0",
67
69
  "@jsenv/prettier-check-project": "5.6.1",
68
70
  "eslint": "7.32.0",
69
- "eslint-plugin-import": "2.24.2",
71
+ "eslint-plugin-import": "2.25.2",
70
72
  "eventsource": "1.1.0",
71
- "prettier": "2.3.2"
73
+ "fastify": "3.22.1",
74
+ "fastify-static": "4.4.2",
75
+ "playwright": "1.16.1",
76
+ "prettier": "2.4.1"
72
77
  }
73
78
  }
@@ -1,4 +1,8 @@
1
- export const applyContentNegotiation = ({ availables, accepteds, getAcceptanceScore }) => {
1
+ export const applyContentNegotiation = ({
2
+ availables,
3
+ accepteds,
4
+ getAcceptanceScore,
5
+ }) => {
2
6
  let highestScore = -1
3
7
  let availableWithHighestScore = null
4
8
 
@@ -1,6 +1,6 @@
1
- import { negotiateContentType } from "../negotiateContentType.js"
2
- import { negotiateContentLanguage } from "../negotiateContentLanguage.js"
3
- import { negotiateContentEncoding } from "../negotiateContentEncoding.js"
1
+ import { negotiateContentType } from "./negotiateContentType.js"
2
+ import { negotiateContentLanguage } from "./negotiateContentLanguage.js"
3
+ import { negotiateContentEncoding } from "./negotiateContentEncoding.js"
4
4
 
5
5
  export const checkContentNegotiation = (request, response, { warn }) => {
6
6
  const requestAcceptHeader = request.headers.accept
@@ -1,5 +1,5 @@
1
- import { applyContentNegotiation } from "./internal/applyContentNegotiation.js"
2
- import { parseMultipleHeader } from "./internal/multiple-header.js"
1
+ import { parseMultipleHeader } from "../internal/multiple-header.js"
2
+ import { applyContentNegotiation } from "./applyContentNegotiation.js"
3
3
 
4
4
  export const negotiateContentEncoding = (request, availableEncodings) => {
5
5
  const { headers = {} } = request
@@ -8,7 +8,9 @@ export const negotiateContentEncoding = (request, availableEncodings) => {
8
8
  return null
9
9
  }
10
10
 
11
- const encodingsAccepted = parseAcceptEncodingHeader(requestAcceptEncodingHeader)
11
+ const encodingsAccepted = parseAcceptEncodingHeader(
12
+ requestAcceptEncodingHeader,
13
+ )
12
14
  return applyContentNegotiation({
13
15
  accepteds: encodingsAccepted,
14
16
  availables: availableEncodings,
@@ -1,5 +1,5 @@
1
- import { applyContentNegotiation } from "./internal/applyContentNegotiation.js"
2
- import { parseMultipleHeader } from "./internal/multiple-header.js"
1
+ import { parseMultipleHeader } from "../internal/multiple-header.js"
2
+ import { applyContentNegotiation } from "./applyContentNegotiation.js"
3
3
 
4
4
  export const negotiateContentLanguage = (request, availableLanguages) => {
5
5
  const { headers = {} } = request
@@ -8,7 +8,9 @@ export const negotiateContentLanguage = (request, availableLanguages) => {
8
8
  return null
9
9
  }
10
10
 
11
- const languagesAccepted = parseAcceptLanguageHeader(requestAcceptLanguageHeader)
11
+ const languagesAccepted = parseAcceptLanguageHeader(
12
+ requestAcceptLanguageHeader,
13
+ )
12
14
  return applyContentNegotiation({
13
15
  accepteds: languagesAccepted,
14
16
  availables: availableLanguages,
@@ -41,10 +43,12 @@ const parseAcceptLanguageHeader = (acceptLanguageHeaderString) => {
41
43
 
42
44
  const getLanguageAcceptanceScore = ({ value, quality }, availableLanguage) => {
43
45
  const [acceptedPrimary, acceptedVariant] = decomposeLanguage(value)
44
- const [availablePrimary, availableVariant] = decomposeLanguage(availableLanguage)
46
+ const [availablePrimary, availableVariant] =
47
+ decomposeLanguage(availableLanguage)
45
48
 
46
49
  const primaryAccepted =
47
- acceptedPrimary === "*" || acceptedPrimary.toLowerCase() === availablePrimary.toLowerCase()
50
+ acceptedPrimary === "*" ||
51
+ acceptedPrimary.toLowerCase() === availablePrimary.toLowerCase()
48
52
  const variantAccepted =
49
53
  acceptedVariant === "*" || compareVariant(acceptedVariant, availableVariant)
50
54
 
@@ -1,5 +1,5 @@
1
- import { applyContentNegotiation } from "./internal/applyContentNegotiation.js"
2
- import { parseMultipleHeader } from "./internal/multiple-header.js"
1
+ import { parseMultipleHeader } from "../internal/multiple-header.js"
2
+ import { applyContentNegotiation } from "./applyContentNegotiation.js"
3
3
 
4
4
  export const negotiateContentType = (request, availableContentTypes) => {
5
5
  const { headers = {} } = request
@@ -39,12 +39,17 @@ const parseAcceptHeader = (acceptHeader) => {
39
39
  return accepts
40
40
  }
41
41
 
42
- const getContentTypeAcceptanceScore = ({ value, quality }, availableContentType) => {
42
+ const getContentTypeAcceptanceScore = (
43
+ { value, quality },
44
+ availableContentType,
45
+ ) => {
43
46
  const [acceptedType, acceptedSubtype] = decomposeContentType(value)
44
- const [availableType, availableSubtype] = decomposeContentType(availableContentType)
47
+ const [availableType, availableSubtype] =
48
+ decomposeContentType(availableContentType)
45
49
 
46
50
  const typeAccepted = acceptedType === "*" || acceptedType === availableType
47
- const subtypeAccepted = acceptedSubtype === "*" || acceptedSubtype === availableSubtype
51
+ const subtypeAccepted =
52
+ acceptedSubtype === "*" || acceptedSubtype === availableSubtype
48
53
 
49
54
  if (typeAccepted && subtypeAccepted) {
50
55
  return quality
@@ -0,0 +1,15 @@
1
+ import { checkContentNegotiation } from "./checkContentNegotiation.js"
2
+
3
+ export const pluginContentNegotiationCheck = () => {
4
+ return {
5
+ content_negotiation_check: {
6
+ onRequest: (request, { warn }) => {
7
+ return {
8
+ onResponse: (response) => {
9
+ checkContentNegotiation(request, response, { warn })
10
+ },
11
+ }
12
+ },
13
+ },
14
+ }
15
+ }
@@ -0,0 +1,156 @@
1
+ import { urlToOrigin } from "@jsenv/filesystem"
2
+
3
+ export const jsenvAccessControlAllowedHeaders = ["x-requested-with"]
4
+
5
+ export const jsenvAccessControlAllowedMethods = [
6
+ "GET",
7
+ "POST",
8
+ "PUT",
9
+ "DELETE",
10
+ "OPTIONS",
11
+ ]
12
+
13
+ export const pluginCORS = ({
14
+ accessControlAllowedOrigins = [],
15
+ accessControlAllowedMethods = jsenvAccessControlAllowedMethods,
16
+ accessControlAllowedHeaders = jsenvAccessControlAllowedHeaders,
17
+ accessControlAllowRequestOrigin = false,
18
+ accessControlAllowRequestMethod = false,
19
+ accessControlAllowRequestHeaders = false,
20
+ accessControlAllowCredentials = false,
21
+ // by default OPTIONS request can be cache for a long time, it's not going to change soon ?
22
+ // we could put a lot here, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
23
+ accessControlMaxAge = 600,
24
+ timingAllowOrigin,
25
+ } = {}) => {
26
+ // TODO: we should check access control params to throw or warn if we find strange values
27
+
28
+ const corsEnabled =
29
+ accessControlAllowRequestOrigin || accessControlAllowedOrigins.length
30
+
31
+ if (!corsEnabled) {
32
+ return {
33
+ cors: {},
34
+ }
35
+ }
36
+
37
+ return {
38
+ cors: {
39
+ onServerParams: ({ plugins }) => {
40
+ if (timingAllowOrigin === undefined && plugins.server_timings) {
41
+ timingAllowOrigin = true
42
+ }
43
+ },
44
+
45
+ onRequest: (request, { shortcircuitResponse }) => {
46
+ if (request.method === "OPTIONS") {
47
+ // when request method is "OPTIONS" we must return a 200 without body
48
+ // So we bypass "requestToResponse" in that scenario using shortcircuitResponse
49
+ shortcircuitResponse({
50
+ status: 200,
51
+ headers: {
52
+ "content-length": 0,
53
+ },
54
+ })
55
+ }
56
+
57
+ return {
58
+ onResponse: () => {
59
+ const accessControlHeaders = generateAccessControlHeaders({
60
+ request,
61
+ accessControlAllowedOrigins,
62
+ accessControlAllowRequestOrigin,
63
+ accessControlAllowedMethods,
64
+ accessControlAllowRequestMethod,
65
+ accessControlAllowedHeaders,
66
+ accessControlAllowRequestHeaders,
67
+ accessControlAllowCredentials,
68
+ accessControlMaxAge,
69
+ timingAllowOrigin,
70
+ })
71
+
72
+ return {
73
+ headers: accessControlHeaders,
74
+ }
75
+ },
76
+ }
77
+ },
78
+ },
79
+ }
80
+ }
81
+
82
+ // https://www.w3.org/TR/cors/
83
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
84
+ const generateAccessControlHeaders = ({
85
+ request: { headers },
86
+ accessControlAllowedOrigins,
87
+ accessControlAllowRequestOrigin,
88
+ accessControlAllowedMethods,
89
+ accessControlAllowRequestMethod,
90
+ accessControlAllowedHeaders,
91
+ accessControlAllowRequestHeaders,
92
+ accessControlAllowCredentials,
93
+ // by default OPTIONS request can be cache for a long time, it's not going to change soon ?
94
+ // we could put a lot here, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
95
+ accessControlMaxAge = 600,
96
+ timingAllowOrigin,
97
+ } = {}) => {
98
+ const vary = []
99
+
100
+ const allowedOriginArray = [...accessControlAllowedOrigins]
101
+ if (accessControlAllowRequestOrigin) {
102
+ if ("origin" in headers && headers.origin !== "null") {
103
+ allowedOriginArray.push(headers.origin)
104
+ vary.push("origin")
105
+ } else if ("referer" in headers) {
106
+ allowedOriginArray.push(urlToOrigin(headers.referer))
107
+ vary.push("referer")
108
+ } else {
109
+ allowedOriginArray.push("*")
110
+ }
111
+ }
112
+
113
+ const allowedMethodArray = [...accessControlAllowedMethods]
114
+ if (
115
+ accessControlAllowRequestMethod &&
116
+ "access-control-request-method" in headers
117
+ ) {
118
+ const requestMethodName = headers["access-control-request-method"]
119
+ if (!allowedMethodArray.includes(requestMethodName)) {
120
+ allowedMethodArray.push(requestMethodName)
121
+ vary.push("access-control-request-method")
122
+ }
123
+ }
124
+
125
+ const allowedHeaderArray = [...accessControlAllowedHeaders]
126
+ if (
127
+ accessControlAllowRequestHeaders &&
128
+ "access-control-request-headers" in headers
129
+ ) {
130
+ const requestHeaderNameArray =
131
+ headers["access-control-request-headers"].split(", ")
132
+ requestHeaderNameArray.forEach((headerName) => {
133
+ const headerNameLowerCase = headerName.toLowerCase()
134
+ if (!allowedHeaderArray.includes(headerNameLowerCase)) {
135
+ allowedHeaderArray.push(headerNameLowerCase)
136
+ if (!vary.includes("access-control-request-headers")) {
137
+ vary.push("access-control-request-headers")
138
+ }
139
+ }
140
+ })
141
+ }
142
+
143
+ return {
144
+ "access-control-allow-origin": allowedOriginArray.join(", "),
145
+ "access-control-allow-methods": allowedMethodArray.join(", "),
146
+ "access-control-allow-headers": allowedHeaderArray.join(", "),
147
+ ...(accessControlAllowCredentials
148
+ ? { "access-control-allow-credentials": true }
149
+ : {}),
150
+ "access-control-max-age": accessControlMaxAge,
151
+ ...(timingAllowOrigin
152
+ ? { "timing-allow-origin": allowedOriginArray.join(", ") }
153
+ : {}),
154
+ ...(vary.length ? { vary: vary.join(", ") } : {}),
155
+ }
156
+ }
package/src/fetchUrl.js CHANGED
@@ -25,7 +25,9 @@ export const fetchUrl = async (
25
25
  try {
26
26
  url = String(new URL(url))
27
27
  } catch (e) {
28
- throw new Error(`fetchUrl first argument must be an absolute url, received ${url}`)
28
+ throw new Error(
29
+ `fetchUrl first argument must be an absolute url, received ${url}`,
30
+ )
29
31
  }
30
32
 
31
33
  if (url.startsWith("file://")) {
@@ -48,12 +50,15 @@ export const fetchUrl = async (
48
50
  contentTypeMap,
49
51
  ...options,
50
52
  })
51
- const response = new Response(typeof body === "string" ? Buffer.from(body) : body, {
52
- url,
53
- status,
54
- statusText,
55
- headers,
56
- })
53
+ const response = new Response(
54
+ typeof body === "string" ? Buffer.from(body) : body,
55
+ {
56
+ url,
57
+ status,
58
+ statusText,
59
+ headers,
60
+ },
61
+ )
57
62
  return response
58
63
  }
59
64
 
@@ -1,5 +1,6 @@
1
- import { createServer } from "net"
1
+ import { createServer } from "node:net"
2
2
  import { createCancellationToken } from "@jsenv/cancellation"
3
+
3
4
  import { listen } from "./internal/listen.js"
4
5
 
5
6
  export const findFreePort = async (
@@ -1,5 +1,5 @@
1
- import { compositionMappingToComposeStrict } from "./internal/compositionMappingToComposeStrict.js"
2
- import { composeResponseHeaders } from "./internal/composeResponseHeaders.js"
1
+ import { compositionMappingToComposeStrict } from "./compositionMappingToComposeStrict.js"
2
+ import { composeResponseHeaders } from "./composeResponseHeaders.js"
3
3
 
4
4
  const responseCompositionMapping = {
5
5
  status: (prevStatus, status) => status,
@@ -12,4 +12,6 @@ const responseCompositionMapping = {
12
12
  },
13
13
  }
14
14
 
15
- export const composeResponse = compositionMappingToComposeStrict(responseCompositionMapping)
15
+ export const composeResponse = compositionMappingToComposeStrict(
16
+ responseCompositionMapping,
17
+ )
@@ -6,7 +6,9 @@ export const compositionMappingToCompose = (
6
6
  },
7
7
  { caseSensitive = true } = {},
8
8
  ) => {
9
- const reducer = compositionMappingToReducer(compositionMapping, { caseSensitive })
9
+ const reducer = compositionMappingToReducer(compositionMapping, {
10
+ caseSensitive,
11
+ })
10
12
  return (...objects) => objects.reduce(reducer, createInitial())
11
13
  }
12
14
 
@@ -25,7 +27,12 @@ const compositionMappingToReducer = (compositionMapping, { caseSensitive }) => {
25
27
  const composedKey = caseSensitive ? key : key.toLowerCase()
26
28
  composed[composedKey] = caseSensitive
27
29
  ? composeProperty(key, previous, current, compositionMapping)
28
- : composePropertyCaseInsensitive(key, previous, current, compositionMapping)
30
+ : composePropertyCaseInsensitive(
31
+ key,
32
+ previous,
33
+ current,
34
+ compositionMapping,
35
+ )
29
36
  })
30
37
  return composed
31
38
  }
@@ -42,16 +49,26 @@ const composeProperty = (key, previous, current, compositionMapping) => {
42
49
  return current[key]
43
50
  }
44
51
 
45
- const keyExistingInComposer = keyExistsIn(key, compositionMapping) ? key : null
52
+ const keyExistingInComposer = keyExistsIn(key, compositionMapping)
53
+ ? key
54
+ : null
46
55
  if (!keyExistingInComposer) {
47
56
  return current[key]
48
57
  }
49
58
 
50
59
  const composerForProperty = compositionMapping[keyExistingInComposer]
51
- return composerForProperty(previous[keyExistingInPrevious], current[keyExistingInCurrent])
60
+ return composerForProperty(
61
+ previous[keyExistingInPrevious],
62
+ current[keyExistingInCurrent],
63
+ )
52
64
  }
53
65
 
54
- const composePropertyCaseInsensitive = (key, previous, current, compositionMapping) => {
66
+ const composePropertyCaseInsensitive = (
67
+ key,
68
+ previous,
69
+ current,
70
+ compositionMapping,
71
+ ) => {
55
72
  const keyLowercased = key.toLowerCase()
56
73
  const keyExistingInCurrent = keyExistsIn(key, current)
57
74
  ? key
@@ -80,7 +97,10 @@ const composePropertyCaseInsensitive = (key, previous, current, compositionMappi
80
97
  }
81
98
 
82
99
  const composerForProperty = compositionMapping[keyExistingInComposer]
83
- return composerForProperty(previous[keyExistingInPrevious], current[keyExistingInCurrent])
100
+ return composerForProperty(
101
+ previous[keyExistingInPrevious],
102
+ current[keyExistingInCurrent],
103
+ )
84
104
  }
85
105
 
86
106
  const keyExistsIn = (key, object) => {