ADR-006: Vitest Browser Mode + React 19 CJS Interop Strategy
ADR-006: Vitest Browser Mode + React 19 CJS Interop Strategy
Section titled “ADR-006: Vitest Browser Mode + React 19 CJS Interop Strategy”Status: Accepted Date: 2026-06-20 Deciders: Development team
Context
Section titled “Context”Our Storybook component testing uses @storybook/addon-vitest with Vitest’s browser mode (Playwright + Chromium). When running test:storybook, we encountered two cascading module resolution failures:
use-sync-external-store/shim/with-selector.jsdoes not provide an export nameduseSyncExternalStoreWithSelectorreact-dom/index.jsdoes not provide an export namedflushSync
Both failures stem from the same root cause: React 19 ships CJS-only npm packages. The index.js entry points use module.exports = require('./cjs/...') re-export chains. Vite’s CJS-to-ESM converter cannot follow these chains to detect named exports.
Decision
Section titled “Decision”We adopted a two-part fix:
1. @vitejs/plugin-react (standard approach)
Section titled “1. @vitejs/plugin-react (standard approach)”Added to vitest.config.ts plugins array. Per Vitest browser mode docs, this is required for React projects — it handles react-dom CJS interop automatically.
2. Alias for use-sync-external-store/shim/with-selector (targeted workaround)
Section titled “2. Alias for use-sync-external-store/shim/with-selector (targeted workaround)”The use-sync-external-store shim uses module.exports = require(...) to re-export its CJS build. Vite cannot follow this chain. We alias the import to the underlying CJS file where named exports are detectable:
resolve: { alias: { "use-sync-external-store/shim/with-selector": path.join( uSesRoot, "cjs/use-sync-external-store-shim/with-selector.development.js", ), },},This is a known React packaging limitation (facebook/react#24590). React added proper exports fields in PR #25231, but the underlying CJS chain still trips Vite.
Alternatives Considered
Section titled “Alternatives Considered”| Option | Verdict |
|---|---|
@storybook/test-runner (Jest + Playwright) | Avoids CJS issues but is slower, requires running Storybook server separately, and Storybook themselves recommend Vitest for Vite projects |
jsdom environment instead of browser | No real CSS rendering, no real user events — defeats the purpose of component tests |
| Do nothing (tests were already broken) | Not acceptable — tests provide value and should run in CI |
| React 18 instead of React 19 | React 18 has the same CJS packaging; downgrading doesn’t help |
Consequences
Section titled “Consequences”- Storybook Vitest tests now pass:
pnpm test:storybookruns 55 test files in Chromium via Playwright - The alias is a single file path — well-documented with inline comments linking to the React issues
- No runtime impact — the alias only affects the test environment
Related
Section titled “Related”- facebook/react#24590 — Bug: Support ESM for the use-sync-external-store shim
- facebook/react#25231 — PR: Add exports field to package.json
- Vitest Browser Mode docs