Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion internal/documentation/docs/updates/migrate-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ 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`**
- **@ui5/cli: Option `--cache-mode` has been renamed to `--snapshot-cache`**

- **@ui5/cli: Project/Workspace options are now scoped per command**

- **@ui5/server: Live Reload is enabled by default for `ui5 serve`**

Expand Down Expand Up @@ -80,6 +82,20 @@ With UI5 CLI v5, the option `--cache-mode` (for commands `ui5 build` and `ui5 se

When legacy `--cache-mode` is used, the behavior remains the same but a deprecation warning is logged. When both `--snapshot-cache` and `--cache-mode` are used, the `--snapshot-cache` flag always gets priority.

## Project/Workspace Options Scoped per Command

In previous versions, the options `--config` / `-c`, `--dependency-definition`, `--workspace-config`, and `--workspace` / `-w` were accepted by every UI5 CLI command. Several commands silently ignored them.

With UI5 CLI v5, these options are now only accepted by commands that actually consume them. Passing them to other commands now fails with `Unknown argument: <option>`.

| Command | `--config` / `--dependency-definition` | `--workspace` / `--workspace-config` |
| ----------------------------- | -------------------------------------- | ------------------------------------ |
| `ui5 build`, `serve`, `tree` | ✓ | ✓ |
| `ui5 add`, `remove`, `use` | ✓ | — |
| `ui5 config`, `init`, `versions` | — | — |

If you previously passed any of these options to a command that did not use them, remove them from your invocation.

## UI5 CLI Init Command

The `ui5 init` command now generates projects with Specification Version 5.0 by default.
Expand Down
55 changes: 2 additions & 53 deletions packages/cli/lib/cli/base.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,11 @@
import chalk from "chalk";
import {isLogLevelEnabled} from "@ui5/logger";
import ConsoleWriter from "@ui5/logger/writers/Console";
import {dedupeArray} from "./options.js";

