Describe the bug
When defining 2 or more environments (using Environment API) in the Vite config, mutations made by a plugin's configEnvironment hook in one environment can leak into other environments.
This happens because resolveConfig constructs a single defaultNonClientEnvironmentOptions object and reuses it for all non-client environments in the merge loop. Since mergeConfig does a shallow spread, nested objects like resolve retain their shared reference. When a plugin then mutates config.resolve for one environment, all other non-client environments see the same change.
In practice, this surfaces with Vitest when using 2+ environments (from Environment API) and custom environment (from Vitest) - Vitest's configEnvironment hook sets resolve.noExternal = true per environment, which propagates to all non-client environments, affecting this condition for the next configEnvironment call, causing CJS dependencies (like jsdom) to crash with ReferenceError: require is not defined. Take this with a grain of salt as this is only my theory and I don't have full understanding of Vite/Vitest.
If you name an environment client, it is unaffected because it gets a separately constructed defaultClientEnvironmentOptions. A single non-client environment also works fine since there's nothing to leak to.
Reproduction
https://github.com/Filipoliko/vitest-environment-bug
Steps to reproduce
Run npm ci followed by npm test in the reproduction repository. Check out the README file in the project for more info.
Check out also PR #22250 for a unit test in this repository that also showcases this issue.
System Info
System:
OS: macOS 26.4.1
CPU: (10) arm64 Apple M1 Pro
Memory: 147.92 MB / 16.00 GB
Shell: 5.2.37 - /opt/homebrew/bin/bash
Binaries:
Node: 24.14.1 - /Users/filip.satek/.nvm/versions/node/v24.14.1/bin/node
npm: 11.11.0 - /Users/filip.satek/.nvm/versions/node/v24.14.1/bin/npm
pnpm: 10.33.0 - /Users/filip.satek/.nvm/versions/node/v24.14.1/bin/pnpm
bun: 1.2.12 - /opt/homebrew/bin/bun
Browsers:
Chrome: 147.0.7727.57
Edge: 113.0.1774.57
Firefox: 149.0.2
Safari: 26.4
Used Package Manager
npm
Logs
$ npm test
> vitest-environment@1.0.0 test
> vitest run
RUN v4.1.4 /Users/filip.satek/git/vitest-environment
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Vitest caught 1 unhandled error during the test run.
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Error ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Error: [vitest-pool]: Failed to start forks worker for test files /Users/filip.satek/git/vitest-environment/test.spec.ts.
❯ node_modules/vitest/dist/chunks/cli-api.lDy4N9kC.js:3448:94
❯ processTicksAndRejections node:internal/process/task_queues:104:5
❯ Pool.schedule node_modules/vitest/dist/chunks/cli-api.lDy4N9kC.js:3448:5
Caused by: ReferenceError: require is not defined
❯ eval node_modules/vite/dist/node/module-runner.js:992:9
❯ ESModulesEvaluator.runInlinedModule node_modules/vite/dist/node/module-runner.js:992:161
❯ ModuleRunner.directRequest node_modules/vite/dist/node/module-runner.js:1247:80
❯ processTicksAndRejections node:internal/process/task_queues:104:5
❯ ModuleRunner.cachedRequest node_modules/vite/dist/node/module-runner.js:1154:73
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Test Files no tests
Tests no tests
Errors 1 error
Start at 12:31:44
Duration 238ms (transform 117ms, setup 0ms, import 0ms, tests 0ms, environment 0ms)
Validations
Describe the bug
When defining 2 or more environments (using Environment API) in the Vite config, mutations made by a plugin's
configEnvironmenthook in one environment can leak into other environments.This happens because
resolveConfigconstructs a singledefaultNonClientEnvironmentOptionsobject and reuses it for all non-client environments in the merge loop. SincemergeConfigdoes a shallow spread, nested objects likeresolveretain their shared reference. When a plugin then mutatesconfig.resolvefor one environment, all other non-client environments see the same change.In practice, this surfaces with Vitest when using 2+ environments (from Environment API) and custom environment (from Vitest) - Vitest's
configEnvironmenthook setsresolve.noExternal = trueper environment, which propagates to all non-client environments, affecting this condition for the next configEnvironment call, causing CJS dependencies (like jsdom) to crash withReferenceError: require is not defined. Take this with a grain of salt as this is only my theory and I don't have full understanding of Vite/Vitest.If you name an environment
client, it is unaffected because it gets a separately constructeddefaultClientEnvironmentOptions. A single non-client environment also works fine since there's nothing to leak to.Reproduction
https://github.com/Filipoliko/vitest-environment-bug
Steps to reproduce
Run
npm cifollowed bynpm testin the reproduction repository. Check out the README file in the project for more info.Check out also PR #22250 for a unit test in this repository that also showcases this issue.
System Info
System: OS: macOS 26.4.1 CPU: (10) arm64 Apple M1 Pro Memory: 147.92 MB / 16.00 GB Shell: 5.2.37 - /opt/homebrew/bin/bash Binaries: Node: 24.14.1 - /Users/filip.satek/.nvm/versions/node/v24.14.1/bin/node npm: 11.11.0 - /Users/filip.satek/.nvm/versions/node/v24.14.1/bin/npm pnpm: 10.33.0 - /Users/filip.satek/.nvm/versions/node/v24.14.1/bin/pnpm bun: 1.2.12 - /opt/homebrew/bin/bun Browsers: Chrome: 147.0.7727.57 Edge: 113.0.1774.57 Firefox: 149.0.2 Safari: 26.4Used Package Manager
npm
Logs
Validations