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

enforce-export-coverage

v1.0.0

Published

Enforce that every exported TypeScript function has a corresponding test

Readme

enforce-export-coverage

A static analysis tool that enforces every exported TypeScript function has a corresponding test — without running your test suite.

Designed to run as a fast pre-commit hook on staged files, or as a full project scan in CI.


Why not just set Jest's coverageThreshold: { functions: 100 }?

It's a fair question. The two approaches overlap but are not equivalent:

| | enforce-export-coverage | Jest functions: 100 | |---|---|---| | Requires a dedicated test | Yes — the function name must appear as a call in a test file | No — executing the function as a side effect of any test counts | | Scope | Exported functions only (your public API) | All functions, including private helpers and anonymous callbacks | | Speed | Sub-second static analysis, no test execution | Requires running the full test suite | | Pre-commit (staged files only) | Yes | Impractical — too slow for every commit | | Actionable output | Names the exact function and file missing a test | Reports a percentage; you must open the HTML report to find what's missing |

The honest take: if you have collectCoverageFrom configured and coverageThreshold: { functions: 100 } in CI, you catch most of the same problems at the pipeline level. The two tools are complementary: this one gives you fast, intent-based, export-focused enforcement before you commit; Jest's threshold gives you a hard runtime gate afterwards. Running both is not redundant — one is proactive, one is a safety net.


Installation

npm install --save-dev enforce-export-coverage

Or install globally to use the CLI anywhere:

npm install -g enforce-export-coverage

CLI usage

Check specific files (pre-commit hook pattern)

Pass file paths as arguments. Non-.ts files and test files are automatically skipped.

enforce-export-coverage src/handlers/foo.ts src/utils/bar.ts

Scan the entire source root

enforce-export-coverage --all

All options

enforce-export-coverage [files...]

Options:
  --all                  Scan all .ts files under srcRoot instead of reading from arguments
  --project-root <path>  Project root directory (default: cwd)
  --src-root <path>      Source directory, relative to project root (overrides config)
  --test-root <path>     Test mirror directory, relative to project root (overrides config)
  --config <path>        Path to a JSON config file
  --help                 Show this message

Exit codes:
  0  All exported functions have tests
  1  One or more exported functions are untested

Configuration

Configuration can be placed in a dedicated file or inside package.json. All fields are optional and fall back to the defaults shown below.

enforce-export-coverage.config.json

{
    "srcRoot": "src",
    "testRoot": "test",
    "testSuffixes": [".test.ts", ".spec.ts"],
    "testConventions": ["co-located", "__tests__", "mirror"],
    "ignore": []
}

package.json (alternative)

{
    "enforceExportCoverage": {
        "srcRoot": "srv",
        "testRoot": "test",
        "testConventions": ["mirror"]
    }
}

Config options

| Option | Type | Default | Description | |---|---|---|---| | srcRoot | string | "src" | Source directory to scan, relative to the project root | | testRoot | string | "test" | Root for mirror-style test files, relative to the project root | | testSuffixes | string[] | [".test.ts", ".spec.ts"] | File suffixes that identify test files | | testConventions | string[] | ["co-located", "__tests__", "mirror"] | Which test file location patterns to check (see below) | | ignore | string[] | [] | Substrings matched against project-relative paths — matching files are skipped |

Test conventions

The tool checks three conventions and considers a function tested if any of them finds a matching test file:

  • co-located — test file lives next to the source file
    src/handlers/foo.tssrc/handlers/foo.test.ts

  • __tests__ — test file lives in a __tests__ subdirectory
    src/handlers/foo.tssrc/handlers/__tests__/foo.test.ts

  • mirror — test file mirrors the source tree under testRoot
    src/handlers/foo.tstest/handlers/foo.test.ts


Integrations

Pre-commit hook with lint-staged

lint-staged passes staged file paths as arguments automatically:

{
    "lint-staged": {
        "src/**/*.ts": "enforce-export-coverage"
    }
}

Pre-commit hook with Husky

# .husky/pre-commit
npx lint-staged

Or directly, using git diff to get staged .ts files:

# .husky/pre-commit
STAGED=$(git diff --cached --name-only --diff-filter=ACM | grep '\.ts$' | grep -v '\.test\.ts$')
[ -z "$STAGED" ] && exit 0
enforce-export-coverage $STAGED

CI — full project scan

# GitHub Actions example
- name: Enforce export coverage
  run: enforce-export-coverage --all --src-root srv --test-root test

Programmatic API

The package exports all its internals for use in custom scripts or other tools.

import { validateFunctionCoverage } from 'enforce-export-coverage';

const result = validateFunctionCoverage(['./src/handlers/foo.ts'], {
    projectRoot: '/path/to/project',
    srcRoot: 'src',
    testRoot: 'test',
    testConventions: ['mirror'],
});

if (!result.success) {
    for (const { functionName, filePath } of result.violations) {
        console.error(`'${functionName}' in ${filePath} has no test`);
    }
}

validateFunctionCoverage(sourceFiles, options?)

| Parameter | Type | Description | |---|---|---| | sourceFiles | string[] | Absolute paths to source files to check | | options.projectRoot | string | Absolute project root (default: process.cwd()) | | options.configPath | string | Path to a JSON config file | | options.srcRoot | string | Overrides srcRoot from config | | options.testRoot | string | Overrides testRoot from config | | options.testSuffixes | string[] | Overrides testSuffixes from config | | options.testConventions | string[] | Overrides testConventions from config | | options.ignore | string[] | Overrides ignore from config |

Returns { success: boolean, violations: Array<{ functionName: string, filePath: string }> }.

Other exports

import {
    loadConfig,           // load + merge config from project files
    resolveTestFilePaths, // resolve candidate test paths for a source file
    getExportedFunctionNames, // extract exported function names via ts-morph
    isTestedIn,           // check if a function name is called in a test file
    findFiles,            // recursively find .ts files under a directory
} from 'enforce-export-coverage';

How it works

  1. For each source file, ts-morph parses the AST and extracts the names of all exported functions (both export function foo() and export const foo = () => {}).
  2. Candidate test file paths are resolved according to the configured conventions.
  3. Each candidate test file is read, comments are stripped, and a word-boundary regex checks for a call expression matching the function name. A property access (.foo() does not count.
  4. If no candidate file contains a call to the function, a violation is recorded.

License

MIT