command-line-basics
Version:
Auto-add help and version CLI and update notification checks
218 lines (197 loc) • 5.93 kB
JavaScript
import {readFile} from 'fs/promises';
import {resolve} from 'path';
import updateNotifier from 'update-notifier';
import commandLineArgs from 'command-line-args';
import commandLineUsage from 'command-line-usage';
/**
* @typedef {any} JSONValue
*/
/**
* @typedef {{
* packageJsonPath?: string
* }} PackageJsonPathOptions
*/
/**
* @param {PackageJsonPathOptions} options
* @param {string} cwd
* @returns {Promise<JSONValue>}
*/
const getPackageJson = async (options, cwd) => {
let {packageJsonPath} = options;
packageJsonPath = packageJsonPath
? resolve(cwd, packageJsonPath)
: resolve(process.cwd(), 'package.json');
return JSON.parse(
// @ts-expect-error It's ok
await readFile(packageJsonPath)
);
};
/**
* @typedef {{
* pkg?: JSONValue,
* autoAddVersion?: boolean,
* autoAddHelp?: boolean,
* autoAddHeader?: boolean,
* autoAddContent?: boolean,
* autoAddOptionsHeader?: boolean
* } & PackageJsonPathOptions} AutoAddOptions
*/
/**
* @typedef {{
* optionsPath: string,
* options?: AutoAddOptions,
* cwd?: string,
* pkg?: JSONValue
* }} OptionsPath
*/
/**
* @param {string|OptionsPath} optionsPath
* @param {AutoAddOptions} [options]
* @throws {TypeError}
* @returns {Promise<{
* definitions: import('command-line-usage').OptionDefinition[],
* sections: import('command-line-usage').Section[]
* }>}
*/
const autoAdd = async (optionsPath, options) => {
if (!optionsPath) {
throw new TypeError(`You must include an \`optionsPath\`.`);
}
let cwd;
if (typeof optionsPath === 'object') {
({optionsPath, options, cwd} = optionsPath);
}
options = options || {};
cwd = cwd || process.cwd();
const {
pkg = await getPackageJson(options, cwd)
} = options;
optionsPath = resolve(cwd, optionsPath);
const {
definitions: optionDefinitions, sections: cliSections
} =
// /* eslint-disable no-unsanitized/method -- User prompted */
/**
* @type {{
* definitions: import('command-line-usage').OptionDefinition[],
* sections: import('command-line-usage').Section[]
* }}
*/ (await import(optionsPath));
// /* eslint-enable no-unsanitized/method -- User prompted */
if (options.autoAddVersion !== false && optionDefinitions.every(
(def) => def.name !== 'version' && def.alias !== 'v'
)) {
const versionInfo = {name: 'version', type: Boolean, alias: 'v'};
optionDefinitions.push(versionInfo);
if (cliSections[1] && 'optionList' in cliSections[1] &&
cliSections[1].optionList &&
cliSections[1].optionList.every(
(def) => def.name !== 'version' && def.alias !== 'v'
)
) {
cliSections[1].optionList.push(versionInfo);
}
}
if (options.autoAddHelp !== false && optionDefinitions.every(
(def) => def.name !== 'help' && def.alias !== 'h'
)) {
const helpInfo = {name: 'help', type: Boolean, alias: 'h'};
optionDefinitions.push(helpInfo);
if (cliSections[1] && 'optionList' in cliSections[1] &&
cliSections[1].optionList &&
cliSections[1].optionList.every(
(def) => def.name !== 'help' && def.alias !== 'h'
)
) {
cliSections[1].optionList.push(helpInfo);
}
}
if (cliSections[0]) {
if (!cliSections[0].header && options.autoAddHeader !== false) {
cliSections[0].header = pkg.name;
}
if (
(!('content' in cliSections[0]) || !cliSections[0].content) &&
options.autoAddContent !== false &&
pkg.description
) {
/** @type {import('command-line-usage').Content} */ (
cliSections[0]
).content = pkg.description;
}
}
if (cliSections[1] && !cliSections[1].header &&
options.autoAddOptionsHeader !== false
) {
cliSections[1].header = 'Options';
}
return {definitions: optionDefinitions, sections: cliSections};
};
/**
* @callback NotifierCallback
* @param {import('update-notifier').UpdateNotifier} notifier
* @returns {void}
*/
/**
* @typedef {AutoAddOptions & {
* commandLineArgsOptions?: import('command-line-args').ParseOptions
* updateNotifierOptions?: import('update-notifier').Settings,
* updateNotifierNotifyOptions?: import('update-notifier').NotifyOptions|false
* }} CliBasicsOptions
*/
/**
* @param {string|({
* optionsPath: string,
* options?: CliBasicsOptions,
* cwd?: string
* notifierCallback?: NotifierCallback
* })} optionsPath
* @param {CliBasicsOptions} [options]
* @param {NotifierCallback} [notifierCallback]
* @throws {TypeError}
* @returns {Promise<import('command-line-args').CommandLineOptions|null>}
*/
const cliBasics = async (optionsPath, options, notifierCallback) => {
if (!optionsPath) {
throw new TypeError(`You must include an \`optionsPath\`.`);
}
let cwd;
if (typeof optionsPath === 'object') {
({optionsPath, options, cwd, notifierCallback} = optionsPath);
}
options = options || {};
cwd = cwd || process.cwd();
const pkg = await getPackageJson(options, cwd);
// check if a new version is available and print an update notification
const notifier = updateNotifier({
pkg, ...options.updateNotifierOptions
});
if (options.updateNotifierNotifyOptions !== false &&
notifier.update && notifier.update.latest !== pkg.version
) {
notifier.notify({
defer: false, ...options.updateNotifierNotifyOptions
});
}
if (notifierCallback) {
notifierCallback(notifier);
}
const {
definitions: optionDefinitions, sections: cliSections
} = await autoAdd({optionsPath, cwd, options, pkg});
const userOptions = commandLineArgs(
optionDefinitions, options.commandLineArgsOptions
);
const {help, version} = userOptions;
if (help) {
const usage = commandLineUsage(cliSections);
console.log(usage);
return null;
}
if (version) {
console.log(pkg.version);
return null;
}
return userOptions;
};
export {autoAdd, cliBasics};