Fixing msw with jest compilitation
Testing with MSW jest and vitest
If you've tried setting up Mock Service Worker (MSW) v2 with Jest, you've probably hit this error:
SyntaxError: Unexpected token 'export'
I spent an afternoon figuring out why, and the answer reveals something important about the JavaScript testing ecosystem in 2024.
The Problem: ESM in a CommonJS World
MSW v2 embraced modern JavaScript by shipping as ESM (ECMAScript Modules). This is great for the ecosystem, but it creates a challenge: most test runners still operate in a CommonJS world.
When Jest tries to run your tests, it encounters ESM code in node_modules
and doesn't know what to do with it. By default, Jest ignores everything in node_modules
- it assumes packages are pre-compiled and ready to run.
But MSW v2 isn't. Neither is its dependency until-async
. They contain raw export
statements that Node's CommonJS loader can't parse.
The Journey to a Solution
Attempt 1: "Just add msw to transformIgnorePatterns"
My first attempt was the standard advice you'll find online:
transformIgnorePatterns: [ 'node_modules/(?!(msw)/)', ]
Result: Still broken. The error was coming from until-async
, a transitive dependency.
Attempt 2: "List all MSW dependencies"
Okay, let me enumerate all the ESM packages:
transformIgnorePatterns: [ 'node_modules/(?!(msw|@mswjs|@open-draft|until-async|...)/)', ]
Result: Still broken. The regex wasn't matching packages in pnpm's nested structure: node_modules/.pnpm/until-async@3.0.2/node_modules/until-async/
Attempt 3: Nuclear option
transformIgnorePatterns: []
Finally! Tests pass. But now Jest is transforming everything in node_modules
, including all of axios, and any other package. This works but doesn't scale.
Attempt 4: The Goldilocks solution
The breakthrough was understanding that the pattern needs to match the package name anywhere in the path, not just immediately after node_modules/
:
transformIgnorePatterns: [ 'node_modules/(?!.*(msw|until-async))', ]
This pattern:
- Works with npm, yarn, and pnpm's nested structures
- Only transforms the packages that actually need it
- Keeps the transform list minimal for maintainability
The Full Jest Configuration
Here's what you actually need for Jest + MSW v2:
// jest.config.js module.exports = { preset: 'ts-jest', testEnvironment: 'node', setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'], // The critical part: transform MSW's ESM dependencies transformIgnorePatterns: [ 'node_modules/(?!.*(msw|until-async))', ], // ts-jest needs to transform both TS and JS files transform: { '^.+\\.tsx?$': 'ts-jest', '^.+\\.jsx?$': ['ts-jest', { tsconfig: { allowJs: true, // Allow ts-jest to process JavaScript }, }], }, };
Why the JS transform matters: By default, ts-jest
only transforms TypeScript files. But MSW's dependencies are JavaScript. The explicit transform
config with allowJs: true
tells TypeScript's compiler to process those JS files too.
The Vitest Alternative
Want to skip all that complexity? Here's the equivalent Vitest config:
// vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', setupFiles: ['./src/setupTests.ts'], include: ['src/**/*.test.ts'], }, });
That's it. No transformIgnorePatterns
. No special handling for ESM. No listing dependencies.
Vitest uses Vite under the hood, which was designed for the ESM era. It handles MSW's modern module format natively.
Performance Comparison
I ran the same two tests with both runners:
Test Runner | Time | Configuration Complexity |
---|---|---|
Jest | 1.08s | High (transform patterns, allowJs, etc.) |
Vitest | 0.38s | Low (basic test config only) |
Vitest is 2.8x faster and requires significantly less configuration.
Key Takeaways
1. The transformIgnorePatterns
breakthrough
The pattern 'node_modules/(?!.*(msw|until-async))'
is the key. It matches package names at any nesting level, so it works with:
- npm/yarn:
node_modules/msw/lib/index.js
- pnpm:
node_modules/.pnpm/until-async@3.0.2/node_modules/until-async/lib/index.js
2. Start minimal, expand when needed
I initially listed 8+ packages in the transform pattern. In reality, only msw
and until-async
needed it.
Principle: Add packages to the transform list only when you encounter actual errors. This keeps your config maintainable and your transform step fast.
3. Vitest is ready for modern JavaScript
Jest was designed in 2014 when CommonJS was the standard. It works great, but it shows its age when dealing with ESM.
Vitest was designed in 2021 with ESM as a first-class citizen. The difference is striking.
4. TypeScript needs allowJs: true
When using ts-jest
, you need to explicitly allow JavaScript transformation:
'^.+\\.jsx?$': ['ts-jest', { tsconfig: { allowJs: true }, }],
Without this, ts-jest skips the ESM files in node_modules, and they remain unparseable.
Recommendations
For new projects: Use Vitest. The simplicity speaks for itself.
For existing Jest projects: Use the optimized pattern shown above. It scales well and works across package managers.
When debugging: If you hit ESM errors, check the actual file path in the error message. That tells you exactly which package needs to be added to your transform pattern.
The Code
Want to see the full setup? Check out the demo repo: jest-with-msw
The repo includes:
- Working Jest configuration with minimal transform pattern
- Working Vitest configuration
- MSW v2 handlers and setup
- TypeScript configuration
- Tests that run with both runners
Final Thoughts
The JavaScript ecosystem is in a transition period. ESM is the future, but CommonJS is still deeply embedded in our tooling.
Jest remains an excellent test runner - mature, feature-rich, and widely adopted. But when dealing with modern ESM packages like MSW v2, you'll need to bridge that gap with careful configuration.
Vitest represents the next generation: built for ESM from day one, faster by default, and refreshingly simple to configure.
The choice depends on your constraints. But after spending an afternoon wrestling with transformIgnorePatterns
, I know which one I'll reach for next time.
What's your experience with Jest and ESM? Have you made the switch to Vitest? Let me know in the comments!