Illuma provides a plugin system that allows you to extend its core functionality. The plugin system supports three types of plugins:
- Context Scanners – Extend injection detection to support custom patterns
- Diagnostics Modules – Analyze and report on container state after bootstrap
- Middlewares – Intercept and modify instance creation
- 🔌 Plugin System
The Illuma class is the central hub for managing plugins in Illuma. It provides static methods to register plugins globally, which will then be automatically invoked at the appropriate times during the container lifecycle.
import { Illuma } from '@illuma/core';
// Register a context scanner
Illuma.extendContextScanner(myScanner);
// Register a diagnostics module
Illuma.extendDiagnostics(myDiagnostics);
// Register a global middleware
Illuma.registerGlobalMiddleware(myMiddleware);Key characteristics:
- Plugins are registered globally and affect all container instances
- Context scanners run during detection phase (before building dependency graph)
- Middlewares run during instantiation phase (when creating instances)
- Diagnostics modules run after each container bootstrap completes
- Multiple plugins can be registered and execute in registration order
Note: Plugins must be registered before creating any container instances to ensure they are applied correctly. Execution order is not guaranteed due to potential imports of external packages via NPM.
Context scanners are plugins that extend Illuma's ability to detect dependency injections. By default, Illuma detects dependencies through nodeInject() calls. Context scanners allow you to add support for:
- Custom decorators (e.g.,
@CustomInject()) - Metadata-based injection patterns
- Property decorators
- Framework-specific injection patterns
- Alternative injection APIs
A context scanner must implement the iContextScanner interface:
import type { iInjectionNode } from '@illuma/core';
interface iContextScanner {
/**
* Scans the provided factory function for dependency injections.
*
* @param factory - The factory function to scan for dependencies
* @returns A set of detected injection nodes
*/
scan(factory: any): Set<iInjectionNode<any>>;
}Parameters:
factory: The factory function being analyzed (could be a class constructor or factory function)
Returns:
- A
Set<iInjectionNode<any>>containing all detected injection points
Important notes:
- Register scanners before providing services
- Scanners run in registration order
- Multiple scanners can be registered
- Scanners are global and affect all containers
Diagnostics modules analyze the container state after bootstrap and provide insights, warnings, or custom reporting. They receive a comprehensive report about the container's state, including:
- Total number of dependency nodes
- List of unused dependencies
- Bootstrap performance metrics
A diagnostics module must implement the iDiagnosticsModule interface:
import type { TreeNode } from '@illuma/core';
interface iDiagnosticsReport {
readonly totalNodes: number; // Total dependency nodes in container
readonly unusedNodes: TreeNode<unknown>[]; // Nodes that weren't resolved
readonly bootstrapDuration: number; // Bootstrap time in milliseconds
}
interface iDiagnosticsModule {
readonly onReport: (report: iDiagnosticsReport) => void;
}Report fields:
totalNodes: Total number of dependency nodes registeredunusedNodes: Array of nodes that were never resolved during bootstrapbootstrapDuration: Time taken to bootstrap the container (in ms)
import type { iDiagnosticsModule, iDiagnosticsReport } from '@illuma/core';
export class PerformanceReporter implements iDiagnosticsModule {
public onReport(report: iDiagnosticsReport): void {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('📊 Container Performance Report');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(`⚡ Bootstrap Time: ${report.bootstrapDuration}ms`);
console.log(`📦 Total Dependencies: ${report.totalNodes}`);
console.log(`✅ Used Dependencies: ${report.totalNodes - report.unusedNodes.length}`);
console.log(`⚠️ Unused Dependencies: ${report.unusedNodes.length}`);
if (report.bootstrapDuration > 1000) {
console.warn('⚠️ WARNING: Bootstrap took longer than 1 second!');
}
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
}
}Throw an error if any dependencies are unused (strict mode):
import type { iDiagnosticsModule, iDiagnosticsReport } from '@illuma/core';
export class StrictUnusedValidator implements iDiagnosticsModule {
public onReport(report: iDiagnosticsReport): void {
if (report.unusedNodes.length > 1) { // Leave one unused for entry point
const unusedList = report.unusedNodes
.map(node => ` - ${node.toString()}`)
.join('\n');
throw new Error(
`Strict mode violation: Found ${report.unusedNodes.length} unused dependencies:\n${unusedList}`
);
}
}
}Send diagnostics to a logging service:
import type { iDiagnosticsModule, iDiagnosticsReport } from '@illuma/core';
export class JsonDiagnosticsLogger implements iDiagnosticsModule {
constructor(private readonly loggerService: LoggerService) {}
public onReport(report: iDiagnosticsReport): void {
const diagnostics = {
timestamp: new Date().toISOString(),
container: {
totalNodes: report.totalNodes,
usedNodes: report.totalNodes - report.unusedNodes.length,
unusedNodes: report.unusedNodes.map(node => node.toString()),
bootstrapDuration: report.bootstrapDuration,
},
metrics: {
usageRate: ((report.totalNodes - report.unusedNodes.length) / report.totalNodes) * 100,
isHealthy: report.unusedNodes.length === 0,
performanceGrade: this.getPerformanceGrade(report.bootstrapDuration),
}
};
this.loggerService.log('container.diagnostics', diagnostics);
}
private getPerformanceGrade(durationMs: number): string {
if (durationMs < 20) return 'A';
if (durationMs < 50) return 'B';
if (durationMs < 100) return 'C';
return 'D';
}
}Diagnostics modules should be registered before bootstrapping the container. To enable the diagnostics system, you must call enableIllumaDiagnostics() from @illuma/core/plugins:
import { Illuma, NodeContainer } from '@illuma/core';
import { PerformanceReporter } from './diagnostics';
import { enableIllumaDiagnostics } from '@illuma/core/plugins';
// 1. Enable diagnostics system
enableIllumaDiagnostics();
// 2. Register custom diagnostics module
Illuma.extendDiagnostics(new PerformanceReporter());
// 3. Create and configure container
const container = new NodeContainer({ measurePerformance: true });
container.provide([
UserService,
DatabaseService,
LoggerService
]);
// 4. Bootstrap - diagnostics will run after this
container.bootstrap();
// Output:
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 📊 Container Performance Report
// ...Important notes:
- Call
enableIllumaDiagnostics()before bootstrapping to enable diagnostics - Register custom modules before calling
bootstrap() - Multiple modules can be registered
- Modules execute in registration order
- Set
measurePerformance: truein container options to get accurate timing
Note: The
diagnostics: trueoption inNodeContainerconstructor is no longer supported since version2.0.0. UseenableIllumaDiagnostics()instead.
Middlewares allow you to intercept and modify the instantiation process of dependencies in the container. They sit between the factory function execution and the returned instance, enabling you to:
- Log dependency creation
- Measure instantiation time
- Wrap instances in Proxies
- Modify or replace instances
Middlewares can be registered globally (for all containers) or locally (per container). They are also inherited by child containers from parent containers.
A middleware is a function that matches the iMiddleware signature:
import type { NodeBase } from '@illuma/core';
interface iInstantiationParams<T = unknown> {
readonly token: NodeBase<T>; // The token being instantiated
readonly factory: () => T; // The factory function that creates the instance
readonly deps: Set<Token<unknown>>; // The dependencies of the instance
}
type iMiddleware<T = unknown> = (
params: iInstantiationParams<T>,
next: (params: iInstantiationParams<T>) => T,
) => T;Parameters:
params: Context about what is being instantiatednext: Function that calls the next middleware or the actual factory
Returns:
- The resulting instance
T(or a modified/proxied version of it)
Log every time a dependency is instantiated:
import type { iMiddleware } from '@illuma/core';
export const loggerMiddleware: iMiddleware = (params, next) => {
console.log(`[Middleware] Creating instance of: ${params.token.name}`);
const start = Date.now();
const instance = next(params);
const duration = Date.now() - start;
console.log(`[Middleware] Created ${params.token.name} in ${duration}ms`);
return instance;
};Automatically wrap certain services in a Proxy:
import type { iMiddleware } from '@illuma/core';
export const proxyMiddleware: iMiddleware = (params, next) => {
const instance = next(params);
// Only apply to classes ending with "Service"
if (params.token.name.endsWith('Service')) {
return new Proxy(instance as object, {
get(target, prop) {
console.log(`Accessing ${params.token.name}.${String(prop)}`);
return Reflect.get(target, prop);
}
});
}
return instance;
};Affects all containers created thereafter.
import { Illuma } from '@illuma/core';
Illuma.registerGlobalMiddleware(loggerMiddleware);Affects only the specific container instance.
import { NodeContainer } from '@illuma/core';
const container = new NodeContainer();
container.registerMiddleware(proxyMiddleware); // Local middleware
container.provide([UserService]);
container.bootstrap();- Keep scanners focused: Each scanner should handle one injection pattern
- Avoid side effects: Scanners should only read, not modify state
- Handle errors gracefully: Don't let scanner errors break the container
- Performance matters: Scanners run for every provider, keep them fast
- Test thoroughly: Test scanners with various factory function types
Scanner performance tips:
export class OptimizedScanner implements iContextScanner {
public scan(factory: any): Set<iInjectionNode<any>> {
// Early return for non-functions just in case for future API changes
if (typeof factory !== 'function') {
return new Set();
}
// Cache metadata lookups
const metadata = this.getCachedMetadata(factory);
if (!metadata) {
return new Set();
}
// Process efficiently
return this.processMetadata(metadata);
}
}Support property-based injection using decorators:
import type { iContextScanner, NodeToken, iInjectionNode } from '@illuma/core';
const PROPERTY_INJECT_KEY = Symbol('di:properties');
// Property decorator
export function InjectProperty<T>(token: NodeToken<T>) {
return function (target: any, propertyKey: string) {
const properties = Reflect.getMetadata(PROPERTY_INJECT_KEY, target.constructor) || [];
properties.push({ propertyKey, token });
Reflect.defineMetadata(PROPERTY_INJECT_KEY, properties, target.constructor);
};
}
// Scanner implementation
export class PropertyInjectionScanner implements iContextScanner {
public scan(factory: any): Set<iInjectionNode<any>> {
const injections = new Set<iInjectionNode<any>>();
if (typeof factory !== 'function') {
return injections;
}
const properties = Reflect.getMetadata(PROPERTY_INJECT_KEY, factory);
if (!properties) {
return injections;
}
for (const { token } of properties) {
injections.add({ token, optional: false });
}
return injections;
}
}Register the scanner:
Illuma.extendContextScanner(new PropertyInjectionScanner());Now properties decorated with @InjectProperty() will be detected, but not injected automatically. You will need to implement property injection logic yourself.
Only report diagnostics in development mode:
import { enableIllumaDiagnostics } from '@illuma/core/plugins';
import type { iDiagnosticsModule, iDiagnosticsReport } from '@illuma/core';
export class ConditionalReporter implements iDiagnosticsModule {
constructor(
private readonly enabled: boolean = process.env.NODE_ENV !== 'production'
) {}
public onReport(report: iDiagnosticsReport): void {
if (!this.enabled) {
return;
}
// Detailed reporting for development
console.group('🔍 Container Diagnostics (Development Mode)');
console.log('Total Nodes:', report.totalNodes);
console.log('Bootstrap Duration:', `${report.bootstrapDuration}ms`);
if (report.unusedNodes.length > 0) {
console.group('⚠️ Unused Dependencies:');
for (const node of report.unusedNodes) {
console.log(` - ${node.toString()}`);
}
console.groupEnd();
} else {
console.log('✅ All dependencies are being used');
}
console.groupEnd();
}
}
// Usage - enable diagnostics and register the reporter
if (process.env.NODE_ENV === 'development') {
enableIllumaDiagnostics();
Illuma.extendDiagnostics(new ConditionalReporter());
}Understanding when plugins execute is crucial for proper usage:
// 1. Enable diagnostics (if needed)
enableIllumaDiagnostics();
// 2. Register plugins (before container creation)
Illuma.extendContextScanner(myScanner);
Illuma.extendDiagnostics(myDiagnosticsModule);
// 3. Create container
const container = new NodeContainer({ measurePerformance: true });
// 4. Provide services (scanners run here for each provider)
container.provide([
UserService, // Scanner runs
DatabaseService, // Scanner runs
LoggerService // Scanner runs
]);
// 5. Bootstrap (diagnostics modules run after this)
container.bootstrap();
// → All diagnostics modules execute with reportTimeline:
- Enable Diagnostics: Call
enableIllumaDiagnostics()to activate the system - Plugin Registration: Plugins added to global registry
- Provider Registration: Context scanners run for each provider
- Bootstrap: Container resolves dependencies
- Post-Bootstrap: Diagnostics modules receive report
- GitHub: git-illuma/reflect
- NPM: @illuma/reflect
- Explore the API Reference for detailed type information
- Learn about Tokens for creating custom injection tokens
- Check out Providers to understand provider types
- Read Troubleshooting for common issues
For questions or issues with plugins, please open an issue on GitHub.