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.4.0
NgModule Support - Load complete NgModules with your plugins!
// Plugin with NgModule support
export const PluginManifest = {
name: 'dashboard',
version: '1.0.0',
entryComponent: DashboardComponent,
entryModule: DashboardModule // NEW: Full NgModule support!
};Key Benefits:
- ✅ Load plugins with their own services, pipes, and directives
- ✅ Full dependency injection within the plugin module
- ✅ Automatic NgModule cleanup on unload
- ✅ Backward compatible - standalone components still work!
What's New in v1.3.0
Bulk Operations - Load and unload multiple plugins efficiently!
// Load multiple plugins in parallel
await pluginManager.loadAndActivateMany([
{ name: 'analytics', container: analyticsContainer },
{ name: 'reports', container: reportsContainer }
]);
// Unload all plugins at once
await pluginManager.unloadAll();
// Filter plugins by metadata
const proPlugins = pluginManager.getPluginsByMetadata({ tier: 'PRO' });What's New in v1.2.0
Remote Plugin Loading - Load plugins from external URLs at runtime!
// Load plugin from CDN
await pluginManager.registerRemotePlugin({
name: 'analytics',
remoteUrl: 'https://cdn.yourapp.com/plugins/analytics.js',
exposedModule: 'AnalyticsPlugin'
});
// Hot reload - update plugin without page refresh
await pluginManager.unregisterRemotePlugin('analytics');
await pluginManager.registerRemotePlugin({ /* new version */ });Key Features:
- ✅ Load plugins from CDNs or remote servers
- ✅ True plugin unloading with script tag removal
- ✅ Hot reload plugins without app restart
- ✅ Cache management and statistics
- ✅ Helper methods:
loadAndActivate(),loadRemoteAndActivate() - ✅ Perfect for SaaS multi-tenant and plugin marketplaces
Previous Releases:
- v1.1.2: Memory optimization with complete cleanup
- v1.1.0: Critical stability fixes (timeouts, race conditions, memory leaks)
No Migration Required: Fully backward compatible with v1.1.x
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
Remote Loading (v1.2.0)
- Remote plugin loading: Load from CDN, remote servers, or any HTTPS URL
- Hot reload support: Update plugins without page refresh
- Script tag management: Automatic injection and cleanup
- Cache control: Built-in caching with statistics and manual cache clearing
- Helper methods:
loadAndActivate(),loadRemoteAndActivate()for common patterns
Stability & Safety
- Lifecycle hook timeout protection: Prevents infinite hangs (default: 5s)
- Memory leak prevention: Complete cleanup including script tags and references
- 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'
};2b. Create a Plugin with NgModule (v1.4.0)
For plugins that need their own services, pipes, or directives:
import { NgModule, Component, Injectable } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PluginLifecycle, PluginContext } from '@angular-dynamic/plugin-system';
// Plugin-specific service
@Injectable()
export class DashboardService {
getData() {
return { visits: 1234, sales: 567 };
}
}
// Plugin component
@Component({
selector: 'dashboard-plugin',
template: `
<div class="dashboard">
<h2>Dashboard</h2>
<p>Visits: {{ data.visits }}</p>
<p>Sales: {{ data.sales }}</p>
</div>
`
})
export class DashboardComponent implements PluginLifecycle {
data = { visits: 0, sales: 0 };
constructor(private dashboardService: DashboardService) {}
async onLoad(context: PluginContext): Promise<void> {
this.data = this.dashboardService.getData();
}
}
// Plugin NgModule
@NgModule({
declarations: [DashboardComponent],
imports: [CommonModule],
providers: [DashboardService]
})
export class DashboardModule {}
// Export manifest with entryModule
export const PluginManifest = {
name: 'dashboard',
version: '1.0.0',
entryComponent: DashboardComponent,
entryModule: DashboardModule // NgModule with services!
};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.2.0: Remote Loading
registerRemotePlugin(config: RemotePluginConfig): Promise<PluginMetadata>;
unregisterRemotePlugin(pluginName: string): Promise<void>;
getRemoteCacheStats(): { size: number; entries: Array<{url: string; loadedAt: Date}> };
clearRemoteCache(): void;
// v1.2.0: Helper Methods
loadAndActivate(pluginName: string, viewContainer: ViewContainerRef): Promise<ComponentRef>;
loadRemoteAndActivate(config: RemotePluginConfig, viewContainer: ViewContainerRef): Promise<ComponentRef>;
// v1.3.0: Bulk Operations
unloadAll(): Promise<void>;
loadAndActivateMany(plugins: Array<{name: string, container: ViewContainerRef}>): Promise<ComponentRef[]>;
getPluginsByMetadata(filter: Record<string, any>): PluginMetadata[];
// v1.1.0: Monitoring
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 Router Integration: Plugins cannot register routes dynamically (planned for v2)
- No Advanced Sandboxing: Isolation is via injector only, not iframe-based security sandbox (planned for v3)
- No Marketplace Integration: No built-in plugin discovery or installation system (planned for v3)
- Basic Remote Loading: v1.2 supports remote loading via script tags; advanced features like plugin signing, CDN trust verification, and SRI (Subresource Integrity) are planned for v2
⚠️ Security Note: Remote loading executes external code in your application context. See SECURITY.md for critical security practices including CSP configuration, allowlisting, and source verification.
These limitations keep v1.x 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
- Advanced remote loading: Plugin signing, CDN trust verification, SRI support
- Configuration management system
- Enhanced debugging and dev tools
v3.0 - Enterprise Features
- Advanced sandboxing with iframe-based security isolation
- Plugin marketplace integration
- Permissions and security policies
- Analytics and telemetry hooks
- Multi-version plugin support
Note: Roadmap features are subject to community feedback and real-world adoption patterns.
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.