mirror of
https://github.com/appium/appium.git
synced 2026-04-22 18:40:00 -05:00
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:
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user