@jsenv/server 12.3.1 → 12.4.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.4.1",
|
|
4
4
|
"description": "Write your Node.js server using pure functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -45,26 +45,26 @@
|
|
|
45
45
|
"@jsenv/abort": "4.1.2",
|
|
46
46
|
"@jsenv/logger": "4.0.1",
|
|
47
47
|
"@jsenv/url-meta": "6.0.3",
|
|
48
|
-
"node-fetch": "2.6.
|
|
48
|
+
"node-fetch": "2.6.7",
|
|
49
49
|
"path-to-regexp": "6.2.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@jsenv/assert": "2.4.1",
|
|
53
|
-
"@jsenv/core": "25.
|
|
53
|
+
"@jsenv/core": "25.3.0",
|
|
54
54
|
"@jsenv/eslint-config": "16.0.9",
|
|
55
|
-
"@jsenv/filesystem": "2.
|
|
56
|
-
"@jsenv/github-release-package": "1.
|
|
55
|
+
"@jsenv/filesystem": "2.7.1",
|
|
56
|
+
"@jsenv/github-release-package": "1.3.0",
|
|
57
57
|
"@jsenv/https-local": "1.0.3",
|
|
58
|
-
"@jsenv/importmap-eslint-resolver": "5.2.
|
|
59
|
-
"@jsenv/importmap-node-module": "5.1.
|
|
60
|
-
"@jsenv/package-publish": "1.
|
|
58
|
+
"@jsenv/importmap-eslint-resolver": "5.2.5",
|
|
59
|
+
"@jsenv/importmap-node-module": "5.1.3",
|
|
60
|
+
"@jsenv/package-publish": "1.7.0",
|
|
61
61
|
"@jsenv/performance-impact": "2.2.1",
|
|
62
62
|
"eslint": "8.7.0",
|
|
63
63
|
"eslint-plugin-import": "2.25.4",
|
|
64
64
|
"eventsource": "1.1.0",
|
|
65
|
-
"fastify": "3.
|
|
65
|
+
"fastify": "3.27.0",
|
|
66
66
|
"fastify-static": "4.5.0",
|
|
67
|
-
"playwright": "1.
|
|
67
|
+
"playwright": "1.18.0",
|
|
68
68
|
"prettier": "2.5.1"
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -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
|
|
|
@@ -23,8 +23,8 @@ export const populateNodeResponse = async (
|
|
|
23
23
|
status,
|
|
24
24
|
statusText,
|
|
25
25
|
headers,
|
|
26
|
+
onHeadersSent,
|
|
26
27
|
})
|
|
27
|
-
onHeadersSent()
|
|
28
28
|
|
|
29
29
|
if (!body) {
|
|
30
30
|
onEnd()
|
|
@@ -135,31 +135,31 @@ export const populateNodeResponse = async (
|
|
|
135
135
|
})
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
const writeHead = (
|
|
138
|
+
const writeHead = (
|
|
139
|
+
responseStream,
|
|
140
|
+
{ status, statusText, headers, onHeadersSent },
|
|
141
|
+
) => {
|
|
139
142
|
const responseIsHttp2ServerResponse =
|
|
140
143
|
responseStream instanceof Http2ServerResponse
|
|
141
144
|
const responseIsServerHttp2Stream =
|
|
142
145
|
responseStream.constructor.name === "ServerHttp2Stream"
|
|
143
|
-
|
|
144
146
|
let nodeHeaders = headersToNodeHeaders(headers, {
|
|
145
147
|
// https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L112
|
|
146
148
|
ignoreConnectionHeader:
|
|
147
149
|
responseIsHttp2ServerResponse || responseIsServerHttp2Stream,
|
|
148
150
|
})
|
|
149
|
-
|
|
151
|
+
if (statusText === undefined) {
|
|
152
|
+
statusText = statusTextFromStatus(status)
|
|
153
|
+
}
|
|
150
154
|
if (responseIsServerHttp2Stream) {
|
|
151
155
|
nodeHeaders = {
|
|
152
156
|
...nodeHeaders,
|
|
153
157
|
":status": status,
|
|
154
158
|
}
|
|
155
159
|
responseStream.respond(nodeHeaders)
|
|
160
|
+
onHeadersSent({ nodeHeaders, status, statusText })
|
|
156
161
|
return
|
|
157
162
|
}
|
|
158
|
-
|
|
159
|
-
if (statusText === undefined) {
|
|
160
|
-
statusText = statusTextFromStatus(status)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
163
|
// nodejs strange signature for writeHead force this
|
|
164
164
|
// https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers
|
|
165
165
|
if (
|
|
@@ -167,10 +167,11 @@ const writeHead = (responseStream, { status, statusText, headers }) => {
|
|
|
167
167
|
responseIsHttp2ServerResponse
|
|
168
168
|
) {
|
|
169
169
|
responseStream.writeHead(status, nodeHeaders)
|
|
170
|
+
onHeadersSent({ nodeHeaders, status, statusText })
|
|
170
171
|
return
|
|
171
172
|
}
|
|
172
|
-
|
|
173
173
|
responseStream.writeHead(status, statusText, nodeHeaders)
|
|
174
|
+
onHeadersSent({ nodeHeaders, status, statusText })
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
const statusTextFromStatus = (status) =>
|
|
@@ -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, {
|
|
@@ -664,12 +667,10 @@ export const startServer = async ({
|
|
|
664
667
|
})
|
|
665
668
|
onRequestHandled(requestNode)
|
|
666
669
|
},
|
|
667
|
-
onHeadersSent: () => {
|
|
670
|
+
onHeadersSent: ({ status, statusText }) => {
|
|
668
671
|
addRequestLog(requestNode, {
|
|
669
672
|
type: "info",
|
|
670
|
-
value: `${colorizeResponseStatus(
|
|
671
|
-
responseProperties.statusText
|
|
672
|
-
}`,
|
|
673
|
+
value: `${colorizeResponseStatus(status)} ${statusText}`,
|
|
673
674
|
})
|
|
674
675
|
},
|
|
675
676
|
onEnd: () => {
|
|
@@ -685,6 +686,7 @@ export const startServer = async ({
|
|
|
685
686
|
const responseProperties = await handleRequest(request, {
|
|
686
687
|
requestNode: rootRequestNode,
|
|
687
688
|
})
|
|
689
|
+
nodeRequest.resume()
|
|
688
690
|
if (receiveRequestOperation.signal.aborted) {
|
|
689
691
|
return
|
|
690
692
|
}
|