Analysis and transformation utilities used by plugins. Plugins must use this API and never manipulate the AST directly.
The @angular-modernizer/api package provides high-level, reusable tools that plugins use to analyze and transform Angular codebases. This package contains the core business logic that powers the entire platform.
The API follows the "API-Driven Plugin" pattern:
// Incorrect: plugin implements low-level AST logic
class BadPlugin {
analyze(sourceFile: SourceFile) {
// 100 lines of AST manipulation code
}
}
// Correct: plugin uses API tools
class GoodPlugin {
analyze(context: AnalysisContext) {
const usages = context.api.analysis.templateAnalyzer.analyze(template);
const hasOnPush = context.api.transformation.importManager.hasImport(
context.sourceFile,
'ChangeDetectionStrategy',
'@angular/core',
);
// Focus on business logic, not implementation details
}
}
The PublicApi is the tool set plugins receive as context.api. createPublicApi(project, options?) builds it for one ts-morph project (options: rootPath, pattern, componentDecorators):
import { createPublicApi } from '@angular-modernizer/api';
import { Project } from 'ts-morph';
const project = new Project({ tsConfigFilePath: 'tsconfig.json' });
const api = createPublicApi(project);
const templateUsages = api.analysis.templateAnalyzer.analyze(template);
api.transformation.importManager.addNamedImport(sourceFile, '@angular/core', 'inject');
It contains:
| Path | Tool |
|---|---|
analysis.servicePatternRecognizer |
ServicePatternRecognizer |
analysis.serviceDetector |
ServiceDetector |
analysis.responsibilityClassifier |
ResponsibilityClassifier |
analysis.symbolLocator |
SymbolLocator |
analysis.memberWriteFinder |
MemberWriteFinder |
analysis.templateLoader |
ComponentTemplateLoader |
analysis.templateAnalyzer |
TemplateAnalyzer |
transformation.ngModuleManager |
NgModuleManager |
transformation.importManager |
ImportManager |
ngMorph |
NgMorphAdapter |
project |
the ts-morph Project |
Other tools (DependencyAnalyzer, SelectorMapper, TemplateUsageAnalyzer, the investigation classes such as UsageFinder or CallGraphBuilder) are exported from the package and created directly.
Purpose: extract all template usages.
class TemplateAnalyzer {
analyze(template: string): TemplateUsage[];
extractInlineTemplate(decoratorText: string): string | undefined;
usesSelector(template: string, selector: string): boolean;
}
interface TemplateUsage {
type: 'element' | 'directive' | 'pipe' | 'component' | 'structural-directive' | 'control-flow';
name: string;
line?: number;
column?: number;
raw?: string;
attributes?: Record<string, string>;
metadata?: Record<string, unknown>;
}
Design: extracts all usages from templates without filtering or heuristics. Provides raw data for downstream decision-making.
Example:
const api = createPublicApi(project);
const usages = api.analysis.templateAnalyzer.analyze(`
<app-user [user]="currentUser" (save)="onSave()">
<div *ngIf="loading">Loading...</div>
<input [(ngModel)]="name">
</app-user>
`);
// Result (one entry per usage, with line and column):
// - element 'app-user', element 'div', element 'input'
// - structural-directive '*ngIf'
// - directive usages for the bindings
Purpose: build comprehensive symbol maps. Not part of PublicApi; create it with new DependencyAnalyzer().
class DependencyAnalyzer {
analyzeDependencies(sourceFile: SourceFile): Dependency[];
dependsOn(sourceFile: SourceFile, targetPath: string): boolean;
buildSymbolMap(moduleFile: SourceFile, project?: Project): SymbolMap;
getTransitiveDependencies(sourceFile: SourceFile, project: Project): Dependency[];
detectCircularDependencies(project: Project): { cycle: string[]; severity: 'warning' | 'error' }[];
extractSymbolInfo(sourceFile: SourceFile, className: string): SymbolInfo | undefined;
}
interface Dependency {
from: string;
to: string;
type: 'import' | 'component-usage' | 'directive-usage' | 'service-injection';
symbol?: string;
metadata?: Record<string, unknown>;
}
interface SymbolMap {
declared: Map<string, SymbolInfo>;
imported: Map<string, SymbolInfo>;
exported: Map<string, SymbolInfo>;
available: Map<string, SymbolInfo>;
}
interface SymbolInfo {
className: string;
selectors: string[];
inputs: string[];
outputs: string[];
type: 'component' | 'directive' | 'pipe' | 'module';
filePath: string;
}
Key features:
Example:
const analyzer = new DependencyAnalyzer();
const moduleFile = project.addSourceFileAtPath('user.module.ts');
const symbolMap = analyzer.buildSymbolMap(moduleFile, project);
// symbolMap.declared: UserComponent with selectors ['app-user'], inputs ['user'], outputs ['save']
// symbolMap.exported: the symbols UserModule exports
Also supports advanced analysis:
// Detect circular dependencies
const cycles = analyzer.detectCircularDependencies(project);
// Get transitive dependencies
const transitiveDeps = analyzer.getTransitiveDependencies(sourceFile, project);
// Extract symbol information
const symbolInfo = analyzer.extractSymbolInfo(sourceFile, 'MyComponent');
console.log(symbolInfo?.inputs); // ['input1', 'customAlias']
console.log(symbolInfo?.outputs); // ['output1']
Purpose: classify the public methods of a class by concern and backend area, and decide whether it is a service bag.
const result = api.analysis.responsibilityClassifier.classifyClass(classDecl);
result.methods; // role per public method: a concern, 'delegation' or 'uncertain'
result.concerns; // concern groups ('data-access', 'event', 'cache', 'ui-dom', ...) with `counted`
result.areas; // backend areas of the data-access methods (from HTTP URLs) with `counted`
result.uncertain; // methods without a signal >= 0.7, with their signals
result.assessment; // { isBag, kind: 'service' | 'static-utility' | 'domain-candidate' | 'none', reason, confidence }
Roles come from typed signals (HttpClient calls, reactive fields, DOM, file, dialog/router, storage, validators, ...); a method name alone never decides. resolveUrlTemplate(expr) (also exported) resolves an HTTP URL expression to a path template and its backend area. Used by the architecture:service-bag rule and the service-bag-transform.
Purpose: safely manage TypeScript import statements.
class ImportManager {
addNamedImport(sourceFile: SourceFile, moduleSpecifier: string, namedImport: string, alias?: string): void;
removeNamedImport(sourceFile: SourceFile, namedImport: string, moduleSpecifier?: string): void;
getImports(sourceFile: SourceFile): ImportInfo[];
organizeImports(sourceFile: SourceFile): void;
mergeImports(sourceFile: SourceFile): void;
cleanupEmptyImports(sourceFile: SourceFile): void;
hasImport(sourceFile: SourceFile, symbol: string, module?: string): boolean;
getImportAlias(sourceFile: SourceFile, symbol: string, module?: string): string | undefined;
}
Key features:
Example:
const importManager = new ImportManager();
importManager.addNamedImport(sourceFile, '@angular/core', 'OnInit');
importManager.addNamedImport(sourceFile, '@angular/core', 'Directive', 'NgDirective');
importManager.removeNamedImport(sourceFile, 'OnInit', '@angular/core');
const hasImport = importManager.hasImport(sourceFile, 'Component', '@angular/core');
const alias = importManager.getImportAlias(sourceFile, 'Component', '@angular/core');
importManager.mergeImports(sourceFile);
importManager.cleanupEmptyImports(sourceFile);
Budgets from __tests__/performance/benchmark.test.ts (asserted only with PERF_ASSERTS=1):
import { createPublicApi, DependencyAnalyzer } from '@angular-modernizer/api';
import { ContextFactory } from '@angular-modernizer/plugin-system';
const api = createPublicApi(project);
// 1. Analyze template dependencies
const template = `<div *ngIf="loading" [user]="user" (save)="onSave()"></div>`;
const templateUsages = api.analysis.templateAnalyzer.analyze(template);
// 2. Get component dependencies
const sourceFile = project.addSourceFileAtPath('user.component.ts');
const dependencies = new DependencyAnalyzer().analyzeDependencies(sourceFile);
// 3. Transform component to standalone
const context = ContextFactory.createTransformContext({
sourceFile,
project,
api,
config: {},
});
// The plugin uses the API to:
// - Add standalone: true to @Component decorator
// - Build imports array from templateUsages + dependencies
// - Add necessary import statements
All API methods are stateless and side-effect free:
const result1 = api.analysis.templateAnalyzer.analyze(template);
const result2 = api.analysis.templateAnalyzer.analyze(template);
assert.deepEqual(result1, result2); // Always true
Tools work together seamlessly:
const templateUsages = api.analysis.templateAnalyzer.analyze(template);
const dependencies = new DependencyAnalyzer().analyzeDependencies(sourceFile);
const service = api.analysis.serviceDetector.detectService(classDecl);
APIs handle edge cases gracefully:
// Handles malformed templates
const usages = api.analysis.templateAnalyzer.analyze('<invalid html>>>');
// Returns empty array or best-effort parsing
// A file without imports
const dependencies = new DependencyAnalyzer().analyzeDependencies(emptyFile);
// Returns an empty array, no crash
Efficient algorithms for large codebases:
ServiceDetector, SelectorMapper)pnpm --filter @angular-modernizer/api test
pnpm --filter @angular-modernizer/api test -- --coverage
Test categories:
Testing patterns:
import { createPublicApi, type PublicApi } from '@angular-modernizer/api';
import { Project } from 'ts-morph';
describe('TemplateAnalyzer', () => {
let api: PublicApi;
let project: Project;
beforeEach(() => {
project = new Project({ useInMemoryFileSystem: true });
api = createPublicApi(project);
});
it('should extract component usages', () => {
const template = '<app-user [user]="user"></app-user>';
const usages = api.analysis.templateAnalyzer.analyze(template);
expect(usages).toContainEqual(
expect.objectContaining({ type: 'element', name: 'app-user' }),
);
});
});
packages/api/
├── src/
│ ├── analysis/ # TemplateAnalyzer, DependencyAnalyzer, ResponsibilityClassifier, ...
│ ├── investigation/ # UsageFinder, CallGraphBuilder, TemplateInspector, ...
│ ├── metrics/
│ ├── transformation/ # ImportManager, NgModuleManager, NgMorphAdapter
│ ├── public-api.ts # PublicApi, createPublicApi
│ └── index.ts
├── __tests__/
│ ├── analysis/
│ ├── investigation/
│ ├── metrics/
│ ├── performance/
│ └── transformation/
├── package.json
├── tsconfig.json
├── jest.config.js
└── README.md
NgMorphAdapter)When adding new API tools:
// 1. Create the tool interface
export interface NewAnalyzer {
analyze(input: InputType): OutputType;
}
// 2. Implement the tool
export class NewAnalyzerImpl implements NewAnalyzer {
analyze(input: InputType): OutputType {
// Implementation
}
}
// 3. Add to the PublicApi interface (src/public-api.ts)
interface PublicApi {
analysis: {
// ... existing tools
newAnalyzer: NewAnalyzer;
};
}
// 4. Create it in createPublicApi(project, options?)
// analysis: { ..., newAnalyzer: new NewAnalyzerImpl() }
// 5. Export it from src/index.ts
MIT License - See LICENSE file for details
Analysis, transformation and investigation tools for Angular codebases, built on ts-morph. Plugins use these tools through
context.apiinstead of manipulating the AST directly.Main entry points: