Skip to content

Commit 0151662

Browse files
authored
refactor: handling gradle tasks, mode, args and show build output file (#36)
* refactor: handling gradle tasks, mode, args and show build output file * update output * fix tests
1 parent 6828f1d commit 0151662

File tree

11 files changed

+191
-129
lines changed

11 files changed

+191
-129
lines changed

packages/cli/src/lib/commands/fingerprint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ export async function nativeFingerprintCommand(
3434
logger.debug(`Duration: ${(duration / 1000).toFixed(1)}s`);
3535
}
3636
loader.stop(`Fingerprint calculated: ${fingerprint.hash}`);
37-
outro('Success.');
37+
outro('Success 🎉.');
3838
}

packages/plugin-platform-android/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@react-native-community/cli-config-android": "^15.1.2",
1313
"chalk": "^5.3.0",
1414
"nano-spawn": "^0.2.0",
15+
"picocolors": "^1.1.1",
1516
"tslib": "^2.3.0"
1617
},
1718
"devDependencies": {

packages/plugin-platform-android/src/lib/commands/buildAndroid/__tests__/buildAndroid.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import fs, { PathLike } from 'node:fs';
12
import { vi, test, Mock, MockedFunction } from 'vitest';
23
import { AndroidProjectConfig } from '@react-native-community/cli-types';
34
import { logger } from '@callstack/rnef-tools';
45
import spawn from 'nano-spawn';
56
import { select } from '@clack/prompts';
67
import { buildAndroid, type BuildFlags } from '../buildAndroid.js';
8+
import color from 'picocolors';
79

10+
const actualFs = await vi.importMock('node:fs');
11+
12+
vi.mock('node:fs');
813
vi.mock('nano-spawn', () => {
914
return {
1015
default: vi.fn(),
@@ -71,15 +76,45 @@ beforeEach(() => {
7176
vi.clearAllMocks();
7277
});
7378

74-
test('buildAndroid runs gradle build with correct configuration for debug', async () => {
75-
(spawn as Mock).mockResolvedValueOnce({ output: 'output' });
79+
function mockCallAdbGetCpu(file: string, args: string[]) {
80+
return (
81+
file === '/mock/android/home/platform-tools/adb' &&
82+
args?.[0] === 'shell' &&
83+
args?.[1] === 'getprop' &&
84+
args?.[2] === 'ro.product.cpu.abi'
85+
);
86+
}
87+
88+
function spawnMockImplementation(file: string, args: string[]) {
89+
if (mockCallAdbGetCpu(file, args)) {
90+
return { output: 'arm64-v8a' };
91+
}
92+
return { output: '...' };
93+
}
94+
95+
test('buildAndroid runs gradle build with correct configuration for debug and outputs build path', async () => {
96+
(spawn as Mock).mockImplementation((file, args) =>
97+
spawnMockImplementation(file, args)
98+
);
99+
vi.mocked(fs.existsSync).mockImplementation((file: PathLike) => {
100+
if (file === '/android/app/build/outputs/bundle/debug/app-debug.aab') {
101+
return true;
102+
}
103+
return (actualFs as typeof fs).existsSync(file);
104+
});
105+
76106
await buildAndroid(androidProject, args);
77107

78108
expect(vi.mocked(spawn)).toBeCalledWith(
79109
'./gradlew',
80110
['app:bundleDebug', '-x', 'lint'],
81111
{ stdio: 'inherit', cwd: '/android' }
82112
);
113+
expect(mocks.stopMock).toBeCalledWith(
114+
`Build output: ${color.cyan(
115+
'/android/app/build/outputs/bundle/debug/app-debug.aab'
116+
)}`
117+
);
83118
});
84119

85120
test('buildAndroid fails gracefully when gradle errors', async () => {
@@ -106,11 +141,20 @@ test('buildAndroid runs selected "bundleRelease" task in interactive mode', asyn
106141
if (file === './gradlew' && args[0] === 'tasks') {
107142
return { output: gradleTaskOutput };
108143
}
144+
if (mockCallAdbGetCpu(file, args)) {
145+
return { output: 'arm64-v8a' };
146+
}
109147
return { output: 'output' };
110148
});
111149
(select as MockedFunction<typeof select>).mockResolvedValueOnce(
112150
Promise.resolve('bundleRelease')
113151
);
152+
vi.mocked(fs.existsSync).mockImplementation((file: PathLike) => {
153+
if (file === '/android/app/build/outputs/bundle/release/app-release.aab') {
154+
return true;
155+
}
156+
return (actualFs as typeof fs).existsSync(file);
157+
});
114158

115159
await buildAndroid(androidProject, { ...args, interactive: true });
116160

@@ -130,5 +174,5 @@ test('buildAndroid runs selected "bundleRelease" task in interactive mode', asyn
130174
'Searching for available Gradle tasks...'
131175
);
132176
expect(mocks.stopMock).toBeCalledWith('Found 2 Gradle tasks.');
133-
expect(mocks.outroMock).toBeCalledWith('Success.');
177+
expect(mocks.outroMock).toBeCalledWith('Success 🎉.');
134178
});

packages/plugin-platform-android/src/lib/commands/buildAndroid/buildAndroid.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { AndroidProjectConfig } from '@react-native-community/cli-types';
22
import { runGradle } from '../runGradle.js';
33
import { promptForTaskSelection } from '../listAndroidTasks.js';
4-
import { intro, outro } from '@clack/prompts';
4+
import { findOutputFile } from '../runAndroid/tryInstallAppOnDevice.js';
5+
import { outro, spinner } from '@clack/prompts';
56
import { logger } from '@callstack/rnef-tools';
7+
import color from 'picocolors';
8+
import { toPascalCase } from '../toPascalCase.js';
69

710
export interface BuildFlags {
8-
mode?: string;
11+
mode: string;
912
activeArchOnly?: boolean;
1013
tasks?: Array<string>;
1114
extraParams?: Array<string>;
@@ -17,14 +20,21 @@ export async function buildAndroid(
1720
args: BuildFlags
1821
) {
1922
normalizeArgs(args);
20-
intro('Building Android app.');
2123

22-
const selectedTask = args.interactive
23-
? await promptForTaskSelection('bundle', androidProject.sourceDir)
24-
: undefined;
24+
const tasks = args.interactive
25+
? [await promptForTaskSelection('bundle', androidProject.sourceDir)]
26+
: [...(args.tasks ?? []), `bundle${toPascalCase(args.mode)}`];
2527

26-
await runGradle({ taskType: 'bundle', androidProject, args, selectedTask });
27-
outro('Success.');
28+
await runGradle({ tasks, androidProject, args });
29+
30+
const outputFilePath = await findOutputFile(androidProject, tasks);
31+
32+
if (outputFilePath) {
33+
const loader = spinner();
34+
loader.start('');
35+
loader.stop(`Build output: ${color.cyan(outputFilePath)}`);
36+
}
37+
outro('Success 🎉.');
2838
}
2939

3040
function normalizeArgs(args: BuildFlags) {
@@ -33,6 +43,9 @@ function normalizeArgs(args: BuildFlags) {
3343
'Both "--tasks" and "--mode" parameters were passed. Using "--tasks" for building the app.'
3444
);
3545
}
46+
if (!args.mode) {
47+
args.mode = 'debug';
48+
}
3649
}
3750

3851
export const options = [

packages/plugin-platform-android/src/lib/commands/runAndroid/__tests__/runAndroid.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ function spawnMockImplementation(
292292
return { output: '...' };
293293
}
294294

295-
test.each([['release'], [undefined], ['staging']])(
295+
test.each([['release'], ['debug'], ['staging']])(
296296
'runAndroid runs gradle build with correct configuration for --mode %s and launches on emulator-5552',
297297
async (mode) => {
298298
(spawn as Mock).mockImplementation((file, args) =>
@@ -301,7 +301,7 @@ test.each([['release'], [undefined], ['staging']])(
301301
const logErrorSpy = vi.spyOn(logger, 'error');
302302
await runAndroid({ ...androidProject }, { ...args, mode }, '/');
303303

304-
expect(mocks.outroMock).toBeCalledWith('Success.');
304+
expect(mocks.outroMock).toBeCalledWith('Success 🎉.');
305305
expect(logErrorSpy).not.toBeCalled();
306306

307307
// Runs installDebug with only active architecture arm64-v8a
@@ -349,7 +349,7 @@ test('runAndroid runs gradle build with custom --appId, --appIdSuffix and --main
349349
'/'
350350
);
351351

352-
expect(mocks.outroMock).toBeCalledWith('Success.');
352+
expect(mocks.outroMock).toBeCalledWith('Success 🎉.');
353353
expect(logErrorSpy).not.toBeCalled();
354354

355355
// launches com.custom.suffix app with OtherActivity on emulator-5552
@@ -375,7 +375,7 @@ test('runAndroid fails to launch an app on not-connected device when specified w
375375
'/'
376376
);
377377
} catch {
378-
expect(mocks.outroMock).not.toBeCalledWith('Success.');
378+
expect(mocks.outroMock).not.toBeCalledWith('Success 🎉.');
379379
expect(logErrorSpy).toBeCalledWith(
380380
'Device "emulator-5554" not found. Please run it first or use a different one.'
381381
);
@@ -385,8 +385,8 @@ test('runAndroid fails to launch an app on not-connected device when specified w
385385
test.each([
386386
['release', true],
387387
['release', false],
388-
[undefined, true],
389-
[undefined, false],
388+
['debug', true],
389+
['debug', false],
390390
])(
391391
`runAndroid launches an app on a selected device emulator-5554 when connected in --mode %s and --interactive %b`,
392392
async (mode, interactive) => {

packages/plugin-platform-android/src/lib/commands/runAndroid/__tests__/runOnAllDevices.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,17 @@ describe('--appFolder', () => {
7070

7171
it('uses task "install[Variant]" as default task', async () => {
7272
await runGradle({
73-
taskType: 'install',
74-
args: { ...args, mode: 'debug' },
73+
tasks: ['installDebug'],
74+
args: { ...args },
7575
androidProject,
7676
});
7777
expect((spawn as Mock).mock.calls[0][1]).toContain('app:installDebug');
7878
});
7979

8080
it('uses appName and default variant', async () => {
8181
await runGradle({
82-
taskType: 'install',
83-
args: { ...args, mode: 'debug' },
82+
tasks: ['installDebug'],
83+
args: { ...args },
8484
androidProject: { ...androidProject, appName: 'someApp' },
8585
});
8686

@@ -89,8 +89,8 @@ describe('--appFolder', () => {
8989

9090
it('uses appName and custom variant', async () => {
9191
await runGradle({
92-
taskType: 'install',
93-
args: { ...args, mode: 'release' },
92+
tasks: ['installRelease'],
93+
args: { ...args },
9494
androidProject: { ...androidProject, appName: 'anotherApp' },
9595
});
9696

@@ -101,8 +101,8 @@ describe('--appFolder', () => {
101101

102102
it('uses only task argument', async () => {
103103
await runGradle({
104-
taskType: 'install',
105-
args: { ...args, tasks: ['someTask'] },
104+
tasks: ['installDebug', 'someTask'],
105+
args: { ...args },
106106
androidProject,
107107
});
108108

@@ -111,8 +111,8 @@ describe('--appFolder', () => {
111111

112112
it('uses appName and custom task argument', async () => {
113113
await runGradle({
114-
taskType: 'install',
115-
args: { ...args, tasks: ['someTask'] },
114+
tasks: ['someTask', 'installDebug'],
115+
args: { ...args },
116116
androidProject: { ...androidProject, appName: 'anotherApp' },
117117
});
118118

@@ -121,8 +121,8 @@ describe('--appFolder', () => {
121121

122122
it('uses multiple tasks', async () => {
123123
await runGradle({
124-
taskType: 'install',
125-
args: { ...args, tasks: ['clean', 'someTask'] },
124+
tasks: ['clean', 'someTask'],
125+
args: { ...args },
126126
androidProject,
127127
});
128128

packages/plugin-platform-android/src/lib/commands/runAndroid/runAndroid.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import path from 'path';
1414
import { BuildFlags, options } from '../buildAndroid/buildAndroid.js';
1515
import { promptForTaskSelection } from '../listAndroidTasks.js';
1616
import { runGradle } from '../runGradle.js';
17-
import { intro, outro, select } from '@clack/prompts';
17+
import { outro, select } from '@clack/prompts';
1818
import chalk from 'chalk';
1919

2020
export interface Flags extends BuildFlags {
@@ -37,52 +37,39 @@ export async function runAndroid(
3737
args: Flags,
3838
projectRoot: string
3939
) {
40-
intro('Building and running Android app.');
4140
normalizeArgs(args, projectRoot);
4241

4342
const { deviceId } = args.interactive
4443
? await selectAndLaunchDevice()
4544
: { deviceId: args.device };
4645

47-
const selectedTask = args.interactive
48-
? await promptForTaskSelection(
49-
deviceId ? 'assemble' : 'install',
50-
androidProject.sourceDir
51-
)
52-
: undefined;
46+
const mainTaskType = deviceId ? 'assemble' : 'install';
47+
const tasks = args.interactive
48+
? [await promptForTaskSelection(mainTaskType, androidProject.sourceDir)]
49+
: [...(args.tasks ?? []), `${mainTaskType}${toPascalCase(args.mode)}`];
5350

5451
if (deviceId) {
55-
await runGradle({
56-
taskType: 'assemble',
57-
androidProject,
58-
args,
59-
selectedTask,
60-
});
52+
await runGradle({ tasks, androidProject, args });
6153
if (!(await getDevices()).find((d) => d === deviceId)) {
6254
logger.error(
6355
`Device "${deviceId}" not found. Please run it first or use a different one.`
6456
);
6557
process.exit(1);
6658
}
67-
await tryInstallAppOnDevice(deviceId, androidProject, args, selectedTask);
59+
await tryInstallAppOnDevice(deviceId, androidProject, args, tasks);
6860
await tryLaunchAppOnDevice(deviceId, androidProject, args);
6961
} else {
7062
if ((await getDevices()).length === 0) {
7163
await tryLaunchEmulator();
7264
}
7365

74-
await runGradle({
75-
taskType: 'install',
76-
androidProject,
77-
args,
78-
selectedTask,
79-
});
66+
await runGradle({ tasks, androidProject, args });
8067

8168
for (const device of await getDevices()) {
8269
await tryLaunchAppOnDevice(device, androidProject, args);
8370
}
8471
}
85-
outro('Success.');
72+
outro('Success 🎉.');
8673
}
8774

8875
async function selectAndLaunchDevice() {
@@ -113,6 +100,10 @@ function normalizeArgs(args: Flags, projectRoot: string) {
113100
);
114101
}
115102

103+
if (!args.mode) {
104+
args.mode = 'debug';
105+
}
106+
116107
// turn on activeArchOnly for debug to speed up local builds
117108
if (args.mode !== 'release' && args.activeArchOnly === undefined) {
118109
args.activeArchOnly = true;

0 commit comments

Comments
 (0)