0
# Custom Resolvers
1
2
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.
3
4
## Capabilities
5
6
### CustomResolver Function Type
7
8
Function interface for implementing custom resolution logic.
9
10
```typescript { .api }
11
/**
12
* Custom resolver function that can override default resolution behavior
13
* @param context - Custom resolution context with resolver function
14
* @param moduleName - Module name to resolve
15
* @param platform - Target platform for resolution
16
* @returns Resolution result or delegated resolution
17
*/
18
type CustomResolver = (
19
context: CustomResolutionContext,
20
moduleName: string,
21
platform: string | null
22
) => Resolution;
23
24
interface CustomResolutionContext extends ResolutionContext {
25
/** The custom resolver function (reference to self) */
26
readonly resolveRequest: CustomResolver;
27
}
28
```
29
30
### Custom Resolver Options
31
32
Configuration options passed to custom resolvers.
33
34
```typescript { .api }
35
type CustomResolverOptions = Readonly<{
36
[option: string]: unknown;
37
}>;
38
```
39
40
**Usage Example:**
41
42
```javascript
43
const customResolverOptions = {
44
// Custom options for your resolver
45
useCache: true,
46
cacheSize: 1000,
47
specialModules: ['@my-company/special'],
48
transformPaths: {
49
'@components': './src/components',
50
'@utils': './src/utils'
51
}
52
};
53
54
const context = {
55
// ... other context properties
56
customResolverOptions,
57
resolveRequest: myCustomResolver
58
};
59
```
60
61
### Creating Custom Resolvers
62
63
Implementation patterns for custom resolver functions.
64
65
**Basic Custom Resolver:**
66
67
```javascript
68
function myCustomResolver(context, moduleName, platform) {
69
// Handle special module patterns
70
if (moduleName.startsWith('@my-company/')) {
71
const specialPath = resolveSpecialModule(moduleName);
72
if (specialPath) {
73
return { type: 'sourceFile', filePath: specialPath };
74
}
75
}
76
77
// Delegate to default resolution for other modules
78
const defaultResolve = require('metro-resolver').resolve;
79
return defaultResolve(context, moduleName, platform);
80
}
81
```
82
83
**Path Alias Resolver:**
84
85
```javascript
86
function aliasResolver(context, moduleName, platform) {
87
const { transformPaths } = context.customResolverOptions;
88
89
// Check for path aliases
90
for (const [alias, realPath] of Object.entries(transformPaths)) {
91
if (moduleName.startsWith(alias)) {
92
const transformedName = moduleName.replace(alias, realPath);
93
94
// Resolve the transformed path
95
const defaultResolve = require('metro-resolver').resolve;
96
return defaultResolve(context, transformedName, platform);
97
}
98
}
99
100
// Fallback to default resolution
101
const defaultResolve = require('metro-resolver').resolve;
102
return defaultResolve(context, moduleName, platform);
103
}
104
```
105
106
### Resolver Delegation
107
108
Pattern for delegating to the default resolver while adding custom logic.
109
110
```typescript { .api }
111
/**
112
* Default resolve function for delegation from custom resolvers
113
*/
114
declare const resolve: (
115
context: ResolutionContext,
116
moduleName: string,
117
platform: string | null
118
) => Resolution;
119
```
120
121
**Delegation Pattern:**
122
123
```javascript
124
function wrapperResolver(context, moduleName, platform) {
125
// Pre-processing
126
console.log(`Resolving: ${moduleName} for platform: ${platform}`);
127
128
try {
129
// Delegate to default resolver
130
const defaultResolve = require('metro-resolver').resolve;
131
const result = defaultResolve(context, moduleName, platform);
132
133
// Post-processing
134
console.log(`Resolved to: ${result.filePath || 'asset'}`);
135
return result;
136
137
} catch (error) {
138
// Custom error handling
139
console.error(`Failed to resolve ${moduleName}:`, error.message);
140
throw error;
141
}
142
}
143
```
144
145
### Advanced Custom Resolver Examples
146
147
Complex resolver implementations for specific use cases.
148
149
**Conditional Resolution Resolver:**
150
151
```javascript
152
function conditionalResolver(context, moduleName, platform) {
153
const { NODE_ENV } = process.env;
154
155
// Development-only modules
156
if (moduleName.endsWith('.dev') && NODE_ENV !== 'development') {
157
return { type: 'empty' };
158
}
159
160
// Production-only modules
161
if (moduleName.endsWith('.prod') && NODE_ENV === 'development') {
162
return { type: 'empty' };
163
}
164
165
// Strip conditional suffixes for actual resolution
166
const cleanModuleName = moduleName.replace(/\.(dev|prod)$/, '');
167
168
const defaultResolve = require('metro-resolver').resolve;
169
return defaultResolve(context, cleanModuleName, platform);
170
}
171
```
172
173
**Virtual Module Resolver:**
174
175
```javascript
176
function virtualModuleResolver(context, moduleName, platform) {
177
const virtualModules = {
178
'virtual:config': () => generateConfigModule(),
179
'virtual:env': () => generateEnvModule(),
180
'virtual:features': () => generateFeatureFlags()
181
};
182
183
if (moduleName.startsWith('virtual:')) {
184
const generator = virtualModules[moduleName];
185
if (generator) {
186
// Create temporary file with generated content
187
const tempPath = createTempFile(generator());
188
return { type: 'sourceFile', filePath: tempPath };
189
}
190
}
191
192
const defaultResolve = require('metro-resolver').resolve;
193
return defaultResolve(context, moduleName, platform);
194
}
195
```
196
197
**Cache-Enabled Resolver:**
198
199
```javascript
200
class CachedCustomResolver {
201
constructor() {
202
this.cache = new Map();
203
this.resolver = this.resolve.bind(this);
204
}
205
206
resolve(context, moduleName, platform) {
207
const cacheKey = `${moduleName}:${platform}:${context.originModulePath}`;
208
209
if (this.cache.has(cacheKey)) {
210
return this.cache.get(cacheKey);
211
}
212
213
try {
214
const result = this.performResolution(context, moduleName, platform);
215
this.cache.set(cacheKey, result);
216
return result;
217
} catch (error) {
218
// Don't cache errors
219
throw error;
220
}
221
}
222
223
performResolution(context, moduleName, platform) {
224
// Custom resolution logic
225
const defaultResolve = require('metro-resolver').resolve;
226
return defaultResolve(context, moduleName, platform);
227
}
228
229
clearCache() {
230
this.cache.clear();
231
}
232
}
233
234
const cachedResolver = new CachedCustomResolver();
235
```
236
237
### Custom Resolver Context
238
239
Accessing and utilizing the custom resolution context.
240
241
```typescript { .api }
242
interface CustomResolutionContext extends ResolutionContext {
243
/** Reference to the custom resolver function */
244
readonly resolveRequest: CustomResolver;
245
}
246
```
247
248
**Context Usage:**
249
250
```javascript
251
function contextAwareResolver(context, moduleName, platform) {
252
// Access custom options
253
const { specialHandling } = context.customResolverOptions;
254
255
// Access origin information
256
const { originModulePath } = context;
257
258
// Check if resolving from a special directory
259
if (originModulePath.includes('/special/') && specialHandling) {
260
return handleSpecialModule(context, moduleName, platform);
261
}
262
263
// Use file system operations
264
const packagePath = path.dirname(originModulePath) + '/package.json';
265
const packageInfo = context.getPackage(packagePath);
266
267
if (packageInfo?.customField) {
268
return handleCustomField(context, moduleName, platform, packageInfo.customField);
269
}
270
271
// Default resolution
272
const defaultResolve = require('metro-resolver').resolve;
273
return defaultResolve(context, moduleName, platform);
274
}
275
```
276
277
### Error Handling in Custom Resolvers
278
279
Proper error handling patterns for custom resolvers.
280
281
```javascript
282
function robustCustomResolver(context, moduleName, platform) {
283
try {
284
// Custom resolution logic
285
const customResult = attemptCustomResolution(moduleName, platform);
286
if (customResult) {
287
return customResult;
288
}
289
} catch (error) {
290
// Log custom resolution errors but don't fail
291
console.warn(`Custom resolution failed for ${moduleName}:`, error.message);
292
}
293
294
try {
295
// Fallback to default resolution
296
const defaultResolve = require('metro-resolver').resolve;
297
return defaultResolve(context, moduleName, platform);
298
} catch (error) {
299
// Enhance error with custom information
300
if (error instanceof FailedToResolveNameError) {
301
error.message += `\n(Custom resolver attempted for: ${moduleName})`;
302
}
303
throw error;
304
}
305
}
306
```
307
308
### Custom Resolver Integration
309
310
Integration patterns with build systems and bundlers.
311
312
**Metro Integration:**
313
314
```javascript
315
// metro.config.js
316
module.exports = {
317
resolver: {
318
resolverMainFields: ['react-native', 'browser', 'main'],
319
resolveRequest: (context, moduleName, platform) => {
320
return myCustomResolver(context, moduleName, platform);
321
},
322
},
323
};
324
```
325
326
**Webpack Integration:**
327
328
```javascript
329
// webpack.config.js
330
const MetroResolver = require('metro-resolver');
331
332
module.exports = {
333
resolve: {
334
plugins: [
335
{
336
apply(resolver) {
337
resolver.hooks.resolve.tapAsync('MetroCustomResolver', (request, resolveContext, callback) => {
338
try {
339
const result = MetroResolver.resolve(context, request.request, null);
340
callback(null, { path: result.filePath });
341
} catch (error) {
342
callback(error);
343
}
344
});
345
}
346
}
347
]
348
}
349
};
350
```
351
352
### Performance Considerations
353
354
Optimization strategies for custom resolvers.
355
356
**Efficient Pattern Matching:**
357
358
```javascript
359
function optimizedResolver(context, moduleName, platform) {
360
// Use efficient string operations
361
const firstChar = moduleName[0];
362
363
switch (firstChar) {
364
case '@':
365
return handleScopedPackage(context, moduleName, platform);
366
case '.':
367
return handleRelativePath(context, moduleName, platform);
368
case '/':
369
return handleAbsolutePath(context, moduleName, platform);
370
default:
371
return handleBareSpecifier(context, moduleName, platform);
372
}
373
}
374
```
375
376
**Lazy Loading:**
377
378
```javascript
379
function lazyResolver(context, moduleName, platform) {
380
// Lazy load expensive resolution logic
381
if (moduleName.startsWith('heavy-module')) {
382
const heavyResolver = require('./heavy-resolver');
383
return heavyResolver(context, moduleName, platform);
384
}
385
386
const defaultResolve = require('metro-resolver').resolve;
387
return defaultResolve(context, moduleName, platform);
388
}
389
```
390
391
### Testing Custom Resolvers
392
393
Unit testing patterns for custom resolver functions.
394
395
```javascript
396
const MetroResolver = require('metro-resolver');
397
398
describe('Custom Resolver', () => {
399
const mockContext = {
400
allowHaste: false,
401
assetExts: ['png', 'jpg'],
402
sourceExts: ['js', 'ts'],
403
mainFields: ['main'],
404
originModulePath: '/app/src/index.js',
405
fileSystemLookup: jest.fn(),
406
getPackage: jest.fn(),
407
customResolverOptions: { useAliases: true }
408
};
409
410
test('resolves alias modules', () => {
411
const result = myCustomResolver(mockContext, '@components/Button', null);
412
expect(result.type).toBe('sourceFile');
413
expect(result.filePath).toContain('/src/components/Button');
414
});
415
416
test('delegates to default resolver', () => {
417
const result = myCustomResolver(mockContext, 'react', null);
418
expect(result.type).toBe('sourceFile');
419
expect(result.filePath).toContain('node_modules/react');
420
});
421
});
422
```