Skip to content
Open
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
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

181 changes: 181 additions & 0 deletions packages/cli/lib/cli/commands/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import chalk from "chalk";
import path from "node:path";
import os from "node:os";
import process from "node:process";
import baseMiddleware from "../middlewares/base.js";
import {getUi5DataDir} from "../../framework/utils.js";
import * as frameworkCache from "@ui5/project/ui5Framework/cache";
import CacheManager from "@ui5/project/build/cache/CacheManager";

const cacheCommand = {
command: "cache",
describe: "Manage the UI5 CLI cache (downloaded framework packages and incremental build data)",
middlewares: [baseMiddleware],
handler: handleCache
};

cacheCommand.builder = function(cli) {
return cli
.demandCommand(1, "Command required. Available command is 'clean'")
.command("clean", "Remove all cached UI5 data", {
Comment thread
d3xter666 marked this conversation as resolved.
handler: handleCache,
builder: function(yargs) {
return yargs
.option("yes", {
alias: "y",
describe: "Skip the confirmation prompt, e.g. for use in CI pipelines",
default: false,
type: "boolean",
})
.example("$0 cache clean",
"Remove all cached UI5 data after confirming the prompt")
.example("$0 cache clean --yes",
"Remove all cached UI5 data without confirmation (e.g. in CI)")
.example("UI5_DATA_DIR=/custom/path $0 cache clean",
"Remove cached data from a non-default UI5 data directory");
},
middlewares: [baseMiddleware],
});
};

const LABEL_FRAMEWORK = "UI5 Framework packages";
const LABEL_BUILD = "Build cache (DB)";
// Pad labels to equal width for two-column alignment
const LABEL_WIDTH = Math.max(LABEL_FRAMEWORK.length, LABEL_BUILD.length);

/**
* Format a byte size as a human-readable string.
*
* @param {number} bytes Size in bytes
* @returns {string} Formatted size string
*/
function formatSize(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
} else if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}

/**
* Format a count with its singular/plural word, e.g. "340 files" or "1 file".
*
* @param {number} count
* @returns {string}
*/
function formatFileCount(count) {
return `${count} ${count === 1 ? "file" : "files"}`;
}

/**
* Pad a label to the shared column width.
*
* @param {string} label
* @returns {string}
*/
function padLabel(label) {
return label.padEnd(LABEL_WIDTH);
}

async function handleCache(argv) {
// Resolve UI5 data directory — uses the same resolution chain as ui5 build/serve:
// UI5_DATA_DIR env var → ui5DataDir config (~/.ui5rc) → default ~/.ui5
// Relative paths are resolved against process.cwd() (project root when invoked from the project).
const ui5DataDir =
(await getUi5DataDir({cwd: process.cwd()})) ?? path.join(os.homedir(), ".ui5");

// Abort early if a framework operation is holding a lock — before prompting the user
if (await frameworkCache.isFrameworkLocked(ui5DataDir)) {
process.stderr.write(
`${chalk.red("Error:")} Framework cache is currently locked by an active operation. ` +
"Please wait for it to finish and try again.\n"
);
process.exitCode = 1;
return;
}

// Inform the user immediately — getCacheInfo (especially countFiles) may take a moment
process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`);

// Check what items exist before cleaning (orchestrate both domains)
const frameworkInfo = await frameworkCache.getCacheInfo(ui5DataDir);
const buildInfo = await CacheManager.getCacheInfo(ui5DataDir);

if (!frameworkInfo && !buildInfo) {
process.stderr.write("Nothing to clean\n");
return;
}

// Compute absolute paths once — producers return relative sub-path segments
const frameworkAbsPath = frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null;
const buildAbsPath = buildInfo ? path.join(ui5DataDir, buildInfo.path) : null;

// Capture build size now — reused for the ✓ line to avoid a before/after mismatch
// (getDatabaseSize ≠ VACUUM-freed bytes returned by clearAllRecords)
const buildPreSize = buildInfo?.size ?? 0;

// Display items that will be removed
process.stderr.write(chalk.bold("\nThe following cached data will be removed:\n\n"));
if (frameworkInfo) {
const detail = formatFileCount(frameworkInfo.count);
process.stderr.write(
` ${chalk.yellow("•")} ${padLabel(LABEL_FRAMEWORK)} ${frameworkAbsPath} (${detail})\n`
);
}
if (buildInfo) {
const detail = buildPreSize > 0 ? formatSize(buildPreSize) : "";
process.stderr.write(
` ${chalk.yellow("•")} ${padLabel(LABEL_BUILD)} ${buildAbsPath} (${detail})\n`
);
}
process.stderr.write("\n");

// Ask for confirmation (skip with --yes)
if (!argv.yes) {
const {default: yesno} = await import("yesno");
const confirmed = await yesno({
question: "Do you want to continue? (y/N)",
defaultValue: false
});
if (!confirmed) {
process.stderr.write("Cancelled\n");
return;
}
}

// Perform the actual cleanup (orchestrate both domains)
const frameworkResult = await frameworkCache.cleanCache(ui5DataDir);
const buildResult = await CacheManager.cleanCache(ui5DataDir);

process.stderr.write("\n");
if (frameworkResult) {
const detail = formatFileCount(frameworkResult.count);
process.stderr.write(
`${chalk.green("✓")} Removed ${chalk.bold(LABEL_FRAMEWORK)}` +
` (${frameworkAbsPath} · ${detail})\n`
);
}
if (buildResult) {
// Use pre-clean size so the number matches what was shown before confirmation
const detail = buildPreSize > 0 ? formatSize(buildPreSize) : "";
process.stderr.write(
`${chalk.green("✓")} Removed ${chalk.bold(LABEL_BUILD)}` +
` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n`
);
}

// Success summary
const cleaned = [];
if (frameworkResult) {
cleaned.push(LABEL_FRAMEWORK);
}
if (buildResult) {
cleaned.push(LABEL_BUILD);
}
process.stderr.write(`\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`);
}

export default cacheCommand;
2 changes: 1 addition & 1 deletion packages/cli/lib/framework/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export async function frameworkResolverResolveVersion({frameworkName, frameworkV
});
}

async function getUi5DataDir({cwd}) {
export async function getUi5DataDir({cwd}) {
// ENV var should take precedence over the dataDir from the configuration.
let ui5DataDir = process.env.UI5_DATA_DIR;
if (!ui5DataDir) {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"pretty-hrtime": "^1.0.3",
"semver": "^7.7.2",
"update-notifier": "^7.3.1",
"yargs": "^18.0.0"
"yargs": "^18.0.0",
"yesno": "^0.4.0"
},
"devDependencies": {
"@istanbuljs/esm-loader-hook": "^0.3.0",
Expand Down
Loading
Loading