Skip to content

Commit 56a4531

Browse files
committed
feat(commands): give ctx.fail a help flag and migrate commands off $errors
ctx.fail always printed the usage help suggestion, so a command failing over the environment or the project state - a missing platform, a file that already exists - either misreported it as a command-line mistake or kept injecting the errors service for the plain form, which most of the built-ins did. ctx.fail(message, options?) takes a CommandFailOptions bag; help defaults to true and { help: false } fails without the suggestion. The type is exported from nativescript/contracts. The built-in commands now fail through their context: failWithHelp calls became ctx.fail and plain fail calls became ctx.fail with help off, and the errors service injection went with them where nothing else used it. Calls that pass format arguments or an error object stay on the errors service.
1 parent 3927d99 commit 56a4531

40 files changed

Lines changed: 294 additions & 171 deletions

‎defining-commands.md‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -349,15 +349,22 @@ The run context
349349
- `ctx.injector` — this invocation's injector, a child of the one the command
350350
was registered against; see
351351
[Injection, and the first `await`](#injection-and-the-first-await).
352-
- `ctx.fail(message)` — fails the command with `message` and a usage help
353-
suggestion.
352+
- `ctx.fail(message, options?)` — fails the command with `message`, followed
353+
by a usage help suggestion unless `options.help` is `false`.
354354

355355
`run` may be synchronous or `async`; the CLI awaits the result and treats a
356356
rejection as a command failure.
357357

358358
### Failing a command
359359

360-
`ctx.fail(message)` is the idiomatic way to stop a command:
360+
`ctx.fail(message)` is the idiomatic way to stop a command. By default it
361+
follows the message with the usage help suggestion — the "Run `ns widget add
362+
--help`" line — which is what the user needs when they got the command line
363+
wrong: a missing argument, an unknown value, an invalid combination of options.
364+
365+
When the command line was fine and something else is not — the environment,
366+
the project, a file on disk — the help suggestion only gets in the way. Pass
367+
`{ help: false }` to print the message alone:
361368

362369
```ts
363370
defineCommand({
@@ -369,17 +376,22 @@ defineCommand({
369376
ctx.fail("--output is required.");
370377
}
371378

379+
if (fs.existsSync(ctx.options.output)) {
380+
ctx.fail(`${ctx.options.output} already exists.`, { help: false });
381+
}
382+
372383
/* ... */
373384
},
374385
});
375386
```
376387

377-
It is available on the `canExecute` context as well, and it returns `never`, so
378-
it can end a branch without a `return`. The message must be a non-empty string.
388+
It is available on the `setup` and `canExecute` contexts as well, and it
389+
returns `never`, so it can end a branch without a `return`. The message must be
390+
a non-empty string, and `options`, when given, a plain object. The two forms
391+
map onto the `errors` service's `failWithHelp` and `fail`.
379392

380-
Throwing is equivalent and keeps working — `ctx.fail` is sugar over the
381-
`errors` service's `failWithHelp`, which is what adds the "Run `ns widget add
382-
--help`" line. Throw when you already have an `Error` to propagate; call
393+
Throwing keeps working too: an error thrown from a handler propagates
394+
unchanged. Throw when you already have an `Error` to propagate; call
383395
`ctx.fail` when you are writing the message.
384396

385397
Injection, and the first `await`

‎lib/commands/add-platform.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
IPlatformValidationService,
55
} from "../declarations";
66
import { IProjectData } from "../definitions/project";
7-
import { IErrors } from "../common/declarations";
87
import {
98
Command,
109
CommandOptionsSchema,
@@ -23,7 +22,6 @@ export class AddPlatformCommand extends Command({
2322
options: addPlatformCommandOptions,
2423
arguments: "any",
2524
}) {
26-
private $errors = inject<IErrors>("errors");
2725
private $platformCommandHelper = inject<IPlatformCommandHelper>(
2826
"platformCommandHelper",
2927
);
@@ -40,7 +38,7 @@ export class AddPlatformCommand extends Command({
4038
public async canExecute(): Promise<boolean> {
4139
const args = this.args;
4240
if (!args || args.length === 0) {
43-
this.$errors.failWithHelp(
41+
this.context.fail(
4442
"No platform specified. Please specify a platform to add.",
4543
);
4644
}
@@ -55,8 +53,9 @@ export class AddPlatformCommand extends Command({
5553
this.$projectData,
5654
)
5755
) {
58-
this.$errors.fail(
56+
this.context.fail(
5957
`Applications for platform ${arg} cannot be built on this OS`,
58+
{ help: false },
6059
);
6160
}
6261

‎lib/commands/apple-login.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { IErrors } from "../common/declarations";
21
import { defineCommand } from "../common/define-command";
32
import { inject } from "../common/di";
43
import { IApplePortalSessionService } from "../services/apple-portal/definitions";
@@ -11,7 +10,6 @@ export const appleLoginCommandDefinition = defineCommand({
1110
const $applePortalSessionService = inject<IApplePortalSessionService>(
1211
"applePortalSessionService",
1312
);
14-
const $errors = inject<IErrors>("errors");
1513
const $logger = inject<ILogger>("logger");
1614
const $prompter = inject<IPrompter>("prompter");
1715

@@ -32,8 +30,9 @@ export const appleLoginCommandDefinition = defineCommand({
3230
password,
3331
});
3432
if (!user.areCredentialsValid) {
35-
$errors.fail(
33+
context.fail(
3634
`Invalid username and password combination. Used '${username}' as the username.`,
35+
{ help: false },
3736
);
3837
}
3938

‎lib/commands/appstore-list.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { IErrors } from "../common/declarations";
21
import {
32
Command,
43
CommandOptionsSchema,
@@ -31,7 +30,6 @@ export class ListiOSAppsCommand extends Command({
3130
private $devicePlatformsConstants = inject<Mobile.IDevicePlatformsConstants>(
3231
"devicePlatformsConstants",
3332
);
34-
private $errors = inject<IErrors>("errors");
3533
private $logger = inject<ILogger>("logger");
3634
private $platformValidationService = inject<IPlatformValidationService>(
3735
"platformValidationService",
@@ -51,8 +49,9 @@ export class ListiOSAppsCommand extends Command({
5149
this.$projectData,
5250
)
5351
) {
54-
this.$errors.fail(
52+
this.context.fail(
5553
`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`,
54+
{ help: false },
5655
);
5756
}
5857

@@ -76,8 +75,9 @@ export class ListiOSAppsCommand extends Command({
7675
},
7776
);
7877
if (!user.areCredentialsValid) {
79-
this.$errors.fail(
78+
this.context.fail(
8079
`Invalid username and password combination. Used '${username}' as the username.`,
80+
{ help: false },
8181
);
8282
}
8383

‎lib/commands/appstore-upload.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as path from "path";
2-
import { IErrors, IHostInfo } from "../common/declarations";
2+
import { IHostInfo } from "../common/declarations";
33
import {
44
booleanOption,
55
Command,
@@ -41,7 +41,6 @@ export class PublishIOSCommand extends Command({
4141
private $devicePlatformsConstants = inject<Mobile.IDevicePlatformsConstants>(
4242
"devicePlatformsConstants",
4343
);
44-
private $errors = inject<IErrors>("errors");
4544
private $hostInfo = inject<IHostInfo>("hostInfo");
4645
private $itmsTransporterService = inject<IITMSTransporterService>(
4746
"itmsTransporterService",
@@ -61,7 +60,9 @@ export class PublishIOSCommand extends Command({
6160

6261
public canExecute(): boolean {
6362
if (!this.$hostInfo.isDarwin) {
64-
this.$errors.fail("iOS publishing is only available on macOS.");
63+
this.context.fail("iOS publishing is only available on macOS.", {
64+
help: false,
65+
});
6566
}
6667

6768
if (
@@ -70,8 +71,9 @@ export class PublishIOSCommand extends Command({
7071
this.$projectData,
7172
)
7273
) {
73-
this.$errors.fail(
74+
this.context.fail(
7475
`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`,
76+
{ help: false },
7577
);
7678
}
7779

@@ -135,8 +137,9 @@ export class PublishIOSCommand extends Command({
135137
},
136138
);
137139
if (!user.areCredentialsValid) {
138-
this.$errors.fail(
140+
this.context.fail(
139141
`Invalid username and password combination. Used '${username}' as the username.`,
142+
{ help: false },
140143
);
141144
}
142145

‎lib/commands/build.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
import { IBuildController, IBuildDataService } from "../definitions/build";
1313
import { IMigrateController } from "../definitions/migrate";
1414
import { IProjectData } from "../definitions/project";
15-
import { IErrors } from "../common/declarations";
1615
import {
1716
booleanOption,
1817
CommandName,
@@ -52,7 +51,6 @@ const defineBuildCommand = <const TName extends CommandName>(
5251
async canExecute(context): Promise<boolean> {
5352
const $devicePlatformsConstants =
5453
inject<Mobile.IDevicePlatformsConstants>("devicePlatformsConstants");
55-
const $errors = inject<IErrors>("errors");
5654
const $migrateController =
5755
inject<IMigrateController>("migrateController");
5856
const $platformValidationService = inject<IPlatformValidationService>(
@@ -82,8 +80,9 @@ const defineBuildCommand = <const TName extends CommandName>(
8280
$projectData,
8381
)
8482
) {
85-
$errors.fail(
83+
context.fail(
8684
`Applications for platform ${platform} can not be built on this OS`,
85+
{ help: false },
8786
);
8887
}
8988

@@ -96,7 +95,7 @@ const defineBuildCommand = <const TName extends CommandName>(
9695
context.options.release &&
9796
!hasValidAndroidSigning(context.options)
9897
) {
99-
$errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE);
98+
context.fail(ANDROID_RELEASE_BUILD_ERROR_MESSAGE);
10099
}
101100

102101
return validatePlatformOptions(context, platform);

‎lib/commands/config.ts‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { IProjectConfigService } from "../definitions/project";
22
import { SupportedConfigValues } from "../tools/config-manipulation/config-transformer";
3-
import { IErrors } from "../common/declarations";
43
import { CommandContext, defineCommand } from "../common/define-command";
54
import { inject } from "../common/di";
65
import { color } from "../color";
@@ -38,9 +37,7 @@ function getConvertedValue(v: any): any {
3837

3938
function requireConfigKey(context: CommandContext): void {
4039
if (!context.args[0]) {
41-
context.injector
42-
.get<IErrors>("errors")
43-
.failWithHelp("You must specify a key. Eg: ios.id");
40+
context.fail("You must specify a key. Eg: ios.id");
4441
}
4542
}
4643

@@ -93,12 +90,10 @@ export const configSetCommandDefinition = defineCommand({
9390
description: "Sets a value in the project configuration.",
9491
arguments: "any",
9592
async canExecute(context): Promise<boolean> {
96-
const $errors = inject<IErrors>("errors");
97-
9893
requireConfigKey(context);
9994

10095
if (!context.args[1]) {
101-
$errors.failWithHelp("You must specify a value.");
96+
context.fail("You must specify a value.");
10297
}
10398

10499
return true;
@@ -108,13 +103,13 @@ export const configSetCommandDefinition = defineCommand({
108103
"projectConfigService",
109104
);
110105
const $logger = inject<ILogger>("logger");
111-
const $errors = inject<IErrors>("errors");
112106

113107
const [key, value] = context.args;
114108
const current = $projectConfigService.getValue(key);
115109
if (current && typeof current === "object") {
116-
$errors.fail(
110+
context.fail(
117111
`Unable to change object values. Please update individual values instead.\nEg: ns config set android.codeCache true`,
112+
{ help: false },
118113
);
119114
}
120115
const convertedValue = getConvertedValue(value);

‎lib/commands/debug.ts‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IErrors, ISysInfo } from "../common/declarations";
1+
import { ISysInfo } from "../common/declarations";
22
import { commandShortcutsEnabled } from "../common/contracts/key-shortcuts";
33
import {
44
booleanOption,
@@ -61,7 +61,6 @@ async function canExecuteDebugCommand(
6161
context.injector.get<Mobile.IDevicePlatformsConstants>(
6262
"devicePlatformsConstants",
6363
);
64-
const $errors = context.injector.get<IErrors>("errors");
6564
const $migrateController =
6665
context.injector.get<IMigrateController>("migrateController");
6766
const $platformValidationService =
@@ -86,13 +85,14 @@ async function canExecuteDebugCommand(
8685
if (
8786
!$platformValidationService.isPlatformSupportedForOS(platform, $projectData)
8887
) {
89-
$errors.fail(
88+
context.fail(
9089
`Applications for platform ${platform} can not be built on this OS`,
90+
{ help: false },
9191
);
9292
}
9393

9494
if (context.options.release) {
95-
$errors.failWithHelp("--release flag is not applicable to this command.");
95+
context.fail("--release flag is not applicable to this command.");
9696
}
9797

9898
return canExecuteCommandBase(context, platform, {
@@ -235,7 +235,6 @@ const defineApplePlatformDebugCommand = <const TName extends CommandName>(
235235
async canExecute(context): Promise<boolean> {
236236
const $devicePlatformsConstants =
237237
inject<Mobile.IDevicePlatformsConstants>("devicePlatformsConstants");
238-
const $errors = inject<IErrors>("errors");
239238
const $platformValidationService = inject<IPlatformValidationService>(
240239
"platformValidationService",
241240
);
@@ -261,14 +260,16 @@ const defineApplePlatformDebugCommand = <const TName extends CommandName>(
261260
$projectData,
262261
)
263262
) {
264-
$errors.fail(
263+
context.fail(
265264
`Applications for platform ${platform} can not be built on this OS`,
265+
{ help: false },
266266
);
267267
}
268268

269269
if (!isValidTimeoutOption(context.options.timeout)) {
270-
$errors.fail(
270+
context.fail(
271271
`Timeout option specifies the seconds NativeScript CLI will wait to find the inspector socket port from device's logs. Must be a number.`,
272+
{ help: false },
272273
);
273274
}
274275

