1/**
2 * Plugin loader: imports ESM plugin modules from disk.
3 */
4
5import * as path from "node:path";
6import { pathToFileURL } from "node:url";
7import type { Plugin, LoadedPlugin } from "./types";
8
9// Load a plugin module (ESM .js/.mjs) from disk.
10async function importPlugin(modulePath: string): Promise<Plugin | null> {
11 try {
12 const mod = await import(pathToFileURL(modulePath).href);
13 const plugin = (mod?.default ?? mod) as Plugin;
14 if (plugin && typeof plugin === "object") return plugin;
15 return null;
16 } catch {
17 return null;
18 }
19}
20
21export async function loadPlugins(options: {
22 rootDir: string;
23 pluginPaths?: string[];
24}): Promise<LoadedPlugin[]> {
25 const { rootDir, pluginPaths = [] } = options;
26 const loaded: LoadedPlugin[] = [];
27
28 for (const [index, rel] of pluginPaths.entries()) {
29 const absPath = path.isAbsolute(rel) ? rel : path.join(rootDir, rel);
30 const plugin = await importPlugin(absPath);
31 if (!plugin) continue;
32 // Derive a stable id from plugin or filename
33 const fallbackId = plugin.pluginId ?? path.parse(absPath).name ?? `plugin_${index + 1}`;
34 loaded.push({ ...plugin, pluginId: fallbackId });
35 }
36
37 return loaded;
38}