Skip to content

Commit ada9516

Browse files
authored
feat: add "clean" command (#1582)
* feat: add "clean" command * prompt when --include is omitted * uncheck npm and yarn by default * replace execute with execa * add some tests * don't show cocoapods on Windows * show hints for each group * don't stop when encountering an unknown group * dim the group description * add chalk to dependencies
1 parent e044e7b commit ada9516

File tree

13 files changed

+359
-17
lines changed

13 files changed

+359
-17
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"@react-native-community/eslint-config": "^2.0.0",
3232
"@types/glob": "^7.1.1",
3333
"@types/jest": "^26.0.15",
34-
"@types/node": "^10.0.0",
34+
"@types/node": "^12.0.0",
3535
"@types/node-fetch": "^2.3.7",
3636
"babel-jest": "^26.6.2",
3737
"babel-plugin-module-resolver": "^3.2.0",
@@ -55,6 +55,6 @@
5555
"typescript": "^3.8.0"
5656
},
5757
"resolutions": {
58-
"@types/node": "^10.0.0"
58+
"@types/node": "^12.0.0"
5959
}
6060
}

packages/cli-clean/package.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@react-native-community/cli-clean",
3+
"version": "8.0.0-alpha.0",
4+
"license": "MIT",
5+
"main": "build/index.js",
6+
"publishConfig": {
7+
"access": "public"
8+
},
9+
"types": "build/index.d.ts",
10+
"dependencies": {
11+
"@react-native-community/cli-tools": "^8.0.0-alpha.0",
12+
"chalk": "^4.1.2",
13+
"execa": "^1.0.0",
14+
"prompts": "^2.4.0"
15+
},
16+
"files": [
17+
"build",
18+
"!*.d.ts",
19+
"!*.map"
20+
],
21+
"devDependencies": {
22+
"@react-native-community/cli-types": "^8.0.0-alpha.0",
23+
"@types/execa": "^0.9.0",
24+
"@types/prompts": "^2.0.9"
25+
},
26+
"homepage": "https://github.com/react-native-community/cli/tree/master/packages/cli-clean",
27+
"repository": {
28+
"type": "git",
29+
"url": "https://github.com/react-native-community/cli.git",
30+
"directory": "packages/cli-clean"
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import execa from 'execa';
2+
import os from 'os';
3+
import prompts from 'prompts';
4+
import {clean} from '../clean';
5+
6+
jest.mock('execa', () => jest.fn());
7+
jest.mock('prompts', () => jest.fn());
8+
9+
describe('clean', () => {
10+
const mockConfig: any = {};
11+
12+
afterEach(() => {
13+
jest.resetAllMocks();
14+
});
15+
16+
it('throws if project root is not set', () => {
17+
expect(clean([], mockConfig, mockConfig)).rejects.toThrow();
18+
});
19+
20+
it('prompts if `--include` is omitted', async () => {
21+
prompts.mockReturnValue({cache: []});
22+
23+
await clean([], mockConfig, {include: '', projectRoot: process.cwd()});
24+
25+
expect(execa).not.toBeCalled();
26+
expect(prompts).toBeCalled();
27+
});
28+
29+
it('stops Watchman and clears out caches', async () => {
30+
await clean([], mockConfig, {
31+
include: 'watchman',
32+
projectRoot: process.cwd(),
33+
});
34+
35+
expect(prompts).not.toBeCalled();
36+
expect(execa).toBeCalledWith(
37+
os.platform() === 'win32' ? 'tskill' : 'killall',
38+
['watchman'],
39+
expect.anything(),
40+
);
41+
expect(execa).toBeCalledWith(
42+
'watchman',
43+
['watch-del-all'],
44+
expect.anything(),
45+
);
46+
});
47+
});

packages/cli-clean/src/clean.ts

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import {getLoader} from '@react-native-community/cli-tools';
2+
import type {Config as CLIConfig} from '@react-native-community/cli-types';
3+
import chalk from 'chalk';
4+
import execa from 'execa';
5+
import {existsSync as fileExists, rmdir} from 'fs';
6+
import os from 'os';
7+
import path from 'path';
8+
import prompts from 'prompts';
9+
import {promisify} from 'util';
10+
11+
type Args = {
12+
include?: string;
13+
projectRoot: string;
14+
verifyCache?: boolean;
15+
};
16+
17+
type Task = {
18+
label: string;
19+
action: () => Promise<void>;
20+
};
21+
22+
type CleanGroups = {
23+
[key: string]: {
24+
description: string;
25+
tasks: Task[];
26+
};
27+
};
28+
29+
const DEFAULT_GROUPS = ['metro', 'watchman'];
30+
31+
const rmdirAsync = promisify(rmdir);
32+
33+
function cleanDir(directory: string): Promise<void> {
34+
if (!fileExists(directory)) {
35+
return Promise.resolve();
36+
}
37+
38+
return rmdirAsync(directory, {maxRetries: 3, recursive: true});
39+
}
40+
41+
function findPath(startPath: string, files: string[]): string | undefined {
42+
// TODO: Find project files via `@react-native-community/cli`
43+
for (const file of files) {
44+
const filename = path.resolve(startPath, file);
45+
if (fileExists(filename)) {
46+
return filename;
47+
}
48+
}
49+
50+
return undefined;
51+
}
52+
53+
async function promptForCaches(
54+
groups: CleanGroups,
55+
): Promise<string[] | undefined> {
56+
const {caches} = await prompts({
57+
type: 'multiselect',
58+
name: 'caches',
59+
message: 'Select all caches to clean',
60+
choices: Object.entries(groups).map(([cmd, group]) => ({
61+
title: `${cmd} ${chalk.dim(`(${group.description})`)}`,
62+
value: cmd,
63+
selected: DEFAULT_GROUPS.includes(cmd),
64+
})),
65+
min: 1,
66+
});
67+
return caches;
68+
}
69+
70+
export async function clean(
71+
_argv: string[],
72+
_config: CLIConfig,
73+
cleanOptions: Args,
74+
): Promise<void> {
75+
const {include, projectRoot, verifyCache} = cleanOptions;
76+
if (!fileExists(projectRoot)) {
77+
throw new Error(`Invalid path provided! ${projectRoot}`);
78+
}
79+
80+
const COMMANDS: CleanGroups = {
81+
android: {
82+
description: 'Android build caches, e.g. Gradle',
83+
tasks: [
84+
{
85+
label: 'Clean Gradle cache',
86+
action: async () => {
87+
const candidates =
88+
os.platform() === 'win32'
89+
? ['android/gradlew.bat', 'gradlew.bat']
90+
: ['android/gradlew', 'gradlew'];
91+
const gradlew = findPath(projectRoot, candidates);
92+
if (gradlew) {
93+
const script = path.basename(gradlew);
94+
await execa(
95+
os.platform() === 'win32' ? script : `./${script}`,
96+
['clean'],
97+
{cwd: path.dirname(gradlew)},
98+
);
99+
}
100+
},
101+
},
102+
],
103+
},
104+
...(os.platform() === 'darwin'
105+
? {
106+
cocoapods: {
107+
description: 'CocoaPods cache',
108+
tasks: [
109+
{
110+
label: 'Clean CocoaPods cache',
111+
action: async () => {
112+
await execa('pod', ['cache', 'clean', '--all'], {
113+
cwd: projectRoot,
114+
});
115+
},
116+
},
117+
],
118+
},
119+
}
120+
: undefined),
121+
metro: {
122+
description: 'Metro, haste-map caches',
123+
tasks: [
124+
{
125+
label: 'Clean Metro cache',
126+
action: () => cleanDir(`${os.tmpdir()}/metro-*`),
127+
},
128+
{
129+
label: 'Clean Haste cache',
130+
action: () => cleanDir(`${os.tmpdir()}/haste-map-*`),
131+
},
132+
{
133+
label: 'Clean React Native cache',
134+
action: () => cleanDir(`${os.tmpdir()}/react-*`),
135+
},
136+
],
137+
},
138+
npm: {
139+
description:
140+
'`node_modules` folder in the current package, and optionally verify npm cache',
141+
tasks: [
142+
{
143+
label: 'Remove node_modules',
144+
action: () => cleanDir(`${projectRoot}/node_modules`),
145+
},
146+
...(verifyCache
147+
? [
148+
{
149+
label: 'Verify npm cache',
150+
action: async () => {
151+
await execa('npm', ['cache', 'verify'], {cwd: projectRoot});
152+
},
153+
},
154+
]
155+
: []),
156+
],
157+
},
158+
watchman: {
159+
description: 'Stop Watchman and delete its cache',
160+
tasks: [
161+
{
162+
label: 'Stop Watchman',
163+
action: async () => {
164+
await execa(
165+
os.platform() === 'win32' ? 'tskill' : 'killall',
166+
['watchman'],
167+
{cwd: projectRoot},
168+
);
169+
},
170+
},
171+
{
172+
label: 'Delete Watchman cache',
173+
action: async () => {
174+
await execa('watchman', ['watch-del-all'], {cwd: projectRoot});
175+
},
176+
},
177+
],
178+
},
179+
yarn: {
180+
description: 'Yarn cache',
181+
tasks: [
182+
{
183+
label: 'Clean Yarn cache',
184+
action: async () => {
185+
await execa('yarn', ['cache', 'clean'], {cwd: projectRoot});
186+
},
187+
},
188+
],
189+
},
190+
};
191+
192+
const groups = include ? include.split(',') : await promptForCaches(COMMANDS);
193+
if (!groups || groups.length === 0) {
194+
return;
195+
}
196+
197+
const spinner = getLoader();
198+
for (const group of groups) {
199+
const commands = COMMANDS[group];
200+
if (!commands) {
201+
spinner.warn(`Unknown group: ${group}`);
202+
continue;
203+
}
204+
205+
for (const {action, label} of commands.tasks) {
206+
spinner.start(label);
207+
await action()
208+
.then(() => {
209+
spinner.succeed();
210+
})
211+
.catch((e) => {
212+
spinner.fail(`${label} » ${e}`);
213+
});
214+
}
215+
}
216+
}
217+
218+
export default {
219+
func: clean,
220+
name: 'clean',
221+
description:
222+
'Cleans your project by removing React Native related caches and modules.',
223+
options: [
224+
{
225+
name: '--include <string>',
226+
description:
227+
'Comma-separated flag of caches to clear e.g. `npm,yarn`. If omitted, an interactive prompt will appear.',
228+
},
229+
{
230+
name: '--project-root <string>',
231+
description:
232+
'Root path to your React Native project. When not specified, defaults to current working directory.',
233+
default: process.cwd(),
234+
},
235+
{
236+
name: '--verify-cache',
237+
description:
238+
'Whether to verify the cache. Currently only applies to npm cache.',
239+
default: false,
240+
},
241+
],
242+
};

packages/cli-clean/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {default as clean} from './clean';
2+
3+
export const commands = {clean};

packages/cli-clean/tsconfig.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "src",
5+
"outDir": "build"
6+
},
7+
"references": [
8+
{"path": "../tools"},
9+
{"path": "../cli-types"},
10+
{"path": "../cli-config"}
11+
]
12+
}

