npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

architecture-linter

v1.0.0

Published

A CLI tool that enforces architecture rules in TypeScript projects

Readme

architecture-linter

npm version npm downloads VS Code Marketplace License: MIT GitHub Sponsors

Enforce architectural layer rules in TypeScript projects from the command line.

architecture-linter reads a .context.yml configuration file and scans your TypeScript source tree for dependency violations — such as a controller importing a repository directly, bypassing the service layer.


Quick start

npm install --save-dev architecture-linter
npx architecture-linter init   # generate .context.yml from your folder structure
npx architecture-linter scan   # check for violations

Expected output when a violation exists:

Scanning project...

❌ Architecture violation detected

   File:   controllers/orderController.ts
   Import: repositories/orderRepository
   Rule:   Controller cannot import Repository

── Violations by layer ──────────────────
   controller       1 violation(s)
   service          0 violation(s)
   repository       0 violation(s)

Found 1 violation in 3 file(s) scanned.

Installation

As a local dev dependency (recommended)

npm install --save-dev architecture-linter

Add a script to your package.json:

{
  "scripts": {
    "lint:arch": "architecture-linter scan"
  }
}

Global install

npm install -g architecture-linter

Quick setup for a new project

Run init to auto-generate a .context.yml by inspecting your folder structure. The command detects common layer names (controller, service, repository, middleware, etc.) from top-level and src/ subdirectory names.

architecture-linter init

Then edit the generated file to add your constraints, and run:

architecture-linter scan

Framework presets

Use a built-in preset to get a sensible starting configuration for popular architectural patterns. Declare it with the extends key in .context.yml:

extends: nestjs

User-defined layers and rules always take precedence over preset defaults.

| Preset | Layers | |---|---| | nestjs | module, controller, service, repository, guard, interceptor, pipe, decorator, dto, entity | | clean-architecture | entity, usecase, repository, infrastructure, interface | | hexagonal | domain, port, adapter, application, infrastructure | | nextjs | page, component, hook, lib, api, store, util |

Extending multiple presets

extends:
  - clean-architecture
  - nestjs

Overriding a preset rule

extends: nestjs

rules:
  # Override the nestjs default — allow controllers to import repositories directly
  controller:
    cannot_import: []

Configuration reference

Create a .context.yml in your project root (or pass --context to override). When --context is omitted, the linter walks up the directory tree until a .context.yml is found — just like ESLint.

# Optional: extend a built-in preset
extends: nestjs

architecture:
  layers:
    - controller
    - service
    - repository

rules:
  # Blacklist: this layer must NOT import from any layer in the list.
  controller:
    cannot_import:
      - repository

  # Whitelist: this layer may ONLY import from layers in the list.
  service:
    can_only_import:
      - repository

  repository:
    cannot_import: []

# Glob patterns (project-relative) for files to skip entirely.
exclude:
  - "**/*.spec.ts"
  - "**/*.test.ts"
  - "**/__mocks__/**"

# Manual path alias overrides (supplements tsconfig.json paths automatically).
aliases:
  "@repositories": "src/repositories"
  "@services": "src/services"

Rule options

| Option | Type | Description | |---|---|---| | cannot_import | string[] | Blacklist — the layer must not import from any listed layer | | can_only_import | string[] | Whitelist — the layer may only import from listed layers | | files | string (glob) | Scope this rule to source files matching the pattern |

cannot_import and can_only_import are mutually exclusive. Use one per layer rule.

Scoping a rule to specific files

rules:
  controller:
    files: "src/controllers/admin/**"
    cannot_import:
      - repository

Path alias resolution

The linter automatically reads compilerOptions.paths from your tsconfig.json and resolves aliased imports before checking rules. No extra config needed for standard TypeScript path aliases.

For monorepos or non-standard setups, add manual overrides via the aliases key:

aliases:
  "@repositories": "src/repositories"

Manual aliases take precedence over any tsconfig.json entries with the same key.

Inline suppression with arch-ignore

