@allurereport/static-server
Version:
Minimalistic web-server for serving static files
172 lines (171 loc) • 5.9 kB
JavaScript
import watchDirectory from "@allurereport/directory-watcher";
import * as console from "node:console";
import { createReadStream } from "node:fs";
import { readFile, readdir, stat } from "node:fs/promises";
import { createServer } from "node:http";
import { basename, extname, join, resolve } from "node:path";
import { cwd } from "node:process";
import openUrl from "open";
import { TYPES_BY_EXTENSION, identity, injectLiveReloadScript } from "./utils.js";
export const renderDirectory = async (entries, dirPath) => {
const links = [];
const files = [];
const dirs = [];
for (const entry of entries) {
const stats = await stat(entry);
if (stats.isDirectory()) {
dirs.push(entry);
}
else {
files.push(entry);
}
}
dirs
.sort((a, b) => a.localeCompare(b))
.forEach((entry) => {
links.push(`<a href="./${basename(entry)}/">${basename(entry)}/</a>`);
});
files
.sort((a, b) => a.localeCompare(b))
.forEach((entry) => {
links.push(`<a href="./${basename(entry)}">${basename(entry)}</a>`);
});
return `
<!DOCTYPE html>
<html lang="en">
<head>
<title>Allure Server</title>
<meta charset="UTF-8" />
</head>
<body>
<p>Allure Static Server</p>
<ul>
${dirPath ? '<li><a href="../">../</a></li>' : ""}
${links.map((link) => `<li>${link}</li>`).join("")}
</ul>
</body>
</html>
`;
};
export const serve = async (options) => {
const { port, live = false, servePath, open = false } = options ?? {};
const pathToServe = servePath ? resolve(cwd(), servePath) : cwd();
const clients = new Set();
const server = createServer(async (req, res) => {
const hostHeaderIdx = req.rawHeaders.findIndex((header) => header === "Host") + 1;
const host = req.rawHeaders[hostHeaderIdx];
const { pathname, search } = new URL(`http://${host}${req.url}`);
const query = new URLSearchParams(search);
if (pathname === "/__live_reload") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
clients.add(res);
req.on("close", () => {
clients.delete(res);
});
return;
}
let fsPath = join(pathToServe, pathname.replace(/\?.*$/, ""));
let stats;
try {
stats = await stat(fsPath);
}
catch (err) {
if (err.code === "ENOENT") {
res.writeHead(404);
}
else {
res.writeHead(500);
}
return res.end();
}
if (stats.isDirectory() && !pathname.endsWith("/")) {
res.writeHead(301, {
Location: `${pathname}/`,
});
return res.end();
}
if (stats.isDirectory()) {
const files = await readdir(fsPath);
if (files.includes("index.html")) {
fsPath = join(fsPath, "index.html");
stats = await stat(fsPath);
}
else {
const html = await renderDirectory(files.map((file) => resolve(fsPath, file)), pathname !== "/");
const htmlContent = injectLiveReloadScript(html);
const byteLength = Buffer.byteLength(htmlContent);
res.writeHead(200, {
"Content-Type": "text/html",
"Content-Length": byteLength,
});
res.write(htmlContent);
return res.end();
}
}
const fileExtension = extname(fsPath);
const contentType = TYPES_BY_EXTENSION[fileExtension] ?? "application/octet-stream";
if (contentType === "text/html" && !query.has("attachment")) {
const html = await readFile(fsPath, "utf-8");
const htmlContent = injectLiveReloadScript(html);
const byteLength = Buffer.byteLength(htmlContent);
res.writeHead(200, {
"Content-Type": contentType,
"Content-Length": byteLength,
});
res.write(htmlContent);
return res.end();
}
res.writeHead(200, {
"Content-Type": contentType,
"Content-Length": stats.size,
});
createReadStream(fsPath)
.pipe(res)
.on("close", () => res.end());
});
const triggerReload = () => {
clients.forEach((client) => {
client.write("data: reload\n\n");
});
};
const serverPort = await new Promise((res) => {
server.listen(port, () => {
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("could not start a server: invalid server address is returned");
}
res(address.port);
});
});
const unwatch = live
? watchDirectory(pathToServe, triggerReload, {
ignoreInitial: true,
})
: identity;
console.info(`Allure is running on http://localhost:${serverPort}`);
if (open) {
openUrl(`http://localhost:${serverPort}`);
}
return {
url: `http://localhost:${serverPort}`,
port: serverPort,
reload: async () => {
triggerReload();
},
open: async (url) => {
if (url.startsWith("/")) {
await openUrl(new URL(url, `http://localhost:${serverPort}`).toString());
return;
}
await openUrl(url);
},
stop: async () => {
server.unref();
await unwatch();
},
};
};