Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2943ccf
feat(experimental): allow disabling the module runner
sheremet-va Dec 8, 2025
085be59
fix: throw an error in vmForks/vmThreads
sheremet-va Dec 8, 2025
c25402a
feat: implement nativeModuleRunner
sheremet-va Dec 8, 2025
318eb6b
chore: cleanup types
sheremet-va Dec 8, 2025
0611b57
fix: watcher respects non-client/ssr environments
sheremet-va Dec 8, 2025
3e4caa2
feat: support watch mode
sheremet-va Dec 8, 2025
4e5464b
fix: support module.register
sheremet-va Dec 8, 2025
3c40971
feat: support import.meta.vitest
sheremet-va Dec 8, 2025
c047490
chore: cli-config
sheremet-va Dec 8, 2025
b83f29c
chore: add sample project
sheremet-va Dec 9, 2025
3fd5618
chore: cleanup
sheremet-va Dec 9, 2025
e4b0d2e
chore: cleanup
sheremet-va Dec 9, 2025
fe86182
feat: support automock, autospy and redirect mock types
sheremet-va Dec 9, 2025
3f7e2b3
fix: show module as external in UI
sheremet-va Dec 9, 2025
2273518
fix: run setup file for every test, execute in-source tests as separa…
sheremet-va Dec 9, 2025
6c44609
feat: first implementation of factory mocking
sheremet-va Dec 10, 2025
4091d0d
chore: collect
sheremet-va Dec 12, 2025
0926f28
chore: refactor native module mocker
sheremet-va Dec 12, 2025
f76caa0
refactor: cleanup exports collection
sheremet-va Dec 12, 2025
d6f72bd
fix: support importActual and recursive factory
sheremet-va Dec 12, 2025
086c47c
fix: support vi.importMock
sheremet-va Dec 12, 2025
06a351e
fix: support mocking deps
sheremet-va Dec 12, 2025
8b4f29b
fix(mocker): support top level import if dependency is not circular
sheremet-va Dec 15, 2025
f50aa81
fix: allow `export *` when automocking
sheremet-va Dec 15, 2025
a4e603e
test: add more tests
sheremet-va Dec 15, 2025
d458c67
fix: update loading errors
sheremet-va Dec 15, 2025
34f1bb3
Merge branch 'main' of github.com:vitest-dev/vitest into 12-08-feat_e…
sheremet-va Dec 15, 2025
d3db1bf
chore: cleanup
sheremet-va Dec 15, 2025
b653ee3
chore: cleanup
sheremet-va Dec 15, 2025
0ab2dab
fix: override is false by default
sheremet-va Dec 16, 2025
69ca0d8
chore: cleanup
sheremet-va Dec 16, 2025
4786dd1
refactor: move the example to test/
sheremet-va Dec 16, 2025
5bb68aa
docs: cleanup
sheremet-va Dec 16, 2025
74a84c4
chore: add try/catch
sheremet-va Dec 17, 2025
c81d363
Merge branch 'main' of github.com:vitest-dev/vitest into 12-08-feat_e…
sheremet-va Dec 17, 2025
f7def15
fix: listen for unhandled errors in vm pool
sheremet-va Dec 17, 2025
3f96c63
fix(windows): support circular manual mock
sheremet-va Dec 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions docs/config/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,126 @@ Note that if the file path is too long, Vitest will truncate it at the start unt
::: info
[Vitest UI](/guide/ui#import-breakdown) shows a breakdown of imports automatically if at least one file took longer than 500 milliseconds to load. You can manually set this option to `false` to disable this.
:::

## experimental.viteModuleRunner <Version type="experimental">4.0.16</Version> {#experimental-vitemodulerunner}

- **Type:** `boolean`
- **Default:** `true`

Controls whether Vitest uses Vite's [module runner](https://vite.dev/guide/api-environment-runtimes#modulerunner) to run the code or fallback to the native `import`.

If this option is defined in the root config, all [projects](/guide/projects) will inherit it automatically.

We recommend disabling the module runner if you are running tests in the same environment as your code (server backend or simple scripts, for example). However, we still recommend running `jsdom`/`happy-dom` tests with the module runner or in [the browser](/guide/browser/) as it doesn't require any additional configuration.

Disabling this flag will disable _all_ file transforms:

- test files and your source code are not processed by Vite
- your global setup files are not processed
- your custom runner/pool/environment files are not processed
- your config file is still processed by Vite (this happens before Vitest knows the `viteModuleRunner` flag)

::: warning
At the moment, Vitest still requires Vite for certain functionality like the module graph or watch mode.
:::

### Module Runner

By default, Vitest runs tests in a very permissive module runner sandbox powered by Vite's [Environment API](https://vite.dev/guide/api-environment.html#environment-api). Every file is categorized as either an "inline" module or an "external" module.

Module runner runs all "inline" modules. It provides `import.meta.env`, `require`, `__dirname`, `__filename`, static `import`, and has its own module resolution mechanism. This makes it very easy to run code when you don't want to configure the environment and just need to test that the bare JavaScript logic you wrote works as intended.

All "external" modules run in native mode, meaning they are executed outside of the module runner sandbox. If you are running tests in Node.js, these files are imported with the native `import` keyword and processed by Node.js directly.

While running JSDOM/happy-dom tests in a permissive fake environment might be justified, running Node.js tests in a non-Node.js environment is counter-productive as it can hide and silence potential errors you may encounter in production, especially if your code doesn't require any additional transformations provided by Vite plugins.

### Limitations

Some Vitest features rely on files being transformed. Vitest uses synchronous [Node.js Loaders API](https://nodejs.org/api/module.html#customization-hooks) to transform certain files to support these features:

- [`import.meta.vitest`](/guide/in-source)
- [`vi.mock`](/api/vi#vi-mock)
- [`vi.hoisted`](/api/vi#vi-hoisted)

::: warning
This means that Vitest requires at least Node 22.15 for those features to work. At the moment, they also do not work in Deno or Bun.
:::

This could affect performance because Vitest needs to read the file and process it. If you do not use these features, you can disable the transforms by setting `experimental.nodeLoader` to `false`. Vitest only reads test files and setup files while looking for `vi.mock` or `vi.hoisted`. Using these in other files won't hoist them to the top of the file and can lead to unexpected results.

Some features will not work due to the nature of `viteModuleRunner`, including:

- no `import.meta.env`: `import.meta.env` is a Vite feature, use `process.env` instead
- no `plugins`: plugins are not applied because there is no transformation phase
- no `alias`: aliases are not applied because there is no transformation phase

With regards to mocking, it is also important to point out that ES modules do not support property override. This means that code like this won't work anymore:

```ts
import * as module from './some-module.js'
import { vi } from 'vitest'

vi.spyOn(module, 'function').mockImplementation(() => 42)
```

However, Vitest supports autospying on modules without overriding their implementation. When `vi.mock` is called with a `spy: true` argument, the module is mocked in a way that preserves original implementations, but all exported functions are wrapped in a `vi.fn()` spy:

```ts
import * as module from './some-module.js'
import { vi } from 'vitest'

vi.mock('./some-module.js', { spy: true })

module.function.mockImplementation(() => 42)
```

### TypeScript

If you are using Node.js 22.18/23.6 or higher, TypeScript will be [transformed natively](https://nodejs.org/en/learn/typescript/run-natively) by Node.js.

::: warning TypeScript with Node.js 22.6-22.18
If you are using Node.js version between 22.6 and 22.18, you can also enable native TypeScript support via `--experimental-strip-types` flag:

```shell
NODE_OPTIONS="--experimental-strip-types" vitest
```

Note that Node.js will print an experimental warning for every test file; you can silence the warning by providing `--no-warnings` flag:

```shell
NODE_OPTIONS="--experimental-strip-types --no-warnings" vitest
```
:::

If you are using TypeScript and Node.js version lower than 22.6, then you will need to either:

- build your test files and source code and run those files directly
- import a [custom loader](https://nodejs.org/api/module.html#customization-hooks) via `execArgv` flag

```ts
import { defineConfig } from 'vitest/config'

const tsxApi = import.meta.resolve('tsx/esm/api')

export default defineConfig({
test: {
execArgv: [
`--import=data:text/javascript,import * as tsx from "${tsxCli}";tsx.register()`,
],
experimental: {
viteModuleRunner: false,
},
},
})
```

If you are running tests in Deno, TypeScript files are processed by the runtime without any additional configurations.

## experimental.nodeLoader <Version type="experimental">4.0.16</Version> {#experimental-nodeloader}

- **Type:** `boolean`
- **Default:** `true`

If module runner is disabled, Vitest uses a module loader to transform files to support `import.meta.vitest`, `vi.mock` and `vi.hoisted`.

If you don't use these features, you can disable this.
3 changes: 3 additions & 0 deletions examples/native/jsSetup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { initJsSetup } from './src/setups.ts'

initJsSetup()
22 changes: 22 additions & 0 deletions examples/native/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@vitest/example-native",
"type": "module",
"private": true,
"license": "MIT",
"main": "index.js",
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitest/ui": "latest",
"tinyspy": "^4.0.4",
"vite": "latest",
"vitest": "latest"
},
"stackblitz": {
"startCommand": "npm run test:ui"
}
}
1 change: 1 addition & 0 deletions examples/native/src/__mocks__/redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default true
4 changes: 4 additions & 0 deletions examples/native/src/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const squared = (n: number) => n * n
export * from './dependency.ts'
export { helloMe as hello } from './index.ts'
export default 'hello world'
3 changes: 3 additions & 0 deletions examples/native/src/dependency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function add(a: number, b: number) {
return a + b
}
4 changes: 4 additions & 0 deletions examples/native/src/from-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { answer } from './mock-async.ts'

const topLevelAnswer = answer
export { topLevelAnswer }
13 changes: 13 additions & 0 deletions examples/native/src/in-source/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function add(...args: number[]) {
return args.reduce((a, b) => a + b, 0)
}

// in-source test suites
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('add', () => {
expect(add()).toBe(0)
expect(add(1)).toBe(1)
expect(add(1, 2, 3)).toBe(6)
})
}
24 changes: 24 additions & 0 deletions examples/native/src/in-source/fibonacci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { add } from './add.ts'

export function fibonacci(n: number): number {
if (n < 2) {
return n
}
return add(fibonacci(n - 1), fibonacci(n - 2))
}

if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('fibonacci', () => {
expect(fibonacci(0)).toBe(0)
expect(fibonacci(1)).toBe(1)
expect(fibonacci(2)).toBe(1)
expect(fibonacci(3)).toBe(2)
expect(fibonacci(4)).toBe(3)
expect(fibonacci(5)).toBe(5)
expect(fibonacci(6)).toBe(8)
expect(fibonacci(7)).toBe(13)
expect(fibonacci(8)).toBe(21)
expect(fibonacci(9)).toMatchInlineSnapshot('34')
})
}
2 changes: 2 additions & 0 deletions examples/native/src/in-source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './add.ts'
export * from './fibonacci.ts'
2 changes: 2 additions & 0 deletions examples/native/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { add, hello, squared } from './basic.ts'
export const helloMe = 'world'
3 changes: 3 additions & 0 deletions examples/native/src/minus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function minus(a: number, b: number): number {
return a - b
}
1 change: 1 addition & 0 deletions examples/native/src/mock-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const answer = 0
1 change: 1 addition & 0 deletions examples/native/src/mock-js.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare function mockJs(): number
3 changes: 3 additions & 0 deletions examples/native/src/mock-js.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function mockJs() {
return 0
}
3 changes: 3 additions & 0 deletions examples/native/src/mock-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function syncMock() {
return 0
}
3 changes: 3 additions & 0 deletions examples/native/src/no-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function notMocked() {
return true
}
2 changes: 2 additions & 0 deletions examples/native/src/redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
throw new Error('Should be redirected!')
export default false
19 changes: 19 additions & 0 deletions examples/native/src/setups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { vi } from 'vitest'

let jsSetup = false
let tsSetup = false

export const initJsSetup = vi.fn(() => {
jsSetup = true
})

export const initTsSetup = vi.fn(() => {
tsSetup = true
})

export function getSetupStates() {
return {
jsSetup,
tsSetup,
}
}
7 changes: 7 additions & 0 deletions examples/native/test/__snapshots__/suite.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`suite name > snapshot 1`] = `
{
"foo": "bar",
}
`;
9 changes: 9 additions & 0 deletions examples/native/test/automock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { expect, test, vi } from 'vitest'
import { squared } from '../src/basic.ts'

vi.mock(import('../src/basic.ts'))

test('squared is mocked', () => {
expect(vi.isMockFunction(squared)).toBe(true)
expect(squared(2)).toBe(undefined)
})
10 changes: 10 additions & 0 deletions examples/native/test/autospy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect, test, vi } from 'vitest'
import { squared } from '../src/basic.ts'

vi.mock(import('../src/basic.ts'), { spy: true })

test('squared is mocked', () => {
expect(vi.isMockFunction(squared)).toBe(true)
expect(squared(2)).toBe(4)
expect(squared).toHaveBeenCalled()
})
33 changes: 33 additions & 0 deletions examples/native/test/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { assert, expect, test } from 'vitest'
import { squared } from '../src/basic.ts'
// importing from the barrel file
import { add } from '../src/in-source/index.ts'

// Edit an assertion and save to see HMR in action

test('Math.sqrt()', () => {
expect(Math.sqrt(4)).toBe(2)
expect(Math.sqrt(144)).toBe(12)
expect(Math.sqrt(2)).toBe(Math.SQRT2)
})

test('add', () => {
expect(add(1, 2)).toBe(3)
})

test('Squared', () => {
expect(squared(2)).toBe(4)
expect(squared(12)).toBe(144)
})

test('JSON', () => {
const input = {
foo: 'hello',
bar: 'world',
}

const output = JSON.stringify(input)

expect(output).eq('{"foo":"hello","bar":"world"}')
assert.deepEqual(JSON.parse(output), input, 'matches original')
})
Loading
Loading