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, an embedded jest: block from vue.config / craco.config, or the next/jest wrapper from the Next.js docs. 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 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, rewrites jest invocations in your scripts, and rewrites the @testing-library/jest-dom import to its /vitest entry in detected setup files. Add --strict in CI to fail the build when manual-review warnings are emitted. Since 0.4.0, next-step commands follow your package manager (detected from the lockfile, or --pm pnpm|yarn|bun), and --target-vitest 3 emits Vitest 3 output for teams not yet on 4 - the same switch as the Vitest 4 / Vitest 3 toggle above.
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, wrapper calls whose argument is a variable (the next/jest createJestConfig(customJestConfig) shape - the inner config is resolved, and @vitejs/plugin-react plus vite-tsconfig-paths are added to cover what the SWC preset handled implicitly), arrow / async-arrow function-form configs, and multi-statement configs that build the object across statements (const config = {...}; config.x = y; module.exports = config) - those are folded statically, never executed. 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, including (?:…) groups and pnpm layouts |
| testEnvironmentOptions | → | test.environmentOptions | nested under the resolved environment (jsdom / happyDOM); flat keys are ignored by Vitest |
| projects | → | test.projects | each project object remapped to { test: { … } }; glob entries pass through |
| testRegex | → | test.include | simple patterns (suffix matchers, __tests__/, the CRA default) translated to equivalent globs; anything untranslatable falls back with a warning |
| 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.- Function-form configs whose result depends on runtime values (env vars, awaited calls, loops). Single-return forms and static multi-statement bodies - including
config.x = …assignment chains - are folded automatically; if-guarded assignments fold unconditionally with a verify warning. For genuinely dynamic logic, 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, and emits a verify warning so you can double-check the extracted object. Since 0.4.0 it also folds static multi-statement bodies - a local config object built up with "config.x = y" assignments and then returned or exported - without ever executing your code. Only genuinely dynamic logic (env vars, awaited calls, loops) still requires you to invoke the function manually and paste the resulting object.
Does it handle next/jest configs from the Next.js docs?
Yes. The standard shape (createJestConfig = nextJest({ dir }) wrapping a custom config object) is resolved to the inner config and converted. Because the Next.js SWC preset handled TS/JSX transforms, CSS module stubbing, and tsconfig path aliases implicitly, the output adds @vitejs/plugin-react and vite-tsconfig-paths and flags anything that may still need a mock in a setup file.
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 --apply flag writes vitest.config.ts to disk, updates package.json scripts and devDependencies, adds the required peers (jsdom, happy-dom, vite-tsconfig-paths, vite-plugin-svgr, @vitest/coverage-v8) at sensible major ranges, and rewrites the @testing-library/jest-dom import in detected setup files. It refuses to run on a dirty git tree unless you pass --force, so your migration diff stays reviewable. 0.4.0 added --pm npm|pnpm|yarn|bun (auto-detected from the lockfile) and --target-vitest 3|4 for teams not yet on Vitest 4.
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.4.0.
- v0.4.02026-06-13latest
- feat**stable warning codes** — every `Warning` now carries a
- feat**testRegex regex-to-glob conversion** — simple suffix
- feat**multi-statement static evaluation** — `const config =
- feat**path-aware `<rootDir>` normalizer** (`normalizeRootDir`,
- feat`watchPathIgnorePatterns` with static strings now emits the
- featnon-trivial `moduleNameMapper` keys (mid-string capture
- feat`jest-junit` maps to Vitest's built-in
- feat`@swc/jest` transforms are dropped like `ts-jest`/
- featdeprecated Jest field aliases — `setupTestFrameworkScriptFile`
- feat`workerThreads: true` (Jest 30) maps to `pool: 'threads'`;
- feat**package-manager-aware next steps** —
- feat**`--target-vitest 3|4`** (API:
- feat`--apply` now rewrites `'@testing-library/jest-dom'` (and
- feattest: **real-world fixture suite** — `tests/fixtures/` holds jest configs in
- feattest: no-`eval` source gate (`tests/no-eval.test.ts`) — the converter source
- v0.3.02026-06-11
- featdetect `next/jest` wrapper configs (`createJestConfig(...)`
- featresolve identifier arguments passed to unknown wrapper calls,
- featremap `projects` entries to the Vitest `{ test: { ... } }`
- fixnest `testEnvironmentOptions` under the resolved environment
- fixdrop `customExportConditions` with a manual warning.
- fixstrip non-capturing-group syntax from
- v0.2.12026-05-30
- 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