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

@nielspeter/eess-ts

v0.14.0

Published

Architecture testing for TypeScript — the TS dialect of the eess family (formerly ts-archunit)

Readme

@nielspeter/eess-ts

npm version CI License: MIT Node.js >= 24

Architecture guardrails for AI coding agents. Executable rules that catch structural violations in CI — before they reach your codebase.

Inspired by Java's ArchUnit. Powered by ts-morph.

The TypeScript dialect of the eess family. Formerly published as @nielspeter/ts-archunit — that name still works as an alias, so existing installs are unaffected. New projects should prefer @nielspeter/eess-ts. Migrating? See Migrating from ts-archunit.

Documentation · Getting Started · What Can It Check?

Quickstart — zero to a red gate

A green check you've never seen fail proves nothing. This path takes you to a real, correct violation — timed at seconds on a warm npm cache, minutes cold:

npm install -D @nielspeter/eess-ts   # Node >= 24; your project may be ESM or CJS
npx eess-ts init                     # scaffolds arch.rules.ts — editable guardrail rules
npx eess-ts check                    # runs them against your source

On most existing codebases the floor fires immediately (a silent catch {}, an empty stub body, an eval). If yours comes up clean — good; now prove the gate is real the same way eess proves its own gates in CI: introduce a violation, watch it go red, revert.

echo 'export function probe(s: string): unknown { return eval(s) }' >> src/probe.ts
npx eess-ts check               # ✗ red — preset/recommended/no-eval, with why + fix
rm src/probe.ts                 # green again — and now you trust the green

Then wire the rules into your agent's loop — CI annotations, a Claude Code hook, and an AGENTS.md block regenerated from the actual rules: agent integration recipes.

The Problem

AI coding agents don't know your architecture. They generate code that compiles, passes type checks, and looks correct in isolation — but violates the structural decisions your team spent months establishing.

An agent will:

  • Call parseInt instead of the shared extractCount() helper
  • Throw new Error() instead of your typed NotFoundError
  • Import the database driver directly from a service instead of going through the repository
  • Copy-paste a parser function instead of using the shared utility
  • Skip validation in a route handler

Code review catches some of this. But at scale — with multiple agents generating PRs across a large codebase — review becomes the bottleneck. You need automated enforcement.

The Solution

eess-ts turns your architecture decisions into executable tests. They run in CI. Violations show up inline on the PR with clear messages explaining what's wrong, why it matters, and how to fix it — exactly the context an agent needs to self-correct.

classes(p)
  .that()
  .extend('BaseRepository')
  .should()
  .notContain(newExpr('Error'))
  .rule({
    id: 'repo/typed-errors',
    because: 'Generic Error loses context and prevents consistent error handling',
    suggestion: 'Use NotFoundError, ValidationError, or DomainError instead',
  })
  .check()

When an agent violates this rule, it sees:

Architecture Violation [repo/typed-errors]

  WebhookRepository.findById contains new 'Error' at line 42
  at src/repositories/webhook.repository.ts:42

      41 |     if (!result) {
    > 42 |       throw new Error(`Webhook '${id}' not found`)
      43 |     }

  Why: Generic Error loses context and prevents consistent error handling
  Fix: Use NotFoundError, ValidationError, or DomainError instead

The because and suggestion fields give the agent everything it needs to fix the violation without human intervention.

Why Not Just Import Rules?

Every other tool (dependency-cruiser, eslint-plugin-boundaries, ts-arch) only checks which files import which. That's necessary but insufficient.

AI agents don't violate architecture by importing wrong files. They violate it by writing the wrong code in the right place — inlining logic instead of delegating, using raw APIs instead of abstractions, skipping validation, throwing generic errors.

eess-ts checks what happens inside your functions:

// "Services must delegate to repositories, not hardcode data"
functions(p)
  .that()
  .resideInFolder('**/services/**')
  .should()
  .satisfy(mustCall(/Repository/))
  .check()

// "No eval anywhere in production code"
modules(p).that().resideInFolder('src/**').should().satisfy(moduleNoEval()).check()

// "Route handlers must validate input"
functions(p)
  .that()
  .resideInFolder('**/handlers/**')
  .should()
  .satisfy(mustCall(/validate|parse/))
  .check()

