From f72b79efb454bf8db5466024e1553f6b3790193e Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Fri, 18 Mar 2022 15:00:11 +0100 Subject: [PATCH 1/5] wip: rewriting doctor very much a WIP, wording, formatting, API etc all subject to change. --- packages/doctorv2/package.json | 15 +++ .../doctorv2/src/helpers/child-process.ts | 57 ++++++++ packages/doctorv2/src/helpers/index.ts | 2 + packages/doctorv2/src/helpers/results.ts | 32 +++++ packages/doctorv2/src/helpers/semver.ts | 30 +++++ packages/doctorv2/src/index.ts | 126 ++++++++++++++++++ .../src/requirements/android/android-sdk.ts | 92 +++++++++++++ .../src/requirements/android/index.ts | 8 ++ .../doctorv2/src/requirements/android/java.ts | 92 +++++++++++++ .../doctorv2/src/requirements/common/index.ts | 91 +++++++++++++ .../src/requirements/ios/cocoapods.ts | 32 +++++ .../doctorv2/src/requirements/ios/index.ts | 10 ++ .../doctorv2/src/requirements/ios/python.ts | 53 ++++++++ .../doctorv2/src/requirements/ios/xcode.ts | 64 +++++++++ packages/doctorv2/tsconfig.json | 10 ++ packages/doctorv2/yarn.lock | 32 +++++ 16 files changed, 746 insertions(+) create mode 100644 packages/doctorv2/package.json create mode 100644 packages/doctorv2/src/helpers/child-process.ts create mode 100644 packages/doctorv2/src/helpers/index.ts create mode 100644 packages/doctorv2/src/helpers/results.ts create mode 100644 packages/doctorv2/src/helpers/semver.ts create mode 100644 packages/doctorv2/src/index.ts create mode 100644 packages/doctorv2/src/requirements/android/android-sdk.ts create mode 100644 packages/doctorv2/src/requirements/android/index.ts create mode 100644 packages/doctorv2/src/requirements/android/java.ts create mode 100644 packages/doctorv2/src/requirements/common/index.ts create mode 100644 packages/doctorv2/src/requirements/ios/cocoapods.ts create mode 100644 packages/doctorv2/src/requirements/ios/index.ts create mode 100644 packages/doctorv2/src/requirements/ios/python.ts create mode 100644 packages/doctorv2/src/requirements/ios/xcode.ts create mode 100644 packages/doctorv2/tsconfig.json create mode 100644 packages/doctorv2/yarn.lock diff --git a/packages/doctorv2/package.json b/packages/doctorv2/package.json new file mode 100644 index 0000000000..372ebbd5ce --- /dev/null +++ b/packages/doctorv2/package.json @@ -0,0 +1,15 @@ +{ + "name": "@nativescript/doctorv2", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc" + }, + "devDependencies": { + "typescript": "~4.5.5" + }, + "dependencies": { + "ansi-colors": "^4.1.1", + "semver": "^7.3.5" + } +} diff --git a/packages/doctorv2/src/helpers/child-process.ts b/packages/doctorv2/src/helpers/child-process.ts new file mode 100644 index 0000000000..c77beb6b94 --- /dev/null +++ b/packages/doctorv2/src/helpers/child-process.ts @@ -0,0 +1,57 @@ +import { exec as _exec } from "child_process"; +import type { ExecOptions } from "child_process"; +import { returnFalse } from "."; + +export interface IExecResult { + stdout: string; + stderr: string; +} + +export function exec( + command: string, + options?: ExecOptions +): Promise { + return new Promise((resolve, reject) => { + _exec(command, options, (err, stdout, stderr) => { + if (err) { + return reject(err); + } + + resolve({ + stdout, + stderr, + }); + }); + }); +} + +export function execSafe( + command: string, + options?: ExecOptions +): Promise { + return exec(command, options).catch(returnFalse); +} + +/* +export class ChildProcess { + public exec( + command: string, + options?: childProcess.ExecOptions + ): Promise { + return new Promise((resolve, reject) => { + childProcess.exec(command, options, (err, stdout, stderr) => { + if (err) { + reject(err); + } + + const result: IProcessInfo = { + stdout, + stderr, + }; + + resolve(result); + }); + }); + } +} +*/ diff --git a/packages/doctorv2/src/helpers/index.ts b/packages/doctorv2/src/helpers/index.ts new file mode 100644 index 0000000000..01d4121000 --- /dev/null +++ b/packages/doctorv2/src/helpers/index.ts @@ -0,0 +1,2 @@ +export const returnFalse: () => false = () => false; +export const returnNull: () => null = () => null; diff --git a/packages/doctorv2/src/helpers/results.ts b/packages/doctorv2/src/helpers/results.ts new file mode 100644 index 0000000000..4d769a4251 --- /dev/null +++ b/packages/doctorv2/src/helpers/results.ts @@ -0,0 +1,32 @@ +import { IRequirementResult, ResultType } from ".."; + +export function result( + type: ResultType, + data: Omit +): IRequirementResult { + return { + type, + ...data, + }; +} + +export function ok(message: string, details?: string) { + return result(ResultType.OK, { + message, + details, + }); +} + +export function error(message: string, details?: string) { + return result(ResultType.ERROR, { + message, + details, + }); +} + +export function warn(message: string, details?: string) { + return result(ResultType.WARN, { + message, + details, + }); +} diff --git a/packages/doctorv2/src/helpers/semver.ts b/packages/doctorv2/src/helpers/semver.ts new file mode 100644 index 0000000000..4df4ac752d --- /dev/null +++ b/packages/doctorv2/src/helpers/semver.ts @@ -0,0 +1,30 @@ +import * as semver from "semver"; + +export function isInRange( + version: string, + range: { min: string; max: string } +) { + try { + const _version = semver.coerce(version); + const _range = `${range.min} - ${range.max}`; + const _inRange = semver.satisfies(_version, _range); + + // console.log({ + // _version: _version.toString(), + // _range, + // _inRange, + // }); + + return _inRange; + } catch (err) { + console.log("isInRange err", err); + return false; + } +} + +export function notInRange( + version: string, + range: { min: string; max: string } +) { + return !isInRange(version, range); +} diff --git a/packages/doctorv2/src/index.ts b/packages/doctorv2/src/index.ts new file mode 100644 index 0000000000..30eec329dc --- /dev/null +++ b/packages/doctorv2/src/index.ts @@ -0,0 +1,126 @@ +import { redBright, yellowBright, green } from "ansi-colors"; + +export type TPlatform = "android" | "ios"; + +export type TPlatforms = { + [platform in TPlatform]?: boolean; +}; + +export const enum ResultType { + ERROR = "ERROR", + OK = "OK", + WARN = "WARN", +} + +const resultTypeColorMap = { + [ResultType.ERROR]: redBright, + [ResultType.OK]: green, + [ResultType.WARN]: yellowBright, +}; + +export interface IRequirementResult { + type: ResultType; + message: string; + details?: string; + platforms?: TPlatforms; +} + +export type RequirementFunction = ( + results: IRequirementResult[] +) => Promise; + +// todo: rename or whatever, but this is augmented by all requirements that provide new info +export interface RequirementDetails { + base?: true; +} + +export const details: RequirementDetails = {}; + +import { commonRequirements } from "./requirements/common"; +import { androidRequirements } from "./requirements/android"; +import { iosRequirements } from "./requirements/ios"; + +const allRequirements = [ + ...commonRequirements, + ...androidRequirements, + ...iosRequirements, +]; + +console.time("allRequirements"); + +const globalResults: IRequirementResult[] = []; +const promises: ReturnType< + RequirementFunction +>[] = allRequirements.map((f: RequirementFunction) => f(globalResults)); + +Promise.allSettled(promises).then((results) => { + // const res: IRequirementResult[] = []; + for (const result of results) { + if (result.status === "fulfilled") { + if (Array.isArray(result.value)) { + globalResults.push(...result.value); + } else if (result.value) { + globalResults.push(result.value); + } + } + + if (result.status === "rejected") { + console.log(result.reason); + globalResults.push({ + type: ResultType.WARN, + message: `Failed to verify requirement: ${result.reason}`, + }); + } + } + + const filtered = globalResults.filter(Boolean); + console.timeEnd("allRequirements"); + + console.log("-".repeat(100)); + console.log(details); + console.log("-".repeat(100)); + + printResults(filtered); +}); + +// "custom reporter" that prints the results - should live outside of this package... +function printResults(res: IRequirementResult[]) { + const stats = { + total: 0, + [ResultType.OK]: 0, + [ResultType.WARN]: 0, + [ResultType.ERROR]: 0, + }; + console.log(""); + res + .map((requirementResult) => { + const color = resultTypeColorMap[requirementResult.type]; + const pad = " ".repeat(5 - requirementResult.type.length); + + stats.total++; + stats[requirementResult.type]++; + + const details = requirementResult.details + ? `\n ${pad}${" ".repeat(2 + requirementResult.type.length)} - ` + + requirementResult.details + : ""; + + return ( + ` ${pad}[${color(requirementResult.type)}] ${ + requirementResult.message + }` + details + ); + }) + .forEach((line) => { + console.log(line); + }); + console.log(""); + console.log( + ` ${green(`${stats[ResultType.OK]} ok`)}, ${redBright( + `${stats[ResultType.ERROR]} errors` + )}, ${yellowBright(`${stats[ResultType.WARN]} warnings`)} / ${ + stats.total + } total` + ); + console.log(""); +} diff --git a/packages/doctorv2/src/requirements/android/android-sdk.ts b/packages/doctorv2/src/requirements/android/android-sdk.ts new file mode 100644 index 0000000000..8418e5d296 --- /dev/null +++ b/packages/doctorv2/src/requirements/android/android-sdk.ts @@ -0,0 +1,92 @@ +import { details, RequirementFunction } from "../.."; +import { execSafe } from "../../helpers/child-process"; +import { error, ok } from "../../helpers/results"; + +// example: augment details with new values +declare module "../.." { + interface RequirementDetails { + android?: { + sdkPath: string; + sdkFrom: string; + installedTargets: string[]; + }; + adb?: { version: string }; + } +} + +details.android = null; +details.adb = null; + +/** + * Excerpt from: https://developer.android.com/studio/command-line/variables#envar + * + * ANDROID_SDK_ROOT - Sets the path to the SDK installation directory. + * Once set, the value does not typically change, and can be shared by multiple users on the same machine. + * ANDROID_HOME, which also points to the SDK installation directory, is deprecated. + * If you continue to use it, the following rules apply: + * + * - If ANDROID_HOME is defined and contains a valid SDK installation, its value is used instead of the value in ANDROID_SDK_ROOT. + * - If ANDROID_HOME is not defined, the value in ANDROID_SDK_ROOT is used. + * - If ANDROID_HOME is defined but does not exist or does not contain a valid SDK installation, the value in ANDROID_SDK_ROOT is used instead. + */ + +const getAndroidSdkInfo = () => { + if (details.android) { + return details.android; + } + + details.android = { + sdkPath: null, + sdkFrom: null, + installedTargets: [], + }; + + const isValidSDK = (path: string) => { + // todo + + return true; + }; + + const ANDROID_HOME = process.env["ANDROID_HOME"]; + const ANDROID_SDK_ROOT = process.env["ANDROID_SDK_ROOT"]; + + if (ANDROID_HOME && isValidSDK(ANDROID_HOME)) { + details.android.sdkPath = ANDROID_HOME; + details.android.sdkFrom = "ANDROID_HOME"; + return details.android; + } + + if (ANDROID_SDK_ROOT && !ANDROID_HOME && isValidSDK(ANDROID_SDK_ROOT)) { + details.android.sdkPath = ANDROID_SDK_ROOT; + details.android.sdkFrom = "ANDROID_SDK_ROOT"; + return details.android; + } +}; + +const androidSdk: RequirementFunction = async (results) => { + const sdk = getAndroidSdkInfo(); + + if (!sdk.sdkPath) { + return error("Could not find Android SDK"); + } + + return ok(`Found Android SDK at ${sdk.sdkPath} (from ${sdk.sdkFrom})`); +}; + +const ADB_VERSION_RE = /Android Debug Bridge version (.+)\n/im; +const androidAdb: RequirementFunction = async (results) => { + const res = await execSafe("adb --version"); + + if (res) { + const [, version] = res.stdout.match(ADB_VERSION_RE); + + details.adb = { version }; + } + + return error("Could not find adb", "Make sure it's available in your PATH"); +}; + +export const androidSdkRequirements: RequirementFunction[] = [ + androidSdk, + androidAdb, +]; diff --git a/packages/doctorv2/src/requirements/android/index.ts b/packages/doctorv2/src/requirements/android/index.ts new file mode 100644 index 0000000000..d3a4a192f3 --- /dev/null +++ b/packages/doctorv2/src/requirements/android/index.ts @@ -0,0 +1,8 @@ +import { RequirementFunction } from "../.."; +import { androidSdkRequirements } from "./android-sdk"; +import { javaRequirements } from "./java"; + +export const androidRequirements: RequirementFunction[] = [ + ...androidSdkRequirements, + ...javaRequirements, +]; diff --git a/packages/doctorv2/src/requirements/android/java.ts b/packages/doctorv2/src/requirements/android/java.ts new file mode 100644 index 0000000000..465b0e2d5d --- /dev/null +++ b/packages/doctorv2/src/requirements/android/java.ts @@ -0,0 +1,92 @@ +import { existsSync } from "fs"; +import { resolve } from "path"; +import { details, RequirementFunction } from "../.."; +import { execSafe } from "../../helpers/child-process"; +import { error, ok, warn } from "../../helpers/results"; +import { notInRange } from "../../helpers/semver"; + +// import type { RequirementDetails } from "../.."; + +const JAVAC_VERSION_RE = /javac\s(.+)\n/im; +const JAVA_VERSION_RE = /(?:java|openjdk)\s(.+) /im; + +// example: augment details with new values +declare module "../.." { + interface RequirementDetails { + java?: { version: string; path: string }; + javac?: { version: string }; + } +} + +// initialize details... +details.java = null; +details.javac = null; + +const javacRequirement: RequirementFunction = async (results) => { + const JAVA_HOME = process.env["JAVA_HOME"]; + + if (!JAVA_HOME) { + return error("JAVA_HOME is not set"); + } + + // results.push(ok("JAVA_HOME is set")); + + let javaExecutablePath = resolve(JAVA_HOME, "bin/javac"); + + if (!existsSync(javaExecutablePath)) { + javaExecutablePath = null; + results.push( + warn( + "JAVA_HOME does not contain javac", + "make sure your JAVA_HOME points to an JDK and not a JRE" + ) + ); + } + + if (!javaExecutablePath) { + // try resolving from path + javaExecutablePath = await execSafe("which javac").then((res) => { + return res ? res.stdout.trim() : null; + }); + } + + if (!javaExecutablePath) { + return error("Could not find javac", "Make sure you install a JDK"); + } + + const res = await execSafe(`"${javaExecutablePath}" --version`); + if (res) { + const [, version] = res.stdout.match(JAVAC_VERSION_RE); + details.javac = { version }; + // console.log("javac", { version }); + + return ok("javac found"); + } + + return error("javac not found"); +}; + +const javaRequirement: RequirementFunction = async (results) => { + const res = await execSafe(`java --version`); + if (res) { + // console.log(res); + const [, version] = res.stdout.match(JAVA_VERSION_RE); + // console.log("java", { version }); + + // todo: path should be the path to java executable instead of JAVA_HOME... + details.java = { version, path: process.env.JAVA_HOME }; + + if (notInRange(version, { min: "11", max: "17" })) { + return warn("java version might not be supported"); + } + + return ok("java found"); + } + + return error("java not found"); +}; + +export const javaRequirements: RequirementFunction[] = [ + javacRequirement, + javaRequirement, +]; diff --git a/packages/doctorv2/src/requirements/common/index.ts b/packages/doctorv2/src/requirements/common/index.ts new file mode 100644 index 0000000000..940d490e21 --- /dev/null +++ b/packages/doctorv2/src/requirements/common/index.ts @@ -0,0 +1,91 @@ +import { platform, arch, release } from "os"; +import { details, RequirementFunction } from "../.."; +import { returnFalse } from "../../helpers"; +import { exec, execSafe } from "../../helpers/child-process"; +import { error, ok } from "../../helpers/results"; + +declare module "../.." { + interface RequirementDetails { + platform?: string; + arch?: string; + os?: string; + shell?: string; + node?: { version: string }; + nativescript?: { version: string }; + } +} + +details.platform = null; +details.arch = null; +details.os = null; +details.shell = null; +details.node = null; +details.nativescript = null; + +export const commonRequirements: RequirementFunction[] = [ + // platform info + async () => { + details.platform = platform(); + details.arch = arch(); + details.os = await execSafe("uname -a").then((res) => { + return res ? res.stdout : null; + }); + details.shell = process.env.SHELL ?? "bash"; + details.nativescript = null; + }, + + async () => { + const res = await execSafe("node -v"); + + const NODE_VERSION_RE = /v?(.+)\n/im; + + if (res) { + const [, version] = res.stdout.match(NODE_VERSION_RE); + + details.node = { + version, + }; + } + }, + + // nativescript cli + async () => { + const res = await execSafe("ns -v --json"); + + if (res) { + const version = res.stdout.trim(); + + // console.log("nativescript", { + // version, + // }); + details.nativescript = { + version, + }; + + return ok("NativeScript CLI is installed"); + } + return error( + "NativeScript CLI not installed", + "Check your PATH, or install via npm i -g nativescript" + ); + }, +]; + +// async function headRequirement() { +// return ok("Your head is in place"); +// }, +// async () => { +// return ok("The mainframe is stable"); +// }, +// async () => { +// return ok("Coffee is ready"); +// }, +// async () => { +// return ok("The monitor is on"); +// }, +// async () => { +// throw new Error("im bad and I like to fail."); +// }, +// async () => { +// return error("Your brain is missing.", "Drink some more coffee!"); +// }, diff --git a/packages/doctorv2/src/requirements/ios/cocoapods.ts b/packages/doctorv2/src/requirements/ios/cocoapods.ts new file mode 100644 index 0000000000..acf5fcb1e4 --- /dev/null +++ b/packages/doctorv2/src/requirements/ios/cocoapods.ts @@ -0,0 +1,32 @@ +import { execSafe } from "../../helpers/child-process"; +import { error, ok } from "../../helpers/results"; +import { details, RequirementFunction } from "../.."; + +declare module "../.." { + interface RequirementDetails { + cocoapods?: { version: string }; + } +} + +details.cocoapods = null; + +async function CocoaPodsRequirement() { + const res = await execSafe(`pod --version`); + + if (res) { + const version = res.stdout.trim(); + + // console.log("cocoapods", { + // version, + // }); + details.cocoapods = { version }; + + return ok(`CocoaPods is installed`); + } + + return error("CocoaPods is missing"); +} + +export const cocoaPodsRequirements: RequirementFunction[] = [ + CocoaPodsRequirement, +]; diff --git a/packages/doctorv2/src/requirements/ios/index.ts b/packages/doctorv2/src/requirements/ios/index.ts new file mode 100644 index 0000000000..052f56b39f --- /dev/null +++ b/packages/doctorv2/src/requirements/ios/index.ts @@ -0,0 +1,10 @@ +import { RequirementFunction } from "../.."; +import { cocoaPodsRequirements } from "./cocoapods"; +import { pythonRequirements } from "./python"; +import { xcodeRequirements } from "./xcode"; + +export const iosRequirements: RequirementFunction[] = [ + ...pythonRequirements, + ...xcodeRequirements, + ...cocoaPodsRequirements, +]; diff --git a/packages/doctorv2/src/requirements/ios/python.ts b/packages/doctorv2/src/requirements/ios/python.ts new file mode 100644 index 0000000000..adc9d90a1c --- /dev/null +++ b/packages/doctorv2/src/requirements/ios/python.ts @@ -0,0 +1,53 @@ +import { exec, execSafe } from "../../helpers/child-process"; +import { error, ok, warn } from "../../helpers/results"; +import { details, RequirementFunction } from "../.."; + +const VERSION_RE = /Python\s(.+)\n/; + +declare module "../.." { + interface RequirementDetails { + python?: { version: string }; + } +} + +details.python = null; + +async function PythonRequirement() { + const res = await execSafe(`python3 --version`); + + if (res) { + const [, version] = res.stdout.match(VERSION_RE); + // console.log("python", { + // version, + // }); + + details.python = { version }; + } + + try { + await exec(`python3 -c "import six"`); + // prettier-ignore + return [ + ok(`Python3 is installed`), + ok('Python3 "six" is installed') + ]; + } catch (err) { + if (err.code === 1) { + // error.code = 1 means Python is found, but failed to import "six" + return [ + ok("Python3 is installed"), + warn( + `Python3 "six" is not installed.`, + "Some debugger features might not work correctly" + ), + ]; + } + } + + return [ + error(`Python3 is not installed`), + error(`Python3 "six" is not installed.`), + ]; +} + +export const pythonRequirements: RequirementFunction[] = [PythonRequirement]; diff --git a/packages/doctorv2/src/requirements/ios/xcode.ts b/packages/doctorv2/src/requirements/ios/xcode.ts new file mode 100644 index 0000000000..443b278e92 --- /dev/null +++ b/packages/doctorv2/src/requirements/ios/xcode.ts @@ -0,0 +1,64 @@ +import { execSafe } from "../../helpers/child-process"; +import { error, ok } from "../../helpers/results"; +import { details, RequirementFunction } from "../.."; + +const VERSION_RE = /Xcode\s(.+)\n/; +const BUILD_VERSION_RE = /Build version\s(.+)\n/; + +declare module "../.." { + interface RequirementDetails { + xcode?: { version: string; buildVersion: string }; + xcodeproj?: { version: string }; + } +} + +details.xcode = null; +details.xcodeproj = null; + +async function XCodeRequirement() { + const res = await execSafe(`xcodebuild -version`); + + if (res) { + const [, version] = res.stdout.match(VERSION_RE); + const [, buildVersion] = res.stdout.match(BUILD_VERSION_RE); + // console.log("xcode", { + // version, + // buildVersion, + // }); + + details.xcode = { + version, + buildVersion, + }; + // prettier-ignore + return ok(`XCode is installed`) + } + + return error( + `XCode is missing.`, + `Install XCode through the AppStore (or download from https://developer.apple.com/)` + ); +} + +async function XCodeProjRequirement() { + const res = await execSafe(`xcodeproj --version`); + + if (res) { + const version = res.stdout.trim(); + + // console.log("xcodeproj", { + // version, + // }); + + details.xcodeproj = { version }; + + return ok(`xcodeproj is installed`); + } + + return error("xcodeproj is missing"); +} + +export const xcodeRequirements: RequirementFunction[] = [ + XCodeRequirement, + XCodeProjRequirement, +]; diff --git a/packages/doctorv2/tsconfig.json b/packages/doctorv2/tsconfig.json new file mode 100644 index 0000000000..86edca173c --- /dev/null +++ b/packages/doctorv2/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["ESNext"], + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "outDir": "./dist" + }, + "include": ["src/"] +} diff --git a/packages/doctorv2/yarn.lock b/packages/doctorv2/yarn.lock new file mode 100644 index 0000000000..5f3dd63899 --- /dev/null +++ b/packages/doctorv2/yarn.lock @@ -0,0 +1,32 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +typescript@~4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== From 3a8168b1962da9fe4551621cce44c560ef0146d4 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Fri, 18 Mar 2022 18:24:28 +0100 Subject: [PATCH 2/5] feat: additional information and cleanups --- packages/doctorv2/src/helpers/index.ts | 21 +++ packages/doctorv2/src/helpers/semver.ts | 25 ++- packages/doctorv2/src/index.ts | 48 ++++- .../src/requirements/android/android-sdk.ts | 111 +++++++++++- .../src/requirements/android/index.ts | 1 + .../doctorv2/src/requirements/android/java.ts | 59 ++++--- .../doctorv2/src/requirements/common/index.ts | 166 +++++++++++++----- .../src/requirements/ios/cocoapods.ts | 18 +- .../doctorv2/src/requirements/ios/index.ts | 2 +- .../doctorv2/src/requirements/ios/python.ts | 52 +++--- .../doctorv2/src/requirements/ios/xcode.ts | 16 +- 11 files changed, 392 insertions(+), 127 deletions(-) diff --git a/packages/doctorv2/src/helpers/index.ts b/packages/doctorv2/src/helpers/index.ts index 01d4121000..8e89b755e7 100644 --- a/packages/doctorv2/src/helpers/index.ts +++ b/packages/doctorv2/src/helpers/index.ts @@ -1,2 +1,23 @@ export const returnFalse: () => false = () => false; export const returnNull: () => null = () => null; + +export const safeMatch = (text: string, regex: RegExp) => { + const match = text.match(regex); + + if (Array.isArray(match)) { + return match; + } + + return []; +}; + +export const safeMatchAll = (text: string, regex: RegExp) => { + const matches = []; + let match = null; + + while ((match = regex.exec(text)) !== null) { + matches.push(match); + } + + return matches; +}; diff --git a/packages/doctorv2/src/helpers/semver.ts b/packages/doctorv2/src/helpers/semver.ts index 4df4ac752d..959303d73e 100644 --- a/packages/doctorv2/src/helpers/semver.ts +++ b/packages/doctorv2/src/helpers/semver.ts @@ -2,11 +2,22 @@ import * as semver from "semver"; export function isInRange( version: string, - range: { min: string; max: string } + range: { min?: string; max?: string } ) { try { const _version = semver.coerce(version); - const _range = `${range.min} - ${range.max}`; + + let _range: string; + if (range.min && !range.max) { + _range = `>=${range.min}`; + } else if (range.max && !range.min) { + _range = `<=${range.max}`; + } else if (range.min && range.max) { + _range = `${range.min} - ${range.max}`; + } else { + // no min or max - return true + return true; + } const _inRange = semver.satisfies(_version, _range); // console.log({ @@ -24,7 +35,15 @@ export function isInRange( export function notInRange( version: string, - range: { min: string; max: string } + range: { min?: string; max?: string } ) { return !isInRange(version, range); } + +// export function padVersion(version: string, digits = 3) { +// if (version) { +// const zeroesToAppend = digits - version.split(".").length; +// return version + ".0".repeat(zeroesToAppend); +// } +// return version; +// } diff --git a/packages/doctorv2/src/index.ts b/packages/doctorv2/src/index.ts index 30eec329dc..fa37b19400 100644 --- a/packages/doctorv2/src/index.ts +++ b/packages/doctorv2/src/index.ts @@ -1,4 +1,4 @@ -import { redBright, yellowBright, green } from "ansi-colors"; +import { redBright, yellowBright, green, gray } from "ansi-colors"; export type TPlatform = "android" | "ios"; @@ -115,12 +115,44 @@ function printResults(res: IRequirementResult[]) { console.log(line); }); console.log(""); - console.log( - ` ${green(`${stats[ResultType.OK]} ok`)}, ${redBright( - `${stats[ResultType.ERROR]} errors` - )}, ${yellowBright(`${stats[ResultType.WARN]} warnings`)} / ${ - stats.total - } total` - ); + + const pluralize = (count: number, singular: string, plural: string) => { + if (count === 0 || count > 1) { + return plural; + } + return singular; + }; + + const oks = + stats[ResultType.OK] > 0 + ? green(`${stats[ResultType.OK]} ok`) + : gray(`${stats[ResultType.OK]} ok`); + const errors = + stats[ResultType.ERROR] > 0 + ? redBright( + `${stats[ResultType.ERROR]} ${pluralize( + stats[ResultType.ERROR], + "error", + "errors" + )}` + ) + : gray(`${stats[ResultType.ERROR]} errors`); + const warnings = + stats[ResultType.WARN] > 0 + ? yellowBright( + `${stats[ResultType.WARN]} ${pluralize( + stats[ResultType.WARN], + "warning", + "warnings" + )}` + ) + : gray(`${stats[ResultType.WARN]} warnings`); + + console.log(` ${oks}, ${warnings}, ${errors} / ${stats.total} total`); + console.log(""); + + if (stats[ResultType.ERROR] === 0) { + console.log(green.bold("No issues detected.")); + } } diff --git a/packages/doctorv2/src/requirements/android/android-sdk.ts b/packages/doctorv2/src/requirements/android/android-sdk.ts index 8418e5d296..13ae64aae6 100644 --- a/packages/doctorv2/src/requirements/android/android-sdk.ts +++ b/packages/doctorv2/src/requirements/android/android-sdk.ts @@ -1,5 +1,9 @@ -import { details, RequirementFunction } from "../.."; +import { existsSync, readdirSync } from "fs"; +import { resolve } from "path"; + +import { safeMatch, safeMatchAll } from "../../helpers"; import { execSafe } from "../../helpers/child-process"; +import { details, RequirementFunction } from "../.."; import { error, ok } from "../../helpers/results"; // example: augment details with new values @@ -9,6 +13,9 @@ declare module "../.." { sdkPath: string; sdkFrom: string; installedTargets: string[]; + installedBuildTools: string[]; + installedNDKVersions: string[]; + installedSystemImages: string[]; }; adb?: { version: string }; } @@ -39,6 +46,9 @@ const getAndroidSdkInfo = () => { sdkPath: null, sdkFrom: null, installedTargets: [], + installedBuildTools: [], + installedNDKVersions: [], + installedSystemImages: [], }; const isValidSDK = (path: string) => { @@ -67,26 +77,117 @@ const androidSdk: RequirementFunction = async (results) => { const sdk = getAndroidSdkInfo(); if (!sdk.sdkPath) { - return error("Could not find Android SDK"); + return error(`Could not find Android SDK`); } return ok(`Found Android SDK at ${sdk.sdkPath} (from ${sdk.sdkFrom})`); }; +const androidTargets: RequirementFunction = async (results) => { + const sdk = getAndroidSdkInfo(); + + if (!sdk.sdkPath) { + return; + } + + const sdkPlatformsPath = resolve(sdk.sdkPath, "platforms"); + if (existsSync(sdkPlatformsPath)) { + details.android.installedTargets = readdirSync(sdkPlatformsPath); + } +}; + +const androidBuildTools: RequirementFunction = async (results) => { + const sdk = getAndroidSdkInfo(); + + if (!sdk.sdkPath) { + return; + } + + const sdkBuildToolsPath = resolve(sdk.sdkPath, "build-tools"); + if (existsSync(sdkBuildToolsPath)) { + details.android.installedBuildTools = readdirSync(sdkBuildToolsPath); + } +}; + +const androidNDK: RequirementFunction = async (results) => { + const sdk = getAndroidSdkInfo(); + + if (!sdk.sdkPath) { + return; + } + + const sdkNDKPath = resolve(sdk.sdkPath, "ndk"); + if (existsSync(sdkNDKPath)) { + details.android.installedNDKVersions = readdirSync(sdkNDKPath); + } +}; + +const ANDROID_IMAGE_RE = /system-images;([\S \t]+)/g; +const androidImages: RequirementFunction = async (results) => { + const sdk = getAndroidSdkInfo(); + + if (!sdk.sdkPath) { + return; + } + + const possibleSdkManagers = [ + resolve(sdk.sdkPath, "tools/bin/sdkmanager"), + resolve(sdk.sdkPath, "cmdline-tools/latest/bin/sdkmanager"), + ]; + + for (const sdkManagerPath of possibleSdkManagers) { + const res = await execSafe(`"${sdkManagerPath}" --list`); + + if (res) { + const matches = safeMatchAll( + res.stdout.split("Available")[0], + ANDROID_IMAGE_RE + ); + + const images = matches + // output from sdkManager: + // android-17;google_apis;x86 | 7 | Google APIs Intel x86 Atom System Image | system-images/android-17/google_apis/x86 + .map(([, match]) => match.split("|").map((part: string) => part.trim())) + // image: android-17;google_apis;x86 + // _: 7 + // details: Google APIs Intel x86 Atom System Image + // system-images/android-17/google_apis/x86 + .map(([image, _, details]) => { + const version = image.split(";")[0]; + const deatails = details.replace(" System Image", ""); + + return `${version} | ${deatails}`; + }); + + details.android.installedSystemImages = images; + // break the loop on first successful sdkmanager output + break; + } + } +}; + const ADB_VERSION_RE = /Android Debug Bridge version (.+)\n/im; const androidAdb: RequirementFunction = async (results) => { const res = await execSafe("adb --version"); if (res) { - const [, version] = res.stdout.match(ADB_VERSION_RE); - + const [, version] = safeMatch(res.stdout, ADB_VERSION_RE); details.adb = { version }; + + return ok(`adb from the Android SDK is found (${version})`); } - return error("Could not find adb", "Make sure it's available in your PATH"); + return error( + `Could not find adb from the Android SDK`, + `Make sure you have a valid Android SDK installed, and it's available in your PATH` + ); }; export const androidSdkRequirements: RequirementFunction[] = [ androidSdk, + androidTargets, + androidBuildTools, + androidNDK, + androidImages, androidAdb, ]; diff --git a/packages/doctorv2/src/requirements/android/index.ts b/packages/doctorv2/src/requirements/android/index.ts index d3a4a192f3..965938bd40 100644 --- a/packages/doctorv2/src/requirements/android/index.ts +++ b/packages/doctorv2/src/requirements/android/index.ts @@ -1,4 +1,5 @@ import { RequirementFunction } from "../.."; + import { androidSdkRequirements } from "./android-sdk"; import { javaRequirements } from "./java"; diff --git a/packages/doctorv2/src/requirements/android/java.ts b/packages/doctorv2/src/requirements/android/java.ts index 465b0e2d5d..d4e4bd1231 100644 --- a/packages/doctorv2/src/requirements/android/java.ts +++ b/packages/doctorv2/src/requirements/android/java.ts @@ -1,9 +1,11 @@ import { existsSync } from "fs"; import { resolve } from "path"; -import { details, RequirementFunction } from "../.."; -import { execSafe } from "../../helpers/child-process"; + import { error, ok, warn } from "../../helpers/results"; +import { execSafe } from "../../helpers/child-process"; +import { details, RequirementFunction } from "../.."; import { notInRange } from "../../helpers/semver"; +import { safeMatch } from "../../helpers"; // import type { RequirementDetails } from "../.."; @@ -22,6 +24,29 @@ declare module "../.." { details.java = null; details.javac = null; +const javaRequirement: RequirementFunction = async (results) => { + const res = await execSafe(`java --version`); + if (res) { + // console.log(res); + const [, version] = safeMatch(res.stdout, JAVA_VERSION_RE); + // console.log("java", { version }); + + // todo: path should be the path to java executable instead of JAVA_HOME... + details.java = { version, path: process.env.JAVA_HOME }; + + if (notInRange(version, { min: "11", max: "17" })) { + return warn( + `java executable found (${version}), but version might not be supported`, + `The installed java version may not work` + ); + } + + return ok(`java executable found (${version})`); + } + + return error("java executable not found"); +}; + const javacRequirement: RequirementFunction = async (results) => { const JAVA_HOME = process.env["JAVA_HOME"]; @@ -51,42 +76,22 @@ const javacRequirement: RequirementFunction = async (results) => { } if (!javaExecutablePath) { - return error("Could not find javac", "Make sure you install a JDK"); + return error(`javac executable not found`, `Make sure you install a JDK`); } const res = await execSafe(`"${javaExecutablePath}" --version`); if (res) { - const [, version] = res.stdout.match(JAVAC_VERSION_RE); + const [, version] = safeMatch(res.stdout, JAVAC_VERSION_RE); details.javac = { version }; // console.log("javac", { version }); - return ok("javac found"); + return ok(`javac executable found (${version})`); } - return error("javac not found"); -}; - -const javaRequirement: RequirementFunction = async (results) => { - const res = await execSafe(`java --version`); - if (res) { - // console.log(res); - const [, version] = res.stdout.match(JAVA_VERSION_RE); - // console.log("java", { version }); - - // todo: path should be the path to java executable instead of JAVA_HOME... - details.java = { version, path: process.env.JAVA_HOME }; - - if (notInRange(version, { min: "11", max: "17" })) { - return warn("java version might not be supported"); - } - - return ok("java found"); - } - - return error("java not found"); + return error(`javac executable not found`); }; export const javaRequirements: RequirementFunction[] = [ - javacRequirement, javaRequirement, + javacRequirement, ]; diff --git a/packages/doctorv2/src/requirements/common/index.ts b/packages/doctorv2/src/requirements/common/index.ts index 940d490e21..26848c472c 100644 --- a/packages/doctorv2/src/requirements/common/index.ts +++ b/packages/doctorv2/src/requirements/common/index.ts @@ -1,74 +1,146 @@ -import { platform, arch, release } from "os"; +import { platform, arch, cpus } from "os"; + +import { error, ok, warn } from "../../helpers/results"; +import { execSafe } from "../../helpers/child-process"; import { details, RequirementFunction } from "../.."; -import { returnFalse } from "../../helpers"; -import { exec, execSafe } from "../../helpers/child-process"; -import { error, ok } from "../../helpers/results"; +import { safeMatch } from "../../helpers"; declare module "../.." { interface RequirementDetails { + os?: string; platform?: string; arch?: string; - os?: string; + cpu?: string; shell?: string; node?: { version: string }; + npm?: { version: string }; + yarn?: { version: string }; + pnpm?: { version: string }; nativescript?: { version: string }; } } +details.os = null; details.platform = null; details.arch = null; -details.os = null; +details.cpu = null; details.shell = null; details.node = null; +details.npm = null; +details.yarn = null; +details.pnpm = null; details.nativescript = null; -export const commonRequirements: RequirementFunction[] = [ - // platform info - async () => { - details.platform = platform(); - details.arch = arch(); - details.os = await execSafe("uname -a").then((res) => { - return res ? res.stdout : null; - }); - details.shell = process.env.SHELL ?? "bash"; - details.nativescript = null; - }, - - async () => { - const res = await execSafe("node -v"); - - const NODE_VERSION_RE = /v?(.+)\n/im; - - if (res) { - const [, version] = res.stdout.match(NODE_VERSION_RE); - - details.node = { - version, - }; - } - }, +const platformInfo: RequirementFunction = async () => { + details.os = await execSafe("uname -a").then((res) => { + return res ? res.stdout : null; + }); + details.platform = platform(); + details.arch = arch(); + + try { + const _cpus = cpus(); + details.cpu = "(" + _cpus.length + ") " + arch() + " " + _cpus[0].model; + } catch (err) { + details.cpu = "Unknown"; + } + + details.shell = process.env.SHELL ?? "bash"; +}; + +const NODE_VERSION_RE = /v?(.+)\n/im; +const node: RequirementFunction = async () => { + const res = await execSafe("node -v"); + + if (res) { + const [, version] = safeMatch(res.stdout, NODE_VERSION_RE); + + details.node = { + version, + }; + } +}; + +const NPM_VERSION_RE = /v?(.+)\n/im; +const npm: RequirementFunction = async () => { + const res = await execSafe("npm -v"); + + if (res) { + const [, version] = safeMatch(res.stdout, NPM_VERSION_RE); - // nativescript cli - async () => { - const res = await execSafe("ns -v --json"); + details.npm = { + version, + }; + } +}; + +const YARN_VERSION_RE = /(.+)\n/im; +const yarn: RequirementFunction = async () => { + const res = await execSafe("yarn -v"); + + if (res) { + const [, version] = safeMatch(res.stdout, YARN_VERSION_RE); + + details.yarn = { + version, + }; + } +}; + +const PNPM_VERSION_RE = /(.+)\n/im; +const pnpm: RequirementFunction = async () => { + const res = await execSafe("pnpm -v"); + + if (res) { + const [, version] = safeMatch(res.stdout, PNPM_VERSION_RE); + + details.pnpm = { + version, + }; + } +}; + +const NATIVESCRIPT_VERSION_RE = /^(.+)\n/im; +const nativescriptCli: RequirementFunction = async () => { + const res = await execSafe("ns -v"); - if (res) { - const version = res.stdout.trim(); + if (res) { + const [, version] = safeMatch(res.stdout, NATIVESCRIPT_VERSION_RE); - // console.log("nativescript", { - // version, - // }); - details.nativescript = { - version, - }; + const isUpToDate = res.stdout.includes("Up to date."); - return ok("NativeScript CLI is installed"); + // console.log("nativescript", { + // version, + // }); + details.nativescript = { + version, + }; + + if (isUpToDate) { + return ok(`NativeScript CLI is installed (${version})`); } - return error( - "NativeScript CLI not installed", - "Check your PATH, or install via npm i -g nativescript" + + const NATIVESCRIPT_NEW_VERSION_RE = /(New version.+)\n/; + const [, message] = safeMatch(res.stdout, NATIVESCRIPT_NEW_VERSION_RE); + + return warn( + `NativeScript CLI update available (${version})`, + message ?? "Update available" ); - }, + } + return error( + "NativeScript CLI not installed", + "Check your PATH, or install via npm i -g nativescript" + ); +}; + +export const commonRequirements: RequirementFunction[] = [ + platformInfo, + node, + npm, + yarn, + pnpm, + nativescriptCli, ]; // async function headRequirement() { diff --git a/packages/doctorv2/src/requirements/ios/cocoapods.ts b/packages/doctorv2/src/requirements/ios/cocoapods.ts index acf5fcb1e4..7d224b9eae 100644 --- a/packages/doctorv2/src/requirements/ios/cocoapods.ts +++ b/packages/doctorv2/src/requirements/ios/cocoapods.ts @@ -1,6 +1,7 @@ import { execSafe } from "../../helpers/child-process"; -import { error, ok } from "../../helpers/results"; import { details, RequirementFunction } from "../.."; +import { error, ok } from "../../helpers/results"; +import { notInRange } from "../../helpers/semver"; declare module "../.." { interface RequirementDetails { @@ -21,10 +22,21 @@ async function CocoaPodsRequirement() { // }); details.cocoapods = { version }; - return ok(`CocoaPods is installed`); + const minVersion = "1.0.0"; + if (notInRange(version, { min: minVersion })) { + return error( + `CocoaPods is installed (${version}) but does not satisfy the minimum version of ${minVersion}`, + `Update CocoaPods to at least ${minVersion}` + ); + } + + return ok(`CocoaPods is installed (${version})`); } - return error("CocoaPods is missing"); + return error( + `CocoaPods is missing`, + `You need to install CocoaPods to be able to build and run projects.` + ); } export const cocoaPodsRequirements: RequirementFunction[] = [ diff --git a/packages/doctorv2/src/requirements/ios/index.ts b/packages/doctorv2/src/requirements/ios/index.ts index 052f56b39f..6da0e87255 100644 --- a/packages/doctorv2/src/requirements/ios/index.ts +++ b/packages/doctorv2/src/requirements/ios/index.ts @@ -1,6 +1,6 @@ -import { RequirementFunction } from "../.."; import { cocoaPodsRequirements } from "./cocoapods"; import { pythonRequirements } from "./python"; +import { RequirementFunction } from "../.."; import { xcodeRequirements } from "./xcode"; export const iosRequirements: RequirementFunction[] = [ diff --git a/packages/doctorv2/src/requirements/ios/python.ts b/packages/doctorv2/src/requirements/ios/python.ts index adc9d90a1c..4e02045299 100644 --- a/packages/doctorv2/src/requirements/ios/python.ts +++ b/packages/doctorv2/src/requirements/ios/python.ts @@ -1,6 +1,7 @@ -import { exec, execSafe } from "../../helpers/child-process"; import { error, ok, warn } from "../../helpers/results"; +import { execSafe } from "../../helpers/child-process"; import { details, RequirementFunction } from "../.."; +import { safeMatch } from "../../helpers"; const VERSION_RE = /Python\s(.+)\n/; @@ -12,42 +13,39 @@ declare module "../.." { details.python = null; -async function PythonRequirement() { +const pythonRequirement: RequirementFunction = async () => { const res = await execSafe(`python3 --version`); if (res) { - const [, version] = res.stdout.match(VERSION_RE); + const [, version] = safeMatch(res.stdout, VERSION_RE); // console.log("python", { // version, // }); details.python = { version }; + + return ok(`Python is installed (${version})`); } + return error( + `Python (3.x) is not installed`, + `Make sure you have 'python3' in your PATH` + ); +}; + +const pythonSixRequirement: RequirementFunction = async () => { + const hasSix = await execSafe(`python3 -c "import six"`); - try { - await exec(`python3 -c "import six"`); - // prettier-ignore - return [ - ok(`Python3 is installed`), - ok('Python3 "six" is installed') - ]; - } catch (err) { - if (err.code === 1) { - // error.code = 1 means Python is found, but failed to import "six" - return [ - ok("Python3 is installed"), - warn( - `Python3 "six" is not installed.`, - "Some debugger features might not work correctly" - ), - ]; - } + if (hasSix) { + return ok(`Python package "six" is installed`); } - return [ - error(`Python3 is not installed`), - error(`Python3 "six" is not installed.`), - ]; -} + return warn( + `Python package "six" is not installed`, + "Some debugger features might not work correctly" + ); +}; -export const pythonRequirements: RequirementFunction[] = [PythonRequirement]; +export const pythonRequirements: RequirementFunction[] = [ + pythonRequirement, + pythonSixRequirement, +]; diff --git a/packages/doctorv2/src/requirements/ios/xcode.ts b/packages/doctorv2/src/requirements/ios/xcode.ts index 443b278e92..2c611b4f60 100644 --- a/packages/doctorv2/src/requirements/ios/xcode.ts +++ b/packages/doctorv2/src/requirements/ios/xcode.ts @@ -1,6 +1,7 @@ import { execSafe } from "../../helpers/child-process"; -import { error, ok } from "../../helpers/results"; import { details, RequirementFunction } from "../.."; +import { error, ok } from "../../helpers/results"; +import { safeMatch } from "../../helpers"; const VERSION_RE = /Xcode\s(.+)\n/; const BUILD_VERSION_RE = /Build version\s(.+)\n/; @@ -19,8 +20,8 @@ async function XCodeRequirement() { const res = await execSafe(`xcodebuild -version`); if (res) { - const [, version] = res.stdout.match(VERSION_RE); - const [, buildVersion] = res.stdout.match(BUILD_VERSION_RE); + const [, version] = safeMatch(res.stdout, VERSION_RE); + const [, buildVersion] = safeMatch(res.stdout, BUILD_VERSION_RE); // console.log("xcode", { // version, // buildVersion, @@ -31,7 +32,7 @@ async function XCodeRequirement() { buildVersion, }; // prettier-ignore - return ok(`XCode is installed`) + return ok(`XCode is installed (${version} / ${buildVersion})`) } return error( @@ -52,10 +53,13 @@ async function XCodeProjRequirement() { details.xcodeproj = { version }; - return ok(`xcodeproj is installed`); + return ok(`xcodeproj is installed (${version})`); } - return error("xcodeproj is missing"); + return error( + `xcodeproj is missing`, + `The xcodeproj gem is required to build projects.` + ); } export const xcodeRequirements: RequirementFunction[] = [ From 748cfc0b9b9a30c728b23c8b9725af7d94d25739 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Fri, 18 Mar 2022 19:54:24 +0100 Subject: [PATCH 3/5] refactor: extract printer to it's own file --- packages/doctorv2/src/index.ts | 82 +------------------- packages/doctorv2/src/printers/pretty.ts | 95 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 81 deletions(-) create mode 100644 packages/doctorv2/src/printers/pretty.ts diff --git a/packages/doctorv2/src/index.ts b/packages/doctorv2/src/index.ts index fa37b19400..709e1742fc 100644 --- a/packages/doctorv2/src/index.ts +++ b/packages/doctorv2/src/index.ts @@ -1,4 +1,4 @@ -import { redBright, yellowBright, green, gray } from "ansi-colors"; +import { printResults } from "./printers/pretty"; export type TPlatform = "android" | "ios"; @@ -12,12 +12,6 @@ export const enum ResultType { WARN = "WARN", } -const resultTypeColorMap = { - [ResultType.ERROR]: redBright, - [ResultType.OK]: green, - [ResultType.WARN]: yellowBright, -}; - export interface IRequirementResult { type: ResultType; message: string; @@ -82,77 +76,3 @@ Promise.allSettled(promises).then((results) => { printResults(filtered); }); - -// "custom reporter" that prints the results - should live outside of this package... -function printResults(res: IRequirementResult[]) { - const stats = { - total: 0, - [ResultType.OK]: 0, - [ResultType.WARN]: 0, - [ResultType.ERROR]: 0, - }; - console.log(""); - res - .map((requirementResult) => { - const color = resultTypeColorMap[requirementResult.type]; - const pad = " ".repeat(5 - requirementResult.type.length); - - stats.total++; - stats[requirementResult.type]++; - - const details = requirementResult.details - ? `\n ${pad}${" ".repeat(2 + requirementResult.type.length)} - ` + - requirementResult.details - : ""; - - return ( - ` ${pad}[${color(requirementResult.type)}] ${ - requirementResult.message - }` + details - ); - }) - .forEach((line) => { - console.log(line); - }); - console.log(""); - - const pluralize = (count: number, singular: string, plural: string) => { - if (count === 0 || count > 1) { - return plural; - } - return singular; - }; - - const oks = - stats[ResultType.OK] > 0 - ? green(`${stats[ResultType.OK]} ok`) - : gray(`${stats[ResultType.OK]} ok`); - const errors = - stats[ResultType.ERROR] > 0 - ? redBright( - `${stats[ResultType.ERROR]} ${pluralize( - stats[ResultType.ERROR], - "error", - "errors" - )}` - ) - : gray(`${stats[ResultType.ERROR]} errors`); - const warnings = - stats[ResultType.WARN] > 0 - ? yellowBright( - `${stats[ResultType.WARN]} ${pluralize( - stats[ResultType.WARN], - "warning", - "warnings" - )}` - ) - : gray(`${stats[ResultType.WARN]} warnings`); - - console.log(` ${oks}, ${warnings}, ${errors} / ${stats.total} total`); - - console.log(""); - - if (stats[ResultType.ERROR] === 0) { - console.log(green.bold("No issues detected.")); - } -} diff --git a/packages/doctorv2/src/printers/pretty.ts b/packages/doctorv2/src/printers/pretty.ts new file mode 100644 index 0000000000..7096104f7c --- /dev/null +++ b/packages/doctorv2/src/printers/pretty.ts @@ -0,0 +1,95 @@ +import { redBright, yellowBright, green, gray } from "ansi-colors"; + +import { IRequirementResult, ResultType } from ".."; + +const resultTypePrefix = { + [ResultType.OK]: ` [${green("OK")}]`, + [ResultType.WARN]: ` [${yellowBright("WARN")}]`, + [ResultType.ERROR]: `[${redBright("ERROR")}]`, +}; + +const indent = " ".repeat(1); +// 7 = longest prefix [ERROR] length +const padding = indent + " ".repeat(7); + +export function printResults(res: IRequirementResult[]) { + const stats = { + total: 0, + [ResultType.OK]: 0, + [ResultType.WARN]: 0, + [ResultType.ERROR]: 0, + }; + console.log(""); + res + .map((requirementResult) => { + // increment stats counters + stats.total++; + stats[requirementResult.type]++; + + const prefix = resultTypePrefix[requirementResult.type]; + const details = requirementResult.details?.split("\n") ?? []; + let paddedDetails = details + .map((line) => { + if (line.length) { + return `${padding} → ` + line; + } + }) + .filter(Boolean) + .join("\n"); + + // if we have details, we need to insert a newline + if (paddedDetails.length) { + paddedDetails = "\n" + paddedDetails + "\n"; + } + + return `${indent}${prefix} ${requirementResult.message}${paddedDetails}`; + }) + .forEach((line) => { + console.log(line); + }); + console.log(""); + + const pluralize = (count: number, singular: string, plural: string) => { + if (count === 0 || count > 1) { + return plural; + } + return singular; + }; + + const oks = + stats[ResultType.OK] > 0 + ? green(`${stats[ResultType.OK]} ok`) + : gray(`${stats[ResultType.OK]} ok`); + const errors = + stats[ResultType.ERROR] > 0 + ? redBright( + `${stats[ResultType.ERROR]} ${pluralize( + stats[ResultType.ERROR], + "error", + "errors" + )}` + ) + : gray(`${stats[ResultType.ERROR]} errors`); + const warnings = + stats[ResultType.WARN] > 0 + ? yellowBright( + `${stats[ResultType.WARN]} ${pluralize( + stats[ResultType.WARN], + "warning", + "warnings" + )}` + ) + : gray(`${stats[ResultType.WARN]} warnings`); + + console.log(`${indent}${oks}, ${warnings}, ${errors} / ${stats.total} total`); + + console.log(""); + + if (stats[ResultType.ERROR] === 0) { + console.log(green.bold(`${indent}√ No issues detected.`)); + console.log(""); + } else { + console.log(redBright.bold(`${indent}× Some issues detected.`)); + console.log(""); + } +} From af467a5b371c110ef6660b5bf747d9d655fe03d7 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 23 Mar 2022 00:23:36 +0100 Subject: [PATCH 4/5] chore: cleanup --- packages/doctorv2/src/printers/pretty.ts | 20 ++++++++- .../src/requirements/android/android-sdk.ts | 43 +++++++++++++++---- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/doctorv2/src/printers/pretty.ts b/packages/doctorv2/src/printers/pretty.ts index 7096104f7c..9452b3e940 100644 --- a/packages/doctorv2/src/printers/pretty.ts +++ b/packages/doctorv2/src/printers/pretty.ts @@ -19,6 +19,7 @@ export function printResults(res: IRequirementResult[]) { [ResultType.WARN]: 0, [ResultType.ERROR]: 0, }; + let lastResultType; console.log(""); res .map((requirementResult) => { @@ -42,7 +43,24 @@ export function printResults(res: IRequirementResult[]) { paddedDetails = "\n" + paddedDetails + "\n"; } - return `${indent}${prefix} ${requirementResult.message}${paddedDetails}`; + // todo: implement verbose mode to print OK result details + // strip them for now... + if (paddedDetails.length && requirementResult.type === ResultType.OK) { + paddedDetails = ""; + } + + let optionalNewLine = ""; + + if ( + lastResultType === ResultType.OK && + requirementResult.type !== ResultType.OK + ) { + optionalNewLine = "\n"; + } + + lastResultType = requirementResult.type; + + return `${optionalNewLine}${indent}${prefix} ${requirementResult.message}${paddedDetails}`; }) .forEach((line) => { console.log(line); diff --git a/packages/doctorv2/src/requirements/android/android-sdk.ts b/packages/doctorv2/src/requirements/android/android-sdk.ts index 13ae64aae6..662730b311 100644 --- a/packages/doctorv2/src/requirements/android/android-sdk.ts +++ b/packages/doctorv2/src/requirements/android/android-sdk.ts @@ -4,7 +4,7 @@ import { resolve } from "path"; import { safeMatch, safeMatchAll } from "../../helpers"; import { execSafe } from "../../helpers/child-process"; import { details, RequirementFunction } from "../.."; -import { error, ok } from "../../helpers/results"; +import { error, ok, warn } from "../../helpers/results"; // example: augment details with new values declare module "../.." { @@ -36,7 +36,6 @@ details.adb = null; * - If ANDROID_HOME is not defined, the value in ANDROID_SDK_ROOT is used. * - If ANDROID_HOME is defined but does not exist or does not contain a valid SDK installation, the value in ANDROID_SDK_ROOT is used instead. */ - const getAndroidSdkInfo = () => { if (details.android) { return details.android; @@ -77,10 +76,10 @@ const androidSdk: RequirementFunction = async (results) => { const sdk = getAndroidSdkInfo(); if (!sdk.sdkPath) { - return error(`Could not find Android SDK`); + return error(`Android SDK: Could not find an Android SDK`); } - return ok(`Found Android SDK at ${sdk.sdkPath} (from ${sdk.sdkFrom})`); + return ok(`Android SDK: found at "${sdk.sdkPath}" (from ${sdk.sdkFrom})`); }; const androidTargets: RequirementFunction = async (results) => { @@ -93,7 +92,17 @@ const androidTargets: RequirementFunction = async (results) => { const sdkPlatformsPath = resolve(sdk.sdkPath, "platforms"); if (existsSync(sdkPlatformsPath)) { details.android.installedTargets = readdirSync(sdkPlatformsPath); + + return ok( + `Android SDK: found valid targets`, + details.android.installedTargets.join("\n") + ); } + + return warn( + `Android SDK: no targets found`, + `Make sure to install at least one target through Android Studio (or sdkmanager)` + ); }; const androidBuildTools: RequirementFunction = async (results) => { @@ -106,7 +115,17 @@ const androidBuildTools: RequirementFunction = async (results) => { const sdkBuildToolsPath = resolve(sdk.sdkPath, "build-tools"); if (existsSync(sdkBuildToolsPath)) { details.android.installedBuildTools = readdirSync(sdkBuildToolsPath); + + return ok( + `Android SDK: found valid build tools`, + details.android.installedBuildTools.join("\n") + ); } + + return error( + `Android SDK: no build tools found`, + `Make sure to install at least one build tool version through Android Studio (or sdkmanager)` + ); }; const androidNDK: RequirementFunction = async (results) => { @@ -160,10 +179,18 @@ const androidImages: RequirementFunction = async (results) => { }); details.android.installedSystemImages = images; - // break the loop on first successful sdkmanager output - break; + + return ok( + `Android SDK: found emulator images`, + details.android.installedSystemImages.join("\n") + ); } } + + return warn( + `Android SDK: emulator images found`, + `You will not be able to run apps in an emulator.\nMake sure to install at least one emulator image through Android Studio (or sdkmanager)` + ); }; const ADB_VERSION_RE = /Android Debug Bridge version (.+)\n/im; @@ -174,11 +201,11 @@ const androidAdb: RequirementFunction = async (results) => { const [, version] = safeMatch(res.stdout, ADB_VERSION_RE); details.adb = { version }; - return ok(`adb from the Android SDK is found (${version})`); + return ok(`Android SDK: found adb (${version})`); } return error( - `Could not find adb from the Android SDK`, + `Android SDK: could not find adb`, `Make sure you have a valid Android SDK installed, and it's available in your PATH` ); }; From 9ef89179d7c7b8ff43b208038aebfe697cc2931e Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Sun, 27 Mar 2022 22:16:36 +0200 Subject: [PATCH 5/5] feat: md printer + cleanup --- packages/doctorv2/package.json | 3 +- packages/doctorv2/src/index.ts | 9 +++ packages/doctorv2/src/printers/markdown.ts | 55 +++++++++++++++++++ packages/doctorv2/src/printers/pretty.ts | 2 +- .../doctorv2/src/requirements/common/index.ts | 45 +++++++++++---- .../doctorv2/src/requirements/ios/xcode.ts | 22 ++++++++ 6 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 packages/doctorv2/src/printers/markdown.ts diff --git a/packages/doctorv2/package.json b/packages/doctorv2/package.json index 372ebbd5ce..eb260428db 100644 --- a/packages/doctorv2/package.json +++ b/packages/doctorv2/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "private": true, "scripts": { - "build": "tsc" + "build": "tsc", + "dev": "tsc --watch" }, "devDependencies": { "typescript": "~4.5.5" diff --git a/packages/doctorv2/src/index.ts b/packages/doctorv2/src/index.ts index 709e1742fc..2eb4651e99 100644 --- a/packages/doctorv2/src/index.ts +++ b/packages/doctorv2/src/index.ts @@ -1,4 +1,5 @@ import { printResults } from "./printers/pretty"; +import { printResults as printResultsMD } from "./printers/markdown"; export type TPlatform = "android" | "ios"; @@ -23,6 +24,10 @@ export type RequirementFunction = ( results: IRequirementResult[] ) => Promise; +// export interface RequirementFunction { +// platforms: TPlatforms +// } + // todo: rename or whatever, but this is augmented by all requirements that provide new info export interface RequirementDetails { base?: true; @@ -75,4 +80,8 @@ Promise.allSettled(promises).then((results) => { console.log("-".repeat(100)); printResults(filtered); + + const data = { results: filtered, details }; + + printResultsMD(data); }); diff --git a/packages/doctorv2/src/printers/markdown.ts b/packages/doctorv2/src/printers/markdown.ts new file mode 100644 index 0000000000..f54a900404 --- /dev/null +++ b/packages/doctorv2/src/printers/markdown.ts @@ -0,0 +1,55 @@ +import { green } from "ansi-colors"; +import { details, IRequirementResult, RequirementDetails } from ".."; + +export function printResults(data: { + results: IRequirementResult[]; + details: RequirementDetails; +}) { + const asYamlList = (list: string[]) => { + if (Array.isArray(list)) { + return "\n" + list.map((item: string) => ` - ${item}`).join("\n"); + } + + return list ?? "Not Found"; + }; + + const md = [ + ``, + "```yaml", + `OS: ${details.os.name} ${details.os.version}`, + `CPU: ${details.cpu}`, + `Shell: ${details.shell}`, + `node: ${details.node.version} (${details.node.path})`, + `npm: ${details.npm.version}`, + `nativescript: ${details.nativescript.version}`, + ``, + `# android`, + `java: ${details.java.version}`, + `javac: ${details.javac.version}`, + `ndk: ${asYamlList(details.android.installedNDKVersions)}`, + `apis: ${asYamlList(details.android.installedTargets)}`, + `build_tools: ${asYamlList(details.android.installedBuildTools)}`, + `system_images: ${asYamlList(details.android.installedSystemImages)}`, + ``, + `# ios`, + `xcode: ${details.xcode.version} (${details.xcode.buildVersion})`, + `cocoapods: ${details.cocoapods.version}`, + `python: ${details.python.version}`, + // `ruby: ${details.ruby.version}`, + `platforms: ${asYamlList(details.ios.platforms)}`, + "```", + ``, + `### Dependencies`, + ``, + "```json", + '"dependencies": ' + JSON.stringify({}, null, 2) + ",", + '"devDependencies": ' + JSON.stringify({}, null, 2), + "```", + ``, + ``, + green.bold(`√ Results have been copied to your clipboard`), + ``, + ].join("\n"); + + console.log(md); +} diff --git a/packages/doctorv2/src/printers/pretty.ts b/packages/doctorv2/src/printers/pretty.ts index 9452b3e940..3af2091e1a 100644 --- a/packages/doctorv2/src/printers/pretty.ts +++ b/packages/doctorv2/src/printers/pretty.ts @@ -19,7 +19,7 @@ export function printResults(res: IRequirementResult[]) { [ResultType.WARN]: 0, [ResultType.ERROR]: 0, }; - let lastResultType; + let lastResultType: ResultType; console.log(""); res .map((requirementResult) => { diff --git a/packages/doctorv2/src/requirements/common/index.ts b/packages/doctorv2/src/requirements/common/index.ts index 26848c472c..d780994c59 100644 --- a/packages/doctorv2/src/requirements/common/index.ts +++ b/packages/doctorv2/src/requirements/common/index.ts @@ -1,4 +1,4 @@ -import { platform, arch, cpus } from "os"; +import * as os from "os"; import { error, ok, warn } from "../../helpers/results"; import { execSafe } from "../../helpers/child-process"; @@ -7,12 +7,16 @@ import { safeMatch } from "../../helpers"; declare module "../.." { interface RequirementDetails { - os?: string; - platform?: string; - arch?: string; + os?: { + platform?: string; + name?: string; + version?: string; + arch?: string; + uname?: string; + }; cpu?: string; shell?: string; - node?: { version: string }; + node?: { version: string; path?: string }; npm?: { version: string }; yarn?: { version: string }; pnpm?: { version: string }; @@ -21,8 +25,6 @@ declare module "../.." { } details.os = null; -details.platform = null; -details.arch = null; details.cpu = null; details.shell = null; details.node = null; @@ -31,16 +33,32 @@ details.yarn = null; details.pnpm = null; details.nativescript = null; +const osNameMap: { [key: string]: string } = { + darwin: "macOS", + win32: "Windows", + aix: "Aix", + freebsd: "FreeBSD", + linux: "Linux", + openbsd: "OpenBSD", + sunos: "SunOS", +}; + const platformInfo: RequirementFunction = async () => { - details.os = await execSafe("uname -a").then((res) => { + const uname = await execSafe("uname -a").then((res) => { return res ? res.stdout : null; }); - details.platform = platform(); - details.arch = arch(); + + details.os = { + platform: os.platform(), + name: osNameMap[os.platform()] ?? "Unknown", + version: os.release(), + arch: os.arch(), + uname, + }; try { - const _cpus = cpus(); - details.cpu = "(" + _cpus.length + ") " + arch() + " " + _cpus[0].model; + const _cpus = os.cpus(); + details.cpu = "(" + _cpus.length + ") " + os.arch() + " " + _cpus[0].model; } catch (err) { details.cpu = "Unknown"; } @@ -51,12 +69,15 @@ const platformInfo: RequirementFunction = async () => { const NODE_VERSION_RE = /v?(.+)\n/im; const node: RequirementFunction = async () => { const res = await execSafe("node -v"); + const whichRes = await execSafe("which node"); if (res) { const [, version] = safeMatch(res.stdout, NODE_VERSION_RE); + const path = whichRes ? whichRes.stdout.trim() : ""; details.node = { version, + path, }; } }; diff --git a/packages/doctorv2/src/requirements/ios/xcode.ts b/packages/doctorv2/src/requirements/ios/xcode.ts index 2c611b4f60..1b72c725d3 100644 --- a/packages/doctorv2/src/requirements/ios/xcode.ts +++ b/packages/doctorv2/src/requirements/ios/xcode.ts @@ -10,11 +10,15 @@ declare module "../.." { interface RequirementDetails { xcode?: { version: string; buildVersion: string }; xcodeproj?: { version: string }; + ios?: { + platforms?: string[]; + }; } } details.xcode = null; details.xcodeproj = null; +details.ios = null; async function XCodeRequirement() { const res = await execSafe(`xcodebuild -version`); @@ -62,7 +66,25 @@ async function XCodeProjRequirement() { ); } +const iosSDKs: RequirementFunction = async () => { + const res = await execSafe("xcodebuild -showsdks"); + + if (res) { + const platforms = res.stdout.match(/[\w]+\s[\d|.]+/g); + + const uniqPlatforms = Array.from(new Set([...platforms])); + + details.ios = { + ...details.ios, + platforms: uniqPlatforms, + }; + } +}; +// '') +// .then(sdks => ) + export const xcodeRequirements: RequirementFunction[] = [ XCodeRequirement, XCodeProjRequirement, + iosSDKs, ];