Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions packages/@stylexjs/unplugin/__tests__/unplugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,54 @@ describe('@stylexjs/unplugin', () => {
expect(css).toContain('.x1aif7nf');
});
});

// Regression: https://github.com/facebook/stylex/issues/1836
// The Vite adapter's shared-store polling timer must never keep the process
// alive. Vitest's Vite server has no `httpServer`, so the `close` cleanup is
// skipped and a still-referenced interval would hang `vitest run` on exit.
describe('Vite dev shared-store polling timer', () => {
function makeServer(httpServer) {
return {
middlewares: { use: () => {} },
ws: { send: () => {} },
httpServer,
};
}

test('is unref’d when the server has no httpServer (Vitest)', () => {
const plugin = unplugin.vite({ dev: true });
expect(typeof plugin.configureServer).toBe('function');

const server = makeServer(null);
plugin.configureServer(server);

const interval = server.__stylexSharedPollingInterval;
expect(interval).toBeDefined();
// The timer must not hold the event loop open.
expect(typeof interval.hasRef).toBe('function');
expect(interval.hasRef()).toBe(false);
clearInterval(interval);
});

test('is cleared when the httpServer closes', () => {
const plugin = unplugin.vite({ dev: true });
const closeListeners = [];
const httpServer = {
once(event, fn) {
if (event === 'close') closeListeners.push(fn);
},
};
const server = makeServer(httpServer);
plugin.configureServer(server);

const interval = server.__stylexSharedPollingInterval;
expect(interval).toBeDefined();
expect(closeListeners.length).toBe(1);
// Simulate the server closing.
closeListeners[0]();
// After close the timer is cleared: calling clearInterval again is safe,
// and the interval no longer has a ref either way.
expect(() => clearInterval(interval)).not.toThrow();
});
Comment on lines +479 to +517

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — rewritten in 91b2507 with fake timers and a ws.send spy: bumps the shared store version, asserts one stylex:css-update send on the next tick, then simulates close and asserts no further sends after another version bump.

});
});
11 changes: 11 additions & 0 deletions packages/@stylexjs/unplugin/src/vite.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,18 @@ function attachViteHooks(plugin) {
} catch {}
}
}, 150);
// This dev-only polling timer must never keep the process alive. Under
// Vitest (and Vite middleware mode) the server has no `httpServer`, so
// the `close` cleanup below is skipped; a still-referenced interval
// would then leak and hang the process on exit (~10s under
// `vitest run`) (#1836).
if (typeof interval.unref === 'function') {
interval.unref();
}
server.httpServer?.once('close', () => clearInterval(interval));
// Exposed so tests (and hosts that tear the server down without an
// `httpServer` close event) can observe/clear the timer.
server.__stylexSharedPollingInterval = interval;
}
},
resolveId(id) {
Expand Down