@jsenv/server 12.5.8 → 12.6.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/README.md +1 -2
- package/package.json +3 -4
- package/src/fetch_filesystem.js +21 -7
- package/src/fetch_url.js +1 -2
- package/src/internal/colorizeResponseStatus.js +5 -6
- package/src/internal/getServerOrigins.js +18 -25
- package/src/internal/listen.js +0 -3
- package/src/serve_directory.js +1 -2
- package/src/startServer.js +22 -7
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# server [](https://www.npmjs.com/package/@jsenv/server)
|
|
1
|
+
# server [](https://www.npmjs.com/package/@jsenv/server)
|
|
2
2
|
|
|
3
3
|
`@jsenv/server` helps to write flexible server code with a declarative API.
|
|
4
4
|
|
|
@@ -7,7 +7,6 @@ import { startServer } from "@jsenv/server"
|
|
|
7
7
|
|
|
8
8
|
await startServer({
|
|
9
9
|
protocol: "http",
|
|
10
|
-
ip: "127.0.0.1",
|
|
11
10
|
port: 8080,
|
|
12
11
|
requestToResponse: () => {
|
|
13
12
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.6.1",
|
|
4
4
|
"description": "Write your Node.js server using pure functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
"node": ">=16.13.0"
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
|
-
"access": "public"
|
|
20
|
-
"registry": "https://registry.npmjs.org"
|
|
19
|
+
"access": "public"
|
|
21
20
|
},
|
|
22
21
|
"type": "module",
|
|
23
22
|
"exports": {
|
|
@@ -68,4 +67,4 @@
|
|
|
68
67
|
"playwright": "1.18.1",
|
|
69
68
|
"prettier": "2.5.1"
|
|
70
69
|
}
|
|
71
|
-
}
|
|
70
|
+
}
|
package/src/fetch_filesystem.js
CHANGED
|
@@ -39,20 +39,37 @@ export const fetchFileSystem = async (
|
|
|
39
39
|
? "private,max-age=0,must-revalidate"
|
|
40
40
|
: "no-store",
|
|
41
41
|
canReadDirectory = false,
|
|
42
|
-
rootDirectoryUrl,
|
|
42
|
+
rootDirectoryUrl, // = `${pathToFileURL(process.cwd())}/`,
|
|
43
43
|
} = {},
|
|
44
44
|
) => {
|
|
45
45
|
const urlString = asUrlString(filesystemUrl)
|
|
46
46
|
if (!urlString) {
|
|
47
|
-
return
|
|
47
|
+
return create500Response(
|
|
48
48
|
`fetchFileSystem first parameter must be a file url, got ${filesystemUrl}`,
|
|
49
49
|
)
|
|
50
50
|
}
|
|
51
51
|
if (!urlString.startsWith("file://")) {
|
|
52
|
-
return
|
|
52
|
+
return create500Response(
|
|
53
53
|
`fetchFileSystem url must use "file://" scheme, got ${filesystemUrl}`,
|
|
54
54
|
)
|
|
55
55
|
}
|
|
56
|
+
if (rootDirectoryUrl) {
|
|
57
|
+
let rootDirectoryUrlString = asUrlString(rootDirectoryUrl)
|
|
58
|
+
if (!rootDirectoryUrlString) {
|
|
59
|
+
return create500Response(
|
|
60
|
+
`rootDirectoryUrl must be a string or an url, got ${rootDirectoryUrl}`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
if (!rootDirectoryUrlString.endsWith("/")) {
|
|
64
|
+
rootDirectoryUrlString = `${rootDirectoryUrlString}/`
|
|
65
|
+
}
|
|
66
|
+
if (!urlString.startsWith(rootDirectoryUrlString)) {
|
|
67
|
+
return create500Response(
|
|
68
|
+
`fetchFileSystem url must be inside root directory, got ${urlString}`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
rootDirectoryUrl = rootDirectoryUrlString
|
|
72
|
+
}
|
|
56
73
|
|
|
57
74
|
// here you might be tempted to add || cacheControl === 'no-cache'
|
|
58
75
|
// but no-cache means ressource can be cache but must be revalidated (yeah naming is strange)
|
|
@@ -180,7 +197,7 @@ export const fetchFileSystem = async (
|
|
|
180
197
|
}
|
|
181
198
|
}
|
|
182
199
|
|
|
183
|
-
const
|
|
200
|
+
const create500Response = (message) => {
|
|
184
201
|
return {
|
|
185
202
|
status: 500,
|
|
186
203
|
headers: {
|
|
@@ -419,12 +436,10 @@ const asUrlString = (value) => {
|
|
|
419
436
|
if (value instanceof URL) {
|
|
420
437
|
return value.href
|
|
421
438
|
}
|
|
422
|
-
|
|
423
439
|
if (typeof value === "string") {
|
|
424
440
|
if (isFileSystemPath(value)) {
|
|
425
441
|
return fileSystemPathToUrl(value)
|
|
426
442
|
}
|
|
427
|
-
|
|
428
443
|
try {
|
|
429
444
|
const urlObject = new URL(value)
|
|
430
445
|
return String(urlObject)
|
|
@@ -432,6 +447,5 @@ const asUrlString = (value) => {
|
|
|
432
447
|
return null
|
|
433
448
|
}
|
|
434
449
|
}
|
|
435
|
-
|
|
436
450
|
return null
|
|
437
451
|
}
|
package/src/fetch_url.js
CHANGED
|
@@ -28,12 +28,11 @@ export const fetchUrl = async (
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if (url.startsWith("file://")) {
|
|
31
|
-
const origin = new URL(url).origin
|
|
32
31
|
const responseProperties = await fetchFileSystem(url, {
|
|
33
32
|
signal,
|
|
34
33
|
method,
|
|
35
34
|
headers,
|
|
36
|
-
rootDirectoryUrl:
|
|
35
|
+
rootDirectoryUrl: "file://",
|
|
37
36
|
cacheStrategy,
|
|
38
37
|
canReadDirectory,
|
|
39
38
|
contentTypeMap,
|
|
@@ -11,22 +11,21 @@ const cyan = "\x1b[36m"
|
|
|
11
11
|
|
|
12
12
|
export const colorizeResponseStatus = (status) => {
|
|
13
13
|
const statusType = statusToType(status)
|
|
14
|
-
|
|
15
14
|
if (statusType === "information") return `${cyan}${status}${close}`
|
|
16
15
|
if (statusType === "success") return `${green}${status}${close}`
|
|
17
16
|
if (statusType === "redirection") return `${magenta}${status}${close}`
|
|
18
|
-
if (statusType === "
|
|
19
|
-
if (statusType === "
|
|
17
|
+
if (statusType === "client_error") return `${yellow}${status}${close}`
|
|
18
|
+
if (statusType === "server_error") return `${red}${status}${close}`
|
|
20
19
|
return status
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
|
24
|
-
const statusToType = (status) => {
|
|
23
|
+
export const statusToType = (status) => {
|
|
25
24
|
if (statusIsInformation(status)) return "information"
|
|
26
25
|
if (statusIsSuccess(status)) return "success"
|
|
27
26
|
if (statusIsRedirection(status)) return "redirection"
|
|
28
|
-
if (statusIsClientError(status)) return "
|
|
29
|
-
if (statusIsServerError(status)) return "
|
|
27
|
+
if (statusIsClientError(status)) return "client_error"
|
|
28
|
+
if (statusIsServerError(status)) return "server_error"
|
|
30
29
|
return "unknown"
|
|
31
30
|
}
|
|
32
31
|
|
|
@@ -2,43 +2,37 @@ import { networkInterfaces } from "os"
|
|
|
2
2
|
import { URL } from "url"
|
|
3
3
|
|
|
4
4
|
export const getServerOrigins = ({ protocol, ip, port }) => {
|
|
5
|
+
const isInternalIp = ip === "127.0.0.1"
|
|
6
|
+
const internalOrigin = createServerOrigin({
|
|
7
|
+
protocol,
|
|
8
|
+
hostname: "localhost",
|
|
9
|
+
port,
|
|
10
|
+
})
|
|
11
|
+
if (isInternalIp) {
|
|
12
|
+
return { internal: internalOrigin }
|
|
13
|
+
}
|
|
14
|
+
const isAnyIp = !ip || ip === "::" || ip === "0.0.0.0"
|
|
5
15
|
return {
|
|
6
|
-
|
|
7
|
-
external: createServerOrigin({
|
|
16
|
+
internal: internalOrigin,
|
|
17
|
+
external: createServerOrigin({
|
|
18
|
+
protocol,
|
|
19
|
+
hostname: isAnyIp ? getExternalIp(ip) : ip,
|
|
20
|
+
port,
|
|
21
|
+
}),
|
|
8
22
|
}
|
|
9
23
|
}
|
|
10
24
|
|
|
11
|
-
const createServerOrigin = ({ protocol,
|
|
25
|
+
const createServerOrigin = ({ protocol, hostname, port }) => {
|
|
12
26
|
const url = new URL("https://127.0.0.1:80")
|
|
13
|
-
|
|
14
27
|
url.protocol = protocol
|
|
15
|
-
url.hostname =
|
|
28
|
+
url.hostname = hostname
|
|
16
29
|
url.port = port
|
|
17
|
-
|
|
18
30
|
return url.origin
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
const ipToHostname = (
|
|
22
|
-
ip,
|
|
23
|
-
{
|
|
24
|
-
preferLocalhost = true,
|
|
25
|
-
preferLocalIp = false,
|
|
26
|
-
preferExternalIp = false,
|
|
27
|
-
} = {},
|
|
28
|
-
) => {
|
|
29
|
-
if (ip === "0.0.0.0" || !ip) {
|
|
30
|
-
if (preferLocalhost) return "localhost"
|
|
31
|
-
if (preferLocalIp) return "127.0.0.1"
|
|
32
|
-
if (preferExternalIp) return getExternalIp(ip) || "0.0.0.0"
|
|
33
|
-
return "0.0.0.0"
|
|
34
|
-
}
|
|
35
|
-
return ip
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
const getExternalIp = () => {
|
|
39
34
|
const networkInterfaceMap = networkInterfaces()
|
|
40
35
|
let internalIPV4NetworkAddress
|
|
41
|
-
|
|
42
36
|
Object.keys(networkInterfaceMap).find((key) => {
|
|
43
37
|
const networkAddressArray = networkInterfaceMap[key]
|
|
44
38
|
return networkAddressArray.find((networkAddress) => {
|
|
@@ -48,6 +42,5 @@ const getExternalIp = () => {
|
|
|
48
42
|
return true
|
|
49
43
|
})
|
|
50
44
|
})
|
|
51
|
-
|
|
52
45
|
return internalIPV4NetworkAddress ? internalIPV4NetworkAddress.address : null
|
|
53
46
|
}
|
package/src/internal/listen.js
CHANGED
|
@@ -20,7 +20,6 @@ const listen = async ({
|
|
|
20
20
|
ip,
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
|
-
|
|
24
23
|
listeningOperation.throwIfAborted()
|
|
25
24
|
port = await startListening({ server, port, ip })
|
|
26
25
|
listeningOperation.addAbortCallback(() => stopListening(server))
|
|
@@ -43,7 +42,6 @@ export const findFreePort = async (
|
|
|
43
42
|
} = {},
|
|
44
43
|
) => {
|
|
45
44
|
const findFreePortOperation = Abort.startOperation()
|
|
46
|
-
|
|
47
45
|
try {
|
|
48
46
|
findFreePortOperation.addAbortSignal(signal)
|
|
49
47
|
findFreePortOperation.throwIfAborted()
|
|
@@ -61,7 +59,6 @@ export const findFreePort = async (
|
|
|
61
59
|
}
|
|
62
60
|
return testUntil(nextPort, ip)
|
|
63
61
|
}
|
|
64
|
-
|
|
65
62
|
const freePort = await testUntil(initialPort, ip)
|
|
66
63
|
return freePort
|
|
67
64
|
} finally {
|
package/src/serve_directory.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { readdirSync } from "node:fs"
|
|
2
|
-
import { pathToFileURL } from "node:url"
|
|
3
2
|
|
|
4
3
|
import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
|
|
5
4
|
|
|
6
5
|
export const serveDirectory = (
|
|
7
6
|
url,
|
|
8
|
-
{ headers = {}, rootDirectoryUrl
|
|
7
|
+
{ headers = {}, rootDirectoryUrl } = {},
|
|
9
8
|
) => {
|
|
10
9
|
url = String(url)
|
|
11
10
|
url = url[url.length - 1] === "/" ? url : `${url}/`
|
package/src/startServer.js
CHANGED
|
@@ -16,7 +16,10 @@ import {
|
|
|
16
16
|
applyRedirectionToRequest,
|
|
17
17
|
} from "./internal/request_factory.js"
|
|
18
18
|
import { populateNodeResponse } from "./internal/populateNodeResponse.js"
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
statusToType,
|
|
21
|
+
colorizeResponseStatus,
|
|
22
|
+
} from "./internal/colorizeResponseStatus.js"
|
|
20
23
|
import { getServerOrigins } from "./internal/getServerOrigins.js"
|
|
21
24
|
import { listen, stopListening } from "./internal/listen.js"
|
|
22
25
|
import { composeTwoResponses } from "./internal/response_composition.js"
|
|
@@ -37,6 +40,7 @@ import {
|
|
|
37
40
|
export const startServer = async ({
|
|
38
41
|
signal = new AbortController().signal,
|
|
39
42
|
logLevel,
|
|
43
|
+
startLog = true,
|
|
40
44
|
serverName = "server",
|
|
41
45
|
|
|
42
46
|
protocol = "http",
|
|
@@ -44,7 +48,8 @@ export const startServer = async ({
|
|
|
44
48
|
http1Allowed = true,
|
|
45
49
|
redirectHttpToHttps,
|
|
46
50
|
allowHttpRequestOnHttps = false,
|
|
47
|
-
|
|
51
|
+
listenAnyIp = false,
|
|
52
|
+
ip = listenAnyIp ? undefined : "127.0.0.1",
|
|
48
53
|
port = 0, // assign a random available port
|
|
49
54
|
portHint,
|
|
50
55
|
privateKey,
|
|
@@ -204,7 +209,7 @@ export const startServer = async ({
|
|
|
204
209
|
|
|
205
210
|
status = "opened"
|
|
206
211
|
const serverOrigins = getServerOrigins({ protocol, ip, port })
|
|
207
|
-
const serverOrigin = serverOrigins.
|
|
212
|
+
const serverOrigin = serverOrigins.internal
|
|
208
213
|
|
|
209
214
|
const removeConnectionErrorListener = listenServerConnectionError(
|
|
210
215
|
nodeServer,
|
|
@@ -676,8 +681,15 @@ export const startServer = async ({
|
|
|
676
681
|
onRequestHandled(requestNode)
|
|
677
682
|
},
|
|
678
683
|
onHeadersSent: ({ status, statusText }) => {
|
|
684
|
+
const statusType = statusToType(status)
|
|
679
685
|
addRequestLog(requestNode, {
|
|
680
|
-
type:
|
|
686
|
+
type: {
|
|
687
|
+
information: "info",
|
|
688
|
+
success: "info",
|
|
689
|
+
redirection: "info",
|
|
690
|
+
client_error: "warn",
|
|
691
|
+
server_error: "error",
|
|
692
|
+
}[statusType],
|
|
681
693
|
value: `${colorizeResponseStatus(status)} ${
|
|
682
694
|
responseProperties.statusMessage || statusText
|
|
683
695
|
}`,
|
|
@@ -724,9 +736,11 @@ export const startServer = async ({
|
|
|
724
736
|
// ensure we don't try to handle new requests while server is stopping
|
|
725
737
|
stopCallbackList.add(removeRequestListener)
|
|
726
738
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
739
|
+
if (startLog) {
|
|
740
|
+
logger.info(
|
|
741
|
+
`${serverName} started at ${serverOrigin} (${serverOrigins.external})`,
|
|
742
|
+
)
|
|
743
|
+
}
|
|
730
744
|
|
|
731
745
|
const addEffect = (callback) => {
|
|
732
746
|
const cleanup = callback()
|
|
@@ -738,6 +752,7 @@ export const startServer = async ({
|
|
|
738
752
|
return {
|
|
739
753
|
getStatus: () => status,
|
|
740
754
|
origin: serverOrigin,
|
|
755
|
+
origins: serverOrigins,
|
|
741
756
|
nodeServer,
|
|
742
757
|
stop,
|
|
743
758
|
stoppedPromise,
|