Skip to content

Latest commit

 

History

History
758 lines (574 loc) · 21.8 KB

File metadata and controls

758 lines (574 loc) · 21.8 KB

ArchUnit-TS

npm version npm downloads License CI

TypeScript Node.js Code Coverage CodeQL

GitHub issues GitHub stars PRs Welcome Conventional Commits


A TypeScript/JavaScript architecture testing library that allows you to specify and assert architecture rules in a fluent, expressive API.

Inspired by ArchUnit for Java

InstallationQuick StartAPI DocsExamplesContributingFAQ


Overview

ArchUnit-TS helps you maintain clean architecture in your TypeScript and JavaScript projects by:

  • Enforcing architectural rules as executable code
  • Detecting violations early in your CI/CD pipeline
  • Documenting architecture through executable specifications
  • Preventing architectural drift over time

Features

  • Fluent API for defining architecture rules
  • Naming conventions enforcement
  • Package/module dependency rules
  • Decorator/annotation checking
  • Layered architecture support
  • Cyclic dependency detection
  • Custom predicates for flexible class filtering
  • Dependency graph visualization (interactive HTML and Graphviz DOT formats)
  • Report generation (HTML, JSON, JUnit XML, Markdown)
  • CLI tool for command-line usage
  • Watch mode for automatic re-checking on file changes
  • Severity levels (errors vs warnings) for flexible enforcement
  • TypeScript & JavaScript support
  • Integration with Jest and other test frameworks
  • Zero runtime dependencies in production

Installation

npm install --save-dev archunit-ts

or

yarn add --dev archunit-ts

Quick Start

Basic Example

import { createArchUnit, ArchRuleDefinition } from 'archunit-ts';

describe('Architecture Tests', () => {
  it('services should reside in services package', async () => {
    const archUnit = createArchUnit();

    const rule = ArchRuleDefinition.classes()
      .that()
      .haveSimpleNameEndingWith('Service')
      .should()
      .resideInPackage('services');

    const violations = await archUnit.checkRule('./src', rule);
    expect(violations).toHaveLength(0);
  });
});

Naming Convention Rules

import { ArchRuleDefinition } from 'archunit-ts';

// Classes ending with 'Controller' should reside in controllers package
const controllerRule = ArchRuleDefinition.classes()
  .that()
  .haveSimpleNameEndingWith('Controller')
  .should()
  .resideInPackage('controllers');

// Classes ending with 'Repository' should reside in repositories package
const repositoryRule = ArchRuleDefinition.classes()
  .that()
  .haveSimpleNameEndingWith('Repository')
  .should()
  .resideInPackage('repositories');

Decorator/Annotation Rules

import { ArchRuleDefinition } from 'archunit-ts';

// Classes with @Service decorator should reside in services package
const serviceRule = ArchRuleDefinition.classes()
  .that()
  .areAnnotatedWith('Service')
  .should()
  .resideInPackage('services');

// Classes in services package should be annotated with @Service
const serviceAnnotationRule = ArchRuleDefinition.classes()
  .that()
  .resideInPackage('services')
  .should()
  .beAnnotatedWith('Service');

Layered Architecture

import { layeredArchitecture } from 'archunit-ts';

const layerRule = layeredArchitecture()
  .layer('Controllers')
  .definedBy('controllers')
  .layer('Services')
  .definedBy('services')
  .layer('Repositories')
  .definedBy('repositories')
  .layer('Models')
  .definedBy('models')
  // Define access rules
  .whereLayer('Controllers')
  .mayOnlyAccessLayers('Services')
  .whereLayer('Services')
  .mayOnlyAccessLayers('Repositories', 'Models')
  .whereLayer('Repositories')
  .mayOnlyAccessLayers('Models')
  .whereLayer('Models')
  .mayNotAccessLayers('Controllers', 'Services', 'Repositories');

const violations = await archUnit.checkRule('./src', layerRule);

Dependency Rules

import { ArchRuleDefinition } from 'archunit-ts';

// Classes in domain should not depend on infrastructure
const domainRule = ArchRuleDefinition.classes()
  .that()
  .resideInPackage('domain')
  .should()
  .notDependOnClassesThat()
  .resideInPackage('infrastructure');

Severity Levels

Control whether violations fail the build (errors) or just warn (warnings):

import { ArchRuleDefinition } from 'archunit-ts';

// ERROR: Will fail the build (default)
const strictRule = ArchRuleDefinition.classes()
  .that()
  .resideInPackage('services')
  .should()
  .haveSimpleNameEndingWith('Service');

