Implementation of Metro's resolution logic for JavaScript modules, assets, and packages within React Native and Metro bundler projects.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
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;
}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
};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);
}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;
}
}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();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);
}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;
}
}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);
}
});
}
}
]
}
};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);
}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');
});
});