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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@ptkl/forge-plugin-system

v1.4.1

Published

A flexible and powerful plugin system for React applications with multi-slot architecture and hooks support

Readme

Forge Plugin System

A flexible, TypeScript-based plugin system for React applications that supports component slots, script hooks, and lifecycle management.

Features

  • Component Slots: Render plugin components in designated slots
  • Script Hooks: Execute plugin scripts with lifecycle hooks
  • Return Values: Capture and return results from plugin script execution
  • Async Support: Full support for asynchronous plugin operations
  • Error Handling: Comprehensive error handling with detailed feedback
  • TypeScript: Full TypeScript support with type safety

Installation

npm install forge-plugin-system

Basic Usage

Plugin Definition

import { Plugin } from 'forge-plugin-system';

const myPlugin: Plugin = {
  meta: {
    name: 'my-plugin',
    version: '1.0.0',
    enabled: true
  },
  // Component for rendering
  Component: MyPluginComponent,
  // Script execution
  execute: (args) => {
    console.log('Plugin executed with args:', args);
    return { success: true, data: 'processed' };
  },
  // Lifecycle hooks
  hooks: {
    'app::init': () => console.log('App initialized'),
    'custom::hook': (args) => {
      // Process args and return result
      return processData(args);
    }
  }
};

Hook Script Execution with Results

The hookScript function returns results from plugin execution with intelligent aggregation:

import { hookScript } from 'forge-plugin-system';

// Default behavior - aggregated results
const result = await hookScript('custom::hook', { data: 'input' });

console.log(result);
// Output:
// {
//   executed: ['plugin1', 'plugin2'],           // Successfully executed plugins
//   errors: [],                                 // Any errors that occurred
//   result: combinedOutput                      // Aggregated result from all plugins
// }

// Get individual plugin results when needed
const individualResults = await hookScript('custom::hook', { data: 'input' }, {
  returnIndividualResults: true
});

console.log(individualResults);
// Output:
// {
//   executed: ['plugin1', 'plugin2'],
//   errors: [],
//   results: [                                  // Individual results per plugin
//     { plugin: 'plugin1', result: 'output1' },
//     { plugin: 'plugin2', result: 'output2' }
//   ]
// }

Available Hook Options

const result = await hookScript('my-hook', args, {
  slot: 'specific-slot',           // Target specific slot
  pluginName: 'specific-plugin',   // Target specific plugin
  force: true,                     // Force re-execution
  throwOnError: false,             // Continue on errors (default)
  returnIndividualResults: false,  // Return per-plugin results instead of aggregated
  resultAggregator: (results) => { // Custom aggregation function
    return results.reduce((acc, val) => acc + val, 0);
  }
});

Result Aggregation

By default, hookScript intelligently aggregates results from multiple plugins:

// Object results are merged
const objectResults = await hookScript('get-config');
// If plugins return: [{ a: 1 }, { b: 2 }]
// Result: { a: 1, b: 2 }

// Array results are flattened
const arrayResults = await hookScript('get-items'); 
// If plugins return: [['item1'], ['item2', 'item3']]
// Result: ['item1', 'item2', 'item3']

// Single result is returned as-is
const singleResult = await hookScript('calculate', { a: 5, b: 3 });
// If one plugin returns: 8
// Result: 8

// Mixed types become arrays
const mixedResults = await hookScript('mixed-hook');
// If plugins return: ['text', 42, { obj: true }]
// Result: ['text', 42, { obj: true }]

Error Handling

try {
  const result = await hookScript('risky-hook', args, { throwOnError: true });
  // Handle successful results
  result.results.forEach(({ plugin, result }) => {
    console.log(`${plugin} returned:`, result);
  });
} catch (error) {
  // Handle first error encountered
  console.error('Plugin execution failed:', error);
}

Plugin Types and Return Values

Plugins can return any type of data, and results are automatically aggregated:

const dataPlugin: Plugin = {
  meta: { name: 'data-plugin' },
  hooks: {
    'fetch-users': async () => {
      const users = await fetchUsersFromAPI();
      return { users, count: users.length };
    },
    'calculate': (args) => {
      return args.a + args.b;
    },
    'transform': (args) => {
      return args.data.map(item => transform(item));
    }
  }
};

// Usage - aggregated results by default
const userData = await hookScript('fetch-users');
console.log(userData.result); // Combined data from all plugins

const calcResult = await hookScript('calculate', { a: 5, b: 3 });
console.log(calcResult.result); // 8 (if only one plugin) or aggregated results

// Get individual results when needed
const { results } = await hookScript('fetch-users', null, { 
  returnIndividualResults: true 
});
const firstPluginData = results[0]?.result; // { users: [...], count: 10 }

Custom Aggregation

Define custom logic for combining plugin results:

// Collect all user data from multiple sources
const allUsers = await hookScript('get-users', null, {
  resultAggregator: (results) => {
    const users = [];
    results.forEach(result => {
      if (result.users) users.push(...result.users);
    });
    return { users: [...new Set(users)], total: users.length };
  }
});

// Calculate statistics across plugins
const stats = await hookScript('calculate-metrics', data, {
  resultAggregator: (results) => ({
    min: Math.min(...results),
    max: Math.max(...results),
    avg: results.reduce((a, b) => a + b, 0) / results.length
  })
});