// WARNING: Won't fail the build, but will show in output
const lenientRule = ArchRuleDefinition.classes()
  .that()
  .resideInPackage('legacy')
  .should()
  .haveSimpleNameEndingWith('Service')
  .asWarning();

// Progressive enforcement: Start with warnings, promote to errors later
const phase1Rule = someRule.asWarning(); // Phase 1: Team addresses issues
const phase2Rule = someRule.asError(); // Phase 2: Enforce strictly

Use cases:

  • Gradual adoption: Mark legacy code violations as warnings
  • Soft launches: Introduce new rules as warnings first
  • Non-blocking checks: Informational rules that shouldn't fail builds
  • Progressive enforcement: Start lenient, get stricter over time

API Documentation

See API Documentation for complete API documentation.

Entry Points

  • ArchRuleDefinition - Define architecture rules
  • createArchUnit() - Create analyzer instance
  • layeredArchitecture() - Define layered architecture
  • ArchUnitTS.assertNoViolations() - Assert no violations

Real-World Examples

Express.js API Structure

describe('Express API Architecture', () => {
  it('should enforce MVC pattern', async () => {
    const archUnit = createArchUnit();

    const rules = [
      // Controllers should only depend on services
      ArchRuleDefinition.classes()
        .that()
        .resideInPackage('controllers')
        .should()
        .onlyDependOnClassesThat()
        .resideInAnyPackage('services', 'models'),

      // Services should not depend on controllers
      ArchRuleDefinition.classes()
        .that()
        .resideInPackage('services')
        .should()
        .notDependOnClassesThat()
        .resideInPackage('controllers'),

      // Models should not depend on anything
      ArchRuleDefinition.classes()
        .that()
        .resideInPackage('models')
        .should()
        .notDependOnClassesThat()
        .resideInAnyPackage('controllers', 'services'),
    ];

    const violations = await archUnit.checkRules('./src', rules);
    expect(violations).toHaveLength(0);
  });
});

NestJS Application

describe('NestJS Architecture', () => {
  it('should enforce module boundaries', async () => {
    const archUnit = createArchUnit();

    const rules = [
      // Controllers should be annotated with @Controller
      ArchRuleDefinition.classes()
        .that()
        .haveSimpleNameEndingWith('Controller')
        .should()
        .beAnnotatedWith('Controller'),

      // Services should be annotated with @Injectable
      ArchRuleDefinition.classes()
        .that()
        .haveSimpleNameEndingWith('Service')
        .should()
        .beAnnotatedWith('Injectable'),

      // Repositories should reside in database module
      ArchRuleDefinition.classes()
        .that()
        .haveSimpleNameEndingWith('Repository')
        .should()
        .resideInPackage('database'),
    ];

    const violations = await archUnit.checkRules('./src', rules);
    expect(violations).toHaveLength(0);
  });
});

Integration with Test Frameworks

Jest

import { createArchUnit, ArchRuleDefinition, ArchUnitTS } from 'archunit-ts';

describe('Architecture Tests', () => {
  let archUnit: ArchUnitTS;

  beforeAll(() => {
    archUnit = createArchUnit();
  });

  it('should follow naming conventions', async () => {
    const rule = ArchRuleDefinition.classes()
      .that()
      .resideInPackage('services')
      .should()
      .haveSimpleNameEndingWith('Service');

    const violations = await archUnit.checkRule('./src', rule);

    // Assert no violations
    ArchUnitTS.assertNoViolations(violations);
  });
});

Mocha

import { expect } from 'chai';
import { createArchUnit, ArchRuleDefinition } from 'archunit-ts';

describe('Architecture Tests', () => {
  it('should enforce package rules', async () => {
    const archUnit = createArchUnit();

    const rule = ArchRuleDefinition.classes()
      .that()
      .areAnnotatedWith('Service')
      .should()
      .resideInPackage('services');

    const violations = await archUnit.checkRule('./src', rule);
    expect(violations).to.have.lengthOf(0);
  });
});

Configuration

Custom File Patterns

By default, ArchUnit-TS analyzes **/*.ts, **/*.tsx, **/*.js, and **/*.jsx files. You can customize this:

const archUnit = createArchUnit();

const violations = await archUnit.checkRule(
  './src',
  rule,
  ['**/*.ts', '**/*.tsx'] // Only TypeScript files
);

Ignoring Files

Automatically ignored:

  • node_modules/
  • dist/
  • build/
  • *.d.ts files

CLI Usage

The CLI tool allows you to run architecture checks from the command line without writing test code.

Basic Usage

npx archunit-ts check ./src

