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