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