Skip to content

Commit b3602bc

Browse files
feat(dev): add moduleDirectories option to the vitest config (#3337)
Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>
1 parent 8254737 commit b3602bc

File tree

19 files changed

+109
-13
lines changed

19 files changed

+109
-13
lines changed

docs/api/vi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ import { vi } from 'vitest'
234234
```
235235
:::
236236

237-
If there is a `__mocks__` folder alongside a file that you are mocking, and the factory is not provided, Vitest will try to find a file with the same name in the `__mocks__` subfolder and use it as an actual module. If you are mocking a dependency, Vitest will try to find a `__mocks__` folder in the [root](/config/#root) of the project (default is `process.cwd()`).
237+
If there is a `__mocks__` folder alongside a file that you are mocking, and the factory is not provided, Vitest will try to find a file with the same name in the `__mocks__` subfolder and use it as an actual module. If you are mocking a dependency, Vitest will try to find a `__mocks__` folder in the [root](/config/#root) of the project (default is `process.cwd()`). You can tell Vitest where the dependencies are located through the [deps.moduleDirectories](/config/#deps-moduledirectories) config option.
238238

239239
For example, you have this file structure:
240240

docs/config/index.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,29 @@ TypeError: default is not a function
190190

191191
By default, Vitest assumes you are using a bundler to bypass this and will not fail, but you can disable this behaviour manually, if you code is not processed.
192192

193+
#### deps.moduleDirectories
194+
195+
- **Type:** `string[]`
196+
- **Default**: `['node_modules']`
197+
198+
A list of directories that should be treated as module directories. This config option affects the behavior of [`vi.mock`](/api/vi#vi-mock): when no factory is provided and the path of what you are mocking matches one of the `moduleDirectories` values, Vitest will try to resolve the mock by looking for a `__mocks__` folder in the [root](/config/#root) of the project.
199+
200+
This option will also affect if a file should be treated as a module when externalizing dependencies. By default, Vitest imports external modules with native Node.js bypassing Vite transformation step.
201+
202+
Setting this option will _override_ the default, if you wish to still search `node_modules` for packages include it along with any other options:
203+
204+
```ts
205+
import { defineConfig } from 'vitest/config'
206+
207+
export default defineConfig({
208+
test: {
209+
deps: {
210+
moduleDirectories: ['node_modules', path.resolve('../../packages')],
211+
}
212+
},
213+
})
214+
```
215+
193216
### runner
194217

195218
- **Type**: `VitestRunnerConstructor`
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function () {
2+
return 'mocked'
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function () {
2+
return 'original'
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "module"
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @ts-expect-error not typed aliased import
2+
import getState from 'custom-lib'
3+
4+
vi.mock('custom-lib')
5+
6+
it('state is mocked', () => {
7+
expect(getState()).toBe('mocked')
8+
})

examples/mocks/vite.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="vitest" />
22

3+
import { resolve } from 'node:path'
34
import { defineConfig } from 'vite'
45

56
// https://vitejs.dev/config/
@@ -20,12 +21,18 @@ export default defineConfig({
2021
},
2122
},
2223
],
24+
resolve: {
25+
alias: [
26+
{ find: /^custom-lib$/, replacement: resolve(__dirname, 'projects', 'custom-lib') },
27+
],
28+
},
2329
test: {
2430
globals: true,
2531
environment: 'node',
2632
deps: {
2733
external: [/src\/external/],
2834
interopDefault: true,
35+
moduleDirectories: ['node_modules', 'projects'],
2936
},
3037
},
3138
})

packages/vite-node/src/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ function parseServerOptions(serverOptions: ViteNodeServerOptionsCLI): ViteNodeSe
128128
? new RegExp(dep)
129129
: dep
130130
}),
131+
moduleDirectories: serverOptions.deps?.moduleDirectories
132+
? toArray(serverOptions.deps?.moduleDirectories)
133+
: undefined,
131134
},
132135

133136
transformMode: {

packages/vite-node/src/externalize.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { existsSync } from 'node:fs'
22
import { isNodeBuiltin, isValidNodeImport } from 'mlly'
3+
import { join } from 'pathe'
34
import type { DepsHandlingOptions } from './types'
45
import { slash } from './utils'
56

@@ -109,35 +110,37 @@ async function _shouldExternalize(
109110
if (options?.cacheDir && id.includes(options.cacheDir))
110111
return id
111112

112-
if (matchExternalizePattern(id, options?.inline))
113+
const moduleDirectories = options?.moduleDirectories || ['/node_modules/']
114+
115+
if (matchExternalizePattern(id, moduleDirectories, options?.inline))
113116
return false
114-
if (matchExternalizePattern(id, options?.external))
117+
if (matchExternalizePattern(id, moduleDirectories, options?.external))
115118
return id
116119

117-
const isNodeModule = id.includes('/node_modules/')
118-
const guessCJS = isNodeModule && options?.fallbackCJS
120+
const isLibraryModule = moduleDirectories.some(dir => id.includes(dir))
121+
const guessCJS = isLibraryModule && options?.fallbackCJS
119122
id = guessCJS ? (guessCJSversion(id) || id) : id
120123

121-
if (matchExternalizePattern(id, defaultInline))
124+
if (matchExternalizePattern(id, moduleDirectories, defaultInline))
122125
return false
123-
if (matchExternalizePattern(id, depsExternal))
126+
if (matchExternalizePattern(id, moduleDirectories, depsExternal))
124127
return id
125128

126129
const isDist = id.includes('/dist/')
127-
if ((isNodeModule || isDist) && await isValidNodeImport(id))
130+
if ((isLibraryModule || isDist) && await isValidNodeImport(id))
128131
return id
129132

130133
return false
131134
}
132135

133-
function matchExternalizePattern(id: string, patterns?: (string | RegExp)[] | true) {
136+
function matchExternalizePattern(id: string, moduleDirectories: string[], patterns?: (string | RegExp)[] | true) {
134137
if (patterns == null)
135138
return false
136139
if (patterns === true)
137140
return true
138141
for (const ex of patterns) {
139142
if (typeof ex === 'string') {
140-
if (id.includes(`/node_modules/${ex}/`))
143+
if (moduleDirectories.some(dir => id.includes(join(dir, id))))
141144
return true
142145
}
143146
else {

packages/vite-node/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type Awaitable<T> = T | PromiseLike<T>
99
export interface DepsHandlingOptions {
1010
external?: (string | RegExp)[]
1111
inline?: (string | RegExp)[] | true
12+
moduleDirectories?: string[]
1213
cacheDir?: string
1314
/**
1415
* Try to guess the CJS version of a package when it's invalid ESM

0 commit comments

Comments
 (0)