Skip to content

Commit

Permalink
feat(cli): Config update - simplified android & ios, added --remoteEx…
Browse files Browse the repository at this point in the history
…po (#17)
  • Loading branch information
dawidk92 authored Jun 13, 2024
1 parent 48e5825 commit 2c1f253
Show file tree
Hide file tree
Showing 20 changed files with 326 additions and 290 deletions.
6 changes: 4 additions & 2 deletions packages/cli/src/commands/main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
150 changes: 63 additions & 87 deletions packages/cli/src/commands/main/helpers/getArguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,27 @@ import {
} from './utils';

type Parameters<T extends 'default' | 'withDefaults' = 'default'> = {
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<ParameterDefaults>);
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;
};
Expand All @@ -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(
Expand Down Expand Up @@ -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>', 'Path to Sherlo config', DEFAULT_CONFIG_PATH)
const command = new Command();
command
.option('--token <token>', 'Project token')
.option('--android <path>', 'Path to Android build in .apk format')
.option('--ios <path>', 'Path to iOS build in .app (or compressed .tar.gz / .tar) format')
.option('--config <path>', 'Config file path', DEFAULT_CONFIG_PATH)
.option('--projectRoot <path>', '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 <number>', 'Index of build you want to update in async mode', parseInt)
.option(
'--projectRoot <path>',
'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 <token>', 'Sherlo project token')
.option('--android <path>', 'Path to Android build in .apk format')
.option('--ios <path>', 'Path to iOS simulator build in .app or .tar/.tar.gz file format');
.option(
'--asyncBuildIndex <number>',
'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,
Expand All @@ -217,17 +195,15 @@ 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)',
})
);
}

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',
})
);
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/commands/main/helpers/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
24 changes: 18 additions & 6 deletions packages/cli/src/commands/main/helpers/utils/parseConfigFile.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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]
)
);
Expand Down
Loading

0 comments on commit 2c1f253

Please sign in to comment.