Definition
When you run tests with Jest and some npm package is distributed only in the modern format (ESM), so import throws an error and won't load, this is the workaround where you copy that package's code into a TS file inside your own repo and make the test use that copy instead.
Let's unpack the terms first: ESM (ECMAScript Modules) is the modern JavaScript module format using import/export; CJS (CommonJS) is the older format using require, and Jest's default environment is CJS. Vendoring is copying external package code into your own project; a shim is a thin piece of replacement code inserted in place of the original. moduleNameMapper is the path-substitution setting that tells Jest "when this import name appears, actually load this file."
To sum up, a Jest ESM vendor shim is the technique where, in a Jest (ts-jest—the tool that runs TypeScript in Jest, CommonJS preset) test, instead of importing an ESM-only npm package directly, you vendor the package source (or a thin wrapper) into a TypeScript file inside the repo and then reroute the import path to that file with moduleNameMapper.
Why It's Needed
Modern npm packages are increasingly distributed only as a single ESM ("type": "module") build. But Jest's default pipeline tries to run the code inside node_modules as-is without transforming (transform) it to CJS, so when it meets ESM syntax it throws SyntaxError: Cannot use import statement outside a module.
There is also the approach of adding the package to transformIgnorePatterns (the Jest setting that specifies which packages to transform-and-read), but the nested paths / per-version paths where pnpm/yarn install packages differ by environment, so it's easy to become flaky between CI and local. A vendor shim eliminates that path problem entirely.
How It Works
- Identify the failing import
from 'esm-only-pkg'. - Write a TS shim at
__tests__/vendor/esm-only-pkg.tsorsrc/ordering/vendor/(export only the needed API). jest.config:
- ts-jest transforms the shim → require-able in the CJS test runtime.
- For vendored upstream code, if there are lint/tsc issues, file-level suppress + a comment noting the replacement path.
| Approach | Pro | Con |
|---|---|---|
| transformIgnorePatterns | upstream as-is | nested path fragile |
| Jest experimental ESM | idiomatic import | config/speed burden |
| vendor shim | stable/predictable | sync/license responsibility |
Practical Application
- The shim should export only the surface the tests/bundle actually use — minimize forking the whole package.
- If pre-commit turbo lint checks even the vendored files, unrelated upstream lint can block the commit → file-header suppression or a lint-ignore-path policy.
isolatedModules: true(ts-jest) skips type checking — afterpnpm testis green, verifytsc --noEmitseparately.
Tradeoffs
- A vendor shim is stable, but requires manual sync on upstream updates.
- Overusing
@ts-nocheckis a type-safety illusion — wrap new code in a typed wrapper.
When Not to Use It
- When the package provides a CJS dual build and Jest already imports it stably.
- When a migration to move the whole project to Vitest ESM is underway instead of a shim (duplicate investment).
Common Mistakes
- Changing only the transformIgnorePatterns regex, yet still failing on the pnpm
.pnpm/path. - test green = ship — leaving type holes under a vendor
@ts-nocheck. - Using dynamic
import()in a test without a shim — Jest mock timing issues.
Related Concepts
- fractional-index-ordering — a common motive for using an ESM-only ordering library in Jest