A small, dependency-graph based approach for local pre-commit checks in a React + TypeScript + Vite/Vitest app.
The goal is similar to lint-staged, but with one important difference: changed source files should trigger tests that depend on them, even when those test files were not changed locally.
-
Get changed files from git:
- staged files for pre-commit:
git diff --cached --name-only - or branch changes:
git diff origin/main...HEAD --name-only
- staged files for pre-commit:
-
Load the TypeScript project with
ts-morphusingtsconfig.json. -
Build a reverse import/export graph:
src/lib/money.ts <- src/components/MoneyCell.tsx <- src/components/MoneyCell.test.tsx
-
Walk from changed files through reverse dependencies.
-
Collect matching test files:
*.test.ts,*.test.tsx,*.spec.ts,*.spec.tsx. -
Run:
vitest run <related test files>
If a global/config file changes, or no related tests are found, the script defaults to running the full suite. This makes it safer for pre-commit use.
bun add -d ts-morph vitest @types/node lint-staged
mkdir -p scripts
cp scripts/related-tests.mjs ./scripts/related-tests.mjs
chmod +x scripts/related-tests.mjsAdd scripts to package.json:
{
"scripts": {
"lint": "eslint .",
"test": "vitest run",
"test:related": "bun scripts/related-tests.mjs --staged --run",
"test:related:branch": "bun scripts/related-tests.mjs --base origin/main --run"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
}
}Run locally:
bun run test:relatedPreview without running tests:
bun scripts/related-tests.mjs --stagedGet machine-readable output:
bun scripts/related-tests.mjs --staged --jsonThis does not require Husky. Save the included pre-commit file as .git/hooks/pre-commit and make it executable:
cp pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commitExample .git/hooks/pre-commit:
#!/usr/bin/env sh
set -e
bunx lint-staged
bun run test:relatedThat gives you two layers:
lint-stagedruns fast file-oriented checks/fixes on staged files.test:relatedasks git for the staged files, usests-morphto find dependent test files, then runs those tests.
The simplest package.json config keeps lint-staged focused on file-oriented lint/format work:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
}
}If you want more control, use the included lint-staged.config.mjs instead:
export default {
'*.{js,jsx,ts,tsx}': (files) => [
`eslint --fix ${files.map((file) => JSON.stringify(file)).join(' ')}`,
],
}Then your pre-commit hook can run:
bunx lint-staged --config lint-staged.config.mjs
bun run test:relatedYou can put bun run test:related inside lint-staged if you prefer. The script ignores extra file arguments when using --staged. Just avoid also running it separately in the pre-commit hook, or it will run twice.
lint-staged is great for commands that operate on the changed files themselves. Tests often need the opposite lookup: given a changed implementation file, find unmodified tests that import it directly or indirectly.
This script computes that dependent test set using TypeScript import resolution.
The script runs the full test suite when files like these change:
package.json- Bun/npm/yarn/pnpm lockfiles
vite.config.tsvitest.config.tsvitest.setup.tstsconfig*.json
You can customize fullSuitePatterns in scripts/related-tests.mjs for generated files, app-wide providers, test utilities, theme files, API clients, etc.
# staged files, suitable for pre-commit
bun scripts/related-tests.mjs --staged --run
# compare current branch to origin/main
bun scripts/related-tests.mjs --base origin/main --run
# inspect related tests without running them
bun scripts/related-tests.mjs --staged
# pass explicit files
bun scripts/related-tests.mjs --files src/foo.ts src/components/Button.tsx --run
# avoid full-suite fallback when no related tests are found
bun scripts/related-tests.mjs --staged --run --allow-emptyThis is intentionally lightweight. It handles normal TypeScript import/export declarations and barrel files, but you may want full-suite fallbacks for:
- dynamic imports
- generated clients
- path aliases with unusual TypeScript config
- test setup files
- CSS/assets that affect component snapshots
- shared providers or app bootstrap files
For CI-scale affected-project orchestration, tools like Nx or Turborepo are more complete. For fast local pre-commit validation in a Vite/Vitest TypeScript app, this approach is usually enough.