core/analyzers/dependency.analyzer.ts

core/analyzers/dependency.analyzer.ts is a file in GovLab Stats. 77 lines of code and 16 definitions.

import { DEPENDENCY_FIELDS, PACKAGE_MANIFEST } from "#configuration/constants/package.constants";
import type { DependencyGraph, Fans, GraphLink, GraphNode, GraphRow } from "#types/dependency.types";
import { field, stringField } from "#core/selectors/field.selector";
import { TOP_PACKAGES } from "#configuration/constants/metric.constants";
import { isRecord } from "#core/predicates/record.predicate";
import path from "node:path";
import { posixOf } from "#core/selectors/source.selector";
import { readJson } from "#core/loaders/data.loader";
import { workspaceMembers } from "#core/resolvers/package.resolver";

const declaredDependencies = function declaredDependencies(pkg: unknown): Set<string> {
    return new Set(
        DEPENDENCY_FIELDS.flatMap((fieldName) => {
            const block = field(pkg, fieldName);
            return isRecord(block) ? Object.keys(block) : [];
        }),
    );
};

const buildNodes = function buildNodes(root: string, dirs: readonly string[]): Map<string, GraphNode> {
    const nodes = new Map<string, GraphNode>();
    for (const dir of dirs) {
        const pkg = readJson(path.join(dir, PACKAGE_MANIFEST));
        const name = stringField(pkg, "name");
        if (name.length > 0) {
            nodes.set(name, { deps: declaredDependencies(pkg), name, rel: posixOf(path.relative(root, dir)) });
        }
    }
    return nodes;
};

const computeFans = function computeFans(nodes: Map<string, GraphNode>): Fans {
    const nodeList = [...nodes.values()];
    const internal = new Map(nodeList.map((node) => [node.name, [...node.deps].filter((dep) => nodes.has(dep))]));
    const fanOut = new Map(nodeList.map((node) => [node.name, internal.get(node.name)?.length ?? 0]));
    const fanIn = new Map(
        nodeList.map((node) => [
            node.name,
            nodeList.filter((other) => (internal.get(other.name) ?? []).includes(node.name)).length,
        ]),
    );
    return {
        edges: [...internal.values()].reduce((sum, deps) => sum + deps.length, 0),
        fanIn,
        fanOut,
        leaves: nodeList.filter((node) => (internal.get(node.name) ?? []).length === 0).length,
    };
};

const topRows = function topRows(counts: Map<string, number>, nodes: Map<string, GraphNode>): GraphRow[] {
    return [...counts.entries()]
        .filter(([, count]) => count > 0)
        .toSorted((a, b) => b[1] - a[1])
        .slice(0, TOP_PACKAGES)
        .map(([name, count]) => ({ count, name, rel: nodes.get(name)?.rel ?? "" }));
};

const linkedRows = function linkedRows(fans: Fans, nodes: Map<string, GraphNode>): GraphLink[] {
    return [...nodes.values()]
        .map((node) => ({
            fanIn: fans.fanIn.get(node.name) ?? 0,
            fanOut: fans.fanOut.get(node.name) ?? 0,
            name: node.name,
            rel: node.rel,
        }))
        .filter((row) => row.fanIn > 0 || row.fanOut > 0)
        .toSorted((a, b) => b.fanIn - a.fanIn || b.fanOut - a.fanOut);
};

export const collectDependencyGraph = function collectDependencyGraph(root: string): DependencyGraph {
    const nodes = buildNodes(root, workspaceMembers(root));
    const fans = computeFans(nodes);
    const links = linkedRows(fans, nodes);
    return {
        edges: fans.edges,
        isolated: nodes.size - links.length,
        leaves: fans.leaves,
        links,
        nodeCount: nodes.size,
        topFanIn: topRows(fans.fanIn, nodes),
        topFanOut: topRows(fans.fanOut, nodes),
    };
};