@jsenv/server 10.1.6 → 12.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 +4 -1
- package/main.js +3 -5
- package/package.json +15 -15
- package/src/internal/{nodeRequestToRequest.js → request_factory.js} +61 -40
- package/src/plugin_request_waiting_check.js +0 -2
- package/src/ressource_aliases/plugin_ressource_aliases.js +55 -0
- package/src/service_composition/routing.js +35 -0
- package/src/service_composition/service_composition.js +7 -36
- package/src/startServer.js +14 -5
package/README.md
CHANGED
|
@@ -70,7 +70,10 @@ const notFoundService = (request) => {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const server = await startServer({
|
|
73
|
-
requestToResponse: composeServices(
|
|
73
|
+
requestToResponse: composeServices({
|
|
74
|
+
index: indexService,
|
|
75
|
+
otherwise: notFoundService,
|
|
76
|
+
}),
|
|
74
77
|
})
|
|
75
78
|
|
|
76
79
|
const fetch = await import("node-fetch")
|
package/main.js
CHANGED
|
@@ -5,11 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export { startServer } from "./src/startServer.js"
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
composeServicesWithTiming,
|
|
11
|
-
redirectRequest,
|
|
12
|
-
} from "./src/service_composition/service_composition.js"
|
|
8
|
+
export { composeServices } from "./src/service_composition/service_composition.js"
|
|
9
|
+
export { setupRoutes } from "./src/service_composition/routing.js"
|
|
13
10
|
export { jsenvServerInternalErrorToResponse } from "./src/jsenvServerInternalErrorToResponse.js"
|
|
14
11
|
export { readRequestBody } from "./src/readRequestBody.js"
|
|
15
12
|
export { fetchFileSystem } from "./src/fetch_filesystem.js"
|
|
@@ -46,6 +43,7 @@ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_
|
|
|
46
43
|
// others
|
|
47
44
|
export { fetchUrl } from "./src/fetch_url.js"
|
|
48
45
|
export { composeTwoResponses } from "./src/internal/response_composition.js"
|
|
46
|
+
export { pluginRessourceAliases } from "./src/ressource_aliases/plugin_ressource_aliases.js"
|
|
49
47
|
export { pluginRequestWaitingCheck } from "./src/plugin_request_waiting_check.js"
|
|
50
48
|
export { findFreePort } from "./src/internal/listen.js"
|
|
51
49
|
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": "12.0.0",
|
|
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
|
+
"node": ">=16.12.0"
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
},
|
|
26
26
|
"./*": "./*"
|
|
27
27
|
},
|
|
28
|
+
"main": "./main.js",
|
|
28
29
|
"files": [
|
|
29
30
|
"/src/",
|
|
30
31
|
"/main.js"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
33
34
|
"eslint-check": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
|
|
34
|
-
"generate
|
|
35
|
+
"importmap-generate": "node ./script/importmap/generate_importmap.mjs",
|
|
35
36
|
"measure-performances": "node --expose-gc ./script/performance/generate_performance_report.mjs --log",
|
|
36
37
|
"test": "node --unhandled-rejections=strict ./script/test/test.mjs",
|
|
37
38
|
"test-with-coverage": "npm run test -- --coverage",
|
|
@@ -47,28 +48,27 @@
|
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@jsenv/abort": "4.1.1",
|
|
49
50
|
"@jsenv/logger": "4.0.1",
|
|
50
|
-
"
|
|
51
|
+
"@jsenv/url-meta": "6.0.1",
|
|
52
|
+
"node-fetch": "2.6.2",
|
|
53
|
+
"path-to-regexp": "6.2.0"
|
|
51
54
|
},
|
|
52
55
|
"devDependencies": {
|
|
53
|
-
"@babel/core": "7.16.0",
|
|
54
|
-
"@babel/eslint-parser": "7.16.0",
|
|
55
56
|
"@jsenv/assert": "2.3.2",
|
|
56
|
-
"@jsenv/
|
|
57
|
-
"@jsenv/
|
|
58
|
-
"@jsenv/eslint-config": "16.0.8",
|
|
57
|
+
"@jsenv/core": "23.10.0",
|
|
58
|
+
"@jsenv/eslint-config": "16.0.9",
|
|
59
59
|
"@jsenv/filesystem": "2.5.1",
|
|
60
60
|
"@jsenv/github-release-package": "1.2.3",
|
|
61
|
-
"@jsenv/https-local": "1.0.
|
|
61
|
+
"@jsenv/https-local": "1.0.3",
|
|
62
62
|
"@jsenv/importmap-eslint-resolver": "5.1.2",
|
|
63
|
-
"@jsenv/importmap-node-module": "2.
|
|
63
|
+
"@jsenv/importmap-node-module": "2.7.1",
|
|
64
64
|
"@jsenv/package-publish": "1.6.2",
|
|
65
65
|
"@jsenv/performance-impact": "2.2.1",
|
|
66
66
|
"@jsenv/prettier-check-project": "5.6.1",
|
|
67
|
-
"eslint": "
|
|
68
|
-
"eslint-plugin-import": "2.25.
|
|
67
|
+
"eslint": "8.2.0",
|
|
68
|
+
"eslint-plugin-import": "2.25.3",
|
|
69
69
|
"eventsource": "1.1.0",
|
|
70
|
-
"fastify": "3.
|
|
71
|
-
"fastify-static": "4.
|
|
70
|
+
"fastify": "3.24.0",
|
|
71
|
+
"fastify-static": "4.5.0",
|
|
72
72
|
"playwright": "1.16.3",
|
|
73
73
|
"prettier": "2.4.1"
|
|
74
74
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { nodeStreamToObservable } from "./nodeStreamToObservable.js"
|
|
2
2
|
import { headersFromObject } from "./headersFromObject.js"
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
|
|
5
5
|
const headers = headersFromObject(nodeRequest.headers)
|
|
6
6
|
const body = nodeStreamToObservable(nodeRequest)
|
|
7
7
|
|
|
@@ -18,67 +18,88 @@ export const nodeRequestToRequest = (nodeRequest, { signal, serverOrigin }) => {
|
|
|
18
18
|
requestOrigin = serverOrigin
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const urlObject = new URL(nodeRequest.url, requestOrigin)
|
|
22
|
-
|
|
23
21
|
return Object.freeze({
|
|
24
22
|
signal,
|
|
25
23
|
http2: Boolean(nodeRequest.stream),
|
|
26
|
-
url: String(urlObject),
|
|
27
24
|
origin: requestOrigin,
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
...getPropertiesFromRessource({
|
|
26
|
+
ressource: nodeRequest.url,
|
|
27
|
+
baseUrl: requestOrigin,
|
|
28
|
+
}),
|
|
30
29
|
method: nodeRequest.method,
|
|
31
30
|
headers,
|
|
32
31
|
body,
|
|
33
32
|
})
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
export const applyRedirectionToRequest = (
|
|
36
|
+
request,
|
|
37
|
+
{ ressource, pathname, ...rest },
|
|
38
|
+
) => {
|
|
39
|
+
return {
|
|
40
|
+
...request,
|
|
41
|
+
...(ressource
|
|
42
|
+
? getPropertiesFromRessource({
|
|
43
|
+
ressource,
|
|
44
|
+
baseUrl: request.url,
|
|
45
|
+
})
|
|
46
|
+
: pathname
|
|
47
|
+
? getPropertiesFromPathname({
|
|
48
|
+
pathname,
|
|
49
|
+
baseUrl: request.url,
|
|
50
|
+
})
|
|
51
|
+
: {}),
|
|
52
|
+
...rest,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
const getPropertiesFromRessource = ({ ressource, baseUrl }) => {
|
|
57
|
+
const urlObject = new URL(ressource, baseUrl)
|
|
58
|
+
let pathname = urlObject.pathname
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
url: String(urlObject),
|
|
62
|
+
pathname,
|
|
63
|
+
ressource,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
46
66
|
|
|
67
|
+
const getPropertiesFromPathname = ({ pathname, baseUrl }) => {
|
|
68
|
+
return getPropertiesFromRessource({
|
|
69
|
+
ressource: `${pathname}${new URL(baseUrl).search}`,
|
|
70
|
+
baseUrl,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const createPushRequest = (request, { signal, pathname, method }) => {
|
|
47
75
|
const pushRequest = Object.freeze({
|
|
48
76
|
...request,
|
|
49
77
|
parent: request,
|
|
50
78
|
signal,
|
|
51
79
|
http2: true,
|
|
52
|
-
...(pathname
|
|
80
|
+
...(pathname
|
|
81
|
+
? getPropertiesFromPathname({
|
|
82
|
+
pathname,
|
|
83
|
+
baseUrl: request.url,
|
|
84
|
+
})
|
|
85
|
+
: {}),
|
|
53
86
|
method: method || request.method,
|
|
54
|
-
headers:
|
|
87
|
+
headers: getHeadersInheritedByPushRequest(request),
|
|
55
88
|
body: undefined,
|
|
56
89
|
})
|
|
57
|
-
|
|
58
90
|
return pushRequest
|
|
59
91
|
}
|
|
60
92
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const urlObject = new URL(url)
|
|
68
|
-
return {
|
|
69
|
-
url,
|
|
70
|
-
pathname: urlObject.pathname,
|
|
71
|
-
ressource,
|
|
72
|
-
}
|
|
73
|
-
}
|
|
93
|
+
const getHeadersInheritedByPushRequest = (request) => {
|
|
94
|
+
const headersInherited = { ...request.headers }
|
|
95
|
+
// mtime sent by the client in request headers concerns the main request
|
|
96
|
+
// Time remains valid for request to other ressources so we keep it
|
|
97
|
+
// in child requests
|
|
98
|
+
// delete childHeaders["if-modified-since"]
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
request
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
80
|
-
...request,
|
|
81
|
-
...getRequestRedirectionProperties(request, { ressource, pathname }),
|
|
82
|
-
...rest,
|
|
83
|
-
}
|
|
100
|
+
// eTag sent by the client in request headers concerns the main request
|
|
101
|
+
// A request made to an other ressource must not inherit the eTag
|
|
102
|
+
delete headersInherited["if-none-match"]
|
|
103
|
+
|
|
104
|
+
return headersInherited
|
|
84
105
|
}
|
|
@@ -16,8 +16,6 @@ export const pluginRequestWaitingCheck = ({
|
|
|
16
16
|
} = {}) => {
|
|
17
17
|
return {
|
|
18
18
|
plugin_request_waiting_check: {
|
|
19
|
-
init: () => {},
|
|
20
|
-
|
|
21
19
|
onRequest: (request, { logger }) => {
|
|
22
20
|
const timeout = setTimeout(
|
|
23
21
|
() => requestWaitingCallback({ request, logger, requestWaitingMs }),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { urlToMeta, normalizeStructuredMetaMap } from "@jsenv/url-meta"
|
|
2
|
+
|
|
3
|
+
export const pluginRessourceAliases = (ressourceAliases) => {
|
|
4
|
+
const ressourceAliasMap = {}
|
|
5
|
+
Object.keys(ressourceAliases).forEach((key) => {
|
|
6
|
+
ressourceAliasMap[ressourceAliases[key]] = key
|
|
7
|
+
})
|
|
8
|
+
const structuredMetaMap = normalizeStructuredMetaMap(
|
|
9
|
+
{ alias: ressourceAliasMap },
|
|
10
|
+
"file:///",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const getRessourceAlias = (pathname) => {
|
|
14
|
+
const { alias } = urlToMeta({
|
|
15
|
+
url: `file://${pathname}`,
|
|
16
|
+
structuredMetaMap,
|
|
17
|
+
})
|
|
18
|
+
return alias
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
ressource_aliases: {
|
|
23
|
+
onRequest: (request, { redirectRequest }) => {
|
|
24
|
+
const ressourceAlias = getRessourceAlias(request.pathname)
|
|
25
|
+
if (!ressourceAlias) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const urlObjectAfterAlias = replaceUrlRessource(
|
|
30
|
+
request.url,
|
|
31
|
+
ressourceAlias,
|
|
32
|
+
)
|
|
33
|
+
const ressource = `${urlObjectAfterAlias.pathname}${urlObjectAfterAlias.search}`
|
|
34
|
+
redirectRequest({
|
|
35
|
+
ressourceBeforeAlias: request.ressource,
|
|
36
|
+
ressource,
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const replaceUrlRessource = (url, ressource) => {
|
|
44
|
+
const urlObject = new URL(url)
|
|
45
|
+
|
|
46
|
+
const searchSeparatorIndex = ressource.indexOf("?")
|
|
47
|
+
if (searchSeparatorIndex === -1) {
|
|
48
|
+
urlObject.pathname = ressource
|
|
49
|
+
return urlObject
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pathname = ressource.slice(0, searchSeparatorIndex)
|
|
53
|
+
urlObject.pathname = pathname
|
|
54
|
+
return urlObject
|
|
55
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRequire } from "node:module"
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url)
|
|
4
|
+
|
|
5
|
+
export const setupRoutes = (routes) => {
|
|
6
|
+
const { match } = require("path-to-regexp")
|
|
7
|
+
|
|
8
|
+
const candidates = Object.keys(routes).map((pathPattern) => {
|
|
9
|
+
const applyPatternMatching = match(pathPattern, {
|
|
10
|
+
decode: decodeURIComponent,
|
|
11
|
+
})
|
|
12
|
+
return {
|
|
13
|
+
applyPatternMatching,
|
|
14
|
+
requestHandler: routes[pathPattern],
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return (request, { pushResponse, redirectRequest }) => {
|
|
19
|
+
let result
|
|
20
|
+
const found = candidates.find((candidate) => {
|
|
21
|
+
result = candidate.applyPatternMatching(request.pathname)
|
|
22
|
+
return Boolean(result)
|
|
23
|
+
})
|
|
24
|
+
if (found) {
|
|
25
|
+
return found.requestHandler(
|
|
26
|
+
{
|
|
27
|
+
...request,
|
|
28
|
+
routeParams: result.params,
|
|
29
|
+
},
|
|
30
|
+
{ pushResponse, redirectRequest },
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,37 +1,13 @@
|
|
|
1
1
|
import { timeFunction } from "@jsenv/server/src/server_timing/timing_measure.js"
|
|
2
2
|
import { composeTwoResponses } from "@jsenv/server/src/internal/response_composition.js"
|
|
3
|
-
import { applyRedirectionToRequest } from "@jsenv/server/src/internal/
|
|
3
|
+
import { applyRedirectionToRequest } from "@jsenv/server/src/internal/request_factory.js"
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const composeServices = (...callbacks) => {
|
|
14
|
-
return (request, ...rest) => {
|
|
15
|
-
return firstOperationMatching({
|
|
16
|
-
array: callbacks,
|
|
17
|
-
start: (callback) => callback(request, ...rest),
|
|
18
|
-
predicate: (returnValue) => {
|
|
19
|
-
if (returnValue === null || typeof returnValue !== "object") {
|
|
20
|
-
return false
|
|
21
|
-
}
|
|
22
|
-
const requestRedirection = returnValue[requestRedirectionSymbol]
|
|
23
|
-
if (requestRedirection) {
|
|
24
|
-
request = applyRedirectionToRequest(request, requestRedirection)
|
|
25
|
-
return false
|
|
26
|
-
}
|
|
27
|
-
return true
|
|
28
|
-
},
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
}
|
|
5
|
+
export const composeServices = (namedServices) => {
|
|
6
|
+
return async (request, { pushResponse }) => {
|
|
7
|
+
const redirectRequest = (requestRedirection) => {
|
|
8
|
+
request = applyRedirectionToRequest(request, requestRedirection)
|
|
9
|
+
}
|
|
32
10
|
|
|
33
|
-
export const composeServicesWithTiming = (namedServices) => {
|
|
34
|
-
return async (request, ...rest) => {
|
|
35
11
|
const servicesTiming = {}
|
|
36
12
|
const response = await firstOperationMatching({
|
|
37
13
|
array: Object.keys(namedServices).map((serviceName) => {
|
|
@@ -39,7 +15,7 @@ export const composeServicesWithTiming = (namedServices) => {
|
|
|
39
15
|
}),
|
|
40
16
|
start: async ({ serviceName, serviceFn }) => {
|
|
41
17
|
const [serviceTiming, value] = await timeFunction(serviceName, () =>
|
|
42
|
-
serviceFn(request,
|
|
18
|
+
serviceFn(request, { pushResponse, redirectRequest }),
|
|
43
19
|
)
|
|
44
20
|
Object.assign(servicesTiming, serviceTiming)
|
|
45
21
|
return value
|
|
@@ -48,11 +24,6 @@ export const composeServicesWithTiming = (namedServices) => {
|
|
|
48
24
|
if (returnValue === null || typeof returnValue !== "object") {
|
|
49
25
|
return false
|
|
50
26
|
}
|
|
51
|
-
const requestRedirection = returnValue[requestRedirectionSymbol]
|
|
52
|
-
if (requestRedirection) {
|
|
53
|
-
request = applyRedirectionToRequest(request, requestRedirection)
|
|
54
|
-
return false
|
|
55
|
-
}
|
|
56
27
|
return true
|
|
57
28
|
},
|
|
58
29
|
})
|
package/src/startServer.js
CHANGED
|
@@ -12,9 +12,10 @@ import { createPolyglotServer } from "./internal/server-polyglot.js"
|
|
|
12
12
|
import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js"
|
|
13
13
|
import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js"
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
fromNodeRequest,
|
|
16
16
|
createPushRequest,
|
|
17
|
-
|
|
17
|
+
applyRedirectionToRequest,
|
|
18
|
+
} from "./internal/request_factory.js"
|
|
18
19
|
import { populateNodeResponse } from "./internal/populateNodeResponse.js"
|
|
19
20
|
import { colorizeResponseStatus } from "./internal/colorizeResponseStatus.js"
|
|
20
21
|
import { getServerOrigins } from "./internal/getServerOrigins.js"
|
|
@@ -265,9 +266,9 @@ export const startServer = async ({
|
|
|
265
266
|
return stopCallbackList.add(abort)
|
|
266
267
|
})
|
|
267
268
|
|
|
268
|
-
const request =
|
|
269
|
-
signal: receiveRequestOperation.signal,
|
|
269
|
+
const request = fromNodeRequest(nodeRequest, {
|
|
270
270
|
serverOrigin,
|
|
271
|
+
signal: receiveRequestOperation.signal,
|
|
271
272
|
})
|
|
272
273
|
|
|
273
274
|
// Handling request is asynchronous, we buffer logs for that request
|
|
@@ -417,6 +418,7 @@ export const startServer = async ({
|
|
|
417
418
|
pathname: path,
|
|
418
419
|
method,
|
|
419
420
|
})
|
|
421
|
+
|
|
420
422
|
try {
|
|
421
423
|
const responseProperties = await handleRequest(pushRequest, {
|
|
422
424
|
requestNode,
|
|
@@ -473,12 +475,17 @@ export const startServer = async ({
|
|
|
473
475
|
const shortcircuitResponse = (value) => {
|
|
474
476
|
responseFromShortcircuit = value
|
|
475
477
|
}
|
|
478
|
+
const redirectRequest = (requestRedirection) => {
|
|
479
|
+
request = applyRedirectionToRequest(request, requestRedirection)
|
|
480
|
+
}
|
|
476
481
|
Object.keys(plugins).forEach((pluginName) => {
|
|
477
482
|
const { onRequest } = plugins[pluginName]
|
|
478
483
|
if (onRequest) {
|
|
479
484
|
const returnValue = onRequest(request, {
|
|
480
485
|
warn,
|
|
486
|
+
logger,
|
|
481
487
|
shortcircuitResponse,
|
|
488
|
+
redirectRequest,
|
|
482
489
|
})
|
|
483
490
|
if (returnValue) {
|
|
484
491
|
requestPlugins.push(returnValue)
|
|
@@ -725,7 +732,9 @@ const generateResponseDescription = async ({
|
|
|
725
732
|
pushResponse,
|
|
726
733
|
}) => {
|
|
727
734
|
try {
|
|
728
|
-
const returnValue = await requestToResponse(request, {
|
|
735
|
+
const returnValue = await requestToResponse(request, {
|
|
736
|
+
pushResponse,
|
|
737
|
+
})
|
|
729
738
|
return {
|
|
730
739
|
responseProperties: returnValue,
|
|
731
740
|
}
|