diff --git a/internal/documentation/docs/updates/migrate-v5.md b/internal/documentation/docs/updates/migrate-v5.md index b4de575af10..e5f9fe91748 100644 --- a/internal/documentation/docs/updates/migrate-v5.md +++ b/internal/documentation/docs/updates/migrate-v5.md @@ -15,6 +15,8 @@ Or update your global install via: `npm i --global @ui5/cli@next` - **@ui5/cli: `ui5 init` defaults to Specification Version 5.0** +- **Rename: Command Option `--cache-mode` is now `--snapshot-cache`** + ## Node.js and npm Version Support @@ -27,6 +29,12 @@ UI5 CLI 5.x introduces **Specification Version 5.0**, which enables the new Comp Projects using older **Specification Versions** are expected to be **fully compatible with UI5 CLI v5**. +## Rename of Command Option + +With Specification Version 5.0, the option `--cache-mode` (for commands `ui5 build` and `ui5 serve`) has been renamed to `--snapshot-cache`. + +The behavior remains the same. When `--cache-mode` is used, a deprecation warning is logged and `--snapshot-cache` is set to `Default`. + ## UI5 CLI Init Command The `ui5 init` command now generates projects with Specification Version 5.0 by default. diff --git a/package-lock.json b/package-lock.json index 39b760fffd3..68f62a74b77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3615,9 +3615,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3632,9 +3629,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3649,9 +3643,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3666,9 +3657,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3683,9 +3671,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3700,9 +3685,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3717,9 +3699,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3734,9 +3713,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/packages/cli/lib/cli/commands/build.js b/packages/cli/lib/cli/commands/build.js index 31e7b56062c..3a77f5eb11c 100644 --- a/packages/cli/lib/cli/commands/build.js +++ b/packages/cli/lib/cli/commands/build.js @@ -1,5 +1,6 @@ import baseMiddleware from "../middlewares/base.js"; -import path from "node:path"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("cli:commands:build"); const build = { command: "build", @@ -84,6 +85,17 @@ build.builder = function(cli) { default: false, type: "boolean" }) + .option("cache", { + describe: + "Cache mode to use for building UI5 projects. " + + "The 'Default' behavior is to always use the cache if available. 'Force' uses the cache only " + + "(if it's unavailable or invalid, the build fails). 'ReadOnly' does not create or update any " + + "cache but makes use of a cache if available. 'Off' does not use any cache and always triggers " + + "a rebuild of the project", + type: "string", + default: "Default", + choices: ["Default", "Force", "ReadOnly", "Off"], + }) .option("create-build-manifest", { describe: "Store build metadata in a '.ui5' directory in the build destination, " + "allowing reuse of the build result in other builds", @@ -107,13 +119,30 @@ build.builder = function(cli) { type: "string" }) .option("cache-mode", { + // Deprecated + hidden: true, + describe: + "As of UI5 CLI version 5, renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior.", + type: "string", + choices: ["Default", "Force", "Off"], + }) + .coerce("cache-mode", (opt) => { + // Log a warning if this option is used + if (opt !== undefined) { + log.warn("As of UI5 CLI version 5, '--cache-mode' was renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior."); + } + return opt; + }) + .option("snapshot-cache", { describe: "Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " + "The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " + "does not create any requests. 'Off' invalidates any existing cache and updates from the repository", type: "string", - default: "Default", - choices: ["Default", "Force", "Off"] + defaultDescription: "Default", + choices: ["Default", "Force", "Off"], }) .option("experimental-css-variables", { describe: @@ -160,13 +189,13 @@ async function handleBuild(argv) { filePath: argv.dependencyDefinition, rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback }); } else { graph = await graphFromPackageDependencies({ rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback workspaceConfigPath: argv.workspaceConfig, workspaceName: argv.workspace === false ? null : argv.workspace, }); @@ -174,7 +203,6 @@ async function handleBuild(argv) { const buildSettings = graph.getRoot().getBuilderSettings() || {}; await graph.build({ graph, - cacheDir: path.join(graph.getRoot().getRootPath(), ".ui5-cache"), destPath: argv.dest, cleanDest: argv["clean-dest"], createBuildManifest: argv["create-build-manifest"], @@ -196,6 +224,7 @@ async function handleBuild(argv) { excludedTasks: argv["exclude-task"], cssVariables: argv["experimental-css-variables"], outputStyle: argv["output-style"], + cache: argv["cache"], }); } diff --git a/packages/cli/lib/cli/commands/serve.js b/packages/cli/lib/cli/commands/serve.js index 218c72ab1d0..a64926ec297 100644 --- a/packages/cli/lib/cli/commands/serve.js +++ b/packages/cli/lib/cli/commands/serve.js @@ -2,6 +2,8 @@ import path from "node:path"; import os from "node:os"; import chalk from "chalk"; import baseMiddleware from "../middlewares/base.js"; +import {getLogger} from "@ui5/logger"; +const log = getLogger("cli:commands:serve"); // Serve const serve = { @@ -61,19 +63,47 @@ serve.builder = function(cli) { default: false, type: "boolean" }) + .option("cache", { + describe: + "Cache mode to use for building UI5 projects. " + + "The 'Default' behavior is to always use the cache if available. 'Force' uses the cache only " + + "(if it's unavailable or invalid, the build fails). 'Read-only' does not create or update any " + + "cache but makes use of a cache if available. 'Off' does not use any cache and always triggers " + + "a rebuild of the project", + type: "string", + default: "Default", + choices: ["Default", "Force", "ReadOnly", "Off"], + }) .option("framework-version", { describe: "Overrides the framework version defined by the project. " + "Takes the same value as the version part of \"ui5 use\"", type: "string" }) .option("cache-mode", { + // Deprecated + hidden: true, + describe: + "As of UI5 CLI version 5, renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior.", + type: "string", + choices: ["Default", "Force", "Off"], + }) + .coerce("cache-mode", (opt) => { + // Log a warning if this option is used + if (opt !== undefined) { + log.warn("As of UI5 CLI version 5, '--cache-mode' was renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior."); + } + return opt; + }) + .option("snapshot-cache", { describe: "Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " + "The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " + "does not create any requests. 'Off' invalidates any existing cache and updates from the repository", type: "string", - default: "Default", - choices: ["Default", "Force", "Off"] + defaultDescription: "Default", + choices: ["Default", "Force", "Off"], }) .example("ui5 serve", "Start a web server for the current project") .example("ui5 serve --h2", "Enable the HTTP/2 protocol for the web server (requires SSL certificate)") @@ -95,13 +125,13 @@ serve.handler = async function(argv) { filePath: argv.dependencyDefinition, rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback }); } else { graph = await graphFromPackageDependencies({ rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback workspaceConfigPath: argv.workspaceConfig, workspaceName: argv.workspace === false ? null : argv.workspace, }); @@ -137,7 +167,8 @@ serve.handler = async function(argv) { cert: argv.h2 ? argv.cert : undefined, key: argv.h2 ? argv.key : undefined, sendSAPTargetCSP: !!argv.sapCspPolicies, - serveCSPReports: !!argv.serveCspReports + serveCSPReports: !!argv.serveCspReports, + cache: argv.cache, }; if (serverConfig.h2) { diff --git a/packages/cli/lib/cli/commands/tree.js b/packages/cli/lib/cli/commands/tree.js index e683a72b676..fa2b2db1c01 100644 --- a/packages/cli/lib/cli/commands/tree.js +++ b/packages/cli/lib/cli/commands/tree.js @@ -28,7 +28,13 @@ tree.builder = function(cli) { "Takes the same value as the version part of \"ui5 use\"", type: "string" }) - .option("cache-mode", { + .hide("cache-mode", { + describe: + "As of UI5 CLI version 5, renamed to '--snapshot-cache'. " + + "Use '--snapshot-cache' to control this behavior.", + type: "string", + }) + .option("snapshot-cache", { describe: "Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " + "The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " + @@ -51,13 +57,13 @@ tree.handler = async function(argv) { graph = await graphFromStaticFile({ filePath: argv.dependencyDefinition, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache, }); } else { graph = await graphFromPackageDependencies({ rootConfigPath: argv.config, versionOverride: argv.frameworkVersion, - cacheMode: argv.cacheMode, + snapshotCache: argv.snapshotCache, workspaceConfigPath: argv.workspaceConfig, workspaceName: argv.workspace === false ? null : argv.workspace, }); diff --git a/packages/cli/test/lib/cli/commands/build.js b/packages/cli/test/lib/cli/commands/build.js index b2104346577..26f4f5b6afc 100644 --- a/packages/cli/test/lib/cli/commands/build.js +++ b/packages/cli/test/lib/cli/commands/build.js @@ -25,6 +25,8 @@ function getDefaultArgv() { "experimentalCssVariables": false, "cache-mode": "Default", "cacheMode": "Default", + "snapshot-cache": "Default", + "snapshotCache": "Default", "output-style": "Default", "$0": "ui5" }; @@ -133,15 +135,15 @@ test.serial("ui5 build --framework-version", async (t) => { versionOverride: "1.99.0", workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); -test.serial("ui5 build --cache-mode", async (t) => { +test.serial("ui5 build --snapshot-cache", async (t) => { const {build, argv, graphFromPackageDependenciesStub} = t.context; - argv.cacheMode = "Off"; + argv.snapshotCache = "Off"; await build.handler(argv); @@ -152,7 +154,7 @@ test.serial("ui5 build --cache-mode", async (t) => { versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Off", + snapshotCache: "Off", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -171,7 +173,7 @@ test.serial("ui5 build --config", async (t) => { versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -190,7 +192,7 @@ test.serial("ui5 build --workspace", async (t) => { versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: "dolphin", - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -209,7 +211,7 @@ test.serial("ui5 build --no-workspace", async (t) => { versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: null, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -229,7 +231,7 @@ test.serial("ui5 build --workspace-config", async (t) => { versionOverride: undefined, workspaceConfigPath: fakePath, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromPackageDependencies got called with expected arguments" ); }); @@ -247,7 +249,7 @@ test.serial("ui5 build --dependency-definition", async (t) => { filePath: "dependencies.yaml", rootConfigPath: undefined, versionOverride: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromStaticFile got called with expected arguments" ); }); @@ -266,7 +268,7 @@ test.serial("ui5 build --dependency-definition --config", async (t) => { filePath: "dependencies.yaml", rootConfigPath: "ui5-test.yaml", versionOverride: undefined, - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromStaticFile got called with expected arguments" ); }); @@ -286,7 +288,7 @@ test.serial("ui5 build --dependency-definition --config --framework-version", as filePath: "dependencies.yaml", rootConfigPath: "ui5-test.yaml", versionOverride: "1.99.0", - cacheMode: "Default", + snapshotCache: "Default", }, "generateProjectGraph.graphFromStaticFile got called with expected arguments" ); }); diff --git a/packages/cli/test/lib/cli/commands/serve.js b/packages/cli/test/lib/cli/commands/serve.js index 0332a573b7a..2d783fba3a4 100644 --- a/packages/cli/test/lib/cli/commands/serve.js +++ b/packages/cli/test/lib/cli/commands/serve.js @@ -26,6 +26,8 @@ function getDefaultArgv() { "serveCspReports": false, "cache-mode": "Default", "cacheMode": "Default", + "snapshot-cache": "Default", + "snapshotCache": "Default", "$0": "ui5" }; } @@ -97,7 +99,7 @@ test.serial("ui5 serve: default", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -145,7 +147,7 @@ test.serial("ui5 serve --h2", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -188,7 +190,7 @@ test.serial("ui5 serve --accept-remote-connections", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, ` @@ -233,7 +235,7 @@ test.serial("ui5 serve --open", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -275,7 +277,7 @@ test.serial("ui5 serve --open (opens default url)", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -318,7 +320,7 @@ test.serial("ui5 serve --config", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: fakePath, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -355,7 +357,7 @@ test.serial("ui5 serve --dependency-definition", async (t) => { t.is(graph.graphFromStaticFile.callCount, 1); t.deepEqual(graph.graphFromStaticFile.getCall(0).args, [{ filePath: fakePath, versionOverride: undefined, - cacheMode: "Default", rootConfigPath: undefined + snapshotCache: "Default", rootConfigPath: undefined }]); t.is(t.context.consoleOutput, `Server started @@ -395,7 +397,7 @@ test.serial("ui5 serve --dependency-definition / --config", async (t) => { t.is(graph.graphFromStaticFile.callCount, 1); t.deepEqual(graph.graphFromStaticFile.getCall(0).args, [{ filePath: fakeDependenciesPath, versionOverride: undefined, - cacheMode: "Default", rootConfigPath: fakeConfigPath + snapshotCache: "Default", rootConfigPath: fakeConfigPath }]); t.is(t.context.consoleOutput, `Server started @@ -432,7 +434,7 @@ test.serial("ui5 serve --framework-version", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: "1.234.5", workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -456,10 +458,10 @@ URL: http://localhost:8080 ]); }); -test.serial("ui5 serve --cache-mode", async (t) => { +test.serial("ui5 serve --snapshotCache", async (t) => { const {argv, serve, graph, server, fakeGraph} = t.context; - argv.cacheMode = "Force"; + argv.snapshotCache = "Force"; serve.handler(argv); await new Promise((resolve) => setTimeout(resolve, 0)); @@ -469,7 +471,7 @@ test.serial("ui5 serve --cache-mode", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Force", + snapshotCache: "Force", }]); t.is(t.context.consoleOutput, `Server started @@ -506,7 +508,7 @@ test.serial("ui5 serve --workspace", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: "dolphin", - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -543,7 +545,7 @@ test.serial("ui5 serve --no-workspace", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: null, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -581,7 +583,7 @@ test.serial("ui5 serve --workspace-config", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: fakePath, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -618,7 +620,7 @@ test.serial("ui5 serve --sap-csp-policies", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -655,7 +657,7 @@ test.serial("ui5 serve --serve-csp-reports", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -692,7 +694,7 @@ test.serial("ui5 serve --simple-index", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -736,7 +738,7 @@ test.serial("ui5 serve with ui5.yaml port setting", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -787,7 +789,7 @@ test.serial("ui5 serve --h2 with ui5.yaml port setting", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started @@ -845,7 +847,7 @@ test.serial("ui5 serve --h2 with ui5.yaml port setting and port CLI argument", a t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, `Server started diff --git a/packages/cli/test/lib/cli/commands/tree.js b/packages/cli/test/lib/cli/commands/tree.js index f8e8fcad689..9d50acffb01 100644 --- a/packages/cli/test/lib/cli/commands/tree.js +++ b/packages/cli/test/lib/cli/commands/tree.js @@ -15,6 +15,8 @@ function getDefaultArgv() { "silent": false, "cache-mode": "Default", "cacheMode": "Default", + "snapshot-cache": "Default", + "snapshotCache": "Default", "flat": false, "level": undefined, "$0": "ui5" @@ -78,7 +80,7 @@ test.serial("ui5 tree (Without dependencies)", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -148,7 +150,7 @@ test.serial("ui5 tree", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -228,7 +230,7 @@ test.serial("ui5 tree --flat", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -305,7 +307,7 @@ test.serial("ui5 tree --level 1", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -395,7 +397,7 @@ test.serial("ui5 tree (With extensions)", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -441,7 +443,7 @@ test.serial("ui5 tree --perf", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -482,7 +484,7 @@ test.serial("ui5 tree --framework-version", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: "1.234.5", workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -495,10 +497,10 @@ ${chalk.italic("None")} `); }); -test.serial("ui5 tree --cache-mode", async (t) => { +test.serial("ui5 tree --snapshot-cache", async (t) => { const {argv, tree, traverseBreadthFirst, graph} = t.context; - argv.cacheMode = "Force"; + argv.snapshotCache = "Force"; traverseBreadthFirst.callsFake(async (fn) => { await fn({ @@ -521,7 +523,7 @@ test.serial("ui5 tree --cache-mode", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Force", + snapshotCache: "Force", }]); t.is(t.context.consoleOutput, @@ -561,7 +563,7 @@ test.serial("ui5 tree --config", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: fakePath, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -600,7 +602,7 @@ test.serial("ui5 tree --workspace", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: "dolphin", - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -639,7 +641,7 @@ test.serial("ui5 tree --no-workspace", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: undefined, workspaceName: null, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -679,7 +681,7 @@ test.serial("ui5 tree --workspace-config", async (t) => { t.deepEqual(graph.graphFromPackageDependencies.getCall(0).args, [{ rootConfigPath: undefined, versionOverride: undefined, workspaceConfigPath: fakePath, workspaceName: undefined, - cacheMode: "Default", + snapshotCache: "Default", }]); t.is(t.context.consoleOutput, @@ -717,7 +719,7 @@ test.serial("ui5 tree --dependency-definition", async (t) => { t.is(graph.graphFromPackageDependencies.callCount, 0); t.is(graph.graphFromStaticFile.callCount, 1); t.deepEqual(graph.graphFromStaticFile.getCall(0).args, [{ - filePath: fakePath, versionOverride: undefined, cacheMode: "Default" + filePath: fakePath, versionOverride: undefined, snapshotCache: "Default" }]); t.is(t.context.consoleOutput, diff --git a/packages/project/lib/build/BuildServer.js b/packages/project/lib/build/BuildServer.js index ebeb9744e78..fbf500823bd 100644 --- a/packages/project/lib/build/BuildServer.js +++ b/packages/project/lib/build/BuildServer.js @@ -4,6 +4,7 @@ import BuildReader from "./BuildReader.js"; import WatchHandler from "./helpers/WatchHandler.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("build:BuildServer"); +import Cache from "./cache/Cache.js"; class AbortBuildError extends Error { constructor(message) { @@ -186,8 +187,10 @@ class BuildServer extends EventEmitter { throw new Error(`Project '${projectName}' not found in project graph`); } const projectBuildStatus = this.#projectBuildStatus.get(projectName); + const cacheMode = this.#projectBuilder._buildContext.getBuildConfig().cache; - if (projectBuildStatus.isFresh()) { + // When cache=Off, always rebuild - don't use in-memory cached readers + if (cacheMode !== Cache.Off && projectBuildStatus.isFresh()) { return projectBuildStatus.getReader(); } const {promise, resolve, reject} = Promise.withResolvers(); diff --git a/packages/project/lib/build/ProjectBuilder.js b/packages/project/lib/build/ProjectBuilder.js index 429e4f4f69f..85c0caf8f53 100644 --- a/packages/project/lib/build/ProjectBuilder.js +++ b/packages/project/lib/build/ProjectBuilder.js @@ -32,6 +32,8 @@ class ProjectBuilder { * @property {Array.} [includedTasks=[]] List of tasks to be included * @property {Array.} [excludedTasks=[]] List of tasks to be excluded. * If the wildcard '*' is provided, only the included tasks will be executed. + * @property {module:@ui5/project/build/cache/Cache} [cache=Cache.Default] + * Cache mode to use for building UI5 projects */ /** diff --git a/packages/project/lib/build/cache/Cache.js b/packages/project/lib/build/cache/Cache.js new file mode 100644 index 00000000000..cf2dd4a1bd6 --- /dev/null +++ b/packages/project/lib/build/cache/Cache.js @@ -0,0 +1,18 @@ +/** + * Cache modes for building UI5 projects + * + * @public + * @readonly + * @enum {string} + * @property {string} Default Use cache if available + * @property {string} Force Use cache only (if it's unavailable or invalid, the build fails) + * @property {string} ReadOnly Do not create or update any cache but make use of a cache if available + * @property {string} Off Do not use any cache and always rebuild + * @module @ui5/project/build/cache/Cache + */ +export default { + Default: "Default", + Force: "Force", + ReadOnly: "ReadOnly", + Off: "Off" +}; diff --git a/packages/project/lib/build/cache/ProjectBuildCache.js b/packages/project/lib/build/cache/ProjectBuildCache.js index e583a64b453..3e91050d205 100644 --- a/packages/project/lib/build/cache/ProjectBuildCache.js +++ b/packages/project/lib/build/cache/ProjectBuildCache.js @@ -9,6 +9,7 @@ import StageCache from "./StageCache.js"; import ResourceIndex from "./index/ResourceIndex.js"; import {matchResourceMetadataStrict} from "./utils.js"; const log = getLogger("build:cache:ProjectBuildCache"); +import Cache from "./Cache.js"; export const INDEX_STATES = Object.freeze({ RESTORING_PROJECT_INDICES: "restoring_project_indices", @@ -49,6 +50,7 @@ export default class ProjectBuildCache { #project; #buildSignature; #cacheManager; + #cacheMode; #currentProjectReader; #currentDependencyReader; #sourceIndex; @@ -81,13 +83,15 @@ export default class ProjectBuildCache { * @param {@ui5/project/specifications/Project} project Project instance * @param {string} buildSignature Build signature for the current build * @param {object} cacheManager Cache manager instance for reading/writing cache data + * @param {string} cacheMode Cache mode to use for building UI5 projects */ - constructor(project, buildSignature, cacheManager) { + constructor(project, buildSignature, cacheManager, cacheMode) { log.verbose( `ProjectBuildCache for project ${project.getName()} uses build signature ${buildSignature}`); this.#project = project; this.#buildSignature = buildSignature; this.#cacheManager = cacheManager; + this.#cacheMode = cacheMode; } /** @@ -100,10 +104,11 @@ export default class ProjectBuildCache { * @param {@ui5/project/specifications/Project} project Project instance * @param {string} buildSignature Build signature for the current build * @param {object} cacheManager Cache manager instance + * @param {string} cacheMode Cache mode to use for building UI5 projects * @returns {Promise<@ui5/project/build/cache/ProjectBuildCache>} Initialized cache instance */ - static async create(project, buildSignature, cacheManager) { - return new ProjectBuildCache(project, buildSignature, cacheManager); + static async create(project, buildSignature, cacheManager, cacheMode) { + return new ProjectBuildCache(project, buildSignature, cacheManager, cacheMode); } /** @@ -116,6 +121,10 @@ export default class ProjectBuildCache { * @returns {Promise} */ async initSourceIndex() { + // When cache=Off, always reinitialize to clear cached state + if (this.#cacheMode === Cache.Off && this.#combinedIndexState !== INDEX_STATES.RESTORING_PROJECT_INDICES) { + this.#combinedIndexState = INDEX_STATES.RESTORING_PROJECT_INDICES; + } if (this.#combinedIndexState !== INDEX_STATES.RESTORING_PROJECT_INDICES) { // Already initialized (e.g. reused across builds) return; @@ -180,6 +189,14 @@ export default class ProjectBuildCache { const changesDetected = await this.#flushPendingChanges(); if (changesDetected) { this.#resultCacheState = RESULT_CACHE_STATES.PENDING_VALIDATION; + // Force mode: Fail immediately if changes were detected + if (this.#cacheMode === Cache.Force) { + throw new Error( + `Cache is in "Force" mode but cache is stale for project ${this.#project.getName()} ` + + `due to detected source file changes. ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.` + ); + } } if (log.isLevelEnabled("perf")) { log.perf( @@ -189,6 +206,14 @@ export default class ProjectBuildCache { this.#combinedIndexState = INDEX_STATES.FRESH; } + // When cache=Off, don't validate or use result cache + if (this.#cacheMode === Cache.Off) { + log.verbose(`Cache is in "Off" mode for project ${this.#project.getName()}. ` + + `Skipping result cache validation`); + this.#resultCacheState = RESULT_CACHE_STATES.NO_CACHE; + return false; + } + if (this.#resultCacheState === RESULT_CACHE_STATES.PENDING_VALIDATION) { log.verbose(`Project ${this.#project.getName()} cache requires validation due to detected changes.`); const findStart = performance.now(); @@ -289,6 +314,10 @@ export default class ProjectBuildCache { * @returns {boolean} True if the cache is fresh */ isFresh() { + // When cache=Off, always return false to force rebuilds + if (this.#cacheMode === Cache.Off) { + return false; + } return this.#combinedIndexState === INDEX_STATES.FRESH && this.#resultCacheState === RESULT_CACHE_STATES.FRESH_AND_IN_USE; } @@ -1283,6 +1312,25 @@ export default class ProjectBuildCache { */ async #initSourceIndex() { const sourceReader = this.#project.getSourceReader(); + + if (this.#cacheMode === Cache.Off) { + // Caching disabled: Create fresh index + log.verbose(`Cache is in "Off" mode. ` + + `Initializing fresh source index for project ${this.#project.getName()}`); + this.#sourceIndex = await ResourceIndex.create(await sourceReader.byGlob("/**/*"), + Date.now()); + this.#combinedIndexState = INDEX_STATES.INITIAL; + // Clear any existing task cache from previous builds + this.#taskCache.clear(); + this.#stageCache = new StageCache(); + // Reset ProjectResources to initial stage if it exists (clear any cached result stage) + const currentStage = this.#project.getProjectResources().getStage(); + if (currentStage && currentStage.getId() !== "initial") { + this.#project.getProjectResources().useStage("initial"); + } + return; + } + const [resources, indexCache] = await Promise.all([ await sourceReader.byGlob("/**/*"), await this.#cacheManager.readIndexCache(this.#project.getId(), this.#buildSignature, "source"), @@ -1333,6 +1381,17 @@ export default class ProjectBuildCache { this.#taskCache.set(buildTaskCache.getTaskName(), buildTaskCache); } + // Force mode: Fail if cache is stale (source files changed OR pending changes exist) + if (this.#cacheMode === Cache.Force && + (changedPaths.length > 0 || this.#changedProjectSourcePaths.length > 0)) { + const totalChanges = changedPaths.length + this.#changedProjectSourcePaths.length; + throw new Error( + `Cache is in "Force" mode but cache is stale for project ${this.#project.getName()} ` + + `due to ${totalChanges} changed source file(s). ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.` + ); + } + if (!changedPaths.length) { // Source index is up-to-date with no changes this.#cachedSourceSignature = resourceIndex.getSignature(); @@ -1343,6 +1402,10 @@ export default class ProjectBuildCache { // Now awaiting initialization of dependency indices this.#combinedIndexState = INDEX_STATES.RESTORING_DEPENDENCY_INDICES; } else { + if (this.#cacheMode === Cache.Force) { + throw new Error(`Cache is in "Force" mode but no cache found for project ${this.#project.getName()}. ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.`); + } // No index cache found, create new index this.#sourceIndex = await ResourceIndex.create(resources, Date.now()); this.#combinedIndexState = INDEX_STATES.INITIAL; @@ -1405,6 +1468,16 @@ export default class ProjectBuildCache { * @returns {Promise} */ async writeCache() { + // OFF or ReadOnly modes: Skip all cache writes + if (this.#cacheMode === Cache.Off || this.#cacheMode === Cache.ReadOnly) { + log.verbose( + `Skipping cache write for project ${this.#project.getName()} ` + + `(cache mode: ${this.#cacheMode})` + ); + return; + } + + // Default and Force modes: Write cache normally const cacheWriteStart = performance.now(); this.#cacheManager.beginMetadataBatch(); try { diff --git a/packages/project/lib/build/helpers/BuildContext.js b/packages/project/lib/build/helpers/BuildContext.js index ee868ab3b3b..c14308fb103 100644 --- a/packages/project/lib/build/helpers/BuildContext.js +++ b/packages/project/lib/build/helpers/BuildContext.js @@ -4,6 +4,7 @@ import CacheManager from "../cache/CacheManager.js"; import {getBaseSignature} from "./getBuildSignature.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("build:helpers:BuildContext"); +import Cache from "../cache/Cache.js"; /** * Context of a build process @@ -21,6 +22,7 @@ class BuildContext { createBuildManifest = false, outputStyle = OutputStyleEnum.Default, includedTasks = [], excludedTasks = [], + cache = Cache.Default, } = {}) { if (!graph) { throw new Error(`Missing parameter 'graph'`); @@ -73,8 +75,11 @@ class BuildContext { outputStyle, includedTasks, excludedTasks, + cache }; - this._buildSignatureBase = getBaseSignature(this._buildConfig); + // eslint-disable-next-line no-unused-vars + const {cache: _ignoreMe, ...signatureConfig} = this._buildConfig; // Clones buildConfig omitting the cache mode + this._buildSignatureBase = getBaseSignature(signatureConfig); this._taskRepository = taskRepository; diff --git a/packages/project/lib/build/helpers/ProjectBuildContext.js b/packages/project/lib/build/helpers/ProjectBuildContext.js index 426fecb140a..bed574f9e02 100644 --- a/packages/project/lib/build/helpers/ProjectBuildContext.js +++ b/packages/project/lib/build/helpers/ProjectBuildContext.js @@ -56,8 +56,9 @@ class ProjectBuildContext { static async create(buildContext, project, cacheManager, baseSignature) { const buildSignature = getProjectSignature( baseSignature, project, buildContext.getGraph(), buildContext.getTaskRepository()); + const cacheMode = buildContext.getBuildConfig().cache; const buildCache = await ProjectBuildCache.create( - project, buildSignature, cacheManager); + project, buildSignature, cacheManager, cacheMode); return new ProjectBuildContext( buildContext, project, diff --git a/packages/project/lib/graph/ProjectGraph.js b/packages/project/lib/graph/ProjectGraph.js index f3d2ccc0384..b0d8f1cfaf0 100644 --- a/packages/project/lib/graph/ProjectGraph.js +++ b/packages/project/lib/graph/ProjectGraph.js @@ -1,6 +1,7 @@ import OutputStyleEnum from "../build/helpers/ProjectBuilderOutputStyle.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("graph:ProjectGraph"); +import Cache from "../../../project/lib/build/cache/Cache.js"; /** @@ -713,6 +714,8 @@ class ProjectGraph { * @param {Array.} [parameters.excludedTasks=[]] List of tasks to be excluded. * @param {module:@ui5/project/build/ProjectBuilderOutputStyle} [parameters.outputStyle=Default] * Processes build results into a specific directory structure. + * @param {module:@ui5/project/build/cache/Cache} [parameters.cache=Default] + * Cache mode to use for building UI5 projects * @returns {Promise} Promise resolving to undefined once build has finished */ async build({ @@ -722,6 +725,7 @@ class ProjectGraph { selfContained = false, cssVariables = false, jsdoc = false, createBuildManifest = false, includedTasks = [], excludedTasks = [], outputStyle = OutputStyleEnum.Default, + cache = Cache.Default, }) { this.seal(); // Do not allow further changes to the graph if (this._builtOrServed) { @@ -740,6 +744,7 @@ class ProjectGraph { selfContained, cssVariables, jsdoc, createBuildManifest, includedTasks, excludedTasks, outputStyle, + cache } }); return await builder.buildToTarget({ @@ -754,6 +759,7 @@ class ProjectGraph { initialBuildIncludedDependencies = [], initialBuildExcludedDependencies = [], selfContained = false, cssVariables = false, jsdoc = false, createBuildManifest = false, includedTasks = [], excludedTasks = [], + cache = Cache.Default, }) { this.seal(); // Do not allow further changes to the graph if (this._builtOrServed) { @@ -773,6 +779,7 @@ class ProjectGraph { createBuildManifest, includedTasks, excludedTasks, outputStyle: OutputStyleEnum.Default, + cache } }); const { diff --git a/packages/project/lib/graph/graph.js b/packages/project/lib/graph/graph.js index 0885265447f..bf1bd5c3583 100644 --- a/packages/project/lib/graph/graph.js +++ b/packages/project/lib/graph/graph.js @@ -31,8 +31,8 @@ const log = getLogger("generateProjectGraph"); * Whether framework dependencies should be added to the graph * @param {string|null} [options.workspaceName=default] * Name of the workspace configuration that should be used. "default" if not provided. - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] - * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache] + * Snapshot cache mode to use when consuming SNAPSHOT versions of a framework * @param {string} [options.workspaceConfigPath=ui5-workspace.yaml] * Workspace configuration file to use if no object has been provided * @param {@ui5/project/graph/Workspace~Configuration} [options.workspaceConfiguration] @@ -42,7 +42,7 @@ const log = getLogger("generateProjectGraph"); */ export async function graphFromPackageDependencies({ cwd, rootConfiguration, rootConfigPath, - versionOverride, cacheMode, resolveFrameworkDependencies = true, + versionOverride, snapshotCache, resolveFrameworkDependencies = true, workspaceName="default", workspaceConfiguration, workspaceConfigPath = "ui5-workspace.yaml" }) { @@ -73,7 +73,7 @@ export async function graphFromPackageDependencies({ const projectGraph = await projectGraphBuilder(provider, workspace); if (resolveFrameworkDependencies) { - await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, cacheMode, workspace}); + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, snapshotCache, workspace}); } return projectGraph; @@ -98,8 +98,8 @@ export async function graphFromPackageDependencies({ * Configuration file to use for the root module instead the default ui5.yaml. Either a path relative to * cwd or an absolute path. In both case, platform-specific path segment separators must be used. * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] - * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache] + * Snapshot cache mode to use when consuming SNAPSHOT versions of a framework * @param {string} [options.resolveFrameworkDependencies=true] * Whether framework dependencies should be added to the graph * @returns {Promise<@ui5/project/graph/ProjectGraph>} Promise resolving to a Project Graph instance @@ -107,7 +107,7 @@ export async function graphFromPackageDependencies({ export async function graphFromStaticFile({ filePath = "projectDependencies.yaml", cwd, rootConfiguration, rootConfigPath, - versionOverride, cacheMode, resolveFrameworkDependencies = true + versionOverride, snapshotCache, resolveFrameworkDependencies = true }) { log.verbose(`Creating project graph using static file...`); const { @@ -128,7 +128,7 @@ export async function graphFromStaticFile({ const projectGraph = await projectGraphBuilder(provider); if (resolveFrameworkDependencies) { - await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, cacheMode}); + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, snapshotCache}); } return projectGraph; @@ -150,8 +150,8 @@ export async function graphFromStaticFile({ * Configuration file to use for the root module instead the default ui5.yaml. Either a path relative to * cwd or an absolute path. In both case, platform-specific path segment separators must be used. * @param {string} [options.versionOverride] Framework version to use instead of the one defined in the root project - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] - * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache] + * Snapshot cache mode to use when consuming SNAPSHOT versions of a framework * @param {string} [options.resolveFrameworkDependencies=true] * Whether framework dependencies should be added to the graph * @returns {Promise<@ui5/project/graph/ProjectGraph>} Promise resolving to a Project Graph instance @@ -159,7 +159,7 @@ export async function graphFromStaticFile({ export async function graphFromObject({ dependencyTree, cwd, rootConfiguration, rootConfigPath, - versionOverride, cacheMode, resolveFrameworkDependencies = true + versionOverride, snapshotCache, resolveFrameworkDependencies = true }) { log.verbose(`Creating project graph using object...`); const { @@ -178,7 +178,7 @@ export async function graphFromObject({ const projectGraph = await projectGraphBuilder(dependencyTreeProvider); if (resolveFrameworkDependencies) { - await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, cacheMode}); + await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride, snapshotCache}); } return projectGraph; diff --git a/packages/project/lib/graph/helpers/ui5Framework.js b/packages/project/lib/graph/helpers/ui5Framework.js index 19737a4bd7b..660cc78427e 100644 --- a/packages/project/lib/graph/helpers/ui5Framework.js +++ b/packages/project/lib/graph/helpers/ui5Framework.js @@ -282,15 +282,15 @@ export default { * @param {object} [options] * @param {string} [options.versionOverride] Framework version to use instead of the root projects framework * version - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode] - * Cache mode to use when consuming SNAPSHOT versions of a framework + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache] + * Snapshot cache mode to use when consuming SNAPSHOT versions of a framework * @param {@ui5/project/graph/Workspace} [options.workspace] * Optional workspace instance to use for overriding node resolutions * @returns {Promise<@ui5/project/graph/ProjectGraph>} * Promise resolving with the given graph instance to allow method chaining */ enrichProjectGraph: async function(projectGraph, options = {}) { - const {workspace, cacheMode} = options; + const {workspace, snapshotCache} = options; const rootProject = projectGraph.getRoot(); const frameworkName = rootProject.getFrameworkName(); const frameworkVersion = rootProject.getFrameworkVersion(); @@ -386,7 +386,7 @@ export default { cwd, version, providedLibraryMetadata, - cacheMode, + snapshotCache, ui5DataDir }); diff --git a/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js index 7002bddbd27..01c4b843541 100644 --- a/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js +++ b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js @@ -34,21 +34,21 @@ class Sapui5MavenSnapshotResolver extends AbstractResolver { * @param {string} [options.cwd=process.cwd()] Current working directory * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages, * metadata and configuration used by the resolvers. Relative to `process.cwd()` - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode=Default] - * Cache mode to use + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [options.snapshotCache=Default] + * Snapshot cache mode to use */ constructor(options) { super(options); const { - cacheMode, + snapshotCache, } = options; this._installer = new Installer({ ui5DataDir: this._ui5DataDir, snapshotEndpointUrlCb: Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(options.snapshotEndpointUrl), - cacheMode, + snapshotCache, }); this._loadDistMetadata = null; diff --git a/packages/project/lib/ui5Framework/maven/Installer.js b/packages/project/lib/ui5Framework/maven/Installer.js index 4dd6d1bc8cd..2c8e45fb7f6 100644 --- a/packages/project/lib/ui5Framework/maven/Installer.js +++ b/packages/project/lib/ui5Framework/maven/Installer.js @@ -6,7 +6,7 @@ const StreamZip = _StreamZip.async; import {promisify} from "node:util"; import Registry from "./Registry.js"; import AbstractInstaller from "../AbstractInstaller.js"; -import CacheMode from "./CacheMode.js"; +import SnapshotCache from "./SnapshotCache.js"; import {rmrf} from "../../utils/fs.js"; const stat = promisify(fs.stat); const readFile = promisify(fs.readFile); @@ -27,9 +27,10 @@ class Installer extends AbstractInstaller { * @param {Function} parameters.snapshotEndpointUrlCb Callback that returns a Promise , * resolving to the Maven repository URL. * Example: https://registry.corp/vendor/build-snapshots/ - * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [parameters.cacheMode=Default] Cache mode to use + * @param {module:@ui5/project/ui5Framework/maven/SnapshotCache} [parameters.snapshotCache=Default] + * Snapshot cache mode to use */ - constructor({ui5DataDir, snapshotEndpointUrlCb, cacheMode = CacheMode.Default}) { + constructor({ui5DataDir, snapshotEndpointUrlCb, snapshotCache = SnapshotCache.Default}) { super(ui5DataDir); this._artifactsDir = path.join(ui5DataDir, "framework", "artifacts"); @@ -37,20 +38,20 @@ class Installer extends AbstractInstaller { this._metadataDir = path.join(ui5DataDir, "framework", "metadata"); this._stagingDir = path.join(ui5DataDir, "framework", "staging"); - this._cacheMode = cacheMode; + this._snapshotCache = snapshotCache; this._snapshotEndpointUrlCb = snapshotEndpointUrlCb; if (!this._snapshotEndpointUrlCb) { throw new Error(`Installer: Missing Snapshot-Endpoint URL callback parameter`); } - if (!Object.values(CacheMode).includes(cacheMode)) { - throw new Error(`Installer: Invalid value '${cacheMode}' for cacheMode parameter. ` + - `Must be one of ${Object.values(CacheMode).join(", ")}`); + if (!Object.values(SnapshotCache).includes(snapshotCache)) { + throw new Error(`Installer: Invalid value '${snapshotCache}' for snapshotCache parameter. ` + + `Must be one of ${Object.values(SnapshotCache).join(", ")}`); } log.verbose(`Installing Maven artifacts to: ${this._artifactsDir}`); log.verbose(`Installing Packages to: ${this._packagesDir}`); - log.verbose(`Caching mode: ${this._cacheMode}`); + log.verbose(`Snapshot cache mode: ${this._snapshotCache}`); } async getRegistry() { @@ -122,7 +123,7 @@ class Installer extends AbstractInstaller { return this._synchronize("metadata-" + fsId, async () => { const localMetadata = await this._getLocalArtifactMetadata(fsId); - if (this._cacheMode === CacheMode.Force && !localMetadata.revision) { + if (this._snapshotCache === SnapshotCache.Force && !localMetadata.revision) { throw new Error(`Could not find artifact ` + `${logId} in local cache`); } @@ -130,8 +131,8 @@ class Installer extends AbstractInstaller { const now = new Date().getTime(); const timeSinceLastCheck = now - localMetadata.lastCheck; - if (this._cacheMode !== CacheMode.Force && - (timeSinceLastCheck > CACHE_TIME || this._cacheMode === CacheMode.Off)) { + if (this._snapshotCache !== SnapshotCache.Force && + (timeSinceLastCheck > CACHE_TIME || this._snapshotCache === SnapshotCache.Off)) { // No cached metadata (-> timeSinceLastCheck equals time since 1970) or // too old metadata or disabled cache // => Retrieve metadata from repository diff --git a/packages/project/lib/ui5Framework/maven/Registry.js b/packages/project/lib/ui5Framework/maven/Registry.js index ec5b2293e75..7003c49b230 100644 --- a/packages/project/lib/ui5Framework/maven/Registry.js +++ b/packages/project/lib/ui5Framework/maven/Registry.js @@ -65,8 +65,8 @@ class Registry { `You can change the configured URL using the following command: ` + `'ui5 config set mavenSnapshotEndpointUrl '`); - // TODO: Allow cacheMode to be set from outside - // `You may be able to continue working offline. For this, set --cache-mode to "force"`); + // TODO: Allow snapshotCache to be set from outside + // `You may be able to continue working offline. For this, set --snapshot-cache to "force"`); // ` or use the --offline flag`); // TODO: Implement --offline flag } throw new Error( @@ -108,8 +108,8 @@ class Registry { `You can change the configured URL using the following command: ` + `'ui5 config set mavenSnapshotEndpointUrl '`); - // TODO: Allow cacheMode to be set from outside - // `You may be able to continue working offline. For this, set --cache-mode to "force"`); + // TODO: Allow snapshotCache to be set from outside + // `You may be able to continue working offline. For this, set --snapshot-cache to "force"`); // ` or use the --offline flag`); // TODO: Implement --offline flag } throw new Error(`Failed to retrieve artifact ` + diff --git a/packages/project/lib/ui5Framework/maven/CacheMode.js b/packages/project/lib/ui5Framework/maven/SnapshotCache.js similarity index 77% rename from packages/project/lib/ui5Framework/maven/CacheMode.js rename to packages/project/lib/ui5Framework/maven/SnapshotCache.js index d1b5af0d422..5e7927090f6 100644 --- a/packages/project/lib/ui5Framework/maven/CacheMode.js +++ b/packages/project/lib/ui5Framework/maven/SnapshotCache.js @@ -1,7 +1,7 @@ /** - * Cache modes for maven consumption + * Snapshot cache modes for Maven consumption * * @public * @readonly @@ -9,7 +9,7 @@ * @property {string} Default Cache everything, invalidate after 9 hours * @property {string} Force Use cache only. Do not send any requests to the repository * @property {string} Off Invalidate the cache and update from the repository - * @module @ui5/project/ui5Framework/maven/CacheMode + * @module @ui5/project/ui5Framework/maven/SnapshotCache */ export default { Default: "Default", diff --git a/packages/project/package.json b/packages/project/package.json index b571af7fc37..22f9a8ce37f 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -19,12 +19,13 @@ "type": "module", "exports": { "./config/Configuration": "./lib/config/Configuration.js", + "./build/cache/Cache": "./lib/build/cache/Cache.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/CacheMode": "./lib/ui5Framework/maven/CacheMode.js", + "./ui5Framework/maven/SnapshotCache": "./lib/ui5Framework/maven/SnapshotCache.js", "./validation/validator": "./lib/validation/validator.js", "./validation/ValidationError": "./lib/validation/ValidationError.js", "./graph/ProjectGraph": "./lib/graph/ProjectGraph.js", diff --git a/packages/project/test/lib/build/BuildServer.integration.js b/packages/project/test/lib/build/BuildServer.integration.js index 6039881410b..4e999796b1f 100644 --- a/packages/project/test/lib/build/BuildServer.integration.js +++ b/packages/project/test/lib/build/BuildServer.integration.js @@ -5,6 +5,7 @@ import {setTimeout} from "node:timers/promises"; import fs from "node:fs/promises"; import {graphFromPackageDependencies} from "../../../lib/graph/graph.js"; import {setLogLevel} from "@ui5/logger"; +import Cache from "../../../lib/build/cache/Cache.js"; // Ensures that all logging code paths are tested setLogLevel("silly"); @@ -355,6 +356,262 @@ test.serial("Serve application.a, request application resource AND library resou ); }); +test.serial("Serve application.a with --cache=Default", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve and request with empty cache --> all tasks execute + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2: Request with valid cache, no changes --> nothing rebuilds (all cached) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for cache test");\n`); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the changes + + // #3: Request with valid cache, source changes --> only affected tasks rebuild + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Verify the changed file is served + const resource = await fixtureTester.requestResource({resource: "/test.js"}); + const servedFileContent = await resource.getString(); + t.true(servedFileContent.includes(`test("line added for cache test");`), + "Served resource contains changed file content"); +}); + +test.serial("Serve application.a with --cache=Off", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve and request with cache=Off --> all tasks execute, cache not written + await fixtureTester.serveProject({config: {cache: Cache.Off}}); + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2: Request with cache=Off (again) --> all tasks execute again (no cache reuse) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Restart server with cache=Default + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + + // #3: Request with cache=Default --> all tasks execute (no cache from previous Off mode) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #4: Request with cache=Default (again) --> nothing rebuilds (cache now exists) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: {} + } + }); + + // Restart server with cache=Off + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.Off}}); + + // #5: Request with cache=Off --> all tasks execute (ignores existing cache) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); +}); + +test.serial("Serve application.a with --cache=ReadOnly", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve and request with cache=Default --> all tasks execute, cache written + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Restart server with ReadOnly mode + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.ReadOnly}}); + + // #2: Request with cache=ReadOnly, no changes --> nothing rebuilds (cache used) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for ReadOnly test");\n`); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the changes + + // #3: Request with cache=ReadOnly --> affected tasks rebuild, BUT cache not updated + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Verify the changed file is served + const resource = await fixtureTester.requestResource({resource: "/test.js"}); + const servedFileContent = await resource.getString(); + t.true(servedFileContent.includes(`test("line added for ReadOnly test");`), + "Served resource contains changed file content"); + + // Restart server with Default mode + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + + // #4: Request with cache=Default, no new changes --> rebuilds again (cache from #3 missing) + // This validates that ReadOnly didn't write the cache in step #3 + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); +}); + +test.serial("Serve application.a with --cache=Force (1)", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve and request with cache=Default --> all tasks execute, cache written + await fixtureTester.serveProject({config: {cache: Cache.Default}}); + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: { + "application.a": {} + } + } + }); + + // Restart server with Force mode + await fixtureTester.teardown(); + await fixtureTester.serveProject({config: {cache: Cache.Force}, expectBuildErrors: true}); + + // #2: Request with cache=Force, no changes --> nothing rebuilds (cache used) + await fixtureTester.requestResource({ + resource: "/test.js", + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for Force test");\n`); + + await setTimeout(500); // Wait for the file watcher to detect and propagate the changes + + // #3: Request with cache=Force --> ERROR (cache invalid due to source changes) + const error = await t.throwsAsync(async () => { + await fixtureTester.requestResource({ + resource: "/test.js", + }); + }); + + t.truthy(error, "Request with Force mode should throw error when cache is stale"); + t.true(error.message.includes(`Cache is in "Force" mode but cache is stale for project application.a`)); + + // Wait for async error handling to complete + await setTimeout(50); +}); + +test.serial("Serve application.a with --cache=Force (2)", async (t) => { + const fixtureTester = t.context.fixtureTester = new FixtureTester(t, "application.a"); + + // #1: Serve with cache=Force on empty cache --> ERROR when requesting resource + await fixtureTester.serveProject({config: {cache: Cache.Force}, expectBuildErrors: true}); + + const error = await t.throwsAsync(async () => { + await fixtureTester.requestResource({ + resource: "/test.js", + }); + }); + + t.truthy(error, "Request with Force mode should throw error when cache is empty"); + t.true(error.message.includes(`Cache is in "Force" mode but no cache found for project application.a`)); + + // Wait for async error handling to complete + await setTimeout(50); +}); + function getFixturePath(fixtureName) { return fileURLToPath(new URL(`../../fixtures/${fixtureName}`, import.meta.url)); } @@ -392,11 +649,15 @@ class FixtureTester { async teardown() { if (this.buildServer) { - await this.buildServer.destroy(); + try { + await this.buildServer.destroy(); + } catch { + // Ignore errors during teardown (e.g., failed Force mode builds) + } } } - async serveProject({graphConfig = {}, config = {}} = {}) { + async serveProject({graphConfig = {}, config = {}, expectBuildErrors = false} = {}) { await this._initialize(); const graph = this.graph = await graphFromPackageDependencies({ @@ -407,7 +668,9 @@ class FixtureTester { // Execute the build this.buildServer = await graph.serve(config); this.buildServer.on("error", (err) => { - this._t.fail(`Build server error: ${err.message}`); + if (!expectBuildErrors) { + this._t.fail(`Build server error: ${err.message}`); + } }); this._reader = this.buildServer.getReader(); } diff --git a/packages/project/test/lib/build/ProjectBuilder.integration.js b/packages/project/test/lib/build/ProjectBuilder.integration.js index 8662471cefc..a38abc34193 100644 --- a/packages/project/test/lib/build/ProjectBuilder.integration.js +++ b/packages/project/test/lib/build/ProjectBuilder.integration.js @@ -4,6 +4,7 @@ import {fileURLToPath} from "node:url"; import fs from "node:fs/promises"; import {graphFromPackageDependencies} from "../../../lib/graph/graph.js"; import {setLogLevel} from "@ui5/logger"; +import Cache from "../../../lib/build/cache/Cache.js"; // Ensures that all logging code paths are tested setLogLevel("silly"); @@ -2451,6 +2452,230 @@ test.serial("Build with dependencies: Verify sap-ui-version.json generation and "buildTimestamp unchanged when cached (no source changes)"); }); +test.serial("Build application.a with --cache=Default", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1 Build with empty cache --> all tasks execute + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Default}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2 Build with valid cache, no changes --> nothing rebuilds (all cached) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for cache test");\n`); + + // #3 Build with valid cache, source changes --> only affected tasks rebuild + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Verify the changed file is in the destPath + const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}); + t.true(builtFileContent.includes(`test("line added for cache test");`), + "Build dest contains changed file content"); +}); + +test.serial("Build application.a with --cache=Off", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1 Build with cache=Off --> all tasks execute, cache not written + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Off}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2 Build with cache=Off (again) --> all tasks execute again (no cache reuse) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Off}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #3 Build with cache=Default --> all tasks execute (no cache from previous builds) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #4 Build with cache=Default (again) --> nothing rebuilds (cache now exists) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: {} + } + }); + + // #5 Build with cache=Off --> all tasks execute (ignores existing cache) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Off}, + assertions: { + projects: { + "application.a": {} + } + } + }); +}); + +test.serial("Build application.a with --cache=ReadOnly", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1 Build with cache=Default --> all tasks execute, cache written + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Default}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2 Build with cache=ReadOnly, no changes --> nothing rebuilds (cache used) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.ReadOnly}, + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for ReadOnly test");\n`); + + // #3 Build with cache=ReadOnly --> affected tasks rebuild, BUT cache not updated + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.ReadOnly}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); + + // Verify the changed file is in the destPath + const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}); + t.true(builtFileContent.includes(`test("line added for ReadOnly test");`), + "Build dest contains changed file content"); + + // #4 Build with cache=Default, no new changes --> rebuilds again (cache from #3 missing) + // This validates that ReadOnly didn't write the cache in step #3 + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Default}, + assertions: { + projects: { + "application.a": { + skippedTasks: [ + "escapeNonAsciiCharacters", + "replaceCopyright", + "enhanceManifest", + "generateFlexChangesBundle", + ] + } + } + } + }); +}); + +test.serial("Build application.a with --cache=Force (1)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1: Build with cache=Default --> all tasks execute, cache written + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Default}, + assertions: { + projects: { + "application.a": {} + } + } + }); + + // #2: Build with cache=Force, no changes --> nothing rebuilds (cache used) + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Force}, + assertions: { + projects: {} + } + }); + + // Change a source file in application.a + const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`; + await fs.appendFile(changedFilePath, `\ntest("line added for Force test");\n`); + + // #3: Build with cache=Force --> ERROR (cache invalid due to source changes) + const error = await t.throwsAsync(async () => { + await fixtureTester.buildProject({ + config: {destPath, cleanDest: true, cache: Cache.Force}, + }); + }); + + t.truthy(error, "Build with Force mode should throw error when cache is stale"); + t.true(error.message.includes(`Cache is in "Force" mode but cache is stale for project application.a ` + + `due to 1 changed source file(s). ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.`)); +}); + +test.serial("Build application.a with --cache=Force (2)", async (t) => { + const fixtureTester = new FixtureTester(t, "application.a"); + const destPath = fixtureTester.destPath; + + // #1: Build with cache=Force on empty cache --> ERROR with clear message + const error = await t.throwsAsync(async () => { + await fixtureTester.buildProject({ + config: {destPath, cleanDest: false, cache: Cache.Force}, + }); + }); + + t.truthy(error, "Build with Force mode should throw error when cache is empty"); + t.true(error.message.includes(`Cache is in "Force" mode but no cache found for project application.a. ` + + `Use "Default", "ReadOnly" or "Off" to rebuild.`)); +}); + function getFixturePath(fixtureName) { return fileURLToPath(new URL(`../../fixtures/${fixtureName}`, import.meta.url)); } @@ -2485,7 +2710,7 @@ class FixtureTester { this._initialized = true; } - async buildProject({graphConfig = {}, config = {}, assertions = {}} = {}) { + async buildProject({graphConfig = {}, config = {}, assertions} = {}) { await this._initialize(); this._sinon.resetHistory(); diff --git a/packages/project/test/lib/graph/graph.integration.js b/packages/project/test/lib/graph/graph.integration.js index 9b459a0f823..fcff4e57957 100644 --- a/packages/project/test/lib/graph/graph.integration.js +++ b/packages/project/test/lib/graph/graph.integration.js @@ -3,7 +3,7 @@ import path from "node:path"; import sinonGlobal from "sinon"; import esmock from "esmock"; import Workspace from "../../../lib/graph/Workspace.js"; -import CacheMode from "../../../lib/ui5Framework/maven/CacheMode.js"; +import SnapshotCache from "../../../lib/ui5Framework/maven/SnapshotCache.js"; const __dirname = import.meta.dirname; const fixturesPath = path.join(__dirname, "..", "..", "fixtures"); @@ -254,7 +254,7 @@ test.serial("graphFromPackageDependencies with inactive workspace file at custom versionOverride: "versionOverride", workspaceName: "default", workspaceConfigPath: path.join(libraryHPath, "custom-ui5-workspace.yaml"), - cacheMode: CacheMode.Force + snapshotCache: SnapshotCache.Force }); t.is(res, "graph"); @@ -278,6 +278,6 @@ test.serial("graphFromPackageDependencies with inactive workspace file at custom t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", workspace: null, - cacheMode: "Force" + snapshotCache: "Force" }, "enrichProjectGraph got called with correct options"); }); diff --git a/packages/project/test/lib/graph/graph.js b/packages/project/test/lib/graph/graph.js index 4cc4c1386c0..799033a59db 100644 --- a/packages/project/test/lib/graph/graph.js +++ b/packages/project/test/lib/graph/graph.js @@ -2,7 +2,7 @@ import test from "ava"; import path from "node:path"; import sinonGlobal from "sinon"; import esmock from "esmock"; -import CacheMode from "../../../lib/ui5Framework/maven/CacheMode.js"; +import SnapshotCache from "../../../lib/ui5Framework/maven/SnapshotCache.js"; const __dirname = import.meta.dirname; const fixturesPath = path.join(__dirname, "..", "..", "fixtures"); @@ -58,7 +58,7 @@ test.serial("graphFromPackageDependencies", async (t) => { rootConfiguration: "rootConfiguration", rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", - cacheMode: CacheMode.Off, + snapshotCache: SnapshotCache.Off, workspaceName: null }); @@ -84,7 +84,7 @@ test.serial("graphFromPackageDependencies", async (t) => { t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", workspace: undefined, - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); @@ -101,7 +101,7 @@ test.serial("graphFromPackageDependencies with workspace name", async (t) => { rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", workspaceName: "dolphin", - cacheMode: CacheMode.Off + snapshotCache: SnapshotCache.Off }); t.is(res, "graph"); @@ -133,7 +133,7 @@ test.serial("graphFromPackageDependencies with workspace name", async (t) => { t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", workspace: "workspace", - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); @@ -231,7 +231,7 @@ test.serial("graphFromPackageDependencies with empty workspace", async (t) => { rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", workspaceName: "dolphin", - cacheMode: CacheMode.Off + snapshotCache: SnapshotCache.Off }); t.is(res, "graph"); @@ -263,7 +263,7 @@ test.serial("graphFromPackageDependencies with empty workspace", async (t) => { t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", workspace: null, - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); @@ -317,7 +317,7 @@ test.serial("graphFromStaticFile", async (t) => { rootConfiguration: "rootConfiguration", rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", - cacheMode: CacheMode.Off + snapshotCache: SnapshotCache.Off }); t.is(res, "graph"); @@ -344,7 +344,7 @@ test.serial("graphFromStaticFile", async (t) => { "enrichProjectGraph got called with graph"); t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); @@ -380,7 +380,7 @@ test.serial("usingObject", async (t) => { rootConfiguration: "rootConfiguration", rootConfigPath: "/rootConfigPath", versionOverride: "versionOverride", - cacheMode: "Off" + snapshotCache: "Off" }); t.is(res, "graph"); @@ -401,7 +401,7 @@ test.serial("usingObject", async (t) => { "enrichProjectGraph got called with graph"); t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], { versionOverride: "versionOverride", - cacheMode: "Off" + snapshotCache: "Off" }, "enrichProjectGraph got called with correct options"); }); diff --git a/packages/project/test/lib/graph/helpers/ui5Framework.js b/packages/project/test/lib/graph/helpers/ui5Framework.js index ceae8c52e54..b134ac187ac 100644 --- a/packages/project/test/lib/graph/helpers/ui5Framework.js +++ b/packages/project/test/lib/graph/helpers/ui5Framework.js @@ -5,7 +5,7 @@ import esmock from "esmock"; import DependencyTreeProvider from "../../../../lib/graph/providers/DependencyTree.js"; import projectGraphBuilder from "../../../../lib/graph/projectGraphBuilder.js"; import Specification from "../../../../lib/specifications/Specification.js"; -import CacheMode from "../../../../lib/ui5Framework/maven/CacheMode.js"; +import SnapshotCache from "../../../../lib/ui5Framework/maven/SnapshotCache.js"; const __dirname = import.meta.dirname; @@ -128,7 +128,7 @@ test.serial("enrichProjectGraph", async (t) => { t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: dependencyTree.configuration.framework.version, ui5DataDir: undefined, @@ -239,7 +239,7 @@ test.serial("enrichProjectGraph SNAPSHOT", async (t) => { const projectGraph = await projectGraphBuilder(provider); await ui5Framework.enrichProjectGraph(projectGraph, { - cacheMode: CacheMode.Force + snapshotCache: SnapshotCache.Force }); t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once"); @@ -341,7 +341,7 @@ test.serial("enrichProjectGraph: With versionOverride", async (t) => { t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9", ui5DataDir: undefined, @@ -404,7 +404,7 @@ test.serial("enrichProjectGraph: With versionOverride containing snapshot versio t.is(Sapui5MavenSnapshotResolverStub.callCount, 1, "Sapui5MavenSnapshotResolverStub#constructor should be called once"); t.deepEqual(Sapui5MavenSnapshotResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9-SNAPSHOT", ui5DataDir: undefined, @@ -467,7 +467,7 @@ test.serial("enrichProjectGraph: With versionOverride containing latest-snapshot t.is(Sapui5MavenSnapshotResolverStub.callCount, 1, "Sapui5MavenSnapshotResolverStub#constructor should be called once"); t.deepEqual(Sapui5MavenSnapshotResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9-SNAPSHOT", ui5DataDir: undefined, @@ -627,7 +627,7 @@ test.serial("enrichProjectGraph should resolve framework project with version an t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGrap should be called once"); t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.2.3", ui5DataDir: undefined, @@ -732,7 +732,7 @@ test.serial("enrichProjectGraph should resolve framework project " + t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.99.9", ui5DataDir: undefined, @@ -997,7 +997,7 @@ test.serial("enrichProjectGraph should use framework library metadata from works t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: "1.111.1", ui5DataDir: undefined, @@ -1056,7 +1056,7 @@ test.serial("enrichProjectGraph should allow omitting framework version in case t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, ui5DataDir: undefined, version: undefined, @@ -1113,7 +1113,7 @@ test.serial("enrichProjectGraph should use UI5 data dir from env var", async (t) t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: dependencyTree.configuration.framework.version, ui5DataDir: expectedUi5DataDir, @@ -1169,7 +1169,7 @@ test.serial("enrichProjectGraph should use UI5 data dir from configuration", asy t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: dependencyTree.configuration.framework.version, ui5DataDir: expectedUi5DataDir, @@ -1225,7 +1225,7 @@ test.serial("enrichProjectGraph should use absolute UI5 data dir from configurat t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{ - cacheMode: undefined, + snapshotCache: undefined, cwd: dependencyTree.path, version: dependencyTree.configuration.framework.version, ui5DataDir: expectedUi5DataDir, diff --git a/packages/project/test/lib/package-exports.js b/packages/project/test/lib/package-exports.js index 305cdbb04b1..437d29d3287 100644 --- a/packages/project/test/lib/package-exports.js +++ b/packages/project/test/lib/package-exports.js @@ -24,7 +24,7 @@ test("check number of exports", (t) => { "ui5Framework/Openui5Resolver", "ui5Framework/Sapui5Resolver", "ui5Framework/Sapui5MavenSnapshotResolver", - "ui5Framework/maven/CacheMode", + "ui5Framework/maven/SnapshotCache", "validation/validator", "validation/ValidationError", "graph/ProjectGraph", diff --git a/packages/project/test/lib/ui5framework/maven/Installer.js b/packages/project/test/lib/ui5framework/maven/Installer.js index 86b00754cdb..c07e9e204bc 100644 --- a/packages/project/test/lib/ui5framework/maven/Installer.js +++ b/packages/project/test/lib/ui5framework/maven/Installer.js @@ -683,7 +683,7 @@ test.serial("_fetchArtifactMetadata: Cache available but disabled", async (t) => cwd: "/cwd/", ui5DataDir: "/ui5Data/", snapshotEndpointUrlCb: () => {}, - cacheMode: "Off" + snapshotCache: "Off" }); sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); @@ -719,7 +719,7 @@ test.serial("_fetchArtifactMetadata: Cache outdated but enforced", async (t) => cwd: "/cwd/", ui5DataDir: "/ui5Data/", snapshotEndpointUrlCb: () => {}, - cacheMode: "Force" + snapshotCache: "Force" }); sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); @@ -756,7 +756,7 @@ test.serial("_fetchArtifactMetadata throws", async (t) => { cwd: "/cwd/", ui5DataDir: "/ui5Data/", snapshotEndpointUrlCb: () => {}, - cacheMode: "Force" + snapshotCache: "Force" }); sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback()); diff --git a/packages/server/lib/server.js b/packages/server/lib/server.js index 668318f41c8..c27de5b2f73 100644 --- a/packages/server/lib/server.js +++ b/packages/server/lib/server.js @@ -4,6 +4,7 @@ import MiddlewareManager from "./middleware/MiddlewareManager.js"; import {createReaderCollection} from "@ui5/fs/resourceFactory"; import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized"; import {getLogger} from "@ui5/logger"; +import Cache from "@ui5/project/build/cache/Cache"; const log = getLogger("server"); /** @@ -128,6 +129,7 @@ async function _addSsl({app, key, cert}) { * are send for any requested *.html file * @param {boolean} [options.serveCSPReports=false] Enable CSP reports serving for request url * '/.ui5/csp/csp-reports.json' + * @param {string} [options.cache="Default"] Cache mode to use for building UI5 projects. * @param {Function} error Error callback. Will be called when an error occurs outside of request handling. * @returns {Promise} Promise resolving once the server is listening. * It resolves with an object containing the port, @@ -136,7 +138,8 @@ async function _addSsl({app, key, cert}) { */ export async function serve(graph, { port: requestedPort, changePortIfInUse = false, h2 = false, key, cert, - acceptRemoteConnections = false, sendSAPTargetCSP = false, simpleIndex = false, serveCSPReports = false + acceptRemoteConnections = false, sendSAPTargetCSP = false, + simpleIndex = false, serveCSPReports = false, cache = Cache.Default }, error) { const rootProject = graph.getRoot(); @@ -175,6 +178,7 @@ export async function serve(graph, { const buildServer = await graph.serve({ initialBuildIncludedDependencies, excludedTasks: ["minify", "generateLibraryPreload", "generateComponentPreload", "generateBundle"], + cache, }); const resources = {