1*082815dcSEvan Baconimport assert from 'assert';
2*082815dcSEvan Baconimport { boolish } from 'getenv';
3*082815dcSEvan Bacon
4*082815dcSEvan Baconimport { ConfigPlugin, StaticPlugin } from '../Plugin.types';
5*082815dcSEvan Baconimport { PluginError } from '../utils/errors';
6*082815dcSEvan Baconimport {
7*082815dcSEvan Bacon  assertInternalProjectRoot,
8*082815dcSEvan Bacon  normalizeStaticPlugin,
9*082815dcSEvan Bacon  resolveConfigPluginFunction,
10*082815dcSEvan Bacon} from '../utils/plugin-resolver';
11*082815dcSEvan Bacon
12*082815dcSEvan Baconconst EXPO_DEBUG = boolish('EXPO_DEBUG', false);
13*082815dcSEvan Bacon
14*082815dcSEvan Bacon// Show all error info related to plugin resolution.
15*082815dcSEvan Baconconst EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS = boolish('EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS', false);
16*082815dcSEvan Bacon// Force using the fallback unversioned plugin instead of a local versioned copy,
17*082815dcSEvan Bacon// this should only be used for testing the CLI.
18*082815dcSEvan Baconconst EXPO_USE_UNVERSIONED_PLUGINS = boolish('EXPO_USE_UNVERSIONED_PLUGINS', false);
19*082815dcSEvan Bacon
20*082815dcSEvan Baconfunction isModuleMissingError(name: string, error: Error): boolean {
21*082815dcSEvan Bacon  // @ts-ignore
22*082815dcSEvan Bacon  if (['MODULE_NOT_FOUND', 'PLUGIN_NOT_FOUND'].includes(error.code)) {
23*082815dcSEvan Bacon    return true;
24*082815dcSEvan Bacon  }
25*082815dcSEvan Bacon  return error.message.includes(`Cannot find module '${name}'`);
26*082815dcSEvan Bacon}
27*082815dcSEvan Bacon
28*082815dcSEvan Baconfunction isUnexpectedTokenError(error: Error): boolean {
29*082815dcSEvan Bacon  if (
30*082815dcSEvan Bacon    error instanceof SyntaxError ||
31*082815dcSEvan Bacon    (error instanceof PluginError && error.code === 'INVALID_PLUGIN_IMPORT')
32*082815dcSEvan Bacon  ) {
33*082815dcSEvan Bacon    return (
34*082815dcSEvan Bacon      // These are the most common errors that'll be thrown when a package isn't transpiled correctly.
35*082815dcSEvan Bacon      !!error.message.match(/Unexpected token/) ||
36*082815dcSEvan Bacon      !!error.message.match(/Cannot use import statement/)
37*082815dcSEvan Bacon    );
38*082815dcSEvan Bacon  }
39*082815dcSEvan Bacon  return false;
40*082815dcSEvan Bacon}
41*082815dcSEvan Bacon
42*082815dcSEvan Bacon/**
43*082815dcSEvan Bacon * Resolves static module plugin and potentially falls back on a provided plugin if the module cannot be resolved
44*082815dcSEvan Bacon *
45*082815dcSEvan Bacon * @param config
46*082815dcSEvan Bacon * @param fallback Plugin with `_resolverError` explaining why the module couldn't be used
47*082815dcSEvan Bacon * @param projectRoot optional project root, fallback to _internal.projectRoot. Used for testing.
48*082815dcSEvan Bacon * @param _isLegacyPlugin Used to suppress errors thrown by plugins that are applied automatically
49*082815dcSEvan Bacon */
50*082815dcSEvan Baconexport const withStaticPlugin: ConfigPlugin<{
51*082815dcSEvan Bacon  plugin: StaticPlugin | ConfigPlugin | string;
52*082815dcSEvan Bacon  fallback?: ConfigPlugin<{ _resolverError: Error } & any>;
53*082815dcSEvan Bacon  projectRoot?: string;
54*082815dcSEvan Bacon  _isLegacyPlugin?: boolean;
55*082815dcSEvan Bacon}> = (config, props) => {
56*082815dcSEvan Bacon  let projectRoot = props.projectRoot;
57*082815dcSEvan Bacon  if (!projectRoot) {
58*082815dcSEvan Bacon    projectRoot = config._internal?.projectRoot;
59*082815dcSEvan Bacon    assertInternalProjectRoot(projectRoot);
60*082815dcSEvan Bacon  }
61*082815dcSEvan Bacon
62*082815dcSEvan Bacon  let [pluginResolve, pluginProps] = normalizeStaticPlugin(props.plugin);
63*082815dcSEvan Bacon  // Ensure no one uses this property by accident.
64*082815dcSEvan Bacon  assert(
65*082815dcSEvan Bacon    !pluginProps?._resolverError,
66*082815dcSEvan Bacon    `Plugin property '_resolverError' is a reserved property of \`withStaticPlugin\``
67*082815dcSEvan Bacon  );
68*082815dcSEvan Bacon
69*082815dcSEvan Bacon  let withPlugin: ConfigPlugin<unknown>;
70*082815dcSEvan Bacon
71*082815dcSEvan Bacon  if (
72*082815dcSEvan Bacon    // Function was provided, no need to resolve: [withPlugin, {}]
73*082815dcSEvan Bacon    typeof pluginResolve === 'function'
74*082815dcSEvan Bacon  ) {
75*082815dcSEvan Bacon    withPlugin = pluginResolve;
76*082815dcSEvan Bacon  } else if (typeof pluginResolve === 'string') {
77*082815dcSEvan Bacon    try {
78*082815dcSEvan Bacon      // Resolve and evaluate plugins.
79*082815dcSEvan Bacon      withPlugin = resolveConfigPluginFunction(projectRoot, pluginResolve);
80*082815dcSEvan Bacon
81*082815dcSEvan Bacon      // Only force if the project has the versioned plugin, otherwise use default behavior.
82*082815dcSEvan Bacon      // This helps see which plugins are being skipped.
83*082815dcSEvan Bacon      if (
84*082815dcSEvan Bacon        EXPO_USE_UNVERSIONED_PLUGINS &&
85*082815dcSEvan Bacon        !!withPlugin &&
86*082815dcSEvan Bacon        !!props._isLegacyPlugin &&
87*082815dcSEvan Bacon        !!props.fallback
88*082815dcSEvan Bacon      ) {
89*082815dcSEvan Bacon        console.log(`Force "${pluginResolve}" to unversioned plugin`);
90*082815dcSEvan Bacon        withPlugin = props.fallback;
91*082815dcSEvan Bacon      }
92*082815dcSEvan Bacon    } catch (error: any) {
93*082815dcSEvan Bacon      if (EXPO_DEBUG) {
94*082815dcSEvan Bacon        if (EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS) {
95*082815dcSEvan Bacon          // Log the error in debug mode for plugins with fallbacks (like the Expo managed plugins).
96*082815dcSEvan Bacon          console.log(`Error resolving plugin "${pluginResolve}"`);
97*082815dcSEvan Bacon          console.log(error);
98*082815dcSEvan Bacon          console.log();
99*082815dcSEvan Bacon        } else {
100*082815dcSEvan Bacon          const shouldMuteWarning =
101*082815dcSEvan Bacon            props._isLegacyPlugin &&
102*082815dcSEvan Bacon            (isModuleMissingError(pluginResolve, error) || isUnexpectedTokenError(error));
103*082815dcSEvan Bacon          if (!shouldMuteWarning) {
104*082815dcSEvan Bacon            if (isModuleMissingError(pluginResolve, error)) {
105*082815dcSEvan Bacon              // Prevent causing log spew for basic resolution errors.
106*082815dcSEvan Bacon              console.log(`Could not find plugin "${pluginResolve}"`);
107*082815dcSEvan Bacon            } else {
108*082815dcSEvan Bacon              // Log the error in debug mode for plugins with fallbacks (like the Expo managed plugins).
109*082815dcSEvan Bacon              console.log(`Error resolving plugin "${pluginResolve}"`);
110*082815dcSEvan Bacon              console.log(error);
111*082815dcSEvan Bacon              console.log();
112*082815dcSEvan Bacon            }
113*082815dcSEvan Bacon          }
114*082815dcSEvan Bacon        }
115*082815dcSEvan Bacon      }
116*082815dcSEvan Bacon      // TODO: Maybe allow for `PluginError`s to be thrown so external plugins can assert invalid options.
117*082815dcSEvan Bacon
118*082815dcSEvan Bacon      // If the static module failed to resolve, attempt to use a fallback.
119*082815dcSEvan Bacon      // This enables support for built-in plugins with versioned variations living in other packages.
120*082815dcSEvan Bacon      if (props.fallback) {
121*082815dcSEvan Bacon        if (!pluginProps) pluginProps = {};
122*082815dcSEvan Bacon        // Pass this to the fallback plugin for potential warnings about needing to install a versioned package.
123*082815dcSEvan Bacon        pluginProps._resolverError = error;
124*082815dcSEvan Bacon        withPlugin = props.fallback;
125*082815dcSEvan Bacon      } else {
126*082815dcSEvan Bacon        // If no fallback, throw the resolution error.
127*082815dcSEvan Bacon        throw error;
128*082815dcSEvan Bacon      }
129*082815dcSEvan Bacon    }
130*082815dcSEvan Bacon  } else {
131*082815dcSEvan Bacon    throw new PluginError(
132*082815dcSEvan Bacon      `Plugin is an unexpected type: ${typeof pluginResolve}`,
133*082815dcSEvan Bacon      'INVALID_PLUGIN_TYPE'
134*082815dcSEvan Bacon    );
135*082815dcSEvan Bacon  }
136*082815dcSEvan Bacon
137*082815dcSEvan Bacon  // Execute the plugin.
138*082815dcSEvan Bacon  config = withPlugin(config, pluginProps);
139*082815dcSEvan Bacon  return config;
140*082815dcSEvan Bacon};
141