Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add "clean" command #1582

Merged
merged 10 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@react-native-community/eslint-config": "^2.0.0",
"@types/glob": "^7.1.1",
"@types/jest": "^26.0.15",
"@types/node": "^10.0.0",
"@types/node": "^12.0.0",
"@types/node-fetch": "^2.3.7",
"babel-jest": "^26.6.2",
"babel-plugin-module-resolver": "^3.2.0",
Expand All @@ -55,6 +55,6 @@
"typescript": "^3.8.0"
},
"resolutions": {
"@types/node": "^10.0.0"
"@types/node": "^12.0.0"
}
}
32 changes: 32 additions & 0 deletions packages/cli-clean/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@react-native-community/cli-clean",
"version": "8.0.0-alpha.0",
"license": "MIT",
"main": "build/index.js",
"publishConfig": {
"access": "public"
},
"types": "build/index.d.ts",
"dependencies": {
"@react-native-community/cli-tools": "^8.0.0-alpha.0",
"chalk": "^4.1.2",
"execa": "^1.0.0",
"prompts": "^2.4.0"
},
"files": [
"build",
"!*.d.ts",
"!*.map"
],
"devDependencies": {
"@react-native-community/cli-types": "^8.0.0-alpha.0",
"@types/execa": "^0.9.0",
"@types/prompts": "^2.0.9"
},
"homepage": "https://github.com/react-native-community/cli/tree/master/packages/cli-clean",
"repository": {
"type": "git",
"url": "https://github.com/react-native-community/cli.git",
"directory": "packages/cli-clean"
}
}
47 changes: 47 additions & 0 deletions packages/cli-clean/src/__tests__/clean.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import execa from 'execa';
import os from 'os';
import prompts from 'prompts';
import {clean} from '../clean';

jest.mock('execa', () => jest.fn());
jest.mock('prompts', () => jest.fn());

describe('clean', () => {
const mockConfig: any = {};

afterEach(() => {
jest.resetAllMocks();
});

it('throws if project root is not set', () => {
expect(clean([], mockConfig, mockConfig)).rejects.toThrow();
});

it('prompts if `--include` is omitted', async () => {
prompts.mockReturnValue({cache: []});

await clean([], mockConfig, {include: '', projectRoot: process.cwd()});

expect(execa).not.toBeCalled();
expect(prompts).toBeCalled();
});

it('stops Watchman and clears out caches', async () => {
await clean([], mockConfig, {
include: 'watchman',
projectRoot: process.cwd(),
});

expect(prompts).not.toBeCalled();
expect(execa).toBeCalledWith(
os.platform() === 'win32' ? 'tskill' : 'killall',
['watchman'],
expect.anything(),
);
expect(execa).toBeCalledWith(
'watchman',
['watch-del-all'],
expect.anything(),
);
});
});
242 changes: 242 additions & 0 deletions packages/cli-clean/src/clean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import {getLoader} from '@react-native-community/cli-tools';
import type {Config as CLIConfig} from '@react-native-community/cli-types';
import chalk from 'chalk';
Copy link
Member

@thymikee thymikee Apr 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll need to add chalk to package.json. Hope this fixes the CI that started to fail with weird unrelated error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops 😄

import execa from 'execa';
import {existsSync as fileExists, rmdir} from 'fs';
import os from 'os';
import path from 'path';
import prompts from 'prompts';
import {promisify} from 'util';

type Args = {
include?: string;
projectRoot: string;
verifyCache?: boolean;
};

type Task = {
label: string;
action: () => Promise<void>;
};

type CleanGroups = {
[key: string]: {
description: string;
tasks: Task[];
};
};

const DEFAULT_GROUPS = ['metro', 'watchman'];

const rmdirAsync = promisify(rmdir);

function cleanDir(directory: string): Promise<void> {
if (!fileExists(directory)) {
return Promise.resolve();
}

return rmdirAsync(directory, {maxRetries: 3, recursive: true});
}

function findPath(startPath: string, files: string[]): string | undefined {
// TODO: Find project files via `@react-native-community/cli`
for (const file of files) {
const filename = path.resolve(startPath, file);
if (fileExists(filename)) {
return filename;
}
}

return undefined;
}

async function promptForCaches(
groups: CleanGroups,
): Promise<string[] | undefined> {
const {caches} = await prompts({
type: 'multiselect',
name: 'caches',
message: 'Select all caches to clean',
choices: Object.entries(groups).map(([cmd, group]) => ({
title: `${cmd} ${chalk.dim(`(${group.description})`)}`,
value: cmd,
selected: DEFAULT_GROUPS.includes(cmd),
})),
min: 1,
});
return caches;
}

