mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
206 lines
6.5 KiB
TypeScript
206 lines
6.5 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { $ } from "bun";
|
|
import { fingerprintPaths } from "./build-change-detector";
|
|
|
|
const ROOT = join(import.meta.dir, "..");
|
|
const CACHE_PATH = join(ROOT, "data/_generated/build-smart-cache.json");
|
|
const GENERATED_WIDGETS = join(ROOT, "frontend/src/generated/chapter-widgets.ts");
|
|
const GENERATED_SUBJECT_MODULES = join(ROOT, "frontend/src/generated/subject-modules.ts");
|
|
const BUNDLE_PATH = join(ROOT, "themes/openfreebooks/static/js/bundle.js");
|
|
const PUBLIC_INDEX = join(ROOT, "public/index.html");
|
|
const CHAPTER_LASTMOD_PATH = join(ROOT, "data/_generated/chapter-lastmod.json");
|
|
const PAGEFIND_DIR = join(ROOT, "static/pagefind");
|
|
|
|
type BuildCache = {
|
|
version: 1;
|
|
fingerprints: {
|
|
syncInputs: string;
|
|
jsInputs: string;
|
|
siteInputs: string;
|
|
metadataInputs: string;
|
|
generatedModules: string;
|
|
};
|
|
};
|
|
|
|
async function listCurriculumFiles(): Promise<string[]> {
|
|
const dataDir = join(ROOT, "data");
|
|
const entries = await readdir(dataDir, { withFileTypes: true });
|
|
return entries
|
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
.map((entry) => join(dataDir, entry.name));
|
|
}
|
|
|
|
async function listContentJsInputs(): Promise<string[]> {
|
|
const contentDir = join(ROOT, "content");
|
|
const inputs: string[] = [];
|
|
const queue = [contentDir];
|
|
while (queue.length) {
|
|
const current = queue.pop();
|
|
if (!current) continue;
|
|
const entries = await readdir(current, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.name.startsWith(".")) continue;
|
|
const abs = join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
queue.push(abs);
|
|
continue;
|
|
}
|
|
if (entry.name === "subject.ts" || abs.includes("/widgets/")) {
|
|
inputs.push(abs);
|
|
}
|
|
}
|
|
}
|
|
return inputs;
|
|
}
|
|
|
|
async function listStaticSourceInputs(): Promise<string[]> {
|
|
const staticDir = join(ROOT, "static");
|
|
if (!existsSync(staticDir)) return [];
|
|
const inputs: string[] = [];
|
|
const queue = [staticDir];
|
|
while (queue.length) {
|
|
const current = queue.pop();
|
|
if (!current) continue;
|
|
const entries = await readdir(current, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.name.startsWith(".")) continue;
|
|
const abs = join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
if (abs === join(staticDir, "pagefind")) continue;
|
|
if (abs === join(staticDir, "chapters")) continue;
|
|
if (abs === join(staticDir, "css")) continue;
|
|
queue.push(abs);
|
|
continue;
|
|
}
|
|
inputs.push(abs);
|
|
}
|
|
}
|
|
return inputs;
|
|
}
|
|
|
|
async function readCache(): Promise<BuildCache | null> {
|
|
if (!existsSync(CACHE_PATH)) return null;
|
|
try {
|
|
const parsed = JSON.parse(await readFile(CACHE_PATH, "utf8")) as BuildCache;
|
|
if (parsed.version !== 1) return null;
|
|
return parsed;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function runStep(command: string) {
|
|
const result = await $`bash -lc ${command}`;
|
|
if (result.exitCode !== 0) {
|
|
process.exit(result.exitCode);
|
|
}
|
|
}
|
|
|
|
async function runFullBuild() {
|
|
await runStep("bun run check:static-html");
|
|
await runStep("bun run validate:curriculum");
|
|
await runStep("rm -f themes/openfreebooks/static/main.css public/main.css");
|
|
await runStep("bun run sync:chapters -- --full");
|
|
await runStep("vite build");
|
|
await runStep("bun run generate:site-metadata");
|
|
await runStep("zola build");
|
|
await runStep("bun run index:search");
|
|
}
|
|
|
|
async function main() {
|
|
if (process.env.CI === "true") {
|
|
await runFullBuild();
|
|
return;
|
|
}
|
|
|
|
const curriculumFiles = await listCurriculumFiles();
|
|
const contentJsInputs = await listContentJsInputs();
|
|
const staticSourceInputs = await listStaticSourceInputs();
|
|
const fingerprints = {
|
|
syncInputs: await fingerprintPaths(ROOT, [
|
|
join(ROOT, "content"),
|
|
join(ROOT, "scripts/sync-chapters.ts"),
|
|
join(ROOT, "scripts/build-change-detector.ts"),
|
|
...curriculumFiles,
|
|
]),
|
|
jsInputs: await fingerprintPaths(ROOT, [
|
|
join(ROOT, "frontend"),
|
|
join(ROOT, "vite.config.ts"),
|
|
...contentJsInputs,
|
|
]),
|
|
siteInputs: await fingerprintPaths(ROOT, [
|
|
join(ROOT, "content"),
|
|
join(ROOT, "themes/openfreebooks/templates"),
|
|
join(ROOT, "themes/openfreebooks/sass"),
|
|
join(ROOT, "zola.toml"),
|
|
...staticSourceInputs,
|
|
]),
|
|
metadataInputs: await fingerprintPaths(ROOT, [
|
|
join(ROOT, "content"),
|
|
join(ROOT, "zola.toml"),
|
|
join(ROOT, "scripts/generate-site-metadata.ts"),
|
|
...curriculumFiles,
|
|
]),
|
|
generatedModules: "",
|
|
};
|
|
|
|
const previous = await readCache();
|
|
const previousFingerprints = previous?.fingerprints;
|
|
const firstRun = !previous;
|
|
const hasGeneratedModules = existsSync(GENERATED_WIDGETS) && existsSync(GENERATED_SUBJECT_MODULES);
|
|
const needsSync =
|
|
firstRun || !hasGeneratedModules || previousFingerprints?.syncInputs !== fingerprints.syncInputs;
|
|
|
|
await runStep("bun run check:static-html");
|
|
await runStep("bun run validate:curriculum");
|
|
|
|
if (needsSync) {
|
|
await runStep("bun run sync:chapters");
|
|
}
|
|
|
|
fingerprints.generatedModules = await fingerprintPaths(ROOT, [GENERATED_WIDGETS, GENERATED_SUBJECT_MODULES]);
|
|
|
|
const needsJs =
|
|
firstRun ||
|
|
!existsSync(BUNDLE_PATH) ||
|
|
previousFingerprints?.jsInputs !== fingerprints.jsInputs ||
|
|
previousFingerprints?.generatedModules !== fingerprints.generatedModules;
|
|
if (needsJs) {
|
|
await runStep("rm -f themes/openfreebooks/static/main.css public/main.css");
|
|
await runStep("vite build");
|
|
}
|
|
|
|
const needsMetadata =
|
|
firstRun ||
|
|
!existsSync(CHAPTER_LASTMOD_PATH) ||
|
|
previousFingerprints?.metadataInputs !== fingerprints.metadataInputs;
|
|
if (needsMetadata) {
|
|
await runStep("bun run generate:site-metadata");
|
|
}
|
|
|
|
const needsSite =
|
|
firstRun ||
|
|
!existsSync(PUBLIC_INDEX) ||
|
|
needsJs ||
|
|
needsMetadata ||
|
|
previousFingerprints?.siteInputs !== fingerprints.siteInputs;
|
|
if (needsSite) {
|
|
await runStep("zola build");
|
|
}
|
|
|
|
if (needsSite || !existsSync(PAGEFIND_DIR)) {
|
|
await runStep("bun run index:search");
|
|
}
|
|
|
|
console.log(
|
|
`[build-smart] sync=${needsSync} js=${needsJs} metadata=${needsMetadata} site=${needsSite} firstRun=${firstRun}`
|
|
);
|
|
|
|
const nextCache: BuildCache = { version: 1, fingerprints };
|
|
await mkdir(dirname(CACHE_PATH), { recursive: true });
|
|
await writeFile(CACHE_PATH, `${JSON.stringify(nextCache, null, 2)}\n`);
|
|
}
|
|
|
|
main();
|