Angular Modernizer
    Preparing search index...

    Module @angular-modernizer/plugin-system - v1.2.0

    Plugin contracts for the Angular Modernizer.

    Defines what a plugin is (Plugin), the three rule kinds it can contribute (AnalysisRule, TransformRule, ValidationRule) and the context objects the kernel passes to them (AnalysisContext, TransformContext, ValidationContext). ContextFactory builds those contexts. IKernel is the minimal kernel view a plugin sees during Plugin.initialize.

    Plugin authors import only from @angular-modernizer/plugin-system.

    @angular-modernizer/plugin-system

    Plugin contracts and context-based dependency injection for the Angular Modernization Platform.

    The @angular-modernizer/plugin-system package defines the interfaces and context types that enable the API-driven plugin architecture. It provides the context objects and dependency injection patterns that keep plugins stateless and composable.

    The package contains interfaces plus one class, ContextFactory. Its only runtime dependency is ts-morph (types of SourceFile and Project).

    The plugin system uses a context-driven pattern where all dependencies are passed through context objects rather than constructor injection:

    // Constructor injection - tight coupling, avoid this
    class OldStylePlugin {
    constructor(
    private api: PublicApi,
    private project: Project,
    ) {}
    analyze() {
    /* uses this.api, this.project */
    }
    }

    // Context injection - loose coupling, use this
    class CorrectPlugin {
    constructor() {} // Stateless

    analyze(context: AnalysisContext) {
    const { api, project, sourceFile } = context;
    }
    }

    The contract all plugins must implement:

    interface Plugin {
    readonly name: string;
    readonly version: string;
    readonly description?: string;
    readonly author?: string;

    initialize(kernel: IKernel): Promise<void>;
    getAnalysisRules(): AnalysisRule[];
    getTransformRules(): TransformRule[];
    getValidationRules(): ValidationRule[];
    }

    IKernel is intentionally minimal: it only exposes getProjectRoot(). Plugins that need project-level access must defer that work until analyze() or transform() is called, where they receive the full context.

    Analyzes a file and returns one result per finding.

    interface AnalysisRule {
    readonly id: string;
    readonly name: string;
    readonly description: string;
    readonly severity: 'error' | 'warning' | 'info';
    readonly category: string;
    readonly tags?: string[];

    analyze(context: AnalysisContext): Promise<AnalysisResult[]>;
    }

    interface AnalysisResult {
    ruleId: string;
    message: string;
    filePath: string;
    line?: number;
    column?: number;
    suggestedFix?: string;
    severity?: 'error' | 'warning' | 'info';
    metadata?: Record<string, unknown>;
    }

    Modifies code automatically. Implementations must be idempotent, atomic, and reversible.

    interface TransformRule {
    readonly id: string;
    readonly name: string;
    readonly description: string;
    readonly category: string;
    readonly tags?: string[];

    transform(context: TransformContext): Promise<TransformResult>;
    }

    interface TransformResult {
    ruleId: string;
    modified: boolean;
    message: string;
    filePath: string;
    changeCount?: number;
    metadata?: Record<string, unknown>;
    }

    Validates transformations and code quality.

    interface ValidationRule {
    readonly id: string;
    readonly name: string;
    readonly description: string;
    readonly category: string;

    validate(context: ValidationContext): Promise<ValidationResult>;
    }

    interface ValidationResult {
    ruleId: string;
    passed: boolean;
    message: string;
    filePath: string;
    details?: string;
    metadata?: Record<string, unknown>;
    }

    Optional hooks: onBeforeAnalysis, onAfterAnalysis, onBeforeTransform, onAfterTransform, onShutdown (all () => Promise<void>).

    Context objects provide scoped access to dependencies. Always construct them via ContextFactory, never by hand. All contexts share BaseContext; api is typed by a generic parameter TApi (pass PublicApi from @angular-modernizer/api).

    interface BaseContext<TApi> {
    readonly sourceFile: SourceFile;
    readonly project: Project;
    readonly api: TApi;
    readonly config: Record<string, unknown>;
    readonly filePath: string;
    }

    Read-only access for analysis operations.

    interface AnalysisContext<TApi> extends BaseContext<TApi> {
    readonly type: 'analysis';
    readonly options?: { deep?: boolean; maxDepth?: number };
    }

    Read-write access for transformation operations.

    interface TransformContext<TApi> extends BaseContext<TApi> {
    readonly type: 'transform';
    readonly options?: { dryRun?: boolean; autoSave?: boolean; format?: boolean };
    }

    Access for validation operations after transformation.

    interface ValidationContext<TApi> extends BaseContext<TApi> {
    readonly type: 'validation';
    readonly options?: { failFast?: boolean };
    }

    The ContextFactory creates properly typed context objects and fills type, filePath (from sourceFile) and config (default {}). Always use it; never construct context objects directly.

    class ContextFactory {
    static createAnalysisContext<TApi>(params: {
    sourceFile: SourceFile;
    project: Project;
    api: TApi;
    config?: Record<string, unknown>;
    options?: AnalysisContext<TApi>['options'];
    }): AnalysisContext<TApi>;
    static createTransformContext<TApi>(params: { /* same, with TransformContext options */ }): TransformContext<TApi>;
    static createValidationContext<TApi>(params: { /* same, with ValidationContext options */ }): ValidationContext<TApi>;
    }

    AnalysisContext requires type, sourceFile, project, api, config and filePath. Tests that pass only { sourceFile } may compile under ts-jest's lenient inline tsconfig but will fail in pnpm run type-check.

    import {
    type Plugin,
    type IKernel,
    type AnalysisRule,
    type TransformRule,
    type ValidationRule,
    } from '@angular-modernizer/plugin-system';

    class MyPlugin implements Plugin {
    readonly name = '@my-org/plugin-custom';
    readonly version = '1.0.0';
    readonly description = 'Custom analysis and transformation rules';

    async initialize(kernel: IKernel): Promise<void> {
    // Optional initialization
    }

    getAnalysisRules(): AnalysisRule[] {
    return [new MyAnalysisRule()];
    }

    getTransformRules(): TransformRule[] {
    return [new MyTransformRule()];
    }

    getValidationRules(): ValidationRule[] {
    return [];
    }
    }
    import { Node } from 'ts-morph';
    import {
    type AnalysisRule,
    type AnalysisContext,
    type AnalysisResult,
    } from '@angular-modernizer/plugin-system';

    class DirectInstantiationRule implements AnalysisRule {
    readonly id = 'my-plugin:direct-instantiation';
    readonly name = 'Direct Service Instantiation';
    readonly description = 'Detects direct instantiation of services (DIP violation)';
    readonly severity = 'warning' as const;
    readonly category = 'solid';

    async analyze(context: AnalysisContext): Promise<AnalysisResult[]> {
    const { sourceFile, filePath } = context;
    const results: AnalysisResult[] = [];

    sourceFile.forEachDescendant((node) => {
    if (!Node.isNewExpression(node)) return;
    const className = node.getExpression().getText();
    if (!className.endsWith('Service')) return;

    results.push({
    ruleId: this.id,
    message: `Direct instantiation of ${className} violates Dependency Inversion Principle`,
    filePath,
    line: node.getStartLineNumber(),
    severity: this.severity,
    });
    });

    return results;
    }
    }
    import {
    type TransformRule,
    type TransformContext,
    type TransformResult,
    } from '@angular-modernizer/plugin-system';

    class AddInjectImportRule implements TransformRule {
    readonly id = 'my-plugin:add-inject-import';
    readonly name = 'Add inject import';
    readonly description = 'Adds the inject import from @angular/core';
    readonly category = 'angular';

    async transform(context: TransformContext): Promise<TransformResult> {
    const { sourceFile, filePath, api } = context;

    if (api.transformation.importManager.hasImport(sourceFile, 'inject', '@angular/core')) {
    return { ruleId: this.id, modified: false, message: 'Import already present', filePath };
    }

    api.transformation.importManager.addNamedImport(sourceFile, '@angular/core', 'inject');
    return { ruleId: this.id, modified: true, message: 'Added inject import', filePath, changeCount: 1 };
    }
    }
    import { ContextFactory } from '@angular-modernizer/plugin-system';
    import { createPublicApi } from '@angular-modernizer/api';

    const analysisContext = ContextFactory.createAnalysisContext({
    sourceFile,
    project,
    api: createPublicApi(project),
    config: {
    solid: { godSwitch: { maxCases: 10 } },
    },
    });

    const transformContext = ContextFactory.createTransformContext({
    sourceFile,
    project,
    api: createPublicApi(project),
    options: { dryRun: true },
    });

    All dependencies are injected via context objects:

    // Correct: context injection
    class MyRule implements AnalysisRule {
    analyze(context: AnalysisContext) {
    const { api, project, sourceFile } = context;
    }
    }

    // Wrong: constructor injection
    class MyRule implements AnalysisRule {
    constructor(private api: PublicApi) {}
    analyze(context: AnalysisContext) { /* uses this.api */ }
    }

    Rules must not hold internal state between invocations:

    // Correct: stateless
    class MyRule implements AnalysisRule {
    analyze(context: AnalysisContext): Promise<AnalysisResult[]> {
    // Same context always produces same result
    }
    }

    // Wrong: stateful
    class MyRule implements AnalysisRule {
    private analyzedFiles = new Set<string>(); // Do not do this
    }

    Context objects must not be mutated:

    // Correct
    function analyze(context: AnalysisContext) {
    const { sourceFile, api } = context;
    }

    // Wrong
    function analyze(context: AnalysisContext) {
    context.config.additionalSetting = true; // Do not do this
    }

    Rule IDs must follow plugin-name:rule-name format (e.g., angular:component-wrapper-instantiation, solid:srp-single-responsibility-violation). The prefix must match the plugin's registered name.

    Plugins must not manipulate the ts-morph AST directly. All analysis and transformation must go through context.api. Direct sourceFile.getClasses() calls bypass the API's caching and abstraction layers.

    packages/plugin-system/
    src/
    plugin-interface.ts # IKernel, Plugin, rule and result interfaces, PluginLifecycle
    contexts.ts # BaseContext, AnalysisContext, TransformContext, ValidationContext, ContextFactory
    index.ts
    package.json
    tsconfig.json
    jest.config.js
    README.md

    The package has no tests of its own (jest --passWithNoTests). Rule tests in the plugin packages build contexts with ContextFactory:

    import { ContextFactory } from '@angular-modernizer/plugin-system';
    import { createPublicApi } from '@angular-modernizer/api';
    import { Project } from 'ts-morph';

    const project = new Project({ useInMemoryFileSystem: true });
    const sourceFile = project.createSourceFile('/test/user.service.ts', 'export class UserService {}');
    const context = ContextFactory.createAnalysisContext({
    sourceFile,
    project,
    api: createPublicApi(project),
    });

    const results = await new MyAnalysisRule().analyze(context);
    1. Keep rules focused: one rule, one responsibility
    2. Use descriptive IDs: solid:dip-violation-direct-instantiation, not rule1
    3. Provide suggestedFix when possible
    4. Handle errors gracefully: do not throw on malformed code
    5. Write comprehensive tests covering edge cases
    1. Group related rules: one plugin per domain (SOLID, standalone, etc.)
    2. Use semantic versioning for API compatibility
    3. Initialize efficiently: defer I/O until analyze() or transform()
    1. Destructure contexts: const { api, sourceFile } = context;
    2. Read rule-specific settings from config
    3. Validate required context properties
    4. Return meaningful results including metadata and suggested fixes
    1. Maintain backward compatibility: do not break existing interfaces
    2. Add comprehensive types: strong typing for all interfaces
    3. Write integration tests: test rule composition and context usage
    4. Document with examples: show real usage patterns
    5. Consider extensibility: design for future enhancements
    1. Define the interface following existing patterns
    2. Add to Plugin interface: getNewRuleType(): NewRuleType[]
    3. Create context type: NewRuleContext
    4. Add factory method: ContextFactory.createNewRuleContext()
    5. Update documentation with examples

    Classes

    ContextFactory

    Interfaces

    AnalysisContext
    AnalysisResult
    AnalysisRule
    BaseContext
    IKernel
    Plugin
    PluginLifecycle
    TransformContext
    TransformResult
    TransformRule
    ValidationContext
    ValidationResult
    ValidationRule