`@stylexjs/unplugin` prevents Vitest from exiting - ~581 leaked file handles in an 80-module suite, plus an unrefd timer
- Dominant language
- JavaScript
- Stars
- 10.3k
- Forks
- 481
- Avg merge
- 3d 8h
- Merged PRs (30d)
- 13
Description
### Describe the issue
`@stylexjs/unplugin` prevents Vitest from exiting. Two independent causes, either of which is enough on its own:
1. **Babel looks for a config file on every transform.** `runBabelTransform()` in `packages/@stylexjs/unplugin/src/core.js` sets `babelrc: false` but not `configFile`, so every `transformAsync()` call sends Babel looking for a root config file: up to 7 filesystem probes for `babel.config.{js,cjs,mjs,json,cts,mts,ts}`. In async mode these are callback-based `fs.readFile`/`fs.stat` operations, which Vitest's `hanging-process` reporter counts as live handles. The count is a property of the suite rather than of the bug - roughly 7 handles per module transformed, so the ~80-module suite below reports ~581 while a small project reports ~20.
2. **A poll timer that is never unreferenced.** This one does not scale - it is a single timer, present in any run that loads `stylex.vite()`. `configureServer()` in `packages/@stylexjs/unplugin/src/vite.js` starts a 150ms `setInterval` polling the CSS-rules version, and clears it on `server.httpServer`'s `'close'` event. Under Vitest's middleware mode `httpServer` is `null`, so nothing ever clears it and it holds the event loop open.
Cause 2 alone hangs a run, and reproduces in four lines.
### Expected behavior
The process exits as soon as tests finish, with nothing reported by `vitest run --reporter=hanging-process`.
### Steps to reproduce
**Minimal** - cause 2 only, no test files or DOM environment needed. Isolated by @baptisteArno ([comment](https://github.com/facebook/stylex/issues/1533#issuecomment-4882821751)):
```ts
// vitest.config.ts
import * as stylex from '@stylexjs/unplugin';
export default { plugins: [stylex.vite()] };
```
```
npx vitest run --passWithNoTests
```
The run completes, then waits out the full teardown timeout:
```
close timed out after 10000ms
Tests closed successfully but something prevents Vite server from exiting
```
**Both causes, with handle counts** - https://github.com/lecstor/stylex-filehandle-repro
```
npx vitest run --reporter=hanging-process
```
### Environment
Reproduces across five months of releases. Also hit by [@stephenh](https://github.com/facebook/stylex/issues/1533#issuecomment-4104140292) on the original stack; later versions confirmed by [@baptisteArno](https://github.com/facebook/stylex/issues/1533#issuecomment-4882821751):
| Component | First report | Confirmed since |
| --- | --- | --- |
| `@stylexjs/unplugin` | 0.17.5 | 0.19.0 |
| Vitest | 3.x | 4.1.9 |
| Vite | 7.x | 8.x |
| Runtime | Node 24.1.0 | Node 25.6.1, Bun 1.3.12 |
| OS | macOS arm64 | macOS arm64 |
### Workarounds
- **Avoid cause 2 entirely:** use `stylex.rollup()` rather than `stylex.vite()` in the Vitest config, if your tests only need the StyleX transform and not the dev-server CSS/HMR middleware. Found by @baptisteArno, who used it to drop a forced `process.exit` wrapper around Vitest.
- **Or shorten the wait:** `test: { teardownTimeout: 1000 }` force-exits after 1s instead of 10s.
### Additional comments
#1534 fixes both causes with two one-line changes: `configFile: false` in `core.js` and `interval.unref()` in `vite.js`.
Contributor guide
Assessment
This issue has not been assessed yet.