// Sitegen! Clover's static site generator, built with love. function main() { return withSpinner({ text: "Recovering State", successText: ({ elapsed }) => "sitegen! update in " + elapsed.toFixed(1) + "s", failureText: () => "sitegen FAIL", }, sitegen); } async function sitegen(status) { const startTime = performance.now(); let root = path.resolve(import_meta.dirname, "../src"); const join = (...sub) => path.join(root, ...sub); const incr = new Incremental(); const sections: Section[] = require(path.join(root, "sections.ts")).siteSections; const views = []; const staticFiles = []; let pages = []; let scripts = []; const backendFiles = []; status.text = "Scanning Project"; for (const section of sections) { const { root: sectionRoot } = section; const sectionPath = (...sub) => path.join(sectionRoot, ...sub); const rootPrefix = root === sectionRoot ? "" : path.relative(root, sectionRoot) + "/"; const kinds = [ { dir: sectionPath("pages"), list: pages, prefix: "/", exclude: [".css", ".client.ts", ".client.tsx"], }, { dir: sectionPath("static"), list: staticFiles, prefix: "/", ext: true }, { dir: sectionPath("scripts"), list: scripts, prefix: rootPrefix }, { dir: sectionPath("views"), list: views, prefix: rootPrefix }, ]; for (const { dir, list, prefix, exclude = [], ext = false } of kinds) { const pages2 = fs.readDirRecOptional(dir); page: for (const page of pages2) { if (page.isDirectory()) continue; for (const ext2 of exclude) { if (page.name.endsWith(ext2)) continue page; } const file = path.relative(dir, page.parentPath + "/" + page.name); const trim = ext ? file : file.slice(0, -path.extname(file).length).replaceAll(".", "/"); let id = prefix + trim.replaceAll("\\", "/"); if (prefix === "/" && id.endsWith("/index")) { id = id.slice(0, -"/index".length) || "/"; } list.push({ id, file: path.join(page.parentPath, page.name) }); } } let backendFile = [ sectionPath("backend.ts"), sectionPath("backend.tsx"), ].find((file) => fs.existsSync(file)); if (backendFile) backendFiles.push(backendFile); } scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/)); const globalCssPath = join("global.css"); status.text = "Building"; const cssOnce = new OnceMap(); const cssQueue = new Queue({ name: "Bundle", fn: ([, files, theme]) => css.bundleCssFiles(files, theme), passive: true, getItemText: ([id]) => id, maxJobs: 2, }); const ssrResults = []; function loadSsrModule(page) { require(page.file); } async function doSsrPage(page) { const module2 = require(page.file); const Page = module2.default; if (!Page) { throw new Error("Page is missing a 'default' export."); } const metadata = module2.meta; if (!metadata) { throw new Error("Page is missing 'meta' attribute with a title."); } const theme = { bg: "#fff", fg: "#050505", primary: "#2e7dab", ...module2.theme, }; const renderedMetaPromise = Promise.resolve( typeof metadata === "function" ? metadata({ ssr: true }) : metadata, ).then((m) => meta.resolveAndRenderMetadata(m)); const cssImports = [globalCssPath, ...hot.getCssImports(page.file)]; const cssPromise = cssOnce.get( cssImports.join(":") + JSON.stringify(theme), () => cssQueue.add([page.id, cssImports, theme]), ); const sitegenApi = sg.initRender(); const body = await (0, import_ssr.ssrAsync)( /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Page, {}), { sitegen: sitegenApi, }, ); const inlineCss = await cssPromise; const renderedMeta = await renderedMetaPromise; if (!renderedMeta.includes("")) { throw new Error( "Page is missing 'meta.title'. All pages need a title tag.", ); } ssrResults.push({ body, head: renderedMeta, inlineCss, scriptFiles: Array.from(sitegenApi.scripts), page, }); } const spinnerFormat = status.format; status.format = () => ""; const moduleLoadQueue = new import_queue.Queue({ name: "Load Render Module", fn: loadSSRModule, getItemText, maxJobs: 1, }); moduleLoadQueue.addMany(pages); await moduleLoadQueue.done({ method: "stop" }); const pageQueue = new import_queue.Queue({ name: "Render", fn: doSsrPage, getItemText, maxJobs: 2, }); pageQueue.addMany(pages); await pageQueue.done({ method: "stop" }); status.format = spinnerFormat; const referencedScripts = Array.from( new Set(ssrResults.flatMap((r) => r.scriptFiles)), ); const extraPublicScripts = scripts.map((entry) => entry.file); const uniqueCount = new Set([ ...referencedScripts, ...extraPublicScripts, ]).size; status.text = `Bundle ${uniqueCount} Scripts`; await bundle.bundleClientJavaScript( referencedScripts, extraPublicScripts, incr, ); async function doStaticFile(page) { const body = await fs.readFile(page.file); incr.putAsset({ srcId: "static:" + page.file, key: page.id, body, }); } const staticQueue = new import_queue.Queue({ name: "Load Static", fn: doStaticFile, getItemText, maxJobs: 16, }); status.format = () => ""; staticQueue.addMany(staticFiles); await staticQueue.done({ method: "stop" }); status.format = spinnerFormat; status.text = `Concat ${ssrResults.length} Pages`; await Promise.all( ssrResults.map(async ({ page, body, head, inlineCss, scriptFiles }) => { const doc = wrapDocument({ body, head, inlineCss, scripts: scriptFiles.map( (id) => UNWRAP( incr.out.script.get( path.basename(id).replace(/\.client\.[jt]sx?$/, ""), ), ), ).map((x) => `{${x}}`).join(""), }); incr.putAsset({ srcId: "page:" + page.file, key: page.id, body: doc, headers: { "Content-Type": "text/html", }, }); }), ); status.format = () => ""; status.text = ``; await incr.wait(); status.format = spinnerFormat; status.text = `Incremental Flush`; incr.flush(); incr.serializeToDisk(); return { elapsed: (performance.now() - startTime) / 1e3 }; } import type { Section } from "./sitegen-lib.ts"; import { OnceMap, Queue } from "./queue.ts"; import { Incremental } from "./incremental.ts"; import * as bundle from "./bundle.ts"; import * as css from "./css.ts"; import * as fs from './fs.ts'; import { withSpinner, Spinner } from "@paperclover/console/Spinner";