Skip to content

Environment API - Cross env poisoning via hooks #22249

@Filipoliko

Description

@Filipoliko

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions