Skip to main content

vitest-migration

Migrate an Nx repo package's unit tests from Jest to Vitest, reusing the shared setup that packages/workspace established. Use when asked to "move <pkg> to vitest", "migrate <pkg> tests off jest", or "run <pkg> unit tests with vitest".

Source facts

Repository
nrwl/nx
Last source activity
September 25, 2026 at 13:23
Detected SKILL.md language
English
Stars
29,380
Forks
3,007

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
vitest-migration
description
Migrate an Nx repo package's unit tests from Jest to Vitest, reusing the shared setup that packages/workspace established. Use when asked to "move <pkg> to vitest", "migrate <pkg> tests off jest", or "run <pkg> unit tests with vitest".
allowed-tools
Read, Glob, Grep, Agent, Edit(*), Write(*), Bash(pnpm nx *), Bash(npx nx *), Bash(nx *), Bash(git *), Bash(ls *), Bash(cat *), Bash(head *), Bash(tail *), Bash(sed *), Bash(grep *), Bash(rg *), Bash(find *), Bash(wc *), Bash(echo *), Bash(mkdir *), Bash(rm *), Bash(mv *), Bash(node *), Bash(npx oxfmt *), Bash(gh pr view *), Bash(gh pr diff *)
# Migrate a package's unit tests to Vitest Move `packages/<name>`'s unit tests from Jest to Vitest 4, inferred through the `@nx/vitest` plugin. **Start from `packages/workspace`, not `packages/nx`.** The shared machinery a sibling package needs already exists — read these first and reuse them as-is: - `tools/vitest/setup.mts` — the port of `scripts/unit-test-setup.js`; every migrated package loads it as its `setupFiles` - `tools/vitest/nx-source-resolver.mts` — resolves `nx` / `@nx/*` to this repo's source, for both vite and node - `tools/vitest/tsconfig.json` — a leaf tsconfig whose only job is to stop vite's tsconfig lookup. **Do not move these files to the workspace root.** With no tsconfig beside them, the nearest one is the root solution file, and vite walks its `references` — reading all ~114 project tsconfigs on every run, which lands as a sandbox violation. `tools/vitest` is deliberately a plain directory, not an Nx project: adding `project.json` makes `@nx/js:typescript-sync` demand a project reference to a test-only tool from each consuming package's _published_ `tsconfig.lib.json` (`composite: false` does not suppress it) - `packages/workspace/vitest.config.mts` — the config those two plug into - `packages/workspace/project.json` — `test.inputs` naming the shared scripts `packages/nx` (PR #36754, commit `32dd3fb533`) is the _original_ migration but a poor template: it is the one package that imports almost no siblings, so it needs neither the source resolver nor the CJS-channel mocks. Consult it only for `vitest-write-guard.cjs` and `src/internal-testing-utils/cjs-mock.ts`. `packages/angular-rspack/vitest.config.mts` is the simple end of the spectrum (no nx source at all). ## Argument The package name (e.g. `js`, `devkit`, `workspace`). The package lives at `packages/<name>/`. ## Why this is not a find-and-replace Jest and Vitest disagree on module semantics, not just API names. The mechanical `jest.*` → `vi.*` rename is maybe 80% of the diff and 20% of the work. The rest is: which _channel_ a mock reaches (ESM graph vs CJS `require()`), whether a namespace is frozen, and what `resetAllMocks` does to a spy. Budget for hand-fixing specs after the codemod. --- ## Step 0 — Survey the package Run these and write the answers into `tmp/notes/vitest-migration-<name>.md` before touching anything: ```bash ls packages/<name>/jest.config.cts packages/<name>/jest*.js 2>/dev/null cat packages/<name>/jest.config.cts cat packages/<name>/tsconfig.spec.json grep -rl "\.spec\.ts" -c packages/<name>/src | wc -l # rough spec count pnpm nx show project <name> --json | head -40 ``` Capture: 1. **Spec count and current runtime.** Run `pnpm nx test <name> --skip-nx-cache` once and record the reported test count and wall time. That number is the parity target in Step 6 — you cannot verify the migration without it. 2. **Jest config specials** — anything beyond `displayName`/`preset`/ `moduleFileExtensions` is behavior you must reproduce: - `setupFiles` (e.g. `packages/devkit/jest-setup-nx-workspace-data-dir.js`) - `moduleNameMapper` (path shims; also `identity-obj-proxy` for CSS) - `testEnvironment: 'jsdom'` → needs `environment: 'jsdom'` and the `jsdom` dep - `modulePathIgnorePatterns` / `testPathIgnorePatterns` → `exclude` - `resolver` → `resolve.conditions` (see Step 2) 3. **Inherited preset behavior** (`jest.preset.js`) that Vitest does _not_ get for free: - `setupFiles: ['../../scripts/unit-test-setup.js']` — the workspace-wide project-graph / workspace-context / native guards. **This must be ported** (Step 3). - `resolver: '../../scripts/patched-jest-resolver.js'` — maps `@nx/*` and `nx/*` onto `packages/*` source, **and** sets `NX_WORKSPACE_ROOT_PATH=<repo>/tmp/unit` as a side effect. Both are reproduced by the shared scripts (Steps 2 and 3). - `moduleNameMapper` ESM shims (`@clack/prompts`, `ora`, `chalk`, `yargs-parser`, `prettier`, `magic-string`, `oxfmt`). Most are pure ESM interop Vitest does not need — but check each for _behavior_ before dropping it. `@clack/prompts` is load-bearing: the stub answers `undefined` where the real library drives a **synchronous** prompt, and a generator that asks a question blocks the worker forever with no test timeout. `tools/vitest/setup.mts` already keeps that one. `prettier`'s stub also pins `resolveConfig: () => null`, which matters if the package snapshots formatted output. - `maxWorkers: 1` — Vitest runs files in parallel. Any spec relying on cross-file ordering or a shared mutable temp dir will now fail. This is the main source of "it passed under jest" flakes. 4. **Native bindings** — does the package load `nx/src/native` or a `.node` file? If yes you need `pool: 'forks'` and the native shim plugin from `packages/nx/vitest.config.mts`. 5. **Lazy `require()` of TS source** — `grep -rn "require(" packages/<name>/src --include=*.ts | grep -v "^.*spec"`. Every bare `require()` of a local `.ts` file needs `@swc-node/register` (Step 2) and can only be mocked through `mockCjsModule` (Step 4). --- ## Step 1 — Target inference `@nx/vitest` is already registered in `nx.json` for `packages/**/*`, so a `vitest.config.mts` at the package root is enough to infer `<name>:test`. Verify the plugin block still reads: ```json { "plugin": "@nx/vitest", "options": { "testTargetName": "test" }, "include": ["packages/**/*"], "exclude": ["**/out-tsc/**"] } ``` `@nx/jest` infers `test` from `jest.config.*` presence. **Both plugins would claim `test`**, so `jest.config.cts` must be deleted in the same change, not left behind "just in case". Also delete any `jest-resolver.js` and drop `project.json` target overrides that reference jest inputs (see the `packages/nx` diff — a `"test": { "inputs": [..., "patched-jest-resolver.js"] }` block was removed). --- ## Step 2 — Write `packages/<name>/vitest.config.mts` Start from `packages/nx/vitest.config.mts` and keep only what the survey justified. The load-bearing pieces and why: ```ts export default defineConfig({ root: import.meta.dirname, cacheDir: '../../node_modules/.vite/<name>/unit', test: { watch: false, globals: true, // specs use bare describe/it/expect/vi environment: 'node', // or 'jsdom' if the jest config said so include: ['**/*.spec.ts'], exclude: ['**/node_modules/**'], setupFiles: ['./vitest.setup.mts'], testTimeout: 35000, // matches jest.preset.js pool: 'forks', // ONLY if native .node bindings are loaded; // they are not thread-safe across workers teardownTimeout: 60_000, // specs holding native contexts exit slowly; // the jest setup hid this behind --forceExit execArgv: ['--conditions=@nx/nx-source'], server: { deps: { external: [/\.node$/] } }, }, resolve: { conditions: ['@nx/nx-source'], }, plugins: [nxSourceResolver()], // tools/vitest/nx-source-resolver.mts }); ``` Rules for resolution — the part that most looks solved and isn't: - **`conditions: ['@nx/nx-source']` does NOT replace the jest resolver.** `node_modules/nx` and `node_modules/@nx/*` are the _published_ tarballs (dist only, no source), and their exports maps advertise `@nx/nx-source` entries pointing at `./src/index.ts` files the tarball does not ship — so the condition resolves to a file that isn't there. Use `nxSourceResolver()` from `tools/vitest/nx-source-resolver.mts`, which maps `nx` / `@nx/*` through the _local_ `packages/<pkg>/package.json`, with a file fallback for deep imports no exports entry covers (`@nx/workspace/src/...`). - **`execArgv: ['--conditions=@nx/nx-source']` on its own actively breaks node resolution**, for the same reason: a lazy `require('@nx/js')` dies with `Cannot find module '.../node_modules/@nx/js/src/index.ts'`. Keep the flag, but `tools/vitest/setup.mts` must also patch `Module._resolveFilename` with the same mapping so both channels agree. - **Aliases use regex, not strings.** Vite string aliases do prefix matching, so `'@nx/devkit'` would rewrite `@nx/devkit/internal` too. Use `{ find: /^@nx\/devkit$/, replacement: ... }`. - `packages/nx` predates the shared resolver and hard-codes `nx/src/*` and `nx/bin/*` aliases instead. Don't copy that — the resolver covers it. - If the package imports `yargs` with CJS-namespace style (`yargs.terminalWidth()`), alias it to `node_modules/yargs/index.cjs`. - If the package loads `nx/src/native`, copy the `nx-native-shim` plugin verbatim — `src/native/index.js` requires TS files and cannot run outside a transform, so it must be routed to the generated `native-bindings.js` and externalized. --- ## Step 3 — Wire up the shared setup Point the config at the shared file; do not write a per-package copy: ```ts setupFiles: ['../../tools/vitest/setup.mts'], ``` `tools/vitest/setup.mts` is the port of `scripts/unit-test-setup.js` (which is jest-only — `jest.doMock` — so it can never be imported from vitest). Read it before assuming anything is missing; it already does all of the following, and each line is there because its absence broke `packages/workspace`: - `NX_DAEMON=false`, `npm_config_user_agent` deleted, `FORCE_COLOR` deleted and `NO_COLOR=1` (snapshots are recorded colorless). - `NX_WORKSPACE_ROOT_PATH` under `tmp/unit/<pid>` — **per worker process**, unlike jest. The jest resolver set a single `tmp/unit` as a side effect; with vitest's parallel workers one shared root makes every worker queue on the same lock ("Waiting for graph construction in another process to complete", 35s timeouts). - `NX_ISOLATE_PLUGINS=false`. Otherwise plugin isolation spawns a worker subprocess per plugin that is never torn down, and the spec file stalls to its timeout. Two `packages/nx` specs already carry this same note. - `@swc-node/register`, with `Error.prepareStackTrace` **restored immediately after**: the hook installs source-map-support, which mis-maps vite-transformed frames and breaks error locations _and_ inline-snapshot updates. - `Module._resolveFilename` patched with the source mapping (Step 2), plus `@clack/prompts` → `scripts/jest-mocks/clack-prompts.js`. - `vi.doMock` graph/workspace-context/native guards, keyed by **absolute physical path** — mocking the `nx/src/...` specifier routes through the pnpm symlink and keys as a different module, so the mock silently never applies. - **The same graph mocks again, on the CJS channel**, via a `Module._load` patch. This is the one most easily missed and the most expensive to debug: generators reach graph builders through lazy `require()`, which `vi.mock` cannot see, and the unmocked `createProjectGraphAsync` takes `project-graph.lock` and **deadlocks the worker** — no output, and no test timeout fires, because the main thread is blocked in a futex. - Pass-through helpers are plain functions, not `vi.fn()`, so a suite's `vi.resetAllMocks()` cannot wipe them into `() => undefined`. Add to the shared file (not a package-local one) if the package needs a guard nothing else does, and say so in the PR — every migrated package loads it. Two more rules: 1. **Do not alias a `jest` global in the setup.** A stray `jest.mock` would not be hoisted by vitest's transform and would silently fail to intercept. Let it throw. 2. If the package's specs can write repo files, copy `packages/nx/vitest-write-guard.cjs` and load it through `execArgv: ['--require', ...]`. It must be `execArgv`, not `setupFiles`: node snapshots a module's ESM named exports on first import, so a patch applied from a setup file is invisible to `import { writeFile } from 'fs'`. (The `packages/nx` migration found a spec that had been overwriting the repo's real `nx.json`.) Finally, name the shared files in the package's `project.json` so the cache sees them — they live outside `{projectRoot}`, so nothing else invalidates on an edit: ```json "test": { "inputs": [ "...", "{workspaceRoot}/tools/vitest/**/*", "{workspaceRoot}/scripts/jest-mocks/clack-prompts.js" ] } ``` These are not optional bookkeeping. `@nx/vitest` infers `setup.mts` and its tsconfig (nx#36920), but nothing infers the resolver the config imports or the clack mock the setup loads by path — `default` is project-scoped — so leaving them off does not fail loudly; it serves a **stale cache hit** the next time someone edits them. Keep the whole `tools/vitest/**/*` glob rather than naming the resolver alone, so a helper added there later is covered too. --- ## Step 4 — `tsconfig.spec.json` ```jsonc { "compilerOptions": { "types": ["vitest/globals", "node"], // was ["jest", "node"] }, "include": [ // ... "vitest.config.mts", // replaces "jest.config.ts" "vitest.setup.mts", ], } ``` Drop `@types/jest` from the package's `devDependencies` only if no other project in the repo still needs it there. --- ## Step 5 — Codemod the specs Apply mechanically, then hand-fix. Prefer one script over 200 manual edits, and commit the codemod pass separately from the hand fixes so review can follow. Two rules before you run anything: - **Never codemod the whole package blindly.** Some files contain `jest.*` in _strings_, not calls — a spec for a codemod that rewrites `jest.mock(...)`
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub