Skip to content

Commit e58a9e8

Browse files
committed
refactor(commands): infer command services from setup
A command's services type is now read off its setup function with ReturnType instead of being declared beside it and kept in sync by hand.
1 parent 2b1ee61 commit e58a9e8

58 files changed

Lines changed: 310 additions & 549 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

defining-commands.md

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,11 @@ options: {
169169

170170
So a redeclaration of the same name with the same type is silent. What the CLI
171171
still warns about at registration is a redeclaration that changes what the
172-
spelling *means*:
172+
spelling _means_:
173173

174174
- a declared option whose name matches a CLI-wide one but whose type differs —
175175
`verbose: stringOption()` against the CLI's boolean `--verbose`;
176-
- an alias that belongs to a *different* CLI-wide option — `output:
176+
- an alias that belongs to a _different_ CLI-wide option — `output:
177177
stringOption({ alias: "p" })` steals `--path`'s shorthand. Restating an
178178
option's own shorthand (`path: stringOption({ alias: "p" })`) is fine.
179179

@@ -449,6 +449,40 @@ top of `run`; nothing else changes. "Once per invocation" means once across
449449
`canExecute`, `run` and `postRun` together — whichever of them the CLI reaches
450450
first triggers it, and the rest reuse the value.
451451

452+
When several commands share a setup, or a helper outside the definition takes
453+
the services as a parameter, lift it into a named function and derive the type
454+
from it instead of writing the shape out by hand:
455+
456+
```ts
457+
export function setupWidgetAddCommand() {
458+
const projectData = inject(ProjectData);
459+
projectData.initializeProjectData();
460+
return { projectData, widgets: inject(WidgetService) };
461+
}
462+
export type IWidgetAddCommandServices = ReturnType<
463+
typeof setupWidgetAddCommand
464+
>;
465+
466+
export function canAddWidget(services: IWidgetAddCommandServices): boolean {
467+
return !!services.projectData.projectDir;
468+
}
469+
470+
export default defineCommand({
471+
name: "widget|add",
472+
arguments: "any",
473+
setup: setupWidgetAddCommand,
474+
canExecute: (ctx, services) => canAddWidget(services),
475+
async run(ctx, { widgets }) {
476+
await widgets.add(ctx.args);
477+
},
478+
});
479+
```
480+
481+
Leave the setup function's return type off: the alias reads what the body
482+
infers, so annotating the function with the alias makes the pair circular. Read
483+
a setup curried over a parameter — `setupX(platform)` returning the setup
484+
itself — through its inner function, `ReturnType<ReturnType<typeof setupX>>`.
485+
452486
`run`'s return value, and `postRun`
453487
-----------------------------------
454488

@@ -504,7 +538,7 @@ all — or the definition itself, which it defines on your behalf, so registerin
504538
a command is one call. Either way the definition is validated before it reaches
505539
the registry. It claims every name the definition declares, through the
506540
`CommandRegistry` the target injector provides, and returns a
507-
`DeferredCommandResult` — see *The owner is ambient* below. The command instance
541+
`DeferredCommandResult` — see _The owner is ambient_ below. The command instance
508542
is built by a factory on first resolution and cached.
509543

510544
Pass providers as the second argument to scope the command to a child injector
@@ -523,7 +557,7 @@ That is how one definition serves several commands that differ only in data —
523557
the platform each one targets — instead of one command subclassing another.
524558

525559
**Which injector it registers against is not a parameter.** It is the injector
526-
of the current injection context — see *The owner is ambient* below — and the
560+
of the current injection context — see _The owner is ambient_ below — and the
527561
CLI's own injector outside one. To register against some other injector, run
528562
the call in its context:
529563

@@ -722,16 +756,16 @@ A definition is compiled into an ordinary `ICommand`, so nothing downstream —
722756
the registry, the router, hooks, help, analytics — knows the difference. The
723757
mapping is:
724758

725-
| Definition | `ICommand` |
726-
| --------------------------------- | --------------------------------------------------- |
727-
| `options` | `dashedOptions` |
728-
| `run` | `execute`, wrapped in an injection context |
729-
| `arguments`, `canExecute` | `canExecute`: policy enforced, then the refinement |
730-
| `setup` | — run inside `canExecute`/`execute`, memoised |
731-
| `postRun` | `postCommandAction`, with `run`'s return value |
732-
| `allowUnknownOptions` | `skipOptionsValidation` |
733-
|| `allowedParameters`, always `[]` |
734-
| `disableAnalytics`, `enableHooks` | passed through unchanged |
759+
| Definition | `ICommand` |
760+
| --------------------------------- | -------------------------------------------------- |
761+
| `options` | `dashedOptions` |
762+
| `run` | `execute`, wrapped in an injection context |
763+
| `arguments`, `canExecute` | `canExecute`: policy enforced, then the refinement |
764+
| `setup` | — run inside `canExecute`/`execute`, memoised |
765+
| `postRun` | `postCommandAction`, with `run`'s return value |
766+
| `allowUnknownOptions` | `skipOptionsValidation` |
767+
|| `allowedParameters`, always `[]` |
768+
| `disableAnalytics`, `enableHooks` | passed through unchanged |
735769

736770
The compiled command always exposes `canExecute`, because `CommandsService`
737771
stops consulting `allowedParameters` as soon as a command has one — the adapter

lib/commands/add-platform.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
canExecuteCommandBase,
33
injectPlatformCommandServices,
4-
IPlatformCommandServices,
54
} from "./command-base";
65
import { IPlatformCommandHelper } from "../declarations";
76
import { IErrors } from "../common/declarations";
@@ -21,12 +20,7 @@ export type AddPlatformCommandContext = CommandContext<
2120
typeof addPlatformCommandOptions
2221
>;
2322

24-
export interface IAddPlatformCommandServices extends IPlatformCommandServices {
25-
$errors: IErrors;
26-
$platformCommandHelper: IPlatformCommandHelper;
27-
}
28-
29-
export function setupAddPlatformCommand(): IAddPlatformCommandServices {
23+
export function setupAddPlatformCommand() {
3024
const services = {
3125
...injectPlatformCommandServices(),
3226
$errors: inject<IErrors>("errors"),
@@ -39,6 +33,10 @@ export function setupAddPlatformCommand(): IAddPlatformCommandServices {
3933
return services;
4034
}
4135

36+
export type IAddPlatformCommandServices = ReturnType<
37+
typeof setupAddPlatformCommand
38+
>;
39+
4240
export async function canExecuteAddPlatformCommand(
4341
context: AddPlatformCommandContext,
4442
services: IAddPlatformCommandServices,

lib/commands/apple-login.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,7 @@ import { IApplePortalSessionService } from "../services/apple-portal/definitions
55

66
export type AppleLoginCommandContext = CommandContext;
77

8-
export interface IAppleLoginCommandServices {
9-
$applePortalSessionService: IApplePortalSessionService;
10-
$errors: IErrors;
11-
$logger: ILogger;
12-
$prompter: IPrompter;
13-
}
14-
15-
export function setupAppleLoginCommand(): IAppleLoginCommandServices {
8+
export function setupAppleLoginCommand() {
169
return {
1710
$applePortalSessionService: inject<IApplePortalSessionService>(
1811
"applePortalSessionService",
@@ -23,6 +16,10 @@ export function setupAppleLoginCommand(): IAppleLoginCommandServices {
2316
};
2417
}
2518

19+
export type IAppleLoginCommandServices = ReturnType<
20+
typeof setupAppleLoginCommand
21+
>;
22+
2623
export async function runAppleLoginCommand(
2724
context: AppleLoginCommandContext,
2825
services: IAppleLoginCommandServices,

lib/commands/appstore-list.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,7 @@ export type ListiOSAppsCommandContext = CommandContext<
2222
typeof listiOSAppsCommandOptions
2323
>;
2424

25-
export interface IListiOSAppsCommandServices {
26-
$applePortalApplicationService: IApplePortalApplicationService;
27-
$applePortalSessionService: IApplePortalSessionService;
28-
$devicePlatformsConstants: Mobile.IDevicePlatformsConstants;
29-
$errors: IErrors;
30-
$logger: ILogger;
31-
$platformValidationService: IPlatformValidationService;
32-
$projectData: IProjectData;
33-
$prompter: IPrompter;
34-
}
35-
36-
export function setupListiOSAppsCommand(): IListiOSAppsCommandServices {
25+
export function setupListiOSAppsCommand() {
3726
const services = {
3827
$applePortalApplicationService: inject<IApplePortalApplicationService>(
3928
"applePortalApplicationService",
@@ -57,6 +46,10 @@ export function setupListiOSAppsCommand(): IListiOSAppsCommandServices {
5746
return services;
5847
}
5948

49+
export type IListiOSAppsCommandServices = ReturnType<
50+
typeof setupListiOSAppsCommand
51+
>;
52+
6053
export async function runListiOSAppsCommand(
6154
context: ListiOSAppsCommandContext,
6255
services: IListiOSAppsCommandServices,

lib/commands/appstore-upload.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,7 @@ export type PublishIOSCommandContext = CommandContext<
3232
typeof publishIOSCommandOptions
3333
>;
3434

35-
export interface IPublishIOSCommandServices {
36-
$applePortalSessionService: IApplePortalSessionService;
37-
$buildController: BuildController;
38-
$devicePlatformsConstants: Mobile.IDevicePlatformsConstants;
39-
$errors: IErrors;
40-
$hostInfo: IHostInfo;
41-
$itmsTransporterService: IITMSTransporterService;
42-
$logger: ILogger;
43-
$options: IOptions;
44-
$platformValidationService: IPlatformValidationService;
45-
$projectData: IProjectData;
46-
$prompter: IPrompter;
47-
}
48-
49-
export function setupPublishIOSCommand(): IPublishIOSCommandServices {
35+
export function setupPublishIOSCommand() {
5036
const services = {
5137
$applePortalSessionService: inject<IApplePortalSessionService>(
5238
"applePortalSessionService",
@@ -73,6 +59,10 @@ export function setupPublishIOSCommand(): IPublishIOSCommandServices {
7359
return services;
7460
}
7561

62+
export type IPublishIOSCommandServices = ReturnType<
63+
typeof setupPublishIOSCommand
64+
>;
65+
7666
export function canExecutePublishIOSCommand(
7767
context: PublishIOSCommandContext,
7868
services: IPublishIOSCommandServices,

lib/commands/build.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
import {
66
canExecuteCommandBase,
77
injectPlatformCommandServices,
8-
IPlatformCommandServices,
98
validatePlatformOptions,
109
} from "./command-base";
1110
import { hasValidAndroidSigning } from "../common/helpers";
@@ -40,17 +39,6 @@ const buildCommandOptions = {
4039
keyStoreAliasPassword: stringOption(),
4140
} satisfies CommandOptionsSchema;
4241

43-
interface IBuildCommandServices extends IPlatformCommandServices {
44-
platform: string;
45-
isAndroid: boolean;
46-
$errors: IErrors;
47-
$logger: ILogger;
48-
$buildController: IBuildController;
49-
$buildDataService: IBuildDataService;
50-
$migrateController: IMigrateController;
51-
$androidBundleValidatorHelper: IAndroidBundleValidatorHelper;
52-
}
53-
5442
const defineBuildCommand = <const TName extends CommandName>(
5543
name: TName,
5644
buildPlatform: BuildPlatform,
@@ -60,7 +48,7 @@ const defineBuildCommand = <const TName extends CommandName>(
6048
description: "Builds the project for the selected target platform.",
6149
options: buildCommandOptions,
6250
arguments: "none",
63-
setup(): IBuildCommandServices {
51+
setup() {
6452
const devicePlatformsConstants = inject<Mobile.IDevicePlatformsConstants>(
6553
"devicePlatformsConstants",
6654
);

lib/commands/clean.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,7 @@ const cleanCommandOptions = {
8888

8989
export type CleanCommandContext = CommandContext<typeof cleanCommandOptions>;
9090

91-
export interface ICleanCommandServices {
92-
$childProcess: IChildProcess;
93-
$logger: ILogger;
94-
$projectCleanupService: IProjectCleanupService;
95-
$projectConfigService: IProjectConfigService;
96-
$projectData: IProjectData;
97-
$projectService: IProjectService;
98-
$prompter: IPrompter;
99-
$staticConfig: IStaticConfig;
100-
$terminalSpinnerService: ITerminalSpinnerService;
101-
}
102-
103-
export function setupCleanCommand(): ICleanCommandServices {
91+
export function setupCleanCommand() {
10492
return {
10593
$childProcess: inject<IChildProcess>("childProcess"),
10694
$logger: inject<ILogger>("logger"),
@@ -120,6 +108,8 @@ export function setupCleanCommand(): ICleanCommandServices {
120108
};
121109
}
122110

111+
export type ICleanCommandServices = ReturnType<typeof setupCleanCommand>;
112+
123113
async function getNSProjectPathsInDirectory(
124114
services: ICleanCommandServices,
125115
dir = process.cwd(),

lib/commands/command-base.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,8 @@ import {
99
import { ArgumentSpec } from "../common/define-command";
1010
import { inject, Injector } from "../common/di";
1111

12-
/**
13-
* What the platform-validation helpers below need. A command definition's
14-
* `setup` returns this shape (see `injectPlatformCommandServices`), so its
15-
* result can be handed straight to them.
16-
*/
17-
export interface IPlatformCommandServices {
18-
$options: IOptions;
19-
$platformsDataService: IPlatformsDataService;
20-
$platformValidationService: IPlatformValidationService;
21-
$projectData: IProjectData;
22-
}
23-
2412
/** Callable from `setup` and from `canExecute` before their first `await`. */
25-
export function injectPlatformCommandServices(): IPlatformCommandServices {
13+
export function injectPlatformCommandServices() {
2614
return {
2715
$options: inject<IOptions>("options"),
2816
$platformsDataService: inject<IPlatformsDataService>(
@@ -35,6 +23,15 @@ export function injectPlatformCommandServices(): IPlatformCommandServices {
3523
};
3624
}
3725

26+
/**
27+
* What the platform-validation helpers below need. A command definition's
28+
* `setup` returns this shape (see `injectPlatformCommandServices`), so its
29+
* result can be handed straight to them.
30+
*/
31+
export type IPlatformCommandServices = ReturnType<
32+
typeof injectPlatformCommandServices
33+
>;
34+
3835
/**
3936
* The declarative form of `$platformCommandParameter`. Initializing the
4037
* project data is what makes the platform check possible, so it stays part of

lib/commands/config.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@ import { CommandContext, defineCommand } from "../common/define-command";
55
import { inject } from "../common/di";
66
import { color } from "../color";
77

8-
export interface IConfigCommandServices {
9-
$projectConfigService: IProjectConfigService;
10-
$logger: ILogger;
11-
$errors: IErrors;
12-
}
13-
14-
export function injectConfigCommandServices(): IConfigCommandServices {
8+
export function injectConfigCommandServices() {
159
return {
1610
$projectConfigService: inject<IProjectConfigService>(
1711
"projectConfigService",
@@ -21,6 +15,10 @@ export function injectConfigCommandServices(): IConfigCommandServices {
2115
};
2216
}
2317

18+
export type IConfigCommandServices = ReturnType<
19+
typeof injectConfigCommandServices
20+
>;
21+
2422
function getValueString(value: SupportedConfigValues, depth = 0): string {
2523
const indent = () => " ".repeat(depth);
2624
if (typeof value === "object") {

lib/commands/create-project.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,18 @@ export type CreateProjectCommandContext = CommandContext<
5353
typeof createProjectCommandOptions
5454
>;
5555

56-
export interface ICreateProjectCommandServices {
57-
$projectService: IProjectService;
58-
$logger: ILogger;
59-
$prompter: IPrompter;
60-
}
61-
62-
export function setupCreateProjectCommand(): ICreateProjectCommandServices {
56+
export function setupCreateProjectCommand() {
6357
return {
6458
$projectService: inject<IProjectService>("projectService"),
6559
$logger: inject<ILogger>("logger"),
6660
$prompter: inject<IPrompter>("prompter"),
6761
};
6862
}
6963

64+
export type ICreateProjectCommandServices = ReturnType<
65+
typeof setupCreateProjectCommand
66+
>;
67+
7068
interface ITemplateChoice {
7169
key?: string;
7270
value: string;

0 commit comments

Comments
 (0)