CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-metro-resolver

Implementation of Metro's resolution logic for JavaScript modules, assets, and packages within React Native and Metro bundler projects.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

custom-resolvers.mddocs/

Custom Resolvers

Extensibility system allowing custom resolution logic to be integrated into the resolution pipeline. Custom resolvers enable advanced resolution strategies, special module handling, and integration with custom module systems.

Capabilities

CustomResolver Function Type

Function interface for implementing custom resolution logic.

/**
 * Custom resolver function that can override default resolution behavior
 * @param context - Custom resolution context with resolver function
 * @param moduleName - Module name to resolve
 * @param platform - Target platform for resolution
 * @returns Resolution result or delegated resolution
 */
type CustomResolver = (
  context: CustomResolutionContext,
  moduleName: string,
  platform: string | null
) => Resolution;

interface CustomResolutionContext extends ResolutionContext {
  /** The custom resolver function (reference to self) */
  readonly resolveRequest: CustomResolver;
}

Custom Resolver Options

Configuration options passed to custom resolvers.

type CustomResolverOptions = Readonly<{
  [option: string]: unknown;
}>;

Usage Example:

const customResolverOptions = {
  // Custom options for your resolver
  useCache: true,
  cacheSize: 1000,
  specialModules: ['@my-company/special'],
  transformPaths: {
    '@components': './src/components',
    '@utils': './src/utils'
  }
};

const context = {
  // ... other context properties
  customResolverOptions,
  resolveRequest: myCustomResolver
};

Creating Custom Resolvers

Implementation patterns for custom resolver functions.

Basic Custom Resolver:

function myCustomResolver(context, moduleName, platform) {
  // Handle special module patterns
  if (moduleName.startsWith('@my-company/')) {
    const specialPath = resolveSpecialModule(moduleName);
    if (specialPath) {
      return { type: 'sourceFile', filePath: specialPath };
    }
  }
  
  // Delegate to default resolution for other modules
  const defaultResolve = require('metro-resolver').resolve;
  return defaultResolve(context, moduleName, platform);
}

Path Alias Resolver:

function aliasResolver(context, moduleName, platform) {
  const { transformPaths } = context.customResolverOptions;
  
  // Check for path aliases
  for (const [alias, realPath] of Object.entries(transformPaths)) {
    if (moduleName.startsWith(alias)) {
      const transformedName = moduleName.replace(alias, realPath);
      
      // Resolve the transformed path
      const defaultResolve = require('metro-resolver').resolve;
      return defaultResolve(context, transformedName, platform);
    }
  }
  
  // Fallback to default resolution
  const defaultResolve = require('metro-resolver').resolve;
  return defaultResolve(context, moduleName, platform);
}

Resolver Delegation

Pattern for delegating to the default resolver while adding custom logic.

/**
 * Default resolve function for delegation from custom resolvers
 */
declare const resolve: (
  context: ResolutionContext,
  moduleName: string,
  platform: string | null
) => Resolution;

Delegation Pattern:

function wrapperResolver(context, moduleName, platform) {
  // Pre-processing
  console.log(`Resolving: ${moduleName} for platform: ${platform}`);
  
  try {
    // Delegate to default resolver
    const defaultResolve = require('metro-resolver').resolve;
    const result = defaultResolve(context, moduleName, platform);
    
    // Post-processing
    console.log(`Resolved to: ${result.filePath || 'asset'}`);
    return result;
    
  } catch (error) {
    // Custom error handling
    console.error(`Failed to resolve ${moduleName}:`, error.message);
    throw error;
  }
}

Advanced Custom Resolver Examples

Complex resolver implementations for specific use cases.

Conditional Resolution Resolver:

function conditionalResolver(context, moduleName, platform) {
  const { NODE_ENV } = process.env;
  
  // Development-only modules
  if (moduleName.endsWith('.dev') && NODE_ENV !== 'development') {
    return { type: 'empty' };
  }
  
  // Production-only modules  
  if (moduleName.endsWith('.prod') && NODE_ENV === 'development') {
    return { type: 'empty' };
  }
  
  // Strip conditional suffixes for actual resolution
  const cleanModuleName = moduleName.replace(/\.(dev|prod)$/, '');
  
  const defaultResolve = require('metro-resolver').resolve;
  return defaultResolve(context, cleanModuleName, platform);
}

Virtual Module Resolver:

function virtualModuleResolver(context, moduleName, platform) {
  const virtualModules = {
    'virtual:config': () => generateConfigModule(),
    'virtual:env': () => generateEnvModule(),
    'virtual:features': () => generateFeatureFlags()
  };
  
  if (moduleName.startsWith('virtual:')) {
    const generator = virtualModules[moduleName];
    if (generator) {
      // Create temporary file with generated content
      const tempPath = createTempFile(generator());
      return { type: 'sourceFile', filePath: tempPath };
    }
  }
  
  const defaultResolve = require('metro-resolver').resolve;
  return defaultResolve(context, moduleName, platform);
}

Cache-Enabled Resolver:

class CachedCustomResolver {
  constructor() {
    this.cache = new Map();
    this.resolver = this.resolve.bind(this);
  }
  
