[unplugin] Vite adapter leaks a setInterval under Vitest (no httpServer to clear it), tests hang 10s on exit
- Dominant language
- JavaScript
- Stars
- 10.3k
- Forks
- 481
- Avg merge
- 3d 8h
- Merged PRs (30d)
- 13
Description
### Describe the issue
`@stylexjs/unplugin`'s Vite adapter keeps the process alive after tests finish when used under Vitest. Every `vitest run` ends with:
```
close timed out after 10000ms
Tests closed successfully but something prevents Vite server from exiting
```
Cause (in the published `@stylexjs/unplugin@0.19.0`, `lib/vite.js`, `configureServer`): when `shared` is set, the adapter starts a 150 ms `setInterval` to broadcast `stylex:css-update` over `server.ws`, and clears it only via `server.httpServer?.once('close', ...)`. Vitest's Vite server has no `httpServer`, so the `once` is skipped and the interval is never cleared.
```js
const interval = setInterval(() => { /* ... server.ws.send(...) */ }, 150);
server.httpServer?.once('close', () => clearInterval(interval));
```
Vitest's `--reporter=hanging-process` shows the leaked handles. Suite wall time goes from ~4.5 s to ~15 s.
`devMode: 'off'` is not a workaround: it also skips the transform, so `stylex.defineVars` / `stylex.create` throw `Unexpected 'stylex.defineVars' call at runtime` in tests. `devMode: 'css-only'` still leaks the interval.
### Expected behavior
`vitest run` exits immediately after tests complete, or the plugin does not start dev-only timers when there is no HTTP server to attach to.
### Steps to reproduce
1. Vite 6.4 + `@vitejs/plugin-react` + Vitest 3.2 project.
2. `vite.config.ts`: `plugins: [stylex.vite(), react()]` with `stylex` from `@stylexjs/unplugin@0.19.0`.
3. Any component using `stylex.create` and a Vitest test that renders it (jsdom).
4. `npx vitest run` → tests pass, then the 10 s close timeout above.
### Test case
Minimal component + test that reproduces it:
```ts
// Card.tsx
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({ card: { padding: '16px' } });
export function Card() { return
```
```ts
// Card.test.tsx (// @vitest-environment jsdom)
import { render } from '@testing-library/react';
import { it } from 'vitest';
import { Card } from './Card';
it('renders', () => { render(); });
```
### Additional comments
Proposed fixes, either works:
- Guard the timer on the server actually having an HTTP server / WebSocket to notify: `if (server.httpServer) { ... }`.
- Or use `interval.unref?.()` so a leaked timer cannot keep Node alive, and additionally clear it in a `buildEnd`/`closeBundle` hook.
Workaround we are using: skip the unplugin adapter when `process.env.VITEST` is set and compile through `@stylexjs/babel-plugin` via `@vitejs/plugin-react`'s `babel.plugins` instead. Works, but means two compile paths in one config.
Environment: Node 24.13, Vite 6.4.2, Vitest 3.2.4, `@stylexjs/unplugin` 0.19.0, `@stylexjs/stylex` 0.19.0, macOS.
Contributor guide
Assessment
This issue has not been assessed yet.