facebook / facebook/stylex

`@stylexjs/unplugin` prevents Vitest from exiting - ~581 leaked file handles in an 80-module suite, plus an unrefd timer

Open
#1,533 2 comments 5 reactions 0 assignees View on GitHub
bug
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

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.