JSPM

@angular-dynamic/plugin-system

1.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 29
  • Score
    100M100P100Q52279F
  • License MIT

Dynamic plugin system for Angular applications

Package Exports

  • @angular-dynamic/plugin-system

Readme

Angular Dynamic Plugin System

npm version License: MIT Build Status TypeScript Angular

A production-ready, type-safe plugin system for Angular 16+ applications that enables runtime loading, isolated execution, and lifecycle management of plugins.

What's New in v1.1.0

Version 1.1.0 addresses critical production issues identified in post-release audits while maintaining 100% backward compatibility with v1.0.0.

Critical Fixes:

  • Lifecycle hook timeout protection (prevents infinite hangs)
  • Memory leak fixes (ComponentRef and context cleanup)
  • Race condition protection (component creation/destruction)
  • Concurrent unload safety (prevents double-destroy errors)

Optional Enhancements:

  • Enhanced debug mode with granular logging
  • Plugin inspection API for monitoring health
  • Improved error messages with actionable guidance

No Migration Required: All v1.0.0 code works without modification.

What Problem Does This Solve?

Modern Angular applications, especially SaaS platforms, multi-tenant systems, and extensible applications, often need to:

  • Load features dynamically at runtime without rebuilding the application
  • Enable/disable functionality for specific users or tenants
  • Support third-party extensions and plugins
  • Reduce initial bundle size by lazy-loading optional features
  • Provide isolated execution environments for untrusted code

This library provides a standard, production-ready solution for dynamic plugin architecture in Angular applications.

Features