To suppress a single violation without removing the import, add an // arch-ignore: comment on the line immediately before the import:

// arch-ignore: controller cannot import repository
import { OrderRepository } from '../repositories/orderRepository';

The hint must match the rule string (case-insensitive): <sourceLayer> cannot import <targetLayer>.


How layer detection works

The linter infers a file's layer from its directory name. Both singular and plural forms are recognised (including irregular plurals such as repositoryrepositories).

| Path | Detected layer | |---|---| | controllers/orderController.ts | controller | | services/orderService.ts | service | | repositories/orderRepository.ts | repository | | src/controllers/admin/ctrl.ts | controller |


CLI reference

scan

architecture-linter scan [options]

Options:
  -c, --context <path>   Path to the .context.yml file (auto-detected if omitted)
  -p, --project <path>   Root directory of the project to scan   (default: .)
  -f, --format <format>  Output format: text or json              (default: text)
  -s, --strict           Report files not assigned to any layer
  -q, --quiet            Suppress the "Scanning project..." banner
  -e, --explain          Print why/impact/how-to-fix guidance per violation
  -x, --fix              Show a suggested fix for each violation
  -w, --watch            Watch for file changes and re-scan automatically
  -h, --help             Display help

--explain — understand each violation

architecture-linter scan --explain

Adds three sections below each violation:

  • Why this matters — the architectural reason this rule exists
  • Impact — what goes wrong if the violation is left in place
  • How to fix — a concrete recommendation

--fix — get a suggested fix

architecture-linter scan --fix

Prints a short actionable message per violation, e.g.:

🔧 Suggested fix
   Instead of importing 'repository' directly, route through an allowed
   intermediary layer: 'service'.

--watch — re-scan on file changes

architecture-linter scan --watch

Watches the project directory for .ts file changes and re-runs the scan automatically. Press Ctrl+C to stop.

init

architecture-linter init [options]

Options:
  -p, --project <path>   Root directory of the project  (default: .)

Generates a starter .context.yml by detecting layer names from directory structure. Fails safely if a .context.yml already exists.

ci

architecture-linter ci [options]

Options:
  --platform <platform>  CI platform to target: github  (default: github)
  -p, --project <path>   Root directory of the project  (default: .)

Generates a ready-to-use CI workflow file. Currently supports GitHub Actions:

architecture-linter ci
# Creates: .github/workflows/arch-lint.yml

Fails safely if the workflow file already exists.

score

architecture-linter score [options]

Options:
  -c, --context <path>   Path to the .context.yml config file (auto-detected)
  -p, --project <path>   Root directory of the project to scan  (default: .)
  -f, --format <format>  Output format: text or json              (default: text)

Calculates an architecture health score from 0 to 100 based on three weighted components:

| Component | Max pts | What it measures | |---|---|---| | Violation density | 60 | How few import violations exist relative to total imports | | Layer coverage | 25 | What fraction of files belong to a declared layer | | Rule completeness | 15 | What fraction of layers have at least one rule defined |

Grades: A (90–100) · B (75–89) · C (60–74) · D (40–59) · F (0–39)

npx architecture-linter score

Example output:

Architecture Health Score

  87/100  Grade: B  █████████████████░░░

  Breakdown:
    Violation density    52/60  pts
    Layer coverage       25/25  pts
    Rule completeness    10/15  pts

  Stats:
    Files scanned:       24
    Total imports:       87
    Violations:          4
    Classified files:    24/24
    Layers with rules:   2/3

The score is also included in scan --format json output under the score key.


badge

Generates a shields.io badge URL for the architecture health score. Drop it in your README to show the current grade at a glance.

architecture-linter badge [options]

| Option | Description | Default | |---|---|---| | -c, --context <path> | Path to .context.yml | auto-detect | | -p, --project <path> | Project root directory | . | | -f, --format <format> | Output format: url or markdown | url | | -o, --output <path> | Write badge to a file instead of stdout | — |

# Print a shields.io URL
npx architecture-linter badge

