UNPKG

@appium/docutils

Version:

Documentation generation utilities for Appium and related projects

232 lines (214 loc) 6.46 kB
/** * Implementation of a generic "create and/or update some file" task * @module */ import {fs} from '@appium/support'; import {getLogger} from './logger'; import path from 'node:path'; import {createPatch} from 'diff'; import {NormalizedPackageJson} from 'read-pkg'; import {JsonValue, JsonObject} from 'type-fest'; import {DocutilsError} from './error'; import {relative} from './util'; import _ from 'lodash'; import {stringifyJson, readPackageJson, writeFileString} from './fs'; import {NAME_ERR_ENOENT} from './constants'; const log = getLogger('init'); const dryRunLog = getLogger('dry-run', log); /** * Creates a unified patch for display in "dry run" mode * @param filename - File name to use * @param oldData - Old data * @param newData - New Data * @returns Patch string */ function makePatch<T extends JsonValue>( filename: string, oldData: T | string, newData: T | string, serializer: ScaffoldTaskSerializer<T> = stringifyJson ) { return createPatch( filename, _.isString(oldData) ? oldData : serializer(oldData), _.isString(newData) ? newData : serializer(newData) ); } /** * Options for a task which are not the {@link ScaffoldTaskOptions base options} */ export type TaskSpecificOpts<Opts extends ScaffoldTaskOptions> = Omit< Opts, keyof ScaffoldTaskOptions >; /** * A function which performs some scaffolding task. * * @see {@linkcode createScaffoldTask} */ export type ScaffoldTask<Opts extends ScaffoldTaskOptions, T extends JsonObject> = ( opts: Opts ) => Promise<ScaffoldTaskResult<T>>; /** * Factory for a {@linkcode ScaffoldTask}. * * @param defaultFilename Default file to create * @param defaultContent Default content to use * @param description Description of task * @param opts Options * @returns A scaffold task */ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends JsonObject>( defaultFilename: string, defaultContent: T, description: string, { transform = _.identity, deserialize = JSON.parse, serialize = stringifyJson, }: CreateScaffoldTaskOptions<Opts, T> = {} ): ScaffoldTask<Opts, T> { return async ({ overwrite = false, cwd = process.cwd(), packageJson: packageJsonPath, dest, dryRun = false, ...opts }: Opts): Promise<ScaffoldTaskResult<T>> => { const relativePath = relative(cwd); const {pkgPath, pkg} = await readPackageJson( packageJsonPath ? path.dirname(packageJsonPath) : cwd, true ); const pkgDir = path.dirname(pkgPath); dest = dest ?? path.join(pkgDir, defaultFilename); const relativeDest = relativePath(dest); log.debug('Initializing %s', relativeDest); let destChangesNeeded = false; let isNew = false; let destContent: T; let result: ScaffoldTaskResult<T>; try { destContent = deserialize(await fs.readFile(dest, 'utf8')); log.debug('Found existing file %s', relativeDest); } catch (e) { const err = e as NodeJS.ErrnoException; if (err.code !== NAME_ERR_ENOENT) { throw err; } destChangesNeeded = true; log.debug('Creating new file %s', relativeDest); destContent = {} as T; isNew = true; } const defaults: T = transform(defaultContent, opts, pkg); const finalDestContent: T = _.defaultsDeep({}, destContent, defaults); destChangesNeeded = destChangesNeeded || !_.isEqual(destContent, finalDestContent); if (destChangesNeeded) { log.info('Changes needed in %s', relativeDest); log.debug('Original %s: %O', relativeDest, destContent); log.debug('Final %s: %O', relativeDest, finalDestContent); const patch = makePatch(dest, destContent, finalDestContent, serialize); if (dryRun) { dryRunLog.info('Would apply the following patch: \n\n%s', patch); result = {path: dest, content: finalDestContent}; return result; } if (!isNew && !overwrite) { log.info('File %s already exists, continuing (enable overwrite with "--force")', relativeDest); log.debug('Tried to apply patch:\n\n%s', patch); } else { try { await writeFileString(dest, finalDestContent); if (isNew) { log.success('Initialized %s', description); } else { log.success('Updated %s', description); } } catch (e) { const err = e as NodeJS.ErrnoException; throw new DocutilsError(`Could not write to ${relativeDest}. Reason: ${err.message}`, { cause: err, }); } } } else { log.info('No changes necessary for %s', relativeDest); } log.success(`${description}: done`); return {path: dest, content: finalDestContent}; }; } /** * Optional function which can be used to post-process the content of a file. Usually used to merge * various options with existing content */ export type ScaffoldTaskTransformer<Opts extends ScaffoldTaskOptions, T extends JsonValue> = ( content: Readonly<T>, opts: TaskSpecificOpts<Opts>, pkg: Readonly<NormalizedPackageJson> ) => T; /** * A function which deserializes a string into a JS value. */ export type ScaffoldTaskDeserializer<T> = (content: string) => T; /** * A function which serializes a JS value into a string. */ export type ScaffoldTaskSerializer<T> = (content: T) => string; /** * Options for {@linkcode createScaffoldTask} */ export interface CreateScaffoldTaskOptions<Opts extends ScaffoldTaskOptions, T extends JsonValue> { /** * Transformer function */ transform?: ScaffoldTaskTransformer<Opts, T>; /** * Deserializer function */ deserialize?: ScaffoldTaskDeserializer<T>; /** * Serializer function */ serialize?: ScaffoldTaskSerializer<T>; } /** * Base options for all scaffold tasks */ export interface ScaffoldTaskOptions { /** * Current working directory */ cwd?: string; /** * Destination file */ dest?: string; /** * If `true` will not write files */ dryRun?: boolean; /** * If `true` will overwrite fields in `typedoc.json` */ overwrite?: boolean; /** * Path to `package.json` */ packageJson?: string; } /** * The return value of a {@linkcode ScaffoldTask} */ export interface ScaffoldTaskResult<T> { /** * The content of whatever it wrote or would write */ content: T; /** * The filepath of whatever it wrote or would write */ path: string; }