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