@jsenv/server 12.5.0 → 12.5.4
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
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "12.5.
|
|
3
|
+
"version": "12.5.4",
|
|
4
4
|
"description": "Write your Node.js server using pure functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"node": ">=16.13.0"
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
|
-
"access": "public"
|
|
19
|
+
"access": "public",
|
|
20
|
+
"registry": "https://registry.npmjs.org"
|
|
20
21
|
},
|
|
21
22
|
"type": "module",
|
|
22
23
|
"exports": {
|
|
@@ -67,4 +68,4 @@
|
|
|
67
68
|
"playwright": "1.18.1",
|
|
68
69
|
"prettier": "2.5.1"
|
|
69
70
|
}
|
|
70
|
-
}
|
|
71
|
+
}
|
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 {
|
|
@@ -239,7 +175,7 @@ export const fetchFileSystem = async (
|
|
|
239
175
|
...(cacheControl ? { "cache-control": cacheControl } : {}),
|
|
240
176
|
},
|
|
241
177
|
},
|
|
242
|
-
convertFileSystemErrorToResponseProperties(e),
|
|
178
|
+
convertFileSystemErrorToResponseProperties(e) || {},
|
|
243
179
|
)
|
|
244
180
|
}
|
|
245
181
|
}
|
|
@@ -6,21 +6,18 @@ export const convertFileSystemErrorToResponseProperties = (error) => {
|
|
|
6
6
|
statusText: `EACCES: No permission to read file at ${error.path}`,
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
-
|
|
10
9
|
if (isErrorWithCode(error, "EPERM")) {
|
|
11
10
|
return {
|
|
12
11
|
status: 403,
|
|
13
12
|
statusText: `EPERM: No permission to read file at ${error.path}`,
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
|
-
|
|
17
15
|
if (isErrorWithCode(error, "ENOENT")) {
|
|
18
16
|
return {
|
|
19
17
|
status: 404,
|
|
20
18
|
statusText: `ENOENT: File not found at ${error.path}`,
|
|
21
19
|
}
|
|
22
20
|
}
|
|
23
|
-
|
|
24
21
|
// file access may be temporarily blocked
|
|
25
22
|
// (by an antivirus scanning it because recently modified for instance)
|
|
26
23
|
if (isErrorWithCode(error, "EBUSY")) {
|
|
@@ -32,7 +29,6 @@ export const convertFileSystemErrorToResponseProperties = (error) => {
|
|
|
32
29
|
},
|
|
33
30
|
}
|
|
34
31
|
}
|
|
35
|
-
|
|
36
32
|
// emfile means there is too many files currently opened
|
|
37
33
|
if (isErrorWithCode(error, "EMFILE")) {
|
|
38
34
|
return {
|
|
@@ -43,15 +39,13 @@ export const convertFileSystemErrorToResponseProperties = (error) => {
|
|
|
43
39
|
},
|
|
44
40
|
}
|
|
45
41
|
}
|
|
46
|
-
|
|
47
42
|
if (isErrorWithCode(error, "EISDIR")) {
|
|
48
43
|
return {
|
|
49
44
|
status: 500,
|
|
50
45
|
statusText: `EISDIR: Unexpected directory operation at ${error.path}`,
|
|
51
46
|
}
|
|
52
47
|
}
|
|
53
|
-
|
|
54
|
-
return Promise.reject(error)
|
|
48
|
+
return null
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
const isErrorWithCode = (error, code) => {
|
|
@@ -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
|
+
}
|