Skip to main content
Metro is the JavaScript bundler for React Native. Expo provides enhanced Metro configuration with additional features and optimizations.

Getting Started

Create a metro.config.js file in your project root:
// Learn more: https://docs.expo.dev/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

module.exports = config;

Default Configuration

Expo’s default Metro config includes:
  • Asset extensions for images, fonts, and other files
  • Source extensions for .js, .jsx, .ts, .tsx, .json
  • Transformer configuration for React Native and web
  • Resolver configuration for React Native platform resolution
  • Support for package.json exports field
  • CSS and styled-components support for web
  • Environment variable support

Configuration Options

Resolver Options

Control how Metro resolves modules.

assetExts

resolver.assetExts
string[]
File extensions to treat as assets.
const config = getDefaultConfig(__dirname);

config.resolver.assetExts.push('db', 'mp3', 'ttf', 'obj', 'png', 'jpg');

module.exports = config;

sourceExts

resolver.sourceExts
string[]
File extensions to treat as source code.
const config = getDefaultConfig(__dirname);

config.resolver.sourceExts.push('mjs', 'cjs');

module.exports = config;
When adding to sourceExts, ensure the extension isn’t already in assetExts.

platforms

resolver.platforms
string[]
Supported platform extensions.
config.resolver.platforms = ['ios', 'android', 'web', 'native'];

nodeModulesPaths

resolver.nodeModulesPaths
string[]
Additional directories to search for modules.
const path = require('path');

config.resolver.nodeModulesPaths = [
  path.resolve(__dirname, 'node_modules'),
  path.resolve(__dirname, '../node_modules'),
];

resolveRequest

resolver.resolveRequest
function
Custom resolution function.
config.resolver.resolveRequest = (context, moduleName, platform) => {
  // Custom resolution logic
  if (moduleName === 'my-custom-module') {
    return {
      filePath: path.resolve(__dirname, 'custom-module.js'),
      type: 'sourceFile',
    };
  }
  
  // Fall back to default resolution
  return context.resolveRequest(context, moduleName, platform);
};

unstable_enablePackageExports

resolver.unstable_enablePackageExports
boolean
default:"true"
Enable support for package.json exports field.
config.resolver.unstable_enablePackageExports = true;

unstable_conditionNames

resolver.unstable_conditionNames
string[]
Condition names for package exports resolution.
config.resolver.unstable_conditionNames = ['require', 'import', 'react-native'];

Transformer Options

Control how Metro transforms code.

babelTransformerPath

transformer.babelTransformerPath
string
Path to the Babel transformer.
config.transformer.babelTransformerPath = require.resolve(
  './custom-babel-transformer.js'
);

minifierPath

transformer.minifierPath
string
Path to the minifier. Defaults to Metro’s minifier.
config.transformer.minifierPath = require.resolve('metro-minify-terser');

minifierConfig

transformer.minifierConfig
object
Configuration for the minifier.
config.transformer.minifierConfig = {
  compress: {
    drop_console: true,
  },
};

getTransformOptions

transformer.getTransformOptions
function
Function returning transform options per bundle.
config.transformer.getTransformOptions = async () => ({
  transform: {
    experimentalImportSupport: false,
    inlineRequires: true,
  },
});

assetPlugins

transformer.assetPlugins
string[]
Asset plugins to use during transformation.
config.transformer.assetPlugins = [
  require.resolve('./my-asset-plugin.js'),
];

Server Options

Configure the Metro development server.

port

server.port
number
default:"8081"
Port to run Metro on.
config.server.port = 8082;

enhanceMiddleware

server.enhanceMiddleware
function
Add custom middleware to the Metro server.
config.server.enhanceMiddleware = (middleware, server) => {
  return (req, res, next) => {
    // Custom middleware logic
    if (req.url === '/custom-endpoint') {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ custom: true }));
      return;
    }
    
    return middleware(req, res, next);
  };
};

Watcher Options

Configure file watching behavior.

watchFolders

watchFolders
string[]
Additional folders for Metro to watch.
const path = require('path');

config.watchFolders = [
  path.resolve(__dirname, '../shared-package'),
];
Useful for monorepos where packages are outside the project directory.

hasteMapModulePath

hasteMapModulePath
string
Path to custom Haste map implementation.

Expo-Specific Features

Environment Variables

Expo automatically supports environment variables:
// Access in your app
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
Only variables prefixed with EXPO_PUBLIC_ are available in your app.

CSS Support (Web)

Expo’s Metro config includes CSS support for web:
import './styles.css';

Asset Handling

Import assets directly:
import logo from './assets/logo.png';

<Image source={logo} />

Platform-Specific Extensions

Metro resolves platform-specific files automatically:
  • Button.ios.tsx - iOS only
  • Button.android.tsx - Android only
  • Button.web.tsx - Web only
  • Button.native.tsx - iOS and Android
  • Button.tsx - All platforms (fallback)

Common Customizations

Adding SVG Support

npx expo install react-native-svg
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');

const config = getDefaultConfig(__dirname);

config.transformer.babelTransformerPath = require.resolve(
  'react-native-svg-transformer'
);

config.resolver.assetExts = config.resolver.assetExts.filter(
  (ext) => ext !== 'svg'
);

config.resolver.sourceExts.push('svg');

module.exports = config;

Monorepo Configuration

const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '../..');

const config = getDefaultConfig(projectRoot);

// Watch all files in the monorepo
config.watchFolders = [workspaceRoot];

// Let Metro know where to resolve packages
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(workspaceRoot, 'node_modules'),
];

module.exports = config;

Web-Specific Configuration

const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

// Add web-specific extensions
config.resolver.sourceExts.push('web.js', 'web.jsx', 'web.ts', 'web.tsx');

module.exports = config;

Custom Babel Configuration

Metro uses babel.config.js for transformation:
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      // Your custom plugins
      'react-native-reanimated/plugin',
    ],
  };
};
See Babel Preset Reference for more.

Extending the Babel Transformer

Create a custom transformer:
// custom-babel-transformer.js
const expoTransformer = require('@expo/metro-config/babel-transformer');

module.exports.transform = function (src, filename, options) {
  // Custom transformation
  if (filename.endsWith('.custom.js')) {
    // Do custom transformation
  }
  
  // Use Expo's transformer
  return expoTransformer.transform(src, filename, options);
};
Then reference it:
const config = getDefaultConfig(__dirname);

config.transformer.babelTransformerPath = require.resolve(
  './custom-babel-transformer.js'
);

module.exports = config;

Performance Optimization

Cache Configuration

config.cacheStores = [
  new FileStore({
    root: path.join(__dirname, '.cache/metro'),
  }),
];

Worker Configuration

config.maxWorkers = 4; // Limit concurrent workers

Transform Caching

config.transformer.enableBabelRCLookup = false; // Skip .babelrc lookups
config.transformer.enableBabelRuntime = true; // Use Babel runtime

Troubleshooting

Clear Metro Cache

npx expo start --clear

Reset Metro

rm -rf node_modules/.cache/metro
npx expo start --clear

Debug Metro Configuration

Log the full configuration:
const config = getDefaultConfig(__dirname);

console.log(JSON.stringify(config, null, 2));

module.exports = config;

TypeScript Support

Metro automatically supports TypeScript with no configuration:
// Works out of the box
import { Component } from './Component';
Configure TypeScript in tsconfig.json, not Metro.

Source Maps

Metro generates source maps automatically in development. For production:
config.serializer.sourceMapUrl = 'http://localhost:8081/index.map';