Skip to content

Commit

Permalink
feat: interactive mode (#1829)
Browse files Browse the repository at this point in the history
* feat: interactive mode

* fix: code review fixes

* fix tests

* code review adjustments
  • Loading branch information
adamTrz committed Feb 20, 2023
1 parent c548166 commit 9d17728
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 36 deletions.
Expand Up @@ -84,10 +84,11 @@ async function buildAndroid(
args.mode || args.variant,
tasks,
'assemble',
androidProject.sourceDir,
);

if (args.extraParams) {
gradleArgs = [...gradleArgs, ...args.extraParams];
gradleArgs.push(...args.extraParams);
}

if (args.activeArchOnly) {
Expand Down Expand Up @@ -183,5 +184,5 @@ export default {
name: 'build-android',
description: 'builds your app',
func: buildAndroid,
options: options,
options,
};
Expand Up @@ -11,6 +11,41 @@ import execa from 'execa';
import {Flags} from '..';
import {AndroidProjectConfig} from '@react-native-community/cli-types';

const gradleTaskOutput = `
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project 'Bar'
------------------------------------------------------------
Android tasks
-------------
androidDependencies - Displays the Android dependencies of the project.
signingReport - Displays the signing info for the base and test modules
sourceSets - Prints out all the source sets defined in this project.
Build tasks
-----------
assemble - Assemble main outputs for all the variants.
assembleAndroidTest - Assembles all the Test applications.
assembleDebug - Assembles main outputs for all Debug variants.
assembleRelease - Assembles main outputs for all Release variants.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
bundle - Assemble bundles for all the variants.
bundleDebug - Assembles bundles for all Debug variants.
bundleRelease - Assembles bundles for all Release variants.
Install tasks
-------------
installDebug - Installs the Debug build.
installDebugAndroidTest - Installs the android (on device) tests for the Debug build.
installRelease - Installs the Release build.
uninstallAll - Uninstall all applications.
`;

jest.mock('execa');
jest.mock('../getAdbPath');
jest.mock('../tryLaunchEmulator');
Expand All @@ -35,6 +70,7 @@ describe('--appFolder', () => {
};
beforeEach(() => {
jest.clearAllMocks();
(execa.sync as jest.Mock).mockReturnValueOnce({stdout: gradleTaskOutput});
});

it('uses task "install[Variant]" as default task', async () => {
Expand Down
@@ -1,13 +1,45 @@
import {toPascalCase} from './toPascalCase';
import type {BuildFlags} from '../buildAndroid';
import {getGradleTasks} from './listAndroidTasks';
import {CLIError, logger} from '@react-native-community/cli-tools';

export function getTaskNames(
appName: string,
mode: BuildFlags['mode'] = 'debug',
tasks: BuildFlags['tasks'],
taskPrefix: 'assemble' | 'install',
sourceDir: string,
): Array<string> {
const appTasks = tasks || [taskPrefix + toPascalCase(mode)];
let appTasks = tasks || [taskPrefix + toPascalCase(mode)];

// Check against build flavors for "install" task ("assemble" don't care about it so much and will build all)
if (!tasks && taskPrefix === 'install') {
const actionableInstallTasks = getGradleTasks('install', sourceDir);
if (!actionableInstallTasks.find((t) => t.task.includes(appTasks[0]))) {
const installTasksForMode = actionableInstallTasks.filter((t) =>
t.task.toLowerCase().includes(mode),
);
if (!installTasksForMode.length) {
throw new CLIError(
`Couldn't find "${appTasks
.map((taskName) => taskName.replace(taskPrefix, ''))
.join(
', ',
)}" build variant. Available variants are: ${actionableInstallTasks
.map((t) => `"${t.task.replace(taskPrefix, '')}"`)
.join(', ')}.`,
);
}
logger.warn(
`Found multiple tasks for "install" command: ${installTasksForMode
.map((t) => t.task)
.join(', ')}.\nSelecting first available: ${
installTasksForMode[0].task
}.`,
);
appTasks = [installTasksForMode[0].task];
}
}

return appName
? appTasks.map((command) => `${appName}:${command}`)
Expand Down
60 changes: 47 additions & 13 deletions packages/cli-platform-android/src/commands/runAndroid/index.ts
Expand Up @@ -21,6 +21,7 @@ import chalk from 'chalk';
import path from 'path';
import {build, runPackager, BuildFlags, options} from '../buildAndroid';
import {promptForTaskSelection} from './listAndroidTasks';
import {getTaskNames} from './getTaskNames';

export interface Flags extends BuildFlags {
appId: string;
Expand Down Expand Up @@ -86,19 +87,19 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) {

const adbPath = getAdbPath();

let {tasks} = args;
let selectedTask;

if (args.interactive) {
const selectedTask = await promptForTaskSelection(
const task = await promptForTaskSelection(
'install',
androidProject.sourceDir,
);
if (selectedTask) {
tasks = [selectedTask];
if (task) {
selectedTask = task;
}
}

if (args.listDevices) {
if (args.listDevices || args.interactive) {
if (args.deviceId) {
logger.warn(
'Both "deviceId" and "list-devices" parameters were passed to "run" command. We will list available devices and let you choose from one',
Expand All @@ -108,7 +109,9 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) {
const device = await listAndroidDevices();
if (!device) {
throw new CLIError(
'Failed to select device, please try to run app without "list-devices" command.',
`Failed to select device, please try to run app without ${
args.listDevices ? 'list-devices' : 'interactive'
} command.`,
);
}

Expand All @@ -117,6 +120,7 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) {
{...args, deviceId: device.deviceId},
adbPath,
androidProject,
selectedTask,
);
}

Expand All @@ -130,33 +134,49 @@ async function buildAndRun(args: Flags, androidProject: AndroidProject) {
{...args, deviceId: emulator},
adbPath,
androidProject,
selectedTask,
);
}
throw new CLIError(
`Failed to launch emulator. Reason: ${chalk.dim(result.error || '')}`,
);
}

if (args.deviceId) {
return runOnSpecificDevice({...args, tasks}, adbPath, androidProject);
return runOnSpecificDevice(args, adbPath, androidProject, selectedTask);
} else {
return runOnAllDevices({...args, tasks}, cmd, adbPath, androidProject);
return runOnAllDevices(args, cmd, adbPath, androidProject);
}
}

function runOnSpecificDevice(
args: Flags,
adbPath: string,
androidProject: AndroidProject,
selectedTask?: string,
) {
const devices = adb.getDevices(adbPath);
const {deviceId} = args;

// if coming from run-android command and we have selected task
// from interactive mode we need to create appropriate build task
// eg 'installRelease' -> 'assembleRelease'
const buildTask = selectedTask?.replace('install', 'assemble') ?? 'build';

if (devices.length > 0 && deviceId) {
if (devices.indexOf(deviceId) !== -1) {
// using '-x lint' in order to ignore linting errors while building the apk
let gradleArgs = ['build', '-x', 'lint'];
let gradleArgs = getTaskNames(
androidProject.appName,
args.mode || args.variant,
args.tasks ?? [buildTask],
'install',
androidProject.sourceDir,
);

// using '-x lint' in order to ignore linting errors while building the apk
gradleArgs.push('-x', 'lint');
if (args.extraParams) {
gradleArgs = [...gradleArgs, ...args.extraParams];
gradleArgs.push(...args.extraParams);
}

if (args.port) {
Expand All @@ -179,7 +199,13 @@ function runOnSpecificDevice(
build(gradleArgs, androidProject.sourceDir);
}

installAndLaunchOnDevice(args, deviceId, adbPath, androidProject);
installAndLaunchOnDevice(
args,
deviceId,
adbPath,
androidProject,
selectedTask,
);
} else {
logger.error(
`Could not find device with the id: "${deviceId}". Please choose one of the following:`,
Expand All @@ -196,9 +222,17 @@ function installAndLaunchOnDevice(
selectedDevice: string,
adbPath: string,
androidProject: AndroidProject,
selectedTask?: string,
) {
tryRunAdbReverse(args.port, selectedDevice);
tryInstallAppOnDevice(args, adbPath, selectedDevice, androidProject);

tryInstallAppOnDevice(
args,
adbPath,
selectedDevice,
androidProject,
selectedTask,
);
tryLaunchAppOnDevice(
selectedDevice,
androidProject.packageName,
Expand Down
Expand Up @@ -28,7 +28,7 @@ export const parseTasksFromGradleFile = (
return instalTasks;
};

export const promptForTaskSelection = async (
export const getGradleTasks = (
taskType: 'install' | 'build',
sourceDir: string,
) => {
Expand All @@ -37,15 +37,22 @@ export const promptForTaskSelection = async (
const out = execa.sync(cmd, ['tasks'], {
cwd: sourceDir,
}).stdout;
const installTasks = parseTasksFromGradleFile(taskType, out);
if (!installTasks.length) {
return parseTasksFromGradleFile(taskType, out);
};

export const promptForTaskSelection = async (
taskType: 'install' | 'build',
sourceDir: string,
): Promise<string | undefined> => {
const tasks = getGradleTasks(taskType, sourceDir);
if (!tasks.length) {
throw new CLIError(`No actionable ${taskType} tasks were found...`);
}
const {task} = await prompts({
const {task}: {task: string} = await prompts({
type: 'select',
name: 'task',
message: `Select ${taskType} task you want to perform`,
choices: installTasks.map((t: GradleTask) => ({
choices: tasks.map((t: GradleTask) => ({
title: `${chalk.bold(t.task)} - ${t.description}`,
value: t.task,
})),
Expand Down
Expand Up @@ -59,10 +59,11 @@ async function runOnAllDevices(
args.mode || args.variant,
args.tasks,
'install',
androidProject.sourceDir,
);

if (args.extraParams) {
gradleArgs = [...gradleArgs, ...args.extraParams];
gradleArgs.push(...args.extraParams);
}

if (args.port != null) {
Expand Down Expand Up @@ -120,8 +121,7 @@ async function runOnAllDevices(

function createInstallError(error: Error & {stderr: string}) {
const stderr = (error.stderr || '').toString();
let message = '';

let message = error.message ?? '';
// Pass the error message from the command to stdout because we pipe it to
// parent process so it's not visible
logger.log(stderr);
Expand Down
Expand Up @@ -10,19 +10,33 @@ function tryInstallAppOnDevice(
adbPath: string,
device: string,
androidProject: AndroidProject,
selectedTask?: string,
) {
try {
// "app" is usually the default value for Android apps with only 1 app
const {appName, sourceDir} = androidProject;
const variant = (args.mode || 'debug').toLowerCase();

const defaultVariant = (args.mode || 'debug').toLowerCase();

// handle if selected task from interactive mode includes build flavour as well, eg. installProductionDebug should create ['production','debug'] array
const variantFromSelectedTask = selectedTask
?.replace('install', '')
.split(/(?=[A-Z])/);

// create path to output file, eg. `production/debug`
const variantPath =
variantFromSelectedTask?.join('/')?.toLowerCase() ?? defaultVariant;
// create output file name, eg. `production-debug`
const variantAppName =
variantFromSelectedTask?.join('-')?.toLowerCase() ?? defaultVariant;

let pathToApk;
if (!args.binaryPath) {
const buildDirectory = `${sourceDir}/${appName}/build/outputs/apk/${variant}`;
const buildDirectory = `${sourceDir}/${appName}/build/outputs/apk/${variantPath}`;
const apkFile = getInstallApkName(
appName,
adbPath,
variant,
variantAppName,
device,
buildDirectory,
);
Expand Down
14 changes: 8 additions & 6 deletions packages/cli-platform-ios/src/commands/buildIOS/index.ts
Expand Up @@ -255,11 +255,6 @@ export const iosBuildOptions = [
description:
'Location for iOS build artifacts. Corresponds to Xcode\'s "-derivedDataPath".',
},
{
name: '--interactive',
description:
'Explicitly select which scheme and configuration to use before running a build',
},
{
name: '--extra-params <string>',
description: 'Custom params that will be passed to xcodebuild command.',
Expand All @@ -285,5 +280,12 @@ export default {
cmd: 'react-native build-ios --simulator "IPhone 11"',
},
],
options: iosBuildOptions,
options: [
...iosBuildOptions,
{
name: '--interactive',
description:
'Explicitly select which scheme and configuration to use before running a build',
},
],
};

0 comments on commit 9d17728

Please sign in to comment.