UNPKG

@appium/docutils

Version:

Documentation generation utilities for Appium and related projects

252 lines (228 loc) 7.88 kB
/** * Functions which touch the filesystem * @module */ import path from 'node:path'; import {fs, util} from '@appium/support'; import {exec} from 'teen_process'; import type {JsonValue} from 'type-fest'; import * as YAML from 'yaml'; import { MESSAGE_PYTHON_MISSING, NAME_MIKE, NAME_MKDOCS, NAME_MKDOCS_YML, NAME_PACKAGE_JSON, NAME_PYTHON, } from './constants'; import {DocutilsError} from './error'; import {getLogger} from './logger'; import type {MkDocsYml} from './model'; import {findPackageRoot, type NormalizedPackageJson, type PackageJson, mergeDefaultsDeep, readPackage} from './utils'; const log = getLogger('fs'); /** * Finds path to closest `package.json` * * Caches result */ const findPkgDir = util.memoize(findPackageRoot); /** * Stringifies a thing into a YAML * @param value Something to yamlify * @returns Some nice YAML 4 u */ export const stringifyYaml = (value: JsonValue): string => YAML.stringify(value, undefined, {indent: 2}); /** * Pretty-stringifies a JSON value * @param value Something to stringify * @returns JSON string */ export const stringifyJson = (value: JsonValue): string => JSON.stringify(value, undefined, 2); /** * Reads a YAML file, parses it and caches the result */ const readYaml = util.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> { try { return path.join(await findPkgDir(cwd), filename); } catch { return undefined; } } /** * 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 = util.memoize(async (cwd = process.cwd()) => await findInPkgDir(NAME_MKDOCS_YML, cwd)); /** * 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. Defaults to `true`. * @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}> { let pkgDir: string; try { pkgDir = await findPkgDir(cwd); } catch { 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); const pkg = await readPackage({cwd: pkgDir, normalize: normalize !== false}); return {pkg, pkgPath}; } /** * Given a directory to start from, reads a `package.json` file and returns its path and contents */ export const readPackageJson = util.memoize(_readPkgJson); /** * Reads a JSON file and parses it */ export const readJson = util.memoize( async <T extends JsonValue>(filepath: string): Promise<T> => JSON.parse(await fs.readFile(filepath, 'utf8')), ); type WhichFunction = (cmd: string, opts?: {nothrow: boolean}) => Promise<string | null>; /** * 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 = typeof content === 'string' ? content : JSON.stringify(content, undefined, 2); return fs.writeFile(filepath, data, { encoding: 'utf8', }); } /** * `which` with memoization */ const cachedWhich = util.memoize(fs.which as WhichFunction); /** * Finds `python` executable */ const whichPython = async () => await cachedWhich(NAME_PYTHON, {nothrow: true}); /** * Finds `python3` executable */ const whichPython3 = async () => await cachedWhich(`${NAME_PYTHON}3`, {nothrow: true}); /** * Check if `mkdocs` is installed */ export const isMkDocsInstalled = util.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 = 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 = util.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: (filepath: string, cwd?: string) => Promise<MkDocsYml> = util.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 = mergeDefaultsDeep(mkDocsYml, inheritYml); inheritPath = inheritYml.INHERIT ? path.resolve(path.dirname(inheritPath), inheritYml.INHERIT) : undefined; } } return mkDocsYml; }, );