Skip to content

Instantly share code, notes, and snippets.

@davemo
Last active May 20, 2026 12:37
Show Gist options
  • Select an option

  • Save davemo/2940e1e48bd6661061b06e62123532f6 to your computer and use it in GitHub Desktop.

Select an option

Save davemo/2940e1e48bd6661061b06e62123532f6 to your computer and use it in GitHub Desktop.
Run only Vitest tests related to changed TypeScript files with ts-morph

Run only Vitest tests related to changed files

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.

How it works

  1. 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
  2. Load the TypeScript project with ts-morph using tsconfig.json.

  3. Build a reverse import/export graph:

    src/lib/money.ts
      <- src/components/MoneyCell.tsx
      <- src/components/MoneyCell.test.tsx
  4. Walk from changed files through reverse dependencies.

  5. Collect matching test files: *.test.ts, *.test.tsx, *.spec.ts, *.spec.tsx.

  6. 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.

Install for a Bun project

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.mjs

Add 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:related

Preview without running tests:

bun scripts/related-tests.mjs --staged

Get machine-readable output:

bun scripts/related-tests.mjs --staged --json

Pre-commit setup with plain git hooks

This 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-commit

Example .git/hooks/pre-commit:

#!/usr/bin/env sh
set -e

bunx lint-staged
bun run test:related

That gives you two layers:

  1. lint-staged runs fast file-oriented checks/fixes on staged files.
  2. test:related asks git for the staged files, uses ts-morph to find dependent test files, then runs those tests.

lint-staged config

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:related

You 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.

Why not only use lint-staged?

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.

Safety fallbacks

The script runs the full test suite when files like these change:

  • package.json
  • Bun/npm/yarn/pnpm lockfiles
  • vite.config.ts
  • vitest.config.ts
  • vitest.setup.ts
  • tsconfig*.json

You can customize fullSuitePatterns in scripts/related-tests.mjs for generated files, app-wide providers, test utilities, theme files, API clients, etc.

Commands

# 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-empty

Limitations

This 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.

/**
* Optional lint-staged config.
*
* Keep lint-staged focused on file-oriented work. The git pre-commit hook can
* run related tests once after lint-staged succeeds.
*/
export default {
'*.{js,jsx,ts,tsx}': (files) => [
`eslint --fix ${files.map((file) => JSON.stringify(file)).join(' ')}`,
],
}
{
"devDependencies": {
"@types/node": "latest",
"lint-staged": "latest",
"ts-morph": "latest",
"vitest": "latest"
},
"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"
]
}
}
#!/usr/bin/env sh
# Save as .git/hooks/pre-commit and chmod +x .git/hooks/pre-commit
#
# This uses lint-staged for formatting/linting staged files and then runs
# related tests for the staged changes.
set -e
bunx lint-staged
bun run test:related
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment