skip to content

In a React Native app, how does Metro decide whether an imported file is source code or an asset, and what does require('./logo.png') return?

level: juniorimportance: must knowfreq 50%

answer

  1. two extension lists in resolver
  2. sourceExts get transformed into JavaScript
  3. assetExts get copied, not parsed
  4. @2x and @3x density variants
  5. require of an image returns a number

basics

~20 s

Metro classifies a file by its extension: resolver.sourceExts (js, jsx, json, ts, tsx by default) are transformed and bundled as code, resolver.assetExts (png, jpg, ttf, mp4 and more) are copied as assets. require('./logo.png') returns a numeric asset registry ID that Image resolves.

solid answer

~40 s

Metro's resolver keeps two lists. Files whose extension is in `resolver.sourceExts`, by default `js`, `jsx`, `json`, `ts` and `tsx`, are source: Metro transforms them with Babel and puts them in the JavaScript bundle. Files whose extension is in `resolver.assetExts`, a long default list of images, video, audio, fonts and a few documents, are assets: Metro does not parse them, it registers them and copies the file into the app or serves it from the dev server. So `require('./logo.png')` returns a **number**, an ID in React Native's asset registry, which `<Image source={...}>` turns into the right file, including `[email protected]` or `[email protected]` for the screen density. An extension in neither list fails to resolve, and one moved from assets to sources needs a transformer that can turn it into JavaScript.

code

javascript · 9 lines
javascript
// metro.config.js (Expo project)
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

// Treat 3D models as assets so require('./model.glb') resolves.
config.resolver.assetExts.push('glb');

module.exports = config;

go deeper

for a junior

Recall the two lists, what each default holds, and that an image require returns a number that Image understands.

for a middle

Explain density suffixes, why a runtime-built require path fails, and how Expo's defaults differ from plain Metro's.

for a senior

Diagnose a new file type that will not resolve, and know when moving an extension to sourceExts also needs a transformer.

for a principal

Set conventions for assets in a large app: where they live, which formats are allowed, and how their size is budgeted.

## Metro sorts files by extension **Metro** is the bundler behind React Native and Expo. When code says `import Button from './Button'` or `require('./logo.png')`, Metro's **resolver** turns that string into a file on disk, and the file's extension decides what happens next. Two resolver options hold the rules: | Option | Default | What Metro does with a match | |---|---|---| | `resolver.sourceExts` | `['js', 'jsx', 'json', 'ts', 'tsx']` | Transforms it with Babel into JavaScript and includes it in the bundle | | `resolver.assetExts` | Images (`png`, `jpg`, `gif`, `webp`, `svg`, ...), video (`mp4`, `mov`, ...), audio (`mp3`, `wav`, ...), fonts (`ttf`, `otf`), `pdf`, `html`, `zip` and more | Registers it as an asset and copies the file; never parses it | Expo's `getDefaultConfig` from `expo/metro-config` starts from these and extends them: it adds `cjs` (and `css`, `scss`, `sass` when CSS support is on) to `sourceExts`, adds `heic`, `avif` and `db` to `assetExts`, and removes from `assetExts` anything that is also a source extension. ## What an asset import returns For an asset, Metro generates a tiny module that registers the file's metadata (name, type, dimensions, available scales) with React Native's **asset registry**. The import evaluates to the registry's return value, a **number**: ```tsx const logo = require('./assets/logo.png'); // typeof logo === 'number' <Image source={logo} /> ``` `Image` looks that ID up and loads the right file. In development it comes from the Metro server; in a release build the file is copied into the app package. ## Density variants come for free For images, Metro also looks for **scale suffixes** listed in `resolver.assetResolutions` (default `['1', '1.5', '2', '3', '4']`). Place `logo.png`, `[email protected]` and `[email protected]` side by side, import `./logo.png`, and each device gets the variant closest to its pixel density. Code never names the `@2x` file. ## When an extension is in the wrong list 1. **In neither list**: the import fails to resolve. Adding a 3D model format such as `glb` means pushing it onto `assetExts`. 2. **Moved from assets to sources**: Metro will try to parse the file as JavaScript. The classic case is SVG as a React component, which needs a custom transformer as well as moving `svg` from `assetExts` to `sourceExts`. 3. **In both lists**: when an import names the file with its extension, Metro checks `assetExts` first, so the file behaves as an asset; Expo's default removes this overlap for you. ## Changing the lists safely Both options are plain arrays on `config.resolver`, and assigning a new array **replaces** the defaults rather than merging with them. Three habits avoid surprises: - **Append, do not assign**: `config.resolver.assetExts.push('glb')` keeps every default image and font type; `config.resolver.assetExts = ['glb']` silently breaks every PNG in the app. - **Move, do not duplicate**: when an extension changes lists, filter it out of one array and push it onto the other in the same config. - **Restart Metro** after changing either list, because the config file is read when the dev server starts. In a bare React Native project these edits go into the object passed to `mergeConfig` alongside `getDefaultConfig` from `@react-native/metro-config`; in an Expo project they go onto the object returned by `getDefaultConfig` from `expo/metro-config`. ## Only what you import is shipped Metro does not copy every file with an asset extension. An asset reaches the bundle only if some module requires it, which is why `require` with a string built at runtime, such as `require('./flags/' + code + '.png')`, fails with an "Invalid call" error: Metro resolves import strings at build time and needs a literal. ## Interview checkpoints - `json` is a **source** extension, so `require('./data.json')` returns the parsed object, not an asset ID. - `svg` is an **asset** by default: without extra configuration it is an image file reference, not a component. - Fonts (`ttf`, `otf`) are assets too; loading them into the text system is a separate runtime step.

  • Why does require('./flags/' + code + '.png') fail in React Native?
    Metro resolves every `require` and `import` string at build time to decide what goes into the bundle. A string assembled at runtime cannot be resolved ahead of time, so Metro rejects it. The usual fix is an explicit map of literal `require` calls, one per flag, and a lookup by code.
  • What do you change to import an .svg file as a component instead of an image?
    Two things: move `svg` out of `resolver.assetExts` and into `resolver.sourceExts`, so the resolver treats it as code, and configure a transformer that converts SVG markup into a JavaScript component, since Babel alone cannot parse it. Changing only the resolver lists makes Metro fail to parse the file.

saying these in an interview costs you the question

  • Thinks require of a PNG returns a file path or URI string
  • Believes Metro bundles every image in the project folder
  • Expects to reference [email protected] explicitly for Retina screens
  • Replaces assetExts with a one-item list and loses the defaults
  • Assumes adding svg to sourceExts alone makes SVGs importable components