Move from Jest to Vitest without the manual rewrite.
Paste a jest.config.js, jest.config.ts, jest.config.json, a package.json with a jest key, or an embedded jest: block from vue.config / craco.config. Get back a working Vitest config - standalone or merged into an existing Vite config - with notes for anything that does not map cleanly. AST-based, not regex.
npm i -D @shiftkit/jest-to-vitest- Mapped: All fields converted deterministically.
- Semantics: No behavioral differences between Jest and Vitest for this config.
- Coverage: Output backed by an extensive test suite.
Couldn’t map safely. You decide.
Mapped, but semantics differ. Worth a look.
Bookkeeping. Nothing to fix.
Migration checklist
A short, opinionated runbook from someone who did this on a 200-test repo. Work top-down - or skip steps 1, 2, and 7 by running the CLI’s --apply mode (see below).
- Save the new config. Drop the output above into
vitest.config.tsat your project root. - Update package.json scripts. Replace
jestwithvitest runin CI, andvitestin dev. The watch UI is interactive by default. - Install required peers.See the “Install required peers” block above (or check the
// Next stepscomments at the bottom of the generated file). - Swap the test imports. Replace
jest.fn(),jest.mock(),jest.spyOn()withvi.*equivalents. A codemod is available as@vitest/migrate. - Run once.
vitest run --reporter=verbose. Async timing bugs Jest hid will surface here. - Address the manual items above (currently 0). Verify items are usually fine but worth a glance.
- Regenerate snapshots if needed.
vitest run --update. - Remove old deps.
npm uninstall jest ts-jest @types/jest babel-jest @swc/jest.
Skip the manual steps: --apply
Prefer to do this from the terminal? The 0.2.0 CLI applies the config to disk for you. Refuses to run on a dirty git tree, so your diff stays reviewable:
npx @shiftkit/jest-to-vitest --apply --delete-oldWrites vitest.config.ts, removes jest / @types/jest / ts-jest / babel-jest / @swc/jest from devDependencies, adds vitest and any required peers at sensible major ranges, and rewrites jest invocations in your scripts. Add --strict in CI to fail the build when manual-review warnings are emitted.
What gets mapped
The converter parses your config with Babel rather than regex, so it understands TypeScript, satisfies, defineConfig() wrappers, module re-exports, plain jest.config.json, embedded jest: blocks inside vue.config / craco.config, and arrow / async-arrow function-form configs with a single return. Below is a partial mapping table - the converter handles many more fields than fit here, with a warning surfaced for anything that is not a clean direct mapping.
| Jest | Maps to | Vitest | Notes |
|---|---|---|---|
| testEnvironment | → | test.environment | direct |
| moduleNameMapper | → | resolve.alias | regex anchors stripped; relative paths resolved with path.resolve |
| setupFilesAfterEnv | → | test.setupFiles | semantic verify warning |
| coverageThreshold.global | → | coverage.thresholds | flattened |
| transformIgnorePatterns | → | server.deps.inline | extracts package names |
| moduleFileExtensions | → | resolve.extensions | direct |
| testURL | → | environmentOptions.jsdom.url | direct |
Unsupported options
These Jest options have no clean Vitest equivalent. The converter emits a manual-review warning rather than silently dropping or mismapping them:
testRunner: ‘jest-jasmine2’. No equivalent. Vitest tests should already pass on the default runner.resolvercustom resolvers. Vite’s resolver is plugin-based; rewrite as a Vite plugin.globalTeardown. No separate Vitest config key - return a teardown function fromglobalSetupinstead.testResultsProcessor. Replace with a Vitest reporter or a post-processing step.snapshotResolver. Vitest’sresolveSnapshotPathhas a different function signature; port the resolver manually.testSequencer. Vitest’s sequencer constructor must come fromvitest/node; the Jest-style class won’t plug in directly.- Multi-statement or dynamic function-form configs (e.g.
module.exports = async () => { /* logic */ return {...} }). Single-return arrow and async-arrow forms are handled automatically; for anything more complex, invoke the function once and paste the resolved object.
How accurate is the output?
Mappings are deterministic and covered by an extensive test suite. Behavioral differences (hook order, vi.mock hoisting) are surfaced as warnings rather than auto-fixed, because the right call depends on your test bodies.
FAQ
Is my config uploaded anywhere?
No. Parsing runs entirely in your browser, and your config is never sent anywhere.
What if my Jest config is a function?
The converter handles arrow-function and async-arrow configs with a single return-statement body, and emits a verify warning so you can double-check the extracted object. For multi-statement or dynamic function-form configs, invoke the function manually and paste the resulting object.
Will it convert my test files?
No, this tool only handles config. For test bodies, use a codemod like @vitest/migrate.
Why does the output say "manual review required" instead of a percentage?
Because percentages are misleading here. The converter either mapped a field correctly or it didn't. The answer is binary per field. The label tells you whether you need to do anything before merging.
Can I run this from the command line instead of pasting into the browser?
Yes. Install with "npm i -D @shiftkit/jest-to-vitest" or run "npx @shiftkit/jest-to-vitest jest.config.js". The 0.2.0 release added an --apply flag that writes vitest.config.ts to disk, updates package.json scripts and devDependencies, and adds the required peers (jsdom, happy-dom, vite-tsconfig-paths, vite-plugin-svgr, @vitest/coverage-v8) at sensible major ranges. --apply refuses to run on a dirty git tree unless you pass --force, so your migration diff stays reviewable.
How do I run this in CI as a migration check?
Use --strict to exit non-zero whenever a manual-review warning is emitted, or --json to pipe a machine-readable payload into your own checks. A reusable GitHub Action (JustShift/jest-to-vitest@v1) mirrors the CLI flags and exposes warning-count / manual-count / json outputs.
How is the package itself secured against supply-chain attacks?
Releases are published from GitHub Actions via npm Trusted Publishing (OIDC, no long-lived tokens) and each version ships with a provenance attestation, so you can verify the tarball was built from the tagged commit. We also recommend setting "min-release-age=7" in your .npmrc - that single line would have blocked every major 2026 npm supply-chain incident (Axios, Trivy, LiteLLM, Telnyx, Checkmarx), because malicious versions are typically caught and unpublished within hours.
Changelog
Pulled from the package’s CHANGELOG.md at build time. The currently installed version is v0.2.1.
- v0.2.12026-05-30latest
- fix`vite-plugin-svgr` is now emitted **only** when a `moduleNameMapper` entry's _target_ is an SVG-to-component transformer (e.g. `@svgr/*`, `jest-svg-transformer`). Previously any key mentioning `.svg` — including generic stubs mapped to `fileMock.js` or `identity-obj-proxy` — silently pulled in `vite-plugin-svgr` and rewrote SVG imports to `?react`. Such stubs now emit a verify warning instead, with no unexpected dependency or import-semantics change.
- fixsingle-extension asset/font keys (e.g. `\.svg$`, `\.png$`, `\.woff2$`) are now recognized as asset/font stubs instead of becoming invalid regex-keyed `resolve.alias` entries.
- chorebump default `--apply` devDependency ranges to current majors — `jsdom@^29`, `happy-dom@^20`, `vite-tsconfig-paths@^6`, `vite-plugin-svgr@^5` (`vitest@^4` unchanged).
- featstyle(converter): `server.deps.inline` package names now use single quotes, matching the rest of the generated config.
- featci: tests are now type-checked in CI via `tsconfig.test.json` (`npm run lint` covers `tests/` too).
- featci: the release workflow now creates a **GitHub Release** (notes sourced from this changelog) after publishing to npm.
- featdocs: `RELEASING.md` rewritten as a single, consistent, step-by-step trunk-based guide.
- v0.2.02026-05-11
- feat`--apply` mode auto-detects `jest.config.{ts,mts,cts,js,mjs,cjs,json}` (or `package.json#jest`), writes `vitest.config.ts`, updates `package.json` deps and scripts. Refuses to run on a dirty git tree (`--force` to override). Optional `--delete-old` removes the original Jest config.
- feat`--json` emits `{ output, warnings, flags }` (or the apply payload) as JSON for CI integration
- feat`--no-format` disables output pretty-printing
- featpretty-printer for multi-line arrays and objects (e.g. `projects`, `coverageThreshold`); no new dependencies
- featinline `// VERIFY:` and `// MANUAL:` comments alongside field-tied warnings (`mockReset`, `setupFiles`, `reporters`, `deps`, `thresholds`, `environmentOptions`, `globalSetup`) so they survive `> vitest.config.ts` redirection
- featSVG stubs in `moduleNameMapper` now emit `vite-plugin-svgr` plugin import, `plugins: [svgr()]`, and install step (was previously just a warning)
- featdetect CommonJS extensions (`.js`, `.cjs`) in `globalSetup` and emit a verify warning (Vitest expects ESM default export)
- featnew `format: boolean` option in `ConvertOptions`; new `needsSvgr` flag in `ConversionFlags`
- featfirst-party GitHub Action (`JustShift/jest-to-vitest@v1`) wrapping the CLI with `--json` parsing into `output-file` / `warning-count` / `manual-count` / `json` outputs
- v0.1.42026-05-10
- chorerelicense from MIT to Apache-2.0 (stronger patent grant; SPDX-clean field for npm)
- v0.1.0Initial release
- featAST-based Jest → Vitest config converter
- featCLI: `jest-to-vitest` reads from a file or stdin
- featProgrammatic API: `convertJestToVitest(source, options)`
- featHandles `module.exports`, `export default`, function-form configs, embedded `vue.config` / `craco.config` blocks, and `package.json` `"jest"` keys
- featMaps coverage, transforms, fakeTimers, poolOptions (Vitest 4), moduleNameMapper, and ~50 other fields
- featEmits `manual` / `verify` / `info` warnings for fields that need human review