# Print a Markdown image tag ready to paste into your README
npx architecture-linter badge --format markdown

# Write the badge URL to a file (useful in CI)
npx architecture-linter badge --output .badge-url.txt

Badge colours: A = bright green, B = green, C = yellow, D = orange, F = red.


scan --format sarif

Outputs a SARIF 2.1.0 document — the standard format for GitHub Code Scanning. Upload it with the github/codeql-action/upload-sarif action to get violations as native PR annotations.

npx architecture-linter scan --format sarif > results.sarif

Example GitHub Actions step:

- name: Run architecture linter (SARIF)
  run: npx architecture-linter scan --format sarif > arch.sarif

- name: Upload SARIF to GitHub Code Scanning
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: arch.sarif

scan --baseline — ratchet mode

Saves the current violation count and fails only when violations increase. Perfect for adopting the linter on an existing codebase without having to fix everything at once.

# First run: creates .arch-baseline.json and exits 0
npx architecture-linter scan --baseline

# Subsequent runs: fails only if violations exceeded the saved baseline
npx architecture-linter scan --baseline

# Explicitly update the baseline after cleaning up violations
npx architecture-linter scan --baseline --update-baseline

# Use a custom baseline file path
npx architecture-linter scan --baseline baselines/prod.json

The baseline file (default .arch-baseline.json) records the violation count, timestamp, and tool version. Commit it to track regression over time.


scan --detect-circular

Detects circular dependencies between architectural layers using Tarjan's SCC algorithm. A cycle exists when layer A (transitively) imports layer B which imports back into layer A.

npx architecture-linter scan --detect-circular

Text output example:

↻  Circular dependencies detected between layers:

   controller → service → controller

Circular dependencies are also included in --format json output under the circularDeps key:

{
  "circularDeps": [
    { "cycle": ["controller", "service", "controller"] }
  ]
}

scan --monorepo

Scans all workspace packages defined in the root package.json workspaces field. Each package uses its own .context.yml if one is present; otherwise falls back to the root config.

npx architecture-linter scan --monorepo

Each package is scanned independently and prefixed in the output:

Scanning @myorg/api-gateway...
✅ No architecture violations found. (12 file(s) scanned)

Scanning @myorg/user-service...
❌ Architecture violation detected
   ...

The command exits 1 if any package has violations.


Exit codes

| Code | Meaning | |---|---| | 0 | No violations found | | 1 | One or more violations found (or a fatal error occurred) |


GitHub Action

The easiest way to enforce architecture rules on every pull request — no Node.js setup needed, violations appear as inline code annotations.

# .github/workflows/arch-lint.yml
name: Architecture Lint

on:
  push:
    branches: ["**"]
  pull_request:
    branches: ["**"]

jobs:
  arch-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: cvalingam/[email protected]
        with:
          config: .context.yml          # path to your config (default: .context.yml)
          fail-on-violations: 'true'    # fail the job on violations (default: true)
          token: ${{ secrets.GITHUB_TOKEN }}  # enables PR comment summary (optional)

Action inputs

| Input | Default | Description | |---|---|---| | config | .context.yml | Path to the config file | | working-directory | . | Root directory to scan | | fail-on-violations | true | Fail the step when violations are found | | token | '' | GitHub token — enables PR comment with violation table |

Action outputs

| Output | Description | |---|---| | violations | Number of violations found (usable in subsequent steps) |

When violations are found, each one appears as a red annotation directly on the diff line in the PR, and a summary comment is posted to the PR thread.


GitHub Action

The easiest way to enforce architecture rules on every pull request — no manual setup required. Violations are posted as inline PR annotations and an optional PR comment summary:

# .github/workflows/arch-lint.yml
name: Architecture Lint

on:
  pull_request:
    branches: ["**"]

jobs:
  arch-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Enforce architecture rules
        uses: cvalingam/[email protected]
        with:
          token: ${{ secrets.GITHUB_TOKEN }}   # for PR comment (optional)

Inputs

