metro-bundler

SKILL.md

Metro Bundler Expert

Comprehensive expertise in React Native's Metro bundler, including configuration, optimization, custom transformers, caching strategies, and troubleshooting common bundling issues.

What I Know

Metro Fundamentals

What is Metro?

  • JavaScript bundler for React Native
  • Transforms and bundles JavaScript modules
  • Handles assets (images, fonts, etc.)
  • Provides fast refresh for development
  • Generates source maps for debugging

Key Concepts

  • Transformer: Converts source code (TypeScript, JSX) to JavaScript
  • Resolver: Locates modules in the file system
  • Serializer: Combines modules into bundles
  • Cache: Speeds up subsequent builds

Metro Configuration

Basic metro.config.js

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

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

module.exports = config;

Custom Configuration

const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

const defaultConfig = getDefaultConfig(__dirname);

const config = {
  transformer: {
    // Enable Babel transformer
    babelTransformerPath: require.resolve('react-native-svg-transformer'),

    // Source map options
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },

  resolver: {
    // Custom asset extensions
    assetExts: defaultConfig.resolver.assetExts.filter(ext => ext !== 'svg'),

    // Custom source extensions
    sourceExts: [...defaultConfig.resolver.sourceExts, 'svg', 'cjs'],

    // Node module resolution
    nodeModulesPaths: [
      './node_modules',
      '../../node_modules',  // For monorepos
    ],

    // Custom platform-specific extensions
    platforms: ['ios', 'android', 'native'],
  },

  server: {
    // Custom port
    port: 8081,

    // Enhanced logging
    enhanceMiddleware: (middleware) => {
      return (req, res, next) => {
        console.log(`Metro request: ${req.url}`);
        return middleware(req, res, next);
      };
    },
  },

  watchFolders: [
    // Watch external folders (monorepos)
    path.resolve(__dirname, '..', 'shared-library'),
  ],

  resetCache: true,  // Reset cache on start (dev only)
};

module.exports = mergeConfig(defaultConfig, config);

Optimization Strategies

Inline Requires

// metro.config.js
module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        inlineRequires: true,  // Lazy load modules (faster startup)
      },
    }),
  },
};

// Before (eager loading)
import UserProfile from './UserProfile';
import Settings from './Settings';

function App() {
  return (
    <View>
      {showProfile ? <UserProfile /> : <Settings />}
    </View>
  );
}

// After inline requires (lazy loading)
function App() {
  return (
    <View>
      {showProfile ?
        <require('./UserProfile').default /> :
        <require('./Settings').default />
      }
    </View>
  );
}

Bundle Splitting (Experimental)

// metro.config.js
module.exports = {
  serializer: {
    createModuleIdFactory: () => {
      // Generate stable module IDs for better caching
      return (path) => {
        return require('crypto')
          .createHash('sha1')
          .update(path)
          .digest('hex')
          .substring(0, 8);
      };
    },
  },
};

Asset Optimization

// metro.config.js
module.exports = {
  transformer: {
    // Minify assets
    minifierPath: require.resolve('metro-minify-terser'),
    minifierConfig: {
      compress: {
        drop_console: true,  // Remove console.log in production
        drop_debugger: true,
      },
      output: {
        comments: false,
      },
    },
  },

  resolver: {
    // Optimize asset resolution
    assetExts: [
      'png', 'jpg', 'jpeg', 'gif', 'webp',  // Images
      'mp3', 'wav', 'm4a', 'aac',           // Audio
      'mp4', 'mov',                          // Video
      'ttf', 'otf', 'woff', 'woff2',        // Fonts
    ],
  },
};

Custom Transformers

SVG Transformer

# Install
npm install react-native-svg react-native-svg-transformer

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

const config = getDefaultConfig(__dirname);

config.transformer = {
  ...config.transformer,
  babelTransformerPath: require.resolve('react-native-svg-transformer'),
};

config.resolver = {
  ...config.resolver,
  assetExts: config.resolver.assetExts.filter(ext => ext !== 'svg'),
  sourceExts: [...config.resolver.sourceExts, 'svg'],
};

module.exports = config;
// Usage in code
import Logo from './assets/logo.svg';

