@jsenv/server 12.5.2 → 12.5.5
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/fetch_filesystem.js +11 -75
- package/src/internal/response_composition.js +1 -0
- package/src/serve_directory.js +66 -0
- package/src/startServer.js +15 -10
package/main.js
CHANGED
|
@@ -41,6 +41,7 @@ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_
|
|
|
41
41
|
|
|
42
42
|
// others
|
|
43
43
|
export { fetchUrl } from "./src/fetch_url.js"
|
|
44
|
+
export { serveDirectory } from "./src/serve_directory.js"
|
|
44
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"
|
package/package.json
CHANGED
package/src/fetch_filesystem.js
CHANGED
|
@@ -16,9 +16,9 @@ import { composeTwoResponses } from "./internal/response_composition.js"
|
|
|
16
16
|
import { convertFileSystemErrorToResponseProperties } from "./internal/convertFileSystemErrorToResponseProperties.js"
|
|
17
17
|
import { jsenvContentTypeMap } from "./jsenvContentTypeMap.js"
|
|
18
18
|
import { timeFunction } from "./server_timing/timing_measure.js"
|
|
19
|
-
import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
|
|
20
19
|
import { negotiateContentEncoding } from "./content_negotiation/negotiateContentEncoding.js"
|
|
21
20
|
import { urlToContentType } from "./urlToContentType.js"
|
|
21
|
+
import { serveDirectory } from "./serve_directory.js"
|
|
22
22
|
|
|
23
23
|
const ETAG_MEMORY_MAP = new Map()
|
|
24
24
|
|
|
@@ -39,7 +39,7 @@ export const fetchFileSystem = async (
|
|
|
39
39
|
? "private,max-age=0,must-revalidate"
|
|
40
40
|
: "no-store",
|
|
41
41
|
canReadDirectory = false,
|
|
42
|
-
rootDirectoryUrl
|
|
42
|
+
rootDirectoryUrl,
|
|
43
43
|
} = {},
|
|
44
44
|
) => {
|
|
45
45
|
const urlString = asUrlString(filesystemUrl)
|
|
@@ -88,83 +88,19 @@ export const fetchFileSystem = async (
|
|
|
88
88
|
"file service>read file stat",
|
|
89
89
|
() => statSync(sourceFilesystemPath),
|
|
90
90
|
)
|
|
91
|
-
|
|
92
91
|
if (sourceStat.isDirectory()) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
statusText: "not allowed to read directory",
|
|
100
|
-
timing: readStatTiming,
|
|
101
|
-
}
|
|
92
|
+
if (canReadDirectory) {
|
|
93
|
+
return serveDirectory(urlString, {
|
|
94
|
+
headers,
|
|
95
|
+
canReadDirectory,
|
|
96
|
+
rootDirectoryUrl,
|
|
97
|
+
})
|
|
102
98
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
() => promises.readdir(sourceFilesystemPath),
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
const responseProducers = {
|
|
110
|
-
"application/json": () => {
|
|
111
|
-
const directoryContentJson = JSON.stringify(directoryContentArray)
|
|
112
|
-
return {
|
|
113
|
-
status: 200,
|
|
114
|
-
headers: {
|
|
115
|
-
"content-type": "application/json",
|
|
116
|
-
"content-length": directoryContentJson.length,
|
|
117
|
-
},
|
|
118
|
-
body: directoryContentJson,
|
|
119
|
-
timing: {
|
|
120
|
-
...readStatTiming,
|
|
121
|
-
...readDirectoryTiming,
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
"text/html": () => {
|
|
126
|
-
const directoryAsHtml = `<!DOCTYPE html>
|
|
127
|
-
<html>
|
|
128
|
-
<head>
|
|
129
|
-
<title>Directory explorer</title>
|
|
130
|
-
<meta charset="utf-8" />
|
|
131
|
-
<link rel="icon" href="data:," />
|
|
132
|
-
</head>
|
|
133
|
-
|
|
134
|
-
<body>
|
|
135
|
-
<h1>Content of directory ${sourceUrl}</h1>
|
|
136
|
-
<ul>
|
|
137
|
-
${directoryContentArray.map((filename) => {
|
|
138
|
-
const fileUrl = String(new URL(filename, sourceUrl))
|
|
139
|
-
const fileUrlRelativeToServer = fileUrl.slice(
|
|
140
|
-
String(rootDirectoryUrl).length,
|
|
141
|
-
)
|
|
142
|
-
return `<li>
|
|
143
|
-
<a href="/${fileUrlRelativeToServer}">${fileUrlRelativeToServer}</a>
|
|
144
|
-
</li>`
|
|
145
|
-
}).join(`
|
|
146
|
-
`)}
|
|
147
|
-
</ul>
|
|
148
|
-
</body>
|
|
149
|
-
</html>`
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
status: 200,
|
|
153
|
-
headers: {
|
|
154
|
-
"content-type": "text/html",
|
|
155
|
-
"content-length": Buffer.byteLength(directoryAsHtml),
|
|
156
|
-
},
|
|
157
|
-
body: directoryAsHtml,
|
|
158
|
-
}
|
|
159
|
-
},
|
|
99
|
+
return {
|
|
100
|
+
status: 403,
|
|
101
|
+
statusText: "not allowed to read directory",
|
|
160
102
|
}
|
|
161
|
-
const bestContentType = negotiateContentType(
|
|
162
|
-
{ headers },
|
|
163
|
-
Object.keys(responseProducers),
|
|
164
|
-
)
|
|
165
|
-
return responseProducers[bestContentType || "application/json"]()
|
|
166
103
|
}
|
|
167
|
-
|
|
168
104
|
// not a file, give up
|
|
169
105
|
if (!sourceStat.isFile()) {
|
|
170
106
|
return {
|
|
@@ -11,6 +11,7 @@ export const composeTwoResponses = (firstResponse, secondResponse) => {
|
|
|
11
11
|
const RESPONSE_KEYS_COMPOSITION = {
|
|
12
12
|
status: (prevStatus, status) => status,
|
|
13
13
|
statusText: (prevStatusText, statusText) => statusText,
|
|
14
|
+
statusMessage: (prevStatusMessage, statusMessage) => statusMessage,
|
|
14
15
|
headers: composeTwoHeaders,
|
|
15
16
|
body: (prevBody, body) => body,
|
|
16
17
|
bodyEncoding: (prevEncoding, encoding) => encoding,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs"
|
|
2
|
+
import { pathToFileURL } from "node:url"
|
|
3
|
+
|
|
4
|
+
import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
|
|
5
|
+
|
|
6
|
+
export const serveDirectory = (
|
|
7
|
+
url,
|
|
8
|
+
{ headers = {}, rootDirectoryUrl = `${pathToFileURL(process.cwd())}/` } = {},
|
|
9
|
+
) => {
|
|
10
|
+
url = String(url)
|
|
11
|
+
url = url[url.length - 1] === "/" ? url : `${url}/`
|
|
12
|
+
const directoryContentArray = readdirSync(new URL(url))
|
|
13
|
+
const responseProducers = {
|
|
14
|
+
"application/json": () => {
|
|
15
|
+
const directoryContentJson = JSON.stringify(directoryContentArray)
|
|
16
|
+
return {
|
|
17
|
+
status: 200,
|
|
18
|
+
headers: {
|
|
19
|
+
"content-type": "application/json",
|
|
20
|
+
"content-length": directoryContentJson.length,
|
|
21
|
+
},
|
|
22
|
+
body: directoryContentJson,
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"text/html": () => {
|
|
26
|
+
const directoryAsHtml = `<!DOCTYPE html>
|
|
27
|
+
<html>
|
|
28
|
+
<head>
|
|
29
|
+
<title>Directory explorer</title>
|
|
30
|
+
<meta charset="utf-8" />
|
|
31
|
+
<link rel="icon" href="data:," />
|
|
32
|
+
</head>
|
|
33
|
+
|
|
34
|
+
<body>
|
|
35
|
+
<h1>Content of directory ${url}</h1>
|
|
36
|
+
<ul>
|
|
37
|
+
${directoryContentArray.map((filename) => {
|
|
38
|
+
const fileUrl = String(new URL(filename, url))
|
|
39
|
+
const fileUrlRelativeToServer = fileUrl.slice(
|
|
40
|
+
String(rootDirectoryUrl).length,
|
|
41
|
+
)
|
|
42
|
+
return `<li>
|
|
43
|
+
<a href="/${fileUrlRelativeToServer}">${fileUrlRelativeToServer}</a>
|
|
44
|
+
</li>`
|
|
45
|
+
}).join(`
|
|
46
|
+
`)}
|
|
47
|
+
</ul>
|
|
48
|
+
</body>
|
|
49
|
+
</html>`
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
status: 200,
|
|
53
|
+
headers: {
|
|
54
|
+
"content-type": "text/html",
|
|
55
|
+
"content-length": Buffer.byteLength(directoryAsHtml),
|
|
56
|
+
},
|
|
57
|
+
body: directoryAsHtml,
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
const bestContentType = negotiateContentType(
|
|
62
|
+
{ headers },
|
|
63
|
+
Object.keys(responseProducers),
|
|
64
|
+
)
|
|
65
|
+
return responseProducers[bestContentType || "application/json"]()
|
|
66
|
+
}
|
package/src/startServer.js
CHANGED
|
@@ -291,26 +291,27 @@ export const startServer = async ({
|
|
|
291
291
|
// keep buffering until root request write logs for everyone
|
|
292
292
|
return
|
|
293
293
|
}
|
|
294
|
-
|
|
295
294
|
const writeLog = ({ type, value }, depth) => {
|
|
296
295
|
if (depth > 0) {
|
|
297
296
|
value = value.replace(/^(?!\s*$)/gm, " ".repeat(depth))
|
|
298
297
|
}
|
|
299
298
|
logger[type](value)
|
|
300
299
|
}
|
|
301
|
-
|
|
302
300
|
const visitRequestNodeToLog = (requestNode, depth) => {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
const firstLog = requestNode.logs.shift()
|
|
302
|
+
const lastLog = requestNode.logs.pop()
|
|
303
|
+
const middleLogs = requestNode.logs
|
|
304
|
+
writeLog(firstLog, depth)
|
|
305
|
+
middleLogs.forEach((log) => {
|
|
306
|
+
writeLog(log, depth + 1)
|
|
306
307
|
})
|
|
307
308
|
requestNode.children.forEach((child) => {
|
|
308
309
|
visitRequestNodeToLog(child, depth + 1)
|
|
309
310
|
})
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
if (lastLog) {
|
|
312
|
+
writeLog(lastLog, depth + 1)
|
|
313
|
+
}
|
|
312
314
|
}
|
|
313
|
-
|
|
314
315
|
visitRequestNodeToLog(rootRequestNode, 0)
|
|
315
316
|
}
|
|
316
317
|
nodeRequest.on("error", (error) => {
|
|
@@ -589,6 +590,7 @@ export const startServer = async ({
|
|
|
589
590
|
const {
|
|
590
591
|
status = 501,
|
|
591
592
|
statusText,
|
|
593
|
+
statusMessage,
|
|
592
594
|
headers = {},
|
|
593
595
|
body,
|
|
594
596
|
...rest
|
|
@@ -596,6 +598,7 @@ export const startServer = async ({
|
|
|
596
598
|
const responseProperties = {
|
|
597
599
|
status,
|
|
598
600
|
statusText,
|
|
601
|
+
statusMessage,
|
|
599
602
|
headers,
|
|
600
603
|
body,
|
|
601
604
|
...rest,
|
|
@@ -669,10 +672,12 @@ export const startServer = async ({
|
|
|
669
672
|
})
|
|
670
673
|
onRequestHandled(requestNode)
|
|
671
674
|
},
|
|
672
|
-
onHeadersSent: ({ status, statusText }) => {
|
|
675
|
+
onHeadersSent: ({ status, statusText, statusMessage }) => {
|
|
673
676
|
addRequestLog(requestNode, {
|
|
674
677
|
type: "info",
|
|
675
|
-
value: `${colorizeResponseStatus(status)} ${
|
|
678
|
+
value: `${colorizeResponseStatus(status)} ${
|
|
679
|
+
statusMessage || statusText
|
|
680
|
+
}`,
|
|
676
681
|
})
|
|
677
682
|
},
|
|
678
683
|
onEnd: () => {
|