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.
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
- Vite 6.4 +
@vitejs/plugin-react + Vitest 3.2 project.
vite.config.ts: plugins: [stylex.vite(), react()] with stylex from @stylexjs/unplugin@0.19.0.
- Any component using
stylex.create and a Vitest test that renders it (jsdom).
npx vitest run → tests pass, then the 10 s close timeout above.
Test case
Minimal component + test that reproduces it:
// Card.tsx
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({ card: { padding: '16px' } });
export function Card() { return <div {...stylex.props(styles.card)} />; }
// Card.test.tsx (// @vitest-environment jsdom)
import { render } from '@testing-library/react';
import { it } from 'vitest';
import { Card } from './Card';
it('renders', () => { render(<Card />); });
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.
Describe the issue
@stylexjs/unplugin's Vite adapter keeps the process alive after tests finish when used under Vitest. Everyvitest runends with:Cause (in the published
@stylexjs/unplugin@0.19.0,lib/vite.js,configureServer): whensharedis set, the adapter starts a 150 mssetIntervalto broadcaststylex:css-updateoverserver.ws, and clears it only viaserver.httpServer?.once('close', ...). Vitest's Vite server has nohttpServer, so theonceis skipped and the interval is never cleared.Vitest's
--reporter=hanging-processshows 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, sostylex.defineVars/stylex.createthrowUnexpected 'stylex.defineVars' call at runtimein tests.devMode: 'css-only'still leaks the interval.Expected behavior
vitest runexits 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
@vitejs/plugin-react+ Vitest 3.2 project.vite.config.ts:plugins: [stylex.vite(), react()]withstylexfrom@stylexjs/unplugin@0.19.0.stylex.createand a Vitest test that renders it (jsdom).npx vitest run→ tests pass, then the 10 s close timeout above.Test case
Minimal component + test that reproduces it:
Additional comments
Proposed fixes, either works:
if (server.httpServer) { ... }.interval.unref?.()so a leaked timer cannot keep Node alive, and additionally clear it in abuildEnd/closeBundlehook.Workaround we are using: skip the unplugin adapter when
process.env.VITESTis set and compile through@stylexjs/babel-pluginvia@vitejs/plugin-react'sbabel.pluginsinstead. Works, but means two compile paths in one config.Environment: Node 24.13, Vite 6.4.2, Vitest 3.2.4,
@stylexjs/unplugin0.19.0,@stylexjs/stylex0.19.0, macOS.