function App() {
  return <Logo width={120} height={40} />;
}

Multiple File Extensions

// metro.config.js
module.exports = {
  resolver: {
    // Add .web.js, .native.js for platform-specific code
    sourceExts: ['js', 'json', 'ts', 'tsx', 'jsx', 'web.js', 'native.js'],

    // Custom resolution logic
    resolveRequest: (context, moduleName, platform) => {
      if (moduleName === 'my-module') {
        // Custom module resolution
        return {
          filePath: '/custom/path/to/module.js',
          type: 'sourceFile',
        };
      }

      return context.resolveRequest(context, moduleName, platform);
    },
  },
};

Caching Strategies

Cache Management

# Clear Metro cache
npx react-native start --reset-cache
npm start -- --reset-cache  # Expo

# Or manually
rm -rf $TMPDIR/react-*
rm -rf $TMPDIR/metro-*

# Clear watchman cache
watchman watch-del-all

# Clear all caches (nuclear option)
npm run clear  # If configured in package.json

Cache Configuration

// metro.config.js
const path = require('path');

module.exports = {
  cacheStores: [
    // Custom cache directory
    {
      get: (key) => {
        const cachePath = path.join(__dirname, '.metro-cache', key);
        // Implement custom cache retrieval
      },
      set: (key, value) => {
        const cachePath = path.join(__dirname, '.metro-cache', key);
        // Implement custom cache storage
      },
    },
  ],

  // Reset cache on config changes
  resetCache: process.env.RESET_CACHE === 'true',
};

Monorepo Setup

Workspaces Configuration

// metro.config.js (in app directory)
const path = require('path');
const { getDefaultConfig } = require('@react-native/metro-config');

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

const config = getDefaultConfig(projectRoot);

// Watch workspace directories
config.watchFolders = [workspaceRoot];

// Resolve modules from workspace
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(workspaceRoot, 'node_modules'),
];

// Avoid hoisting issues
config.resolver.disableHierarchicalLookup = false;

module.exports = config;

Symlink Handling

// metro.config.js
module.exports = {
  resolver: {
    // Enable symlink support
    unstable_enableSymlinks: true,

    // Resolve symlinked packages
    resolveRequest: (context, moduleName, platform) => {
      const resolution = context.resolveRequest(context, moduleName, platform);

      if (resolution && resolution.type === 'sourceFile') {
        // Resolve real path for symlinks
        const realPath = require('fs').realpathSync(resolution.filePath);
        return {
          ...resolution,
          filePath: realPath,
        };
      }

      return resolution;
    },
  },
};

Common Issues & Solutions

"Unable to resolve module"

# Solution 1: Clear cache
npx react-native start --reset-cache

# Solution 2: Reinstall dependencies
rm -rf node_modules
npm install

# Solution 3: Check import paths
# Ensure case-sensitive imports match file names
import UserProfile from './userProfile';  # ❌ Wrong case
import UserProfile from './UserProfile';  # ✅ Correct

# Solution 4: Add to metro.config.js
module.exports = {
  resolver: {
    extraNodeModules: {
      'my-module': path.resolve(__dirname, 'node_modules/my-module'),
    },
  },
};

"Port 8081 already in use"

# Find and kill process
lsof -ti:8081 | xargs kill -9

# Or start on different port
npx react-native start --port 8082

# Update code to use new port
adb reverse tcp:8082 tcp:8082  # Android

"Invariant Violation: Module AppRegistry is not a registered callable module"

# Clear all caches
rm -rf $TMPDIR/react-*
rm -rf $TMPDIR/metro-*
watchman watch-del-all
rm -rf node_modules
npm install
npx react-native start --reset-cache

"TransformError: ... SyntaxError"

// Add Babel plugin to metro.config.js
module.exports = {
  transformer: {
    babelTransformerPath: require.resolve('./customBabelTransformer.js'),
  },
};

// customBabelTransformer.js
module.exports = require('metro-react-native-babel-preset');

When to Use This Skill

