diff --git a/packages/cli/src/commands/main/constants.ts b/packages/cli/src/commands/main/constants.ts index 7e332822..860149af 100644 --- a/packages/cli/src/commands/main/constants.ts +++ b/packages/cli/src/commands/main/constants.ts @@ -3,12 +3,14 @@ const docsDomain = 'https://docs.sherlo.io'; export const docsLink = { config: `${docsDomain}/getting-started/config`, + configProperties: `${docsDomain}/getting-started/config#properties`, configToken: `${docsDomain}/getting-started/config#token`, - configApps: `${docsDomain}/getting-started/config#apps`, configAndroid: `${docsDomain}/getting-started/config#android`, configIos: `${docsDomain}/getting-started/config#ios`, configDevices: `${docsDomain}/getting-started/config#devices`, devices: `${docsDomain}/devices`, + remoteExpoBuilds: `${docsDomain}/getting-started/builds?framework=expo&eas-build=remote`, + scriptFlags: `${docsDomain}/getting-started/testing#supported-flags`, }; -export const iOSFileTypes = ['.app', '.tar', '.tar.gz'] as const; +export const iOSFileTypes = ['.app', '.tar.gz', '.tar'] as const; diff --git a/packages/cli/src/commands/main/helpers/getArguments.ts b/packages/cli/src/commands/main/helpers/getArguments.ts index c5bea75d..7d505148 100644 --- a/packages/cli/src/commands/main/helpers/getArguments.ts +++ b/packages/cli/src/commands/main/helpers/getArguments.ts @@ -14,33 +14,27 @@ import { } from './utils'; type Parameters = { - async?: boolean; - asyncBuildIndex?: number; token?: string; android?: string; ios?: string; + remoteExpo?: boolean; + async?: boolean; + asyncBuildIndex?: number; gitInfo?: Build['gitInfo']; // Can be passed only in GitHub Action -} & (T extends 'withDefaults' - ? { - config: string; - projectRoot: string; - } - : { - config?: string; - projectRoot?: string; - }); +} & (T extends 'withDefaults' ? ParameterDefaults : Partial); +type ParameterDefaults = { config: string; projectRoot: string }; type Arguments = SyncArguments | AsyncInitArguments | AsyncUploadArguments; type SyncArguments = { mode: 'sync'; token: string; - config: Config<'withPaths'>; + config: Config<'withBuildPaths'>; gitInfo: Build['gitInfo']; }; type AsyncInitArguments = { - mode: 'asyncInit'; + mode: 'asyncInit' | 'remoteExpo'; token: string; - config: Config<'withoutPaths'>; + config: Config<'withoutBuildPaths'>; gitInfo: Build['gitInfo']; projectRoot: string; }; @@ -53,53 +47,57 @@ type AsyncUploadArguments = { }; function getArguments(githubActionParameters?: Parameters): Arguments { - const parameters = githubActionParameters ?? program.parse(process.argv).opts(); - const updatedParameters = updateParameters(parameters); + const params = githubActionParameters ?? command.parse(process.argv).opts(); + const parameters = applyParameterDefaults(params); - const config = parseConfigFile(updatedParameters.config); - const updatedConfig = updateConfig(config, updatedParameters); + const configPath = nodePath.resolve(parameters.projectRoot, parameters.config); + const configFile = parseConfigFile(configPath); + const config = getConfig(configFile, parameters); let mode: Mode = 'sync'; - if (updatedParameters.async && !updatedParameters.asyncBuildIndex) { + if (parameters.remoteExpo) { + mode = 'remoteExpo'; + } else if (parameters.async && !parameters.asyncBuildIndex) { mode = 'asyncInit'; - } else if (updatedParameters.asyncBuildIndex) { + } else if (parameters.asyncBuildIndex) { mode = 'asyncUpload'; } - validateConfigToken(updatedConfig); - const { token } = updatedConfig; + validateConfigToken(config); + const { token } = config; switch (mode) { case 'sync': { - validateConfigPlatforms(updatedConfig, 'withPaths'); - validateConfigDevices(updatedConfig); + validateConfigPlatforms(config, 'withBuildPaths'); + validateConfigDevices(config); // validateFilters(updatedConfig); return { mode, token, - config: updatedConfig as Config<'withPaths'>, + config: config as Config<'withBuildPaths'>, gitInfo: githubActionParameters?.gitInfo ?? getGitInfo(), } satisfies SyncArguments; } + case 'remoteExpo': case 'asyncInit': { - validateConfigPlatforms(updatedConfig, 'withoutPaths'); - validateConfigDevices(updatedConfig); + // validateConfigPlatforms(config, 'withoutBuildPaths'); + validateConfigDevices(config); // validateFilters(updatedConfig); return { mode, token, - config: updatedConfig as Config<'withoutPaths'>, + config: config as Config<'withoutBuildPaths'>, gitInfo: githubActionParameters?.gitInfo ?? getGitInfo(), - projectRoot: updatedParameters.projectRoot, + projectRoot: parameters.projectRoot, } satisfies AsyncInitArguments; } case 'asyncUpload': { - const { path, platform } = getAsyncUploadArguments(updatedParameters); - const { asyncBuildIndex } = updatedParameters; + const { path, platform } = getAsyncUploadArguments(parameters); + const { asyncBuildIndex } = parameters; if (!asyncBuildIndex) { throw new Error( @@ -128,84 +126,64 @@ export default getArguments; const DEFAULT_CONFIG_PATH = 'sherlo.config.json'; const DEFAULT_PROJECT_ROOT = '.'; -const program = new Command(); - -program - .option('--config ', 'Path to Sherlo config', DEFAULT_CONFIG_PATH) +const command = new Command(); +command + .option('--token ', 'Project token') + .option('--android ', 'Path to Android build in .apk format') + .option('--ios ', 'Path to iOS build in .app (or compressed .tar.gz / .tar) format') + .option('--config ', 'Config file path', DEFAULT_CONFIG_PATH) + .option('--projectRoot ', 'Root of the React Native project', DEFAULT_PROJECT_ROOT) .option( - '--async', - 'Run Sherlo in async mode, meaning you don’t have to provide builds immediately' + '--remoteExpo', + 'Run Sherlo in remote Expo mode, waiting for the builds to complete on the Expo servers' ) - .option('--asyncBuildIndex ', 'Index of build you want to update in async mode', parseInt) .option( - '--projectRoot ', - 'Root of the react native project when working with monorepo', - DEFAULT_PROJECT_ROOT + '--async', + "Run Sherlo in async mode, meaning you don't have to provide builds immediately" ) - .option('--token ', 'Sherlo project token') - .option('--android ', 'Path to Android build in .apk format') - .option('--ios ', 'Path to iOS simulator build in .app or .tar/.tar.gz file format'); + .option( + '--asyncBuildIndex ', + 'Index of build you want to update in async mode', + parseInt + ); -function updateParameters(parameters: Parameters): Parameters<'withDefaults'> { - // Set defaults if are not defined (can happen in GitHub action case) - const projectRoot = parameters.projectRoot ?? DEFAULT_PROJECT_ROOT; - const config = parameters.config ?? DEFAULT_CONFIG_PATH; +function applyParameterDefaults(params: Parameters): Parameters<'withDefaults'> { + const projectRoot = params.projectRoot ?? DEFAULT_PROJECT_ROOT; + const config = params.config ?? DEFAULT_CONFIG_PATH; - // Update paths based on project root return { - ...parameters, + ...params, projectRoot, - config: nodePath.join(projectRoot, config), - android: parameters.android ? nodePath.join(projectRoot, parameters.android) : undefined, - ios: parameters.ios ? nodePath.join(projectRoot, parameters.ios) : undefined, + config, }; } -function updateConfig( - config: InvalidatedConfig, - updatedParameters: Parameters<'withDefaults'> +function getConfig( + configFile: InvalidatedConfig, + parameters: Parameters<'withDefaults'> ): InvalidatedConfig { + const { projectRoot } = parameters; + // Take token from parameters or config file - const token = updatedParameters.token ?? config.token; + const token = parameters.token ?? configFile.token; // Set a proper android path - let androidPath: string | undefined; - if (updatedParameters.android) { - androidPath = updatedParameters.android; - } else if (config.android?.path) { - androidPath = nodePath.join(updatedParameters.projectRoot, config.android.path); - } - const android = config.android - ? { - ...config.android, - path: androidPath, - } - : undefined; + let android = parameters.android ?? configFile.android; + android = android ? nodePath.resolve(projectRoot, android) : undefined; // Set a proper ios path - let iosPath: string | undefined; - if (updatedParameters.ios) { - iosPath = updatedParameters.ios; - } else if (config.ios?.path) { - iosPath = nodePath.join(updatedParameters.projectRoot, config.ios.path); - } - const ios = config.ios - ? { - ...config.ios, - path: iosPath, - } - : undefined; + let ios = parameters.ios ?? configFile.ios; + ios = ios ? nodePath.resolve(projectRoot, ios) : undefined; // Set defaults for devices - let { devices } = config; - devices = devices?.map((device) => ({ + const devices = configFile.devices?.map((device) => ({ ...device, osLanguage: device?.osLanguage ?? defaultDeviceOsLanguage, osTheme: device?.osTheme ?? defaultDeviceOsTheme, })); return { - ...config, + ...configFile, token, android, ios, @@ -217,9 +195,8 @@ function getAsyncUploadArguments(parameters: Parameters): { path: string; platfo if (parameters.android && parameters.ios) { throw new Error( getErrorMessage({ - type: 'default', message: - 'Don\'t use "asyncBuildIndex" if you\'re providing android and ios at the same time', + 'If you are providing both Android and iOS at the same time, use Sherlo in regular mode (without the `--async` flag)', }) ); } @@ -227,7 +204,6 @@ function getAsyncUploadArguments(parameters: Parameters): { path: string; platfo if (!parameters.android && !parameters.ios) { throw new Error( getErrorMessage({ - type: 'default', message: 'When using "asyncBuildIndex" you need to provide one build path, ios or android', }) ); diff --git a/packages/cli/src/commands/main/helpers/utils/index.ts b/packages/cli/src/commands/main/helpers/utils/index.ts index ade09b15..c299566f 100644 --- a/packages/cli/src/commands/main/helpers/utils/index.ts +++ b/packages/cli/src/commands/main/helpers/utils/index.ts @@ -1,4 +1,3 @@ -export { default as getConfigErrorMessage } from './getConfigErrorMessage'; export { default as getGitInfo } from './getGitInfo'; export { default as parseConfigFile } from './parseConfigFile'; export { default as validateConfigDevices } from './validateConfigDevices'; diff --git a/packages/cli/src/commands/main/helpers/utils/parseConfigFile.ts b/packages/cli/src/commands/main/helpers/utils/parseConfigFile.ts index 32cf9974..62154a2d 100644 --- a/packages/cli/src/commands/main/helpers/utils/parseConfigFile.ts +++ b/packages/cli/src/commands/main/helpers/utils/parseConfigFile.ts @@ -1,7 +1,7 @@ import fs from 'fs'; +import { docsLink } from '../../constants'; import { InvalidatedConfig } from '../../types'; -import { getErrorMessage } from '../../utils'; -import getConfigErrorMessage from './getConfigErrorMessage'; +import { getConfigErrorMessage, getErrorMessage } from '../../utils'; /* * 1. Both `include` and `exclude` can be defined as a string or an array of @@ -11,7 +11,14 @@ function parseConfigFile(path: string): InvalidatedConfig { try { const config = JSON.parse(fs.readFileSync(path, 'utf8')); - if (!config) throw new Error('config is undefined'); + if (!config) { + throw new Error( + getErrorMessage({ + type: 'unexpected', + message: `parsed config file "${path}" is undefined`, + }) + ); + } /* 1 */ // const { exclude, include } = config; @@ -24,14 +31,19 @@ function parseConfigFile(path: string): InvalidatedConfig { switch (nodeError.code) { case 'ENOENT': - throw new Error(getConfigErrorMessage(`file "${path}" not found`)); + throw new Error( + getConfigErrorMessage( + `config file "${path}" not found; make sure the path is correct or pass the \`--projectRoot\` flag to the script`, + docsLink.scriptFlags + ) + ); case 'EACCES': - throw new Error(getConfigErrorMessage(`file "${path}" cannot be accessed`)); + throw new Error(getConfigErrorMessage(`config file "${path}" cannot be accessed`)); case 'EISDIR': throw new Error(getConfigErrorMessage(`"${path}" is a directory, not a config file`)); default: if (error instanceof SyntaxError) { - throw new Error(getConfigErrorMessage(`file "${path}" is not valid JSON`)); + throw new Error(getConfigErrorMessage(`config file "${path}" is not valid JSON`)); } else { throw new Error( getErrorMessage({ diff --git a/packages/cli/src/commands/main/helpers/utils/validateConfigDevices.ts b/packages/cli/src/commands/main/helpers/utils/validateConfigDevices.ts index 502bccbb..0013bee7 100644 --- a/packages/cli/src/commands/main/helpers/utils/validateConfigDevices.ts +++ b/packages/cli/src/commands/main/helpers/utils/validateConfigDevices.ts @@ -1,8 +1,8 @@ +import { DeviceTheme } from '@sherlo/api-types'; import { devices as sherloDevices } from '@sherlo/shared'; import { docsLink } from '../../constants'; import { InvalidatedConfig } from '../../types'; -import getConfigErrorMessage from './getConfigErrorMessage'; -import { DeviceTheme } from '@sherlo/api-types'; +import { getConfigErrorMessage } from '../../utils'; function validateConfigDevices(config: InvalidatedConfig): void { const { devices } = config; diff --git a/packages/cli/src/commands/main/helpers/utils/validateConfigPlatformPath.ts b/packages/cli/src/commands/main/helpers/utils/validateConfigPlatformPath.ts index 355da56f..93e3d6c0 100644 --- a/packages/cli/src/commands/main/helpers/utils/validateConfigPlatformPath.ts +++ b/packages/cli/src/commands/main/helpers/utils/validateConfigPlatformPath.ts @@ -1,7 +1,7 @@ import { Platform } from '@sherlo/api-types'; import fs from 'fs'; import { docsLink, iOSFileTypes } from '../../constants'; -import getConfigErrorMessage from './getConfigErrorMessage'; +import { getConfigErrorMessage } from '../../utils'; const learnMoreLink: { [platform in Platform]: string } = { android: docsLink.configAndroid, @@ -16,17 +16,14 @@ const fileType: { [platformName in Platform]: readonly string[] } = { function validateConfigPlatformPath(path: string | undefined, platform: Platform): void { if (!path || typeof path !== 'string') { throw new Error( - getConfigErrorMessage( - `for ${platform}, path must be a defined string`, - learnMoreLink[platform] - ) + getConfigErrorMessage(`${platform} must be a defined string`, learnMoreLink[platform]) ); } if (!fs.existsSync(path) || !hasValidExtension({ path, platform })) { throw new Error( getConfigErrorMessage( - `for ${platform}, path must be a valid ${formatValidFileTypes(platform)} file`, + `${platform} must be a valid ${formatValidFileTypes(platform)} file`, learnMoreLink[platform] ) ); diff --git a/packages/cli/src/commands/main/helpers/utils/validateConfigPlatforms.ts b/packages/cli/src/commands/main/helpers/utils/validateConfigPlatforms.ts index 32bf99f8..0e9a92a5 100644 --- a/packages/cli/src/commands/main/helpers/utils/validateConfigPlatforms.ts +++ b/packages/cli/src/commands/main/helpers/utils/validateConfigPlatforms.ts @@ -1,91 +1,94 @@ import { Platform } from '@sherlo/api-types'; import { docsLink } from '../../constants'; -import { getErrorMessage } from '../../utils'; import { ConfigMode, InvalidatedConfig } from '../../types'; -import getConfigErrorMessage from './getConfigErrorMessage'; +import { getConfigErrorMessage } from '../../utils'; import validateConfigPlatformPath from './validateConfigPlatformPath'; -function validateConfigPlatforms(config: InvalidatedConfig, mode: ConfigMode): void { +function validateConfigPlatforms(config: InvalidatedConfig, configMode: ConfigMode): void { const { android, ios } = config; - if (!android && !ios) { + if (configMode === 'withBuildPaths' && !android && !ios) { throw new Error( - getConfigErrorMessage('at least one platform must be defined', docsLink.configApps) + getConfigErrorMessage('at least one platform build path must be defined', docsLink.config) ); } - if (android) validatePlatform(config, 'android', mode); + if (android) validatePlatform(config, 'android', configMode); - if (ios) validatePlatform(config, 'ios', mode); + if (ios) validatePlatform(config, 'ios', configMode); } export default validateConfigPlatforms; /* ========================================================================== */ -function validatePlatform(config: InvalidatedConfig, platform: Platform, mode: ConfigMode): void { - validatePlatformSpecificParameters(config, platform); +function validatePlatform( + config: InvalidatedConfig, + platform: Platform, + configMode: ConfigMode +): void { + // validatePlatformSpecificParameters(config, platform); - if (mode === 'withPaths') { - validateConfigPlatformPath(config[platform]?.path, platform); + if (configMode === 'withBuildPaths') { + validateConfigPlatformPath(config[platform], platform); } } -function validatePlatformSpecificParameters(config: InvalidatedConfig, platform: Platform): void { - if (platform === 'android') { - const { android } = config; - if (!android) { - throw new Error( - getErrorMessage({ - type: 'unexpected', - message: 'android should be defined', - }) - ); - } +// function validatePlatformSpecificParameters(config: InvalidatedConfig, platform: Platform): void { +// if (platform === 'android') { +// const { android } = config; +// if (!android) { +// throw new Error( +// getErrorMessage({ +// type: 'unexpected', +// message: 'android should be defined', +// }) +// ); +// } - if ( - !android.packageName || - typeof android.packageName !== 'string' || - !android.packageName.includes('.') - ) { - throw new Error( - getConfigErrorMessage( - 'for android, packageName must be a valid string', - docsLink.configAndroid - ) - ); - } +// if ( +// !android.packageName || +// typeof android.packageName !== 'string' || +// !android.packageName.includes('.') +// ) { +// throw new Error( +// getConfigErrorMessage( +// 'for android, packageName must be a valid string', +// docsLink.configAndroid +// ) +// ); +// } - if (android.activity && typeof android.activity !== 'string') { - throw new Error( - getConfigErrorMessage( - 'for android, if activity is defined, it must be a string', - docsLink.configAndroid - ) - ); - } - } else if (platform === 'ios') { - const { ios } = config; - if (!ios) { - throw new Error( - getErrorMessage({ - type: 'unexpected', - message: 'ios should be defined', - }) - ); - } +// if (android.activity && typeof android.activity !== 'string') { +// throw new Error( +// getConfigErrorMessage( +// 'for android, if activity is defined, it must be a string', +// docsLink.configAndroid +// ) +// ); +// } +// } else if (platform === 'ios') { +// const { ios } = config; +// if (!ios) { +// throw new Error( +// getErrorMessage({ +// type: 'unexpected', +// message: 'ios should be defined', +// }) +// ); +// } - if ( - !ios.bundleIdentifier || - typeof ios.bundleIdentifier !== 'string' || - !ios.bundleIdentifier.includes('.') - ) { - throw new Error( - getConfigErrorMessage( - 'for ios, bundleIdentifier must be a valid string', - docsLink.configIos - ) - ); - } - } -} +// if ( +// !ios.bundleIdentifier || +// typeof ios.bundleIdentifier !== 'string' || +// !ios.bundleIdentifier.includes('.') +// ) { +// throw new Error( +// getConfigErrorMessage( +// 'for ios, bundleIdentifier must be a valid string', +// docsLink.configIos +// ) +// ); +// } +// } +// } diff --git a/packages/cli/src/commands/main/helpers/utils/validateConfigToken.ts b/packages/cli/src/commands/main/helpers/utils/validateConfigToken.ts index 78358d32..64dad857 100644 --- a/packages/cli/src/commands/main/helpers/utils/validateConfigToken.ts +++ b/packages/cli/src/commands/main/helpers/utils/validateConfigToken.ts @@ -1,8 +1,7 @@ import { projectApiTokenLength, teamIdLength } from '@sherlo/shared'; import { docsLink } from '../../constants'; -import { getTokenParts } from '../../utils'; import { InvalidatedConfig } from '../../types'; -import getConfigErrorMessage from './getConfigErrorMessage'; +import { getConfigErrorMessage, getTokenParts } from '../../utils'; function validateConfigToken( config: InvalidatedConfig diff --git a/packages/cli/src/commands/main/main.ts b/packages/cli/src/commands/main/main.ts index 7d17fd0f..7cd91710 100644 --- a/packages/cli/src/commands/main/main.ts +++ b/packages/cli/src/commands/main/main.ts @@ -14,8 +14,9 @@ async function main(githubActionParameters?: Parameters[0]) return syncMode(args); } + case 'remoteExpo': case 'asyncInit': { - return asyncInitMode(args); + return asyncInitMode(args, args.mode === 'remoteExpo'); } case 'asyncUpload': { diff --git a/packages/cli/src/commands/main/modes/asyncInitMode.ts b/packages/cli/src/commands/main/modes/asyncInitMode.ts index 8c89e489..8a9e9bb6 100644 --- a/packages/cli/src/commands/main/modes/asyncInitMode.ts +++ b/packages/cli/src/commands/main/modes/asyncInitMode.ts @@ -1,20 +1,30 @@ import { Build } from '@sherlo/api-types'; import SDKApiClient from '@sherlo/sdk-client'; +import fs from 'fs'; +import path from 'path'; +import { docsLink } from '../constants'; import { getErrorMessage, getTokenParts } from '../utils'; import { Config } from '../types'; -import { createExpoSherloTempFile, getAppBuildUrl, getBuildRunConfig } from './utils'; +import { getAppBuildUrl, getBuildRunConfig } from './utils'; + +async function asyncInitMode( + { + token, + config, + gitInfo, + projectRoot, + }: { + token: string; + config: Config<'withoutBuildPaths'>; + gitInfo: Build['gitInfo']; + projectRoot: string; + }, + isExpoRemoteMode: boolean +): Promise<{ buildIndex: number; url: string }> { + if (isExpoRemoteMode) { + validateEasBuildOnSuccessScript(projectRoot); + } -async function asyncInitMode({ - token, - config, - gitInfo, - projectRoot, -}: { - token: string; - config: Config<'withoutPaths'>; - gitInfo: Build['gitInfo']; - projectRoot: string; -}): Promise<{ buildIndex: number; url: string }> { const { apiToken, projectIndex, teamId } = getTokenParts(token); const client = SDKApiClient(apiToken); @@ -22,7 +32,7 @@ async function asyncInitMode({ .openBuild({ teamId, projectIndex, - gitInfo: gitInfo, + gitInfo, asyncUpload: true, buildRunConfig: getBuildRunConfig({ config }), }) @@ -33,7 +43,9 @@ async function asyncInitMode({ const buildIndex = build.index; const url = getAppBuildUrl({ buildIndex, projectIndex, teamId }); - createExpoSherloTempFile({ projectRoot, buildIndex, url, token }); + if (isExpoRemoteMode) { + createExpoSherloTempFile({ buildIndex, projectRoot, token, url }); + } console.log( `Sherlo is awaiting your builds to be uploaded asynchronously.\nBuild index is ${buildIndex}.\n` @@ -43,3 +55,55 @@ async function asyncInitMode({ } export default asyncInitMode; + +/* ========================================================================== */ + +function validateEasBuildOnSuccessScript(projectRoot: string): void { + const packageJsonPath = path.resolve(projectRoot, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + throw new Error( + getErrorMessage({ + message: `package.json file not found at location "${projectRoot}"; make sure the directory is correct or pass the \`--projectRoot\` flag to the script`, + learnMoreLink: docsLink.scriptFlags, + }) + ); + } + + const packageJsonData = fs.readFileSync(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageJsonData); + + if (!packageJson.scripts || !packageJson.scripts['eas-build-on-success']) { + throw new Error( + getErrorMessage({ + message: '"eas-build-on-success" script is not defined in package.json', + learnMoreLink: docsLink.remoteExpoBuilds, + }) + ); + } +} + +function createExpoSherloTempFile({ + projectRoot, + buildIndex, + url, + token, +}: { + projectRoot: string; + buildIndex: number; + token: string; + url: string; +}): void { + const expoDir = path.resolve(projectRoot, '.expo'); + + // Check if the directory exists + if (!fs.existsSync(expoDir)) { + // If the directory does not exist, create it + fs.mkdirSync(expoDir, { recursive: true }); + } + + // Now that we've ensured the directory exists, write the file + fs.writeFileSync( + path.resolve(expoDir, 'sherlo.json'), + JSON.stringify({ buildIndex, token, url }) + ); +} diff --git a/packages/cli/src/commands/main/modes/syncMode.ts b/packages/cli/src/commands/main/modes/syncMode.ts index d4357363..4f9783bc 100644 --- a/packages/cli/src/commands/main/modes/syncMode.ts +++ b/packages/cli/src/commands/main/modes/syncMode.ts @@ -1,12 +1,13 @@ import { Build } from '@sherlo/api-types'; import SDKApiClient from '@sherlo/sdk-client'; -import { getErrorMessage, getTokenParts } from '../utils'; +import { docsLink } from '../constants'; import { Config } from '../types'; +import { getConfigErrorMessage, getErrorMessage, getTokenParts } from '../utils'; import { getAppBuildUrl, getBuildRunConfig, getBuildUploadUrls, - getConfigPlatforms, + getPlatformsToTest, uploadMobileBuilds, } from './utils'; @@ -16,22 +17,42 @@ async function syncMode({ gitInfo, }: { token: string; - config: Config<'withPaths'>; + config: Config<'withBuildPaths'>; gitInfo: Build['gitInfo']; }): Promise<{ buildIndex: number; url: string }> { const { apiToken, projectIndex, teamId } = getTokenParts(token); const client = SDKApiClient(apiToken); + const platformsToTest = getPlatformsToTest(config); + + if (platformsToTest.includes('android') && !config.android) { + throw new Error( + getConfigErrorMessage( + 'path to the Android build is not provided, despite at least one Android testing device having been defined', + docsLink.configAndroid + ) + ); + } + + if (platformsToTest.includes('ios') && !config.ios) { + throw new Error( + getConfigErrorMessage( + 'path to the iOS build is not provided, despite at least one iOS testing device having been defined', + docsLink.configIos + ) + ); + } + const buildUploadUrls = await getBuildUploadUrls(client, { - platforms: getConfigPlatforms(config), + platforms: platformsToTest, projectIndex, teamId, }); await uploadMobileBuilds( { - android: config.android?.path, - ios: config.ios?.path, + android: platformsToTest.includes('android') ? config.android : undefined, + ios: platformsToTest.includes('ios') ? config.ios : undefined, }, buildUploadUrls ); @@ -40,9 +61,9 @@ async function syncMode({ .openBuild({ teamId, projectIndex, - gitInfo: gitInfo, + gitInfo, buildRunConfig: getBuildRunConfig({ - config: config, + config, buildPresignedUploadUrls: buildUploadUrls, }), }) diff --git a/packages/cli/src/commands/main/modes/utils/createExpoSherloTempFile.ts b/packages/cli/src/commands/main/modes/utils/createExpoSherloTempFile.ts deleted file mode 100644 index b793a58e..00000000 --- a/packages/cli/src/commands/main/modes/utils/createExpoSherloTempFile.ts +++ /dev/null @@ -1,45 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - -function createExpoSherloTempFile({ - projectRoot, - buildIndex, - url, - token, -}: { - projectRoot: string; - buildIndex: number; - token: string; - url: string; -}): void { - const packageJsonPath = path.join(projectRoot, 'package.json'); - if (!fs.existsSync(packageJsonPath)) { - throw new Error(`No package.json found in projectRoot directory (${projectRoot}).`); - } - - try { - const packageJsonData = fs.readFileSync(packageJsonPath, 'utf8'); - const packageJson = JSON.parse(packageJsonData); - - // Check if the scripts section is defined and if 'eas-build-on-success' script exists - if (packageJson.scripts && packageJson.scripts['eas-build-on-success']) { - const expoDir = path.join(projectRoot, '.expo'); - - // Check if the directory exists - if (!fs.existsSync(expoDir)) { - // If the directory does not exist, create it - fs.mkdirSync(expoDir, { recursive: true }); - } - - // Now that we've ensured the directory exists, write the file - fs.writeFileSync( - path.join(expoDir, 'sherlo.json'), - JSON.stringify({ buildIndex, token, url }) - ); - } - } catch (err) { - // If there's an error, continue - } -} - -export default createExpoSherloTempFile; diff --git a/packages/cli/src/commands/main/modes/utils/getBuildRunConfig.ts b/packages/cli/src/commands/main/modes/utils/getBuildRunConfig.ts index ab033b8c..e963bfa5 100644 --- a/packages/cli/src/commands/main/modes/utils/getBuildRunConfig.ts +++ b/packages/cli/src/commands/main/modes/utils/getBuildRunConfig.ts @@ -7,31 +7,33 @@ function getBuildRunConfig({ config, }: { buildPresignedUploadUrls?: GetBuildUploadUrlsReturn['buildPresignedUploadUrls']; - config: Config<'withPaths'> | Config<'withoutPaths'>; + config: Config<'withBuildPaths'> | Config<'withoutBuildPaths'>; }): BuildRunConfig { - const { android, ios, devices } = config; + const { devices } = config; - const androidDevices = getConvertedDevices(devices, 'android'); - const iosDevices = getConvertedDevices(devices, 'ios'); + const androidDevices = getPlatformDevices(devices, 'android'); + const iosDevices = getPlatformDevices(devices, 'ios'); return { // include, // exclude, - android: android - ? { - devices: androidDevices, - packageName: android.packageName, - activity: android.activity, - s3Key: buildPresignedUploadUrls?.android?.s3Key || '', - } - : undefined, - ios: ios - ? { - devices: iosDevices, - bundleIdentifier: ios.bundleIdentifier, - s3Key: buildPresignedUploadUrls?.ios?.s3Key || '', - } - : undefined, + android: + androidDevices.length > 0 + ? { + devices: androidDevices, + // packageName: android.packageName, + // activity: android.activity, + s3Key: buildPresignedUploadUrls?.android?.s3Key || '', + } + : undefined, + ios: + iosDevices.length > 0 + ? { + devices: iosDevices, + // bundleIdentifier: ios.bundleIdentifier, + s3Key: buildPresignedUploadUrls?.ios?.s3Key || '', + } + : undefined, }; } @@ -39,8 +41,8 @@ export default getBuildRunConfig; /* ========================================================================== */ -function getConvertedDevices(devices: Config['devices'], platform: Platform): Device[] { - return devices +function getPlatformDevices(configDevices: Config['devices'], platform: Platform): Device[] { + return configDevices .filter(({ id }) => sherloDevices[id]?.os === platform) .map((device) => ({ id: device.id, diff --git a/packages/cli/src/commands/main/modes/utils/getConfigPlatforms.ts b/packages/cli/src/commands/main/modes/utils/getConfigPlatforms.ts deleted file mode 100644 index 6cf32afb..00000000 --- a/packages/cli/src/commands/main/modes/utils/getConfigPlatforms.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Platform } from '@sherlo/api-types'; -import { Config } from '../../types'; - -function getConfigPlatforms(config: Config): Platform[] { - const result: Platform[] = []; - - if (config.android) result.push('android'); - if (config.ios) result.push('ios'); - - return result; -} - -export default getConfigPlatforms; diff --git a/packages/cli/src/commands/main/modes/utils/getPlatformsToTest.ts b/packages/cli/src/commands/main/modes/utils/getPlatformsToTest.ts new file mode 100644 index 00000000..9582037f --- /dev/null +++ b/packages/cli/src/commands/main/modes/utils/getPlatformsToTest.ts @@ -0,0 +1,18 @@ +import { Platform } from '@sherlo/api-types'; +import { devices } from '@sherlo/shared'; +import { Config } from '../../types'; + +function getPlatformsToTest(config: Config): Platform[] { + const platforms = new Set<'android' | 'ios'>(); + + config.devices.forEach((deviceConfig) => { + const device = devices[deviceConfig.id]; + if (device) { + platforms.add(device.os); + } + }); + + return Array.from(platforms); +} + +export default getPlatformsToTest; diff --git a/packages/cli/src/commands/main/modes/utils/index.ts b/packages/cli/src/commands/main/modes/utils/index.ts index afaaee6c..656c589f 100644 --- a/packages/cli/src/commands/main/modes/utils/index.ts +++ b/packages/cli/src/commands/main/modes/utils/index.ts @@ -1,6 +1,5 @@ -export { default as createExpoSherloTempFile } from './createExpoSherloTempFile'; export { default as getAppBuildUrl } from './getAppBuildUrl'; export { default as getBuildRunConfig } from './getBuildRunConfig'; export { default as getBuildUploadUrls } from './getBuildUploadUrls'; -export { default as getConfigPlatforms } from './getConfigPlatforms'; +export { default as getPlatformsToTest } from './getPlatformsToTest'; export { default as uploadMobileBuilds } from './uploadMobileBuilds'; diff --git a/packages/cli/src/commands/main/types.ts b/packages/cli/src/commands/main/types.ts index c1697203..3d2a1857 100644 --- a/packages/cli/src/commands/main/types.ts +++ b/packages/cli/src/commands/main/types.ts @@ -2,19 +2,16 @@ import { DeviceID, DeviceTheme } from '@sherlo/api-types'; import { PartialDeep } from 'type-fest'; import { iOSFileTypes } from './constants'; -export type Mode = 'sync' | 'asyncInit' | 'asyncUpload'; +export type Mode = 'sync' | 'remoteExpo' | 'asyncInit' | 'asyncUpload'; -export type ConfigMode = 'withPaths' | 'withoutPaths'; +export type ConfigMode = 'withBuildPaths' | 'withoutBuildPaths'; -export type Config = { +export type Config = CM extends 'withBuildPaths' + ? BaseConfig & ConfigBuildPaths + : BaseConfig; + +type BaseConfig = { token: string; - android?: { - packageName: string; - activity?: string; - } & PathProperty; - ios?: { - bundleIdentifier: string; - } & PathProperty; devices: { id: DeviceID; osVersion: string; @@ -23,7 +20,10 @@ export type Config = { }[]; }; -type PathProperty = CM extends 'withPaths' ? { path: string } : {}; +type ConfigBuildPaths = { + android?: string; + ios?: string; +}; export type InvalidatedConfig = PartialDeep; diff --git a/packages/cli/src/commands/main/helpers/utils/getConfigErrorMessage.ts b/packages/cli/src/commands/main/utils/getConfigErrorMessage.ts similarity index 72% rename from packages/cli/src/commands/main/helpers/utils/getConfigErrorMessage.ts rename to packages/cli/src/commands/main/utils/getConfigErrorMessage.ts index bf574d88..e0a2c157 100644 --- a/packages/cli/src/commands/main/helpers/utils/getConfigErrorMessage.ts +++ b/packages/cli/src/commands/main/utils/getConfigErrorMessage.ts @@ -1,5 +1,5 @@ -import { getErrorMessage } from '../../utils'; -import { docsLink } from '../../constants'; +import { docsLink } from '../constants'; +import getErrorMessage from './getErrorMessage'; function getConfigErrorMessage(message: string, learnMoreLink?: string): string { return getErrorMessage({ diff --git a/packages/cli/src/commands/main/utils/index.ts b/packages/cli/src/commands/main/utils/index.ts index d6125e48..83054ef1 100644 --- a/packages/cli/src/commands/main/utils/index.ts +++ b/packages/cli/src/commands/main/utils/index.ts @@ -1,2 +1,3 @@ +export { default as getConfigErrorMessage } from './getConfigErrorMessage'; export { default as getErrorMessage } from './getErrorMessage'; export { default as getTokenParts } from './getTokenParts'; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index bd83ad9c..df531caf 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1 +1 @@ -export { default as main } from './commands/main'; +export { main } from './commands';