Skip to content

Commit 7079d1a

Browse files
committed
feat(di): add contracts for the core service tranche
Twelve services gain a typed token: Logger, Errors, FileSystem, ChildProcess, HostInfo, HttpClient, TempService, Prompter, ProjectData, ProjectDataService, PackageManager and DevicesService. Resolution is class-first with a fallback to the token's registry name, so each contract is an alias over the existing registration - no implementation or registration changes. The contracts entry point stays side-effect-free: every import but the Contract decorator itself is type-only, so an extension carrying a duplicated CLI copy cannot boot a second runtime by loading it.
1 parent 5fa07dc commit 7079d1a

15 files changed

Lines changed: 1196 additions & 4 deletions

dependency-injection.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,31 @@ import { inject, DoctorService } from "nativescript/contracts";
258258
Available contracts
259259
-------------------
260260

261-
The first tranche, growing as services migrate:
261+
Growing as services migrate. Every token below is a typed alias onto the
262+
registration it names — resolving by token and resolving by the legacy name
263+
return the same instance.
264+
265+
The contract is also the single source of truth for the service's shape: the
266+
ambient interface the CLI has always published extends it (`interface ILogger
267+
extends Logger {}`), so the two cannot drift apart. Add a member to the
268+
contract and every existing caller sees it.
262269

263270
| Token | Legacy name |
264271
|---|---|
272+
| `ChildProcess` | `childProcess` |
273+
| `DevicesService` | `devicesService` |
265274
| `DoctorService` | `doctorService` |
275+
| `Errors` | `errors` |
276+
| `FileSystem` | `fs` |
277+
| `HostInfo` | `hostInfo` |
278+
| `HttpClient` | `httpClient` |
279+
| `Logger` | `logger` |
280+
| `PackageManager` | `packageManager` |
281+
| `ProjectData` | `projectData` |
282+
| `ProjectDataService` | `projectDataService` |
266283
| `ProjectNameService` | `projectNameService` |
284+
| `Prompter` | `prompter` |
285+
| `TempService` | `tempService` |
267286

268287
Related guides
269288
--------------

lib/contracts/child-process.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Contract } from "../common/di/contract";
2+
import type * as child_process from "child_process";
3+
import type {
4+
IExecOptions,
5+
ISpawnFromEventOptions,
6+
ISpawnResult,
7+
} from "../common/declarations";
8+
9+
/**
10+
* Promise-based wrapper around Node's `child_process` module.
11+
*/
12+
@Contract({ name: "childProcess" })
13+
export abstract class ChildProcess {
14+
abstract exec(
15+
command: string,
16+
options?: any,
17+
execOptions?: IExecOptions,
18+
): Promise<any>;
19+
20+
abstract execFile<T>(command: string, args: string[]): Promise<T>;
21+
22+
abstract spawn(
23+
command: string,
24+
args?: string[],
25+
options?: any,
26+
): child_process.ChildProcess;
27+
28+
abstract spawnFromEvent(
29+
command: string,
30+
args: string[],
31+
event: string,
32+
options?: any,
33+
spawnFromEventOptions?: ISpawnFromEventOptions,
34+
): Promise<ISpawnResult>;
35+
36+
abstract trySpawnFromCloseEvent(
37+
command: string,
38+
args: string[],
39+
options?: any,
40+
spawnFromEventOptions?: ISpawnFromEventOptions,
41+
): Promise<ISpawnResult>;
42+
43+
abstract tryExecuteApplication(
44+
command: string,
45+
args: string[],
46+
event: string,
47+
errorMessage: string,
48+
condition?: (childProcess: any) => boolean,
49+
): Promise<any>;
50+
51+
/**
52+
* This is a special case of the child_process.spawn() functionality for spawning Node.js processes.
53+
* In addition to having all the methods in a normal ChildProcess instance, the returned object has a communication channel built-in.
54+
* Note: Unlike the fork() POSIX system call, child_process.fork() does not clone the current process.
55+
* @param {string} modulePath String The module to run in the child
56+
* @param {string[]} args Array List of string arguments You can access them in the child with 'process.argv'.
57+
* @param {string} options Object
58+
* @return {child_process} ChildProcess object.
59+
*/
60+
abstract fork(
61+
modulePath: string,
62+
args?: string[],
63+
options?: {
64+
cwd?: string;
65+
env?: any;
66+
execPath?: string;
67+
execArgv?: string[];
68+
silent?: boolean;
69+
uid?: number;
70+
gid?: number;
71+
},
72+
): any;
73+
}
74+
75+
// The event-emitter surface is merged in rather than inherited: `extends
76+
// EventEmitter` would need a runtime import, and everything reachable from
77+
// lib/contracts must stay side-effect-free.
78+
export interface ChildProcess extends NodeJS.EventEmitter {}