Specify Rules File

npx archunit-ts check ./src --rules archunit.rules.ts

Generate Reports

ArchUnit-TS can generate reports in multiple formats:

# Generate HTML report
npx archunit-ts check ./src --format html --output reports/architecture.html

# Generate JSON report
npx archunit-ts check ./src --format json --output reports/architecture.json

# Generate JUnit XML report (for CI/CD integration)
npx archunit-ts check ./src --format junit --output reports/architecture.xml

# Generate Markdown report
npx archunit-ts check ./src --format markdown --output reports/architecture.md

# Custom report title
npx archunit-ts check ./src --format html --output report.html --report-title "My Project Architecture"

Watch Mode

Enable automatic architecture checks when files change:

# Start watch mode
npx archunit-ts watch

# Watch with custom config
npx archunit-ts watch --config custom.config.js

# Watch specific patterns
npx archunit-ts watch --pattern "src/**/*.ts"

# Watch with verbose output
npx archunit-ts watch --verbose

Watch mode features:

  • Automatic re-checking on file changes
  • Debounced execution (300ms default)
  • Clear console output with timestamps
  • Shows which files changed
  • Graceful shutdown with Ctrl+C
  • Ignores node_modules, dist, build, and .d.ts files

CLI Options

  • --rules <path> - Path to rules configuration file
  • --format <format> - Report format: html, json, junit, or markdown
  • --output <path> - Output path for report
  • --report-title <title> - Custom title for the report
  • --graph-type <type> - Graph format: dot or html (for graph command)
  • --graph-title <title> - Custom title for the graph
  • --direction <dir> - Graph direction: LR, TB, RL, or BT (for DOT graphs)
  • --include-interfaces - Include interfaces in dependency graph
  • --width <pixels> - Graph width for HTML output (default: 1200)
  • --height <pixels> - Graph height for HTML output (default: 800)
  • --no-color - Disable colored output
  • --no-context - Disable code context in violations
  • --verbose, -v - Show verbose output
  • --help - Show help
  • --version - Show version

Dependency Graph Visualization

ArchUnit-TS can generate visual dependency graphs to help you understand and analyze your codebase structure.

Interactive HTML Graph

Generate an interactive, D3.js-powered dependency graph that you can explore in your browser:

# Generate interactive HTML graph
archunit-ts graph --graph-type html --output ./docs/dependencies.html

# With custom options
archunit-ts graph --graph-type html --output ./graph.html \
  --graph-title "My Project Dependencies" \
  --width 1600 --height 900 \
  --include-interfaces

Features of the HTML graph:

  • Interactive exploration - Click and drag nodes, zoom and pan
  • Real-time filtering - Filter by node type or violations
  • Physics simulation - Adjustable force-directed layout
  • Detailed tooltips - Hover to see dependencies and metadata
  • Cycle detection - Automatically highlights if cycles are present
  • Color-coded nodes - Different colors for classes, interfaces, and violations

Graphviz DOT Format

Generate DOT files for use with Graphviz to create publication-quality diagrams:

# Generate DOT file
archunit-ts graph --graph-type dot --output ./docs/dependencies.dot

# Convert to PNG using Graphviz (requires graphviz installation)
dot -Tpng ./docs/dependencies.dot -o ./docs/dependencies.png

# Convert to SVG
dot -Tsvg ./docs/dependencies.dot -o ./docs/dependencies.svg

# With custom layout direction
archunit-ts graph --graph-type dot --output ./graph.dot --direction LR

DOT graph features:

  • Module clustering - Nodes grouped by module/package
  • Relationship types - Different styles for inheritance, implementation, and imports
  • Metadata labels - Shows decorators and abstract classes
  • Violation highlighting - Nodes with violations are highlighted in red

Programmatic API

Generate graphs programmatically in your code:

import { createArchUnit } from 'archunit-ts';

const archUnit = createArchUnit();

// Generate interactive HTML graph
await archUnit.generateHtmlGraph('./src', './docs/graph.html', {
  graphOptions: {
    title: 'My Application Architecture',
    width: 1600,
    height: 900,
    showLegend: true,
    enablePhysics: true,
  },
  builderOptions: {
    includeInterfaces: true,
  },
});

// Generate DOT graph
await archUnit.generateDotGraph('./src', './docs/graph.dot', {
  graphOptions: {
    title: 'Dependency Graph',
    direction: 'LR',
    clusterByModule: true,
    useColors: true,
  },
});

