1import Debug from 'debug'; 2import path from 'path'; 3 4import { ExportedConfig, Mod, ModConfig, ModPlatform } from '../Plugin.types'; 5import { getHackyProjectName } from '../ios/utils/Xcodeproj'; 6import { PluginError } from '../utils/errors'; 7import * as Warnings from '../utils/warnings'; 8import { assertModResults, ForwardedBaseModOptions } from './createBaseMod'; 9import { withAndroidBaseMods } from './withAndroidBaseMods'; 10import { withIosBaseMods } from './withIosBaseMods'; 11 12const debug = Debug('expo:config-plugins:mod-compiler'); 13 14export function withDefaultBaseMods( 15 config: ExportedConfig, 16 props: ForwardedBaseModOptions = {} 17): ExportedConfig { 18 config = withIosBaseMods(config, props); 19 config = withAndroidBaseMods(config, props); 20 return config; 21} 22 23/** 24 * Get a prebuild config that safely evaluates mods without persisting any changes to the file system. 25 * Currently this only supports infoPlist, entitlements, androidManifest, strings, gradleProperties, and expoPlist mods. 26 * This plugin should be evaluated directly: 27 */ 28export function withIntrospectionBaseMods( 29 config: ExportedConfig, 30 props: ForwardedBaseModOptions = {} 31): ExportedConfig { 32 config = withIosBaseMods(config, { 33 saveToInternal: true, 34 // This writing optimization can be skipped since we never write in introspection mode. 35 // Including empty mods will ensure that all mods get introspected. 36 skipEmptyMod: false, 37 ...props, 38 }); 39 config = withAndroidBaseMods(config, { 40 saveToInternal: true, 41 skipEmptyMod: false, 42 ...props, 43 }); 44 45 if (config.mods) { 46 // Remove all mods that don't have an introspection base mod, for instance `dangerous` mods. 47 for (const platform of Object.keys(config.mods) as ModPlatform[]) { 48 // const platformPreserve = preserve[platform]; 49 for (const key of Object.keys(config.mods[platform] || {})) { 50 // @ts-ignore 51 if (!config.mods[platform]?.[key]?.isIntrospective) { 52 debug(`removing non-idempotent mod: ${platform}.${key}`); 53 // @ts-ignore 54 delete config.mods[platform]?.[key]; 55 } 56 } 57 } 58 } 59 60 return config; 61} 62 63/** 64 * 65 * @param projectRoot 66 * @param config 67 */ 68export async function compileModsAsync( 69 config: ExportedConfig, 70 props: { 71 projectRoot: string; 72 platforms?: ModPlatform[]; 73 introspect?: boolean; 74 assertMissingModProviders?: boolean; 75 } 76): Promise<ExportedConfig> { 77 if (props.introspect === true) { 78 config = withIntrospectionBaseMods(config); 79 } else { 80 config = withDefaultBaseMods(config); 81 } 82 return await evalModsAsync(config, props); 83} 84 85function sortMods(commands: [string, any][], order: string[]): [string, any][] { 86 const allKeys = commands.map(([key]) => key); 87 const completeOrder = [...new Set([...order, ...allKeys])]; 88 const sorted: [string, any][] = []; 89 while (completeOrder.length) { 90 const group = completeOrder.shift()!; 91 const commandSet = commands.find(([key]) => key === group); 92 if (commandSet) { 93 sorted.push(commandSet); 94 } 95 } 96 return sorted; 97} 98 99function getRawClone({ mods, ...config }: ExportedConfig) { 100 // Configs should be fully serializable, so we can clone them without worrying about 101 // the mods. 102 return Object.freeze(JSON.parse(JSON.stringify(config))); 103} 104 105const orders: Record<string, string[]> = { 106 ios: [ 107 // dangerous runs first 108 'dangerous', 109 // run the XcodeProject mod second because many plugins attempt to read from it. 110 'xcodeproj', 111 ], 112}; 113/** 114 * A generic plugin compiler. 115 * 116 * @param config 117 */ 118export async function evalModsAsync( 119 config: ExportedConfig, 120 { 121 projectRoot, 122 introspect, 123 platforms, 124 /** 125 * Throw errors when mods are missing providers. 126 * @default true 127 */ 128 assertMissingModProviders, 129 }: { 130 projectRoot: string; 131 introspect?: boolean; 132 assertMissingModProviders?: boolean; 133 platforms?: ModPlatform[]; 134 } 135): Promise<ExportedConfig> { 136 const modRawConfig = getRawClone(config); 137 for (const [platformName, platform] of Object.entries(config.mods ?? ({} as ModConfig))) { 138 if (platforms && !platforms.includes(platformName as any)) { 139 debug(`skip platform: ${platformName}`); 140 continue; 141 } 142 143 let entries = Object.entries(platform); 144 if (entries.length) { 145 // Move dangerous item to the first position if it exists, this ensures that all dangerous code runs first. 146 entries = sortMods(entries, orders[platformName] ?? ['dangerous']); 147 debug(`run in order: ${entries.map(([name]) => name).join(', ')}`); 148 const platformProjectRoot = path.join(projectRoot, platformName); 149 const projectName = 150 platformName === 'ios' ? getHackyProjectName(projectRoot, config) : undefined; 151 152 for (const [modName, mod] of entries) { 153 const modRequest = { 154 projectRoot, 155 projectName, 156 platformProjectRoot, 157 platform: platformName as ModPlatform, 158 modName, 159 introspect: !!introspect, 160 }; 161 162 if (!(mod as Mod).isProvider) { 163 // In strict mode, throw an error. 164 const errorMessage = `Initial base modifier for "${platformName}.${modName}" is not a provider and therefore will not provide modResults to child mods`; 165 if (assertMissingModProviders !== false) { 166 throw new PluginError(errorMessage, 'MISSING_PROVIDER'); 167 } else { 168 Warnings.addWarningForPlatform( 169 platformName as ModPlatform, 170 `${platformName}.${modName}`, 171 `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.` 172 ); 173 // In loose mode, just skip the mod entirely. 174 continue; 175 } 176 } 177 178 const results = await (mod as Mod)({ 179 ...config, 180 modResults: null, 181 modRequest, 182 modRawConfig, 183 }); 184 185 // Sanity check to help locate non compliant mods. 186 config = assertModResults(results, platformName, modName); 187 // @ts-ignore: `modResults` is added for modifications 188 delete config.modResults; 189 // @ts-ignore: `modRequest` is added for modifications 190 delete config.modRequest; 191 // @ts-ignore: `modRawConfig` is added for modifications 192 delete config.modRawConfig; 193 } 194 } 195 } 196 197 return config; 198} 199