facebook / facebook/stylex

[unplugin] Vite adapter leaks a setInterval under Vitest (no httpServer to clear it), tests hang 10s on exit

Open
#1,836 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.