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