New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@moduul/core

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@moduul/core

Core plugin host system for loading and managing plugins

latest
Source
npmnpm
Version
0.1.4
Version published
Maintainers
1
Created
Source

@moduul/core

Core plugin host system for dynamically loading and managing plugins at runtime.

Features

  • 🔌 Dynamic Plugin Loading - Load plugins from directories or ZIP archives at runtime
  • Single-Plugin Mode - Point directly at one plugin directory instead of scanning a folder
  • �🔥 Hot Reload - Cache-busting support for plugin reloading during development
  • 📦 Multiple Formats - Support for both ESM and CommonJS plugins
  • Validation - Built-in manifest validation with optional custom validators
  • 🎯 Type-Safe - Full TypeScript support with comprehensive type definitions
  • 🚀 Zero Config - Works out of the box with sensible defaults

Installation

npm install @moduul/core

Quick Start

Folder mode (scan a directory of plugins)

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);
}

Single-plugin mode (load one plugin directly)

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);
}

API Reference

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.

Generic Type Parameter

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:

  • Type Safety: Catch errors at compile time instead of runtime
  • IDE Support: Full autocomplete and IntelliSense for plugin methods
  • Documentation: Self-documenting code with clear contracts
  • Refactoring: Safe refactoring with TypeScript's rename/find references

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
}

Constructor

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):

OptionTypeRequiredDescription
folderstringPath to the directory that contains plugin subdirectories or .zip archives
validator(plugin: unknown) => plugin is TType guard to validate each loaded plugin

Single-plugin mode — load exactly one plugin from a specific directory:

OptionTypeRequiredDescription
pluginPathstringPath to a single plugin directory (must contain plugin.manifest.json)
validator(plugin: unknown) => plugin is TType guard to validate the loaded plugin

The two modes are mutually exclusive: use either folder or pluginPath, 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.

Methods

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> | undefined

Finds 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
}

Types

PluginManifest

The 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.

Plugin Structure

A valid plugin must have:

  • plugin.manifest.json - Manifest file at the root
  • Entry point - The JavaScript file specified in entryPoint

Directory Plugin Example

my-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!');
  }
};

ZIP Archive Support

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

Module Formats

package.json:

{
  "type": "module"
}

index.js:

export default {
  execute() {
    return 'result';
  }
};

CommonJS Plugins

index.js:

module.exports = {
  execute() {
    return 'result';
  }
};

Custom Validation

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;
  }
});

Error Handling

The plugin host gracefully handles errors:

  • Invalid manifests - Logged and skipped
  • Missing entry points - Logged and skipped
  • Import errors - Logged and skipped
  • Validation failures - Logged and skipped

Errors are logged to console.warn and don't crash the host.

Hot Reload

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();
}

Dual-Format Package

@moduul/core is published in both ESM and CommonJS formats:

// ESM
import { PluginHost } from '@moduul/core';

// CommonJS
const { PluginHost } = require('@moduul/core');

TypeScript Support

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'
});

Requirements

  • Node.js 20 or higher
  • ESM support (built-in)
  • @moduul/builder - CLI tool for building plugins
  • @moduul/boilerplate - Official plugin template

License

MIT

Keywords

plugin

FAQs

Package last updated on 28 Feb 2026

Did you know?

Socket

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.

Install

Related posts