Skip to content

Commit 5295cd7

Browse files
committed
refactor(commands): drop the free runCommand and canExecuteCommand helpers
The two convenience functions resolved the dispatcher from the current injection context and otherwise from the CLI's root injector. After a handler's first await, or inside a stdin handler, there is no context, so they silently dispatched through the root and ignored the injector the caller actually held: a per-command provider, an extension's own scope, or a test injector. CommandsService is a service like any other and follows the same rule: inject() before the first await, the injector after it. The shortcut actions dispatch through the injector their context carries, and embed injects the contract for its prepare precondition.
1 parent 56a4531 commit 5295cd7

6 files changed

Lines changed: 60 additions & 129 deletions

File tree

‎defining-commands.md‎

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,8 @@ Sharing is either of two things, and neither of them is a bag:
482482
}
483483
```
484484

485-
- **A whole command's precondition** — `canExecuteCommand(name, args)`, which
486-
asks that command itself; see [Asking another
485+
- **A whole command's precondition** — `CommandsService.canExecuteCommand`,
486+
which asks that command itself; see [Asking another
487487
command](#asking-another-command).
488488

489489
### `setup`, when a command has one
@@ -846,21 +846,23 @@ Running a command in process
846846
----------------------------
847847

848848
The `CommandsService` contract dispatches a registered command from inside the
849-
process that is already running. A class command injects it like any other
850-
service; an inline handler or a key shortcut may use the `runCommand`
851-
convenience, which only resolves the contract from the current context:
849+
process that is already running. It is a service like any other, so it follows
850+
the rule every service does: `inject()` before the first `await`, the
851+
injector after it, and a key shortcut's action reaches it through the
852+
injector its context carries:
852853

853854
```ts
854-
import { CommandsService } from "../common/contracts/commands-service";
855-
import { runCommand } from "../common/services/command-definition-adapter";
855+
import { CommandsService } from "nativescript/contracts";
856856

857857
// in a class command
858858
private $commandsService = inject(CommandsService);
859859
await this.$commandsService.runCommand("autocomplete");
860860

861-
// in an inline handler or a shortcut action
862-
await runCommand("open|ios");
863-
await runCommand("install", ["lodash"]);
861+
// in an inline handler, after the first await
862+
await ctx.injector.get(CommandsService).runCommand("install", ["lodash"]);
863+
864+
// in a shortcut action
865+
action: (ctx) => ctx.injector.get(CommandsService).runCommand("open|ios"),
864866
```
865867

866868
The command gets what a typed command line gives it, in the same order: its
@@ -887,10 +889,11 @@ declarations into it rewrites the values the host process is still running on
887889
— `open|ios` declares `watch: false`, which would otherwise leave an `ns start`
888890
out of watch mode for the rest of its life.
889891

890-
Which injector `runCommand` dispatches through follows the rule
891-
`registerCommand` does: the injector of the current injection context, and the
892-
CLI's own outside one. The pipeline itself lives on the contract, so a plugin
893-
that holds an injector can call `CommandsService.runCommand` directly.
892+
There is deliberately no free `runCommand()` function: one that silently fell
893+
back to the CLI's root injector outside an injection context would dispatch
894+
through the wrong scope from exactly the places — after an `await`, inside a
895+
stdin handler — where the mistake is hardest to notice. The injector you hold
896+
is the one to dispatch through.
894897

895898
### Asking another command
896899

@@ -903,15 +906,21 @@ given, whether or not it is registered, so `runCommand(prepareCommandDefinition)
903906
runs exactly what you hold and cannot go stale the way a string can. Its first
904907
name still identifies it for hooks and reporting.
905908

906-
`CommandsService.canExecuteCommand(command, args)` — or the
907-
`canExecuteCommand` convenience — asks a registered command whether it *could*
908-
run, without running it:
909+
`CommandsService.canExecuteCommand(command, args)` asks a registered command
910+
whether it *could* run, without running it:
909911