@@ -278,8 +279,9 @@ const defineApplePlatformDebugCommand = <const TName extends CommandName>(
278279
macOSWarning &&
279280
macOSWarning.severity === SystemWarningsSeverity.high
280281
) {
281-
$errors.fail(
282+
context.fail(
282283
`You cannot use NativeScript Inspector on this OS. To use it, please update your OS.`,
284+
{ help: false },
283285
);
284286
}
285287
}
@@ -305,14 +307,13 @@ export const androidDebugCommand = defineCommand({
305307
options: debugCommandOptions,
306308
arguments: "any",
307309
async canExecute(context): Promise<boolean> {
308-
const $errors = inject<IErrors>("errors");
309310
const $projectData = inject<IProjectData>("projectData");
310311
$projectData.initializeProjectData();
311312

312313
const canExecuteBase = await canExecuteDebugCommand(context, "Android");
313314
if (canExecuteBase) {
314315
if (context.options.aab && !hasValidAndroidSigning(context.options)) {
315-
$errors.failWithHelp(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE);
316+
context.fail(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE);
316317
}
317318
}
318319

‎lib/commands/deploy.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { DeployCommandHelper } from "../helpers/deploy-command-helper";
77
import { hasValidAndroidSigning } from "../common/helpers";
88
import { IMigrateController } from "../definitions/migrate";
99
import { IProjectData } from "../definitions/project";
10-
import { IErrors } from "../common/declarations";
1110
import {
1211
booleanOption,
1312
CommandOptionsSchema,
@@ -34,7 +33,6 @@ export const deployCommandDefinition = defineCommand({
3433
options: deployCommandOptions,
3534
arguments: [platformArgument],
3635
async canExecute(context): Promise<boolean> {
37-
const $errors = inject<IErrors>("errors");
3836
const $migrateController = inject<IMigrateController>("migrateController");
3937
const $mobileHelper = inject<Mobile.IMobileHelper>("mobileHelper");
4038
const $projectData = inject<IProjectData>("projectData");
@@ -59,9 +57,9 @@ export const deployCommandDefinition = defineCommand({
5957
!hasValidAndroidSigning(context.options)
6058
) {
6159
if (context.options.release) {
62-
$errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE);
60+
context.fail(ANDROID_RELEASE_BUILD_ERROR_MESSAGE);
6361
} else {
64-
$errors.failWithHelp(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE);
62+
context.fail(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE);
6563
}
6664
}
6765

0 commit comments

Comments
 (0)