1082815dcSEvan Baconimport Debug from 'debug'; 2082815dcSEvan Bacon 3*8a424bebSJames Ideimport { BaseModOptions, withBaseMod } from './withMod'; 4082815dcSEvan Baconimport { 5082815dcSEvan Bacon ConfigPlugin, 6082815dcSEvan Bacon ExportedConfig, 7082815dcSEvan Bacon ExportedConfigWithProps, 8082815dcSEvan Bacon ModPlatform, 9082815dcSEvan Bacon} from '../Plugin.types'; 10082815dcSEvan Bacon 11082815dcSEvan Baconconst debug = Debug('expo:config-plugins:base-mods'); 12082815dcSEvan Bacon 13082815dcSEvan Baconexport type ForwardedBaseModOptions = Partial< 14082815dcSEvan Bacon Pick<BaseModOptions, 'saveToInternal' | 'skipEmptyMod'> 15082815dcSEvan Bacon>; 16082815dcSEvan Bacon 17082815dcSEvan Baconexport type BaseModProviderMethods< 18082815dcSEvan Bacon ModType, 19*8a424bebSJames Ide Props extends ForwardedBaseModOptions = ForwardedBaseModOptions, 20082815dcSEvan Bacon> = { 21082815dcSEvan Bacon getFilePath: (config: ExportedConfigWithProps<ModType>, props: Props) => Promise<string> | string; 22082815dcSEvan Bacon read: ( 23082815dcSEvan Bacon filePath: string, 24082815dcSEvan Bacon config: ExportedConfigWithProps<ModType>, 25082815dcSEvan Bacon props: Props 26082815dcSEvan Bacon ) => Promise<ModType> | ModType; 27082815dcSEvan Bacon write: ( 28082815dcSEvan Bacon filePath: string, 29082815dcSEvan Bacon config: ExportedConfigWithProps<ModType>, 30082815dcSEvan Bacon props: Props 31082815dcSEvan Bacon ) => Promise<void> | void; 32082815dcSEvan Bacon /** 33082815dcSEvan Bacon * If the mod supports introspection, and avoids making any filesystem modifications during compilation. 34082815dcSEvan Bacon * By enabling, this mod, and all of its descendants will be run in introspection mode. 35082815dcSEvan Bacon * This should only be used for static files like JSON or XML, and not for application files that require regexes, 36082815dcSEvan Bacon * or complex static files that require other files to be generated like Xcode `.pbxproj`. 37082815dcSEvan Bacon */ 38082815dcSEvan Bacon isIntrospective?: boolean; 39082815dcSEvan Bacon}; 40082815dcSEvan Bacon 41082815dcSEvan Baconexport type CreateBaseModProps< 42082815dcSEvan Bacon ModType, 43*8a424bebSJames Ide Props extends ForwardedBaseModOptions = ForwardedBaseModOptions, 44082815dcSEvan Bacon> = { 45082815dcSEvan Bacon methodName: string; 46082815dcSEvan Bacon platform: ModPlatform; 47082815dcSEvan Bacon modName: string; 48082815dcSEvan Bacon} & BaseModProviderMethods<ModType, Props>; 49082815dcSEvan Bacon 50082815dcSEvan Baconexport function createBaseMod< 51082815dcSEvan Bacon ModType, 52*8a424bebSJames Ide Props extends ForwardedBaseModOptions = ForwardedBaseModOptions, 53082815dcSEvan Bacon>({ 54082815dcSEvan Bacon methodName, 55082815dcSEvan Bacon platform, 56082815dcSEvan Bacon modName, 57082815dcSEvan Bacon getFilePath, 58082815dcSEvan Bacon read, 59082815dcSEvan Bacon write, 60082815dcSEvan Bacon isIntrospective, 61082815dcSEvan Bacon}: CreateBaseModProps<ModType, Props>): ConfigPlugin<Props | void> { 62082815dcSEvan Bacon const withUnknown: ConfigPlugin<Props | void> = (config, _props) => { 63082815dcSEvan Bacon const props = _props || ({} as Props); 64082815dcSEvan Bacon return withBaseMod<ModType>(config, { 65082815dcSEvan Bacon platform, 66082815dcSEvan Bacon mod: modName, 67082815dcSEvan Bacon skipEmptyMod: props.skipEmptyMod ?? true, 68082815dcSEvan Bacon saveToInternal: props.saveToInternal ?? false, 69082815dcSEvan Bacon isProvider: true, 70082815dcSEvan Bacon isIntrospective, 71082815dcSEvan Bacon async action({ modRequest: { nextMod, ...modRequest }, ...config }) { 72082815dcSEvan Bacon try { 73082815dcSEvan Bacon let results: ExportedConfigWithProps<ModType> = { 74082815dcSEvan Bacon ...config, 75082815dcSEvan Bacon modRequest, 76082815dcSEvan Bacon }; 77082815dcSEvan Bacon 78082815dcSEvan Bacon const filePath = await getFilePath(results, props); 79082815dcSEvan Bacon debug(`mods.${platform}.${modName}: file path: ${filePath || '[skipped]'}`); 80082815dcSEvan Bacon const modResults = await read(filePath, results, props); 81082815dcSEvan Bacon 82082815dcSEvan Bacon results = await nextMod!({ 83082815dcSEvan Bacon ...results, 84082815dcSEvan Bacon modResults, 85082815dcSEvan Bacon modRequest, 86082815dcSEvan Bacon }); 87082815dcSEvan Bacon 88082815dcSEvan Bacon assertModResults(results, modRequest.platform, modRequest.modName); 89082815dcSEvan Bacon 90082815dcSEvan Bacon await write(filePath, results, props); 91082815dcSEvan Bacon return results; 92082815dcSEvan Bacon } catch (error: any) { 93082815dcSEvan Bacon error.message = `[${platform}.${modName}]: ${methodName}: ${error.message}`; 94082815dcSEvan Bacon throw error; 95082815dcSEvan Bacon } 96082815dcSEvan Bacon }, 97082815dcSEvan Bacon }); 98082815dcSEvan Bacon }; 99082815dcSEvan Bacon 100082815dcSEvan Bacon if (methodName) { 101082815dcSEvan Bacon Object.defineProperty(withUnknown, 'name', { 102082815dcSEvan Bacon value: methodName, 103082815dcSEvan Bacon }); 104082815dcSEvan Bacon } 105082815dcSEvan Bacon 106082815dcSEvan Bacon return withUnknown; 107082815dcSEvan Bacon} 108082815dcSEvan Bacon 109082815dcSEvan Baconexport function assertModResults(results: any, platformName: string, modName: string) { 110082815dcSEvan Bacon // If the results came from a mod, they'd be in the form of [config, data]. 111082815dcSEvan Bacon // Ensure the results are an array and omit the data since it should've been written by a data provider plugin. 112082815dcSEvan Bacon const ensuredResults = results; 113082815dcSEvan Bacon 114082815dcSEvan Bacon // Sanity check to help locate non compliant mods. 115082815dcSEvan Bacon if (!ensuredResults || typeof ensuredResults !== 'object' || !ensuredResults?.mods) { 116082815dcSEvan Bacon throw new Error( 117082815dcSEvan Bacon `Mod \`mods.${platformName}.${modName}\` evaluated to an object that is not a valid project config. Instead got: ${JSON.stringify( 118082815dcSEvan Bacon ensuredResults 119082815dcSEvan Bacon )}` 120082815dcSEvan Bacon ); 121082815dcSEvan Bacon } 122082815dcSEvan Bacon return ensuredResults; 123082815dcSEvan Bacon} 124082815dcSEvan Bacon 125082815dcSEvan Baconfunction upperFirst(name: string): string { 126082815dcSEvan Bacon return name.charAt(0).toUpperCase() + name.slice(1); 127082815dcSEvan Bacon} 128082815dcSEvan Bacon 129082815dcSEvan Baconexport function createPlatformBaseMod< 130082815dcSEvan Bacon ModType, 131*8a424bebSJames Ide Props extends ForwardedBaseModOptions = ForwardedBaseModOptions, 132082815dcSEvan Bacon>({ modName, ...props }: Omit<CreateBaseModProps<ModType, Props>, 'methodName'>) { 133082815dcSEvan Bacon // Generate the function name to ensure it's uniform and also to improve stack traces. 134082815dcSEvan Bacon const methodName = `with${upperFirst(props.platform)}${upperFirst(modName)}BaseMod`; 135082815dcSEvan Bacon return createBaseMod<ModType, Props>({ 136082815dcSEvan Bacon methodName, 137082815dcSEvan Bacon modName, 138082815dcSEvan Bacon ...props, 139082815dcSEvan Bacon }); 140082815dcSEvan Bacon} 141082815dcSEvan Bacon 142082815dcSEvan Bacon/** A TS wrapper for creating provides */ 143082815dcSEvan Baconexport function provider<ModType, Props extends ForwardedBaseModOptions = ForwardedBaseModOptions>( 144082815dcSEvan Bacon props: BaseModProviderMethods<ModType, Props> 145082815dcSEvan Bacon) { 146082815dcSEvan Bacon return props; 147082815dcSEvan Bacon} 148082815dcSEvan Bacon 149082815dcSEvan Bacon/** Plugin to create and append base mods from file providers */ 150082815dcSEvan Baconexport function withGeneratedBaseMods<ModName extends string>( 151082815dcSEvan Bacon config: ExportedConfig, 152082815dcSEvan Bacon { 153082815dcSEvan Bacon platform, 154082815dcSEvan Bacon providers, 155082815dcSEvan Bacon ...props 156082815dcSEvan Bacon }: ForwardedBaseModOptions & { 157edc75823SEvan Bacon /** Officially supports `'ios' | 'android'` (`ModPlatform`). Arbitrary strings are supported for adding out-of-tree platforms. */ 158edc75823SEvan Bacon platform: ModPlatform & string; 159082815dcSEvan Bacon providers: Partial<Record<ModName, BaseModProviderMethods<any, any>>>; 160082815dcSEvan Bacon } 161082815dcSEvan Bacon): ExportedConfig { 162082815dcSEvan Bacon return Object.entries(providers).reduce((config, [modName, value]) => { 163082815dcSEvan Bacon const baseMod = createPlatformBaseMod({ platform, modName, ...(value as any) }); 164082815dcSEvan Bacon return baseMod(config, props); 165082815dcSEvan Bacon }, config); 166082815dcSEvan Bacon} 167