core/coordinators/page.coordinator.ts
core/coordinators/page.coordinator.ts is a file in Bane's Lab Build Scripts. 148 lines of code and 24 definitions.
import {
CATALOG_SITEMAP_ROUTE,
JSON_ROUTE,
MARKDOWN_EXTENSION,
SEARCH_KEY_FILE,
SECTION_PART_WEIGHT,
SECURITY_KEY_FILE,
SECURITY_TXT_FILE,
SOURCE_PART_CHARS,
} from "#configuration/constants/site.constants";
import type {
Loaded,
Prerendered,
RecordBodyRenderer,
RecordRegistry,
SectionSelector,
SourceBodyRenderer,
} from "#types/page.types";
import { sourceSizeOf, sourceTextOf } from "#core/loaders/source.loader";
import {
writeDiscovery,
writePages,
writeRecordPages,
writeSectionPages,
writeSitemapIndex,
writeSitemapParts,
writeSourcePages,
writeText,
} from "#core/persistence/page.persistence";
import { ANATOMY_PAGE } from "@banes-lab/web/ids/page.ids";
import { ANATOMY_TREE_DECLARATIONS } from "@banes-lab/web/registries/anatomy.tree.registry";
import type { BuiltOntology } from "#types/ontology.types";
import { COMPANY_EMAIL } from "@banes-lab/web/strings/company.strings";
import { RUNNER_MODULES } from "#configuration/constants/loader.constants";
import type { SourceRoute } from "#types/source.types";
import { buildCatalog } from "#core/coordinators/catalog.coordinator";
import { closureReport } from "#core/resolvers/catalog.resolver";
import { dirname } from "node:path";
import { discover } from "#core/converters/site.converter";
import { distinctSections } from "#core/converters/section.page.converter";
import { loadFrom } from "#core/loaders/site.loader";
import { loadWebModules } from "#core/loaders/catalog.loader";
import { mkdirSync } from "node:fs";
import { persistJournal } from "#core/persistence/journal.persistence";
import { renderSecurityTxt } from "#core/formatters/site.formatter";
import { signText } from "#core/adapters/security.adapter";
import { sourcePartsOf } from "#core/converters/source.segment.converter";
import { sourceRoutes } from "#core/converters/source.route.converter";
import { stampLedger } from "#core/persistence/route.persistence";
import { textOf } from "@ssot/secrets";
import { withModuleServer } from "#core/factories/server.factory";
import { writeCatalog } from "#core/persistence/catalog.persistence";
import { writeVerbatim } from "@govlab/canonical-write";
const JSON_INDENT = 4;
const sourceRoutesOf = async function sourceRoutesOf(loaded: Loaded): Promise<readonly SourceRoute[]> {
const web = await loadWebModules(loaded.runner);
const licenses = new Map(ANATOMY_TREE_DECLARATIONS.map((declaration) => [declaration.tab, declaration.license]));
const trees = web.anatomy.ANATOMY_TREES.map((tree) => ({ ...tree, label: web.source.treeLabelOf(tree.tab) }));
const routes = sourceRoutes(trees, {
languageOf: web.source.languageOf,
license: (tab) => licenses.get(tab) ?? null,
localPath: web.folder.localPath,
nodeRoute: web.source.nodeRoute,
sourceTitle: web.source.sourceTitle,
tabPath: (tab) => loaded.links.tabLink(ANATOMY_PAGE, tab),
});
return routes.flatMap((route) =>
route.text !== null && sourceSizeOf(route.text) > SOURCE_PART_CHARS
? sourcePartsOf(route, sourceTextOf(route.text) ?? "", SOURCE_PART_CHARS)
: [route],
);
};
export const prerenderSite = async function prerenderSite(
root: string,
outDir: string,
ontology: BuiltOntology,
): Promise<Prerendered> {
return withModuleServer(root, async (server) => {
const loaded = await loadFrom(server);
const registry = await loaded.runner.import<RecordRegistry>(RUNNER_MODULES.recordRegistry.path);
registry.registerRecords([...ontology.collections.values()].flatMap((index) => Object.keys(index)));
const discovery = await discover(loaded);
const stamped = await stampLedger(discovery, new Date());
await writePages(outDir, loaded, discovery, stamped.dates);
const pageSitemap = writeDiscovery(outDir, discovery, stamped);
writeText(outDir, SEARCH_KEY_FILE, textOf("INDEXNOW_KEY"));
const security = signText(
textOf("SECURITY_PGP_KEY"),
renderSecurityTxt(discovery.site, COMPANY_EMAIL, new Date()),
);
writeText(outDir, SECURITY_TXT_FILE, security.signed);
writeText(outDir, SECURITY_KEY_FILE, security.publicKey);
const { files, journal, records, unresolved } = await buildCatalog(
loaded.runner,
discovery,
loaded.exportSections,
ontology,
);
const catalog = writeCatalog(outDir, files);
await persistJournal(journal);
mkdirSync(dirname(closureReport()), { recursive: true });
writeVerbatim(closureReport(), JSON.stringify(unresolved, null, JSON_INDENT));
const leaves = [...files.keys()].filter(
(address) => !address.startsWith(JSON_ROUTE) && address.endsWith(MARKDOWN_EXTENSION),
);
writeSitemapParts(
outDir,
CATALOG_SITEMAP_ROUTE,
leaves.toSorted((left, right) => left.localeCompare(right)).map((address) => discovery.site + address),
);
const sources = await sourceRoutesOf(loaded);
const body = await loaded.runner.import<SourceBodyRenderer>(RUNNER_MODULES.sourceBody.path);
const sourceSitemaps = writeSourcePages(
outDir,
{ body, main: loaded.main, renderer: loaded.renderer, site: discovery.site },
sources,
);
const recordBody = await loaded.runner.import<RecordBodyRenderer>(RUNNER_MODULES.recordBody.path);
const recordSitemaps = writeRecordPages(
outDir,
{ body: recordBody, main: loaded.main, renderer: loaded.renderer, site: discovery.site },
records,
);
const selector = await loaded.runner.import<SectionSelector>(RUNNER_MODULES.sectionSelector.path);
const sections = distinctSections(
discovery.pages.flatMap((page) => selector.sectionSeedsOf(page.id, SECTION_PART_WEIGHT)),
new Set(records.map((record) => record.title)),
);
const sectionSitemaps = writeSectionPages(
outDir,
{ main: loaded.main, renderer: loaded.renderer, site: discovery.site },
sections,
);
writeSitemapIndex(outDir, discovery.site, [
pageSitemap,
...[...sourceSitemaps, ...recordSitemaps, ...sectionSitemaps].map(
(route): readonly [string, null] => [route, null],
),
]);
return {
catalog,
records: records.length,
routes: discovery.routes.length,
sections: sections.length,
sources: sources.length,
};
});
};