lib/contracts/devices-service.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { Contract } from "../common/di/contract";
2+
import type { IAppInstalledInfo } from "../common/declarations";
3+
4+
/**
5+
* The EventEmitter surface is merged in rather than redeclared as abstract
6+
* members: the contract must stay free of runtime imports, so it cannot extend
7+
* `EventEmitter`, and merging keeps the signatures (including the `this`
8+
* returns) tied to the ambient definition instead of a hand-copied snapshot.
9+
*/
10+
export interface DevicesService extends NodeJS.EventEmitter {}
11+
12+
/**
13+
* Discovers connected devices and emulators and executes actions against them.
14+
*/
15+
@Contract({ name: "devicesService" })
16+
export abstract class DevicesService {
17+
/** The platform the service has been initialized for. */
18+
abstract platform: string;
19+
20+
/** Whether any device matching the current initialization options is attached. */
21+
abstract hasDevices: boolean;
22+
23+
/** The number of devices matching the current initialization options. */
24+
abstract deviceCount: number;
25+
26+
abstract execute<T>(
27+
action: (device: Mobile.IDevice) => Promise<T>,
28+
canExecute?: (dev: Mobile.IDevice) => boolean,
29+
options?: { allowNoDevices?: boolean },
30+
): Promise<Mobile.IDeviceActionResult<T>[]>;
31+
32+
/**
33+
* Initializes DevicesService, so after that device operations could be executed.
34+
* @param {IDevicesServicesInitializationOptions} data Defines the options which will be used for whole devicesService.
35+
* @return {Promise<void>}
36+
*/
37+
abstract initialize(
38+
data?: Mobile.IDevicesServicesInitializationOptions,
39+
): Promise<void>;
40+
41+
/**
42+
* Add an IDeviceDiscovery instance which will from now on report devices. The instance should implement IDeviceDiscovery and raise "deviceFound" and "deviceLost" events.
43+
* @param {IDeviceDiscovery} deviceDiscovery Instance, implementing IDeviceDiscovery and raising raise "deviceFound" and "deviceLost" events.
44+
* @return {void}
45+
*/
46+
abstract addDeviceDiscovery(deviceDiscovery: Mobile.IDeviceDiscovery): void;
47+
48+
abstract getDevices(): Mobile.IDeviceInfo[];
49+
50+
/**
51+
* Gets device instance by specified identifier or number.
52+
* @param {string} deviceOption The specified device identifier or number.
53+
* @returns {Promise<Mobile.IDevice>} Instance of IDevice.
54+
*/
55+
abstract getDevice(deviceOption: string): Promise<Mobile.IDevice>;
56+
57+
abstract getDevicesForPlatform(platform: string): Mobile.IDevice[];
58+
59+
abstract getDeviceInstances(): Mobile.IDevice[];
60+
61+
abstract getDeviceByDeviceOption(): Mobile.IDevice;
62+
63+
abstract isAndroidDevice(device: Mobile.IDevice): boolean;
64+
65+
abstract isiOSDevice(device: Mobile.IDevice): boolean;
66+
67+
abstract isiOSSimulator(device: Mobile.IDevice): boolean;
68+
69+
abstract isOnlyiOSSimultorRunning(): boolean;
70+
71+
abstract isAppInstalledOnDevices(
72+
deviceIdentifiers: string[],
73+
appIdentifier: string,
74+
framework: string,
75+
projectDir: string,
76+
): Promise<IAppInstalledInfo>[];
77+
78+
abstract setLogLevel(logLevel: string, deviceIdentifier?: string): void;
79+
80+
abstract deployOnDevices(
81+
deviceIdentifiers: string[],
82+
packageFile: string,
83+
packageName: string,
84+
framework: string,
85+
projectDir: string,
86+
): Promise<void>[];
87+
88+
abstract getDeviceByIdentifier(identifier: string): Mobile.IDevice;
89+
90+
abstract mapAbstractToTcpPort(
91+
deviceIdentifier: string,
92+
appIdentifier: string,
93+
framework: string,
94+
): Promise<string>;
95+
96+
abstract getDebuggableApps(
97+
deviceIdentifiers: string[],
98+
): Promise<Mobile.IDeviceApplicationInformation[]>[];
99+
100+
abstract getDebuggableViews(
101+
deviceIdentifier: string,
102+
appIdentifier: string,
103+
): Promise<Mobile.IDebugWebViewInfo[]>;
104+
105+
/**
106+
* Returns all applications installed on the specified device.
107+
* @param {string} deviceIdentifer The identifier of the device for which to get installed applications.
108+
* @returns {Promise<string[]>} Array of all application identifiers of the apps installed on device.
109+
*/
110+
abstract getInstalledApplications(
111+
deviceIdentifier: string,
112+
): Promise<string[]>;
113+
114+
/**
115+
* Returns all available iOS and/or Android emulators.
116+
* @param options The options that can be passed to filter the result.
117+
* @returns {Promise<Mobile.IListEmulatorsOutput>} Dictionary with the following format: { ios: { devices: Mobile.IDeviceInfo[], errors: string[] }, android: { devices: Mobile.IDeviceInfo[], errors: string[]}}.
118+
*/
119+
abstract getEmulatorImages(
120+
options?: Mobile.IListEmulatorsOptions,
121+
): Promise<Mobile.IListEmulatorsOutput>;
122+
123+
/**
124+
* Starts an emulator by provided options.
125+
* @param options
126+
* @returns {Promise<string[]>} - Returns array of errors.
127+
*/
128+
abstract startEmulator(
129+
options?: Mobile.IStartEmulatorOptions,
130+
): Promise<string[]>;
131+
132+
/**
133+
* Starts polling for attached devices, raising the deviceFound/deviceLost
134+
* events as the set of attached devices changes. Calling it while a poll
135+
* is already running is a no-op.
136+
* @param {Mobile.IDeviceLookingOptions} deviceInitOpts Options describing which devices to look for and how often to poll.
137+
* @returns {void}
138+
*/
139+
abstract startDeviceDetectionInterval(
140+
deviceInitOpts?: Mobile.IDeviceLookingOptions,
141+
): void;
142+
143+
/**
144+
* Stops the poll started by startDeviceDetectionInterval.
145+
* @returns {void}
146+
*/
147+
abstract stopDeviceDetectionInterval(): void;
148+
149+
/**
150+
* Starts polling for available emulator images, raising the
151+
* emulatorImageFound/emulatorImageLost events as the set changes.
152+
* @param {Mobile.IHasDetectionInterval} opts Options describing how often to poll.
153+
* @returns {void}
154+
*/
155+
abstract startEmulatorDetectionInterval(
156+
opts?: Mobile.IHasDetectionInterval,
157+
): void;
158+
159+
/**
160+
* Stops the poll started by startEmulatorDetectionInterval.
161+
* @returns {void}
162+
*/
163+
abstract stopEmulatorDetectionInterval(): void;
164+
165+
/**
166+
* Returns a single device based on the specified options. If more than one devices are matching,
167+
* prompts the user for a manual choice or returns the first one for non interactive terminals.
168+
*/
169+
abstract pickSingleDevice(
170+
options: Mobile.IPickSingleDeviceOptions,
171+
): Promise<Mobile.IDevice>;
172+
173+
abstract getPlatformsFromDeviceDescriptors(
174+
deviceDescriptors: ILiveSyncDeviceDescriptor[],
175+
): string[];
176+
}