export async function clean(
_argv: string[],
_config: CLIConfig,
cleanOptions: Args,
): Promise<void> {
const {include, projectRoot, verifyCache} = cleanOptions;
if (!fileExists(projectRoot)) {
throw new Error(`Invalid path provided! ${projectRoot}`);
}

const COMMANDS: CleanGroups = {
android: {
description: 'Android build caches, e.g. Gradle',
tasks: [
{
label: 'Clean Gradle cache',
action: async () => {
const candidates =
os.platform() === 'win32'
? ['android/gradlew.bat', 'gradlew.bat']
: ['android/gradlew', 'gradlew'];
const gradlew = findPath(projectRoot, candidates);
if (gradlew) {
const script = path.basename(gradlew);
await execa(
os.platform() === 'win32' ? script : `./${script}`,
['clean'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small FYI: clean won't work for users on New Architecture due to a bug on AGP that is causing clean to depend on preBuild. I agree that ./gradlew clean would be the correct approach here, but maybe we should not advertise it yet? Or have a isNewArchitectureEnabled flag inside the CLI to check this and run a rm?

{cwd: path.dirname(gradlew)},
);
}
},
},
],
},
...(os.platform() === 'darwin'
? {
cocoapods: {
description: 'CocoaPods cache',
tasks: [
{
label: 'Clean CocoaPods cache',
action: async () => {
await execa('pod', ['cache', 'clean', '--all'], {
cwd: projectRoot,
});
},
},
],
},
}
: undefined),
metro: {
description: 'Metro, haste-map caches',
tasks: [
{
label: 'Clean Metro cache',
action: () => cleanDir(`${os.tmpdir()}/metro-*`),
},
{
label: 'Clean Haste cache',
action: () => cleanDir(`${os.tmpdir()}/haste-map-*`),
},
{
label: 'Clean React Native cache',
action: () => cleanDir(`${os.tmpdir()}/react-*`),
},
],
},
npm: {
description:
'`node_modules` folder in the current package, and optionally verify npm cache',
tasks: [
{
label: 'Remove node_modules',
action: () => cleanDir(`${projectRoot}/node_modules`),
},
...(verifyCache
? [
{
label: 'Verify npm cache',
action: async () => {
await execa('npm', ['cache', 'verify'], {cwd: projectRoot});
},
},
]
: []),
],
},
watchman: {
description: 'Stop Watchman and delete its cache',
tasks: [
{
label: 'Stop Watchman',
action: async () => {
await execa(
os.platform() === 'win32' ? 'tskill' : 'killall',
['watchman'],
{cwd: projectRoot},
);
},
},
{
label: 'Delete Watchman cache',
action: async () => {
await execa('watchman', ['watch-del-all'], {cwd: projectRoot});
},
},
],
},
yarn: {
description: 'Yarn cache',
tasks: [
{
label: 'Clean Yarn cache',
action: async () => {
await execa('yarn', ['cache', 'clean'], {cwd: projectRoot});
},
},
],
},
};

const groups = include ? include.split(',') : await promptForCaches(COMMANDS);
if (!groups || groups.length === 0) {
return;
}

const spinner = getLoader();
for (const group of groups) {
const commands = COMMANDS[group];
if (!commands) {
spinner.warn(`Unknown group: ${group}`);
continue;
}

for (const {action, label} of commands.tasks) {
spinner.start(label);
await action()
.then(() => {
spinner.succeed();
})
.catch((e) => {
spinner.fail(`${label} » ${e}`);
});
}
}
}

export default {
func: clean,
name: 'clean',
description:
'Cleans your project by removing React Native related caches and modules.',
options: [
{
name: '--include <string>',
description:
'Comma-separated flag of caches to clear e.g. `npm,yarn`. If omitted, an interactive prompt will appear.',
},
{
name: '--project-root <string>',
description:
'Root path to your React Native project. When not specified, defaults to current working directory.',
default: process.cwd(),
},
{
name: '--verify-cache',
description:
'Whether to verify the cache. Currently only applies to npm cache.',
default: false,
},
],
};
3 changes: 3 additions & 0 deletions packages/cli-clean/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {default as clean} from './clean';

export const commands = {clean};
12 changes: 12 additions & 0 deletions packages/cli-clean/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "build"
},
"references": [
{"path": "../tools"},
{"path": "../cli-types"},
{"path": "../cli-config"}
]
}
1 change: 0 additions & 1 deletion packages/cli-doctor/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ const doctorCommand = (async (_, options) => {
removeKeyPressListener();

process.exit(0);
return;
}

if (
Expand Down
6 changes: 3 additions & 3 deletions packages/cli-doctor/src/tools/windows/androidWinHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ export const installComponent = (component: string, androidSdkRoot: string) => {
const child = executeCommand(command);
let stderr = '';

child.stdout.on('data', (data) => {
child.stdout?.on('data', (data) => {
if (data.includes('(y/N)')) {
child.stdin.write('y\n');
child.stdin?.write('y\n');
}
});

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

Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"testEnvironment": "node"
},
"dependencies": {
"@react-native-community/cli-clean": "^8.0.0-alpha.0",
"@react-native-community/cli-config": "^8.0.0-alpha.0",
"@react-native-community/cli-debugger-ui": "^8.0.0-alpha.0",
"@react-native-community/cli-doctor": "^8.0.0-alpha.0",
Expand Down
Loading