Agent skill

Vitest Migration

by nrwl in nrwl/nx

Migrate an Nx repo package's unit tests from Jest to Vitest, reusing the shared setup that packages/workspace established.

MITAuto-check passedTesting & QA

Install Vitest Migration

skills CLI
$ npx skills add nrwl/nx --skill vitest-migration -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install nrwl/nx vitest-migration --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/nrwl/nx.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/vitest-migration .claude/skills/vitest-migration && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
vitest-migration
GitHub stars
29k
Token cost
~7.3k tokens
SKILL.md length
2,980 words
Files
1
Skills in repo
21
Repo updated
First seen
Licence
MIT

At a glance

Migrate an Nx repo package's unit tests from Jest to Vitest, reusing the shared setup that packages/workspace established.

  • Works in 10 steps: Survey the package → Target inference → Write packages//vitest.config.mts → …
  • Asked to move <pkg to vitest
  • SKILL.md covers Argument, Why this is not a…, Step 0 — Survey the package and Step 1 — Target inference, plus 10 more sections
  • Calls pnpm, npx and nx

What it does

Vitest Migration is an agent skill from nrwl/nx. 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".

Its SKILL.md is about 7.3k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Testing & QA, covering Unit testing. It works with Vitest, Jest and TypeScript. The repository describes itself as: The Monorepo Platform that amplifies both developers and AI agents. Nx optimizes your builds, scales your CI, and fixes failed PRs automatically. Ship in half the time. The licence is MIT.

When your agent uses it

  • Asked to move <pkg to vitest
  • Migrate <pkg tests off jest
  • Run <pkg unit tests with vitest

Example prompts

  • “move <pkg to vitest”
  • “migrate <pkg tests off jest”
  • “run <pkg unit tests with vitest”
  • “/vitest-migration”

Requirements

  • Node.js
  • Pre-approved tools (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 *)

Workflow steps

10 steps, taken from the step headings in SKILL.md.

  1. Survey the package
  2. Target inference
  3. Write packages//vitest.config.mts
  4. Wire up the shared setup
  5. tsconfig.spec.json
  6. Codemod the specs
  7. Hand-fix the semantic gaps
  8. Snapshots
  9. Verify
  10. Clean up and document

What it can do on your machine

Read from SKILL.md and the folder at commit db71d69. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves these tools, so the agent can use them without asking each time:

    • Read
    • Glob
    • Grep
    • Agent
    • Edit(*)
    • Write(*)
    • Bash(pnpm nx *)
    • Bash(npx nx *)
    • Bash(nx *)
    • Bash(git *)

    …and 17 more on the same allowed-tools line.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • pnpm
    • npx
    • nx
    • git
    • jq

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md. Its commands use pnpm, npx and git, which can reach the network depending on how they are called.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Vitest Migration loads about 7.3k tokens when it runs. Until then it costs about 63 tokens; SKILL.md has 2,980 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~63
When it runs · the whole SKILL.md, loaded when a task matches
~7.3k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from nrwl/nx at commit db71d69, republished under its MIT licence (© nrwl). 2,980 words, ~7,335 tokens.

