1import { ExpoConfig } from '@expo/config';
2import {
3  normalizeStaticPlugin,
4  resolveConfigPluginFunctionWithInfo,
5} from '@expo/config-plugins/build/utils/plugin-resolver';
6import { getAutoPlugins } from '@expo/prebuild-config';
7
8import * as Log from '../../log';
9import { attemptAddingPluginsAsync } from '../../utils/modifyConfigPlugins';
10
11const AUTO_PLUGINS = getAutoPlugins();
12
13/**
14 * Resolve if a package has a config plugin.
15 * For sanity, we'll only support config plugins that use the `app.config.js` entry file,
16 * this is because a package like `lodash` could be a "valid" config plugin and break the prebuild process.
17 *
18 * @param projectRoot
19 * @param packageName
20 * @returns
21 */
22function packageHasConfigPlugin(projectRoot: string, packageName: string) {
23  try {
24    const info = resolveConfigPluginFunctionWithInfo(projectRoot, packageName);
25    if (info.isPluginFile) {
26      return info.plugin;
27    }
28  } catch {}
29  return false;
30}
31
32/**
33 * Get a list of plugins that were are supplied as string module IDs.
34 * @example
35 * ```json
36 * {
37 *   "plugins": [
38 *     "expo-camera",
39 *     ["react-native-firebase", ...]
40 *   ]
41 * }
42 * ```
43 *   ↓ ↓ ↓ ↓ ↓ ↓
44 *
45 * `['expo-camera', 'react-native-firebase']`
46 *
47 */
48export function getNamedPlugins(plugins: NonNullable<ExpoConfig['plugins']>): string[] {
49  const namedPlugins: string[] = [];
50  for (const plugin of plugins) {
51    try {
52      // @ts-ignore
53      const [normal] = normalizeStaticPlugin(plugin);
54      if (typeof normal === 'string') {
55        namedPlugins.push(normal);
56      }
57    } catch {
58      // ignore assertions
59    }
60  }
61  return namedPlugins;
62}
63
64/** Attempts to ensure that non-auto plugins are added to the `app.json` `plugins` array when modules with Expo Config Plugins are installed. */
65export async function autoAddConfigPluginsAsync(
66  projectRoot: string,
67  exp: Pick<ExpoConfig, 'plugins'>,
68  packages: string[]
69) {
70  Log.debug('Checking config plugins...');
71
72  const currentPlugins = exp.plugins || [];
73  const normalized = getNamedPlugins(currentPlugins);
74
75  Log.debug(`Existing plugins: ${normalized.join(', ')}`);
76
77  const plugins = packages.filter((pkg) => {
78    if (normalized.includes(pkg)) {
79      // already included in plugins array
80      return false;
81    }
82    // Check if the package has a valid plugin. Must be a well-made plugin for it to work with this.
83    const plugin = packageHasConfigPlugin(projectRoot, pkg);
84
85    Log.debug(
86      `Package "${pkg}" has plugin: ${!!plugin}` + (plugin ? ` (args: ${plugin.length})` : '')
87    );
88
89    if (AUTO_PLUGINS.includes(pkg)) {
90      Log.debug(`Package "${pkg}" is an auto plugin, skipping...`);
91      return false;
92    }
93
94    return !!plugin;
95  });
96
97  await attemptAddingPluginsAsync(projectRoot, exp, plugins);
98}
99