enforce-export-coverage
v1.0.0
Published
Enforce that every exported TypeScript function has a corresponding test
Maintainers
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-coverageOr install globally to use the CLI anywhere:
npm install -g enforce-export-coverageCLI 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.tsScan the entire source root
enforce-export-coverage --allAll 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 untestedConfiguration
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 filesrc/handlers/foo.ts→src/handlers/foo.test.ts__tests__— test file lives in a__tests__subdirectorysrc/handlers/foo.ts→src/handlers/__tests__/foo.test.tsmirror— test file mirrors the source tree undertestRootsrc/handlers/foo.ts→test/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-stagedOr 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 $STAGEDCI — full project scan
# GitHub Actions example
- name: Enforce export coverage
run: enforce-export-coverage --all --src-root srv --test-root testProgrammatic 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
- For each source file,
ts-morphparses the AST and extracts the names of all exported functions (bothexport function foo()andexport const foo = () => {}). - Candidate test file paths are resolved according to the configured conventions.
- 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. - If no candidate file contains a call to the function, a violation is recorded.
License
MIT