910912
```ts
911-
import { canExecuteCommand } from "../common/services/command-definition-adapter";
913+
import { CommandsService } from "nativescript/contracts";
914+
915+
private $commandsService = inject(CommandsService);
912916

913917
async canExecute(): Promise<boolean> {
914-
if (!(await canExecuteCommand("prepare", [this.args[0]]))) {
918+
if (
919+
!(await this.$commandsService.canExecuteCommand(
920+
prepareCommandDefinition,
921+
[this.args[0]],
922+
))
923+
) {
915924
return false;
916925
}
917926

@@ -931,10 +940,10 @@ Pass only the arguments the child's own `arguments` policy accepts. The child
931940
enforces that policy before its `canExecute`, so forwarding a caller's whole
932941
argument list to a child that declares fewer is a rejection, not a wider check.
933942

934-
`canExecuteCommand` is a thin call onto `CommandsService.canExecuteCommand`
935-
(which the deprecated `canExecuteCommandInProcess` also calls), and follows
936-
`runCommand` in everything else: the same injector rule, the same option priming and
937-
restoration.
943+
`canExecuteCommand` follows `runCommand` in everything else: the same option
944+
priming and restoration, the same routing of a parent name to its subcommand.
945+
The deprecated `canExecuteCommandInProcess` and `executeCommandInProcess`
946+
call the two methods with a name.
938947

939948
### Key shortcuts
940949

@@ -947,7 +956,7 @@ an `action` that runs it:
947956
key: "I",
948957
description: "Open project in Xcode",
949958
when: onPlatform("iOS"),
950-
action: () => runCommand("open|ios"),
959+
action: (ctx) => ctx.injector.get(CommandsService).runCommand("open|ios"),
951960
}
952961
```
953962