packages/cli-doctor/src/commands/doctor.ts

-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ const doctorCommand = (async (_, options) => {
228228
removeKeyPressListener();
229229

230230
process.exit(0);
231-
return;
232231
}
233232

234233
if (

packages/cli-doctor/src/tools/windows/androidWinHelpers.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ export const installComponent = (component: string, androidSdkRoot: string) => {
4747
const child = executeCommand(command);
4848
let stderr = '';
4949

50-
child.stdout.on('data', (data) => {
50+
child.stdout?.on('data', (data) => {
5151
if (data.includes('(y/N)')) {
52-
child.stdin.write('y\n');
52+
child.stdin?.write('y\n');
5353
}
5454
});
5555

56-
child.stderr.on('data', (data) => {
56+
child.stderr?.on('data', (data) => {
5757
stderr += data.toString('utf-8');
5858
});
5959

packages/cli/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"testEnvironment": "node"
2525
},
2626
"dependencies": {
27+
"@react-native-community/cli-clean": "^8.0.0-alpha.0",
2728
"@react-native-community/cli-config": "^8.0.0-alpha.0",
2829
"@react-native-community/cli-debugger-ui": "^8.0.0-alpha.0",
2930
"@react-native-community/cli-doctor": "^8.0.0-alpha.0",

0 commit comments

Comments
 (0)