|
| 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 | +}; |
0 commit comments