Simple plugin loader for Node.js applications
npm install @libria/plugin-loaderonLoad and onUnload hooks for setup and cleanup
bash
npm install @libria/plugin-loader
`
Quick Start
$3
Create a plugin with definePlugin:
`typescript
// plugins/greeter/src/index.ts
import { definePlugin } from '@libria/plugin-loader';
interface GreeterAPI {
greet(name: string): string;
}
export default definePlugin({
id: 'greeter',
pluginType: 'util',
create(ctx) {
return {
api: {
greet(name) {
return Hello, ${name}!;
}
}
};
}
});
`
$3
Each plugin needs a plugin.json in its directory:
`json
{
"id": "greeter",
"name": "Greeter Plugin",
"pluginType": "util",
"version": "1.0.0",
"module": "./dist/index.mjs"
}
`
$3
`typescript
import { PluginManager } from '@libria/plugin-loader';
const manager = new PluginManager();
// Load all plugins from a directory
await manager.loadPlugins(['./plugins/*']);
// Use a plugin
const greeter = manager.getPlugin('greeter');
console.log(greeter.greet('World')); // "Hello, World!"
`
Plugin Manifest
The plugin.json file defines your plugin's metadata:
`json
{
"id": "my-plugin",
"name": "My Plugin",
"pluginType": "feature",
"version": "1.0.0",
"description": "Optional description",
"module": "./dist/index.mjs",
"main": "./dist/index.cjs",
"dependencies": [
{ "id": "other-plugin", "version": "^1.0.0" }
]
}
`
| Field | Required | Description |
|-------|----------|-------------|
| id | Yes | Unique identifier for the plugin |
| pluginType | Yes | Category/type of the plugin (for queries) |
| version | Yes | Semver version string |
| name | No | Human-readable name |
| description | No | Plugin description |
| module | No* | Path to ESM entry point |
| main | No* | Path to CJS entry point |
| dependencies | No | Array of plugin dependencies |
*At least one of module or main is required.
Plugin Dependencies
Plugins can depend on other plugins. Dependencies are loaded first, and the loading order is determined by topological sort.
`json
{
"id": "plugin-a",
"pluginType": "feature",
"version": "1.0.0",
"module": "./dist/index.mjs",
"dependencies": [
{ "id": "plugin-b", "version": "^1.0.0" },
{ "id": "plugin-c", "version": ">=2.0.0" }
]
}
`
Access dependencies in your plugin via the context:
`typescript
export default definePlugin({
id: 'plugin-a',
pluginType: 'feature',
create(ctx) {
// Dependencies are guaranteed to be loaded
const pluginB = ctx.getPlugin('plugin-b');
const pluginC = ctx.getPlugin('plugin-c');
return {
api: {
doSomething() {
return pluginB.getValue() + pluginC.calculate();
}
}
};
}
});
`
$3
Dependencies use semver ranges:
- "1.0.0" - Exact version
- "^1.0.0" - Compatible with 1.x.x
- "~1.0.0" - Compatible with 1.0.x
- ">=1.0.0 <2.0.0" - Range
- "*" - Any version
Async Initialization
Plugin factories can be async for loading configs, connecting to services, etc:
`typescript
export default definePlugin({
id: 'database',
pluginType: 'service',
async create(ctx) {
const config = await loadConfig();
const connection = await connectToDatabase(config);
return {
api: {
query: (sql) => connection.query(sql),
close: () => connection.close()
}
};
}
});
`
Lifecycle Hooks
Plugins can implement lifecycle hooks for setup and cleanup:
`typescript
export default definePlugin({
id: 'my-plugin',
pluginType: 'feature',
create(ctx) {
let intervalId: NodeJS.Timeout;
return {
api: {
// ... your API
},
onLoad() {
// Called after plugin is registered
console.log('Plugin loaded!');
intervalId = setInterval(() => {
console.log('heartbeat');
}, 1000);
},
async onUnload() {
// Called before plugin is unloaded
// Can be async for cleanup
clearInterval(intervalId);
await saveState();
console.log('Plugin unloaded!');
}
};
}
});
`
Hot-Reloading
Reload plugins without restarting your application:
`typescript
const manager = new PluginManager();
await manager.loadPlugins(['./plugins/*']);
// Manual reload
await manager.reloadPlugin('my-plugin');
// Watch for file changes
await manager.watch(['./plugins/*'], (id, event, error) => {
if (event === 'reload') {
console.log(Plugin ${id} reloaded successfully);
}
if (event === 'error') {
console.error(Failed to reload ${id}:, error);
}
});
// Stop watching
await manager.stopWatching();
`
Plugin Queries
Query loaded plugins by type or get metadata:
`typescript
// Get all loaded plugin IDs
const ids = manager.getPluginIds();
// ['greeter', 'database', 'logger']
// Check if a plugin is loaded
if (manager.hasPlugin('greeter')) {
// ...
}
// Get metadata for a plugin
const meta = manager.getPluginMetadata('greeter');
// { id: 'greeter', name: 'Greeter Plugin', version: '1.0.0', ... }
// Get all plugins of a specific type
const services = manager.getPluginsByType('service');
// [{ id: 'database', ... }, { id: 'cache', ... }]
// Get all metadata
const allMeta = manager.getAllMetadata();
`
Graceful Shutdown
Properly unload all plugins (calls onUnload hooks in reverse order):
`typescript
process.on('SIGTERM', async () => {
await manager.shutdown();
process.exit(0);
});
`
Error Handling
The library provides typed errors for common scenarios:
`typescript
import {
PluginLoadError, // Failed to load plugin module
PluginInvalidExportError, // Plugin doesn't export a valid factory
PluginNotFoundError, // Plugin not found (getPlugin, reload, unload)
ManifestNotFoundError, // plugin.json not found during reload
DuplicatePluginError, // Attempting to register same ID twice
CircularDependencyError, // Circular dependency detected
DependencyNotFoundError, // Required dependency not found
VersionMismatchError, // Dependency version doesn't match
} from '@libria/plugin-loader';
try {
await manager.loadPlugins(['./plugins/*']);
} catch (err) {
if (err instanceof CircularDependencyError) {
console.error('Circular dependency:', err.cycle.join(' -> '));
}
if (err instanceof VersionMismatchError) {
console.error(
${err.packageId}@${err.actualVersion} doesn't satisfy ${err.requiredVersion}
);
}
}
try {
await manager.reloadPlugin('my-plugin');
} catch (err) {
if (err instanceof PluginNotFoundError) {
console.error(Plugin ${err.id} is not loaded);
}
if (err instanceof ManifestNotFoundError) {
console.error(Manifest missing for ${err.pluginId} in ${err.dir});
}
}
`
API Reference
$3
| Method | Description |
|--------|-------------|
| loadPlugins(patterns: string[]) | Load plugins from glob patterns |
| getPlugin | Get a plugin's API by ID |
| hasPlugin(id: string): boolean | Check if a plugin is loaded |
| getPluginIds(): string[] | Get all loaded plugin IDs |
| getPluginMetadata(id: string) | Get metadata for a plugin |
| getPluginsByType(type: string) | Get all plugins of a type |
| getAllMetadata() | Get all plugin metadata |
| reloadPlugin(id: string) | Hot-reload a specific plugin |
| unloadPlugin(id: string) | Unload a specific plugin |
| watch(patterns, callback) | Watch for file changes |
| stopWatching() | Stop watching for changes |
| shutdown() | Unload all plugins and cleanup |
| getContext() | Get the internal plugin context |
$3
Helper function for defining plugins with proper typing.
$3
Passed to the create function:
| Method | Description |
|--------|-------------|
| getPlugin | Get another plugin's API |
| hasPlugin(id: string): boolean | Check if a plugin is loaded |
$3
The return type of create:
`typescript
interface LibriaPlugin {
api: T;
onLoad?(): void | Promise;
onUnload?(): void | Promise;
}
`
Directory Structure
Recommended project structure:
`
my-app/
├── src/
│ └── index.ts
├── plugins/
│ ├── greeter/
│ │ ├── plugin.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── dist/
│ │ └── index.mjs
│ └── database/
│ ├── plugin.json
│ ├── src/
│ │ └── index.ts
│ └── dist/
│ └── index.mjs
└── package.json
``