or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

asset-resolution.mdcustom-resolvers.mderror-handling.mdindex.mdpackage-resolution.mdresolution-context.mdresolution-engine.md

custom-resolvers.mddocs/

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

```