const fs = require("fs"); const http = require("http"); const path = require("path"); const { repoRoot } = require("./lib/meals"); const DEFAULT_HOST = "127.0.0.1"; const DEFAULT_PORT = 4321; const MIME_TYPES = { ".css": "text/css; charset=utf-8", ".gif": "image/gif", ".html": "text/html; charset=utf-8", ".ico": "image/x-icon", ".jpg": "image/jpeg", ".js": "application/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".map": "application/json; charset=utf-8", ".mp3": "audio/mpeg", ".png": "image/png", ".svg": "image/svg+xml", ".ttf": "font/ttf", ".txt": "text/plain; charset=utf-8", ".webmanifest": "application/manifest+json; charset=utf-8", ".woff": "font/woff", ".woff2": "font/woff2", }; function parseArgs(argv) { const options = { host: process.env.HOST || DEFAULT_HOST, port: Number.parseInt(process.env.PORT || String(DEFAULT_PORT), 10), }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; const value = argv[index + 1]; if (arg === "--host" && value) { options.host = value; index += 1; continue; } if (arg === "--port" && value) { options.port = Number.parseInt(value, 10); index += 1; } } if (!Number.isInteger(options.port) || options.port <= 0 || options.port > 65535) { throw new Error("Port must be an integer between 1 and 65535"); } return options; } function getContentType(filePath) { return MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream"; } function resolveRequestPath(requestUrl) { const url = new URL(requestUrl, "http://localhost"); const pathname = decodeURIComponent(url.pathname); const requestedPath = pathname === "/" ? "/index.html" : pathname; const absolutePath = path.resolve(repoRoot, `.${requestedPath}`); const withinRepo = absolutePath === repoRoot || absolutePath.startsWith(`${repoRoot}${path.sep}`); if (!withinRepo) { return null; } if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) { return path.join(absolutePath, "index.html"); } return absolutePath; } function sendFile(response, filePath) { const stream = fs.createReadStream(filePath); response.writeHead(200, { "Content-Type": getContentType(filePath), "Cache-Control": "no-cache", }); stream.pipe(response); stream.on("error", () => { response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" }); response.end("Failed to read file"); }); } function createServer() { return http.createServer((request, response) => { const filePath = resolveRequestPath(request.url || "/"); if (!filePath) { response.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" }); response.end("Forbidden"); return; } if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); response.end("Not found"); return; } sendFile(response, filePath); }); } function main() { const options = parseArgs(process.argv.slice(2)); const server = createServer(); server.listen(options.port, options.host, () => { console.log(`Serving ${repoRoot} at http://${options.host}:${options.port}`); }); } if (require.main === module) { try { main(); } catch (error) { console.error(error.message); process.exitCode = 1; } }