added examples
This commit is contained in:
110
dist/PluginLoader.d.ts
vendored
Normal file
110
dist/PluginLoader.d.ts
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { NotificationOptions } from './PluginInterfaces.js';
|
||||
import type { IPluginEventClient } from './interfaces.js';
|
||||
import type { PluginRegistry } from './PluginRegistry.js';
|
||||
/**
|
||||
* Configuration options for constructing a {@link PluginLoader}.
|
||||
*
|
||||
* @remarks Node.js only — this class uses `fs`, `path`, and `module` from the Node standard library.
|
||||
*/
|
||||
export interface PluginLoaderOptions {
|
||||
/** The registry to register and wire loaded plugins into. */
|
||||
registry: PluginRegistry;
|
||||
/**
|
||||
* Optional event client forwarded to each plugin's context.
|
||||
* Any object with `on(event, handler)` / `off(event, handler)` works.
|
||||
*/
|
||||
eventClient?: IPluginEventClient;
|
||||
/**
|
||||
* Called when a plugin invokes `context.notify(...)`.
|
||||
* The host is responsible for displaying the notification.
|
||||
*/
|
||||
onNotify?: (options: NotificationOptions) => void | Promise<void>;
|
||||
}
|
||||
/**
|
||||
* Node.js-specific loader that discovers and registers plugins from the filesystem.
|
||||
*
|
||||
* Provide a directory; the loader scans each immediate subdirectory, resolves the
|
||||
* entry point, imports the module (CJS or ESM), normalises it to the {@link Plugin}
|
||||
* interface, and wires it into the supplied {@link PluginRegistry}.
|
||||
*
|
||||
* Supports:
|
||||
* - Native plugins exporting `onLoad` / `onUnload`
|
||||
* - Legacy plugins exporting `activate` / `deactivate` (with a compat shim)
|
||||
* - `plugin-metadata.json` for stable plugin IDs
|
||||
* - `package.json` `"main"` for custom entry points (falls back to `index.js`)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const loader = new PluginLoader({ registry, eventClient: myEmitter });
|
||||
* const ids = await loader.loadFromDirectory('/path/to/plugins');
|
||||
* console.log('Loaded:', ids);
|
||||
* ```
|
||||
*/
|
||||
export declare class PluginLoader {
|
||||
private readonly registry;
|
||||
private readonly eventClient?;
|
||||
private readonly onNotify?;
|
||||
constructor(options: PluginLoaderOptions);
|
||||
/**
|
||||
* Scans `dir` for plugin subdirectories and loads each one.
|
||||
* Subdirectories that lack a recognisable entry point are silently skipped;
|
||||
* those that throw during loading emit a console warning and are skipped.
|
||||
*
|
||||
* @param dir - Absolute path to the plugins folder.
|
||||
* @returns IDs of all plugins that loaded successfully.
|
||||
*/
|
||||
loadFromDirectory(dir: string): Promise<string[]>;
|
||||
/**
|
||||
* Loads a single plugin from an absolute directory path.
|
||||
*
|
||||
* Resolution order for the plugin ID:
|
||||
* 1. `plugin-metadata.json` → `id` field
|
||||
* 2. `path.basename(pluginDir)`
|
||||
*
|
||||
* Resolution order for the entry point:
|
||||
* 1. `package.json` → `main` field
|
||||
* 2. `index.js`
|
||||
*
|
||||
* @param pluginDir - Absolute path to the plugin directory.
|
||||
* @returns The plugin ID used for registration.
|
||||
* @throws If no entry point is found or the module cannot be loaded.
|
||||
*/
|
||||
loadPlugin(pluginDir: string): Promise<string>;
|
||||
/**
|
||||
* Reads and parses `plugin-metadata.json` from the plugin directory.
|
||||
* Returns `null` if the file is absent or malformed.
|
||||
*
|
||||
* @param pluginDir - Plugin root directory.
|
||||
*/
|
||||
private _readMetadata;
|
||||
/**
|
||||
* Resolves the JavaScript entry point for a plugin.
|
||||
* Reads `main` from `package.json` when present; falls back to `index.js`.
|
||||
*
|
||||
* @param pluginDir - Plugin root directory.
|
||||
* @throws If no entry file can be found.
|
||||
*/
|
||||
private _resolveEntry;
|
||||
/**
|
||||
* Imports a plugin module from an absolute file path.
|
||||
*
|
||||
* Attempts CJS evaluation first (via `new Function` with a `createRequire`-backed
|
||||
* `require` shim). Falls back to native ESM `import()` on `SyntaxError`.
|
||||
*
|
||||
* @param filePath - Absolute path to the plugin entry file.
|
||||
* @throws On load or evaluation errors that are not `SyntaxError`.
|
||||
*/
|
||||
private _importModule;
|
||||
/**
|
||||
* Normalises a raw plugin module export to the {@link Plugin} interface.
|
||||
*
|
||||
* - Native: object with `onLoad` function — used as-is.
|
||||
* - Legacy: object with `activate` function — wrapped into `onLoad` / `onUnload`
|
||||
* with a compatibility shim for removed APIs (`registerHook`, `runHook`, `getConfig`).
|
||||
*
|
||||
* @param raw - Raw module exports.
|
||||
* @throws If the module exports neither `onLoad` nor `activate`.
|
||||
*/
|
||||
private _normalise;
|
||||
}
|
||||
//# sourceMappingURL=PluginLoader.d.ts.map
|
||||
1
dist/PluginLoader.d.ts.map
vendored
Normal file
1
dist/PluginLoader.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"PluginLoader.d.ts","sourceRoot":"","sources":["../src/PluginLoader.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAU,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAY1D;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,6DAA6D;IAC7D,QAAQ,EAAE,cAAc,CAAC;IAEzB;;;OAGG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAEjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAyD;gBAEvE,OAAO,EAAE,mBAAmB;IAUxC;;;;;;;OAOG;IACG,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAoBvD;;;;;;;;;;;;;;OAcG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA2BpD;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAYrB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAmBrB;;;;;;;;OAQG;YACW,aAAa;IA+B3B;;;;;;;;;OASG;IACH,OAAO,CAAC,UAAU;CA8BnB"}
|
||||
200
dist/PluginLoader.js
vendored
Normal file
200
dist/PluginLoader.js
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { createRequire } from 'module';
|
||||
import { createPluginContext } from './PluginContext.js';
|
||||
/**
|
||||
* Node.js-specific loader that discovers and registers plugins from the filesystem.
|
||||
*
|
||||
* Provide a directory; the loader scans each immediate subdirectory, resolves the
|
||||
* entry point, imports the module (CJS or ESM), normalises it to the {@link Plugin}
|
||||
* interface, and wires it into the supplied {@link PluginRegistry}.
|
||||
*
|
||||
* Supports:
|
||||
* - Native plugins exporting `onLoad` / `onUnload`
|
||||
* - Legacy plugins exporting `activate` / `deactivate` (with a compat shim)
|
||||
* - `plugin-metadata.json` for stable plugin IDs
|
||||
* - `package.json` `"main"` for custom entry points (falls back to `index.js`)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const loader = new PluginLoader({ registry, eventClient: myEmitter });
|
||||
* const ids = await loader.loadFromDirectory('/path/to/plugins');
|
||||
* console.log('Loaded:', ids);
|
||||
* ```
|
||||
*/
|
||||
export class PluginLoader {
|
||||
constructor(options) {
|
||||
this.registry = options.registry;
|
||||
this.eventClient = options.eventClient;
|
||||
this.onNotify = options.onNotify;
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* Scans `dir` for plugin subdirectories and loads each one.
|
||||
* Subdirectories that lack a recognisable entry point are silently skipped;
|
||||
* those that throw during loading emit a console warning and are skipped.
|
||||
*
|
||||
* @param dir - Absolute path to the plugins folder.
|
||||
* @returns IDs of all plugins that loaded successfully.
|
||||
*/
|
||||
async loadFromDirectory(dir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const loaded = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory())
|
||||
continue;
|
||||
const pluginDir = path.join(dir, entry.name);
|
||||
try {
|
||||
const id = await this.loadPlugin(pluginDir);
|
||||
loaded.push(id);
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(`[PluginLoader] Skipped "${entry.name}":`, err);
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
/**
|
||||
* Loads a single plugin from an absolute directory path.
|
||||
*
|
||||
* Resolution order for the plugin ID:
|
||||
* 1. `plugin-metadata.json` → `id` field
|
||||
* 2. `path.basename(pluginDir)`
|
||||
*
|
||||
* Resolution order for the entry point:
|
||||
* 1. `package.json` → `main` field
|
||||
* 2. `index.js`
|
||||
*
|
||||
* @param pluginDir - Absolute path to the plugin directory.
|
||||
* @returns The plugin ID used for registration.
|
||||
* @throws If no entry point is found or the module cannot be loaded.
|
||||
*/
|
||||
async loadPlugin(pluginDir) {
|
||||
const metadata = this._readMetadata(pluginDir);
|
||||
const entryPath = this._resolveEntry(pluginDir);
|
||||
const pluginId = metadata?.id ?? path.basename(pluginDir);
|
||||
const raw = await this._importModule(entryPath);
|
||||
const plugin = this._normalise(raw);
|
||||
const context = createPluginContext({
|
||||
pluginId,
|
||||
eventClient: this.eventClient,
|
||||
onNotify: this.onNotify,
|
||||
}, this.registry);
|
||||
this.registry.registerPlugin(pluginId, plugin, context);
|
||||
await plugin.onLoad(context);
|
||||
return pluginId;
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* Reads and parses `plugin-metadata.json` from the plugin directory.
|
||||
* Returns `null` if the file is absent or malformed.
|
||||
*
|
||||
* @param pluginDir - Plugin root directory.
|
||||
*/
|
||||
_readMetadata(pluginDir) {
|
||||
const metaPath = path.join(pluginDir, 'plugin-metadata.json');
|
||||
if (!fs.existsSync(metaPath))
|
||||
return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resolves the JavaScript entry point for a plugin.
|
||||
* Reads `main` from `package.json` when present; falls back to `index.js`.
|
||||
*
|
||||
* @param pluginDir - Plugin root directory.
|
||||
* @throws If no entry file can be found.
|
||||
*/
|
||||
_resolveEntry(pluginDir) {
|
||||
const pkgPath = path.join(pluginDir, 'package.json');
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
if (pkg.main)
|
||||
return path.resolve(pluginDir, pkg.main);
|
||||
}
|
||||
catch { /* fall through to index.js */ }
|
||||
}
|
||||
const indexPath = path.join(pluginDir, 'index.js');
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
throw new Error(`No entry point found in "${pluginDir}"`);
|
||||
}
|
||||
return indexPath;
|
||||
}
|
||||
/**
|
||||
* Imports a plugin module from an absolute file path.
|
||||
*
|
||||
* Attempts CJS evaluation first (via `new Function` with a `createRequire`-backed
|
||||
* `require` shim). Falls back to native ESM `import()` on `SyntaxError`.
|
||||
*
|
||||
* @param filePath - Absolute path to the plugin entry file.
|
||||
* @throws On load or evaluation errors that are not `SyntaxError`.
|
||||
*/
|
||||
async _importModule(filePath) {
|
||||
const code = fs.readFileSync(filePath, 'utf8');
|
||||
const pluginRequire = createRequire(pathToFileURL(filePath).href);
|
||||
// CJS path — eval with a real require shim so the plugin can require deps
|
||||
try {
|
||||
const m = { exports: {} };
|
||||
new Function('module', 'exports', '__dirname', '__filename', 'require', code)(m, m.exports, path.dirname(filePath), filePath, pluginRequire);
|
||||
const result = m.exports?.['default'] ?? m.exports;
|
||||
if (result && typeof result === 'object' && Object.keys(result).length > 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (!(err instanceof SyntaxError))
|
||||
throw err;
|
||||
// SyntaxError means ESM syntax — fall through below
|
||||
}
|
||||
// ESM path — import via file URL
|
||||
const mod = await import(pathToFileURL(filePath).href);
|
||||
return (mod['default'] ?? mod);
|
||||
}
|
||||
/**
|
||||
* Normalises a raw plugin module export to the {@link Plugin} interface.
|
||||
*
|
||||
* - Native: object with `onLoad` function — used as-is.
|
||||
* - Legacy: object with `activate` function — wrapped into `onLoad` / `onUnload`
|
||||
* with a compatibility shim for removed APIs (`registerHook`, `runHook`, `getConfig`).
|
||||
*
|
||||
* @param raw - Raw module exports.
|
||||
* @throws If the module exports neither `onLoad` nor `activate`.
|
||||
*/
|
||||
_normalise(raw) {
|
||||
if (typeof raw['onLoad'] === 'function')
|
||||
return raw;
|
||||
if (typeof raw['activate'] === 'function') {
|
||||
const activate = raw['activate'];
|
||||
const deactivate = raw['deactivate'];
|
||||
return {
|
||||
name: typeof raw['name'] === 'string' ? raw['name'] : undefined,
|
||||
version: typeof raw['version'] === 'string' ? raw['version'] : undefined,
|
||||
onLoad: async (ctx) => {
|
||||
const legacyCtx = Object.assign(Object.create(ctx), {
|
||||
registerHook: (name) => ctx.log(`[compat] registerHook("${name}") — not supported`),
|
||||
runHook: (name) => {
|
||||
ctx.log(`[compat] runHook("${name}")`);
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
getConfig: (key) => ctx.settings.get(key),
|
||||
on: (event, handler) => ctx.events.on(event, handler),
|
||||
});
|
||||
await activate(legacyCtx);
|
||||
},
|
||||
onUnload: deactivate ? async () => { await deactivate(); } : undefined,
|
||||
};
|
||||
}
|
||||
throw new Error('Plugin does not export `onLoad` or `activate` — cannot load');
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=PluginLoader.js.map
|
||||
1
dist/PluginLoader.js.map
vendored
Normal file
1
dist/PluginLoader.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/index.d.ts
vendored
2
dist/index.d.ts
vendored
@@ -8,4 +8,6 @@ export { PluginRegistry } from './PluginRegistry.js';
|
||||
export type { PluginRegistryOptions } from './PluginRegistry.js';
|
||||
export { createPluginContext } from './PluginContext.js';
|
||||
export type { PluginContextOptions } from './PluginContext.js';
|
||||
export { PluginLoader } from './PluginLoader.js';
|
||||
export type { PluginLoaderOptions } from './PluginLoader.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
2
dist/index.d.ts.map
vendored
2
dist/index.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC"}
|
||||
1
dist/index.js
vendored
1
dist/index.js
vendored
@@ -3,4 +3,5 @@ export { PluginTab } from './types.js';
|
||||
export { generateThemeCSS } from './theme-css.js';
|
||||
export { PluginRegistry } from './PluginRegistry.js';
|
||||
export { createPluginContext } from './PluginContext.js';
|
||||
export { PluginLoader } from './PluginLoader.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAOhD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsBvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAOhD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAsBvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
||||
Reference in New Issue
Block a user