// Work with the graph data structure
const graph = await archUnit.createDependencyGraph('./src');
const stats = graph.getStats();
console.log(`Nodes: ${stats.nodeCount}, Edges: ${stats.edgeCount}`);
console.log(`Has cycles: ${stats.hasCycles}`);

Use Cases

  • Onboarding - Help new team members understand the codebase structure
  • Architecture reviews - Visualize actual vs intended architecture
  • Refactoring planning - Identify highly coupled modules
  • Documentation - Auto-generate architecture diagrams
  • Cycle detection - Find and eliminate circular dependencies

Report Generation

ArchUnit-TS provides comprehensive reporting capabilities to visualize and share architecture violations.

Supported Formats

  1. HTML - Interactive, styled reports with statistics
  2. JSON - Machine-readable format for tooling integration
  3. JUnit XML - CI/CD integration (Jenkins, GitHub Actions, etc.)
  4. Markdown - Documentation and PR integration

Programmatic API

You can also generate reports programmatically:

import { createArchUnit, createReportManager, ReportFormat, ArchRuleDefinition } from 'archunit-ts';

const archUnit = createArchUnit();
const reportManager = createReportManager();

// Define and check rules
const rule = ArchRuleDefinition.classes()
  .that()
  .resideInPackage('services')
  .should()
  .haveSimpleNameEndingWith('Service');

const violations = await archUnit.checkRule('./src', rule);

// Generate HTML report
await reportManager.generateReport(violations, {
  format: ReportFormat.HTML,
  outputPath: 'reports/architecture.html',
  title: 'Architecture Report',
  includeTimestamp: true,
  includeStats: true,
});

// Generate multiple reports at once
await reportManager.generateMultipleReports(
  violations,
  [ReportFormat.HTML, ReportFormat.JSON, ReportFormat.JUNIT],
  'reports/',
  {
    title: 'Architecture Analysis',
  }
);

Report Contents

All reports include:

  • Metadata: Title, timestamp, total violations
  • Statistics: Total files affected, rules checked, pass/fail counts
  • Violations: Detailed list grouped by file and rule
  • Source Locations: File paths and line numbers for each violation

HTML Report Features

  • Clean, responsive design
  • Color-coded statistics
  • Violations grouped by file
  • Direct links to source code locations
  • Success indicators when no violations found

CI/CD Integration

Use JUnit format for seamless CI/CD integration:

# GitHub Actions example
- name: Run Architecture Tests
  run: npx archunit-ts check ./src --format junit --output reports/architecture.xml

- name: Publish Test Results
  uses: EnricoMi/publish-unit-test-result-action@v2
  if: always()
  with:
    files: reports/architecture.xml
// Jenkins Pipeline example
stage('Architecture Tests') {
  steps {
    sh 'npx archunit-ts check ./src --format junit --output reports/architecture.xml'
  }
  post {
    always {
      junit 'reports/architecture.xml'
    }
  }
}

Best Practices

  1. Run in CI/CD: Add architecture tests to your CI/CD pipeline
  2. Test Early: Run architecture tests alongside unit tests
  3. Start Small: Begin with simple rules and expand gradually
  4. Document Intent: Use clear, descriptive rule definitions
  5. Fail Fast: Configure tests to fail on first violation for faster feedback
  6. Generate Reports: Use reports to communicate violations to your team

Examples

Check the /examples directory for complete working examples:

  • examples/express-api/ - Express.js REST API
  • examples/nestjs-app/ - NestJS application
  • examples/clean-architecture/ - Clean architecture example

Why ArchUnit-TS?

Prevent Architecture Drift

As codebases grow, they tend to drift from their intended architecture. ArchUnit-TS helps prevent this by making architecture testable:

// ❌ This would fail if a developer accidentally adds a dependency
const rule = ArchRuleDefinition.classes()
  .that()
  .resideInPackage('domain')
  .should()
  .notDependOnClassesThat()
  .resideInPackage('infrastructure');

Living Documentation

Architecture tests serve as executable documentation that never gets outdated:

// This test documents that controllers should only use services
describe('Architecture Rules', () => {
  it('controllers should only depend on services', async () => {
    // Test doubles as documentation
  });
});

Early Detection

Catch architectural violations in CI/CD before they reach code review:

✓ Architecture Tests
  ✓ services should reside in services package
  ✓ controllers should only depend on services
  ✗ domain should not depend on infrastructure

    Violation: UserEntity depends on PostgresClient
    Location: src/domain/entities/UserEntity.ts:5

Documentation

Community & Support

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for details.

License

MIT © Manjericao Team

Acknowledgments

Inspired by ArchUnit for Java, created by TNG Technology Consulting.

Support