  resolve(context, moduleName, platform) {
    const cacheKey = `${moduleName}:${platform}:${context.originModulePath}`;
    
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey);
    }
    
    try {
      const result = this.performResolution(context, moduleName, platform);
      this.cache.set(cacheKey, result);
      return result;
    } catch (error) {
      // Don't cache errors
      throw error;
    }
  }
  
  performResolution(context, moduleName, platform) {
    // Custom resolution logic
    const defaultResolve = require('metro-resolver').resolve;
    return defaultResolve(context, moduleName, platform);
  }
  
  clearCache() {
    this.cache.clear();
  }
}

const cachedResolver = new CachedCustomResolver();

Custom Resolver Context

Accessing and utilizing the custom resolution context.

interface CustomResolutionContext extends ResolutionContext {
  /** Reference to the custom resolver function */
  readonly resolveRequest: CustomResolver;
}

Context Usage:

function contextAwareResolver(context, moduleName, platform) {
  // Access custom options
  const { specialHandling } = context.customResolverOptions;
  
  // Access origin information
  const { originModulePath } = context;
  
  // Check if resolving from a special directory
  if (originModulePath.includes('/special/') && specialHandling) {
    return handleSpecialModule(context, moduleName, platform);
  }
  
  // Use file system operations
  const packagePath = path.dirname(originModulePath) + '/package.json';
  const packageInfo = context.getPackage(packagePath);
  
  if (packageInfo?.customField) {
    return handleCustomField(context, moduleName, platform, packageInfo.customField);
  }
  
  // Default resolution
  const defaultResolve = require('metro-resolver').resolve;
  return defaultResolve(context, moduleName, platform);
}

Error Handling in Custom Resolvers

Proper error handling patterns for custom resolvers.

function robustCustomResolver(context, moduleName, platform) {
  try {
    // Custom resolution logic
    const customResult = attemptCustomResolution(moduleName, platform);
    if (customResult) {
      return customResult;
    }
  } catch (error) {
    // Log custom resolution errors but don't fail
    console.warn(`Custom resolution failed for ${moduleName}:`, error.message);
  }
  
  try {
    // Fallback to default resolution
    const defaultResolve = require('metro-resolver').resolve;
    return defaultResolve(context, moduleName, platform);
  } catch (error) {
    // Enhance error with custom information
    if (error instanceof FailedToResolveNameError) {
      error.message += `\n(Custom resolver attempted for: ${moduleName})`;
    }
    throw error;
  }
}

Custom Resolver Integration

Integration patterns with build systems and bundlers.

Metro Integration:

// metro.config.js
module.exports = {
  resolver: {
    resolverMainFields: ['react-native', 'browser', 'main'],
    resolveRequest: (context, moduleName, platform) => {
      return myCustomResolver(context, moduleName, platform);
    },
  },
};

Webpack Integration:

// webpack.config.js
const MetroResolver = require('metro-resolver');

module.exports = {
  resolve: {
    plugins: [
      {
        apply(resolver) {
          resolver.hooks.resolve.tapAsync('MetroCustomResolver', (request, resolveContext, callback) => {
            try {
              const result = MetroResolver.resolve(context, request.request, null);
              callback(null, { path: result.filePath });
            } catch (error) {
              callback(error);
            }
          });
        }
      }
    ]
  }
};

Performance Considerations

Optimization strategies for custom resolvers.

Efficient Pattern Matching:

function optimizedResolver(context, moduleName, platform) {
  // Use efficient string operations
  const firstChar = moduleName[0];
  
  switch (firstChar) {
    case '@':
      return handleScopedPackage(context, moduleName, platform);
    case '.':
      return handleRelativePath(context, moduleName, platform);
    case '/':
      return handleAbsolutePath(context, moduleName, platform);
    default:
      return handleBareSpecifier(context, moduleName, platform);
  }
}

Lazy Loading:

function lazyResolver(context, moduleName, platform) {
  // Lazy load expensive resolution logic
  if (moduleName.startsWith('heavy-module')) {
    const heavyResolver = require('./heavy-resolver');
    return heavyResolver(context, moduleName, platform);
  }
  
  const defaultResolve = require('metro-resolver').resolve;
  return defaultResolve(context, moduleName, platform);
}

Testing Custom Resolvers

Unit testing patterns for custom resolver functions.

const MetroResolver = require('metro-resolver');

describe('Custom Resolver', () => {
  const mockContext = {
    allowHaste: false,
    assetExts: ['png', 'jpg'],
    sourceExts: ['js', 'ts'],
    mainFields: ['main'],
    originModulePath: '/app/src/index.js',
    fileSystemLookup: jest.fn(),
    getPackage: jest.fn(),
    customResolverOptions: { useAliases: true }
  };
  
  test('resolves alias modules', () => {
    const result = myCustomResolver(mockContext, '@components/Button', null);
    expect(result.type).toBe('sourceFile');
    expect(result.filePath).toContain('/src/components/Button');
  });
  
  test('delegates to default resolver', () => {
    const result = myCustomResolver(mockContext, 'react', null);
    expect(result.type).toBe('sourceFile');
    expect(result.filePath).toContain('node_modules/react');
  });
});

docs

asset-resolution.md

custom-resolvers.md

error-handling.md

index.md

package-resolution.md

resolution-context.md

resolution-engine.md

tile.json