Skip to content

Commit d5e28f2

Browse files
committed
refactor: Revise actual cleanup UX
1 parent e45d5ff commit d5e28f2

3 files changed

Lines changed: 118 additions & 9 deletions

File tree

packages/cli/lib/cli/commands/cache.js

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import baseMiddleware from "../middlewares/base.js";
66
import {getUi5DataDir} from "../../framework/utils.js";
77
import * as frameworkCache from "@ui5/project/ui5Framework/cache";
88
import CacheManager from "@ui5/project/build/cache/CacheManager";
9+
import prettyHrtime from "pretty-hrtime";
910

1011
const cacheCommand = {
1112
command: "cache",
@@ -86,6 +87,64 @@ function padLabel(label) {
8687
return label.padEnd(LABEL_WIDTH);
8788
}
8889

90+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
91+
const PROGRESS_DEBOUNCE_MS = 150;
92+
// Reserve enough columns for the fixed parts of the progress line so the path
93+
// never causes the line to wrap on a standard 80-column terminal.
94+
const PATH_MAX_COLS = 40;
95+
96+
/**
97+
* Build a progress handler for framework cache deletion.
98+
* Returns a function to pass as onProgress to cleanCache(), plus a finalise()
99+
* to call when deletion completes (clears the in-progress line).
100+
*
101+
* The line is written to stderr with \r so it overwrites itself on each tick,
102+
* producing a single updating line rather than a scrolling log.
103+
*
104+
* @param {string} label Short label shown on the progress line
105+
* @param {[number, number]} startHrtime process.hrtime() snapshot taken when deletion began
106+
* @param {function([number, number]): string} prettyHrtime Formatting function from the pretty-hrtime package
107+
* @returns {{onProgress: function(string): void, finalise: function(): void}}
108+
*/
109+
function createProgressHandler(label, startHrtime, prettyHrtime) {
110+
let lastPrintMs = 0;
111+
let frameIndex = 0;
112+
let lastVisibleLen = 0;
113+
114+
function onProgress(entryPath) {
115+
const now = Date.now();
116+
if (now - lastPrintMs < PROGRESS_DEBOUNCE_MS) return;
117+
lastPrintMs = now;
118+
119+
const elapsed = prettyHrtime(process.hrtime(startHrtime));
120+
const spinner = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
121+
frameIndex++;
122+
123+
// Trim path so the whole line stays within 80 columns
124+
let displayPath = entryPath;
125+
if (displayPath.length > PATH_MAX_COLS) {
126+
displayPath = "…" + displayPath.slice(-(PATH_MAX_COLS - 1));
127+
}
128+
129+
// Build visible text (no ANSI) first to get accurate length for overwrite padding
130+
const visibleText = ` ${spinner} ${label} ${displayPath} ${elapsed}`;
131+
// Then the styled version for actual output
132+
const styledText = ` ${spinner} ${label} ${chalk.dim(displayPath)} ${elapsed}`;
133+
134+
// Pad to cover any longer previous line, then overwrite in place
135+
const padded = styledText + " ".repeat(Math.max(0, lastVisibleLen - visibleText.length));
136+
lastVisibleLen = visibleText.length;
137+
138+
process.stderr.write(`\r${padded}`);
139+
}
140+
141+
function finalise() {
142+
process.stderr.write(`\r${" ".repeat(lastVisibleLen)}\r`);
143+
}
144+
145+
return {onProgress, finalise};
146+
}
147+
89148
async function handleCache(argv) {
90149
// Resolve UI5 data directory — uses the same resolution chain as ui5 build/serve:
91150
// UI5_DATA_DIR env var → ui5DataDir config (~/.ui5rc) → default ~/.ui5
@@ -154,7 +213,16 @@ async function handleCache(argv) {
154213
}
155214

156215
// Perform the actual cleanup (orchestrate both domains)
157-
const frameworkResult = await frameworkCache.cleanCache(ui5DataDir);
216+
let frameworkResult;
217+
if (frameworkInfo) {
218+
const startHrtime = process.hrtime();
219+
const {onProgress, finalise} = createProgressHandler(LABEL_FRAMEWORK, startHrtime, prettyHrtime);
220+
try {
221+
frameworkResult = await frameworkCache.cleanCache(ui5DataDir, onProgress);
222+
} finally {
223+
finalise();
224+
}
225+
}
158226
const buildResult = await CacheManager.cleanCache(ui5DataDir);
159227

160228
process.stderr.write("\n");

packages/project/lib/ui5Framework/cache.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
* package contents. Inner levels are parallelised with Promise.all to avoid serial
1414
* I/O on large caches.
1515
*
16-
*
1716
* @param {string} packagesDir Absolute path to the packages directory
1817
* @returns {Promise<{projects: number, libraries: number, versions: number}|null>}
1918
* Null if the directory does not exist or contains no installed libraries.
@@ -63,9 +62,42 @@ async function getPackageStats(packagesDir) {
6362
}
6463
}));
6564

