Skip to content

Commit 72fbb34

Browse files
committed
refactor(commands): declare the project with provideProject()
Every command that needs a project called initializeProjectData() on the process-wide project object, from a constructor, a setup or a handler, and a command that forgot - preview - worked only when another service had happened to initialize it first. provideProject() contributes a COMMAND_PRECONDITIONS entry that resolves the project the command line names before the command's setup and arguments policy, failing with the usual "no project found" error when there is none. The 42 commands that need a project declare it in their providers and inject the ProjectData contract; the initializer call is gone from all of them, along with the constructors and setups that only existed to make it. The four commands for which a project is optional - clean and the device file commands - resolve it behind their own check. The gradle build-args service resolves the project of the build's directory through the project-data service instead of initializing the shared object.
1 parent 375bde5 commit 72fbb34

41 files changed

Lines changed: 381 additions & 266 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 & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -446,16 +446,53 @@ A handler resolves what it needs itself, at the top of its own body:
446446
export default defineCommand({
447447
name: "widget|add",
448448
arguments: "any",
449+
providers: [provideProject()],
449450
async run(ctx) {
450451
const widgets = inject(WidgetService);
451452
const projectData = inject(ProjectData);
452453

453-
projectData.initializeProjectData();
454-
await widgets.add(ctx.args);
454+
await widgets.add(ctx.args, projectData);
455455
},
456456
});
457457
```
458458

459+
### Declaring what the command needs: `providers` and preconditions
460+
461+
A definition may carry `providers`, added to each invocation's own injector
462+
next to the context, so a factory or class among them can inject the
463+
invocation and is built once per invocation. One token in that list is
464+
special: `COMMAND_PRECONDITIONS` is a multi token, and every
465+
`{ provide: COMMAND_PRECONDITIONS, multi: true, useValue: check }` contributes
466+
a **precondition** — a check on the environment the command runs in, as
467+
opposed to `canExecute`, which judges the arguments. Preconditions run when
468+
the invocation opens, in declaration order, before `setup` and before the
469+
arguments policy, inside the injection context, and a throw fails the
470+
invocation. That fixed order is the point: being outside a project is what a
471+
bad invocation reports first.
472+
473+
The precondition every project command declares comes from a helper:
474+
475+
```ts
476+
import { provideProject } from "../command-base";
477+
478+
export default defineCommand({
479+
name: "platform|clean",
480+
providers: [provideProject()],
481+
run(ctx) {
482+
const projectData = inject(ProjectData); // the project the command line names
483+
},
484+
});
485+
```
486+
487+
`provideProject()` resolves the project from `--path` or the working
488+
directory and fails the invocation with the usual "no project found" error
489+
when there is none. A command that does not declare it — `doctor`, `create`,
490+
the `device` family — pays nothing, and a command that needs the project only
491+
when it is there, like `clean`, resolves it itself behind its own check. Never
492+
call `initializeProjectData()` from a command; declare the provider. A plugin
493+
adds its own preconditions the same way, with its own helper returning a
494+
multi provider for the token.
495+
459496
The injection context is synchronous, so the `inject()` calls belong **above
460497
the first `await`** — see [Injection, and the first
461498
`await`](#injection-and-the-first-await). Resolve everything the handler needs
@@ -552,16 +589,12 @@ export class PlatformCleanCommand extends Command({
552589
description: "Removes and adds again the selected platform.",
553590
options: { frameworkPath: stringOption() },
554591
arguments: "any",
592+
providers: [provideProject()],
555593
}) {
556594
private $platformCommandHelper = inject<IPlatformCommandHelper>(
557595
"platformCommandHelper",
558596
);
559-
private $projectData = inject<IProjectData>("projectData");
560-
561-
constructor() {
562-
super();
563-
this.$projectData.initializeProjectData();
564-
}
597+
private $projectData = inject(ProjectData);
565598

566599
public async run(): Promise<void> {
567600
await this.$platformCommandHelper.cleanPlatforms(
@@ -625,16 +658,14 @@ across invocations, and resolves nothing outside a running one.
625658
read as `this.$x`:
626659

627660
```ts
628-
export class PlatformAddCommand extends Command({ name: "platform|add" }) {
629-
private $projectData = inject<IProjectData>("projectData");
661+
export class PlatformAddCommand extends Command({
662+
name: "platform|add",
663+
providers: [provideProject()],
664+
}) {
665+
private $projectData = inject(ProjectData);
630666
private $platformHelper = inject<IPlatformCommandHelper>(
631667
"platformCommandHelper",
632668
);
633-
634-
constructor() {
635-
super();
636-
this.$projectData.initializeProjectData();
637-
}
638669
// ...
639670
}
640671
```
@@ -700,8 +731,8 @@ the registry. It claims every name the definition declares, through the
700731
is built by a factory on first resolution and cached.
701732

702733
Pass providers as the second argument to add them to each invocation's child
703-
injector, the one `ctx.injector` names — how a definition is parameterized per
704-
registration:
734+
injector, the one `ctx.injector` names, next to the definition's own
735+
`providers` — how a definition is parameterized per registration:
705736

706737
```ts
707738
for (const [name, platform] of buildCommandPlatforms) {

‎lib/commands/add-platform.ts‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { canExecuteCommandBase } from "./command-base";
1+
import { canExecuteCommandBase, provideProject } from "./command-base";
22
import {
33
IPlatformCommandHelper,
44
IPlatformValidationService,
55
} from "../declarations";
6-
import { IProjectData } from "../definitions/project";
76
import {
87
Command,
98
CommandOptionsSchema,
109
stringOption,
1110
} from "../common/define-command";
1211
import { inject } from "../common/di";
12+
import { ProjectData } from "../contracts/project-data";
1313

1414
const addPlatformCommandOptions = {
1515
frameworkPath: stringOption(),
@@ -21,19 +21,15 @@ export class AddPlatformCommand extends Command({
2121
"Configures the current project to target the selected platform.",
2222
options: addPlatformCommandOptions,
2323
arguments: "any",
24+
providers: [provideProject()],
2425
}) {
2526
private $platformCommandHelper = inject<IPlatformCommandHelper>(
2627
"platformCommandHelper",
2728
);
2829
private $platformValidationService = inject<IPlatformValidationService>(
2930
"platformValidationService",
3031
);
31-
private $projectData = inject<IProjectData>("projectData");
32-
33-
constructor() {
34-
super();
35-
this.$projectData.initializeProjectData();
36-
}
32+
private $projectData = inject(ProjectData);
3733

3834
public async canExecute(): Promise<boolean> {
3935
const args = this.args;

‎lib/commands/appstore-list.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import {
66
import { inject } from "../common/di";
77
import { createTable } from "../common/helpers";
88
import { IPlatformValidationService } from "../declarations";
9-
import { IProjectData } from "../definitions/project";
109
import {
1110
IApplePortalApplicationService,
1211
IApplePortalSessionService,
1312
} from "../services/apple-portal/definitions";
13+
import { ProjectData } from "../contracts/project-data";
14+
import { provideProject } from "./command-base";
1415

1516
const listiOSAppsCommandOptions = {
1617
appleSessionBase64: stringOption(),
@@ -21,6 +22,7 @@ export class ListiOSAppsCommand extends Command({
2122
description: "Lists the applications in App Store Connect.",
2223
options: listiOSAppsCommandOptions,
2324
arguments: [{ name: "appleId" }, { name: "password" }],
25+
providers: [provideProject()],
2426
}) {
2527
private $applePortalApplicationService =
2628
inject<IApplePortalApplicationService>("applePortalApplicationService");
@@ -34,14 +36,9 @@ export class ListiOSAppsCommand extends Command({
3436
private $platformValidationService = inject<IPlatformValidationService>(
3537
"platformValidationService",
3638
);
37-
private $projectData = inject<IProjectData>("projectData");
39+
private $projectData = inject(ProjectData);
3840
private $prompter = inject<IPrompter>("prompter");
3941

40-
constructor() {
41-
super();
42-
this.$projectData.initializeProjectData();
43-
}
44-
4542
public async run(): Promise<void> {
4643
if (
4744
!this.$platformValidationService.isPlatformSupportedForOS(

‎lib/commands/appstore-upload.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
IOptions,
1616
IPlatformValidationService,
1717
} from "../declarations";
18-
import { IProjectData } from "../definitions/project";
1918
import { IApplePortalSessionService } from "../services/apple-portal/definitions";
19+
import { ProjectData } from "../contracts/project-data";
20+
import { provideProject } from "./command-base";
2021

2122
const publishIOSCommandOptions = {
2223
appleApplicationSpecificPassword: stringOption(),
@@ -33,6 +34,7 @@ export class PublishIOSCommand extends Command({
3334
options: publishIOSCommandOptions,
3435
// Arguments have never been rejected here, only ignored past the third.
3536
arguments: "any",
37+
providers: [provideProject()],
3638
}) {
3739
private $applePortalSessionService = inject<IApplePortalSessionService>(
3840
"applePortalSessionService",
@@ -50,14 +52,9 @@ export class PublishIOSCommand extends Command({
5052
private $platformValidationService = inject<IPlatformValidationService>(
5153
"platformValidationService",
5254
);
53-
private $projectData = inject<IProjectData>("projectData");
55+
private $projectData = inject(ProjectData);
5456
private $prompter = inject<IPrompter>("prompter");
5557

56-
constructor() {
57-
super();
58-
this.$projectData.initializeProjectData();
59-
}
60-
6158
public canExecute(): boolean {
6259
if (!this.$hostInfo.isDarwin) {
6360
this.context.fail("iOS publishing is only available on macOS.", {

‎lib/commands/build.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import {
66
canExecuteCommandBase,
77
platformSigningOptions,
8+
provideProject,
89
validatePlatformOptions,
910
} from "./command-base";
1011
import { hasValidAndroidSigning } from "../common/helpers";
@@ -15,7 +16,6 @@ import {
1516
} from "../declarations";
1617
import { IBuildController, IBuildDataService } from "../definitions/build";
1718
import { IMigrateController } from "../definitions/migrate";
18-
import { IProjectData } from "../definitions/project";
1919
import {
2020
booleanOption,
2121
CommandName,
@@ -24,6 +24,7 @@ import {
2424
stringOption,
2525
} from "../common/define-command";
2626
import { inject } from "../common/di";
27+
import { ProjectData } from "../contracts/project-data";
2728

2829
/**
2930
* Which `$devicePlatformsConstants` entry a command builds for. The constants
@@ -53,6 +54,7 @@ const defineBuildCommand = <const TName extends CommandName>(
5354
description: "Builds the project for the selected target platform.",
5455
options: buildCommandOptions,
5556
arguments: "none",
57+
providers: [provideProject()],
5658
async canExecute(context): Promise<boolean> {
5759
const $devicePlatformsConstants =
5860
inject<Mobile.IDevicePlatformsConstants>("devicePlatformsConstants");
@@ -61,14 +63,13 @@ const defineBuildCommand = <const TName extends CommandName>(
6163
const $platformValidationService = inject<IPlatformValidationService>(
6264
"platformValidationService",
6365
);
64-
const $projectData = inject<IProjectData>("projectData");
66+
const $projectData = inject(ProjectData);
6567
const platform = $devicePlatformsConstants[buildPlatform];
6668
const isAndroid = $devicePlatformsConstants.isAndroid(platform);
6769
// Only the android build checks the runtime version.
6870
const $androidBundleValidatorHelper = isAndroid
6971
? inject<IAndroidBundleValidatorHelper>("androidBundleValidatorHelper")
7072
: null;
71-
$projectData.initializeProjectData();
7273

7374
if (!context.options.force) {
7475
await $migrateController.validate({
@@ -112,10 +113,9 @@ const defineBuildCommand = <const TName extends CommandName>(
112113
inject<Mobile.IDevicePlatformsConstants>("devicePlatformsConstants");
113114
const $logger = inject<ILogger>("logger");
114115
const $options = inject<IOptions>("options");
115-
const $projectData = inject<IProjectData>("projectData");
116+
const $projectData = inject(ProjectData);
116117
const platform = $devicePlatformsConstants[buildPlatform];
117118
const isAndroid = $devicePlatformsConstants.isAndroid(platform);
118-
$projectData.initializeProjectData();
119119

120120
const buildData = $buildDataService.getBuildData(
121121
$projectData.projectDir,

‎lib/commands/clean.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ import {
1818
IProjectCleanupResult,
1919
IProjectCleanupService,
2020
IProjectConfigService,
21-
IProjectData,
2221
IProjectService,
2322
} from "../definitions/project";
2423
import {
2524
ITerminalSpinner,
2625
ITerminalSpinnerService,
2726
} from "../definitions/terminal-spinner-service";
27+
import { ProjectData } from "../contracts/project-data";
2828

2929
function bytesToHumanReadable(bytes: number): string {
3030
const units = ["B", "KB", "MB", "GB", "TB"];
@@ -323,7 +323,6 @@ export const cleanCommandDefinition = defineCommand({
323323
const $projectConfigService = inject<IProjectConfigService>(
324324
"projectConfigService",
325325
);
326-
const $projectData = inject<IProjectData>("projectData");
327326
const $projectService = inject<IProjectService>("projectService");
328327
const $terminalSpinnerService = inject<ITerminalSpinnerService>(
329328
"terminalSpinnerService",
@@ -340,6 +339,11 @@ export const cleanCommandDefinition = defineCommand({
340339
return cleanMultipleProjects(context, spinner);
341340
}
342341

342+
// The project is optional: outside one the command cleans the projects
343+
// below, so it is resolved behind the check rather than declared.
344+
const $projectData = inject(ProjectData);
345+
$projectData.initializeProjectData();
346+
343347
spinner.start("Cleaning project...\n");
344348

345349
let pathsToClean = [

‎lib/commands/command-base.ts‎

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import {
1111
CommandOptionsSchema,
1212
objectOption,
1313
} from "../common/define-command";
14-
import { Injector } from "../common/di";
14+
import { Injector, inject } from "../common/di";
15+
import type { Provider } from "../common/di/providers";
16+
import { ProjectData } from "../contracts/project-data";
17+
import {
18+
COMMAND_PRECONDITIONS,
19+
CommandPrecondition,
20+
} from "../common/contracts/command-preconditions";
1521

1622
/**
1723
* The CLI-wide signing options `validatePlatformOptions` checks. A command
@@ -31,17 +37,36 @@ type PlatformSigningContext = Pick<
3137
>;
3238

3339
/**
34-
* The declarative form of `$platformCommandParameter`. Initializing the
35-
* project data is what makes the platform check possible, so it stays part of
36-
* validating the argument instead of moving to the command's own handlers,
37-
* which the adapter runs only after argument enforcement.
40+
* Declares that a command runs inside a project: the project the command line
41+
* names, through `--path` or the working directory, is resolved before the
42+
* command's setup and arguments policy, and its absence fails the invocation
43+
* with the "no project found" error. `inject(ProjectData)` then reads it.
44+
*/
45+
export function provideProject(): Provider {
46+
return {
47+
provide: COMMAND_PRECONDITIONS,
48+
multi: true,
49+
useValue: requireProject,
50+
};
51+
}
52+
53+
const requireProject: CommandPrecondition = () => {
54+
const projectData = inject<IProjectData>("projectData");
55+
if (typeof projectData.initializeProjectData === "function") {
56+
projectData.initializeProjectData();
57+
}
58+
};
59+
60+
/**
61+
* The declarative form of `$platformCommandParameter`. The command declares
62+
* `provideProject()`, so resolving the project here is what makes the
63+
* platform check possible before the command's own handlers run.
3864
*/
3965
export function validatePlatformArgument(
4066
targetInjector: Injector,
4167
platform: string,
4268
): void {
43-
const projectData = targetInjector.get<IProjectData>("projectData");
44-
projectData.initializeProjectData();
69+
const projectData = targetInjector.get(ProjectData);
4570
targetInjector
4671
.get<IPlatformValidationService>("platformValidationService")
4772
.validatePlatform(platform, projectData);
@@ -60,7 +85,7 @@ export function validatePlatformOptions(
6085
context: PlatformSigningContext,
6186
platform: string,
6287
): Promise<boolean> {
63-
const $projectData = context.injector.get<IProjectData>("projectData");
88+
const $projectData = context.injector.get(ProjectData);
6489

6590
return context.injector
6691
.get<IPlatformValidationService>("platformValidationService")
@@ -78,7 +103,7 @@ async function validatePlatformBase(
78103
notConfiguredEnvOptions: INotConfiguredEnvOptions,
79104
): Promise<IValidatePlatformOutput> {
80105
const $options = context.injector.get<IOptions>("options");
81-
const $projectData = context.injector.get<IProjectData>("projectData");
106+
const $projectData = context.injector.get(ProjectData);
82107
const platformData = context.injector
83108
.get<IPlatformsDataService>("platformsDataService")
84109
.getPlatformData(platform, $projectData);

0 commit comments

Comments
 (0)