| Input | Default | Description | |---|---|---| | config | .context.yml | Path to your config file | | working-directory | . | Root of the project to scan | | fail-on-violations | true | Fail the step when violations are found | | token | '' | GITHUB_TOKEN — enables PR comment summary |

Outputs

| Output | Description | |---|---| | violations | Number of violations found |


CI integration (manual setup)

Run the linter on every push and pull request. The ci command generates this for you (architecture-linter ci), or add the step manually:

# .github/workflows/arch-lint.yml
name: Architecture Lint

on:
  push:
    branches: ["**"]
  pull_request:
    branches: ["**"]

jobs:
  arch-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npx architecture-linter scan --strict

JSON output

architecture-linter scan --format json
architecture-linter scan --format json --fix --explain
{
  "filesScanned": 3,
  "violations": [
    {
      "file": "controllers/orderController.ts",
      "importPath": "repositories/orderRepository",
      "rawSpecifier": "../repositories/orderRepository",
      "sourceLayer": "controller",
      "targetLayer": "repository",
      "rule": "Controller cannot import Repository",
      "fix": "Instead of importing 'repository' directly, route through an allowed intermediary layer: 'service'.",
      "explanation": {
        "why": "...",
        "impact": "...",
        "fix": "..."
      }
    }
  ],
  "unclassifiedFiles": [],
  "violationsByLayer": {
    "controller": 1,
    "service": 0,
    "repository": 0
  }
}

Development

# Install dependencies
npm install

# Run directly with ts-node (no build required)
npx ts-node src/cli.ts scan --context examples/sample.context.yml --project examples/sample-project

# Build to dist/
npm run build

# Run the full test suite
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage report
npm run test:coverage

# Clean build artefacts
npm run clean

Project structure

architecture-linter/
├── src/
│   ├── cli.ts               # Commander-based CLI entry point
│   ├── contextParser.ts     # Loads and validates .context.yml; walks up directory tree
│   ├── dependencyScanner.ts # Walks .ts files and extracts imports via ts-morph
│   ├── ruleEngine.ts        # Matches imports against rules; builds violations
│   ├── aliasResolver.ts     # Resolves tsconfig.json path aliases
│   ├── presets.ts           # Built-in framework presets
│   ├── explainer.ts         # Why/impact/fix guidance for --explain
│   └── types.ts             # Shared TypeScript interfaces
│
├── src/__tests__/           # Jest test suite (105 tests)
│
├── examples/
│   ├── sample.context.yml         # Example rule configuration
│   ├── sample-project/            # ❌ intentional violation for demo
│   ├── alias-test/                # Demo of path alias resolution
│   └── preset-test/               # Demo of framework presets
│
├── .github/workflows/ci.yml  # Runs tests on every push/PR
├── jest.config.js
├── package.json
├── tsconfig.json
└── README.md

How it works

  1. Parse configcontextParser loads .context.yml, merges any preset declared via extends, validates required fields, and walks up the directory tree when no explicit path is provided.
  2. Scan filesdependencyScanner uses fast-glob to find every .ts file (respecting exclude patterns) and ts-morph to parse import declarations. Path aliases are resolved via aliasResolver before rules are applied. Each import is checked for a preceding // arch-ignore: comment.
  3. Check rulesruleEngine evaluates cannot_import / can_only_import rules against each import. Violations are collected with optional fix suggestions and layer-level counts.
  4. Report — results are printed as human-readable text (with colour) or machine-readable JSON. The process exits 0 for clean, 1 for violations.
  5. Apply rulesruleEngine maps each file and resolved import to an architectural layer, then evaluates cannot_import (blacklist) and can_only_import (whitelist) rules. Per-rule files glob scoping is applied via minimatch.
  6. Report — The CLI prints every violation with the file, import path, and rule broken. A per-layer summary is shown at the end. --format json emits machine-readable output.

Roadmap (post-MVP)

  • --fix flag to suggest corrected import paths
  • SARIF output format for GitHub Advanced Security integration
  • Watch mode (--watch)
  • Support for TypeScript path alias resolution (@app/repositories)

