hornwitser.no/cli.ts
Hornwitser 82323c9158 Add development HTTP server
Add an HTTP server for development purposes that replies to requests
with both the static resources and on the fly generated pages.  This
serves the following needs.

- Modern module scripts requires an origin supporting CORS policies,
  which is not supported when pages are read from disk by the browser.

- Provide a way for implemting automatic reloading of pages when changes
  are made to the source files.

- Act as the starting point for making interactive features such as
  comments and analytics.
2025-02-02 10:03:55 +01:00

106 lines
2.9 KiB
TypeScript

import * as fs from "node:fs";
import * as posix from "node:path/posix";
import { prettify, htmlDocument } from "antihtml";
import { pages } from "./content/pages.js";
import type { Page } from "./content/types.js";
import { resolveRefs } from "./utils/resolve-refs.js";
import { createServer } from "./utils/http-server.js";
function pageToHtml(page: Page) {
if (!page.ref.startsWith("/")) {
throw new Error(`ref "${page.ref}" for "${page.title}" is not absolute.`);
}
return htmlDocument(
prettify(
resolveRefs(
page.content,
posix.dirname(page.ref),
)
)
)
}
function assembleResources() {
const webDir = "public";
const resources = new Map<string, string | Page>();
for (const entry of fs.readdirSync(webDir, { recursive: true, withFileTypes: true })) {
if (!entry.isFile())
continue;
const parentPath = entry.parentPath.replace(/\\/g, "/");
const ref = `${parentPath.slice(webDir.length)}/${entry.name}`;
resources.set(ref, `${parentPath}/${entry.name}`);
}
for (const page of pages) {
if (resources.has(page.ref)) {
const existing = resources.get(page.ref)!;
const other = typeof existing === "string" ? "a static resource" : `"${existing.title}"`;
throw new Error(`ref "${page.ref}" is taken up by both "${page.title}" and ${other}`)
}
resources.set(page.ref, page);
}
return resources;
}
function build() {
const outDir = "build/web";
const dirsCreated = new Set<string>()
for (const [ref, resource] of assembleResources()) {
const refDir = `${outDir}${posix.dirname(ref)}`;
if (!dirsCreated.has(refDir)) {
fs.mkdirSync(refDir, { recursive: true });
}
console.log(`writing ${outDir}${ref}`);
let content;
if (typeof resource === "string") {
content = fs.readFileSync(resource);
} else {
content = pageToHtml(resource);
}
fs.writeFileSync(`${outDir}${ref}`, content);
}
}
function serve() {
const resources = assembleResources();
const server = createServer(
(ref) => {
const resource = resources.get(ref);
if (resource === undefined)
return undefined;
if (typeof resource === "string")
return fs.readFileSync(resource);
return Buffer.from(pageToHtml(resource));
}
);
server.listen(8080);
console.log("Listening on http://localhost:8080");
}
function printUsage() {
console.log("Usage: cli.js <cmd>");
console.log(" build - Copy resources and generated pages to build directory.");
console.log(" serve - Host website on localhost:8080 for development purposes.");
}
function main(runtime: string, script: string, args: string[]) {
if (!args.length) {
printUsage();
process.exitCode = 1;
return;
}
const [command, ...commandArgs] = args;
if (command === "build") {
build();
} else if (command === "serve") {
serve();
} else {
console.log(`Error: Unkown sub-command ${command}`);
printUsage();
process.exitCode = 1;
}
}
const [runtime, script, ...args] = process.argv;
main(runtime, script, args)