@appium/docutils
Version:
Documentation generation utilities for Appium and related projects
298 lines (281 loc) • 7.72 kB
text/typescript
/**
* Scaffolding functions for CLI `init` command
*
* @module
*/
import * as JSON5 from 'json5';
import {NAME_MKDOCS_YML, NAME_TSCONFIG_JSON, PIP_ENV_VARS, REQUIREMENTS_TXT_PATH} from './constants';
import YAML from 'yaml';
import {exec} from 'teen_process';
import {Simplify} from 'type-fest';
import {DocutilsError} from './error';
import {createScaffoldTask, ScaffoldTaskOptions} from './scaffold';
import type { ScaffoldTask } from './scaffold';
import {getLogger} from './logger';
import {MkDocsYml, TsConfigJson} from './model';
import _ from 'lodash';
import {requirePython, stringifyJson5, stringifyYaml} from './fs';
/**
* Data for the base `mkdocs.yml` file
*/
const BASE_MKDOCS_YML: Readonly<MkDocsYml> = Object.freeze({
INHERIT: './node_modules/@appium/docutils/base-mkdocs.yml',
docs_dir: 'docs',
site_dir: 'site',
});
/**
* Data for the base `tsconfig.json` file
*/
const BASE_TSCONFIG_JSON: Readonly<TsConfigJson> = Object.freeze({
$schema: 'https://json.schemastore.org/tsconfig',
extends: '@appium/tsconfig/tsconfig.json',
compilerOptions: {
outDir: 'build',
},
});
const log = getLogger('init');
const dryRunLog = getLogger('dry-run', log);
/**
* Files included, by default, in `tsconfig.json`
*/
const DEFAULT_INCLUDE = ['lib', 'test', 'index.js'];
/**
* Function which scaffolds a `tsconfig.json` file
*/
export const initTsConfigJson = createScaffoldTask<InitTsConfigOptions, TsConfigJson>(
NAME_TSCONFIG_JSON,
BASE_TSCONFIG_JSON,
'TypeScript configuration',
{
/**
* Merges the contents of the `include` property with any passed on the CLI. If neither exists,
* uses the default set of includes.
* @param content Parsed and/or scaffolded `tsconfig.json`
* @param opts Options specific to this task
* @returns `tsconfig.json` content with potentially-modified `include` prop
*/
transform: (content, {include}) => {
include = [...(content.include ?? include ?? [])];
if (_.isEmpty(include)) {
include = [...DEFAULT_INCLUDE];
}
return {
...content,
include: _.uniq(include),
};
},
deserialize: JSON5.parse,
serialize: stringifyJson5,
},
);
/**
* Function which scaffolds an `mkdocs.yml` file
*/
export const initMkDocs: ScaffoldTask<InitMkDocsOptions, MkDocsYml> = createScaffoldTask<InitMkDocsOptions, MkDocsYml>(
NAME_MKDOCS_YML,
BASE_MKDOCS_YML,
'MkDocs configuration',
{
deserialize: YAML.parse,
serialize: stringifyYaml,
transform: (content, opts, pkg) => {
let siteName = opts.siteName ?? content.site_name;
if (!siteName) {
siteName = pkg.name ?? '(no name)';
if (siteName) {
log.info('Using site name from package.json: %s', siteName);
}
}
let repoUrl: string | undefined = opts.repoUrl ?? content.repo_url;
if (!repoUrl) {
repoUrl = pkg.repository?.url;
if (repoUrl) {
log.info('Using repo URL from package.json: %s', repoUrl);
}
}
let repoName = opts.repoName ?? content.repo_name;
if (repoUrl && !repoName) {
let {pathname} = new URL(repoUrl);
pathname = pathname.slice(1);
const pathparts = pathname.split('/');
const owner = pathparts[0];
let repo = pathparts[1];
repo = repo.replace(/\.git$/, '');
repoName = [owner, repo].join('/');
if (repoName) {
log.info('Using repo name from package.json: %s', repoName);
}
}
let siteDescription = opts.siteDescription ?? content.site_description;
if (!siteDescription) {
siteDescription = pkg.description;
if (siteDescription) {
log.info('Using site description URL from package.json: %s', siteDescription);
}
}
return {
...content,
site_name: siteName,
repo_url: repoUrl,
repo_name: repoName,
site_description: siteDescription,
};
},
},
);
/**
* Installs Python dependencies
* @param opts Options
*/
export async function initPython({
pythonPath,
dryRun = false,
upgrade = false,
}: InitPythonOptions = {}): Promise<void> {
const foundPythonPath = await requirePython(pythonPath);
const args = ['-m', 'pip', 'install', '-r', REQUIREMENTS_TXT_PATH];
if (upgrade) {
args.push('--upgrade');
}
if (dryRun) {
dryRunLog.info(
'Would execute command: %s %s (environment variables: %s)',
foundPythonPath,
args.join(' '),
PIP_ENV_VARS,
);
} else {
log.debug('Executing command: %s %s (environment variables: %s)',
foundPythonPath,
args.join(' '),
PIP_ENV_VARS,
);
log.info('Installing Python dependencies...');
try {
const result = await exec(foundPythonPath, args, {env: PIP_ENV_VARS, shell: true});
const {code, stdout} = result;
if (code !== 0) {
throw new DocutilsError(`Could not install Python dependencies. Reason: ${stdout}`);
}
} catch (err) {
throw new DocutilsError(
`Could not install Python dependencies. Reason: ${(err as Error).message}`,
);
}
}
log.success('Successfully installed Python dependencies');
}
/**
* Options for {@linkcode initMkDocs}
*/
export interface InitMkDocsOptions extends ScaffoldTaskOptions {
copyright?: string;
repoName?: string;
repoUrl?: string;
siteDescription?: string;
siteName?: string;
}
/**
* Main handler for `init` command.
*
* This runs tasks in serial; it _could_ run in parallel, but it has deleterious effects upon
* console output which would need mitigation.
*/
export async function init({
typescript,
python,
tsconfigJson: tsconfigJsonPath,
packageJson: packageJsonPath,
overwrite,
include,
mkdocs,
mkdocsYml: mkdocsYmlPath,
siteName,
repoName,
repoUrl,
copyright,
dryRun,
cwd,
pythonPath,
upgrade,
}: InitOptions = {}): Promise<void> {
if (typescript && !upgrade) {
await initTsConfigJson({
dest: tsconfigJsonPath,
packageJson: packageJsonPath,
overwrite,
include,
dryRun,
cwd,
});
}
if (python) {
await initPython({pythonPath, dryRun, upgrade});
}
if (mkdocs && !upgrade) {
await initMkDocs({
dest: mkdocsYmlPath,
cwd,
siteName,
repoUrl,
repoName,
copyright,
packageJson: packageJsonPath,
overwrite,
dryRun,
});
}
}
export interface InitTsConfigOptions extends ScaffoldTaskOptions {
/**
* List of source files (globs supported); typically `src` or `lib`
*/
include?: string[];
}
export interface InitPythonOptions extends ScaffoldTaskOptions {
/**
* Path to `python` (v3.x) executable
*/
pythonPath?: string;
/**
* If true, upgrade only
*/
upgrade?: boolean;
}
/**
* Options for `init` command handler
*
* The props of the various "path" options are rewritten as `dest` for the scaffold tasks functions.
*/
export type InitOptions = Simplify<
Omit<InitPythonOptions & InitTsConfigOptions & InitMkDocsOptions, 'dest'> & {
/**
* If `true` will initialize a `tsconfig.json` file
*/
typescript?: boolean;
/**
* If `true` will install Python deps
*/
python?: boolean;
/**
* If `true` will initialize a `mkdocs.yml` file
*/
mkdocs?: boolean;
/**
* Path to new or existing `tsconfig.json` file
*/
tsconfigJson?: string;
/**
* Path to existing `package.json` file
*/
packageJson?: string;
/**
* Path to new or existing `mkdocs.yml` file
*/
mkdocsYml?: string;
/**
* If `true`, upgrade only
*/
upgrade?: boolean;
}
>;