diff --git a/packages/eas-cli/src/build/__tests__/validate-test.ts b/packages/eas-cli/src/build/__tests__/validate-test.ts index ac2e0b533f..1f61af9b93 100644 --- a/packages/eas-cli/src/build/__tests__/validate-test.ts +++ b/packages/eas-cli/src/build/__tests__/validate-test.ts @@ -61,7 +61,7 @@ describe(validateIconForManagedProjectAsync, () => { }); describe(Platform.IOS, () => { - it('exits if foregroundImage is not a file with .png extension', async () => { + it('exits if icon is not a file with .png extension', async () => { const ctxMock = mock>(); when(ctxMock.workflow).thenReturn(Workflow.MANAGED); when(ctxMock.platform).thenReturn(Platform.IOS); diff --git a/packages/eas-cli/src/build/validate.ts b/packages/eas-cli/src/build/validate.ts index 36f1d9b429..6e5b0740e2 100644 --- a/packages/eas-cli/src/build/validate.ts +++ b/packages/eas-cli/src/build/validate.ts @@ -4,12 +4,7 @@ import fs from 'fs-extra'; import path from 'path'; import Log, { learnMore } from '../log'; -import { - ImageNonPngError, - ImageTransparencyError, - ensurePNGIsNotTransparentAsync, - isPNGAsync, -} from '../utils/image'; +import { ImageTransparencyError, ensurePNGIsNotTransparentAsync, isPNGAsync } from '../utils/image'; import { getVcsClient } from '../vcs'; import { CommonContext } from './context'; @@ -66,52 +61,75 @@ export async function validateIconForManagedProjectAsync( } if (ctx.platform === Platform.ANDROID) { - await validateAndroidIconAsync(ctx as CommonContext); + await validateAndroidPNGsAsync(ctx as CommonContext); } else { - await validateIosIconAsync(ctx as CommonContext); + await validateIosPNGsAsync(ctx as CommonContext); } } -async function validateAndroidIconAsync(ctx: CommonContext): Promise { - if (!ctx.exp.android?.adaptiveIcon?.foregroundImage) { - return; - } - - const { foregroundImage } = ctx.exp.android.adaptiveIcon; - if (!foregroundImage.endsWith('.png')) { - Log.error(`The Android Adaptive Icon foreground image must be a PNG file.`); - Log.error(`"expo.android.adaptiveIcon.foregroundImage" = ${foregroundImage}`); - Errors.exit(1); - } +type ConfigPng = { configPath: string; pngPath: string | undefined }; - if (!(await isPNGAsync(foregroundImage))) { - Log.error(`The Android Adaptive Icon foreground image must be a PNG file.`); - Log.error(`${foregroundImage} is not valid PNG`); - Errors.exit(1); - } +async function validateAndroidPNGsAsync(ctx: CommonContext): Promise { + const pngs: ConfigPng[] = [ + { + configPath: 'exp.icon', + pngPath: ctx.exp.icon, + }, + { + configPath: 'exp.android.icon', + pngPath: ctx.exp.android?.icon, + }, + { + configPath: 'exp.android.adaptiveIcon.foregroundImage', + pngPath: ctx.exp.android?.adaptiveIcon?.foregroundImage, + }, + { + configPath: 'exp.android.adaptiveIcon.backgroundImage', + pngPath: ctx.exp.android?.adaptiveIcon?.backgroundImage, + }, + { + configPath: 'exp.splash.image', + pngPath: ctx.exp.splash?.image, + }, + { + configPath: 'exp.notification.icon', + pngPath: ctx.exp.notification?.icon, + }, + ]; + await validatePNGsAsync(pngs); } -async function validateIosIconAsync(ctx: CommonContext): Promise { +async function validateIosPNGsAsync(ctx: CommonContext): Promise { + const pngs: ConfigPng[] = [ + { + configPath: 'exp.icon', + pngPath: ctx.exp.icon, + }, + { + configPath: 'exp.ios.icon', + pngPath: ctx.exp.ios?.icon, + }, + { + configPath: 'exp.splash.image', + pngPath: ctx.exp.splash?.image, + }, + { + configPath: 'exp.notification.icon', + pngPath: ctx.exp.notification?.icon, + }, + ]; + await validatePNGsAsync(pngs); + const icon = ctx.exp.ios?.icon ?? ctx.exp.icon; if (!icon) { return; } const iconConfigPath = `expo${ctx.exp.ios?.icon ? '.ios' : ''}.icon`; - if (!icon.endsWith('.png')) { - Log.error(`The iOS app icon must be a PNG file.`); - Log.error(`"${iconConfigPath}" = ${icon}`); - Errors.exit(1); - } - try { await ensurePNGIsNotTransparentAsync(icon); } catch (err: any) { - if (err instanceof ImageNonPngError) { - Log.error(`The iOS app icon must be a PNG file.`); - Log.error(`"${iconConfigPath}" = ${icon}`); - Errors.exit(1); - } else if (err instanceof ImageTransparencyError) { + if (err instanceof ImageTransparencyError) { Log.error( `Your iOS app icon (${iconConfigPath}) can't have transparency if you wish to upload your app to the Apple App Store.` ); @@ -122,3 +140,36 @@ async function validateIosIconAsync(ctx: CommonContext): Promise { + const validationPromises = configPngs.map(configPng => validatePNGAsync(configPng)); + const validationResults = await Promise.allSettled(validationPromises); + const failedValidations = validationResults.filter( + (result): result is PromiseRejectedResult => result.status === 'rejected' + ); + + if (failedValidations.length === 0) { + return; + } + + Log.error('PNG images validation failed:'); + for (const { reason } of failedValidations) { + const error: Error = reason; + Log.error(`- ${error.message}`); + } + Errors.exit(1); +} + +async function validatePNGAsync({ configPath, pngPath }: ConfigPng): Promise { + if (!pngPath) { + return; + } + + if (!pngPath.endsWith('.png')) { + throw new Error(`"${configPath}" is not a PNG file`); + } + + if (!(await isPNGAsync(pngPath))) { + throw new Error(`"${configPath}" is not valid PNG`); + } +}