
Security News
The Hidden Blast Radius of the Axios Compromise
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.
@moduul/core
Advanced tools
Core plugin host system for dynamically loading and managing plugins at runtime.
npm install @moduul/core
import { PluginHost } from '@moduul/core';
// Define your plugin interface for type safety
interface MyPlugin {
execute(): Promise<string>;
}
// Create a typed plugin host pointing at a folder of plugin subdirectories
const host = new PluginHost<MyPlugin>({
folder: './plugins'
});
// Load all plugins found in the folder
await host.reload();
// Get all loaded plugins (typed!)
const plugins = host.getAll();
console.log(`Loaded ${plugins.length} plugins`);
// Find a specific plugin
const plugin = host.find('my-plugin');
if (plugin) {
// Full type safety and autocomplete
const result = await plugin.plugin.execute();
console.log(result);
}
import { PluginHost } from '@moduul/core';
interface MyPlugin {
execute(): Promise<string>;
}
// Point directly at a single plugin directory
const host = new PluginHost<MyPlugin>({
pluginPath: '/absolute/path/to/my-plugin'
});
await host.reload();
const plugin = host.find('my-plugin');
if (plugin) {
const result = await plugin.plugin.execute();
console.log(result);
}
PluginHost<T>The main class for managing plugins. Supports a generic type parameter T to define the expected plugin interface, enabling full type safety and IDE autocomplete.
The PluginHost class accepts a generic type parameter that defines the shape of your plugins:
// Define your plugin interface
interface MyPlugin {
name: string;
version: string;
execute(input: string): Promise<string>;
}
// Create a typed host
const host = new PluginHost<MyPlugin>({
folder: './plugins'
});
// Type-safe access to plugins
await host.reload();
const plugin = host.find('my-plugin');
if (plugin) {
// TypeScript knows plugin.plugin is MyPlugin
const result = await plugin.plugin.execute('hello'); // ✓ Type-checked
console.log(plugin.plugin.name); // ✓ Autocomplete works
}
Benefits of using the generic type:
Without generic type (untyped):
const host = new PluginHost({ folder: './plugins' });
const plugin = host.find('my-plugin');
if (plugin) {
// plugin.plugin is `unknown` - no type safety
const result = await (plugin.plugin as any).execute('hello'); // No autocomplete
}
new PluginHost<T>(options: PluginHostOptions<T>)
PluginHostOptions<T> is a union type with two mutually exclusive modes:
Folder mode — scan a directory for plugin subdirectories (or .zip archives):
| Option | Type | Required | Description |
|---|---|---|---|
folder | string | ✓ | Path to the directory that contains plugin subdirectories or .zip archives |
validator | (plugin: unknown) => plugin is T | Type guard to validate each loaded plugin |
Single-plugin mode — load exactly one plugin from a specific directory:
| Option | Type | Required | Description |
|---|---|---|---|
pluginPath | string | ✓ | Path to a single plugin directory (must contain plugin.manifest.json) |
validator | (plugin: unknown) => plugin is T | Type guard to validate the loaded plugin |
The two modes are mutually exclusive: use either
folderorpluginPath, never both.
Example — folder mode with validator:
interface MyPlugin {
execute(): string;
version: string;
}
const host = new PluginHost<MyPlugin>({
folder: './plugins',
// Type guard that validates plugin structure
validator: (plugin): plugin is MyPlugin => {
return (
typeof plugin === 'object' &&
plugin !== null &&
typeof (plugin as any).execute === 'function' &&
typeof (plugin as any).version === 'string'
);
}
});
Example — single-plugin mode with validator:
const host = new PluginHost<MyPlugin>({
pluginPath: '/opt/plugins/my-plugin',
validator: (plugin): plugin is MyPlugin => {
return (
typeof plugin === 'object' &&
plugin !== null &&
typeof (plugin as any).execute === 'function' &&
typeof (plugin as any).version === 'string'
);
}
});
The validator acts as a TypeScript type guard, narrowing unknown to T.
reload(): Promise<void>Scans the plugin folder and loads all valid plugins. Clears previously loaded plugins.
await host.reload();
getAll(): LoadedPlugin<T>[]Returns an array of all loaded plugins with full type information.
interface MyPlugin {
execute(): string;
}
const host = new PluginHost<MyPlugin>({ folder: './plugins' });
await host.reload();
const plugins = host.getAll();
plugins.forEach(({ manifest, plugin }) => {
console.log(`${manifest.name} v${manifest.version}`);
// plugin is typed as MyPlugin
const result = plugin.execute(); // ✓ Type-safe
});
find(name: string): LoadedPlugin<T> | undefinedFinds a plugin by name with full type information.
const plugin = host.find('my-plugin');
if (plugin) {
// plugin is typed as LoadedPlugin<MyPlugin>
console.log('Found:', plugin.manifest.name);
const result = await plugin.plugin.execute(); // ✓ Autocomplete works
}
PluginManifestThe required structure for plugin.manifest.json:
interface PluginManifest {
name: string; // Unique plugin identifier
version: string; // Semantic version
entryPoint: string; // Relative path to main file (e.g., "./dist/index.js")
meta?: { // Optional metadata
description?: string;
author?: string;
[key: string]: unknown;
};
}
LoadedPlugin<T>The structure returned by getAll() and find():
interface LoadedPlugin<T> {
manifest: PluginManifest; // Parsed manifest
plugin: T; // The loaded module (typed)
}
Example usage:
interface MyPlugin {
execute(): string;
}
const host = new PluginHost<MyPlugin>({ folder: './plugins' });
const plugin: LoadedPlugin<MyPlugin> | undefined = host.find('test');
PluginHostOptions<T>Union of PluginHostFolderOptions<T> and PluginHostSingleOptions<T>.
// Folder scan mode
interface PluginHostFolderOptions<T> {
folder: string; // Directory containing plugin subdirectories / ZIPs
validator?: (plugin: unknown) => plugin is T; // Optional type guard
}
// Single-plugin mode
interface PluginHostSingleOptions<T> {
pluginPath: string; // Path to one plugin directory
validator?: (plugin: unknown) => plugin is T; // Optional type guard
}
type PluginHostOptions<T> = PluginHostFolderOptions<T> | PluginHostSingleOptions<T>;
Both constituent types are exported and can be used for narrowing or explicit annotation.
A valid plugin must have:
entryPointmy-plugin/
├── plugin.manifest.json
└── dist/
└── index.js
plugin.manifest.json:
{
"name": "my-plugin",
"version": "1.0.0",
"entryPoint": "./dist/index.js",
"meta": {
"description": "My awesome plugin",
"author": "Your Name"
}
}
dist/index.js:
export default {
name: 'my-plugin',
execute() {
console.log('Hello from plugin!');
}
};
Plugins can be distributed as .zip files. The host automatically extracts them to a temporary directory before loading.
plugins/
├── my-plugin/ # Directory plugin
└── another-plugin.zip # ZIP archive plugin
package.json:
{
"type": "module"
}
index.js:
export default {
execute() {
return 'result';
}
};
index.js:
module.exports = {
execute() {
return 'result';
}
};
Add a validator to enforce plugin structure at runtime and enable type narrowing:
interface MyPlugin {
name: string;
execute(): Promise<string>;
version: string;
}
const host = new PluginHost<MyPlugin>({
folder: './plugins',
// Type guard validates and narrows type from unknown to MyPlugin
validator: (plugin): plugin is MyPlugin => {
return (
typeof plugin === 'object' &&
plugin !== null &&
typeof (plugin as any).name === 'string' &&
typeof (plugin as any).execute === 'function' &&
typeof (plugin as any).version === 'string'
);
}
});
await host.reload();
// All plugins are guaranteed to match MyPlugin interface
const plugins = host.getAll();
plugins.forEach(({ plugin }) => {
// TypeScript knows these properties exist
console.log(`${plugin.name} v${plugin.version}`);
});
Pro Tip: Use a validation library like zod for more robust validation:
import { z } from 'zod';
const PluginSchema = z.object({
name: z.string(),
version: z.string(),
execute: z.function().returns(z.promise(z.string())),
});
type MyPlugin = z.infer<typeof PluginSchema>;
const host = new PluginHost<MyPlugin>({
folder: './plugins',
validator: (plugin): plugin is MyPlugin => {
return PluginSchema.safeParse(plugin).success;
}
});
The plugin host gracefully handles errors:
Errors are logged to console.warn and don't crash the host.
Cache busting is automatic. Each reload() call uses a new timestamp:
// Development workflow
while (developing) {
// Make changes to plugin...
await host.reload(); // Loads fresh version
const plugin = host.find('my-plugin');
await plugin.plugin.execute();
}
@moduul/core is published in both ESM and CommonJS formats:
// ESM
import { PluginHost } from '@moduul/core';
// CommonJS
const { PluginHost } = require('@moduul/core');
Full type definitions included with generic type support. All option types are exported:
import { PluginHost, PluginManifest, LoadedPlugin, PluginHostFolderOptions, PluginHostSingleOptions } from '@moduul/core';
// Define your plugin interface
interface MyPlugin {
execute(input: string): Promise<string>;
cleanup?(): Promise<void>;
}
// Create typed host
const host: PluginHost<MyPlugin> = new PluginHost<MyPlugin>({
folder: './plugins'
});
// Type-safe plugin access
const plugins: LoadedPlugin<MyPlugin>[] = host.getAll();
// Type guard validator
const validator = (plugin: unknown): plugin is MyPlugin => {
return (
typeof plugin === 'object' &&
plugin !== null &&
typeof (plugin as any).execute === 'function'
);
};
// Manifest typing
const manifest: PluginManifest = {
name: 'my-plugin',
version: '1.0.0',
entryPoint: './dist/index.js'
};
Advanced: Multiple Plugin Types
// Define base interface
interface BasePlugin {
name: string;
version: string;
}
// Define specific plugin types
interface DataPlugin extends BasePlugin {
processData(data: unknown): Promise<unknown>;
}
interface UIPlugin extends BasePlugin {
render(): HTMLElement;
}
// Create separate hosts for different plugin types
const dataHost = new PluginHost<DataPlugin>({
folder: './plugins/data',
validator: (p): p is DataPlugin =>
typeof (p as any)?.processData === 'function'
});
const uiHost = new PluginHost<UIPlugin>({
folder: './plugins/ui',
validator: (p): p is UIPlugin =>
typeof (p as any)?.render === 'function'
});
MIT
FAQs
Core plugin host system for loading and managing plugins
We found that @moduul/core 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
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.