| Capability | eess-ts | dependency-cruiser | eslint-plugin-boundaries | Biome | ts-arch | | -------------------------------------------------- | ------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------- | | Import path rules | Yes | Yes | Yes | Yes | Yes | | Body analysis (what's called inside functions) | Yes | No | No | No | No | | Type checking (string vs typed union) | Yes | No | No | No | No | | JSX element rules | Yes | No | No | No | No | | Cross-layer rules | Yes | No | No | No | Partial | | Cycle detection | Yes | Yes | No | No | Yes | | Code smell detection (duplicate bodies) | Yes | No | No | No | No | | Code metrics (cyclomatic, LOC) | Yes | No | No | No | No | | Pattern matching (reusable rule templates) | Yes | No | No | No | No | | Baseline (gradual adoption) | Yes | No | No | No | No | | Diff-aware / PR-only checking | Yes | No | No | No | No | | Watch mode | Yes | No | No | Yes | No | | Inline exclusion comments | Yes | No | Via ESLint | Yes | No | | GitHub PR annotations | Yes | No | Via ESLint | No | No |

Quick Start with Presets

One function call enforces an entire architecture pattern — layer ordering, cycles, import direction, package restrictions:

import { project } from '@nielspeter/eess-ts'
import { layeredArchitecture } from '@nielspeter/eess-ts/presets'

const p = project('tsconfig.json')

layeredArchitecture(p, {
  layers: {
    routes: 'src/routes/**',
    services: 'src/services/**',
    repositories: 'src/repositories/**',
  },
  shared: ['src/shared/**'],
  strict: true,
})

This generates 5 coordinated rules. Override individual rules without disabling the preset:

layeredArchitecture(p, {
  layers: { ... },
  overrides: {
    'preset/layered/type-imports-only': 'off',
  },
})

Shape presets (architecture patterns): layeredArchitecture, dataLayerIsolation, strictBoundaries.

Floor presets (universal safety, scoped by glob):

  • recommended(p) — a thin, universal safety floor (no eval, no Function constructor, no silent catches, no empty bodies) that fires ~never on healthy code. The first three read the whole FILE, so they see a call wherever it is written — at top level, in a class's static block or field initializer, in a callback — not only inside a function body (bug 0333); no-empty-bodies reads functions, since that is what an empty body is a fact about. A finding names the declaration that contains it, or the file when nothing does, and carries a per-file identity so two files never share a baseline entry.
  • agentGuardrails(p, { src }) — the mistakes AI coding agents make most: generic errors, stub comments, empty bodies, copy-paste, and banned inline calls. Each rule carries imperative metadata for explain --format agent.

Every preset runs its rules and throws on any error-severity violation by default — the caller owns reporting (ADR-008). What you pass depends on where you are calling it:

| where | pass | why | | --------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------- | | a rule file (arch.rules.ts) | { report: 'builders' } | returns the builders without running them, so the CLI runs them, reports once, and applies --baseline / --changed | | a test, or your own runner | { report: 'return' } | runs the rules and hands you ArchViolation[] to assert on | | anywhere, advisory | { report: 'warn' } | reports without failing | | | { format: 'json' } | machine-readable output |

'return' in a rule file does not work. A rule file spreads its presets into export default [...], so 'return' splats the preset's result — an ArchViolation[] — into the rules array. What happens next depends on your codebase, and both outcomes are bad:

| your codebase | what you get | | -------------- | -------------------------------------------------------------------------------------------- | | has violations | the loader rejects it: default export entry [0] is not a rule builder (got object), exit 1 | | is clean | the array is empty, so the file exports [] and every rule silently disappears |

tsc --noEmit catches neither — a spread of the wrong array type is not a type error. check now refuses a rule file that contributed no rules, so the clean case fails too rather than printing a green tick; before that it exited 0. Use 'builders' in a rule file. eess-ts init scaffolds it.

Per-rule severity is tunable via overrides.

Scaffold a starter setup

eess-ts init writes a working arch.rules.ts (the floor preset expanded as editable builders), an eess-ts.config.ts, and arch / arch:baseline npm scripts:

npx eess-ts init                        # recommended floor (default)
npx eess-ts init --preset agent-guardrails
npx eess-ts init --dry-run              # preview; writes nothing

Feed Your Architecture to the Agent

The explain command dumps all active rules as structured JSON — pipe it into your agent's system prompt so it knows the constraints before writing code:

npx eess-ts explain arch.rules.ts

For an agent's system prompt specifically, --format agent emits an imperative, sentinel-wrapped rules block (grouped by rule namespace) built from each rule's imperative metadata, with a check-in-the-loop preamble:

npx eess-ts explain arch.rules.ts --format agent
{
  "rules": [
    {
      "id": "repo/typed-errors",
      "rule": "that extend 'BaseRepository' should not contain new 'Error'",
      "because": "Generic Error loses context and prevents consistent error handling",
      "suggestion": "Use NotFoundError, ValidationError, or DomainError instead"
    }
  ]
}

The agent reads the rules, understands the constraints, and generates compliant code from the start. When it doesn't, CI catches it with actionable violation messages.

Custom Rules

The fluent API reads like English:

// Select → Filter → Assert → Execute
classes(p).that().extend('BaseRepository').should().notContain(call('parseInt')).check()

On the root, a plain name such as resideInFile is the predicate, for satisfy() after .that(); where the same word is also a condition, the twin carries the prefix: conditionResideInFile. (In eess-md the plain names lean the other way; a twin always takes the predicate or condition prefix.)

Body Analysis

Inspect what happens inside functions — the differentiator:

// Ban inline parseInt — use the shared helper
classes(p)
  .that()
  .extend('BaseRepository')
  .should()
  .useInsteadOf(call('parseInt'), call('this.extractCount'))
  .check()

// Services must delegate to repositories
functions(p)
  .that()
  .resideInFolder('**/services/**')
  .should()
  .satisfy(mustCall(/Repository/))
  .check()

// No process.env in domain — use dependency injection
functions(p).that().resideInFolder('**/domain/**').should().satisfy(functionNoProcessEnv()).check()

Layer Enforcement

slices(p)
  .assignedFrom({
    controllers: 'src/controllers/**',
    services: 'src/services/**',
    repositories: 'src/repositories/**',
  })
  .should()
  .respectLayerOrder('controllers', 'services', 'repositories')
  .check()

slices(p).matching('src/features/*/').should().beFreeOfCycles().check()

Type-Level Rules

Check property types using the TypeScript type checker:

types(p)
  .that()
  .haveProperty('orderBy')
  .should()
  .havePropertyType('orderBy', not(isString()))
  .rule({
    because: 'Bare string orderBy is a SQL injection surface',
    suggestion: "Use a union type: orderBy?: 'created_at' | 'updated_at'",
  })
  .check()

Standard Rules Library

25+ ready-to-use rules across 8 categories:

import {
  functionNoEval,
  functionNoConsole,
  functionNoJsonParse,
} from '@nielspeter/eess-ts/rules/security'
import { functionNoGenericErrors } from '@nielspeter/eess-ts/rules/errors'
import { mustCall } from '@nielspeter/eess-ts/rules/architecture'
import { noDeadModules, noStubComments, noEmptyBodies } from '@nielspeter/eess-ts/rules/hygiene'

functions(p).that().resideInFolder('src/**').should().satisfy(functionNoEval()).check()
functions(p).that().resideInFolder('src/**').should().satisfy(noEmptyBodies()).check()
functions(p).that().resideInFolder('src/**').should().satisfy(noStubComments()).check()

Categories: rules/typescript, rules/security, rules/errors, rules/naming, rules/dependencies, rules/code-quality, rules/metrics, rules/architecture, rules/hygiene.

Baseline Mode

Adopt rules in existing codebases without fixing every pre-existing violation:

const baseline = withBaseline('arch-baseline.json')

// Only NEW violations fail — existing ones are recorded
classes(p).should().notContain(call('parseInt')).check({ baseline })

GitHub Actions Annotations

Violations appear inline on PR diffs — automatically detected in GitHub Actions:

classes(p).should().notContain(call('eval')).check({ format: detectFormat() })

Smell Detection

Find code drift — duplicate function bodies and inconsistent patterns:

smells.duplicateBodies(p).inFolder('src/routes/**').withMinSimilarity(0.9).warn()

smells
  .inconsistentSiblings(p)
  .inFolder('src/repositories/**')
  .forPattern(call('this.extractCount'))
  .warn()

More Features

Entry Points

| Function | Operates on | Use case | | -------------- | ----------------------------------------- | ----------------------------------------------------- | | modules(p) | Source files | Import/dependency rules | | classes(p) | Class declarations | Inheritance, decorators, methods, body analysis | | functions(p) | Functions, arrow functions, class members | Naming, parameters, body analysis | | types(p) | Interfaces + type aliases | Property types, type safety | | slices(p) | Groups of files | Cycles, layer ordering | | calls(p) | Call expressions | Framework-agnostic route/handler matching | | within(sel) | Scoped callbacks | Rules inside matched call callbacks | | tsconfig(p) | Resolved compiler options | Assert strict, target, etc. via .requires(spec) |

Compared to Other Tools

| Capability | eess-ts | dependency-cruiser | ArchUnitTS | Biome | ts-arch | | ------------------------------------------------- | --------------- | -------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------- | --------------------------------------------- | | Import path rules | Yes | Yes | Yes | Yes (noRestrictedImports) | Yes | | Body analysis (calls, access, constructors) | Yes | No | No | No | No | | Type checking (resolved types via ts-morph) | Yes | No | No | No | No | | Class rules (inheritance, decorators, members) | Yes | No | No | No | No | | Function rules (params, return types, async) | Yes | No | No | No | No | | JSX element rules | Yes | No | No | No | No | | Cross-layer rules (pair-condition per layer) | Yes | No | No | No | Partial (layered slices) | | Cycle detection | Yes | Yes | Yes | No | Yes | | Parameterized presets | Yes | Flat config | No | No | No | | Pattern matching (reusable rule templates) | Yes | No | No | No | No | | Code smell detection (duplicate bodies, siblings) | Yes | No | No | No | No | | Code metrics | Cyclomatic, LOC | No | LCOM, coupling, instability | No | No | | Baseline / gradual adoption | Yes | No | No | No | No | | Diff-aware / PR-only checking | Yes | No | No | No | No | | Watch mode | Yes | No | No | Yes | No | | Inline exclusion comments | Yes | No | No | Yes (biome-ignore) | No | | JSON output format | Yes | Yes | Via vitest | Yes | Via test runner | | GitHub PR annotations | Yes | No | No | No | No | | Violation messages with fix suggestions | Yes | No | No | Partial (autofixes) | No | | explain command (dump rules as JSON for agents) | Yes | No | No | No | No | | PlantUML diagram compliance | No | No | Yes | No | No | | Dependency graph visualization | No | Yes (dot, HTML) | No | No | No | | License checking | No | Yes | No | No | No | | Nx monorepo support | No | No | Yes | No | No |

Use eess-ts when you need to enforce what happens inside functions — call patterns, error types, missing delegation, stub comments — and when AI agents are generating code that needs architectural guardrails. This is the only tool that catches "service calls parseInt instead of extractCount()", flags duplicate function bodies as smells, and can restrict checking to only the files changed in a PR.

Use dependency-cruiser when you only need import direction rules and want fast graph visualization, license compliance checking, or stability metrics. It's faster (no ts-morph project load) and has mature HTML/dot reporting.

Use ArchUnitTS when you need OO metrics (LCOM cohesion, coupling factor, distance from main sequence), PlantUML diagram validation, or Nx monorepo project-graph awareness.

Use Biome when you want a fast Rust-based lint + format toolchain and only need lint-level import restrictions (noRestrictedImports) — not architecture rules over class hierarchies, function bodies, or slice cycles. Pair it with eess-ts if you want both.

Use ts-arch when you want the classic ArchUnit-style fluent API scoped to file/folder dependency rules and layered slices, and don't need body analysis, type-checker rules, or CI integrations like baselines, diff-aware runs, or GitHub annotations.

Use eess-ts + dependency-cruiser together if you want both body-level enforcement and dependency graph visualization.

Install

npm install -D @nielspeter/eess-ts

Requires Node.js >= 24 and a tsconfig.json. Works with vitest (recommended) or jest.

License

MIT