@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.
- package/README.md +63 -23
- package/main.js +32 -17
- package/package.json +20 -15
- package/src/{internal → content_negotiation}/applyContentNegotiation.js +5 -1
- package/src/{internal → content_negotiation}/checkContentNegotiation.js +3 -3
- package/src/{negotiateContentEncoding.js → content_negotiation/negotiateContentEncoding.js} +5 -3
- package/src/{negotiateContentLanguage.js → content_negotiation/negotiateContentLanguage.js} +9 -5
- package/src/{negotiateContentType.js → content_negotiation/negotiateContentType.js} +10 -5
- package/src/content_negotiation/plugin_content_negotiation_check.js +15 -0
- package/src/cors/plugin_cors.js +156 -0
- package/src/fetchUrl.js +12 -7
- package/src/findFreePort.js +2 -1
- package/src/{composeResponse.js → internal/composeResponse.js} +5 -3
- package/src/internal/compositionMappingToCompose.js +26 -6
- package/src/{convertFileSystemErrorToResponseProperties.js → internal/convertFileSystemErrorToResponseProperties.js} +0 -0
- package/src/internal/getServerOrigins.js +5 -1
- package/src/internal/headersFromObject.js +3 -1
- package/src/{headersToObject.js → internal/headersToObject.js} +0 -0
- package/src/internal/listenClientError.js +5 -1
- package/src/internal/listenEvent.js +6 -1
- package/src/internal/listenRequest.js +5 -1
- package/src/internal/listenServerConnectionError.js +31 -23
- package/src/internal/multiple-header.js +11 -5
- package/src/internal/nodeRequestToRequest.js +10 -7
- package/src/internal/observable.js +5 -1
- package/src/internal/populateNodeResponse.js +11 -2
- package/src/internal/server-polyglot.js +11 -2
- package/src/internal/trackServerPendingConnections.js +15 -11
- package/src/internal/trackServerPendingRequests.js +10 -7
- package/src/jsenvServerInternalErrorToResponse.js +7 -3
- package/src/plugin_request_waiting_check.js +34 -0
- package/src/serveFile.js +88 -41
- package/src/server_timing/plugin_server_timing.js +28 -0
- package/src/{serverTiming.js → server_timing/timing_header.js} +43 -62
- package/src/server_timing/timing_measure.js +26 -0
- package/src/service_composition/service_composition.js +65 -0
- package/src/{createSSERoom.js → sse/createSSERoom.js} +22 -6
- package/src/startServer.js +197 -252
- package/src/stopReasons.js +3 -1
- package/src/urlToContentType.js +13 -6
- package/src/jsenvAccessControlAllowedHeaders.js +0 -1
- package/src/jsenvAccessControlAllowedMethods.js +0 -1
- package/src/service-composition.js +0 -41
- package/src/urlToSearchParamValue.js +0 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# server
|
|
1
|
+
# server [](https://www.npmjs.com/package/@jsenv/server) [](https://github.com/jsenv/server/actions?workflow=main) [](https://codecov.io/gh/jsenv/server)
|
|
2
2
|
|
|
3
|
-
`@jsenv/server`
|
|
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
|
|
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
|
|
49
|
+
_Code starting a server with 2 request handlers:_
|
|
50
50
|
|
|
51
51
|
```js
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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:
|
|
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:
|
|
82
|
-
serverCertificatePrivateKey:
|
|
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 {
|
|
9
|
-
export {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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": "
|
|
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 --
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
56
|
-
"@babel/eslint-parser": "7.15.
|
|
57
|
-
"@jsenv/assert": "2.3.
|
|
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": "
|
|
60
|
-
"@jsenv/eslint-config": "16.0.
|
|
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.
|
|
64
|
-
"@jsenv/importmap-node-module": "2.
|
|
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": "
|
|
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.
|
|
71
|
+
"eslint-plugin-import": "2.25.2",
|
|
70
72
|
"eventsource": "1.1.0",
|
|
71
|
-
"
|
|
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,6 +1,6 @@
|
|
|
1
|
-
import { negotiateContentType } from "
|
|
2
|
-
import { negotiateContentLanguage } from "
|
|
3
|
-
import { negotiateContentEncoding } from "
|
|
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 {
|
|
2
|
-
import {
|
|
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(
|
|
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 {
|
|
2
|
-
import {
|
|
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(
|
|
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] =
|
|
46
|
+
const [availablePrimary, availableVariant] =
|
|
47
|
+
decomposeLanguage(availableLanguage)
|
|
45
48
|
|
|
46
49
|
const primaryAccepted =
|
|
47
|
-
acceptedPrimary === "*" ||
|
|
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 {
|
|
2
|
-
import {
|
|
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 = (
|
|
42
|
+
const getContentTypeAcceptanceScore = (
|
|
43
|
+
{ value, quality },
|
|
44
|
+
availableContentType,
|
|
45
|
+
) => {
|
|
43
46
|
const [acceptedType, acceptedSubtype] = decomposeContentType(value)
|
|
44
|
-
const [availableType, availableSubtype] =
|
|
47
|
+
const [availableType, availableSubtype] =
|
|
48
|
+
decomposeContentType(availableContentType)
|
|
45
49
|
|
|
46
50
|
const typeAccepted = acceptedType === "*" || acceptedType === availableType
|
|
47
|
-
const subtypeAccepted =
|
|
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(
|
|
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(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
package/src/findFreePort.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { compositionMappingToComposeStrict } from "./
|
|
2
|
-
import { composeResponseHeaders } from "./
|
|
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(
|
|
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, {
|
|
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(
|
|
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)
|
|
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(
|
|
60
|
+
return composerForProperty(
|
|
61
|
+
previous[keyExistingInPrevious],
|
|
62
|
+
current[keyExistingInCurrent],
|
|
63
|
+
)
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
const composePropertyCaseInsensitive = (
|
|
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(
|
|
100
|
+
return composerForProperty(
|
|
101
|
+
previous[keyExistingInPrevious],
|
|
102
|
+
current[keyExistingInCurrent],
|
|
103
|
+
)
|
|
84
104
|
}
|
|
85
105
|
|
|
86
106
|
const keyExistsIn = (key, object) => {
|
|
File without changes
|