1082815dcSEvan Baconimport Debug from 'debug';
2082815dcSEvan Baconimport path from 'path';
3082815dcSEvan Bacon
4*8a424bebSJames Ideimport { assertModResults, ForwardedBaseModOptions } from './createBaseMod';
5*8a424bebSJames Ideimport { withAndroidBaseMods } from './withAndroidBaseMods';
6*8a424bebSJames Ideimport { withIosBaseMods } from './withIosBaseMods';
7082815dcSEvan Baconimport { ExportedConfig, Mod, ModConfig, ModPlatform } from '../Plugin.types';
8082815dcSEvan Baconimport { getHackyProjectName } from '../ios/utils/Xcodeproj';
9082815dcSEvan Baconimport { PluginError } from '../utils/errors';
10082815dcSEvan Baconimport * as Warnings from '../utils/warnings';
11082815dcSEvan Bacon
12082815dcSEvan Baconconst debug = Debug('expo:config-plugins:mod-compiler');
13082815dcSEvan Bacon
14082815dcSEvan Baconexport function withDefaultBaseMods(
15082815dcSEvan Bacon  config: ExportedConfig,
16082815dcSEvan Bacon  props: ForwardedBaseModOptions = {}
17082815dcSEvan Bacon): ExportedConfig {
18082815dcSEvan Bacon  config = withIosBaseMods(config, props);
19082815dcSEvan Bacon  config = withAndroidBaseMods(config, props);
20082815dcSEvan Bacon  return config;
21082815dcSEvan Bacon}
22082815dcSEvan Bacon
23082815dcSEvan Bacon/**
24082815dcSEvan Bacon * Get a prebuild config that safely evaluates mods without persisting any changes to the file system.
25082815dcSEvan Bacon * Currently this only supports infoPlist, entitlements, androidManifest, strings, gradleProperties, and expoPlist mods.
26082815dcSEvan Bacon * This plugin should be evaluated directly:
27082815dcSEvan Bacon */
28082815dcSEvan Baconexport function withIntrospectionBaseMods(
29082815dcSEvan Bacon  config: ExportedConfig,
30082815dcSEvan Bacon  props: ForwardedBaseModOptions = {}
31082815dcSEvan Bacon): ExportedConfig {
32082815dcSEvan Bacon  config = withIosBaseMods(config, {
33082815dcSEvan Bacon    saveToInternal: true,
34082815dcSEvan Bacon    // This writing optimization can be skipped since we never write in introspection mode.
35082815dcSEvan Bacon    // Including empty mods will ensure that all mods get introspected.
36082815dcSEvan Bacon    skipEmptyMod: false,
37082815dcSEvan Bacon    ...props,
38082815dcSEvan Bacon  });
39082815dcSEvan Bacon  config = withAndroidBaseMods(config, {
40082815dcSEvan Bacon    saveToInternal: true,
41082815dcSEvan Bacon    skipEmptyMod: false,
42082815dcSEvan Bacon    ...props,
43082815dcSEvan Bacon  });
44082815dcSEvan Bacon
45082815dcSEvan Bacon  if (config.mods) {
46082815dcSEvan Bacon    // Remove all mods that don't have an introspection base mod, for instance `dangerous` mods.
47082815dcSEvan Bacon    for (const platform of Object.keys(config.mods) as ModPlatform[]) {
48082815dcSEvan Bacon      // const platformPreserve = preserve[platform];
49082815dcSEvan Bacon      for (const key of Object.keys(config.mods[platform] || {})) {
50082815dcSEvan Bacon        // @ts-ignore
51082815dcSEvan Bacon        if (!config.mods[platform]?.[key]?.isIntrospective) {
52082815dcSEvan Bacon          debug(`removing non-idempotent mod: ${platform}.${key}`);
53082815dcSEvan Bacon          // @ts-ignore
54082815dcSEvan Bacon          delete config.mods[platform]?.[key];
55082815dcSEvan Bacon        }
56082815dcSEvan Bacon      }
57082815dcSEvan Bacon    }
58082815dcSEvan Bacon  }
59082815dcSEvan Bacon
60082815dcSEvan Bacon  return config;
61082815dcSEvan Bacon}
62082815dcSEvan Bacon
63082815dcSEvan Bacon/**
64082815dcSEvan Bacon *
65082815dcSEvan Bacon * @param projectRoot
66082815dcSEvan Bacon * @param config
67082815dcSEvan Bacon */
68082815dcSEvan Baconexport async function compileModsAsync(
69082815dcSEvan Bacon  config: ExportedConfig,
70082815dcSEvan Bacon  props: {
71082815dcSEvan Bacon    projectRoot: string;
72082815dcSEvan Bacon    platforms?: ModPlatform[];
73082815dcSEvan Bacon    introspect?: boolean;
74082815dcSEvan Bacon    assertMissingModProviders?: boolean;
7542d6b312SCedric van Putten    ignoreExistingNativeFiles?: boolean;
76082815dcSEvan Bacon  }
77082815dcSEvan Bacon): Promise<ExportedConfig> {
78082815dcSEvan Bacon  if (props.introspect === true) {
79082815dcSEvan Bacon    config = withIntrospectionBaseMods(config);
80082815dcSEvan Bacon  } else {
81082815dcSEvan Bacon    config = withDefaultBaseMods(config);
82082815dcSEvan Bacon  }
83082815dcSEvan Bacon  return await evalModsAsync(config, props);
84082815dcSEvan Bacon}
85082815dcSEvan Bacon
86082815dcSEvan Baconfunction sortMods(commands: [string, any][], order: string[]): [string, any][] {
87082815dcSEvan Bacon  const allKeys = commands.map(([key]) => key);
88082815dcSEvan Bacon  const completeOrder = [...new Set([...order, ...allKeys])];
89082815dcSEvan Bacon  const sorted: [string, any][] = [];
90082815dcSEvan Bacon  while (completeOrder.length) {
91082815dcSEvan Bacon    const group = completeOrder.shift()!;
92082815dcSEvan Bacon    const commandSet = commands.find(([key]) => key === group);
93082815dcSEvan Bacon    if (commandSet) {
94082815dcSEvan Bacon      sorted.push(commandSet);
95082815dcSEvan Bacon    }
96082815dcSEvan Bacon  }
97082815dcSEvan Bacon  return sorted;
98082815dcSEvan Bacon}
99082815dcSEvan Bacon
100082815dcSEvan Baconfunction getRawClone({ mods, ...config }: ExportedConfig) {
101082815dcSEvan Bacon  // Configs should be fully serializable, so we can clone them without worrying about
102082815dcSEvan Bacon  // the mods.
103082815dcSEvan Bacon  return Object.freeze(JSON.parse(JSON.stringify(config)));
104082815dcSEvan Bacon}
105082815dcSEvan Bacon
106082815dcSEvan Baconconst orders: Record<string, string[]> = {
107082815dcSEvan Bacon  ios: [
108082815dcSEvan Bacon    // dangerous runs first
109082815dcSEvan Bacon    'dangerous',
110082815dcSEvan Bacon    // run the XcodeProject mod second because many plugins attempt to read from it.
111082815dcSEvan Bacon    'xcodeproj',
112082815dcSEvan Bacon  ],
113082815dcSEvan Bacon};
114082815dcSEvan Bacon/**
115082815dcSEvan Bacon * A generic plugin compiler.
116082815dcSEvan Bacon *
117082815dcSEvan Bacon * @param config
118082815dcSEvan Bacon */
119082815dcSEvan Baconexport async function evalModsAsync(
120082815dcSEvan Bacon  config: ExportedConfig,
121082815dcSEvan Bacon  {
122082815dcSEvan Bacon    projectRoot,
123082815dcSEvan Bacon    introspect,
124082815dcSEvan Bacon    platforms,
12542d6b312SCedric van Putten    assertMissingModProviders,
12642d6b312SCedric van Putten    ignoreExistingNativeFiles = false,
12742d6b312SCedric van Putten  }: {
12842d6b312SCedric van Putten    projectRoot: string;
12942d6b312SCedric van Putten    introspect?: boolean;
13042d6b312SCedric van Putten    platforms?: ModPlatform[];
131082815dcSEvan Bacon    /**
132082815dcSEvan Bacon     * Throw errors when mods are missing providers.
133082815dcSEvan Bacon     * @default true
134082815dcSEvan Bacon     */
135082815dcSEvan Bacon    assertMissingModProviders?: boolean;
13642d6b312SCedric van Putten    /** Ignore any existing native files, only use the generated prebuild results. */
13742d6b312SCedric van Putten    ignoreExistingNativeFiles?: boolean;
138082815dcSEvan Bacon  }
139082815dcSEvan Bacon): Promise<ExportedConfig> {
140082815dcSEvan Bacon  const modRawConfig = getRawClone(config);
141082815dcSEvan Bacon  for (const [platformName, platform] of Object.entries(config.mods ?? ({} as ModConfig))) {
142082815dcSEvan Bacon    if (platforms && !platforms.includes(platformName as any)) {
143082815dcSEvan Bacon      debug(`skip platform: ${platformName}`);
144082815dcSEvan Bacon      continue;
145082815dcSEvan Bacon    }
146082815dcSEvan Bacon
147082815dcSEvan Bacon    let entries = Object.entries(platform);
148082815dcSEvan Bacon    if (entries.length) {
149082815dcSEvan Bacon      // Move dangerous item to the first position if it exists, this ensures that all dangerous code runs first.
150ed94abb0SCedric van Putten      entries = sortMods(entries, orders[platformName] ?? ['dangerous']);
151082815dcSEvan Bacon      debug(`run in order: ${entries.map(([name]) => name).join(', ')}`);
152082815dcSEvan Bacon      const platformProjectRoot = path.join(projectRoot, platformName);
153082815dcSEvan Bacon      const projectName =
154082815dcSEvan Bacon        platformName === 'ios' ? getHackyProjectName(projectRoot, config) : undefined;
155082815dcSEvan Bacon
156082815dcSEvan Bacon      for (const [modName, mod] of entries) {
157082815dcSEvan Bacon        const modRequest = {
158082815dcSEvan Bacon          projectRoot,
159082815dcSEvan Bacon          projectName,
160082815dcSEvan Bacon          platformProjectRoot,
161082815dcSEvan Bacon          platform: platformName as ModPlatform,
162082815dcSEvan Bacon          modName,
163082815dcSEvan Bacon          introspect: !!introspect,
16442d6b312SCedric van Putten          ignoreExistingNativeFiles,
165082815dcSEvan Bacon        };
166082815dcSEvan Bacon
167082815dcSEvan Bacon        if (!(mod as Mod).isProvider) {
168082815dcSEvan Bacon          // In strict mode, throw an error.
169082815dcSEvan Bacon          const errorMessage = `Initial base modifier for "${platformName}.${modName}" is not a provider and therefore will not provide modResults to child mods`;
170082815dcSEvan Bacon          if (assertMissingModProviders !== false) {
171082815dcSEvan Bacon            throw new PluginError(errorMessage, 'MISSING_PROVIDER');
172082815dcSEvan Bacon          } else {
173082815dcSEvan Bacon            Warnings.addWarningForPlatform(
174082815dcSEvan Bacon              platformName as ModPlatform,
175082815dcSEvan Bacon              `${platformName}.${modName}`,
176082815dcSEvan Bacon              `Skipping: Initial base modifier for "${platformName}.${modName}" is not a provider and therefore will not provide modResults to child mods. This may be due to an outdated version of Expo CLI.`
177082815dcSEvan Bacon            );
178082815dcSEvan Bacon            // In loose mode, just skip the mod entirely.
179082815dcSEvan Bacon            continue;
180082815dcSEvan Bacon          }
181082815dcSEvan Bacon        }
182082815dcSEvan Bacon
183082815dcSEvan Bacon        const results = await (mod as Mod)({
184082815dcSEvan Bacon          ...config,
185082815dcSEvan Bacon          modResults: null,
186082815dcSEvan Bacon          modRequest,
187082815dcSEvan Bacon          modRawConfig,
188082815dcSEvan Bacon        });
189082815dcSEvan Bacon
190082815dcSEvan Bacon        // Sanity check to help locate non compliant mods.
191082815dcSEvan Bacon        config = assertModResults(results, platformName, modName);
192082815dcSEvan Bacon        // @ts-ignore: `modResults` is added for modifications
193082815dcSEvan Bacon        delete config.modResults;
194082815dcSEvan Bacon        // @ts-ignore: `modRequest` is added for modifications
195082815dcSEvan Bacon        delete config.modRequest;
196082815dcSEvan Bacon        // @ts-ignore: `modRawConfig` is added for modifications
197082815dcSEvan Bacon        delete config.modRawConfig;
198082815dcSEvan Bacon      }
199082815dcSEvan Bacon    }
200082815dcSEvan Bacon  }
201082815dcSEvan Bacon
202082815dcSEvan Bacon  return config;
203082815dcSEvan Bacon}
204