I was about to get started turning my stub react-spreadsheet
package into a real one, when I noticed a new Vitest related warning from VS Code.
This is an updated version of Vitest Monorepo Setup, originally published April 19, 2024. Use the previous version for Vitest 1.6 - 2.x.
This version requires Vitest 3 or later.


Vite Config Files
Which reminded me that I haven’t done anything to better structure my vite.config.ts
file for a monorepo. Vitest is built on Vite and conveniently uses the same config file. I currently have an independent config for each app and package with no attempt to share common settings.
App and package configs are very different. My virtual-scroll-samples
app config is mainly concerned with producing a production build of the app. There are no Vitest settings.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import tsconfigPaths from 'vite-tsconfig-paths'
import sourcemaps from '@gordonmleigh/rollup-plugin-sourcemaps'
import { resolve } from 'path'
export default defineConfig({
plugins: [
react(),
tsconfigPaths()
],
build: {
sourcemap: true,
rollupOptions: {
plugins: [sourcemaps()],
input: {
main: resolve(__dirname, 'index.html'),
"list-and-grid": resolve(__dirname, 'samples/list-and-grid/index.html'),
"trillion-row-list": resolve(__dirname, 'samples/trillion-row-list/index.html'),
"trillion-square-grid": resolve(__dirname, 'samples/trillion-square-grid/index.html'),
"horizontal-list": resolve(__dirname, 'samples/horizontal-list/index.html'),
"paging-functional-test": resolve(__dirname, 'samples/paging-functional-test/index.html'),
"spreadsheet": resolve(__dirname, 'samples/spreadsheet/index.html'),
"padding": resolve(__dirname, 'samples/padding/index.html'),
},
output: {
manualChunks: (id) => {
if (id.includes('node_modules')) {
return 'vendor';
}
}
}
}
}
})
In contrast, my react-virtual-scroll
package config is mostly made up of Vitest settings.
import { defineConfig } from 'vite'
import { configDefaults } from 'vitest/config'
import react from '@vitejs/plugin-react-swc'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
includeSource: ['src/**/*.{js,ts}'],
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
coverage: {
provider: 'istanbul',
include: ['src/**'],
exclude: ['src/test/**','src/*.*test.*','src/*.bench.*'],
},
},
define: {
'import.meta.vitest': 'undefined',
},
})
I copied the react-virtual-scroll
config when I created the infinisheet-types
package. It would be great if I can hoist all the common settings up to the workspace level. Searching for “Vitest Workspace” took me straight to the relevant documentation.
Vitest provides a way to define multiple project configurations in a single Vitest process. It works well for multiple projects within a monorepo setup but can also be used to run tests with different configurations.
As far as I can tell, there’s no equivalent for Vite. I’m going to concentrate on sharing the Vitest settings as the rest of the config file is pretty much a stub anyway.
Root Config
Start by creating a vitest.config.ts
file in your monorepo’s root directory. The simplest root config just tells Vitest where to look for each project.
import { defineConfig } from 'vite'
export default defineConfig({
test: {
projects: ['packages/*/vite.config.ts']
}
})
I assumed that you could put common config into the root level config and then run Vitest per package as normal. I was wrong on both counts.
If you run Vitest in the package directory, it runs as before, ignoring the root config. The root config is only used if you run Vitest in the root directory. It then checks each package directory for a Vite or Vitest config file, loads all the tests from all the packages and executes them together.
You can define projects with their config directly in the root config but it behaves just like a per-package config file. This isn’t shared config, just an alternative place to define test projects. I’m not sure why you’d want to do this. It’s much easier to maintain package specific settings in a dedicated file in each package directory.
Global Settings
It gets more confusing. When running Vitest in the root directory, some configuration, like reporters and coverage, needs to be defined in the root config rather than the per-package config files.
The tests to run are picked up from the per-package configs but coverage specific options, like which files to include in the coverage report, need to be specified here.
export default defineConfig({
test: {
projects: ['packages/*/vite.config.ts']
coverage: {
provider: 'istanbul',
include: ['packages/*/src/**'],
exclude: ['packages/*/src/test/**','src/*.*test.*','src/*.bench.*'],
},
},
})
Taking Stock
At this point I’ve done enough to fix the VS Code warning. The Vitest plugin uses the root config file to load tests from all packages. I can run all the tests, run tests for a selected package or individually. Seems to work fine.


I can also run coverage for the entire repo in one go. In testing with my small monorepo, running at the root level is about 30% quicker than letting lerna run a coverage test for each package. You also get a combined coverage report doing it this way.


Shared Testing Code
My existing react-virtual-scroll
package has three files of common testing code in react-virtual-scroll/src/test
.
setup.ts
: a Vitest setup file that automatically makes Jest DOM assertions available for use in each testwrapper.tsx
: a wrapper around@testing-library/react
that I copied from an official Vitest exampleutils.ts
: my own set of utility functions
Moving them out from under react-virtual-scroll/src
means updating all the import
statements that reference them and updating the TypeScript tsconfig.json
to include them in the set of files processed by the compiler.
My first thought was to add them to my root tsconfig.json
which the per-package tsconfig.json
extends. Unfortunately, that doesn’t work because any property in the per-package config overrides that in the root, even if it’s an array property. Arrays aren’t merged.
I had to add test
to the include array in each per-package stub config.
{
"extends": "../../tsconfig.json",
"include": ["src", "../../shared/test"]
}
Shared Vite Config
The project documentation has a separate section on sharing config. You need to put your shared config in a separate file and then import it and use the mergeConfig
utility to combine with your per-package config. Interestingly, mergeConfig
seems to be generic. It appears to work with Vite settings too.
I was expecting another misunderstanding on my part, but it worked as expected. I was able to move almost everything into a shared vitest.config.ts
. I’ve left the old coverage settings in place so that I can still run per package coverage.
import { defineConfig } from 'vite'
import { configDefaults } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
includeSource: ['src/**/*.{js,ts}'],
environment: 'jsdom',
setupFiles: '../../shared/test/setup-jsdom.ts',
coverage: {
provider: 'istanbul',
include: ['src/**'],
exclude: ['src/test/**','src/*.*test.*','src/*.bench.*'],
},
},
define: {
'import.meta.vitest': 'undefined',
},
})
I could then pull the shared config into a per-package stub. I left the React plugin in my per-package config as I have a mix of React and vanilla TypeScript packages. If needed, I can have a whole cascade of common configs for different scenarios.
import { defineConfig } from 'vite'
import { mergeConfig } from 'vitest/config'
import configShared from '../../shared/vitest.config'
import react from '@vitejs/plugin-react-swc'
export default mergeConfig(
configShared,
defineConfig({
plugins: [react()],
})
)
Conclusion
That was more confusing than it needed to be, but I seem to have ended up in a reasonable place. The VS Code warning has gone, I have a system for managing shared Vite and Vitest settings, and I have the option of running unit tests and coverage either per package or for the entire workspace at once.