@jsenv/server 12.3.0 → 12.4.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/main.js +1 -0
- package/package.json +1 -1
- package/src/from_fetch_response.js +15 -0
- package/src/internal/nodeStreamToObservable.js +15 -6
- package/src/internal/populateNodeResponse.js +15 -7
- package/src/ressource_aliases/plugin_ressource_aliases.js +46 -22
- package/src/ressource_aliases/regex_escape.js +34 -0
- package/src/startServer.js +23 -45
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
|
@@ -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
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// https://github.com/jamestalmage/stream-to-observable/blob/master/index.js
|
|
2
2
|
import { Readable } from "node:stream"
|
|
3
|
-
|
|
4
3
|
import { createObservable } from "./observable.js"
|
|
5
4
|
|
|
6
5
|
const readableStreamLifetimeInSeconds = 120
|
|
7
6
|
|
|
8
7
|
export const nodeStreamToObservable = (nodeStream) => {
|
|
9
8
|
const observable = createObservable(({ next, error, complete }) => {
|
|
9
|
+
if (nodeStream.isPaused()) {
|
|
10
|
+
nodeStream.resume()
|
|
11
|
+
} else if (nodeStream.complete) {
|
|
12
|
+
complete()
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
10
15
|
const cleanup = () => {
|
|
11
16
|
nodeStream.removeListener("data", next)
|
|
12
17
|
nodeStream.removeListener("error", error)
|
|
@@ -14,13 +19,17 @@ export const nodeStreamToObservable = (nodeStream) => {
|
|
|
14
19
|
nodeStream.removeListener("close", cleanup)
|
|
15
20
|
nodeStream.destroy()
|
|
16
21
|
}
|
|
17
|
-
|
|
18
22
|
// should we do nodeStream.resume() in case the stream was paused ?
|
|
19
23
|
nodeStream.once("error", error)
|
|
20
|
-
nodeStream.on("data",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
nodeStream.on("data", (data) => {
|
|
25
|
+
next(data)
|
|
26
|
+
})
|
|
27
|
+
nodeStream.once("close", () => {
|
|
28
|
+
cleanup()
|
|
29
|
+
})
|
|
30
|
+
nodeStream.once("end", () => {
|
|
31
|
+
complete()
|
|
32
|
+
})
|
|
24
33
|
return cleanup
|
|
25
34
|
})
|
|
26
35
|
|
|
@@ -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 = (
|
|
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,15 +1,21 @@
|
|
|
1
1
|
import { urlToMeta, normalizeStructuredMetaMap } from "@jsenv/url-meta"
|
|
2
2
|
|
|
3
|
+
import { escapeRegexpSpecialCharacters } from "./regex_escape.js"
|
|
4
|
+
|
|
3
5
|
export const pluginRessourceAliases = (ressourceAliases) => {
|
|
4
|
-
const
|
|
6
|
+
const alias = {}
|
|
5
7
|
Object.keys(ressourceAliases).forEach((key) => {
|
|
6
|
-
|
|
8
|
+
alias[key] = {
|
|
9
|
+
pattern: key,
|
|
10
|
+
replacement: ressourceAliases[key],
|
|
11
|
+
}
|
|
7
12
|
})
|
|
8
13
|
const structuredMetaMap = normalizeStructuredMetaMap(
|
|
9
|
-
{
|
|
14
|
+
{
|
|
15
|
+
alias,
|
|
16
|
+
},
|
|
10
17
|
"file:///",
|
|
11
18
|
)
|
|
12
|
-
|
|
13
19
|
const getRessourceAlias = (pathname) => {
|
|
14
20
|
const { alias } = urlToMeta({
|
|
15
21
|
url: `file://${pathname}`,
|
|
@@ -25,14 +31,35 @@ export const pluginRessourceAliases = (ressourceAliases) => {
|
|
|
25
31
|
if (!ressourceAlias) {
|
|
26
32
|
return
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const { pattern, replacement } = ressourceAlias
|
|
35
|
+
let newValue
|
|
36
|
+
const ressourceBeforeAlias = request.ressource
|
|
37
|
+
if (replacement.includes("*")) {
|
|
38
|
+
let regexsource = ""
|
|
39
|
+
let i = 0
|
|
40
|
+
while (i < pattern.length) {
|
|
41
|
+
const char = pattern[i]
|
|
42
|
+
i++
|
|
43
|
+
if (char === "*") {
|
|
44
|
+
regexsource += "(.+)"
|
|
45
|
+
} else {
|
|
46
|
+
regexsource += escapeRegexpSpecialCharacters(char)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const regex = new RegExp(regexsource)
|
|
50
|
+
const matchResult = ressourceBeforeAlias.match(regex)
|
|
51
|
+
const parts = replacement.split("*")
|
|
52
|
+
newValue = parts.reduce((previous, value, index) => {
|
|
53
|
+
return `${previous}${value}${
|
|
54
|
+
index === parts.length - 1 ? "" : matchResult[index + 1]
|
|
55
|
+
}`
|
|
56
|
+
}, "")
|
|
57
|
+
} else {
|
|
58
|
+
newValue = replacement
|
|
59
|
+
}
|
|
60
|
+
const ressource = replaceRessource(ressourceBeforeAlias, newValue)
|
|
34
61
|
redirectRequest({
|
|
35
|
-
ressourceBeforeAlias
|
|
62
|
+
ressourceBeforeAlias,
|
|
36
63
|
ressource,
|
|
37
64
|
})
|
|
38
65
|
},
|
|
@@ -40,16 +67,13 @@ export const pluginRessourceAliases = (ressourceAliases) => {
|
|
|
40
67
|
}
|
|
41
68
|
}
|
|
42
69
|
|
|
43
|
-
const
|
|
44
|
-
const urlObject = new URL(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
urlObject.pathname = ressource
|
|
49
|
-
return urlObject
|
|
70
|
+
const replaceRessource = (ressourceBeforeAlias, newValue) => {
|
|
71
|
+
const urlObject = new URL(ressourceBeforeAlias, "file://")
|
|
72
|
+
const searchSeparatorIndex = newValue.indexOf("?")
|
|
73
|
+
if (searchSeparatorIndex > -1) {
|
|
74
|
+
return newValue // let new value override search params
|
|
50
75
|
}
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
return urlObject
|
|
76
|
+
urlObject.pathname = newValue
|
|
77
|
+
const ressource = `${urlObject.pathname}${urlObject.search}`
|
|
78
|
+
return ressource
|
|
55
79
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// https://github.com/benjamingr/RegExp.escape/blob/master/polyfill.js
|
|
2
|
+
|
|
3
|
+
export const escapeRegexpSpecialCharacters = (string) => {
|
|
4
|
+
string = String(string)
|
|
5
|
+
let i = 0
|
|
6
|
+
let escapedString = ""
|
|
7
|
+
|
|
8
|
+
while (i < string.length) {
|
|
9
|
+
const char = string[i]
|
|
10
|
+
i++
|
|
11
|
+
escapedString += isRegExpSpecialChar(char) ? `\\${char}` : char
|
|
12
|
+
}
|
|
13
|
+
return escapedString
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isRegExpSpecialChar = (char) => regexpSpecialChars.indexOf(char) > -1
|
|
17
|
+
|
|
18
|
+
const regexpSpecialChars = [
|
|
19
|
+
"/",
|
|
20
|
+
"^",
|
|
21
|
+
"\\",
|
|
22
|
+
"[",
|
|
23
|
+
"]",
|
|
24
|
+
"(",
|
|
25
|
+
")",
|
|
26
|
+
"{",
|
|
27
|
+
"}",
|
|
28
|
+
"?",
|
|
29
|
+
"+",
|
|
30
|
+
"*",
|
|
31
|
+
".",
|
|
32
|
+
"|",
|
|
33
|
+
"$",
|
|
34
|
+
]
|
package/src/startServer.js
CHANGED
|
@@ -230,6 +230,10 @@ export const startServer = async ({
|
|
|
230
230
|
})
|
|
231
231
|
|
|
232
232
|
const requestCallback = async (nodeRequest, nodeResponse) => {
|
|
233
|
+
// pause the stream to let a chance to "requestToResponse"
|
|
234
|
+
// to call "requestRequestBody". Without this the request body readable stream
|
|
235
|
+
// might be closed when we'll try to attach "data" and "end" listeners to it
|
|
236
|
+
nodeRequest.pause()
|
|
233
237
|
if (!nagle) {
|
|
234
238
|
nodeRequest.connection.setNoDelay(true)
|
|
235
239
|
}
|
|
@@ -309,7 +313,6 @@ export const startServer = async ({
|
|
|
309
313
|
|
|
310
314
|
visitRequestNodeToLog(rootRequestNode, 0)
|
|
311
315
|
}
|
|
312
|
-
|
|
313
316
|
nodeRequest.on("error", (error) => {
|
|
314
317
|
if (error.message === "aborted") {
|
|
315
318
|
addRequestLog(rootRequestNode, {
|
|
@@ -570,7 +573,7 @@ export const startServer = async ({
|
|
|
570
573
|
return composeTwoResponses(
|
|
571
574
|
{
|
|
572
575
|
status: 500,
|
|
573
|
-
statusText:
|
|
576
|
+
statusText: "Internal Server Error",
|
|
574
577
|
headers: {
|
|
575
578
|
// ensure error are not cached
|
|
576
579
|
"cache-control": "no-store",
|
|
@@ -580,9 +583,21 @@ export const startServer = async ({
|
|
|
580
583
|
propertiesFromError,
|
|
581
584
|
)
|
|
582
585
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
+
|
|
587
|
+
const {
|
|
588
|
+
status = 501,
|
|
589
|
+
statusText,
|
|
590
|
+
headers = {},
|
|
591
|
+
body,
|
|
592
|
+
...rest
|
|
593
|
+
} = result.returnValue || {}
|
|
594
|
+
const responseProperties = {
|
|
595
|
+
status,
|
|
596
|
+
statusText,
|
|
597
|
+
headers,
|
|
598
|
+
body,
|
|
599
|
+
...rest,
|
|
600
|
+
}
|
|
586
601
|
return responseProperties
|
|
587
602
|
}
|
|
588
603
|
|
|
@@ -652,12 +667,10 @@ export const startServer = async ({
|
|
|
652
667
|
})
|
|
653
668
|
onRequestHandled(requestNode)
|
|
654
669
|
},
|
|
655
|
-
onHeadersSent: () => {
|
|
670
|
+
onHeadersSent: ({ status, statusText }) => {
|
|
656
671
|
addRequestLog(requestNode, {
|
|
657
672
|
type: "info",
|
|
658
|
-
value: `${colorizeResponseStatus(
|
|
659
|
-
responseProperties.statusText
|
|
660
|
-
}`,
|
|
673
|
+
value: `${colorizeResponseStatus(status)} ${statusText}`,
|
|
661
674
|
})
|
|
662
675
|
},
|
|
663
676
|
onEnd: () => {
|
|
@@ -673,6 +686,7 @@ export const startServer = async ({
|
|
|
673
686
|
const responseProperties = await handleRequest(request, {
|
|
674
687
|
requestNode: rootRequestNode,
|
|
675
688
|
})
|
|
689
|
+
nodeRequest.resume()
|
|
676
690
|
if (receiveRequestOperation.signal.aborted) {
|
|
677
691
|
return
|
|
678
692
|
}
|
|
@@ -742,38 +756,6 @@ const createNodeServer = async ({
|
|
|
742
756
|
})
|
|
743
757
|
}
|
|
744
758
|
|
|
745
|
-
const normalizeResponseProperties = (returnValue) => {
|
|
746
|
-
if (returnValue && returnValue.constructor.name === "Response") {
|
|
747
|
-
const responseHeaders = {}
|
|
748
|
-
const headersToIgnore = ["connection"]
|
|
749
|
-
returnValue.headers.forEach((value, name) => {
|
|
750
|
-
if (!headersToIgnore.includes(name)) {
|
|
751
|
-
responseHeaders[name] = value
|
|
752
|
-
}
|
|
753
|
-
})
|
|
754
|
-
return {
|
|
755
|
-
status: returnValue.status,
|
|
756
|
-
statusText: returnValue.statusText,
|
|
757
|
-
headers: responseHeaders,
|
|
758
|
-
body: returnValue.body, // node-fetch assumed
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
const {
|
|
762
|
-
status = 501,
|
|
763
|
-
statusText = statusToStatusText(status),
|
|
764
|
-
headers = {},
|
|
765
|
-
body,
|
|
766
|
-
...rest
|
|
767
|
-
} = returnValue || {}
|
|
768
|
-
return {
|
|
769
|
-
status,
|
|
770
|
-
statusText,
|
|
771
|
-
headers,
|
|
772
|
-
body,
|
|
773
|
-
...rest,
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
759
|
const generateResponseDescription = async ({
|
|
778
760
|
requestToResponse,
|
|
779
761
|
request,
|
|
@@ -797,10 +779,6 @@ const generateResponseDescription = async ({
|
|
|
797
779
|
}
|
|
798
780
|
}
|
|
799
781
|
}
|
|
800
|
-
|
|
801
|
-
const statusToStatusText = (status) =>
|
|
802
|
-
http.STATUS_CODES[status] || "not specified"
|
|
803
|
-
|
|
804
782
|
const testCanPushStream = (http2Stream) => {
|
|
805
783
|
if (!http2Stream.pushAllowed) {
|
|
806
784
|
return {
|