diff --git a/__e2e__/__snapshots__/config.test.ts.snap b/__e2e__/__snapshots__/config.test.ts.snap index 01ee539b2..3b19df155 100644 --- a/__e2e__/__snapshots__/config.test.ts.snap +++ b/__e2e__/__snapshots__/config.test.ts.snap @@ -33,6 +33,7 @@ exports[`shows up current config without unnecessary output 1`] = ` } ], "assets": [], + "healthChecks": [], "platforms": { "ios": {}, "android": {} diff --git a/docs/healthChecks.md b/docs/healthChecks.md new file mode 100644 index 000000000..c8e4e359f --- /dev/null +++ b/docs/healthChecks.md @@ -0,0 +1,175 @@ +# Health Check Plugins + +Plugins can be used to extend the health checks that `react-native doctor` runs. This can be used to add additional checks for out of tree platforms, or other checks that are specific to a community module. + +See [`Plugins`](./plugins.md) for information about how plugins work. + + +## How does it work? + +To provide additional health checks, a package needs to have a `react-native.config.js` at the root folder in order to be discovered by the CLI as a plugin. + +```js +module.exports = { + healthChecks: [ + { + label: 'Foo', + healthchecks: [ + label: 'bar-installed', + getDiagnostics: async () => ({ + needsToBeFixed: !isBarInstalled() + }), + runAutomaticFix: async ({loader}) => { + await installBar(); + loader.succeed(); + }, + } + ], +}; +``` + +> Above is an example of a plugin that extends the healthChecks performed by `react-native doctor` to check if `bar` is installed. + +At the startup, React Native CLI reads configuration from all dependencies listed in `package.json` and reduces them into a single configuration. + +At the end, an array of health check categories is concatenated to be checked when `react-native doctor` is run. + + +## HealthCheckCategory interface + +```ts +type HealthCheckCategory = { + label: string; + healthchecks: HealthCheckInterface[]; +}; +``` + +##### `label` + +Name of the category for this health check. This will be used to group health checks in doctor. + +##### `healthChecks` + +Array of health checks to perorm in this category + + +## HealthCheckInterface interface + +```ts +type HealthCheckInterface = { + label: string; + visible?: boolean | void; + isRequired?: boolean; + description?: string; + getDiagnostics: ( + environmentInfo: EnvironmentInfo, + ) => Promise<{ + version?: string; + versions?: [string]; + versionRange?: string; + needsToBeFixed: boolean | string; + }>; + win32AutomaticFix?: RunAutomaticFix; + darwinAutomaticFix?: RunAutomaticFix; + linuxAutomaticFix?: RunAutomaticFix; + runAutomaticFix: RunAutomaticFix; +}; +``` + +##### `label` + +Name of this health check + +##### `visible` + +If set to false, doctor will ignore this health check + +##### `isRequired` + +Is this health check required or optional? + +##### `description` + +Longer description of this health check + + +##### `getDiagnostics` + +Functions which performs the actual check. Simple checks can just return `needsToBeFixed`. Checks which are looking at versions of an installed component (such as the version of node), can also return `version`, `versions` and `versionRange` to provide better information to be displayed in `react-native doctor` when running the check + +##### `win32AutomaticFix` + +This function will be used to try to fix the issue when `react-native doctor` is run on a windows machine. If this is not specified, `runAutomaticFix` will be run instead. + +##### `darwinAutomaticFix` + +This function will be used to try to fix the issue when `react-native doctor` is run on a macOS machine. If this is not specified, `runAutomaticFix` will be run instead. + +##### `linuxAutomaticFix` + +This function will be used to try to fix the issue when `react-native doctor` is run on a linux machine. If this is not specified, `runAutomaticFix` will be run instead. + +##### `runAutomaticFix` + +This function will be used to try to fix the issue when `react-native doctor` is run and no more platform specific automatic fix function was provided. + + +## RunAutomaticFix interface + +```ts +type RunAutomaticFix = (args: { + loader: Ora; + logManualInstallation: ({ + healthcheck, + url, + command, + message, + }: { + healthcheck?: string; + url?: string; + command?: string; + message?: string; + }) => void; + environmentInfo: EnvironmentInfo; +}) => Promise | void; +``` + +##### `loader` + +A reference to a [`ora`](https://www.npmjs.com/package/ora) instance which should be used to report success / failure, and progress of the fix. The fix function should always call either `loader.succeed()` or `loader.fail()` before returning. + +##### `logManualInstallation` + +If an automated fix cannot be performed, this function should be used to provide instructions to the user on how to manually fix the issue. + +##### `environmentInfo` + +Provides information about the current system + + +### Examples of RunAutomaticFix implementations + +A health check that requires the user to manually go download/install something. This check will immediately display a message to notify the user how to fix the issue. + +```ts +async function needToInstallFoo({loader, logManualInstallation}) { + loader.fail(); + + return logManualInstallation({ + healthcheck: 'Foo', + url: 'https:/foo.com/download', + }); +} +``` + +A health check that runs some commands locally which may fix the issue. This check will display a spinner while the exec commands are running. Then once the commands are complete, the spinner will change to a checkmark. + +```ts + +import { exec } from 'promisify-child-process'; +async function fixFoo({loader}) { + await exec(`foo --install`); + await exec(`foo --fix`); + + loader.succeed(); +} diff --git a/docs/plugins.md b/docs/plugins.md index 82d988b59..c59736205 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -27,6 +27,8 @@ At the startup, React Native CLI reads configuration from all dependencies liste At the end, an array of commands concatenated from all plugins is passed on to the CLI to be loaded after built-in commands. +> See [`healthChecks`](./healthChecks.md) for information on how plugins can provide additional health checks for `react-native doctor`. + ## Command interface ```ts @@ -107,6 +109,7 @@ String that describes this particular usage. A command with arguments and options (if applicable) that can be run in order to achieve the desired goal. + ## Migrating from `rnpm` configuration The changes are mostly cosmetic so the migration should be pretty straight-forward. diff --git a/packages/cli-types/package.json b/packages/cli-types/package.json index 2ebec70e9..a1454adf1 100644 --- a/packages/cli-types/package.json +++ b/packages/cli-types/package.json @@ -5,6 +5,9 @@ "publishConfig": { "access": "public" }, + "dependencies": { + "ora": "^3.4.0" + }, "files": [ "build", "!*.map" diff --git a/packages/cli-types/src/index.ts b/packages/cli-types/src/index.ts index 67af8e461..b5b6ad3de 100644 --- a/packages/cli-types/src/index.ts +++ b/packages/cli-types/src/index.ts @@ -11,6 +11,7 @@ import { AndroidDependencyConfig, AndroidDependencyParams, } from './android'; +import {Ora} from 'ora'; export type InquirerPrompt = any; @@ -120,6 +121,100 @@ export type ProjectConfig = { [key: string]: any; }; +export type NotFound = 'Not Found'; +type AvailableInformation = { + version: string; + path: string; +}; + +type Information = AvailableInformation | NotFound; + +export type EnvironmentInfo = { + System: { + OS: string; + CPU: string; + Memory: string; + Shell: AvailableInformation; + }; + Binaries: { + Node: AvailableInformation; + Yarn: AvailableInformation; + npm: AvailableInformation; + Watchman: AvailableInformation; + }; + SDKs: { + 'iOS SDK': { + Platforms: string[]; + }; + 'Android SDK': + | { + 'API Levels': string[] | NotFound; + 'Build Tools': string[] | NotFound; + 'System Images': string[] | NotFound; + 'Android NDK': string | NotFound; + } + | NotFound; + }; + IDEs: { + 'Android Studio': AvailableInformation | NotFound; + Emacs: AvailableInformation; + Nano: AvailableInformation; + VSCode: AvailableInformation; + Vim: AvailableInformation; + Xcode: AvailableInformation; + }; + Languages: { + Java: Information; + Python: Information; + }; +}; + +export type HealthCheckCategory = { + label: string; + healthchecks: HealthCheckInterface[]; +}; + +export type Healthchecks = { + common: HealthCheckCategory; + android: HealthCheckCategory; + ios?: HealthCheckCategory; +}; + +export type RunAutomaticFix = (args: { + loader: Ora; + logManualInstallation: ({ + healthcheck, + url, + command, + message, + }: { + healthcheck?: string; + url?: string; + command?: string; + message?: string; + }) => void; + environmentInfo: EnvironmentInfo; +}) => Promise | void; + +export type HealthCheckInterface = { + label: string; + visible?: boolean | void; + isRequired?: boolean; + description?: string; + getDiagnostics: ( + environmentInfo: EnvironmentInfo, + ) => Promise<{ + version?: string; + versions?: [string]; + versionRange?: string; + needsToBeFixed: boolean | string; + }>; + win32AutomaticFix?: RunAutomaticFix; + darwinAutomaticFix?: RunAutomaticFix; + linuxAutomaticFix?: RunAutomaticFix; + runAutomaticFix: RunAutomaticFix; +}; + /** * @property root - Root where the configuration has been resolved from * @property reactNativePath - Path to React Native source @@ -128,6 +223,7 @@ export type ProjectConfig = { * @property dependencies - Map of the dependencies that are present in the project * @property platforms - Map of available platforms (build-ins and dynamically loaded) * @property commands - An array of commands that are present in 3rd party packages + * @property healthChecks - An array of health check categories to add to doctor command */ export interface Config extends IOSNativeModulesConfig { root: string; @@ -151,6 +247,7 @@ export interface Config extends IOSNativeModulesConfig { [name: string]: PlatformConfig; }; commands: Command[]; + healthChecks: HealthCheckCategory[]; } /** @@ -175,6 +272,8 @@ export type UserDependencyConfig = { commands: Command[]; // An array of extra platforms to load platforms: Config['platforms']; + // Additional health checks + healthChecks: HealthCheckCategory[]; }; export { diff --git a/packages/cli/src/commands/doctor/doctor.ts b/packages/cli/src/commands/doctor/doctor.ts index d5819e16d..3df151937 100644 --- a/packages/cli/src/commands/doctor/doctor.ts +++ b/packages/cli/src/commands/doctor/doctor.ts @@ -4,13 +4,12 @@ import {getHealthchecks, HEALTHCHECK_TYPES} from './healthchecks'; import {getLoader} from '../../tools/loader'; import printFixOptions, {KEYS} from './printFixOptions'; import runAutomaticFix, {AUTOMATIC_FIX_LEVELS} from './runAutomaticFix'; -import {DetachedCommandFunction} from '@react-native-community/cli-types'; import { + DetachedCommandFunction, HealthCheckCategory, - HealthCheckCategoryResult, - HealthCheckResult, HealthCheckInterface, -} from './types'; +} from '@react-native-community/cli-types'; +import {HealthCheckCategoryResult, HealthCheckResult} from './types'; import getEnvironmentInfo from '../../tools/envinfo'; import {logMessage} from './healthchecks/common'; diff --git a/packages/cli/src/commands/doctor/healthchecks/__tests__/androidHomeEnvVariable.test.ts b/packages/cli/src/commands/doctor/healthchecks/__tests__/androidHomeEnvVariable.test.ts index b537ad2e7..d526c44b5 100644 --- a/packages/cli/src/commands/doctor/healthchecks/__tests__/androidHomeEnvVariable.test.ts +++ b/packages/cli/src/commands/doctor/healthchecks/__tests__/androidHomeEnvVariable.test.ts @@ -1,12 +1,15 @@ import androidHomeEnvVariables from '../androidHomeEnvVariable'; import {NoopLoader} from '../../../../tools/loader'; +import {EnvironmentInfo} from '@react-native-community/cli-types'; import * as common from '../common'; const logSpy = jest.spyOn(common, 'logManualInstallation'); +const {logManualInstallation} = common; describe('androidHomeEnvVariables', () => { const OLD_ENV = process.env; + let environmentInfo: EnvironmentInfo; afterEach(() => { process.env = OLD_ENV; @@ -16,14 +19,18 @@ describe('androidHomeEnvVariables', () => { it('returns true if no ANDROID_HOME is defined', async () => { delete process.env.ANDROID_HOME; - const diagnostics = await androidHomeEnvVariables.getDiagnostics(); + const diagnostics = await androidHomeEnvVariables.getDiagnostics( + environmentInfo, + ); expect(diagnostics.needsToBeFixed).toBe(true); }); it('returns false if ANDROID_HOME is defined', async () => { process.env.ANDROID_HOME = '/fake/path/to/android/home'; - const diagnostics = await androidHomeEnvVariables.getDiagnostics(); + const diagnostics = await androidHomeEnvVariables.getDiagnostics( + environmentInfo, + ); expect(diagnostics.needsToBeFixed).toBe(false); }); @@ -31,7 +38,11 @@ describe('androidHomeEnvVariables', () => { const loader = new NoopLoader(); delete process.env.ANDROID_HOME; - androidHomeEnvVariables.runAutomaticFix({loader}); + androidHomeEnvVariables.runAutomaticFix({ + loader, + logManualInstallation, + environmentInfo, + }); expect(logSpy).toHaveBeenCalledTimes(1); }); diff --git a/packages/cli/src/commands/doctor/healthchecks/__tests__/androidNDK.test.ts b/packages/cli/src/commands/doctor/healthchecks/__tests__/androidNDK.test.ts index b5b4e0abe..f3d0cd72d 100644 --- a/packages/cli/src/commands/doctor/healthchecks/__tests__/androidNDK.test.ts +++ b/packages/cli/src/commands/doctor/healthchecks/__tests__/androidNDK.test.ts @@ -1,11 +1,12 @@ import androidNDK from '../androidNDK'; import getEnvironmentInfo from '../../../../tools/envinfo'; -import {EnvironmentInfo} from '../../types'; +import {EnvironmentInfo} from '@react-native-community/cli-types'; import {NoopLoader} from '../../../../tools/loader'; import * as common from '../common'; const logSpy = jest.spyOn(common, 'logManualInstallation'); +const {logManualInstallation} = common; describe('androidNDK', () => { let environmentInfo: EnvironmentInfo; @@ -57,7 +58,11 @@ describe('androidNDK', () => { it('logs manual installation steps to the screen', () => { const loader = new NoopLoader(); - androidNDK.runAutomaticFix({loader, environmentInfo}); + androidNDK.runAutomaticFix({ + loader, + logManualInstallation, + environmentInfo, + }); expect(logSpy).toHaveBeenCalledTimes(1); }); diff --git a/packages/cli/src/commands/doctor/healthchecks/__tests__/androidSDK.test.ts b/packages/cli/src/commands/doctor/healthchecks/__tests__/androidSDK.test.ts index 1b46bea12..9e6cc6830 100644 --- a/packages/cli/src/commands/doctor/healthchecks/__tests__/androidSDK.test.ts +++ b/packages/cli/src/commands/doctor/healthchecks/__tests__/androidSDK.test.ts @@ -5,13 +5,14 @@ import {cleanup, writeFiles} from '../../../../../../../jest/helpers'; import androidSDK from '../androidSDK'; import getEnvironmentInfo from '../../../../tools/envinfo'; import * as downloadAndUnzip from '../../../../tools/downloadAndUnzip'; -import {EnvironmentInfo} from '../../types'; +import {EnvironmentInfo} from '@react-native-community/cli-types'; import {NoopLoader} from '../../../../tools/loader'; import * as common from '../common'; import * as androidWinHelpers from '../../../../tools/windows/androidWinHelpers'; import * as environmentVariables from '../../../../tools/windows/environmentVariables'; const logSpy = jest.spyOn(common, 'logManualInstallation'); +const {logManualInstallation} = common; jest.mock('execa', () => jest.fn()); @@ -90,7 +91,11 @@ describe('androidSDK', () => { it('logs manual installation steps to the screen for the default fix', () => { const loader = new NoopLoader(); - androidSDK.runAutomaticFix({loader, environmentInfo}); + androidSDK.runAutomaticFix({ + loader, + logManualInstallation, + environmentInfo, + }); expect(logSpy).toHaveBeenCalledTimes(1); }); @@ -124,7 +129,11 @@ describe('androidSDK', () => { return Promise.resolve({hypervisor: 'WHPX', installed: true}); }); - await androidSDK.win32AutomaticFix({loader, environmentInfo}); + await androidSDK.win32AutomaticFix({ + loader, + logManualInstallation, + environmentInfo, + }); // 1. Download and unzip the SDK expect(downloadAndUnzipSpy).toBeCalledTimes(1); diff --git a/packages/cli/src/commands/doctor/healthchecks/__tests__/androidStudio.test.ts b/packages/cli/src/commands/doctor/healthchecks/__tests__/androidStudio.test.ts index 6d40dd9c6..2c8a87cc8 100644 --- a/packages/cli/src/commands/doctor/healthchecks/__tests__/androidStudio.test.ts +++ b/packages/cli/src/commands/doctor/healthchecks/__tests__/androidStudio.test.ts @@ -1,7 +1,7 @@ import execa from 'execa'; import androidStudio from '../androidStudio'; import getEnvironmentInfo from '../../../../tools/envinfo'; -import {EnvironmentInfo} from '../../types'; +import {EnvironmentInfo} from '@react-native-community/cli-types'; import {NoopLoader} from '../../../../tools/loader'; import * as common from '../common'; import * as downloadAndUnzip from '../../../../tools/downloadAndUnzip'; @@ -9,6 +9,7 @@ import * as downloadAndUnzip from '../../../../tools/downloadAndUnzip'; jest.mock('execa', () => jest.fn()); const logSpy = jest.spyOn(common, 'logManualInstallation'); +const {logManualInstallation} = common; describe('androidStudio', () => { let environmentInfo: EnvironmentInfo; @@ -42,7 +43,11 @@ describe('androidStudio', () => { it('logs manual installation steps to the screen for the default fix', async () => { const loader = new NoopLoader(); - await androidStudio.runAutomaticFix({loader, environmentInfo}); + await androidStudio.runAutomaticFix({ + loader, + logManualInstallation, + environmentInfo, + }); expect(logSpy).toHaveBeenCalledTimes(1); }); @@ -54,7 +59,11 @@ describe('androidStudio', () => { .spyOn(downloadAndUnzip, 'downloadAndUnzip') .mockImplementation(() => Promise.resolve()); - await androidStudio.win32AutomaticFix({loader, environmentInfo}); + await androidStudio.win32AutomaticFix({ + loader, + logManualInstallation, + environmentInfo, + }); expect(loaderFailSpy).toHaveBeenCalledTimes(0); expect(logSpy).toHaveBeenCalledTimes(0); diff --git a/packages/cli/src/commands/doctor/healthchecks/__tests__/jdk.test.ts b/packages/cli/src/commands/doctor/healthchecks/__tests__/jdk.test.ts index 29e96c907..62a6e00c2 100644 --- a/packages/cli/src/commands/doctor/healthchecks/__tests__/jdk.test.ts +++ b/packages/cli/src/commands/doctor/healthchecks/__tests__/jdk.test.ts @@ -1,7 +1,7 @@ import execa from 'execa'; import jdk from '../jdk'; import getEnvironmentInfo from '../../../../tools/envinfo'; -import {EnvironmentInfo} from '../../types'; +import {EnvironmentInfo} from '@react-native-community/cli-types'; import {NoopLoader} from '../../../../tools/loader'; import * as common from '../common'; import * as unzip from '../../../../tools/unzip'; @@ -20,6 +20,7 @@ jest.mock('@react-native-community/cli-tools', () => { }); const logSpy = jest.spyOn(common, 'logManualInstallation'); +const {logManualInstallation} = common; describe('jdk', () => { let environmentInfo: EnvironmentInfo; @@ -61,7 +62,7 @@ describe('jdk', () => { it('logs manual installation steps to the screen for the default fix', async () => { const loader = new NoopLoader(); - await jdk.runAutomaticFix({loader, environmentInfo}); + await jdk.runAutomaticFix({loader, logManualInstallation, environmentInfo}); expect(logSpy).toHaveBeenCalledTimes(1); }); @@ -73,7 +74,11 @@ describe('jdk', () => { .spyOn(unzip, 'unzip') .mockImplementation(() => Promise.resolve()); - await jdk.win32AutomaticFix({loader, environmentInfo}); + await jdk.win32AutomaticFix({ + loader, + logManualInstallation, + environmentInfo, + }); expect(loaderFailSpy).toHaveBeenCalledTimes(0); expect(logSpy).toHaveBeenCalledTimes(0); diff --git a/packages/cli/src/commands/doctor/healthchecks/__tests__/python.test.ts b/packages/cli/src/commands/doctor/healthchecks/__tests__/python.test.ts index 04ac250f9..8e30fb682 100644 --- a/packages/cli/src/commands/doctor/healthchecks/__tests__/python.test.ts +++ b/packages/cli/src/commands/doctor/healthchecks/__tests__/python.test.ts @@ -1,7 +1,7 @@ import execa from 'execa'; import python from '../python'; import getEnvironmentInfo from '../../../../tools/envinfo'; -import {EnvironmentInfo} from '../../types'; +import {EnvironmentInfo} from '@react-native-community/cli-types'; import {NoopLoader} from '../../../../tools/loader'; import * as common from '../common'; @@ -11,6 +11,7 @@ jest.mock('@react-native-community/cli-tools', () => ({ })); const logSpy = jest.spyOn(common, 'logManualInstallation'); +const {logManualInstallation} = common; describe('python', () => { let environmentInfo: EnvironmentInfo; @@ -44,7 +45,11 @@ describe('python', () => { it('logs manual installation steps to the screen for the default fix', async () => { const loader = new NoopLoader(); - await python.runAutomaticFix({loader, environmentInfo}); + await python.runAutomaticFix({ + loader, + logManualInstallation, + environmentInfo, + }); expect(logSpy).toHaveBeenCalledTimes(1); }); @@ -53,7 +58,11 @@ describe('python', () => { const loaderSucceedSpy = jest.spyOn(loader, 'succeed'); const loaderFailSpy = jest.spyOn(loader, 'fail'); - await python.win32AutomaticFix({loader, environmentInfo}); + await python.win32AutomaticFix({ + loader, + logManualInstallation, + environmentInfo, + }); expect(loaderFailSpy).toHaveBeenCalledTimes(0); expect(logSpy).toHaveBeenCalledTimes(0); diff --git a/packages/cli/src/commands/doctor/healthchecks/androidHomeEnvVariable.ts b/packages/cli/src/commands/doctor/healthchecks/androidHomeEnvVariable.ts index 77b0a72a8..8ea5d5cb5 100644 --- a/packages/cli/src/commands/doctor/healthchecks/androidHomeEnvVariable.ts +++ b/packages/cli/src/commands/doctor/healthchecks/androidHomeEnvVariable.ts @@ -1,7 +1,5 @@ import chalk from 'chalk'; -import {Ora} from 'ora'; -import {logManualInstallation} from './common'; -import {HealthCheckInterface} from '../types'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; // List of answers on how to set `ANDROID_HOME` for each platform const URLS = { @@ -25,7 +23,7 @@ export default { getDiagnostics: async () => ({ needsToBeFixed: !process.env.ANDROID_HOME, }), - runAutomaticFix: async ({loader}: {loader: Ora}) => { + runAutomaticFix: async ({loader, logManualInstallation}) => { // Variable could have been added if installing Android Studio so double checking if (process.env.ANDROID_HOME) { loader.succeed(); diff --git a/packages/cli/src/commands/doctor/healthchecks/androidNDK.ts b/packages/cli/src/commands/doctor/healthchecks/androidNDK.ts index c7a70c888..ce236f268 100644 --- a/packages/cli/src/commands/doctor/healthchecks/androidNDK.ts +++ b/packages/cli/src/commands/doctor/healthchecks/androidNDK.ts @@ -1,9 +1,10 @@ import chalk from 'chalk'; -import {Ora} from 'ora'; -import {logManualInstallation} from './common'; import versionRanges from '../versionRanges'; import {doesSoftwareNeedToBeFixed} from '../checkInstallation'; -import {EnvironmentInfo, HealthCheckInterface} from '../types'; +import { + EnvironmentInfo, + HealthCheckInterface, +} from '@react-native-community/cli-types'; export default { label: 'Android NDK', @@ -22,13 +23,7 @@ export default { versionRange: versionRanges.ANDROID_NDK, }; }, - runAutomaticFix: async ({ - loader, - environmentInfo, - }: { - loader: Ora; - environmentInfo: EnvironmentInfo; - }) => { + runAutomaticFix: async ({loader, logManualInstallation, environmentInfo}) => { const androidSdk = environmentInfo.SDKs['Android SDK']; const isNDKInstalled = androidSdk !== 'Not Found' && androidSdk['Android NDK'] !== 'Not Found'; diff --git a/packages/cli/src/commands/doctor/healthchecks/androidSDK.ts b/packages/cli/src/commands/doctor/healthchecks/androidSDK.ts index e4109a77f..c5d3035c1 100644 --- a/packages/cli/src/commands/doctor/healthchecks/androidSDK.ts +++ b/packages/cli/src/commands/doctor/healthchecks/androidSDK.ts @@ -3,8 +3,10 @@ import fs from 'fs'; import path from 'path'; import {logger} from '@react-native-community/cli-tools'; -import {logManualInstallation} from './common'; -import {HealthCheckInterface, EnvironmentInfo} from '../types'; +import { + HealthCheckInterface, + EnvironmentInfo, +} from '@react-native-community/cli-types'; import findProjectRoot from '../../../tools/config/findProjectRoot'; import { getAndroidSdkRootInstallation, @@ -175,7 +177,7 @@ export default { 'Android SDK configured. You might need to restart your PC for all changes to take effect.', ); }, - runAutomaticFix: async ({loader, environmentInfo}) => { + runAutomaticFix: async ({loader, logManualInstallation, environmentInfo}) => { loader.fail(); if (isSDKInstalled(environmentInfo)) { diff --git a/packages/cli/src/commands/doctor/healthchecks/androidStudio.ts b/packages/cli/src/commands/doctor/healthchecks/androidStudio.ts index a68672d66..8c22ec59e 100644 --- a/packages/cli/src/commands/doctor/healthchecks/androidStudio.ts +++ b/packages/cli/src/commands/doctor/healthchecks/androidStudio.ts @@ -1,7 +1,6 @@ import {join} from 'path'; -import {logManualInstallation} from './common'; -import {HealthCheckInterface} from '../types'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; import {downloadAndUnzip} from '../../../tools/downloadAndUnzip'; import {executeCommand} from '../../../tools/windows/executeWinCommand'; @@ -70,7 +69,7 @@ export default { `Android Studio installed successfully in "${installPath}".`, ); }, - runAutomaticFix: async ({loader}) => { + runAutomaticFix: async ({loader, logManualInstallation}) => { loader.fail(); return logManualInstallation({ diff --git a/packages/cli/src/commands/doctor/healthchecks/cocoaPods.ts b/packages/cli/src/commands/doctor/healthchecks/cocoaPods.ts index bc8ffd449..a34ee9c13 100644 --- a/packages/cli/src/commands/doctor/healthchecks/cocoaPods.ts +++ b/packages/cli/src/commands/doctor/healthchecks/cocoaPods.ts @@ -6,7 +6,7 @@ import { } from '../../../tools/installPods'; import {removeMessage, logError} from './common'; import {brewInstall} from '../../../tools/brewInstall'; -import {HealthCheckInterface} from '../types'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; const label = 'CocoaPods'; diff --git a/packages/cli/src/commands/doctor/healthchecks/index.ts b/packages/cli/src/commands/doctor/healthchecks/index.ts index ebaad4b88..9af8daed8 100644 --- a/packages/cli/src/commands/doctor/healthchecks/index.ts +++ b/packages/cli/src/commands/doctor/healthchecks/index.ts @@ -10,7 +10,11 @@ import androidNDK from './androidNDK'; import xcode from './xcode'; import cocoaPods from './cocoaPods'; import iosDeploy from './iosDeploy'; -import {Healthchecks} from '../types'; +import { + Healthchecks, + HealthCheckCategory, +} from '@react-native-community/cli-types'; +import loadConfig from '../../../tools/config'; export const HEALTHCHECK_TYPES = { ERROR: 'ERROR', @@ -22,33 +26,44 @@ type Options = { contributor: boolean | void; }; -export const getHealthchecks = ({contributor}: Options): Healthchecks => ({ - common: { - label: 'Common', - healthchecks: [ - nodeJS, - yarn, - npm, - ...(process.platform === 'darwin' ? [watchman] : []), - ...(process.platform === 'win32' ? [python] : []), - ], - }, - android: { - label: 'Android', - healthchecks: [ - jdk, - androidStudio, - androidSDK, - androidHomeEnvVariable, - ...(contributor ? [androidNDK] : []), - ], - }, - ...(process.platform === 'darwin' - ? { - ios: { - label: 'iOS', - healthchecks: [xcode, cocoaPods, iosDeploy], - }, - } - : {}), -}); +export const getHealthchecks = ({contributor}: Options): Healthchecks => { + let additionalChecks: HealthCheckCategory[] = []; + + // Doctor can run in a detached mode, where there isn't a config so this can fail + try { + let config = loadConfig(); + additionalChecks = config.healthChecks; + } catch {} + + return { + common: { + label: 'Common', + healthchecks: [ + nodeJS, + yarn, + npm, + ...(process.platform === 'darwin' ? [watchman] : []), + ...(process.platform === 'win32' ? [python] : []), + ], + }, + android: { + label: 'Android', + healthchecks: [ + jdk, + androidStudio, + androidSDK, + androidHomeEnvVariable, + ...(contributor ? [androidNDK] : []), + ], + }, + ...(process.platform === 'darwin' + ? { + ios: { + label: 'iOS', + healthchecks: [xcode, cocoaPods, iosDeploy], + }, + } + : {}), + ...additionalChecks, + }; +}; diff --git a/packages/cli/src/commands/doctor/healthchecks/iosDeploy.ts b/packages/cli/src/commands/doctor/healthchecks/iosDeploy.ts index 5e31502f4..19eb4fabd 100644 --- a/packages/cli/src/commands/doctor/healthchecks/iosDeploy.ts +++ b/packages/cli/src/commands/doctor/healthchecks/iosDeploy.ts @@ -4,8 +4,8 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import {isSoftwareNotInstalled, PACKAGE_MANAGERS} from '../checkInstallation'; import {packageManager} from './packageManagers'; -import {logManualInstallation, logError, removeMessage} from './common'; -import {HealthCheckInterface} from '../types'; +import {logError, removeMessage} from './common'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; import {Ora} from 'ora'; const label = 'ios-deploy'; @@ -60,7 +60,7 @@ export default { getDiagnostics: async () => ({ needsToBeFixed: await isSoftwareNotInstalled('ios-deploy'), }), - runAutomaticFix: async ({loader}) => { + runAutomaticFix: async ({loader, logManualInstallation}) => { loader.stop(); const installationCommand = identifyInstallationCommand(); diff --git a/packages/cli/src/commands/doctor/healthchecks/jdk.ts b/packages/cli/src/commands/doctor/healthchecks/jdk.ts index 27b5605d2..c025d43a0 100644 --- a/packages/cli/src/commands/doctor/healthchecks/jdk.ts +++ b/packages/cli/src/commands/doctor/healthchecks/jdk.ts @@ -1,8 +1,7 @@ import {join} from 'path'; import versionRanges from '../versionRanges'; import {doesSoftwareNeedToBeFixed} from '../checkInstallation'; -import {logManualInstallation} from './common'; -import {HealthCheckInterface} from '../types'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; import {downloadAndUnzip} from '../../../tools/downloadAndUnzip'; import { @@ -10,8 +9,6 @@ import { updateEnvironment, } from '../../../tools/windows/environmentVariables'; -import {Ora} from 'ora'; - export default { label: 'JDK', getDiagnostics: async ({Languages}) => ({ @@ -29,7 +26,7 @@ export default { : Languages.Java.version, versionRange: versionRanges.JAVA, }), - win32AutomaticFix: async ({loader}: {loader: Ora}) => { + win32AutomaticFix: async ({loader}) => { try { // Installing JDK 11 because later versions seem to cause issues with gradle at the moment const installerUrl = @@ -57,7 +54,7 @@ export default { loader.fail(e); } }, - runAutomaticFix: async () => { + runAutomaticFix: async ({logManualInstallation}) => { logManualInstallation({ healthcheck: 'JDK', url: 'https://openjdk.java.net/', diff --git a/packages/cli/src/commands/doctor/healthchecks/nodeJS.ts b/packages/cli/src/commands/doctor/healthchecks/nodeJS.ts index c9f9dc47b..6b36cbbff 100644 --- a/packages/cli/src/commands/doctor/healthchecks/nodeJS.ts +++ b/packages/cli/src/commands/doctor/healthchecks/nodeJS.ts @@ -1,7 +1,6 @@ import versionRanges from '../versionRanges'; import {doesSoftwareNeedToBeFixed} from '../checkInstallation'; -import {logManualInstallation} from './common'; -import {HealthCheckInterface} from '../types'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; export default { label: 'Node.js', @@ -13,7 +12,7 @@ export default { version: Binaries.Node.version, versionRange: versionRanges.NODE_JS, }), - runAutomaticFix: async ({loader}) => { + runAutomaticFix: async ({loader, logManualInstallation}) => { loader.fail(); logManualInstallation({ diff --git a/packages/cli/src/commands/doctor/healthchecks/packageManagers.ts b/packages/cli/src/commands/doctor/healthchecks/packageManagers.ts index d83af28c7..51b83a621 100644 --- a/packages/cli/src/commands/doctor/healthchecks/packageManagers.ts +++ b/packages/cli/src/commands/doctor/healthchecks/packageManagers.ts @@ -5,7 +5,7 @@ import { doesSoftwareNeedToBeFixed, } from '../checkInstallation'; import {install} from '../../../tools/install'; -import {HealthCheckInterface} from '../types'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; const packageManager = (() => { if (fs.existsSync('yarn.lock')) { diff --git a/packages/cli/src/commands/doctor/healthchecks/python.ts b/packages/cli/src/commands/doctor/healthchecks/python.ts index fc3993a0c..087914dc1 100644 --- a/packages/cli/src/commands/doctor/healthchecks/python.ts +++ b/packages/cli/src/commands/doctor/healthchecks/python.ts @@ -1,12 +1,10 @@ import {fetchToTemp} from '@react-native-community/cli-tools'; import versionRanges from '../versionRanges'; import {doesSoftwareNeedToBeFixed} from '../checkInstallation'; -import {logManualInstallation} from './common'; -import {HealthCheckInterface} from '../types'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; import {updateEnvironment} from '../../../tools/windows/environmentVariables'; import {join} from 'path'; -import {Ora} from 'ora'; import {executeCommand} from '../../../tools/windows/executeWinCommand'; export default { @@ -26,7 +24,7 @@ export default { : Languages.Python.version, versionRange: versionRanges.PYTHON, }), - win32AutomaticFix: async ({loader}: {loader: Ora}) => { + win32AutomaticFix: async ({loader}) => { try { const arch = process.arch === 'x64' ? 'amd64.' : ''; const installerUrl = `https://www.python.org/ftp/python/2.7.9/python-2.7.9.${arch}msi`; @@ -51,7 +49,7 @@ export default { loader.fail(e); } }, - runAutomaticFix: async () => { + runAutomaticFix: async ({logManualInstallation}) => { /** * Python is only needed on Windows so this method should never be called. * Leaving it in case that changes and as an example of how to have a diff --git a/packages/cli/src/commands/doctor/healthchecks/watchman.ts b/packages/cli/src/commands/doctor/healthchecks/watchman.ts index 7fcfa8e33..e3e8c2074 100644 --- a/packages/cli/src/commands/doctor/healthchecks/watchman.ts +++ b/packages/cli/src/commands/doctor/healthchecks/watchman.ts @@ -1,7 +1,7 @@ import versionRanges from '../versionRanges'; import {doesSoftwareNeedToBeFixed} from '../checkInstallation'; import {install} from '../../../tools/install'; -import {HealthCheckInterface} from '../types'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; const label = 'Watchman'; diff --git a/packages/cli/src/commands/doctor/healthchecks/xcode.ts b/packages/cli/src/commands/doctor/healthchecks/xcode.ts index 2eae0da7e..9d2f8c125 100644 --- a/packages/cli/src/commands/doctor/healthchecks/xcode.ts +++ b/packages/cli/src/commands/doctor/healthchecks/xcode.ts @@ -1,7 +1,6 @@ import versionRanges from '../versionRanges'; import {doesSoftwareNeedToBeFixed} from '../checkInstallation'; -import {logManualInstallation} from './common'; -import {HealthCheckInterface} from '../types'; +import {HealthCheckInterface} from '@react-native-community/cli-types'; export default { label: 'Xcode', @@ -18,7 +17,7 @@ export default { versionRange: versionRanges.XCODE, }; }, - runAutomaticFix: async ({loader}) => { + runAutomaticFix: async ({loader, logManualInstallation}) => { loader.fail(); logManualInstallation({ diff --git a/packages/cli/src/commands/doctor/runAutomaticFix.ts b/packages/cli/src/commands/doctor/runAutomaticFix.ts index 7e7a4df35..f70d58735 100644 --- a/packages/cli/src/commands/doctor/runAutomaticFix.ts +++ b/packages/cli/src/commands/doctor/runAutomaticFix.ts @@ -2,7 +2,9 @@ import chalk from 'chalk'; import ora, {Ora} from 'ora'; import {logger} from '@react-native-community/cli-tools'; import {HEALTHCHECK_TYPES} from './healthchecks'; -import {EnvironmentInfo, HealthCheckCategoryResult} from './types'; +import {EnvironmentInfo} from '@react-native-community/cli-types'; +import {HealthCheckCategoryResult} from './types'; +import {logManualInstallation} from './healthchecks/common'; export enum AUTOMATIC_FIX_LEVELS { ALL_ISSUES = 'ALL_ISSUES', @@ -86,6 +88,7 @@ export default async function ({ try { await healthcheckToRun.runAutomaticFix({ loader: spinner, + logManualInstallation, environmentInfo, }); } catch (error) { diff --git a/packages/cli/src/commands/doctor/types.ts b/packages/cli/src/commands/doctor/types.ts index 233d65cb4..ca4d7cefa 100644 --- a/packages/cli/src/commands/doctor/types.ts +++ b/packages/cli/src/commands/doctor/types.ts @@ -1,87 +1,4 @@ -import {Ora} from 'ora'; - -type NotFound = 'Not Found'; -type AvailableInformation = { - version: string; - path: string; -}; - -type Information = AvailableInformation | NotFound; - -export type EnvironmentInfo = { - System: { - OS: string; - CPU: string; - Memory: string; - Shell: AvailableInformation; - }; - Binaries: { - Node: AvailableInformation; - Yarn: AvailableInformation; - npm: AvailableInformation; - Watchman: AvailableInformation; - }; - SDKs: { - 'iOS SDK': { - Platforms: string[]; - }; - 'Android SDK': - | { - 'API Levels': string[] | NotFound; - 'Build Tools': string[] | NotFound; - 'System Images': string[] | NotFound; - 'Android NDK': string | NotFound; - } - | NotFound; - }; - IDEs: { - 'Android Studio': AvailableInformation | NotFound; - Emacs: AvailableInformation; - Nano: AvailableInformation; - VSCode: AvailableInformation; - Vim: AvailableInformation; - Xcode: AvailableInformation; - }; - Languages: { - Java: Information; - Python: Information; - }; -}; - -export type HealthCheckCategory = { - label: string; - healthchecks: HealthCheckInterface[]; -}; - -export type Healthchecks = { - common: HealthCheckCategory; - android: HealthCheckCategory; - ios?: HealthCheckCategory; -}; - -export type RunAutomaticFix = (args: { - loader: Ora; - environmentInfo: EnvironmentInfo; -}) => Promise | void; - -export type HealthCheckInterface = { - label: string; - visible?: boolean | void; - isRequired?: boolean; - description?: string; - getDiagnostics: ( - environmentInfo: EnvironmentInfo, - ) => Promise<{ - version?: string; - versions?: [string]; - versionRange?: string; - needsToBeFixed: boolean | string; - }>; - win32AutomaticFix?: RunAutomaticFix; - darwinAutomaticFix?: RunAutomaticFix; - linuxAutomaticFix?: RunAutomaticFix; - runAutomaticFix: RunAutomaticFix; -}; +import {RunAutomaticFix, NotFound} from '@react-native-community/cli-types'; export type HealthCheckResult = { label: string; diff --git a/packages/cli/src/commands/init/templateName.ts b/packages/cli/src/commands/init/templateName.ts index b0a7a2b98..e013d09f7 100644 --- a/packages/cli/src/commands/init/templateName.ts +++ b/packages/cli/src/commands/init/templateName.ts @@ -95,7 +95,7 @@ export function processTemplateName(templateName: string) { return handleVersionedPackage(templateName); } if ( - !['github', '@'].some(str => templateName.includes(str)) && + !['github', '@'].some((str) => templateName.includes(str)) && templateName.includes('/') ) { return handleGitHubRepo(templateName); diff --git a/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.ts.snap b/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.ts.snap index 0c5415a93..b84cd250e 100644 --- a/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.ts.snap +++ b/packages/cli/src/tools/config/__tests__/__snapshots__/index-test.ts.snap @@ -5,6 +5,7 @@ Object { "assets": Array [], "commands": Array [], "dependencies": Object {}, + "healthChecks": Array [], "platforms": Object {}, "project": Object {}, "reactNativePath": "<>", diff --git a/packages/cli/src/tools/config/index.ts b/packages/cli/src/tools/config/index.ts index 3e55d99bf..7f427a44a 100644 --- a/packages/cli/src/tools/config/index.ts +++ b/packages/cli/src/tools/config/index.ts @@ -74,6 +74,7 @@ function loadConfig(projectRoot: string = findProjectRoot()): Config { get assets() { return findAssets(projectRoot, userConfig.assets); }, + healthChecks: [], platforms: userConfig.platforms, get project() { if (lazyProject) { @@ -143,6 +144,7 @@ function loadConfig(projectRoot: string = findProjectRoot()): Config { ...acc.platforms, ...config.platforms, }, + healthChecks: [...acc.healthChecks, ...config.healthChecks], }) as Config; }, initialConfig); diff --git a/packages/cli/src/tools/config/schema.ts b/packages/cli/src/tools/config/schema.ts index 25a412fad..aabe18c17 100644 --- a/packages/cli/src/tools/config/schema.ts +++ b/packages/cli/src/tools/config/schema.ts @@ -31,6 +31,25 @@ const command = t.object({ ), }); +/** + * Schema for HealthChecksT + */ +const healthCheck = t.object({ + label: t.string().required(), + healthchecks: t.array().items( + t.object({ + label: t.string().required(), + isRequired: t.string(), + description: t.string(), + getDiagnostics: t.func(), + win32AutomaticFix: t.func(), + darwinAutomaticFix: t.func(), + linuxAutomaticFix: t.func(), + runAutomaticFix: t.func().required(), + }), + ), +}); + /** * Schema for UserDependencyConfigT */ @@ -83,6 +102,7 @@ export const dependencyConfig = t }), ).default({}), commands: t.array().items(command).default([]), + healthChecks: t.array().items(healthCheck).default([]), }) .unknown(true) .default(); diff --git a/packages/cli/src/tools/envinfo.ts b/packages/cli/src/tools/envinfo.ts index 584c521f1..e23c670cb 100644 --- a/packages/cli/src/tools/envinfo.ts +++ b/packages/cli/src/tools/envinfo.ts @@ -1,6 +1,6 @@ // @ts-ignore import envinfo from 'envinfo'; -import {EnvironmentInfo} from '../commands/doctor/types'; +import {EnvironmentInfo} from '@react-native-community/cli-types'; import {platform} from 'os'; /**