UNPKG

@allurereport/static-server

Version:

Minimalistic web-server for serving static files

172 lines (171 loc) 5.9 kB
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(); }, }; };