nativescript
Version:
Command-line interface for building NativeScript projects
258 lines • 10.9 kB
JavaScript
;
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