feat(docutils): implement mkdocs validation

- Better grouping of `validate` command options in `--help`
- Move some more constants into the module
- Removed the "guess" functions and replaced them with functions which use `which` to actually find the necessary executables
- Moved `isStringArray()` to `util`
- Fixed some error messages and added more
- Simplified use of `DocutilsValidator#fail()`
- Removed option for custom path to `requirements.txt`
This commit is contained in:
Christopher Hiller
2023-02-02 17:44:47 -08:00
parent 706efcf9cf
commit abfeb21be6
9 changed files with 332 additions and 152 deletions
+33 -26
View File
@@ -6,9 +6,34 @@ import logger from '../../logger';
const log = logger.withTag('validate');
const NAME_GROUP_VALIDATE = 'Validation:';
const NAME_GROUP_VALIDATE = 'Validation Behavior:';
const NAME_GROUP_VALIDATE_PATHS = 'Paths:';
const opts = {
mkdocs: {
default: true,
description: 'Validate MkDocs environment',
group: NAME_GROUP_VALIDATE,
type: 'boolean',
},
mkdocsYml: {
defaultDescription: './mkdocs.yml',
description: 'Path to mkdocs.yml',
group: NAME_GROUP_VALIDATE_PATHS,
nargs: 1,
normalize: true,
requiresArg: true,
type: 'string',
},
'npm-path': {
defaultDescription: '(derived from shell)',
description: 'Path to npm executable',
group: NAME_GROUP_VALIDATE_PATHS,
nargs: 1,
normalize: true,
requiresArg: true,
type: 'string',
},
python: {
default: true,
description: 'Validate Python 3 environment',
@@ -18,7 +43,7 @@ const opts = {
'python-path': {
defaultDescription: '(derived from shell)',
description: 'Path to python 3 executable',
group: NAME_GROUP_VALIDATE,
group: NAME_GROUP_VALIDATE_PATHS,
nargs: 1,
normalize: true,
requiresArg: true,
@@ -27,7 +52,7 @@ const opts = {
'tsconfig-json': {
defaultDescription: './tsconfig.json',
describe: 'Path to tsconfig.json',
group: NAME_GROUP_VALIDATE,
group: NAME_GROUP_VALIDATE_PATHS,
nargs: 1,
normalize: true,
requiresArg: true,
@@ -35,14 +60,14 @@ const opts = {
},
typedoc: {
default: true,
description: 'Validate TypoDoc config',
description: 'Validate TypoDoc environment',
group: NAME_GROUP_VALIDATE,
type: 'boolean',
},
'typedoc-json': {
defaultDescription: './typedoc.json',
describe: 'Path to typedoc.json',
group: NAME_GROUP_VALIDATE,
group: NAME_GROUP_VALIDATE_PATHS,
nargs: 1,
normalize: true,
requiresArg: true,
@@ -50,28 +75,10 @@ const opts = {
},
typescript: {
default: true,
description: 'Validate TypeScript config',
description: 'Validate TypeScript environment',
group: NAME_GROUP_VALIDATE,
type: 'boolean',
},
mkdocsYml: {
defaultDescription: './mkdocs.yml',
description: 'Path to mkdocs.yml',
group: NAME_GROUP_VALIDATE,
nargs: 1,
normalize: true,
requiresArg: true,
type: 'string',
},
npmPath: {
defaultDescription: '(derived from shell)',
description: 'Path to npm executable',
group: NAME_GROUP_VALIDATE,
nargs: 1,
normalize: true,
requiresArg: true,
type: 'string',
},
} as const;
opts as Record<string, Options>;
type ValidateOptions = InferredOptionTypes<typeof opts>;
@@ -80,10 +87,10 @@ const validateCommand: CommandModule<{}, ValidateOptions> = {
describe: 'Validate Environment',
builder: opts,
async handler(args) {
if (!args.python && !args.typedoc && !args.typescript) {
if (!args.python && !args.typedoc && !args.typescript && !args.mkdocs) {
// specifically not a DocutilsError
throw new Error(
'No validation targets specified; one or more of --python, --typescript or --typedoc must be provided'
'No validation targets specified; one or more of --python, --typescript, --typedoc or --mkdocs must be provided'
);
}
+13
View File
@@ -71,6 +71,19 @@ export const NAME_NPM = 'npm';
*/
export const NAME_TYPESCRIPT = 'typescript';
/**
* Code for a "file not found" error
*/
export const NAME_ERR_ENOENT = 'ENOENT';
/**
* Code for a "file already exists" error
*/
export const NAME_ERR_EEXIST = 'EEXIST';
/**
* Default log level
*/
export const DEFAULT_LOG_LEVEL = 'info';
/**
* Blocking I/O
+50 -19
View File
@@ -13,7 +13,14 @@ import _ from 'lodash';
import _pkgDir from 'pkg-dir';
import logger from './logger';
import {Application, TypeDocReader} from 'typedoc';
import {NAME_TYPEDOC_JSON, NAME_MKDOCS_YML, NAME_PACKAGE_JSON} from './constants';
import {
NAME_TYPEDOC_JSON,
NAME_MKDOCS_YML,
NAME_PACKAGE_JSON,
NAME_MKDOCS,
NAME_NPM,
NAME_PYTHON,
} from './constants';
import {DocutilsError} from './error';
const log = logger.withTag('fs');
@@ -66,37 +73,41 @@ export const readYaml = _.memoize(async (filepath: string) =>
YAML.parse(await fs.readFile(filepath, 'utf8'))
);
/**
* Finds a file from `cwd`. Searches up to the package root (dir containing `package.json`).
*
* @param filename Filename to look for
* @param cwd Dir it should be in
* @returns
*/
export async function findInPkgDir(
filename: string,
cwd = process.cwd()
): Promise<string | undefined> {
const pkgDir = await findPkgDir(cwd);
if (!pkgDir) {
return;
}
return path.join(pkgDir, filename);
}
/**
* Finds a `typedoc.json`, expected to be a sibling of `package.json`
*
* Caches the result. Does not check if `typedoc.json` actually exists
* Caches the result.
* @param cwd - Current working directory
* @param packageJsonPath - Path to `package.json`
* @returns Path to `typedoc.json`
*/
export const guessTypeDocJsonPath = _.memoize(
async (cwd = process.cwd(), packageJsonPath?: string) => {
const {pkgPath} = await readPackageJson(packageJsonPath ? path.dirname(packageJsonPath) : cwd);
const pkgDir = path.dirname(pkgPath);
return path.join(pkgDir, NAME_TYPEDOC_JSON);
}
);
export const findTypeDocJsonPath = _.memoize(_.partial(findInPkgDir, NAME_TYPEDOC_JSON));
/**
* Finds an `mkdocs.yml`, expected to be a sibling of `package.json`
*
* Caches the result. Does not check if `mkdocs.yml` actually exists
* Caches the result.
* @param cwd - Current working directory
* @param packageJsonPath - Path to `package.json`
* @returns Path to `mkdocs.yml`
*/
export const guessMkDocsYmlPath = _.memoize(
async (cwd = process.cwd(), packageJsonPath?: string) => {
const {pkgPath} = await readPackageJson(packageJsonPath ? path.dirname(packageJsonPath) : cwd);
const pkgDir = path.dirname(pkgPath);
return path.join(pkgDir, NAME_MKDOCS_YML);
}
);
export const findMkDocsYml = _.memoize(_.partial(findInPkgDir, NAME_MKDOCS_YML));
/**
* Given a directory path, finds closest `package.json` and reads it.
@@ -183,3 +194,23 @@ export function safeWriteFile(filepath: string, content: JsonValue, overwrite =
flag: overwrite ? 'w' : 'wx',
});
}
/**
* `which` with memoization
*/
export const cachedWhich = _.memoize(fs.which);
/**
* Finds `mkdocs` executable
*/
export const whichMkDocs = _.partial(cachedWhich, NAME_MKDOCS);
/**
* Finds `npm` executable
*/
export const whichNpm = _.partial(cachedWhich, NAME_NPM);
/**
* Finds `python` executable
*/
export const whichPython = _.partial(cachedWhich, NAME_PYTHON);
+29 -19
View File
@@ -5,40 +5,48 @@
*/
import {exec, SubProcess, TeenProcessExecOptions} from 'teen_process';
import {NAME_MKDOCS} from './constants';
import {guessMkDocsYmlPath, readYaml} from './fs';
import {NAME_BIN, NAME_MKDOCS, NAME_MKDOCS_YML} from './constants';
import {findMkDocsYml, readYaml, whichMkDocs} from './fs';
import logger from './logger';
import {relative, stopwatch, TupleToObject} from './util';
import _ from 'lodash';
import {DocutilsError} from './error';
const log = logger.withTag('mkdocs');
/**
* Runs `mkdocs serve`
* @param mkdocsPath Path to `mkdocs` executable
* @param args Extra args to `mkdocs build`
* @param opts Extra options for `teen_process.Subprocess.start`
* @param mkDocsPath Path to `mkdocs` executable
*/
function doServe(
mkdocsPath: string = NAME_MKDOCS,
async function doServe(
args: string[] = [],
{startDetector, detach, timeoutMs}: TeenProcessSubprocessStartOpts = {}
{startDetector, detach, timeoutMs}: TeenProcessSubprocessStartOpts = {},
mkDocsPath?: string
) {
const proc = new SubProcess(mkdocsPath, ['serve', ...args]);
return proc.start(startDetector, detach, timeoutMs);
mkDocsPath = mkDocsPath ?? (await whichMkDocs());
const finalArgs = ['serve', ...args];
log.debug('Launching %s with args: %O', mkDocsPath, finalArgs);
const proc = new SubProcess(mkDocsPath, finalArgs);
return await proc.start(startDetector, detach, timeoutMs);
}
/**
* Runs `mkdocs build`
* @param mkdocsPath Path to `mkdocs` executable
* @param args Extra args to `mkdocs build`
* @param opts Extra options to `teen_process.exec`
* @param mkDocsPath Path to `mkdocs` executable
*/
function doBuild(
mkdocsPath: string = NAME_MKDOCS,
async function doBuild(
args: string[] = [],
opts: TeenProcessExecOptions = {}
opts: TeenProcessExecOptions = {},
mkDocsPath?: string
) {
return exec(mkdocsPath, ['build', ...args], opts);
mkDocsPath = mkDocsPath ?? (await whichMkDocs());
const finalArgs = ['build', ...args];
log.debug('Launching %s with args: %O', mkDocsPath, finalArgs);
return await exec(mkDocsPath, finalArgs, opts);
}
/**
@@ -50,25 +58,27 @@ export async function buildMkDocs({
siteDir,
theme = NAME_MKDOCS,
cwd = process.cwd(),
packageJson: packageJsonPath,
serve = false,
serveOpts,
execOpts,
}: BuildMkDocsOpts = {}) {
const stop = stopwatch('build-mkdocs');
mkdocsYmlPath = mkdocsYmlPath ?? (await guessMkDocsYmlPath(cwd, packageJsonPath));
mkdocsYmlPath = mkdocsYmlPath ?? (await findMkDocsYml(cwd));
if (!mkdocsYmlPath) {
throw new DocutilsError(
`Could not find ${NAME_MKDOCS_YML} from ${cwd}; run "${NAME_BIN} init" to create it`
);
}
const relativePath = relative(cwd);
const mkdocsArgs = ['-f', mkdocsYmlPath, '-t', theme];
if (siteDir) {
mkdocsArgs.push('-d', siteDir);
}
if (serve) {
log.debug('Launching %s serve with args: %O', NAME_MKDOCS, mkdocsArgs);
// unsure about how SIGHUP is handled here
await doServe(NAME_MKDOCS, mkdocsArgs, serveOpts);
await doServe(mkdocsArgs, serveOpts);
} else {
log.debug('Launching %s build with args: %O', NAME_MKDOCS, mkdocsArgs);
await doBuild(NAME_MKDOCS, mkdocsArgs, execOpts);
await doBuild(mkdocsArgs, execOpts);
let relSiteDir;
if (siteDir) {
relSiteDir = relativePath(siteDir);
+22 -16
View File
@@ -8,10 +8,16 @@
import {fs} from '@appium/support';
import _ from 'lodash';
import path from 'node:path';
import {DEFAULT_REL_TYPEDOC_OUT_PATH} from './constants';
import {
guessMkDocsYmlPath,
guessTypeDocJsonPath,
DEFAULT_REL_TYPEDOC_OUT_PATH,
NAME_BIN,
NAME_MKDOCS_YML,
NAME_TYPEDOC_JSON,
} from './constants';
import {DocutilsError} from './error';
import {
findMkDocsYml,
findTypeDocJsonPath,
readTypedocJson,
readYaml,
safeWriteFile,
@@ -19,7 +25,7 @@ import {
} from './fs';
import logger from './logger';
import {MkDocsYml} from './model';
import {relative} from './util';
import {relative, isStringArray} from './util';
const DEFAULT_REFERENCE_HEADER = 'Reference';
@@ -33,16 +39,25 @@ const log = logger.withTag('mkdocs-nav');
export async function updateNav<S extends string>({
cwd = process.cwd(),
mkdocsYml: mkDocsYmlPath,
packageJson: packageJsonPath,
referenceHeader = <S>DEFAULT_REFERENCE_HEADER,
noReferenceHeader = false,
typedocJson: typeDocJsonPath,
dryRun = false,
}: UpdateNavOpts<S> = {}) {
[mkDocsYmlPath, typeDocJsonPath] = await Promise.all([
mkDocsYmlPath ?? guessMkDocsYmlPath(cwd, packageJsonPath),
typeDocJsonPath ?? guessTypeDocJsonPath(cwd, packageJsonPath),
mkDocsYmlPath ?? findMkDocsYml(cwd),
typeDocJsonPath ?? findTypeDocJsonPath(cwd),
]);
if (!mkDocsYmlPath) {
throw new DocutilsError(
`Could not find ${NAME_MKDOCS_YML} from ${cwd}; run "${NAME_BIN} init" to create it`
);
}
if (!typeDocJsonPath) {
throw new DocutilsError(
`Could not find ${NAME_TYPEDOC_JSON} from ${cwd}; run "${NAME_BIN} init" to create it`
);
}
const relativePath = relative(cwd);
const relMkDocsYmlPath = relativePath(mkDocsYmlPath);
const typeDocJson = readTypedocJson(typeDocJsonPath);
@@ -153,15 +168,6 @@ export async function updateNav<S extends string>({
}
}
/**
* Type guard to narrow an array to a string array
* @param value any value
* @returns `true` if the array is `string[]`
*/
const isStringArray = _.overEvery(_.isArray, _.partial(_.every, _, _.isString)) as (
value: any
) => value is string[];
/**
* Options for {@linkcode updateNav}
*/
+1 -3
View File
@@ -13,9 +13,7 @@ import {DocutilsError} from './error';
import {relative} from './util';
import _ from 'lodash';
import {stringifyJson, readPackageJson, safeWriteFile} from './fs';
const NAME_ERR_ENOENT = 'ENOENT';
const NAME_ERR_EEXIST = 'EEXIST';
import {NAME_ERR_ENOENT, NAME_ERR_EEXIST} from './constants';
const log = logger.withTag('init');
const dryRunLog = log.withTag('dry-run');
+14 -5
View File
@@ -9,9 +9,14 @@ import glob from 'glob';
import _ from 'lodash';
import path from 'node:path';
import {Application, ArgumentsReader, TypeDocOptions, TypeDocReader} from 'typedoc';
import {DEFAULT_LOG_LEVEL, DEFAULT_REL_TYPEDOC_OUT_PATH, NAME_TYPEDOC_JSON} from './constants';
import {
DEFAULT_LOG_LEVEL,
DEFAULT_REL_TYPEDOC_OUT_PATH,
NAME_BIN,
NAME_TYPEDOC_JSON,
} from './constants';
import {DocutilsError} from './error';
import {guessTypeDocJsonPath, readTypedocJson} from './fs';
import {findTypeDocJsonPath, readTypedocJson} from './fs';
import logger from './logger';
import {relative, stopwatch} from './util';
@@ -128,13 +133,17 @@ const TypeDocLogLevelMap: Record<LogLevelName, string> = {
export async function buildReference({
typedocJson: typeDocJsonPath,
cwd = process.cwd(),
packageJson: packageJsonPath,
tsconfigJson: tsconfig,
logLevel = DEFAULT_LOG_LEVEL,
title,
}: BuildReferenceOptions = {}) {
const stop = stopwatch('buildReference');
typeDocJsonPath = typeDocJsonPath ?? (await guessTypeDocJsonPath(cwd, packageJsonPath));
typeDocJsonPath = typeDocJsonPath ?? (await findTypeDocJsonPath(cwd));
if (!typeDocJsonPath) {
throw new DocutilsError(
`Could not find ${NAME_TYPEDOC_JSON} from ${cwd}; run "${NAME_BIN}" to create it`
);
}
const pkgRoot = fs.findRoot(cwd);
const relativePath = relative(cwd);
const relativeTypeDocJsonPath = relativePath(typeDocJsonPath);
@@ -148,7 +157,7 @@ export async function buildReference({
} catch (err) {
log.error(err);
throw new DocutilsError(
`Could not read ${relativeTypeDocJsonPath}; please execute "appium docutils init" to create it`
`Could not read ${relativeTypeDocJsonPath}; run "${NAME_BIN} init" to create it`
);
}
+9
View File
@@ -35,3 +35,12 @@ export type TupleToObject<
T extends readonly any[],
M extends Record<Exclude<keyof T, keyof any[]>, PropertyKey>
> = {[K in Exclude<keyof T, keyof any[]> as M[K]]: T[K]};
/**
* Type guard to narrow an array to a string array
* @param value any value
* @returns `true` if the array is `string[]`
*/
export const isStringArray = _.overEvery(_.isArray, _.partial(_.every, _, _.isString)) as (
value: any
) => value is string[];
+161 -64
View File
@@ -16,6 +16,9 @@ import {
DEFAULT_REL_TYPEDOC_OUT_PATH,
DOCUTILS_PKG,
NAME_BIN,
NAME_ERR_ENOENT,
NAME_MKDOCS,
NAME_MKDOCS_YML,
NAME_NPM,
NAME_PACKAGE_JSON,
NAME_PIP,
@@ -28,9 +31,18 @@ import {
REQUIREMENTS_TXT_PATH,
} from './constants';
import {DocutilsError} from './error';
import {findPkgDir, readJson5, readTypedocJson} from './fs';
import {
findPkgDir,
findMkDocsYml,
readJson5,
readTypedocJson,
readYaml,
whichMkDocs,
whichNpm,
whichPython,
} from './fs';
import logger from './logger';
import {PipPackage, TypeDocJson} from './model';
import {MkDocsYml, PipPackage, TypeDocJson} from './model';
import {relative} from './util';
/**
@@ -48,6 +60,11 @@ const TYPESCRIPT_VERSION_REGEX = /Version\s(\d+\.\d+\..+)/;
*/
const TYPEDOC_VERSION_REGEX = /TypeDoc\s(\d+\.\d+\..+)/;
/**
* Matches the MkDocs version string from `mkdocs --version`
*/
const MKDOCS_VERSION_REGEX = /mkdocs,\s+version\s+(\d+\.\d+\.\S+)/;
const log = logger.withTag('validate');
/**
@@ -57,7 +74,8 @@ export type ValidationKind =
| typeof NAME_PYTHON
| typeof NAME_TYPESCRIPT
| typeof NAME_TYPEDOC
| typeof NAME_NPM;
| typeof NAME_NPM
| typeof NAME_MKDOCS;
/**
* This class is designed to run _all_ validation checks (as requested by the user), and emit events for
@@ -76,14 +94,14 @@ export class DocutilsValidator extends EventEmitter {
protected readonly cwd: string;
/**
* Path to `npm` executable. Defaults to `npm`
* Path to `npm` executable.
*/
protected readonly npmPath: string;
protected readonly npmPath: string | undefined;
/**
* Path to `python` executable. Defaults to `python`
* Path to `python` executable.
*/
protected readonly pythonPath: string;
protected readonly pythonPath: string | undefined;
/**
* List of validations to perform
@@ -149,6 +167,8 @@ export class DocutilsValidator extends EventEmitter {
*/
public static readonly SUCCESS = 'ok';
private requirementsTxt: PipPackage[] | undefined;
/**
* Creates a listener to track errors emitted
*/
@@ -156,11 +176,11 @@ export class DocutilsValidator extends EventEmitter {
super();
this.packageJsonPath = opts.packageJson;
this.pythonPath = opts.pythonPath ?? NAME_PYTHON;
this.pythonPath = opts.pythonPath;
this.cwd = opts.cwd ?? process.cwd();
this.tsconfigJsonPath = opts.tsconfigJson;
this.typeDocJsonPath = opts.typedocJson;
this.npmPath = opts.npm ?? NAME_NPM;
this.npmPath = opts.npm;
this.mkDocsYmlPath = opts.mkdocsYml;
if (opts.python) {
@@ -175,6 +195,9 @@ export class DocutilsValidator extends EventEmitter {
this.validations.add(NAME_TYPEDOC);
this.validations.add(NAME_NPM);
}
if (opts.mkdocs) {
this.validations.add(NAME_MKDOCS);
}
// this just tracks the emitted errors
this.on(DocutilsValidator.FAILURE, (err: DocutilsError) => {
@@ -194,6 +217,11 @@ export class DocutilsValidator extends EventEmitter {
await this.validatePythonDeps();
}
if (this.validations.has(NAME_MKDOCS)) {
await this.validateMkDocs();
await this.validateMkDocsConfig();
}
if (this.validations.has(NAME_NPM)) {
await this.validateNpmVersion();
}
@@ -220,9 +248,10 @@ export class DocutilsValidator extends EventEmitter {
* @param err A validation error
* @returns
*/
protected fail(err: DocutilsError) {
if (!this.emittedErrors.has(err.message)) {
this.emit(DocutilsValidator.FAILURE, err);
protected fail(err: DocutilsError | string) {
const dErr = _.isString(err) ? new DocutilsError(err) : err;
if (!this.emittedErrors.has(dErr.message)) {
this.emit(DocutilsValidator.FAILURE, dErr);
}
}
@@ -248,16 +277,19 @@ export class DocutilsValidator extends EventEmitter {
/**
* Parses a `requirements.txt` file and returns an array of packages
* @param requirementsTxtPath Path to `requirements.txt`
*
* Caches the result.
* @returns List of package data w/ name and version
*/
protected async parseRequirementsTxt(
requirementsTxtPath = REQUIREMENTS_TXT_PATH
): Promise<PipPackage[]> {
protected async parseRequirementsTxt(): Promise<PipPackage[]> {
if (this.requirementsTxt) {
return this.requirementsTxt;
}
let requiredPackages: PipPackage[] = [];
try {
let requirementsTxt = await fs.readFile(requirementsTxtPath, 'utf8');
let requirementsTxt = await fs.readFile(REQUIREMENTS_TXT_PATH, 'utf8');
requirementsTxt = requirementsTxt.trim();
log.debug('Raw %s: %s', NAME_REQUIREMENTS_TXT, requirementsTxt);
for (const line of requirementsTxt.split(/\r?\n/)) {
@@ -267,7 +299,7 @@ export class DocutilsValidator extends EventEmitter {
}
log.debug('Parsed %s: %O', NAME_REQUIREMENTS_TXT, requiredPackages);
} catch {
throw new DocutilsError(`Could not find ${requirementsTxtPath}. This is a bug`);
throw new DocutilsError(`Could not find ${REQUIREMENTS_TXT_PATH}. This is a bug`);
}
return requiredPackages;
@@ -280,10 +312,79 @@ export class DocutilsValidator extends EventEmitter {
this.emittedErrors.clear();
}
protected async validateMkDocs() {
let mkDocsPath: string | undefined;
try {
mkDocsPath = await whichMkDocs();
} catch {
// _pretty sure_ the exception code is always ENOENT
return this.fail(
`Could not find ${NAME_MKDOCS} executable in PATH. If it is installed, check your PATH environment variable.`
);
}
let rawMkDocsVersion: string | undefined;
try {
({stdout: rawMkDocsVersion} = await exec(mkDocsPath, ['--version']));
} catch (err) {
return this.fail(`${mkDocsPath} --version failed: ${err}`);
}
const match = rawMkDocsVersion.match(MKDOCS_VERSION_REGEX);
if (match) {
const version = match[1];
const reqs = await this.parseRequirementsTxt();
const mkDocsPipPkg = _.find(reqs, {name: NAME_MKDOCS});
if (!mkDocsPipPkg) {
throw new DocutilsError(
`No ${NAME_MKDOCS} package in ${REQUIREMENTS_TXT_PATH}. This is a bug.`
);
}
const {version: mkDocsReqdVersion} = mkDocsPipPkg;
if (version !== mkDocsReqdVersion) {
return this.fail(
`${NAME_MKDOCS} at ${mkDocsPath} is v${version}, but ${REQUIREMENTS_TXT_PATH} requires v${mkDocsReqdVersion}`
);
}
} else {
throw new DocutilsError(
`Could not parse version from "${mkDocsPath} --version". This is a bug. Output was ${rawMkDocsVersion}`
);
}
this.ok('MkDocs install OK');
}
/**
* @todo implement
* 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.
*/
protected async validateMkDocs() {}
protected async validateMkDocsConfig() {
let mkDocsYmlPath = this.mkDocsYmlPath ?? (await findMkDocsYml(this.cwd));
if (!mkDocsYmlPath) {
return this.fail(
`Could not find ${NAME_MKDOCS_YML} from ${this.cwd}. Run "${NAME_BIN} init" to create it`
);
}
let mkDocsYml: MkDocsYml | undefined;
try {
mkDocsYml = await readYaml(mkDocsYmlPath);
} catch (e) {
const err = e as NodeJS.ErrnoException;
if (err.code === NAME_ERR_ENOENT) {
return this.fail(
`Could not find ${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(`No site_name in ${mkDocsYmlPath}; this is required by MkDocs`);
}
this.ok('MkDocs config OK');
}
/**
* Validates that the version of `npm` matches what's described in this package's `engines` field.
@@ -296,18 +397,19 @@ export class DocutilsValidator extends EventEmitter {
throw new DocutilsError('Could not find property engines.npm in package.json. This is a bug');
}
try {
const {stdout: npmVersion} = await exec(this.npmPath, ['-v']);
if (!satisfies(npmVersion.trim(), npmEngineRange)) {
this.fail(
new DocutilsError(
`${NAME_NPM} is version ${npmVersion}, but ${npmEngineRange} is required`
)
const npmPath = this.npmPath ?? (await whichNpm());
if (!npmPath) {
throw new DocutilsError(
`Could not find ${NAME_NPM} in PATH. That seems weird, doesn't it?`
);
}
const {stdout: npmVersion} = await exec(npmPath, ['-v']);
if (!satisfies(npmVersion.trim(), npmEngineRange)) {
this.fail(`${NAME_NPM} is version ${npmVersion}, but ${npmEngineRange} is required`);
return;
}
} catch {
this.fail(new DocutilsError(`Could not find ${this.npmPath} in PATH. Is it installed?`));
return;
return this.fail(`Could not find ${this.npmPath} in PATH. Is it installed?`);
}
this.ok(`${NAME_NPM} version OK`);
}
@@ -320,8 +422,12 @@ export class DocutilsValidator extends EventEmitter {
*/
protected async validatePythonDeps() {
let pipListOutput: string;
const pythonPath = this.pythonPath ?? (await whichPython());
if (!pythonPath) {
return this.fail(`Could not find ${NAME_PYTHON} in PATH. Is it installed?`);
}
try {
({stdout: pipListOutput} = await exec(this.pythonPath, [
({stdout: pipListOutput} = await exec(pythonPath, [
'-m',
NAME_PIP,
'list',
@@ -329,8 +435,7 @@ export class DocutilsValidator extends EventEmitter {
'json',
]));
} catch {
this.fail(new DocutilsError(`Could not find ${NAME_PIP} in PATH. Is it installed?`));
return;
return this.fail(`Could not find ${NAME_PIP} in PATH. Is it installed?`);
}
let installedPkgs: PipPackage[];
@@ -383,12 +488,7 @@ export class DocutilsValidator extends EventEmitter {
);
}
if (msgParts.length) {
this.fail(
new DocutilsError(
`Required Python dependency validation failed:\n\n${msgParts.join('\n\n')}`
)
);
return;
return this.fail(`Required Python dependency validation failed:\n\n${msgParts.join('\n\n')}`);
}
this.ok('Python dependencies OK');
@@ -398,19 +498,20 @@ export class DocutilsValidator extends EventEmitter {
* Asserts that the Python version is 3.x
*/
protected async validatePythonVersion() {
const pythonPath = this.pythonPath ?? (await whichPython());
if (!pythonPath) {
return this.fail(`Could not find ${NAME_PYTHON} in PATH. Is it installed?`);
}
try {
const {stdout} = await exec(this.pythonPath, ['--version']);
const {stdout} = await exec(pythonPath, ['--version']);
if (!stdout.includes(PYTHON_VER_STR)) {
this.fail(
new DocutilsError(
`Could not find Python 3.x in PATH; found ${stdout}. Please use --python-path`
)
return this.fail(
`Could not find Python 3.x in PATH; found ${stdout}. Please use --python-path`
);
return;
}
} catch {
this.fail(new DocutilsError(`Could not find Python 3.x in PATH.`));
return;
return this.fail(`Could not find Python 3.x in PATH.`);
}
this.ok('Python version OK');
}
@@ -428,8 +529,7 @@ export class DocutilsValidator extends EventEmitter {
cwd: pkgDir,
}));
} catch {
this.fail(new DocutilsError(`Could not find ${NAME_TYPEDOC} executable from ${pkgDir}`));
return;
return this.fail(`Could not find ${NAME_TYPEDOC} executable from ${pkgDir}`);
}
if (rawTypeDocVersion) {
@@ -444,12 +544,9 @@ export class DocutilsValidator extends EventEmitter {
const reqdTypeDocVersion = DOCUTILS_PKG.dependencies!.typedoc!;
if (!satisfies(typeDocVersion, reqdTypeDocVersion)) {
this.fail(
new DocutilsError(
`Found TypeDoc version ${typeDocVersion}, but ${reqdTypeDocVersion} is required`
)
return this.fail(
`Found TypeDoc version ${typeDocVersion}, but ${reqdTypeDocVersion} is required`
);
return;
}
this.ok('TypeDoc install OK');
}
@@ -461,8 +558,7 @@ export class DocutilsValidator extends EventEmitter {
protected async validateTypeDocConfig() {
const pkgDir = await this.findPkgDir();
if (!pkgDir) {
this.fail(new DocutilsError(`Could not find package.json in ${this.cwd}`));
return;
return this.fail(new DocutilsError(`Could not find package.json in ${this.cwd}`));
}
const typeDocJsonPath = (this.typeDocJsonPath =
this.typeDocJsonPath ?? path.join(pkgDir, NAME_TYPEDOC_JSON));
@@ -513,7 +609,7 @@ export class DocutilsValidator extends EventEmitter {
protected async validateTypeScript() {
const pkgDir = await this.findPkgDir();
if (!pkgDir) {
return this.fail(new DocutilsError(`Could not find package.json in ${this.cwd}`));
return this.fail(`Could not find package.json in ${this.cwd}`);
}
let typeScriptVersion: string;
let rawTypeScriptVersion: string;
@@ -522,9 +618,7 @@ export class DocutilsValidator extends EventEmitter {
cwd: pkgDir,
}));
} catch {
return this.fail(
new DocutilsError(`Could not find TypeScript compiler ("tsc") from ${pkgDir}`)
);
return this.fail(`Could not find TypeScript compiler ("tsc") from ${pkgDir}`);
}
let match = rawTypeScriptVersion.match(TYPESCRIPT_VERSION_REGEX);
@@ -532,9 +626,7 @@ export class DocutilsValidator extends EventEmitter {
typeScriptVersion = match[1];
} else {
return this.fail(
new DocutilsError(
`Could not parse TypeScript version from "tsc --version"; output was:\n ${rawTypeScriptVersion}`
)
`Could not parse TypeScript version from "tsc --version"; output was:\n ${rawTypeScriptVersion}`
);
}
@@ -548,14 +640,15 @@ export class DocutilsValidator extends EventEmitter {
if (!satisfies(typeScriptVersion, reqdTypeScriptVersion)) {
return this.fail(
new DocutilsError(
`Found TypeScript version ${typeScriptVersion}, but ${reqdTypeScriptVersion} is required`
)
`Found TypeScript version ${typeScriptVersion}, but ${reqdTypeScriptVersion} is required`
);
}
this.ok('TypeScript install OK');
}
/**
* Validates a `tsconfig.json` file
*/
protected async validateTypeScriptConfig() {
const pkgDir = await this.findPkgDir();
if (!pkgDir) {
@@ -628,4 +721,8 @@ export interface DocutilsValidatorOpts {
* If `true`, run TypeScript validation
*/
typescript?: boolean;
/**
* If `true`, run MkDocs validation
*/
mkdocs?: boolean;
}