Skip to content

Commit aec4b73

Browse files
feat(cli): Add --cache option for build and serve commands (#1368)
Surface the incremental build cache through the CLI. - build / serve: new --cache flag with Default/Force/Off modes. Serve defaults to skipping bundling for fast iteration; build keeps prior behaviour by default. CLI accepts lower-case values and validates via yargs coerce - tree: deprecate the old --cache-mode flag in favour of --cache - Maven snapshot resolver: rename CacheMode -> SnapshotCache so the --cache option (which controls the build cache) does not collide with the existing maven snapshot cache in name or docs Closes #1368. Co-authored-by: Merlin Beutlberger <m.beutlberger@sap.com>
1 parent 2c89a9c commit aec4b73

14 files changed

Lines changed: 447 additions & 174 deletions

File tree

packages/cli/lib/cli/commands/build.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import baseMiddleware from "../middlewares/base.js";
2+
import {getLogger} from "@ui5/logger";
3+
const log = getLogger("cli:commands:build");
24

35
const build = {
46
command: "build",
@@ -83,6 +85,24 @@ build.builder = function(cli) {
8385
default: false,
8486
type: "boolean"
8587
})
88+
.option("cache", {
89+
describe:
90+
"Cache mode to use for building UI5 projects. " +
91+
"The 'Default' behavior is to always use the cache if available. 'Force' uses the cache only. " +
92+
"If the cache is unavailable or invalid, the build fails. 'ReadOnly' does not create or update any " +
93+
"cache but makes use of a cache if available. 'Off' does not use any cache and always triggers " +
94+
"a rebuild of the project",
95+
type: "string",
96+
default: "Default",
97+
choices: ["Default", "Force", "ReadOnly", "Off"],
98+
})
99+
.coerce("cache", (opt) => {
100+
const lower = opt.toLowerCase();
101+
if (lower === "readonly" || lower === "read-only") {
102+
return "ReadOnly";
103+
}
104+
return lower.charAt(0).toUpperCase() + lower.slice(1);
105+
})
86106
.option("create-build-manifest", {
87107
describe: "Store build metadata in a '.ui5' directory in the build destination, " +
88108
"allowing reuse of the build result in other builds",
@@ -106,13 +126,30 @@ build.builder = function(cli) {
106126
type: "string"
107127
})
108128
.option("cache-mode", {
129+
// Deprecated
130+
hidden: true,
131+
describe:
132+
"As of UI5 CLI version 5, renamed to '--snapshot-cache'. " +
133+
"Use '--snapshot-cache' to control this behavior.",
134+
type: "string",
135+
choices: ["Default", "Force", "Off"],
136+
})
137+
.coerce("cache-mode", (opt) => {
138+
// Log a warning if this option is used
139+
if (opt !== undefined) {
140+
log.warn("As of UI5 CLI version 5, '--cache-mode' is renamed to '--snapshot-cache'. " +
141+
"Use '--snapshot-cache' to control this behavior.");
142+
}
143+
return opt;
144+
})
145+
.option("snapshot-cache", {
109146
describe:
110147
"Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " +
111148
"The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " +
112149
"does not create any requests. 'Off' invalidates any existing cache and updates from the repository",
113150
type: "string",
114-
default: "Default",
115-
choices: ["Default", "Force", "Off"]
151+
defaultDescription: "Default", // Use "defaultDescription" to allow undefined (needed for evaluation)
152+
choices: ["Default", "Force", "Off"],
116153
})
117154
.option("experimental-css-variables", {
118155
describe:
@@ -159,13 +196,13 @@ async function handleBuild(argv) {
159196
filePath: argv.dependencyDefinition,
160197
rootConfigPath: argv.config,
161198
versionOverride: argv.frameworkVersion,
162-
cacheMode: argv.cacheMode,
199+
snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback
163200
});
164201
} else {
165202
graph = await graphFromPackageDependencies({
166203
rootConfigPath: argv.config,
167204
versionOverride: argv.frameworkVersion,
168-
cacheMode: argv.cacheMode,
205+
snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback
169206
workspaceConfigPath: argv.workspaceConfig,
170207
workspaceName: argv.workspace === false ? null : argv.workspace,
171208
});
@@ -194,6 +231,7 @@ async function handleBuild(argv) {
194231
excludedTasks: argv["exclude-task"],
195232
cssVariables: argv["experimental-css-variables"],
196233
outputStyle: argv["output-style"],
234+
cache: argv["cache"],
197235
});
198236
}
199237

packages/cli/lib/cli/commands/serve.js

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import path from "node:path";
22
import os from "node:os";
33
import chalk from "chalk";
44
import baseMiddleware from "../middlewares/base.js";
5+
import {getLogger} from "@ui5/logger";
6+
const log = getLogger("cli:commands:serve");
57

68
// Serve
79
const serve = {
@@ -61,19 +63,54 @@ serve.builder = function(cli) {
6163
default: false,
6264
type: "boolean"
6365
})
66+
.option("cache", {
67+
describe:
68+
"Cache mode to use for building UI5 projects. " +
69+
"The 'Default' behavior is to always use the build-cache if available. 'Force' uses the cache only. " +
70+
"If the build-cache is unavailable or invalid, the server will fail to build the project. " +
71+
"'ReadOnly' does not create or update any cache but makes use of a cache if available. " +
72+
"'Off' does not use any build-cache and always triggers a rebuild of the project",
73+
type: "string",
74+
default: "Default",
75+
choices: ["Default", "Force", "ReadOnly", "Off"],
76+
})
77+
.coerce("cache", (opt) => {
78+
const lower = opt.toLowerCase();
79+
if (lower === "readonly" || lower === "read-only") {
80+
return "ReadOnly";
81+
}
82+
return lower.charAt(0).toUpperCase() + lower.slice(1);
83+
})
6484
.option("framework-version", {
6585
describe: "Overrides the framework version defined by the project. " +
6686
"Takes the same value as the version part of \"ui5 use\"",
6787
type: "string"
6888
})
6989
.option("cache-mode", {
90+
// Deprecated
91+
hidden: true,
92+
describe:
93+
"As of UI5 CLI version 5, renamed to '--snapshot-cache'. " +
94+
"Use '--snapshot-cache' to control this behavior.",
95+
type: "string",
96+
choices: ["Default", "Force", "Off"],
97+
})
98+
.coerce("cache-mode", (opt) => {
99+
// Log a warning if this option is used
100+
if (opt !== undefined) {
101+
log.warn("As of UI5 CLI version 5, '--cache-mode' is renamed to '--snapshot-cache'. " +
102+
"Use '--snapshot-cache' to control this behavior.");
103+
}
104+
return opt;
105+
})
106+
.option("snapshot-cache", {
70107
describe:
71108
"Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " +
72109
"The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " +
73110
"does not create any requests. 'Off' invalidates any existing cache and updates from the repository",
74111
type: "string",
75-
default: "Default",
76-
choices: ["Default", "Force", "Off"]
112+
defaultDescription: "Default", // Use "defaultdescription" to allow undefined (needed for evaluation)
113+
choices: ["Default", "Force", "Off"],
77114
})
78115
.example("ui5 serve", "Start a web server for the current project")
79116
.example("ui5 serve --h2", "Enable the HTTP/2 protocol for the web server (requires SSL certificate)")
@@ -95,13 +132,13 @@ serve.handler = async function(argv) {
95132
filePath: argv.dependencyDefinition,
96133
rootConfigPath: argv.config,
97134
versionOverride: argv.frameworkVersion,
98-
cacheMode: argv.cacheMode,
135+
snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback
99136
});
100137
} else {
101138
graph = await graphFromPackageDependencies({
102139
rootConfigPath: argv.config,
103140
versionOverride: argv.frameworkVersion,
104-
cacheMode: argv.cacheMode,
141+
snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback
105142
workspaceConfigPath: argv.workspaceConfig,
106143
workspaceName: argv.workspace === false ? null : argv.workspace,
107144
});
@@ -137,7 +174,8 @@ serve.handler = async function(argv) {
137174
cert: argv.h2 ? argv.cert : undefined,
138175
key: argv.h2 ? argv.key : undefined,
139176
sendSAPTargetCSP: !!argv.sapCspPolicies,
140-
serveCSPReports: !!argv.serveCspReports
177+
serveCSPReports: !!argv.serveCspReports,
178+
cache: argv.cache,
141179
};
142180

143181
if (serverConfig.h2) {
@@ -146,7 +184,10 @@ serve.handler = async function(argv) {
146184
serverConfig.cert = cert;
147185
}
148186

149-
const {h2, port: actualPort} = await serverServe(graph, serverConfig);
187+
const {promise: pOnError, reject} = Promise.withResolvers();
188+
const {h2, port: actualPort} = await serverServe(graph, serverConfig, function(err) {
189+
reject(err);
190+
});
150191

151192
const protocol = h2 ? "https" : "http";
152193
let browserUrl = protocol + "://localhost:" + actualPort;
@@ -183,6 +224,7 @@ serve.handler = async function(argv) {
183224
const {default: open} = await import("open");
184225
open(browserUrl);
185226
}
227+
await pOnError; // Await errors that should bubble into the yargs handler
186228
};
187229

188230
export default serve;

packages/cli/lib/cli/commands/tree.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Tree
22
import baseMiddleware from "../middlewares/base.js";
33
import chalk from "chalk";
4+
import {getLogger} from "@ui5/logger";
5+
const log = getLogger("cli:commands:tree");
46

57
const tree = {
68
command: "tree",
@@ -29,13 +31,30 @@ tree.builder = function(cli) {
2931
type: "string"
3032
})
3133
.option("cache-mode", {
34+
// Deprecated
35+
hidden: true,
36+
describe:
37+
"As of UI5 CLI version 5, renamed to '--snapshot-cache'. " +
38+
"Use '--snapshot-cache' to control this behavior.",
39+
type: "string",
40+
choices: ["Default", "Force", "Off"],
41+
})
42+
.coerce("cache-mode", (opt) => {
43+
// Log a warning if this option is used
44+
if (opt !== undefined) {
45+
log.warn("As of UI5 CLI version 5, '--cache-mode' is renamed to '--snapshot-cache'. " +
46+
"Use '--snapshot-cache' to control this behavior.");
47+
}
48+
return opt;
49+
})
50+
.option("snapshot-cache", {
3251
describe:
3352
"Cache mode to use when consuming SNAPSHOT versions of framework dependencies. " +
3453
"The 'Default' behavior is to invalidate the cache after 9 hours. 'Force' uses the cache only and " +
3554
"does not create any requests. 'Off' invalidates any existing cache and updates from the repository",
3655
type: "string",
37-
default: "Default",
38-
choices: ["Default", "Force", "Off"]
56+
defaultDescription: "Default", // Use "defaultDescription" to allow undefined (needed for evaluation)
57+
choices: ["Default", "Force", "Off"],
3958
});
4059
};
4160