lib/contracts/errors.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Contract } from "../common/di/contract";
2+
import type { IFailOptions } from "../common/declarations";
3+
4+
/**
5+
* Raises CLI failures and wraps command execution so they are reported and
6+
* turned into a process exit code.
7+
*/
8+
@Contract({ name: "errors" })
9+
export abstract class Errors {
10+
abstract fail(formatStr: string, ...args: any[]): never;
11+
abstract fail(opts: IFailOptions, ...args: any[]): never;
12+
13+
/**
14+
* @deprecated use `fail` instead
15+
*/
16+
abstract failWithoutHelp(message: string, ...args: any[]): never;
17+
/**
18+
* @deprecated use `fail` instead
19+
*/
20+
abstract failWithoutHelp(opts: IFailOptions, ...args: any[]): never;
21+
22+
abstract failWithHelp(formatStr: string, ...args: any[]): never;
23+
abstract failWithHelp(opts: IFailOptions, ...args: any[]): never;
24+
25+
abstract beginCommand(
26+
action: () => Promise<boolean>,
27+
printCommandHelp: () => Promise<void>,
28+
): Promise<boolean>;
29+
30+
abstract verifyHeap(message: string): void;
31+
32+
abstract printCallStack: boolean;
33+
}

0 commit comments

Comments
 (0)