Core Capabilities

  • Runtime plugin loading via dynamic imports
  • Isolated injector per plugin for dependency isolation
  • Type-safe plugin lifecycle hooks
  • Defensive error handling (plugin failures don't crash host)
  • Observable-based state management
  • Concurrent plugin loading with configurable limits
  • Timeout support with automatic cleanup
  • Compatible with standalone components
  • TypeScript strict mode compliant

Stability & Safety (v1.1.0)

  • Lifecycle hook timeout protection: Prevents infinite hangs (default: 5s)
  • Memory leak prevention: Automatic cleanup of component references and contexts
  • Race condition protection: Safe concurrent operations on plugin lifecycle
  • Enhanced error handling: Actionable error messages with troubleshooting guidance
  • Debug mode: Granular logging for development and troubleshooting

Installation

npm install @angular-dynamic/plugin-system

Quick Start

1. Configure the Plugin System

import { ApplicationConfig } from '@angular/core';
import { providePluginSystem } from '@angular-dynamic/plugin-system';

export const appConfig: ApplicationConfig = {
  providers: [
    providePluginSystem({
      globalTimeout: 30000,
      maxConcurrentLoads: 3,
      enableDevMode: false,
      // v1.1.0: Lifecycle hook timeout protection
      lifecycleHookTimeout: 5000 // Default: 5000ms, set to 0 to disable
    })
  ]
};

2. Create a Plugin

import { Component } from '@angular/core';
import { PluginLifecycle, PluginContext } from '@angular-dynamic/plugin-system';

@Component({
  selector: 'invoice-plugin',
  standalone: true,
  template: `<h1>Invoice Plugin</h1>`
})
export class InvoicePluginComponent implements PluginLifecycle {
  async onLoad(context: PluginContext): Promise<void> {
    // Plugin initialization logic
  }

  async onActivate(context: PluginContext): Promise<void> {
    // Called when plugin is rendered
  }

  async onDeactivate(): Promise<void> {
    // Called before plugin is removed
  }

  async onDestroy(): Promise<void> {
    // Cleanup logic
  }
}

export const PluginManifest = {
  name: 'invoice',
  version: '1.0.0',
  entryComponent: InvoicePluginComponent,
  displayName: 'Invoice Plugin',
  description: 'Handles invoice management'
};

3. Register and Load Plugins

import { Component, OnInit } from '@angular/core';
import { PluginManager } from '@angular-dynamic/plugin-system';

@Component({
  selector: 'app-root',
  template: `
    <plugin-outlet [plugin]="'invoice'"></plugin-outlet>
  `,
  standalone: true,
  imports: [PluginOutletComponent]
})
export class AppComponent implements OnInit {
  constructor(private pluginManager: PluginManager) {}

  ngOnInit(): void {
    this.pluginManager.register({
      name: 'invoice',
      loadFn: () => import('./plugins/invoice-plugin'),
      config: {
        autoLoad: true
      }
    });
  }
}

4. Use Plugin Outlet

<plugin-outlet [plugin]="'invoice'"></plugin-outlet>

API Reference

PluginManager

Main orchestrator for plugin lifecycle management.

@Injectable({ providedIn: 'root' })
export class PluginManager {
  // Core Methods
  register(config: PluginRegistration): void;
  load(pluginName: string): Promise<PluginMetadata>;
  loadMany(pluginNames: string[]): Promise<PluginMetadata[]>;
  unregister(pluginName: string): Promise<void>;
  getPluginState(pluginName: string): PluginState | undefined;
  isReady(pluginName: string): boolean;

  // v1.1.0: New Methods
  isUnloading(pluginName: string): boolean;
  getPluginInfo(pluginName: string): PluginInfo | undefined;

  // Observables
  readonly pluginState$: Observable<PluginStateEvent>;
}

PluginRegistration

Configuration for registering a plugin.

interface PluginRegistration {
  name: string;
  loadFn: () => Promise<LoadedPluginModule>;
  config?: {
    autoLoad?: boolean;
    retryOnError?: boolean;
    maxRetries?: number;
    timeout?: number;
    allowedServices?: Array<InjectionToken<any> | Type<any>>;
    metadata?: Record<string, any>;
  };
}

PluginLifecycle

Interface for plugin components to implement lifecycle hooks.

interface PluginLifecycle {
  onLoad?(context: PluginContext): void | Promise<void>;
  onActivate?(context: PluginContext): void | Promise<void>;
  onDeactivate?(): void | Promise<void>;
  onDestroy?(): void | Promise<void>;
}

PluginContext

Bridge between host and plugin for controlled communication.

interface PluginContext {
  readonly pluginName: string;
  readonly hostInjector: Injector;
  getService<T>(token: InjectionToken<T> | Type<T>): T | null;
  emit(eventName: string, data?: any): void;
  subscribe(eventName: string, handler: (data: any) => void): () => void;
}

PluginState

Enum representing plugin lifecycle states.

enum PluginState {
  REGISTERED = 'REGISTERED',
  LOADING = 'LOADING',
  LOADED = 'LOADED',
  ACTIVE = 'ACTIVE',
  ERROR = 'ERROR',
  UNLOADING = 'UNLOADING',
  UNLOADED = 'UNLOADED'
}

Advanced Usage

Service Access Control

pluginManager.register({
  name: 'invoice',
  loadFn: () => import('./plugins/invoice-plugin'),
  config: {
    allowedServices: [HttpClient, Router]
  }
});

Lifecycle Hooks

providePluginSystem({
  lifecycleHooks: {
    beforeLoad: async (pluginName) => {
      // Pre-load logic
    },
    afterLoad: async (pluginName) => {
      // Post-load logic
    },
    onError: (pluginName, error) => {
      // Error handling
    }
  }
})

Debug Mode (v1.1.0)

providePluginSystem({
  enableDevMode: true,
  debugOptions: {
    logLifecycleHooks: true,      // Log hook calls and timing
    logStateTransitions: true,     // Log state changes
    validateManifests: true,       // Strict manifest validation
    throwOnWarnings: false         // Treat warnings as errors
  },
  lifecycleHookTimeout: 10000     // Custom timeout (default: 5000ms)
})

Plugin Health Monitoring (v1.1.0)

// Get detailed plugin information
const info = pluginManager.getPluginInfo('invoice');
if (info) {
  console.log(`State: ${info.state}`);
  console.log(`Loaded at: ${info.loadedAt}`);
  console.log(`Error count: ${info.errorCount}`);
  if (info.lastError) {
    console.error(`Last error: ${info.lastError.message}`);
  }
}

// Check if plugin is unloading
if (pluginManager.isUnloading('invoice')) {
  console.log('Plugin is currently being unloaded');
}

Plugin State Monitoring

pluginManager.pluginState$.subscribe(event => {
  console.log(`Plugin ${event.pluginName} is now ${event.state}`);
});

Concurrent Loading

await pluginManager.loadMany(['invoice', 'reports', 'analytics']);

Error Handling

All plugin errors are defensive and will not crash the host application.

try {
  await pluginManager.load('invoice');
} catch (error) {
  if (error instanceof PluginLoadError) {
    // Handle load failure
  }
}

Architecture

The plugin system consists of:

  • PluginManager: Orchestrates plugin lifecycle
  • PluginRegistry: Manages plugin state and metadata
  • PluginInjector: Creates isolated Angular injectors
  • PluginContext: Provides controlled host-plugin communication
  • PluginOutlet: Component for rendering plugins in templates

Plugin Lifecycle

Each plugin progresses through a well-defined lifecycle:

REGISTERED → LOADING → LOADED → ACTIVE → UNLOADING → UNLOADED

Lifecycle Hooks

Plugins can implement optional lifecycle hooks:

  • onLoad(context): Called when the plugin module is loaded
  • onActivate(context): Called when the plugin component is rendered
  • onDeactivate(): Called before the plugin component is removed
  • onDestroy(): Called during plugin cleanup

All hooks support both synchronous and asynchronous execution.

Production Considerations

Lifecycle Hook Timeouts (v1.1.0)

Plugin lifecycle hooks (onLoad, onActivate, onDeactivate, onDestroy) have a default timeout of 5 seconds to prevent infinite hangs. If a hook doesn't complete within this time, a PluginLifecycleTimeoutError is thrown.

Best Practices:

  • Keep lifecycle hooks lightweight
  • Move heavy operations to background tasks
  • Increase timeout for plugins with legitimate long initialization: lifecycleHookTimeout: 10000
  • Disable timeout only for trusted plugins: lifecycleHookTimeout: 0
// Example: Plugin with long initialization
providePluginSystem({
  lifecycleHookTimeout: 15000 // 15 seconds for data-intensive plugins
})

Memory Management (v1.1.0)

Version 1.1.0 includes automatic memory leak prevention:

  • Component references cleared after destruction
  • Plugin contexts destroyed on unload and load failures
  • Injectors properly cleaned up
  • Event handlers removed when plugin unloads

For long-running applications:

  • Monitor plugin load/unload cycles
  • Avoid excessive rapid reloading
  • Use getPluginInfo() to track error counts

Known Limitations

The current version has the following intentional limitations:

  • No Plugin Dependencies: Plugins cannot declare dependencies on other plugins (planned for v2)
  • No Version Checking: No automatic compatibility validation between plugins (planned for v2)
  • No Hot Reload: Plugin updates require reloading (HMR support planned for v2)
  • No Router Integration: Plugins cannot register routes dynamically (planned for v2)
  • No Advanced Sandboxing: Isolation is via injector only, not iframe-based (planned for v3)
  • No Remote Loading: Plugins must be bundled with the application (planned for v2)
  • No Marketplace: No built-in plugin discovery or installation system (planned for v3)

These limitations keep v1 focused, stable, and production-ready while maintaining a clear roadmap for future enhancements.

Roadmap

v2.0 - Enhanced Plugin Management

  • Plugin dependency resolution and loading order
  • Version compatibility checking
  • Dynamic route registration for plugins
  • Remote plugin loading from CDN/server
  • Configuration management system
  • Enhanced debugging and dev tools

v3.0 - Enterprise Features

  • Advanced sandboxing with iframe isolation
  • Plugin marketplace integration
  • Permissions and security policies
  • Analytics and telemetry hooks
  • Plugin signing and verification
  • Multi-version plugin support

Requirements

  • Angular >= 16.0.0
  • TypeScript >= 5.0.0
  • RxJS >= 7.5.0

Documentation

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

License

MIT - See LICENSE for details.