@angular-modernizer/plugin-architecture
v1.2.0
Published
Architecture and code quality analysis rules for the Angular Modernization Platform
Maintainers
Readme
@angular-modernizer/plugin-architecture
Architecture and code quality analysis plugin for the Angular Modernization Platform.
Overview
The plugin provides 17 analysis rules for structural problems in Angular code bases: import cycles, imports across layer boundaries, files in the wrong layer, oversized classes and components, services that mix concerns, and groups of shared code that could become a library. All rules only report; the plugin has no transformation or validation rules. Fixes are done by transform rules in other plugins (for example the service bag split in @angular-modernizer/plugin-angular).
ArchitecturePlugin is the entry point for the kernel. Besides the rules, the package exports building blocks that other plugins and tools reuse: the value-import graph, the module resolver, the layer path matcher, the library extraction scanner and helpers for responsibility patterns and RxJS subscription safety.
Installation
npm install @angular-modernizer/plugin-architectureThe package is an ES module and needs Node.js ^20.19.0 || ^22.12.0 || >=24.0.0. It depends on @angular-modernizer/plugin-system, @angular-modernizer/api, @angular-modernizer/core and ts-morph.
Analysis Rules
| Rule ID | Severity | Description |
|-|-|-|
| solid:circular-dependency | error | Runtime import cycles (aliases, relative paths, barrels) between project files; cycles with DI or decorator usage are flagged as NG0919 risks |
| solid:import-direction-violation | error | Imports against built-in layer conventions (app, features, shared, core, libs, ...) inferred from paths and tsconfig aliases |
| architecture:layer-boundary-violation | error | Imports that violate the layer topology configured in layers or layersSource |
| architecture:content-placement | warning | Files outside the layers the placement map allows for their content type |
| solid:god-object-violation | error | Classes with more than 30 methods, more than 1000 lines, more than 10 dependencies or mixed architectural concerns |
| architecture:component-complexity | warning | Components with more than 10 methods, more than 2 concerns or without OnPush |
| architecture:service-architecture | warning | Service files that export interfaces, types or enums, and services with more than 5 constructor dependencies |
| architecture:service-bag | error | Services and static helpers with 11 or more public methods that mix concerns or backend areas |
| architecture:static-in-stateful-class | warning | Public static methods in undecorated classes with instance state |
| architecture:library-extraction-opportunity | warning | Cohesive artifact groups in shared folders that could be extracted into Angular libraries |
| architecture:model-separation | warning | Files that export models, interfaces, enums, tokens, constants, functions or classes next to implementation code |
| architecture:rxjs-memory-leak | warning | Undisposed subscriptions and missing takeUntil patterns |
| architecture:advanced-content-placement | warning | Folder structure and domain-driven design patterns |
| architecture:error-handling-pattern | warning | Empty or log-only catch blocks, silent catchError, thrown non-Error values, unhandled async errors |
| architecture:inheritance-analysis | warning | Deep inheritance chains, god object base classes, composition over inheritance |
| architecture:performance-pattern | warning | Performance anti-patterns: complex template getters and expressions, missing trackBy, OnPush or virtual scrolling |
| architecture:cross-file-architecture | warning | Architectural dependencies across multiple files and architectural drift |
The first ten rule classes are exported (CircularDependencyRule, ImportDirectionViolationRule, LayerBoundaryViolationRule, ContentPlacementRule, GodObjectRule, ComponentComplexityRule, ServiceArchitectureRule, ServiceBagAnalysisRule, StaticInStatefulClassRule, LibraryExtractionAnalysisRule). The last seven are only available through ArchitecturePlugin.getAnalysisRules().
Three rule IDs use the solid: prefix; the rules belong to this plugin.
Configuration
ArchitecturePlugin.initialize() reads .angular-modernizer.json from the project root of the kernel (the directory of tsConfigPath, otherwise the current working directory). If the file cannot be loaded, the error is logged and all rules run with their defaults and without layers.
Layers
architecture:layer-boundary-violation and architecture:content-placement need a layer topology. Without it both rules report nothing.
{
"layers": [
{
"name": "core",
"alias": "@core",
"paths": ["src/app/core/**"],
"canImportFrom": []
},
{
"name": "shared",
"alias": "@shared",
"paths": ["src/app/shared/**"],
"canImportFrom": ["@core"]
},
{
"name": "features",
"alias": "@features",
"paths": ["src/app/features/**"],
"canImportFrom": ["@core", "@shared"]
}
]
}name: layer name used in messages and incanImportFromalias: tsconfig path alias of the layerpaths: globs relative to the project root. A layer withoutpathsuses the directory its alias maps to in tsconfigpaths. The most specific matching pattern wins, independent of the order of the layers.canImportFrom: names or aliases of the layers this layer may import from. An empty array forbids imports from all other layers.
The layer-boundary rule checks every import and export ... from. Specifiers are resolved with TypeScript module resolution (tsconfig paths, baseUrl, relative paths, index.ts); unresolved aliased imports fall back to alias-prefix matching. Type-only imports are reported too and marked with typeOnly: true in the metadata.
Layers from an external source
A project that already defines its layers for an arch-validator script can reference that module instead of repeating it:
{
"layersSource": {
"format": "arch-validator",
"path": "tools/arch-validator/config.js",
"exportName": "CONFIG"
}
}Each layer key becomes name, paths becomes [key + '/**'], pathAlias becomes alias and allowedImports becomes canImportFrom. When both layers and layersSource are set, layersSource wins and the differences are logged and returned by ArchitecturePlugin.getLayerDriftWarnings().
Content placement
architecture:content-placement also needs a placement map: the layers (names or aliases) allowed per content type. Content types: service, component, guard, interceptor, directive, pipe, model, utility.
{
"rules": {
"architecture:content-placement": {
"placement": {
"service": ["@core", "@shared"],
"guard": ["@core"],
"component": ["@shared", "@features"]
}
}
}
}Barrel files (index.ts, public-api.ts, exports.ts), specs, tests, config files and declaration files are skipped.
Library extraction
architecture:library-extraction-opportunity reads these fields from rules['architecture:library-extraction-opportunity']:
| Field | Default | Meaning |
|-|-|-|
| minCohesionScore | 0.3 | Minimum cohesion (0-1) of an artifact cluster |
| minReusabilityScore | 20 | Minimum reusability score (0-100) of an artifact |
| targetFolders | ["shared", "common", "core", "libs"] | Folder names to scan; a path segment matches when it contains a name |
| categoryOverrides | {} | Fixed category per class name (ui, forms, layout, utils, data, core, feature) |
| excludePatterns | [] | Globs of files to skip; spec and test files are always skipped |
The other fields of the config type (libraryType, outputFolder, libraryPrefix, ...) are used by the library extraction transform, not by this rule.
The scan covers the whole project. The rule runs it once per project and file count and reports each opportunity once, at its primary file.
Other rules
The remaining rules have fixed thresholds and need no configuration. solid:circular-dependency resolves imports with the tsconfig the project was loaded from (else the nearest tsconfig.json) and reads files from disk, so the result does not depend on which files a scan has loaded.
Usage
Via MCP
The scan-architecture MCP tool runs this plugin; scan-all runs it together with the other analysis plugins.
{
"rootPath": "/path/to/angular/project",
"rules": ["solid:circular-dependency", "architecture:layer-boundary-violation"]
}Without rules, all 17 rules run.
With the Kernel
import path from 'node:path';
import { Kernel } from '@angular-modernizer/core';
import { createPublicApi } from '@angular-modernizer/api';
import { ContextFactory } from '@angular-modernizer/plugin-system';
import { ArchitecturePlugin } from '@angular-modernizer/plugin-architecture';
const kernel = new Kernel({
tsConfigPath: path.join(projectPath, 'tsconfig.json'),
plugins: [new ArchitecturePlugin()],
});
await kernel.initialize();
const project = kernel.getProject();
const api = createPublicApi(project);
const rules = kernel
.getPlugin('@angular-modernizer/plugin-architecture')!
.getAnalysisRules();
for (const sourceFile of project.getSourceFiles()) {
const context = ContextFactory.createAnalysisContext({
sourceFile,
project,
api,
});
for (const rule of rules) {
const results = await rule.analyze(context);
results.forEach((r) => console.info(`${r.ruleId} ${r.filePath}:${r.line}: ${r.message}`));
}
}Single rule
import { LayerBoundaryViolationRule } from '@angular-modernizer/plugin-architecture';
const rule = new LayerBoundaryViolationRule(layers, { projectRoot });
const results = await rule.analyze(context);Commands
pnpm --filter @angular-modernizer/plugin-architecture build
pnpm --filter @angular-modernizer/plugin-architecture testRelated Packages
- @angular-modernizer/plugin-system - plugin contracts
- @angular-modernizer/api - public API used by the rules
- @angular-modernizer/core - kernel and configuration
- @angular-modernizer/plugin-solid - SOLID analysis rules
