97 lines
2.8 KiB
JavaScript
97 lines
2.8 KiB
JavaScript
import { createReadStream, statSync } from "node:fs";
|
|
import { createServer } from "node:http";
|
|
import { dirname, extname, resolve, sep } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const defaultEditorDirectory = resolve(packageRoot, "editor");
|
|
|
|
const contentTypes = new Map([
|
|
[".css", "text/css; charset=utf-8"],
|
|
[".html", "text/html; charset=utf-8"],
|
|
[".js", "text/javascript; charset=utf-8"],
|
|
[".json", "application/json; charset=utf-8"],
|
|
[".svg", "image/svg+xml"],
|
|
[".wasm", "application/wasm"],
|
|
]);
|
|
|
|
function respond(response, statusCode, message) {
|
|
response.writeHead(statusCode, {
|
|
"Content-Type": "text/plain; charset=utf-8",
|
|
"Cache-Control": "no-store",
|
|
"X-Content-Type-Options": "nosniff",
|
|
});
|
|
response.end(message);
|
|
}
|
|
|
|
export function createEditorServer({ editorDirectory = defaultEditorDirectory } = {}) {
|
|
const root = resolve(editorDirectory);
|
|
const rootPrefix = `${root}${sep}`;
|
|
|
|
return createServer((request, response) => {
|
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
respond(response, 405, "Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
let pathname;
|
|
try {
|
|
pathname = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
|
|
} catch {
|
|
respond(response, 400, "Bad Request");
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/")) pathname += "index.html";
|
|
const filePath = resolve(root, `.${pathname}`);
|
|
if (filePath !== root && !filePath.startsWith(rootPrefix)) {
|
|
respond(response, 403, "Forbidden");
|
|
return;
|
|
}
|
|
|
|
let stats;
|
|
try {
|
|
stats = statSync(filePath);
|
|
} catch {
|
|
respond(response, 404, "Not Found");
|
|
return;
|
|
}
|
|
|
|
if (!stats.isFile()) {
|
|
respond(response, 404, "Not Found");
|
|
return;
|
|
}
|
|
|
|
response.writeHead(200, {
|
|
"Content-Type": contentTypes.get(extname(filePath).toLowerCase()) ?? "application/octet-stream",
|
|
"Content-Length": stats.size,
|
|
"Cache-Control": "no-store",
|
|
"X-Content-Type-Options": "nosniff",
|
|
});
|
|
|
|
if (request.method === "HEAD") {
|
|
response.end();
|
|
return;
|
|
}
|
|
|
|
const stream = createReadStream(filePath);
|
|
stream.on("error", () => response.destroy());
|
|
stream.pipe(response);
|
|
});
|
|
}
|
|
|
|
export function startEditorServer({ host = "127.0.0.1", port = 55173 } = {}) {
|
|
const server = createEditorServer();
|
|
|
|
return new Promise((resolvePromise, reject) => {
|
|
const onError = (error) => reject(error);
|
|
server.once("error", onError);
|
|
server.listen(port, host, () => {
|
|
server.off("error", onError);
|
|
const address = server.address();
|
|
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
resolvePromise({ server, url: `http://${host}:${actualPort}/` });
|
|
});
|
|
});
|
|
}
|