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
File extensions to treat as assets.
const config = getDefaultConfig(__dirname);
config.resolver.assetExts.push('db', 'mp3', 'ttf', 'obj', 'png', 'jpg');
module.exports = config;
sourceExts
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.
Supported platform extensions.
config.resolver.platforms = ['ios', 'android', 'web', 'native'];
nodeModulesPaths
resolver.nodeModulesPaths
Additional directories to search for modules.
const path = require('path');
config.resolver.nodeModulesPaths = [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, '../node_modules'),
];
resolveRequest
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
Enable support for package.json exports field.
config.resolver.unstable_enablePackageExports = true;
unstable_conditionNames
resolver.unstable_conditionNames
Condition names for package exports resolution.
config.resolver.unstable_conditionNames = ['require', 'import', 'react-native'];
Control how Metro transforms code.
transformer.babelTransformerPath
Path to the Babel transformer.
config.transformer.babelTransformerPath = require.resolve(
'./custom-babel-transformer.js'
);
minifierPath
Path to the minifier. Defaults to Metro’s minifier.
config.transformer.minifierPath = require.resolve('metro-minify-terser');
minifierConfig
transformer.minifierConfig
Configuration for the minifier.
config.transformer.minifierConfig = {
compress: {
drop_console: true,
},
};
transformer.getTransformOptions
Function returning transform options per bundle.
config.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
});
assetPlugins
Asset plugins to use during transformation.
config.transformer.assetPlugins = [
require.resolve('./my-asset-plugin.js'),
];
Server Options
Configure the Metro development server.
port
config.server.port = 8082;
enhanceMiddleware
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
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
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:
Asset Handling
Import assets directly:
import logo from './assets/logo.png';
<Image source={logo} />
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.
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;
Cache Configuration
config.cacheStores = [
new FileStore({
root: path.join(__dirname, '.cache/metro'),
}),
];
Worker Configuration
config.maxWorkers = 4; // Limit concurrent workers
config.transformer.enableBabelRCLookup = false; // Skip .babelrc lookups
config.transformer.enableBabelRuntime = true; // Use Babel runtime
Troubleshooting
Clear Metro Cache
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';