1/** 2 * Copyright © 2022 650 Industries. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7import { ConfigT as MetroConfig } from 'metro-config'; 8import { ResolutionContext } from 'metro-resolver'; 9 10import { isFailedToResolveNameError, isFailedToResolvePathError } from './metroErrors'; 11import { importMetroResolverFromProject } from './resolveFromProject'; 12 13const debug = require('debug')('expo:metro:withMetroResolvers') as typeof console.log; 14 15export type MetroResolver = NonNullable<MetroConfig['resolver']['resolveRequest']>; 16 17/** Expo Metro Resolvers can return `null` to skip without throwing an error. Metro Resolvers will throw either a `FailedToResolveNameError` or `FailedToResolvePathError`. */ 18export type ExpoCustomMetroResolver = ( 19 ...args: Parameters<MetroResolver> 20) => ReturnType<MetroResolver> | null; 21 22/** @returns `MetroResolver` utilizing the upstream `resolve` method. */ 23export function getDefaultMetroResolver(projectRoot: string): MetroResolver { 24 const { resolve } = importMetroResolverFromProject(projectRoot); 25 return (context: ResolutionContext, moduleName: string, platform: string | null) => { 26 return resolve(context, moduleName, platform); 27 }; 28} 29 30/** 31 * Extend the Metro config `resolver.resolveRequest` method with additional resolvers that can 32 * exit early by returning a `Resolution` or skip to the next resolver by returning `null`. 33 * 34 * @param config Metro config. 35 * @param projectRoot path to the project root used to resolve the default Metro resolver. 36 * @param resolvers custom MetroResolver to chain. 37 * @returns a new `MetroConfig` with the `resolver.resolveRequest` method chained. 38 */ 39export function withMetroResolvers( 40 config: MetroConfig, 41 projectRoot: string, 42 resolvers: ExpoCustomMetroResolver[] 43): MetroConfig { 44 debug( 45 `Appending ${ 46 resolvers.length 47 } custom resolvers to Metro config. (has custom resolver: ${!!config.resolver.resolveRequest})` 48 ); 49 const originalResolveRequest = 50 config.resolver.resolveRequest || getDefaultMetroResolver(projectRoot); 51 52 return { 53 ...config, 54 resolver: { 55 ...config.resolver, 56 resolveRequest(...args: Parameters<MetroResolver>) { 57 for (const resolver of resolvers) { 58 try { 59 const resolution = resolver(...args); 60 if (resolution) { 61 return resolution; 62 } 63 } catch (error: any) { 64 // If the error is directly related to a resolver not being able to resolve a module, then 65 // we can ignore the error and try the next resolver. Otherwise, we should throw the error. 66 const isResolutionError = 67 isFailedToResolveNameError(error) || isFailedToResolvePathError(error); 68 if (!isResolutionError) { 69 throw error; 70 } 71 debug( 72 `Custom resolver threw: ${error.constructor.name}. (module: ${args[1]}, platform: ${args[2]})` 73 ); 74 } 75 } 76 // If we haven't returned by now, use the original resolver or upstream resolver. 77 return originalResolveRequest(...args); 78 }, 79 }, 80 }; 81} 82