
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@libria/plugin-loader
Advanced tools
A TypeScript-first plugin system for Node.js applications with dependency resolution, lifecycle hooks, and hot-reloading.
onLoad and onUnload hooks for setup and cleanupnpm install @libria/plugin-loader
Create a plugin with definePlugin:
// plugins/greeter/src/index.ts
import { definePlugin } from '@libria/plugin-loader';
interface GreeterAPI {
greet(name: string): string;
}
export default definePlugin<GreeterAPI>({
id: 'greeter',
pluginType: 'util',
create(ctx) {
return {
api: {
greet(name) {
return `Hello, ${name}!`;
}
}
};
}
});
Each plugin needs a plugin.json in its directory:
{
"id": "greeter",
"name": "Greeter Plugin",
"pluginType": "util",
"version": "1.0.0",
"module": "./dist/index.mjs"
}
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<GreeterAPI>('greeter');
console.log(greeter.greet('World')); // "Hello, World!"
The plugin.json file defines your plugin's metadata:
{
"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.
Plugins can depend on other plugins. Dependencies are loaded first, and the loading order is determined by topological sort.
{
"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:
export default definePlugin<MyAPI>({
id: 'plugin-a',
pluginType: 'feature',
create(ctx) {
// Dependencies are guaranteed to be loaded
const pluginB = ctx.getPlugin<PluginBAPI>('plugin-b');
const pluginC = ctx.getPlugin<PluginCAPI>('plugin-c');
return {
api: {
doSomething() {
return pluginB.getValue() + pluginC.calculate();
}
}
};
}
});
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 versionPlugin factories can be async for loading configs, connecting to services, etc:
export default definePlugin<DatabaseAPI>({
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()
}
};
}
});
Plugins can implement lifecycle hooks for setup and cleanup:
export default definePlugin<MyAPI>({
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!');
}
};
}
});
Reload plugins without restarting your application:
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();
Query loaded plugins by type or get metadata:
// 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();
Properly unload all plugins (calls onUnload hooks in reverse order):
process.on('SIGTERM', async () => {
await manager.shutdown();
process.exit(0);
});
The library provides typed errors for common scenarios:
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}`);
}
}
PluginManager| Method | Description |
|---|---|
loadPlugins(patterns: string[]) | Load plugins from glob patterns |
getPlugin<T>(id: string): T | 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 |
definePlugin<T>(factory: PluginFactory<T>)Helper function for defining plugins with proper typing.
PluginContextPassed to the create function:
| Method | Description |
|---|---|
getPlugin<T>(id: string): T | Get another plugin's API |
hasPlugin(id: string): boolean | Check if a plugin is loaded |
LibriaPlugin<T>The return type of create:
interface LibriaPlugin<T> {
api: T;
onLoad?(): void | Promise<void>;
onUnload?(): void | Promise<void>;
}
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
MIT
FAQs
Simple plugin loader for Node.js applications
We found that @libria/plugin-loader demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.