feat(docutils): add support for deploying with mike
This is enabled via `appium-docs build --deploy`.  Instead of invoking `mkdocs` directly, `mike` will be invoked.
Implementation is in `lib/builder/deploy.ts`.  It's very similar to the MkDocs implementation in `lib/builder/site.ts`; just with new/different options.
I marked the `Mike` class as deprecated, since the new one is fancy
ADD
packages/docutils/lib/builder/deploy.ts
+/**
+ * Functions for running `mike`
+ *
+ * @module
+ */
+
+import {exec, SubProcess, TeenProcessExecOptions} from 'teen_process';
+import {
+ DEFAULT_DEPLOY_BRANCH,
+ DEFAULT_DEPLOY_REMOTE,
+ DEFAULT_SERVE_HOST,
+ DEFAULT_SERVE_PORT,
+ NAME_BIN,
+ NAME_MKDOCS_YML,
+} from '../constants';
+import {DocutilsError} from '../error';
+import {findMkDocsYml, whichMike} from '../fs';
+import logger from '../logger';
+import {argify, stopwatch, TeenProcessSubprocessStartOpts} from '../util';
+
+const log = logger.withTag('builder:deploy');
+
+/**
+ * Runs `mike serve`
+ * @param args Extra args to `mike build`
+ * @param opts Extra options for `teen_process.Subprocess.start`
+ * @param mikePath Path to `mike` executable
+ */
+async function doServe(
+ args: string[] = [],
+ {startDetector, detach, timeoutMs}: TeenProcessSubprocessStartOpts = {},
+ mikePath?: string
+) {
+ mikePath = mikePath ?? (await whichMike());
+ const finalArgs = ['serve', ...args];
+ log.debug('Launching %s with args: %O', mikePath, finalArgs);
+ const proc = new SubProcess(mikePath, finalArgs);
+ return await proc.start(startDetector, detach, timeoutMs);
+}
+
+/**
+ * Runs `mike build`
+ * @param args Extra args to `mike build`
+ * @param opts Extra options to `teen_process.exec`
+ * @param mikePath Path to `mike` executable
+ */
+async function doDeploy(args: string[] = [], opts: TeenProcessExecOptions = {}, mikePath?: string) {
+ mikePath = mikePath ?? (await whichMike());
+ const finalArgs = ['deploy', ...args];
+ log.debug('Launching %s with args: %O', mikePath, finalArgs);
+ return await exec(mikePath, finalArgs, opts);
+}
+
+/**
+ * Runs `mike build` or `mike serve`
+ * @param opts
+ */
+export async function deploy({
+ mkDocsYml: mkDocsYmlPath,
+ cwd = process.cwd(),
+ serve = false,
+ push = false,
+ branch = DEFAULT_DEPLOY_BRANCH,
+ remote = DEFAULT_DEPLOY_REMOTE,
+ prefix,
+ message,
+ deployVersion,
+ alias,
+ rebase = true,
+ port = DEFAULT_SERVE_PORT,
+ host = DEFAULT_SERVE_HOST,
+ serveOpts,
+ execOpts,
+}: DeployOpts = {}) {
+ const stop = stopwatch('deploy');
+ mkDocsYmlPath = mkDocsYmlPath ?? (await findMkDocsYml(cwd));
+ if (!mkDocsYmlPath) {
+ throw new DocutilsError(
+ `Could not find ${NAME_MKDOCS_YML} from ${cwd}; run "${NAME_BIN} init" to create it`
+ );
+ }
+
+ const mikeOpts = {
+ 'config-file': mkDocsYmlPath,
+ push,
+ remote,
+ branch,
+ prefix,
+ message,
+ deployVersion,
+ alias,
+ rebase,
+ port,
+ host,
+ };
+ const mikeArgs = argify(mikeOpts);
+ if (serve) {
+ // unsure about how SIGHUP is handled here
+ await doServe(mikeArgs, serveOpts);
+ } else {
+ await doDeploy(mikeArgs, execOpts);
+
+ log.success('Mike finished deployment into branch %s (%dms)', branch, stop());
+ }
+}
+
+/**
+ * Options for {@linkcode deploy}.
+ */
+export interface DeployOpts {
+ /**
+ * Path to `mike.yml`
+ */
+ mkDocsYml?: string;
+
+ /**
+ * Current working directory
+ * @defaultValue `process.cwd()`
+ */
+ cwd?: string;
+
+ /**
+ * Path to `package.json`
+ *
+ * Used to find `mike.yml` if unspecified.
+ */
+ packageJson?: string;
+
+ /**
+ * If `true`, run `mike serve` instead of `mike build`
+ */
+ serve?: boolean;
+
+ /**
+ * If `true`, push `branch` to `remote`
+ */
+ push?: boolean;
+ /**
+ * Branch to commit to
+ * @defaultValue gh-pages
+ */
+ branch?: string;
+ /**
+ * Remote to push to
+ * @defaultValue origin
+ */
+ remote?: string;
+ /**
+ * Subdirectory within `branch` to deploy to
+ */
+ prefix?: string;
+ /**
+ * Commit message
+ */
+ message?: string;
+ /**
+ * Version (dir) to deploy build to
+ */
+ deployVersion?: string;
+ /**
+ * Alias for the build (e.g., `latest`); triggers alias update
+ */
+ alias?: string;
+ /**
+ * If `true`, rebase `branch` before pushing
+ */
+ rebase?: boolean;
+ /**
+ * Port to serve on
+ * @defaultValue 8000
+ */
+ port?: number;
+ /**
+ * Host to serve on
+ * @defaultValue localhost
+ */
+ host?: string;
+
+ /**
+ * Extra options for {@linkcode teen_process.exec}
+ */
+ execOpts?: TeenProcessExecOptions;
+
+ /**
+ * Extra options for {@linkcode teen_process.Subprocess.start}
+ */
+ serveOpts?: TeenProcessSubprocessStartOpts;
+}

