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

@gabadi/crap4js

v0.1.0

Published

CRAP (Change Risk Anti-Pattern) metric analyzer for JavaScript/TypeScript projects

Readme

crap4js

CRAP (Change Risk Anti-Pattern) metric analyzer for JavaScript/TypeScript projects.

Combines cyclomatic complexity with test coverage to identify functions that are both complex and under-tested — the riskiest code to change. This is a JS/TS-native port in the spirit of Robert C. "Uncle Bob" Martin's family of CRAP analyzers — including crap4clj (Clojure) and crap4go (Go) — adapted for JavaScript and TypeScript source analysis using native tooling. The report format and metric semantics most closely follow crap4clj (see Compatibility with crap4clj below).

Quick Start

Install from npm:

npm install --save-dev @gabadi/crap4js
# or
pnpm add -D @gabadi/crap4js

Alternatively, pin to a release tag directly from GitHub (see Releases) — note the source repo is private, so this route requires access:

npm install --save-dev github:gabadi/crap4js#v0.1.0
# or
pnpm add -D github:gabadi/crap4js#v0.1.0

Run against your project (requires coverage/lcov.info):

npx crap4js
# or
pnpm exec crap4js

Generate coverage and analyze in one step:

npx crap4js --cov-cmd "npx vitest run --coverage"
# or
pnpm exec crap4js --cov-cmd "pnpm exec vitest run --coverage"

Output

CRAP Report
===========
Function                       Source/Module                         CC    Cov%     CRAP
----------------------------------------------------------------------------------------
complexFunction                src/logic.ts                          12    45.0%    130.2
simpleHelper                   src/utils.ts                           1   100.0%      1.0

CLI Usage

Usage: crap4js [options] [module-filter ...]

Runs coverage, computes CRAP scores, and prints a report sorted worst first.

Options:
  -h, --help       Print this help message and exit.
  -V, --version    Print version and exit.
  --format <type>  Output format: table (default) or json.
  --cov-cmd <cmd>  Coverage command to run in analyzed project root.
  --root <path>    Root directory to analyze (default: current directory).

Arguments:
  module-filter     Optional source path fragment. When present, only
                    matching source files are analyzed.

Examples

Analyze with existing coverage:

pnpm exec crap4js

Generate coverage and analyze:

pnpm exec crap4js --cov-cmd "pnpm exec vitest run --coverage"

JSON output for tooling integration:

pnpm exec crap4js --format json

Filter to specific modules:

pnpm exec crap4js auth user
pnpm exec crap4js src/core/utils

Analyze a different project root:

pnpm exec crap4js --root ./packages/api

Library API

crap4js can also be used as a library:

import {
  analyzeProject,
  crapScore,
  sortByCrap,
  formatReport,
  formatJson,
  parseLcov,
  extractFunctions,
} from "@gabadi/crap4js";

// Analyze a full project
const result = await analyzeProject({
  rootDir: "/path/to/project",
  moduleFilters: [],
  coverageCommand: null,
  format: "json",
});

console.log(formatReport(result.entries));

// Use individual functions
const score = crapScore(5, 0.0); // 30.0 (CC=5, 0% coverage)
const entries = extractFunctions(sourceText, "module.ts");

Exported Functions

| Function | Description | |----------|-------------| | analyzeProject(options) | Full project analysis with coverage, sorting, and diagnostics | | analyzeFile(sourcePath, sourceText, lcovData) | Single-file CRAP analysis | | loadLcov(lcovPath) | Parse an LCOV file from disk | | crapScore(complexity, coveragePercent) | Compute CRAP score (null for indeterminate) | | sortByCrap(entries) | Sort entries worst-first, N/A last | | extractFunctions(sourceText, sourcePath) | Extract callable functions with complexity | | parseLcov(text) | Parse LCOV text into line coverage data | | coverageForRange(lineCov, startLine, endLine) | Coverage for a function's line range | | lcovCoverageForSource(lcovData, sourcePath) | Find LCOV data for a source file | | lcovDiagnostics(lcovData, sourcePath) | Diagnostics for unmatched source paths | | formatReport(entries) | Format entries as text table | | formatJson(result) | Format analysis result as JSON | | findSourceFiles(rootDir, filters) | Discover supported JS/TS source files |

