Package Exports
- @angular-dynamic/plugin-system
Readme
Angular Dynamic Plugin System
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-systemQuick 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 → UNLOADEDLifecycle 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
- Architecture Guide - Deep dive into system design
- API Reference - Complete API documentation
- Migration Guide - Upgrading between versions
- Contributing Guide - How to contribute
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
License
MIT - See LICENSE for details.