core/loaders/plugin.loader.ts

core/loaders/plugin.loader.ts is a file in GovLab Quality. 135 lines of code and 28 definitions.

import { type Dirent, promises as fs } from "node:fs";
import {
    namespaceWithoutPlugin,
    pluginFolderUnreadable,
    pluginLoadFailed,
    pluginShape,
    pluginWithoutDefault,
    reservedNamespace,
} from "#configuration/strings/plugin.strings";
import { SOURCE_EXTENSIONS } from "#configuration/constants/specifier.constants";
import { homedir } from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { relativePath } from "@ssot/paths";

interface EslintPluginEntry {
    file: string;
    tool: "eslint";
    plugins: Record<string, unknown>;
}

interface StylelintPluginEntry {
    file: string;
    tool: "stylelint";
    plugins: unknown[];
}

type PluginEntry = EslintPluginEntry | StylelintPluginEntry;

const PLUGIN_DIR = relativePath("govlabHost.plugins");
const MISSING_CODE = "ENOENT";

const isObject = function isObject(value: unknown): value is Record<string, unknown> {
    return value !== null && typeof value === "object" && !Array.isArray(value);
};

const isSource = function isSource(name: string): boolean {
    return SOURCE_EXTENSIONS.some((suffix) => name.endsWith(suffix));
};

const readEntries = async function readEntries(dir: string): Promise<Dirent[]> {
    try {
        return await fs.readdir(dir, { withFileTypes: true });
    } catch (error) {
        if (isObject(error) && error["code"] === MISSING_CODE) {
            return [];
        }
        throw new Error(pluginFolderUnreadable(dir), { cause: error });
    }
};

const readSourceFiles = async function readSourceFiles(dir: string): Promise<string[]> {
    const entries = await readEntries(dir);
    return entries
        .filter((entry) => entry.isFile() && isSource(entry.name))
        .map((entry) => entry.name)
        .sort((a, b) => a.localeCompare(b));
};

const pluginFolders = function pluginFolders(consumerRoot: string, allowGlobal: boolean): string[] {
    const folders = [path.join(consumerRoot, PLUGIN_DIR)];
    if (allowGlobal) {
        folders.push(path.join(homedir(), PLUGIN_DIR));
    }
    return folders;
};

const importPluginFile = async function importPluginFile(file: string): Promise<unknown> {
    try {
        const mod: unknown = await import(pathToFileURL(file).href);
        return mod;
    } catch (error) {
        throw new Error(pluginLoadFailed(file), { cause: error });
    }
};

const loadEntry = async function loadEntry(file: string): Promise<PluginEntry> {
    const mod = await importPluginFile(file);
    const entry = isObject(mod) ? mod["default"] : null;
    if (!isObject(entry)) {
        throw new Error(pluginWithoutDefault(file));
    }
    if (entry["tool"] === "eslint" && isObject(entry["plugins"])) {
        return { file, plugins: entry["plugins"], tool: "eslint" };
    }
    const declared = entry["plugins"];
    if (entry["tool"] === "stylelint" && Array.isArray(declared)) {
        return { file, plugins: declared.map((plugin: unknown) => plugin), tool: "stylelint" };
    }
    throw new Error(pluginShape(file));
};

const loadDir = async function loadDir(dir: string): Promise<PluginEntry[]> {
    const names = await readSourceFiles(dir);
    return Promise.all(names.map(async (name) => loadEntry(path.join(dir, name))));
};

const mergePluginInto = function mergePluginInto(
    target: Record<string, unknown>,
    incoming: Record<string, unknown>,
): Record<string, unknown> {
    const merged: Record<string, unknown> = { ...target };
    for (const [key, value] of Object.entries(incoming)) {
        const existing = merged[key];
        merged[key] = isObject(existing) && isObject(value) ? { ...existing, ...value } : value;
    }
    return merged;
};

const claimEslint = function claimEslint(
    eslint: Record<string, unknown>,
    entry: EslintPluginEntry,
    reserved: Set<string>,
): void {
    for (const [namespace, plugin] of Object.entries(entry.plugins)) {
        if (reserved.has(namespace)) {
            throw new Error(reservedNamespace(entry.file, namespace));
        }
        if (!isObject(plugin)) {
            throw new Error(namespaceWithoutPlugin(entry.file, namespace));
        }
        const existing = isObject(eslint[namespace]) ? eslint[namespace] : {};
        eslint[namespace] = mergePluginInto(existing, plugin);
    }
};

const partitionPlugins = function partitionPlugins(
    entries: PluginEntry[],
    reserved: Set<string>,
): { eslint: Record<string, unknown>; stylelint: unknown[] } {
    const eslint: Record<string, unknown> = {};
    const stylelint: unknown[] = [];
    for (const entry of entries) {
        if (entry.tool === "eslint") {
            claimEslint(eslint, entry, reserved);
        } else {
            stylelint.push(...entry.plugins);
        }
    }
    return { eslint, stylelint };
};

export const loadUserPlugins = async function loadUserPlugins(
    consumerRoot: string,
    options: { allowGlobal?: boolean; reservedNamespaces?: Set<string> } = {},
): Promise<{ eslint: Record<string, unknown>; stylelint: unknown[] }> {
    const reserved = options.reservedNamespaces ?? new Set<string>();
    const folders = pluginFolders(consumerRoot, options.allowGlobal ?? false);
    const entries = (await Promise.all(folders.map(async (dir) => loadDir(dir)))).flat();
    return partitionPlugins(entries, reserved);
};