@@ -51,13 +70,13 @@ tree.handler = async function(argv) {
5170
graph = await graphFromStaticFile({
5271
filePath: argv.dependencyDefinition,
5372
versionOverride: argv.frameworkVersion,
54-
cacheMode: argv.cacheMode,
73+
snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback
5574
});
5675
} else {
5776
graph = await graphFromPackageDependencies({
5877
rootConfigPath: argv.config,
5978
versionOverride: argv.frameworkVersion,
60-
cacheMode: argv.cacheMode,
79+
snapshotCache: argv.snapshotCache ?? argv.cacheMode ?? "Default", // Use cacheMode as fallback
6180
workspaceConfigPath: argv.workspaceConfig,
6281
workspaceName: argv.workspace === false ? null : argv.workspace,
6382
});

packages/cli/test/lib/cli/base.js

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -280,27 +280,3 @@ test.serial("ui5 --no-update-notifier", async (t) => {
280280
t.regex(stdout, /@ui5\/cli:/, "Output includes version information");
281281
t.false(failed, "Command should not fail");
282282
});
283-
284-
test.serial("ui5 --output-style", async (t) => {
285-
await t.throwsAsync(ui5(["build", "--output-style", "nonExistent"]), {
286-
message: /Argument: output-style, Given: "Nonexistent", Choices: "Default", "Flat", "Namespace"/s
287-
}, "Coercion correctly capitalizes the first letter and makes the rest lowercase");
288-
289-
290-
// "--output-style" uses a coerce to transform the input into the correct letter case.
291-
// It is hard/unmaintainable to spy on internal implementation, so we check the output.
292-
// The coerce goes before the real ui5 build, so we just need to check whether
293-
// an invalid "--output-style" choice exception is not thrown.
294-
// Of course, the build would throw another exception, because there's nothing actually to build.
295-
await t.throwsAsync(ui5(["build", "--output-style", "flat"]), {
296-
message: /^((?!Argument: output-style, Given: "Flat).)*$/s
297-
}, "Does not throw an exception because of the --output-style input");
298-
299-
await t.throwsAsync(ui5(["build", "--output-style", "nAmEsPaCe"]), {
300-
message: /^((?!Argument: output-style, Given: "Namespace).)*$/s
301-
}, "Does not throw an exception because of the --output-style input");
302-
303-
await t.throwsAsync(ui5(["build", "--output-style", "Default"]), {
304-
message: /^((?!Argument: output-style, Given: "Default).)*$/s
305-
}, "Does not throw an exception because of the --output-style input");
306-
});

0 commit comments

Comments
 (0)