export default function(cli) {
cli.usage("Usage: ui5 <command> [options]")
.demandCommand(1, "Command required")
.option("config", {
alias: "c",
describe: "Path to project configuration file in YAML format",
type: "string"
})
.option("dependency-definition", {
describe: "Path to a YAML file containing the project's dependency tree. " +
"This option will disable resolution of node package dependencies.",
type: "string"
})
.option("workspace-config", {
describe: "Path to workspace configuration file in YAML format",
type: "string"
})
.option("workspace", {
alias: "w",
describe: "Name of the workspace configuration to use",
default: "default",
type: "string"
})
.option("loglevel", {
alias: "log-level",
describe: "Set the logging level",
Expand All @@ -47,43 +28,11 @@ export default function(cli) {
default: false,
type: "boolean"
})
.coerce([
// base.js
"config", "dependency-definition", "workspace-config", "workspace", "log-level",

// tree.js, build.js & serve.js
"framework-version", "cache-mode",

// build.js
"dest",

// serve.js
"open", "port", "key", "cert",
], (arg) => {
// If an option is specified multiple times, yargs creates an array for all the values,
// independently of whether the option is of type "array" or "string".
// This is unexpected for options listed above, which should all only have only one definitive value.
// The yargs behavior could be disabled by using the parserConfiguration "duplicate-arguments-array": true
// However, yargs would then cease to create arrays for those options where we *do* expect the
// automatic creation of arrays in case the option is specified multiple times. Like "--include-task".
// Also see https://github.com/yargs/yargs/issues/1318
// Note: This is not necessary for options of type "boolean"
if (Array.isArray(arg)) {
// If the option is specified multiple times, use the value of the last option
return arg[arg.length - 1];
}
return arg;
})
.coerce(["log-level"], dedupeArray)
.showHelpOnFail(true)
.strict(true)
.alias("help", "h")
.alias("version", "v")
.example("ui5 <command> --dependency-definition /path/to/projectDependencies.yaml",
"Execute command using a static dependency tree instead of resolving node package dependencies")
.example("ui5 <command> --config /path/to/ui5.yaml",
"Execute command using a project configuration from custom path")
.example("ui5 <command> --workspace dolphin",
"Execute command using the 'dolphin' workspace of a ui5-workspace.yaml")
.example("ui5 <command> --log-level silly",
"Execute command with the maximum log output")
.fail(function(msg, err, yargs) {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/lib/cli/commands/add.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Add
import base from "../middlewares/base.js";
import {applyProjectConfigOptions} from "../options.js";
const addCommand = {
command: "add [--development] [--optional] <framework-libraries..>",
describe: "Add SAPUI5/OpenUI5 framework libraries to the project configuration.",
middlewares: [base]
};

addCommand.builder = function(cli) {
applyProjectConfigOptions(cli);
return cli
.positional("framework-libraries", {
describe: "Framework library names",
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/lib/cli/commands/build.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import baseMiddleware from "../middlewares/base.js";
import {applyProjectConfigOptions, applyWorkspaceOptions, dedupeArray} from "../options.js";
import {getLogger} from "@ui5/logger";
const log = getLogger("cli:commands:build");

Expand All @@ -10,6 +11,8 @@ const build = {
};

build.builder = function(cli) {
applyProjectConfigOptions(cli);
applyWorkspaceOptions(cli);
return cli
.command("jsdoc", "Build JSDoc resources", {
handler: handleBuild,
Expand Down Expand Up @@ -97,6 +100,7 @@ build.builder = function(cli) {
choices: ["Default", "Force", "ReadOnly", "Off"],
})
.coerce("cache", (opt) => {
opt = dedupeArray(opt);
const lower = opt.toLowerCase();
if (lower === "readonly" || lower === "read-only") {
return "ReadOnly";
Expand Down Expand Up @@ -135,6 +139,7 @@ build.builder = function(cli) {
choices: ["Default", "Force", "Off"],
})
.coerce("cache-mode", (opt) => {
opt = dedupeArray(opt);
// Log a warning if this option is used
if (opt !== undefined) {
log.warn("As of UI5 CLI version 5, '--cache-mode' is renamed to '--snapshot-cache'. " +
Expand Down Expand Up @@ -172,8 +177,10 @@ build.builder = function(cli) {
choices: ["Default", "Flat", "Namespace"],
})
.coerce("output-style", (opt) => {
opt = dedupeArray(opt);
return opt.charAt(0).toUpperCase() + opt.slice(1).toLowerCase();
})
.coerce(["framework-version", "dest"], dedupeArray)
.example("ui5 build", "Preload build for project without dependencies")
.example("ui5 build self-contained", "Self-contained build for project")
.example("ui5 build --exclude-task=* --include-task=minify generateComponentPreload",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/lib/cli/commands/remove.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Remove
import baseMiddleware from "../middlewares/base.js";
import {applyProjectConfigOptions} from "../options.js";

const removeCommand = {
command: "remove <framework-libraries..>",
Expand All @@ -8,6 +9,7 @@ const removeCommand = {
};

removeCommand.builder = function(cli) {
applyProjectConfigOptions(cli);
return cli
.positional("framework-libraries", {
describe: "Framework library names",
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/lib/cli/commands/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from "node:path";
import os from "node:os";
import chalk from "chalk";
import baseMiddleware from "../middlewares/base.js";
import {applyProjectConfigOptions, applyWorkspaceOptions, dedupeArray} from "../options.js";
import {getLogger} from "@ui5/logger";
const log = getLogger("cli:commands:serve");

Expand All @@ -13,6 +14,8 @@ const serve = {
};

serve.builder = function(cli) {
applyProjectConfigOptions(cli);
applyWorkspaceOptions(cli);
return cli
.option("port", {
describe: "Port to bind on (default for HTTP: 8080, HTTP/2: 8443)",
Expand Down Expand Up @@ -82,6 +85,7 @@ serve.builder = function(cli) {
choices: ["Default", "Force", "ReadOnly", "Off"],
})
.coerce("cache", (opt) => {
opt = dedupeArray(opt);
const lower = opt.toLowerCase();
if (lower === "readonly" || lower === "read-only") {
return "ReadOnly";
Expand All @@ -103,6 +107,7 @@ serve.builder = function(cli) {
choices: ["Default", "Force", "Off"],
})
.coerce("cache-mode", (opt) => {
opt = dedupeArray(opt);
// Log a warning if this option is used
if (opt !== undefined) {
log.warn("As of UI5 CLI version 5, '--cache-mode' is renamed to '--snapshot-cache'. " +
Expand All @@ -119,6 +124,7 @@ serve.builder = function(cli) {
defaultDescription: "Default", // Use "defaultDescription" to allow undefined (needed for evaluation)
choices: ["Default", "Force", "Off"],
})
.coerce(["framework-version", "open", "port", "key", "cert"], dedupeArray)
.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)")
.example("ui5 serve --config /path/to/ui5.yaml", "Use the project configuration from a custom path")
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/lib/cli/commands/tree.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Tree
import baseMiddleware from "../middlewares/base.js";
import {applyProjectConfigOptions, applyWorkspaceOptions, dedupeArray} from "../options.js";
import chalk from "chalk";
import {getLogger} from "@ui5/logger";
const log = getLogger("cli:commands:tree");
Expand All @@ -14,6 +15,8 @@ const tree = {
};

tree.builder = function(cli) {
applyProjectConfigOptions(cli);
applyWorkspaceOptions(cli);
return cli
.option("flat", {
describe: "Output a flat list of all dependencies instead of a tree hierarchy",
Expand All @@ -40,6 +43,7 @@ tree.builder = function(cli) {
choices: ["Default", "Force", "Off"],
})
.coerce("cache-mode", (opt) => {
opt = dedupeArray(opt);
// Log a warning if this option is used
if (opt !== undefined) {
log.warn("As of UI5 CLI version 5, '--cache-mode' is renamed to '--snapshot-cache'. " +
Expand All @@ -55,7 +59,8 @@ tree.builder = function(cli) {
type: "string",
defaultDescription: "Default", // Use "defaultDescription" to allow undefined (needed for evaluation)
choices: ["Default", "Force", "Off"],
});
})
.coerce(["framework-version"], dedupeArray);
};

tree.handler = async function(argv) {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/lib/cli/commands/use.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Use
import baseMiddleware from "../middlewares/base.js";
import {applyProjectConfigOptions} from "../options.js";

const useCommand = {
command: "use <framework-info>",
Expand All @@ -8,6 +9,7 @@ const useCommand = {
};

useCommand.builder = function(cli) {
applyProjectConfigOptions(cli);
return cli
.positional("framework-info", {
describe: "Framework name, version or both (name@version).\n" +
Expand Down
78 changes: 78 additions & 0 deletions packages/cli/lib/cli/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Shared yargs option factories used by the CLI commands.
*
* Project-graph related options are not relevant for every command (e.g. "ui5 versions"
* does not load a project). To keep the option definitions in a single place while only
* exposing them on commands that actually consume them, each command builder opts in
* to the option groups it needs via the helpers below.
*/

/**
* Coerce function that keeps only the last value when an option is specified multiple times.
*
* If an option is specified multiple times, yargs creates an array for all the values,
* independently of whether the option is of type "array" or "string".
* This is unexpected for the options listed in the helpers below, which should all
* only have one definitive value.
*
* The yargs behavior could be disabled by using the parserConfiguration
* "duplicate-arguments-array": true. However, yargs would then cease to create arrays
* for those options where we *do* expect the automatic creation of arrays in case the
* option is specified multiple times. Like "--include-task".
* Also see https://github.com/yargs/yargs/issues/1318
*
* Note: This is not necessary for options of type "boolean".
*
* @param {any} arg The yargs value (may be an array if the option was specified multiple times)
* @returns {any} The last value when arg is an array, otherwise arg unchanged
*/
export function dedupeArray(arg) {
if (Array.isArray(arg)) {
return arg[arg.length - 1];
}
return arg;
}

/**
* Adds the project configuration related options ("--config" / "-c" and
* "--dependency-definition") to the given yargs instance.
*
* @param {object} cli The yargs instance
* @returns {object} The yargs instance
*/
export function applyProjectConfigOptions(cli) {
return cli
.option("config", {
alias: "c",
describe: "Path to project configuration file in YAML format",
type: "string"
})
.option("dependency-definition", {
describe: "Path to a YAML file containing the project's dependency tree. " +
"This option will disable resolution of node package dependencies.",
type: "string"
})
.coerce(["config", "dependency-definition"], dedupeArray);
}

/**
* Adds the workspace related options ("--workspace-config" and "--workspace" / "-w")
* to the given yargs instance.
*
* @param {object} cli The yargs instance
* @returns {object} The yargs instance
*/
export function applyWorkspaceOptions(cli) {
return cli
.option("workspace-config", {
describe: "Path to workspace configuration file in YAML format",
type: "string"
})
.option("workspace", {
alias: "w",
describe: "Name of the workspace configuration to use",
default: "default",
type: "string"
})
.coerce(["workspace-config", "workspace"], dedupeArray);
}
37 changes: 37 additions & 0 deletions packages/cli/test/lib/cli/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,40 @@ test.serial("ui5 --no-update-notifier", async (t) => {
t.regex(stdout, /@ui5\/cli:/, "Output includes version information");
t.false(failed, "Command should not fail");
});

// Strict-rejection tests for project/workspace options on commands that should not accept them.
// These commands do not load a project graph, so the corresponding global options have been
// removed from their builders in UI5 CLI v5.
[
{command: "versions", flag: "--config", value: "foo"},
{command: "versions", flag: "--dependency-definition", value: "foo"},
{command: "versions", flag: "--workspace", value: "foo"},
{command: "versions", flag: "--workspace-config", value: "foo"},
{command: "init", flag: "--config", value: "foo"},
{command: "init", flag: "--workspace", value: "foo"},
{command: "config", flag: "--config", value: "foo", args: ["list"]},
{command: "config", flag: "--workspace", value: "foo", args: ["list"]},
].forEach(({command, flag, value, args = []}) => {
test.serial(`ui5 ${command} ${flag}: rejects unknown argument`, async (t) => {
const {stderr, exitCode} = await ui5([command, ...args, flag, value], {reject: false});
t.is(exitCode, 1, "Command exits with error code 1");
t.regex(stripAnsi(stderr), new RegExp(`Unknown arguments?: .*\\b${flag.replace(/^--/, "")}\\b`),
"Stderr contains 'Unknown argument' message");
});
});

// Strict-rejection tests for workspace options on add/remove/use which do operate on ui5.yaml
// but not within a workspace context.
[
{command: "add", flag: "--workspace", value: "foo", args: ["sap.ui.core"]},
{command: "add", flag: "--workspace-config", value: "foo", args: ["sap.ui.core"]},
{command: "remove", flag: "--workspace", value: "foo", args: ["sap.ui.core"]},
{command: "use", flag: "--workspace", value: "foo", args: ["latest"]},
].forEach(({command, flag, value, args}) => {
test.serial(`ui5 ${command} ${flag}: rejects unknown argument`, async (t) => {
const {stderr, exitCode} = await ui5([command, ...args, flag, value], {reject: false});
t.is(exitCode, 1, "Command exits with error code 1");
t.regex(stripAnsi(stderr), new RegExp(`Unknown arguments?: .*\\b${flag.replace(/^--/, "")}\\b`),
"Stderr contains 'Unknown argument' message");
});
});
Loading
Loading