Files

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}/` });
});
});
}