Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 87faf968051cbf825a36d7ba407a5efebfaa888c
parent 1ddf9f95a156ac050c53f24e2b4ed82d4896f642
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  5 Oct 2026 12:24:05 -0400

export: `start` serves out/ with scripts/serve-out.mjs (moment dirs served, not listed)

The report-site e2e server moves from e2e-report/serve.mjs to
scripts/serve-out.mjs and gains `/x` -> `x.html` and more content types.
`start` runs it on EXPORT_DEV_PORT (3000); the e2e:report config points at
the new path. `serve` stays a dependency (the container's site service and
the two-origin suite use it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
MENVIRONMENT.md | 2+-
Mcommon/lib/ports.mjs | 2+-
Dexport/e2e-report/serve.mjs | 103-------------------------------------------------------------------------------
Mexport/package.json | 2+-
Mexport/playwright.report.config.ts | 4++--
Aexport/scripts/serve-out.mjs | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 123 insertions(+), 108 deletions(-)

diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md @@ -110,7 +110,7 @@ Every local server's default port, from `common/lib/ports.mjs`. The primary chec | `EDITOR_PORT` | `3001` | Editor real dev/start (`pnpm dev:editor`). A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `PORT` | `3011` | Editor test server + Playwright editor baseURL. A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `EXPORT_PORT` | `3010` | Export server launched by the editor e2e. A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | -| `EXPORT_DEV_PORT` | `3000` | Export real dev (`pnpm dev:export`). A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | +| `EXPORT_DEV_PORT` | `3000` | Export real dev (`pnpm dev:export`) and its built `out/` (`pnpm start:export`). A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `EXPORT_E2E_PORT` | `3020` | Export's own Playwright suite. A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `OLLAMA_STUB_PORT` | `11435` | Digest-lane stub server in the editor e2e suite. A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `HOMEPAGE_DEV_PORT` | `3030` | Homepage real dev (`pnpm dev:homepage`). A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | diff --git a/common/lib/ports.mjs b/common/lib/ports.mjs @@ -35,7 +35,7 @@ export const PORTS = Object.freeze({ EDITOR_PORT: { base: 3001, what: "editor real dev/start (`pnpm dev:editor`)" }, PORT: { base: 3011, what: "editor test server + Playwright editor baseURL" }, EXPORT_PORT: { base: 3010, what: "export server launched by the editor e2e" }, - EXPORT_DEV_PORT: { base: 3000, what: "export real dev (`pnpm dev:export`)" }, + EXPORT_DEV_PORT: { base: 3000, what: "export real dev (`pnpm dev:export`) and its built `out/` (`pnpm start:export`)" }, EXPORT_E2E_PORT: { base: 3020, what: "export's own Playwright suite" }, OLLAMA_STUB_PORT: { base: 11435, what: "digest-lane stub server in the editor e2e suite" }, HOMEPAGE_DEV_PORT: { base: 3030, what: "homepage real dev (`pnpm dev:homepage`)" }, diff --git a/export/e2e-report/serve.mjs b/export/e2e-report/serve.mjs @@ -1,103 +0,0 @@ -// A static server for the staged cited build (playwright.report.config.ts), -// serving a directory the way Cloudflare Pages and the container's Caddy do: -// `/dir/` is `dir/index.html`, `/dir` redirects to `/dir/`. -// -// Not `serve` (export's `start`): serve-handler stats a path whose last segment -// looks like it has an extension as a file, and lists the directory instead of -// serving its index.html — and a span moment's directory is named -// `<start>-<end>` in seconds with two decimals (`3126.00-3151.00`), so every -// video and audio moment page came back as a directory listing. -// -// Byte ranges are honoured (a media element asks for them). -// -// node e2e-report/serve.mjs <dir> <port> - -import fs from "node:fs"; -import http from "node:http"; -import path from "node:path"; - -const [rootArg, portArg] = process.argv.slice(2); -if (!rootArg || !portArg) { - console.error("usage: serve.mjs <dir> <port>"); - process.exit(2); -} -const ROOT = path.resolve(rootArg); -const PORT = Number(portArg); - -const TYPES = { - ".html": "text/html; charset=utf-8", - ".txt": "text/plain; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".js": "text/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".csv": "text/csv; charset=utf-8", - ".xml": "application/xml; charset=utf-8", - ".webmanifest": "application/manifest+json", - ".svg": "image/svg+xml", - ".png": "image/png", - ".ico": "image/x-icon", - ".woff2": "font/woff2", - ".mp4": "video/mp4", - ".m4a": "audio/mp4", -}; - -function statOrNull(p) { - try { - return fs.statSync(p); - } catch { - return null; - } -} - -function sendFile(req, res, file, status = 200) { - const size = fs.statSync(file).size; - const headers = { - "Content-Type": TYPES[path.extname(file).toLowerCase()] ?? "application/octet-stream", - "Accept-Ranges": "bytes", - }; - const range = status === 200 ? /^bytes=(\d*)-(\d*)$/.exec(req.headers.range ?? "") : null; - if (range && (range[1] || range[2])) { - let start = range[1] ? Number(range[1]) : size - Number(range[2]); - let end = range[1] && range[2] ? Number(range[2]) : size - 1; - start = Math.max(0, start); - end = Math.min(end, size - 1); - if (start > end) { - res.writeHead(416, { "Content-Range": `bytes */${size}` }).end(); - return; - } - res.writeHead(206, { ...headers, "Content-Range": `bytes ${start}-${end}/${size}`, "Content-Length": end - start + 1 }); - if (req.method === "HEAD") return void res.end(); - fs.createReadStream(file, { start, end }).pipe(res); - return; - } - res.writeHead(status, { ...headers, "Content-Length": size }); - if (req.method === "HEAD") return void res.end(); - fs.createReadStream(file).pipe(res); -} - -http - .createServer((req, res) => { - const url = new URL(req.url ?? "/", "http://localhost"); - let pathname; - try { - pathname = decodeURIComponent(url.pathname); - } catch { - return void res.writeHead(400).end(); - } - const file = path.join(ROOT, pathname); - if (file !== ROOT && !file.startsWith(ROOT + path.sep)) return void res.writeHead(403).end(); - const st = statOrNull(file); - if (st?.isDirectory()) { - if (!pathname.endsWith("/")) { - return void res.writeHead(308, { Location: `${url.pathname}/${url.search}` }).end(); - } - const index = path.join(file, "index.html"); - if (statOrNull(index)?.isFile()) return sendFile(req, res, index); - } else if (st?.isFile()) { - return sendFile(req, res, file); - } - const notFound = path.join(ROOT, "404.html"); - if (statOrNull(notFound)?.isFile()) return sendFile(req, res, notFound, 404); - res.writeHead(404).end(); - }) - .listen(PORT, () => console.log(`[report-site] serving ${ROOT} on http://localhost:${PORT}`)); diff --git a/export/package.json b/export/package.json @@ -14,7 +14,7 @@ "compose:hub": "tsx ../common/bin/archilyzer.ts compose hub", "build": "tsx ../common/bin/archilyzer.ts build site", "build:hub": "tsx ../common/bin/archilyzer.ts build hub", - "start": "serve out", + "start": "node scripts/serve-out.mjs out ${EXPORT_DEV_PORT:-3000}", "lint": "eslint", "test": "tsx --test \"app/**/*.test.ts\"", "e2e": "node ../scripts/queue-lock.mjs --ports EXPORT_E2E_PORT:3020 -- playwright test", diff --git a/export/playwright.report.config.ts b/export/playwright.report.config.ts @@ -9,7 +9,7 @@ import { REPORT_SITE_OUT, stageReportSite } from "./e2e-report/stage"; // The CITED report-site suite: a real static export of a cited fixture site // (one fact-check citing every kind, its moments, their media), staged and // built OUTSIDE export/ — e2e-report/stage.ts says why — and served as Pages -// serves a deployed site (e2e-report/serve.mjs; not `serve`, which lists a +// serves a deployed site (scripts/serve-out.mjs; not `serve`, which lists a // span moment's directory instead of serving its page). What the specs see is // what a cited build ships, and audit.spec.ts holds the same out/ to the cited // allowlist audit. @@ -34,7 +34,7 @@ export default defineConfig({ fullyParallel: false, workers: 1, webServer: { - command: `node e2e-report/serve.mjs ${REPORT_SITE_OUT} ${PORT}`, + command: `node scripts/serve-out.mjs ${REPORT_SITE_OUT} ${PORT}`, url: `${baseURL}/site.json`, timeout: 60_000, reuseExistingServer: !process.env.CI, diff --git a/export/scripts/serve-out.mjs b/export/scripts/serve-out.mjs @@ -0,0 +1,118 @@ +// A static server for a built site (export's `start`, and the report-site e2e +// in playwright.report.config.ts), serving a directory the way Cloudflare +// Pages does: `/dir/` is `dir/index.html`, `/dir` redirects to `/dir/`, `/x` +// is `x.html` when there is one. +// +// Not `serve`: serve-handler stats a path whose last segment looks like it has +// an extension as a file, and lists the directory instead of serving its +// index.html — and a span moment's directory is named `<start>-<end>` in +// seconds with two decimals (`3126.00-3151.00`), so every video and audio +// moment page came back as a directory listing. +// +// Byte ranges are honoured (a media element asks for them). +// +// node scripts/serve-out.mjs <dir> <port> + +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; + +const [rootArg, portArg] = process.argv.slice(2); +if (!rootArg || !portArg) { + console.error("usage: serve-out.mjs <dir> <port>"); + process.exit(2); +} +const ROOT = path.resolve(rootArg); +const PORT = Number(portArg); + +const TYPES = { + ".html": "text/html; charset=utf-8", + ".txt": "text/plain; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".csv": "text/csv; charset=utf-8", + ".xml": "application/xml; charset=utf-8", + ".webmanifest": "application/manifest+json", + ".svg": "image/svg+xml", + ".png": "image/png", + ".ico": "image/x-icon", + ".woff2": "font/woff2", + ".woff": "font/woff", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".gif": "image/gif", + ".mp4": "video/mp4", + ".webm": "video/webm", + ".m4a": "audio/mp4", + ".mp3": "audio/mpeg", + ".vtt": "text/vtt; charset=utf-8", + ".srt": "text/plain; charset=utf-8", + ".md": "text/markdown; charset=utf-8", + ".zip": "application/zip", + ".wasm": "application/wasm", +}; + +function statOrNull(p) { + try { + return fs.statSync(p); + } catch { + return null; + } +} + +function sendFile(req, res, file, status = 200) { + const size = fs.statSync(file).size; + const headers = { + "Content-Type": TYPES[path.extname(file).toLowerCase()] ?? "application/octet-stream", + "Accept-Ranges": "bytes", + }; + const range = status === 200 ? /^bytes=(\d*)-(\d*)$/.exec(req.headers.range ?? "") : null; + if (range && (range[1] || range[2])) { + let start = range[1] ? Number(range[1]) : size - Number(range[2]); + let end = range[1] && range[2] ? Number(range[2]) : size - 1; + start = Math.max(0, start); + end = Math.min(end, size - 1); + if (start > end) { + res.writeHead(416, { "Content-Range": `bytes */${size}` }).end(); + return; + } + res.writeHead(206, { ...headers, "Content-Range": `bytes ${start}-${end}/${size}`, "Content-Length": end - start + 1 }); + if (req.method === "HEAD") return void res.end(); + fs.createReadStream(file, { start, end }).pipe(res); + return; + } + res.writeHead(status, { ...headers, "Content-Length": size }); + if (req.method === "HEAD") return void res.end(); + fs.createReadStream(file).pipe(res); +} + +http + .createServer((req, res) => { + const url = new URL(req.url ?? "/", "http://localhost"); + let pathname; + try { + pathname = decodeURIComponent(url.pathname); + } catch { + return void res.writeHead(400).end(); + } + const file = path.join(ROOT, pathname); + if (file !== ROOT && !file.startsWith(ROOT + path.sep)) return void res.writeHead(403).end(); + const st = statOrNull(file); + if (st?.isDirectory()) { + if (!pathname.endsWith("/")) { + return void res.writeHead(308, { Location: `${url.pathname}/${url.search}` }).end(); + } + const index = path.join(file, "index.html"); + if (statOrNull(index)?.isFile()) return sendFile(req, res, index); + } else if (st?.isFile()) { + return sendFile(req, res, file); + } else if (!pathname.endsWith("/") && statOrNull(`${file}.html`)?.isFile()) { + return sendFile(req, res, `${file}.html`); + } + const notFound = path.join(ROOT, "404.html"); + if (statOrNull(notFound)?.isFile()) return sendFile(req, res, notFound, 404); + res.writeHead(404).end(); + }) + .listen(PORT, () => console.log(`[serve-out] serving ${ROOT} on http://localhost:${PORT}`));