kist
Version:
Lightweight Package Pipeline Processor with Plugin Architecture
185 lines • 7.35 kB
JavaScript
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
import { join } from "path";
import { AbstractProcess } from "../abstract/AbstractProcess.js";
export class PluginManager extends AbstractProcess {
static instance = null;
loadedPlugins = new Map();
pluginActions = new Map();
constructor() {
super();
this.logInfo("PluginManager initialized.");
}
static getInstance() {
if (!PluginManager.instance) {
PluginManager.instance = new PluginManager();
}
return PluginManager.instance;
}
static resetInstance() {
PluginManager.instance = null;
}
async discoverPlugins(options) {
this.logInfo("Starting plugin discovery...");
const prefixes = options?.pluginPrefixes || [
"@getkist/action-",
"kist-plugin-",
];
await this.discoverNpmPlugins(prefixes);
if (options?.localPluginsPath) {
await this.discoverLocalPlugins(options.localPluginsPath);
}
this.logInfo(`Plugin discovery complete. Loaded ${this.loadedPlugins.size} plugins.`);
}
async discoverNpmPlugins(prefixes) {
const nodeModulesPath = join(process.cwd(), "node_modules");
try {
const directories = readdirSync(nodeModulesPath, {
withFileTypes: true,
});
for (const dir of directories) {
if (dir.isDirectory() && dir.name.startsWith("@")) {
await this.discoverScopedPlugins(join(nodeModulesPath, dir.name), prefixes);
}
for (const prefix of prefixes) {
if (dir.isDirectory() &&
!prefix.startsWith("@") &&
dir.name.startsWith(prefix)) {
await this.loadPlugin(join(nodeModulesPath, dir.name), dir.name);
}
}
}
}
catch (error) {
this.logError("Failed to discover npm plugins.", error);
}
}
async discoverScopedPlugins(scopePath, prefixes) {
try {
const packages = readdirSync(scopePath, { withFileTypes: true });
for (const pkg of packages) {
for (const prefix of prefixes) {
const scopePrefix = prefix.split("/")[1];
const pkgPath = join(scopePath, pkg.name);
const isDir = pkg.isDirectory() ||
(pkg.isSymbolicLink() &&
statSync(pkgPath).isDirectory());
if (isDir &&
scopePrefix &&
pkg.name.startsWith(scopePrefix)) {
const fullName = `${scopePath.split("/").pop()}/${pkg.name}`;
await this.loadPlugin(join(scopePath, pkg.name), fullName);
}
}
}
}
catch (_error) {
this.logDebug(`No scoped plugins found in ${scopePath}`);
}
}
async discoverLocalPlugins(localPath) {
try {
const pluginPath = join(process.cwd(), localPath);
const directories = readdirSync(pluginPath, {
withFileTypes: true,
});
for (const dir of directories) {
if (dir.isDirectory()) {
await this.loadPlugin(join(pluginPath, dir.name), `local:${dir.name}`);
}
}
}
catch (_error) {
this.logDebug(`No local plugins found at ${localPath}`);
}
}
async loadPlugin(pluginPath, pluginName) {
try {
this.logDebug(`Loading plugin: ${pluginName}`);
let entryPoint = pluginPath;
const packageJsonPath = join(pluginPath, "package.json");
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const mainEntry = packageJson.module ||
packageJson.main ||
packageJson.exports?.["."]?.import ||
packageJson.exports?.["."]?.require ||
packageJson.exports?.["."] ||
"dist/index.js";
entryPoint = join(pluginPath, mainEntry);
}
catch (jsonError) {
this.logDebug(`Failed to parse package.json for ${pluginName}`);
}
}
const pluginModule = await import(entryPoint);
const plugin = pluginModule.default || pluginModule;
if (!plugin || typeof plugin.registerActions !== "function") {
this.logWarn(`Plugin ${pluginName} does not implement ActionPlugin interface.`);
return;
}
const metadata = {
name: pluginName,
version: typeof plugin.version === "string"
? plugin.version
: "unknown",
description: typeof plugin.description === "string"
? plugin.description
: undefined,
actions: [],
};
const actions = plugin.registerActions();
for (const [actionName, actionClass] of Object.entries(actions)) {
this.pluginActions.set(actionName, actionClass);
metadata.actions.push(actionName);
this.logDebug(` - Registered action: ${actionName}`);
}
this.loadedPlugins.set(pluginName, metadata);
this.logInfo(`Plugin "${pluginName}" loaded successfully with ${metadata.actions.length} actions.`);
}
catch (error) {
this.logError(`Failed to load plugin ${pluginName}:`, error);
}
}
registerPlugin(plugin, name) {
this.logInfo(`Manually registering plugin: ${name}`);
const metadata = {
name,
version: typeof plugin.version === "string"
? plugin.version
: "unknown",
description: typeof plugin.description === "string"
? plugin.description
: undefined,
actions: [],
};
const actions = plugin.registerActions();
for (const [actionName, actionClass] of Object.entries(actions)) {
this.pluginActions.set(actionName, actionClass);
metadata.actions.push(actionName);
}
this.loadedPlugins.set(name, metadata);
this.logInfo(`Plugin "${name}" registered with ${metadata.actions.length} actions.`);
}
getPluginActions() {
return new Map(this.pluginActions);
}
getLoadedPlugins() {
return Array.from(this.loadedPlugins.values());
}
getPluginMetadata(name) {
return this.loadedPlugins.get(name);
}
isPluginLoaded(name) {
return this.loadedPlugins.has(name);
}
listPluginActions() {
return Array.from(this.pluginActions.keys());
}
clearPlugins() {
this.loadedPlugins.clear();
this.pluginActions.clear();
this.logInfo("All plugins cleared.");
}
}
//# sourceMappingURL=PluginManager.js.map