66-
return librarySet.size > 0
67-
? {projects: totalProjects, libraries: librarySet.size, versions: versionSet.size}
68-
: null;
65+
return librarySet.size > 0 ?
66+
{projects: totalProjects, libraries: librarySet.size, versions: versionSet.size} :
67+
null;
68+
}
69+
70+
/**
71+
* Recursively remove a directory, calling onProgress(entryPath) for every
72+
* entry (file or directory) just before it is deleted.
73+
*
74+
* Uses manual traversal instead of fs.rm so callers can observe deletion
75+
* progress. Intentionally serial — parallelising unlink() calls does not
76+
* improve throughput on a single filesystem and makes the progress callback
77+
* ordering unpredictable.
78+
*
79+
* @param {string} dirPath Absolute path to the directory to remove
80+
* @param {function(string): void} onProgress Called with the path of each
81+
* entry immediately before it is deleted
82+
* @returns {Promise<void>}
83+
*/
84+
async function rmRecursive(dirPath, onProgress) {
85+
let entries;
86+
try {
87+
entries = await fs.readdir(dirPath, {withFileTypes: true});
88+
} catch {
89+
return;
90+
}
91+
for (const entry of entries) {
92+
const entryPath = path.join(dirPath, entry.name);
93+
onProgress(entryPath);
94+
if (entry.isDirectory()) {
95+
await rmRecursive(entryPath, onProgress);
96+
await fs.rmdir(entryPath);
97+
} else {
98+
await fs.unlink(entryPath);
99+
}
100+
}
69101
}
70102

71103
/**
@@ -113,11 +145,14 @@ export async function isFrameworkLocked(ui5DataDir) {
113145
* deleting files while a download is in progress.
114146
*
115147
* @param {string} ui5DataDir Resolved absolute path to UI5 data directory
148+
* @param {function(string): void} [onProgress] Optional callback invoked with
149+
* the absolute path of each entry just before it is deleted. Use for
150+
* progress display. Omit for silent deletion (falls back to fs.rm).
116151
* @returns {Promise<{path: string, libraryCount: number, projectCount: number, versionCount: number}|null>}
117152
* Removal result, or null if nothing was installed.
118153
* @throws {Error} If a framework operation is currently active (active lockfiles detected)
119154
*/
120-
export async function cleanCache(ui5DataDir) {
155+
export async function cleanCache(ui5DataDir, onProgress) {
121156
const frameworkDir = getFrameworkDir(ui5DataDir);
122157

123158
try {
@@ -138,7 +173,13 @@ export async function cleanCache(ui5DataDir) {
138173
);
139174
}
140175

141-
await fs.rm(frameworkDir, {recursive: true, force: true});
176+
if (onProgress) {
177+
await rmRecursive(frameworkDir, onProgress);
178+
await fs.rmdir(frameworkDir);
179+
} else {
180+
await fs.rm(frameworkDir, {recursive: true, force: true});
181+
}
182+
142183
return {
143184
path: FRAMEWORK_DIR_NAME,
144185
libraryCount: stats.libraries,

packages/project/test/lib/ui5framework/cache.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ test("getCacheInfo: counts projects, libraries and versions", async (t) => {
6161
t.truthy(result);
6262
t.is(result.path, "framework");
6363
t.is(result.projectCount, 2);
64-
t.is(result.libraryCount, 2); // sap.m counted once (deduplicated across projects)
65-
t.is(result.versionCount, 3); // 1.120.0, 1.148.0, 1.38.1
64+
t.is(result.libraryCount, 2); // sap.m counted once (deduplicated across projects)
65+
t.is(result.versionCount, 3); // 1.120.0, 1.148.0, 1.38.1
6666
});
6767

6868
test("getCacheInfo: deduplicates library names across projects", async (t) => {

0 commit comments

Comments
 (0)