UNPKG

@appium/docutils

Version:

Documentation generation utilities for Appium and related projects

300 lines (275 loc) 8.58 kB
/** * Functions which touch the filesystem * @module */ import {fs} from '@appium/support'; import * as JSON5 from 'json5'; import _ from 'lodash'; import path from 'node:path'; import _pkgDir from 'pkg-dir'; import readPkg, {NormalizedPackageJson, PackageJson} from 'read-pkg'; import {JsonValue} from 'type-fest'; import YAML from 'yaml'; import { MESSAGE_PYTHON_MISSING, NAME_MIKE, NAME_MKDOCS, NAME_MKDOCS_YML, NAME_NPM, NAME_PACKAGE_JSON, NAME_PYTHON, } from './constants'; import {DocutilsError} from './error'; import {getLogger} from './logger'; import {MkDocsYml} from './model'; import {exec} from 'teen_process'; const log = getLogger('fs'); /** * Finds path to closest `package.json` * * Caches result */ export const findPkgDir = _.memoize(_pkgDir); /** * Stringifies a thing into a YAML * @param value Something to yamlify * @returns Some nice YAML 4 u */ export const stringifyYaml: (value: JsonValue) => string = _.partialRight( YAML.stringify, {indent: 2}, undefined, ); /** * Stringifies something into JSON5. I think the only difference between this and `JSON.stringify` * is that if an object has a `toJSON5()` method, it will be used. * @param value Something to stringify * @returns JSON5 string */ export const stringifyJson5: (value: JsonValue) => string = _.partialRight(JSON5.stringify, { indent: 2, }); /** * Pretty-stringifies a JSON value * @param value Something to stringify * @returns JSON string */ export const stringifyJson: (value: JsonValue) => string = _.partialRight( JSON.stringify, 2, undefined, ); /** * Reads a YAML file, parses it and caches the result */ const readYaml = _.memoize(async (filepath: string) => YAML.parse(await fs.readFile(filepath, 'utf8'), { prettyErrors: false, logLevel: 'silent', }), ); /** * 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 an `mkdocs.yml`, expected to be a sibling of `package.json` * * Caches the result. * @param cwd - Current working directory * @returns Path to `mkdocs.yml` */ export const findMkDocsYml = _.memoize(_.partial(findInPkgDir, NAME_MKDOCS_YML)); /** * Given a directory path, finds closest `package.json` and reads it. * @param cwd - Current working directory * @param normalize - Whether or not to normalize the result * @returns A {@linkcode PackageJson} object if `normalize` is `false`, otherwise a {@linkcode NormalizedPackageJson} object */ async function _readPkgJson( cwd: string, normalize: true, ): Promise<{pkgPath: string; pkg: NormalizedPackageJson}>; async function _readPkgJson( cwd: string, normalize?: false, ): Promise<{pkgPath: string; pkg: PackageJson}>; async function _readPkgJson( cwd: string, normalize?: boolean, ): Promise<{pkgPath: string; pkg: PackageJson | NormalizedPackageJson}> { const pkgDir = await findPkgDir(cwd); if (!pkgDir) { throw new DocutilsError( `Could not find a ${NAME_PACKAGE_JSON} near ${cwd}; please create it before using this utility`, ); } const pkgPath = path.join(pkgDir, NAME_PACKAGE_JSON); log.debug('Found `package.json` at %s', pkgPath); if (normalize) { const pkg = await readPkg({cwd: pkgDir, normalize}); return {pkg, pkgPath}; } else { const pkg = await readPkg({cwd: pkgDir}); return {pkg, pkgPath}; } } /** * Given a directory to start from, reads a `package.json` file and returns its path and contents */ export const readPackageJson = _.memoize(_readPkgJson); /** * Reads a JSON5 file and parses it */ export const readJson5 = _.memoize( async <T extends JsonValue>(filepath: string): Promise<T> => JSON5.parse(await fs.readFile(filepath, 'utf8')), ); /** * Reads a JSON file and parses it */ export const readJson = _.memoize( async <T extends JsonValue>(filepath: string): Promise<T> => JSON.parse(await fs.readFile(filepath, 'utf8')), ); /** * Writes contents to a file. Any JSON objects are stringified * @param filepath - Path to file * @param content - File contents */ export function writeFileString(filepath: string, content: JsonValue) { const data: string = _.isString(content) ? content : JSON.stringify(content, undefined, 2); return fs.writeFile(filepath, data, { encoding: 'utf8', }); } type WhichFunction = (cmd: string, opts?: {nothrow: boolean}) => Promise<string|null>; /** * `which` with memoization */ const cachedWhich = _.memoize(fs.which as WhichFunction); /** * Finds `npm` executable */ export const whichNpm = _.partial(cachedWhich, NAME_NPM, {nothrow: true}); /** * Finds `python` executable */ const whichPython = _.partial(cachedWhich, NAME_PYTHON, {nothrow: true}); /** * Finds `python3` executable */ const whichPython3 = _.partial(cachedWhich, `${NAME_PYTHON}3`, {nothrow: true}); /** * Check if `mkdocs` is installed */ export const isMkDocsInstalled = _.memoize(async (): Promise<boolean> => { // see if it's in PATH const mkDocsPath = await cachedWhich(NAME_MKDOCS, {nothrow: true}); if (mkDocsPath) { return true; } // if it isn't, it should be invokable via `python -m` const pythonPath = await findPython(); if (!pythonPath) { return false; } try { await exec(pythonPath, ['-m', NAME_MKDOCS]); return true; } catch { return false; } }); /** * `mike` cannot be invoked via `python -m`, so we need to find the script. */ export const findMike = _.partial(async () => { // see if it's in PATH let mikePath = await cachedWhich(NAME_MIKE, {nothrow: true}); if (mikePath) { return mikePath; } // if it isn't, it may be in a user dir const pythonPath = await findPython(); if (!pythonPath) { return; } try { // the user dir can be found this way. // usually it's something like ~/.local const {stdout} = await exec(pythonPath, ['-m', 'site', '--user-base']); if (stdout) { mikePath = path.join(stdout.trim(), 'bin', 'mike'); if (await fs.isExecutable(mikePath)) { return mikePath; } } } catch {} }); /** * Finds the `python3` or `python` executable in the user's `PATH`. * * `python3` is preferred over `python`, since the latter could be Python 2. */ export const findPython = _.memoize( async (): Promise<string | null> => (await whichPython3()) ?? (await whichPython()), ); /** * Check if a path to Python exists, otherwise raise DocutilsError */ export async function requirePython(pythonPath?: string): Promise<string> { const foundPythonPath = pythonPath ?? (await findPython()); if (!foundPythonPath) { throw new DocutilsError(MESSAGE_PYTHON_MISSING); } return foundPythonPath; } /** * Reads an `mkdocs.yml` file, merges inherited configs, and returns the result. The result is cached. * * **IMPORTANT**: The paths of `site_dir` and `docs_dir` are resolved to absolute paths, since they * are expressed as relative paths, and each inherited config file can live in different paths. * @param filepath Patgh to an `mkdocs.yml` file * @returns Parsed `mkdocs.yml` file */ export const readMkDocsYml = _.memoize( async (filepath: string, cwd = process.cwd()): Promise<MkDocsYml> => { let mkDocsYml = (await readYaml(filepath)) as MkDocsYml; if (mkDocsYml.site_dir) { mkDocsYml.site_dir = path.resolve(cwd, path.dirname(filepath), mkDocsYml.site_dir); } if (mkDocsYml.INHERIT) { let inheritPath: string | undefined = path.resolve(path.dirname(filepath), mkDocsYml.INHERIT); while (inheritPath) { const inheritYml = (await readYaml(inheritPath)) as MkDocsYml; if (inheritYml.site_dir) { inheritYml.site_dir = path.resolve(path.dirname(inheritPath), inheritYml.site_dir); log.debug('Resolved site_dir to %s', inheritYml.site_dir); } if (inheritYml.docs_dir) { inheritYml.docs_dir = path.resolve(path.dirname(inheritPath), inheritYml.docs_dir); log.debug('Resolved docs_dir to %s', inheritYml.docs_dir); } mkDocsYml = _.defaultsDeep(mkDocsYml, inheritYml); inheritPath = inheritYml.INHERIT ? path.resolve(path.dirname(inheritPath), inheritYml.INHERIT) : undefined; } } return mkDocsYml; }, );