Skip to content

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

Description

@gdegas

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

  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:

// 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions