Skip to content

Commit 3d8b118

Browse files
committed
feat(contracts): ship nativescript/contracts with a first tranche
DoctorService and ProjectNameService become @contract abstract classes; their impls are renamed *Impl (externally invisible - outside resolution is by string name or token, never class identity). The subpath resolves through contracts/package.json rather than an exports map, so existing deep requires keep working, and the entry point is side-effect-free so a duplicated CLI copy in an extensions tree never boots a second runtime.
1 parent f15d250 commit 3d8b118

10 files changed

Lines changed: 237 additions & 116 deletions

File tree

contracts/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"main": "../lib/contracts/index.js",
3+
"types": "../lib/contracts/index.d.ts"
4+
}

lib/contracts/doctor-service.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Contract } from "../common/di/contract";
2+
import type { ISpawnResult } from "../common/declarations";
3+
import type { IOptions } from "../declarations";
4+
5+
/**
6+
* Verifies the host OS configuration — the code behind `ns doctor`.
7+
*/
8+
@Contract({ name: "doctorService" })
9+
export abstract class DoctorService {
10+
/**
11+
* Verifies the host OS configuration and prints warnings to the users.
12+
* @param configOptions Defines if the result should be tracked by Analytics.
13+
*/
14+
abstract printWarnings(configOptions?: {
15+
trackResult?: boolean;
16+
projectDir?: string;
17+
runtimeVersion?: string;
18+
options?: IOptions;
19+
forceCheck?: boolean;
20+
platform?: string;
21+
}): Promise<void>;
22+
23+
/** Runs the setup script on the host machine. */
24+
abstract runSetupScript(): Promise<ISpawnResult>;
25+
26+
/**
27+
* Checks whether the environment is properly configured for local builds.
28+
*/
29+
abstract canExecuteLocalBuild(configuration?: {
30+
platform?: string;
31+
projectDir?: string;
32+
runtimeVersion?: string;
33+
forceCheck?: boolean;
34+
}): Promise<boolean>;
35+
36+
/** Checks and notifies users of deprecated short imports in their app. */
37+
abstract checkForDeprecatedShortImportsInAppDir(projectDir: string): void;
38+
}

lib/contracts/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// The `nativescript/contracts` entry point (resolved via contracts/package.json
2+
// — deliberately no `exports` map, so existing deep requires keep working).
3+
//
4+
// This module must stay side-effect-free: an extension's duplicated CLI copy
5+
// may load it, and it must never boot a second runtime. In particular nothing
6+
// here may import lib/common/yok (whose import creates global.$injector).
7+
8+
export {
9+
Contract,
10+
getContractName,
11+
CONTRACT_NAME,
12+
} from "../common/di/contract";
13+
export type { IContractOptions } from "../common/di/contract";
14+
export { inject, runInInjectionContext } from "../common/di/inject";
15+
export { forwardRef, resolveForwardRef } from "../common/di/forward-ref";
16+
export { Injector } from "../common/di/injector";
17+
export { provide, provideLazy } from "../common/di/providers";
18+
export type {
19+
Provider,
20+
ProviderToken,
21+
Type,
22+
AbstractType,
23+
} from "../common/di/providers";
24+
25+
export { DoctorService } from "./doctor-service";
26+
export { ProjectNameService } from "./project-name-service";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Contract } from "../common/di/contract";
2+
3+
@Contract({ name: "projectNameService" })
4+
export abstract class ProjectNameService {
5+
/**
6+
* Ensures the passed project name is valid; prompts for action otherwise.
7+
* @returns The selected name of the project.
8+
*/
9+
abstract ensureValidName(
10+
projectName: string,
11+
validateOptions?: { force: boolean },
12+
): Promise<string>;
13+
}

