In a React Native app's Jest suite, why does a calendar's untranspiled date library fail on import statements, and how do you fix it?
answer
- node_modules skipped by default
- negative lookahead allowlist
- Metro compiles every module, Jest does not
- override replaces the preset's pattern
- one regex, not several entries
basics
~10 sJest skips transforming files that match transformIgnorePatterns, and the React Native preset only allowlists react-native and @react-native packages, so an ESM date library reaches Node untranspiled. Add the package to the same negative-lookahead regex.
solid answer
~40 sJest compiles a file with `babel-jest` only when its path does not match `transformIgnorePatterns`. The React Native preset's value is `node_modules/(?!((jest-)?react-native|@react-native(-community)?)/)`, so every other package is loaded as-is, and a date library shipped as ES modules hits Node's CommonJS loader and fails on `import`. The app still works on the phone because Metro runs every module through Babel when it bundles. The fix is to widen the allowlist inside **one** regex: `node_modules/(?!((jest-)?react-native|@react-native(-community)?|some-date-lib)/)`. Setting `transformIgnorePatterns` replaces the preset's value, so the React Native entries must stay in it, and splitting entries into separate strings fails because a file is skipped if it matches any of them.
code
javascript · 7 lines// jest.config.js
module.exports = {
preset: '@react-native/jest-preset',
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?|some-date-lib)/)',
],
};go deeper
Recall that Jest does not compile node_modules by default and that the error means an ES-module package reached Node untranspiled.
Explain the preset's negative-lookahead pattern, why Metro hides the problem in the app, and how to add the package without losing the React Native entries.
Show you know the traps: any-match semantics across entries, transitive untranspiled dependencies, pnpm's nested paths, and when mocking the package is a worse trade.
Weigh a growing allowlist's transform cost and fragility against pinning dependencies that ship CommonJS, and set a team convention for where the pattern lives.
## The symptom and where it comes from A calendar screen imports a date-formatting package. On the phone the screen works. In Jest, the suite dies before a single assertion with `SyntaxError: Cannot use import statement outside a module`, and the stack trace points into `node_modules/some-date-lib/`. The error means Node's module loader received a file that still contains ES module syntax (`import`/`export`) and was told to treat it as CommonJS. The package was never compiled on its way into the test. ## How Jest decides what to compile Jest's **transform** step runs source files through a compiler (here `babel-jest`) before executing them. Which files get compiled is controlled by **`transformIgnorePatterns`**: an array of regular expressions matched against each file's full path. If a path matches **any** pattern, Jest loads it untouched. The React Native preset sets: ```js transformIgnorePatterns: [ 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)/)', ], ``` Read it as: "ignore anything under `node_modules/` **unless** the next path segment is `react-native`, `jest-react-native`, or the `@react-native` or `@react-native-community` scope." That exception exists because React Native itself ships Flow, JSX and ES modules. Every other package, including your date library, is assumed to be published as ready-to-run CommonJS. Two details of the regex catch people: - The allowlisted name must be followed by `/`, so `react-native-gesture-handler/` is **not** covered by the `react-native` entry; library docs for such packages tell you to add them explicitly. - `@react-native(-community)?/` covers whole scopes, so `@react-native-community/netinfo` is already compiled. ## Why the app runs but the test does not When Metro builds the bundle, it passes every module, dependencies included, through its Babel transformer with `@react-native/babel-preset`. An ES-module dependency is therefore compiled at bundle time and never reaches the engine raw. Jest has no such rule for `node_modules`, so the same package works in the app and fails in the test. ## The fix 1. Identify the untranspiled package from the stack trace (and any untranspiled package **it** imports, which fails next). 2. Add it to the lookahead alternatives of the single pattern. 3. Keep the React Native entries, because your `transformIgnorePatterns` replaces the preset's rather than merging with it. 4. Re-run the suite; if another dependency fails the same way, repeat. | Attempt | Result | |---|---| | `['node_modules/(?!((jest-)?react-native\|@react-native(-community)?\|some-date-lib)/)']` | Works: one lookahead, all exceptions | | `['node_modules/(?!some-date-lib/)']` | Breaks React Native itself: the preset's exceptions are gone | | `['node_modules/(?!react-native/)', 'node_modules/(?!some-date-lib/)']` | Neither is compiled: each file matches the other pattern | **Package managers that nest paths** change the path being matched. With pnpm, files live under `node_modules/.pnpm/<name>@<version>/node_modules/<name>/`, so the first `node_modules/` is followed by `.pnpm`, not the package name; Expo's unit-testing guide shows a pnpm variant of the pattern that allows an optional `.pnpm/` segment for this reason. ## Recognising the same failure in other forms The message depends on which untranspiled syntax Node meets first, but the cause is the same: - `Cannot use import statement outside a module` or an unexpected `export` token: ES module syntax. - An unexpected token at a `<`: JSX left in a published file. - An unexpected token at a type annotation: Flow or TypeScript syntax left in a published file. In every case the stack trace names the file, and the file's path shows which package to add to the allowlist. If the path is inside `react-native` or `@react-native/*` itself, the preset's entries were lost from an override. ## Alternatives and their costs - **Widening the allowlist** is the usual answer. Each extra package compiled costs some transform time on the first run; Jest caches transformed files afterwards. - **Replacing the dependency with a mock** avoids compiling it, but then the test no longer exercises the real date logic, which for a calendar is usually the point. - **Switching Jest to native ESM** is a larger change to the whole runner and is not what React Native's preset is designed around. ## What to say in an interview The strong answer names the mechanism in one breath: Jest does not compile `node_modules`, the preset allowlists only React Native's own packages, Metro compiles everything so the app hides the problem, and the fix is one combined negative lookahead that keeps the preset's entries. Mentioning the "matches any pattern" trap and the pnpm path shows you have fixed this more than once.
- Why does adding a second string to transformIgnorePatterns for the date library break React Native's own files?Jest skips a file when its path matches any pattern in the array. A pattern that exempts only `some-date-lib` matches every `react-native` file, so React Native is no longer compiled, and the pattern that exempts `react-native` matches the date library. All exceptions must sit in one lookahead joined with `|`.
- The date library is fixed, but now its own dependency fails with the same SyntaxError; why?The allowlist is per package path, not per import graph. A transitive dependency that is also published untranspiled matches the ignore pattern under its own `node_modules/<name>/` path, so it must be added to the same lookahead.
- In a pnpm workspace the widened pattern still fails; what is different about the path?pnpm stores packages under `node_modules/.pnpm/<name>@<version>/node_modules/<name>/`. The first `node_modules/` segment is followed by `.pnpm`, which is not in the allowlist, so the whole path matches the ignore pattern. The pattern needs an optional `.pnpm/` segment, as Expo's testing guide shows.
transformIgnorePatterns is a customs desk with a short list of passports that get inspected and repacked; every other bag walks straight through. A bag that needs repacking but is not on the list arrives unusable at the other side.
saying these in an interview costs you the question
- Adding a separate transformIgnorePatterns entry per package is the safe way to allowlist
- Jest merges your transformIgnorePatterns with the preset's value
- The default react-native entry also covers react-native-gesture-handler
- If the app bundles fine, the dependency must already be CommonJS
- The fix is to add the library to transform instead of transformIgnorePatterns