-
Notifications
You must be signed in to change notification settings - Fork 905
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
Changes from all commits
ae6511a
a9dbb09
eeb9834
5da6a19
b6a7555
d25846b
30f92c2
043d78f
da94494
37bd255
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
} | ||
} |
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(), | ||
); | ||
}); | ||
}); |
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'; | ||
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'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
{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, | ||
}, | ||
], | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import {default as clean} from './clean'; | ||
|
||
export const commands = {clean}; |
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"} | ||
] | ||
} |
There was a problem hiding this comment.
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 errorThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops 😄