In Metro 0.87, which config file names and formats can a React Native project use, and how does Metro determine projectRoot?
answer
- js, cjs, mjs, then ts, cts, mts
- Node's native type stripping loads TS
- .config/metro.* and package.json metro key
- YAML and .es6 configs removed
- projectRoot: the config file's folder
basics
~20 sMetro 0.87 finds metro.config.js/.cjs/.mjs, then .ts/.cts/.mts, then .json, then the same names under .config/, then a metro key in package.json; --config overrides the search. TypeScript loads through Node's type stripping, YAML is gone, and projectRoot defaults to the config file's folder.
solid answer
~40 sMetro 0.87 searches in priority order: `metro.config.js`, `.cjs` or `.mjs`; `metro.config.ts`, `.cts` or `.mts`; `metro.config.json`; the same variants as `.config/metro.*`; and finally a `metro` key in `package.json`. `--config <path>` skips the search. TypeScript configs are loaded with Node's native type stripping, which works out of the box for erasable TypeScript on Node 22.18 or later and 24 or later. Metro 0.87 made TS and ESM configs stable and removed YAML configs and `.es6` files. When Metro loads a config it treats the file's directory as the project root, or the parent when the file sits in `.config/`, and the React Native template passes `__dirname` to `getDefaultConfig`, so `projectRoot` is the app folder. Files outside it must be reachable through `watchFolders`. Expo CLI does not load Metro configs from outside the repository.
code
typescript · 11 lines// metro.config.mts (React Native 0.87, Node 22.18+)
import rnMetroConfig from '@react-native/metro-config';
import type {MetroConfig} from 'metro-config';
const {getDefaultConfig, mergeConfig} = rnMetroConfig;
const config: MetroConfig = {
server: {port: 8082},
};
export default mergeConfig(getDefaultConfig(import.meta.dirname), config);go deeper
Recall that metro.config.js is the default file and that projectRoot is the app folder the config sits in.
List the supported formats and their priority, explain that TypeScript configs rely on Node's type stripping, and know YAML is gone.
Diagnose upgrade and CI failures caused by removed formats or old Node versions, and set projectRoot or watchFolders deliberately in multi-package repositories.
Standardise config format and Node version across the repository so tooling behaves the same on every developer machine and CI runner.
## Where Metro looks When a CLI starts Metro without an explicit path, Metro walks up from the working directory and, in each folder, checks these names in priority order: 1. `metro.config.js`, `metro.config.cjs`, `metro.config.mjs` (CommonJS or ES modules); 2. `metro.config.ts`, `metro.config.cts`, `metro.config.mts` (TypeScript); 3. `metro.config.json`; 4. the same set inside a `.config` folder: `.config/metro.js` up to `.config/metro.json`; 5. a top-level **`metro`** key in `package.json`. `--config <path>`, accepted by `npx react-native start` and `bundle`, skips the search entirely. The React Native CLI insists that a config exists: with none it stops with "No Metro config found". Expo CLI treats the file as optional and falls back to its own defaults. ## TypeScript and ESM configs Metro 0.87, the version React Native 0.87 uses, made **TypeScript and ES module configs stable**. A `.ts`, `.cts` or `.mts` config is loaded with **Node's native TypeScript support**, not with a bundled compiler: - on Node 22.18.0 or later, or 24.0.0 or later, **erasable** TypeScript (type annotations, `import type`) runs out of the box; - on older Node versions the `--experimental-strip-types` flag is needed; - non-erasable syntax such as `enum` or parameter properties is not supported by type stripping, so keep the config to plain annotations. A typed config imports its type from `metro-config` (`import type {MetroConfig} from 'metro-config'`) and uses `export default`. ## What was removed | Format | Status in Metro 0.87 | |---|---| | `metro.config.js` / `.cjs` / `.mjs` | supported | | `metro.config.ts` / `.cts` / `.mts` | supported, stable | | `metro.config.json`, `package.json` `metro` key | supported | | YAML config | removed; Metro throws an error asking you to migrate to JavaScript | | `.es6` config files | removed | An old project that still has a YAML Metro config fails on upgrade with that migration message. ## How projectRoot is decided **`projectRoot`** is the folder Metro treats as the root of the app: the starting point for resolution and, by default, the folder it watches. Three things set it: - When Metro loads a config file, it computes defaults for the **directory containing the file**. If the file lives in `.config/`, the parent directory is used instead, because a `.config` folder is assumed to sit inside the project root. - The template passes **`__dirname`** to `getDefaultConfig`, which sets `projectRoot` to the config file's folder explicitly. - A `projectRoot` key in the config, or the `--projectRoot` CLI flag, overrides both. Files outside `projectRoot` are invisible to Metro unless their folders are listed in **`watchFolders`**. That is why monorepos either widen what Metro can see or rely on a framework that configures it, as `expo/metro-config` does. ## Choosing a format - Keep **`metro.config.js`** when the rest of the toolchain is CommonJS and the config is short; it is what both templates generate. - Use **`metro.config.mts`** or **`.ts`** when you want type checking on config keys and your Node version supports type stripping. - Use **`.config/metro.*`** only if the repository already collects tool configs there. - Avoid the `package.json` key for anything non-trivial: it cannot hold functions such as `getTransformOptions` or `resolveRequest`. ## Common mistakes - Writing `module.exports` in a `metro.config.mjs` or `.mts` file: ES module configs must use `export default`. - Using `__dirname` in an ES module config: it does not exist there; `import.meta.dirname` does. - Leaving two config files in one folder, for example an old `metro.config.json` next to a new `metro.config.js`: only the first in priority order is loaded, and the other silently does nothing. - Moving the config into `.config/` and expecting `projectRoot` to change: Metro deliberately treats the parent as the root. ## Expo specifics Expo's guide adds two constraints: it does not load Metro configs from YAML files, and it does not load a Metro config located outside the repository. It generates `metro.config.js` via `npx expo customize metro.config.js`. ## Why interviewers ask This is a niche question, usually asked after an upgrade story: a YAML config that stopped loading, a TypeScript config failing on an old Node version, or a monorepo where Metro could not see a sibling package because `projectRoot` was the app folder.
- After upgrading to React Native 0.87, Metro stops with a message about YAML config. Why?Metro 0.87 removed support for YAML config files. Metro detects the old file and throws an error asking you to migrate to a JavaScript config such as `metro.config.js`. Move the settings into a JS or TS config that extends `@react-native/metro-config`.
- A metro.config.ts fails to load on a CI machine but works locally. What would you check first?The Node version. Metro loads TypeScript configs with Node's native type stripping, which works without flags on Node 22.18 or later and 24 or later. An older CI image needs `--experimental-strip-types` or a Node upgrade, and the config must use only erasable syntax.
saying these in an interview costs you the question
- Metro compiles a metro.config.ts with the project's Babel preset
- YAML Metro configs still load in Metro 0.87
- projectRoot is always the folder where the CLI is run
- Files outside projectRoot are watched automatically
- A package.json metro key can hold resolveRequest functions