Skip to content

Commit 7b17cc0

Browse files
committed
refactor: CLI package orchestrates cache cleanup
Provide common interface for cache cleanup, but distribute the real cleanup among the respective destinations
1 parent 1865be0 commit 7b17cc0

11 files changed

Lines changed: 416 additions & 484 deletions

File tree

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

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import process from "node:process";
55
import readline from "node:readline";
66
import baseMiddleware from "../middlewares/base.js";
77
import Configuration from "@ui5/project/config/Configuration";
8-
import {cleanCache, getCacheInfo} from "@ui5/project/cache/CacheCleanup";
8+
import * as frameworkCache from "@ui5/project/ui5Framework/cache";
9+
import CacheManager from "@ui5/project/build/cache/CacheManager";
910

1011
const cacheCommand = {
1112
command: "cache",
@@ -78,8 +79,16 @@ async function handleCache() {
7879
ui5DataDir = path.join(os.homedir(), ".ui5");
7980
}
8081

81-
// Check what items exist before cleaning
82-
const items = await getCacheInfo({ui5DataDir});
82+
// Check what items exist before cleaning (orchestrate both domains)
83+
const items = [];
84+
const frameworkInfo = await frameworkCache.getCacheInfo(ui5DataDir);
85+
if (frameworkInfo) {
86+
items.push(frameworkInfo);
87+
}
88+
const buildInfo = await CacheManager.getCacheInfo(ui5DataDir);
89+
if (buildInfo) {
90+
items.push(buildInfo);
91+
}
8392

8493
if (items.length === 0) {
8594
process.stderr.write("Nothing to clean\n");
@@ -103,18 +112,27 @@ async function handleCache() {
103112
return;
104113
}
105114

106-
// Perform the actual cleanup
107-
const result = await cleanCache({ui5DataDir});
115+
// Perform the actual cleanup (orchestrate both domains)
116+
const removed = [];
117+
const frameworkResult = await frameworkCache.cleanCache(ui5DataDir);
118+
if (frameworkResult) {
119+
removed.push(frameworkResult);
120+
}
121+
const buildResult = await CacheManager.cleanCache(ui5DataDir);
122+
if (buildResult) {
123+
removed.push(buildResult);
124+
}
108125

109126
process.stderr.write("\n");
110-
for (const entry of result.entries) {
127+
for (const entry of removed) {
111128
const sizeStr = entry.size > 0 ? ` (${formatSize(entry.size)})` : "";
112129
process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(entry.path)}${sizeStr}\n`);
113130
}
114131

132+
const totalRemoved = removed.reduce((sum, entry) => sum + entry.size, 0);
115133
process.stderr.write(
116-
`\n${chalk.green("Success:")} Cleaned ${result.totalCount} ${result.totalCount === 1 ? "entry" : "entries"}` +
117-
(result.totalSize > 0 ? `, freed ${formatSize(result.totalSize)}` : "") + "\n"
134+
`\n${chalk.green("Success:")} Cleaned ${removed.length} ${removed.length === 1 ? "entry" : "entries"}` +
135+
(totalRemoved > 0 ? `, freed ${formatSize(totalRemoved)}` : "") + "\n"
118136
);
119137
}
120138

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

Lines changed: 57 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ test.beforeEach(async (t) => {
2323
t.context.Configuration = Configuration;
2424
sinon.stub(Configuration, "fromFile").resolves(new Configuration({}));
2525

26-
t.context.cleanCacheStub = sinon.stub();
27-
t.context.getCacheInfoStub = sinon.stub();
26+
t.context.frameworkCacheGetCacheInfo = sinon.stub();
27+
t.context.frameworkCacheCleanCache = sinon.stub();
28+
t.context.buildCacheGetCacheInfo = sinon.stub();
29+
t.context.buildCacheCleanCache = sinon.stub();
2830

2931
// Mock readline to simulate user confirmation
3032
const mockRLInterface = {
@@ -36,9 +38,15 @@ test.beforeEach(async (t) => {
3638

3739
t.context.cache = await esmock.p("../../../../lib/cli/commands/cache.js", {
3840
"@ui5/project/config/Configuration": t.context.Configuration,
39-
"@ui5/project/cache/CacheCleanup": {
40-
cleanCache: t.context.cleanCacheStub,
41-
getCacheInfo: t.context.getCacheInfoStub,
41+
"@ui5/project/ui5Framework/cache": {
42+
getCacheInfo: t.context.frameworkCacheGetCacheInfo,
43+
cleanCache: t.context.frameworkCacheCleanCache
44+
},
45+
"@ui5/project/build/cache/CacheManager": {
46+
default: class {
47+
static getCacheInfo = t.context.buildCacheGetCacheInfo;
48+
static cleanCache = t.context.buildCacheCleanCache;
49+
}
4250
},
4351
"node:readline": {
4452
createInterface: t.context.readlineCreateInterfaceStub,
@@ -67,41 +75,38 @@ test("Command builder", async (t) => {
6775
});
6876

6977
test.serial("ui5 cache clean: nothing to clean", async (t) => {
70-
const {cache, argv, stderrWriteStub, cleanCacheStub, getCacheInfoStub} = t.context;
78+
const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo,
79+
buildCacheCleanCache, buildCacheGetCacheInfo} = t.context;
7180

7281
// Simulate no cache items
73-
getCacheInfoStub.resolves([]);
82+
frameworkCacheGetCacheInfo.resolves(null);
83+
buildCacheGetCacheInfo.resolves(null);
7484

7585
argv["_"] = ["cache", "clean"];
7686
await cache.handler(argv);
7787

7888
t.is(stderrWriteStub.firstCall.firstArg, "Nothing to clean\n");
79-
t.is(cleanCacheStub.callCount, 0, "cleanCache should not be called");
89+
t.is(frameworkCacheCleanCache.callCount, 0, "frameworkCache.cleanCache should not be called");
90+
t.is(buildCacheCleanCache.callCount, 0, "buildCache.cleanCache should not be called");
8091
});
8192

8293
test.serial("ui5 cache clean: removes entries and reports", async (t) => {
83-
const {cache, argv, stderrWriteStub, cleanCacheStub, getCacheInfoStub,
84-
mockRLInterface} = t.context;
94+
const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo,
95+
buildCacheCleanCache, buildCacheGetCacheInfo, mockRLInterface} = t.context;
8596

8697
// Simulate existing cache items
87-
getCacheInfoStub.resolves([
88-
{path: "framework/", size: 15 * 1024 * 1024, type: "directory"},
89-
{path: "buildCache/ (database records)", size: 8 * 1024 * 1024, type: "database"},
90-
]);
98+
frameworkCacheGetCacheInfo.resolves({path: "framework/", size: 15 * 1024 * 1024, type: "directory"});
99+
buildCacheGetCacheInfo.resolves({
100+
path: "buildCache/v0_7 (database records)", size: 8 * 1024 * 1024, type: "database"
101+
});
91102

92103
// Mock user confirmation
93104
mockRLInterface.question.callsFake((question, callback) => {
94105
callback("y");
95106
});
96107

97-
cleanCacheStub.resolves({
98-
entries: [
99-
{path: "framework", type: "framework", size: 15 * 1024 * 1024},
100-
{path: "buildCache", type: "buildCache", size: 8 * 1024 * 1024},
101-
],
102-
totalSize: 23 * 1024 * 1024,
103-
totalCount: 2,
104-
});
108+
frameworkCacheCleanCache.resolves({path: "framework", type: "framework", size: 15 * 1024 * 1024});
109+
buildCacheCleanCache.resolves({path: "buildCache/v0_7", type: "buildCache", size: 8 * 1024 * 1024});
105110

106111
argv["_"] = ["cache", "clean"];
107112
await cache.handler(argv);
@@ -112,7 +117,8 @@ test.serial("ui5 cache clean: removes entries and reports", async (t) => {
112117
"Confirmation question should ask to continue");
113118

114119
// Check that cleanCache was called
115-
t.is(cleanCacheStub.callCount, 1, "cleanCache should be called once");
120+
t.is(frameworkCacheCleanCache.callCount, 1, "frameworkCache.cleanCache should be called once");
121+
t.is(buildCacheCleanCache.callCount, 1, "buildCache.cleanCache should be called once");
116122

117123
// Check output
118124
const allOutput = stderrWriteStub.args.map((a) => a[0]).join("");
@@ -122,13 +128,12 @@ test.serial("ui5 cache clean: removes entries and reports", async (t) => {
122128
});
123129

124130
test.serial("ui5 cache clean: user cancels", async (t) => {
125-
const {cache, argv, stderrWriteStub, cleanCacheStub, getCacheInfoStub,
126-
mockRLInterface} = t.context;
131+
const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo,
132+
buildCacheCleanCache, buildCacheGetCacheInfo, mockRLInterface} = t.context;
127133

128134
// Simulate existing cache items
129-
getCacheInfoStub.resolves([
130-
{path: "framework/", size: 5 * 1024 * 1024, type: "directory"}
131-
]);
135+
frameworkCacheGetCacheInfo.resolves({path: "framework/", size: 5 * 1024 * 1024, type: "directory"});
136+
buildCacheGetCacheInfo.resolves(null);
132137

133138
// Mock user cancellation
134139
mockRLInterface.question.callsFake((question, callback) => {
@@ -141,8 +146,9 @@ test.serial("ui5 cache clean: user cancels", async (t) => {
141146
// Check that confirmation was asked
142147
t.is(mockRLInterface.question.callCount, 1, "Should ask for confirmation");
143148

144-
// Check that cleanCache was NOT called
145-
t.is(cleanCacheStub.callCount, 0, "cleanCache should not be called when user cancels");
149+
// Check that cleanup was NOT called
150+
t.is(frameworkCacheCleanCache.callCount, 0, "frameworkCache.cleanCache should not be called when user cancels");
151+
t.is(buildCacheCleanCache.callCount, 0, "buildCache.cleanCache should not be called when user cancels");
146152

147153
// Check output
148154
const allOutput = stderrWriteStub.args.map((a) => a[0]).join("");
@@ -160,75 +166,62 @@ test.serial("Command definition is correct", (t) => {
160166
});
161167

162168
test.serial("ui5 cache clean: accepts 'yes' as confirmation", async (t) => {
163-
const {cache, argv, cleanCacheStub, getCacheInfoStub, mockRLInterface} = t.context;
169+
const {cache, argv, frameworkCacheCleanCache, frameworkCacheGetCacheInfo,
170+
buildCacheGetCacheInfo, mockRLInterface} = t.context;
164171

165-
getCacheInfoStub.resolves([
166-
{path: "framework/", size: 1024, type: "directory"}
167-
]);
172+
frameworkCacheGetCacheInfo.resolves({path: "framework/", size: 1024, type: "directory"});
173+
buildCacheGetCacheInfo.resolves(null);
168174

169175
mockRLInterface.question.callsFake((question, callback) => {
170176
callback("yes");
171177
});
172178

173-
cleanCacheStub.resolves({
174-
entries: [{path: "framework", type: "framework", size: 1024}],
175-
totalSize: 1024,
176-
totalCount: 1,
177-
});
179+
frameworkCacheCleanCache.resolves({path: "framework", type: "framework", size: 1024});
178180

179181
argv["_"] = ["cache", "clean"];
180182
await cache.handler(argv);
181183

182-
t.is(cleanCacheStub.callCount, 1, "cleanCache should be called with 'yes' confirmation");
184+
t.is(frameworkCacheCleanCache.callCount, 1, "frameworkCache.cleanCache should be called with 'yes' confirmation");
183185
});
184186

185187
test.serial("ui5 cache clean: formats byte sizes correctly", async (t) => {
186-
const {cache, argv, stderrWriteStub, cleanCacheStub, getCacheInfoStub, mockRLInterface} = t.context;
188+
const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo,
189+
buildCacheCleanCache, buildCacheGetCacheInfo, mockRLInterface} = t.context;
187190

188191
// Test with small bytes (B), KB, and GB sizes
189-
getCacheInfoStub.resolves([
190-
{path: "small", size: 512, type: "directory"}, // < 1024 = B
191-
{path: "medium", size: 50 * 1024, type: "directory"}, // KB
192-
{path: "large", size: 2 * 1024 * 1024 * 1024, type: "directory"}, // GB
193-
]);
192+
frameworkCacheGetCacheInfo.resolves({path: "small", size: 512, type: "directory"}); // < 1024 = B
193+
buildCacheGetCacheInfo.resolves({path: "medium", size: 50 * 1024, type: "database"}); // KB
194194

195195
mockRLInterface.question.callsFake((question, callback) => {
196196
callback("y");
197197
});
198198

199-
cleanCacheStub.resolves({
200-
entries: [
201-
{path: "small", type: "directory", size: 512},
202-
{path: "medium", type: "directory", size: 50 * 1024},
203-
{path: "large", type: "directory", size: 2 * 1024 * 1024 * 1024},
204-
],
205-
totalSize: 2 * 1024 * 1024 * 1024 + 50 * 1024 + 512,
206-
totalCount: 3,
207-
});
199+
frameworkCacheCleanCache.resolves({path: "small", type: "directory", size: 512});
200+
buildCacheCleanCache.resolves({path: "medium", type: "database", size: 50 * 1024});
208201

209202
argv["_"] = ["cache", "clean"];
210203
await cache.handler(argv);
211204

212205
const allOutput = stderrWriteStub.args.map((a) => a[0]).join("");
213206
t.true(allOutput.includes("512 B"), "Shows bytes format");
214207
t.true(allOutput.includes("50.0 KB"), "Shows KB format");
215-
t.true(allOutput.includes("2.0 GB"), "Shows GB format");
216208
});
217209

218210
test.serial("ui5 cache clean: uses UI5_DATA_DIR from environment", async (t) => {
219-
const {cache, argv, getCacheInfoStub} = t.context;
211+
const {cache, argv, frameworkCacheGetCacheInfo, buildCacheGetCacheInfo} = t.context;
220212
const originalEnv = process.env.UI5_DATA_DIR;
221213

222214
try {
223215
process.env.UI5_DATA_DIR = "/custom/ui5/path";
224216

225-
getCacheInfoStub.resolves([]);
217+
frameworkCacheGetCacheInfo.resolves(null);
218+
buildCacheGetCacheInfo.resolves(null);
226219

227220
argv["_"] = ["cache", "clean"];
228221
await cache.handler(argv);
229222

230-
t.is(getCacheInfoStub.callCount, 1, "getCacheInfo called");
231-
t.true(getCacheInfoStub.firstCall.args[0].ui5DataDir.includes("custom/ui5/path"),
223+
t.is(frameworkCacheGetCacheInfo.callCount, 1, "frameworkCache.getCacheInfo called");
224+
t.true(frameworkCacheGetCacheInfo.firstCall.args[0].includes("custom/ui5/path"),
232225
"Uses environment variable path");
233226
} finally {
234227
if (originalEnv) {
@@ -240,19 +233,20 @@ test.serial("ui5 cache clean: uses UI5_DATA_DIR from environment", async (t) =>
240233
});
241234

242235
test.serial("ui5 cache clean: uses config.getUi5DataDir when no env var", async (t) => {
243-
const {cache, argv, getCacheInfoStub, Configuration} = t.context;
236+
const {cache, argv, frameworkCacheGetCacheInfo, buildCacheGetCacheInfo, Configuration} = t.context;
244237
const originalEnv = process.env.UI5_DATA_DIR;
245238

246239
try {
247240
delete process.env.UI5_DATA_DIR;
248241

249242
Configuration.fromFile.resolves(new Configuration({ui5DataDir: "/config/path"}));
250-
getCacheInfoStub.resolves([]);
243+
frameworkCacheGetCacheInfo.resolves(null);
244+
buildCacheGetCacheInfo.resolves(null);
251245

252246
argv["_"] = ["cache", "clean"];
253247
await cache.handler(argv);
254248

255-
t.is(getCacheInfoStub.callCount, 1, "getCacheInfo called");
249+
t.is(frameworkCacheGetCacheInfo.callCount, 1, "frameworkCache.getCacheInfo called");
256250
} finally {
257251
if (originalEnv) {
258252
process.env.UI5_DATA_DIR = originalEnv;

packages/project/lib/build/cache/BuildCacheStorage.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,4 +604,15 @@ export default class BuildCacheStorage {
604604
this.#db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
605605
this.#db.close();
606606
}
607+
608+
/**
609+
* Get the total size of the database file
610+
*
611+
* @returns {number} Database size in bytes
612+
*/
613+
getDatabaseSize() {
614+
const pageCount = this.#db.prepare("PRAGMA page_count").get().page_count;
615+
const pageSize = this.#db.prepare("PRAGMA page_size").get().page_size;
616+
return pageCount * pageSize;
617+
}
607618
}

packages/project/lib/build/cache/CacheManager.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,4 +384,66 @@ export default class CacheManager {
384384
cacheManagerInstances.delete(this.#cacheDir);
385385
}
386386
}
387+
388+
/**
389+
* Get build cache info for the current version.
390+
*
391+
* @static
392+
* @param {string} ui5DataDir Resolved absolute path to UI5 data directory
393+
* @returns {Promise<{path: string, size: number, type: string}|null>} Build cache info or null
394+
*/
395+
static async getCacheInfo(ui5DataDir) {
396+
const buildCacheDir = path.join(ui5DataDir, "buildCache");
397+
const dbDir = path.join(buildCacheDir, CACHE_VERSION);
398+
399+
try {
400+
const storage = new BuildCacheStorage(dbDir);
401+
try {
402+
if (storage.hasRecords()) {
403+
const size = storage.getDatabaseSize();
404+
return {
405+
path: `buildCache/${CACHE_VERSION}`,
406+
size,
407+
type: "database"
408+
};
409+
}
410+
} finally {
411+
storage.close();
412+
}
413+
} catch {
414+
// Skip if database can't be opened
415+
}
416+
return null;
417+
}
418+
419+
/**
420+
* Clean build cache by clearing all records from SQLite database for the current version.
421+
*
422+
* @static
423+
* @param {string} ui5DataDir Resolved absolute path to UI5 data directory
424+
* @returns {Promise<{path: string, type: string, size: number}|null>} Removal result or null
425+
*/
426+
static async cleanCache(ui5DataDir) {
427+
const buildCacheDir = path.join(ui5DataDir, "buildCache");
428+
const dbDir = path.join(buildCacheDir, CACHE_VERSION);
429+
430+
try {
431+
const storage = new BuildCacheStorage(dbDir);
432+
try {
433+
if (storage.hasRecords()) {
434+
const freedSize = storage.clearAllRecords();
435+
return {
436+
path: `buildCache/${CACHE_VERSION}`,
437+
type: "buildCache",
438+
size: freedSize
439+
};
440+
}
441+
} finally {
442+
storage.close();
443+
}
444+
} catch {
445+
// Skip if database can't be cleared
446+
}
447+
return null;
448+
}
387449
}

0 commit comments

Comments
 (0)