Download SKILL.mdSave it as .claude/skills/vitest-migration/SKILL.md (or your agent's skills folder).
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(...) text, generator specs asserting on jest.config.cts contents, or '@nx/jest:jest' executor names. Build the file list from a grep for real API usage (grep -l 'jest\.[a-z]' | grep -v the string-only ones) and eyeball it.
  • Match across newlines. jest\n .fn() and jest\n .spyOn(...) are common in this repo and a line-based s/jest\.fn(/vi.fn(/ silently misses them, leaving ReferenceError: jest is not defined at collection. Use perl -0p (or equivalent) and re-grep for a bare \bjest\b afterwards.
JestVitestNote
jest.fn / jest.spyOn / jest.mock / jest.doMock / jest.unmock / jest.clearAllMocks / jest.restoreAllMocks / jest.resetModules / jest.mockedsame with vi.pure rename
jest.requireActual<T>(x)await vi.importActual<T>(x)factory must become async
jest.requireMock(x)await vi.importMock(x)factory must become async
jest.isolateModules(() => { require(x) })vi.resetModules() + await import(x)for CJS-loaded modules use delete cjsRequire.cache[cjsRequire.resolve(x)]; cjsRequire(x)
jest.isolateModulesAsync(async () => …)vi.resetModules() then fresh await import()s
jest.Mockimport type { Mock } from 'vitest'type-only import
jest.SpyInstanceimport type { MockInstance } from 'vitest'type-only import
jest.MockedFunctionimport type { MockedFunction } from 'vitest'type-only import
it('x', (done) => …)return a promiseVitest has no done callback
xdescribe / xitdescribe.skip / it.skipnot defined in Vitest
import { jest } from '@jest/globals'deletevi is global with globals: true

Hoisting is real in Vitest: vi.mock calls are lifted to the top of the file, above imports and above any const the factory closes over. Anything a factory needs must go through vi.hoisted(() => …).


Show full SKILL.md (1,276 more words)Show less

Step 6 — Hand-fix the semantic gaps

This is where the time goes. The catalogue, from the packages/nx migration:

Frozen ESM namespaces. vi.spyOn(semverNamespace, 'gt') throws on a node builtin or an external ESM package — the namespace object is frozen. Mock at the module level in spy mode, which keeps the real implementations until a test overrides one:

ts
vi.mock('semver', { spy: true });
vi.mock('child_process', { spy: true });

Modules the source loads with bare require(). vi.mock never sees that channel. Use the helper (add it if the package does not have one — it lives in packages/nx/src/internal-testing-utils/cjs-mock.ts and patches Module._load):

ts
import { mockCjsModule } from '<path>/internal-testing-utils/cjs-mock';
mockCjsModule(import.meta.url, './run', { runCommand: vi.fn() });

Registrations are per-file (Vitest forks per file), but a swap made for a single test must be undone with unmockCjsModule / resetCjsMocks or it leaks into later tests in the same file.

Class mocks must be constructible. vi.fn() returning an object is not new-able the way jest's auto-mock was. Return a real function with a prototype, as GuardedWorkspaceContext does in vitest.setup.mts.

vi.resetAllMocks() restores a spy's real implementation rather than leaving () => undefined like jest. Specs that relied on the jest behavior (expecting undefined after a reset) need explicit mockReturnValue(undefined).

Setup-file mocks must not use vi.fn() for pass-through helpers. A spec calling vi.resetAllMocks() would wipe the implementation and surface as "is not iterable" downstream. Use plain functions, as the workspace-context mock does.

Hooks that must not return a mock. beforeEach(() => vi.fn()) — Vitest treats a returned function as a teardown callback. Make the body a block.

Parallelism. With maxWorkers: 1 gone, two spec files sharing a temp dir, a process.chdir, or a module-level singleton will now collide. Fix by giving each file its own TempFs root; reach for test.sequential/isolate: false only after proving the collision is not the spec's own bug.

A spec that hangs with no output and no timeout. The test timeout cannot fire, because the worker's main thread is blocked in a futex — so the usual "which test is slow" reflexes give you nothing. Diagnose it from the outside:

bash
p=$(pgrep -f "workers/forks" | head -1)
cat /proc/$p/wchan                      # futex_do_wait == blocked, not busy
ps -o pcpu= -p $p                       # ~0% confirms it is not just slow
ls -l /proc/$p/fd | grep -v socket      # the lock file it is stuck on
pgrep -aP $p                            # a spawned worker/install it waits for

In packages/workspace this was project-graph.lock: real graph construction running on the CJS channel. The three causes seen so far are all handled by tools/vitest/setup.mts — CJS graph mocks, NX_ISOLATE_PLUGINS=false, and the @clack/prompts stub — so first check the setup is actually loaded before hunting further.

Watch for latent test bugs. Both spec bugs the packages/nx migration uncovered were assertions that passed while the mock never applied. If a spec starts failing after the mock finally lands, the test was wrong — fix the expectation, do not paper over it by restoring the broken mock.


Step 7 — Snapshots

Vitest joins describe and test names with > where jest used a space, so every existing key reads as new: a plain run appends a full second copy of the file and leaves the jest entries orphaned. Regenerate from a pristine tree so -u also drops the old keys:

bash
git checkout -- 'packages/<name>/**/__snapshots__/*.snap'
cp -r <snapshots> tmp/snapshot-baseline/          # keep the jest originals
pnpm nx test <name> --skip-nx-cache -- -u

Then prove the values did not move: parse both sides into {key: value}, normalize the separator (' > ' → ' '), and diff. Key counts and every value must match — that is the real parity check, not the pass/fail.

toThrowErrorMatchingInlineSnapshot is the known exception: vitest records [Error: msg] where jest recorded "msg". Same message, different serializer.

Vitest's serializer differs from Jest's elsewhere too. Regenerate, then read the diff:

bash
pnpm nx test <name> --skip-nx-cache -- -u
git diff --stat -- 'packages/<name>/**/__snapshots__/*'

Snapshot churn should be formatting only (quoting, indentation, Object { → {). Any change in content is a real behavior difference — investigate it before accepting. Colorless output is guaranteed by the NO_COLOR pin in the setup file; if you see ANSI codes land in a snapshot, that pin is missing.


Step 8 — Verify

bash
# same test count as Step 0, and it should be dramatically faster
pnpm nx test <name> --skip-nx-cache

# parallel-safety: repeat runs must be stable, not just green once
pnpm nx test <name> --skip-nx-cache
pnpm nx test <name> --skip-nx-cache

# a single file still works (paths relative to the package root)
pnpm nx run <name>:test -- src/utils/some-file.spec.ts

# nothing else broke - EVERY target the project has, not a set you picked
pnpm nx show project <name> --json | jq '.targets | keys'
pnpm nx run-many -t test,build,lint,oxlint -p <name> --skip-nx-cache
pnpm nx sync:check

oxlint is a separate target from lint. Running test,build,lint and calling it green is how a restricted-import error reaches CI: this repo bans nx/src/... imports in favour of @nx/devkit/internal*, and only oxlint catches it. Read the target list rather than assuming the usual three.

Parity is test count, not just a green run. A dropped include pattern or a silently-skipped directory shows up as a lower count, and a green suite hides it. If the count differs, find every missing file before proceeding.

Note the caching caveat: after a mechanical sweep, nx affected can replay a stale cached pass. Always validate with --skip-nx-cache.

Do not run a full nx affected: a new or moved file under a workspace-root directory marks all ~90 projects affected, which is hours of jest for changes that touch nothing jest reads. Run one still-on-jest package as a canary instead — devkit is the most entangled.


Step 8b — Sandbox violations

The migration is not done when CI is green. Nx Cloud reports the task's file reads against its declared inputs, and a vitest suite reads things the jest one did not. Fetch them once the PR has run:

bash
npx nx-cloud get sandbox-reports --branch <PR-number> --since 1d
npx nx-cloud validate sandbox-violations \
  .nx/workspace-data/sandbox-reports/<PR-number>/index.json --json

nx reset deletes .nx/workspace-data, and the downloaded reports with it — re-download after one.

The per-task JSON carries processTree plus a pid on every read, which is how you attribute a violation instead of guessing. Map them:

python
tree = {p['pid']: p for p in report['processTree']}
for r in report['unexpectedReads']:
    print(r['path'], tree.get(r['pid'], {}).get('cmd'))

The two this migration produced, both worth checking for:

  • Every project's tsconfig.json, read by the vitest main process. The timestamps show the root solution tsconfig read milliseconds after a workspace-root source file. Cause and fix are in the Step 3 note about where the shared setup lives. Confirm with the resolver vite itself uses rather than a filesystem tracer — tsconfck's parse() reports what it consulted, and a tracer on fs misses it because the ESM node:fs/promises bindings are snapshotted before a --require preload can patch them:

    js
    const { parse } =
      await import('<repo>/node_modules/.pnpm/tsconfck@*/node_modules/tsconfck/src/index.js');
    console.log((await parse('tools/vitest/setup.mts')).referenced?.length); // 0 == leaf, 114 == solution root
  • The repo's .editorconfig, read by a worker. formatFiles resolves prettier config from disk at tree.root, and a spec that points tree.root into the repo (e.g. process.cwd()) picks it up; the jest prettier shim pinned resolveConfig: () => null and hid it. Give that spec a TempFs root instead of declaring the file as an input.

Prefer declaring an input over excluding a path: an over-broad input costs cache misses, an over-broad exclusion buys wrong cache hits. But a violation that only exists because a file sits in the wrong place is a layout bug — fix the layout. Declaring 114 tsconfigs as inputs would have been "correct" and would have quietly wrecked the cache for every vitest suite in the repo.


Step 9 — Clean up and document

  • Delete packages/<name>/jest.config.cts and any package-local jest resolver or setup file whose behavior you ported.
  • If this was the last jest project touching a scripts/jest-mocks/* shim or a branch of scripts/unit-test-setup.js, delete it. If not, leave it and say so in the PR body — the packages/nx PR explicitly deferred the dead scripts/unit-test-setup.js branches to a follow-up rather than mixing them in.
  • Update CONTRIBUTING.md — it documents npx jest <path> for targeting a single test. Add the package to the vitest note next to packages/nx.
  • Format: npx oxfmt <changed files> (check the branch's own check target first — a feature branch may still run pretty-quick).
  • Do not run a full nx affected: a new file under scripts/ marks all 90+ projects affected, which is hours of jest. Nothing jest reads has changed (jest.preset.js, scripts/unit-test-setup.js, scripts/patched-jest-resolver.js are untouched), so run one still-on-jest package as a canary instead — devkit is the most entangled.
  • Write tmp/notes/vitest-migration-<name>.md: before/after test count and wall time, the list of hand-fixed specs and why, and anything deferred.

Commit shape

Follow the reference PR — small, reviewable, conventional-commit slices:

  1. chore(<scope>): add vitest config and setup for <name> unit tests
  2. chore(<scope>): codemod jest.* to vi.* in <name> specs
  3. one commit per class of hand fix (CJS-channel mocks, frozen namespaces, constructor mocks, hook cleanup, …)
  4. chore(<scope>): regen <name> snapshots for vitest
  5. chore(<scope>): remove <name> jest config

PR body: fill the template, state the before/after test count and wall time, list what the vitest config reproduces from the jest setup, and call out any latent test bug the migration exposed.

© nrwl, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/vitest-migration of nrwl/nx.

Open the folder on GitHubat commit db71d69

Compare with similar skills

Vitest Migration next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Vitest Migration compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Vitest Migration this skillnrwl/nx29k—~7.3kAutomated safety check: PassMIT
Vitest Skillsickn33/agentic-awesome-skills47k1 repos~1.2kAutomated safety check: PassMIT
Code Testing Agentmicrosoft/testfx1k—~2.7kAutomated safety check: PassMIT
Test Taggingmicrosoft/testfx1k—~4.3kAutomated safety check: PassMIT
Vitestjezweb/claude-skills1.1k—~3kAutomated safety check: PassMIT
Test CommanderEliasOulkadi/shokunin114—~3kAutomated safety check: NotesMIT

Similar skills

  • Vitest Skill

    sickn33/agentic-awesome-skills

    Generates Vitest tests in JavaScript/TypeScript with Vite-native speed.

    47k GitHub starsUsed in 1 repo~1.2k tokens
    Testing & QAAuto-check passed
  • Code Testing Agent

    microsoft/testfx

    Official

    Generates and writes new unit tests for any programming language — scaffolds .NET test projects, pytest suites, Vitest/Jest suites, Go test files, and JUnit suites, and configures coverage tooling…

    1k GitHub stars~2.7k tokensUpdated today
    Testing & QAAuto-check passed
  • Test Tagging

    microsoft/testfx

    Official

    Analyzes test suites in any language and tags each test with a standardized set of traits (positive, negative, critical-path, boundary, smoke, regression, integration, performance, security).

    1k GitHub stars~4.3k tokensUpdated today
    Testing & QAAuto-check passed
  • Vitest

    jezweb/claude-skills

    Set up Vitest testing in any project — detects type (Cloudflare Workers, React, Node, library), generates vitest.config.ts, test setup, utilities, and a sample test.

    1.1k GitHub stars~3k tokensUpdated 3 days ago
    Testing & QAAuto-check passed
  • Test Commander

    EliasOulkadi/shokunin

    Generate unit, integration, E2E, and visual regression tests following the Testing Trophy methodology (80% integration).

    114 GitHub stars~3k tokensUpdated 4 days ago
    Testing & QAAuto-check: notes
  • Testing Unit

    yonatangross/orchestkit

    Unit testing patterns for isolated business logic tests — AAA pattern, parametrized tests (test.each, @pytest.mark.parametrize), fixture scoping (function/module/session), mocking with MSW/VCR at…

    290 GitHub stars~2.4k tokensUpdated today
    Testing & QAAuto-check passed
  • Monitor CI

    nrwl/nx

    Monitor Nx Cloud CI pipeline and handle self-healing fixes. An agent skill from nrwl/nx.

    29k GitHub starsUsed in 6 repos~4.7k tokens
    Auto-check passed
  • Nx Import

    nrwl/nx

    Import, merge, or combine repositories into an Nx workspace using nx import.

    29k GitHub starsUsed in 6 repos~3.5k tokens
    Auto-check passed
  • Run Nx generators with prioritization for workspace-plugin generators.

    29k GitHub starsUsed in 2 repos~592 tokens
    Auto-check: notes
  • Author or scope a first-party Nx migration. An agent skill from nrwl/nx.

    29k GitHub stars~12k tokensUpdated today
    Auto-check: notes
  • Generate code using nx generators. An agent skill from nrwl/nx.

    29k GitHub starsUsed in 1 repo~2.2k tokens
    Auto-check passed
  • Check modified Nx documentation pages against the astro-docs style guide.

    29k GitHub stars~1.3k tokensUpdated today
    Auto-check passed

Categories

Questions about Vitest Migration

What does Vitest Migration do?

Migrate an Nx repo package's unit tests from Jest to Vitest, reusing the shared setup that packages/workspace established. Vitest Migration is an agent skill from nrwl/nx. Migrate an Nx repo package's unit tests from Jest to Vitest, reusing the shared setup that packages/workspace established.

When should I use Vitest Migration?

Vitest Migration fits situations like: asked to move <pkg to vitest; migrate <pkg tests off jest; run <pkg unit tests with vitest.

How do I install Vitest Migration in Claude Code?

Run `npx skills add nrwl/nx --skill vitest-migration -a claude-code`. Or copy the skill folder (.claude/skills/vitest-migration in nrwl/nx) into .claude/skills/vitest-migration in your project. Claude Code loads it when a task matches its description.

How do I install Vitest Migration in Codex?

Run `npx skills add nrwl/nx --skill vitest-migration -a codex`. Or copy the skill folder (.claude/skills/vitest-migration in nrwl/nx) into .agents/skills/vitest-migration in your project. Codex loads it when a task matches its description.

Can I use Vitest Migration in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add nrwl/nx --skill vitest-migration -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/vitest-migration, .gemini/skills/vitest-migration, .github/skills/vitest-migration and .opencode/skills/vitest-migration in your project.

What does Vitest Migration need to run?

Going by SKILL.md and its folder, Vitest Migration needs the command-line tools its instructions call (pnpm, npx, nx, git and jq). Our summary lists: Node.js. Its frontmatter pre-approves these 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 *).

Does Vitest Migration access the network?

SKILL.md contains no URLs. Its commands use npx and git, which can reach the network depending on how they are called. This is read from the text; nothing was executed.

Is Vitest Migration safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Vitest Migration use?

Vitest Migration is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Vitest Migration use?

About 7.3k tokens (SKILL.md is roughly 29k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Vitest Migration?

Skills that share tags, products or a category with Vitest Migration: Vitest Skill (sickn33/agentic-awesome-skills, 47k stars), Code Testing Agent (microsoft/testfx, 1k stars), Test Tagging (microsoft/testfx, 1k stars) and Vitest (jezweb/claude-skills, 1.1k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Vitest Migration?

nrwl (a GitHub organization) maintains it in nrwl/nx, which has 29,399 GitHub stars. The repository holds 21 skills in this directory. The repository was last updated on October 9, 2026.

Source: nrwl/nx on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.