lib/services/doctor-service.ts

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,19 @@ import {
2424
import { IJsonFileSettingsService } from "../common/definitions/json-file-settings-service";
2525
import { IInjector } from "../common/definitions/yok";
2626
import { injector } from "../common/yok";
27+
import { DoctorService as DoctorServiceContract } from "../contracts/doctor-service";
2728
import { color } from "../color";
2829
import { ITerminalSpinnerService } from "../definitions/terminal-spinner-service";
2930

30-
export class DoctorService implements IDoctorService {
31+
export class DoctorServiceImpl
32+
implements IDoctorService, DoctorServiceContract
33+
{
3134
private static DarwinSetupScriptLocation = path.join(
3235
__dirname,
3336
"..",
3437
"..",
3538
"setup",
36-
"mac-startup-shell-script.sh"
39+
"mac-startup-shell-script.sh",
3740
);
3841
private static WindowsSetupScriptExecutable = "powershell.exe";
3942
private static WindowsSetupScriptArguments = [
@@ -50,15 +53,15 @@ export class DoctorService implements IDoctorService {
5053
private get jsonFileSettingsPath(): string {
5154
return path.join(
5255
this.$settingsService.getProfileDir(),
53-
"doctor-cache.json"
56+
"doctor-cache.json",
5457
);
5558
}
5659

5760
@cache()
5861
private get $jsonFileSettingsService(): IJsonFileSettingsService {
5962
return this.$injector.resolve<IJsonFileSettingsService>(
6063
"jsonFileSettingsService",
61-
{ jsonFileSettingsPath: this.jsonFileSettingsPath }
64+
{ jsonFileSettingsPath: this.jsonFileSettingsPath },
6265
);
6366
}
6467

@@ -72,7 +75,7 @@ export class DoctorService implements IDoctorService {
7275
private $fs: IFileSystem,
7376
private $terminalSpinnerService: ITerminalSpinnerService,
7477
private $versionsService: IVersionsService,
75-
private $settingsService: ISettingsService
78+
private $settingsService: ISettingsService,
7679
) {}
7780

7881
public async printWarnings(configOptions?: {
@@ -96,17 +99,17 @@ export class DoctorService implements IDoctorService {
9699
text: `Getting environment information ${EOL}`,
97100
},
98101
() =>
99-
this.getInfos({ forceCheck: configOptions.forceCheck }, getInfosData)
102+
this.getInfos({ forceCheck: configOptions.forceCheck }, getInfosData),
100103
);
101104

102105
const warnings = infos.filter(
103-
(info) => info.type === constants.WARNING_TYPE_NAME
106+
(info) => info.type === constants.WARNING_TYPE_NAME,
104107
);
105108
const hasWarnings = warnings.length > 0;
106109

107110
const hasAndroidWarnings =
108111
warnings.filter((warning) =>
109-
_.includes(warning.platforms, constants.ANDROID_PLATFORM_NAME)
112+
_.includes(warning.platforms, constants.ANDROID_PLATFORM_NAME),
110113
).length > 0;
111114
if (hasAndroidWarnings) {
112115
this.printPackageManagerTip();
@@ -126,18 +129,18 @@ export class DoctorService implements IDoctorService {
126129
this.$logger.info(color.bold("No issues were detected."));
127130
await this.$jsonFileSettingsService.saveSetting(
128131
this.getKeyForConfiguration(getInfosData),
129-
infos
132+
infos,
130133
);
131134
this.printInfosCore(infos);
132135
}
133136

134137
try {
135138
await this.$versionsService.printVersionsInformation(
136-
configOptions.platform
139+
configOptions.platform,
137140
);
138141
} catch (err) {
139142
this.$logger.error(
140-
"Cannot get the latest versions information from npm. Please try again later."
143+
"Cannot get the latest versions information from npm. Please try again later.",
141144
);
142145
}
143146

@@ -146,7 +149,7 @@ export class DoctorService implements IDoctorService {
146149

147150
await this.$injector
148151
.resolve<IPlatformEnvironmentRequirements>(
149-
"platformEnvironmentRequirements"
152+
"platformEnvironmentRequirements",
150153
)
151154
.checkEnvironmentRequirements({
152155
platform: configOptions.platform,
@@ -171,20 +174,20 @@ export class DoctorService implements IDoctorService {
171174
}
172175

173176
this.$logger.info(
174-
"Running the setup script to try and automatically configure your environment."
177+
"Running the setup script to try and automatically configure your environment.",
175178
);
176179

177180
if (this.$hostInfo.isDarwin) {
178181
await this.runSetupScriptCore(
179-
DoctorService.DarwinSetupScriptLocation,
180-
[]
182+
DoctorServiceImpl.DarwinSetupScriptLocation,
183+
[],
181184
);
182185
}
183186

184187
if (this.$hostInfo.isWindows) {
185188
await this.runSetupScriptCore(
186-
DoctorService.WindowsSetupScriptExecutable,
187-
DoctorService.WindowsSetupScriptArguments
189+
DoctorServiceImpl.WindowsSetupScriptExecutable,
190+
DoctorServiceImpl.WindowsSetupScriptArguments,
188191
);
189192
}
190193

@@ -211,7 +214,7 @@ export class DoctorService implements IDoctorService {
211214
};
212215
const infos = await this.getInfos(
213216
{ forceCheck: configuration && configuration.forceCheck },
214-
sysInfoConfig
217+
sysInfoConfig,
215218
);
216219
const warnings = this.filterInfosByType(infos, constants.WARNING_TYPE_NAME);
217220
const hasWarnings = warnings.length > 0;
@@ -228,7 +231,7 @@ export class DoctorService implements IDoctorService {
228231
infos.map((info) => this.$logger.trace(info.message));
229232
await this.$jsonFileSettingsService.saveSetting(
230233
this.getKeyForConfiguration(sysInfoConfig),
231-
infos
234+
infos,
232235
);
233236
}
234237

@@ -243,35 +246,34 @@ export class DoctorService implements IDoctorService {
243246
public checkForDeprecatedShortImportsInAppDir(projectDir: string): void {
244247
if (projectDir) {
245248
try {
246-
const files = this.$projectDataService.getAppExecutableFiles(
247-
projectDir
248-
);
249+
const files =
250+
this.$projectDataService.getAppExecutableFiles(projectDir);
249251
const shortImports = this.getDeprecatedShortImportsInFiles(
250252
files,
251-
projectDir
253+
projectDir,
252254
);
253255
if (shortImports.length) {
254256
this.$logger.printMarkdown(
255-
"Detected short imports in your application. Please note that `short imports are deprecated` since NativeScript 5.2.0. More information can be found in this blogpost https://www.nativescript.org/blog/say-goodbye-to-short-imports-in-nativescript"
257+
"Detected short imports in your application. Please note that `short imports are deprecated` since NativeScript 5.2.0. More information can be found in this blogpost https://www.nativescript.org/blog/say-goodbye-to-short-imports-in-nativescript",
256258
);
257259
shortImports.forEach((shortImport) => {
258260
this.$logger.printMarkdown(
259-
`In file \`${shortImport.file}\` line \`${shortImport.line}\` is short import. Add \`tns-core-modules/\` in front of the required/imported module.`
261+
`In file \`${shortImport.file}\` line \`${shortImport.line}\` is short import. Add \`tns-core-modules/\` in front of the required/imported module.`,
260262
);
261263
});
262264
}
263265
} catch (err) {
264266
this.$logger.trace(
265267
`Unable to validate if project has short imports. Error is`,
266-
err
268+
err,
267269
);
268270
}
269271
}
270272
}
271273

272274
protected getDeprecatedShortImportsInFiles(
273275
files: string[],
274-
projectDir: string
276+
projectDir: string,
275277
): { file: string; line: string }[] {
276278
const shortImportRegExp = this.getShortImportRegExp(projectDir);
277279
const shortImports: { file: string; line: string }[] = [];
@@ -280,13 +282,13 @@ export class DoctorService implements IDoctorService {
280282
const fileContent = this.$fs.readText(file);
281283
const strippedComments = helpers.stripComments(fileContent);
282284
const linesToCheck = _.flatten(
283-
strippedComments.split(/\r?\n/).map((line) => line.split(";"))
285+
strippedComments.split(/\r?\n/).map((line) => line.split(";")),
284286
);
285287

286288
const linesWithRequireStatements = linesToCheck.filter(
287289
(line) =>
288290
/\btns-core-modules\b/.exec(line) === null &&
289-
(/\bimport\b/.exec(line) || /\brequire\b/.exec(line))
291+
(/\bimport\b/.exec(line) || /\brequire\b/.exec(line)),
290292
);
291293

292294
for (const line of linesWithRequireStatements) {
@@ -305,14 +307,14 @@ export class DoctorService implements IDoctorService {
305307
const pathToTnsCoreModules = path.join(
306308
projectDir,
307309
NODE_MODULES_FOLDER_NAME,
308-
TNS_CORE_MODULES_NAME
310+
TNS_CORE_MODULES_NAME,
309311
);
310312
const coreModulesSubDirs = this.$fs
311313
.readDirectory(pathToTnsCoreModules)
312314
.filter((entry) =>
313315
this.$fs
314316
.getFsStats(path.join(pathToTnsCoreModules, entry))
315-
.isDirectory()
317+
.isDirectory(),
316318
);
317319

318320
const stringRegularExpressionsPerDir = coreModulesSubDirs.map((c) => {
@@ -332,26 +334,26 @@ export class DoctorService implements IDoctorService {
332334

333335
private async runSetupScriptCore(
334336
executablePath: string,
335-
setupScriptArgs: string[]
337+
setupScriptArgs: string[],
336338
): Promise<ISpawnResult> {
337339
return this.$childProcess.spawnFromEvent(
338340
executablePath,
339341
setupScriptArgs,
340342
"close",
341-
{ stdio: "inherit" }
343+
{ stdio: "inherit" },
342344
);
343345
}
344346

345347
private printPackageManagerTip() {
346348
if (this.$hostInfo.isWindows) {
347349
this.$logger.info(
348350
"TIP: To avoid setting up the necessary environment variables, you can use the chocolatey package manager to install the Android SDK and its dependencies." +
349-
EOL
351+
EOL,
350352
);
351353
} else if (this.$hostInfo.isDarwin) {
352354
this.$logger.info(
353355
"TIP: To avoid setting up the necessary environment variables, you can use the Homebrew package manager to install the Android SDK and its dependencies." +
354-
EOL
356+
EOL,
355357
);
356358
}
357359
}
@@ -390,20 +392,20 @@ export class DoctorService implements IDoctorService {
390392

391393
private filterInfosByType(
392394
infos: NativeScriptDoctor.IInfo[],
393-
type: string
395+
type: string,
394396
): NativeScriptDoctor.IInfo[] {
395397
return infos.filter((info) => info.type === type);
396398
}
397399

398400
private getKeyForConfiguration(
399-
sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig
401+
sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig,
400402
): string {
401403
const nativeScriptData =
402404
sysInfoConfig &&
403405
sysInfoConfig.projectDir &&
404406
JSON.stringify(
405407
this.$fs.readJson(path.join(sysInfoConfig.projectDir, "package.json"))
406-
.nativescript
408+
.nativescript,
407409
);
408410
const delimiter = "__";
409411
const key = [
@@ -426,26 +428,26 @@ export class DoctorService implements IDoctorService {
426428

427429
private async getInfos(
428430
cacheConfig: { forceCheck: boolean },
429-
sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig
431+
sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig,
430432
): Promise<NativeScriptDoctor.IInfo[]> {
431433
const key = this.getKeyForConfiguration(sysInfoConfig);
432434

433435
const infosFromCache = cacheConfig.forceCheck
434436
? null
435437
: await this.$jsonFileSettingsService.getSettingValue<
436438
NativeScriptDoctor.IInfo[]
437-
>(key);
439+
>(key);
438440

439441
this.$logger.trace(
440442
`getInfos cacheConfig options:`,
441443
cacheConfig,
442444
" current info from cache: ",
443-
infosFromCache
445+
infosFromCache,
444446
);
445447

446448
const infos = infosFromCache || (await doctor.getInfos(sysInfoConfig));
447449

448450
return infos;
449451
}
450452
}
451-
injector.register("doctorService", DoctorService);
453+
injector.register("doctorService", DoctorServiceImpl);

0 commit comments

Comments
 (0)