hornwitser.no/src/cli.ts

136 lines
3.6 KiB
TypeScript
Raw Normal View History

import { type ChildProcess, fork } from "node:child_process";
import * as fs from "node:fs";
import * as posix from "node:path/posix";
import { prettify, htmlDocument } from "antihtml";
import { pages } from "./pages.js";
import type { Page } from "./types.js";
import { resolveRefs } from "./utils/resolve-refs.js";
import { createServer } from "./utils/http-server.js";
const srcDir = "build/node";
const webDir = "web";
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 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 watch(script: string) {
let child: ChildProcess;
function start() {
child = fork(script, ["serve"]);
}
function restart() {
if (child.exitCode === null) {
child.kill();
child.once("close", start);
} else {
start();
}
}
let restartTimeout: ReturnType<typeof setTimeout>;
function onChange() {
clearTimeout(restartTimeout);
restartTimeout = setTimeout(restart, 100)
}
fs.watch(webDir, { recursive: true }, onChange);
fs.watch(srcDir, { recursive: true }, onChange);
start();
}
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.");
console.log(" watch - Run serve and automatically restart it on changes.");
}
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 if (command === "watch") {
watch(script);
} else {
console.log(`Error: Unkown sub-command ${command}`);
printUsage();
process.exitCode = 1;
}
}
const [runtime, script, ...args] = process.argv;
main(runtime, script, args)