npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@moduul/core

v0.1.4

Published

Core plugin host system for loading and managing plugins

Readme

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

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

  1. plugin.manifest.json - Manifest file at the root
  2. 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

ESM Plugins (Recommended)

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)

Related Packages

License

MIT