Lifecycle Hooks

Standard lifecycle hooks are available:

  • app::init - Application initialization
  • app::start - Application start
  • app::cleanup - Application cleanup
  • before::enter - Before route/component enter
  • before::leave - Before route/component leave
  • plugin::load - Plugin loading
  • plugin::execute - Plugin execution
  • slot::enter - Slot activation

Component Slots

import { PluginSlot } from 'forge-plugin-system';

function MyApp() {
  return (
    <div>
      <PluginSlot 
        slotName="header" 
        executeScripts={true}  // Also execute scripts for this slot
      />
      <main>
        <PluginSlot slotName="content" />
      </main>
      <PluginSlot slotName="footer" />
    </div>
  );
}

Advanced Usage

Sequential vs Concurrent Execution

By default, plugins execute concurrently. Use throwOnError: true for sequential execution:

// Concurrent (default) - all plugins run in parallel
const concurrent = await hookScript('process-data', data);

// Sequential - stops on first error
const sequential = await hookScript('critical-process', data, { 
  throwOnError: true 
});

Plugin Registry Management

import { usePlugins } from 'forge-plugin-system';

function PluginManager() {
  const { plugins, registerPlugin, getStatus } = usePlugins();
  
  const handlePluginAdd = (plugin: Plugin) => {
    registerPlugin(plugin);
  };
  
  const status = getStatus();
  console.log(`${status.executedPlugins}/${status.totalPlugins} plugins executed`);
  
  return (
    <div>
      <h3>Registered Plugins: {plugins.length}</h3>
      <button onClick={() => handlePluginAdd(newPlugin)}>
        Add Plugin
      </button>
    </div>
  );
}

Plugin Permissions

Plugins can register permissions that are automatically prefixed with the plugin ID:

import { Plugin } from 'forge-plugin-system';

const myPlugin: Plugin = {
  meta: { 
    name: 'Sattrakt', 
    version: '1.0.0',
    id: 'sattrakt' // Plugin ID used as prefix
  },
  
  // Register permissions WITHOUT prefix
  permissions: [
    'custom_analytics',
    'advanced_filtering',
    'export_special_reports',
    'integration_management'
  ],
  
  // ... rest of plugin
};

// Permissions are automatically prefixed: 
// sattrakt::custom_analytics
// sattrakt::advanced_filtering
// sattrakt::export_special_reports
// sattrakt::integration_management

Using Plugin Permissions:

import { usePluginPermissions } from 'forge-plugin-system';

function MyComponent() {
  const { 
    getAllPermissions, 
    getPermissions, 
    hasPermission,
    getPluginsWithPermission 
  } = usePluginPermissions();
  
  // Get all permissions from all plugins
  const allPerms = getAllPermissions();
  // ['sattrakt::custom_analytics', 'sattrakt::advanced_filtering', ...]
  
  // Get permissions for a specific plugin
  const sattraktPerms = getPermissions('sattrakt');
  
  // Check if a permission exists
  if (hasPermission('sattrakt::custom_analytics')) {
    // Show analytics feature
  }
  
  // Find all plugins with a specific permission
  const pluginsWithExport = getPluginsWithPermission('export_special_reports');
  // ['sattrakt']
  
  return <div>...</div>;
}

Debugging

Enable debug mode to see detailed execution logs:

import { enablePluginDebug } from 'forge-plugin-system';

// Enable debugging
enablePluginDebug(true);

// Or in browser console
window.enableHookDebug(true);
window.debugPluginHooks(); // Show debug info

API Reference

hookScript(hook, args?, options?)

Executes plugin scripts for the specified hook with intelligent result aggregation.

Parameters:

  • hook: string - Hook name to execute
  • args?: any - Arguments to pass to plugin scripts
  • options?: object - Execution options
    • slot?: string - Target specific slot
    • pluginName?: string - Target specific plugin
    • force?: boolean - Force re-execution
    • throwOnError?: boolean - Stop on first error
    • returnIndividualResults?: boolean - Return per-plugin results instead of aggregated
    • resultAggregator?: (results: any[]) => any - Custom aggregation function

Returns (default - aggregated):

Promise<{
  executed: string[];                           // Successfully executed plugin names
  errors: Array<{ plugin: string; error: Error }>; // Execution errors
  result?: any;                                 // Aggregated result from all plugins
}>

Returns (when returnIndividualResults: true):

Promise<{
  executed: string[];                           // Successfully executed plugin names
  errors: Array<{ plugin: string; error: Error }>; // Execution errors
  results: Array<{ plugin: string; result: any }>; // Individual plugin results
}>

Plugin Interface

interface Plugin {
  meta: PluginMeta;
  Component?: ComponentType<any>;
  execute?: (args?: any) => any | Promise<any>;
  cleanup?: () => void | Promise<void>;
  hooks?: {
    [hookName: string]: (args?: any) => any | Promise<any>;
  };
  slots?: {
    [slotName: string]: {
      components?: { [name: string]: ComponentType<any> };
      scripts?: { [hookName: string]: (args?: any) => any | Promise<any> };
    };
  };
}

License

MIT License - see LICENSE file for details.