This document provides a technical overview of how ArchUnitTS works under the hood.
ArchUnitTS is built on top of the TypeScript Compiler, Node's File System and more. It uses graph analysis techniques to enforce architectural rules.
┌─────────────────────────────────────────────────────┐
│ User API Layer │
│ (projectFiles(), classes(), metrics()) │
├─────────────────────────────────────────────────────┤
│ Rule Definition & Fluent API │
│ (shouldNot(), dependOn(), haveNoCycles()) │
├─────────────────────────────────────────────────────┤
│ Graph Extraction │
│ (TypeScript AST → Dependency Graph) │
├─────────────────────────────────────────────────────┤
│ Analysis & Validation │
│ (Cycle Detection, Dependency Analysis, Metrics) │
├─────────────────────────────────────────────────────┤
│ TypeScript Compiler API │
│ (AST Parsing, Type Checking, etc.) │
└─────────────────────────────────────────────────────┘
The heart of ArchUnitTS lies in src/common/extraction/extract-graph.ts. This module transforms TypeScript source code into a navigable dependency graph using the TypeScript Compiler API.
Key TypeScript APIs Used:
ts.createProgram(): Creates a program instance that represents a compilation unitTypeChecker: Provides semantic analysis capabilities (type resolution, symbol lookup)SourceFile.forEachChild(): Traverses the Abstract Syntax Tree (AST)ts.SyntaxKind: Identifies different node types in the AST
Pseudo Code of the extraction process visits every node in the TypeScript AST:
function visitNode(node: ts.Node, sourceFile: ts.SourceFile) {
switch (node.kind) {
case ts.SyntaxKind.ImportDeclaration:
handleImportDeclaration(node as ts.ImportDeclaration);
break;
case ts.SyntaxKind.ClassDeclaration:
handleClassDeclaration(node as ts.ClassDeclaration);
break;
case ts.SyntaxKind.CallExpression:
handleCallExpression(node as ts.CallExpression);
break;
// ... more node types
}
ts.forEachChild(node, (child) => visitNode(child, sourceFile));
}ArchUnitTS categorizes imports into different types using src/common/util/import-kinds-helper.ts.
- Parse Import Statements: Extract module specifiers from
ImportDeclarationnodes - Resolve Module Paths: Use TypeScript's module resolution to find actual file paths
- Classify Dependencies: Determine if import is external, internal, or built-in
- Build Dependency Edges: Create graph connections between files
The dependency graph is the core data structure that powers all architectural analysis. It's defined in src/common/extraction/graph.ts.
- Node Creation: Each TypeScript file becomes a graph node
- Edge Creation: Import statements create directed edges between nodes
- Metadata Extraction: Collect classes, functions, and other code elements
The src/metrics/extraction/extract-class-info.ts module analyzes TypeScript classes to extract structural information:
- Methods: Public, private, static methods with complexity metrics
- Properties: Field declarations and their access modifiers
- Inheritance: Base classes and implemented interfaces
- Dependencies: Classes referenced within the class body
ArchUnitTS implements efficient cycle detection using Tarjan's Strongly Connected Components algorithm.
The rule engine in src/files/fluentapi/files.ts provides a fluent API for defining architectural constraints:
The metrics system analyzes code quality indicators:
- Lines of Code (LOC): Physical and logical line counts
- Cyclomatic Complexity: Measure of code complexity
- LCOM (Lack of Cohesion of Methods): Class cohesion metric
- Dependency Counts: Number of incoming/outgoing dependencies
- Afferent/Efferent Coupling: Package-level coupling metrics
ArchUnitTS provides seamless integration with popular testing frameworks through custom matchers:
Simplified pseudo code:
declare global {
namespace jest {
interface Matchers<R> {
toPassAsync(): Promise<R>;
}
}
}
expect.extend({
async toPassAsync(rule: ArchRule) {
const violations = await rule.evaluate();
return {
pass: violations.length === 0,
message: () =>
violations.length > 0
? `Architecture rule failed:\n${violations.map((v) => v.message).join('\n')}`
: 'Architecture rule passed',
};
},
});Here's how ArchUnitTS processes your architectural rules:
// User calls projectFiles()
const files = projectFiles().inFolder('src');- Workspace Discovery: Scan for TypeScript/JavaScript files
- Configuration Loading: Read
tsconfig.jsonand project settings - File Filtering: Apply folder and pattern filters
// User defines rule
const rule = files.should().haveNoCycles();- Graph Extraction: Parse all files and build dependency graph
- Rule Compilation: Convert fluent API calls into executable rules
- Optimization: Cache results and optimize analysis order
// User executes rule
await expect(rule).toPassAsync();- Rule Execution: Run validation algorithms on the graph
- Violation Collection: Gather all rule violations
- Result Formatting: Prepare human-readable error messages
ArchUnitTS implements several optimizations for large codebases including caching and lazy loading.
ArchUnitTS ensures that all project files appear in the dependency graph, even if they don't import other project files. This is achieved by adding self-referencing edges for every file in the project.
Why this matters:
- Standalone utility files are included in architectural analysis
- Entry point files without imports are visible in the graph
- Complete project coverage for architectural rules
- No files are accidentally excluded from analysis
Example:
// Even if utils.ts doesn't import anything from your project,
// it will still appear in the graph with a self-edge: utils.ts -> utils.ts
// This ensures files like these are always analyzed:
// - Configuration files
// - Standalone utilities
// - Entry points
// - Constants files
// - Type definition filesThe graph will contain:
- Import edges: Real dependencies between files (A imports B)
- Self edges: Every project file references itself (ensures inclusion)
This guarantees comprehensive architectural analysis across your entire codebase.
