@jsenv/server 15.5.3 → 16.0.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/README.md +79 -86
- package/dist/html/4xx.html +196 -0
- package/dist/html/500.html +17 -0
- package/dist/html/route_inspector.html +2435 -0
- package/dist/js/formidable.js +17 -0
- package/dist/js/vendors.js +2001 -0
- package/dist/js/ws.js +6863 -0
- package/dist/jsenv_server.js +8829 -0
- package/package.json +20 -9
- package/src/content_negotiation/pick_content_version.js +63 -0
- package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
- package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
- package/src/interfacing_with_node/from_node_request.js +455 -0
- package/src/interfacing_with_node/get_observable_value_type.js +43 -0
- package/src/interfacing_with_node/observable.js +91 -75
- package/src/interfacing_with_node/observable_from.js +34 -0
- package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
- package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
- package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
- package/src/interfacing_with_node/observable_from_promise.js +13 -0
- package/src/interfacing_with_node/write_node_response.js +80 -35
- package/src/internal/colorizeResponseStatus.js +2 -0
- package/src/internal/headers_composition.js +42 -15
- package/src/internal/multiple-header.js +27 -12
- package/src/internal/object_composition.js +2 -2
- package/src/internal/response_composition.js +16 -3
- package/src/main.js +15 -11
- package/src/progressive_response.js +38 -0
- package/src/replace_placeholder_in_html.js +12 -0
- package/src/request_body_handling.js +22 -0
- package/src/router/router.js +878 -0
- package/src/server_events.js +431 -0
- package/src/service_controller.js +79 -73
- package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
- package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
- package/src/services/cors/jsenv_service_cors.js +10 -18
- package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
- package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
- package/src/services/error_handler/client/500.html +17 -0
- package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
- package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
- package/src/services/open_file/jsenv_service_open_file.js +37 -0
- package/src/services/route_inspector/client/code_block.js +215 -0
- package/src/services/route_inspector/client/route_inspector.html +1363 -0
- package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
- package/src/start_server.js +575 -607
- package/src/web_socket_response.js +53 -0
- package/src/from_fetch_response.js +0 -15
- package/src/interfacing_with_node/body.js +0 -81
- package/src/interfacing_with_node/request_factory.js +0 -125
- package/src/readRequestBody.js +0 -32
- package/src/server_timing/timing_measure.js +0 -26
- package/src/service_composition/path_to_regexp.js +0 -436
- package/src/service_composition/routing.js +0 -31
- package/src/sse/sse_room.js +0 -313
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const css = /* css */ `
|
|
2
|
+
dialog {
|
|
3
|
+
opacity: 0;
|
|
4
|
+
transition: all 0.3s allow-discrete;
|
|
5
|
+
}
|
|
6
|
+
dialog::backdrop {
|
|
7
|
+
background-image: linear-gradient(
|
|
8
|
+
45deg,
|
|
9
|
+
magenta,
|
|
10
|
+
rebeccapurple,
|
|
11
|
+
dodgerblue,
|
|
12
|
+
green
|
|
13
|
+
);
|
|
14
|
+
opacity: 0;
|
|
15
|
+
transition: all 0.3s allow-discrete;
|
|
16
|
+
}
|
|
17
|
+
dialog:open {
|
|
18
|
+
opacity: 1;
|
|
19
|
+
}
|
|
20
|
+
dialog:open::backdrop {
|
|
21
|
+
opacity: 0.75;
|
|
22
|
+
}`;
|
|
23
|
+
|
|
24
|
+
const html = /* html */ `<style>
|
|
25
|
+
${css}
|
|
26
|
+
</style>
|
|
27
|
+
<dialog>
|
|
28
|
+
<p>Connection with server is lost</p>
|
|
29
|
+
<button autofocus name="reconnect">Retry</button>
|
|
30
|
+
</dialog>`;
|
|
31
|
+
|
|
32
|
+
class JsenvAutoreloadOnServerRestart extends HTMLElement {
|
|
33
|
+
constructor({ url } = {}) {
|
|
34
|
+
super();
|
|
35
|
+
const root = this.attachShadow({ mode: "open" });
|
|
36
|
+
root.innerHTML = html;
|
|
37
|
+
|
|
38
|
+
const dialog = root.querySelector("dialog");
|
|
39
|
+
const reconnectButton = dialog.querySelector('button[name="reconnect"]');
|
|
40
|
+
|
|
41
|
+
(async () => {
|
|
42
|
+
const connect = () => {
|
|
43
|
+
const websocket = new WebSocket(url);
|
|
44
|
+
const connectedPromise = new Promise((resolve) => {
|
|
45
|
+
websocket.onerror = () => {
|
|
46
|
+
resolve(null);
|
|
47
|
+
};
|
|
48
|
+
websocket.onopen = () => {
|
|
49
|
+
resolve(websocket);
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
return connectedPromise;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const initialWebSocket = await connect();
|
|
56
|
+
initialWebSocket.onclose = async () => {
|
|
57
|
+
console.info("connection to server lost, trying to reconnect");
|
|
58
|
+
const retry = async () => {
|
|
59
|
+
let attemptCount = 0;
|
|
60
|
+
let timeToWait = 100;
|
|
61
|
+
const tryToReconnect = async () => {
|
|
62
|
+
const newWebsocket = await connect();
|
|
63
|
+
if (newWebsocket) {
|
|
64
|
+
dialog.close();
|
|
65
|
+
console.info("reconnected to server, reloading the page...");
|
|
66
|
+
window.location.reload(true);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
attemptCount++;
|
|
70
|
+
if (attemptCount === 10) {
|
|
71
|
+
dialog.showModal();
|
|
72
|
+
reconnectButton.onclick = async () => {
|
|
73
|
+
reconnectButton.disabled = true;
|
|
74
|
+
const previousContext = reconnectButton.textContent;
|
|
75
|
+
reconnectButton.textContent = "Retrying...";
|
|
76
|
+
await retry();
|
|
77
|
+
reconnectButton.textContent = previousContext;
|
|
78
|
+
reconnectButton.disabled = false;
|
|
79
|
+
};
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
timeToWait += 100;
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, timeToWait));
|
|
84
|
+
await tryToReconnect();
|
|
85
|
+
};
|
|
86
|
+
await tryToReconnect();
|
|
87
|
+
};
|
|
88
|
+
retry();
|
|
89
|
+
};
|
|
90
|
+
})();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!customElements.get("jsenv-autoreload-on-server-restart")) {
|
|
94
|
+
customElements.define(
|
|
95
|
+
"jsenv-autoreload-on-server-restart",
|
|
96
|
+
JsenvAutoreloadOnServerRestart,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const jsenvAutoreloadOnServerRestartElement =
|
|
101
|
+
new JsenvAutoreloadOnServerRestart({
|
|
102
|
+
url: (() => {
|
|
103
|
+
const websocketScheme =
|
|
104
|
+
self.location.protocol === "https:" ? "wss" : "ws";
|
|
105
|
+
const websocketUrl = `${websocketScheme}://${self.location.host}/.internal/alive.websocket`;
|
|
106
|
+
return websocketUrl;
|
|
107
|
+
})(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// script is injected into document.body
|
|
111
|
+
if (document.body) {
|
|
112
|
+
document.currentScript.parentNode.replaceChild(
|
|
113
|
+
jsenvAutoreloadOnServerRestartElement,
|
|
114
|
+
document.currentScript,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
// script injected outisde document.body (likely in head)
|
|
118
|
+
else {
|
|
119
|
+
document.currentScript.parentNode.removeChild(document.currentScript);
|
|
120
|
+
document.onreadystatechange = () => {
|
|
121
|
+
if (document.readyState === "interactive") {
|
|
122
|
+
document.body.appendChild(jsenvAutoreloadOnServerRestartElement);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { LazyServerEvents } from "../../server_events.js";
|
|
2
|
+
|
|
3
|
+
export const jsenvServiceAutoreloadOnRestart = () => {
|
|
4
|
+
const aliveServerEvents = new LazyServerEvents(() => {});
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
name: "jsenv:autoreload_on_server_restart",
|
|
8
|
+
|
|
9
|
+
routes: [
|
|
10
|
+
{
|
|
11
|
+
endpoint: "GET /.internal/alive.websocket",
|
|
12
|
+
description:
|
|
13
|
+
"Client can connect to this websocket endpoint to detect when server connection is lost.",
|
|
14
|
+
declarationSource: import.meta.url,
|
|
15
|
+
/* eslint-disable no-undef */
|
|
16
|
+
clientCodeExample: () => {
|
|
17
|
+
const websocket = new WebSocket(
|
|
18
|
+
"ws://localhost/.internal/alive.websocket",
|
|
19
|
+
);
|
|
20
|
+
websocket.onclose = () => {
|
|
21
|
+
// server connection closed
|
|
22
|
+
window.location.reload();
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
fetch: aliveServerEvents.fetch,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
endpoint: "GET /.internal/alive.eventsource",
|
|
29
|
+
description: `Client can connect to this eventsource endpoint to detect when server connection is lost.
|
|
30
|
+
This endpoint exists mostly to demo eventsource as there is already the websocket endpoint.`,
|
|
31
|
+
declarationSource: import.meta.url,
|
|
32
|
+
/* eslint-disable no-undef */
|
|
33
|
+
clientCodeExample: async () => {
|
|
34
|
+
const eventSource = new EventSource("/.internal/alive.eventsource");
|
|
35
|
+
eventSource.onerror = () => {
|
|
36
|
+
// server connection closed
|
|
37
|
+
window.location.reload();
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
/* eslint-enable no-undef */
|
|
41
|
+
fetch: aliveServerEvents.fetch,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jsenv/server is already registering a route to handle OPTIONS request
|
|
3
|
+
* so here we just need to add the CORS headers to the response
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
export const jsenvAccessControlAllowedHeaders = ["x-requested-with"];
|
|
2
7
|
|
|
3
8
|
export const jsenvAccessControlAllowedMethods = [
|
|
@@ -21,7 +26,7 @@ export const jsenvServiceCORS = ({
|
|
|
21
26
|
accessControlMaxAge = 600,
|
|
22
27
|
timingAllowOrigin = false,
|
|
23
28
|
} = {}) => {
|
|
24
|
-
// TODO: we should check access control params
|
|
29
|
+
// TODO: we should check access control params and throw/warn if we find strange values
|
|
25
30
|
|
|
26
31
|
const corsEnabled =
|
|
27
32
|
accessControlAllowRequestOrigin || accessControlAllowedOrigins.length;
|
|
@@ -32,22 +37,7 @@ export const jsenvServiceCORS = ({
|
|
|
32
37
|
|
|
33
38
|
return {
|
|
34
39
|
name: "jsenv:cors",
|
|
35
|
-
|
|
36
|
-
handleRequest: (request) => {
|
|
37
|
-
// when request method is "OPTIONS" we must return a 200 without body
|
|
38
|
-
// So we bypass "requestToResponse" in that scenario using shortcircuitResponse
|
|
39
|
-
if (request.method === "OPTIONS") {
|
|
40
|
-
return {
|
|
41
|
-
status: 200,
|
|
42
|
-
headers: {
|
|
43
|
-
"content-length": 0,
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
injectResponseHeaders: (response, { request }) => {
|
|
40
|
+
injectResponseProperties: (request) => {
|
|
51
41
|
const accessControlHeaders = generateAccessControlHeaders({
|
|
52
42
|
request,
|
|
53
43
|
accessControlAllowedOrigins,
|
|
@@ -60,7 +50,9 @@ export const jsenvServiceCORS = ({
|
|
|
60
50
|
accessControlMaxAge,
|
|
61
51
|
timingAllowOrigin,
|
|
62
52
|
});
|
|
63
|
-
return
|
|
53
|
+
return {
|
|
54
|
+
headers: accessControlHeaders,
|
|
55
|
+
};
|
|
64
56
|
},
|
|
65
57
|
};
|
|
66
58
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>${status} ${statusText}</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: Arial, sans-serif;
|
|
10
|
+
background-color: #f8f9fa;
|
|
11
|
+
color: #333;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
margin: 0;
|
|
17
|
+
}
|
|
18
|
+
.error_container {
|
|
19
|
+
text-align: center;
|
|
20
|
+
background: #fff;
|
|
21
|
+
padding: 20px;
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
24
|
+
}
|
|
25
|
+
.error_status {
|
|
26
|
+
font-size: 5rem;
|
|
27
|
+
color: #dc3545;
|
|
28
|
+
margin: 0;
|
|
29
|
+
}
|
|
30
|
+
.error_status_text {
|
|
31
|
+
font-size: 1.5rem;
|
|
32
|
+
margin: 10px 0;
|
|
33
|
+
word-break: break-word;
|
|
34
|
+
text-align: left;
|
|
35
|
+
}
|
|
36
|
+
.error_message {
|
|
37
|
+
font-size: 1rem;
|
|
38
|
+
color: #555;
|
|
39
|
+
word-break: break-word;
|
|
40
|
+
text-align: left;
|
|
41
|
+
}
|
|
42
|
+
.home_link {
|
|
43
|
+
margin-top: 15px;
|
|
44
|
+
display: inline-block;
|
|
45
|
+
padding: 10px 15px;
|
|
46
|
+
background-color: #007bff;
|
|
47
|
+
color: #fff;
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
border-radius: 5px;
|
|
50
|
+
}
|
|
51
|
+
.home_link:hover {
|
|
52
|
+
background-color: #0056b3;
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<div class="error_container">
|
|
58
|
+
<p class="error_status">${status}</p>
|
|
59
|
+
<p class="error_status_text">${statusText}</p>
|
|
60
|
+
<p class="error_message">${statusMessage}</p>
|
|
61
|
+
<a href="/" jsenv-ignore class="home_link">Return to Home</a>
|
|
62
|
+
</div>
|
|
63
|
+
<script src="/@jsenv/server/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js?inline"></script>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { pickContentType } from "../../content_negotiation/pick_content_type.js";
|
|
3
|
+
import { replacePlaceholdersInHtml } from "../../replace_placeholder_in_html.js";
|
|
4
|
+
|
|
5
|
+
const clientErrorHtmlTemplateFileUrl = import.meta.resolve("./client/4xx.html");
|
|
6
|
+
|
|
7
|
+
export const jsenvServiceDefaultBody4xx5xx = () => {
|
|
8
|
+
return {
|
|
9
|
+
name: "jsenv:default_body_4xx_5xx",
|
|
10
|
+
|
|
11
|
+
injectResponseProperties: (request, responseProperties) => {
|
|
12
|
+
if (responseProperties.body !== undefined) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (responseProperties.status >= 400 && responseProperties.status < 500) {
|
|
16
|
+
return generateBadStatusResponse(request, responseProperties);
|
|
17
|
+
}
|
|
18
|
+
if (responseProperties.status >= 500 && responseProperties.status < 600) {
|
|
19
|
+
return generateBadStatusResponse(request, responseProperties);
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const generateBadStatusResponse = (
|
|
27
|
+
request,
|
|
28
|
+
{ status, statusText, statusMessage },
|
|
29
|
+
) => {
|
|
30
|
+
const contentTypeNegotiated = pickContentType(request, [
|
|
31
|
+
"text/html",
|
|
32
|
+
"text/plain",
|
|
33
|
+
"application/json",
|
|
34
|
+
]);
|
|
35
|
+
if (contentTypeNegotiated === "text/html") {
|
|
36
|
+
const htmlTemplate = readFileSync(
|
|
37
|
+
new URL(clientErrorHtmlTemplateFileUrl),
|
|
38
|
+
"utf8",
|
|
39
|
+
);
|
|
40
|
+
if (statusMessage) {
|
|
41
|
+
statusMessage = statusMessage.replace(/https?:\/\/\S+/g, (url) => {
|
|
42
|
+
return `<a href="${url}">${url}</a>`;
|
|
43
|
+
});
|
|
44
|
+
statusMessage = statusMessage.replace(
|
|
45
|
+
/(^|\s)(\/\S+)/g,
|
|
46
|
+
(match, startOrSpace, resource) => {
|
|
47
|
+
let end = "";
|
|
48
|
+
if (resource[resource.length - 1] === ".") {
|
|
49
|
+
resource = resource.slice(0, -1);
|
|
50
|
+
end = ".";
|
|
51
|
+
}
|
|
52
|
+
return `${startOrSpace}<a href="${resource}">${resource}</a>${end}`;
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
statusMessage = statusMessage.replace(/\r\n|\r|\n/g, "<br />");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const html = replacePlaceholdersInHtml(htmlTemplate, {
|
|
59
|
+
status,
|
|
60
|
+
statusText,
|
|
61
|
+
statusMessage: statusMessage || "",
|
|
62
|
+
});
|
|
63
|
+
return new Response(html, {
|
|
64
|
+
headers: { "content-type": "text/html" },
|
|
65
|
+
status,
|
|
66
|
+
statusText,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (contentTypeNegotiated === "text/plain") {
|
|
70
|
+
return new Response(statusMessage, {
|
|
71
|
+
status,
|
|
72
|
+
statusText,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return Response.json(
|
|
76
|
+
{ statusMessage },
|
|
77
|
+
{
|
|
78
|
+
status,
|
|
79
|
+
statusText,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Internal server error</title>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<link rel="icon" href="data:," />
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Internal server error</h1>
|
|
11
|
+
<p>${errorMessage}</p>
|
|
12
|
+
<details>
|
|
13
|
+
<summary>See internal error details</summary>
|
|
14
|
+
${errorDetailsContent}
|
|
15
|
+
</details>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { pickContentType } from "../../content_negotiation/pick_content_type.js";
|
|
3
|
+
import { replacePlaceholdersInHtml } from "../../replace_placeholder_in_html.js";
|
|
4
|
+
|
|
5
|
+
const internalErrorHtmlFileUrl = import.meta.resolve("./client/500.html");
|
|
2
6
|
|
|
3
7
|
export const jsenvServiceErrorHandler = ({ sendErrorDetails = false } = {}) => {
|
|
4
8
|
return {
|
|
@@ -31,7 +35,6 @@ export const jsenvServiceErrorHandler = ({ sendErrorDetails = false } = {}) => {
|
|
|
31
35
|
const renderHtmlForErrorWithoutDetails = () => {
|
|
32
36
|
return `<p>Details not available: to enable them use jsenvServiceErrorHandler({ sendErrorDetails: true }).</p>`;
|
|
33
37
|
};
|
|
34
|
-
|
|
35
38
|
const renderHtmlForErrorWithDetails = () => {
|
|
36
39
|
if (serverInternalErrorIsAPrimitive) {
|
|
37
40
|
return `<pre>${JSON.stringify(
|
|
@@ -43,38 +46,54 @@ export const jsenvServiceErrorHandler = ({ sendErrorDetails = false } = {}) => {
|
|
|
43
46
|
return `<pre>${serverInternalError.stack}</pre>`;
|
|
44
47
|
};
|
|
45
48
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<details>
|
|
62
|
-
<summary>See internal error details</summary>
|
|
63
|
-
${
|
|
64
|
-
sendErrorDetails
|
|
65
|
-
? renderHtmlForErrorWithDetails()
|
|
66
|
-
: renderHtmlForErrorWithoutDetails()
|
|
67
|
-
}
|
|
68
|
-
</details>
|
|
69
|
-
</body>
|
|
70
|
-
</html>`;
|
|
49
|
+
const internalErrorHtmlTemplate = readFileSync(
|
|
50
|
+
new URL(internalErrorHtmlFileUrl),
|
|
51
|
+
"utf8",
|
|
52
|
+
);
|
|
53
|
+
const internalErrorHtml = replacePlaceholdersInHtml(
|
|
54
|
+
internalErrorHtmlTemplate,
|
|
55
|
+
{
|
|
56
|
+
errorMessage: serverInternalErrorIsAPrimitive
|
|
57
|
+
? `Code inside server has thrown a literal.`
|
|
58
|
+
: `Code inside server has thrown an error.`,
|
|
59
|
+
errorDetailsContent: sendErrorDetails
|
|
60
|
+
? renderHtmlForErrorWithDetails()
|
|
61
|
+
: renderHtmlForErrorWithoutDetails(),
|
|
62
|
+
},
|
|
63
|
+
);
|
|
71
64
|
|
|
72
65
|
return {
|
|
73
66
|
headers: {
|
|
74
67
|
"content-type": "text/html",
|
|
75
|
-
"content-length": Buffer.byteLength(
|
|
68
|
+
"content-length": Buffer.byteLength(internalErrorHtml),
|
|
76
69
|
},
|
|
77
|
-
body,
|
|
70
|
+
body: internalErrorHtml,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
"text/plain": () => {
|
|
74
|
+
let internalErrorMessage = serverInternalErrorIsAPrimitive
|
|
75
|
+
? `Code inside server has thrown a literal:`
|
|
76
|
+
: `Code inside server has thrown an error:`;
|
|
77
|
+
if (sendErrorDetails) {
|
|
78
|
+
if (serverInternalErrorIsAPrimitive) {
|
|
79
|
+
internalErrorMessage += `\n${JSON.stringify(
|
|
80
|
+
serverInternalError,
|
|
81
|
+
null,
|
|
82
|
+
" ",
|
|
83
|
+
)}`;
|
|
84
|
+
} else {
|
|
85
|
+
internalErrorMessage += `\n${serverInternalError.stack}`;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
internalErrorMessage += `\nDetails not available: to enable them use jsenvServiceErrorHandler({ sendErrorDetails: true }).`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
headers: {
|
|
93
|
+
"content-type": "text/plain",
|
|
94
|
+
"content-length": Buffer.byteLength(internalErrorMessage),
|
|
95
|
+
},
|
|
96
|
+
body: internalErrorMessage,
|
|
78
97
|
};
|
|
79
98
|
},
|
|
80
99
|
"application/json": () => {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const jsenvServerRootDirectoryUrl = import.meta.resolve("../../../");
|
|
5
|
+
|
|
6
|
+
export const jsenvServiceInternalClientFiles = () => {
|
|
7
|
+
return {
|
|
8
|
+
name: "jsenv:internal_client_files",
|
|
9
|
+
|
|
10
|
+
routes: [
|
|
11
|
+
{
|
|
12
|
+
endpoint: "GET /@jsenv/server/*",
|
|
13
|
+
description: "Serve @jsenv/server internal files.",
|
|
14
|
+
availableMediaTypes: ["text/javascript"],
|
|
15
|
+
declarationSource: import.meta.url,
|
|
16
|
+
fetch: (request) => {
|
|
17
|
+
const path = request.params[0];
|
|
18
|
+
const jsenvServerClientFileUrl = new URL(
|
|
19
|
+
`./${path}`,
|
|
20
|
+
jsenvServerRootDirectoryUrl,
|
|
21
|
+
);
|
|
22
|
+
const fileContent = readFileSync(jsenvServerClientFileUrl, "utf8");
|
|
23
|
+
return new Response(fileContent, {
|
|
24
|
+
headers: {
|
|
25
|
+
"content-type": CONTENT_TYPE.fromUrlExtension(
|
|
26
|
+
jsenvServerClientFileUrl,
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { urlToFileSystemPath } from "@jsenv/urls";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
4
|
+
export const jsenvServiceOpenFile = () => {
|
|
5
|
+
return {
|
|
6
|
+
name: "jsenv:open_file",
|
|
7
|
+
routes: [
|
|
8
|
+
{
|
|
9
|
+
endpoint: "GET /.internal/open_file/*",
|
|
10
|
+
description: "Can be used to open a given file in your editor.",
|
|
11
|
+
declarationSource: import.meta.url,
|
|
12
|
+
fetch: (request) => {
|
|
13
|
+
let file = decodeURIComponent(request.params[0]);
|
|
14
|
+
if (!file) {
|
|
15
|
+
return {
|
|
16
|
+
status: 400,
|
|
17
|
+
body: "Missing file in url",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const fileUrl = new URL(file);
|
|
21
|
+
const filePath = urlToFileSystemPath(fileUrl);
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const launch = require("launch-editor");
|
|
24
|
+
launch(filePath, () => {
|
|
25
|
+
// ignore error for now
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
status: 200,
|
|
29
|
+
headers: {
|
|
30
|
+
"cache-control": "no-store",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
};
|