diff --git a/packages/doctorv2/package.json b/packages/doctorv2/package.json new file mode 100644 index 0000000000..eb260428db --- /dev/null +++ b/packages/doctorv2/package.json @@ -0,0 +1,16 @@ +{ + "name": "@nativescript/doctorv2", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + }, + "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..8e89b755e7 --- /dev/null +++ b/packages/doctorv2/src/helpers/index.ts @@ -0,0 +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/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..959303d73e --- /dev/null +++ b/packages/doctorv2/src/helpers/semver.ts @@ -0,0 +1,49 @@ +import * as semver from "semver"; + +export function isInRange( + version: string, + range: { min?: string; max?: string } +) { + try { + const _version = semver.coerce(version); + + 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({ + // _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); +} + +// 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 new file mode 100644 index 0000000000..2eb4651e99 --- /dev/null +++ b/packages/doctorv2/src/index.ts @@ -0,0 +1,87 @@ +import { printResults } from "./printers/pretty"; +import { printResults as printResultsMD } from "./printers/markdown"; + +export type TPlatform = "android" | "ios"; + +export type TPlatforms = { + [platform in TPlatform]?: boolean; +}; + +export const enum ResultType { + ERROR = "ERROR", + OK = "OK", + WARN = "WARN", +} + +export interface IRequirementResult { + type: ResultType; + message: string; + details?: string; + platforms?: TPlatforms; +} + +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; +} + +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); + + 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 new file mode 100644 index 0000000000..3af2091e1a --- /dev/null +++ b/packages/doctorv2/src/printers/pretty.ts @@ -0,0 +1,113 @@ +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, + }; + let lastResultType: ResultType; + 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"; + } + + // 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); + }); + 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(""); + } +} 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..662730b311 --- /dev/null +++ b/packages/doctorv2/src/requirements/android/android-sdk.ts @@ -0,0 +1,220 @@ +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, warn } from "../../helpers/results"; + +// example: augment details with new values +declare module "../.." { + interface RequirementDetails { + android?: { + sdkPath: string; + sdkFrom: string; + installedTargets: string[]; + installedBuildTools: string[]; + installedNDKVersions: string[]; + installedSystemImages: 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: [], + installedBuildTools: [], + installedNDKVersions: [], + installedSystemImages: [], + }; + + 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(`Android SDK: Could not find an Android SDK`); + } + + return ok(`Android SDK: found 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); + + 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) => { + const sdk = getAndroidSdkInfo(); + + if (!sdk.sdkPath) { + return; + } + + 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) => { + 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; + + 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; +const androidAdb: RequirementFunction = async (results) => { + const res = await execSafe("adb --version"); + + if (res) { + const [, version] = safeMatch(res.stdout, ADB_VERSION_RE); + details.adb = { version }; + + return ok(`Android SDK: found adb (${version})`); + } + + return error( + `Android SDK: could not find adb`, + `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 new file mode 100644 index 0000000000..965938bd40 --- /dev/null +++ b/packages/doctorv2/src/requirements/android/index.ts @@ -0,0 +1,9 @@ +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..d4e4bd1231 --- /dev/null +++ b/packages/doctorv2/src/requirements/android/java.ts @@ -0,0 +1,97 @@ +import { existsSync } from "fs"; +import { resolve } from "path"; + +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 "../.."; + +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 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"]; + + 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(`javac executable not found`, `Make sure you install a JDK`); + } + + const res = await execSafe(`"${javaExecutablePath}" --version`); + if (res) { + const [, version] = safeMatch(res.stdout, JAVAC_VERSION_RE); + details.javac = { version }; + // console.log("javac", { version }); + + return ok(`javac executable found (${version})`); + } + + return error(`javac executable not found`); +}; + +export const javaRequirements: RequirementFunction[] = [ + javaRequirement, + javacRequirement, +]; diff --git a/packages/doctorv2/src/requirements/common/index.ts b/packages/doctorv2/src/requirements/common/index.ts new file mode 100644 index 0000000000..d780994c59 --- /dev/null +++ b/packages/doctorv2/src/requirements/common/index.ts @@ -0,0 +1,184 @@ +import * as os from "os"; + +import { error, ok, warn } from "../../helpers/results"; +import { execSafe } from "../../helpers/child-process"; +import { details, RequirementFunction } from "../.."; +import { safeMatch } from "../../helpers"; + +declare module "../.." { + interface RequirementDetails { + os?: { + platform?: string; + name?: string; + version?: string; + arch?: string; + uname?: string; + }; + cpu?: string; + shell?: string; + node?: { version: string; path?: string }; + npm?: { version: string }; + yarn?: { version: string }; + pnpm?: { version: string }; + nativescript?: { version: string }; + } +} + +details.os = null; +details.cpu = null; +details.shell = null; +details.node = null; +details.npm = null; +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 () => { + const uname = await execSafe("uname -a").then((res) => { + return res ? res.stdout : null; + }); + + details.os = { + platform: os.platform(), + name: osNameMap[os.platform()] ?? "Unknown", + version: os.release(), + arch: os.arch(), + uname, + }; + + try { + const _cpus = os.cpus(); + details.cpu = "(" + _cpus.length + ") " + os.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"); + 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, + }; + } +}; + +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); + + 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] = safeMatch(res.stdout, NATIVESCRIPT_VERSION_RE); + + const isUpToDate = res.stdout.includes("Up to date."); + + // console.log("nativescript", { + // version, + // }); + details.nativescript = { + version, + }; + + if (isUpToDate) { + return ok(`NativeScript CLI is installed (${version})`); + } + + 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() { +// 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..7d224b9eae --- /dev/null +++ b/packages/doctorv2/src/requirements/ios/cocoapods.ts @@ -0,0 +1,44 @@ +import { execSafe } from "../../helpers/child-process"; +import { details, RequirementFunction } from "../.."; +import { error, ok } from "../../helpers/results"; +import { notInRange } from "../../helpers/semver"; + +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 }; + + 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`, + `You need to install CocoaPods to be able to build and run projects.` + ); +} + +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..6da0e87255 --- /dev/null +++ b/packages/doctorv2/src/requirements/ios/index.ts @@ -0,0 +1,10 @@ +import { cocoaPodsRequirements } from "./cocoapods"; +import { pythonRequirements } from "./python"; +import { RequirementFunction } from "../.."; +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..4e02045299 --- /dev/null +++ b/packages/doctorv2/src/requirements/ios/python.ts @@ -0,0 +1,51 @@ +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/; + +declare module "../.." { + interface RequirementDetails { + python?: { version: string }; + } +} + +details.python = null; + +const pythonRequirement: RequirementFunction = async () => { + const res = await execSafe(`python3 --version`); + + if (res) { + 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"`); + + if (hasSix) { + return ok(`Python package "six" is installed`); + } + + return warn( + `Python package "six" is not installed`, + "Some debugger features might not work correctly" + ); +}; + +export const pythonRequirements: RequirementFunction[] = [ + pythonRequirement, + pythonSixRequirement, +]; diff --git a/packages/doctorv2/src/requirements/ios/xcode.ts b/packages/doctorv2/src/requirements/ios/xcode.ts new file mode 100644 index 0000000000..1b72c725d3 --- /dev/null +++ b/packages/doctorv2/src/requirements/ios/xcode.ts @@ -0,0 +1,90 @@ +import { execSafe } from "../../helpers/child-process"; +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/; + +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`); + + if (res) { + const [, version] = safeMatch(res.stdout, VERSION_RE); + const [, buildVersion] = safeMatch(res.stdout, BUILD_VERSION_RE); + // console.log("xcode", { + // version, + // buildVersion, + // }); + + details.xcode = { + version, + buildVersion, + }; + // prettier-ignore + return ok(`XCode is installed (${version} / ${buildVersion})`) + } + + 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 (${version})`); + } + + return error( + `xcodeproj is missing`, + `The xcodeproj gem is required to build projects.` + ); +} + +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, +]; 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==