License

MIT

architecture-linter reads a .context.yml configuration file and scans your TypeScript source tree for dependency violations — such as a controller importing a repository directly, bypassing the service layer.


Quick start

# 1. Install dependencies
npm install

# 2. Build
npm run build

# 3. Scan the bundled example project
node dist/cli.js scan \
  --context examples/sample.context.yml \
  --project examples/sample-project

Expected output:

Scanning project...

❌ Architecture violation detected

   File:    controllers/orderController.ts
   Import:  repositories/orderRepository
   Rule:    Controller cannot import Repository

Found 1 violation in 3 file(s) scanned.

Installation

As a local dev dependency

npm install --save-dev architecture-linter

Then add a script to your package.json:

{
  "scripts": {
    "lint:arch": "architecture-linter scan"
  }
}

Global install

npm install -g architecture-linter

Configuration

Create a .context.yml file in your project root (or pass --context to point to a different path).

architecture:
  layers:
    - controller
    - service
    - repository

rules:
  controller:
    cannot_import:
      - repository   # Controllers must go through the service layer

  service:
    cannot_import: []

  repository:
    cannot_import: []

How layer detection works

The linter infers a file's layer from its directory name. A file inside a directory called controllers/ or controller/ is automatically assigned to the controller layer. Both singular and plural forms are recognised.

| Path | Detected layer | |---|---| | controllers/orderController.ts | controller | | services/orderService.ts | service | | repositories/orderRepository.ts | repository |

The same logic applies to import paths: a relative import that resolves into a repositories/ directory is treated as a repository-layer import.


CLI reference

architecture-linter scan [options]

Options:
  -c, --context <path>   Path to the .context.yml file  (default: .context.yml)
  -p, --project <path>   Root directory of the project  (default: .)
  -V, --version          Print version number
  -h, --help             Display help

Exit codes

| Code | Meaning | |---|---| | 0 | No violations found | | 1 | One or more violations found (or a fatal error occurred) |

This makes the tool suitable for use in CI pipelines:

# .github/workflows/ci.yml (example)
- name: Architecture lint
  run: npx architecture-linter scan

Development

# Run directly with ts-node (no build step required)
npx ts-node src/cli.ts scan --context examples/sample.context.yml --project examples/sample-project

# Build to dist/
npm run build

# Run the compiled output
node dist/cli.js scan --context examples/sample.context.yml --project examples/sample-project

# Clean build artefacts
npm run clean

Project structure

architecture-linter/
├── src/
│   ├── cli.ts               # Commander-based CLI entry point
│   ├── contextParser.ts     # Loads and validates .context.yml
│   ├── dependencyScanner.ts # Walks .ts files and extracts imports via ts-morph
│   ├── ruleEngine.ts        # Matches imports against rules and returns violations
│   └── types.ts             # Shared TypeScript interfaces
│
├── examples/
│   ├── sample.context.yml                        # Example rule configuration
│   └── sample-project/
│       ├── controllers/orderController.ts        # ❌ contains an intentional violation
│       ├── services/orderService.ts
│       └── repositories/orderRepository.ts
│
├── package.json
├── tsconfig.json
└── README.md

How it works

  1. Parse configcontextParser loads .context.yml using js-yaml and validates the required fields.
  2. Scan filesdependencyScanner uses fast-glob to find every .ts file in the project and ts-morph to parse its import declarations. Relative imports are resolved to project-relative paths.
  3. Apply rulesruleEngine maps each file and each resolved import path to an architectural layer, then checks the cannot_import rules.
  4. Report — The CLI prints every violation with the offending file, the resolved import path, and the rule that was broken.

Roadmap (post-MVP)

  • --fix flag to suggest corrected import paths
  • JSON / SARIF output format for CI integration
  • Wildcard layer patterns (src/*/controllers/**)
  • Support for path alias resolution (@app/repositories)
  • Watch mode

License

MIT