chore(docutils): break out fs-related functions in util into their own module
ADD
packages/docutils/lib/fs.ts
+/**
+ * Functions which touch the filesystem
+ * @module
+ */
+
+import YAML from 'yaml';
+import readPkg, {NormalizedPackageJson, PackageJson} from 'read-pkg';
+import path from 'node:path';
+import {JsonValue} from 'type-fest';
+import {fs} from '@appium/support';
+import * as JSON5 from 'json5';
+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 {DocutilsError} from './error';
+
+const log = logger.withTag('fs');
+
+/**
+ * Finds path to closest `package.json`
+ *
+ * Caches result
+ */
+export const findPkgDir = _.memoize(_pkgDir);
+
+/**
+ * Stringifies a thing into a YAML
+ *
+ * The indent is 4 because Prettier
+ * @param value Something to yamlify
+ * @returns Some nice YAML 4 u
+ */
+export const stringifyYaml: (value: JsonValue) => string = _.partialRight(
+ YAML.stringify,
+ {indent: 4},
+ 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
+ */
+export const readYaml = _.memoize(async (filepath: string) =>
+ YAML.parse(await fs.readFile(filepath, 'utf8'))
+);
+
+/**
+ * Finds a `typedoc.json`, expected to be a sibling of `package.json`
+ *
+ * Caches the result. Does not check if `typedoc.json` actually exists
+ * @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);
+ }
+);
+
+/**
+ * Finds an `mkdocs.yml`, expected to be a sibling of `package.json`
+ *
+ * Caches the result. Does not check if `mkdocs.yml` actually exists
+ * @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);
+ }
+);
+
+/**
+ * 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): 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 `typedoc.json` file and returns its parsed contents.
+ *
+ * TypeDoc expands the "extends" field, which is why we use its facilities. It, unfortunately, is a
+ * blocking operation.
+ */
+export const readTypedocJson = _.memoize((typedocJsonPath: string) => {
+ const app = new Application();
+ app.options.setValue('plugin', 'none');
+ app.options.setValue('logger', 'none');
+ // yes, this is how you do it. yes, it could be easier. no, I don't know why.
+ app.options.setValue('options', typedocJsonPath);
+ app.options.addReader(new TypeDocReader());
+ app.bootstrap();
+ return app.options.getRawValues();
+});
+
+/**
+ * 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 a file, but will not overwrite an existing file unless `overwrite` is true
+ *
+ * Will stringify JSON objects
+ * @param filepath - Path to file
+ * @param content - File contents
+ * @param overwrite - If `true`, overwrite existing files
+ */
+export function safeWriteFile(filepath: string, content: JsonValue, overwrite = false) {
+ const data: string = _.isString(content) ? content : JSON.stringify(content, undefined, 2);
+ return fs.writeFile(filepath, data, {
+ encoding: 'utf8',
+ flag: overwrite ? 'w' : 'wx',
+ });
+}

MODIFY
packages/docutils/lib/init.ts
packages/docutils/lib/init.ts
@@ -13,8 +13,8 @@ import {DocutilsError} from './error';
import {createScaffoldTask, ScaffoldTaskOptions} from './scaffold';
import logger from './logger';
import {MkDocsYml, TsConfigJson, TypeDocJson} from './model';
-import {stringifyJson5, stringifyYaml} from './util';
import _ from 'lodash';
+import {stringifyJson5, stringifyYaml} from './fs';
/**
* Data for the base `mkdocs.yml` file

MODIFY
packages/docutils/lib/mkdocs.ts
packages/docutils/lib/mkdocs.ts
import {exec, SubProcess} from 'teen_process';
import {NAME_MKDOCS} from './constants';
-import {findMkDocsYml, stopwatch} from './util';
+import {guessMkDocsYmlPath} from './fs';
import logger from './logger';
+import {stopwatch} from './util';
const log = logger.withTag('mkdocs');
@@ -34,7 +35,7 @@ export async function buildMkDocs({
packageJson: packageJsonPath,
}: BuildMkDocsOpts = {}) {
const stop = stopwatch('build-mkdocs');
- mkdocsYmlPath = mkdocsYmlPath ?? (await findMkDocsYml(cwd, packageJsonPath));
+ mkdocsYmlPath = mkdocsYmlPath ?? (await guessMkDocsYmlPath(cwd, packageJsonPath));
const mkdocsArgs = ['build', '-f', mkdocsYmlPath, '-t', theme];
if (siteDir) {
mkdocsArgs.push('-d', siteDir);

MODIFY
packages/docutils/lib/nav.ts
packages/docutils/lib/nav.ts
+import {fs} from '@appium/support';
import _ from 'lodash';
import path from 'node:path';
-import {fs} from '@appium/support';
+import {DEFAULT_REL_TYPEDOC_OUT_PATH} from './constants';
import {
- findMkDocsYml,
- findTypeDocJson,
+ guessMkDocsYmlPath,
+ guessTypeDocJsonPath,
readTypedocJson,
readYaml,
- relative,
safeWriteFile,
stringifyYaml,
-} from './util';
-import {DEFAULT_REL_TYPEDOC_OUT_PATH} from './constants';
+} from './fs';
import logger from './logger';
import {MkDocsYml} from './model';
-import {IterableElement} from 'type-fest';
+import {relative} from './util';
const DEFAULT_REFERENCE_HEADER = 'Reference';
@@ -33,8 +32,8 @@ export async function updateNav<S extends string>({
dryRun = false,
}: UpdateNavOpts<S> = {}) {
[mkDocsYmlPath, typeDocJsonPath] = await Promise.all([
- mkDocsYmlPath ?? findMkDocsYml(cwd, packageJsonPath),
- typeDocJsonPath ?? findTypeDocJson(cwd, packageJsonPath),
+ mkDocsYmlPath ?? guessMkDocsYmlPath(cwd, packageJsonPath),
+ typeDocJsonPath ?? guessTypeDocJsonPath(cwd, packageJsonPath),
]);
const relativePath = relative(cwd);
const relMkDocsYmlPath = relativePath(mkDocsYmlPath);

MODIFY
packages/docutils/lib/scaffold.ts
packages/docutils/lib/scaffold.ts
@@ -5,8 +5,9 @@ import {createPatch} from 'diff';
import {NormalizedPackageJson} from 'read-pkg';
import {JsonValue, JsonObject} from 'type-fest';
import {DocutilsError} from './error';
-import {relative, readPackageJson, safeWriteFile} from './util';
+import {relative} from './util';
import _ from 'lodash';
+import {stringifyJson, readPackageJson, safeWriteFile} from './fs';
const NAME_ERR_ENOENT = 'ENOENT';
const NAME_ERR_EEXIST = 'EEXIST';
@@ -14,8 +15,6 @@ const NAME_ERR_EEXIST = 'EEXIST';
const log = logger.withTag('init');
const dryRunLog = logger.withTag('dry-run');
-const jsonStringify = _.partial(JSON.stringify, _, undefined, 2);
-
/**
* Creates a unified patch for display in "dry run" mode
* @param filename - File name to use
@@ -23,11 +22,11 @@ const jsonStringify = _.partial(JSON.stringify, _, undefined, 2);
* @param newData - New Data
* @returns Patch string
*/
-function makePatch(
+function makePatch<T extends JsonValue>(
filename: string,
- oldData: JsonValue | string,
- newData: JsonValue | string,
- serializer = jsonStringify
+ oldData: T | string,
+ newData: T | string,
+ serializer: ScaffoldTaskSerializer<T> = stringifyJson
) {
return createPatch(
filename,
@@ -61,7 +60,7 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
{
transform = _.identity,
deserialize = JSON.parse,
- serialize = jsonStringify,
+ serialize = stringifyJson,
}: CreateScaffoldTaskOptions<Opts, T> = {}
): ScaffoldTask<Opts, T> {
return async ({

MODIFY
packages/docutils/lib/typedoc.ts
packages/docutils/lib/typedoc.ts
@@ -5,8 +5,9 @@ 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 {DocutilsError} from './error';
+import {guessTypeDocJsonPath, readTypedocJson} from './fs';
import log from './logger';
-import {findTypeDocJson, readTypedocJson, relative, stopwatch} from './util';
+import {relative, stopwatch} from './util';
/**
* Replaces TypeDoc's homebrew "glob" implementation with a real one
@@ -96,7 +97,7 @@ export async function buildReference({
title,
}: BuildReferenceOptions = {}) {
const stop = stopwatch('buildReference');
- typeDocJsonPath = typeDocJsonPath ?? (await findTypeDocJson(cwd, packageJsonPath));
+ typeDocJsonPath = typeDocJsonPath ?? (await guessTypeDocJsonPath(cwd, packageJsonPath));
const pkgRoot = fs.findRoot(cwd);
const relativePath = relative(cwd);
const relativeTypeDocJsonPath = relativePath(typeDocJsonPath);

MODIFY
packages/docutils/lib/util.ts
packages/docutils/lib/util.ts
* @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} from 'read-pkg';
-import {JsonValue, PackageJson} from 'type-fest';
-import {Application, LogLevel, TypeDocReader} from 'typedoc';
-import {discoverPlugins, loadPlugins} from 'typedoc/dist/lib/utils';
-import YAML from 'yaml';
-import {NAME_MKDOCS_YML, NAME_PACKAGE_JSON, NAME_TYPEDOC_JSON} from './constants';
-import {DocutilsError} from './error';
-import log from './logger';
/**
- * Computes a relative path
+ * Computes a relative path, prepending `./`
*/
export const relative = _.curry(
(from: string, to: string): string => `.${path.sep}${path.relative(from, to)}`
);
-/**
- * Finds path to closest `package.json`
- */
-export const findPkgDir = _.memoize(_pkgDir);
-
-/**
- * Writes a file, but will not overwrite an existing file unless `overwrite` is true
- *
- * Will stringify JSON objects
- * @param filepath - Path to file
- * @param content - File contents
- * @param overwrite - If `true`, overwrite existing files
- */
-export function safeWriteFile(filepath: string, content: JsonValue, overwrite = false) {
- const data: string = _.isString(content) ? content : JSON.stringify(content, undefined, 2);
- return fs.writeFile(filepath, data, {
- encoding: 'utf8',
- flag: overwrite ? 'w' : 'wx',
- });
-}
-
-/**
- * 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'))
-);
-
-export const readJson5 = _.memoize(
- async <T extends JsonValue>(filepath: string): Promise<T> =>
- JSON5.parse(await fs.readFile(filepath, 'utf8'))
-);
-
/**
* A stopwatch-like thing
+ *
+ * Used for displaying elapsed time in milliseconds
* @param id - Unique identifier
* @returns Function that returns the elapsed time in milliseconds
*/
@@ -73,105 +30,3 @@ export function stopwatch(id: string) {
};
}
stopwatch.cache = new Map<string, number>();
-
-/**
- * Computes where a `typedoc.json` _should_ live
- */
-
-export const findTypeDocJson = _.memoize(
- /**
- * @param cwd - Current working directory
- * @param packageJsonPath - Path to `package.json`
- */
- 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);
- }
-);
-
-/**
- * @param cwd - Current working directory
- * @param packageJsonPath - Path to `package.json`
- * @returns Path to `mkdocs.yml`
- */
-export const findMkDocsYml = _.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);
-});
-
-/**
- * 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): 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 `typedoc.json` file and returns its parsed contents.
- *
- * TypeDoc expands the "extends" field, which is why we use its facilities. It, unfortunately, is a
- * blocking operation.
- */
-export const readTypedocJson = _.memoize((typedocJsonPath: string) => {
- const app = new Application();
- app.options.setValue('plugin', 'none');
- app.options.setValue('logger', 'none');
- // yes, this is how you do it. yes, it could be easier. no, I don't know why.
- app.options.setValue('options', typedocJsonPath);
- app.options.addReader(new TypeDocReader());
- app.bootstrap();
- return app.options.getRawValues();
-});
-
-/**
- * Stringifies a thing into a YAML
- *
- * The indent is 4 because Prettier
- * @param value Something to yamlify
- * @returns Some nice YAML 4 u
- */
-export const stringifyYaml = (value: JsonValue) => YAML.stringify(value, {indent: 4});
-
-/**
- * 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) => JSON5.stringify(value, undefined, 2);
-
-export const readYaml = _.memoize(async (filepath: string) =>
- YAML.parse(await fs.readFile(filepath, 'utf8'))
-);

MODIFY
packages/docutils/lib/validate.ts
packages/docutils/lib/validate.ts
@@ -22,9 +22,10 @@ import {
REQUIREMENTS_TXT_PATH,
} from './constants';
import {DocutilsError} from './error';
+import {findPkgDir, readJson5, readTypedocJson} from './fs';
import logger from './logger';
import {PipPackage, TypeDocJson} from './model';
-import {findPkgDir, readJson5, readTypedocJson, relative} from './util';
+import {relative} from './util';
/**
* Matches the Python version string from `python --version`

chore(docutils): break out fs-related functions in util into their own module