website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
tests/helpers/static-server.js
| 09c6a56 | 1 | import { createServer } from "node:http"; |
| 09c6a56 | 2 | import { readFile, stat } from "node:fs/promises"; |
| 09c6a56 | 3 | import path from "node:path"; |
| 09c6a56 | 4 | |
| 09c6a56 | 5 | const MIME_TYPES = { |
| 09c6a56 | 6 | ".html": "text/html; charset=utf-8", |
| 09c6a56 | 7 | ".css": "text/css; charset=utf-8", |
| 09c6a56 | 8 | ".js": "text/javascript; charset=utf-8", |
| 09c6a56 | 9 | ".json": "application/json; charset=utf-8", |
| 09c6a56 | 10 | ".svg": "image/svg+xml", |
| 09c6a56 | 11 | ".png": "image/png", |
| 09c6a56 | 12 | ".jpg": "image/jpeg", |
| 09c6a56 | 13 | ".jpeg": "image/jpeg", |
| 09c6a56 | 14 | ".ico": "image/x-icon", |
| 09c6a56 | 15 | ".xml": "application/xml; charset=utf-8", |
| 09c6a56 | 16 | ".txt": "text/plain; charset=utf-8", |
| 09c6a56 | 17 | ".woff2": "font/woff2", |
| 09c6a56 | 18 | ".pdf": "application/pdf", |
| 09c6a56 | 19 | }; |
| 09c6a56 | 20 | |
| 09c6a56 | 21 | async function resolveFile(rootDir, urlPath) { |
| 09c6a56 | 22 | const decoded = decodeURIComponent(urlPath.split("?")[0]); |
| 09c6a56 | 23 | const safeSuffix = path.normalize(decoded).replace(/^(\.\.[/\\])+/, ""); |
| 09c6a56 | 24 | const requestedPath = path.join(rootDir, safeSuffix); |
| 09c6a56 | 25 | |
| 09c6a56 | 26 | const candidates = requestedPath.endsWith("/") |
| 09c6a56 | 27 | ? [path.join(requestedPath, "index.html")] |
| 09c6a56 | 28 | : [requestedPath, path.join(requestedPath, "index.html"), `${requestedPath}.html`]; |
| 09c6a56 | 29 | |
| 09c6a56 | 30 | for (const candidate of candidates) { |
| 09c6a56 | 31 | try { |
| 09c6a56 | 32 | const info = await stat(candidate); |
| 09c6a56 | 33 | if (info.isFile()) return candidate; |
| 09c6a56 | 34 | } catch { |
| 09c6a56 | 35 | // try next candidate |
| 09c6a56 | 36 | } |
| 09c6a56 | 37 | } |
| 09c6a56 | 38 | return null; |
| 09c6a56 | 39 | } |
| 09c6a56 | 40 | |
| 09c6a56 | 41 | /** |
| 09c6a56 | 42 | * Serves a pre-built static site directory (e.g. Eleventy's `dist/`) over HTTP, |
| 09c6a56 | 43 | * falling back to `404.html` so tests see the same not-found page as production. |
| 09c6a56 | 44 | */ |
| 09c6a56 | 45 | export async function startStaticServer(rootDir, { notFoundFile = "404.html" } = {}) { |
| 09c6a56 | 46 | const server = createServer(async (req, res) => { |
| 09c6a56 | 47 | try { |
| 09c6a56 | 48 | const filePath = await resolveFile(rootDir, req.url ?? "/"); |
| 09c6a56 | 49 | if (filePath) { |
| 09c6a56 | 50 | const body = await readFile(filePath); |
| 09c6a56 | 51 | res.writeHead(200, { "content-type": MIME_TYPES[path.extname(filePath)] ?? "application/octet-stream" }); |
| 09c6a56 | 52 | res.end(body); |
| 09c6a56 | 53 | return; |
| 09c6a56 | 54 | } |
| 09c6a56 | 55 | const fallback = path.join(rootDir, notFoundFile); |
| 09c6a56 | 56 | try { |
| 09c6a56 | 57 | const body = await readFile(fallback); |
| 09c6a56 | 58 | res.writeHead(404, { "content-type": "text/html; charset=utf-8" }); |
| 09c6a56 | 59 | res.end(body); |
| 09c6a56 | 60 | } catch { |
| 09c6a56 | 61 | res.writeHead(404, { "content-type": "text/plain" }); |
| 09c6a56 | 62 | res.end("Not found"); |
| 09c6a56 | 63 | } |
| 09c6a56 | 64 | } catch (err) { |
| 09c6a56 | 65 | res.writeHead(500, { "content-type": "text/plain" }); |
| 09c6a56 | 66 | res.end(String(err)); |
| 09c6a56 | 67 | } |
| 09c6a56 | 68 | }); |
| 09c6a56 | 69 | |
| 09c6a56 | 70 | await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); |
| 09c6a56 | 71 | const { port } = server.address(); |
| 09c6a56 | 72 | return { |
| 09c6a56 | 73 | url: `http://127.0.0.1:${port}`, |
| 09c6a56 | 74 | close: () => new Promise((resolve) => server.close(resolve)), |
| 09c6a56 | 75 | }; |
| 09c6a56 | 76 | } |