Ask me when you need help with:

  • Configuring Metro bundler
  • Custom transformers (SVG, images, etc.)
  • Optimizing bundle size and startup time
  • Setting up monorepo with Metro
  • Troubleshooting "Unable to resolve module" errors
  • Clearing Metro cache effectively
  • Configuring source maps
  • Platform-specific file resolution
  • Debugging bundling performance
  • Custom asset handling
  • Port conflicts (8081)
  • Symlink resolution in monorepos

Essential Commands

Development

# Start Metro bundler
npx react-native start

# Start with cache cleared
npx react-native start --reset-cache

# Start with custom port
npx react-native start --port 8082

# Start with verbose logging
npx react-native start --verbose

# Expo dev server
npx expo start

# Expo with cache cleared
npx expo start -c

Debugging

# Check Metro status
curl http://localhost:8081/status

# Get bundle (for debugging)
curl http://localhost:8081/index.bundle?platform=ios > bundle.js

# Check source map
curl http://localhost:8081/index.map?platform=ios > bundle.map

# List all modules in bundle
curl http://localhost:8081/index.bundle?platform=ios&dev=false&minify=false

Cache Management

# Clear Metro cache
rm -rf $TMPDIR/react-*
rm -rf $TMPDIR/metro-*

# Clear watchman
watchman watch-del-all

# Clear all (comprehensive)
npm run clear  # Custom script
# Or manually:
rm -rf $TMPDIR/react-*
rm -rf $TMPDIR/metro-*
watchman watch-del-all
rm -rf node_modules
npm install

Pro Tips & Tricks

1. Bundle Analysis

Analyze bundle size to find optimization opportunities:

# Generate bundle with source map
npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output ./bundle.js \
  --sourcemap-output ./bundle.map

# Analyze with source-map-explorer
npm install -g source-map-explorer
source-map-explorer bundle.js bundle.map

2. Environment-Specific Configuration

// metro.config.js
const isDev = process.env.NODE_ENV !== 'production';

module.exports = {
  transformer: {
    minifierConfig: {
      compress: {
        drop_console: !isDev,  // Remove console.log in production
      },
    },
  },

  serializer: {
    getModulesRunBeforeMainModule: () => [
      // Polyfills for production
      ...(!isDev ? [require.resolve('./polyfills.js')] : []),
    ],
  },
};

3. Custom Asset Pipeline

// metro.config.js
module.exports = {
  transformer: {
    // Optimize images during bundling
    assetPlugins: ['expo-asset/tools/hashAssetFiles'],
  },

  resolver: {
    assetExts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'],

    // Custom asset resolution
    resolveAsset: (dirPath, assetName, extension) => {
      const basePath = `${dirPath}/${assetName}`;

      // Try @2x, @3x variants
      const variants = ['@3x', '@2x', ''];
      for (const variant of variants) {
        const path = `${basePath}${variant}.${extension}`;
        if (require('fs').existsSync(path)) {
          return path;
        }
      }

      return null;
    },
  },
};

4. Preloading Heavy Modules

// index.js
import { AppRegistry } from 'react-native';
import App from './App';

// Preload heavy modules
import('./src/heavyModule').then(() => {
  console.log('Heavy module preloaded');
});

AppRegistry.registerComponent('MyApp', () => App);

5. Development Performance Boost

// metro.config.js
const isDev = process.env.NODE_ENV !== 'production';

module.exports = {
  transformer: {
    // Skip minification in dev
    minifierPath: isDev ? undefined : require.resolve('metro-minify-terser'),

    // Faster source maps in dev
    getTransformOptions: async () => ({
      transform: {
        inlineRequires: !isDev,  // Only in production
      },
    }),
  },

  server: {
    // Increase file watching performance
    watchFolders: isDev ? [] : undefined,
  },
};

Integration with SpecWeave

Configuration Management

  • Document Metro configuration in docs/internal/architecture/
  • Track bundle size across increments
  • Include bundling optimization in tasks.md

Performance Monitoring

  • Set bundle size thresholds
  • Track startup time improvements
  • Document optimization strategies

Troubleshooting

  • Maintain runbook for common Metro issues
  • Document cache clearing procedures
  • Track bundling errors in increment reports
Weekly Installs
11
Installed on
claude-code10
antigravity8
opencode7
cursor7
codex7
gemini-cli7