UNPKG

nativescript

Version:

Command-line interface for building NativeScript projects

258 lines • 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CleanCommand = void 0; const color_1 = require("../color"); const yok_1 = require("../common/yok"); const constants = require("../constants"); const os = require("os"); const path_1 = require("path"); const promises_1 = require("fs/promises"); const helpers_1 = require("../common/helpers"); function bytesToHumanReadable(bytes) { const units = ["B", "KB", "MB", "GB", "TB"]; let unit = 0; while (bytes >= 1024) { bytes /= 1024; unit++; } return `${bytes.toFixed(2)} ${units[unit]}`; } /** * A helper function to map an array of values to promises with a concurrency limit. * The mapper function should return a promise. It will be called for each value in the values array. * The concurrency limit is the number of promises that can be running at the same time. * * This function will return a promise that resolves when all values have been mapped. * * @param values A static array of values to map to promises * @param mapper A function that maps a value to a promise * @param concurrency The number of promises that can be running at the same time * @returns Promise<void> */ function promiseMap(values, mapper, concurrency = 10) { let index = 0; let pending = 0; let done = false; return new Promise((resolve, reject) => { const next = () => { done = index === values.length; if (done && pending === 0) { return resolve(); } while (pending < concurrency && index < values.length) { const value = values[index++]; pending++; mapper(value) .then(() => { pending--; next(); }) .catch(); } }; next(); }); } class CleanCommand { constructor($projectCleanupService, $projectConfigService, $terminalSpinnerService, $projectService, $prompter, $logger, $options, $childProcess, $staticConfig) { this.$projectCleanupService = $projectCleanupService; this.$projectConfigService = $projectConfigService; this.$terminalSpinnerService = $terminalSpinnerService; this.$projectService = $projectService; this.$prompter = $prompter; this.$logger = $logger; this.$options = $options; this.$childProcess = $childProcess; this.$staticConfig = $staticConfig; this.allowedParameters = []; } async execute(args) { var _a, _b; const isDryRun = (_a = this.$options.dryRun) !== null && _a !== void 0 ? _a : false; const isJSON = (_b = this.$options.json) !== null && _b !== void 0 ? _b : false; const spinner = this.$terminalSpinnerService.createSpinner({ isSilent: isJSON, }); if (!this.$projectService.isValidNativeScriptProject()) { return this.cleanMultipleProjects(spinner); } spinner.start("Cleaning project...\n"); let pathsToClean = [ constants.HOOKS_DIR_NAME, constants.PLATFORMS_DIR_NAME, constants.NODE_MODULES_FOLDER_NAME, constants.PACKAGE_LOCK_JSON_FILE_NAME, ]; try { const overridePathsToClean = this.$projectConfigService.getValue("cli.pathsToClean"); const additionalPaths = this.$projectConfigService.getValue("cli.additionalPathsToClean"); // allow overriding default paths to clean if (Array.isArray(overridePathsToClean)) { pathsToClean = overridePathsToClean; } if (Array.isArray(additionalPaths)) { pathsToClean.push(...additionalPaths); } } catch (err) { // ignore } const res = await this.$projectCleanupService.clean(pathsToClean, { dryRun: isDryRun, silent: isJSON, stats: isJSON, }); if (res.stats && isJSON) { console.log(JSON.stringify({ ok: res.ok, dryRun: isDryRun, stats: Object.fromEntries(res.stats.entries()), }, null, 2)); return; } if (res.ok) { spinner.succeed("Project successfully cleaned."); } else { spinner.fail(color_1.color.red("Project unsuccessfully cleaned.")); } } async cleanMultipleProjects(spinner) { if (!(0, helpers_1.isInteractive)() || this.$options.json) { // interactive terminal is required, and we can't output json in an interactive command. this.$logger.warn("No project found in the current directory."); return; } const shouldScan = await this.$prompter.confirm("No project found in the current directory. Would you like to scan for all projects in sub-directories instead?"); if (!shouldScan) { return; } spinner.start("Scanning for projects... Please wait."); const paths = await this.getNSProjectPathsInDirectory(); spinner.succeed(`Found ${paths.length} projects.`); let computed = 0; const updateProgress = () => { const current = color_1.color.grey(`${computed}/${paths.length}`); spinner.start(`Gathering cleanable sizes. This may take a while... ${current}`); }; // update the progress initially updateProgress(); const projects = new Map(); await promiseMap(paths, (p) => { return this.$childProcess .exec(`node ${this.$staticConfig.cliBinPath} clean --dry-run --json --disable-analytics`, { cwd: p, }) .then((res) => { const paths = JSON.parse(res).stats; return Object.values(paths).reduce((a, b) => a + b, 0); }) .catch((err) => { this.$logger.trace("Failed to get project size for %s, Error is:", p, err); return -1; }) .then((size) => { if (size > 0 || size === -1) { // only store size if it's larger than 0 or -1 (error while getting size) projects.set(p, size); } // update the progress after each processed project computed++; updateProgress(); }); }, os.cpus().length); spinner.clear(); spinner.stop(); this.$logger.clearScreen(); const totalSize = Array.from(projects.values()) .filter((s) => s > 0) .reduce((a, b) => a + b, 0); const pathsToClean = await this.$prompter.promptForChoice(`Found ${projects.size} cleanable project(s) with a total size of: ${color_1.color.green(bytesToHumanReadable(totalSize))}. Select projects to clean`, Array.from(projects.keys()).map((p) => { const size = projects.get(p); let description; if (size === -1) { description = " - could not get size"; } else { description = ` - ${bytesToHumanReadable(size)}`; } return { title: `${p}${color_1.color.grey(description)}`, value: p, }; }), true, { optionsPerPage: process.stdout.rows - 6, // 6 lines are taken up by the instructions }); this.$logger.clearScreen(); spinner.warn(`This will run "${color_1.color.yellow(`ns clean`)}" in all the selected projects and ${color_1.color.red.bold("delete files from your system")}!`); spinner.warn(`This action cannot be undone!`); let confirmed = await this.$prompter.confirm("Are you sure you want to clean the selected projects?"); if (!confirmed) { return; } spinner.info("Cleaning... This might take a while..."); let totalSizeCleaned = 0; for (let i = 0; i < pathsToClean.length; i++) { const currentPath = pathsToClean[i]; spinner.start(`Cleaning ${color_1.color.cyan(currentPath)}... ${i + 1}/${pathsToClean.length}`); const ok = await this.$childProcess .exec(`node ${this.$staticConfig.cliBinPath} clean ${this.$options.dryRun ? "--dry-run" : ""} --json --disable-analytics`, { cwd: currentPath, }) .then((res) => { const cleanupRes = JSON.parse(res); return cleanupRes.ok; }) .catch((err) => { this.$logger.trace('Failed to clean project "%s"', currentPath, err); return false; }); if (ok) { const cleanedSize = projects.get(currentPath); const cleanedSizeStr = color_1.color.grey(`- ${bytesToHumanReadable(cleanedSize)}`); spinner.succeed(`Cleaned ${color_1.color.cyan(currentPath)} ${cleanedSizeStr}`); totalSizeCleaned += cleanedSize; } else { spinner.fail(`Failed to clean ${color_1.color.cyan(currentPath)} - skipped`); } } spinner.clear(); spinner.stop(); spinner.succeed(`Done! We've just freed up ${color_1.color.green(bytesToHumanReadable(totalSizeCleaned))}! Woohoo! 🎉`); if (this.$options.dryRun) { spinner.info('Note: the "--dry-run" flag was used, so no files were actually deleted.'); } } async getNSProjectPathsInDirectory(dir = process.cwd()) { let nsDirs = []; const getFiles = async (dir) => { if (dir.includes("node_modules")) { // skip traversing node_modules return; } const dirents = await (0, promises_1.readdir)(dir, { withFileTypes: true }).catch((err) => { this.$logger.trace('Failed to read directory "%s". Error is:', dir, err); return []; }); const hasNSConfig = dirents.some((ent) => ent.name.includes("nativescript.config.ts") || ent.name.includes("nativescript.config.js")); if (hasNSConfig) { nsDirs.push(dir); // found a NativeScript project, stop traversing return; } await Promise.all(dirents.map((dirent) => { const res = (0, path_1.resolve)(dir, dirent.name); if (dirent.isDirectory()) { return getFiles(res); } })); }; await getFiles(dir); return nsDirs; } } exports.CleanCommand = CleanCommand; yok_1.injector.registerCommand("clean", CleanCommand); //# sourceMappingURL=clean.js.map