core/analyzers/site.analyzer.ts
core/analyzers/site.analyzer.ts is a file in GovLab Stats. 102 lines of code and 21 definitions.
import type { AppStats, PageStats, Tally } from "#types/site.types";
import { PAGE_MARKER, STYLE_EXTENSION } from "#configuration/constants/site.constants";
import { existsSync, readFileSync, statSync } from "node:fs";
import { extensionOf, posixOf } from "#core/selectors/source.selector";
import { isAuthoredRole, roleOf } from "#core/classifiers/source.classifier";
import { readdirSafe, walkFiles } from "#core/loaders/folder.loader";
import type { PathExclusion } from "@govlab/quality/config";
import { countLines } from "#core/analyzers/text.analyzer";
import { isGeneratedPath } from "#core/predicates/source.predicate";
import path from "node:path";
import { relativePath } from "@ssot/paths";
const EMPTY: AppStats = {
assetBytes: 0,
assetFiles: 0,
pages: [],
present: false,
sourceFiles: 0,
sourceLines: 0,
styles: 0,
subsystems: [],
};
const descendantDirs = function descendantDirs(
absDir: string,
exclude: readonly string[],
ignore: PathExclusion,
): number {
return readdirSafe(absDir)
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(absDir, entry.name))
.filter((abs) => !ignore(abs) && !exclude.includes(abs))
.reduce((count, abs) => count + 1 + descendantDirs(abs, exclude, ignore), 0);
};
const isAuthoredSource = function isAuthoredSource(abs: string): boolean {
const role = roleOf(extensionOf(path.basename(abs)));
return isAuthoredRole(role) && !isGeneratedPath(abs, abs);
};
const codeLinesOf = function codeLinesOf(abs: string): number {
return countLines(readFileSync(abs, "utf8")).code;
};
const sourceLinesOf = function sourceLinesOf(files: readonly string[]): { files: number; lines: number } {
const counted = files.filter(isAuthoredSource).map(codeLinesOf);
return { files: counted.length, lines: counted.reduce((sum, lines) => sum + lines, 0) };
};
const pagesUnder = function pagesUnder(memberAbs: string, memberRel: string, ignore: PathExclusion): PageStats[] {
const markers = walkFiles(memberAbs, ignore).filter((abs) => path.basename(abs) === PAGE_MARKER);
const depths = markers.map((abs) => path.relative(memberAbs, abs).split(path.sep).length);
const shallowest = Math.min(...depths, Number.MAX_SAFE_INTEGER);
const pageDirs = markers.map((abs) => path.dirname(abs));
return markers
.map((abs, index) => {
const dir = path.dirname(abs);
const nested = pageDirs.filter((other) => other.startsWith(`${dir}${path.sep}`));
const own = walkFiles(dir, ignore).filter(
(file) => !nested.some((inner) => file.startsWith(`${inner}${path.sep}`)),
);
const counted = sourceLinesOf(own);
return {
files: counted.files,
isRoot: depths.at(index) === shallowest,
lines: counted.lines,
name: path.basename(dir),
rel: `${memberRel}/${posixOf(path.relative(memberAbs, dir))}`,
systems: descendantDirs(dir, nested, ignore),
};
})
.toSorted((a, b) => Number(a.isRoot) - Number(b.isRoot) || b.lines - a.lines);
};
const tallyOne = function tallyOne(tally: Tally, abs: string): void {
if (extensionOf(path.basename(abs)) === STYLE_EXTENSION) {
tally.styles += 1;
}
if (isAuthoredSource(abs)) {
tally.sourceFiles += 1;
tally.sourceLines += codeLinesOf(abs);
return;
}
tally.assetBytes += statSync(abs).size;
tally.assetFiles += 1;
};
export const collectApp = function collectApp(
root: string,
containerRoots: readonly string[],
ignore: PathExclusion,
): AppStats {
const memberRel = relativePath("app.root");
const memberAbs = path.join(root, memberRel);
if (!existsSync(memberAbs)) {
return EMPTY;
}
const tally: Tally = { assetBytes: 0, assetFiles: 0, sourceFiles: 0, sourceLines: 0, styles: 0 };
for (const abs of walkFiles(memberAbs, ignore)) {
tallyOne(tally, abs);
}
const subsystems = containerRoots
.map((container) => ({
container,
folders: readdirSafe(path.join(root, container)).filter((entry) => entry.isDirectory()).length,
}))
.filter((entry) => entry.folders > 0)
.toSorted((a, b) => b.folders - a.folders);
return { ...tally, pages: pagesUnder(memberAbs, memberRel, ignore), present: true, subsystems };
};