‎lib/commands/embedding/embed.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { IProjectConfigService, IProjectData } from "../../definitions/project";
55
import { Command } from "../../common/define-command";
66
import { IFileSystem } from "../../common/declarations";
77
import { inject } from "../../common/di";
8-
import { canExecuteCommand } from "../../common/services/command-definition-adapter";
8+
import { CommandsService } from "../../common/contracts/commands-service";
99
import { platformArgument } from "../command-base";
1010
import {
1111
prepareCommandDefinition,
@@ -35,6 +35,7 @@ export class EmbedCommand extends Command({
3535
{ name: "hostProjectModuleName" },
3636
],
3737
}) {
38+
private $commandsService = inject(CommandsService);
3839
private $fs = inject<IFileSystem>("fs");
3940
private $logger = inject<ILogger>("logger");
4041
private $options = inject<IOptions>("options");
@@ -57,7 +58,7 @@ export class EmbedCommand extends Command({
5758
// `prepare` takes the platform alone; the host project arguments are this
5859
// command's own and it would reject them.
5960
if (
60-
!(await canExecuteCommand(
61+
!(await this.$commandsService.canExecuteCommand(
6162
prepareCommandDefinition,
6263
this.args.slice(0, 1),
6364
))

‎lib/common/services/command-definition-adapter.ts‎

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
CommandOptionSpec,
3232
CommandOptionType,
3333
CommandOptionsSchema,
34-
CommandReference,
3534
DefinedCommand,
3635
RegisterableCommand,
3736
defineCommand,
@@ -572,31 +571,6 @@ const namesOf = (definition: DefinedCommand<any, any, any>): string[] =>
572571
const contextInjector = (): Injector =>
573572
getCurrentInjector() || <Injector>(<any>getRootInjector());
574573

575-
/**
576-
* Convenience over `CommandsService.runCommand` for code that has no injected
577-
* service at hand, such as a key shortcut action or an inline handler; the
578-
* contract is the API, this only resolves it from the current context.
579-
*/
580-
export async function runCommand(
581-
command: CommandReference,
582-
args: string[] = [],
583-
): Promise<void> {
584-
await contextInjector().get(CommandsService).runCommand(command, args);
585-
}
586-
587-
/**
588-
* Convenience over `CommandsService.canExecuteCommand`, resolved from the
589-
* current context the way `runCommand` is.
590-
*/
591-
export async function canExecuteCommand(
592-
command: CommandReference,
593-
args: string[] = [],
594-
): Promise<boolean> {
595-
return contextInjector()
596-
.get(CommandsService)
597-
.canExecuteCommand(command, args);
598-
}
599-
600574
/**
601575
* Registers a command with the CLI. Takes a Command() class, the result of
602576
* defineCommand(), or a bare definition, which it defines on the caller's

‎lib/services/key-shortcuts.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
KeyShortcutRegistration,
1414
KeyShortcutRegistry,
1515
} from "../common/contracts/key-shortcuts";
16-
import { runCommand } from "../common/services/command-definition-adapter";
16+
import { CommandsService } from "../common/contracts/commands-service";
1717
import { injector } from "../common/yok";
1818
import { IProjectDataService } from "../definitions/project";
1919
import { IStartService } from "../definitions/start-service";
@@ -318,7 +318,10 @@ export function openIdeShortcut(
318318
description,
319319
group: platform,
320320
when: onPlatform(platform),
321-
action: () => runCommand(`open|${platform.toLowerCase()}`),
321+
action: (ctx) =>
322+
ctx.injector
323+
.get(CommandsService)
324+
.runCommand(`open|${platform.toLowerCase()}`),
322325
};
323326
}
324327

@@ -367,7 +370,7 @@ export function keyShortcuts(): KeyShortcut<NsKeyContext>[] {
367370
key: "n",
368371
description: "Install dependencies",
369372
group: WORKFLOW_GROUP,
370-
action: () => runCommand("install"),
373+
action: (ctx) => ctx.injector.get(CommandsService).runCommand("install"),
371374
},
372375
];
373376
}

‎test/define-command.ts‎

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {
3030
stringOption,
3131
} from "../lib/common/define-command";
3232
import {
33-
canExecuteCommand,
3433
createCommandFromDefinition,
3534
registerBuiltInCommand,
3635
registerCommand,
@@ -1511,12 +1510,12 @@ describe("defineCommand", () => {
15111510
});
15121511

15131512
const verdicts = [
1514-
await runInInjectionContext(testInjector, () =>
1515-
canExecuteCommand("dctest-can-yes", ["ok"]),
1516-
),
1517-
await runInInjectionContext(testInjector, () =>
1518-
canExecuteCommand("dctest-can-yes", ["nope"]),
1519-
),
1513+
await testInjector
1514+
.resolve("commandsService")
1515+
.canExecuteCommand("dctest-can-yes", ["ok"]),
1516+
await testInjector
1517+
.resolve("commandsService")
1518+
.canExecuteCommand("dctest-can-yes", ["nope"]),
15201519
];
15211520

15221521
assert.deepEqual(verdicts, [true, false]);
@@ -1616,9 +1615,9 @@ describe("defineCommand", () => {
16161615
);
16171616

16181617
await assert.isRejected(
1619-
runInInjectionContext(testInjector, () =>
1620-
canExecuteCommand("dctest-can-none", ["stray"]),
1621-
),
1618+
testInjector
1619+
.resolve("commandsService")
1620+
.canExecuteCommand("dctest-can-none", ["stray"]),
16221621
/doesn't accept parameters/,
16231622
);
16241623
assert.isFalse(consulted);
@@ -1642,19 +1641,19 @@ describe("defineCommand", () => {
16421641
);
16431642

16441643
assert.isTrue(
1645-
await runInInjectionContext(testInjector, () =>
1646-
canExecuteCommand("dctest-can-setup"),
1647-
),
1644+
await testInjector
1645+
.resolve("commandsService")
1646+
.canExecuteCommand("dctest-can-setup"),
16481647
);
16491648
});
16501649

16511650
it("fails by name for a command that is not registered", async () => {
16521651
const testInjector = createInProcessInjector();
16531652

16541653
await assert.isRejected(
1655-
runInInjectionContext(testInjector, () =>
1656-
canExecuteCommand("dctest-can-missing"),
1657-
),
1654+
testInjector
1655+
.resolve("commandsService")
1656+
.canExecuteCommand("dctest-can-missing"),
16581657
/Unknown command 'dctest-can-missing'/,
16591658
);
16601659
});

‎test/services/key-shortcuts.ts‎

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { assert } from "chai";
22
import { EventEmitter } from "events";
33
import { RunOnDeviceEvents } from "../../lib/constants";
44
import { getContractName } from "../../lib/common/di/contract";
5-
import { runInInjectionContext } from "../../lib/common/di/inject";
65
import { Injector } from "../../lib/common/di/injector";
7-
import { runCommand } from "../../lib/common/services/command-definition-adapter";
86
import { KeyShortcutRegistryService } from "../../lib/services/key-shortcut-registry";
97
import {
108
findShortcut,
@@ -213,7 +211,7 @@ describe("key shortcuts", () => {
213211
]);
214212
});
215213

216-
it("routes the IDE shortcuts through the open commands", async () => {
214+
it("routes the IDE shortcuts through the context's commands service", async () => {
217215
const invoked: string[] = [];
218216
const dispatcher = fakeInjector(
219217
new Map<any, any>([
@@ -226,13 +224,11 @@ describe("key shortcuts", () => {
226224
],
227225
]),
228226
);
229-
const ctx = context();
227+
const ctx = context({ injector: dispatcher });
230228
const resolved = resolveShortcuts(keyShortcuts(), ctx);
231229

232230
for (const key of ["A", "I", "V", "n"]) {
233-
await runInInjectionContext(dispatcher, () =>
234-
findShortcut(resolved, key).action(ctx),
235-
);
231+
await findShortcut(resolved, key).action(ctx);
236232
}
237233

238234
assert.deepEqual(invoked, [
@@ -952,57 +948,6 @@ describe("key shortcuts", () => {
952948
);
953949
});
954950
});
955-
956-
describe("runCommand", () => {
957-
it("dispatches through the commands service of the current context", async () => {
958-
const dispatched: { name: string; args: string[] }[] = [];
959-
const dispatcher = fakeInjector(
960-
new Map<any, any>([
961-
[
962-
"commandsService",
963-
{
964-
runCommand: async (name: string, args: string[]): Promise<void> =>
965-
void dispatched.push({ name, args }),
966-
},
967-
],
968-
]),
969-
);
970-
971-
await runInInjectionContext(dispatcher, () =>
972-
runCommand("open|ios", ["--verbose"]),
973-
);
974-
await runInInjectionContext(dispatcher, () => runCommand("install"));
975-
976-
assert.deepEqual(dispatched, [
977-
{ name: "open|ios", args: ["--verbose"] },
978-
{ name: "install", args: [] },
979-
]);
980-
});
981-
982-
it("lets a failure reach the caller", async () => {
983-
const dispatcher = fakeInjector(
984-
new Map<any, any>([
985-
[
986-
"commandsService",
987-
{
988-
runCommand: async (): Promise<void> => {
989-
throw new Error("Unable to execute command 'open ios'.");
990-
},
991-
},
992-
],
993-
]),
994-
);
995-
996-
let raised: Error = null;
997-
try {
998-
await runInInjectionContext(dispatcher, () => runCommand("open|ios"));
999-
} catch (err) {
1000-
raised = err;
1001-
}
1002-
1003-
assert.equal(raised.message, "Unable to execute command 'open ios'.");
1004-
});
1005-
});
1006951
});
1007952

1008953
function noop(): void {

0 commit comments

Comments
 (0)