-
Notifications
You must be signed in to change notification settings - Fork 81
feat: Clean cache command #1394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
45566c2
b7d99c6
884b8cb
f60225e
f9df598
530c683
a7c0c58
12dd2fa
65b140b
96c3e19
d0e0874
c0a6d9f
21e1153
d77ab0f
e65e56b
c072427
3a5bfcc
787500b
8f10278
ad13a10
ded22b2
a52f318
a813fb2
5830b25
e50420b
3651dac
e7ce795
a1bc296
392ddf5
c036d9b
9de5353
f986638
a082def
1931f47
470759f
4d39800
740f11c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| import chalk from "chalk"; | ||
| import path from "node:path"; | ||
| import os from "node:os"; | ||
| import process from "node:process"; | ||
| import baseMiddleware from "../middlewares/base.js"; | ||
| import {getUi5DataDir} from "../../framework/utils.js"; | ||
| import * as frameworkCache from "@ui5/project/ui5Framework/cache"; | ||
| import CacheManager from "@ui5/project/build/cache/CacheManager"; | ||
|
|
||
| const cacheCommand = { | ||
| command: "cache", | ||
| describe: "Manage the UI5 CLI cache (downloaded framework packages and 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 confirming the prompt") | ||
| .example("$0 cache clean --yes", | ||
| "Remove all cached UI5 data without confirmation (e.g. in CI)") | ||
| .example("UI5_DATA_DIR=/custom/path $0 cache clean", | ||
| "Remove cached data from a non-default UI5 data directory") | ||
| .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 'ui5DataDir' configuration option (see 'ui5 config --help').\n\n" + | ||
| "Two cache types are removed:\n" + | ||
| " UI5 Framework packages Downloaded UI5 library files " + | ||
| "(~/.ui5/framework/)\n" + | ||
| " Build cache (DB) build data " + | ||
| "(~/.ui5/buildCache/)" | ||
| ); | ||
| }, | ||
| 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); | ||
| } | ||
|
|
||
| async function handleCache(argv) { | ||
| // Resolve UI5 data directory — uses the same resolution chain as ui5 build/serve: | ||
| // UI5_DATA_DIR env var → ui5DataDir config (~/.ui5rc) → default ~/.ui5 | ||
| // Relative paths are resolved against process.cwd() (project root when invoked from the project). | ||
| const ui5DataDir = | ||
| (await getUi5DataDir({cwd: process.cwd()})) ?? path.join(os.homedir(), ".ui5"); | ||
|
Comment on lines
+101
to
+102
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be worth spending the time to provide one API to calculate the data dir. We already had many places for this, and now there are even more.. |
||
|
|
||
| // Abort early if a framework operation is holding a lock — before prompting the user | ||
| if (await frameworkCache.isFrameworkLocked(ui5DataDir)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there is a mixup of different parts of the caches. This lock is set by the server, and my assumption is that it exists to prevent removal of cached framework libraries, and to prevent removal of the build cache, which both might be in use. There is a special case in which we actually could remove the cache (no framework deps + cache off), but I think it's okay to leave this special case out here and still abort. So I think it should not belong to the |
||
| process.stderr.write( | ||
| `${chalk.red("Error:")} Framework cache is currently locked by an active operation. ` + | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I now receive this error when a server is running, but when I start a build, and then run the clean command, the build fails as files (framework / build cache) have been removed in between. |
||
| "Please wait for it to finish and try again.\n" | ||
| ); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| // Inform the user immediately — getPackageStats may take a moment on a large cache | ||
| process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); | ||
|
|
||
| // Check what items exist before cleaning (orchestrate both domains) | ||
| const frameworkInfo = await frameworkCache.getCacheInfo(ui5DataDir); | ||
| const buildInfo = await CacheManager.getCacheInfo(ui5DataDir); | ||
|
|
||
| if (!frameworkInfo && !buildInfo) { | ||
| process.stderr.write("Nothing to clean\n"); | ||
| return; | ||
| } | ||
|
|
||
| // Compute absolute paths once — producers return relative sub-path segments | ||
| const frameworkAbsPath = frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null; | ||
| const buildAbsPath = buildInfo ? path.join(ui5DataDir, buildInfo.path) : null; | ||
|
|
||
| // Capture build size now — reused for the ✓ line to avoid a before/after mismatch | ||
| // (getDatabaseSize ≠ VACUUM-freed bytes returned by clearAllRecords) | ||
| const buildPreSize = buildInfo?.size ?? 0; | ||
|
|
||
| // Display items that will be removed | ||
| process.stderr.write(chalk.bold("\nThe following cached data will be removed:\n\n")); | ||
| if (frameworkInfo) { | ||
| const detail = 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` | ||
| ); | ||
| } | ||
| process.stderr.write("\n"); | ||
|
|
||
| // Ask for confirmation (skip with --yes) | ||
| if (!argv.yes) { | ||
| const {default: yesno} = await import("yesno"); | ||
| const confirmed = await yesno({ | ||
| question: "Do you want to continue? (y/N)", | ||
| defaultValue: false | ||
| }); | ||
| if (!confirmed) { | ||
| process.stderr.write("Cancelled\n"); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| // Perform the actual cleanup (orchestrate both domains) | ||
| const frameworkResult = await frameworkCache.cleanCache(ui5DataDir); | ||
| const buildResult = await CacheManager.cleanCache(ui5DataDir); | ||
|
|
||
| process.stderr.write("\n"); | ||
| if (frameworkResult) { | ||
| const detail = formatFrameworkStats(frameworkResult.libraryCount, frameworkResult.versionCount); | ||
| process.stderr.write( | ||
| `${chalk.green("✓")} Removed ${chalk.bold(LABEL_FRAMEWORK)}` + | ||
| ` (${frameworkAbsPath} · ${detail})\n` | ||
| ); | ||
| } | ||
| if (buildResult) { | ||
| // Use pre-clean size so the number matches what was shown before confirmation | ||
| const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; | ||
| process.stderr.write( | ||
| `${chalk.green("✓")} Removed ${chalk.bold(LABEL_BUILD)}` + | ||
| ` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n` | ||
| ); | ||
| } | ||
|
|
||
| // Success summary | ||
| const cleaned = []; | ||
| if (frameworkResult) { | ||
| cleaned.push(LABEL_FRAMEWORK); | ||
| } | ||
| if (buildResult) { | ||
| cleaned.push(LABEL_BUILD); | ||
| } | ||
| process.stderr.write(`\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`); | ||
| } | ||
|
|
||
| export default cacheCommand; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -185,46 +185,59 @@ serve.handler = async function(argv) { | |
| } | ||
|
|
||
| const {promise: pOnError, reject} = Promise.withResolvers(); | ||
| const {h2, port: actualPort} = await serverServe(graph, serverConfig, function(err) { | ||
| const serverResult = await serverServe(graph, serverConfig, function(err) { | ||
| reject(err); | ||
| }); | ||
|
|
||
| const protocol = h2 ? "https" : "http"; | ||
| let browserUrl = protocol + "://localhost:" + actualPort; | ||
| if (argv.acceptRemoteConnections) { | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.bold("⚠️ This server is accepting connections from all hosts on your network")); | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.dim.underline("Please Note:")); | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.bold.dim( | ||
| "* This server is intended for development purposes only. Do not use it in production.")); | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.dim( | ||
| "* Vulnerable (custom-)middleware can pose a threat to your system when exposed to the network")); | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.dim( | ||
| "* The use of proxy-middleware with preconfigured credentials might enable unauthorized access " + | ||
| "to a target system for third parties on your network")); | ||
| process.stderr.write("\n\n"); | ||
| } | ||
| process.stdout.write("Server started"); | ||
| process.stdout.write("\n"); | ||
| process.stdout.write("URL: " + browserUrl); | ||
| process.stdout.write("\n"); | ||
| const {h2, port: actualPort} = serverResult; | ||
|
|
||
| const onSignal = () => { | ||
| serverResult.close(() => process.exit(0)); | ||
| }; | ||
| process.once("SIGINT", onSignal); | ||
| process.once("SIGTERM", onSignal); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the docs of lockfile package:
So why do we need to close the server now? So far this has worked fine for us without the need to cleanly call Also, those handlers are only registered for the CLI command, not for the API usage. In addition, we also listen to |
||
|
|
||
| try { | ||
| const protocol = h2 ? "https" : "http"; | ||
| let browserUrl = protocol + "://localhost:" + actualPort; | ||
| if (argv.acceptRemoteConnections) { | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.bold("⚠️ This server is accepting connections from all hosts on your network")); | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.dim.underline("Please Note:")); | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.bold.dim( | ||
| "* This server is intended for development purposes only. Do not use it in production.")); | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.dim( | ||
| "* Vulnerable (custom-)middleware can pose a threat to your system when exposed to the network")); | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(chalk.dim( | ||
| "* The use of proxy-middleware with preconfigured credentials might enable unauthorized access " + | ||
| "to a target system for third parties on your network")); | ||
| process.stderr.write("\n\n"); | ||
| } | ||
| process.stdout.write("Server started"); | ||
| process.stdout.write("\n"); | ||
| process.stdout.write("URL: " + browserUrl); | ||
| process.stdout.write("\n"); | ||
|
|
||
| if (argv.open !== undefined) { | ||
| if (typeof argv.open === "string") { | ||
| let relPath = argv.open || "/"; | ||
| if (!relPath.startsWith("/")) { | ||
| relPath = "/" + relPath; | ||
| if (argv.open !== undefined) { | ||
| if (typeof argv.open === "string") { | ||
| let relPath = argv.open || "/"; | ||
| if (!relPath.startsWith("/")) { | ||
| relPath = "/" + relPath; | ||
| } | ||
| browserUrl += relPath; | ||
| } | ||
| browserUrl += relPath; | ||
| const {default: open} = await import("open"); | ||
| open(browserUrl); | ||
| } | ||
| const {default: open} = await import("open"); | ||
| open(browserUrl); | ||
| await pOnError; // Await errors that should bubble into the yargs handler | ||
| } finally { | ||
| process.off("SIGINT", onSignal); | ||
| process.off("SIGTERM", onSignal); | ||
| } | ||
| await pOnError; // Await errors that should bubble into the yargs handler | ||
| }; | ||
|
|
||
| export default serve; | ||
Uh oh!
There was an error while loading. Please reload this page.