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; } 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 { 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 { 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 { 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 { 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 { 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 { const names = await readSourceFiles(dir); return Promise.all(names.map(async (name) => loadEntry(path.join(dir, name)))); }; const mergePluginInto = function mergePluginInto( target: Record, incoming: Record, ): Record { const merged: Record = { ...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, entry: EslintPluginEntry, reserved: Set, ): 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, ): { eslint: Record; stylelint: unknown[] } { const eslint: Record = {}; 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 } = {}, ): Promise<{ eslint: Record; stylelint: unknown[] }> { const reserved = options.reservedNamespaces ?? new Set(); const folders = pluginFolders(consumerRoot, options.allowGlobal ?? false); const entries = (await Promise.all(folders.map(async (dir) => loadDir(dir)))).flat(); return partitionPlugins(entries, reserved); };