Exported Types

  • CrapEntry — Analyzed function entry with name, source, complexity, coverage, CRAP score
  • AnalysisResult — Full result including entries, diagnostics, and summary
  • AnalysisOptions — Configuration for project analysis
  • AnalysisSummary — Aggregate statistics (totals, averages, worst score)
  • ExtractedFunction — Parsed function with name, line range, complexity
  • Diagnostic — Union type for coverage, parse, and missing-path diagnostics
  • LcovData — Parsed LCOV coverage data structure

Coverage Requirements

crap4js requires LCOV coverage data at coverage/lcov.info in the analyzed project root. This is the standard output format for:

  • Vitest with @vitest/coverage-v8 or @vitest/coverage-istanbul
  • Jest with jest --coverage
  • c8 / nyc / Istanbul
  • Any tool that emits LCOV format

The --cov-cmd option runs the coverage command in the analyzed project root, deleting stale coverage first. If no coverage command is provided, crap4js uses any existing coverage/lcov.info.

CRAP Formula

CRAP(fn) = CC² × (1 - coverageFraction)³ + CC
  • CC = cyclomatic complexity (decision points + 1)
  • coverageFraction = covered lines / total executable lines

| Score | Risk | |-------|------| | 1-5 | Low — clean code | | 5-30 | Moderate — refactor or add tests | | 30+ | High — complex and under-tested |

What It Counts

Decision points that increase cyclomatic complexity in JS/TS:

  • if, else if
  • Conditional expressions (?:)
  • Logical operators: &&, ||, ??
  • Loops: for, for...of, for...in, while, do...while
  • catch clauses
  • Each case and default in switch

Optional chaining (?.) is not a decision point — it is a null-guard absent from Uncle Bob's original Go/Java/Clojure spec. Counting it would inflate CC on idiomatic TypeScript that uses ?. for safe property access.

Type annotations, interfaces, generics, imports, and type-only syntax do not increase complexity.

Supported Callable Forms