MODIFY
packages/docutils/lib/builder/index.ts
packages/docutils/lib/builder/index.ts
+export * from './deploy';
export * from './site';
export * from './reference';

MODIFY
packages/docutils/lib/cli/command/build.ts
packages/docutils/lib/cli/command/build.ts
import {CommandModule, InferredOptionTypes, Options} from 'yargs';
-import {buildReferenceDocs, buildSite} from '../../builder';
+import {buildReferenceDocs, buildSite, deploy} from '../../builder';
+import {NAME_BIN} from '../../constants';
import logger from '../../logger';
import {updateNav} from '../../nav';
import {stopwatch} from '../../util';
const log = logger.withTag('build');
-const NAME_GROUP_BUILD = 'Build API:';
+const NAME_GROUP_BUILD = 'Build Options:';
+const NAME_GROUP_DEPLOY = 'Deployment Options:';
+const NAME_GROUP_SERVE = 'Serve Options:';
+const NAME_GROUP_BUILD_PATHS = 'Paths:';
const opts = {
reference: {
@@ -24,7 +28,7 @@ const opts = {
'site-dir': {
alias: 'd',
describe: 'HTML output directory',
- group: NAME_GROUP_BUILD,
+ group: NAME_GROUP_BUILD_PATHS,
nargs: 1,
requiresArg: true,
type: 'string',
@@ -35,7 +39,7 @@ const opts = {
'package-json': {
defaultDescription: './package.json',
describe: 'Path to package.json',
- group: NAME_GROUP_BUILD,
+ group: NAME_GROUP_BUILD_PATHS,
nargs: 1,
normalize: true,
requiresArg: true,
@@ -52,7 +56,16 @@ const opts = {
'tsconfig-json': {
defaultDescription: './tsconfig.json',
describe: 'Path to tsconfig.json',
- group: NAME_GROUP_BUILD,
+ group: NAME_GROUP_BUILD_PATHS,
+ nargs: 1,
+ normalize: true,
+ requiresArg: true,
+ type: 'string',
+ },
+ 'mkdocs-yml': {
+ defaultDescription: './mkdocs.yml',
+ description: 'Path to mkdocs.yml',
+ group: NAME_GROUP_BUILD_PATHS,
nargs: 1,
normalize: true,
requiresArg: true,
@@ -61,25 +74,116 @@ const opts = {
'typedoc-json': {
defaultDescription: './typedoc.json',
describe: 'Path to typedoc.json',
- group: NAME_GROUP_BUILD,
+ group: NAME_GROUP_BUILD_PATHS,
nargs: 1,
normalize: true,
requiresArg: true,
type: 'string',
},
- 'reference-header': {
- describe: 'Navigation header for API reference',
- default: 'Reference',
+ all: {
+ describe: 'Output all reference docs (not just Appium comands)',
group: NAME_GROUP_BUILD,
+ implies: 'site',
+ type: 'boolean',
+ },
+ deploy: {
+ describe: 'Commit HTML output',
+ group: NAME_GROUP_DEPLOY,
+ type: 'boolean',
+ implies: 'site',
+ },
+ push: {
+ describe: 'Push after deploy',
+ group: NAME_GROUP_DEPLOY,
+ type: 'boolean',
+ implies: 'deploy',
+ },
+ branch: {
+ alias: 'b',
+ describe: 'Branch to commit to',
+ implies: 'deploy',
+ group: NAME_GROUP_DEPLOY,
+ type: 'string',
+ requiresArg: true,
nargs: 1,
+ defaultDescription: 'gh-pages',
+ },
+ remote: {
+ alias: 'r',
+ describe: 'Remote to push to',
+ implies: ['deploy', 'push'],
+ group: NAME_GROUP_DEPLOY,
+ type: 'string',
requiresArg: true,
+ nargs: 1,
+ defaultDescription: 'origin',
+ },
+ prefix: {
+ describe: 'Subdirectory within <branch> to commit to',
+ implies: ['deploy', 'branch'],
+ group: NAME_GROUP_DEPLOY,
type: 'string',
+ nargs: 1,
+ requiresArg: true,
},
- 'no-reference-header': {
- describe: 'Do not add a navigation header for API reference',
- group: NAME_GROUP_BUILD,
+ message: {
+ alias: 'm',
+ describe: 'Commit message',
+ implies: 'deploy',
+ group: NAME_GROUP_DEPLOY,
+ type: 'string',
+ nargs: 1,
+ requiresArg: true,
+ },
+ 'deploy-version': {
+ describe: 'Version (directory) to deploy build to',
+ implies: 'deploy',
+ group: NAME_GROUP_DEPLOY,
+ type: 'string',
+ nargs: 1,
+ requiresArg: true,
+ defaultDescription: '(derived from package.json)',
+ },
+ alias: {
+ describe: 'Alias for the build (e.g., "latest"); triggers alias update',
+ implies: 'deploy',
+ group: NAME_GROUP_DEPLOY,
+ type: 'string',
+ nargs: 1,
+ requiresArg: true,
+ defaultDescription: 'latest',
+ },
+ rebase: {
+ describe: 'Rebase <branch> with remote before deploy',
+ implies: ['deploy', 'branch', 'remote'],
+ group: NAME_GROUP_DEPLOY,
type: 'boolean',
},
+ serve: {
+ describe: 'Start development server',
+ group: NAME_GROUP_SERVE,
+ type: 'boolean',
+ },
+ port: {
+ alias: 'p',
+ describe: 'Development server port',
+ group: NAME_GROUP_SERVE,
+ type: 'number',
+ defaultDescription: '8000',
+ implies: 'serve',
+ nargs: 1,
+ requiresArg: true,
+ },
+ host: {
+ alias: 'h',
+ describe: 'Development server host',
+ group: NAME_GROUP_SERVE,
+ type: 'string',
+ nargs: 1,
+ requiresArg: true,
+ implies: 'serve',
+ defaultDescription: 'localhost',
+ },
} as const;
opts as Record<string, Options>;
@@ -88,7 +192,17 @@ type BuildOptions = InferredOptionTypes<typeof opts>;
const buildCommand: CommandModule<{}, BuildOptions> = {
command: 'build',
describe: 'Build Appium extension documentation',
- builder: opts,
+ builder: (yargs) =>
+ yargs.options(opts).check((argv) => {
+ // either this method doesn't provide camel-cased props, or the types are wrong.
+ if (argv.deploy === true && argv['site-dir']) {
+ log.error(
+ `--site-dir is unsupported when running "${NAME_BIN} deploy"; use --prefix if needd, but remember that the default behavior is to deploy to the root of the branch (${argv.branch}) instead of a subdirectory`
+ );
+ return false;
+ }
+ return true;
+ }),
async handler(args) {
const stop = stopwatch('build');
log.debug('Build command called with args: %O', args);
@@ -98,13 +212,17 @@ const buildCommand: CommandModule<{}, BuildOptions> = {
'Cannot use both --no-site (--site=false) and --no-reference (--reference=false)'
);
}
- if (args.site) {
+ if (args.reference) {
await buildReferenceDocs(args);
}
- if (args.reference) {
+ if (args.site) {
await updateNav(args);
+ if (args.deploy) {
+ await deploy(args);
+ } else {
await buildSite(args);
}
+ }
log.success('Done! (total: %dms)', stop());
},
};

MODIFY
packages/docutils/lib/constants.ts
packages/docutils/lib/constants.ts
* Constants used across various modules in this package
* @module
*/
-
import {LogLevel} from 'consola';
import {readFileSync} from 'node:fs';
import {fs} from '@appium/support';
@@ -49,6 +48,11 @@ export const NAME_SCHEMA = '$schema';
*/
export const NAME_MKDOCS = 'mkdocs';
+/**
+ * Name of the `mike` executable
+ */
+export const NAME_MIKE = 'mike';
+
/**
* Name of the `typedoc` executable
*/
@@ -109,6 +113,26 @@ export const REQUIREMENTS_TXT_PATH = path.join(PKG_ROOT_DIR, NAME_REQUIREMENTS_T
*/
export const DEFAULT_REL_TYPEDOC_OUT_PATH = path.join('docs', 'reference');
+/**
+ * The default branch to deploy to
+ */
+export const DEFAULT_DEPLOY_BRANCH = 'gh-pages';
+
+/**
+ * The default remote to push the deployed branch to
+ */
+export const DEFAULT_DEPLOY_REMOTE = 'origin';
+
+/**
+ * The default port for serving docs
+ */
+export const DEFAULT_SERVE_PORT = 8000;
+
+/**
+ * The default host for serving docs
+ */
+export const DEFAULT_SERVE_HOST = 'localhost';
+
/**
* Mapping of `@appium/docutils`' log levels to `consola` log levels
*/
@@ -119,3 +143,8 @@ export const LogLevelMap = {
info: LogLevel.Info,
debug: LogLevel.Debug,
} as const;
+
+/**
+ * Default site nav header text
+ */
+export const DEFAULT_NAV_HEADER = 'Reference';

MODIFY
packages/docutils/lib/fs.ts
packages/docutils/lib/fs.ts
@@ -20,6 +20,7 @@ import {
NAME_MKDOCS,
NAME_NPM,
NAME_PYTHON,
+ NAME_MIKE,
} from './constants';
import {DocutilsError} from './error';
import {MkDocsYml} from './model';
@@ -217,6 +218,11 @@ export const whichNpm = _.partial(cachedWhich, NAME_NPM);
*/
export const whichPython = _.partial(cachedWhich, NAME_PYTHON);
+/**
+ * Finds `mike` executable
+ */
+export const whichMike = _.partial(cachedWhich, NAME_MIKE);
+
/**
* Reads an `mkdocs.yml` file, merges inherited configs, and returns the result. The result is cached.
*

MODIFY
packages/docutils/lib/mike.js
packages/docutils/lib/mike.js
@@ -6,6 +6,9 @@ const DEFAULT_REMOTE = 'origin';
const DEFAULT_BRANCH = 'gh-pages';
const MIKE_VER_STRING = 'mike 1.';
+/**
+ * @deprecated Use the `deploy` export from `@appium/docutils`
+ */
export class Mike {
/** @type {string} */ remote;
/** @type {string} */ branch;

feat(docutils): add support for deploying with mike

This is enabled via `appium-docs build --deploy`.  Instead of invoking `mkdocs` directly, `mike` will be invoked.

Implementation is in `lib/builder/deploy.ts`.  It's very similar to the MkDocs implementation in `lib/builder/site.ts`; just with new/different options.

I marked the `Mike` class as deprecated, since the new one is fancy