From 9a2a2e67155bcb9308aa8e59c500347a79b010af Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Fri, 17 Jul 2026 09:32:16 +0300 Subject: [PATCH 01/52] feat(cli): Add ui5 cache clean command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `ui5 cache clean` to remove framework packages and build cache data. Displays what will be removed with library/version stats, asks for confirmation (skip with --yes), and handles orphaned staging dirs from previously interrupted cleans. No process-coordination locks — that will be added separately. --- packages/cli/lib/cli/commands/cache.js | 292 ++++++++++++++++++ packages/cli/package.json | 3 +- .../lib/build/cache/BuildCacheStorage.js | 51 +++ packages/project/lib/ui5Framework/cache.js | 200 ++++++++++++ packages/project/package.json | 2 + 5 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 packages/cli/lib/cli/commands/cache.js create mode 100644 packages/project/lib/ui5Framework/cache.js diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js new file mode 100644 index 00000000000..052871bcb41 --- /dev/null +++ b/packages/cli/lib/cli/commands/cache.js @@ -0,0 +1,292 @@ +import chalk from "chalk"; +import path from "node:path"; +import process from "node:process"; +import baseMiddleware from "../middlewares/base.js"; +import {resolveUi5DataDir} from "@ui5/project/utils/dataDir"; +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 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", { + 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 confirmation") + .example("$0 cache clean --yes", + "Remove all cached UI5 data without confirmation (e.g. in CI scenarios)") + .example("UI5_DATA_DIR=/custom/path $0 cache clean", + "Remove cached data from a non-default UI5 data directory") + .epilogue( + "The cache is stored in the UI5 data directory (default: ~/.ui5).\n" + + "Override the location with the UI5_DATA_DIR environment variable or\n" + + "the 'ui5 config set ui5DataDir' configuration option (see 'ui5 config --help').\n\n" + + "The following cache types are removed:\n" + + " UI5 framework packages: Downloaded UI5 library files " + + "(~/.ui5/framework/)\n" + + " Build cache (DB): Build data " + + "(~/.ui5/buildCache/)\n" + + " Orphaned framework data: Incomplete directories from previously interrupted cleanups\n" + + " (~/.ui5/.framework_to_delete_*/)" + ); + }, + 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 framework cache stats as a human-readable detail string. + * E.g. "1,189 versions of 155 libraries" or "1 version of 1 library". + * + * @param {number} libraryCount + * @param {number} versionCount + * @returns {string} + */ +function formatFrameworkStats(libraryCount, versionCount) { + const v = `${versionCount.toLocaleString("en-US")} ${versionCount === 1 ? "version" : "versions"}`; + const l = `${libraryCount.toLocaleString("en-US")} ${libraryCount === 1 ? "library" : "libraries"}`; + return `${v} of ${l}`; +} + +/** + * Pad a label to the shared column width. + * + * @param {string} label + * @returns {string} + */ +function padLabel(label) { + return label.padEnd(LABEL_WIDTH); +} + +/** + * Display information about the cached data that will be removed, + * including the absolute paths and details about the framework and build caches, + * and any orphaned staging directories from previously interrupted clean operations. + * + * @param {object} data + * @param {object|null} data.frameworkInfo + * @param {object|null} data.buildInfo + * @param {string|null} data.frameworkAbsPath + * @param {string|null} data.buildAbsPath + * @param {number} data.buildPreSize + * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfo + */ +async function displayCacheInfo({ + frameworkInfo, + buildInfo, + frameworkAbsPath, + buildAbsPath, + buildPreSize, + orphanedInfo, +}) { + process.stderr.write(chalk.bold("\nThe following cached data will be removed:\n\n")); + if (frameworkInfo) { + const detail = formatFrameworkStats(frameworkInfo.libraryCount, frameworkInfo.versionCount); + 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` + ); + } + if (orphanedInfo && orphanedInfo.length > 0) { + process.stderr.write( + ` ${chalk.yellow("•")} ${chalk.bold("Orphaned framework data")}` + + ` (incomplete previous clean — ` + + `${orphanedInfo.length} director${orphanedInfo.length === 1 ? "y" : "ies"})\n` + ); + for (const orphan of orphanedInfo) { + const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); + process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); + } + } + process.stderr.write("\n"); +} + +/** + * Display the result of the cache cleanup operation, + * including which caches were removed and their details, + * and any orphaned staging directories that were also cleaned up. + * + * @param {object} data + * @param {object|null} data.frameworkResult + * @param {object|null} data.buildResult + * @param {string|null} data.frameworkAbsPath + * @param {string|null} data.buildAbsPath + * @param {number} data.buildPreSize + * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfoWithAbsPaths + */ +async function displayCleanupResult({ + frameworkResult, + buildResult, + frameworkAbsPath, + buildAbsPath, + buildPreSize, + orphanedInfoWithAbsPaths, +}) { + process.stderr.write("\n"); + if (frameworkResult && frameworkAbsPath) { + const detail = formatFrameworkStats( + frameworkResult.libraryCount, + frameworkResult.versionCount, + ); + process.stderr.write( + `${chalk.green("✓")} Removed ${chalk.bold(LABEL_FRAMEWORK)}` + + ` (${frameworkAbsPath} · ${detail})\n`, + ); + } + if (orphanedInfoWithAbsPaths && orphanedInfoWithAbsPaths.length > 0) { + process.stderr.write( + `${chalk.green("✓")} Removed ${chalk.bold("Orphaned framework data")}` + + ` (${orphanedInfoWithAbsPaths.length}` + + ` director${orphanedInfoWithAbsPaths.length === 1 ? "y" : "ies"})\n` + ); + for (const orphan of orphanedInfoWithAbsPaths) { + const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); + process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); + } + } + if (buildResult) { + 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 (orphanedInfoWithAbsPaths && orphanedInfoWithAbsPaths.length > 0) { + cleaned.push("Orphaned framework data"); + } + if (buildResult) { + cleaned.push(LABEL_BUILD); + } + process.stderr.write( + `\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`, + ); +} + +/** + * Prompt the user for confirmation before proceeding with cache cleanup. + * + * @param {Yargs.Arguments} argv + * @returns {Promise} Confirmation result + */ +async function getConfirmation(argv) { + if (argv.yes) { + return true; + } + const {default: yesno} = await import("yesno"); + return yesno({ + question: "Do you want to continue? (y/N)", + defaultValue: false + }); +} + +async function handleCache(argv) { + const ui5DataDir = await resolveUi5DataDir(); + + process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); + + const [frameworkInfo, buildInfo, orphanedInfo] = await Promise.all([ + frameworkCache.getCacheInfo(ui5DataDir), + CacheManager.getCacheInfo(ui5DataDir), + frameworkCache.getOrphanedInfo(ui5DataDir), + ]); + + if (!frameworkInfo && !buildInfo && orphanedInfo.length === 0) { + 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; + const buildPreSize = buildInfo?.size ?? 0; + const preCleanOrphanedInfo = orphanedInfo.map( + (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) + ); + + await displayCacheInfo({ + frameworkInfo, + buildInfo, + frameworkAbsPath, + buildAbsPath, + buildPreSize, + orphanedInfo: preCleanOrphanedInfo, + }); + + const confirmed = await getConfirmation(argv); + if (!confirmed) { + process.stderr.write("Cancelled\n"); + return; + } + + const [frameworkResult, buildResult] = await Promise.all([ + frameworkCache.cleanCache(ui5DataDir), + CacheManager.cleanCache(ui5DataDir), + ]); + + const [additionalFrameworkResult] = await Promise.all([ + frameworkCache.cleanAdditional(ui5DataDir), + CacheManager.cleanAdditional(ui5DataDir), + ]); + const orphanedInfoWithAbsPaths = additionalFrameworkResult.map( + (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) + ); + + await displayCleanupResult({ + frameworkResult, + buildResult, + frameworkAbsPath, + buildAbsPath, + buildPreSize, + orphanedInfoWithAbsPaths, + }); +} + +export default cacheCommand; diff --git a/packages/cli/package.json b/packages/cli/package.json index 7620da20f01..0c0982b095a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -64,7 +64,8 @@ "pretty-hrtime": "^1.0.3", "semver": "^7.8.5", "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", diff --git a/packages/project/lib/build/cache/BuildCacheStorage.js b/packages/project/lib/build/cache/BuildCacheStorage.js index fc91a486888..3489afb9253 100644 --- a/packages/project/lib/build/cache/BuildCacheStorage.js +++ b/packages/project/lib/build/cache/BuildCacheStorage.js @@ -511,6 +511,46 @@ export default class BuildCacheStorage { return new Set(rows.map((row) => row.integrity)); } + /** + * Clears all records from all tables and runs VACUUM. + * Returns the number of bytes freed. + * + * @returns {number} Number of bytes freed + */ + clearAllRecords() { + const bytesBefore = this.getDatabaseSize(); + + this.#db.exec("BEGIN"); + this.#db.exec("DELETE FROM content"); + this.#db.exec("DELETE FROM index_cache"); + this.#db.exec("DELETE FROM stage_metadata"); + this.#db.exec("DELETE FROM task_metadata"); + this.#db.exec("DELETE FROM result_metadata"); + this.#db.exec("COMMIT"); + this.#db.exec("VACUUM"); + + const bytesAfter = this.getDatabaseSize(); + + return bytesBefore - bytesAfter; + } + + /** + * Checks if the database has any records in any table. + * + * @returns {boolean} True if there are any records + */ + hasRecords() { + const tables = ["content", "index_cache", "stage_metadata", "task_metadata", "result_metadata"]; + for (const table of tables) { + const {is_populated: isPopulated} = + this.#db.prepare(`SELECT EXISTS(SELECT 1 FROM ${table} LIMIT 1) as is_populated`).get(); + if (isPopulated) { + return true; + } + } + return false; + } + /** * Closes the database connection */ @@ -525,4 +565,15 @@ export default class BuildCacheStorage { this.#db.exec("PRAGMA wal_checkpoint(TRUNCATE)"); this.#db.close(); } + + /** + * Get the total size of the database file + * + * @returns {number} Database size in bytes + */ + getDatabaseSize() { + const pageCount = this.#db.prepare("PRAGMA page_count").get().page_count; + const pageSize = this.#db.prepare("PRAGMA page_size").get().page_size; + return pageCount * pageSize; + } } diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js new file mode 100644 index 00000000000..3019cabbef2 --- /dev/null +++ b/packages/project/lib/ui5Framework/cache.js @@ -0,0 +1,200 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import {getRandomValues} from "node:crypto"; + +const FRAMEWORK_DIR_NAME = "framework"; + +/** + * Prefix used for staging directories created during an atomic framework cache clean. + * The directory is renamed to this prefix + a random hex suffix before deletion so that + * the original path immediately becomes unavailable to concurrent processes. + */ +const STAGING_DIR_PREFIX = ".framework_to_delete_"; + +/** + * Count unique libraries and versions in the packages/ subdirectory. + * + * Library names are deduplicated globally: sap.m under @openui5 and @sapui5 counts + * as one library. + * + * @param {string} frameworkDir Absolute path to the framework directory + * @returns {Promise<{libraries: number, versions: number}|null>} + * Null if the directory does not exist or contains no installed libraries. + */ +async function getPackageStats(frameworkDir) { + try { + await fs.access(frameworkDir); + } catch { + return null; + } + + const packagesDir = path.join(frameworkDir, "packages"); + let projectDirs; + try { + projectDirs = await fs.readdir(packagesDir, {withFileTypes: true}); + } catch { + return null; + } + + const extractSubDir = (dirList) => { + return dirList.filter((e) => e.isDirectory()) + .map((currentDir) => { + try { + return fs.readdir(path.join(currentDir.parentPath, currentDir.name), {withFileTypes: true}); + } catch { + return; + } + }); + }; + + const libDirs = (await Promise.all(extractSubDir(projectDirs))).flat(); + const versionDirs = (await Promise.all(extractSubDir(libDirs))).flat(); + + const librarySet = new Set(libDirs.map((e) => e.name)); + const versionSet = new Set(versionDirs.map((e) => e.name)); + + return librarySet.size > 0 ? + {libraries: librarySet.size, versions: versionSet.size} : + null; +} + +/** + * Get framework cache info. + * + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise<{path: string, libraryCount: number, versionCount: number}|null>} + * Framework cache info, or null if no packages are installed. + */ +export async function getCacheInfo(ui5DataDir) { + const frameworkDir = path.join(ui5DataDir, FRAMEWORK_DIR_NAME); + const stats = await getPackageStats(frameworkDir); + if (!stats) { + return null; + } + return { + path: FRAMEWORK_DIR_NAME, + libraryCount: stats.libraries, + versionCount: stats.versions, + }; +} + +/** + * Scans ui5DataDir for orphaned staging directories left behind by previously + * interrupted clean operations (i.e. process killed after rename but before deletion). + * Returns stats per orphan without deleting anything. + * + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise>} + */ +export async function getOrphanedInfo(ui5DataDir) { + let entries; + try { + entries = await fs.readdir(ui5DataDir, {withFileTypes: true}); + } catch { + return []; + } + + const orphans = entries.filter( + (e) => e.isDirectory() && e.name.startsWith(STAGING_DIR_PREFIX) + ); + + if (orphans.length === 0) { + return []; + } + + const results = await Promise.all(orphans.map(async (orphan) => { + const orphanDir = path.join(ui5DataDir, orphan.name); + const stats = await getPackageStats(orphanDir); + if (!stats) { + return null; + } + return { + path: orphan.name, + libraryCount: stats.libraries, + versionCount: stats.versions, + }; + })); + + return results.filter(Boolean); +} + +/** + * Scans ui5DataDir for orphaned staging directories left behind by previously + * interrupted clean operations (i.e. process killed after rename but before deletion). + * + * Returns an array of result objects — one per orphaned directory found — each + * containing the path, library count and version count so the caller can include + * them in the cleanup summary. + * + * Deletion failures are swallowed per entry so one stuck directory does not prevent + * the others from being removed. + * + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise>} + */ +export async function cleanAdditional(ui5DataDir) { + const orphans = await getOrphanedInfo(ui5DataDir); + + for (const orphan of orphans) { + const orphanDir = path.join(ui5DataDir, orphan.path); + try { + await fs.rm(orphanDir, {recursive: true, force: true}); + } catch { + // Ignore deletion errors + } + } + + return orphans; +} + +/** + * Clean the framework cache directory. + * + * Uses an atomic rename to make the framework directory disappear in a single + * filesystem operation. The caller is responsible for holding the cleanup lock + * for the full duration of this call: + * + * 1. Clear cacache's in-process memoization (no path needed — global operation). + * 2. Atomically rename framework/ to a hidden staging dir. + * After this point the original path no longer exists: concurrent builds will + * see it as absent and create a fresh framework/ directory. + * 3. Delete the staging dir recursively. Its contents are now fully private + * to this operation. + * + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise<{path: string, libraryCount: number, versionCount: number}|null>} + * Removal result, or null if no framework packages were installed. + */ +export async function cleanCache(ui5DataDir) { + const frameworkDir = path.join(ui5DataDir, FRAMEWORK_DIR_NAME); + const stats = await getPackageStats(frameworkDir); + if (!stats) { + return null; + } + + // Clear cacache's in-process memoization before the rename. + // clearMemoized() operates globally (no path argument) and is synchronous. + try { + const {clearMemoized} = await import("cacache"); + clearMemoized(); + } catch { + // cacache not available — no-op + } + + // Atomically rename framework/ to a staging directory. + // fs.rename is a single syscall and completes in microseconds. + // After this line the original path no longer exists. + const stagingDir = path.join( + ui5DataDir, + `${STAGING_DIR_PREFIX}${Buffer.from(getRandomValues(new Uint8Array(2))).toString("hex")}` + ); + await fs.rename(frameworkDir, stagingDir); + + await fs.rm(stagingDir, {recursive: true, force: true}); + + return { + path: FRAMEWORK_DIR_NAME, + libraryCount: stats.libraries, + versionCount: stats.versions, + }; +} diff --git a/packages/project/package.json b/packages/project/package.json index 17b2e8957c4..89a32ceeb2c 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -20,12 +20,14 @@ "exports": { "./config/Configuration": "./lib/config/Configuration.js", "./build/cache/Cache": "./lib/build/cache/Cache.js", + "./build/cache/CacheManager": "./lib/build/cache/CacheManager.js", "./specifications/Specification": "./lib/specifications/Specification.js", "./specifications/SpecificationVersion": "./lib/specifications/SpecificationVersion.js", "./ui5Framework/Sapui5MavenSnapshotResolver": "./lib/ui5Framework/Sapui5MavenSnapshotResolver.js", "./ui5Framework/Openui5Resolver": "./lib/ui5Framework/Openui5Resolver.js", "./ui5Framework/Sapui5Resolver": "./lib/ui5Framework/Sapui5Resolver.js", "./ui5Framework/maven/SnapshotCache": "./lib/ui5Framework/maven/SnapshotCache.js", + "./ui5Framework/cache": "./lib/ui5Framework/cache.js", "./validation/validator": "./lib/validation/validator.js", "./validation/ValidationError": "./lib/validation/ValidationError.js", "./graph/ProjectGraph": "./lib/graph/ProjectGraph.js", From 09370e9ce680e284f750863ea0c6dd775b60d66c Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Mon, 20 Jul 2026 10:13:27 +0300 Subject: [PATCH 02/52] refactor: Remove stale exports --- packages/project/test/lib/package-exports.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/project/test/lib/package-exports.js b/packages/project/test/lib/package-exports.js index 684e8634a84..42023fde407 100644 --- a/packages/project/test/lib/package-exports.js +++ b/packages/project/test/lib/package-exports.js @@ -13,19 +13,21 @@ test("export of package.json", (t) => { // Check number of definied exports test("check number of exports", (t) => { const packageJson = require("@ui5/project/package.json"); - t.is(Object.keys(packageJson.exports).length, 14); + t.is(Object.keys(packageJson.exports).length, 15); }); // Public API contract (exported modules) [ "config/Configuration", "build/cache/Cache", + "build/cache/CacheManager", "specifications/Specification", "specifications/SpecificationVersion", "ui5Framework/Openui5Resolver", "ui5Framework/Sapui5Resolver", "ui5Framework/Sapui5MavenSnapshotResolver", "ui5Framework/maven/SnapshotCache", + "ui5Framework/cache", "validation/validator", "validation/ValidationError", "graph/ProjectGraph", From 53fb381fd5a938cde18576ca1ae0fdbf12336d35 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Mon, 20 Jul 2026 16:03:45 +0300 Subject: [PATCH 03/52] test: Add test cases --- packages/cli/test/lib/cli/commands/cache.js | 395 ++++++++++++++++++ .../project/test/lib/ui5framework/cache.js | 231 ++++++++++ 2 files changed, 626 insertions(+) create mode 100644 packages/cli/test/lib/cli/commands/cache.js create mode 100644 packages/project/test/lib/ui5framework/cache.js diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js new file mode 100644 index 00000000000..801953204d1 --- /dev/null +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -0,0 +1,395 @@ +import test from "ava"; +import path from "node:path"; +import sinon from "sinon"; +import esmock from "esmock"; + +function getDefaultArgv() { + return { + "_": ["cache", "clean"], + "loglevel": "info", + "log-level": "info", + "logLevel": "info", + "perf": false, + "silent": false, + "$0": "ui5" + }; +} + +// Stable absolute path used as the resolved ui5DataDir in most tests +const TEST_UI5_DATA_DIR = path.resolve("test-ui5-home"); + +// Typical framework stub result shape: { path, libraryCount, versionCount } +const FRAMEWORK_STUB = {path: "framework", libraryCount: 18, versionCount: 5}; + +test.beforeEach(async (t) => { + t.context.argv = getDefaultArgv(); + t.context.stderrWriteStub = sinon.stub(process.stderr, "write"); + + // Prevent real env var from leaking into tests + delete process.env.UI5_DATA_DIR; + + t.context.resolveUi5DataDirStub = sinon.stub().resolves(TEST_UI5_DATA_DIR); + + t.context.frameworkCacheGetCacheInfo = sinon.stub(); + t.context.frameworkCacheCleanCache = sinon.stub(); + t.context.frameworkCacheCleanAdditional = sinon.stub().resolves([]); + t.context.frameworkCacheGetOrphanedInfo = sinon.stub().resolves([]); + t.context.buildCacheGetCacheInfo = sinon.stub(); + t.context.buildCacheCleanCache = sinon.stub(); + + t.context.yesnoStub = sinon.stub(); + + t.context.cache = await esmock.p("../../../../lib/cli/commands/cache.js", { + "@ui5/project/utils/dataDir": { + resolveUi5DataDir: t.context.resolveUi5DataDirStub, + }, + "@ui5/project/ui5Framework/cache": { + getCacheInfo: t.context.frameworkCacheGetCacheInfo, + cleanCache: t.context.frameworkCacheCleanCache, + cleanAdditional: t.context.frameworkCacheCleanAdditional, + getOrphanedInfo: t.context.frameworkCacheGetOrphanedInfo, + }, + "@ui5/project/build/cache/CacheManager": { + default: class { + static getCacheInfo = t.context.buildCacheGetCacheInfo; + static cleanCache = t.context.buildCacheCleanCache; + static cleanAdditional = sinon.stub().resolves([]); + } + }, + "yesno": { + default: t.context.yesnoStub, + }, + }); +}); + +test.afterEach.always((t) => { + sinon.restore(); + esmock.purge(t.context.cache); + process.exitCode = undefined; + delete process.env.UI5_DATA_DIR; +}); + +// ─── Command structure ────────────────────────────────────────────────────── + +test("Command builder", async (t) => { + const cacheModule = await import("../../../../lib/cli/commands/cache.js"); + const cliStub = { + demandCommand: sinon.stub().returnsThis(), + command: sinon.stub().returnsThis(), + example: sinon.stub().returnsThis(), + }; + const result = cacheModule.default.builder(cliStub); + t.is(result, cliStub, "Builder returns cli instance"); + t.is(cliStub.demandCommand.callCount, 1, "demandCommand called once"); + t.is(cliStub.command.callCount, 1, "command called once"); + t.is(cliStub.example.callCount, 0, "example not called on parent command"); +}); + +test.serial("Command definition is correct", (t) => { + t.is(t.context.cache.command, "cache"); + t.is(t.context.cache.describe, + "Manage the UI5 CLI cache (downloaded framework packages and build data)"); + t.is(typeof t.context.cache.builder, "function"); + t.is(typeof t.context.cache.handler, "function"); +}); + +// ─── ui5DataDir resolution ────────────────────────────────────────────────── + +test.serial("ui5 cache clean: uses resolved path from resolveUi5DataDir", async (t) => { + const {cache, argv, frameworkCacheGetCacheInfo, buildCacheGetCacheInfo, + stderrWriteStub, resolveUi5DataDirStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves(null); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + t.is(resolveUi5DataDirStub.callCount, 1, "resolveUi5DataDir called exactly once"); + t.deepEqual(resolveUi5DataDirStub.getCall(0).args, [], + "resolveUi5DataDir called with no arguments"); + + t.is(frameworkCacheGetCacheInfo.firstCall.args[0], TEST_UI5_DATA_DIR, + "getCacheInfo receives the path returned by resolveUi5DataDir"); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes(TEST_UI5_DATA_DIR), "Resolved ui5DataDir shown in checking line"); +}); + +// ─── Basic flow ───────────────────────────────────────────────────────────── + +test.serial("ui5 cache clean: nothing to clean", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo} = t.context; + + frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves(null); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("Checking cache at"), "Prints checking line"); + t.true(allOutput.includes("Nothing to clean"), "Prints nothing to clean"); + t.is(frameworkCacheCleanCache.callCount, 0, "frameworkCache.cleanCache not called"); + t.is(buildCacheCleanCache.callCount, 0, "buildCache.cleanCache not called"); +}); + +test.serial("ui5 cache clean: removes both entries and reports", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 8 * 1024 * 1024}); + + yesnoStub.resolves(true); + + frameworkCacheCleanCache.resolves(FRAMEWORK_STUB); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 7 * 1024 * 1024}); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + t.is(yesnoStub.callCount, 1, "Should ask for confirmation"); + t.is(frameworkCacheCleanCache.callCount, 1, "frameworkCache.cleanCache called once"); + t.is(buildCacheCleanCache.callCount, 1, "buildCache.cleanCache called once"); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("Checking cache at"), "Prints checking line"); + t.true(allOutput.includes(TEST_UI5_DATA_DIR), "Shows resolved ui5DataDir"); + t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "framework")), "Shows absolute framework path"); + t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "buildCache/v0_7")), "Shows absolute build path"); + t.true(allOutput.includes("5 versions of 18 libraries"), "Shows library stats format"); + t.true(allOutput.includes("8.0 MB"), "Shows pre-clean build cache size"); + t.false(allOutput.includes("7.0 MB"), "Does not show VACUUM-freed size"); + t.true(allOutput.includes("Cleaned UI5 Framework packages and Build cache (DB)"), + "Shows success summary"); +}); + +test.serial("ui5 cache clean: user cancels", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves(null); + yesnoStub.resolves(false); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + t.is(yesnoStub.callCount, 1, "Should ask for confirmation"); + t.is(frameworkCacheCleanCache.callCount, 0, "cleanCache not called when user cancels"); + t.is(buildCacheCleanCache.callCount, 0, "buildCache.cleanCache not called when user cancels"); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("Cancelled"), "Shows cancelled message"); + t.false(allOutput.includes("Success"), "Does not show success message"); +}); + +test.serial("ui5 cache clean: framework only — formats library stats correctly", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheGetCacheInfo, yesnoStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves(null); + yesnoStub.resolves(true); + frameworkCacheCleanCache.resolves(FRAMEWORK_STUB); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + let allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("5 versions of 18 libraries"), "Shows plural format"); + t.false(allOutput.includes("Build cache (DB)"), "Does not mention build cache"); + + // Singular + stderrWriteStub.resetHistory(); + const singleStub = {path: "framework", libraryCount: 1, versionCount: 1}; + frameworkCacheGetCacheInfo.resetBehavior(); + frameworkCacheCleanCache.resetBehavior(); + frameworkCacheGetCacheInfo.resolves(singleStub); + frameworkCacheCleanCache.resolves(singleStub); + + argv["yes"] = true; + await cache.handler(argv); + + allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("1 version of 1 library"), "Uses singular 'version' and 'library'"); +}); + +test.serial("ui5 cache clean: thousands separator in library stats", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheGetCacheInfo, yesnoStub} = t.context; + + const largeStub = {path: "framework", libraryCount: 155, versionCount: 1189}; + frameworkCacheGetCacheInfo.resolves(largeStub); + buildCacheGetCacheInfo.resolves(null); + yesnoStub.resolves(true); + frameworkCacheCleanCache.resolves(largeStub); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("1,189 versions of 155 libraries"), + "Shows thousands separator for large counts"); +}); + +test.serial("ui5 cache clean: build only", async (t) => { + const {cache, argv, stderrWriteStub, buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 50 * 1024}); + yesnoStub.resolves(true); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 50 * 1024}); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.false(allOutput.includes("UI5 Framework packages"), "Does not mention framework"); + t.true(allOutput.includes("50.0 KB"), "Shows build cache size"); + t.true(allOutput.includes("Cleaned Build cache (DB)"), "Success mentions build cache only"); +}); + +test.serial("ui5 cache clean: formats byte sizes correctly (< 1 KB)", async (t) => { + const {cache, argv, stderrWriteStub, buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 500}); + yesnoStub.resolves(true); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 500}); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("500 B"), "Shows bytes format for size < 1 KB"); +}); + +test.serial("ui5 cache clean: formats KB sizes correctly", async (t) => { + const {cache, argv, stderrWriteStub, buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 50 * 1024}); + yesnoStub.resolves(true); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 50 * 1024}); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("50.0 KB"), "Shows KB format"); +}); + +test.serial("ui5 cache clean: formats GB sizes correctly", async (t) => { + const {cache, argv, stderrWriteStub, buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves({path: "large", size: 2.5 * 1024 * 1024 * 1024}); + yesnoStub.resolves(true); + buildCacheCleanCache.resolves({path: "large", size: 2.5 * 1024 * 1024 * 1024}); + + argv["_"] = ["cache", "clean"]; + argv["yes"] = true; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("2.5 GB"), "Shows GB format"); +}); + +test.serial("ui5 cache clean --yes: skips confirmation prompt", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 5 * 1024 * 1024}); + frameworkCacheCleanCache.resolves(FRAMEWORK_STUB); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 5 * 1024 * 1024}); + + argv["_"] = ["cache", "clean"]; + argv["yes"] = true; + await cache.handler(argv); + + t.is(yesnoStub.callCount, 0, "Should not ask for confirmation with --yes"); + t.is(frameworkCacheCleanCache.callCount, 1, "frameworkCache.cleanCache called"); + t.is(buildCacheCleanCache.callCount, 1, "buildCache.cleanCache called"); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("Success"), "Shows success message"); +}); + +test.serial("ui5 cache clean: shows orphaned framework data in pre-confirmation summary", async (t) => { + const {cache, argv, stderrWriteStub, yesnoStub, + frameworkCacheCleanCache, frameworkCacheGetOrphanedInfo} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + t.context.buildCacheGetCacheInfo.resolves(null); + frameworkCacheGetOrphanedInfo.resolves([ + {path: ".framework_to_delete_abcd", libraryCount: 5, versionCount: 2}, + ]); + frameworkCacheCleanCache.resolves(null); + + yesnoStub.resolves(true); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("Orphaned framework data"), "Shows orphaned section in pre-confirm summary"); + t.true(allOutput.includes("incomplete previous clean"), "Shows orphaned context message"); + t.true(allOutput.includes("1 directory"), "Shows singular 'directory' for one orphan"); + t.true(allOutput.includes(".framework_to_delete_abcd"), "Shows orphaned dir path"); + t.true(allOutput.includes("2 versions of 5 libraries"), "Shows orphaned dir stats"); +}); + +test.serial("ui5 cache clean: shows orphaned framework data in post-clean summary", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheGetOrphanedInfo, + frameworkCacheCleanCache, frameworkCacheCleanAdditional} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves({path: "framework", libraryCount: 3, versionCount: 1}); + t.context.buildCacheGetCacheInfo.resolves(null); + frameworkCacheGetOrphanedInfo.resolves([ + {path: ".framework_to_delete_ab12", libraryCount: 3, versionCount: 1}, + {path: ".framework_to_delete_cd34", libraryCount: 3, versionCount: 1}, + ]); + frameworkCacheCleanCache.resolves({path: "framework", libraryCount: 3, versionCount: 1}); + frameworkCacheCleanAdditional.resolves([ + {path: ".framework_to_delete_ab12", libraryCount: 3, versionCount: 1}, + {path: ".framework_to_delete_cd34", libraryCount: 3, versionCount: 1}, + ]); + + argv["_"] = ["cache", "clean"]; + argv["yes"] = true; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("Removed Orphaned framework data"), "Shows orphaned section in result"); + t.true(allOutput.includes("2 directories"), "Shows plural 'directories' for multiple orphans"); + t.true(allOutput.includes(".framework_to_delete_ab12"), "Shows first orphaned dir path"); + t.true(allOutput.includes(".framework_to_delete_cd34"), "Shows second orphaned dir path"); +}); + +test.serial("ui5 cache clean: shows orphaned-only success summary when no active framework", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheGetOrphanedInfo, + frameworkCacheCleanCache, frameworkCacheCleanAdditional} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + t.context.buildCacheGetCacheInfo.resolves(null); + frameworkCacheGetOrphanedInfo.resolves([ + {path: ".framework_to_delete_zz99", libraryCount: 10, versionCount: 3}, + ]); + frameworkCacheCleanCache.resolves(null); + frameworkCacheCleanAdditional.resolves([ + {path: ".framework_to_delete_zz99", libraryCount: 10, versionCount: 3}, + ]); + + argv["_"] = ["cache", "clean"]; + argv["yes"] = true; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("Orphaned framework data"), "Shows orphaned section"); + t.true(allOutput.includes("Cleaned Orphaned framework data"), "Success summary mentions orphaned data"); + t.false(allOutput.includes("UI5 Framework packages"), "Does not mention main framework when absent"); +}); diff --git a/packages/project/test/lib/ui5framework/cache.js b/packages/project/test/lib/ui5framework/cache.js new file mode 100644 index 00000000000..326835ad710 --- /dev/null +++ b/packages/project/test/lib/ui5framework/cache.js @@ -0,0 +1,231 @@ +import test from "ava"; +import path from "node:path"; +import fs from "node:fs/promises"; +import sinon from "sinon"; +import esmock from "esmock"; +import {getCacheInfo, cleanCache, cleanAdditional} from "../../../lib/ui5Framework/cache.js"; + +const TEST_DIR = path.join(import.meta.dirname, "..", "..", "tmp", "ui5framework-cache"); + +test.beforeEach(async (t) => { + const testDir = path.join(TEST_DIR, `${Date.now()}-${Math.random().toString(36).slice(2)}`); + await fs.mkdir(testDir, {recursive: true}); + t.context.testDir = testDir; +}); + +test.afterEach.always(async (t) => { + await fs.rm(t.context.testDir, {recursive: true, force: true}); + sinon.restore(); +}); + + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +async function mkPackage(testDir, project, library, version) { + const dir = path.join(testDir, "framework", "packages", project, library, version); + await fs.mkdir(dir, {recursive: true}); + await fs.writeFile(path.join(dir, "package.json"), JSON.stringify({name: `${project}/${library}`, version})); +} + +async function mkPackageIn(baseDir, project, library, version) { + const dir = path.join(baseDir, "packages", project, library, version); + await fs.mkdir(dir, {recursive: true}); + await fs.writeFile(path.join(dir, "package.json"), JSON.stringify({name: `${project}/${library}`, version})); +} + +// ─── getCacheInfo ───────────────────────────────────────────────────────────── + +test("getCacheInfo: non-existent framework directory returns null", async (t) => { + const result = await getCacheInfo(t.context.testDir); + t.is(result, null); +}); + +test("getCacheInfo: framework dir exists but no packages/ subdir returns null", async (t) => { + await fs.mkdir(path.join(t.context.testDir, "framework", "cacache"), {recursive: true}); + const result = await getCacheInfo(t.context.testDir); + t.is(result, null); +}); + +test("getCacheInfo: packages/ exists but is empty returns null", async (t) => { + await fs.mkdir(path.join(t.context.testDir, "framework", "packages"), {recursive: true}); + const result = await getCacheInfo(t.context.testDir); + t.is(result, null); +}); + +test("getCacheInfo: counts libraries and versions", async (t) => { + // 2 unique library names across 2 scopes, 3 unique versions + await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.120.0"); + await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.148.0"); + await mkPackage(t.context.testDir, "@sapui5", "sap.m", "1.38.1"); + + const result = await getCacheInfo(t.context.testDir); + t.truthy(result); + t.is(result.path, "framework"); + t.is(result.libraryCount, 2); // sap.m counted once (deduplicated across scopes) + t.is(result.versionCount, 3); // 1.120.0, 1.148.0, 1.38.1 +}); + +test("getCacheInfo: deduplicates versions across libraries", async (t) => { + // Both libraries have 1.120.0 — version should count once + await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.120.0"); + + const result = await getCacheInfo(t.context.testDir); + t.truthy(result); + t.is(result.libraryCount, 2); + t.is(result.versionCount, 1); // 1.120.0 deduplicated +}); + +test("getCacheInfo: single library and version", async (t) => { + await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + + const result = await getCacheInfo(t.context.testDir); + t.truthy(result); + t.is(result.libraryCount, 1); + t.is(result.versionCount, 1); +}); + +// ─── cleanCache ─────────────────────────────────────────────────────────────── + +test("cleanCache: returns null for non-existent framework directory", async (t) => { + const result = await cleanCache(t.context.testDir); + t.is(result, null); +}); + +test("cleanCache: returns null when packages/ has no installed libraries", async (t) => { + await fs.mkdir(path.join(t.context.testDir, "framework", "packages"), {recursive: true}); + const result = await cleanCache(t.context.testDir); + t.is(result, null); +}); + +test("cleanCache: renames then removes framework directory and returns stats", async (t) => { + await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.120.0"); + await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.148.0"); + + const frameworkDir = path.join(t.context.testDir, "framework"); + const result = await cleanCache(t.context.testDir); + + t.truthy(result); + t.is(result.path, "framework"); + t.is(result.libraryCount, 2); + t.is(result.versionCount, 2); // 1.120.0, 1.148.0 + + // framework/ is gone — getCacheInfo returns null + t.is(await getCacheInfo(t.context.testDir), null); + + // No staging dirs remain after a successful clean + const entries = await fs.readdir(t.context.testDir); + t.false(entries.some((e) => e.startsWith(".framework_to_delete_")), + "no staging dirs remain after successful clean"); + + // packages/ is gone + await t.throwsAsync(fs.access(path.join(frameworkDir, "packages"))); +}); + +test("cleanCache: removes directory with multiple scopes", async (t) => { + await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + await mkPackage(t.context.testDir, "@sapui5", "sap.m", "1.38.1"); + + const result = await cleanCache(t.context.testDir); + + t.truthy(result); + t.is(result.libraryCount, 1); // sap.m deduplicated + t.is(result.versionCount, 2); + + t.is(await getCacheInfo(t.context.testDir), null); +}); + +test("cleanCache: does not include orphaned field in result", async (t) => { + await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + + const result = await cleanCache(t.context.testDir); + + t.truthy(result); + t.false(Object.prototype.hasOwnProperty.call(result, "orphaned"), + "cleanCache result does not include orphaned — use cleanAdditional for that"); +}); + +test("cleanCache: does not remove orphaned staging dirs — that is cleanAdditional's job", async (t) => { + await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + + const orphanDir = path.join(t.context.testDir, ".framework_to_delete_abcd"); + await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.100.0"); + + await cleanCache(t.context.testDir); + + // Orphan is still present after cleanCache — cleanAdditional handles it + await t.notThrowsAsync(fs.access(orphanDir), "orphaned dir is not touched by cleanCache"); +}); + +// ─── cleanAdditional ────────────────────────────────────────────────────────── + +test("cleanAdditional: returns empty array when no orphaned staging dirs exist", async (t) => { + const result = await cleanAdditional(t.context.testDir); + t.deepEqual(result, []); +}); + +test("cleanAdditional: detects and removes orphaned staging dirs, reports them", async (t) => { + const orphanDir = path.join(t.context.testDir, ".framework_to_delete_abcd"); + await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.100.0"); + await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.110.0"); + + const result = await cleanAdditional(t.context.testDir); + + t.is(result.length, 1, "one orphaned dir reported"); + const orphanResult = result[0]; + t.true(orphanResult.path.startsWith(".framework_to_delete_"), "orphan path has staging prefix"); + t.is(orphanResult.libraryCount, 1); + t.is(orphanResult.versionCount, 2); + + await t.throwsAsync(fs.access(orphanDir), {code: "ENOENT"}, "orphaned staging dir removed"); +}); + +test("cleanAdditional: removes multiple orphaned staging dirs and reports each", async (t) => { + const orphan1 = path.join(t.context.testDir, ".framework_to_delete_1111"); + const orphan2 = path.join(t.context.testDir, ".framework_to_delete_2222"); + + await mkPackageIn(orphan1, "@openui5", "sap.m", "1.90.0"); + await mkPackageIn(orphan2, "@openui5", "sap.ui.core", "1.91.0"); + await mkPackageIn(orphan2, "@openui5", "sap.ui.core", "1.92.0"); + + const result = await cleanAdditional(t.context.testDir); + + t.is(result.length, 2, "two orphaned dirs reported"); + + const sorted = [...result].sort((a, b) => a.path.localeCompare(b.path)); + t.is(sorted[0].libraryCount, 1); + t.is(sorted[0].versionCount, 1); + t.is(sorted[1].libraryCount, 1); + t.is(sorted[1].versionCount, 2); + + await t.throwsAsync(fs.access(orphan1), {code: "ENOENT"}); + await t.throwsAsync(fs.access(orphan2), {code: "ENOENT"}); +}); + +test("cleanAdditional: orphaned dir deletion failure is non-fatal", async (t) => { + const orphanDir = path.join(t.context.testDir, ".framework_to_delete_fail"); + await mkPackageIn(orphanDir, "@openui5", "sap.m", "1.80.0"); + + const rmStub = sinon.stub().callsFake(async (p, opts) => { + if (p === orphanDir) { + throw new Error("simulated deletion failure"); + } + return fs.rm(p, opts); + }); + + const {cleanAdditional: cleanAdditionalMocked} = await esmock.p( + "../../../lib/ui5Framework/cache.js", + {"node:fs/promises": {...fs, rm: rmStub}} + ); + + try { + const result = await t.notThrowsAsync(cleanAdditionalMocked(t.context.testDir)); + t.truthy(result, "cleanAdditional completes despite orphan deletion failure"); + } finally { + esmock.purge(cleanAdditionalMocked); + await fs.rm(orphanDir, {recursive: true, force: true}).catch(() => {}); + } +}); + From 6dcd54fa8e7afec05e0c249afc2a8fe286c7241e Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Mon, 20 Jul 2026 16:39:53 +0300 Subject: [PATCH 04/52] fix: Cherry-pick missing methods --- packages/cli/lib/cli/commands/cache.js | 4 +- .../project/lib/build/cache/CacheManager.js | 86 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 052871bcb41..30d4b9c7d8a 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -39,7 +39,7 @@ cacheCommand.builder = function(cli) { "The following cache types are removed:\n" + " UI5 framework packages: Downloaded UI5 library files " + "(~/.ui5/framework/)\n" + - " Build cache (DB): Build data " + + " Build cache (Db): Build data " + "(~/.ui5/buildCache/)\n" + " Orphaned framework data: Incomplete directories from previously interrupted cleanups\n" + " (~/.ui5/.framework_to_delete_*/)" @@ -50,7 +50,7 @@ cacheCommand.builder = function(cli) { }; const LABEL_FRAMEWORK = "UI5 Framework packages"; -const LABEL_BUILD = "Build cache (DB)"; +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); diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index e1e37a6f521..247f7c4bdf8 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -1,6 +1,7 @@ import path from "node:path"; import os from "node:os"; import Configuration from "../../config/Configuration.js"; +import {access} from "node:fs/promises"; import {getLogger} from "@ui5/logger"; import BuildCacheStorage from "./BuildCacheStorage.js"; @@ -337,4 +338,89 @@ export default class CacheManager { cacheManagerInstances.delete(this.#cacheDir); } } + + /** + * Checks if the cache database exists and is accessible for the given directory. + * + * @param {string} dbDir Path to DB + * @returns {Promise} True if the cache database exists and is accessible + */ + static async #isCacheDbAvailable(dbDir) { + const dbPath = path.join(dbDir, "cache.db"); + try { + await access(dbPath); + } catch { + return false; + } + return true; + } + + /** + * Get build cache info for the current version. + * + * @static + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise<{path: string, size: number}|null>} Build cache info or null + */ + static async getCacheInfo(ui5DataDir) { + const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); + const isAvailable = await CacheManager.#isCacheDbAvailable(dbDir); + if (!isAvailable) { + return null; + } + + const storage = new BuildCacheStorage(dbDir); + try { + if (storage.hasRecords()) { + const size = storage.getDatabaseSize(); + return { + path: `buildCache/${CACHE_VERSION}`, + size, + }; + } + } finally { + storage.close(); + } + return null; + } + + /** + * Clean build cache by clearing all records from SQLite database for the current version. + * + * @static + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise<{path: string, size: number}|null>} Removal result or null + */ + static async cleanCache(ui5DataDir) { + const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); + const isAvailable = await CacheManager.#isCacheDbAvailable(dbDir); + if (!isAvailable) { + return null; + } + + const storage = new BuildCacheStorage(dbDir); + try { + if (storage.hasRecords()) { + const freedSize = storage.clearAllRecords(); + return { + path: `buildCache/${CACHE_VERSION}`, + size: freedSize, + }; + } + } finally { + storage.close(); + } + return null; + } + + /** + * Clean additional build cache resources that are safe to remove independently. + * + * @static + * @param {string} _ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise} Always resolves with an empty array + */ + static async cleanAdditional(_ui5DataDir) { + return []; + } } From cdc0a74dca664f12cbb4dc96b084f57a24a064c8 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Mon, 20 Jul 2026 16:40:48 +0300 Subject: [PATCH 05/52] docs: Update documentation --- .../docs/pages/Troubleshooting.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/documentation/docs/pages/Troubleshooting.md b/internal/documentation/docs/pages/Troubleshooting.md index 7fe8c4486a3..7812cff8bc1 100644 --- a/internal/documentation/docs/pages/Troubleshooting.md +++ b/internal/documentation/docs/pages/Troubleshooting.md @@ -26,22 +26,27 @@ Only remove these directories when no UI5 CLI process and no `@ui5/*` API consum #### Resolution -To free disk space, remove the relevant subdirectory. - -To only remove framework downloads: +Use the dedicated cache clean command, which safely removes all cached data: ```sh -rm -rf ~/.ui5/framework/ +ui5 cache clean ``` -To only remove the build cache: +This displays the cache location, the amount of data that gets removed, and asks for confirmation before proceeding. To skip the confirmation prompt (for example in CI environments), use the `--yes` flag: ```sh -rm -rf ~/.ui5/buildCache/ +ui5 cache clean --yes ``` +The command removes the following cached data: +- **UI5 framework packages** — downloaded UI5 library files (`~/.ui5/framework/`) +- **Build cache (Db)** — build data (`~/.ui5/buildCache/`) +- **Orphaned framework data** — incomplete framework directories left over from previously interrupted cleanup operations (`~/.ui5/.framework_to_delete_*/`) + +Any required framework dependencies will be re-downloaded during the next UI5 CLI invocation. + ::: info -If you have configured a custom data directory via `UI5_DATA_DIR` or `ui5 config set ui5DataDir`, replace `~/.ui5/` with that path. See [Changing UI5 CLI's Data Directory](#changing-ui5-cli-s-data-directory). +If you have configured a custom data directory via `UI5_DATA_DIR` or `ui5 config set ui5DataDir`, the `ui5 cache clean` command will clean up that location instead of the default `~/.ui5/`. See [Changing UI5 CLI's Data Directory](#changing-ui5-cli-s-data-directory). ::: ## Environment Variables From 2beb71caee00a4892200aa01998aaafddcfc7e61 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 21 Jul 2026 10:51:07 +0300 Subject: [PATCH 06/52] docs: Update JSdocs to reflect latest changes --- packages/project/lib/build/cache/CacheManager.js | 3 +++ packages/project/lib/ui5Framework/cache.js | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index 247f7c4bdf8..a6ec8785079 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -416,6 +416,9 @@ export default class CacheManager { /** * Clean additional build cache resources that are safe to remove independently. * + * Note: This method is a placeholder for interface compatibility across + * cleanup tasks and currently does not perform any cleanup. + * * @static * @param {string} _ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise} Always resolves with an empty array diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index 3019cabbef2..1e5e8f32308 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -7,7 +7,7 @@ const FRAMEWORK_DIR_NAME = "framework"; /** * Prefix used for staging directories created during an atomic framework cache clean. * The directory is renamed to this prefix + a random hex suffix before deletion so that - * the original path immediately becomes unavailable to concurrent processes. + * the original path is immediately removed and the deletion can proceed outside the rename. */ const STAGING_DIR_PREFIX = ".framework_to_delete_"; @@ -151,8 +151,7 @@ export async function cleanAdditional(ui5DataDir) { * Clean the framework cache directory. * * Uses an atomic rename to make the framework directory disappear in a single - * filesystem operation. The caller is responsible for holding the cleanup lock - * for the full duration of this call: + * filesystem operation: * * 1. Clear cacache's in-process memoization (no path needed — global operation). * 2. Atomically rename framework/ to a hidden staging dir. From 5778b320a3b8324b26a4f01fbd99e53d05e10c66 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 21 Jul 2026 10:51:42 +0300 Subject: [PATCH 07/52] refactor: Rename orphaned dirs to start with underscore --- packages/cli/lib/cli/commands/cache.js | 2 +- packages/cli/test/lib/cli/commands/cache.js | 26 +++++++++---------- packages/project/lib/ui5Framework/cache.js | 2 +- .../project/test/lib/ui5framework/cache.js | 14 +++++----- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 30d4b9c7d8a..76440780059 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -42,7 +42,7 @@ cacheCommand.builder = function(cli) { " Build cache (Db): Build data " + "(~/.ui5/buildCache/)\n" + " Orphaned framework data: Incomplete directories from previously interrupted cleanups\n" + - " (~/.ui5/.framework_to_delete_*/)" + " (~/.ui5/_framework_to_delete_*/)" ); }, middlewares: [baseMiddleware], diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 801953204d1..c0a7e35cdbd 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -162,7 +162,7 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { t.true(allOutput.includes("5 versions of 18 libraries"), "Shows library stats format"); t.true(allOutput.includes("8.0 MB"), "Shows pre-clean build cache size"); t.false(allOutput.includes("7.0 MB"), "Does not show VACUUM-freed size"); - t.true(allOutput.includes("Cleaned UI5 Framework packages and Build cache (DB)"), + t.true(allOutput.includes("Cleaned UI5 Framework packages and Build cache (Db)"), "Shows success summary"); }); @@ -200,7 +200,7 @@ test.serial("ui5 cache clean: framework only — formats library stats correctly let allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("5 versions of 18 libraries"), "Shows plural format"); - t.false(allOutput.includes("Build cache (DB)"), "Does not mention build cache"); + t.false(allOutput.includes("Build cache (Db)"), "Does not mention build cache"); // Singular stderrWriteStub.resetHistory(); @@ -249,7 +249,7 @@ test.serial("ui5 cache clean: build only", async (t) => { const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.false(allOutput.includes("UI5 Framework packages"), "Does not mention framework"); t.true(allOutput.includes("50.0 KB"), "Shows build cache size"); - t.true(allOutput.includes("Cleaned Build cache (DB)"), "Success mentions build cache only"); + t.true(allOutput.includes("Cleaned Build cache (Db)"), "Success mentions build cache only"); }); test.serial("ui5 cache clean: formats byte sizes correctly (< 1 KB)", async (t) => { @@ -326,7 +326,7 @@ test.serial("ui5 cache clean: shows orphaned framework data in pre-confirmation t.context.frameworkCacheGetCacheInfo.resolves(null); t.context.buildCacheGetCacheInfo.resolves(null); frameworkCacheGetOrphanedInfo.resolves([ - {path: ".framework_to_delete_abcd", libraryCount: 5, versionCount: 2}, + {path: "_framework_to_delete_abcd", libraryCount: 5, versionCount: 2}, ]); frameworkCacheCleanCache.resolves(null); @@ -339,7 +339,7 @@ test.serial("ui5 cache clean: shows orphaned framework data in pre-confirmation t.true(allOutput.includes("Orphaned framework data"), "Shows orphaned section in pre-confirm summary"); t.true(allOutput.includes("incomplete previous clean"), "Shows orphaned context message"); t.true(allOutput.includes("1 directory"), "Shows singular 'directory' for one orphan"); - t.true(allOutput.includes(".framework_to_delete_abcd"), "Shows orphaned dir path"); + t.true(allOutput.includes("_framework_to_delete_abcd"), "Shows orphaned dir path"); t.true(allOutput.includes("2 versions of 5 libraries"), "Shows orphaned dir stats"); }); @@ -350,13 +350,13 @@ test.serial("ui5 cache clean: shows orphaned framework data in post-clean summar t.context.frameworkCacheGetCacheInfo.resolves({path: "framework", libraryCount: 3, versionCount: 1}); t.context.buildCacheGetCacheInfo.resolves(null); frameworkCacheGetOrphanedInfo.resolves([ - {path: ".framework_to_delete_ab12", libraryCount: 3, versionCount: 1}, - {path: ".framework_to_delete_cd34", libraryCount: 3, versionCount: 1}, + {path: "_framework_to_delete_ab12", libraryCount: 3, versionCount: 1}, + {path: "_framework_to_delete_cd34", libraryCount: 3, versionCount: 1}, ]); frameworkCacheCleanCache.resolves({path: "framework", libraryCount: 3, versionCount: 1}); frameworkCacheCleanAdditional.resolves([ - {path: ".framework_to_delete_ab12", libraryCount: 3, versionCount: 1}, - {path: ".framework_to_delete_cd34", libraryCount: 3, versionCount: 1}, + {path: "_framework_to_delete_ab12", libraryCount: 3, versionCount: 1}, + {path: "_framework_to_delete_cd34", libraryCount: 3, versionCount: 1}, ]); argv["_"] = ["cache", "clean"]; @@ -366,8 +366,8 @@ test.serial("ui5 cache clean: shows orphaned framework data in post-clean summar const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("Removed Orphaned framework data"), "Shows orphaned section in result"); t.true(allOutput.includes("2 directories"), "Shows plural 'directories' for multiple orphans"); - t.true(allOutput.includes(".framework_to_delete_ab12"), "Shows first orphaned dir path"); - t.true(allOutput.includes(".framework_to_delete_cd34"), "Shows second orphaned dir path"); + t.true(allOutput.includes("_framework_to_delete_ab12"), "Shows first orphaned dir path"); + t.true(allOutput.includes("_framework_to_delete_cd34"), "Shows second orphaned dir path"); }); test.serial("ui5 cache clean: shows orphaned-only success summary when no active framework", async (t) => { @@ -377,11 +377,11 @@ test.serial("ui5 cache clean: shows orphaned-only success summary when no active t.context.frameworkCacheGetCacheInfo.resolves(null); t.context.buildCacheGetCacheInfo.resolves(null); frameworkCacheGetOrphanedInfo.resolves([ - {path: ".framework_to_delete_zz99", libraryCount: 10, versionCount: 3}, + {path: "_framework_to_delete_zz99", libraryCount: 10, versionCount: 3}, ]); frameworkCacheCleanCache.resolves(null); frameworkCacheCleanAdditional.resolves([ - {path: ".framework_to_delete_zz99", libraryCount: 10, versionCount: 3}, + {path: "_framework_to_delete_zz99", libraryCount: 10, versionCount: 3}, ]); argv["_"] = ["cache", "clean"]; diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index 1e5e8f32308..3a840bd39b1 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -9,7 +9,7 @@ const FRAMEWORK_DIR_NAME = "framework"; * The directory is renamed to this prefix + a random hex suffix before deletion so that * the original path is immediately removed and the deletion can proceed outside the rename. */ -const STAGING_DIR_PREFIX = ".framework_to_delete_"; +const STAGING_DIR_PREFIX = "_framework_to_delete_"; /** * Count unique libraries and versions in the packages/ subdirectory. diff --git a/packages/project/test/lib/ui5framework/cache.js b/packages/project/test/lib/ui5framework/cache.js index 326835ad710..a8189b75119 100644 --- a/packages/project/test/lib/ui5framework/cache.js +++ b/packages/project/test/lib/ui5framework/cache.js @@ -117,7 +117,7 @@ test("cleanCache: renames then removes framework directory and returns stats", a // No staging dirs remain after a successful clean const entries = await fs.readdir(t.context.testDir); - t.false(entries.some((e) => e.startsWith(".framework_to_delete_")), + t.false(entries.some((e) => e.startsWith("_framework_to_delete_")), "no staging dirs remain after successful clean"); // packages/ is gone @@ -150,7 +150,7 @@ test("cleanCache: does not include orphaned field in result", async (t) => { test("cleanCache: does not remove orphaned staging dirs — that is cleanAdditional's job", async (t) => { await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); - const orphanDir = path.join(t.context.testDir, ".framework_to_delete_abcd"); + const orphanDir = path.join(t.context.testDir, "_framework_to_delete_abcd"); await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.100.0"); await cleanCache(t.context.testDir); @@ -167,7 +167,7 @@ test("cleanAdditional: returns empty array when no orphaned staging dirs exist", }); test("cleanAdditional: detects and removes orphaned staging dirs, reports them", async (t) => { - const orphanDir = path.join(t.context.testDir, ".framework_to_delete_abcd"); + const orphanDir = path.join(t.context.testDir, "_framework_to_delete_abcd"); await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.100.0"); await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.110.0"); @@ -175,7 +175,7 @@ test("cleanAdditional: detects and removes orphaned staging dirs, reports them", t.is(result.length, 1, "one orphaned dir reported"); const orphanResult = result[0]; - t.true(orphanResult.path.startsWith(".framework_to_delete_"), "orphan path has staging prefix"); + t.true(orphanResult.path.startsWith("_framework_to_delete_"), "orphan path has staging prefix"); t.is(orphanResult.libraryCount, 1); t.is(orphanResult.versionCount, 2); @@ -183,8 +183,8 @@ test("cleanAdditional: detects and removes orphaned staging dirs, reports them", }); test("cleanAdditional: removes multiple orphaned staging dirs and reports each", async (t) => { - const orphan1 = path.join(t.context.testDir, ".framework_to_delete_1111"); - const orphan2 = path.join(t.context.testDir, ".framework_to_delete_2222"); + const orphan1 = path.join(t.context.testDir, "_framework_to_delete_1111"); + const orphan2 = path.join(t.context.testDir, "_framework_to_delete_2222"); await mkPackageIn(orphan1, "@openui5", "sap.m", "1.90.0"); await mkPackageIn(orphan2, "@openui5", "sap.ui.core", "1.91.0"); @@ -205,7 +205,7 @@ test("cleanAdditional: removes multiple orphaned staging dirs and reports each", }); test("cleanAdditional: orphaned dir deletion failure is non-fatal", async (t) => { - const orphanDir = path.join(t.context.testDir, ".framework_to_delete_fail"); + const orphanDir = path.join(t.context.testDir, "_framework_to_delete_fail"); await mkPackageIn(orphanDir, "@openui5", "sap.m", "1.80.0"); const rmStub = sinon.stub().callsFake(async (p, opts) => { From 01825d9a9a9fed2e6ae4eb52de5e26bc6343f201 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 21 Jul 2026 12:24:56 +0300 Subject: [PATCH 08/52] docs: Adjust JSdoc comments --- packages/project/lib/ui5Framework/cache.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index 3a840bd39b1..c0b5adf4559 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -47,8 +47,8 @@ async function getPackageStats(frameworkDir) { }); }; - const libDirs = (await Promise.all(extractSubDir(projectDirs))).flat(); - const versionDirs = (await Promise.all(extractSubDir(libDirs))).flat(); + const libDirs = (await Promise.all(extractSubDir(projectDirs))).filter(Boolean).flat(); + const versionDirs = (await Promise.all(extractSubDir(libDirs))).filter(Boolean).flat(); const librarySet = new Set(libDirs.map((e) => e.name)); const versionSet = new Set(versionDirs.map((e) => e.name)); @@ -150,13 +150,13 @@ export async function cleanAdditional(ui5DataDir) { /** * Clean the framework cache directory. * - * Uses an atomic rename to make the framework directory disappear in a single + * Returns null if no framework packages are installed. + * Otherwise uses an atomic rename to make the framework directory disappear in a single * filesystem operation: * * 1. Clear cacache's in-process memoization (no path needed — global operation). - * 2. Atomically rename framework/ to a hidden staging dir. - * After this point the original path no longer exists: concurrent builds will - * see it as absent and create a fresh framework/ directory. + * 2. Atomically rename framework/ to a staging dir. + * After this point the original path no longer exists. * 3. Delete the staging dir recursively. Its contents are now fully private * to this operation. * From 5b593d9f6234bf683fc9d614d949a36cce8ae794 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 21 Jul 2026 12:31:31 +0300 Subject: [PATCH 09/52] docs: Update JSDoc --- packages/project/lib/build/cache/CacheManager.js | 3 +++ packages/project/lib/ui5Framework/cache.js | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index a6ec8785079..371cfb98fe3 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -358,6 +358,7 @@ export default class CacheManager { /** * Get build cache info for the current version. * + * @public * @static * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise<{path: string, size: number}|null>} Build cache info or null @@ -387,6 +388,7 @@ export default class CacheManager { /** * Clean build cache by clearing all records from SQLite database for the current version. * + * @public * @static * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise<{path: string, size: number}|null>} Removal result or null @@ -419,6 +421,7 @@ export default class CacheManager { * Note: This method is a placeholder for interface compatibility across * cleanup tasks and currently does not perform any cleanup. * + * @public * @static * @param {string} _ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise} Always resolves with an empty array diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index c0b5adf4559..e1399889a98 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -2,6 +2,13 @@ import fs from "node:fs/promises"; import path from "node:path"; import {getRandomValues} from "node:crypto"; +/** + * Utilities for cleaning the UI5 framework cache. + * + * @public + * @module @ui5/project/ui5Framework/cache + */ + const FRAMEWORK_DIR_NAME = "framework"; /** @@ -61,6 +68,7 @@ async function getPackageStats(frameworkDir) { /** * Get framework cache info. * + * @public * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise<{path: string, libraryCount: number, versionCount: number}|null>} * Framework cache info, or null if no packages are installed. @@ -83,6 +91,7 @@ export async function getCacheInfo(ui5DataDir) { * interrupted clean operations (i.e. process killed after rename but before deletion). * Returns stats per orphan without deleting anything. * + * @public * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise>} */ @@ -129,6 +138,7 @@ export async function getOrphanedInfo(ui5DataDir) { * Deletion failures are swallowed per entry so one stuck directory does not prevent * the others from being removed. * + * @public * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise>} */ @@ -160,6 +170,7 @@ export async function cleanAdditional(ui5DataDir) { * 3. Delete the staging dir recursively. Its contents are now fully private * to this operation. * + * @public * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise<{path: string, libraryCount: number, versionCount: number}|null>} * Removal result, or null if no framework packages were installed. From 573495d9be60f61069a189d4d759a993d988f138 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 21 Jul 2026 16:33:45 +0300 Subject: [PATCH 10/52] fix(cli): Resolve ui5DataDir without shared resolver --- packages/cli/lib/cli/commands/cache.js | 19 ++++++++- packages/cli/test/lib/cli/commands/cache.js | 42 +++++++++++++++----- packages/project/test/lib/package-exports.js | 2 +- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 76440780059..6c40d1912df 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -1,8 +1,9 @@ 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 {resolveUi5DataDir} from "@ui5/project/utils/dataDir"; +import Configuration from "@ui5/project/config/Configuration"; import * as frameworkCache from "@ui5/project/ui5Framework/cache"; import CacheManager from "@ui5/project/build/cache/CacheManager"; @@ -227,8 +228,22 @@ async function getConfirmation(argv) { }); } +async function resolveCacheUi5DataDir() { + // TODO: Consolidate ui5DataDir resolution once PR #1455 follow-up cleanup is done. + // Keep behavior aligned with existing main-branch resolution order. + let ui5DataDir = process.env.UI5_DATA_DIR; + if (!ui5DataDir) { + const config = await Configuration.fromFile(); + ui5DataDir = config.getUi5DataDir(); + } + if (ui5DataDir) { + return path.resolve(process.cwd(), ui5DataDir); + } + return path.join(os.homedir(), ".ui5"); +} + async function handleCache(argv) { - const ui5DataDir = await resolveUi5DataDir(); + const ui5DataDir = await resolveCacheUi5DataDir(); process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index c0a7e35cdbd..68f86b4f072 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -1,5 +1,6 @@ import test from "ava"; import path from "node:path"; +import os from "node:os"; import sinon from "sinon"; import esmock from "esmock"; @@ -28,7 +29,10 @@ test.beforeEach(async (t) => { // Prevent real env var from leaking into tests delete process.env.UI5_DATA_DIR; - t.context.resolveUi5DataDirStub = sinon.stub().resolves(TEST_UI5_DATA_DIR); + t.context.configurationGetUi5DataDirStub = sinon.stub().returns(TEST_UI5_DATA_DIR); + t.context.configurationFromFileStub = sinon.stub().resolves({ + getUi5DataDir: t.context.configurationGetUi5DataDirStub, + }); t.context.frameworkCacheGetCacheInfo = sinon.stub(); t.context.frameworkCacheCleanCache = sinon.stub(); @@ -40,8 +44,10 @@ test.beforeEach(async (t) => { t.context.yesnoStub = sinon.stub(); t.context.cache = await esmock.p("../../../../lib/cli/commands/cache.js", { - "@ui5/project/utils/dataDir": { - resolveUi5DataDir: t.context.resolveUi5DataDirStub, + "@ui5/project/config/Configuration": { + default: { + fromFile: t.context.configurationFromFileStub, + }, }, "@ui5/project/ui5Framework/cache": { getCacheInfo: t.context.frameworkCacheGetCacheInfo, @@ -95,9 +101,9 @@ test.serial("Command definition is correct", (t) => { // ─── ui5DataDir resolution ────────────────────────────────────────────────── -test.serial("ui5 cache clean: uses resolved path from resolveUi5DataDir", async (t) => { +test.serial("ui5 cache clean: uses resolved path from configuration", async (t) => { const {cache, argv, frameworkCacheGetCacheInfo, buildCacheGetCacheInfo, - stderrWriteStub, resolveUi5DataDirStub} = t.context; + stderrWriteStub, configurationFromFileStub, configurationGetUi5DataDirStub} = t.context; frameworkCacheGetCacheInfo.resolves(null); buildCacheGetCacheInfo.resolves(null); @@ -105,17 +111,35 @@ test.serial("ui5 cache clean: uses resolved path from resolveUi5DataDir", async argv["_"] = ["cache", "clean"]; await cache.handler(argv); - t.is(resolveUi5DataDirStub.callCount, 1, "resolveUi5DataDir called exactly once"); - t.deepEqual(resolveUi5DataDirStub.getCall(0).args, [], - "resolveUi5DataDir called with no arguments"); + t.is(configurationFromFileStub.callCount, 1, "Configuration.fromFile called exactly once"); + t.is(configurationGetUi5DataDirStub.callCount, 1, "Configuration#getUi5DataDir called exactly once"); t.is(frameworkCacheGetCacheInfo.firstCall.args[0], TEST_UI5_DATA_DIR, - "getCacheInfo receives the path returned by resolveUi5DataDir"); + "getCacheInfo receives the path returned by configuration"); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes(TEST_UI5_DATA_DIR), "Resolved ui5DataDir shown in checking line"); }); +test.serial("ui5 cache clean: falls back to ~/.ui5 when getUi5DataDir has no value", async (t) => { + const {cache, argv, frameworkCacheGetCacheInfo, buildCacheGetCacheInfo, + stderrWriteStub, configurationGetUi5DataDirStub} = t.context; + + const fallbackUi5DataDir = path.join(os.homedir(), ".ui5"); + configurationGetUi5DataDirStub.returns(undefined); + frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves(null); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + t.is(frameworkCacheGetCacheInfo.firstCall.args[0], fallbackUi5DataDir, + "getCacheInfo receives default ~/.ui5 path when no configured value exists"); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes(fallbackUi5DataDir), "Fallback ui5DataDir shown in checking line"); +}); + // ─── Basic flow ───────────────────────────────────────────────────────────── test.serial("ui5 cache clean: nothing to clean", async (t) => { diff --git a/packages/project/test/lib/package-exports.js b/packages/project/test/lib/package-exports.js index 42023fde407..be59e0f6f92 100644 --- a/packages/project/test/lib/package-exports.js +++ b/packages/project/test/lib/package-exports.js @@ -13,7 +13,7 @@ test("export of package.json", (t) => { // Check number of definied exports test("check number of exports", (t) => { const packageJson = require("@ui5/project/package.json"); - t.is(Object.keys(packageJson.exports).length, 15); + t.is(Object.keys(packageJson.exports).length, 16); }); // Public API contract (exported modules) From dbd18a11193359f3f392d03f53359ea17c3da70a Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 21 Jul 2026 16:42:33 +0300 Subject: [PATCH 11/52] test(cli): Cover cache ui5DataDir precedence --- packages/cli/test/lib/cli/commands/cache.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 68f86b4f072..8fd50a9de18 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -121,7 +121,25 @@ test.serial("ui5 cache clean: uses resolved path from configuration", async (t) t.true(allOutput.includes(TEST_UI5_DATA_DIR), "Resolved ui5DataDir shown in checking line"); }); -test.serial("ui5 cache clean: falls back to ~/.ui5 when getUi5DataDir has no value", async (t) => { +test.serial("ui5 cache clean: prefers UI5_DATA_DIR env var over configuration", async (t) => { + const {cache, argv, frameworkCacheGetCacheInfo, buildCacheGetCacheInfo, + configurationFromFileStub} = t.context; + + const envUi5DataDir = path.resolve("env-ui5-home"); + process.env.UI5_DATA_DIR = envUi5DataDir; + frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves(null); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + t.is(configurationFromFileStub.callCount, 0, + "Configuration.fromFile must not be called when UI5_DATA_DIR is set"); + t.is(frameworkCacheGetCacheInfo.firstCall.args[0], envUi5DataDir, + "getCacheInfo receives value from UI5_DATA_DIR"); +}); + +test.serial("ui5 cache clean: falls back to ~/.ui5 when configuration has no value", async (t) => { const {cache, argv, frameworkCacheGetCacheInfo, buildCacheGetCacheInfo, stderrWriteStub, configurationGetUi5DataDirStub} = t.context; From eeff30083dd6c914060b8e60f3dce9da0430e073 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 10:54:21 +0300 Subject: [PATCH 12/52] docs: Adjust documentation --- internal/documentation/docs/pages/Troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/documentation/docs/pages/Troubleshooting.md b/internal/documentation/docs/pages/Troubleshooting.md index 7812cff8bc1..8a25b4cc98f 100644 --- a/internal/documentation/docs/pages/Troubleshooting.md +++ b/internal/documentation/docs/pages/Troubleshooting.md @@ -41,7 +41,7 @@ ui5 cache clean --yes The command removes the following cached data: - **UI5 framework packages** — downloaded UI5 library files (`~/.ui5/framework/`) - **Build cache (Db)** — build data (`~/.ui5/buildCache/`) -- **Orphaned framework data** — incomplete framework directories left over from previously interrupted cleanup operations (`~/.ui5/.framework_to_delete_*/`) +- **Orphaned framework data** — incomplete framework directories left over from previously interrupted cleanup operations (`~/.ui5/_framework_to_delete_*/`) Any required framework dependencies will be re-downloaded during the next UI5 CLI invocation. From f3f317a1ad581f4d1fb8d3d8228eb524fc4c62c8 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 12:14:04 +0300 Subject: [PATCH 13/52] refactor: Align cleaners on common API --- packages/cli/lib/cli/commands/cache.js | 13 +- packages/cli/test/lib/cli/commands/cache.js | 24 +- .../project/lib/build/cache/CacheManager.js | 15 + packages/project/lib/ui5Framework/cache.js | 296 +++++++++--------- .../project/test/lib/ui5framework/cache.js | 42 +-- 5 files changed, 209 insertions(+), 181 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 6c40d1912df..a917744bd4a 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -4,7 +4,7 @@ import os from "node:os"; import process from "node:process"; import baseMiddleware from "../middlewares/base.js"; import Configuration from "@ui5/project/config/Configuration"; -import * as frameworkCache from "@ui5/project/ui5Framework/cache"; +import FrameworkCache from "@ui5/project/ui5Framework/cache"; import CacheManager from "@ui5/project/build/cache/CacheManager"; const cacheCommand = { @@ -247,10 +247,11 @@ async function handleCache(argv) { process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); - const [frameworkInfo, buildInfo, orphanedInfo] = await Promise.all([ - frameworkCache.getCacheInfo(ui5DataDir), + const [frameworkInfo, orphanedInfo, buildInfo] = await Promise.all([ + FrameworkCache.getCacheInfo(ui5DataDir), + FrameworkCache.getAdditionalCacheInfo(ui5DataDir), CacheManager.getCacheInfo(ui5DataDir), - frameworkCache.getOrphanedInfo(ui5DataDir), + CacheManager.getAdditionalCacheInfo(ui5DataDir), ]); if (!frameworkInfo && !buildInfo && orphanedInfo.length === 0) { @@ -282,12 +283,12 @@ async function handleCache(argv) { } const [frameworkResult, buildResult] = await Promise.all([ - frameworkCache.cleanCache(ui5DataDir), + FrameworkCache.cleanCache(ui5DataDir), CacheManager.cleanCache(ui5DataDir), ]); const [additionalFrameworkResult] = await Promise.all([ - frameworkCache.cleanAdditional(ui5DataDir), + FrameworkCache.cleanAdditional(ui5DataDir), CacheManager.cleanAdditional(ui5DataDir), ]); const orphanedInfoWithAbsPaths = additionalFrameworkResult.map( diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 8fd50a9de18..e068a3c1ae7 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -37,7 +37,7 @@ test.beforeEach(async (t) => { t.context.frameworkCacheGetCacheInfo = sinon.stub(); t.context.frameworkCacheCleanCache = sinon.stub(); t.context.frameworkCacheCleanAdditional = sinon.stub().resolves([]); - t.context.frameworkCacheGetOrphanedInfo = sinon.stub().resolves([]); + t.context.frameworkCacheGetAdditionalCacheInfo = sinon.stub().resolves([]); t.context.buildCacheGetCacheInfo = sinon.stub(); t.context.buildCacheCleanCache = sinon.stub(); @@ -50,10 +50,12 @@ test.beforeEach(async (t) => { }, }, "@ui5/project/ui5Framework/cache": { - getCacheInfo: t.context.frameworkCacheGetCacheInfo, - cleanCache: t.context.frameworkCacheCleanCache, - cleanAdditional: t.context.frameworkCacheCleanAdditional, - getOrphanedInfo: t.context.frameworkCacheGetOrphanedInfo, + default: class { + static getCacheInfo = t.context.frameworkCacheGetCacheInfo; + static cleanCache = t.context.frameworkCacheCleanCache; + static cleanAdditional = t.context.frameworkCacheCleanAdditional; + static getAdditionalCacheInfo = t.context.frameworkCacheGetAdditionalCacheInfo; + } }, "@ui5/project/build/cache/CacheManager": { default: class { @@ -363,11 +365,11 @@ test.serial("ui5 cache clean --yes: skips confirmation prompt", async (t) => { test.serial("ui5 cache clean: shows orphaned framework data in pre-confirmation summary", async (t) => { const {cache, argv, stderrWriteStub, yesnoStub, - frameworkCacheCleanCache, frameworkCacheGetOrphanedInfo} = t.context; + frameworkCacheCleanCache, frameworkCacheGetAdditionalCacheInfo} = t.context; t.context.frameworkCacheGetCacheInfo.resolves(null); t.context.buildCacheGetCacheInfo.resolves(null); - frameworkCacheGetOrphanedInfo.resolves([ + frameworkCacheGetAdditionalCacheInfo.resolves([ {path: "_framework_to_delete_abcd", libraryCount: 5, versionCount: 2}, ]); frameworkCacheCleanCache.resolves(null); @@ -386,12 +388,12 @@ test.serial("ui5 cache clean: shows orphaned framework data in pre-confirmation }); test.serial("ui5 cache clean: shows orphaned framework data in post-clean summary", async (t) => { - const {cache, argv, stderrWriteStub, frameworkCacheGetOrphanedInfo, + const {cache, argv, stderrWriteStub, frameworkCacheGetAdditionalCacheInfo, frameworkCacheCleanCache, frameworkCacheCleanAdditional} = t.context; t.context.frameworkCacheGetCacheInfo.resolves({path: "framework", libraryCount: 3, versionCount: 1}); t.context.buildCacheGetCacheInfo.resolves(null); - frameworkCacheGetOrphanedInfo.resolves([ + frameworkCacheGetAdditionalCacheInfo.resolves([ {path: "_framework_to_delete_ab12", libraryCount: 3, versionCount: 1}, {path: "_framework_to_delete_cd34", libraryCount: 3, versionCount: 1}, ]); @@ -413,12 +415,12 @@ test.serial("ui5 cache clean: shows orphaned framework data in post-clean summar }); test.serial("ui5 cache clean: shows orphaned-only success summary when no active framework", async (t) => { - const {cache, argv, stderrWriteStub, frameworkCacheGetOrphanedInfo, + const {cache, argv, stderrWriteStub, frameworkCacheGetAdditionalCacheInfo, frameworkCacheCleanCache, frameworkCacheCleanAdditional} = t.context; t.context.frameworkCacheGetCacheInfo.resolves(null); t.context.buildCacheGetCacheInfo.resolves(null); - frameworkCacheGetOrphanedInfo.resolves([ + frameworkCacheGetAdditionalCacheInfo.resolves([ {path: "_framework_to_delete_zz99", libraryCount: 10, versionCount: 3}, ]); frameworkCacheCleanCache.resolves(null); diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index 371cfb98fe3..fe362e43dee 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -429,4 +429,19 @@ export default class CacheManager { static async cleanAdditional(_ui5DataDir) { return []; } + + /** + * Get additional build cache info that is safe to remove independently. + * + * Note: This method is a placeholder for interface compatibility across + * cleanup tasks and currently does not return any additional info. + * + * @public + * @static + * @param {string} _ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise} Always resolves with an empty array + */ + static async getAdditionalCacheInfo(_ui5DataDir) { + return []; + } } diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index e1399889a98..1b8c720021b 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -18,6 +18,159 @@ const FRAMEWORK_DIR_NAME = "framework"; */ const STAGING_DIR_PREFIX = "_framework_to_delete_"; +/** + * Provides static utilities for inspecting and cleaning the UI5 framework cache. + * + * @public + */ +export default class FrameworkCache { + /** + * Get framework cache info. + * + * @public + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise<{path: string, libraryCount: number, versionCount: number}|null>} + * Framework cache info, or null if no packages are installed. + */ + static async getCacheInfo(ui5DataDir) { + const frameworkDir = path.join(ui5DataDir, FRAMEWORK_DIR_NAME); + const stats = await getPackageStats(frameworkDir); + if (!stats) { + return null; + } + return { + path: FRAMEWORK_DIR_NAME, + libraryCount: stats.libraries, + versionCount: stats.versions, + }; + } + + /** + * Get additional framework cache info. + * + * Scans ui5DataDir for orphaned staging directories left behind by previously + * interrupted clean operations (i.e. process killed after rename but before deletion). + * Returns stats per orphan without deleting anything. + * + * @public + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise>} + */ + static async getAdditionalCacheInfo(ui5DataDir) { + let entries; + try { + entries = await fs.readdir(ui5DataDir, {withFileTypes: true}); + } catch { + return []; + } + + const orphans = entries.filter( + (e) => e.isDirectory() && e.name.startsWith(STAGING_DIR_PREFIX) + ); + + if (orphans.length === 0) { + return []; + } + + const results = await Promise.all(orphans.map(async (orphan) => { + const orphanDir = path.join(ui5DataDir, orphan.name); + const stats = await getPackageStats(orphanDir); + if (!stats) { + return null; + } + return { + path: orphan.name, + libraryCount: stats.libraries, + versionCount: stats.versions, + }; + })); + + return results.filter(Boolean); + } + + /** + * Scans ui5DataDir for orphaned staging directories left behind by previously + * interrupted clean operations (i.e. process killed after rename but before deletion). + * + * Returns an array of result objects — one per orphaned directory found — each + * containing the path, library count and version count so the caller can include + * them in the cleanup summary. + * + * Deletion failures are swallowed per entry so one stuck directory does not prevent + * the others from being removed. + * + * @public + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise>} + */ + static async cleanAdditional(ui5DataDir) { + const orphans = await FrameworkCache.getAdditionalCacheInfo(ui5DataDir); + + for (const orphan of orphans) { + const orphanDir = path.join(ui5DataDir, orphan.path); + try { + await fs.rm(orphanDir, {recursive: true, force: true}); + } catch { + // Ignore deletion errors + } + } + + return orphans; + } + + /** + * Clean the framework cache directory. + * + * Returns null if no framework packages are installed. + * Otherwise uses an atomic rename to make the framework directory disappear in a single + * filesystem operation: + * + * 1. Clear cacache's in-process memoization (no path needed — global operation). + * 2. Atomically rename framework/ to a staging dir. + * After this point the original path no longer exists. + * 3. Delete the staging dir recursively. Its contents are now fully private + * to this operation. + * + * @public + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise<{path: string, libraryCount: number, versionCount: number}|null>} + * Removal result, or null if no framework packages were installed. + */ + static async cleanCache(ui5DataDir) { + const frameworkDir = path.join(ui5DataDir, FRAMEWORK_DIR_NAME); + const stats = await getPackageStats(frameworkDir); + if (!stats) { + return null; + } + + // Clear cacache's in-process memoization before the rename. + // clearMemoized() operates globally (no path argument) and is synchronous. + try { + const {clearMemoized} = await import("cacache"); + clearMemoized(); + } catch { + // cacache not available — no-op + } + + // Atomically rename framework/ to a staging directory. + // fs.rename is a single syscall and completes in microseconds. + // After this line the original path no longer exists. + const stagingDir = path.join( + ui5DataDir, + `${STAGING_DIR_PREFIX}${Buffer.from(getRandomValues(new Uint8Array(2))).toString("hex")}` + ); + await fs.rename(frameworkDir, stagingDir); + + await fs.rm(stagingDir, {recursive: true, force: true}); + + return { + path: FRAMEWORK_DIR_NAME, + libraryCount: stats.libraries, + versionCount: stats.versions, + }; + } +} + /** * Count unique libraries and versions in the packages/ subdirectory. * @@ -65,146 +218,3 @@ async function getPackageStats(frameworkDir) { null; } -/** - * Get framework cache info. - * - * @public - * @param {string} ui5DataDir Resolved absolute path to UI5 data directory - * @returns {Promise<{path: string, libraryCount: number, versionCount: number}|null>} - * Framework cache info, or null if no packages are installed. - */ -export async function getCacheInfo(ui5DataDir) { - const frameworkDir = path.join(ui5DataDir, FRAMEWORK_DIR_NAME); - const stats = await getPackageStats(frameworkDir); - if (!stats) { - return null; - } - return { - path: FRAMEWORK_DIR_NAME, - libraryCount: stats.libraries, - versionCount: stats.versions, - }; -} - -/** - * Scans ui5DataDir for orphaned staging directories left behind by previously - * interrupted clean operations (i.e. process killed after rename but before deletion). - * Returns stats per orphan without deleting anything. - * - * @public - * @param {string} ui5DataDir Resolved absolute path to UI5 data directory - * @returns {Promise>} - */ -export async function getOrphanedInfo(ui5DataDir) { - let entries; - try { - entries = await fs.readdir(ui5DataDir, {withFileTypes: true}); - } catch { - return []; - } - - const orphans = entries.filter( - (e) => e.isDirectory() && e.name.startsWith(STAGING_DIR_PREFIX) - ); - - if (orphans.length === 0) { - return []; - } - - const results = await Promise.all(orphans.map(async (orphan) => { - const orphanDir = path.join(ui5DataDir, orphan.name); - const stats = await getPackageStats(orphanDir); - if (!stats) { - return null; - } - return { - path: orphan.name, - libraryCount: stats.libraries, - versionCount: stats.versions, - }; - })); - - return results.filter(Boolean); -} - -/** - * Scans ui5DataDir for orphaned staging directories left behind by previously - * interrupted clean operations (i.e. process killed after rename but before deletion). - * - * Returns an array of result objects — one per orphaned directory found — each - * containing the path, library count and version count so the caller can include - * them in the cleanup summary. - * - * Deletion failures are swallowed per entry so one stuck directory does not prevent - * the others from being removed. - * - * @public - * @param {string} ui5DataDir Resolved absolute path to UI5 data directory - * @returns {Promise>} - */ -export async function cleanAdditional(ui5DataDir) { - const orphans = await getOrphanedInfo(ui5DataDir); - - for (const orphan of orphans) { - const orphanDir = path.join(ui5DataDir, orphan.path); - try { - await fs.rm(orphanDir, {recursive: true, force: true}); - } catch { - // Ignore deletion errors - } - } - - return orphans; -} - -/** - * Clean the framework cache directory. - * - * Returns null if no framework packages are installed. - * Otherwise uses an atomic rename to make the framework directory disappear in a single - * filesystem operation: - * - * 1. Clear cacache's in-process memoization (no path needed — global operation). - * 2. Atomically rename framework/ to a staging dir. - * After this point the original path no longer exists. - * 3. Delete the staging dir recursively. Its contents are now fully private - * to this operation. - * - * @public - * @param {string} ui5DataDir Resolved absolute path to UI5 data directory - * @returns {Promise<{path: string, libraryCount: number, versionCount: number}|null>} - * Removal result, or null if no framework packages were installed. - */ -export async function cleanCache(ui5DataDir) { - const frameworkDir = path.join(ui5DataDir, FRAMEWORK_DIR_NAME); - const stats = await getPackageStats(frameworkDir); - if (!stats) { - return null; - } - - // Clear cacache's in-process memoization before the rename. - // clearMemoized() operates globally (no path argument) and is synchronous. - try { - const {clearMemoized} = await import("cacache"); - clearMemoized(); - } catch { - // cacache not available — no-op - } - - // Atomically rename framework/ to a staging directory. - // fs.rename is a single syscall and completes in microseconds. - // After this line the original path no longer exists. - const stagingDir = path.join( - ui5DataDir, - `${STAGING_DIR_PREFIX}${Buffer.from(getRandomValues(new Uint8Array(2))).toString("hex")}` - ); - await fs.rename(frameworkDir, stagingDir); - - await fs.rm(stagingDir, {recursive: true, force: true}); - - return { - path: FRAMEWORK_DIR_NAME, - libraryCount: stats.libraries, - versionCount: stats.versions, - }; -} diff --git a/packages/project/test/lib/ui5framework/cache.js b/packages/project/test/lib/ui5framework/cache.js index a8189b75119..728334fdb33 100644 --- a/packages/project/test/lib/ui5framework/cache.js +++ b/packages/project/test/lib/ui5framework/cache.js @@ -3,7 +3,7 @@ import path from "node:path"; import fs from "node:fs/promises"; import sinon from "sinon"; import esmock from "esmock"; -import {getCacheInfo, cleanCache, cleanAdditional} from "../../../lib/ui5Framework/cache.js"; +import FrameworkCache from "../../../lib/ui5Framework/cache.js"; const TEST_DIR = path.join(import.meta.dirname, "..", "..", "tmp", "ui5framework-cache"); @@ -36,19 +36,19 @@ async function mkPackageIn(baseDir, project, library, version) { // ─── getCacheInfo ───────────────────────────────────────────────────────────── test("getCacheInfo: non-existent framework directory returns null", async (t) => { - const result = await getCacheInfo(t.context.testDir); + const result = await FrameworkCache.getCacheInfo(t.context.testDir); t.is(result, null); }); test("getCacheInfo: framework dir exists but no packages/ subdir returns null", async (t) => { await fs.mkdir(path.join(t.context.testDir, "framework", "cacache"), {recursive: true}); - const result = await getCacheInfo(t.context.testDir); + const result = await FrameworkCache.getCacheInfo(t.context.testDir); t.is(result, null); }); test("getCacheInfo: packages/ exists but is empty returns null", async (t) => { await fs.mkdir(path.join(t.context.testDir, "framework", "packages"), {recursive: true}); - const result = await getCacheInfo(t.context.testDir); + const result = await FrameworkCache.getCacheInfo(t.context.testDir); t.is(result, null); }); @@ -59,7 +59,7 @@ test("getCacheInfo: counts libraries and versions", async (t) => { await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.148.0"); await mkPackage(t.context.testDir, "@sapui5", "sap.m", "1.38.1"); - const result = await getCacheInfo(t.context.testDir); + const result = await FrameworkCache.getCacheInfo(t.context.testDir); t.truthy(result); t.is(result.path, "framework"); t.is(result.libraryCount, 2); // sap.m counted once (deduplicated across scopes) @@ -71,7 +71,7 @@ test("getCacheInfo: deduplicates versions across libraries", async (t) => { await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.120.0"); - const result = await getCacheInfo(t.context.testDir); + const result = await FrameworkCache.getCacheInfo(t.context.testDir); t.truthy(result); t.is(result.libraryCount, 2); t.is(result.versionCount, 1); // 1.120.0 deduplicated @@ -80,7 +80,7 @@ test("getCacheInfo: deduplicates versions across libraries", async (t) => { test("getCacheInfo: single library and version", async (t) => { await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); - const result = await getCacheInfo(t.context.testDir); + const result = await FrameworkCache.getCacheInfo(t.context.testDir); t.truthy(result); t.is(result.libraryCount, 1); t.is(result.versionCount, 1); @@ -89,13 +89,13 @@ test("getCacheInfo: single library and version", async (t) => { // ─── cleanCache ─────────────────────────────────────────────────────────────── test("cleanCache: returns null for non-existent framework directory", async (t) => { - const result = await cleanCache(t.context.testDir); + const result = await FrameworkCache.cleanCache(t.context.testDir); t.is(result, null); }); test("cleanCache: returns null when packages/ has no installed libraries", async (t) => { await fs.mkdir(path.join(t.context.testDir, "framework", "packages"), {recursive: true}); - const result = await cleanCache(t.context.testDir); + const result = await FrameworkCache.cleanCache(t.context.testDir); t.is(result, null); }); @@ -105,7 +105,7 @@ test("cleanCache: renames then removes framework directory and returns stats", a await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.148.0"); const frameworkDir = path.join(t.context.testDir, "framework"); - const result = await cleanCache(t.context.testDir); + const result = await FrameworkCache.cleanCache(t.context.testDir); t.truthy(result); t.is(result.path, "framework"); @@ -113,7 +113,7 @@ test("cleanCache: renames then removes framework directory and returns stats", a t.is(result.versionCount, 2); // 1.120.0, 1.148.0 // framework/ is gone — getCacheInfo returns null - t.is(await getCacheInfo(t.context.testDir), null); + t.is(await FrameworkCache.getCacheInfo(t.context.testDir), null); // No staging dirs remain after a successful clean const entries = await fs.readdir(t.context.testDir); @@ -128,19 +128,19 @@ test("cleanCache: removes directory with multiple scopes", async (t) => { await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); await mkPackage(t.context.testDir, "@sapui5", "sap.m", "1.38.1"); - const result = await cleanCache(t.context.testDir); + const result = await FrameworkCache.cleanCache(t.context.testDir); t.truthy(result); t.is(result.libraryCount, 1); // sap.m deduplicated t.is(result.versionCount, 2); - t.is(await getCacheInfo(t.context.testDir), null); + t.is(await FrameworkCache.getCacheInfo(t.context.testDir), null); }); test("cleanCache: does not include orphaned field in result", async (t) => { await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); - const result = await cleanCache(t.context.testDir); + const result = await FrameworkCache.cleanCache(t.context.testDir); t.truthy(result); t.false(Object.prototype.hasOwnProperty.call(result, "orphaned"), @@ -153,7 +153,7 @@ test("cleanCache: does not remove orphaned staging dirs — that is cleanAdditio const orphanDir = path.join(t.context.testDir, "_framework_to_delete_abcd"); await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.100.0"); - await cleanCache(t.context.testDir); + await FrameworkCache.cleanCache(t.context.testDir); // Orphan is still present after cleanCache — cleanAdditional handles it await t.notThrowsAsync(fs.access(orphanDir), "orphaned dir is not touched by cleanCache"); @@ -162,7 +162,7 @@ test("cleanCache: does not remove orphaned staging dirs — that is cleanAdditio // ─── cleanAdditional ────────────────────────────────────────────────────────── test("cleanAdditional: returns empty array when no orphaned staging dirs exist", async (t) => { - const result = await cleanAdditional(t.context.testDir); + const result = await FrameworkCache.cleanAdditional(t.context.testDir); t.deepEqual(result, []); }); @@ -171,7 +171,7 @@ test("cleanAdditional: detects and removes orphaned staging dirs, reports them", await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.100.0"); await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.110.0"); - const result = await cleanAdditional(t.context.testDir); + const result = await FrameworkCache.cleanAdditional(t.context.testDir); t.is(result.length, 1, "one orphaned dir reported"); const orphanResult = result[0]; @@ -190,7 +190,7 @@ test("cleanAdditional: removes multiple orphaned staging dirs and reports each", await mkPackageIn(orphan2, "@openui5", "sap.ui.core", "1.91.0"); await mkPackageIn(orphan2, "@openui5", "sap.ui.core", "1.92.0"); - const result = await cleanAdditional(t.context.testDir); + const result = await FrameworkCache.cleanAdditional(t.context.testDir); t.is(result.length, 2, "two orphaned dirs reported"); @@ -215,16 +215,16 @@ test("cleanAdditional: orphaned dir deletion failure is non-fatal", async (t) => return fs.rm(p, opts); }); - const {cleanAdditional: cleanAdditionalMocked} = await esmock.p( + const FrameworkCacheMocked = await esmock.p( "../../../lib/ui5Framework/cache.js", {"node:fs/promises": {...fs, rm: rmStub}} ); try { - const result = await t.notThrowsAsync(cleanAdditionalMocked(t.context.testDir)); + const result = await t.notThrowsAsync(FrameworkCacheMocked.cleanAdditional(t.context.testDir)); t.truthy(result, "cleanAdditional completes despite orphan deletion failure"); } finally { - esmock.purge(cleanAdditionalMocked); + esmock.purge(FrameworkCacheMocked); await fs.rm(orphanDir, {recursive: true, force: true}).catch(() => {}); } }); From d8ebb21cfb2bcf51102cee8caa6d081101081623 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 12:27:34 +0300 Subject: [PATCH 14/52] docs: Adjust Troubleshooting cleanup section --- internal/documentation/docs/pages/Troubleshooting.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/documentation/docs/pages/Troubleshooting.md b/internal/documentation/docs/pages/Troubleshooting.md index 8a25b4cc98f..284a88231d4 100644 --- a/internal/documentation/docs/pages/Troubleshooting.md +++ b/internal/documentation/docs/pages/Troubleshooting.md @@ -20,13 +20,9 @@ UI5 CLI stores several kinds of data under your user's home directory in `~/.ui5 | `~/.ui5/buildCache/` | Build cache used by `ui5 build` and `ui5 serve` (see [Build Cache Control](./Builder.md#build-cache-control)) | Yes — rebuilt on next `ui5 build` / `ui5 serve` | | `~/.ui5/server/` | Locally generated SSL certificate and private key for HTTPS / HTTP/2 mode | Yes — regenerated on next HTTPS server start; the new certificate must be re-trusted | -::: warning -Only remove these directories when no UI5 CLI process and no `@ui5/*` API consumer is actively running. Deleting files that are in use can cause running builds or servers to fail or produce inconsistent results. -::: - #### Resolution -Use the dedicated cache clean command, which safely removes all cached data: +Use the dedicated cache clean command, which removes all cached data: ```sh ui5 cache clean @@ -49,6 +45,10 @@ Any required framework dependencies will be re-downloaded during the next UI5 CL If you have configured a custom data directory via `UI5_DATA_DIR` or `ui5 config set ui5DataDir`, the `ui5 cache clean` command will clean up that location instead of the default `~/.ui5/`. See [Changing UI5 CLI's Data Directory](#changing-ui5-cli-s-data-directory). ::: +::: warning +Only remove these directories, or run `ui5 cache clean`, when no UI5 CLI process and no `@ui5/*` API consumer is actively running. Running `ui5 cache clean` while `ui5 build` or `ui5 serve` is in progress can break the running process and lead to failed or inconsistent results. +::: + ## Environment Variables ### Changing the Log Level From c2afd62291489e0fa28c90b2456ad12e791c3b7d Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 12:50:05 +0300 Subject: [PATCH 15/52] refactor: DRY cleanups --- packages/cli/lib/cli/commands/cache.js | 16 ++------- packages/project/lib/ui5Framework/cache.js | 22 ++++++------ .../project/test/lib/ui5framework/cache.js | 34 ++++++++----------- 3 files changed, 27 insertions(+), 45 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index a917744bd4a..1d4860bab4d 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -32,19 +32,7 @@ cacheCommand.builder = function(cli) { .example("$0 cache clean --yes", "Remove all cached UI5 data without confirmation (e.g. in CI scenarios)") .example("UI5_DATA_DIR=/custom/path $0 cache clean", - "Remove cached data from a non-default UI5 data directory") - .epilogue( - "The cache is stored in the UI5 data directory (default: ~/.ui5).\n" + - "Override the location with the UI5_DATA_DIR environment variable or\n" + - "the 'ui5 config set ui5DataDir' configuration option (see 'ui5 config --help').\n\n" + - "The following cache types are removed:\n" + - " UI5 framework packages: Downloaded UI5 library files " + - "(~/.ui5/framework/)\n" + - " Build cache (Db): Build data " + - "(~/.ui5/buildCache/)\n" + - " Orphaned framework data: Incomplete directories from previously interrupted cleanups\n" + - " (~/.ui5/_framework_to_delete_*/)" - ); + "Remove cached data from a non-default UI5 data directory"); }, middlewares: [baseMiddleware], }); @@ -229,7 +217,7 @@ async function getConfirmation(argv) { } async function resolveCacheUi5DataDir() { - // TODO: Consolidate ui5DataDir resolution once PR #1455 follow-up cleanup is done. + // TODO: Consolidate ui5DataDir resolution once PR #1456 follow-up cleanup is done. // Keep behavior aligned with existing main-branch resolution order. let ui5DataDir = process.env.UI5_DATA_DIR; if (!ui5DataDir) { diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index 1b8c720021b..3c374fb70d4 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -64,22 +64,22 @@ export default class FrameworkCache { return []; } - const orphans = entries.filter( + const staleDirs = entries.filter( (e) => e.isDirectory() && e.name.startsWith(STAGING_DIR_PREFIX) ); - if (orphans.length === 0) { + if (staleDirs.length === 0) { return []; } - const results = await Promise.all(orphans.map(async (orphan) => { - const orphanDir = path.join(ui5DataDir, orphan.name); - const stats = await getPackageStats(orphanDir); + const results = await Promise.all(staleDirs.map(async (staleDir) => { + const staleDirPath = path.join(ui5DataDir, staleDir.name); + const stats = await getPackageStats(staleDirPath); if (!stats) { return null; } return { - path: orphan.name, + path: staleDir.name, libraryCount: stats.libraries, versionCount: stats.versions, }; @@ -104,18 +104,18 @@ export default class FrameworkCache { * @returns {Promise>} */ static async cleanAdditional(ui5DataDir) { - const orphans = await FrameworkCache.getAdditionalCacheInfo(ui5DataDir); + const staleDirs = await FrameworkCache.getAdditionalCacheInfo(ui5DataDir); - for (const orphan of orphans) { - const orphanDir = path.join(ui5DataDir, orphan.path); + for (const staleDir of staleDirs) { + const staleDirPath = path.join(ui5DataDir, staleDir.path); try { - await fs.rm(orphanDir, {recursive: true, force: true}); + await fs.rm(staleDirPath, {recursive: true, force: true}); } catch { // Ignore deletion errors } } - return orphans; + return staleDirs; } /** diff --git a/packages/project/test/lib/ui5framework/cache.js b/packages/project/test/lib/ui5framework/cache.js index 728334fdb33..4f18fd4f5d9 100644 --- a/packages/project/test/lib/ui5framework/cache.js +++ b/packages/project/test/lib/ui5framework/cache.js @@ -21,12 +21,6 @@ test.afterEach.always(async (t) => { // ─── Helpers ───────────────────────────────────────────────────────────────── -async function mkPackage(testDir, project, library, version) { - const dir = path.join(testDir, "framework", "packages", project, library, version); - await fs.mkdir(dir, {recursive: true}); - await fs.writeFile(path.join(dir, "package.json"), JSON.stringify({name: `${project}/${library}`, version})); -} - async function mkPackageIn(baseDir, project, library, version) { const dir = path.join(baseDir, "packages", project, library, version); await fs.mkdir(dir, {recursive: true}); @@ -54,10 +48,10 @@ test("getCacheInfo: packages/ exists but is empty returns null", async (t) => { test("getCacheInfo: counts libraries and versions", async (t) => { // 2 unique library names across 2 scopes, 3 unique versions - await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); - await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.120.0"); - await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.148.0"); - await mkPackage(t.context.testDir, "@sapui5", "sap.m", "1.38.1"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.ui.core", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.ui.core", "1.148.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@sapui5", "sap.m", "1.38.1"); const result = await FrameworkCache.getCacheInfo(t.context.testDir); t.truthy(result); @@ -68,8 +62,8 @@ test("getCacheInfo: counts libraries and versions", async (t) => { test("getCacheInfo: deduplicates versions across libraries", async (t) => { // Both libraries have 1.120.0 — version should count once - await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); - await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.ui.core", "1.120.0"); const result = await FrameworkCache.getCacheInfo(t.context.testDir); t.truthy(result); @@ -78,7 +72,7 @@ test("getCacheInfo: deduplicates versions across libraries", async (t) => { }); test("getCacheInfo: single library and version", async (t) => { - await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); const result = await FrameworkCache.getCacheInfo(t.context.testDir); t.truthy(result); @@ -100,9 +94,9 @@ test("cleanCache: returns null when packages/ has no installed libraries", async }); test("cleanCache: renames then removes framework directory and returns stats", async (t) => { - await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); - await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.120.0"); - await mkPackage(t.context.testDir, "@openui5", "sap.ui.core", "1.148.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.ui.core", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.ui.core", "1.148.0"); const frameworkDir = path.join(t.context.testDir, "framework"); const result = await FrameworkCache.cleanCache(t.context.testDir); @@ -125,8 +119,8 @@ test("cleanCache: renames then removes framework directory and returns stats", a }); test("cleanCache: removes directory with multiple scopes", async (t) => { - await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); - await mkPackage(t.context.testDir, "@sapui5", "sap.m", "1.38.1"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@sapui5", "sap.m", "1.38.1"); const result = await FrameworkCache.cleanCache(t.context.testDir); @@ -138,7 +132,7 @@ test("cleanCache: removes directory with multiple scopes", async (t) => { }); test("cleanCache: does not include orphaned field in result", async (t) => { - await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); const result = await FrameworkCache.cleanCache(t.context.testDir); @@ -148,7 +142,7 @@ test("cleanCache: does not include orphaned field in result", async (t) => { }); test("cleanCache: does not remove orphaned staging dirs — that is cleanAdditional's job", async (t) => { - await mkPackage(t.context.testDir, "@openui5", "sap.m", "1.120.0"); + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); const orphanDir = path.join(t.context.testDir, "_framework_to_delete_abcd"); await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.100.0"); From 63838f179a9c27daf8139ce9819950e3012a4f09 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 14:15:53 +0300 Subject: [PATCH 16/52] test: Add more test cases --- .../test/lib/build/cache/BuildCacheStorage.js | 55 +++++++++++++++++++ .../test/lib/build/cache/CacheManager.js | 50 +++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/packages/project/test/lib/build/cache/BuildCacheStorage.js b/packages/project/test/lib/build/cache/BuildCacheStorage.js index 9137a4d0f5d..1e395b31827 100644 --- a/packages/project/test/lib/build/cache/BuildCacheStorage.js +++ b/packages/project/test/lib/build/cache/BuildCacheStorage.js @@ -420,6 +420,61 @@ test("findExistingContentIntegrities: Handles large batches", (t) => { t.false(result.has("sha256-nonexistent")); }); +test("hasRecords: Returns false for empty database", (t) => { + t.false(t.context.storage.hasRecords()); +}); + +test("hasRecords: Returns true when content table has records", (t) => { + t.context.storage.putContent("sha256-content", Buffer.from("content")); + t.true(t.context.storage.hasRecords()); +}); + +test("hasRecords: Returns true when index cache table has records", (t) => { + t.context.storage.writeIndexCache("project-a", "build-sig", "source", {v: 1}); + t.true(t.context.storage.hasRecords()); +}); + +test("hasRecords: Returns true when stage metadata table has records", (t) => { + t.context.storage.writeStageCache("project-a", "build-sig", "task/minify", "sig-a", {v: 1}); + t.true(t.context.storage.hasRecords()); +}); + +test("hasRecords: Returns true when task metadata table has records", (t) => { + t.context.storage.writeTaskMetadata("project-a", "build-sig", "minify", "project", {v: 1}); + t.true(t.context.storage.hasRecords()); +}); + +test("hasRecords: Returns true when result metadata table has records", (t) => { + t.context.storage.writeResultMetadata("project-a", "build-sig", "sig-a", {v: 1}); + t.true(t.context.storage.hasRecords()); +}); + +test("getDatabaseSize: Returns positive database size", (t) => { + const size = t.context.storage.getDatabaseSize(); + t.true(Number.isInteger(size)); + t.true(size > 0); +}); + +test("clearAllRecords: Clears all tables and returns freed size", (t) => { + t.context.storage.putContent("sha256-content", Buffer.from("content")); + t.context.storage.writeIndexCache("project-a", "build-sig", "source", {v: 1}); + t.context.storage.writeStageCache("project-a", "build-sig", "task/minify", "sig-a", {v: 1}); + t.context.storage.writeTaskMetadata("project-a", "build-sig", "minify", "project", {v: 1}); + t.context.storage.writeResultMetadata("project-a", "build-sig", "sig-a", {v: 1}); + + t.true(t.context.storage.hasRecords()); + const freedSize = t.context.storage.clearAllRecords(); + + t.true(Number.isInteger(freedSize)); + t.true(freedSize >= 0); + t.false(t.context.storage.hasRecords()); + t.false(t.context.storage.hasContent("sha256-content")); + t.is(t.context.storage.readIndexCache("project-a", "build-sig", "source"), null); + t.is(t.context.storage.readStageCache("project-a", "build-sig", "task/minify", "sig-a"), null); + t.is(t.context.storage.readTaskMetadata("project-a", "build-sig", "minify", "project"), null); + t.is(t.context.storage.readResultMetadata("project-a", "build-sig", "sig-a"), null); +}); + // ===== Pre-compressed content ===== test("putCompressedContent: Stores pre-compressed data retrievable via readContent", (t) => { diff --git a/packages/project/test/lib/build/cache/CacheManager.js b/packages/project/test/lib/build/cache/CacheManager.js index e02d0850d48..fff2d549765 100644 --- a/packages/project/test/lib/build/cache/CacheManager.js +++ b/packages/project/test/lib/build/cache/CacheManager.js @@ -203,3 +203,53 @@ test.serial("transaction: throwing rolls back metadata and content writes", asyn "Metadata should not exist after rollback"); cm.close(); }); + +// Static cleanup/info helpers + +test.serial("getCacheInfo: Returns null when cache db is not available", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + + const info = await CacheManager.getCacheInfo(testDir); + t.is(info, null); +}); + +test.serial("getCacheInfo: Returns null when cache has no records", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + cm.close(); + + const info = await CacheManager.getCacheInfo(testDir); + t.is(info, null); +}); + +test.serial("getCacheInfo: Returns cache info when records exist", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + cm.writeIndexCache("project-x", "build-sig", "source", {value: 1}); + cm.close(); + + const info = await CacheManager.getCacheInfo(testDir); + t.truthy(info); + t.true(Number.isInteger(info.size)); + t.true(info.size > 0); +}); + +test.serial("cleanCache: Clears records and returns removal result", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + cm.writeIndexCache("project-x", "build-sig", "source", {value: 1}); + cm.putContent("sha256-clean", Buffer.from("content")); + cm.close(); + + const result = await CacheManager.cleanCache(testDir); + t.truthy(result); + t.true(Number.isInteger(result.size)); + t.true(result.size >= 0); + + const infoAfterClean = await CacheManager.getCacheInfo(testDir); + t.is(infoAfterClean, null); +}); From cbe1bd0a28a4106a3609df3455a74047576c7bdd Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 14:27:34 +0300 Subject: [PATCH 17/52] refactor: Comment stale invocations --- packages/cli/lib/cli/commands/cache.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 1d4860bab4d..9239a0785e6 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -239,7 +239,7 @@ async function handleCache(argv) { FrameworkCache.getCacheInfo(ui5DataDir), FrameworkCache.getAdditionalCacheInfo(ui5DataDir), CacheManager.getCacheInfo(ui5DataDir), - CacheManager.getAdditionalCacheInfo(ui5DataDir), + // CacheManager.getAdditionalCacheInfo(ui5DataDir), // API compatibility. Currently not needed. ]); if (!frameworkInfo && !buildInfo && orphanedInfo.length === 0) { @@ -277,7 +277,7 @@ async function handleCache(argv) { const [additionalFrameworkResult] = await Promise.all([ FrameworkCache.cleanAdditional(ui5DataDir), - CacheManager.cleanAdditional(ui5DataDir), + // CacheManager.cleanAdditional(ui5DataDir), // API compatibility. Currently not needed. ]); const orphanedInfoWithAbsPaths = additionalFrameworkResult.map( (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) From 7e71d08abcbc25da7a4283421062f2bc90cbab89 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 14:29:50 +0300 Subject: [PATCH 18/52] test: Fix missed stubs --- packages/cli/test/lib/cli/commands/cache.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index e068a3c1ae7..806a3372ab8 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -40,6 +40,8 @@ test.beforeEach(async (t) => { t.context.frameworkCacheGetAdditionalCacheInfo = sinon.stub().resolves([]); t.context.buildCacheGetCacheInfo = sinon.stub(); t.context.buildCacheCleanCache = sinon.stub(); + t.context.buildCacheCleanAdditional = sinon.stub().resolves([]); + t.context.buildCacheGetAdditionalCacheInfo = sinon.stub().resolves([]); t.context.yesnoStub = sinon.stub(); @@ -61,7 +63,8 @@ test.beforeEach(async (t) => { default: class { static getCacheInfo = t.context.buildCacheGetCacheInfo; static cleanCache = t.context.buildCacheCleanCache; - static cleanAdditional = sinon.stub().resolves([]); + static cleanAdditional = t.context.buildCacheCleanAdditional; + static getAdditionalCacheInfo = t.context.buildCacheGetAdditionalCacheInfo; } }, "yesno": { From cf82481e4e23cf5a51f898c55968ee8d22d91404 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 15:01:20 +0300 Subject: [PATCH 19/52] fix: Address potential race condition during dir rename --- packages/project/lib/ui5Framework/cache.js | 17 ++++-- .../project/test/lib/ui5framework/cache.js | 52 +++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index 3c374fb70d4..63085a2db76 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -121,20 +121,23 @@ export default class FrameworkCache { /** * Clean the framework cache directory. * - * Returns null if no framework packages are installed. + * Returns null if no framework packages are installed, or if the + * directory was concurrently removed by another process during the operation. * Otherwise uses an atomic rename to make the framework directory disappear in a single * filesystem operation: * * 1. Clear cacache's in-process memoization (no path needed — global operation). * 2. Atomically rename framework/ to a staging dir. * After this point the original path no longer exists. + * If ENOENT is raised (concurrent deletion), returns null. * 3. Delete the staging dir recursively. Its contents are now fully private * to this operation. * * @public * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise<{path: string, libraryCount: number, versionCount: number}|null>} - * Removal result, or null if no framework packages were installed. + * Removal result, or null if no framework packages were installed or the directory + * was concurrently removed. */ static async cleanCache(ui5DataDir) { const frameworkDir = path.join(ui5DataDir, FRAMEWORK_DIR_NAME); @@ -159,7 +162,15 @@ export default class FrameworkCache { ui5DataDir, `${STAGING_DIR_PREFIX}${Buffer.from(getRandomValues(new Uint8Array(2))).toString("hex")}` ); - await fs.rename(frameworkDir, stagingDir); + try { + await fs.rename(frameworkDir, stagingDir); + } catch (err) { + if (/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT") { + // Directory was removed by another process after our check — already clean. + return null; + } + throw err; + } await fs.rm(stagingDir, {recursive: true, force: true}); diff --git a/packages/project/test/lib/ui5framework/cache.js b/packages/project/test/lib/ui5framework/cache.js index 4f18fd4f5d9..727139b9d0e 100644 --- a/packages/project/test/lib/ui5framework/cache.js +++ b/packages/project/test/lib/ui5framework/cache.js @@ -223,3 +223,55 @@ test("cleanAdditional: orphaned dir deletion failure is non-fatal", async (t) => } }); +test("cleanCache: returns null if framework dir removed between check and rename (ENOENT race)", async (t) => { + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); + + const frameworkDir = path.join(t.context.testDir, "framework"); + const renameStub = sinon.stub().callsFake(async (oldPath, newPath) => { + if (oldPath === frameworkDir) { + const err = Object.assign(new Error("ENOENT: no such file or directory, rename"), {code: "ENOENT"}); + throw err; + } + return fs.rename(oldPath, newPath); + }); + + const FrameworkCacheMocked = await esmock.p( + "../../../lib/ui5Framework/cache.js", + {"node:fs/promises": {...fs, rename: renameStub}} + ); + + try { + const result = await FrameworkCacheMocked.cleanCache(t.context.testDir); + t.is(result, null, "returns null when directory is concurrently removed"); + } finally { + esmock.purge(FrameworkCacheMocked); + await fs.rm(frameworkDir, {recursive: true, force: true}).catch(() => {}); + } +}); + +test("cleanCache: re-throws non-ENOENT errors from fs.rename", async (t) => { + await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); + + const frameworkDir = path.join(t.context.testDir, "framework"); + const renameStub = sinon.stub().callsFake(async (oldPath, newPath) => { + if (oldPath === frameworkDir) { + const err = Object.assign(new Error("EACCES: permission denied, rename"), {code: "EACCES"}); + throw err; + } + return fs.rename(oldPath, newPath); + }); + + const FrameworkCacheMocked = await esmock.p( + "../../../lib/ui5Framework/cache.js", + {"node:fs/promises": {...fs, rename: renameStub}} + ); + + try { + const error = await t.throwsAsync(FrameworkCacheMocked.cleanCache(t.context.testDir)); + t.is(/** @type {NodeJS.ErrnoException} */ (error).code, "EACCES"); + } finally { + esmock.purge(FrameworkCacheMocked); + await fs.rm(frameworkDir, {recursive: true, force: true}).catch(() => {}); + } +}); + From e3f3c4fa6b6b9ef9018289d1f3d6fe0d361ddf02 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 15:08:57 +0300 Subject: [PATCH 20/52] refactor: Long running Db cleanup For tables big enough, it could take a lot of time to run the VACUUM. For that reason, do the same as the directory rename- rename tables and later clean them up. This will not corrupt Db data and will be able to resume if the process is killed --- packages/cli/lib/cli/commands/cache.js | 44 ++++++++- .../lib/build/cache/BuildCacheStorage.js | 82 ++++++++++++++++ .../project/lib/build/cache/CacheManager.js | 75 +++++++++++---- .../test/lib/build/cache/BuildCacheStorage.js | 71 ++++++++++++++ .../test/lib/build/cache/CacheManager.js | 96 ++++++++++++++++++- 5 files changed, 342 insertions(+), 26 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 9239a0785e6..59f95f15fd3 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -96,6 +96,7 @@ function padLabel(label) { * @param {string|null} data.buildAbsPath * @param {number} data.buildPreSize * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfo + * @param {Array<{absPath: string, size: number}>} data.buildAdditionalInfo */ async function displayCacheInfo({ frameworkInfo, @@ -104,6 +105,7 @@ async function displayCacheInfo({ buildAbsPath, buildPreSize, orphanedInfo, + buildAdditionalInfo, }) { process.stderr.write(chalk.bold("\nThe following cached data will be removed:\n\n")); if (frameworkInfo) { @@ -129,6 +131,16 @@ async function displayCacheInfo({ process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); } } + if (buildAdditionalInfo && buildAdditionalInfo.length > 0) { + process.stderr.write( + ` ${chalk.yellow("•")} ${chalk.bold("Stale build cache data")}` + + ` (incomplete previous clean)\n` + ); + for (const entry of buildAdditionalInfo) { + const detail = entry.size > 0 ? formatSize(entry.size) : ""; + process.stderr.write(` ${chalk.dim(entry.absPath)}${detail ? ` (${detail})` : ""}\n`); + } + } process.stderr.write("\n"); } @@ -144,6 +156,7 @@ async function displayCacheInfo({ * @param {string|null} data.buildAbsPath * @param {number} data.buildPreSize * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfoWithAbsPaths + * @param {Array<{absPath: string, size: number}>} data.buildAdditionalResult */ async function displayCleanupResult({ frameworkResult, @@ -152,6 +165,7 @@ async function displayCleanupResult({ buildAbsPath, buildPreSize, orphanedInfoWithAbsPaths, + buildAdditionalResult, }) { process.stderr.write("\n"); if (frameworkResult && frameworkAbsPath) { @@ -182,6 +196,15 @@ async function displayCleanupResult({ ` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n`, ); } + if (buildAdditionalResult && buildAdditionalResult.length > 0) { + for (const entry of buildAdditionalResult) { + const detail = entry.size > 0 ? formatSize(entry.size) : ""; + process.stderr.write( + `${chalk.green("✓")} Cleaned ${chalk.bold("Stale build cache data")}` + + ` (${entry.absPath}${detail ? ` · freed ${detail}` : ""})\n` + ); + } + } // Success summary const cleaned = []; @@ -194,6 +217,9 @@ async function displayCleanupResult({ if (buildResult) { cleaned.push(LABEL_BUILD); } + if (buildAdditionalResult && buildAdditionalResult.length > 0) { + cleaned.push("Stale build cache data"); + } process.stderr.write( `\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`, ); @@ -235,14 +261,14 @@ async function handleCache(argv) { process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); - const [frameworkInfo, orphanedInfo, buildInfo] = await Promise.all([ + const [frameworkInfo, orphanedInfo, buildInfo, buildAdditionalInfo] = await Promise.all([ FrameworkCache.getCacheInfo(ui5DataDir), FrameworkCache.getAdditionalCacheInfo(ui5DataDir), CacheManager.getCacheInfo(ui5DataDir), - // CacheManager.getAdditionalCacheInfo(ui5DataDir), // API compatibility. Currently not needed. + CacheManager.getAdditionalCacheInfo(ui5DataDir), ]); - if (!frameworkInfo && !buildInfo && orphanedInfo.length === 0) { + if (!frameworkInfo && !buildInfo && orphanedInfo.length === 0 && buildAdditionalInfo.length === 0) { process.stderr.write("Nothing to clean\n"); return; } @@ -254,6 +280,9 @@ async function handleCache(argv) { const preCleanOrphanedInfo = orphanedInfo.map( (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) ); + const preCleanBuildAdditionalInfo = buildAdditionalInfo.map( + (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) + ); await displayCacheInfo({ frameworkInfo, @@ -262,6 +291,7 @@ async function handleCache(argv) { buildAbsPath, buildPreSize, orphanedInfo: preCleanOrphanedInfo, + buildAdditionalInfo: preCleanBuildAdditionalInfo, }); const confirmed = await getConfirmation(argv); @@ -275,13 +305,16 @@ async function handleCache(argv) { CacheManager.cleanCache(ui5DataDir), ]); - const [additionalFrameworkResult] = await Promise.all([ + const [additionalFrameworkResult, additionalBuildResult] = await Promise.all([ FrameworkCache.cleanAdditional(ui5DataDir), - // CacheManager.cleanAdditional(ui5DataDir), // API compatibility. Currently not needed. + CacheManager.cleanAdditional(ui5DataDir), ]); const orphanedInfoWithAbsPaths = additionalFrameworkResult.map( (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) ); + const buildAdditionalResult = additionalBuildResult.map( + (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) + ); await displayCleanupResult({ frameworkResult, @@ -290,6 +323,7 @@ async function handleCache(argv) { buildAbsPath, buildPreSize, orphanedInfoWithAbsPaths, + buildAdditionalResult, }); } diff --git a/packages/project/lib/build/cache/BuildCacheStorage.js b/packages/project/lib/build/cache/BuildCacheStorage.js index 3489afb9253..323283c0dba 100644 --- a/packages/project/lib/build/cache/BuildCacheStorage.js +++ b/packages/project/lib/build/cache/BuildCacheStorage.js @@ -2,6 +2,7 @@ import {DatabaseSync} from "node:sqlite"; import {mkdirSync, existsSync} from "node:fs"; import path from "node:path"; import {gzipSync, gunzipSync} from "node:zlib"; +import {getRandomValues} from "node:crypto"; import {getLogger} from "@ui5/logger"; const log = getLogger("build:cache:BuildCacheStorage"); @@ -551,6 +552,87 @@ export default class BuildCacheStorage { return false; } + /** + * Atomically renames all live tables to stale staging names and recreates + * fresh empty tables in a single transaction. The rename is O(1) — only + * sqlite_master is updated — so the operation completes in milliseconds + * regardless of data volume. + * + * Stale tables follow the naming convention _<table>_to_delete_<hex> + * (matching the framework staging-dir prefix) and are cleaned up later by + * {@link dropStaleTables}. + * + * @returns {number} Database size in bytes before the rename (pending reclamation) + */ + markAllTablesAsStale() { + const hex = Buffer.from(getRandomValues(new Uint8Array(2))).toString("hex"); + const suffix = `_to_delete_${hex}`; + const tables = ["content", "index_cache", "stage_metadata", "task_metadata", "result_metadata"]; + + const bytesBefore = this.getDatabaseSize(); + + this.#db.exec("BEGIN"); + try { + for (const table of tables) { + this.#db.exec(`ALTER TABLE ${table} RENAME TO _${table}${suffix}`); + } + this.#createTables(); + this.#db.exec("COMMIT"); + } catch (err) { + this.#db.exec("ROLLBACK"); + throw err; + } + + return bytesBefore; + } + + /** + * Returns true if the database contains any stale staging tables + * (tables whose names follow the _*_to_delete_* pattern). + * + * @returns {boolean} + */ + hasStaleTables() { + const row = this.#db.prepare( + "SELECT COUNT(*) AS cnt FROM sqlite_master WHERE type='table' AND name LIKE '\\_%\\_to\\_delete\\_%' ESCAPE '\\'" + ).get(); + return row.cnt > 0; + } + + /** + * Drops all stale staging tables and runs VACUUM to reclaim disk space. + * This is the slow half of the two-phase cache clean; call it from + * cleanAdditional after the fast {@link markAllTablesAsStale} pass. + * + * @returns {number} Number of bytes freed + */ + dropStaleTables() { + const staleRows = this.#db.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '\\_%\\_to\\_delete\\_%' ESCAPE '\\'" + ).all(); + + if (staleRows.length === 0) { + return 0; + } + + const bytesBefore = this.getDatabaseSize(); + + this.#db.exec("BEGIN"); + try { + for (const {name} of staleRows) { + this.#db.exec(`DROP TABLE "${name}"`); + } + this.#db.exec("COMMIT"); + } catch (err) { + this.#db.exec("ROLLBACK"); + throw err; + } + + this.#db.exec("VACUUM"); + + return bytesBefore - this.getDatabaseSize(); + } + /** * Closes the database connection */ diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index fe362e43dee..b94e5d5ce90 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -386,12 +386,14 @@ export default class CacheManager { } /** - * Clean build cache by clearing all records from SQLite database for the current version. + * Clean build cache by atomically renaming all live tables to stale staging names + * and recreating fresh empty tables. The rename is O(1) and completes in milliseconds. + * Actual disk reclamation (VACUUM) is deferred to {@link cleanAdditional}. * * @public * @static * @param {string} ui5DataDir Resolved absolute path to UI5 data directory - * @returns {Promise<{path: string, size: number}|null>} Removal result or null + * @returns {Promise<{path: string, size: number}|null>} Removal result (size = bytes pending reclamation) or null */ static async cleanCache(ui5DataDir) { const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); @@ -403,10 +405,10 @@ export default class CacheManager { const storage = new BuildCacheStorage(dbDir); try { if (storage.hasRecords()) { - const freedSize = storage.clearAllRecords(); + const bytesBefore = storage.markAllTablesAsStale(); return { path: `buildCache/${CACHE_VERSION}`, - size: freedSize, + size: bytesBefore, }; } } finally { @@ -416,32 +418,65 @@ export default class CacheManager { } /** - * Clean additional build cache resources that are safe to remove independently. - * - * Note: This method is a placeholder for interface compatibility across - * cleanup tasks and currently does not perform any cleanup. + * Drops stale build cache staging tables and runs VACUUM to reclaim disk space. + * Stale tables are created by {@link cleanCache} via an atomic rename; this method + * performs the slow cleanup pass that was deferred. * * @public * @static - * @param {string} _ui5DataDir Resolved absolute path to UI5 data directory - * @returns {Promise} Always resolves with an empty array + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise>} Cleaned entries, or empty array if nothing to clean */ - static async cleanAdditional(_ui5DataDir) { - return []; + static async cleanAdditional(ui5DataDir) { + const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); + const isAvailable = await CacheManager.#isCacheDbAvailable(dbDir); + if (!isAvailable) { + return []; + } + + const storage = new BuildCacheStorage(dbDir); + try { + if (!storage.hasStaleTables()) { + return []; + } + const freedSize = storage.dropStaleTables(); + return [{ + path: `buildCache/${CACHE_VERSION}`, + size: freedSize, + }]; + } finally { + storage.close(); + } } /** - * Get additional build cache info that is safe to remove independently. - * - * Note: This method is a placeholder for interface compatibility across - * cleanup tasks and currently does not return any additional info. + * Returns info about stale build cache staging tables left by a previous + * {@link cleanCache} call that has not yet been followed by {@link cleanAdditional}. * * @public * @static - * @param {string} _ui5DataDir Resolved absolute path to UI5 data directory - * @returns {Promise} Always resolves with an empty array + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise>} Pending entries, or empty array if none */ - static async getAdditionalCacheInfo(_ui5DataDir) { - return []; + static async getAdditionalCacheInfo(ui5DataDir) { + const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); + const isAvailable = await CacheManager.#isCacheDbAvailable(dbDir); + if (!isAvailable) { + return []; + } + + const storage = new BuildCacheStorage(dbDir); + try { + if (!storage.hasStaleTables()) { + return []; + } + const size = storage.getDatabaseSize(); + return [{ + path: `buildCache/${CACHE_VERSION}`, + size, + }]; + } finally { + storage.close(); + } } } diff --git a/packages/project/test/lib/build/cache/BuildCacheStorage.js b/packages/project/test/lib/build/cache/BuildCacheStorage.js index 1e395b31827..840a8f98719 100644 --- a/packages/project/test/lib/build/cache/BuildCacheStorage.js +++ b/packages/project/test/lib/build/cache/BuildCacheStorage.js @@ -526,3 +526,74 @@ test("readContent: Legacy compressed tiny content is still readable", (t) => { t.context.storage.putCompressedContent("sha256-legacy-tiny", compressed); t.deepEqual(t.context.storage.readContent("sha256-legacy-tiny"), content); }); + +// ===== Stale-table operations ===== + +test("markAllTablesAsStale: renames all 5 live tables and recreates fresh ones", (t) => { + t.context.storage.writeIndexCache("p", "sig", "source", {value: 1}); + t.context.storage.putContent("sha256-stale-1", Buffer.from("data")); + + const bytesBefore = t.context.storage.markAllTablesAsStale(); + + t.true(bytesBefore > 0, "returns pre-rename byte count"); + t.false(t.context.storage.hasRecords(), "fresh tables are empty after rename"); + t.true(t.context.storage.hasStaleTables(), "stale tables exist after rename"); +}); + +test("markAllTablesAsStale: fresh tables accept new writes immediately", (t) => { + t.context.storage.writeIndexCache("p", "sig", "source", {old: true}); + t.context.storage.markAllTablesAsStale(); + + t.notThrows(() => { + t.context.storage.writeIndexCache("p", "sig", "source", {new: true}); + }, "can write to fresh tables right after stale rename"); + + t.deepEqual(t.context.storage.readIndexCache("p", "sig", "source"), {new: true}); +}); + +test("markAllTablesAsStale: multiple calls produce independent stale groups", (t) => { + t.context.storage.putContent("sha256-a", Buffer.from("a")); + t.context.storage.markAllTablesAsStale(); + + t.context.storage.putContent("sha256-b", Buffer.from("b")); + t.context.storage.markAllTablesAsStale(); + + t.true(t.context.storage.hasStaleTables(), "stale tables from both calls exist"); + t.false(t.context.storage.hasRecords(), "fresh tables are empty"); +}); + +test("hasStaleTables: returns false when no stale tables exist", (t) => { + t.false(t.context.storage.hasStaleTables()); +}); + +test("hasStaleTables: returns true after markAllTablesAsStale", (t) => { + t.context.storage.putContent("sha256-has", Buffer.from("x")); + t.context.storage.markAllTablesAsStale(); + t.true(t.context.storage.hasStaleTables()); +}); + +test("dropStaleTables: returns 0 when no stale tables exist", (t) => { + t.is(t.context.storage.dropStaleTables(), 0); +}); + +test("dropStaleTables: removes stale tables and frees space", (t) => { + const largeContent = Buffer.alloc(64 * 1024, "x"); + t.context.storage.putContent("sha256-large", largeContent); + t.context.storage.markAllTablesAsStale(); + + const freed = t.context.storage.dropStaleTables(); + + t.true(freed >= 0, "freed bytes is non-negative"); + t.false(t.context.storage.hasStaleTables(), "no stale tables remain after drop"); +}); + +test("dropStaleTables: live tables and data are unaffected", (t) => { + t.context.storage.putContent("sha256-old", Buffer.from("old")); + t.context.storage.markAllTablesAsStale(); + t.context.storage.writeIndexCache("p", "sig", "source", {fresh: true}); + + t.context.storage.dropStaleTables(); + + t.deepEqual(t.context.storage.readIndexCache("p", "sig", "source"), {fresh: true}, + "data written after stale rename is still accessible"); +}); diff --git a/packages/project/test/lib/build/cache/CacheManager.js b/packages/project/test/lib/build/cache/CacheManager.js index fff2d549765..0b1f56c42fd 100644 --- a/packages/project/test/lib/build/cache/CacheManager.js +++ b/packages/project/test/lib/build/cache/CacheManager.js @@ -251,5 +251,99 @@ test.serial("cleanCache: Clears records and returns removal result", async (t) = t.true(result.size >= 0); const infoAfterClean = await CacheManager.getCacheInfo(testDir); - t.is(infoAfterClean, null); + t.is(infoAfterClean, null, "getCacheInfo returns null for empty fresh tables after cleanCache"); + + const additionalInfo = await CacheManager.getAdditionalCacheInfo(testDir); + t.is(additionalInfo.length, 1, "stale tables are reported as additional info"); + t.true(additionalInfo[0].size > 0); +}); + +test.serial("cleanCache: returns null when no records exist", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + cm.close(); + + const result = await CacheManager.cleanCache(testDir); + t.is(result, null); +}); + +test.serial("cleanCache: returns null when db does not exist", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + + const result = await CacheManager.cleanCache(testDir); + t.is(result, null); +}); + +test.serial("getAdditionalCacheInfo: returns empty array when no stale tables", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + cm.writeIndexCache("p", "sig", "source", {v: 1}); + cm.close(); + + const info = await CacheManager.getAdditionalCacheInfo(testDir); + t.deepEqual(info, []); +}); + +test.serial("getAdditionalCacheInfo: returns empty array when db does not exist", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + + const info = await CacheManager.getAdditionalCacheInfo(testDir); + t.deepEqual(info, []); +}); + +test.serial("getAdditionalCacheInfo: reports stale tables after cleanCache", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + cm.writeIndexCache("p", "sig", "source", {v: 1}); + cm.close(); + + await CacheManager.cleanCache(testDir); + + const info = await CacheManager.getAdditionalCacheInfo(testDir); + t.is(info.length, 1); + t.true(info[0].size > 0); + t.truthy(info[0].path); +}); + +test.serial("cleanAdditional: returns empty array when no stale tables", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + cm.writeIndexCache("p", "sig", "source", {v: 1}); + cm.close(); + + const result = await CacheManager.cleanAdditional(testDir); + t.deepEqual(result, []); +}); + +test.serial("cleanAdditional: returns empty array when db does not exist", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + + const result = await CacheManager.cleanAdditional(testDir); + t.deepEqual(result, []); +}); + +test.serial("cleanAdditional: drops stale tables, returns freed size, leaves nothing pending", async (t) => { + const testDir = getUniqueTestDir(); + const CacheManager = (await import("../../../../lib/build/cache/CacheManager.js")).default; + const cm = new CacheManager(path.join(testDir, "buildCache")); + cm.writeIndexCache("p", "sig", "source", {v: 1}); + cm.putContent("sha256-cleanup", Buffer.alloc(64 * 1024, "z")); + cm.close(); + + await CacheManager.cleanCache(testDir); + + const result = await CacheManager.cleanAdditional(testDir); + t.is(result.length, 1); + t.true(result[0].size >= 0); + t.truthy(result[0].path); + + const remainingAdditional = await CacheManager.getAdditionalCacheInfo(testDir); + t.deepEqual(remainingAdditional, [], "no stale tables remain after cleanAdditional"); }); From aa8806b26bdcfbcd277468a6f22170e53d20fd5b Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 15:26:20 +0300 Subject: [PATCH 21/52] refactor: Optimize Db Vacuum --- .../lib/build/cache/BuildCacheStorage.js | 78 ++++++++----------- .../project/lib/build/cache/CacheManager.js | 23 +++--- .../test/lib/build/cache/BuildCacheStorage.js | 67 ++++++++-------- .../test/lib/build/cache/CacheManager.js | 2 +- 4 files changed, 79 insertions(+), 91 deletions(-) diff --git a/packages/project/lib/build/cache/BuildCacheStorage.js b/packages/project/lib/build/cache/BuildCacheStorage.js index 323283c0dba..cec93b87879 100644 --- a/packages/project/lib/build/cache/BuildCacheStorage.js +++ b/packages/project/lib/build/cache/BuildCacheStorage.js @@ -2,7 +2,6 @@ import {DatabaseSync} from "node:sqlite"; import {mkdirSync, existsSync} from "node:fs"; import path from "node:path"; import {gzipSync, gunzipSync} from "node:zlib"; -import {getRandomValues} from "node:crypto"; import {getLogger} from "@ui5/logger"; const log = getLogger("build:cache:BuildCacheStorage"); @@ -84,6 +83,11 @@ export default class BuildCacheStorage { data BLOB NOT NULL, PRIMARY KEY (project_id, build_signature, stage_signature) ) WITHOUT ROWID; + + CREATE TABLE IF NOT EXISTS _vacuum_pending ( + pending INTEGER NOT NULL DEFAULT 0 + ); + INSERT OR IGNORE INTO _vacuum_pending(rowid, pending) VALUES(1, 0); `); } @@ -553,33 +557,40 @@ export default class BuildCacheStorage { } /** - * Atomically renames all live tables to stale staging names and recreates - * fresh empty tables in a single transaction. The rename is O(1) — only - * sqlite_master is updated — so the operation completes in milliseconds - * regardless of data volume. + * Atomically drops all live tables and recreates fresh empty ones in a single + * transaction. The operation completes in milliseconds regardless of data volume — + * DROP TABLE never reads row data; it only removes the schema entry and adds + * pages to the freelist. Call {@link vacuum} afterwards to reclaim disk space. * - * Stale tables follow the naming convention _<table>_to_delete_<hex> - * (matching the framework staging-dir prefix) and are cleaned up later by - * {@link dropStaleTables}. + * A persistent marker is set so that a deferred VACUUM can be detected on the + * next invocation even if the process exits before {@link vacuum} runs. * - * @returns {number} Database size in bytes before the rename (pending reclamation) + * @returns {number} Database size in bytes before the drop (pending reclamation after vacuum) */ - markAllTablesAsStale() { - const hex = Buffer.from(getRandomValues(new Uint8Array(2))).toString("hex"); - const suffix = `_to_delete_${hex}`; + dropAllRecords() { const tables = ["content", "index_cache", "stage_metadata", "task_metadata", "result_metadata"]; - const bytesBefore = this.getDatabaseSize(); this.#db.exec("BEGIN"); try { for (const table of tables) { - this.#db.exec(`ALTER TABLE ${table} RENAME TO _${table}${suffix}`); + this.#db.exec(`DROP TABLE ${table}`); } this.#createTables(); + this.#db.exec("UPDATE _vacuum_pending SET pending = 1 WHERE rowid = 1"); this.#db.exec("COMMIT"); } catch (err) { this.#db.exec("ROLLBACK"); + // "no such table" would only occur if a concurrent process dropped the tables + // between our BEGIN and our first DROP — an edge case that WAL's writer + // serialization makes nearly impossible, but guard for a clear error message. + if (/** @type {NodeJS.ErrnoException} */ (err).message?.includes("no such table")) { + throw new Error( + "Build cache clean was already performed by another process. " + + "Run ui5 cache clean again to complete the deferred VACUUM.", + {cause: err} + ); + } throw err; } @@ -587,49 +598,26 @@ export default class BuildCacheStorage { } /** - * Returns true if the database contains any stale staging tables - * (tables whose names follow the _*_to_delete_* pattern). + * Returns true if a VACUUM is pending — i.e. {@link dropAllRecords} was called + * but {@link vacuum} has not yet run to reclaim the freed disk space. * * @returns {boolean} */ - hasStaleTables() { - const row = this.#db.prepare( - "SELECT COUNT(*) AS cnt FROM sqlite_master WHERE type='table' AND name LIKE '\\_%\\_to\\_delete\\_%' ESCAPE '\\'" - ).get(); - return row.cnt > 0; + hasVacuumPending() { + return this.#db.prepare("SELECT pending FROM _vacuum_pending WHERE rowid = 1").get().pending === 1; } /** - * Drops all stale staging tables and runs VACUUM to reclaim disk space. + * Runs VACUUM to reclaim disk space from freed pages and clears the pending marker. * This is the slow half of the two-phase cache clean; call it from - * cleanAdditional after the fast {@link markAllTablesAsStale} pass. + * cleanAdditional after the fast {@link dropAllRecords} pass. * * @returns {number} Number of bytes freed */ - dropStaleTables() { - const staleRows = this.#db.prepare( - "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '\\_%\\_to\\_delete\\_%' ESCAPE '\\'" - ).all(); - - if (staleRows.length === 0) { - return 0; - } - + vacuum() { const bytesBefore = this.getDatabaseSize(); - - this.#db.exec("BEGIN"); - try { - for (const {name} of staleRows) { - this.#db.exec(`DROP TABLE "${name}"`); - } - this.#db.exec("COMMIT"); - } catch (err) { - this.#db.exec("ROLLBACK"); - throw err; - } - this.#db.exec("VACUUM"); - + this.#db.exec("UPDATE _vacuum_pending SET pending = 0 WHERE rowid = 1"); return bytesBefore - this.getDatabaseSize(); } diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index b94e5d5ce90..2cbc06e1bae 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -386,8 +386,8 @@ export default class CacheManager { } /** - * Clean build cache by atomically renaming all live tables to stale staging names - * and recreating fresh empty tables. The rename is O(1) and completes in milliseconds. + * Clean build cache by atomically dropping all live tables and recreating fresh + * empty ones. The drop is O(1) and completes in milliseconds. * Actual disk reclamation (VACUUM) is deferred to {@link cleanAdditional}. * * @public @@ -405,7 +405,7 @@ export default class CacheManager { const storage = new BuildCacheStorage(dbDir); try { if (storage.hasRecords()) { - const bytesBefore = storage.markAllTablesAsStale(); + const bytesBefore = storage.dropAllRecords(); return { path: `buildCache/${CACHE_VERSION}`, size: bytesBefore, @@ -418,14 +418,13 @@ export default class CacheManager { } /** - * Drops stale build cache staging tables and runs VACUUM to reclaim disk space. - * Stale tables are created by {@link cleanCache} via an atomic rename; this method - * performs the slow cleanup pass that was deferred. + * Runs VACUUM to reclaim disk space from a previous {@link cleanCache} call. + * Only runs if the database has freelist pages (i.e. cleanup was deferred). * * @public * @static * @param {string} ui5DataDir Resolved absolute path to UI5 data directory - * @returns {Promise>} Cleaned entries, or empty array if nothing to clean + * @returns {Promise>} Cleaned entries, or empty array if nothing to reclaim */ static async cleanAdditional(ui5DataDir) { const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); @@ -436,10 +435,10 @@ export default class CacheManager { const storage = new BuildCacheStorage(dbDir); try { - if (!storage.hasStaleTables()) { + if (!storage.hasVacuumPending()) { return []; } - const freedSize = storage.dropStaleTables(); + const freedSize = storage.vacuum(); return [{ path: `buildCache/${CACHE_VERSION}`, size: freedSize, @@ -450,8 +449,8 @@ export default class CacheManager { } /** - * Returns info about stale build cache staging tables left by a previous - * {@link cleanCache} call that has not yet been followed by {@link cleanAdditional}. + * Returns info about pending disk reclamation — i.e. a previous {@link cleanCache} + * whose VACUUM has not yet been run by {@link cleanAdditional}. * * @public * @static @@ -467,7 +466,7 @@ export default class CacheManager { const storage = new BuildCacheStorage(dbDir); try { - if (!storage.hasStaleTables()) { + if (!storage.hasVacuumPending()) { return []; } const size = storage.getDatabaseSize(); diff --git a/packages/project/test/lib/build/cache/BuildCacheStorage.js b/packages/project/test/lib/build/cache/BuildCacheStorage.js index 840a8f98719..875eebddedd 100644 --- a/packages/project/test/lib/build/cache/BuildCacheStorage.js +++ b/packages/project/test/lib/build/cache/BuildCacheStorage.js @@ -527,73 +527,74 @@ test("readContent: Legacy compressed tiny content is still readable", (t) => { t.deepEqual(t.context.storage.readContent("sha256-legacy-tiny"), content); }); -// ===== Stale-table operations ===== +// ===== dropAllRecords / hasFreelistPages / vacuum ===== -test("markAllTablesAsStale: renames all 5 live tables and recreates fresh ones", (t) => { +test("dropAllRecords: drops all live tables and recreates fresh ones", (t) => { t.context.storage.writeIndexCache("p", "sig", "source", {value: 1}); - t.context.storage.putContent("sha256-stale-1", Buffer.from("data")); + t.context.storage.putContent("sha256-drop-1", Buffer.from("data")); - const bytesBefore = t.context.storage.markAllTablesAsStale(); + const bytesBefore = t.context.storage.dropAllRecords(); - t.true(bytesBefore > 0, "returns pre-rename byte count"); - t.false(t.context.storage.hasRecords(), "fresh tables are empty after rename"); - t.true(t.context.storage.hasStaleTables(), "stale tables exist after rename"); + t.true(bytesBefore > 0, "returns pre-drop byte count"); + t.false(t.context.storage.hasRecords(), "fresh tables are empty after drop"); + t.true(t.context.storage.hasVacuumPending(), "vacuum pending marker set after drop"); }); -test("markAllTablesAsStale: fresh tables accept new writes immediately", (t) => { +test("dropAllRecords: fresh tables accept new writes immediately", (t) => { t.context.storage.writeIndexCache("p", "sig", "source", {old: true}); - t.context.storage.markAllTablesAsStale(); + t.context.storage.dropAllRecords(); t.notThrows(() => { t.context.storage.writeIndexCache("p", "sig", "source", {new: true}); - }, "can write to fresh tables right after stale rename"); + }, "can write to fresh tables right after drop"); t.deepEqual(t.context.storage.readIndexCache("p", "sig", "source"), {new: true}); }); -test("markAllTablesAsStale: multiple calls produce independent stale groups", (t) => { +test("dropAllRecords: calling twice succeeds — second drop operates on freshly-created tables", (t) => { t.context.storage.putContent("sha256-a", Buffer.from("a")); - t.context.storage.markAllTablesAsStale(); + t.context.storage.dropAllRecords(); - t.context.storage.putContent("sha256-b", Buffer.from("b")); - t.context.storage.markAllTablesAsStale(); - - t.true(t.context.storage.hasStaleTables(), "stale tables from both calls exist"); - t.false(t.context.storage.hasRecords(), "fresh tables are empty"); + t.notThrows(() => t.context.storage.dropAllRecords(), + "second drop succeeds because #createTables recreated the tables in the first call"); + t.false(t.context.storage.hasRecords()); }); -test("hasStaleTables: returns false when no stale tables exist", (t) => { - t.false(t.context.storage.hasStaleTables()); +test("hasVacuumPending: returns false on a fresh database", (t) => { + t.false(t.context.storage.hasVacuumPending()); }); -test("hasStaleTables: returns true after markAllTablesAsStale", (t) => { +test("hasVacuumPending: returns true after dropAllRecords", (t) => { t.context.storage.putContent("sha256-has", Buffer.from("x")); - t.context.storage.markAllTablesAsStale(); - t.true(t.context.storage.hasStaleTables()); + t.context.storage.dropAllRecords(); + t.true(t.context.storage.hasVacuumPending()); }); -test("dropStaleTables: returns 0 when no stale tables exist", (t) => { - t.is(t.context.storage.dropStaleTables(), 0); +test("hasVacuumPending: returns false after vacuum", (t) => { + t.context.storage.putContent("sha256-vac", Buffer.from("y")); + t.context.storage.dropAllRecords(); + t.context.storage.vacuum(); + t.false(t.context.storage.hasVacuumPending()); }); -test("dropStaleTables: removes stale tables and frees space", (t) => { +test("vacuum: reclaims space and returns freed bytes", (t) => { const largeContent = Buffer.alloc(64 * 1024, "x"); - t.context.storage.putContent("sha256-large", largeContent); - t.context.storage.markAllTablesAsStale(); + t.context.storage.putContent("sha256-large-vac", largeContent); + t.context.storage.dropAllRecords(); - const freed = t.context.storage.dropStaleTables(); + const freed = t.context.storage.vacuum(); t.true(freed >= 0, "freed bytes is non-negative"); - t.false(t.context.storage.hasStaleTables(), "no stale tables remain after drop"); + t.false(t.context.storage.hasVacuumPending(), "vacuum pending cleared after vacuum"); }); -test("dropStaleTables: live tables and data are unaffected", (t) => { +test("vacuum: live data written after drop is unaffected", (t) => { t.context.storage.putContent("sha256-old", Buffer.from("old")); - t.context.storage.markAllTablesAsStale(); + t.context.storage.dropAllRecords(); t.context.storage.writeIndexCache("p", "sig", "source", {fresh: true}); - t.context.storage.dropStaleTables(); + t.context.storage.vacuum(); t.deepEqual(t.context.storage.readIndexCache("p", "sig", "source"), {fresh: true}, - "data written after stale rename is still accessible"); + "data written after drop survives vacuum"); }); diff --git a/packages/project/test/lib/build/cache/CacheManager.js b/packages/project/test/lib/build/cache/CacheManager.js index 0b1f56c42fd..dd89c030446 100644 --- a/packages/project/test/lib/build/cache/CacheManager.js +++ b/packages/project/test/lib/build/cache/CacheManager.js @@ -254,7 +254,7 @@ test.serial("cleanCache: Clears records and returns removal result", async (t) = t.is(infoAfterClean, null, "getCacheInfo returns null for empty fresh tables after cleanCache"); const additionalInfo = await CacheManager.getAdditionalCacheInfo(testDir); - t.is(additionalInfo.length, 1, "stale tables are reported as additional info"); + t.is(additionalInfo.length, 1, "vacuum pending reported as additional info"); t.true(additionalInfo[0].size > 0); }); From 9e59f1b71ca46d1bfa8e34911d67f6dcd1d54d4d Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 15:52:51 +0300 Subject: [PATCH 22/52] refactor: Cleanups and UX alignment --- .../docs/pages/Troubleshooting.md | 7 +- packages/cli/lib/cli/commands/cache.js | 91 ++++++++----------- packages/cli/test/lib/cli/commands/cache.js | 19 ++-- 3 files changed, 50 insertions(+), 67 deletions(-) diff --git a/internal/documentation/docs/pages/Troubleshooting.md b/internal/documentation/docs/pages/Troubleshooting.md index 284a88231d4..c4c2e67a370 100644 --- a/internal/documentation/docs/pages/Troubleshooting.md +++ b/internal/documentation/docs/pages/Troubleshooting.md @@ -35,9 +35,12 @@ ui5 cache clean --yes ``` The command removes the following cached data: -- **UI5 framework packages** — downloaded UI5 library files (`~/.ui5/framework/`) +- **UI5 Framework packages** — downloaded UI5 library files (`~/.ui5/framework/`) - **Build cache (Db)** — build data (`~/.ui5/buildCache/`) -- **Orphaned framework data** — incomplete framework directories left over from previously interrupted cleanup operations (`~/.ui5/_framework_to_delete_*/`) + +If a previous `ui5 cache clean` was interrupted (e.g. process killed or system crash), the command also detects and removes any leftover data from that interrupted operation, listed as separate entries: +- **Orphaned UI5 Framework packages** — incomplete framework directories left over from a previously interrupted cleanup (`~/.ui5/_framework_to_delete_*/`) +- **Orphaned build cache (Db)** — freed database pages not yet reclaimed by VACUUM during a previously interrupted cleanup Any required framework dependencies will be re-downloaded during the next UI5 CLI invocation. diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 59f95f15fd3..86cc7090d9e 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -40,7 +40,9 @@ cacheCommand.builder = function(cli) { const LABEL_FRAMEWORK = "UI5 Framework packages"; const LABEL_BUILD = "Build cache (Db)"; -// Pad labels to equal width for two-column alignment +const LABEL_ORPHANED_FRAMEWORK = "Orphaned UI5 Framework packages"; +const LABEL_ORPHANED_BUILD = "Orphaned build cache (Db)"; +// Pad main labels to equal width for two-column alignment (orphaned labels are bold headers, not padded) const LABEL_WIDTH = Math.max(LABEL_FRAMEWORK.length, LABEL_BUILD.length); /** @@ -86,8 +88,9 @@ function padLabel(label) { /** * Display information about the cached data that will be removed, - * including the absolute paths and details about the framework and build caches, - * and any orphaned staging directories from previously interrupted clean operations. + * including the absolute paths and details about the framework and build caches. + * Orphaned entries (from previously interrupted cleans) are shown as separate + * items only when present. * * @param {object} data * @param {object|null} data.frameworkInfo @@ -117,24 +120,21 @@ async function displayCacheInfo({ if (buildInfo) { const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; process.stderr.write( - ` ${chalk.yellow("•")} ${padLabel(LABEL_BUILD)} ${buildAbsPath} (${detail})\n` + ` ${chalk.yellow("•")} ${padLabel(LABEL_BUILD)} ${buildAbsPath}${detail ? ` (${detail})` : ""}\n` ); } - if (orphanedInfo && orphanedInfo.length > 0) { + if (orphanedInfo?.length > 0) { process.stderr.write( - ` ${chalk.yellow("•")} ${chalk.bold("Orphaned framework data")}` + - ` (incomplete previous clean — ` + - `${orphanedInfo.length} director${orphanedInfo.length === 1 ? "y" : "ies"})\n` + ` ${chalk.yellow("•")} ${chalk.bold(LABEL_ORPHANED_FRAMEWORK)}\n` ); for (const orphan of orphanedInfo) { const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); } } - if (buildAdditionalInfo && buildAdditionalInfo.length > 0) { + if (buildAdditionalInfo?.length > 0) { process.stderr.write( - ` ${chalk.yellow("•")} ${chalk.bold("Stale build cache data")}` + - ` (incomplete previous clean)\n` + ` ${chalk.yellow("•")} ${chalk.bold(LABEL_ORPHANED_BUILD)}\n` ); for (const entry of buildAdditionalInfo) { const detail = entry.size > 0 ? formatSize(entry.size) : ""; @@ -145,12 +145,11 @@ async function displayCacheInfo({ } /** - * Display the result of the cache cleanup operation, - * including which caches were removed and their details, - * and any orphaned staging directories that were also cleaned up. + * Display the result of the cache cleanup operation. + * Orphaned entries are shown as separate items only when present. * * @param {object} data - * @param {object|null} data.frameworkResult + * @param {{libraryCount: number, versionCount: number}|null} data.frameworkResult * @param {object|null} data.buildResult * @param {string|null} data.frameworkAbsPath * @param {string|null} data.buildAbsPath @@ -169,60 +168,41 @@ async function displayCleanupResult({ }) { process.stderr.write("\n"); if (frameworkResult && frameworkAbsPath) { - const detail = formatFrameworkStats( - frameworkResult.libraryCount, - frameworkResult.versionCount, - ); + const detail = formatFrameworkStats(frameworkResult.libraryCount, frameworkResult.versionCount); process.stderr.write( `${chalk.green("✓")} Removed ${chalk.bold(LABEL_FRAMEWORK)}` + - ` (${frameworkAbsPath} · ${detail})\n`, + ` (${frameworkAbsPath} · ${detail})\n` ); } - if (orphanedInfoWithAbsPaths && orphanedInfoWithAbsPaths.length > 0) { + if (buildResult) { + const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; process.stderr.write( - `${chalk.green("✓")} Removed ${chalk.bold("Orphaned framework data")}` + - ` (${orphanedInfoWithAbsPaths.length}` + - ` director${orphanedInfoWithAbsPaths.length === 1 ? "y" : "ies"})\n` + `${chalk.green("✓")} Removed ${chalk.bold(LABEL_BUILD)}` + + ` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n` ); + } + if (orphanedInfoWithAbsPaths?.length > 0) { + process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_ORPHANED_FRAMEWORK)}\n`); for (const orphan of orphanedInfoWithAbsPaths) { const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); } } - if (buildResult) { - const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; - process.stderr.write( - `${chalk.green("✓")} Removed ${chalk.bold(LABEL_BUILD)}` + - ` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n`, - ); - } - if (buildAdditionalResult && buildAdditionalResult.length > 0) { + if (buildAdditionalResult?.length > 0) { + process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_ORPHANED_BUILD)}\n`); for (const entry of buildAdditionalResult) { const detail = entry.size > 0 ? formatSize(entry.size) : ""; - process.stderr.write( - `${chalk.green("✓")} Cleaned ${chalk.bold("Stale build cache data")}` + - ` (${entry.absPath}${detail ? ` · freed ${detail}` : ""})\n` - ); + process.stderr.write(` ${chalk.dim(entry.absPath)}${detail ? ` (freed ${detail})` : ""}\n`); } } // Success summary const cleaned = []; - if (frameworkResult) { - cleaned.push(LABEL_FRAMEWORK); - } - if (orphanedInfoWithAbsPaths && orphanedInfoWithAbsPaths.length > 0) { - cleaned.push("Orphaned framework data"); - } - if (buildResult) { - cleaned.push(LABEL_BUILD); - } - if (buildAdditionalResult && buildAdditionalResult.length > 0) { - cleaned.push("Stale build cache data"); - } - process.stderr.write( - `\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`, - ); + if (frameworkResult) cleaned.push(LABEL_FRAMEWORK); + if (buildResult) cleaned.push(LABEL_BUILD); + if (orphanedInfoWithAbsPaths?.length > 0) cleaned.push(LABEL_ORPHANED_FRAMEWORK); + if (buildAdditionalResult?.length > 0) cleaned.push(LABEL_ORPHANED_BUILD); + process.stderr.write(`\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`); } /** @@ -312,9 +292,12 @@ async function handleCache(argv) { const orphanedInfoWithAbsPaths = additionalFrameworkResult.map( (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) ); - const buildAdditionalResult = additionalBuildResult.map( - (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) - ); + // Only surface build orphaned data in the summary when it existed before this clean started. + // cleanCache() itself sets the vacuum-pending flag, so cleanAdditional() always fires here — + // but the user should only see "Orphaned build cache" when it was leftover from a prior run. + const buildAdditionalResult = preCleanBuildAdditionalInfo.length > 0 ? + additionalBuildResult.map((o) => ({...o, absPath: path.join(ui5DataDir, o.path)})) : + []; await displayCleanupResult({ frameworkResult, diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 806a3372ab8..d9b54a334d1 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -383,10 +383,8 @@ test.serial("ui5 cache clean: shows orphaned framework data in pre-confirmation await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Orphaned framework data"), "Shows orphaned section in pre-confirm summary"); - t.true(allOutput.includes("incomplete previous clean"), "Shows orphaned context message"); - t.true(allOutput.includes("1 directory"), "Shows singular 'directory' for one orphan"); - t.true(allOutput.includes("_framework_to_delete_abcd"), "Shows orphaned dir path"); + t.true(allOutput.includes("Orphaned UI5 Framework packages"), "Shows orphaned header in pre-confirm summary"); + t.true(allOutput.includes("_framework_to_delete_abcd"), "Shows orphaned dir path indented"); t.true(allOutput.includes("2 versions of 5 libraries"), "Shows orphaned dir stats"); }); @@ -411,10 +409,9 @@ test.serial("ui5 cache clean: shows orphaned framework data in post-clean summar await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Removed Orphaned framework data"), "Shows orphaned section in result"); - t.true(allOutput.includes("2 directories"), "Shows plural 'directories' for multiple orphans"); - t.true(allOutput.includes("_framework_to_delete_ab12"), "Shows first orphaned dir path"); - t.true(allOutput.includes("_framework_to_delete_cd34"), "Shows second orphaned dir path"); + t.true(allOutput.includes("Removed Orphaned UI5 Framework packages"), "Shows orphaned header in result"); + t.true(allOutput.includes("_framework_to_delete_ab12"), "Shows first orphaned dir path indented"); + t.true(allOutput.includes("_framework_to_delete_cd34"), "Shows second orphaned dir path indented"); }); test.serial("ui5 cache clean: shows orphaned-only success summary when no active framework", async (t) => { @@ -436,7 +433,7 @@ test.serial("ui5 cache clean: shows orphaned-only success summary when no active await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Orphaned framework data"), "Shows orphaned section"); - t.true(allOutput.includes("Cleaned Orphaned framework data"), "Success summary mentions orphaned data"); - t.false(allOutput.includes("UI5 Framework packages"), "Does not mention main framework when absent"); + t.true(allOutput.includes("Orphaned UI5 Framework packages"), "Shows orphaned header"); + t.true(allOutput.includes("Cleaned Orphaned UI5 Framework packages"), "Success summary mentions orphaned label"); + t.false(allOutput.includes("Removed UI5 Framework packages"), "Does not show main framework removed line when absent"); }); From 298e23db9ffc56be515dcad0ace57c3c295b4467 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 15:54:55 +0300 Subject: [PATCH 23/52] test: Improve coverage --- packages/cli/test/lib/cli/commands/cache.js | 68 ++++++++++++++++++++- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index d9b54a334d1..5b6c09415ec 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -84,16 +84,26 @@ test.afterEach.always((t) => { test("Command builder", async (t) => { const cacheModule = await import("../../../../lib/cli/commands/cache.js"); + const yargsStub = { + option: sinon.stub().returnsThis(), + example: sinon.stub().returnsThis(), + }; const cliStub = { demandCommand: sinon.stub().returnsThis(), - command: sinon.stub().returnsThis(), - example: sinon.stub().returnsThis(), + command: sinon.stub().callsFake((_name, _desc, config) => { + // Invoke the sub-command builder to cover the inner yargs setup + if (config?.builder) { + config.builder(yargsStub); + } + return cliStub; + }), }; const result = cacheModule.default.builder(cliStub); t.is(result, cliStub, "Builder returns cli instance"); t.is(cliStub.demandCommand.callCount, 1, "demandCommand called once"); t.is(cliStub.command.callCount, 1, "command called once"); - t.is(cliStub.example.callCount, 0, "example not called on parent command"); + t.is(yargsStub.option.callCount, 1, "option called for --yes flag"); + t.is(yargsStub.example.callCount, 3, "example called 3 times"); }); test.serial("Command definition is correct", (t) => { @@ -437,3 +447,55 @@ test.serial("ui5 cache clean: shows orphaned-only success summary when no active t.true(allOutput.includes("Cleaned Orphaned UI5 Framework packages"), "Success summary mentions orphaned label"); t.false(allOutput.includes("Removed UI5 Framework packages"), "Does not show main framework removed line when absent"); }); + +test.serial("ui5 cache clean: shows orphaned build cache in pre-confirm and post-clean summary", async (t) => { + const {cache, argv, stderrWriteStub, buildCacheGetAdditionalCacheInfo, + buildCacheCleanCache, buildCacheCleanAdditional} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + t.context.buildCacheGetCacheInfo.resolves(null); + buildCacheGetAdditionalCacheInfo.resolves([ + {path: "buildCache/v0_7", size: 40 * 1024 * 1024}, + ]); + buildCacheCleanCache.resolves(null); + buildCacheCleanAdditional.resolves([ + {path: "buildCache/v0_7", size: 40 * 1024 * 1024}, + ]); + + argv["_"] = ["cache", "clean"]; + argv["yes"] = true; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("Orphaned build cache (Db)"), "Shows orphaned build cache header"); + t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "buildCache/v0_7")), + "Shows orphaned build cache path indented"); + t.true(allOutput.includes("Removed Orphaned build cache (Db)"), "Post-clean result shows orphaned build label"); + t.true(allOutput.includes("freed 40.0 MB"), "Shows freed size in post-clean result"); + t.true(allOutput.includes("Cleaned Orphaned build cache (Db)"), "Success summary mentions orphaned build cache"); +}); + +test.serial("ui5 cache clean: build cache and orphaned build cache with size 0 omit size detail", async (t) => { + const {cache, argv, stderrWriteStub, buildCacheGetAdditionalCacheInfo, + buildCacheCleanCache, buildCacheCleanAdditional, buildCacheGetCacheInfo} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 0}); + buildCacheGetAdditionalCacheInfo.resolves([ + {path: "buildCache/v0_7", size: 0}, + ]); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 0}); + buildCacheCleanAdditional.resolves([ + {path: "buildCache/v0_7", size: 0}, + ]); + + argv["_"] = ["cache", "clean"]; + argv["yes"] = true; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.false(allOutput.includes("0 B"), "Does not show zero size"); + t.true(allOutput.includes("Removed Build cache (Db)"), "Shows build cache result line"); + t.true(allOutput.includes("Removed Orphaned build cache (Db)"), "Shows orphaned build cache result line"); + t.false(allOutput.includes("freed"), "Does not show freed label when size is 0"); +}); From f74804ccead9e4367b557381b070ad01abb9257d Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 15:56:07 +0300 Subject: [PATCH 24/52] docs: Update docs --- internal/documentation/docs/pages/Troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/documentation/docs/pages/Troubleshooting.md b/internal/documentation/docs/pages/Troubleshooting.md index c4c2e67a370..8470a792c1d 100644 --- a/internal/documentation/docs/pages/Troubleshooting.md +++ b/internal/documentation/docs/pages/Troubleshooting.md @@ -40,7 +40,7 @@ The command removes the following cached data: If a previous `ui5 cache clean` was interrupted (e.g. process killed or system crash), the command also detects and removes any leftover data from that interrupted operation, listed as separate entries: - **Orphaned UI5 Framework packages** — incomplete framework directories left over from a previously interrupted cleanup (`~/.ui5/_framework_to_delete_*/`) -- **Orphaned build cache (Db)** — freed database pages not yet reclaimed by VACUUM during a previously interrupted cleanup +- **Orphaned build cache (Db)** — freed database pages not yet reclaimed during a previously interrupted cleanup Any required framework dependencies will be re-downloaded during the next UI5 CLI invocation. From 97e24d48889a2804d32fede032c8a27e8ac08bda Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 22 Jul 2026 16:10:07 +0300 Subject: [PATCH 25/52] refactor: Remove redundant code --- .../lib/build/cache/BuildCacheStorage.js | 23 ------------------- .../test/lib/build/cache/BuildCacheStorage.js | 20 ---------------- 2 files changed, 43 deletions(-) diff --git a/packages/project/lib/build/cache/BuildCacheStorage.js b/packages/project/lib/build/cache/BuildCacheStorage.js index cec93b87879..425c1d02d77 100644 --- a/packages/project/lib/build/cache/BuildCacheStorage.js +++ b/packages/project/lib/build/cache/BuildCacheStorage.js @@ -516,29 +516,6 @@ export default class BuildCacheStorage { return new Set(rows.map((row) => row.integrity)); } - /** - * Clears all records from all tables and runs VACUUM. - * Returns the number of bytes freed. - * - * @returns {number} Number of bytes freed - */ - clearAllRecords() { - const bytesBefore = this.getDatabaseSize(); - - this.#db.exec("BEGIN"); - this.#db.exec("DELETE FROM content"); - this.#db.exec("DELETE FROM index_cache"); - this.#db.exec("DELETE FROM stage_metadata"); - this.#db.exec("DELETE FROM task_metadata"); - this.#db.exec("DELETE FROM result_metadata"); - this.#db.exec("COMMIT"); - this.#db.exec("VACUUM"); - - const bytesAfter = this.getDatabaseSize(); - - return bytesBefore - bytesAfter; - } - /** * Checks if the database has any records in any table. * diff --git a/packages/project/test/lib/build/cache/BuildCacheStorage.js b/packages/project/test/lib/build/cache/BuildCacheStorage.js index 875eebddedd..7a3447ff52f 100644 --- a/packages/project/test/lib/build/cache/BuildCacheStorage.js +++ b/packages/project/test/lib/build/cache/BuildCacheStorage.js @@ -455,26 +455,6 @@ test("getDatabaseSize: Returns positive database size", (t) => { t.true(size > 0); }); -test("clearAllRecords: Clears all tables and returns freed size", (t) => { - t.context.storage.putContent("sha256-content", Buffer.from("content")); - t.context.storage.writeIndexCache("project-a", "build-sig", "source", {v: 1}); - t.context.storage.writeStageCache("project-a", "build-sig", "task/minify", "sig-a", {v: 1}); - t.context.storage.writeTaskMetadata("project-a", "build-sig", "minify", "project", {v: 1}); - t.context.storage.writeResultMetadata("project-a", "build-sig", "sig-a", {v: 1}); - - t.true(t.context.storage.hasRecords()); - const freedSize = t.context.storage.clearAllRecords(); - - t.true(Number.isInteger(freedSize)); - t.true(freedSize >= 0); - t.false(t.context.storage.hasRecords()); - t.false(t.context.storage.hasContent("sha256-content")); - t.is(t.context.storage.readIndexCache("project-a", "build-sig", "source"), null); - t.is(t.context.storage.readStageCache("project-a", "build-sig", "task/minify", "sig-a"), null); - t.is(t.context.storage.readTaskMetadata("project-a", "build-sig", "minify", "project"), null); - t.is(t.context.storage.readResultMetadata("project-a", "build-sig", "sig-a"), null); -}); - // ===== Pre-compressed content ===== test("putCompressedContent: Stores pre-compressed data retrievable via readContent", (t) => { From 4b5e2586e4a06dc2446959d81f1d1f94a864afc8 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 23 Jul 2026 08:18:13 +0300 Subject: [PATCH 26/52] fix: Minor Bugs --- packages/project/lib/ui5Framework/cache.js | 40 ++++++----- .../project/test/lib/ui5framework/cache.js | 66 ++++++++++++++++++- 2 files changed, 89 insertions(+), 17 deletions(-) diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index 63085a2db76..7e4ee814f76 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -97,7 +97,8 @@ export default class FrameworkCache { * them in the cleanup summary. * * Deletion failures are swallowed per entry so one stuck directory does not prevent - * the others from being removed. + * the others from being removed. Only entries that were removed successfully are + * returned. * * @public * @param {string} ui5DataDir Resolved absolute path to UI5 data directory @@ -105,17 +106,19 @@ export default class FrameworkCache { */ static async cleanAdditional(ui5DataDir) { const staleDirs = await FrameworkCache.getAdditionalCacheInfo(ui5DataDir); + const removedStaleDirs = []; for (const staleDir of staleDirs) { const staleDirPath = path.join(ui5DataDir, staleDir.path); try { await fs.rm(staleDirPath, {recursive: true, force: true}); + removedStaleDirs.push(staleDir); } catch { // Ignore deletion errors } } - return staleDirs; + return removedStaleDirs; } /** @@ -207,19 +210,26 @@ async function getPackageStats(frameworkDir) { return null; } - const extractSubDir = (dirList) => { - return dirList.filter((e) => e.isDirectory()) - .map((currentDir) => { - try { - return fs.readdir(path.join(currentDir.parentPath, currentDir.name), {withFileTypes: true}); - } catch { - return; - } - }); - }; - - const libDirs = (await Promise.all(extractSubDir(projectDirs))).filter(Boolean).flat(); - const versionDirs = (await Promise.all(extractSubDir(libDirs))).filter(Boolean).flat(); + /** + * Reads direct subdirectories for each given directory entry and flattens the result. + * Any unreadable subdirectory is skipped. + * + * @param {import("node:fs").Dirent[]} dirList + * @returns {Promise} + */ + async function readSubDirectories(dirList) { + const directoryEntries = dirList.filter((entry) => entry.isDirectory()); + const nestedEntries = await Promise.all(directoryEntries.map((currentDir) => { + return fs.readdir( + path.join(currentDir.parentPath, currentDir.name), + {withFileTypes: true} + ).catch(() => undefined); + })); + return nestedEntries.filter(Boolean).flat(); + } + + const libDirs = await readSubDirectories(projectDirs); + const versionDirs = await readSubDirectories(libDirs); const librarySet = new Set(libDirs.map((e) => e.name)); const versionSet = new Set(versionDirs.map((e) => e.name)); diff --git a/packages/project/test/lib/ui5framework/cache.js b/packages/project/test/lib/ui5framework/cache.js index 727139b9d0e..29adb340b43 100644 --- a/packages/project/test/lib/ui5framework/cache.js +++ b/packages/project/test/lib/ui5framework/cache.js @@ -80,6 +80,36 @@ test("getCacheInfo: single library and version", async (t) => { t.is(result.versionCount, 1); }); +test("getCacheInfo: skips unreadable subdirectories without throwing", async (t) => { + const frameworkDir = path.join(t.context.testDir, "framework"); + await mkPackageIn(frameworkDir, "@openui5", "sap.m", "1.120.0"); + await mkPackageIn(frameworkDir, "@sapui5", "sap.ui.core", "1.110.0"); + + const unreadableScopeDir = path.join(frameworkDir, "packages", "@sapui5"); + const readdirStub = sinon.stub().callsFake(async (dirPath, opts) => { + if (dirPath === unreadableScopeDir) { + const err = Object.assign(new Error("EACCES: permission denied, scandir"), {code: "EACCES"}); + throw err; + } + return fs.readdir(dirPath, opts); + }); + + const FrameworkCacheMocked = await esmock.p( + "../../../lib/ui5Framework/cache.js", + {"node:fs/promises": {...fs, readdir: readdirStub}} + ); + + try { + const result = await FrameworkCacheMocked.getCacheInfo(t.context.testDir); + t.truthy(result); + t.is(result.path, "framework"); + t.is(result.libraryCount, 1); + t.is(result.versionCount, 1); + } finally { + esmock.purge(FrameworkCacheMocked); + } +}); + // ─── cleanCache ─────────────────────────────────────────────────────────────── test("cleanCache: returns null for non-existent framework directory", async (t) => { @@ -215,14 +245,46 @@ test("cleanAdditional: orphaned dir deletion failure is non-fatal", async (t) => ); try { - const result = await t.notThrowsAsync(FrameworkCacheMocked.cleanAdditional(t.context.testDir)); - t.truthy(result, "cleanAdditional completes despite orphan deletion failure"); + const result = await FrameworkCacheMocked.cleanAdditional(t.context.testDir); + t.deepEqual(result, [], "failed deletion is excluded from the returned list"); + await t.notThrowsAsync(fs.access(orphanDir), "failed orphan deletion keeps directory on disk"); } finally { esmock.purge(FrameworkCacheMocked); await fs.rm(orphanDir, {recursive: true, force: true}).catch(() => {}); } }); +test("cleanAdditional: returns only successfully removed orphaned dirs", async (t) => { + const orphanOk = path.join(t.context.testDir, "_framework_to_delete_ok"); + const orphanFail = path.join(t.context.testDir, "_framework_to_delete_fail"); + await mkPackageIn(orphanOk, "@openui5", "sap.m", "1.80.0"); + await mkPackageIn(orphanFail, "@openui5", "sap.ui.core", "1.81.0"); + + const rmStub = sinon.stub().callsFake(async (p, opts) => { + if (p === orphanFail) { + throw new Error("simulated deletion failure"); + } + return fs.rm(p, opts); + }); + + const FrameworkCacheMocked = await esmock.p( + "../../../lib/ui5Framework/cache.js", + {"node:fs/promises": {...fs, rm: rmStub}} + ); + + try { + const result = await FrameworkCacheMocked.cleanAdditional(t.context.testDir); + t.is(result.length, 1); + t.is(result[0].path, "_framework_to_delete_ok"); + + await t.throwsAsync(fs.access(orphanOk), {code: "ENOENT"}); + await t.notThrowsAsync(fs.access(orphanFail)); + } finally { + esmock.purge(FrameworkCacheMocked); + await fs.rm(orphanFail, {recursive: true, force: true}).catch(() => {}); + } +}); + test("cleanCache: returns null if framework dir removed between check and rename (ENOENT race)", async (t) => { await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); From 6a3ccc10d1a39b4633b01895d42891bda0ff1166 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 23 Jul 2026 08:39:49 +0300 Subject: [PATCH 27/52] fix: ESLint issues --- packages/cli/test/lib/cli/commands/cache.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 5b6c09415ec..31db522423c 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -445,7 +445,8 @@ test.serial("ui5 cache clean: shows orphaned-only success summary when no active const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("Orphaned UI5 Framework packages"), "Shows orphaned header"); t.true(allOutput.includes("Cleaned Orphaned UI5 Framework packages"), "Success summary mentions orphaned label"); - t.false(allOutput.includes("Removed UI5 Framework packages"), "Does not show main framework removed line when absent"); + t.false(allOutput.includes("Removed UI5 Framework packages"), + "Does not show main framework removed line when absent"); }); test.serial("ui5 cache clean: shows orphaned build cache in pre-confirm and post-clean summary", async (t) => { From a18397a0215acc2f203f9d29d3f7fb69be5733b6 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 23 Jul 2026 09:23:22 +0300 Subject: [PATCH 28/52] refactor: Return statements --- packages/cli/lib/cli/commands/cache.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 86cc7090d9e..c393ebfd8b8 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -198,10 +198,18 @@ async function displayCleanupResult({ // Success summary const cleaned = []; - if (frameworkResult) cleaned.push(LABEL_FRAMEWORK); - if (buildResult) cleaned.push(LABEL_BUILD); - if (orphanedInfoWithAbsPaths?.length > 0) cleaned.push(LABEL_ORPHANED_FRAMEWORK); - if (buildAdditionalResult?.length > 0) cleaned.push(LABEL_ORPHANED_BUILD); + if (frameworkResult) { + cleaned.push(LABEL_FRAMEWORK); + } + if (buildResult) { + cleaned.push(LABEL_BUILD); + } + if (orphanedInfoWithAbsPaths?.length > 0) { + cleaned.push(LABEL_ORPHANED_FRAMEWORK); + } + if (buildAdditionalResult?.length > 0) { + cleaned.push(LABEL_ORPHANED_BUILD); + } process.stderr.write(`\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`); } @@ -292,9 +300,6 @@ async function handleCache(argv) { const orphanedInfoWithAbsPaths = additionalFrameworkResult.map( (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) ); - // Only surface build orphaned data in the summary when it existed before this clean started. - // cleanCache() itself sets the vacuum-pending flag, so cleanAdditional() always fires here — - // but the user should only see "Orphaned build cache" when it was leftover from a prior run. const buildAdditionalResult = preCleanBuildAdditionalInfo.length > 0 ? additionalBuildResult.map((o) => ({...o, absPath: path.join(ui5DataDir, o.path)})) : []; From f4a25d59c9221310115aced94e14c39192accfd3 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 23 Jul 2026 09:46:52 +0300 Subject: [PATCH 29/52] docs: Fix JSDoc comments --- packages/project/lib/ui5Framework/cache.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index 7e4ee814f76..cddbe0cf359 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -214,8 +214,8 @@ async function getPackageStats(frameworkDir) { * Reads direct subdirectories for each given directory entry and flattens the result. * Any unreadable subdirectory is skipped. * - * @param {import("node:fs").Dirent[]} dirList - * @returns {Promise} + * @param {Array} dirList + * @returns {Promise>} */ async function readSubDirectories(dirList) { const directoryEntries = dirList.filter((entry) => entry.isDirectory()); From 59c37f44cfccb9dd925556300a2e238ac1a92c84 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 23 Jul 2026 15:33:50 +0300 Subject: [PATCH 30/52] feat: Add usage warning for cache clean command --- packages/cli/lib/cli/commands/cache.js | 28 +++++++++++++++------ packages/cli/test/lib/cli/commands/cache.js | 20 +++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index c393ebfd8b8..76287ad1a87 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -7,6 +7,20 @@ import Configuration from "@ui5/project/config/Configuration"; import FrameworkCache from "@ui5/project/ui5Framework/cache"; import CacheManager from "@ui5/project/build/cache/CacheManager"; +const LABEL_FRAMEWORK = "UI5 Framework packages"; +const LABEL_BUILD = "Build cache (Db)"; +const LABEL_ORPHANED_FRAMEWORK = "Orphaned UI5 Framework packages"; +const LABEL_ORPHANED_BUILD = "Orphaned build cache (Db)"; +const CACHE_CLEAN_WARNING = + "Only run ui5 cache clean when no UI5 CLI process and no @ui5/* API consumer is actively running."; +const CACHE_CLEAN_WARNING_IMPACT = + "Running ui5 cache clean while ui5 build or ui5 serve is in progress can break the running process " + + "and lead to failed or inconsistent results."; +const CACHE_CLEAN_HELP_USAGE = + `WARNING: ${CACHE_CLEAN_WARNING}\n${CACHE_CLEAN_WARNING_IMPACT}\n\nUsage: ui5 cache clean [options]`; +// Pad main labels to equal width for two-column alignment (orphaned labels are bold headers, not padded) +const LABEL_WIDTH = Math.max(LABEL_FRAMEWORK.length, LABEL_BUILD.length); + const cacheCommand = { command: "cache", describe: "Manage the UI5 CLI cache (downloaded framework packages and build data)", @@ -21,6 +35,7 @@ cacheCommand.builder = function(cli) { handler: handleCache, builder: function(yargs) { return yargs + .usage(CACHE_CLEAN_HELP_USAGE) .option("yes", { alias: "y", describe: "Skip the confirmation prompt, e.g. for use in CI pipelines", @@ -38,13 +53,6 @@ cacheCommand.builder = function(cli) { }); }; -const LABEL_FRAMEWORK = "UI5 Framework packages"; -const LABEL_BUILD = "Build cache (Db)"; -const LABEL_ORPHANED_FRAMEWORK = "Orphaned UI5 Framework packages"; -const LABEL_ORPHANED_BUILD = "Orphaned build cache (Db)"; -// Pad main labels to equal width for two-column alignment (orphaned labels are bold headers, not padded) -const LABEL_WIDTH = Math.max(LABEL_FRAMEWORK.length, LABEL_BUILD.length); - /** * Format a byte size as a human-readable string. * @@ -86,6 +94,11 @@ function padLabel(label) { return label.padEnd(LABEL_WIDTH); } +function displayCacheCleanWarning() { + process.stderr.write(`${chalk.bold.yellow("Warning:")} ${chalk.italic(CACHE_CLEAN_WARNING)}\n`); + process.stderr.write(`${chalk.italic(CACHE_CLEAN_WARNING_IMPACT)}\n\n`); +} + /** * Display information about the cached data that will be removed, * including the absolute paths and details about the framework and build caches. @@ -223,6 +236,7 @@ async function getConfirmation(argv) { if (argv.yes) { return true; } + displayCacheCleanWarning(); const {default: yesno} = await import("yesno"); return yesno({ question: "Do you want to continue? (y/N)", diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 31db522423c..991a1b30e1e 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -21,6 +21,12 @@ const TEST_UI5_DATA_DIR = path.resolve("test-ui5-home"); // Typical framework stub result shape: { path, libraryCount, versionCount } const FRAMEWORK_STUB = {path: "framework", libraryCount: 18, versionCount: 5}; +const WARNING_PREFIX = "Warning:"; +const WARNING_TEXT = + "Only run ui5 cache clean when no UI5 CLI process and no @ui5/* API consumer is actively running."; +const WARNING_IMPACT_TEXT = + "Running ui5 cache clean while ui5 build or ui5 serve is in progress can break the running process " + + "and lead to failed or inconsistent results."; test.beforeEach(async (t) => { t.context.argv = getDefaultArgv(); @@ -85,6 +91,7 @@ test.afterEach.always((t) => { test("Command builder", async (t) => { const cacheModule = await import("../../../../lib/cli/commands/cache.js"); const yargsStub = { + usage: sinon.stub().returnsThis(), option: sinon.stub().returnsThis(), example: sinon.stub().returnsThis(), }; @@ -102,6 +109,9 @@ test("Command builder", async (t) => { t.is(result, cliStub, "Builder returns cli instance"); t.is(cliStub.demandCommand.callCount, 1, "demandCommand called once"); t.is(cliStub.command.callCount, 1, "command called once"); + t.is(yargsStub.usage.callCount, 1, "usage called once for warning help banner"); + t.true(yargsStub.usage.firstCall.args[0].startsWith("WARNING:"), + "usage banner starts with warning"); t.is(yargsStub.option.callCount, 1, "option called for --yes flag"); t.is(yargsStub.example.callCount, 3, "example called 3 times"); }); @@ -214,6 +224,9 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("Checking cache at"), "Prints checking line"); t.true(allOutput.includes(TEST_UI5_DATA_DIR), "Shows resolved ui5DataDir"); + t.true(allOutput.includes(WARNING_PREFIX), "Shows safety warning before interactive confirmation"); + t.true(allOutput.includes(WARNING_TEXT), "Shows safety warning details"); + t.true(allOutput.includes(WARNING_IMPACT_TEXT), "Shows warning impact details"); t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "framework")), "Shows absolute framework path"); t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "buildCache/v0_7")), "Shows absolute build path"); t.true(allOutput.includes("5 versions of 18 libraries"), "Shows library stats format"); @@ -221,6 +234,12 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { t.false(allOutput.includes("7.0 MB"), "Does not show VACUUM-freed size"); t.true(allOutput.includes("Cleaned UI5 Framework packages and Build cache (Db)"), "Shows success summary"); + const warningCall = stderrWriteStub.getCalls().find((call) => { + return call.args[0].includes(WARNING_PREFIX); + }); + t.truthy(warningCall, "Warning line is written to stderr"); + t.true(warningCall.callId < yesnoStub.firstCall.callId, + "Warning is displayed before the confirmation prompt is shown"); }); test.serial("ui5 cache clean: user cancels", async (t) => { @@ -374,6 +393,7 @@ test.serial("ui5 cache clean --yes: skips confirmation prompt", async (t) => { const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("Success"), "Shows success message"); + t.false(allOutput.includes(WARNING_PREFIX), "Does not show warning when --yes is used"); }); test.serial("ui5 cache clean: shows orphaned framework data in pre-confirmation summary", async (t) => { From 06dd017ee255d106a2b42b6666ea96e88ada68b9 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Mon, 27 Jul 2026 12:46:25 +0300 Subject: [PATCH 31/52] refactor: Rename "orphaned" to "stale" --- .../docs/pages/Troubleshooting.md | 6 +-- .../lib/lbt/resources/ResourceCollector.js | 4 +- packages/cli/lib/cli/commands/cache.js | 54 +++++++++---------- packages/cli/test/lib/cli/commands/cache.js | 34 ++++++------ packages/project/lib/ui5Framework/cache.js | 6 +-- 5 files changed, 52 insertions(+), 52 deletions(-) diff --git a/internal/documentation/docs/pages/Troubleshooting.md b/internal/documentation/docs/pages/Troubleshooting.md index 8470a792c1d..3337cc0267a 100644 --- a/internal/documentation/docs/pages/Troubleshooting.md +++ b/internal/documentation/docs/pages/Troubleshooting.md @@ -36,11 +36,11 @@ ui5 cache clean --yes The command removes the following cached data: - **UI5 Framework packages** — downloaded UI5 library files (`~/.ui5/framework/`) -- **Build cache (Db)** — build data (`~/.ui5/buildCache/`) +- **Build cache** — build data (`~/.ui5/buildCache/`) If a previous `ui5 cache clean` was interrupted (e.g. process killed or system crash), the command also detects and removes any leftover data from that interrupted operation, listed as separate entries: -- **Orphaned UI5 Framework packages** — incomplete framework directories left over from a previously interrupted cleanup (`~/.ui5/_framework_to_delete_*/`) -- **Orphaned build cache (Db)** — freed database pages not yet reclaimed during a previously interrupted cleanup +- **Stale UI5 Framework packages** — incomplete framework directories left over from a previously interrupted cleanup (`~/.ui5/_framework_to_delete_*/`) +- **Stale build cache** — freed database pages not yet reclaimed during a previously interrupted cleanup Any required framework dependencies will be re-downloaded during the next UI5 CLI invocation. diff --git a/packages/builder/lib/lbt/resources/ResourceCollector.js b/packages/builder/lib/lbt/resources/ResourceCollector.js index 927ce2dd1b5..93ee479ac03 100644 --- a/packages/builder/lib/lbt/resources/ResourceCollector.js +++ b/packages/builder/lib/lbt/resources/ResourceCollector.js @@ -55,11 +55,11 @@ class ResourceCollector { } /** - * Comma separated list of components to which orphaned resources should be added. + * Comma separated list of components to which stale resources should be added. * * A component and a separated list of resource patterns of orphans that should be added * to the preceding component. - * If no such list is given, any orphaned resource will be added to the component. + * If no such list is given, any stale resource will be added to the component. * The evaluation logic for the filter list is the same as for the filters * parameters: excludes can be denoted with a leading '-' or '!' and order is significant. * Later filters can override the result of earlier ones. diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 76287ad1a87..accf3179acb 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -8,9 +8,9 @@ import FrameworkCache from "@ui5/project/ui5Framework/cache"; import CacheManager from "@ui5/project/build/cache/CacheManager"; const LABEL_FRAMEWORK = "UI5 Framework packages"; -const LABEL_BUILD = "Build cache (Db)"; -const LABEL_ORPHANED_FRAMEWORK = "Orphaned UI5 Framework packages"; -const LABEL_ORPHANED_BUILD = "Orphaned build cache (Db)"; +const LABEL_BUILD = "Build cache"; +const LABEL_STALE_FRAMEWORK = "Stale UI5 Framework packages"; +const LABEL_STALE_BUILD = "Stale build cache"; const CACHE_CLEAN_WARNING = "Only run ui5 cache clean when no UI5 CLI process and no @ui5/* API consumer is actively running."; const CACHE_CLEAN_WARNING_IMPACT = @@ -18,7 +18,7 @@ const CACHE_CLEAN_WARNING_IMPACT = "and lead to failed or inconsistent results."; const CACHE_CLEAN_HELP_USAGE = `WARNING: ${CACHE_CLEAN_WARNING}\n${CACHE_CLEAN_WARNING_IMPACT}\n\nUsage: ui5 cache clean [options]`; -// Pad main labels to equal width for two-column alignment (orphaned labels are bold headers, not padded) +// Pad main labels to equal width for two-column alignment (stale labels are bold headers, not padded) const LABEL_WIDTH = Math.max(LABEL_FRAMEWORK.length, LABEL_BUILD.length); const cacheCommand = { @@ -102,7 +102,7 @@ function displayCacheCleanWarning() { /** * Display information about the cached data that will be removed, * including the absolute paths and details about the framework and build caches. - * Orphaned entries (from previously interrupted cleans) are shown as separate + * Stale entries (from previously interrupted cleans) are shown as separate * items only when present. * * @param {object} data @@ -111,7 +111,7 @@ function displayCacheCleanWarning() { * @param {string|null} data.frameworkAbsPath * @param {string|null} data.buildAbsPath * @param {number} data.buildPreSize - * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfo + * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.staleInfo * @param {Array<{absPath: string, size: number}>} data.buildAdditionalInfo */ async function displayCacheInfo({ @@ -120,7 +120,7 @@ async function displayCacheInfo({ frameworkAbsPath, buildAbsPath, buildPreSize, - orphanedInfo, + staleInfo, buildAdditionalInfo, }) { process.stderr.write(chalk.bold("\nThe following cached data will be removed:\n\n")); @@ -136,18 +136,18 @@ async function displayCacheInfo({ ` ${chalk.yellow("•")} ${padLabel(LABEL_BUILD)} ${buildAbsPath}${detail ? ` (${detail})` : ""}\n` ); } - if (orphanedInfo?.length > 0) { + if (staleInfo?.length > 0) { process.stderr.write( - ` ${chalk.yellow("•")} ${chalk.bold(LABEL_ORPHANED_FRAMEWORK)}\n` + ` ${chalk.yellow("•")} ${chalk.bold(LABEL_STALE_FRAMEWORK)}\n` ); - for (const orphan of orphanedInfo) { + for (const orphan of staleInfo) { const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); } } if (buildAdditionalInfo?.length > 0) { process.stderr.write( - ` ${chalk.yellow("•")} ${chalk.bold(LABEL_ORPHANED_BUILD)}\n` + ` ${chalk.yellow("•")} ${chalk.bold(LABEL_STALE_BUILD)}\n` ); for (const entry of buildAdditionalInfo) { const detail = entry.size > 0 ? formatSize(entry.size) : ""; @@ -159,7 +159,7 @@ async function displayCacheInfo({ /** * Display the result of the cache cleanup operation. - * Orphaned entries are shown as separate items only when present. + * Stale entries are shown as separate items only when present. * * @param {object} data * @param {{libraryCount: number, versionCount: number}|null} data.frameworkResult @@ -167,7 +167,7 @@ async function displayCacheInfo({ * @param {string|null} data.frameworkAbsPath * @param {string|null} data.buildAbsPath * @param {number} data.buildPreSize - * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfoWithAbsPaths + * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.staleInfoWithAbsPaths * @param {Array<{absPath: string, size: number}>} data.buildAdditionalResult */ async function displayCleanupResult({ @@ -176,7 +176,7 @@ async function displayCleanupResult({ frameworkAbsPath, buildAbsPath, buildPreSize, - orphanedInfoWithAbsPaths, + staleInfoWithAbsPaths, buildAdditionalResult, }) { process.stderr.write("\n"); @@ -194,15 +194,15 @@ async function displayCleanupResult({ ` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n` ); } - if (orphanedInfoWithAbsPaths?.length > 0) { - process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_ORPHANED_FRAMEWORK)}\n`); - for (const orphan of orphanedInfoWithAbsPaths) { + if (staleInfoWithAbsPaths?.length > 0) { + process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_STALE_FRAMEWORK)}\n`); + for (const orphan of staleInfoWithAbsPaths) { const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); } } if (buildAdditionalResult?.length > 0) { - process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_ORPHANED_BUILD)}\n`); + process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_STALE_BUILD)}\n`); for (const entry of buildAdditionalResult) { const detail = entry.size > 0 ? formatSize(entry.size) : ""; process.stderr.write(` ${chalk.dim(entry.absPath)}${detail ? ` (freed ${detail})` : ""}\n`); @@ -217,11 +217,11 @@ async function displayCleanupResult({ if (buildResult) { cleaned.push(LABEL_BUILD); } - if (orphanedInfoWithAbsPaths?.length > 0) { - cleaned.push(LABEL_ORPHANED_FRAMEWORK); + if (staleInfoWithAbsPaths?.length > 0) { + cleaned.push(LABEL_STALE_FRAMEWORK); } if (buildAdditionalResult?.length > 0) { - cleaned.push(LABEL_ORPHANED_BUILD); + cleaned.push(LABEL_STALE_BUILD); } process.stderr.write(`\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`); } @@ -263,14 +263,14 @@ async function handleCache(argv) { process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); - const [frameworkInfo, orphanedInfo, buildInfo, buildAdditionalInfo] = await Promise.all([ + const [frameworkInfo, staleInfo, buildInfo, buildAdditionalInfo] = await Promise.all([ FrameworkCache.getCacheInfo(ui5DataDir), FrameworkCache.getAdditionalCacheInfo(ui5DataDir), CacheManager.getCacheInfo(ui5DataDir), CacheManager.getAdditionalCacheInfo(ui5DataDir), ]); - if (!frameworkInfo && !buildInfo && orphanedInfo.length === 0 && buildAdditionalInfo.length === 0) { + if (!frameworkInfo && !buildInfo && staleInfo.length === 0 && buildAdditionalInfo.length === 0) { process.stderr.write("Nothing to clean\n"); return; } @@ -279,7 +279,7 @@ async function handleCache(argv) { const frameworkAbsPath = frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null; const buildAbsPath = buildInfo ? path.join(ui5DataDir, buildInfo.path) : null; const buildPreSize = buildInfo?.size ?? 0; - const preCleanOrphanedInfo = orphanedInfo.map( + const preCleanStaleInfo = staleInfo.map( (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) ); const preCleanBuildAdditionalInfo = buildAdditionalInfo.map( @@ -292,7 +292,7 @@ async function handleCache(argv) { frameworkAbsPath, buildAbsPath, buildPreSize, - orphanedInfo: preCleanOrphanedInfo, + staleInfo: preCleanStaleInfo, buildAdditionalInfo: preCleanBuildAdditionalInfo, }); @@ -311,7 +311,7 @@ async function handleCache(argv) { FrameworkCache.cleanAdditional(ui5DataDir), CacheManager.cleanAdditional(ui5DataDir), ]); - const orphanedInfoWithAbsPaths = additionalFrameworkResult.map( + const staleInfoWithAbsPaths = additionalFrameworkResult.map( (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) ); const buildAdditionalResult = preCleanBuildAdditionalInfo.length > 0 ? @@ -324,7 +324,7 @@ async function handleCache(argv) { frameworkAbsPath, buildAbsPath, buildPreSize, - orphanedInfoWithAbsPaths, + staleInfoWithAbsPaths, buildAdditionalResult, }); } diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 991a1b30e1e..5d7abcc574a 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -232,7 +232,7 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { t.true(allOutput.includes("5 versions of 18 libraries"), "Shows library stats format"); t.true(allOutput.includes("8.0 MB"), "Shows pre-clean build cache size"); t.false(allOutput.includes("7.0 MB"), "Does not show VACUUM-freed size"); - t.true(allOutput.includes("Cleaned UI5 Framework packages and Build cache (Db)"), + t.true(allOutput.includes("Cleaned UI5 Framework packages and Build cache"), "Shows success summary"); const warningCall = stderrWriteStub.getCalls().find((call) => { return call.args[0].includes(WARNING_PREFIX); @@ -276,7 +276,7 @@ test.serial("ui5 cache clean: framework only — formats library stats correctly let allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("5 versions of 18 libraries"), "Shows plural format"); - t.false(allOutput.includes("Build cache (Db)"), "Does not mention build cache"); + t.false(allOutput.includes("Build cache"), "Does not mention build cache"); // Singular stderrWriteStub.resetHistory(); @@ -325,7 +325,7 @@ test.serial("ui5 cache clean: build only", async (t) => { const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.false(allOutput.includes("UI5 Framework packages"), "Does not mention framework"); t.true(allOutput.includes("50.0 KB"), "Shows build cache size"); - t.true(allOutput.includes("Cleaned Build cache (Db)"), "Success mentions build cache only"); + t.true(allOutput.includes("Cleaned Build cache"), "Success mentions build cache only"); }); test.serial("ui5 cache clean: formats byte sizes correctly (< 1 KB)", async (t) => { @@ -396,7 +396,7 @@ test.serial("ui5 cache clean --yes: skips confirmation prompt", async (t) => { t.false(allOutput.includes(WARNING_PREFIX), "Does not show warning when --yes is used"); }); -test.serial("ui5 cache clean: shows orphaned framework data in pre-confirmation summary", async (t) => { +test.serial("ui5 cache clean: shows stale framework data in pre-confirmation summary", async (t) => { const {cache, argv, stderrWriteStub, yesnoStub, frameworkCacheCleanCache, frameworkCacheGetAdditionalCacheInfo} = t.context; @@ -413,12 +413,12 @@ test.serial("ui5 cache clean: shows orphaned framework data in pre-confirmation await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Orphaned UI5 Framework packages"), "Shows orphaned header in pre-confirm summary"); + t.true(allOutput.includes("Stale UI5 Framework packages"), "Shows stale header in pre-confirm summary"); t.true(allOutput.includes("_framework_to_delete_abcd"), "Shows orphaned dir path indented"); t.true(allOutput.includes("2 versions of 5 libraries"), "Shows orphaned dir stats"); }); -test.serial("ui5 cache clean: shows orphaned framework data in post-clean summary", async (t) => { +test.serial("ui5 cache clean: shows stale framework data in post-clean summary", async (t) => { const {cache, argv, stderrWriteStub, frameworkCacheGetAdditionalCacheInfo, frameworkCacheCleanCache, frameworkCacheCleanAdditional} = t.context; @@ -439,12 +439,12 @@ test.serial("ui5 cache clean: shows orphaned framework data in post-clean summar await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Removed Orphaned UI5 Framework packages"), "Shows orphaned header in result"); + t.true(allOutput.includes("Removed Stale UI5 Framework packages"), "Shows stale header in result"); t.true(allOutput.includes("_framework_to_delete_ab12"), "Shows first orphaned dir path indented"); t.true(allOutput.includes("_framework_to_delete_cd34"), "Shows second orphaned dir path indented"); }); -test.serial("ui5 cache clean: shows orphaned-only success summary when no active framework", async (t) => { +test.serial("ui5 cache clean: shows stale-only success summary when no active framework", async (t) => { const {cache, argv, stderrWriteStub, frameworkCacheGetAdditionalCacheInfo, frameworkCacheCleanCache, frameworkCacheCleanAdditional} = t.context; @@ -463,13 +463,13 @@ test.serial("ui5 cache clean: shows orphaned-only success summary when no active await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Orphaned UI5 Framework packages"), "Shows orphaned header"); - t.true(allOutput.includes("Cleaned Orphaned UI5 Framework packages"), "Success summary mentions orphaned label"); + t.true(allOutput.includes("Stale UI5 Framework packages"), "Shows stale header"); + t.true(allOutput.includes("Cleaned Stale UI5 Framework packages"), "Success summary mentions stale label"); t.false(allOutput.includes("Removed UI5 Framework packages"), "Does not show main framework removed line when absent"); }); -test.serial("ui5 cache clean: shows orphaned build cache in pre-confirm and post-clean summary", async (t) => { +test.serial("ui5 cache clean: shows stale build cache in pre-confirm and post-clean summary", async (t) => { const {cache, argv, stderrWriteStub, buildCacheGetAdditionalCacheInfo, buildCacheCleanCache, buildCacheCleanAdditional} = t.context; @@ -488,15 +488,15 @@ test.serial("ui5 cache clean: shows orphaned build cache in pre-confirm and post await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Orphaned build cache (Db)"), "Shows orphaned build cache header"); + t.true(allOutput.includes("Stale build cache"), "Shows stale build cache header"); t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "buildCache/v0_7")), "Shows orphaned build cache path indented"); - t.true(allOutput.includes("Removed Orphaned build cache (Db)"), "Post-clean result shows orphaned build label"); + t.true(allOutput.includes("Removed Stale build cache"), "Post-clean result shows stale build label"); t.true(allOutput.includes("freed 40.0 MB"), "Shows freed size in post-clean result"); - t.true(allOutput.includes("Cleaned Orphaned build cache (Db)"), "Success summary mentions orphaned build cache"); + t.true(allOutput.includes("Cleaned Stale build cache"), "Success summary mentions stale build cache"); }); -test.serial("ui5 cache clean: build cache and orphaned build cache with size 0 omit size detail", async (t) => { +test.serial("ui5 cache clean: build cache and stale build cache with size 0 omit size detail", async (t) => { const {cache, argv, stderrWriteStub, buildCacheGetAdditionalCacheInfo, buildCacheCleanCache, buildCacheCleanAdditional, buildCacheGetCacheInfo} = t.context; @@ -516,7 +516,7 @@ test.serial("ui5 cache clean: build cache and orphaned build cache with size 0 o const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.false(allOutput.includes("0 B"), "Does not show zero size"); - t.true(allOutput.includes("Removed Build cache (Db)"), "Shows build cache result line"); - t.true(allOutput.includes("Removed Orphaned build cache (Db)"), "Shows orphaned build cache result line"); + t.true(allOutput.includes("Removed Build cache"), "Shows build cache result line"); + t.true(allOutput.includes("Removed Stale build cache"), "Shows stale build cache result line"); t.false(allOutput.includes("freed"), "Does not show freed label when size is 0"); }); diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index cddbe0cf359..2958e3e42ae 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -48,7 +48,7 @@ export default class FrameworkCache { /** * Get additional framework cache info. * - * Scans ui5DataDir for orphaned staging directories left behind by previously + * Scans ui5DataDir for stale staging directories left behind by previously * interrupted clean operations (i.e. process killed after rename but before deletion). * Returns stats per orphan without deleting anything. * @@ -89,10 +89,10 @@ export default class FrameworkCache { } /** - * Scans ui5DataDir for orphaned staging directories left behind by previously + * Scans ui5DataDir for stale staging directories left behind by previously * interrupted clean operations (i.e. process killed after rename but before deletion). * - * Returns an array of result objects — one per orphaned directory found — each + * Returns an array of result objects — one per stale directory found — each * containing the path, library count and version count so the caller can include * them in the cleanup summary. * From 227c115ca41d3085f9b14fa80a1385b50b69c1af Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Mon, 27 Jul 2026 13:50:25 +0300 Subject: [PATCH 32/52] refactor: Group cache messages into Active & Stale groups --- packages/cli/lib/cli/commands/cache.js | 194 +-------------- .../lib/cli/commands/helpers/cacheOutput.js | 231 ++++++++++++++++++ packages/cli/test/lib/cli/commands/cache.js | 99 +++++++- 3 files changed, 324 insertions(+), 200 deletions(-) create mode 100644 packages/cli/lib/cli/commands/helpers/cacheOutput.js diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index accf3179acb..49b9dec92ae 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -6,20 +6,12 @@ import baseMiddleware from "../middlewares/base.js"; import Configuration from "@ui5/project/config/Configuration"; import FrameworkCache from "@ui5/project/ui5Framework/cache"; import CacheManager from "@ui5/project/build/cache/CacheManager"; - -const LABEL_FRAMEWORK = "UI5 Framework packages"; -const LABEL_BUILD = "Build cache"; -const LABEL_STALE_FRAMEWORK = "Stale UI5 Framework packages"; -const LABEL_STALE_BUILD = "Stale build cache"; -const CACHE_CLEAN_WARNING = - "Only run ui5 cache clean when no UI5 CLI process and no @ui5/* API consumer is actively running."; -const CACHE_CLEAN_WARNING_IMPACT = - "Running ui5 cache clean while ui5 build or ui5 serve is in progress can break the running process " + - "and lead to failed or inconsistent results."; -const CACHE_CLEAN_HELP_USAGE = - `WARNING: ${CACHE_CLEAN_WARNING}\n${CACHE_CLEAN_WARNING_IMPACT}\n\nUsage: ui5 cache clean [options]`; -// Pad main labels to equal width for two-column alignment (stale labels are bold headers, not padded) -const LABEL_WIDTH = Math.max(LABEL_FRAMEWORK.length, LABEL_BUILD.length); +import { + CACHE_CLEAN_HELP_USAGE, + displayCacheCleanWarning, + displayCacheInfo, + displayCleanupResult, +} from "./helpers/cacheOutput.js"; const cacheCommand = { command: "cache", @@ -52,180 +44,6 @@ cacheCommand.builder = function(cli) { middlewares: [baseMiddleware], }); }; - -/** - * 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 framework cache stats as a human-readable detail string. - * E.g. "1,189 versions of 155 libraries" or "1 version of 1 library". - * - * @param {number} libraryCount - * @param {number} versionCount - * @returns {string} - */ -function formatFrameworkStats(libraryCount, versionCount) { - const v = `${versionCount.toLocaleString("en-US")} ${versionCount === 1 ? "version" : "versions"}`; - const l = `${libraryCount.toLocaleString("en-US")} ${libraryCount === 1 ? "library" : "libraries"}`; - return `${v} of ${l}`; -} - -/** - * Pad a label to the shared column width. - * - * @param {string} label - * @returns {string} - */ -function padLabel(label) { - return label.padEnd(LABEL_WIDTH); -} - -function displayCacheCleanWarning() { - process.stderr.write(`${chalk.bold.yellow("Warning:")} ${chalk.italic(CACHE_CLEAN_WARNING)}\n`); - process.stderr.write(`${chalk.italic(CACHE_CLEAN_WARNING_IMPACT)}\n\n`); -} - -/** - * Display information about the cached data that will be removed, - * including the absolute paths and details about the framework and build caches. - * Stale entries (from previously interrupted cleans) are shown as separate - * items only when present. - * - * @param {object} data - * @param {object|null} data.frameworkInfo - * @param {object|null} data.buildInfo - * @param {string|null} data.frameworkAbsPath - * @param {string|null} data.buildAbsPath - * @param {number} data.buildPreSize - * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.staleInfo - * @param {Array<{absPath: string, size: number}>} data.buildAdditionalInfo - */ -async function displayCacheInfo({ - frameworkInfo, - buildInfo, - frameworkAbsPath, - buildAbsPath, - buildPreSize, - staleInfo, - buildAdditionalInfo, -}) { - process.stderr.write(chalk.bold("\nThe following cached data will be removed:\n\n")); - if (frameworkInfo) { - const detail = formatFrameworkStats(frameworkInfo.libraryCount, frameworkInfo.versionCount); - 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 ? ` (${detail})` : ""}\n` - ); - } - if (staleInfo?.length > 0) { - process.stderr.write( - ` ${chalk.yellow("•")} ${chalk.bold(LABEL_STALE_FRAMEWORK)}\n` - ); - for (const orphan of staleInfo) { - const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); - process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); - } - } - if (buildAdditionalInfo?.length > 0) { - process.stderr.write( - ` ${chalk.yellow("•")} ${chalk.bold(LABEL_STALE_BUILD)}\n` - ); - for (const entry of buildAdditionalInfo) { - const detail = entry.size > 0 ? formatSize(entry.size) : ""; - process.stderr.write(` ${chalk.dim(entry.absPath)}${detail ? ` (${detail})` : ""}\n`); - } - } - process.stderr.write("\n"); -} - -/** - * Display the result of the cache cleanup operation. - * Stale entries are shown as separate items only when present. - * - * @param {object} data - * @param {{libraryCount: number, versionCount: number}|null} data.frameworkResult - * @param {object|null} data.buildResult - * @param {string|null} data.frameworkAbsPath - * @param {string|null} data.buildAbsPath - * @param {number} data.buildPreSize - * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.staleInfoWithAbsPaths - * @param {Array<{absPath: string, size: number}>} data.buildAdditionalResult - */ -async function displayCleanupResult({ - frameworkResult, - buildResult, - frameworkAbsPath, - buildAbsPath, - buildPreSize, - staleInfoWithAbsPaths, - buildAdditionalResult, -}) { - process.stderr.write("\n"); - if (frameworkResult && frameworkAbsPath) { - const detail = formatFrameworkStats(frameworkResult.libraryCount, frameworkResult.versionCount); - process.stderr.write( - `${chalk.green("✓")} Removed ${chalk.bold(LABEL_FRAMEWORK)}` + - ` (${frameworkAbsPath} · ${detail})\n` - ); - } - if (buildResult) { - const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; - process.stderr.write( - `${chalk.green("✓")} Removed ${chalk.bold(LABEL_BUILD)}` + - ` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n` - ); - } - if (staleInfoWithAbsPaths?.length > 0) { - process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_STALE_FRAMEWORK)}\n`); - for (const orphan of staleInfoWithAbsPaths) { - const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); - process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); - } - } - if (buildAdditionalResult?.length > 0) { - process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_STALE_BUILD)}\n`); - for (const entry of buildAdditionalResult) { - const detail = entry.size > 0 ? formatSize(entry.size) : ""; - process.stderr.write(` ${chalk.dim(entry.absPath)}${detail ? ` (freed ${detail})` : ""}\n`); - } - } - - // Success summary - const cleaned = []; - if (frameworkResult) { - cleaned.push(LABEL_FRAMEWORK); - } - if (buildResult) { - cleaned.push(LABEL_BUILD); - } - if (staleInfoWithAbsPaths?.length > 0) { - cleaned.push(LABEL_STALE_FRAMEWORK); - } - if (buildAdditionalResult?.length > 0) { - cleaned.push(LABEL_STALE_BUILD); - } - process.stderr.write(`\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`); -} - /** * Prompt the user for confirmation before proceeding with cache cleanup. * diff --git a/packages/cli/lib/cli/commands/helpers/cacheOutput.js b/packages/cli/lib/cli/commands/helpers/cacheOutput.js new file mode 100644 index 00000000000..61a8b427794 --- /dev/null +++ b/packages/cli/lib/cli/commands/helpers/cacheOutput.js @@ -0,0 +1,231 @@ +import chalk from "chalk"; +import process from "node:process"; + +const GROUP_FRAMEWORK = "Framework"; +const GROUP_BUILD = "Build"; +const SECTION_ACTIVE_CACHE = "Active Cache"; +const SECTION_STALE_CACHE = "Stale Cache"; + +const CACHE_CLEAN_WARNING = + "Only run ui5 cache clean when no UI5 CLI process and no @ui5/* API consumer is actively running."; +const CACHE_CLEAN_WARNING_IMPACT = + "Running ui5 cache clean while ui5 build or ui5 serve is in progress can break the running process " + + "and lead to failed or inconsistent results."; + +export const CACHE_CLEAN_HELP_USAGE = + `WARNING: ${CACHE_CLEAN_WARNING}\n${CACHE_CLEAN_WARNING_IMPACT}\n\nUsage: ui5 cache clean [options]`; + +const PREVIEW_MARKER = chalk.yellow("•"); +const SUCCESS_MARKER = chalk.green("✓"); +const ITEM_DIVIDER = chalk.dim("·"); + +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`; +} + +function formatFrameworkStats(libraryCount, versionCount) { + const v = `${versionCount.toLocaleString("en-US")} ${versionCount === 1 ? "version" : "versions"}`; + const l = `${libraryCount.toLocaleString("en-US")} ${libraryCount === 1 ? "library" : "libraries"}`; + return `${v} of ${l}`; +} + +function writeSectionHeader(title) { + process.stderr.write(` ${chalk.bold.cyan(title)}\n`); +} + +function writeCategoryHeader(title) { + process.stderr.write(` ${chalk.bold(title)}\n`); +} + +function writePreviewItem(absPath, detail) { + process.stderr.write( + ` ${PREVIEW_MARKER} ${chalk.dim(absPath)}` + + `${detail ? ` ${ITEM_DIVIDER} ${detail}` : ""}\n` + ); +} + +function writeCleanupItem(absPath, detail) { + process.stderr.write( + ` ${SUCCESS_MARKER} Removed ${chalk.dim(absPath)}` + + `${detail ? ` ${ITEM_DIVIDER} ${detail}` : ""}\n` + ); +} + +function writeGroupedSections(sections, itemWriter) { + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + if (i > 0) { + process.stderr.write("\n"); + } + writeSectionHeader(section.title); + for (let j = 0; j < section.categories.length; j++) { + const category = section.categories[j]; + writeCategoryHeader(category.title); + for (const item of category.items) { + itemWriter(item); + } + } + } +} + +export function displayCacheCleanWarning() { + process.stderr.write(`${chalk.bold.yellow("Warning:")} ${chalk.italic(CACHE_CLEAN_WARNING)}\n`); + process.stderr.write(`${chalk.italic(CACHE_CLEAN_WARNING_IMPACT)}\n\n`); +} + +/** + * Display information about the cached data that will be removed. + * Entries are grouped by active and stale cache data. + * + * @param {object} data + * @param {object|null} data.frameworkInfo + * @param {object|null} data.buildInfo + * @param {string|null} data.frameworkAbsPath + * @param {string|null} data.buildAbsPath + * @param {number} data.buildPreSize + * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.staleInfo + * @param {Array<{absPath: string, size: number}>} data.buildAdditionalInfo + */ +export function displayCacheInfo({ + frameworkInfo, + buildInfo, + frameworkAbsPath, + buildAbsPath, + buildPreSize, + staleInfo, + buildAdditionalInfo, +}) { + const sections = []; + + if (frameworkInfo || buildInfo) { + const activeCategories = []; + if (frameworkInfo) { + const detail = formatFrameworkStats(frameworkInfo.libraryCount, frameworkInfo.versionCount); + activeCategories.push({ + title: GROUP_FRAMEWORK, + items: [{absPath: frameworkAbsPath, detail}], + }); + } + if (buildInfo) { + const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; + activeCategories.push({ + title: GROUP_BUILD, + items: [{absPath: buildAbsPath, detail}], + }); + } + sections.push({title: SECTION_ACTIVE_CACHE, categories: activeCategories}); + } + + if (staleInfo?.length > 0 || buildAdditionalInfo?.length > 0) { + const staleCategories = []; + if (staleInfo.length > 0) { + const items = []; + for (const staleEntry of staleInfo) { + const detail = formatFrameworkStats(staleEntry.libraryCount, staleEntry.versionCount); + items.push({absPath: staleEntry.absPath, detail}); + } + staleCategories.push({title: GROUP_FRAMEWORK, items}); + } + if (buildAdditionalInfo.length > 0) { + const items = []; + for (const buildEntry of buildAdditionalInfo) { + const detail = buildEntry.size > 0 ? formatSize(buildEntry.size) : ""; + items.push({absPath: buildEntry.absPath, detail}); + } + staleCategories.push({title: GROUP_BUILD, items}); + } + sections.push({title: SECTION_STALE_CACHE, categories: staleCategories}); + } + + process.stderr.write(`\n${chalk.bold("The following cached data will be removed:")}\n`); + process.stderr.write("\n"); + writeGroupedSections(sections, ({absPath, detail}) => { + writePreviewItem(absPath, detail); + }); + process.stderr.write("\n"); +} + +/** + * Display the result of the cache cleanup operation, grouped by active and stale cache. + * + * @param {object} data + * @param {{libraryCount: number, versionCount: number}|null} data.frameworkResult + * @param {object|null} data.buildResult + * @param {string|null} data.frameworkAbsPath + * @param {string|null} data.buildAbsPath + * @param {number} data.buildPreSize + * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.staleInfoWithAbsPaths + * @param {Array<{absPath: string, size: number}>} data.buildAdditionalResult + */ +export function displayCleanupResult({ + frameworkResult, + buildResult, + frameworkAbsPath, + buildAbsPath, + buildPreSize, + staleInfoWithAbsPaths, + buildAdditionalResult, +}) { + process.stderr.write(`\n${chalk.bold("Cleanup result:")}\n`); + + const sections = []; + + if (frameworkResult || buildResult) { + const activeCategories = []; + if (frameworkResult && frameworkAbsPath) { + const detail = formatFrameworkStats(frameworkResult.libraryCount, frameworkResult.versionCount); + activeCategories.push({ + title: GROUP_FRAMEWORK, + items: [{absPath: frameworkAbsPath, detail}], + }); + } + if (buildResult) { + const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; + activeCategories.push({ + title: GROUP_BUILD, + items: [{absPath: buildAbsPath, detail}], + }); + } + sections.push({title: SECTION_ACTIVE_CACHE, categories: activeCategories}); + } + + if (staleInfoWithAbsPaths?.length > 0 || buildAdditionalResult?.length > 0) { + const staleCategories = []; + if (staleInfoWithAbsPaths.length > 0) { + const items = []; + for (const staleEntry of staleInfoWithAbsPaths) { + const detail = formatFrameworkStats(staleEntry.libraryCount, staleEntry.versionCount); + items.push({absPath: staleEntry.absPath, detail}); + } + staleCategories.push({title: GROUP_FRAMEWORK, items}); + } + if (buildAdditionalResult.length > 0) { + const items = []; + for (const buildEntry of buildAdditionalResult) { + const detail = buildEntry.size > 0 ? `freed ${formatSize(buildEntry.size)}` : ""; + items.push({absPath: buildEntry.absPath, detail}); + } + staleCategories.push({title: GROUP_BUILD, items}); + } + sections.push({title: SECTION_STALE_CACHE, categories: staleCategories}); + } + + process.stderr.write("\n"); + writeGroupedSections(sections, ({absPath, detail}) => { + writeCleanupItem(absPath, detail); + }); + + const cleanedSections = sections.map((section) => { + const categories = section.categories.map((category) => category.title).join(" and "); + return `${section.title} (${categories})`; + }); + + process.stderr.write(`\n${chalk.green("Success:")} Cleaned ${cleanedSections.join(" and ")}\n`); +} diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 5d7abcc574a..3e912d0474a 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -27,6 +27,8 @@ const WARNING_TEXT = const WARNING_IMPACT_TEXT = "Running ui5 cache clean while ui5 build or ui5 serve is in progress can break the running process " + "and lead to failed or inconsistent results."; +const ACTIVE_CACHE_HEADER = "Active Cache"; +const STALE_CACHE_HEADER = "Stale Cache"; test.beforeEach(async (t) => { t.context.argv = getDefaultArgv(); @@ -198,6 +200,8 @@ test.serial("ui5 cache clean: nothing to clean", async (t) => { const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("Checking cache at"), "Prints checking line"); t.true(allOutput.includes("Nothing to clean"), "Prints nothing to clean"); + t.false(allOutput.includes(ACTIVE_CACHE_HEADER), "Does not show Active Cache group when nothing can be cleaned"); + t.false(allOutput.includes(STALE_CACHE_HEADER), "Does not show Stale Cache group when nothing can be cleaned"); t.is(frameworkCacheCleanCache.callCount, 0, "frameworkCache.cleanCache not called"); t.is(buildCacheCleanCache.callCount, 0, "buildCache.cleanCache not called"); }); @@ -232,7 +236,7 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { t.true(allOutput.includes("5 versions of 18 libraries"), "Shows library stats format"); t.true(allOutput.includes("8.0 MB"), "Shows pre-clean build cache size"); t.false(allOutput.includes("7.0 MB"), "Does not show VACUUM-freed size"); - t.true(allOutput.includes("Cleaned UI5 Framework packages and Build cache"), + t.true(allOutput.includes("Cleaned Active Cache (Framework and Build)"), "Shows success summary"); const warningCall = stderrWriteStub.getCalls().find((call) => { return call.args[0].includes(WARNING_PREFIX); @@ -323,9 +327,9 @@ test.serial("ui5 cache clean: build only", async (t) => { await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.false(allOutput.includes("UI5 Framework packages"), "Does not mention framework"); + t.false(allOutput.includes("Framework cache"), "Does not mention framework"); t.true(allOutput.includes("50.0 KB"), "Shows build cache size"); - t.true(allOutput.includes("Cleaned Build cache"), "Success mentions build cache only"); + t.true(allOutput.includes("Cleaned Active Cache (Build)"), "Success mentions active build group"); }); test.serial("ui5 cache clean: formats byte sizes correctly (< 1 KB)", async (t) => { @@ -413,7 +417,8 @@ test.serial("ui5 cache clean: shows stale framework data in pre-confirmation sum await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Stale UI5 Framework packages"), "Shows stale header in pre-confirm summary"); + t.true(allOutput.includes("Stale Cache"), "Shows stale cache group in pre-confirm summary"); + t.true(allOutput.includes("Framework"), "Shows framework subgroup in pre-confirm summary"); t.true(allOutput.includes("_framework_to_delete_abcd"), "Shows orphaned dir path indented"); t.true(allOutput.includes("2 versions of 5 libraries"), "Shows orphaned dir stats"); }); @@ -439,9 +444,17 @@ test.serial("ui5 cache clean: shows stale framework data in post-clean summary", await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Removed Stale UI5 Framework packages"), "Shows stale header in result"); + t.true(allOutput.includes("Cleanup result:"), "Shows cleanup result heading"); + t.true(allOutput.includes("Stale Cache"), "Shows stale cache group in result"); + t.true(allOutput.includes("Framework"), "Shows framework subgroup in result"); t.true(allOutput.includes("_framework_to_delete_ab12"), "Shows first orphaned dir path indented"); t.true(allOutput.includes("_framework_to_delete_cd34"), "Shows second orphaned dir path indented"); + const summaryLine = allOutput.split("\n").find((line) => line.includes("Success:")); + t.truthy(summaryLine, "Output includes success summary line"); + t.true(summaryLine.includes("Active Cache (Framework) and Stale Cache (Framework)"), + "Summary line distinguishes active and stale framework groups"); + t.false(summaryLine.includes("Stale Cache (Framework and Framework)"), + "Summary line does not duplicate framework subgroup within stale section"); }); test.serial("ui5 cache clean: shows stale-only success summary when no active framework", async (t) => { @@ -463,8 +476,10 @@ test.serial("ui5 cache clean: shows stale-only success summary when no active fr await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Stale UI5 Framework packages"), "Shows stale header"); - t.true(allOutput.includes("Cleaned Stale UI5 Framework packages"), "Success summary mentions stale label"); + t.true(allOutput.includes("Stale Cache"), "Shows stale cache group"); + t.true(allOutput.includes("Framework"), "Shows framework subgroup"); + t.true(allOutput.includes("Cleaned Stale Cache (Framework)"), + "Success summary mentions stale framework group"); t.false(allOutput.includes("Removed UI5 Framework packages"), "Does not show main framework removed line when absent"); }); @@ -488,12 +503,13 @@ test.serial("ui5 cache clean: shows stale build cache in pre-confirm and post-cl await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Stale build cache"), "Shows stale build cache header"); + t.true(allOutput.includes("Stale Cache"), "Shows stale cache group"); + t.true(allOutput.includes("Build"), "Shows build subgroup"); t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "buildCache/v0_7")), "Shows orphaned build cache path indented"); - t.true(allOutput.includes("Removed Stale build cache"), "Post-clean result shows stale build label"); + t.true(allOutput.includes("Removed"), "Post-clean result shows removed entries"); t.true(allOutput.includes("freed 40.0 MB"), "Shows freed size in post-clean result"); - t.true(allOutput.includes("Cleaned Stale build cache"), "Success summary mentions stale build cache"); + t.true(allOutput.includes("Cleaned Stale Cache (Build)"), "Success summary mentions stale build group"); }); test.serial("ui5 cache clean: build cache and stale build cache with size 0 omit size detail", async (t) => { @@ -516,7 +532,66 @@ test.serial("ui5 cache clean: build cache and stale build cache with size 0 omit const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.false(allOutput.includes("0 B"), "Does not show zero size"); - t.true(allOutput.includes("Removed Build cache"), "Shows build cache result line"); - t.true(allOutput.includes("Removed Stale build cache"), "Shows stale build cache result line"); + t.true(allOutput.includes("Build"), "Shows build subgroup"); + t.true(allOutput.includes("Removed"), "Shows removed result lines"); t.false(allOutput.includes("freed"), "Does not show freed label when size is 0"); }); + +test.serial("ui5 cache clean: pre-clean summary shows only Active Cache group", async (t) => { + const {cache, argv, stderrWriteStub, yesnoStub} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + t.context.buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 2 * 1024 * 1024}); + t.context.frameworkCacheGetAdditionalCacheInfo.resolves([]); + t.context.buildCacheGetAdditionalCacheInfo.resolves([]); + yesnoStub.resolves(false); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes(ACTIVE_CACHE_HEADER), "Shows Active Cache group header"); + t.false(allOutput.includes(STALE_CACHE_HEADER), "Does not show Stale Cache group header when no stale entries exist"); +}); + +test.serial("ui5 cache clean: pre-clean summary shows only Stale Cache group", async (t) => { + const {cache, argv, stderrWriteStub, yesnoStub} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + t.context.buildCacheGetCacheInfo.resolves(null); + t.context.frameworkCacheGetAdditionalCacheInfo.resolves([ + {path: "_framework_to_delete_abcd", libraryCount: 4, versionCount: 2}, + ]); + t.context.buildCacheGetAdditionalCacheInfo.resolves([ + {path: "buildCache/v0_7", size: 5 * 1024 * 1024}, + ]); + yesnoStub.resolves(false); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.false(allOutput.includes(ACTIVE_CACHE_HEADER), "Does not show Active Cache group header when no active entries exist"); + t.true(allOutput.includes(STALE_CACHE_HEADER), "Shows Stale Cache group header"); +}); + +test.serial("ui5 cache clean: pre-clean summary shows both groups when active and stale entries exist", async (t) => { + const {cache, argv, stderrWriteStub, yesnoStub} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + t.context.buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 6 * 1024 * 1024}); + t.context.frameworkCacheGetAdditionalCacheInfo.resolves([ + {path: "_framework_to_delete_xy12", libraryCount: 3, versionCount: 1}, + ]); + t.context.buildCacheGetAdditionalCacheInfo.resolves([ + {path: "buildCache/v0_6", size: 1 * 1024 * 1024}, + ]); + yesnoStub.resolves(false); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes(ACTIVE_CACHE_HEADER), "Shows Active Cache group header"); + t.true(allOutput.includes(STALE_CACHE_HEADER), "Shows Stale Cache group header"); +}); From 524283b79a6237f9c3fbd39de61e071a0137bc78 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Mon, 27 Jul 2026 15:41:15 +0300 Subject: [PATCH 33/52] fix: ESLint issues --- packages/cli/test/lib/cli/commands/cache.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 3e912d0474a..c2f712db10f 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -551,7 +551,8 @@ test.serial("ui5 cache clean: pre-clean summary shows only Active Cache group", const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes(ACTIVE_CACHE_HEADER), "Shows Active Cache group header"); - t.false(allOutput.includes(STALE_CACHE_HEADER), "Does not show Stale Cache group header when no stale entries exist"); + t.false(allOutput.includes(STALE_CACHE_HEADER), + "Does not show Stale Cache group header when no stale entries exist"); }); test.serial("ui5 cache clean: pre-clean summary shows only Stale Cache group", async (t) => { @@ -571,7 +572,8 @@ test.serial("ui5 cache clean: pre-clean summary shows only Stale Cache group", a await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.false(allOutput.includes(ACTIVE_CACHE_HEADER), "Does not show Active Cache group header when no active entries exist"); + t.false(allOutput.includes(ACTIVE_CACHE_HEADER), + "Does not show Active Cache group header when no active entries exist"); t.true(allOutput.includes(STALE_CACHE_HEADER), "Shows Stale Cache group header"); }); From e664eec00304e67d1a10046c2f8577485e393a3d Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 28 Jul 2026 11:24:54 +0300 Subject: [PATCH 34/52] refactor: DRY for data tables --- packages/project/lib/build/cache/BuildCacheStorage.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/project/lib/build/cache/BuildCacheStorage.js b/packages/project/lib/build/cache/BuildCacheStorage.js index 425c1d02d77..13fb50c491a 100644 --- a/packages/project/lib/build/cache/BuildCacheStorage.js +++ b/packages/project/lib/build/cache/BuildCacheStorage.js @@ -9,6 +9,9 @@ const log = getLogger("build:cache:BuildCacheStorage"); const METADATA_COMPRESSION_THRESHOLD = 4096; const CONTENT_COMPRESSION_THRESHOLD = 128; +/** All live data table names */ +const DATA_TABLES = ["content", "index_cache", "stage_metadata", "task_metadata", "result_metadata"]; + /** * Unified SQLite-backed storage for the build cache * @@ -522,8 +525,7 @@ export default class BuildCacheStorage { * @returns {boolean} True if there are any records */ hasRecords() { - const tables = ["content", "index_cache", "stage_metadata", "task_metadata", "result_metadata"]; - for (const table of tables) { + for (const table of DATA_TABLES) { const {is_populated: isPopulated} = this.#db.prepare(`SELECT EXISTS(SELECT 1 FROM ${table} LIMIT 1) as is_populated`).get(); if (isPopulated) { @@ -545,12 +547,11 @@ export default class BuildCacheStorage { * @returns {number} Database size in bytes before the drop (pending reclamation after vacuum) */ dropAllRecords() { - const tables = ["content", "index_cache", "stage_metadata", "task_metadata", "result_metadata"]; const bytesBefore = this.getDatabaseSize(); this.#db.exec("BEGIN"); try { - for (const table of tables) { + for (const table of DATA_TABLES) { this.#db.exec(`DROP TABLE ${table}`); } this.#createTables(); From bc3d34d03c3f9d13fe0452e3a81d0c7042728d8d Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 28 Jul 2026 11:57:46 +0300 Subject: [PATCH 35/52] refactor: Consolidate common logic in CacheManager --- .../project/lib/build/cache/CacheManager.js | 111 +++++++----------- 1 file changed, 40 insertions(+), 71 deletions(-) diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index 2cbc06e1bae..0b7621b268e 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -356,33 +356,41 @@ export default class CacheManager { } /** - * Get build cache info for the current version. + * Opens a BuildCacheStorage for the versioned build cache directory. * - * @public - * @static - * @param {string} ui5DataDir Resolved absolute path to UI5 data directory - * @returns {Promise<{path: string, size: number}|null>} Build cache info or null + * @param {string} ui5DataDir + * @param {*} defaultValue Value to return when the database is not available + * @param {Function} fn Operation to run against the open storage + * @returns {Promise<*>} */ - static async getCacheInfo(ui5DataDir) { + static async #withStorage(ui5DataDir, defaultValue, fn) { const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); - const isAvailable = await CacheManager.#isCacheDbAvailable(dbDir); - if (!isAvailable) { - return null; + if (!await CacheManager.#isCacheDbAvailable(dbDir)) { + return defaultValue; } - const storage = new BuildCacheStorage(dbDir); try { - if (storage.hasRecords()) { - const size = storage.getDatabaseSize(); - return { - path: `buildCache/${CACHE_VERSION}`, - size, - }; - } + return fn(storage); } finally { storage.close(); } - return null; + } + + /** + * Get build cache info for the current version. + * + * @public + * @static + * @param {string} ui5DataDir Resolved absolute path to UI5 data directory + * @returns {Promise<{path: string, size: number}|null>} Build cache info or null + */ + static getCacheInfo(ui5DataDir) { + return CacheManager.#withStorage(ui5DataDir, null, (storage) => { + if (!storage.hasRecords()) { + return null; + } + return {path: `buildCache/${CACHE_VERSION}`, size: storage.getDatabaseSize()}; + }); } /** @@ -395,26 +403,13 @@ export default class CacheManager { * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise<{path: string, size: number}|null>} Removal result (size = bytes pending reclamation) or null */ - static async cleanCache(ui5DataDir) { - const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); - const isAvailable = await CacheManager.#isCacheDbAvailable(dbDir); - if (!isAvailable) { - return null; - } - - const storage = new BuildCacheStorage(dbDir); - try { - if (storage.hasRecords()) { - const bytesBefore = storage.dropAllRecords(); - return { - path: `buildCache/${CACHE_VERSION}`, - size: bytesBefore, - }; + static cleanCache(ui5DataDir) { + return CacheManager.#withStorage(ui5DataDir, null, (storage) => { + if (!storage.hasRecords()) { + return null; } - } finally { - storage.close(); - } - return null; + return {path: `buildCache/${CACHE_VERSION}`, size: storage.dropAllRecords()}; + }); } /** @@ -426,26 +421,13 @@ export default class CacheManager { * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise>} Cleaned entries, or empty array if nothing to reclaim */ - static async cleanAdditional(ui5DataDir) { - const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); - const isAvailable = await CacheManager.#isCacheDbAvailable(dbDir); - if (!isAvailable) { - return []; - } - - const storage = new BuildCacheStorage(dbDir); - try { + static cleanAdditional(ui5DataDir) { + return CacheManager.#withStorage(ui5DataDir, [], (storage) => { if (!storage.hasVacuumPending()) { return []; } - const freedSize = storage.vacuum(); - return [{ - path: `buildCache/${CACHE_VERSION}`, - size: freedSize, - }]; - } finally { - storage.close(); - } + return [{path: `buildCache/${CACHE_VERSION}`, size: storage.vacuum()}]; + }); } /** @@ -457,25 +439,12 @@ export default class CacheManager { * @param {string} ui5DataDir Resolved absolute path to UI5 data directory * @returns {Promise>} Pending entries, or empty array if none */ - static async getAdditionalCacheInfo(ui5DataDir) { - const dbDir = path.join(ui5DataDir, "buildCache", CACHE_VERSION); - const isAvailable = await CacheManager.#isCacheDbAvailable(dbDir); - if (!isAvailable) { - return []; - } - - const storage = new BuildCacheStorage(dbDir); - try { + static getAdditionalCacheInfo(ui5DataDir) { + return CacheManager.#withStorage(ui5DataDir, [], (storage) => { if (!storage.hasVacuumPending()) { return []; } - const size = storage.getDatabaseSize(); - return [{ - path: `buildCache/${CACHE_VERSION}`, - size, - }]; - } finally { - storage.close(); - } + return [{path: `buildCache/${CACHE_VERSION}`, size: storage.getDatabaseSize()}]; + }); } } From 5e0fc2b119c154047f0ecaa30d4e9c0914bf9a51 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 28 Jul 2026 13:05:43 +0300 Subject: [PATCH 36/52] refactor: Export cache clean modules for internal (@ui5/cli) usage We need to export the cache clean modules for internal usage in the @ui5/cli package. This functionality needs to stay internal to the project and that's why we are not exposing the JSDoc API for these modules. --- packages/cli/lib/cli/commands/cache.js | 4 ++-- packages/cli/test/lib/cli/commands/cache.js | 4 ++-- packages/project/lib/build/cache/CacheManager.js | 2 ++ packages/project/lib/ui5Framework/cache.js | 10 +++------- packages/project/package.json | 4 ++-- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 49b9dec92ae..3efb91985ab 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -4,8 +4,8 @@ import os from "node:os"; import process from "node:process"; import baseMiddleware from "../middlewares/base.js"; import Configuration from "@ui5/project/config/Configuration"; -import FrameworkCache from "@ui5/project/ui5Framework/cache"; -import CacheManager from "@ui5/project/build/cache/CacheManager"; +import FrameworkCache from "@ui5/project/internal/ui5Framework/cache"; +import CacheManager from "@ui5/project/internal/cache/CacheManager"; import { CACHE_CLEAN_HELP_USAGE, displayCacheCleanWarning, diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index c2f712db10f..c0388f087c2 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -59,7 +59,7 @@ test.beforeEach(async (t) => { fromFile: t.context.configurationFromFileStub, }, }, - "@ui5/project/ui5Framework/cache": { + "@ui5/project/internal/ui5Framework/cache": { default: class { static getCacheInfo = t.context.frameworkCacheGetCacheInfo; static cleanCache = t.context.frameworkCacheCleanCache; @@ -67,7 +67,7 @@ test.beforeEach(async (t) => { static getAdditionalCacheInfo = t.context.frameworkCacheGetAdditionalCacheInfo; } }, - "@ui5/project/build/cache/CacheManager": { + "@ui5/project/internal/cache/CacheManager": { default: class { static getCacheInfo = t.context.buildCacheGetCacheInfo; static cleanCache = t.context.buildCacheCleanCache; diff --git a/packages/project/lib/build/cache/CacheManager.js b/packages/project/lib/build/cache/CacheManager.js index 0b7621b268e..3bdb1fb5cce 100644 --- a/packages/project/lib/build/cache/CacheManager.js +++ b/packages/project/lib/build/cache/CacheManager.js @@ -32,6 +32,8 @@ const CACHE_VERSION = "v0_7"; * - Configurable cache location via UI5_DATA_DIR or configuration * - SQLite-backed storage for fast read/write operations * + * @ignore Do not expose this class in the public API documentation. + * It is only used internally by the CLI. * @class */ export default class CacheManager { diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index 2958e3e42ae..c959c828f76 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -2,12 +2,6 @@ import fs from "node:fs/promises"; import path from "node:path"; import {getRandomValues} from "node:crypto"; -/** - * Utilities for cleaning the UI5 framework cache. - * - * @public - * @module @ui5/project/ui5Framework/cache - */ const FRAMEWORK_DIR_NAME = "framework"; @@ -21,7 +15,9 @@ const STAGING_DIR_PREFIX = "_framework_to_delete_"; /** * Provides static utilities for inspecting and cleaning the UI5 framework cache. * - * @public + * @ignore Do not expose this class in the public API documentation. + * It is only used internally by the CLI. + * @class */ export default class FrameworkCache { /** diff --git a/packages/project/package.json b/packages/project/package.json index 89a32ceeb2c..26866cab672 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -18,16 +18,16 @@ ], "type": "module", "exports": { + "./internal/cache/CacheManager": "./lib/build/cache/CacheManager.js", + "./internal/ui5Framework/cache": "./lib/ui5Framework/cache.js", "./config/Configuration": "./lib/config/Configuration.js", "./build/cache/Cache": "./lib/build/cache/Cache.js", - "./build/cache/CacheManager": "./lib/build/cache/CacheManager.js", "./specifications/Specification": "./lib/specifications/Specification.js", "./specifications/SpecificationVersion": "./lib/specifications/SpecificationVersion.js", "./ui5Framework/Sapui5MavenSnapshotResolver": "./lib/ui5Framework/Sapui5MavenSnapshotResolver.js", "./ui5Framework/Openui5Resolver": "./lib/ui5Framework/Openui5Resolver.js", "./ui5Framework/Sapui5Resolver": "./lib/ui5Framework/Sapui5Resolver.js", "./ui5Framework/maven/SnapshotCache": "./lib/ui5Framework/maven/SnapshotCache.js", - "./ui5Framework/cache": "./lib/ui5Framework/cache.js", "./validation/validator": "./lib/validation/validator.js", "./validation/ValidationError": "./lib/validation/ValidationError.js", "./graph/ProjectGraph": "./lib/graph/ProjectGraph.js", From cdaec3b24e2a1ece28281962fb3889af344b55ba Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 28 Jul 2026 15:25:01 +0300 Subject: [PATCH 37/52] refactor: Rename ambiguous const --- packages/project/lib/ui5Framework/cache.js | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index c959c828f76..7a50c7688fa 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -6,11 +6,11 @@ import {getRandomValues} from "node:crypto"; const FRAMEWORK_DIR_NAME = "framework"; /** - * Prefix used for staging directories created during an atomic framework cache clean. + * Prefix used for directories created during an atomic framework cache clean. * The directory is renamed to this prefix + a random hex suffix before deletion so that * the original path is immediately removed and the deletion can proceed outside the rename. */ -const STAGING_DIR_PREFIX = "_framework_to_delete_"; +const PENDING_REMOVAL_DIR_PREFIX = "_framework_to_delete_"; /** * Provides static utilities for inspecting and cleaning the UI5 framework cache. @@ -44,7 +44,7 @@ export default class FrameworkCache { /** * Get additional framework cache info. * - * Scans ui5DataDir for stale staging directories left behind by previously + * Scans ui5DataDir for stale removal directories left behind by previously * interrupted clean operations (i.e. process killed after rename but before deletion). * Returns stats per orphan without deleting anything. * @@ -61,7 +61,7 @@ export default class FrameworkCache { } const staleDirs = entries.filter( - (e) => e.isDirectory() && e.name.startsWith(STAGING_DIR_PREFIX) + (e) => e.isDirectory() && e.name.startsWith(PENDING_REMOVAL_DIR_PREFIX) ); if (staleDirs.length === 0) { @@ -85,7 +85,7 @@ export default class FrameworkCache { } /** - * Scans ui5DataDir for stale staging directories left behind by previously + * Scans ui5DataDir for stale removal directories left behind by previously * interrupted clean operations (i.e. process killed after rename but before deletion). * * Returns an array of result objects — one per stale directory found — each @@ -126,10 +126,10 @@ export default class FrameworkCache { * filesystem operation: * * 1. Clear cacache's in-process memoization (no path needed — global operation). - * 2. Atomically rename framework/ to a staging dir. + * 2. Atomically rename framework/ to a temporary removal dir. * After this point the original path no longer exists. * If ENOENT is raised (concurrent deletion), returns null. - * 3. Delete the staging dir recursively. Its contents are now fully private + * 3. Delete the temporary removal dir recursively. Its contents are now fully private * to this operation. * * @public @@ -154,15 +154,15 @@ export default class FrameworkCache { // cacache not available — no-op } - // Atomically rename framework/ to a staging directory. + // Atomically rename framework/ to a temporary removal directory. // fs.rename is a single syscall and completes in microseconds. // After this line the original path no longer exists. - const stagingDir = path.join( + const removalDir = path.join( ui5DataDir, - `${STAGING_DIR_PREFIX}${Buffer.from(getRandomValues(new Uint8Array(2))).toString("hex")}` + `${PENDING_REMOVAL_DIR_PREFIX}${Buffer.from(getRandomValues(new Uint8Array(2))).toString("hex")}` ); try { - await fs.rename(frameworkDir, stagingDir); + await fs.rename(frameworkDir, removalDir); } catch (err) { if (/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT") { // Directory was removed by another process after our check — already clean. @@ -171,7 +171,7 @@ export default class FrameworkCache { throw err; } - await fs.rm(stagingDir, {recursive: true, force: true}); + await fs.rm(removalDir, {recursive: true, force: true}); return { path: FRAMEWORK_DIR_NAME, From a5a714e1c98305cb978c0203e9629a4990d91d24 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 28 Jul 2026 15:48:41 +0300 Subject: [PATCH 38/52] revert: Bad renaming --- .../lib/lbt/resources/ResourceCollector.js | 4 +- packages/cli/test/lib/cli/commands/cache.js | 10 +-- packages/project/lib/ui5Framework/cache.js | 2 +- .../project/test/lib/ui5framework/cache.js | 90 +++++++++---------- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/packages/builder/lib/lbt/resources/ResourceCollector.js b/packages/builder/lib/lbt/resources/ResourceCollector.js index 93ee479ac03..927ce2dd1b5 100644 --- a/packages/builder/lib/lbt/resources/ResourceCollector.js +++ b/packages/builder/lib/lbt/resources/ResourceCollector.js @@ -55,11 +55,11 @@ class ResourceCollector { } /** - * Comma separated list of components to which stale resources should be added. + * Comma separated list of components to which orphaned resources should be added. * * A component and a separated list of resource patterns of orphans that should be added * to the preceding component. - * If no such list is given, any stale resource will be added to the component. + * If no such list is given, any orphaned resource will be added to the component. * The evaluation logic for the filter list is the same as for the filters * parameters: excludes can be denoted with a leading '-' or '!' and order is significant. * Later filters can override the result of earlier ones. diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index c0388f087c2..78d48fd92ae 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -419,8 +419,8 @@ test.serial("ui5 cache clean: shows stale framework data in pre-confirmation sum const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("Stale Cache"), "Shows stale cache group in pre-confirm summary"); t.true(allOutput.includes("Framework"), "Shows framework subgroup in pre-confirm summary"); - t.true(allOutput.includes("_framework_to_delete_abcd"), "Shows orphaned dir path indented"); - t.true(allOutput.includes("2 versions of 5 libraries"), "Shows orphaned dir stats"); + t.true(allOutput.includes("_framework_to_delete_abcd"), "Shows stale removal dir path indented"); + t.true(allOutput.includes("2 versions of 5 libraries"), "Shows stale removal dir stats"); }); test.serial("ui5 cache clean: shows stale framework data in post-clean summary", async (t) => { @@ -447,8 +447,8 @@ test.serial("ui5 cache clean: shows stale framework data in post-clean summary", t.true(allOutput.includes("Cleanup result:"), "Shows cleanup result heading"); t.true(allOutput.includes("Stale Cache"), "Shows stale cache group in result"); t.true(allOutput.includes("Framework"), "Shows framework subgroup in result"); - t.true(allOutput.includes("_framework_to_delete_ab12"), "Shows first orphaned dir path indented"); - t.true(allOutput.includes("_framework_to_delete_cd34"), "Shows second orphaned dir path indented"); + t.true(allOutput.includes("_framework_to_delete_ab12"), "Shows first stale removal dir path indented"); + t.true(allOutput.includes("_framework_to_delete_cd34"), "Shows second stale removal dir path indented"); const summaryLine = allOutput.split("\n").find((line) => line.includes("Success:")); t.truthy(summaryLine, "Output includes success summary line"); t.true(summaryLine.includes("Active Cache (Framework) and Stale Cache (Framework)"), @@ -506,7 +506,7 @@ test.serial("ui5 cache clean: shows stale build cache in pre-confirm and post-cl t.true(allOutput.includes("Stale Cache"), "Shows stale cache group"); t.true(allOutput.includes("Build"), "Shows build subgroup"); t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "buildCache/v0_7")), - "Shows orphaned build cache path indented"); + "Shows stale build cache path indented"); t.true(allOutput.includes("Removed"), "Post-clean result shows removed entries"); t.true(allOutput.includes("freed 40.0 MB"), "Shows freed size in post-clean result"); t.true(allOutput.includes("Cleaned Stale Cache (Build)"), "Success summary mentions stale build group"); diff --git a/packages/project/lib/ui5Framework/cache.js b/packages/project/lib/ui5Framework/cache.js index 7a50c7688fa..48ffc19005f 100644 --- a/packages/project/lib/ui5Framework/cache.js +++ b/packages/project/lib/ui5Framework/cache.js @@ -46,7 +46,7 @@ export default class FrameworkCache { * * Scans ui5DataDir for stale removal directories left behind by previously * interrupted clean operations (i.e. process killed after rename but before deletion). - * Returns stats per orphan without deleting anything. + * Returns stats per stale directory without deleting anything. * * @public * @param {string} ui5DataDir Resolved absolute path to UI5 data directory diff --git a/packages/project/test/lib/ui5framework/cache.js b/packages/project/test/lib/ui5framework/cache.js index 29adb340b43..b90308b344b 100644 --- a/packages/project/test/lib/ui5framework/cache.js +++ b/packages/project/test/lib/ui5framework/cache.js @@ -139,10 +139,10 @@ test("cleanCache: renames then removes framework directory and returns stats", a // framework/ is gone — getCacheInfo returns null t.is(await FrameworkCache.getCacheInfo(t.context.testDir), null); - // No staging dirs remain after a successful clean + // No stale removal dirs remain after a successful clean const entries = await fs.readdir(t.context.testDir); t.false(entries.some((e) => e.startsWith("_framework_to_delete_")), - "no staging dirs remain after successful clean"); + "no stale removal dirs remain after successful clean"); // packages/ is gone await t.throwsAsync(fs.access(path.join(frameworkDir, "packages"))); @@ -161,62 +161,62 @@ test("cleanCache: removes directory with multiple scopes", async (t) => { t.is(await FrameworkCache.getCacheInfo(t.context.testDir), null); }); -test("cleanCache: does not include orphaned field in result", async (t) => { +test("cleanCache: does not include stale field in result", async (t) => { await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); const result = await FrameworkCache.cleanCache(t.context.testDir); t.truthy(result); - t.false(Object.prototype.hasOwnProperty.call(result, "orphaned"), - "cleanCache result does not include orphaned — use cleanAdditional for that"); + t.false(Object.prototype.hasOwnProperty.call(result, "stale"), + "cleanCache result does not include stale — use cleanAdditional for that"); }); -test("cleanCache: does not remove orphaned staging dirs — that is cleanAdditional's job", async (t) => { +test("cleanCache: does not remove stale removal dirs — that is cleanAdditional's job", async (t) => { await mkPackageIn(path.join(t.context.testDir, "framework"), "@openui5", "sap.m", "1.120.0"); - const orphanDir = path.join(t.context.testDir, "_framework_to_delete_abcd"); - await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.100.0"); + const staleDir = path.join(t.context.testDir, "_framework_to_delete_abcd"); + await mkPackageIn(staleDir, "@openui5", "sap.ui.core", "1.100.0"); await FrameworkCache.cleanCache(t.context.testDir); - // Orphan is still present after cleanCache — cleanAdditional handles it - await t.notThrowsAsync(fs.access(orphanDir), "orphaned dir is not touched by cleanCache"); + // Stale removal dir is still present after cleanCache — cleanAdditional handles it + await t.notThrowsAsync(fs.access(staleDir), "stale removal dir is not touched by cleanCache"); }); // ─── cleanAdditional ────────────────────────────────────────────────────────── -test("cleanAdditional: returns empty array when no orphaned staging dirs exist", async (t) => { +test("cleanAdditional: returns empty array when no stale removal dirs exist", async (t) => { const result = await FrameworkCache.cleanAdditional(t.context.testDir); t.deepEqual(result, []); }); -test("cleanAdditional: detects and removes orphaned staging dirs, reports them", async (t) => { - const orphanDir = path.join(t.context.testDir, "_framework_to_delete_abcd"); - await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.100.0"); - await mkPackageIn(orphanDir, "@openui5", "sap.ui.core", "1.110.0"); +test("cleanAdditional: detects and removes stale removal dirs, reports them", async (t) => { + const staleDir = path.join(t.context.testDir, "_framework_to_delete_abcd"); + await mkPackageIn(staleDir, "@openui5", "sap.ui.core", "1.100.0"); + await mkPackageIn(staleDir, "@openui5", "sap.ui.core", "1.110.0"); const result = await FrameworkCache.cleanAdditional(t.context.testDir); - t.is(result.length, 1, "one orphaned dir reported"); - const orphanResult = result[0]; - t.true(orphanResult.path.startsWith("_framework_to_delete_"), "orphan path has staging prefix"); - t.is(orphanResult.libraryCount, 1); - t.is(orphanResult.versionCount, 2); + t.is(result.length, 1, "one stale removal dir reported"); + const staleResult = result[0]; + t.true(staleResult.path.startsWith("_framework_to_delete_"), "stale path has pending-removal prefix"); + t.is(staleResult.libraryCount, 1); + t.is(staleResult.versionCount, 2); - await t.throwsAsync(fs.access(orphanDir), {code: "ENOENT"}, "orphaned staging dir removed"); + await t.throwsAsync(fs.access(staleDir), {code: "ENOENT"}, "stale removal dir removed"); }); -test("cleanAdditional: removes multiple orphaned staging dirs and reports each", async (t) => { - const orphan1 = path.join(t.context.testDir, "_framework_to_delete_1111"); - const orphan2 = path.join(t.context.testDir, "_framework_to_delete_2222"); +test("cleanAdditional: removes multiple stale removal dirs and reports each", async (t) => { + const stale1 = path.join(t.context.testDir, "_framework_to_delete_1111"); + const stale2 = path.join(t.context.testDir, "_framework_to_delete_2222"); - await mkPackageIn(orphan1, "@openui5", "sap.m", "1.90.0"); - await mkPackageIn(orphan2, "@openui5", "sap.ui.core", "1.91.0"); - await mkPackageIn(orphan2, "@openui5", "sap.ui.core", "1.92.0"); + await mkPackageIn(stale1, "@openui5", "sap.m", "1.90.0"); + await mkPackageIn(stale2, "@openui5", "sap.ui.core", "1.91.0"); + await mkPackageIn(stale2, "@openui5", "sap.ui.core", "1.92.0"); const result = await FrameworkCache.cleanAdditional(t.context.testDir); - t.is(result.length, 2, "two orphaned dirs reported"); + t.is(result.length, 2, "two stale removal dirs reported"); const sorted = [...result].sort((a, b) => a.path.localeCompare(b.path)); t.is(sorted[0].libraryCount, 1); @@ -224,16 +224,16 @@ test("cleanAdditional: removes multiple orphaned staging dirs and reports each", t.is(sorted[1].libraryCount, 1); t.is(sorted[1].versionCount, 2); - await t.throwsAsync(fs.access(orphan1), {code: "ENOENT"}); - await t.throwsAsync(fs.access(orphan2), {code: "ENOENT"}); + await t.throwsAsync(fs.access(stale1), {code: "ENOENT"}); + await t.throwsAsync(fs.access(stale2), {code: "ENOENT"}); }); -test("cleanAdditional: orphaned dir deletion failure is non-fatal", async (t) => { - const orphanDir = path.join(t.context.testDir, "_framework_to_delete_fail"); - await mkPackageIn(orphanDir, "@openui5", "sap.m", "1.80.0"); +test("cleanAdditional: stale removal dir deletion failure is non-fatal", async (t) => { + const staleDir = path.join(t.context.testDir, "_framework_to_delete_fail"); + await mkPackageIn(staleDir, "@openui5", "sap.m", "1.80.0"); const rmStub = sinon.stub().callsFake(async (p, opts) => { - if (p === orphanDir) { + if (p === staleDir) { throw new Error("simulated deletion failure"); } return fs.rm(p, opts); @@ -247,21 +247,21 @@ test("cleanAdditional: orphaned dir deletion failure is non-fatal", async (t) => try { const result = await FrameworkCacheMocked.cleanAdditional(t.context.testDir); t.deepEqual(result, [], "failed deletion is excluded from the returned list"); - await t.notThrowsAsync(fs.access(orphanDir), "failed orphan deletion keeps directory on disk"); + await t.notThrowsAsync(fs.access(staleDir), "failed stale removal dir deletion keeps directory on disk"); } finally { esmock.purge(FrameworkCacheMocked); - await fs.rm(orphanDir, {recursive: true, force: true}).catch(() => {}); + await fs.rm(staleDir, {recursive: true, force: true}).catch(() => {}); } }); -test("cleanAdditional: returns only successfully removed orphaned dirs", async (t) => { - const orphanOk = path.join(t.context.testDir, "_framework_to_delete_ok"); - const orphanFail = path.join(t.context.testDir, "_framework_to_delete_fail"); - await mkPackageIn(orphanOk, "@openui5", "sap.m", "1.80.0"); - await mkPackageIn(orphanFail, "@openui5", "sap.ui.core", "1.81.0"); +test("cleanAdditional: returns only successfully removed stale removal dirs", async (t) => { + const staleOk = path.join(t.context.testDir, "_framework_to_delete_ok"); + const staleFail = path.join(t.context.testDir, "_framework_to_delete_fail"); + await mkPackageIn(staleOk, "@openui5", "sap.m", "1.80.0"); + await mkPackageIn(staleFail, "@openui5", "sap.ui.core", "1.81.0"); const rmStub = sinon.stub().callsFake(async (p, opts) => { - if (p === orphanFail) { + if (p === staleFail) { throw new Error("simulated deletion failure"); } return fs.rm(p, opts); @@ -277,11 +277,11 @@ test("cleanAdditional: returns only successfully removed orphaned dirs", async ( t.is(result.length, 1); t.is(result[0].path, "_framework_to_delete_ok"); - await t.throwsAsync(fs.access(orphanOk), {code: "ENOENT"}); - await t.notThrowsAsync(fs.access(orphanFail)); + await t.throwsAsync(fs.access(staleOk), {code: "ENOENT"}); + await t.notThrowsAsync(fs.access(staleFail)); } finally { esmock.purge(FrameworkCacheMocked); - await fs.rm(orphanFail, {recursive: true, force: true}).catch(() => {}); + await fs.rm(staleFail, {recursive: true, force: true}).catch(() => {}); } }); From 6b8992352faa337080c0d8c57cc8fc7de3a2debe Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 28 Jul 2026 16:22:31 +0300 Subject: [PATCH 39/52] test: Fix failing tests --- packages/project/test/lib/package-exports.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/project/test/lib/package-exports.js b/packages/project/test/lib/package-exports.js index be59e0f6f92..2acf253b658 100644 --- a/packages/project/test/lib/package-exports.js +++ b/packages/project/test/lib/package-exports.js @@ -20,14 +20,14 @@ test("check number of exports", (t) => { [ "config/Configuration", "build/cache/Cache", - "build/cache/CacheManager", + {exportedSpecifier: "internal/cache/CacheManager", mappedModule: "../../lib/build/cache/CacheManager.js"}, "specifications/Specification", "specifications/SpecificationVersion", "ui5Framework/Openui5Resolver", "ui5Framework/Sapui5Resolver", "ui5Framework/Sapui5MavenSnapshotResolver", "ui5Framework/maven/SnapshotCache", - "ui5Framework/cache", + {exportedSpecifier: "internal/ui5Framework/cache", mappedModule: "../../lib/ui5Framework/cache.js"}, "validation/validator", "validation/ValidationError", "graph/ProjectGraph", From 8f195c8476145c7df601858205ceca2f01bb89f8 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 28 Jul 2026 17:14:25 +0300 Subject: [PATCH 40/52] build: Update package-lock --- package-lock.json | 232 ++++++++++++++-------------------------------- 1 file changed, 71 insertions(+), 161 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0759bc1e9ef..386536d57d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -272,6 +272,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.56.0.tgz", "integrity": "sha512-+mKUdYvqOi0BcvpAEyCEw49vSBptufIcfibtHz2bdr1pI789M46Yt0uQEk/sxtK3teh71OQvVFHaTDzShUWewQ==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.56.0", "@algolia/requester-browser-xhr": "5.56.0", @@ -412,6 +413,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1262,20 +1264,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.2", + "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "node_modules/@emnapi/core/node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "license": "MIT", "optional": true, "dependencies": { @@ -2671,9 +2673,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2691,9 +2690,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2711,9 +2707,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2731,9 +2724,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2751,9 +2741,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2771,9 +2758,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2791,9 +2775,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2811,9 +2792,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2859,6 +2837,29 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@oxc-parser/binding-win32-arm64-msvc": { "version": "0.137.0", "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz", @@ -3026,9 +3027,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3043,9 +3041,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3060,9 +3055,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3077,9 +3069,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3094,9 +3083,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3111,9 +3097,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3128,9 +3111,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3145,9 +3125,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3370,9 +3347,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3393,9 +3367,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3416,9 +3387,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3439,9 +3407,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3462,9 +3427,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3485,9 +3447,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3720,9 +3679,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3736,9 +3692,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3752,9 +3705,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3768,9 +3718,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3784,9 +3731,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3800,9 +3744,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3816,9 +3757,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3832,9 +3770,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3848,9 +3783,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3864,9 +3796,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3880,9 +3809,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3896,9 +3822,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3912,9 +3835,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4378,9 +4298,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4397,9 +4314,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4416,9 +4330,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4435,9 +4346,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4606,7 +4514,8 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/linkify-it": { "version": "5.0.0", @@ -4619,6 +4528,7 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "license": "MIT", + "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -4655,8 +4565,7 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", @@ -4697,12 +4606,12 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "aix" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4714,12 +4623,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4731,12 +4640,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4748,12 +4657,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4765,12 +4674,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4782,12 +4691,12 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4799,12 +4708,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4816,12 +4725,12 @@ "cpu": [ "loong64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4833,12 +4742,12 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4850,12 +4759,12 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4867,12 +4776,12 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4884,12 +4793,12 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4901,12 +4810,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4918,12 +4827,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4935,12 +4844,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4952,12 +4861,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4969,12 +4878,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -4986,12 +4895,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -5003,12 +4912,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -5020,12 +4929,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=16.20.0" } @@ -5476,6 +5385,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5562,6 +5472,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5587,6 +5498,7 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.56.0.tgz", "integrity": "sha512-PrqppUmhT4ENdas2pH9caE7efUcxy6EcSFhWzosiVuQBzu2tQ5yLTI6jwomT/1cuBnivzGfxiJCqDNN9FRRh+Q==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.22.0", "@algolia/client-abtesting": "5.56.0", @@ -6325,6 +6237,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", @@ -7406,6 +7319,7 @@ "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -8709,6 +8623,7 @@ "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "packages/*" ], @@ -9562,6 +9477,7 @@ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "license": "MIT", + "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -12173,9 +12089,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12196,9 +12109,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12219,9 +12129,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12242,9 +12149,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12595,6 +12499,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", @@ -14469,6 +14374,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -15622,6 +15528,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -18004,6 +17911,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -18104,6 +18012,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.40", "@vue/compiler-sfc": "3.5.40", @@ -18783,7 +18692,8 @@ "pretty-hrtime": "^1.0.3", "semver": "^7.8.5", "update-notifier": "^7.3.1", - "yargs": "^18.0.0" + "yargs": "^18.0.0", + "yesno": "^0.4.0" }, "bin": { "ui5": "bin/ui5.cjs" From 90c12632459b819d5d5196f9cb4997b72a9f5026 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 28 Jul 2026 17:26:51 +0300 Subject: [PATCH 41/52] refactor: Remove redundant check --- package-lock.json | 51 +++++++++++++++++--------- packages/cli/lib/cli/commands/cache.js | 6 +-- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 386536d57d4..af3f161a293 100644 --- a/package-lock.json +++ b/package-lock.json @@ -272,7 +272,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.56.0.tgz", "integrity": "sha512-+mKUdYvqOi0BcvpAEyCEw49vSBptufIcfibtHz2bdr1pI789M46Yt0uQEk/sxtK3teh71OQvVFHaTDzShUWewQ==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.56.0", "@algolia/requester-browser-xhr": "5.56.0", @@ -413,7 +412,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1284,6 +1282,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -4514,8 +4522,7 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/linkify-it": { "version": "5.0.0", @@ -4528,7 +4535,6 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "license": "MIT", - "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -4565,7 +4571,8 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "devOptional": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", @@ -4612,6 +4619,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4629,6 +4637,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4646,6 +4655,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4663,6 +4673,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4680,6 +4691,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4697,6 +4709,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4714,6 +4727,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4731,6 +4745,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4748,6 +4763,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4765,6 +4781,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4782,6 +4799,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4799,6 +4817,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4816,6 +4835,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4833,6 +4853,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4850,6 +4871,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4867,6 +4889,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4884,6 +4907,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4901,6 +4925,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4918,6 +4943,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -4935,6 +4961,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=16.20.0" } @@ -5385,7 +5412,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5472,7 +5498,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5498,7 +5523,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.56.0.tgz", "integrity": "sha512-PrqppUmhT4ENdas2pH9caE7efUcxy6EcSFhWzosiVuQBzu2tQ5yLTI6jwomT/1cuBnivzGfxiJCqDNN9FRRh+Q==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.22.0", "@algolia/client-abtesting": "5.56.0", @@ -6237,7 +6261,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", @@ -7319,7 +7342,6 @@ "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -8623,7 +8645,6 @@ "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", "dev": true, "license": "MIT", - "peer": true, "workspaces": [ "packages/*" ], @@ -9477,7 +9498,6 @@ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -12499,7 +12519,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", @@ -14374,7 +14393,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -15528,7 +15546,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -17911,7 +17928,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -18012,7 +18028,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.40", "@vue/compiler-sfc": "3.5.40", diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 3efb91985ab..dd7ef8e174f 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -132,9 +132,9 @@ async function handleCache(argv) { const staleInfoWithAbsPaths = additionalFrameworkResult.map( (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) ); - const buildAdditionalResult = preCleanBuildAdditionalInfo.length > 0 ? - additionalBuildResult.map((o) => ({...o, absPath: path.join(ui5DataDir, o.path)})) : - []; + const buildAdditionalResult = additionalBuildResult.map( + (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) + ); await displayCleanupResult({ frameworkResult, From 9fb499326e17c321390082404cc94dd33398b300 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 29 Jul 2026 08:18:04 +0300 Subject: [PATCH 42/52] refactor: Provide complete path of the internal exports --- packages/cli/lib/cli/commands/cache.js | 2 +- packages/cli/test/lib/cli/commands/cache.js | 2 +- packages/project/package.json | 2 +- packages/project/test/lib/package-exports.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index dd7ef8e174f..32a77419880 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -5,7 +5,7 @@ import process from "node:process"; import baseMiddleware from "../middlewares/base.js"; import Configuration from "@ui5/project/config/Configuration"; import FrameworkCache from "@ui5/project/internal/ui5Framework/cache"; -import CacheManager from "@ui5/project/internal/cache/CacheManager"; +import CacheManager from "@ui5/project/internal/build/cache/CacheManager"; import { CACHE_CLEAN_HELP_USAGE, displayCacheCleanWarning, diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 78d48fd92ae..f1e3c71df97 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -67,7 +67,7 @@ test.beforeEach(async (t) => { static getAdditionalCacheInfo = t.context.frameworkCacheGetAdditionalCacheInfo; } }, - "@ui5/project/internal/cache/CacheManager": { + "@ui5/project/internal/build/cache/CacheManager": { default: class { static getCacheInfo = t.context.buildCacheGetCacheInfo; static cleanCache = t.context.buildCacheCleanCache; diff --git a/packages/project/package.json b/packages/project/package.json index 26866cab672..725f41ae49c 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -18,7 +18,7 @@ ], "type": "module", "exports": { - "./internal/cache/CacheManager": "./lib/build/cache/CacheManager.js", + "./internal/build/cache/CacheManager": "./lib/build/cache/CacheManager.js", "./internal/ui5Framework/cache": "./lib/ui5Framework/cache.js", "./config/Configuration": "./lib/config/Configuration.js", "./build/cache/Cache": "./lib/build/cache/Cache.js", diff --git a/packages/project/test/lib/package-exports.js b/packages/project/test/lib/package-exports.js index 2acf253b658..22bafac69f5 100644 --- a/packages/project/test/lib/package-exports.js +++ b/packages/project/test/lib/package-exports.js @@ -20,7 +20,7 @@ test("check number of exports", (t) => { [ "config/Configuration", "build/cache/Cache", - {exportedSpecifier: "internal/cache/CacheManager", mappedModule: "../../lib/build/cache/CacheManager.js"}, + {exportedSpecifier: "internal/build/cache/CacheManager", mappedModule: "../../lib/build/cache/CacheManager.js"}, "specifications/Specification", "specifications/SpecificationVersion", "ui5Framework/Openui5Resolver", From 948dd6df6a15599e938d0792025f45ea5b693d81 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 29 Jul 2026 13:03:12 +0300 Subject: [PATCH 43/52] refactor: Rename --yes flag to --force --- .../docs/pages/Troubleshooting.md | 4 ++-- packages/cli/lib/cli/commands/cache.js | 8 +++---- packages/cli/test/lib/cli/commands/cache.js | 22 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/internal/documentation/docs/pages/Troubleshooting.md b/internal/documentation/docs/pages/Troubleshooting.md index 3337cc0267a..d152bccb2ae 100644 --- a/internal/documentation/docs/pages/Troubleshooting.md +++ b/internal/documentation/docs/pages/Troubleshooting.md @@ -28,10 +28,10 @@ Use the dedicated cache clean command, which removes all cached data: ui5 cache clean ``` -This displays the cache location, the amount of data that gets removed, and asks for confirmation before proceeding. To skip the confirmation prompt (for example in CI environments), use the `--yes` flag: +This displays the cache location, the amount of data that gets removed, and asks for confirmation before proceeding. To skip the confirmation prompt (for example in CI environments), use the `--force` flag: ```sh -ui5 cache clean --yes +ui5 cache clean --force ``` The command removes the following cached data: diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 32a77419880..64d24a62d91 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -28,15 +28,15 @@ cacheCommand.builder = function(cli) { builder: function(yargs) { return yargs .usage(CACHE_CLEAN_HELP_USAGE) - .option("yes", { - alias: "y", + .option("force", { + alias: "f", 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 confirmation") - .example("$0 cache clean --yes", + .example("$0 cache clean --force", "Remove all cached UI5 data without confirmation (e.g. in CI scenarios)") .example("UI5_DATA_DIR=/custom/path $0 cache clean", "Remove cached data from a non-default UI5 data directory"); @@ -51,7 +51,7 @@ cacheCommand.builder = function(cli) { * @returns {Promise} Confirmation result */ async function getConfirmation(argv) { - if (argv.yes) { + if (argv.force) { return true; } displayCacheCleanWarning(); diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index f1e3c71df97..9f1890d1427 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -114,7 +114,7 @@ test("Command builder", async (t) => { t.is(yargsStub.usage.callCount, 1, "usage called once for warning help banner"); t.true(yargsStub.usage.firstCall.args[0].startsWith("WARNING:"), "usage banner starts with warning"); - t.is(yargsStub.option.callCount, 1, "option called for --yes flag"); + t.is(yargsStub.option.callCount, 1, "option called for --force flag"); t.is(yargsStub.example.callCount, 3, "example called 3 times"); }); @@ -290,7 +290,7 @@ test.serial("ui5 cache clean: framework only — formats library stats correctly frameworkCacheGetCacheInfo.resolves(singleStub); frameworkCacheCleanCache.resolves(singleStub); - argv["yes"] = true; + argv["force"] = true; await cache.handler(argv); allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -371,14 +371,14 @@ test.serial("ui5 cache clean: formats GB sizes correctly", async (t) => { buildCacheCleanCache.resolves({path: "large", size: 2.5 * 1024 * 1024 * 1024}); argv["_"] = ["cache", "clean"]; - argv["yes"] = true; + argv["force"] = true; await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("2.5 GB"), "Shows GB format"); }); -test.serial("ui5 cache clean --yes: skips confirmation prompt", async (t) => { +test.serial("ui5 cache clean --force: skips confirmation prompt", async (t) => { const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; @@ -388,16 +388,16 @@ test.serial("ui5 cache clean --yes: skips confirmation prompt", async (t) => { buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 5 * 1024 * 1024}); argv["_"] = ["cache", "clean"]; - argv["yes"] = true; + argv["force"] = true; await cache.handler(argv); - t.is(yesnoStub.callCount, 0, "Should not ask for confirmation with --yes"); + t.is(yesnoStub.callCount, 0, "Should not ask for confirmation with --force"); t.is(frameworkCacheCleanCache.callCount, 1, "frameworkCache.cleanCache called"); t.is(buildCacheCleanCache.callCount, 1, "buildCache.cleanCache called"); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); t.true(allOutput.includes("Success"), "Shows success message"); - t.false(allOutput.includes(WARNING_PREFIX), "Does not show warning when --yes is used"); + t.false(allOutput.includes(WARNING_PREFIX), "Does not show warning when --force is used"); }); test.serial("ui5 cache clean: shows stale framework data in pre-confirmation summary", async (t) => { @@ -440,7 +440,7 @@ test.serial("ui5 cache clean: shows stale framework data in post-clean summary", ]); argv["_"] = ["cache", "clean"]; - argv["yes"] = true; + argv["force"] = true; await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -472,7 +472,7 @@ test.serial("ui5 cache clean: shows stale-only success summary when no active fr ]); argv["_"] = ["cache", "clean"]; - argv["yes"] = true; + argv["force"] = true; await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -499,7 +499,7 @@ test.serial("ui5 cache clean: shows stale build cache in pre-confirm and post-cl ]); argv["_"] = ["cache", "clean"]; - argv["yes"] = true; + argv["force"] = true; await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -527,7 +527,7 @@ test.serial("ui5 cache clean: build cache and stale build cache with size 0 omit ]); argv["_"] = ["cache", "clean"]; - argv["yes"] = true; + argv["force"] = true; await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); From 46db12d0feef564e2c04c4701f5d7306e52b4840 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 29 Jul 2026 13:05:10 +0300 Subject: [PATCH 44/52] fix: ESLint findings --- packages/cli/test/lib/cli/commands/cache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 9f1890d1427..60800ad2bd2 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -67,7 +67,7 @@ test.beforeEach(async (t) => { static getAdditionalCacheInfo = t.context.frameworkCacheGetAdditionalCacheInfo; } }, - "@ui5/project/internal/build/cache/CacheManager": { + "@ui5/project/internal/build/cache/CacheManager": { default: class { static getCacheInfo = t.context.buildCacheGetCacheInfo; static cleanCache = t.context.buildCacheCleanCache; From 34ae0401551435cd03028d7c5e148bdad3ed0abc Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 29 Jul 2026 15:06:54 +0300 Subject: [PATCH 45/52] feat: Enable detailed output only in verbose mode The non-verbose mode is mostly silent, except for the confirmation prompt and the warning message. With --force flag the cache cleanup is silent --- .../docs/pages/Troubleshooting.md | 9 +- packages/cli/lib/cli/commands/cache.js | 99 +++++++------ packages/cli/test/lib/cli/commands/cache.js | 133 +++++++++++++++++- 3 files changed, 196 insertions(+), 45 deletions(-) diff --git a/internal/documentation/docs/pages/Troubleshooting.md b/internal/documentation/docs/pages/Troubleshooting.md index d152bccb2ae..424a72149b1 100644 --- a/internal/documentation/docs/pages/Troubleshooting.md +++ b/internal/documentation/docs/pages/Troubleshooting.md @@ -28,7 +28,14 @@ Use the dedicated cache clean command, which removes all cached data: ui5 cache clean ``` -This displays the cache location, the amount of data that gets removed, and asks for confirmation before proceeding. To skip the confirmation prompt (for example in CI environments), use the `--force` flag: +For a detailed preview and grouped cleanup summary, use the `--verbose` flag: + +```sh +ui5 cache clean --verbose +``` + +To skip the confirmation prompt (for example in CI environments), use the `--force` flag. +In non-verbose mode with `--force`, the command runs completely silent: ```sh ui5 cache clean --force diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 64d24a62d91..aec0e1f28c0 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -2,6 +2,7 @@ import chalk from "chalk"; import path from "node:path"; import os from "node:os"; import process from "node:process"; +import {isLogLevelEnabled} from "@ui5/logger"; import baseMiddleware from "../middlewares/base.js"; import Configuration from "@ui5/project/config/Configuration"; import FrameworkCache from "@ui5/project/internal/ui5Framework/cache"; @@ -76,47 +77,63 @@ async function resolveCacheUi5DataDir() { return path.join(os.homedir(), ".ui5"); } +function withAbsPath(entries, ui5DataDir) { + return entries.map((entry) => { + return {...entry, absPath: path.join(ui5DataDir, entry.path)}; + }); +} + async function handleCache(argv) { const ui5DataDir = await resolveCacheUi5DataDir(); + const isVerbose = isLogLevelEnabled("verbose"); - process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); + if (isVerbose) { + // logger.verbose pollutes output with framework initialization noise. + process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); + } - const [frameworkInfo, staleInfo, buildInfo, buildAdditionalInfo] = await Promise.all([ + const [frameworkInfo, buildInfo] = await Promise.all([ FrameworkCache.getCacheInfo(ui5DataDir), - FrameworkCache.getAdditionalCacheInfo(ui5DataDir), CacheManager.getCacheInfo(ui5DataDir), - CacheManager.getAdditionalCacheInfo(ui5DataDir), ]); - if (!frameworkInfo && !buildInfo && staleInfo.length === 0 && buildAdditionalInfo.length === 0) { - process.stderr.write("Nothing to clean\n"); + const hasActiveCache = Boolean(frameworkInfo || buildInfo); + let staleInfo = []; + let buildStaleInfo = []; + + if (isVerbose || !hasActiveCache) { + [staleInfo, buildStaleInfo] = await Promise.all([ + FrameworkCache.getAdditionalCacheInfo(ui5DataDir), + CacheManager.getAdditionalCacheInfo(ui5DataDir), + ]); + } + + const hasStaleCache = staleInfo.length > 0 || buildStaleInfo.length > 0; + + if (!hasActiveCache && !hasStaleCache) { + if (isVerbose) { + 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; - const buildPreSize = buildInfo?.size ?? 0; - const preCleanStaleInfo = staleInfo.map( - (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) - ); - const preCleanBuildAdditionalInfo = buildAdditionalInfo.map( - (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) - ); - - await displayCacheInfo({ - frameworkInfo, - buildInfo, - frameworkAbsPath, - buildAbsPath, - buildPreSize, - staleInfo: preCleanStaleInfo, - buildAdditionalInfo: preCleanBuildAdditionalInfo, - }); + if (isVerbose) { + await displayCacheInfo({ + frameworkInfo, + buildInfo, + frameworkAbsPath: frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null, + buildAbsPath: buildInfo ? path.join(ui5DataDir, buildInfo.path) : null, + buildPreSize: buildInfo?.size ?? 0, + staleInfo: withAbsPath(staleInfo, ui5DataDir), + buildAdditionalInfo: withAbsPath(buildStaleInfo, ui5DataDir), + }); + } const confirmed = await getConfirmation(argv); if (!confirmed) { - process.stderr.write("Cancelled\n"); + if (isVerbose) { + process.stderr.write("Cancelled\n"); + } return; } @@ -129,22 +146,18 @@ async function handleCache(argv) { FrameworkCache.cleanAdditional(ui5DataDir), CacheManager.cleanAdditional(ui5DataDir), ]); - const staleInfoWithAbsPaths = additionalFrameworkResult.map( - (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) - ); - const buildAdditionalResult = additionalBuildResult.map( - (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) - ); - - await displayCleanupResult({ - frameworkResult, - buildResult, - frameworkAbsPath, - buildAbsPath, - buildPreSize, - staleInfoWithAbsPaths, - buildAdditionalResult, - }); + + if (isVerbose) { + await displayCleanupResult({ + frameworkResult, + buildResult, + frameworkAbsPath: frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null, + buildAbsPath: buildInfo ? path.join(ui5DataDir, buildInfo.path) : null, + buildPreSize: buildInfo?.size ?? 0, + staleInfoWithAbsPaths: withAbsPath(additionalFrameworkResult, ui5DataDir), + buildAdditionalResult: withAbsPath(additionalBuildResult, ui5DataDir), + }); + } } export default cacheCommand; diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 60800ad2bd2..5241be93359 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -3,6 +3,7 @@ import path from "node:path"; import os from "node:os"; import sinon from "sinon"; import esmock from "esmock"; +import {setLogLevel} from "@ui5/logger"; function getDefaultArgv() { return { @@ -31,6 +32,7 @@ const ACTIVE_CACHE_HEADER = "Active Cache"; const STALE_CACHE_HEADER = "Stale Cache"; test.beforeEach(async (t) => { + setLogLevel("info"); t.context.argv = getDefaultArgv(); t.context.stderrWriteStub = sinon.stub(process.stderr, "write"); @@ -82,6 +84,7 @@ test.beforeEach(async (t) => { }); test.afterEach.always((t) => { + setLogLevel("info"); sinon.restore(); esmock.purge(t.context.cache); process.exitCode = undefined; @@ -136,6 +139,7 @@ test.serial("ui5 cache clean: uses resolved path from configuration", async (t) buildCacheGetCacheInfo.resolves(null); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); t.is(configurationFromFileStub.callCount, 1, "Configuration.fromFile called exactly once"); @@ -176,6 +180,7 @@ test.serial("ui5 cache clean: falls back to ~/.ui5 when configuration has no val buildCacheGetCacheInfo.resolves(null); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); t.is(frameworkCacheGetCacheInfo.firstCall.args[0], fallbackUi5DataDir, @@ -195,6 +200,7 @@ test.serial("ui5 cache clean: nothing to clean", async (t) => { buildCacheGetCacheInfo.resolves(null); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -219,6 +225,7 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 7 * 1024 * 1024}); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); t.is(yesnoStub.callCount, 1, "Should ask for confirmation"); @@ -246,6 +253,114 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { "Warning is displayed before the confirmation prompt is shown"); }); +test.serial("ui5 cache clean: non-verbose mode suppresses detailed summaries", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 8 * 1024 * 1024}); + yesnoStub.resolves(true); + frameworkCacheCleanCache.resolves(FRAMEWORK_STUB); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 7 * 1024 * 1024}); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes(WARNING_PREFIX), "Shows warning in non-verbose mode before confirmation"); + t.true(allOutput.includes(WARNING_IMPACT_TEXT), "Shows warning impact in non-verbose mode"); + t.false(allOutput.includes("Checking cache at"), "Does not show checking line in non-verbose mode"); + t.false(allOutput.includes("The following cached data will be removed:"), + "Does not show pre-clean detailed section without --verbose"); + t.false(allOutput.includes("Cleanup result:"), + "Does not show post-clean detailed section without --verbose"); + t.false(allOutput.includes("Success:"), "Does not show success message in non-verbose mode"); + t.false(allOutput.includes("Cancelled"), "Does not show cancelled message in non-verbose mode"); +}); + +test.serial("ui5 cache clean: non-verbose mode with active cache skips additional info lookup", async (t) => { + const {cache, argv, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub, + frameworkCacheGetAdditionalCacheInfo, buildCacheGetAdditionalCacheInfo} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 8 * 1024 * 1024}); + frameworkCacheCleanCache.resolves(FRAMEWORK_STUB); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 7 * 1024 * 1024}); + yesnoStub.resolves(true); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + t.is(frameworkCacheGetAdditionalCacheInfo.callCount, 0, + "Does not fetch additional framework cache info in non-verbose mode when active cache exists"); + t.is(buildCacheGetAdditionalCacheInfo.callCount, 0, + "Does not fetch additional build cache info in non-verbose mode when active cache exists"); +}); + +test.serial("ui5 cache clean: non-verbose mode with stale cache only stays quiet except warning/prompt", async (t) => { + const {cache, argv, stderrWriteStub, yesnoStub, + frameworkCacheGetCacheInfo, buildCacheGetCacheInfo, + frameworkCacheGetAdditionalCacheInfo, buildCacheGetAdditionalCacheInfo, + frameworkCacheCleanAdditional, buildCacheCleanAdditional, + frameworkCacheCleanCache, buildCacheCleanCache} = t.context; + + frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves(null); + frameworkCacheGetAdditionalCacheInfo.resolves([ + {path: "_framework_to_delete_abcd", libraryCount: 5, versionCount: 2}, + ]); + buildCacheGetAdditionalCacheInfo.resolves([]); + frameworkCacheCleanCache.resolves(null); + buildCacheCleanCache.resolves(null); + frameworkCacheCleanAdditional.resolves([ + {path: "_framework_to_delete_abcd", libraryCount: 5, versionCount: 2}, + ]); + buildCacheCleanAdditional.resolves([]); + yesnoStub.resolves(true); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + t.is(yesnoStub.callCount, 1, "Prompts for confirmation in non-verbose mode when stale cache is found"); + t.is(frameworkCacheGetAdditionalCacheInfo.callCount, 1, + "Fetches stale framework info when no active cache exists"); + t.is(buildCacheGetAdditionalCacheInfo.callCount, 1, + "Fetches stale build info when no active cache exists"); + t.is(frameworkCacheCleanCache.callCount, 1, "Still executes framework cache cleanup flow"); + t.is(buildCacheCleanCache.callCount, 1, "Still executes build cache cleanup flow"); + t.is(frameworkCacheCleanAdditional.callCount, 1, "Cleans stale framework cache entries"); + t.is(buildCacheCleanAdditional.callCount, 1, "Cleans stale build cache entries"); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes(WARNING_PREFIX), "Shows warning in non-verbose mode"); + t.true(allOutput.includes(WARNING_IMPACT_TEXT), "Shows warning impact in non-verbose mode"); + t.false(allOutput.includes("Checking cache at"), "Does not show checking line in non-verbose mode"); + t.false(allOutput.includes("The following cached data will be removed:"), + "Does not show detailed pre-clean summary in non-verbose mode"); + t.false(allOutput.includes("Cleanup result:"), + "Does not show detailed cleanup summary in non-verbose mode"); + t.false(allOutput.includes("Success:"), "Does not show success message in non-verbose mode"); + t.false(allOutput.includes("Cancelled"), "Does not show cancelled message in non-verbose mode"); +}); + +test.serial("ui5 cache clean: non-verbose --force mode is completely silent", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 8 * 1024 * 1024}); + frameworkCacheCleanCache.resolves(FRAMEWORK_STUB); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 7 * 1024 * 1024}); + + argv["_"] = ["cache", "clean"]; + argv["force"] = true; + await cache.handler(argv); + + t.is(yesnoStub.callCount, 0, "Does not prompt for confirmation when --force is used"); + t.is(stderrWriteStub.callCount, 0, "Does not write any output in non-verbose --force mode"); +}); + test.serial("ui5 cache clean: user cancels", async (t) => { const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; @@ -262,7 +377,8 @@ test.serial("ui5 cache clean: user cancels", async (t) => { t.is(buildCacheCleanCache.callCount, 0, "buildCache.cleanCache not called when user cancels"); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); - t.true(allOutput.includes("Cancelled"), "Shows cancelled message"); + t.true(allOutput.includes(WARNING_PREFIX), "Shows warning before confirmation"); + t.false(allOutput.includes("Cancelled"), "Does not show cancelled message in non-verbose mode"); t.false(allOutput.includes("Success"), "Does not show success message"); }); @@ -276,6 +392,7 @@ test.serial("ui5 cache clean: framework only — formats library stats correctly frameworkCacheCleanCache.resolves(FRAMEWORK_STUB); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); let allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -308,6 +425,7 @@ test.serial("ui5 cache clean: thousands separator in library stats", async (t) = frameworkCacheCleanCache.resolves(largeStub); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -324,6 +442,7 @@ test.serial("ui5 cache clean: build only", async (t) => { buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 50 * 1024}); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -341,6 +460,7 @@ test.serial("ui5 cache clean: formats byte sizes correctly (< 1 KB)", async (t) buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 500}); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -356,6 +476,7 @@ test.serial("ui5 cache clean: formats KB sizes correctly", async (t) => { buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 50 * 1024}); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -371,6 +492,7 @@ test.serial("ui5 cache clean: formats GB sizes correctly", async (t) => { buildCacheCleanCache.resolves({path: "large", size: 2.5 * 1024 * 1024 * 1024}); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); argv["force"] = true; await cache.handler(argv); @@ -388,6 +510,7 @@ test.serial("ui5 cache clean --force: skips confirmation prompt", async (t) => { buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 5 * 1024 * 1024}); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); argv["force"] = true; await cache.handler(argv); @@ -414,6 +537,7 @@ test.serial("ui5 cache clean: shows stale framework data in pre-confirmation sum yesnoStub.resolves(true); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -440,6 +564,7 @@ test.serial("ui5 cache clean: shows stale framework data in post-clean summary", ]); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); argv["force"] = true; await cache.handler(argv); @@ -472,6 +597,7 @@ test.serial("ui5 cache clean: shows stale-only success summary when no active fr ]); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); argv["force"] = true; await cache.handler(argv); @@ -499,6 +625,7 @@ test.serial("ui5 cache clean: shows stale build cache in pre-confirm and post-cl ]); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); argv["force"] = true; await cache.handler(argv); @@ -527,6 +654,7 @@ test.serial("ui5 cache clean: build cache and stale build cache with size 0 omit ]); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); argv["force"] = true; await cache.handler(argv); @@ -547,6 +675,7 @@ test.serial("ui5 cache clean: pre-clean summary shows only Active Cache group", yesnoStub.resolves(false); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -569,6 +698,7 @@ test.serial("ui5 cache clean: pre-clean summary shows only Stale Cache group", a yesnoStub.resolves(false); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); @@ -591,6 +721,7 @@ test.serial("ui5 cache clean: pre-clean summary shows both groups when active an yesnoStub.resolves(false); argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); await cache.handler(argv); const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); From 79e74015d5187b5d32c16b66a9e048dbc9e000f1 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 29 Jul 2026 15:19:20 +0300 Subject: [PATCH 46/52] refactor: Reword confirmation prompt Current confirmation does not fit well for non-verbose mode. It needs a bit more details. With that change we provide more context during the confirmation prompt. --- packages/cli/lib/cli/commands/cache.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index aec0e1f28c0..4cb1881a5d5 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -58,7 +58,7 @@ async function getConfirmation(argv) { displayCacheCleanWarning(); const {default: yesno} = await import("yesno"); return yesno({ - question: "Do you want to continue? (y/N)", + question: "Proceed with cache cleanup? (y/N)", defaultValue: false }); } @@ -88,7 +88,7 @@ async function handleCache(argv) { const isVerbose = isLogLevelEnabled("verbose"); if (isVerbose) { - // logger.verbose pollutes output with framework initialization noise. + // logger.verbose pollutes output with framework noise. process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); } From 04c5e9eda8453c70b8a57ce9c82c786e6cc32262 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 29 Jul 2026 15:41:15 +0300 Subject: [PATCH 47/52] fix: Do not report wrongly stale cache --- packages/cli/lib/cli/commands/cache.js | 6 ++++-- packages/cli/test/lib/cli/commands/cache.js | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index 4cb1881a5d5..cd0909ce75b 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -148,14 +148,16 @@ async function handleCache(argv) { ]); if (isVerbose) { + const cleanedStaleFramework = staleInfo.length > 0 ? withAbsPath(additionalFrameworkResult, ui5DataDir) : []; + const cleanedStaleBuild = buildStaleInfo.length > 0 ? withAbsPath(additionalBuildResult, ui5DataDir) : []; await displayCleanupResult({ frameworkResult, buildResult, frameworkAbsPath: frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null, buildAbsPath: buildInfo ? path.join(ui5DataDir, buildInfo.path) : null, buildPreSize: buildInfo?.size ?? 0, - staleInfoWithAbsPaths: withAbsPath(additionalFrameworkResult, ui5DataDir), - buildAdditionalResult: withAbsPath(additionalBuildResult, ui5DataDir), + staleInfoWithAbsPaths: cleanedStaleFramework, + buildAdditionalResult: cleanedStaleBuild, }); } } diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 5241be93359..4cd4eaaaa5c 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -242,6 +242,7 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "buildCache/v0_7")), "Shows absolute build path"); t.true(allOutput.includes("5 versions of 18 libraries"), "Shows library stats format"); t.true(allOutput.includes("8.0 MB"), "Shows pre-clean build cache size"); + t.false(allOutput.includes("Stale Cache"), "Does not report stale cache section when only active cache existed"); t.false(allOutput.includes("7.0 MB"), "Does not show VACUUM-freed size"); t.true(allOutput.includes("Cleaned Active Cache (Framework and Build)"), "Shows success summary"); From af83085307a6970bd2a6de7b74e764e6efbc3749 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 29 Jul 2026 17:39:55 +0300 Subject: [PATCH 48/52] fix: Defensive logging for parallel executions --- packages/cli/lib/cli/commands/cache.js | 25 ++++--- .../lib/cli/commands/helpers/cacheOutput.js | 16 +++-- packages/cli/test/lib/cli/commands/cache.js | 71 +++++++++++++++++++ 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index cd0909ce75b..e439e5050a6 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -79,10 +79,17 @@ async function resolveCacheUi5DataDir() { function withAbsPath(entries, ui5DataDir) { return entries.map((entry) => { - return {...entry, absPath: path.join(ui5DataDir, entry.path)}; + return {...entry, absPath: getAbsPath(ui5DataDir, entry)}; }); } +function getAbsPath(ui5DataDir, cacheEntry) { + if (!cacheEntry?.path) { + return null; + } + return path.join(ui5DataDir, cacheEntry.path); +} + async function handleCache(argv) { const ui5DataDir = await resolveCacheUi5DataDir(); const isVerbose = isLogLevelEnabled("verbose"); @@ -112,7 +119,7 @@ async function handleCache(argv) { if (!hasActiveCache && !hasStaleCache) { if (isVerbose) { - process.stderr.write("Nothing to clean\n"); + process.stderr.write(`\n${chalk.italic("Nothing to clean")}\n\n`); } return; } @@ -121,8 +128,8 @@ async function handleCache(argv) { await displayCacheInfo({ frameworkInfo, buildInfo, - frameworkAbsPath: frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null, - buildAbsPath: buildInfo ? path.join(ui5DataDir, buildInfo.path) : null, + frameworkAbsPath: getAbsPath(ui5DataDir, frameworkInfo), + buildAbsPath: getAbsPath(ui5DataDir, buildInfo), buildPreSize: buildInfo?.size ?? 0, staleInfo: withAbsPath(staleInfo, ui5DataDir), buildAdditionalInfo: withAbsPath(buildStaleInfo, ui5DataDir), @@ -132,7 +139,7 @@ async function handleCache(argv) { const confirmed = await getConfirmation(argv); if (!confirmed) { if (isVerbose) { - process.stderr.write("Cancelled\n"); + process.stderr.write(`\n${chalk.italic("Cancelled")}\n\n`); } return; } @@ -150,12 +157,14 @@ async function handleCache(argv) { if (isVerbose) { const cleanedStaleFramework = staleInfo.length > 0 ? withAbsPath(additionalFrameworkResult, ui5DataDir) : []; const cleanedStaleBuild = buildStaleInfo.length > 0 ? withAbsPath(additionalBuildResult, ui5DataDir) : []; + const frameworkResultAbsPath = getAbsPath(ui5DataDir, frameworkResult) || getAbsPath(ui5DataDir, frameworkInfo); + const buildResultAbsPath = getAbsPath(ui5DataDir, buildResult) || getAbsPath(ui5DataDir, buildInfo); await displayCleanupResult({ frameworkResult, buildResult, - frameworkAbsPath: frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null, - buildAbsPath: buildInfo ? path.join(ui5DataDir, buildInfo.path) : null, - buildPreSize: buildInfo?.size ?? 0, + frameworkAbsPath: frameworkResultAbsPath, + buildAbsPath: buildResultAbsPath, + buildPreSize: buildInfo?.size ?? buildResult?.size ?? 0, staleInfoWithAbsPaths: cleanedStaleFramework, buildAdditionalResult: cleanedStaleBuild, }); diff --git a/packages/cli/lib/cli/commands/helpers/cacheOutput.js b/packages/cli/lib/cli/commands/helpers/cacheOutput.js index 61a8b427794..f7bd7f4fd95 100644 --- a/packages/cli/lib/cli/commands/helpers/cacheOutput.js +++ b/packages/cli/lib/cli/commands/helpers/cacheOutput.js @@ -11,6 +11,7 @@ const CACHE_CLEAN_WARNING = const CACHE_CLEAN_WARNING_IMPACT = "Running ui5 cache clean while ui5 build or ui5 serve is in progress can break the running process " + "and lead to failed or inconsistent results."; +const PARALLEL_CLEANUP_NOTICE = "Nothing left to clean. A parallel cleanup might have happened."; export const CACHE_CLEAN_HELP_USAGE = `WARNING: ${CACHE_CLEAN_WARNING}\n${CACHE_CLEAN_WARNING_IMPACT}\n\nUsage: ui5 cache clean [options]`; @@ -173,8 +174,6 @@ export function displayCleanupResult({ staleInfoWithAbsPaths, buildAdditionalResult, }) { - process.stderr.write(`\n${chalk.bold("Cleanup result:")}\n`); - const sections = []; if (frameworkResult || buildResult) { @@ -186,14 +185,16 @@ export function displayCleanupResult({ items: [{absPath: frameworkAbsPath, detail}], }); } - if (buildResult) { + if (buildResult && buildAbsPath) { const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; activeCategories.push({ title: GROUP_BUILD, items: [{absPath: buildAbsPath, detail}], }); } - sections.push({title: SECTION_ACTIVE_CACHE, categories: activeCategories}); + if (activeCategories.length > 0) { + sections.push({title: SECTION_ACTIVE_CACHE, categories: activeCategories}); + } } if (staleInfoWithAbsPaths?.length > 0 || buildAdditionalResult?.length > 0) { @@ -217,6 +218,13 @@ export function displayCleanupResult({ sections.push({title: SECTION_STALE_CACHE, categories: staleCategories}); } + if (sections.length === 0) { + process.stderr.write(`\n${chalk.italic(PARALLEL_CLEANUP_NOTICE)}\n\n`); + return; + } + + process.stderr.write(`\n${chalk.bold("Cleanup result:")}\n`); + process.stderr.write("\n"); writeGroupedSections(sections, ({absPath, detail}) => { writeCleanupItem(absPath, detail); diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index 4cd4eaaaa5c..ae4dbb84393 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -28,6 +28,7 @@ const WARNING_TEXT = const WARNING_IMPACT_TEXT = "Running ui5 cache clean while ui5 build or ui5 serve is in progress can break the running process " + "and lead to failed or inconsistent results."; +const PARALLEL_CLEANUP_NOTICE = "Nothing left to clean. A parallel cleanup might have happened."; const ACTIVE_CACHE_HEADER = "Active Cache"; const STALE_CACHE_HEADER = "Stale Cache"; @@ -254,6 +255,52 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { "Warning is displayed before the confirmation prompt is shown"); }); +test.serial("ui5 cache clean: cleanup result uses fresh active cache paths after confirmation", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves(null); + yesnoStub.resolves(true); + + frameworkCacheCleanCache.resolves(FRAMEWORK_STUB); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 2 * 1024 * 1024}); + + argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.false(allOutput.includes("Removed null"), "Does not print null cache path in cleanup result"); + t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "buildCache/v0_7")), + "Shows absolute build cache path in cleanup result when build cache appears after confirmation"); + t.true(allOutput.includes("Cleaned Active Cache (Framework and Build)"), + "Success summary includes both active framework and build cache"); +}); + +test.serial("ui5 cache clean: reports parallel cleanup in verbose mode", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves(null); + yesnoStub.resolves(true); + frameworkCacheCleanCache.resolves(null); + buildCacheCleanCache.resolves(null); + + argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("The following cached data will be removed:"), + "Keeps pre-confirmation preview output"); + t.true(allOutput.includes(PARALLEL_CLEANUP_NOTICE), + "Reports that cleanup was already performed in parallel"); + t.false(allOutput.includes("Cleanup result:"), "Does not print cleanup result table for no-op cleanup"); + t.false(allOutput.includes("Success:"), "Does not print success summary for no-op cleanup"); +}); + test.serial("ui5 cache clean: non-verbose mode suppresses detailed summaries", async (t) => { const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; @@ -279,6 +326,30 @@ test.serial("ui5 cache clean: non-verbose mode suppresses detailed summaries", a t.false(allOutput.includes("Cancelled"), "Does not show cancelled message in non-verbose mode"); }); +test.serial("ui5 cache clean: does not report parallel cleanup in non-verbose mode", async (t) => { + const {cache, argv, stderrWriteStub, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, + buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub} = t.context; + + frameworkCacheGetCacheInfo.resolves(FRAMEWORK_STUB); + buildCacheGetCacheInfo.resolves(null); + yesnoStub.resolves(true); + frameworkCacheCleanCache.resolves(null); + buildCacheCleanCache.resolves(null); + + argv["_"] = ["cache", "clean"]; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes(WARNING_PREFIX), + "Shows warning in non-verbose mode before confirmation"); + t.true(allOutput.includes(WARNING_IMPACT_TEXT), + "Shows warning impact in non-verbose mode before confirmation"); + t.false(allOutput.includes(PARALLEL_CLEANUP_NOTICE), + "Does not print parallel cleanup notice in non-verbose mode"); + t.false(allOutput.includes("Cleanup result:"), "Does not print cleanup result table for no-op cleanup"); + t.false(allOutput.includes("Success:"), "Does not print success summary for no-op cleanup"); +}); + test.serial("ui5 cache clean: non-verbose mode with active cache skips additional info lookup", async (t) => { const {cache, argv, frameworkCacheCleanCache, frameworkCacheGetCacheInfo, buildCacheCleanCache, buildCacheGetCacheInfo, yesnoStub, From 66c237608a1eaf0bd2b5d10acd5f0085aee64112 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 30 Jul 2026 07:57:13 +0300 Subject: [PATCH 49/52] refactor: Consistent output --- packages/cli/lib/cli/commands/helpers/cacheOutput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/lib/cli/commands/helpers/cacheOutput.js b/packages/cli/lib/cli/commands/helpers/cacheOutput.js index f7bd7f4fd95..b488a31304e 100644 --- a/packages/cli/lib/cli/commands/helpers/cacheOutput.js +++ b/packages/cli/lib/cli/commands/helpers/cacheOutput.js @@ -219,7 +219,7 @@ export function displayCleanupResult({ } if (sections.length === 0) { - process.stderr.write(`\n${chalk.italic(PARALLEL_CLEANUP_NOTICE)}\n\n`); + process.stderr.write(`${chalk.italic(PARALLEL_CLEANUP_NOTICE)}\n`); return; } From c6b31f256a3dcd83602716466447269d6cf9dd35 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 30 Jul 2026 13:29:12 +0300 Subject: [PATCH 50/52] fix: Build info display --- packages/cli/lib/cli/commands/cache.js | 30 +-- .../lib/cli/commands/helpers/cacheOutput.js | 178 +++++++++--------- packages/cli/test/lib/cli/commands/cache.js | 80 +++++++- 3 files changed, 190 insertions(+), 98 deletions(-) diff --git a/packages/cli/lib/cli/commands/cache.js b/packages/cli/lib/cli/commands/cache.js index e439e5050a6..5df7d88285a 100644 --- a/packages/cli/lib/cli/commands/cache.js +++ b/packages/cli/lib/cli/commands/cache.js @@ -119,7 +119,7 @@ async function handleCache(argv) { if (!hasActiveCache && !hasStaleCache) { if (isVerbose) { - process.stderr.write(`\n${chalk.italic("Nothing to clean")}\n\n`); + process.stderr.write(`${chalk.italic("Nothing to clean")}\n`); } return; } @@ -139,32 +139,40 @@ async function handleCache(argv) { const confirmed = await getConfirmation(argv); if (!confirmed) { if (isVerbose) { - process.stderr.write(`\n${chalk.italic("Cancelled")}\n\n`); + process.stderr.write(`${chalk.italic("Cancelled")}\n`); } return; } - const [frameworkResult, buildResult] = await Promise.all([ + if (isVerbose) { + // Get fresh build stale info to distinguish + // between active and stale build cache after cleanup. + buildStaleInfo = await CacheManager.getAdditionalCacheInfo(ui5DataDir); + } + + const [frameworkCleanupResult, buildCleanupResult] = await Promise.all([ FrameworkCache.cleanCache(ui5DataDir), CacheManager.cleanCache(ui5DataDir), ]); - const [additionalFrameworkResult, additionalBuildResult] = await Promise.all([ + const [additionalFrameworkCleanupResult, buildStaleCleanupResult] = await Promise.all([ FrameworkCache.cleanAdditional(ui5DataDir), CacheManager.cleanAdditional(ui5DataDir), ]); if (isVerbose) { - const cleanedStaleFramework = staleInfo.length > 0 ? withAbsPath(additionalFrameworkResult, ui5DataDir) : []; - const cleanedStaleBuild = buildStaleInfo.length > 0 ? withAbsPath(additionalBuildResult, ui5DataDir) : []; - const frameworkResultAbsPath = getAbsPath(ui5DataDir, frameworkResult) || getAbsPath(ui5DataDir, frameworkInfo); - const buildResultAbsPath = getAbsPath(ui5DataDir, buildResult) || getAbsPath(ui5DataDir, buildInfo); + const staleBuildCleanupResult = buildStaleInfo?.length > 0 ? + buildStaleCleanupResult : []; + const cleanedStaleFramework = withAbsPath(additionalFrameworkCleanupResult, ui5DataDir); + const cleanedStaleBuild = withAbsPath(staleBuildCleanupResult, ui5DataDir); + const frameworkResultAbsPath = getAbsPath(ui5DataDir, frameworkCleanupResult); + const buildResultAbsPath = getAbsPath(ui5DataDir, buildCleanupResult); await displayCleanupResult({ - frameworkResult, - buildResult, + frameworkResult: frameworkCleanupResult, + buildResult: buildCleanupResult, frameworkAbsPath: frameworkResultAbsPath, buildAbsPath: buildResultAbsPath, - buildPreSize: buildInfo?.size ?? buildResult?.size ?? 0, + buildSize: buildCleanupResult?.size ?? 0, staleInfoWithAbsPaths: cleanedStaleFramework, buildAdditionalResult: cleanedStaleBuild, }); diff --git a/packages/cli/lib/cli/commands/helpers/cacheOutput.js b/packages/cli/lib/cli/commands/helpers/cacheOutput.js index b488a31304e..b6a2544be1d 100644 --- a/packages/cli/lib/cli/commands/helpers/cacheOutput.js +++ b/packages/cli/lib/cli/commands/helpers/cacheOutput.js @@ -81,6 +81,68 @@ export function displayCacheCleanWarning() { process.stderr.write(`${chalk.italic(CACHE_CLEAN_WARNING_IMPACT)}\n\n`); } +function createFrameworkItems(entries) { + const items = []; + for (const entry of entries) { + const detail = formatFrameworkStats(entry.libraryCount, entry.versionCount); + items.push({absPath: entry.absPath, detail}); + } + return items; +} + +function createBuildItems(entries, detailFormatter) { + const items = []; + for (const entry of entries) { + const detail = detailFormatter(entry.size); + items.push({absPath: entry.absPath, detail}); + } + return items; +} + +function createSections({ + activeFramework, + activeBuild, + staleFrameworkEntries, + staleBuildEntries, + staleBuildDetailFormatter, +}) { + const sections = []; + + const activeCategories = []; + if (activeFramework) { + const detail = formatFrameworkStats(activeFramework.libraryCount, activeFramework.versionCount); + activeCategories.push({ + title: GROUP_FRAMEWORK, + items: [{absPath: activeFramework.absPath, detail}], + }); + } + if (activeBuild) { + const detail = activeBuild.size > 0 ? formatSize(activeBuild.size) : ""; + activeCategories.push({ + title: GROUP_BUILD, + items: [{absPath: activeBuild.absPath, detail}], + }); + } + if (activeCategories.length > 0) { + sections.push({title: SECTION_ACTIVE_CACHE, categories: activeCategories}); + } + + const staleCategories = []; + const staleFrameworkItems = createFrameworkItems(staleFrameworkEntries); + if (staleFrameworkItems.length > 0) { + staleCategories.push({title: GROUP_FRAMEWORK, items: staleFrameworkItems}); + } + const staleBuildItems = createBuildItems(staleBuildEntries, staleBuildDetailFormatter); + if (staleBuildItems.length > 0) { + staleCategories.push({title: GROUP_BUILD, items: staleBuildItems}); + } + if (staleCategories.length > 0) { + sections.push({title: SECTION_STALE_CACHE, categories: staleCategories}); + } + + return sections; +} + /** * Display information about the cached data that will be removed. * Entries are grouped by active and stale cache data. @@ -103,47 +165,20 @@ export function displayCacheInfo({ staleInfo, buildAdditionalInfo, }) { - const sections = []; - - if (frameworkInfo || buildInfo) { - const activeCategories = []; - if (frameworkInfo) { - const detail = formatFrameworkStats(frameworkInfo.libraryCount, frameworkInfo.versionCount); - activeCategories.push({ - title: GROUP_FRAMEWORK, - items: [{absPath: frameworkAbsPath, detail}], - }); - } - if (buildInfo) { - const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; - activeCategories.push({ - title: GROUP_BUILD, - items: [{absPath: buildAbsPath, detail}], - }); - } - sections.push({title: SECTION_ACTIVE_CACHE, categories: activeCategories}); - } - - if (staleInfo?.length > 0 || buildAdditionalInfo?.length > 0) { - const staleCategories = []; - if (staleInfo.length > 0) { - const items = []; - for (const staleEntry of staleInfo) { - const detail = formatFrameworkStats(staleEntry.libraryCount, staleEntry.versionCount); - items.push({absPath: staleEntry.absPath, detail}); - } - staleCategories.push({title: GROUP_FRAMEWORK, items}); - } - if (buildAdditionalInfo.length > 0) { - const items = []; - for (const buildEntry of buildAdditionalInfo) { - const detail = buildEntry.size > 0 ? formatSize(buildEntry.size) : ""; - items.push({absPath: buildEntry.absPath, detail}); - } - staleCategories.push({title: GROUP_BUILD, items}); - } - sections.push({title: SECTION_STALE_CACHE, categories: staleCategories}); - } + const sections = createSections({ + activeFramework: frameworkInfo ? { + absPath: frameworkAbsPath, + libraryCount: frameworkInfo.libraryCount, + versionCount: frameworkInfo.versionCount, + } : null, + activeBuild: buildInfo ? { + absPath: buildAbsPath, + size: buildPreSize, + } : null, + staleFrameworkEntries: staleInfo, + staleBuildEntries: buildAdditionalInfo, + staleBuildDetailFormatter: (size) => size > 0 ? formatSize(size) : "", + }); process.stderr.write(`\n${chalk.bold("The following cached data will be removed:")}\n`); process.stderr.write("\n"); @@ -161,7 +196,7 @@ export function displayCacheInfo({ * @param {object|null} data.buildResult * @param {string|null} data.frameworkAbsPath * @param {string|null} data.buildAbsPath - * @param {number} data.buildPreSize + * @param {number} data.buildSize * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.staleInfoWithAbsPaths * @param {Array<{absPath: string, size: number}>} data.buildAdditionalResult */ @@ -170,53 +205,24 @@ export function displayCleanupResult({ buildResult, frameworkAbsPath, buildAbsPath, - buildPreSize, + buildSize, staleInfoWithAbsPaths, buildAdditionalResult, }) { - const sections = []; - - if (frameworkResult || buildResult) { - const activeCategories = []; - if (frameworkResult && frameworkAbsPath) { - const detail = formatFrameworkStats(frameworkResult.libraryCount, frameworkResult.versionCount); - activeCategories.push({ - title: GROUP_FRAMEWORK, - items: [{absPath: frameworkAbsPath, detail}], - }); - } - if (buildResult && buildAbsPath) { - const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; - activeCategories.push({ - title: GROUP_BUILD, - items: [{absPath: buildAbsPath, detail}], - }); - } - if (activeCategories.length > 0) { - sections.push({title: SECTION_ACTIVE_CACHE, categories: activeCategories}); - } - } - - if (staleInfoWithAbsPaths?.length > 0 || buildAdditionalResult?.length > 0) { - const staleCategories = []; - if (staleInfoWithAbsPaths.length > 0) { - const items = []; - for (const staleEntry of staleInfoWithAbsPaths) { - const detail = formatFrameworkStats(staleEntry.libraryCount, staleEntry.versionCount); - items.push({absPath: staleEntry.absPath, detail}); - } - staleCategories.push({title: GROUP_FRAMEWORK, items}); - } - if (buildAdditionalResult.length > 0) { - const items = []; - for (const buildEntry of buildAdditionalResult) { - const detail = buildEntry.size > 0 ? `freed ${formatSize(buildEntry.size)}` : ""; - items.push({absPath: buildEntry.absPath, detail}); - } - staleCategories.push({title: GROUP_BUILD, items}); - } - sections.push({title: SECTION_STALE_CACHE, categories: staleCategories}); - } + const sections = createSections({ + activeFramework: frameworkResult && frameworkAbsPath ? { + absPath: frameworkAbsPath, + libraryCount: frameworkResult.libraryCount, + versionCount: frameworkResult.versionCount, + } : null, + activeBuild: buildResult && buildAbsPath ? { + absPath: buildAbsPath, + size: buildSize, + } : null, + staleFrameworkEntries: staleInfoWithAbsPaths, + staleBuildEntries: buildAdditionalResult, + staleBuildDetailFormatter: (size) => size > 0 ? `freed ${formatSize(size)}` : "", + }); if (sections.length === 0) { process.stderr.write(`${chalk.italic(PARALLEL_CLEANUP_NOTICE)}\n`); diff --git a/packages/cli/test/lib/cli/commands/cache.js b/packages/cli/test/lib/cli/commands/cache.js index ae4dbb84393..3ac2d0c6e57 100644 --- a/packages/cli/test/lib/cli/commands/cache.js +++ b/packages/cli/test/lib/cli/commands/cache.js @@ -244,7 +244,6 @@ test.serial("ui5 cache clean: removes both entries and reports", async (t) => { t.true(allOutput.includes("5 versions of 18 libraries"), "Shows library stats format"); t.true(allOutput.includes("8.0 MB"), "Shows pre-clean build cache size"); t.false(allOutput.includes("Stale Cache"), "Does not report stale cache section when only active cache existed"); - t.false(allOutput.includes("7.0 MB"), "Does not show VACUUM-freed size"); t.true(allOutput.includes("Cleaned Active Cache (Framework and Build)"), "Shows success summary"); const warningCall = stderrWriteStub.getCalls().find((call) => { @@ -711,6 +710,85 @@ test.serial("ui5 cache clean: shows stale build cache in pre-confirm and post-cl t.true(allOutput.includes("Cleaned Stale Cache (Build)"), "Success summary mentions stale build group"); }); +test.serial("ui5 cache clean: post-clean summary does not duplicate active build cleanup as stale", async (t) => { + const {cache, argv, stderrWriteStub, buildCacheGetAdditionalCacheInfo, + buildCacheGetCacheInfo, buildCacheCleanCache, buildCacheCleanAdditional} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 30 * 1024 * 1024}); + buildCacheGetAdditionalCacheInfo.resolves([]); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 30 * 1024 * 1024}); + buildCacheCleanAdditional.resolves([ + {path: "buildCache/v0_7", size: 30 * 1024 * 1024}, + ]); + + argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); + argv["force"] = true; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("30.0 MB"), "Shows active build cleanup size in post-clean output"); + t.true(allOutput.includes("Cleaned Active Cache (Build)"), + "Success summary reports active build cleanup"); + t.false(allOutput.includes("Stale Cache"), + "Does not duplicate active build cleanup as stale build cleanup"); + t.false(allOutput.includes("freed 30.0 MB"), + "Does not render stale build cleanup details when active build cleanup already covered it"); +}); + +test.serial("ui5 cache clean: keeps stale build cleanup when stale existed pre-confirm", async (t) => { + const {cache, argv, stderrWriteStub, buildCacheGetAdditionalCacheInfo, + buildCacheGetCacheInfo, buildCacheCleanCache, buildCacheCleanAdditional} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves({path: "buildCache/v0_7", size: 30 * 1024 * 1024}); + buildCacheGetAdditionalCacheInfo.resolves([ + {path: "buildCache/v0_7", size: 12 * 1024 * 1024}, + ]); + buildCacheCleanCache.resolves({path: "buildCache/v0_7", size: 30 * 1024 * 1024}); + buildCacheCleanAdditional.resolves([ + {path: "buildCache/v0_7", size: 30 * 1024 * 1024}, + ]); + + argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); + argv["force"] = true; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes("Cleaned Active Cache (Build) and Stale Cache (Build)"), + "Keeps stale build section when stale build existed before confirmation"); + t.true(allOutput.includes("freed 30.0 MB"), + "Shows stale build cleanup details when stale build existed before confirmation"); +}); + +test.serial("ui5 cache clean: post-clean summary ignores stale preview when cleanup result is empty", async (t) => { + const {cache, argv, stderrWriteStub, buildCacheGetAdditionalCacheInfo, + buildCacheGetCacheInfo, buildCacheCleanCache, buildCacheCleanAdditional} = t.context; + + t.context.frameworkCacheGetCacheInfo.resolves(null); + buildCacheGetCacheInfo.resolves(null); + buildCacheGetAdditionalCacheInfo.resolves([ + {path: "buildCache/v0_7", size: 12 * 1024 * 1024}, + ]); + buildCacheCleanCache.resolves(null); + buildCacheCleanAdditional.resolves([]); + + argv["_"] = ["cache", "clean"]; + setLogLevel("verbose"); + argv["force"] = true; + await cache.handler(argv); + + const allOutput = stderrWriteStub.args.map((a) => a[0]).join(""); + t.true(allOutput.includes(path.join(TEST_UI5_DATA_DIR, "buildCache/v0_7")), + "Pre-confirm summary still shows stale build preview entry"); + t.true(allOutput.includes(PARALLEL_CLEANUP_NOTICE), + "Post-clean summary reflects current cleanup state, not stale preview snapshot"); + t.false(allOutput.includes("Cleaned Stale Cache (Build)"), + "Does not claim stale build cleanup without current cleanup result"); +}); + test.serial("ui5 cache clean: build cache and stale build cache with size 0 omit size detail", async (t) => { const {cache, argv, stderrWriteStub, buildCacheGetAdditionalCacheInfo, buildCacheCleanCache, buildCacheCleanAdditional, buildCacheGetCacheInfo} = t.context; From 00c36b63c27c55454183d442c4f5114534aa4634 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 30 Jul 2026 16:07:02 +0300 Subject: [PATCH 51/52] build: Align package-lock.json with main --- package-lock.json | 199 +++++++++++++++++++++++++++++++--------------- 1 file changed, 137 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index af3f161a293..0759bc1e9ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1262,30 +1262,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/core/node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "license": "MIT", "optional": true, "dependencies": { + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, "dependencies": { @@ -2681,6 +2671,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2698,6 +2691,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2715,6 +2711,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2732,6 +2731,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2749,6 +2751,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2766,6 +2771,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2783,6 +2791,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2800,6 +2811,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2845,29 +2859,6 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@oxc-parser/binding-win32-arm64-msvc": { "version": "0.137.0", "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz", @@ -3035,6 +3026,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3049,6 +3043,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3063,6 +3060,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3077,6 +3077,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3091,6 +3094,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3105,6 +3111,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3119,6 +3128,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3133,6 +3145,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3355,6 +3370,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3375,6 +3393,9 @@ "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3395,6 +3416,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3415,6 +3439,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3435,6 +3462,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3455,6 +3485,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3687,6 +3720,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3700,6 +3736,9 @@ "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3713,6 +3752,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3726,6 +3768,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3739,6 +3784,9 @@ "cpu": [ "loong64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3752,6 +3800,9 @@ "cpu": [ "loong64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3765,6 +3816,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3778,6 +3832,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3791,6 +3848,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3804,6 +3864,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3817,6 +3880,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3830,6 +3896,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3843,6 +3912,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4306,6 +4378,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4322,6 +4397,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4338,6 +4416,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4354,6 +4435,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4613,7 +4697,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4631,7 +4714,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4649,7 +4731,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4667,7 +4748,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4685,7 +4765,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4703,7 +4782,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4721,7 +4799,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4739,7 +4816,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4757,7 +4833,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4775,7 +4850,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4793,7 +4867,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4811,7 +4884,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4829,7 +4901,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4847,7 +4918,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4865,7 +4935,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4883,7 +4952,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4901,7 +4969,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4919,7 +4986,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4937,7 +5003,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4955,7 +5020,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -12109,6 +12173,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12129,6 +12196,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12149,6 +12219,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12169,6 +12242,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -18707,8 +18783,7 @@ "pretty-hrtime": "^1.0.3", "semver": "^7.8.5", "update-notifier": "^7.3.1", - "yargs": "^18.0.0", - "yesno": "^0.4.0" + "yargs": "^18.0.0" }, "bin": { "ui5": "bin/ui5.cjs" From ce8f608209935b1406069a8835373fa01c772eef Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Thu, 30 Jul 2026 17:16:34 +0300 Subject: [PATCH 52/52] docs: Update internal/documentation/docs/pages/Troubleshooting.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günter Klatt --- internal/documentation/docs/pages/Troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/documentation/docs/pages/Troubleshooting.md b/internal/documentation/docs/pages/Troubleshooting.md index 424a72149b1..72e541a2779 100644 --- a/internal/documentation/docs/pages/Troubleshooting.md +++ b/internal/documentation/docs/pages/Troubleshooting.md @@ -45,7 +45,7 @@ The command removes the following cached data: - **UI5 Framework packages** — downloaded UI5 library files (`~/.ui5/framework/`) - **Build cache** — build data (`~/.ui5/buildCache/`) -If a previous `ui5 cache clean` was interrupted (e.g. process killed or system crash), the command also detects and removes any leftover data from that interrupted operation, listed as separate entries: +If a previous `ui5 cache clean` was interrupted (for example, because the process was killed or the system crashed), the command also detects and removes any leftover data from that interrupted operation. This data is listed as separate entries: - **Stale UI5 Framework packages** — incomplete framework directories left over from a previously interrupted cleanup (`~/.ui5/_framework_to_delete_*/`) - **Stale build cache** — freed database pages not yet reclaimed during a previously interrupted cleanup