crap4js extracts and scores these JS/TS function forms with stable names:

  • Function declarations: function foo() {}
  • Exported functions: export function foo() {}
  • Arrow functions: const foo = () => {}
  • Function expressions: let foo = function() {}
  • Class methods (including static, private #, getters, setters)
  • Object literal methods: { method() {} }
  • Exports assignments: exports.foo = () => {}, module.exports = function() {}
  • Default exports: export default function() {}

Anonymous functions without stable names (IIFEs, bare callbacks) are excluded.

Indeterminate Coverage

When a function's coverage cannot be matched to LCOV data (for example, the source file is not in the LCOV output), coverage and CRAP are reported as N/A rather than 0.0%. This prevents false high-risk scores from missing coverage data rather than actual zero coverage.

JSON Output Schema

With --format json, crap4js outputs:

{
  "rootDir": "/path/to/project",
  "entries": [
    {
      "name": "functionName",
      "source": "src/module.ts",
      "startLine": 10,
      "endLine": 25,
      "complexity": 5,
      "coveragePercent": 85.0,
      "crap": 5.1
    }
  ],
  "diagnostics": [],
  "summary": {
    "totalFunctions": 42,
    "coveredFunctions": 38,
    "indeterminateFunctions": 4,
    "averageComplexity": 3.2,
    "averageCrap": 7.5,
    "worstCrap": 130.2
  }
}

Null values in coveragePercent and crap indicate indeterminate coverage.

Source Discovery

crap4js discovers source files under the project root:

  • Includes: .js, .jsx, .ts, .tsx implementation files under src/ (or the project root if no src/ directory exists)
  • Excludes: .d.ts declaration files, test/spec files (*.test.*, *.spec.*), node_modules, dist, coverage, build output, and hidden/cache directories

Self-Analysis

crap4js can analyze its own codebase:

pnpm run analyze:self

This generates coverage for the crap4js project and then runs the analyzer on its own source, producing a CRAP report for the tool itself.

Quality Commands

| Command | Description | |---------|-------------| | pnpm run lint | ESLint check on source and test | | pnpm run typecheck | TypeScript strict type check | | pnpm run test | Vitest test suite | | pnpm run coverage | Coverage with LCOV output | | pnpm run build | Build dist artifacts (ESM + CJS + declarations) | | pnpm run arch | Dependency-cruiser architecture boundary check | | pnpm run analyze:self | Self-analysis of crap4js source | | pnpm run mutate | Stryker mutation testing (concurrency 2) | | pnpm run check | Full aggregate quality gate |

Mutation threshold rationale: The break threshold of 50% reflects structural limits of Stryker's perTest coverage analysis against CLI subprocess execution paths and AST-based parsing where guard logic is inherent to the parser. Key survivor categories: (1) CLI subprocess tests invoke built dist/cli.js (not instrumented source), producing no-coverage mutants in cli/main.ts; (2) lcovDiagnostics scoring mutants are informational-only and don't affect CRAP scores; (3) complexity.ts has ~197 equivalent survivors because @typescript-eslint/typescript-estree handles comment/string/template guards at the AST level, making explicit guard clauses in our code unnecessary and their removal undetectable by mutation; (4) core.ts adapter code has filesystem/subprocess paths untestable at source level. Notable achievement: crap.ts and reporting.ts both achieve 100% mutation score. The sortByCrap null-handling survivors were resolved by extracting the comparator into a named crapComparator function and testing it via direct calls (bypassing Array.sort), enabling Stryker's perTest analysis to correctly attribute branch kills. See the mission library mutation-survivors.md for detailed per-category justification.

Architecture Boundaries

The codebase enforces these module boundaries via dependency-cruiser:

  • CRAP metric (crap.ts): Pure domain — no imports from CLI, filesystem, process, or presentation
  • Complexity (complexity.ts): Parser-only — no imports from CLI, core, or reporting
  • Coverage (coverage.ts): LCOV-only — no imports from CLI, core, or reporting
  • Reporting (reporting.ts): Presentation-only — no imports from CLI, core, or config
  • Types (types.ts): Pure definitions — no imports from implementation modules
  • Public API (index.ts): Re-exports only — no CLI internals

An ESLint custom rule also prevents string-concatenated filesystem path separators (e.g., + "/" +) in source modules, enforcing path.join/path.resolve usage for cross-platform path construction.

Compatibility with crap4clj

crap4js preserves the core CRAP formula and report semantics from crap4clj with these JS/TS-native adaptations:

| Aspect | crap4clj | crap4js | |--------|----------|---------| | Coverage source | Cloverage HTML/LCOV | LCOV from Vitest/Jest/c8/nyc | | Callable extraction | Clojure defn/defn- | JS/TS function/method/expression forms | | Decision points | Clojure if/when/and/or/case | JS/TS if/?:/&&/||/??/switch (no ?.) | | Indeterminate coverage | N/A for unmatched HTML | N/A for unmatched LCOV | | Report columns | Function, Namespace, CC, Cov%, CRAP | Function, Source/Module, CC, Cov%, CRAP | | Sorting | CRAP descending, N/A last | CRAP descending, N/A last |

Intentional differences:

  • crap4js uses LCOV as the sole coverage source (no HTML parsing) since JS/TS tools natively produce LCOV
  • crap4js does not count optional chaining (?.) as a decision point — it is a null-guard with no equivalent in Uncle Bob's original spec
  • crap4js does not count when/when-not (Clojure-only) or try/finally (neither adds a decision point)

GitHub Actions Integration

Add crap4js to any project's CI pipeline:

- name: Install crap4js
  run: npm install --save-dev @gabadi/crap4js

- name: Run CRAP analysis
  run: npx crap4js --cov-cmd "npx vitest run --coverage"

Or with JSON output for downstream processing:

- name: CRAP report
  run: npx crap4js --format json > crap-report.json

Development

pnpm install --frozen-lockfile    # install dependencies
pnpm run check                    # run all quality gates

Releasing a New Version

Releases are manual. The release workflow runs on version tags — it does not trigger on merges to main.

Steps:

  1. Run the full quality gate locally and confirm it passes:

    pnpm run check
  2. Bump the version in package.json:

    # edit package.json: "version": "X.Y.Z"
  3. Rebuild dist/ and commit everything:

    pnpm run build
    git add dist/ package.json
    git commit -m "chore: release vX.Y.Z"
  4. Tag and push the tag:

    git tag vX.Y.Z
    git push origin main
    git push origin vX.Y.Z
  5. The release workflow triggers on the tag push. It re-runs the full quality gate and creates a GitHub Release at https://github.com/gabadi/crap4js/releases/tag/vX.Y.Z.

  6. Publish to npm:

    npm publish --access public

Consumers install the new version with:

npm install --save-dev @gabadi/[email protected]

License

MIT