alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
124 lines (104 loc) • 3.78 kB
text/typescript
import { createHash } from "node:crypto";
import { dirname } from "node:path";
import { $inject } from "alepha";
import { FileSystemProvider } from "alepha/system";
import { BuildTask, type BuildTaskContext } from "./BuildTask.ts";
/**
* Generate a static site output.
*
* Ensures index.html, 200.html, and 404.html exist in the client directory,
* then removes all server artifacts from dist, keeping only the public directory.
*/
export class BuildStaticTask extends BuildTask {
protected readonly fs = $inject(FileSystemProvider);
async run(ctx: BuildTaskContext): Promise<void> {
if (ctx.options.target !== "static") {
return;
}
const distDir = ctx.options.output?.dist ?? "dist";
const clientDir = ctx.options.output?.public ?? "public";
const publicDir = this.fs.join(ctx.root, distDir, clientDir);
await ctx.run({
name: "generate static site",
handler: async () => {
if (!ctx.alepha.isConfigured()) {
await ctx.alepha.events.emit("configure", ctx.alepha);
}
const indexPath = this.fs.join(publicDir, "index.html");
const isPrerendered = await this.fs.exists(indexPath);
if (!isPrerendered) {
await this.renderRootPage(ctx, publicDir);
}
const indexHtml = (await this.fs.readFile(indexPath)).toString();
const shell = this.stripRootContent(indexHtml);
if (!isPrerendered) {
await this.fs.writeFile(indexPath, shell);
}
const notFoundPath = this.fs.join(publicDir, "404.html");
if (!(await this.fs.exists(notFoundPath))) {
await this.fs.writeFile(notFoundPath, shell);
}
const spaPath = this.fs.join(publicDir, "200.html");
if (!(await this.fs.exists(spaPath))) {
await this.fs.writeFile(spaPath, shell);
}
const cnamePath = this.fs.join(publicDir, "CNAME");
if (!(await this.fs.exists(cnamePath))) {
const domain =
ctx.options.static?.domain ?? (await this.generateDomain(ctx.root));
await this.fs.writeFile(cnamePath, domain);
}
await this.cleanDist(this.fs.join(ctx.root, distDir), clientDir);
},
});
}
protected async renderRootPage(
ctx: BuildTaskContext,
publicDir: string,
): Promise<void> {
const pages = ctx.alepha.primitives("page") as any[];
const rootPage = pages.find(
(p) => p.options.path === "/" && !p.options.children,
);
if (!rootPage) {
return;
}
const { html } = await rootPage.render({ html: true });
const filepath = this.fs.join(publicDir, "index.html");
await this.fs.mkdir(dirname(filepath));
await this.fs.writeFile(filepath, html);
}
protected stripRootContent(html: string): string {
return html.replace(
/(<body[^>]*>)[\s\S]*?(<\/body>)/,
'$1<div id="root"></div>$2',
);
}
protected async cleanDist(distDir: string, clientDir: string): Promise<void> {
const entries = await this.fs.ls(distDir);
for (const entry of entries) {
if (entry !== clientDir) {
await this.fs.rm(this.fs.join(distDir, entry), { recursive: true });
}
}
}
protected async generateDomain(root: string): Promise<string> {
let name = "app";
try {
const content = (
await this.fs.readFile(this.fs.join(root, "package.json"))
).toString();
const pkg = JSON.parse(content);
if (pkg.name) {
name = pkg.name
.replace(/^@/, "")
.replace(/\//g, "-")
.replace(/[^a-z0-9-]/g, "");
}
} catch {
// fallback to "app"
}
const hash = createHash("sha256").update(name).digest("hex").slice(0, 6);
return `${name}-${hash}.surge.sh`;
}
}