UNPKG

@appium/docutils

Version:

Documentation generation utilities for Appium and related projects

324 lines 12.6 kB
"use strict"; /** * Validates an environment for building documentation; used by `validate` command * * @module */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DocutilsValidator = void 0; const support_1 = require("@appium/support"); const chalk_1 = __importDefault(require("chalk")); const lodash_1 = __importDefault(require("lodash")); const node_events_1 = require("node:events"); const teen_process_1 = require("teen_process"); const constants_1 = require("./constants"); const error_1 = require("./error"); const fs_1 = require("./fs"); const logger_1 = require("./logger"); /** * Matches the Python version string from `python --version` */ const PYTHON_VER_STR = 'Python 3.'; /** * Matches the MkDocs version string from `mkdocs --version` */ const MKDOCS_VERSION_REGEX = /\s+version\s+(\d+\.\d+\.\S+)/; const log = (0, logger_1.getLogger)('validate'); /** * This class is designed to run _all_ validation checks (as requested by the user), and emit events for * each failure encountered. * * Whenever a method _rejects or throws_, this is considered an "unexpected" error, and the validation * will abort. * * @todo Use [`strict-event-emitter-types`](https://npm.im/strict-event-emitter-types) */ class DocutilsValidator extends node_events_1.EventEmitter { /** * Current working directory. Defaults to `process.cwd()` * @todo This cannot yet be overridden by user */ cwd; /** * Path to `python` executable. */ pythonPath; /** * List of validations to perform */ validations = new Set(); /** * Mapping of error messages to errors. * * Used to prevent duplicate emission of errors and track error count; if non-empty, the validation * process should be considered to have failed. * * Reset after {@linkcode DocutilsValidator.validate validate} completes. */ emittedErrors = new Map(); /** * Path to `mkdocs.yml`. If not provided, will be lazily resolved. */ mkDocsYmlPath; /** * Emitted when validation begins with a list of validation kinds to be performed * @event */ static BEGIN = 'begin'; /** * Emitted when validation ends with an error count * @event */ static END = 'end'; /** * Emitted when a validation fails, with the associated {@linkcode DocutilsError} * @event */ static FAILURE = 'fail'; /** * Emitted when a validation succeeds * @event */ static SUCCESS = 'ok'; requirementsTxt; /** * Creates a listener to track errors emitted */ constructor(opts = {}) { super(); this.pythonPath = opts.pythonPath; this.cwd = opts.cwd ?? process.cwd(); this.mkDocsYmlPath = opts.mkdocsYml; if (opts.python) { this.validations.add(constants_1.NAME_PYTHON); } if (opts.mkdocs) { this.validations.add(constants_1.NAME_MKDOCS); } // this just tracks the emitted errors this.on(DocutilsValidator.FAILURE, (err) => { this.emittedErrors.set(err.message, err); }); } /** * Runs the configured validations, then resets internal state upon completion or rejection. */ async validate() { try { this.emit(DocutilsValidator.BEGIN, [...this.validations]); if (this.validations.has(constants_1.NAME_PYTHON)) { await this.validatePythonVersion(); await this.validatePythonDeps(); } if (this.validations.has(constants_1.NAME_MKDOCS)) { await this.validateMkDocs(); await this.validateMkDocsConfig(); } this.emit(DocutilsValidator.END, this.emittedErrors.size); } finally { this.reset(); } } /** * If a thing like `err` has not already been emitted, emit * {@linkcode DocutilsValidator.FAILURE}. * @param err A validation error * @returns */ fail(err) { const dErr = lodash_1.default.isString(err) ? new error_1.DocutilsError(err) : err; if (!this.emittedErrors.has(dErr.message)) { this.emit(DocutilsValidator.FAILURE, dErr); } } /** * Emits a {@linkcode DocutilsValidator.SUCCESS} event * @param message Success message */ ok(message) { this.emit(DocutilsValidator.SUCCESS, message); } /** * Parses a `requirements.txt` file and returns an array of packages * * Caches the result. * @returns List of package data w/ name and version */ async parseRequirementsTxt() { if (this.requirementsTxt) { return this.requirementsTxt; } const requiredPackages = []; try { let requirementsTxt = await support_1.fs.readFile(constants_1.REQUIREMENTS_TXT_PATH, 'utf8'); requirementsTxt = requirementsTxt.trim(); log.debug('Raw %s: %s', constants_1.NAME_REQUIREMENTS_TXT, requirementsTxt); for (const line of requirementsTxt.split(/\r?\n/)) { const [name, version] = line.trim().split('=='); requiredPackages.push({ name, version }); } log.debug('Parsed %s: %O', constants_1.NAME_REQUIREMENTS_TXT, requiredPackages); } catch { throw new error_1.DocutilsError(`Could not find ${constants_1.REQUIREMENTS_TXT_PATH}. This is a bug`); } return (this.requirementsTxt = requiredPackages); } /** * Resets the cache of emitted errors */ reset() { this.emittedErrors.clear(); } /** * Validates that the correct version of `mkdocs` is installed */ async validateMkDocs() { log.debug(`Validating MkDocs version`); const pythonPath = this.pythonPath ?? (await (0, fs_1.findPython)()); if (!pythonPath) { return this.fail(constants_1.MESSAGE_PYTHON_MISSING); } const mkdocsInstalled = await (0, fs_1.isMkDocsInstalled)(); if (!mkdocsInstalled) { return this.fail(`Could not find MkDocs executable; please run "${constants_1.NAME_BIN} init"`); } let rawMkDocsVersion; try { ({ stdout: rawMkDocsVersion } = await (0, teen_process_1.exec)(pythonPath, ['-m', constants_1.NAME_MKDOCS, '--version'])); } catch (err) { return this.fail(`Failed to get MkDocs version: ${err}`); } const match = rawMkDocsVersion.match(MKDOCS_VERSION_REGEX); if (!match) { return this.fail(`Could not parse MkDocs version. Output was ${rawMkDocsVersion}`); } const version = match[1]; const reqs = await this.parseRequirementsTxt(); const mkDocsPipPkg = lodash_1.default.find(reqs, { name: constants_1.NAME_MKDOCS }); if (!mkDocsPipPkg) { throw new error_1.DocutilsError(`No ${constants_1.NAME_MKDOCS} package in ${constants_1.REQUIREMENTS_TXT_PATH}. This is a bug`); } const { version: mkDocsReqdVersion } = mkDocsPipPkg; if (version !== mkDocsReqdVersion) { return this.fail(`MkDocs v${version} is installed, but v${mkDocsReqdVersion} is required`); } this.ok('MkDocs install OK'); } /** * Validates (sort of) an `mkdocs.yml` config file. * * It checks if the file exists, if it can be parsed as YAML, and if it has a `site_name` property. */ async validateMkDocsConfig() { log.debug(`Validating ${constants_1.NAME_MKDOCS_YML}`); const mkDocsYmlPath = this.mkDocsYmlPath ?? (await (0, fs_1.findMkDocsYml)(this.cwd)); if (!mkDocsYmlPath) { return this.fail(`Could not find ${constants_1.NAME_MKDOCS_YML} from ${this.cwd}; please run "${constants_1.NAME_BIN} init"`); } let mkDocsYml; try { mkDocsYml = await (0, fs_1.readMkDocsYml)(mkDocsYmlPath); } catch (e) { const err = e; if (err.code === constants_1.NAME_ERR_ENOENT) { return this.fail(`Could not find ${constants_1.NAME_MKDOCS_YML} at ${mkDocsYmlPath}. Use --mkdocs-yml to specify a different path.`); } return this.fail(`Could not parse ${mkDocsYmlPath}: ${err}`); } if (!mkDocsYml.site_name) { return this.fail(`Could not find required property "site_name" in ${mkDocsYmlPath}`); } this.ok(`MkDocs config at ${mkDocsYmlPath} OK`); } /** * Asserts that the dependencies as listed in `requirements.txt` are installed. * * @privateRemarks This lists all installed packages with `pip` and then compares them to the * contents of our `requirements.txt`. Versions _must_ match exactly. */ async validatePythonDeps() { log.debug(`Validating required ${constants_1.NAME_PYTHON} package versions`); const pythonPath = this.pythonPath ?? (await (0, fs_1.findPython)()); if (!pythonPath) { return this.fail(constants_1.MESSAGE_PYTHON_MISSING); } let pipListOutput; try { ({ stdout: pipListOutput } = await (0, teen_process_1.exec)(pythonPath, [ '-m', constants_1.NAME_PIP, 'list', '--format', 'json', ])); } catch { return this.fail(`Could not find ${constants_1.NAME_PIP} installation for Python at ${pythonPath}. Is it installed?`); } let installedPkgs; try { installedPkgs = JSON.parse(pipListOutput); } catch { return this.fail(`Could not parse output of "${constants_1.NAME_PIP} list" as JSON: ${pipListOutput}`); } const pkgsByName = lodash_1.default.mapValues(lodash_1.default.keyBy(installedPkgs, 'name'), 'version'); log.debug('Installed Python packages: %O', pkgsByName); const requiredPackages = await this.parseRequirementsTxt(); const missingPackages = []; const invalidVersionPackages = []; for (const reqdPkg of requiredPackages) { const version = pkgsByName[reqdPkg.name]; if (!version) { missingPackages.push(reqdPkg); } else if (version !== reqdPkg.version) { invalidVersionPackages.push([reqdPkg, { name: reqdPkg.name, version }]); } } const msgParts = []; if (missingPackages.length) { msgParts.push(`The following required ${support_1.util.pluralize('package', missingPackages.length)} could not be found:\n${missingPackages .map((p) => (0, chalk_1.default) `- {yellow ${p.name}} @ {yellow ${p.version}}`) .join('\n')}`); } if (invalidVersionPackages.length) { msgParts.push(`The following required ${support_1.util.pluralize('package', invalidVersionPackages.length)} are installed, but at the wrong version:\n${invalidVersionPackages .map(([expected, actual]) => (0, chalk_1.default) `- {yellow ${expected.name}} @ {yellow ${expected.version}} (found {red ${actual.version}})`) .join('\n')}`); } if (msgParts.length) { return this.fail(`Required Python dependency validation failed:\n\n${msgParts.join('\n\n')}`); } this.ok('Python dependencies OK'); } /** * Asserts that the Python version is 3.x */ async validatePythonVersion() { log.debug(`Validating ${constants_1.NAME_PYTHON} version`); const pythonPath = this.pythonPath ?? (await (0, fs_1.findPython)()); if (!pythonPath) { return this.fail(constants_1.MESSAGE_PYTHON_MISSING); } try { const { stdout } = await (0, teen_process_1.exec)(pythonPath, ['--version']); if (!stdout.includes(PYTHON_VER_STR)) { return this.fail(`Could not find Python 3.x in PATH; found ${stdout}`); } } catch { return this.fail(`Could not retrieve Python version`); } this.ok('Python version OK'); } } exports.DocutilsValidator = DocutilsValidator; //# sourceMappingURL=validate.js.map