// Sitegen! Clover's static site generator, built with love.
export function main() {
return withSpinner({
text: "Recovering State",
successText: ({ elapsed }) =>
"sitegen! update in " + elapsed.toFixed(1) + "s",
failureText: () => "sitegen FAIL",
}, sitegen);
}
/**
* A filesystem object associated with some ID,
* such as a page's route to it's source file.
*/
interface FileItem {
id: string;
file: string;
}
async function sitegen(status: Spinner) {
const startTime = performance.now();
let root = path.resolve(import.meta.dirname, "../src");
const join = (...sub: string[]) => path.join(root, ...sub);
const incr = new Incremental();
// Sitegen reviews every defined section for resources to process
const sections: Section[] =
require(path.join(root, "sections.ts")).siteSections;
// Static files are compressed and served as-is.
// - "{section}/static/*.png"
let staticFiles: FileItem[] = [];
// Pages are rendered then served as static files.
// - "{section}/pages/*.marko"
let pages: FileItem[] = [];
// Views are dynamically rendered pages called via backend code.
// - "{section}/views/*.tsx"
let views: FileItem[] = [];
// Public scripts are bundled for the client as static assets under "/js/[...]"
// This is used for the file viewer's canvases.
// Note that '.client.ts' can be placed anywhere in the file structure.
// - "{section}/scripts/*.client.ts"
let scripts: FileItem[] = [];
// 'backend.ts'
const backendFiles = [];
// -- Scan for files --
status.text = "Scanning Project";
for (const section of sections) {
const { root: sectionRoot } = section;
const sectionPath = (...sub: string[]) => 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,
exclude: [".css", ".client.ts", ".client.tsx"],
},
];
for (const { dir, list, prefix, exclude = [], ext = false } of kinds) {
const items = fs.readDirRecOptionalSync(dir);
item: for (const item of items) {
if (item.isDirectory()) continue;
for (const e of exclude) {
if (item.name.endsWith(e)) continue item;
}
const file = path.relative(dir, item.parentPath + "/" + item.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(item.parentPath, item.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");
// TODO: invalidate incremental resources
// -- server side render --
status.text = "Building";
const cssOnce = new OnceMap();
const cssQueue = new Queue<[string, string[], css.Theme], string>({
name: "Bundle",
fn: ([, files, theme]) => css.bundleCssFiles(files, theme),
passive: true,
getItemText: ([id]) => id,
maxJobs: 2,
});
interface RenderResult {
body: string;
head: string;
inlineCss: string;
scriptFiles: string[];
item: FileItem;
}
const renderResults: RenderResult[] = [];
async function loadPageModule({ file }: FileItem) {
require(file);
}
async function renderPage(item: FileItem) {
// -- load and validate module --
let { default: Page, meta: metadata, theme: pageTheme, layout } = require(
item.file,
);
if (!Page) throw new Error("Page is missing a 'default' export.");
if (!metadata) {
throw new Error("Page is missing 'meta' export with a title.");
}
if (layout?.theme) pageTheme = layout.theme;
const theme = {
bg: "#fff",
fg: "#050505",
primary: "#2e7dab",
...pageTheme,
};
// -- metadata --
const renderedMetaPromise = Promise.resolve(
typeof metadata === "function" ? metadata({ ssr: true }) : metadata,
).then((m) => meta.resolveAndRenderMetadata(m));
// -- css --
const cssImports = [globalCssPath, ...hot.getCssImports(item.file)];
const cssPromise = cssOnce.get(
cssImports.join(":") + JSON.stringify(theme),
() => cssQueue.add([item.id, cssImports, theme]),
);
// -- html --
const sitegenApi = sg.initRender();
const bodyPromise = await ssr.ssrAsync(